From 7d10b7c05faf52da9c125ac154da205f0fb6db0b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:11:19 +0000 Subject: [PATCH 1/2] fix(app-shell): the approval decision panel draws a localized notice off an approval request page (objectui#12072) Off a sys_approval_request record page (another object's record page, or a page with no record context) the record:approval_decision renderer now draws a short notice that it works only on approval request pages, instead of null. The outside test is the existing objectName comparison, kept as one test. On a request page whose row has not loaded it still draws nothing; with its row it draws the panel unchanged. One new language-pack key, approvalsInbox.decisionPanelOffRequestPage, in all ten packs. The panel stays module-internal: no registration, index import, known-types line or zod arm. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude --- .changeset/12072-decision-panel-notice.md | 11 +++ .../ApprovalDecisionPanel.test.tsx | 78 +++++++++++++++++-- .../ApprovalDecisionPanel.tsx | 34 +++++++- packages/i18n/src/locales/ar.ts | 1 + packages/i18n/src/locales/de.ts | 1 + packages/i18n/src/locales/en.ts | 1 + packages/i18n/src/locales/es.ts | 1 + packages/i18n/src/locales/fr.ts | 1 + packages/i18n/src/locales/ja.ts | 1 + packages/i18n/src/locales/ko.ts | 1 + packages/i18n/src/locales/pt.ts | 1 + packages/i18n/src/locales/ru.ts | 1 + packages/i18n/src/locales/zh.ts | 1 + 13 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 .changeset/12072-decision-panel-notice.md diff --git a/.changeset/12072-decision-panel-notice.md b/.changeset/12072-decision-panel-notice.md new file mode 100644 index 0000000000..45ccefbd13 --- /dev/null +++ b/.changeset/12072-decision-panel-notice.md @@ -0,0 +1,11 @@ +--- +'@object-ui/i18n': minor +'@object-ui/app-shell': patch +--- + +The approval decision panel draws a short notice instead of nothing when it is placed anywhere other than an approval request's record page (objectui#12072). + +- **`@object-ui/app-shell`.** Off a `sys_approval_request` record page, that is on another object's record page or on a page with no record at all, the decision panel (the renderer proposed as `record:approval_decision`, still module-internal) now draws a muted notice saying it works only on approval request pages. A misplaced block is therefore visible to its author instead of silently empty. On a request page whose record has not loaded yet it still draws nothing, and on a request page with its record it draws the panel as before. This replaces the clause of objectui#12045's entry that says the panel renders nothing outside a request page. +- **`@object-ui/i18n`.** One new language-pack key for that notice, `approvalsInbox.decisionPanelOffRequestPage`, in all ten packs. + +**Clause-②: yes (widening).** The published language packs gain one key, `approvalsInbox.decisionPanelOffRequestPage`, so the exported `en` pack and the `TranslationKeys` type read off it gain that one member. No export or prop is added or removed, and no existing key changes. diff --git a/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.test.tsx b/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.test.tsx index d8cd25e0bd..1076f21039 100644 --- a/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.test.tsx +++ b/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.test.tsx @@ -18,6 +18,10 @@ * location, what happens after a decision). The real bar, its dialog and the * POST it sends are pinned over the real record page in the console's * `approvalRequestsDataSource.decisionPage.test.tsx`. + * + * Off a request page the node draws a localized notice instead of nothing + * (objectui#12072), so a block placed on the wrong page is visible to its + * author. On a request page whose row has not loaded it still draws nothing. */ import * as React from 'react'; @@ -34,6 +38,8 @@ vi.mock('../DeclaredActionsBar.js', () => ({ import { ComponentRegistry } from '@object-ui/core'; import { RecordContextProvider, subscribeDataChanges, type DataChange } from '@object-ui/react'; +import { I18nProvider } from '@object-ui/i18n'; +import { builtInLocales } from '@object-ui/i18n/locales'; import { ApprovalDecisionRenderer } from './ApprovalDecisionPanel'; afterEach(() => { @@ -60,6 +66,16 @@ function mountOn(objectName: string, data: Record | undefined) ); } +/** The notice's pack key, and what one pack says for it. */ +const NOTICE_KEY = 'approvalsInbox.decisionPanelOffRequestPage'; +const packNotice = (language: string): unknown => + NOTICE_KEY.split('.').reduce( + (node, part) => (node as Record | undefined)?.[part], + (builtInLocales as Record)[language], + ); + +const noticeText = () => screen.getByTestId('approval-decision-off-request-page').textContent; + describe('the approval decision panel (objectui#12045)', () => { it('is module-internal this round: loading it registers no component type', () => { expect(ComponentRegistry.has('record:approval_decision')).toBe(false); @@ -77,6 +93,8 @@ describe('the approval decision panel (objectui#12045)', () => { expect(props.location).toBe('record_section'); expect(props.record).toMatchObject({ id: 'req_12045', viewer: REQUEST.viewer }); expect(props.exclude, 'nothing is excluded: no other part of this page offers a decision').toBeUndefined(); + expect(screen.getByTestId('approval-decision-panel')).toBeTruthy(); + expect(screen.queryByTestId('approval-decision-off-request-page'), 'no notice on a request page').toBeNull(); }); it('draws no tally for a node that carries none, and still offers the actions', () => { @@ -100,12 +118,62 @@ describe('the approval decision panel (objectui#12045)', () => { ]); }); - it.each([ - ['another object', 'invoice', { id: 'inv_1' }], - ['a request page whose record has not loaded', 'sys_approval_request', undefined], - ])('renders nothing on %s', (_case, objectName, data) => { - const { container } = mountOn(objectName, data); + it('renders nothing on a request page whose record has not loaded', () => { + const { container } = mountOn('sys_approval_request', undefined); expect(container.innerHTML).toBe(''); expect(barProps).toHaveLength(0); }); }); + +describe('off an approval request page, a localized notice instead of nothing (objectui#12072)', () => { + it.each([ + ["another object's record page", () => mountOn('invoice', { id: 'inv_1' })], + ['a page with no record context', () => render()], + ])('draws the notice on %s, and no panel', (_case, mount) => { + mount(); + expect(noticeText()).toBe(packNotice('en')); + expect(screen.queryByTestId('approval-decision-panel')).toBeNull(); + expect(barProps).toHaveLength(0); + }); + + it("keeps the node's className and designer attributes on the notice", () => { + render(); + const wrapper = screen.getByTestId('approval-decision-off-request-page').parentElement!; + expect(wrapper.className).toBe('mt-4'); + expect(wrapper.getAttribute('data-obj-id')).toBe('node_1'); + expect(wrapper.getAttribute('data-obj-type')).toBe('record:approval_decision'); + }); + + it('the notice 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 = packNotice(language); + expect(typeof value, `${language} defines ${NOTICE_KEY}`).toBe('string'); + expect((value as string).trim(), `${language} defines ${NOTICE_KEY}`).not.toBe(''); + } + }); + + it('renders the active language: the zh pack value under a zh provider', () => { + render( + + + + + , + ); + expect(noticeText()).toBe(packNotice('zh')); + }); + + it('CONTROL: the en provider renders the en value, and the two packs differ', () => { + render( + + + + + , + ); + expect(noticeText()).toBe(packNotice('en')); + expect(packNotice('zh')).not.toBe(packNotice('en')); + }); +}); diff --git a/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx b/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx index 229201a847..fe48ddfb6d 100644 --- a/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx +++ b/packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx @@ -37,7 +37,11 @@ * silently ignored prop. * * Outside a `sys_approval_request` record page there is no request to decide - * on, and the panel renders nothing. + * on, so the node draws a short notice saying it works only on approval request + * pages (objectui#12072). A block placed on another object's record page, or on + * a page with no record at all, is thereby visible to its author instead of + * silently empty. On a request page whose row has not loaded yet, the node + * still draws nothing. * * ## After a decision * @@ -68,7 +72,7 @@ */ import * as React from 'react'; -import { notifyDataChanged, useRecordContext } from '@object-ui/react'; +import { notifyDataChanged, useObjectTranslation, useRecordContext } from '@object-ui/react'; import { cn } from '@object-ui/components'; import { DecisionProgressIndicator } from '../approval-progress/DecisionProgressIndicator.js'; import { DeclaredActionsBar } from '../DeclaredActionsBar.js'; @@ -123,15 +127,37 @@ export interface ApprovalDecisionRendererProps { [k: string]: unknown; } -/** The node renderer: the panel over the bound request, or nothing. */ +/** + * The node renderer: the panel over the bound request; off a request page, a + * notice that the block works only there; on a request page whose row has not + * loaded, nothing. + * + * The notice is the muted dashed placeholder `record:path` draws when it cannot + * do its job where it was placed, with its copy from the language packs. + */ export const ApprovalDecisionRenderer: React.FC = ({ schema: _schema, className, ...props }) => { const ctx = useRecordContext(); + const { t } = useObjectTranslation(); const designer = splitDesigner(props); - const row = ctx?.objectName === SYS_APPROVAL_REQUEST_OBJECT ? ctx.data : undefined; + if (ctx?.objectName !== SYS_APPROVAL_REQUEST_OBJECT) { + return ( +
+
+ {String(t('approvalsInbox.decisionPanelOffRequestPage', { + defaultValue: 'The approval decision panel only works on approval request pages.', + }))} +
+
+ ); + } + const row = ctx.data; if (row == null || typeof row !== 'object' || (row as { id?: unknown }).id == null) return null; return (
diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 8f7a5d2465..16ccd4eb59 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -3907,6 +3907,7 @@ const ar = { progressEligible_other: '{{count}} معتمِد مؤهل', progressBar: 'تقدم القرار', declaredActions: 'الإجراءات', + decisionPanelOffRequestPage: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.', attachmentChip: 'مرفق', attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى', approveOneTitle: 'الموافقة على "{{title}}"؟', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index 971c75180c..8de519a2c3 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -3616,6 +3616,7 @@ const de = { progressEligible_other: '{{count}} berechtigte Genehmiger', progressBar: 'Entscheidungsfortschritt', declaredActions: 'Aktionen', + decisionPanelOffRequestPage: 'Der Entscheidungsbereich für Genehmigungen funktioniert nur auf Seiten von Genehmigungsanfragen.', attachmentChip: 'Anhang', attachmentOpenFailed: 'Anhang konnte nicht geöffnet werden – bitte erneut versuchen', approveOneTitle: '„{{title}}“ genehmigen?', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index bbf03a05c2..e31441f3a5 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -4247,6 +4247,7 @@ const en = { progressEligible_other: '{{count}} eligible approvers', progressBar: 'Decision progress', declaredActions: 'Actions', + decisionPanelOffRequestPage: 'The approval decision panel only works on approval request pages.', attachmentChip: 'Attachment', attachmentOpenFailed: 'Could not open the attachment — please try again', approveOneTitle: 'Approve "{{title}}"?', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index dcb2575226..e434204e4a 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -3687,6 +3687,7 @@ const es = { progressEligible_other: '{{count}} aprobadores elegibles', 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.', attachmentChip: 'Adjunto', attachmentOpenFailed: 'No se pudo abrir el archivo adjunto: inténtalo de nuevo', approveOneTitle: '¿Aprobar «{{title}}»?', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index c804867eb6..ecfffb6c5b 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -3684,6 +3684,7 @@ const fr = { progressEligible_other: '{{count}} approbateurs éligibles', 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.', attachmentChip: 'Pièce jointe', attachmentOpenFailed: "Impossible d'ouvrir la pièce jointe — veuillez réessayer", approveOneTitle: 'Approuver « {{title}} » ?', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index 816bc8e04b..ca8a353895 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -3618,6 +3618,7 @@ const ja = { progressEligible_other: '承認可能者 {{count}} 名', progressBar: '承認の進捗', declaredActions: 'アクション', + decisionPanelOffRequestPage: '承認の決定パネルは承認リクエストのページでのみ機能します。', attachmentChip: '添付ファイル', attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください', approveOneTitle: '「{{title}}」を承認しますか?', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index 42aaeb4455..93a4fe9f5b 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -3615,6 +3615,7 @@ const ko = { progressEligible_other: '승인 가능자 {{count}}명', progressBar: '결정 진행 상황', declaredActions: '작업', + decisionPanelOffRequestPage: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.', attachmentChip: '첨부 파일', attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요', approveOneTitle: '"{{title}}"을(를) 승인할까요?', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index b8f8dd0afd..9df49af97d 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -3682,6 +3682,7 @@ const pt = { progressEligible_other: '{{count}} aprovadores elegíveis', 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.', attachmentChip: 'Anexo', attachmentOpenFailed: 'Não foi possível abrir o anexo — tente novamente', approveOneTitle: 'Aprovar "{{title}}"?', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index 8749911f78..4b18efcdd3 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -3774,6 +3774,7 @@ const ru = { progressEligible_other: 'Доступных согласующих: {{count}}', progressBar: 'Ход решения', declaredActions: 'Действия', + decisionPanelOffRequestPage: 'Панель решения по согласованию работает только на страницах запросов на согласование.', attachmentChip: 'Вложение', attachmentOpenFailed: 'Не удалось открыть вложение — попробуйте ещё раз', approveOneTitle: 'Согласовать «{{title}}»?', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index a846fe99c5..b89bcf7f11 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -3767,6 +3767,7 @@ const zh = { progressEligible_other: '{{count}} 位可审批人', progressBar: '决策进度', declaredActions: '操作', + decisionPanelOffRequestPage: '审批决策面板仅在审批请求页面上可用。', attachmentChip: '附件', attachmentOpenFailed: '无法打开附件,请重试', approveOneTitle: '通过“{{title}}”?', From 043680ff85ef50f1bfd8931056ea16adca4f3f6a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:27:47 +0000 Subject: [PATCH 2/2] docs(changeset): objectui#12045's entry is superseded in part by objectui#12072 Append-only rider on the pending objectui#12045 changeset: outside a sys_approval_request record page the decision panel now draws a localized notice instead of nothing, so that entry's "renders nothing" clause no longer holds when both changes release together. Front matter and every existing line unchanged (claim amendment 6090288136). Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude --- .changeset/12045-approval-decision-panel.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/12045-approval-decision-panel.md b/.changeset/12045-approval-decision-panel.md index 4863643e94..270c4820b7 100644 --- a/.changeset/12045-approval-decision-panel.md +++ b/.changeset/12045-approval-decision-panel.md @@ -13,3 +13,5 @@ A pointer field draws the record it points at, the console reads an approval req - **The console's approval request page.** The console's record route for `sys_approval_request` mounts the record page over the routed approvals source, so the request carries the `viewer` block its declared actions gate on and the decision tally. The source now reads one request's `sys_approval_action` timeline from `GET /approvals/requests/:id/actions`, applying the read's `$orderby`, `$top` and `$skip` itself, dropping `$select` and `$expand`, and refusing any other parameter with `UNSUPPORTED_QUERY_PARAM`. On a request's own page the record view no longer asks for approvals opened on the request itself. No language-pack key is added, and `CellRendererProps` is unchanged. + +**Superseded in part (objectui#12072):** outside a `sys_approval_request` record page the panel now draws a short localized notice instead of nothing, so the sentence above saying it renders nothing there no longer holds when both changes release together.