Skip to content

Commit e435ef3

Browse files
hotlongclaude
andcommitted
fix(plugin-detail,i18n,e2e): the five UI gates the record page v3 branch tripped
- useRelatedArrangement reads the 48 h 「新」 clock once per mount (useState initializer) instead of Date.now() in render; every edit still stamps its own time in the handler (react-hooks purity). - RelatedCardStack-27055's tabbed-tier probe reports the scope through a callback prop instead of reassigning an outer variable from a component. - RecordMetaFooter's OWNER_FIELD is SystemFieldName.OWNER_ID from @objectstack/spec/system, backing its "canonical" claim (check:spec-symbols). - RelatedList formats body-table totals and the compact card's date chip in useDisplayLocale(), not the machine locale (machineLocaleCensus-9909). - residue-namespaces-3546 pins RelatedList's three `detail.add` affordances (picker title, compact +, full list Add). - record-header-title-width re-anchors page:header's tail on PageHeaderActions' toolbar now that refresh lives in ⋯, with a mobile tail of one inline action plus ⋯. Claude-Session: https://claude.ai/code/session_cdb7a717def74e719988f089fbaa18ff Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 49b9a99 commit e435ef3

6 files changed

Lines changed: 31 additions & 17 deletions

File tree

‎ui/e2e/record-header-title-width.spec.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -574,16 +574,18 @@ const PAGE_HEADER = {
574574
'sm:flex-row',
575575
]),
576576
titleColumn: classBefore(containersSrc, CONTAINERS_FILE, '<RecordTitleChip', ['flex-col', 'min-w-0']),
577-
// The tail as it renders when the host wires `refresh` (the console does).
578-
tail: classAt(containersSrc, CONTAINERS_FILE, 'if (!refreshButton) return actions;', 1, ['shrink-0']),
577+
// The tail is `PageHeaderActions`' toolbar — the one row whether or not the
578+
// host wires `refresh`, which is the first item of its `⋯` menu since
579+
// objectstack#27055 (no separate `⟳` button).
580+
tail: classBefore(containersSrc, CONTAINERS_FILE, 'role="toolbar"', ['shrink-0']),
579581
chip: classByPattern(chipSrc, CHIP_FILE, /<div className=\{cn\('([^']+)', className\)\}>/, ['min-w-0']),
580582
chipRow: classBefore(chipSrc, CHIP_FILE, '{icon && (', ['flex-wrap']),
581583
// #1 after the anchor is the icon's own span; #2 is the h1, #3 the object label.
582584
h1: classAt(chipSrc, CHIP_FILE, '{icon && (', 2, ['truncate']),
583585
objectLabel: classAt(chipSrc, CHIP_FILE, '{icon && (', 3, ['shrink-0']),
584586
};
585587

586-
/** The mobile tail: one inline action, the `⋯` trigger and the `⟳` chrome. */
588+
/** The mobile tail: one inline action (`mobileMaxVisible` 1) and the `⋯` trigger, which carries refresh. */
587589
const PAGE_HEADER_TAIL_BOX = 'h-9 w-12 shrink-0';
588590

589591
const pageHeaderFixture = (title: string) =>
@@ -598,7 +600,7 @@ const pageHeaderFixture = (title: string) =>
598600
`<h1 class="${PAGE_HEADER.h1}">${title}</h1><span class="${PAGE_HEADER.objectLabel}">Case</span>`,
599601
),
600602
),
601-
) + box(PAGE_HEADER.tail, Array.from({ length: 3 }, () => box(PAGE_HEADER_TAIL_BOX)).join('')),
603+
) + box(PAGE_HEADER.tail, Array.from({ length: 2 }, () => box(PAGE_HEADER_TAIL_BOX)).join('')),
602604
);
603605

604606
const PAGE_HEADER_CANDIDATES = [...Object.values(PAGE_HEADER), PAGE_HEADER_TAIL_BOX]

‎ui/packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -382,12 +382,14 @@ describe('objectui#3546 slice seven — the ratchet residue', () => {
382382
}
383383
}
384384
// PageView uses it twice on the same button (title + aria-label) and
385-
// RelatedList twice in two different affordances — the counts are pinned so a
386-
// silent drop of one of them is visible. AppContent's two record-save toast
385+
// RelatedList three times in three different affordances (the picker title,
386+
// the compact card's icon-only + and the full list's Add button,
387+
// objectstack#27055) — the counts are pinned so a silent drop of one of them
388+
// is visible. AppContent's two record-save toast
387389
// sites share one message builder since objectstack-ai/objectstack#26172,
388390
// so its fallback label is written once.
389391
expect(sourceOf(PAGE_VIEW).split("t('common.editInStudio', { defaultValue: 'Edit in studio' })").length - 1).toBe(2);
390-
expect(sourceOf(RELATED_LIST).split("t('detail.add', { defaultValue: 'Add' })").length - 1).toBe(2);
392+
expect(sourceOf(RELATED_LIST).split("t('detail.add', { defaultValue: 'Add' })").length - 1).toBe(3);
391393
expect(sourceOf(APP_CONTENT).split("t('common.record', { defaultValue: 'Record' })").length - 1).toBe(1);
392394
});
393395

