diff --git a/.changeset/27502-approval-summary-as-submitted.md b/.changeset/27502-approval-summary-as-submitted.md
new file mode 100644
index 00000000000..3cd79ec3bcb
--- /dev/null
+++ b/.changeset/27502-approval-summary-as-submitted.md
@@ -0,0 +1,13 @@
+---
+'@object-ui/app-shell': patch
+'@object-ui/i18n': patch
+---
+
+fix(app-shell): the approval decision panel's record summary is headed "As submitted" (#27502)
+
+Clause-②: yes (widening)
+Metadata-protocol: none
+
+The summary the approval request page's decision panel (`record:approval_decision`) draws comes from the request's record snapshot: the target record as it was submitted, kept as the request's evidence of what was submitted. After a decision the request's status, which the page's highlights show, moves on while the snapshot keeps the submitted values, so the summary read as the record's current state and the page contradicted itself. The summary is now a section headed "As submitted", in the muted label style of the panel's "Waiting on", and that heading is its accessible name. A pending request carries the heading too, and a summary that draws nothing draws no heading. The values, the rows and the reads are unchanged: the panel still reads no record, and the record's current values stay on the Details tab's record card.
+
+New language-pack key in all ten packs: `approvalsInbox.snapshotAsSubmitted`.
diff --git a/ui/packages/app-shell/README.md b/ui/packages/app-shell/README.md
index 3faa2fa44aa..e0429d125fa 100644
--- a/ui/packages/app-shell/README.md
+++ b/ui/packages/app-shell/README.md
@@ -1248,7 +1248,11 @@ key that only looks like an amount by its name never leads. A key the object
does not declare, a framework column, an
unresolved reference id, a field declared `hidden: true` and a field the
reader may not read take no row. Without the target object's definition, the
-summary is not drawn.
+summary is not drawn. The summary is headed "As submitted"
+(`approvalsInbox.snapshotAsSubmitted`): the snapshot is the target record as it
+was submitted, not its current state, so after a decision its values can
+differ from the request's status in the page's highlights. The record's
+current values are the Details tab's record card.
Two more parts come from the same request row. A request no person submitted
(its `submitter_id` absent or null, as the approvals get serves a request a
diff --git a/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.decidedSnapshot-27502.test.tsx b/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.decidedSnapshot-27502.test.tsx
new file mode 100644
index 00000000000..0845afe6c2c
--- /dev/null
+++ b/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.decidedSnapshot-27502.test.tsx
@@ -0,0 +1,159 @@
+/**
+ * ObjectUI
+ * Copyright (c) 2024-present ObjectStack Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+/**
+ * After a decision, the approval request page must not tell two stories
+ * (objectstack#27502, ruling record 6499914328). The page's highlights read the
+ * REQUEST row, whose status moves to approved; the decision panel's summary
+ * reads the request's `payload`, the target record as it was SUBMITTED, which
+ * keeps its submitted status (objectstack#10749, ruling record 5377367175: the
+ * snapshot is kept as the evidence of what was submitted). So the summary is
+ * labelled as the submitted values: a section whose accessible name is the
+ * pack's `approvalsInbox.snapshotAsSubmitted` row, holding the snapshot's own
+ * values.
+ *
+ * The rows are in the shape the approvals get serves the request page after a
+ * reload: a decided request, its snapshot still at submission. The declared
+ * actions bar is doubled: what is under test is the summary's label.
+ */
+
+import * as React from 'react';
+import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
+import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
+
+vi.mock('../DeclaredActionsBar.js', () => ({
+ DeclaredActionsBar: () =>
,
+}));
+
+import { MetadataCtx, RecordContextProvider } from '@object-ui/react';
+import { createI18n, I18nProvider } from '@object-ui/i18n';
+import { builtInLocales } from '@object-ui/i18n/locales';
+import { ApprovalDecisionRenderer } from './ApprovalDecisionPanel';
+import { resetApproverDirectoryCache } from '../../hooks/useApproverDirectory';
+
+const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false });
+
+/** The heading's pack key, and what one pack says for it. */
+const HEADING_KEY = 'approvalsInbox.snapshotAsSubmitted';
+const packHeading = (language: string): unknown =>
+ HEADING_KEY.split('.').reduce(
+ (node, part) => (node as Record | undefined)?.[part],
+ (builtInLocales as Record)[language],
+ );
+
+/** The pack row the heading must come from, rendered by the instance under test. */
+const HEADING = String(i18n.t(HEADING_KEY));
+
+const EXPENSE_REPORT = {
+ name: 'showcase_expense_report',
+ fields: {
+ title: { type: 'text', label: 'Title' },
+ status: {
+ type: 'select',
+ label: 'Status',
+ options: [
+ { value: 'submitted', label: 'Submitted' },
+ { value: 'approved', label: 'Approved' },
+ ],
+ },
+ approved_amount: { type: 'currency', label: 'Approved' },
+ },
+};
+
+const seen: string[] = [];
+const metadata = {
+ getItem: async (type: string, name: string) => {
+ seen.push(`${type}:${name}`);
+ return type === 'object' && name === EXPENSE_REPORT.name ? EXPENSE_REPORT : null;
+ },
+};
+
+/** The target record as it was submitted: it does not move on a decision. */
+const SUBMITTED = { title: 'EXP-DEMO', status: 'submitted', approved_amount: 0 };
+
+function requestRow(status: string, payload: Record = SUBMITTED): Record {
+ return {
+ id: 'areq_27502',
+ process_name: 'flow:showcase_expense_committee',
+ object_name: EXPENSE_REPORT.name,
+ record_id: 'rec_1',
+ status,
+ submitter_id: 'u_dev_admin',
+ pending_approvers: status === 'pending' ? ['u_dev_admin'] : [],
+ pending_approver_names: { u_dev_admin: 'Dev Admin' },
+ viewer: { can_act: status === 'pending', is_submitter: false, can_override: false },
+ payload,
+ };
+}
+
+beforeEach(() => {
+ resetApproverDirectoryCache();
+ seen.length = 0;
+});
+
+afterEach(() => {
+ cleanup();
+});
+
+function mount(data: Record): HTMLElement {
+ render(
+
+
+
+
+
+
+ ,
+ );
+ return screen.getByTestId('approval-decision-panel');
+}
+
+describe('ApprovalDecisionPanel — the summary is labelled as the submitted values (objectstack#27502)', () => {
+ it('a decided request: the snapshot keeps its submitted status, under the heading, and nobody is waited on', async () => {
+ const panel = mount(requestRow('approved'));
+ const summary = await within(panel).findByTestId('record-snapshot-summary');
+
+ // The decided state the reload serves: nobody is waited on any more.
+ expect(within(panel).queryByTestId('approval-decision-pending-approvers')).toBeNull();
+ // The summary is the region the heading names, and the heading is the pack row.
+ expect(HEADING).not.toBe(HEADING_KEY);
+ expect(within(panel).getByRole('region', { name: HEADING })).toBe(summary);
+ expect(summary.querySelector('[data-summary-heading]')?.textContent).toBe(HEADING);
+ // What the section holds is the snapshot as submitted, under the declared select field.
+ const status = summary.querySelector('[data-summary-field="status"]');
+ expect(status?.textContent).toBe('StatusSubmitted');
+ });
+
+ it('a pending request carries the heading too: it names the source, not a status', async () => {
+ const panel = mount(requestRow('pending'));
+ const summary = await within(panel).findByTestId('record-snapshot-summary');
+
+ expect(within(panel).getByTestId('approval-decision-pending-approvers')).toBeTruthy();
+ expect(within(panel).getByRole('region', { name: HEADING })).toBe(summary);
+ });
+
+ it('CONTROL: a summary that draws nothing draws no heading', async () => {
+ // Only keys the object does not declare: the definition answers, and no row is drawn.
+ const panel = mount(requestRow('approved', { legacy_note: 'x' }));
+
+ await waitFor(() => expect(seen).toContain(`object:${EXPENSE_REPORT.name}`));
+ await new Promise((r) => setTimeout(r, 0));
+ expect(within(panel).queryByTestId('record-snapshot-summary')).toBeNull();
+ expect(within(panel).queryByText(HEADING)).toBeNull();
+ });
+
+ it('the heading copy is a key in all ten language packs', () => {
+ const languages = Object.keys(builtInLocales);
+ expect(languages).toHaveLength(10);
+ for (const language of languages) {
+ const value = packHeading(language);
+ expect(typeof value, `${language} defines ${HEADING_KEY}`).toBe('string');
+ expect((value as string).trim(), `${language} defines ${HEADING_KEY}`).not.toBe('');
+ }
+ });
+});
diff --git a/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx b/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx
index ab9898eab18..086737ce03c 100644
--- a/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx
+++ b/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx
@@ -23,7 +23,12 @@
* declared labels and through the record page's own cells, with the
* object's `hidden: true` fields and the reader's unreadable fields left
* out. `RecordSnapshotSummary` draws it from the bound row's `payload`
- * and `payload_display`; see its header for what it reads.
+ * and `payload_display`: the target record as SUBMITTED, kept as the
+ * request's evidence of what was submitted (objectstack#10749), never a
+ * live read. It is headed "As submitted" so a decided request's snapshot
+ * never reads as the record's current state (objectstack#27502); the
+ * current record is the Details tab's pointer card. See its header for
+ * what it reads.
* 2. **Where the flow stands** (objectui#12491, objectstack#26668) — the
* vertical step line of this request's own route, the bound row's
* `flow_route` as the approvals get serves it: the steps its run passed,
diff --git a/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx b/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx
index 5167b9cae35..c05edc2bf45 100644
--- a/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx
+++ b/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx
@@ -48,6 +48,22 @@
* refused read), nothing is drawn: without the declarations there is no
* declared label, no face and no `hidden` to honour.
*
+ * ## Labelled as the submitted values (objectstack#27502)
+ *
+ * The snapshot is the record as it was SUBMITTED, kept at rest as the
+ * request's evidence of what was submitted and served redacted per reader
+ * (objectstack#10749, ruling record 5377367175, Option B). It is not the
+ * record's current state: after a decision the record's own status moves on
+ * while the snapshot's stays. So the summary is a section headed "As
+ * submitted" (`approvalsInbox.snapshotAsSubmitted`, in the muted label style
+ * of the panel's "Waiting on"), its accessible name, and a snapshot value
+ * never reads as the record's current one beside the page's highlights, which
+ * read the request row (ruling record 6499914328 on objectstack#27502). The
+ * heading names the source, not a status, so a pending request carries it
+ * too; a summary that draws nothing draws no heading. The record's current
+ * values are the Details tab's pointer card's to show, a live read of the
+ * target that the reader may be refused; this face reads no record.
+ *
* ## Cost
*
* A row with no snapshot asks for nothing. Otherwise the definition is
@@ -72,7 +88,7 @@ import { isDeadlineSettled, withoutDeniedFields } from '@object-ui/core';
import { getCellRenderer, resolveCellRendererType } from '@object-ui/fields';
import { useSafeFieldLabel } from '@object-ui/i18n';
import { usePermissions } from '@object-ui/permissions';
-import { useMetadataItem, usePredicateScope } from '@object-ui/react';
+import { useMetadataItem, useObjectTranslation, usePredicateScope } from '@object-ui/react';
import type { FieldMetadata } from '@object-ui/types';
import { distinctLabels, planSnapshotSummary, type FieldDef } from './snapshotSummaryPlan.js';
@@ -151,6 +167,8 @@ function DeclaredSummary({ target, fields, shown, display }: {
const summed = planSnapshotSummary(shown, fields, display).summed;
const child = useMetadataItem('object', summed);
const { fieldLabel } = useSafeFieldLabel();
+ const { t } = useObjectTranslation();
+ const headingId = React.useId();
if (summed !== null && child.loading) return null;
// An unreadable definition leaves every `sum` an ordinary row.
@@ -172,7 +190,10 @@ function DeclaredSummary({ target, fields, shown, display }: {
);
return (
-
+
+
+ {String(t('approvalsInbox.snapshotAsSubmitted', { defaultValue: 'As submitted' }))}
+
{plan.lead && (
{labelOf.get(plan.lead)}
@@ -191,6 +212,6 @@ function DeclaredSummary({ target, fields, shown, display }: {
))}
)}
-
+
);
}
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts
index bae90ee21b7..8a49768b953 100644
--- a/ui/packages/i18n/src/locales/ar.ts
+++ b/ui/packages/i18n/src/locales/ar.ts
@@ -4493,6 +4493,7 @@ const ar = {
progressBar: 'تقدم القرار',
declaredActions: 'الإجراءات',
decisionPanelOffRequestPage: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.',
+ snapshotAsSubmitted: 'كما قُدِّم',
attachmentChip: 'مرفق',
attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى',
approveOneTitle: 'الموافقة على "{{title}}"؟',
diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts
index 5867940c73f..8a301d8770c 100644
--- a/ui/packages/i18n/src/locales/de.ts
+++ b/ui/packages/i18n/src/locales/de.ts
@@ -4156,6 +4156,7 @@ const de = {
progressBar: 'Entscheidungsfortschritt',
declaredActions: 'Aktionen',
decisionPanelOffRequestPage: 'Der Entscheidungsbereich für Genehmigungen funktioniert nur auf Seiten von Genehmigungsanfragen.',
+ snapshotAsSubmitted: 'Wie eingereicht',
attachmentChip: 'Anhang',
attachmentOpenFailed: 'Anhang konnte nicht geöffnet werden – bitte erneut versuchen',
approveOneTitle: '„{{title}}“ genehmigen?',
diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts
index 8b48e5efbce..8ab597ab298 100644
--- a/ui/packages/i18n/src/locales/en.ts
+++ b/ui/packages/i18n/src/locales/en.ts
@@ -4935,6 +4935,7 @@ const en = {
progressBar: 'Decision progress',
declaredActions: 'Actions',
decisionPanelOffRequestPage: 'The approval decision panel only works on approval request pages.',
+ snapshotAsSubmitted: 'As submitted',
attachmentChip: 'Attachment',
attachmentOpenFailed: 'Could not open the attachment — please try again',
approveOneTitle: 'Approve "{{title}}"?',
diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts
index 84ba86a6caf..7af89a5b2e1 100644
--- a/ui/packages/i18n/src/locales/es.ts
+++ b/ui/packages/i18n/src/locales/es.ts
@@ -4241,6 +4241,7 @@ const es = {
progressBar: 'Progreso de la decisión',
declaredActions: 'Acciones',
decisionPanelOffRequestPage: 'El panel de decisión de aprobación solo funciona en las páginas de solicitudes de aprobación.',
+ snapshotAsSubmitted: 'Tal como se envió',
attachmentChip: 'Adjunto',
attachmentOpenFailed: 'No se pudo abrir el archivo adjunto: inténtalo de nuevo',
approveOneTitle: '¿Aprobar «{{title}}»?',
diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts
index dd54c82eae1..cf8113c93a9 100644
--- a/ui/packages/i18n/src/locales/fr.ts
+++ b/ui/packages/i18n/src/locales/fr.ts
@@ -4237,6 +4237,7 @@ const fr = {
progressBar: 'Avancement de la décision',
declaredActions: 'Actions',
decisionPanelOffRequestPage: 'Le panneau de décision d\'approbation ne fonctionne que sur les pages de demande d\'approbation.',
+ snapshotAsSubmitted: 'Tel que soumis',
attachmentChip: 'Pièce jointe',
attachmentOpenFailed: "Impossible d'ouvrir la pièce jointe — veuillez réessayer",
approveOneTitle: 'Approuver « {{title}} » ?',
diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts
index f3ea4d22844..77a92a83147 100644
--- a/ui/packages/i18n/src/locales/ja.ts
+++ b/ui/packages/i18n/src/locales/ja.ts
@@ -4158,6 +4158,7 @@ const ja = {
progressBar: '承認の進捗',
declaredActions: 'アクション',
decisionPanelOffRequestPage: '承認の決定パネルは承認リクエストのページでのみ機能します。',
+ snapshotAsSubmitted: '申請時の内容',
attachmentChip: '添付ファイル',
attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください',
approveOneTitle: '「{{title}}」を承認しますか?',
diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts
index 1ee8162c21f..88214ea44be 100644
--- a/ui/packages/i18n/src/locales/ko.ts
+++ b/ui/packages/i18n/src/locales/ko.ts
@@ -4155,6 +4155,7 @@ const ko = {
progressBar: '결정 진행 상황',
declaredActions: '작업',
decisionPanelOffRequestPage: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.',
+ snapshotAsSubmitted: '제출 당시 내용',
attachmentChip: '첨부 파일',
attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요',
approveOneTitle: '"{{title}}"을(를) 승인할까요?',
diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts
index fe721a0f114..6f7bca09440 100644
--- a/ui/packages/i18n/src/locales/pt.ts
+++ b/ui/packages/i18n/src/locales/pt.ts
@@ -4236,6 +4236,7 @@ const pt = {
progressBar: 'Progresso da decisão',
declaredActions: 'Ações',
decisionPanelOffRequestPage: 'O painel de decisão de aprovação só funciona nas páginas de solicitações de aprovação.',
+ snapshotAsSubmitted: 'Conforme enviado',
attachmentChip: 'Anexo',
attachmentOpenFailed: 'Não foi possível abrir o anexo — tente novamente',
approveOneTitle: 'Aprovar "{{title}}"?',
diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts
index 5071cfc11fe..17275f5434b 100644
--- a/ui/packages/i18n/src/locales/ru.ts
+++ b/ui/packages/i18n/src/locales/ru.ts
@@ -4335,6 +4335,7 @@ const ru = {
progressBar: 'Ход решения',
declaredActions: 'Действия',
decisionPanelOffRequestPage: 'Панель решения по согласованию работает только на страницах запросов на согласование.',
+ snapshotAsSubmitted: 'На момент подачи',
attachmentChip: 'Вложение',
attachmentOpenFailed: 'Не удалось открыть вложение — попробуйте ещё раз',
approveOneTitle: 'Согласовать «{{title}}»?',
diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts
index 283878980ba..29ea1b504fd 100644
--- a/ui/packages/i18n/src/locales/zh.ts
+++ b/ui/packages/i18n/src/locales/zh.ts
@@ -4346,6 +4346,7 @@ const zh = {
progressBar: '决策进度',
declaredActions: '操作',
decisionPanelOffRequestPage: '审批决策面板仅在审批请求页面上可用。',
+ snapshotAsSubmitted: '提交时的内容',
attachmentChip: '附件',
attachmentOpenFailed: '无法打开附件,请重试',
approveOneTitle: '通过“{{title}}”?',