Skip to content

Commit e15a87c

Browse files
fix(fields): permission-ref / position-ref cells draw the registry label, the name on title (#27970)
domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_ Fixes #27861 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:用户详情页的「权限集」「岗位」两个页签,列表单元格从显示 API 名(如 `organization_admin_no_bypass`)改为显示目录里的标签(如 "Organization Admin"),鼠标悬停显示 API 名;目录里查不到的名字照原样显示。 - 为什么改:管理员看不出一个用户到底持有哪些权限集,只能逐个点开;编辑表单早已显示标签,只读单元格没有。 - 风险与回滚:只改只读显示,不改存储与写入;目录读取失败或未返回时退回显示 API 名。回滚即还原本 PR。 - 先看:`ui/packages/fields/src/widgets/RegistryRefCell.tsx`(新的只读单元格)、`ui/packages/fields/src/widgets/registryCatalog.ts`(共享目录读取,每个数据源每种类型只请求一次)、`ui/packages/plugin-detail/src/RelatedList.tsx`(把字段的 `widget` 传给单元格类型解析的一行)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-detail/src/__tests__/RelatedList.registryRefColumn.test.tsx`,预期 3 个用例全绿;或在 showcase 里打开 Setup → Users → 任一用户 → Permission Sets,应看到权限集标签,悬停看到 API 名。 - 需要你做的:无,席位落地。 ## What changes `sys_user_permission_set.permission_set` and `sys_user_position.position` are by-name text columns declaring `widget: 'permission-ref'` / `widget: 'position-ref'`. The edit face (`RegistryRefField` in `@object-ui/fields`, not the form renderer the card names) already listed the registry with labels; no read cell did, so a user's Permission Sets and Positions tabs printed the api name. - **A read face in `@object-ui/fields`** (`widgets/RegistryRefCell.tsx`), registered under `permission-ref` / `position-ref`, which `resolveCellRendererType` now promotes a textual field declaring either widget to (`WIDGET_CELL_RENDERER_TYPES`, beside `object-ref` / `recipient-picker`). It draws the registry label with the stored name on `title`. - **One read for the whole list** (`widgets/registryCatalog.ts`): the catalog the edit picker lists, `DataSource.listMetadataItems(type)` (`GET /api/v1/meta/:type`), read once per data source and registry type and shared by every cell; a cell mounting after it settled draws the label on its first render. A failed read is not kept, so the next cell asks again. No new endpoint. `RegistryRefField` now imports its `registryLister` narrowing from that module instead of keeping its own copy; its behaviour is unchanged. - **The fallback**: a name the catalog does not hold, or holds without a label, draws as the name (same face as the text cell); so does every cell while the catalog loads, when it fails, or when no data source can list a registry. - **The related list reaches it**: `RelatedList`'s `makeCell` resolved the cell key from `type` and `format` only, so the declared `widget` never reached the cell resolution there (the grid already passed it). It now passes `widget` too. Side effect named: a related-list column declaring `object-ref` also reaches its console face now; `recipient-picker` still draws its stored text there, because the related list passes no `dependentValues`. ## Tests - `ui/packages/fields/src/widgets/__tests__/RegistryRefCell.readFace.test.tsx` — the widget picks the key; `permission-ref` and `position-ref` each draw the label with the name on `title` from ONE `listMetadataItems` call shared by several cells; a late mount draws the label at once with no second read; **the fallback** (an unknown name and an unlabelled one render the name); no data source, loading and a failed catalog draw the name, and a failed catalog is re-read; an empty value draws the shared No-value glyph and reads nothing. - `ui/packages/plugin-detail/src/__tests__/RelatedList.registryRefColumn.test.tsx` — the real `RelatedList` and data-table, one test per acceptance box: Permission Sets (label, api name on `title`, one catalog read for the column), Positions, and an unknown name rendering as the name. - Census pins updated for the two new registered keys: `cellRenderers.objectLiteral-8596.test.tsx` (fields) and `summaryChip.badgeFitCensus-8464.test.tsx` (plugin-detail, fit side 38 → 40). ## Acceptance notes - The card's mechanism names `ui/packages/components/src/renderers/form/form.tsx` as the edit face; the picker is `ui/packages/fields/src/widgets/RegistryRefField.tsx` (form.tsx only lists the widget names it hands a data source). The catalog reused is that picker's. - The `object-ref` / `recipient-picker` faces live in `@object-ui/app-shell`, kept out of `@object-ui/fields` for the console's eager `ui-components` chunk; this card's ruling puts the registry-name face in `@object-ui/fields`. Its reader is the picker's own catalog call plus a small cache. CI's `check:eager-closure` step weighs it; this branch did not build the console locally. - Labels are whatever `GET /meta/:type` serves; the server already translates the security catalog's labels for that route, so the cell does no client translation. - The edit picker (`RegistryRefField`) still reads its catalog fresh per mount, uncached: a set authored in Studio is listed on the next form, as before. ## Verification All readings on head `8307533a` (after merging `origin/main`), through the shared verification lock. <details><summary>Targeted tests</summary> - `pnpm --dir ui exec vitest run packages/fields/src/widgets/__tests__/RegistryRef packages/fields/src/__tests__/cellRenderers.objectLiteral-8596.test.tsx packages/fields/src/__tests__/widgetRefCells-12613.test.tsx packages/fields/src/__tests__/listCellRendererTypes-8734.test.tsx packages/fields/src/__tests__/fieldFaceSeam-13216.test.tsx packages/plugin-detail/src/__tests__/RelatedList.registryRefColumn.test.tsx packages/plugin-detail/src/__tests__/summaryChip.badgeFitCensus-8464.test.tsx packages/app-shell/src/views/widget-ref-cells/` — Test Files 10 passed (10), Tests 89 passed (89). - Before the merge: `packages/fields/` + `packages/plugin-detail/` whole — 624 files passed, 4 tests red in `cellRenderers.objectLiteral-8596` (the census naming the two new keys, then fixed: 34/34); the cross-package pins (`plugin-grid` tests, `plugin-list` tests, app-shell widget-ref and pointer-card faces, plugin-tree/kanban/form, cli registered-types ratchet, components form wiring, two console services) — 396 files, 3659 tests passed. - `pnpm --filter @object-ui/fields --filter @object-ui/plugin-detail run type-check` — both Done (each runs `tsc --noEmit && tsc -p tsconfig.test.json`, the second covering the test files). </details> <details><summary>Ablations (one-off, restored, blob-verified)</summary> - `RelatedList.tsx`: dropping `widget: def.widget` from the cell resolution — `RelatedList.registryRefColumn.test.tsx` 3 failed (3); restored blob `a15b2c84` = HEAD. - `RegistryRefCell.tsx`: drawing `{name}` in place of `{label ?? name}` — `RegistryRefCell.readFace.test.tsx` 5 failed / 4 passed (the four that pass are the name-fallback, no-source, loading and empty cases, which the mutation does not change); restored blob `7ff0f587` = HEAD. </details> <details><summary>Gates</summary> - `dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED, every exit 0 — including `node scripts/check-sdui-manifest.mjs`, `pnpm check:nul-bytes`, `pnpm check:cross-package-test-inputs`, `pnpm check:test-source-alias`, `pnpm --dir ui exec vitest run scripts/` (169 files passed). - Also run: `pnpm check:changeset-fixed` exit 0; `pnpm check:new-line-citations` (ui) — 0 new citations. - Not run locally: the console build and its `check:eager-closure` budget, which CI's Build & E2E UI job runs. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbDAucNyQ4UtzCJKyh)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3986f9f commit e15a87c

10 files changed

Lines changed: 487 additions & 30 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@object-ui/fields': patch
3+
'@object-ui/plugin-detail': patch
4+
---
5+
6+
fix(fields): a user's Permission Sets and Positions tabs show each set's and position's label, not its api name
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
A text field declaring `widget: 'permission-ref'` or `widget: 'position-ref'` (`sys_user_permission_set.permission_set`, `sys_user_position.position`) now has a read face of its own in list and detail cells: it draws the registry label of the stored name (`Organization Admin` for `organization_admin_no_bypass`), with the name on the cell's `title`. The labels come from the catalog the edit picker already lists (`DataSource.listMetadataItems`, `GET /api/v1/meta/:type`), read once per data source and registry type for every cell. A name the catalog does not hold, or holds without a label, still draws as the name, as does every cell while the catalog loads, when it fails, or when no data source can list a registry. A record page's related list now hands a column's declared `widget` to the cell resolution, as the grid already did, so these columns reach the face there.

‎ui/packages/fields/src/__tests__/cellRenderers.objectLiteral-8596.test.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,11 @@ const OBJECT_LITERAL_CENSUS: ReadonlyArray<readonly [type: string, text: string,
297297
// the text cell's face for it, exactly as `text` does.
298298
['object-ref', '[Object]', false],
299299
['recipient-picker', '[Object]', false],
300+
// ── objectstack#27861: the registry-name read faces ─────────────────────
301+
// `{}` names no item, so nothing is asked and both draw the text cell's
302+
// face for it, exactly as `text` does.
303+
['position-ref', '[Object]', false],
304+
['permission-ref', '[Object]', false],
300305
];
301306

302307
/** A real value per type — the POPULATED half, which refuses the caricature. */
@@ -357,6 +362,8 @@ const POPULATED_CENSUS: ReadonlyArray<readonly [type: string, value: unknown]> =
357362
['record_pointer_card', { objectName: 'invoice', recordId: 'inv_1' }],
358363
['object-ref', 'crm_account'],
359364
['recipient-picker', 'sales_manager'],
365+
['position-ref', 'sales_manager'],
366+
['permission-ref', 'admin_full_access'],
360367
];
361368

