Skip to content

Commit 7d10b7c

Browse files
committed
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 <noreply@anthropic.com>
1 parent c0c0a0d commit 7d10b7c

13 files changed

Lines changed: 124 additions & 9 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/i18n': minor
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
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).
7+
8+
- **`@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.
9+
- **`@object-ui/i18n`.** One new language-pack key for that notice, `approvalsInbox.decisionPanelOffRequestPage`, in all ten packs.
10+
11+
**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.

‎packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.test.tsx‎

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
* location, what happens after a decision). The real bar, its dialog and the
1919
* POST it sends are pinned over the real record page in the console's
2020
* `approvalRequestsDataSource.decisionPage.test.tsx`.
21+
*
22+
* Off a request page the node draws a localized notice instead of nothing
23+
* (objectui#12072), so a block placed on the wrong page is visible to its
24+
* author. On a request page whose row has not loaded it still draws nothing.
2125
*/
2226

2327
import * as React from 'react';
@@ -34,6 +38,8 @@ vi.mock('../DeclaredActionsBar.js', () => ({
3438

3539
import { ComponentRegistry } from '@object-ui/core';
3640
import { RecordContextProvider, subscribeDataChanges, type DataChange } from '@object-ui/react';
41+
import { I18nProvider } from '@object-ui/i18n';
42+
import { builtInLocales } from '@object-ui/i18n/locales';
3743
import { ApprovalDecisionRenderer } from './ApprovalDecisionPanel';
3844

3945
afterEach(() => {
@@ -60,6 +66,16 @@ function mountOn(objectName: string, data: Record<string, unknown> | undefined)
6066
);
6167
}
6268

69+
/** The notice's pack key, and what one pack says for it. */
70+
const NOTICE_KEY = 'approvalsInbox.decisionPanelOffRequestPage';
71+
const packNotice = (language: string): unknown =>
72+
NOTICE_KEY.split('.').reduce<unknown>(
73+
(node, part) => (node as Record<string, unknown> | undefined)?.[part],
74+
(builtInLocales as Record<string, unknown>)[language],
75+
);
76+
77+
const noticeText = () => screen.getByTestId('approval-decision-off-request-page').textContent;
78+
6379
describe('the approval decision panel (objectui#12045)', () => {
6480
it('is module-internal this round: loading it registers no component type', () => {
6581
expect(ComponentRegistry.has('record:approval_decision')).toBe(false);
@@ -77,6 +93,8 @@ describe('the approval decision panel (objectui#12045)', () => {
7793
expect(props.location).toBe('record_section');
7894
expect(props.record).toMatchObject({ id: 'req_12045', viewer: REQUEST.viewer });
7995
expect(props.exclude, 'nothing is excluded: no other part of this page offers a decision').toBeUndefined();
96+
expect(screen.getByTestId('approval-decision-panel')).toBeTruthy();
97+
expect(screen.queryByTestId('approval-decision-off-request-page'), 'no notice on a request page').toBeNull();
8098
});
8199

82100
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)', () => {
100118
]);
101119
});
102120

103-
it.each([
104-
['another object', 'invoice', { id: 'inv_1' }],
105-
['a request page whose record has not loaded', 'sys_approval_request', undefined],
106-
])('renders nothing on %s', (_case, objectName, data) => {
107-
const { container } = mountOn(objectName, data);
121+
it('renders nothing on a request page whose record has not loaded', () => {
122+
const { container } = mountOn('sys_approval_request', undefined);
108123
expect(container.innerHTML).toBe('');
109124
expect(barProps).toHaveLength(0);
110125
});
111126
});
127+
128+
describe('off an approval request page, a localized notice instead of nothing (objectui#12072)', () => {
129+
it.each([
130+
["another object's record page", () => mountOn('invoice', { id: 'inv_1' })],
131+
['a page with no record context', () => render(<ApprovalDecisionRenderer />)],
132+
])('draws the notice on %s, and no panel', (_case, mount) => {
133+
mount();
134+
expect(noticeText()).toBe(packNotice('en'));
135+
expect(screen.queryByTestId('approval-decision-panel')).toBeNull();
136+
expect(barProps).toHaveLength(0);
137+
});
138+
139+
it("keeps the node's className and designer attributes on the notice", () => {
140+
render(<ApprovalDecisionRenderer className="mt-4" data-obj-id="node_1" data-obj-type="record:approval_decision" />);
141+
const wrapper = screen.getByTestId('approval-decision-off-request-page').parentElement!;
142+
expect(wrapper.className).toBe('mt-4');
143+
expect(wrapper.getAttribute('data-obj-id')).toBe('node_1');
144+
expect(wrapper.getAttribute('data-obj-type')).toBe('record:approval_decision');
145+
});
146+
147+
it('the notice copy is a key in all ten language packs', () => {
148+
const languages = Object.keys(builtInLocales);
149+
expect(languages).toHaveLength(10);
150+
for (const language of languages) {
151+
const value = packNotice(language);
152+
expect(typeof value, `${language} defines ${NOTICE_KEY}`).toBe('string');
153+
expect((value as string).trim(), `${language} defines ${NOTICE_KEY}`).not.toBe('');
154+
}
155+
});
156+
157+
it('renders the active language: the zh pack value under a zh provider', () => {
158+
render(
159+
<I18nProvider config={{ defaultLanguage: 'zh', detectBrowserLanguage: false }} persistLanguage={false}>
160+
<RecordContextProvider objectName="invoice" recordId="inv_1" data={{ id: 'inv_1' }}>
161+
<ApprovalDecisionRenderer />
162+
</RecordContextProvider>
163+
</I18nProvider>,
164+
);
165+
expect(noticeText()).toBe(packNotice('zh'));
166+
});
167+
168+
it('CONTROL: the en provider renders the en value, and the two packs differ', () => {
169+
render(
170+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
171+
<RecordContextProvider objectName="invoice" recordId="inv_1" data={{ id: 'inv_1' }}>
172+
<ApprovalDecisionRenderer />
173+
</RecordContextProvider>
174+
</I18nProvider>,
175+
);
176+
expect(noticeText()).toBe(packNotice('en'));
177+
expect(packNotice('zh')).not.toBe(packNotice('en'));
178+
});
179+
});

‎packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
* silently ignored prop.
3838
*
3939
* Outside a `sys_approval_request` record page there is no request to decide
40-
* on, and the panel renders nothing.
40+
* on, so the node draws a short notice saying it works only on approval request
41+
* pages (objectui#12072). A block placed on another object's record page, or on
42+
* a page with no record at all, is thereby visible to its author instead of
43+
* silently empty. On a request page whose row has not loaded yet, the node
44+
* still draws nothing.
4145
*
4246
* ## After a decision
4347
*
@@ -68,7 +72,7 @@
6872
*/
6973

7074
import * as React from 'react';
71-
import { notifyDataChanged, useRecordContext } from '@object-ui/react';
75+
import { notifyDataChanged, useObjectTranslation, useRecordContext } from '@object-ui/react';
7276
import { cn } from '@object-ui/components';
7377
import { DecisionProgressIndicator } from '../approval-progress/DecisionProgressIndicator.js';
7478
import { DeclaredActionsBar } from '../DeclaredActionsBar.js';
@@ -123,15 +127,37 @@ export interface ApprovalDecisionRendererProps {
123127
[k: string]: unknown;
124128
}
125129

126-
/** The node renderer: the panel over the bound request, or nothing. */
130+
/**
131+
* The node renderer: the panel over the bound request; off a request page, a
132+
* notice that the block works only there; on a request page whose row has not
133+
* loaded, nothing.
134+
*
135+
* The notice is the muted dashed placeholder `record:path` draws when it cannot
136+
* do its job where it was placed, with its copy from the language packs.
137+
*/
127138
export const ApprovalDecisionRenderer: React.FC<ApprovalDecisionRendererProps> = ({
128139
schema: _schema,
129140
className,
130141
...props
131142
}) => {
132143
const ctx = useRecordContext();
144+
const { t } = useObjectTranslation();
133145
const designer = splitDesigner(props);
134-
const row = ctx?.objectName === SYS_APPROVAL_REQUEST_OBJECT ? ctx.data : undefined;
146+
if (ctx?.objectName !== SYS_APPROVAL_REQUEST_OBJECT) {
147+
return (
148+
<div className={className} {...designer}>
149+
<div
150+
className="text-xs text-muted-foreground italic px-3 py-2 border border-dashed rounded"
151+
data-testid="approval-decision-off-request-page"
152+
>
153+
{String(t('approvalsInbox.decisionPanelOffRequestPage', {
154+
defaultValue: 'The approval decision panel only works on approval request pages.',
155+
}))}
156+
</div>
157+
</div>
158+
);
159+
}
160+
const row = ctx.data;
135161
if (row == null || typeof row !== 'object' || (row as { id?: unknown }).id == null) return null;
136162
return (
137163
<div className={className} {...designer}>

‎packages/i18n/src/locales/ar.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3907,6 +3907,7 @@ const ar = {
39073907
progressEligible_other: '{{count}} معتمِد مؤهل',
39083908
progressBar: 'تقدم القرار',
39093909
declaredActions: 'الإجراءات',
3910+
decisionPanelOffRequestPage: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.',
39103911
attachmentChip: 'مرفق',
39113912
attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى',
39123913
approveOneTitle: 'الموافقة على "{{title}}"؟',

‎packages/i18n/src/locales/de.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,6 +3616,7 @@ const de = {
36163616
progressEligible_other: '{{count}} berechtigte Genehmiger',
36173617
progressBar: 'Entscheidungsfortschritt',
36183618
declaredActions: 'Aktionen',
3619+
decisionPanelOffRequestPage: 'Der Entscheidungsbereich für Genehmigungen funktioniert nur auf Seiten von Genehmigungsanfragen.',
36193620
attachmentChip: 'Anhang',
36203621
attachmentOpenFailed: 'Anhang konnte nicht geöffnet werden – bitte erneut versuchen',
36213622
approveOneTitle: '„{{title}}“ genehmigen?',

‎packages/i18n/src/locales/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4247,6 +4247,7 @@ const en = {
42474247
progressEligible_other: '{{count}} eligible approvers',
42484248
progressBar: 'Decision progress',
42494249
declaredActions: 'Actions',
4250+
decisionPanelOffRequestPage: 'The approval decision panel only works on approval request pages.',
42504251
attachmentChip: 'Attachment',
42514252
attachmentOpenFailed: 'Could not open the attachment — please try again',
42524253
approveOneTitle: 'Approve "{{title}}"?',

‎packages/i18n/src/locales/es.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3687,6 +3687,7 @@ const es = {
36873687
progressEligible_other: '{{count}} aprobadores elegibles',
36883688
progressBar: 'Progreso de la decisión',
36893689
declaredActions: 'Acciones',
3690+
decisionPanelOffRequestPage: 'El panel de decisión de aprobación solo funciona en las páginas de solicitudes de aprobación.',
36903691
attachmentChip: 'Adjunto',
36913692
attachmentOpenFailed: 'No se pudo abrir el archivo adjunto: inténtalo de nuevo',
36923693
approveOneTitle: '¿Aprobar «{{title}}»?',

‎packages/i18n/src/locales/fr.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3684,6 +3684,7 @@ const fr = {
36843684
progressEligible_other: '{{count}} approbateurs éligibles',
36853685
progressBar: 'Avancement de la décision',
36863686
declaredActions: 'Actions',
3687+
decisionPanelOffRequestPage: 'Le panneau de décision d\'approbation ne fonctionne que sur les pages de demande d\'approbation.',
36873688
attachmentChip: 'Pièce jointe',
36883689
attachmentOpenFailed: "Impossible d'ouvrir la pièce jointe — veuillez réessayer",
36893690
approveOneTitle: 'Approuver « {{title}} » ?',

‎packages/i18n/src/locales/ja.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3618,6 +3618,7 @@ const ja = {
36183618
progressEligible_other: '承認可能者 {{count}} 名',
36193619
progressBar: '承認の進捗',
36203620
declaredActions: 'アクション',
3621+
decisionPanelOffRequestPage: '承認の決定パネルは承認リクエストのページでのみ機能します。',
36213622
attachmentChip: '添付ファイル',
36223623
attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください',
36233624
approveOneTitle: '「{{title}}」を承認しますか?',

‎packages/i18n/src/locales/ko.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3615,6 +3615,7 @@ const ko = {
36153615
progressEligible_other: '승인 가능자 {{count}}명',
36163616
progressBar: '결정 진행 상황',
36173617
declaredActions: '작업',
3618+
decisionPanelOffRequestPage: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.',
36183619
attachmentChip: '첨부 파일',
36193620
attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요',
36203621
approveOneTitle: '"{{title}}"을(를) 승인할까요?',

0 commit comments

Comments
 (0)