Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/12045-approval-decision-panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
11 changes: 11 additions & 0 deletions .changeset/12072-decision-panel-notice.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(() => {
Expand All @@ -60,6 +66,16 @@ function mountOn(objectName: string, data: Record<string, unknown> | 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<unknown>(
(node, part) => (node as Record<string, unknown> | undefined)?.[part],
(builtInLocales as Record<string, unknown>)[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);
Expand All @@ -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', () => {
Expand All @@ -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(<ApprovalDecisionRenderer />)],
])('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(<ApprovalDecisionRenderer className="mt-4" data-obj-id="node_1" data-obj-type="record:approval_decision" />);
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(
<I18nProvider config={{ defaultLanguage: 'zh', detectBrowserLanguage: false }} persistLanguage={false}>
<RecordContextProvider objectName="invoice" recordId="inv_1" data={{ id: 'inv_1' }}>
<ApprovalDecisionRenderer />
</RecordContextProvider>
</I18nProvider>,
);
expect(noticeText()).toBe(packNotice('zh'));
});

it('CONTROL: the en provider renders the en value, and the two packs differ', () => {
render(
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
<RecordContextProvider objectName="invoice" recordId="inv_1" data={{ id: 'inv_1' }}>
<ApprovalDecisionRenderer />
</RecordContextProvider>
</I18nProvider>,
);
expect(noticeText()).toBe(packNotice('en'));
expect(packNotice('zh')).not.toBe(packNotice('en'));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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<ApprovalDecisionRendererProps> = ({
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 (
<div className={className} {...designer}>
<div
className="text-xs text-muted-foreground italic px-3 py-2 border border-dashed rounded"
data-testid="approval-decision-off-request-page"
>
{String(t('approvalsInbox.decisionPanelOffRequestPage', {
defaultValue: 'The approval decision panel only works on approval request pages.',
}))}
</div>
</div>
);
}
const row = ctx.data;
if (row == null || typeof row !== 'object' || (row as { id?: unknown }).id == null) return null;
return (
<div className={className} {...designer}>
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3907,6 +3907,7 @@ const ar = {
progressEligible_other: '{{count}} معتمِد مؤهل',
progressBar: 'تقدم القرار',
declaredActions: 'الإجراءات',
decisionPanelOffRequestPage: 'لوحة قرار الموافقة تعمل فقط في صفحات طلبات الموافقة.',
attachmentChip: 'مرفق',
attachmentOpenFailed: 'تعذّر فتح المرفق — يرجى المحاولة مرة أخرى',
approveOneTitle: 'الموافقة على "{{title}}"؟',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}»?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}} » ?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3618,6 +3618,7 @@ const ja = {
progressEligible_other: '承認可能者 {{count}} 名',
progressBar: '承認の進捗',
declaredActions: 'アクション',
decisionPanelOffRequestPage: '承認の決定パネルは承認リクエストのページでのみ機能します。',
attachmentChip: '添付ファイル',
attachmentOpenFailed: '添付ファイルを開けませんでした。もう一度お試しください',
approveOneTitle: '「{{title}}」を承認しますか?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3615,6 +3615,7 @@ const ko = {
progressEligible_other: '승인 가능자 {{count}}명',
progressBar: '결정 진행 상황',
declaredActions: '작업',
decisionPanelOffRequestPage: '승인 결정 패널은 승인 요청 페이지에서만 작동합니다.',
attachmentChip: '첨부 파일',
attachmentOpenFailed: '첨부 파일을 열 수 없습니다. 다시 시도해 주세요',
approveOneTitle: '"{{title}}"을(를) 승인할까요?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3774,6 +3774,7 @@ const ru = {
progressEligible_other: 'Доступных согласующих: {{count}}',
progressBar: 'Ход решения',
declaredActions: 'Действия',
decisionPanelOffRequestPage: 'Панель решения по согласованию работает только на страницах запросов на согласование.',
attachmentChip: 'Вложение',
attachmentOpenFailed: 'Не удалось открыть вложение — попробуйте ещё раз',
approveOneTitle: 'Согласовать «{{title}}»?',
Expand Down
1 change: 1 addition & 0 deletions packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3767,6 +3767,7 @@ const zh = {
progressEligible_other: '{{count}} 位可审批人',
progressBar: '决策进度',
declaredActions: '操作',
decisionPanelOffRequestPage: '审批决策面板仅在审批请求页面上可用。',
attachmentChip: '附件',
attachmentOpenFailed: '无法打开附件,请重试',
approveOneTitle: '通过“{{title}}”?',
Expand Down
Loading