Skip to content
Merged
23 changes: 23 additions & 0 deletions .changeset/27524-record-page-related-panels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@object-ui/plugin-detail': patch
'@object-ui/i18n': patch
'@object-ui/app-shell': patch
---

feat(plugin-detail)!: the record page's related panels lose collapse — the header opens the whole list, ⠿ + ⋯ on hover, ⋯ holds Hide alone, empty panels fold into one line, and the stored arrangement drops `collapsed`

Clause-②: yes (narrowing)
Metadata-protocol: none

<!-- adr-0087: not-required (runtime-interface-only ui/packages/plugin-detail/src/synth/relatedArrangement.ts#RelatedArrangement, ui/packages/plugin-detail/src/synth/relatedArrangement.ts#ResolvedRelatedArrangement, ui/packages/plugin-detail/src/synth/relatedArrangement.ts#RelatedArrangementEdit, ui/packages/i18n/src/locales/en.ts#TranslationKeys) a reader's per-browser layout preference and its edit vocabulary, and the key type of the console's own translation catalogue: no Zod schema, no spec declaration and no stored metadata reads them, so the compiler is the channel that reaches every caller; the stored preference moves to version 2 and a version-1 record reads as no choice -->

The record page's related column is a stack of same-shaped panels (record page v4).

- **The panel header** is the title, then the count chip. The header row (or the chip) opens the panel's whole list; on hover the title's end shows › and its tooltip reads "View all N". On hover or focus the right end shows ⠿ (drag to reorder; focused, ⌥↑ / ⌥↓ move the panel), + and ⋯, which now holds Hide and nothing else. There is no collapse and no ⤢, and nothing is reserved on the left.
- **Empty panels** (0 rows) fold into one line after the others: "N more empty lists · name · … · Expand". Expand draws them again in their places. The Related tab of an authored page keeps its own line.
- **The compact row's first column** skips the foreign key that points back at the record on the page: a product's line items, whose name field is the product, are named by their next column.
- **The stored arrangement** (`record.related.arrangement`, per reader × object) is `{ v, order, hidden, basedOn, seen }`. `collapsed` is removed with no compatibility read: a reader's earlier record reads as no choice, and the panels show in the default order.

**BREAKING** (`@object-ui/plugin-detail` and `@object-ui/i18n`, TypeScript only): `@object-ui/i18n`'s exported `TranslationKeys` loses the four `detail.relatedCard*` keys named below (a `t()` call with one of them still compiles, its key being a `string`, and reads the key back); and `RelatedArrangement` and `ResolvedRelatedArrangement` no longer carry `collapsed`, `RelatedArrangementEdit` no longer has the `collapse` kind, and `resolveRelatedArrangement` / `editRelatedArrangement` no longer take a last `defaultCollapsed` argument. The only caller is the console's own arranged column. A caller still passing one gets a compile error naming it; the fix is to drop it, since a panel has no collapsed state to set. `RELATED_ARRANGEMENT_RECORD_VERSION` (2) is new.

- The i18n keys `detail.relatedCardMoveUp`, `detail.relatedCardMoveDown`, `detail.relatedCardCollapse` and `detail.relatedCardCount` leave every pack; `detail.relatedPanelViewAll`, `detail.relatedPanelReorder`, `detail.relatedPanelReorderHint` and `detail.relatedEmptyFold` arrive in all ten.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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.
*/

/**
* objectstack#27524, Target 10 — the stored arrangement of the record page's
* related panels is `{ v, order, hidden, basedOn, seen }`. `collapsed` left
* the shape with no compatibility read: a record of the earlier version (the
* one that carried it) is "no choice", and the reader gets the default order
* again. This file runs in the node project (no `window`).
*/

import { describe, it, expect } from 'vitest';
import {
RECORD_RELATED_ARRANGEMENT_PREFERENCE,
objectPreferenceScope,
readWorkspacePreference,
writeWorkspacePreference,
} from './workspacePreference';

const lead = objectPreferenceScope('crm_lead');
const KEY = 'os:pref:record.related.arrangement@object:crm_lead';

const memoryStorage = () => {
const kept = new Map<string, string>();
return {
kept,
getItem: (key: string) => kept.get(key) ?? null,
setItem: (key: string, value: string) => void kept.set(key, value),
};
};

describe('record.related.arrangement — no `collapsed`, no compatibility read (objectstack#27524)', () => {
it('writes { order, hidden, basedOn, seen } and the version, and reads it back', () => {
const storage = memoryStorage();
const value = { order: ['task.lead_id', 'note.parent_id'], hidden: ['call.lead_id'], basedOn: 'abcd1234', seen: { 'task.lead_id': 1 } };
writeWorkspacePreference(RECORD_RELATED_ARRANGEMENT_PREFERENCE, lead, value, storage);
const stored = JSON.parse(storage.kept.get(KEY)!);
expect(Object.keys(stored).sort()).toEqual(['basedOn', 'hidden', 'order', 'seen', 'v']);
expect(stored.v).toBe(2);
expect(readWorkspacePreference(RECORD_RELATED_ARRANGEMENT_PREFERENCE, lead, storage)).toEqual(value);
});

it('reads no choice from a record of the shape that carried `collapsed`', () => {
const earlier = JSON.stringify({ order: ['note.parent_id'], collapsed: ['task.lead_id'], hidden: [], basedOn: 'abcd1234', v: 1 });
const storage = { getItem: (key: string) => (key === KEY ? earlier : null) };
expect(readWorkspacePreference(RECORD_RELATED_ARRANGEMENT_PREFERENCE, lead, storage)).toBeUndefined();
});
});
19 changes: 11 additions & 8 deletions ui/packages/app-shell/src/utils/workspacePreference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { useCallback, useSyncExternalStore } from 'react';
import {
RELATED_ARRANGEMENT_RECORD_VERSION,
relatedArrangementFromRecord,
relatedArrangementToRecord,
type RelatedArrangement,
Expand Down Expand Up @@ -157,18 +158,20 @@ export const RECORD_RELATED_COLUMN_PREFERENCE = defineWorkspacePreference<Prefer
});