‎ui/packages/plugin-detail/src/RecordMetaFooter.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import * as React from 'react';
1010
import { cn, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@object-ui/components';
1111
import { getCellRenderer, resolveCellRendererType } from '@object-ui/fields';
12+
import { SystemFieldName } from '@objectstack/spec/system';
1213
import { AUDIT_FIELD_BY_ROLE } from '@object-ui/types';
1314
import type { FieldMetadata } from '@object-ui/types';
1415
import { useDisplayLocale } from '@object-ui/i18n';
@@ -18,9 +19,9 @@ import { useDefaultRecordPage } from './defaultRecordPage';
1819

1920
/**
2021
* The record's reassignable owner column — the canonical injected
21-
* `owner_id` (`@objectstack/spec` `injected-system-column-provenance.ts`).
22+
* `owner_id`, read from `@objectstack/spec`'s `SystemFieldName`.
2223
*/
23-
const OWNER_FIELD = 'owner_id';
24+
const OWNER_FIELD = SystemFieldName.OWNER_ID;
2425

2526
/**
2627
* Audit field names auto-injected by the framework's `applySystemFields` —

‎ui/packages/plugin-detail/src/RelatedList.tsx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
} from '@object-ui/core';
7676
import { useSafeFieldLabel } from '@object-ui/react';
7777
import { usePermissions } from '@object-ui/permissions';
78+
import { useDisplayLocale } from '@object-ui/i18n';
7879
import { useDetailTranslation } from './useDetailTranslation';
7980
import { isMaskedDetailFieldType } from './fieldEnrichment';
8081
import {
@@ -654,6 +655,8 @@ const BodyTableTotals: React.FC<{ rows: readonly any[]; columns: any[] | undefin
654655
objectSchema,
655656
}) => {
656657
const { t } = useDetailTranslation();
658+
// The totals format in the reader's display locale, never the machine's.
659+
const displayLocale = useDisplayLocale();
657660
const fields: Record<string, any> = (objectSchema?.fields as any) ?? {};
658661
const totals: Array<{ key: string; label: string; text: string }> = [];
659662
for (const col of columns ?? []) {
@@ -673,8 +676,8 @@ const BodyTableTotals: React.FC<{ rows: readonly any[]; columns: any[] | undefin
673676
let text: string;
674677
try {
675678
text = currency
676-
? new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(sum)
677-
: new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(sum);
679+
? new Intl.NumberFormat(displayLocale, { style: 'currency', currency }).format(sum)
680+
: new Intl.NumberFormat(displayLocale, { maximumFractionDigits: 2 }).format(sum);
678681
} catch {
679682
text = String(sum);
680683
}
@@ -772,6 +775,8 @@ export const RelatedList: React.FC<RelatedListProps> = ({
772775
// The host predicate scope a child row's declared deadline is judged in
773776
// (objectui#11815) — the scope the grid's cells read.
774777
const predicateScope = usePredicateScope();
778+
// The compact card's date chip formats in the reader's display locale.
779+
const displayLocale = useDisplayLocale();
775780
const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
776781
// Add-by-picker (generic m2m/junction assignment). `refreshNonce` re-runs the
777782
// auto-fetch after an add/remove so the list reflects the new link rows.
@@ -2687,7 +2692,7 @@ export const RelatedList: React.FC<RelatedListProps> = ({
26872692
if (Number.isNaN(date.getTime())) continue;
26882693
return (
26892694
<span className="shrink-0 text-[11px] text-muted-foreground" data-related-card-chip="date">
2690-
{date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
2695+
{date.toLocaleDateString(displayLocale, { month: 'short', day: 'numeric' })}
26912696
</span>
26922697
);
26932698
}

‎ui/packages/plugin-detail/src/__tests__/RelatedCardStack-27055.test.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,20 @@ describe('the tabbed tier 关联 tab reads the same arrangement (objectstack#270
206206
let stored = editRelatedArrangement(DEFAULT, undefined, { kind: 'move', id: 'call.lead_id', to: 0 }, Date.now());
207207
stored = editRelatedArrangement(DEFAULT, stored, { kind: 'hide', id: 'note.parent_id' }, Date.now());
208208
let arranged: any = null;
209-
let scope: any = null;
210-
const Probe = () => {
211-
scope = React.useContext(RelatedArrangementScopeContext);
209+
const onScope = vi.fn();
210+
const Probe = ({ report }: { report: (value: unknown) => void }) => {
211+
report(React.useContext(RelatedArrangementScopeContext));
212212
return null;
213213
};
214214
render(
215215
<ArrangedRelatedTab page={pageOf()} arrangement={stored} onArrangementChange={vi.fn()}>
216216
{(page) => {
217217
arranged = page;
218-
return <Probe />;
218+
return <Probe report={onScope} />;
219219
}}
220220
</ArrangedRelatedTab>,
221221
);
222+
const scope: any = onScope.mock.calls.at(-1)?.[0];
222223
const children = relatedTabOf(arranged).children;
223224
expect(children.map((n: any) => n.properties?.objectName ?? n.type)).toEqual(['call', 'task', 'record:attachments']);
224225
expect(scope.lastId).toBe('task.lead_id');

‎ui/packages/plugin-detail/src/useRelatedArrangement.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ export function useRelatedArrangement({
6464
const [localArrangement, setLocalArrangement] = React.useState<RelatedArrangement | undefined>(undefined);
6565
const stored = onArrangementChange ? arrangement : localArrangement;
6666
const write = onArrangementChange ?? setLocalArrangement;
67-
const resolved = resolveRelatedArrangement(defaultOrder, stored, Date.now());
67+
// The 48 h 「新」 clock is read once per mount (a render must stay pure);
68+
// every edit below stamps its own time in the event handler.
69+
const [now] = React.useState(() => Date.now());
70+
const resolved = resolveRelatedArrangement(defaultOrder, stored, now);
6871
const edit = (change: RelatedArrangementEdit) =>
6972
write(editRelatedArrangement(defaultOrder, stored, change, Date.now()));
7073

0 commit comments

Comments
 (0)