362369
describe('objectui#8596 — an object literal is not a cell value, and these renderers invented one', () => {

‎ui/packages/fields/src/index.tsx‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3497,16 +3497,21 @@ const TEXTUAL_BASE_TYPES = new Set(['text', 'textarea', 'string', 'longtext', ''
34973497
/**
34983498
* Field `widget` hints whose READ face is a cell of its own (objectui#12613),
34993499
* registered under the widget's own key below. The spec's `widget` names the
3500-
* component a field is drawn with in place of its `type` default; for these two
3501-
* the stored text is a machine reference — an object name, a recipient id —
3502-
* and a host face registered under the key names what it references, as the
3503-
* edit widget's readonly branch does, instead of printing the api name.
3500+
* component a field is drawn with in place of its `type` default; for these
3501+
* the stored text is a machine reference — an object name, a recipient id, a
3502+
* position or permission-set name — and the face registered under the key
3503+
* names what it references instead of printing the api name.
35043504
*
35053505
* Only a widget listed here promotes, and only over a textual base type, the
35063506
* same bound the `format` hints keep: a widget with no read face of its own
3507-
* (`filter-condition`, `position-ref`, …) still draws as its `type`.
3507+
* (`filter-condition`, …) still draws as its `type`.
35083508
*/
3509-
const WIDGET_CELL_RENDERER_TYPES: ReadonlySet<string> = new Set(['object-ref', 'recipient-picker']);
3509+
const WIDGET_CELL_RENDERER_TYPES: ReadonlySet<string> = new Set([
3510+
'object-ref',
3511+
'recipient-picker',
3512+
'position-ref',
3513+
'permission-ref',
3514+
]);
35103515

35113516
/**
35123517
* Resolve the canonical cell-renderer key for a field. Accepts either a
@@ -4223,6 +4228,7 @@ export function getCellRenderer(fieldType: string): React.FC<CellRendererProps>
42234228
return standardMap[fieldType] || TextCellRenderer;
42244229
}
42254230

4231+
import { PositionRefCell, PermissionRefCell } from './widgets/RegistryRefCell.js';
42264232
// Register standard renderers immediately
42274233
registerFieldRenderer('lookup', LookupCellRenderer);
42284234
registerFieldRenderer('master_detail', LookupCellRenderer);
@@ -4243,6 +4249,11 @@ registerFieldRenderer(RECORD_POINTER_CARD_TYPE, RecordPointerTextCellRenderer);
42434249
// the host loads its faces lazily, off the eager closure.
42444250
registerFieldRenderer('object-ref', TextCellRenderer);
42454251
registerFieldRenderer('recipient-picker', TextCellRenderer);
4252+
// The registry-name keys draw their label in this package: the reader is the
4253+
// edit widget's own `listMetadataItems` catalog, one shared read per data
4254+
// source and type, which `check:eager-closure` weighs with the rest of it.
4255+
registerFieldRenderer('position-ref', PositionRefCell);
4256+
registerFieldRenderer('permission-ref', PermissionRefCell);
42464257
// `owner` was registered here to the same UserCellRenderer until objectui#4814
42474258
// retired the spelling — see the TOMBSTONE below. `getCellRenderer('owner')`
42484259
// now reports the prescription and falls to the text cell.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* The READ face of the `position-ref` / `permission-ref` keys: a list or detail
11+
* cell of a textual field declaring either widget (`resolveCellRendererType`)
12+
* draws the registry LABEL of the stored name, with the name itself on
13+
* `title`, so the api name stays one hover away. A user's Permission Sets tab
14+
* reads "Organization Admin" where it read `organization_admin_no_bypass`.
15+
*
16+
* The label comes from the catalog the edit widget lists
17+
* (`./registryCatalog.ts`), read once per data source and registry type for
18+
* every cell that draws one. Until it answers, when it fails, when there is no
19+
* data source that can list a registry, and for a name the catalog does not
20+
* hold or holds without a label, the cell draws the stored name, exactly as the
21+
* text cell does.
22+
*/
23+
24+
import React from 'react';
25+
import { EmptyValue } from '@object-ui/components';
26+
import { isEmptyValue } from '@object-ui/core';
27+
import { SchemaRendererContext } from '@object-ui/react';
28+
import { coerceToSafeValue } from '../coerceToSafeValue.js';
29+
import type { CellRendererProps } from '../index.js';
30+
import { loadRegistryLabels, registryLister, settledRegistryLabels } from './registryCatalog.js';
31+
32+
/** The label `type`'s catalog holds for `name`, or `undefined` (not read, not held). */
33+
function useRegistryLabel(type: string, name: string): string | undefined {
34+
const source = registryLister(React.useContext(SchemaRendererContext)?.dataSource);
35+
const settled = source ? settledRegistryLabels(source, type) : undefined;
36+
const [, setLoaded] = React.useState(0);
37+
38+
React.useEffect(() => {
39+
if (!source || !name || settled) return;
40+
let live = true;
41+
loadRegistryLabels(source, type).then(
42+
() => {
43+
if (live) setLoaded((n) => n + 1);
44+
},
45+
() => {},
46+
);
47+
return () => {
48+
live = false;
49+
};
50+
}, [source, type, name, settled]);
51+
52+
return name ? settled?.get(name) : undefined;
53+
}
54+
55+
function RegistryRefCell({ value, registryType }: CellRendererProps & { registryType: string }) {
56+
const safe = coerceToSafeValue(value);
57+
const name = isEmptyValue(safe) ? '' : String(safe);
58+
const label = useRegistryLabel(registryType, name);
59+
if (!name) return <EmptyValue />;
60+
return (
61+
<span className="block max-w-full truncate" title={name}>
62+
{label ?? name}
63+
</span>
64+
);
65+
}
66+
67+
/** `position-ref`: a stored position name, drawn as the position's label. */
68+
export function PositionRefCell(props: CellRendererProps) {
69+
return <RegistryRefCell {...props} registryType="position" />;
70+
}
71+
72+
/** `permission-ref`: a stored permission-set name, drawn as the set's label. */
73+
export function PermissionRefCell(props: CellRendererProps) {
74+
return <RegistryRefCell {...props} registryType="permission" />;
75+
}

‎ui/packages/fields/src/widgets/RegistryRefField.tsx‎

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { AlertTriangle } from 'lucide-react';
1111
import { Combobox, EmptyValue, Input, cn } from '@object-ui/components';
1212
import { SchemaRendererContext } from '@object-ui/react';
1313
import { AUDIENCE_ANCHOR_POSITIONS } from '@objectstack/spec';
14-
import type { DataSource } from '@object-ui/types';
1514
import type { FieldWidgetComponentProps } from './types.js';
15+
import { registryLister, type RegistryItemHeader } from './registryCatalog.js';
1616
import { toDomProps } from './toDomProps.js';
1717
import { useFieldTranslation } from './useFieldTranslation.js';
1818

@@ -65,11 +65,7 @@ import { useFieldTranslation } from './useFieldTranslation.js';
6565
* no id, no first-item default.
6666
*/
6767

68-
/** One listed item of a registry: the name stored, the label shown beside it. */
69-
export interface RegistryItemHeader {
70-
name: string;
71-
label?: string;
72-
}
68+
export type { RegistryItemHeader };
7369

7470
/** The catalog, in whichever of its four states the load is actually in. */
7571
export type RegistryCatalogState =
@@ -81,20 +77,6 @@ export type RegistryCatalogState =
8177
const IDLE: RegistryCatalogState = { status: 'idle' };
8278
const LOADING: RegistryCatalogState = { status: 'loading' };
8379

84-
type RegistryLister = Required<Pick<DataSource, 'listMetadataItems'>>;
85-
86-
/**
87-
* The data source's registry list, when it has one. The widget contract keeps
88-
* `dataSource` structural (`unknown`, see `FieldWidgetComponentProps`), so the
89-
* member is narrowed here, by the name the `DataSource` contract declares.
90-
*/
91-
function registryLister(dataSource: unknown): RegistryLister | null {
92-
const candidate = dataSource as Partial<RegistryLister> | null | undefined;
93-
return candidate && typeof candidate.listMetadataItems === 'function'
94-
? (candidate as RegistryLister)
95-
: null;
96-
}
97-
9880
function failureMessage(err: unknown): string {
9981
if (err instanceof Error) return err.message;
10082
return typeof err === 'string' ? err : '';
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#27861 — the READ face of `position-ref` / `permission-ref`.
11+
*
12+
* `sys_user_permission_set.permission_set` and `sys_user_position.position`
13+
* are by-name text columns declaring those widgets. A list or detail cell of
14+
* either drew the stored api name (`organization_admin_no_bypass`); it now
15+
* draws the registry label, with the name on `title`, from the catalog the
16+
* edit widget lists (`listMetadataItems`), read once for every cell.
17+
*
18+
* A name the catalog does not hold — or holds without a label — still draws as
19+
* the name, and so does every state with no answer yet: no data source, a
20+
* catalog still loading, a catalog that failed.
21+
*/
22+
import { describe, it, expect, vi, afterEach } from 'vitest';
23+
import React from 'react';
24+
import { render, screen, waitFor, cleanup } from '@testing-library/react';
25+
import '@testing-library/jest-dom';
26+
import { SchemaRendererProvider } from '@object-ui/react';
27+
import { getCellRenderer, resolveCellRendererType } from '../../index';
28+
import { PermissionRefCell, PositionRefCell } from '../RegistryRefCell';
29+
30+
const PERMISSION_SETS = [
31+
{ name: 'organization_admin_no_bypass', label: 'Organization Admin' },
32+
{ name: 'admin_full_access', label: 'Full Access' },
33+
// An item whose author wrote no label reads as its name alone.
34+
{ name: 'auditor' },
35+
];
36+
const POSITIONS = [
37+
{ name: 'sales_rep', label: 'Sales Representative' },
38+
{ name: 'sales_manager', label: 'Sales Manager' },
39+
];
40+
41+
function lister(answer: (type: string) => Promise<Array<{ name: string; label?: string }>>) {
42+
return { listMetadataItems: vi.fn(answer), find: vi.fn() } as any;
43+
}
44+
45+
const FIELD = { name: 'permission_set', type: 'text', label: 'Permission Set', widget: 'permission-ref' } as any;
46+
47+
function mountCells(Cell: typeof PermissionRefCell, values: unknown[], dataSource?: unknown) {
48+
const cells = values.map((value, i) => (
49+
<div key={i} data-testid={`cell-${i}`}>
50+
<Cell value={value} field={FIELD} />
51+
</div>
52+
));
53+
return render(dataSource ? <SchemaRendererProvider dataSource={dataSource as any}>{cells}</SchemaRendererProvider> : <>{cells}</>);
54+
}
55+
56+
function cellText(i: number): HTMLElement {
57+
return screen.getByTestId(`cell-${i}`).firstElementChild as HTMLElement;
58+
}
59+
60+
afterEach(() => cleanup());
61+
62+
describe('objectstack#27861 — the declared widget picks the registry read face', () => {
63+
it('promotes a textual field declaring `permission-ref` / `position-ref` to its own key and face', () => {
64+
expect(resolveCellRendererType({ type: 'text', widget: 'permission-ref' })).toBe('permission-ref');
65+
expect(resolveCellRendererType({ type: 'text', widget: 'position-ref' })).toBe('position-ref');
66+
expect(getCellRenderer('permission-ref')).toBe(PermissionRefCell);
67+
expect(getCellRenderer('position-ref')).toBe(PositionRefCell);
68+
// Control: a non-textual base type keeps its own key.
69+
expect(resolveCellRendererType({ type: 'select', widget: 'permission-ref' })).toBe('select');
70+
});
71+
});
72+
73+
describe('objectstack#27861 — `permission-ref` cells draw the set label', () => {
74+
it("draws each set's label with its api name on title, from ONE catalog read shared by every cell", async () => {
75+
const ds = lister(async () => PERMISSION_SETS);
76+
mountCells(PermissionRefCell, ['organization_admin_no_bypass', 'admin_full_access', 'organization_admin_no_bypass'], ds);
77+
78+
await waitFor(() => expect(cellText(0)).toHaveTextContent('Organization Admin'));
79+
expect(cellText(0)).toHaveAttribute('title', 'organization_admin_no_bypass');
80+
expect(cellText(1)).toHaveTextContent('Full Access');
81+
expect(cellText(1)).toHaveAttribute('title', 'admin_full_access');
82+
expect(cellText(2)).toHaveTextContent('Organization Admin');
83+
84+
expect(ds.listMetadataItems).toHaveBeenCalledTimes(1);
85+
expect(ds.listMetadataItems).toHaveBeenCalledWith('permission');
86+
});
87+
88+
it('a cell mounting after the catalog settled draws the label on its first render, with no second read', async () => {
89+
const ds = lister(async () => PERMISSION_SETS);
90+
mountCells(PermissionRefCell, ['admin_full_access'], ds);
91+
await waitFor(() => expect(cellText(0)).toHaveTextContent('Full Access'));
92+
cleanup();
93+
94+
mountCells(PermissionRefCell, ['organization_admin_no_bypass'], ds);
95+
expect(cellText(0)).toHaveTextContent('Organization Admin');
96+
expect(ds.listMetadataItems).toHaveBeenCalledTimes(1);
97+
});
98+
});
99+
100+
describe('objectstack#27861 — `position-ref` cells draw the position label', () => {
101+
it("draws each position's label with its api name on title, reading the `position` registry", async () => {
102+
const ds = lister(async () => POSITIONS);
103+
mountCells(PositionRefCell, ['sales_manager', 'sales_rep'], ds);
104+
105+
await waitFor(() => expect(cellText(0)).toHaveTextContent('Sales Manager'));
106+
expect(cellText(0)).toHaveAttribute('title', 'sales_manager');
107+
expect(cellText(1)).toHaveTextContent('Sales Representative');
108+
expect(cellText(1)).toHaveAttribute('title', 'sales_rep');
109+
expect(ds.listMetadataItems).toHaveBeenCalledTimes(1);
110+
expect(ds.listMetadataItems).toHaveBeenCalledWith('position');
111+
});
112+
});
113+
114+
describe('objectstack#27861 — THE FALLBACK: a name the catalog cannot label draws as the name', () => {
115+
it('a name the catalog does not list renders the name itself, beside a listed one that resolves', async () => {
116+
const ds = lister(async () => PERMISSION_SETS);
117+
mountCells(PermissionRefCell, ['retired_set', 'admin_full_access', 'auditor'], ds);
118+
119+
// The listed sibling resolving proves the catalog answered before the
120+
// unknown name is judged.
121+
await waitFor(() => expect(cellText(1)).toHaveTextContent('Full Access'));
122+
expect(cellText(0)).toHaveTextContent(/^retired_set$/);
123+
expect(cellText(0)).toHaveAttribute('title', 'retired_set');
124+
// Listed without a label: the name alone, too.
125+
expect(cellText(2)).toHaveTextContent(/^auditor$/);
126+
});
127+
128+
it('with no data source that can list a registry, the cell draws the name', () => {
129+
mountCells(PermissionRefCell, ['admin_full_access']);
130+
expect(cellText(0)).toHaveTextContent(/^admin_full_access$/);
131+
expect(cellText(0)).toHaveAttribute('title', 'admin_full_access');
132+
});
133+
134+
it('while the catalog loads the cell draws the name', () => {
135+
const ds = lister(() => new Promise(() => {}));
136+
mountCells(PermissionRefCell, ['admin_full_access'], ds);
137+
expect(cellText(0)).toHaveTextContent(/^admin_full_access$/);
138+
});
139+
140+
it('a failed catalog draws the name, and is not kept: the next cell asks again', async () => {
141+
let fail = true;
142+
const ds = lister(async () => {
143+
if (fail) throw new Error('403');
144+
return PERMISSION_SETS;
145+
});
146+
mountCells(PermissionRefCell, ['admin_full_access'], ds);
147+
await waitFor(() => expect(ds.listMetadataItems).toHaveBeenCalledTimes(1));
148+
await Promise.resolve();
149+
expect(cellText(0)).toHaveTextContent(/^admin_full_access$/);
150+
cleanup();
151+
152+
fail = false;
153+
mountCells(PermissionRefCell, ['admin_full_access'], ds);
154+
await waitFor(() => expect(cellText(0)).toHaveTextContent('Full Access'));
155+
expect(ds.listMetadataItems).toHaveBeenCalledTimes(2);
156+
});
157+
158+
it('an empty value draws the shared No-value glyph and reads nothing', () => {
159+
const ds = lister(async () => PERMISSION_SETS);
160+
mountCells(PermissionRefCell, [null, ''], ds);
161+
expect(cellText(0)).toHaveAttribute('data-slot', 'empty-value');
162+
expect(cellText(1)).toHaveAttribute('data-slot', 'empty-value');
163+
expect(ds.listMetadataItems).not.toHaveBeenCalled();
164+
});
165+
});

0 commit comments

Comments
 (0)