From 159292eb71b2598829ce60543e963e1e0a8d043c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:44:05 +0000 Subject: [PATCH 1/4] fix(app-shell): label the approval decision panel's record summary as the submitted values (#27502) Claude-Session: https://claude.ai/code/session_01Utp6CaRUZ2GQ5mSm8Z7Aqd Co-authored-by: Claude --- ...cisionPanel.decidedSnapshot-27502.test.tsx | 121 ++++++++++++++++++ .../ApprovalDecisionPanel.tsx | 4 +- .../RecordSnapshotSummary.tsx | 24 +++- ui/packages/i18n/src/locales/ar.ts | 1 + ui/packages/i18n/src/locales/de.ts | 1 + ui/packages/i18n/src/locales/en.ts | 1 + ui/packages/i18n/src/locales/es.ts | 1 + ui/packages/i18n/src/locales/fr.ts | 1 + ui/packages/i18n/src/locales/ja.ts | 1 + ui/packages/i18n/src/locales/ko.ts | 1 + ui/packages/i18n/src/locales/pt.ts | 1 + ui/packages/i18n/src/locales/ru.ts | 1 + ui/packages/i18n/src/locales/zh.ts | 1 + 13 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.decidedSnapshot-27502.test.tsx 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..542a4789df8 --- /dev/null +++ b/ui/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.decidedSnapshot-27502.test.tsx @@ -0,0 +1,121 @@ +/** + * 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). 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.snapshotHeading` 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, 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 { ApprovalDecisionRenderer } from './ApprovalDecisionPanel'; +import { resetApproverDirectoryCache } from '../../hooks/useApproverDirectory'; + +const i18n = createI18n({ defaultLanguage: 'en', detectBrowserLanguage: false }); + +/** The pack row the heading must come from, rendered by the instance under test. */ +const HEADING = String(i18n.t('approvalsInbox.snapshotHeading')); + +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 metadata = { + getItem: async (type: string, name: string) => + type === 'object' && name === EXPENSE_REPORT.name ? EXPENSE_REPORT : null, +}; + +function requestRow(status: string): 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 }, + // The target record as it was submitted: it does not move on a decision. + payload: { title: 'EXP-DEMO', status: 'submitted', approved_amount: 0 }, + }; +} + +beforeEach(() => resetApproverDirectoryCache()); + +afterEach(() => { + cleanup(); +}); + +async function summaryOf(data: Record) { + render( + + + + + + + , + ); + const panel = screen.getByTestId('approval-decision-panel'); + const summary = await within(panel).findByTestId('record-snapshot-summary'); + return { panel, summary }; +} + +describe('ApprovalDecisionPanel — the summary is labelled as the submitted values (objectstack#27502)', () => { + it('a decided request: the snapshot keeps its submitted status, inside a section named as the submitted values', async () => { + const { panel, summary } = await summaryOf(requestRow('approved')); + + // 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('approvalsInbox.snapshotHeading'); + expect(within(panel).getByRole('region', { name: HEADING })).toBe(summary); + // What the section holds is the snapshot, as submitted, never the request's status. + const status = summary.querySelector('[data-summary-field="status"]'); + expect(status?.textContent).toContain('Submitted'); + expect(status?.textContent).not.toContain('Approved'); + }); + + it('the label does not depend on the request status: a pending request names it the same way', async () => { + const { panel, summary } = await summaryOf(requestRow('pending')); + + expect(within(panel).getByTestId('approval-decision-pending-approvers')).toBeTruthy(); + expect(within(panel).getByRole('region', { name: HEADING })).toBe(summary); + }); +}); 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..421bc05eec3 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,9 @@ * 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`, headed "As submitted" so a decided request's + * snapshot never reads as the record's current state (objectstack#27502); + * 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..356e658f0c5 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,19 @@ * 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.snapshotHeading`), 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. 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 +85,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 +164,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 +187,10 @@ function DeclaredSummary({ target, fields, shown, display }: { ); return ( -
+
+
+ {String(t('approvalsInbox.snapshotHeading', { defaultValue: 'As submitted' }))} +
{plan.lead && (
{labelOf.get(plan.lead)}
@@ -191,6 +209,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..89db3aaaffa 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: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.', + snapshotHeading: 'كما قُدِّم', attachmentChip: 'مرفق', attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى', approveOneTitle: 'الموافقة على "{{title}}"؟', diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 5867940c73f..674778e0f13 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.', + snapshotHeading: '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..b492c42c464 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.', + snapshotHeading: '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..c1a04be34dd 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.', + snapshotHeading: '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..760bf993d46 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.', + snapshotHeading: '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..2d2067b5b78 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: '承認の決定パネルは承認リクエストのページでのみ機能します。', + snapshotHeading: '申請時の内容', attachmentChip: '添付ファイル', attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください', approveOneTitle: '「{{title}}」を承認しますか?', diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 1ee8162c21f..49dafb2108a 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: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.', + snapshotHeading: '제출 당시 내용', attachmentChip: '첨부 파일', attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요', approveOneTitle: '"{{title}}"을(를) 승인할까요?', diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index fe721a0f114..7c878e31843 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.', + snapshotHeading: '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..7896c3aba96 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: 'Панель решения по согласованию работает только на страницах запросов на согласование.', + snapshotHeading: 'На момент подачи', attachmentChip: 'Вложение', attachmentOpenFailed: 'Не удалось открыть вложение — попробуйте ещё раз', approveOneTitle: 'Согласовать «{{title}}»?', diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index 283878980ba..18cdd87bbbc 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: '审批决策面板仅在审批请求页面上可用。', + snapshotHeading: '提交时的内容', attachmentChip: '附件', attachmentOpenFailed: '无法打开附件,请重试', approveOneTitle: '通过“{{title}}”?', From 3f1cc69ec8227876d1aa14fbfc4aab817c41a9af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:55:09 +0000 Subject: [PATCH 2/4] fix(app-shell): name the snapshot heading key and pin the decided, pending, empty and ten-pack cases (#27502) Claude-Session: https://claude.ai/code/session_01Utp6CaRUZ2GQ5mSm8Z7Aqd Co-authored-by: Claude --- ...cisionPanel.decidedSnapshot-27502.test.tsx | 92 +++++++++++++------ .../ApprovalDecisionPanel.tsx | 9 +- .../RecordSnapshotSummary.tsx | 17 ++-- ui/packages/i18n/src/locales/ar.ts | 2 +- ui/packages/i18n/src/locales/de.ts | 2 +- ui/packages/i18n/src/locales/en.ts | 2 +- ui/packages/i18n/src/locales/es.ts | 2 +- ui/packages/i18n/src/locales/fr.ts | 2 +- ui/packages/i18n/src/locales/ja.ts | 2 +- ui/packages/i18n/src/locales/ko.ts | 2 +- ui/packages/i18n/src/locales/pt.ts | 2 +- ui/packages/i18n/src/locales/ru.ts | 2 +- ui/packages/i18n/src/locales/zh.ts | 2 +- 13 files changed, 91 insertions(+), 47 deletions(-) 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 index 542a4789df8..0845afe6c2c 100644 --- 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 @@ -8,13 +8,14 @@ /** * After a decision, the approval request page must not tell two stories - * (objectstack#27502). 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.snapshotHeading` row, holding the snapshot's own values. + * (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 @@ -23,7 +24,7 @@ import * as React from 'react'; import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; -import { cleanup, render, screen, within } from '@testing-library/react'; +import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; vi.mock('../DeclaredActionsBar.js', () => ({ DeclaredActionsBar: () =>
, @@ -31,13 +32,22 @@ vi.mock('../DeclaredActionsBar.js', () => ({ 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('approvalsInbox.snapshotHeading')); +const HEADING = String(i18n.t(HEADING_KEY)); const EXPENSE_REPORT = { name: 'showcase_expense_report', @@ -55,12 +65,18 @@ const EXPENSE_REPORT = { }, }; +const seen: string[] = []; const metadata = { - getItem: async (type: string, name: string) => - type === 'object' && name === EXPENSE_REPORT.name ? EXPENSE_REPORT : null, + getItem: async (type: string, name: string) => { + seen.push(`${type}:${name}`); + return type === 'object' && name === EXPENSE_REPORT.name ? EXPENSE_REPORT : null; + }, }; -function requestRow(status: string): Record { +/** 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', @@ -71,18 +87,20 @@ function requestRow(status: string): Record { 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 }, - // The target record as it was submitted: it does not move on a decision. - payload: { title: 'EXP-DEMO', status: 'submitted', approved_amount: 0 }, + payload, }; } -beforeEach(() => resetApproverDirectoryCache()); +beforeEach(() => { + resetApproverDirectoryCache(); + seen.length = 0; +}); afterEach(() => { cleanup(); }); -async function summaryOf(data: Record) { +function mount(data: Record): HTMLElement { render( @@ -92,30 +110,50 @@ async function summaryOf(data: Record) { , ); - const panel = screen.getByTestId('approval-decision-panel'); - const summary = await within(panel).findByTestId('record-snapshot-summary'); - return { panel, summary }; + 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, inside a section named as the submitted values', async () => { - const { panel, summary } = await summaryOf(requestRow('approved')); + 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('approvalsInbox.snapshotHeading'); + expect(HEADING).not.toBe(HEADING_KEY); expect(within(panel).getByRole('region', { name: HEADING })).toBe(summary); - // What the section holds is the snapshot, as submitted, never the request's status. + 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).toContain('Submitted'); - expect(status?.textContent).not.toContain('Approved'); + expect(status?.textContent).toBe('StatusSubmitted'); }); - it('the label does not depend on the request status: a pending request names it the same way', async () => { - const { panel, summary } = await summaryOf(requestRow('pending')); + 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 421bc05eec3..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,9 +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`, headed "As submitted" so a decided request's - * snapshot never reads as the record's current state (objectstack#27502); - * 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 356e658f0c5..c05edc2bf45 100644 --- a/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx +++ b/ui/packages/app-shell/src/views/approval-decision/RecordSnapshotSummary.tsx @@ -55,11 +55,14 @@ * (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.snapshotHeading`), 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. 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. + * 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 * @@ -188,8 +191,8 @@ function DeclaredSummary({ target, fields, shown, display }: { return (
-
- {String(t('approvalsInbox.snapshotHeading', { defaultValue: 'As submitted' }))} +
+ {String(t('approvalsInbox.snapshotAsSubmitted', { defaultValue: 'As submitted' }))}
{plan.lead && (
diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index 89db3aaaffa..8a49768b953 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4493,7 +4493,7 @@ const ar = { progressBar: 'تقدم القرار', declaredActions: 'الإجراءات', decisionPanelOffRequestPage: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.', - snapshotHeading: 'كما قُدِّم', + snapshotAsSubmitted: 'كما قُدِّم', attachmentChip: 'مرفق', attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى', approveOneTitle: 'الموافقة على "{{title}}"؟', diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 674778e0f13..8a301d8770c 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -4156,7 +4156,7 @@ const de = { progressBar: 'Entscheidungsfortschritt', declaredActions: 'Aktionen', decisionPanelOffRequestPage: 'Der Entscheidungsbereich für Genehmigungen funktioniert nur auf Seiten von Genehmigungsanfragen.', - snapshotHeading: 'Wie eingereicht', + 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 b492c42c464..8ab597ab298 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -4935,7 +4935,7 @@ const en = { progressBar: 'Decision progress', declaredActions: 'Actions', decisionPanelOffRequestPage: 'The approval decision panel only works on approval request pages.', - snapshotHeading: 'As submitted', + 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 c1a04be34dd..7af89a5b2e1 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -4241,7 +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.', - snapshotHeading: 'Tal como se envió', + 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 760bf993d46..cf8113c93a9 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -4237,7 +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.', - snapshotHeading: 'Tel que soumis', + 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 2d2067b5b78..77a92a83147 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -4158,7 +4158,7 @@ const ja = { progressBar: '承認の進捗', declaredActions: 'アクション', decisionPanelOffRequestPage: '承認の決定パネルは承認リクエストのページでのみ機能します。', - snapshotHeading: '申請時の内容', + snapshotAsSubmitted: '申請時の内容', attachmentChip: '添付ファイル', attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください', approveOneTitle: '「{{title}}」を承認しますか?', diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index 49dafb2108a..88214ea44be 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -4155,7 +4155,7 @@ const ko = { progressBar: '결정 진행 상황', declaredActions: '작업', decisionPanelOffRequestPage: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.', - snapshotHeading: '제출 당시 내용', + snapshotAsSubmitted: '제출 당시 내용', attachmentChip: '첨부 파일', attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요', approveOneTitle: '"{{title}}"을(를) 승인할까요?', diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index 7c878e31843..6f7bca09440 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -4236,7 +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.', - snapshotHeading: 'Conforme enviado', + 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 7896c3aba96..17275f5434b 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -4335,7 +4335,7 @@ const ru = { progressBar: 'Ход решения', declaredActions: 'Действия', decisionPanelOffRequestPage: 'Панель решения по согласованию работает только на страницах запросов на согласование.', - snapshotHeading: 'На момент подачи', + snapshotAsSubmitted: 'На момент подачи', attachmentChip: 'Вложение', attachmentOpenFailed: 'Не удалось открыть вложение — попробуйте ещё раз', approveOneTitle: 'Согласовать «{{title}}»?', diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index 18cdd87bbbc..29ea1b504fd 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -4346,7 +4346,7 @@ const zh = { progressBar: '决策进度', declaredActions: '操作', decisionPanelOffRequestPage: '审批决策面板仅在审批请求页面上可用。', - snapshotHeading: '提交时的内容', + snapshotAsSubmitted: '提交时的内容', attachmentChip: '附件', attachmentOpenFailed: '无法打开附件,请重试', approveOneTitle: '通过“{{title}}”?', From eda86beabc07c2a2947fa95ee5a7d6f04f808efe Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:55:55 +0000 Subject: [PATCH 3/4] docs(app-shell): the decision panel's summary heading in the README, and its changeset (#27502) Claude-Session: https://claude.ai/code/session_01Utp6CaRUZ2GQ5mSm8Z7Aqd Co-authored-by: Claude --- .changeset/27502-approval-summary-as-submitted.md | 13 +++++++++++++ ui/packages/app-shell/README.md | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/27502-approval-summary-as-submitted.md diff --git a/.changeset/27502-approval-summary-as-submitted.md b/.changeset/27502-approval-summary-as-submitted.md new file mode 100644 index 00000000000..724df86f2c0 --- /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-②: no +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 From 64409b100d0d30592370dabadd7185209770ff99 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 15:37:35 +0000 Subject: [PATCH 4/4] chore(changeset): declare the new i18n pack key as a widening (#27502) Claude-Session: https://claude.ai/code/session_01Utp6CaRUZ2GQ5mSm8Z7Aqd Co-authored-by: Claude --- .changeset/27502-approval-summary-as-submitted.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/27502-approval-summary-as-submitted.md b/.changeset/27502-approval-summary-as-submitted.md index 724df86f2c0..3cd79ec3bcb 100644 --- a/.changeset/27502-approval-summary-as-submitted.md +++ b/.changeset/27502-approval-summary-as-submitted.md @@ -5,7 +5,7 @@ fix(app-shell): the approval decision panel's record summary is headed "As submitted" (#27502) -Clause-②: no +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.