/**
* How the reader arranged the record page's related cards, per object
* (scope `object:OBJECT_NAME`, objectstack#27055): `{ v, order, collapsed,
* hidden, basedOn }` — plus `seen`, the first-seen stamps of relationships new
* since the last drag (the 48 h 「新」 badge needs a clock). The value is a
* DIFF against the default order; `resolveRelatedArrangement` in
* `@object-ui/plugin-detail` lays it over the default. With no choice the
* host passes `EMPTY_RELATED_ARRANGEMENT`: the default order, every card open.
* How the reader arranged the record page's related panels, per object
* (scope `object:OBJECT_NAME`, objectstack#27055, objectstack#27524):
* `{ v, order, hidden, basedOn }` — plus `seen`, the first-seen stamps of
* relationships new since the last drag (the 48 h 「新」 badge needs a clock).
* The value is a DIFF against the default order; `resolveRelatedArrangement`
* in `@object-ui/plugin-detail` lays it over the default. With no choice the
* host passes `EMPTY_RELATED_ARRANGEMENT`: the default order, every panel
* shown. A record of an earlier version (version 1 carried `collapsed`) is
* "no choice": no compatibility read.
*/
export const RECORD_RELATED_ARRANGEMENT_PREFERENCE = defineWorkspacePreference<RelatedArrangement>({
name: 'record.related.arrangement',
codec: versionedPreferenceCodec<RelatedArrangement>({
version: 1,
version: RELATED_ARRANGEMENT_RECORD_VERSION,
toRecord: relatedArrangementToRecord,
fromRecord: relatedArrangementFromRecord,
}),
Expand Down
26 changes: 16 additions & 10 deletions ui/packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1478,19 +1478,25 @@ const ar = {
owner: "المالك",
relatedCardMenu: "المزيد لـ {{title}}",
relatedCardEnlarge: "تكبير",
relatedCardMoveUp: "نقل لأعلى",
relatedCardMoveDown: "نقل لأسفل",
relatedCardExpand: "توسيع",
relatedCardCollapse: "طي",
relatedCardHide: "إخفاء",
relatedCardNew: "جديد",
relatedCardCount: "السجلات: {{count}} · تكبير",
relatedCardCount_zero: "السجلات: {{count}} · تكبير",
relatedCardCount_one: "السجلات: {{count}} · تكبير",
relatedCardCount_two: "السجلات: {{count}} · تكبير",
relatedCardCount_few: "السجلات: {{count}} · تكبير",
relatedCardCount_many: "السجلات: {{count}} · تكبير",
relatedCardCount_other: "السجلات: {{count}} · تكبير",
relatedPanelViewAll: "عرض الكل ({{count}})",
relatedPanelViewAll_zero: "عرض الكل ({{count}})",
relatedPanelViewAll_one: "عرض الكل ({{count}})",
relatedPanelViewAll_two: "عرض الكل ({{count}})",
relatedPanelViewAll_few: "عرض الكل ({{count}})",
relatedPanelViewAll_many: "عرض الكل ({{count}})",
relatedPanelViewAll_other: "عرض الكل ({{count}})",
relatedPanelReorder: "نقل {{title}}",
relatedPanelReorderHint: "اسحب لإعادة الترتيب · ⌥↑ ⌥↓",
relatedEmptyFold: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_zero: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_one: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_two: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_few: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_many: "قوائم فارغة أخرى: {{count}}",
relatedEmptyFold_other: "قوائم فارغة أخرى: {{count}}",
relatedDefaultChanged: "تم تحديث الترتيب الافتراضي",
relatedRestoreDefault: "استعادة الافتراضي",
relatedRestoreDefaultOrder: "استعادة الترتيب الافتراضي",
Expand Down
14 changes: 8 additions & 6 deletions ui/packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1320,15 +1320,17 @@ const de = {
owner: "Inhaber",
relatedCardMenu: "Mehr für {{title}}",
relatedCardEnlarge: "Vergrößern",
relatedCardMoveUp: "Nach oben",
relatedCardMoveDown: "Nach unten",
relatedCardExpand: "Aufklappen",
relatedCardCollapse: "Zuklappen",
relatedCardHide: "Ausblenden",
relatedCardNew: "Neu",
relatedCardCount: "{{count}} Datensätze – vergrößern",
relatedCardCount_one: "{{count}} Datensatz – vergrößern",
relatedCardCount_other: "{{count}} Datensätze – vergrößern",
relatedPanelViewAll: "Alle {{count}} anzeigen",
relatedPanelViewAll_one: "Alle {{count}} anzeigen",
relatedPanelViewAll_other: "Alle {{count}} anzeigen",
relatedPanelReorder: "{{title}} verschieben",
relatedPanelReorderHint: "Zum Sortieren ziehen · ⌥↑ ⌥↓",
relatedEmptyFold: "{{count}} weitere leere Listen",
relatedEmptyFold_one: "{{count}} weitere leere Liste",
relatedEmptyFold_other: "{{count}} weitere leere Listen",
relatedDefaultChanged: "Standardreihenfolge aktualisiert",
relatedRestoreDefault: "Standard wiederherstellen",
relatedRestoreDefaultOrder: "Standardreihenfolge wiederherstellen",
Expand Down
14 changes: 8 additions & 6 deletions ui/packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1508,15 +1508,17 @@ const en = {
owner: 'Owner',
relatedCardMenu: 'More for {{title}}',
relatedCardEnlarge: 'Enlarge',
relatedCardMoveUp: 'Move up',
relatedCardMoveDown: 'Move down',
relatedCardExpand: 'Expand',
relatedCardCollapse: 'Collapse',
relatedCardHide: 'Hide',
relatedCardNew: 'New',
relatedCardCount: '{{count}} records — enlarge',
relatedCardCount_one: '{{count}} record — enlarge',
relatedCardCount_other: '{{count}} records — enlarge',
relatedPanelViewAll: 'View all {{count}}',
relatedPanelViewAll_one: 'View all {{count}}',
relatedPanelViewAll_other: 'View all {{count}}',
relatedPanelReorder: 'Reorder {{title}}',
relatedPanelReorderHint: 'Drag to reorder · ⌥↑ ⌥↓',
relatedEmptyFold: '{{count}} more empty lists',
relatedEmptyFold_one: '{{count}} more empty list',
relatedEmptyFold_other: '{{count}} more empty lists',
relatedDefaultChanged: 'Default order updated',
relatedRestoreDefault: 'Restore default',
relatedRestoreDefaultOrder: 'Restore default order',
Expand Down
17 changes: 10 additions & 7 deletions ui/packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,16 +1361,19 @@ const es = {
owner: "Propietario",
relatedCardMenu: "Más opciones de {{title}}",
relatedCardEnlarge: "Ampliar",
relatedCardMoveUp: "Subir",
relatedCardMoveDown: "Bajar",
relatedCardExpand: "Desplegar",
relatedCardCollapse: "Contraer",
relatedCardHide: "Ocultar",
relatedCardNew: "Nuevo",
relatedCardCount: "{{count}} registros · ampliar",
relatedCardCount_one: "{{count}} registro · ampliar",
relatedCardCount_many: "{{count}} de registros · ampliar",
relatedCardCount_other: "{{count}} registros · ampliar",
relatedPanelViewAll: "Ver todos ({{count}})",
relatedPanelViewAll_one: "Ver todos ({{count}})",
relatedPanelViewAll_many: "Ver todos ({{count}})",
relatedPanelViewAll_other: "Ver todos ({{count}})",
relatedPanelReorder: "Mover {{title}}",
relatedPanelReorderHint: "Arrastra para reordenar · ⌥↑ ⌥↓",
relatedEmptyFold: "{{count}} listas vacías más",
relatedEmptyFold_one: "{{count}} lista vacía más",
relatedEmptyFold_many: "{{count}} listas vacías más",
relatedEmptyFold_other: "{{count}} listas vacías más",
relatedDefaultChanged: "Orden predeterminado actualizado",
relatedRestoreDefault: "Restaurar predeterminado",
relatedRestoreDefaultOrder: "Restaurar orden predeterminado",
Expand Down
17 changes: 10 additions & 7 deletions ui/packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,16 +1358,19 @@ const fr = {
owner: "Propriétaire",
relatedCardMenu: "Plus pour {{title}}",
relatedCardEnlarge: "Agrandir",
relatedCardMoveUp: "Monter",
relatedCardMoveDown: "Descendre",
relatedCardExpand: "Déplier",
relatedCardCollapse: "Replier",
relatedCardHide: "Masquer",
relatedCardNew: "Nouveau",
relatedCardCount: "{{count}} enregistrements · agrandir",
relatedCardCount_one: "{{count}} enregistrement · agrandir",
relatedCardCount_many: "{{count}} enregistrements · agrandir",
relatedCardCount_other: "{{count}} enregistrements · agrandir",
relatedPanelViewAll: "Tout voir ({{count}})",
relatedPanelViewAll_one: "Tout voir ({{count}})",
relatedPanelViewAll_many: "Tout voir ({{count}})",
relatedPanelViewAll_other: "Tout voir ({{count}})",
relatedPanelReorder: "Déplacer {{title}}",
relatedPanelReorderHint: "Glisser pour réordonner · ⌥↑ ⌥↓",
relatedEmptyFold: "{{count}} autres listes vides",
relatedEmptyFold_one: "{{count}} autre liste vide",
relatedEmptyFold_many: "{{count}} autres listes vides",
relatedEmptyFold_other: "{{count}} autres listes vides",
relatedDefaultChanged: "Ordre par défaut mis à jour",
relatedRestoreDefault: "Rétablir par défaut",
relatedRestoreDefaultOrder: "Rétablir l'ordre par défaut",
Expand Down
14 changes: 8 additions & 6 deletions ui/packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,15 +1163,17 @@ const ja = {
owner: "所有者",
relatedCardMenu: "{{title}} のその他の操作",
relatedCardEnlarge: "拡大",
relatedCardMoveUp: "上へ移動",
relatedCardMoveDown: "下へ移動",
relatedCardExpand: "展開",
relatedCardCollapse: "折りたたむ",
relatedCardHide: "非表示",
relatedCardNew: "新規",
relatedCardCount: "{{count}} 件 · 拡大",
relatedCardCount_one: "{{count}} 件 · 拡大",
relatedCardCount_other: "{{count}} 件 · 拡大",
relatedPanelViewAll: "{{count}} 件をすべて表示",
relatedPanelViewAll_one: "{{count}} 件をすべて表示",
relatedPanelViewAll_other: "{{count}} 件をすべて表示",
relatedPanelReorder: "{{title}} を並べ替え",
relatedPanelReorderHint: "ドラッグで並べ替え · ⌥↑ ⌥↓",
relatedEmptyFold: "空の関連 {{count}} 件",
relatedEmptyFold_one: "空の関連 {{count}} 件",
relatedEmptyFold_other: "空の関連 {{count}} 件",
relatedDefaultChanged: "既定の順序が更新されました",
relatedRestoreDefault: "既定に戻す",
relatedRestoreDefaultOrder: "既定の順序に戻す",
Expand Down
14 changes: 8 additions & 6 deletions ui/packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1320,15 +1320,17 @@ const ko = {
owner: "소유자",
relatedCardMenu: "{{title}} 추가 작업",
relatedCardEnlarge: "확대",
relatedCardMoveUp: "위로 이동",
relatedCardMoveDown: "아래로 이동",
relatedCardExpand: "펼치기",
relatedCardCollapse: "접기",
relatedCardHide: "숨기기",
relatedCardNew: "새 항목",
relatedCardCount: "{{count}}개 레코드 · 확대",
relatedCardCount_one: "{{count}}개 레코드 · 확대",
relatedCardCount_other: "{{count}}개 레코드 · 확대",
relatedPanelViewAll: "{{count}}개 모두 보기",
relatedPanelViewAll_one: "{{count}}개 모두 보기",
relatedPanelViewAll_other: "{{count}}개 모두 보기",
relatedPanelReorder: "{{title}} 순서 변경",
relatedPanelReorderHint: "드래그하여 순서 변경 · ⌥↑ ⌥↓",
relatedEmptyFold: "빈 관련 항목 {{count}}개",
relatedEmptyFold_one: "빈 관련 항목 {{count}}개",
relatedEmptyFold_other: "빈 관련 항목 {{count}}개",
relatedDefaultChanged: "기본 순서가 업데이트됨",
relatedRestoreDefault: "기본값 복원",
relatedRestoreDefaultOrder: "기본 순서 복원",
Expand Down
17 changes: 10 additions & 7 deletions ui/packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,16 +1358,19 @@ const pt = {
owner: "Proprietário",
relatedCardMenu: "Mais para {{title}}",
relatedCardEnlarge: "Ampliar",
relatedCardMoveUp: "Mover para cima",
relatedCardMoveDown: "Mover para baixo",
relatedCardExpand: "Expandir",
relatedCardCollapse: "Recolher",
relatedCardHide: "Ocultar",
relatedCardNew: "Novo",
relatedCardCount: "{{count}} registros · ampliar",
relatedCardCount_one: "{{count}} registro · ampliar",
relatedCardCount_many: "{{count}} de registros · ampliar",
relatedCardCount_other: "{{count}} registros · ampliar",
relatedPanelViewAll: "Ver todos ({{count}})",
relatedPanelViewAll_one: "Ver todos ({{count}})",
relatedPanelViewAll_many: "Ver todos ({{count}})",
relatedPanelViewAll_other: "Ver todos ({{count}})",
relatedPanelReorder: "Mover {{title}}",
relatedPanelReorderHint: "Arraste para reordenar · ⌥↑ ⌥↓",
relatedEmptyFold: "Mais {{count}} listas vazias",
relatedEmptyFold_one: "Mais {{count}} lista vazia",
relatedEmptyFold_many: "Mais {{count}} listas vazias",
relatedEmptyFold_other: "Mais {{count}} listas vazias",
relatedDefaultChanged: "Ordem padrão atualizada",
relatedRestoreDefault: "Restaurar padrão",
relatedRestoreDefaultOrder: "Restaurar ordem padrão",
Expand Down
Loading
Loading