Skip to content

Commit 9dd93d0

Browse files
feat(app-shell): Studio field panel turns on referenceNameVisible for lookup and user fields (#29669)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb9MCA6uTswj7rQV3aDUowZD)_ Fixes #29629 Clause-②: no Metadata-protocol: none Claim: 6590761783 ## 维护者速读 - 改了什么:Studio 对象设计器的字段面板(`ObjectFieldInspector`)在 lookup 与 user 字段上新增「不可读时显示名称」复选框(`referenceNameVisible`)。勾选保存 `true`,取消勾选则把该键从字段定义里删掉;user 字段原本没有类型专属区,现在只为这个开关打开,不显示任何 lookup 选择器配置。字段改成其他类型时该键随之删除,lookup 改成 user 则保留。 - 为什么改:#29171 裁决 B 的键已由服务端下发、控制台渲染(#29460、#29462),但在 Studio 里建对象的管理员看不到也设不了它,只能写代码或走通用元数据表单。 - 风险与回滚:只动字段面板的 lookup / user 两处与类型切换;其他字段类型渲染不变(text、master_detail 无开关有测试钉住)。回滚即 revert 本 PR。 - 先看:`ObjectFieldInspector.tsx` 的 `changeType`(`dropsNameVisible` 一行)、`nameVisibleToggle` 的 `onCommit`(勾选写 true / 取消走 `unsetDefKey`)、类型专属区的显示条件。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.referenceNameVisible-29629.test.tsx`,预期 11 个用例全部通过。 - 需要你做的:无,席位落地。 ## What changes The Studio object designer's field panel can now set `FieldSchema.referenceNameVisible` (director ruling 6574398310, letter B), which until now only the generic `field` metadata form and code could reach. | Field in the panel | Before | Now | |---|---|---| | `lookup` | picker config, then the related-list section; no control for the key | a "Show Name When Unreadable" checkbox after the picker config, its help text as the input's accessible description | | `user` | no type-specific section at all | a "User options" section holding that checkbox alone; no reference picker, display field, filters, dependent fields or related list (the target is always `sys_user`) | | `master_detail`, `tree`, `text` and every other type | unchanged | unchanged, no checkbox (the spec refuses the key there) | - **Checked** saves `referenceNameVisible: true` through the inspector's `patchDef`. **Cleared** removes the key through `unsetDefKey`, so the field carries no `referenceNameVisible` at all rather than `false` or an own `undefined` key. A stored `false` (which the spec accepts on these two types) reads unchecked. - **Retype.** `changeType` stripped only `picklist` and option kinds before. It now also removes `referenceNameVisible` when the next type is neither `lookup` nor `user`, since `FieldSchema` refuses the key there, `false` included. `lookup` to `user` keeps it. - **Wording.** The inspector localises through `@object-ui/i18n`'s studio namespace, so the label and help text are two new keys there, `designer.field.lookup.referenceNameVisible` and `designer.field.lookup.referenceNameVisible.help`. The `en` text is the `field.form.ts` row from #29462 word for word; the `zh` text is that branch's zh-CN translation, so both authoring surfaces describe the key in one wording. ## Tests `ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.referenceNameVisible-29629.test.tsx` (new, 11). Every write case ends at the saved field's own keys and at `FieldSchema`: - `lookup` and `user` offer the checkbox; checking it saves `referenceNameVisible: true` and `FieldSchema` accepts the field. - The help text is the checkbox's accessible description. - Clearing removes the key (`hasOwnProperty` false); a stored `false` reads unchecked and checking it writes `true`. - CONTROL: a `text` and a `master_detail` field offer no checkbox. - CONTROL: a `user` field shows none of the lookup controls (related object, picker config, display field, selectable records, depends on, related list). - A retype from `lookup` to Text removes the key (no `FieldSchema` issue at that path); a retype to User keeps it. - zh: the checkbox is labelled from the studio catalogue. Ablations through `scripts/ablation-replace.mjs`, each restored to the HEAD blob with `git diff HEAD` empty, each red: | Mutation | Result | |---|---| | A1 `offersReferenceNameVisible` returns `false` (the `user` path) | 3 failed | | A2 the retype's `delete nextDef.referenceNameVisible` removed | 1 failed | | A3 clearing writes `patchDef` with an `undefined` value instead of `unsetDefKey` | 1 failed | | A4 the lookup render guard set to `false` | 5 failed | ## Acceptance notes - Surface: exactly the claim's files. `ObjectFieldInspector.tsx` (the lookup section, the `user` section predicate, `changeType`), the new test beside it, `ui/packages/i18n/src/namespaces/studio/{en,zh}.ts` (two keys each), and `.changeset/29629-studio-reference-name-visible.md` (patches for `@object-ui/app-shell` and `@object-ui/i18n`). The studio namespace table is a plain string-to-string record outside the locale packs, so the new keys widen no published type; the claim's `Clause-②: no` holds. - The checkbox is drawn in this file rather than with `_shared.tsx`'s `InspectorCheckboxField`, which takes no description; that helper is outside the surface. - In display-patch mode (a base object's field) the checkbox is locked and wrapped like its neighbours (`DisplayPatchLock`); `referenceNameVisible` is not on the base-field display whitelist. - Studio at phone width was not measured: the designer's canvas row did not take a click at 390 px in the probe, which is the designer's own layout and not this control's. ## Verification Real backend (`objectstack dev --seed-admin --fresh` in `examples/app-crm`) and the source Console on Vite, as the admin, on head `6ae9d0e1` (this PR's code is unchanged by the later main merge). Screenshots and scripts are in the project folder `issue-29629/` (`shots/`, `setup.mjs`, `measure.cjs`, `shot.cjs`), not on GitHub. <details><summary>Toggle, publish, reload: the definition carries the key; clearing removes it</summary> - A scratch package `com.example.rnv` over REST: `rnv_payroll`, and `rnv_expense` with `payroll` (lookup to `rnv_payroll`) and `reviewer` (user), neither declaring the key. - Before (`ObjectFieldInspector.tsx` from `origin/main` on the same server): selecting `payroll` shows the picker config and the related list with no such control; selecting `reviewer` shows no type section at all (`before-desktop-payroll.png`, `before-desktop-reviewer.png`). - After, at `/apps/setup/metadata/object/rnv_expense`: the checkbox is on both fields. Checking it on both autosaved the draft (`PUT /api/v1/meta/object/rnv_expense?mode=draft` 200), Publish answered `POST …/rnv_expense/publish` 200, and `GET /api/v1/meta/object/rnv_expense` then served `payroll` as `{ name, type: lookup, label, reference: rnv_payroll, referenceNameVisible: true }` and `reviewer` as `{ name, type: user, label, referenceNameVisible: true }`. After a reload both checkboxes read checked (`after-desktop-payroll.png`, `after-desktop-reviewer.png`). - Clearing `reviewer`'s checkbox and publishing again served `reviewer` as `{ name, type: user, label }`, the key absent, while `payroll` kept `true`. </details> <details><summary>Local gates and suites</summary> - `dispatch-gates --tier`: no path-derived mandate (default tier). Changed lines 294 before the merge. - On the merged head `cb1d7f87` (main `b391feb1` merged, 9 commits, none touching these files): `turbo run type-check` for `@object-ui/app-shell` and `@object-ui/i18n` exit 0 (both executed, cache miss); 150 test files that import `ObjectFieldInspector` or the studio namespace, in three locked batches at 2 workers: 49/49, 47/47, 54/54 files (357, 767, 680 tests). - On the merged head `cb1d7f87`: the derived `dispatch-gates --commands` set, 35 commands, every one exit 0, including `check-sdui-manifest`, `check:type-check-debt` and `pnpm --dir ui exec vitest run scripts/` under the verify lock (170 files passed, 2 skipped); `--ran` reconciles 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Before the merge the same set (36 then, with `check:pm-changeset-deadline-census`) was also all exit 0. - Before the merge: `eslint` on the four touched source files exit 0 (warnings only, all on lines this PR does not touch); ui `check:phantom-deps`, `check:spec-symbols`, `check:i18n-keys`, `check:i18n-drift`, `check:designer-field-key-parity`, `check:metadata-write-doors` exit 0. - The repo-wide sweep (`pnpm lint` and the rest of the farm) is CI's. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jSxKwWNhQ9e5AS942Y)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3ff7fb7 commit 9dd93d0

5 files changed

Lines changed: 296 additions & 2 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/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(app-shell): the Studio field panel can turn on a lookup's or user field's `referenceNameVisible`
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The object designer's field panel (`ObjectFieldInspector`) now offers a "Show Name When Unreadable" checkbox on `lookup` and `user` fields, the two types `FieldSchema` accepts `referenceNameVisible` on. Checking it saves `referenceNameVisible: true`, so a reader of the record sees the referenced record's name as plain text when they cannot open that record; clearing it removes the key. On a `lookup` it sits after the picker config; a `user` field, whose target is always `sys_user`, shows the checkbox alone. A retype to any other type removes the key, since the spec refuses it there (`false` included); `lookup` to `user` keeps it. The label and help text match the generic `field` metadata form's row for the same key, in the studio namespace's `en` and `zh` tables. Other field types are unchanged.
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The field inspector authors `referenceNameVisible` (objectstack#29629), the
5+
* declared name exposure of objectstack#29171 (ruling 6574398310, letter B):
6+
* a reader of the record may see the referenced record's name, as text with
7+
* no link, when they cannot open that record.
8+
*
9+
* - The toggle is offered on `lookup` and `user` only, the two types
10+
* `FieldSchema` accepts the key on. Checking it writes `true`; clearing it
11+
* removes the key, so the field carries no `referenceNameVisible` at all.
12+
* - A `user` field's target is always `sys_user`: the section opens for the
13+
* toggle and offers none of the lookup picker controls.
14+
* - The spec refuses the key, `false` included, on every other type, so a
15+
* retype away from `lookup` / `user` removes it; `lookup` → `user` keeps it.
16+
*
17+
* Every write case ends at the written field's own keys and at `FieldSchema`,
18+
* never at "it rendered".
19+
*/
20+
21+
import * as React from 'react';
22+
import { describe, it, expect, vi, afterEach } from 'vitest';
23+
import { render, screen, cleanup, within } from '@testing-library/react';
24+
import userEvent from '@testing-library/user-event';
25+
import { FieldSchema } from '@objectstack/spec/data';
26+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
27+
28+
const h = vi.hoisted(() => {
29+
const CATALOG: Array<Record<string, unknown>> = [{ name: 'vendor', label: 'Vendor' }];
30+
const get = async (_type: string, name: string) =>
31+
name === 'vendor' ? { name, fields: { name: { type: 'text', label: 'Name' } } } : null;
32+
const client = {
33+
get,
34+
withPreviewDrafts: (_on: boolean) => ({ list: async (_type: string) => CATALOG, get }),
35+
list: async (_type: string) => CATALOG,
36+
listDrafts: async () => [] as unknown[],
37+
};
38+
return { client };
39+
});
40+
vi.mock('../useMetadata', () => ({ useMetadataClient: () => h.client }));
41+
vi.mock('../previews/useObjectFields', () => ({
42+
useObjectFields: () => ({ fields: [], loading: false, error: null }),
43+
}));
44+
45+
import { ObjectFieldInspector } from './ObjectFieldInspector';
46+
import { t, type SupportedLocale } from '../i18n';
47+
48+
// The zh case reads the lazy `studio` catalogue; load it at import so no
49+
// assertion races the loader.
50+
await loadStudioCatalogue('zh');
51+
52+
afterEach(cleanup);
53+
54+
type Def = Record<string, unknown>;
55+
56+
const LABEL = t('designer.field.lookup.referenceNameVisible', 'en-US');
57+
const HELP = t('designer.field.lookup.referenceNameVisible.help', 'en-US');
58+
59+
/** A host that feeds each patch back, as Studio's draft does. */
60+
function renderField(def: Def, opts: { locale?: SupportedLocale } = {}) {
61+
const onPatch = vi.fn();
62+
const initial: Record<string, unknown> = {
63+
name: 'po',
64+
label: 'Purchase order',
65+
fields: { title: { type: 'text', label: 'Title' }, party: def },
66+
};
67+
function Host() {
68+
const [draft, setDraft] = React.useState(initial);
69+
return (
70+
<ObjectFieldInspector
71+
type="object"
72+
name="po"
73+
draft={draft}
74+
selection={{ kind: 'field', id: 'party' }}
75+
onPatch={(patch: Record<string, unknown>) => {
76+
onPatch(patch);
77+
setDraft((d) => ({ ...d, ...patch }));
78+
}}
79+
onClearSelection={vi.fn()}
80+
onSelectionChange={vi.fn()}
81+
readOnly={false}
82+
locale={opts.locale ?? 'en-US'}
83+
/>
84+
);
85+
}
86+
render(<Host />);
87+
/** The `party` definition the host's Save would persist after the last edit. */
88+
const saved = () => (onPatch.mock.calls.at(-1)![0].fields as Record<string, Def>).party;
89+
return { onPatch, saved };
90+
}
91+
92+
const toggle = () => screen.queryByRole('checkbox', { name: LABEL });
93+
const has = (o: object, key: string) => Object.prototype.hasOwnProperty.call(o, key);
94+
95+
/** `FieldSchema`'s rejections of a saved definition, as `code@[path]`. */
96+
const rejections = (def: Def): string[] => {
97+
const r = FieldSchema.safeParse({ name: 'party', ...def });
98+
return r.success ? [] : r.error.issues.map((i) => `${i.code}@[${i.path.join('.')}]`);
99+
};
100+
101+
const LOOKUP: Def = { type: 'lookup', label: 'Vendor', reference: 'vendor' };
102+
const USER: Def = { type: 'user', label: 'Buyer' };
103+
104+
async function retypeTo(typeLabel: string) {
105+
await userEvent.click(screen.getByRole('combobox', { name: t('designer.field.type', 'en-US') }));
106+
const list = await screen.findByRole('listbox', { name: t('designer.field.type', 'en-US') });
107+
await userEvent.click(within(list).getByRole('option', { name: typeLabel }));
108+
}
109+
110+
describe('referenceNameVisible in the field inspector', () => {
111+
it.each([
112+
['lookup', LOOKUP],
113+
['user', USER],
114+
] as const)('a %s field offers the toggle, and checking it saves referenceNameVisible: true', async (_type, def) => {
115+
const { saved } = renderField(def);
116+
const box = toggle();
117+
expect(box).not.toBeNull();
118+
expect((box as HTMLInputElement).checked).toBe(false);
119+
120+
await userEvent.click(box!);
121+
122+
expect(saved().referenceNameVisible).toBe(true);
123+
expect(rejections(saved())).toEqual([]);
124+
expect((toggle() as HTMLInputElement).checked).toBe(true);
125+
});
126+
127+
it('the help text is the toggle\'s accessible description', () => {
128+
renderField(LOOKUP);
129+
expect(screen.getByRole('checkbox', { name: LABEL, description: HELP })).toBeTruthy();
130+
});
131+
132+
it('clearing the toggle removes the key: the saved field carries no referenceNameVisible', async () => {
133+
const { saved } = renderField({ ...LOOKUP, referenceNameVisible: true });
134+
expect((toggle() as HTMLInputElement).checked).toBe(true);
135+
136+
await userEvent.click(toggle()!);
137+
138+
expect(has(saved(), 'referenceNameVisible')).toBe(false);
139+
expect(rejections(saved())).toEqual([]);
140+
expect((toggle() as HTMLInputElement).checked).toBe(false);
141+
});
142+
143+
it('a stored false reads unchecked, and checking it writes true', async () => {
144+
const { saved } = renderField({ ...LOOKUP, referenceNameVisible: false });
145+
expect((toggle() as HTMLInputElement).checked).toBe(false);
146+
147+
await userEvent.click(toggle()!);
148+
149+
expect(saved().referenceNameVisible).toBe(true);
150+
});
151+
152+
it.each([
153+
['text', { type: 'text', label: 'Vendor name' }],
154+
['master_detail', { type: 'master_detail', label: 'Vendor', reference: 'vendor' }],
155+
] as const)('CONTROL — a %s field offers no toggle (the spec refuses the key there)', (_type, def) => {
156+
renderField(def);
157+
expect(toggle()).toBeNull();
158+
expect(screen.queryByTestId('field-reference-name-visible')).toBeNull();
159+
});
160+
161+
it('CONTROL — a user field offers none of the lookup picker controls (its target is always sys_user)', () => {
162+
renderField(USER);
163+
expect(toggle()).not.toBeNull();
164+
expect(screen.queryByText(t('designer.field.relatedObject', 'en-US'))).toBeNull();
165+
expect(screen.queryByText(t('designer.field.lookup.pickerConfig', 'en-US'))).toBeNull();
166+
expect(screen.queryByText(t('designer.field.lookup.displayField', 'en-US'))).toBeNull();
167+
expect(screen.queryByText(t('designer.field.lookup.selectableRecords', 'en-US'))).toBeNull();
168+
expect(screen.queryByText(t('designer.field.lookup.dependsOn', 'en-US'))).toBeNull();
169+
expect(screen.queryByTestId('field-related-list')).toBeNull();
170+
});
171+
172+
it('a retype away from lookup removes the key, so the save is not refused at [referenceNameVisible]', async () => {
173+
const { saved } = renderField({ ...LOOKUP, referenceNameVisible: true });
174+
175+
await retypeTo('Text');
176+
177+
expect(saved().type).toBe('text');
178+
expect(has(saved(), 'referenceNameVisible')).toBe(false);
179+
expect(rejections(saved()).filter((r) => r.endsWith('[referenceNameVisible]'))).toEqual([]);
180+
expect(toggle()).toBeNull();
181+
});
182+
183+
it('a retype from lookup to user keeps the key (both types accept it)', async () => {
184+
const { saved } = renderField({ ...LOOKUP, referenceNameVisible: true });
185+
186+
await retypeTo('User');
187+
188+
expect(saved().type).toBe('user');
189+
expect(saved().referenceNameVisible).toBe(true);
190+
expect((toggle() as HTMLInputElement).checked).toBe(true);
191+
});
192+
193+
it('zh: the toggle is labelled from the studio catalogue', () => {
194+
renderField(LOOKUP, { locale: 'zh-CN' });
195+
expect(screen.getByRole('checkbox', { name: '不可读时显示名称' })).toBeTruthy();
196+
});
197+
});

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx‎

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,17 @@ function isLookup(type: string): boolean {
487487
return type === 'lookup' || type === 'master_detail' || type === 'tree';
488488
}
489489

490+
/**
491+
* The two types `FieldSchema` accepts `referenceNameVisible` on (objectstack#29171,
492+
* ruling 6574398310 letter B): a `lookup`, and a `user` field (the `sys_user`
493+
* reference). Every other type is refused the key at parse, an authored
494+
* `false` included, `master_detail` and `tree` among them — which is why the
495+
* toggle is offered here and the key is dropped by a retype to anything else.
496+
*/
497+
function offersReferenceNameVisible(type: string): boolean {
498+
return type === 'lookup' || type === 'user';
499+
}
500+
490501
/**
491502
* objectui#11786 — what the object write guard holds this field for: its
492503
* options (a choice type with no option source) or its target (a relationship
@@ -1079,19 +1090,40 @@ export function ObjectFieldInspector({
10791090
Array.isArray(def.options)
10801091
&& !takesOptionKind(nextType, def.multiple)
10811092
&& withoutOptionKinds(def.options).some((o, i) => o !== (def.options as unknown[])[i]);
1082-
if (keepsPicklist && !dropsKinds) {
1093+
// The spec refuses `referenceNameVisible` off `lookup` / `user`, `false`
1094+
// included, so a retype to any other type removes the key outright.
1095+
const dropsNameVisible = 'referenceNameVisible' in def && !offersReferenceNameVisible(nextType);
1096+
if (keepsPicklist && !dropsKinds && !dropsNameVisible) {
10831097
patchDef(requireMaster ? { type: nextType, required: true } : { type: nextType });
10841098
return;
10851099
}
10861100
const nextDef: Record<string, unknown> = { ...def, type: nextType };
10871101
if (requireMaster) nextDef.required = true;
10881102
if (!keepsPicklist) delete nextDef.picklist;
10891103
if (dropsKinds) nextDef.options = withoutOptionKinds(def.options as unknown[]);
1104+
if (dropsNameVisible) delete nextDef.referenceNameVisible;
10901105
const nextEntries = [...view.entries];
10911106
nextEntries[idx] = { ...entry, def: nextDef };
10921107
writeView({ shape: view.shape, entries: nextEntries });
10931108
};
10941109

1110+
/**
1111+
* The declared name exposure, offered on `lookup` (beside its picker config)
1112+
* and on `user`. Checking writes `true`; clearing removes the key, so an
1113+
* unset field carries no `referenceNameVisible` at all.
1114+
*/
1115+
const nameVisibleToggle = (
1116+
<DisplayPatchLock>
1117+
<ReferenceNameVisibleField
1118+
value={def.referenceNameVisible === true}
1119+
divided={type === 'lookup'}
1120+
onCommit={(on) => (on ? patchDef({ referenceNameVisible: true }) : unsetDefKey('referenceNameVisible'))}
1121+
disabled={locked}
1122+
locale={locale}
1123+
/>
1124+
</DisplayPatchLock>
1125+
);
1126+
10951127
/** The names the object's other fields carry: what the API name box refuses. */
10961128
const takenNames = view.entries.filter((_, i) => i !== idx).map((e) => e.name);
10971129

@@ -1452,7 +1484,7 @@ export function ObjectFieldInspector({
14521484
</Section>
14531485

14541486
{/* Type-specific */}
1455-
{(isPicklist(type) || isLookup(type) || isComputed(type) || isNumeric(type) || isTexty(type) || offersValueDomain(type)) && (
1487+
{(isPicklist(type) || isLookup(type) || offersReferenceNameVisible(type) || isComputed(type) || isNumeric(type) || isTexty(type) || offersValueDomain(type)) && (
14561488
<Section title={tFormat('designer.field.section.options', locale, { type: typeMetaLabel ?? type })}>
14571489
{isPicklist(type) && (
14581490
<DisplayPatchLock>
@@ -1511,6 +1543,7 @@ export function ObjectFieldInspector({
15111543
locale={locale}
15121544
/>
15131545
</DisplayPatchLock>
1546+
{type === 'lookup' && nameVisibleToggle}
15141547
{(type === 'lookup' || type === 'master_detail') && (
15151548
<DisplayPatchLock>
15161549
<RelatedListFields
@@ -1525,6 +1558,8 @@ export function ObjectFieldInspector({
15251558
)}
15261559
</>
15271560
)}
1561+
{/* A `user` field's target is always `sys_user`: no picker config, only this key. */}
1562+
{type === 'user' && nameVisibleToggle}
15281563
{type === 'formula' && (
15291564
<DisplayPatchLock>
15301565
<CelPredicateField
@@ -2480,6 +2515,53 @@ function LookupConfigFields({
24802515
);
24812516
}
24822517

2518+
/* ─────────────── Declared name exposure (referenceNameVisible) ─────────────── */
2519+
2520+
/**
2521+
* The `referenceNameVisible` checkbox (objectstack#29171, ruling 6574398310
2522+
* letter B): a reader of this record may see the referenced record's name, as
2523+
* plain text with no link, when they cannot open that record. Its label and
2524+
* help text are the generic `field` form's row for the same key, so the two
2525+
* authoring surfaces describe it in one wording. The help text is the input's
2526+
* description, not a separate note.
2527+
*/
2528+
function ReferenceNameVisibleField({
2529+
value,
2530+
onCommit,
2531+
disabled,
2532+
divided,
2533+
locale,
2534+
}: {
2535+
value: boolean;
2536+
onCommit: (on: boolean) => void;
2537+
disabled?: boolean;
2538+
/** Ruled off from the controls above it (a lookup's picker config); a `user` field shows it alone. */
2539+
divided?: boolean;
2540+
locale?: string;
2541+
}) {
2542+
const id = React.useId();
2543+
const helpId = `${id}-help`;
2544+
return (
2545+
<div className={divided ? 'space-y-1 border-t pt-2.5' : 'space-y-1'} data-testid="field-reference-name-visible">
2546+
<label htmlFor={id} className="flex items-center gap-2 text-xs cursor-pointer select-none">
2547+
<input
2548+
id={id}
2549+
type="checkbox"
2550+
checked={value}
2551+
disabled={disabled}
2552+
aria-describedby={helpId}
2553+
onChange={(e) => onCommit(e.target.checked)}
2554+
className="h-3.5 w-3.5"
2555+
/>
2556+
<span>{t('designer.field.lookup.referenceNameVisible', locale)}</span>
2557+
</label>
2558+
<p id={helpId} className="text-[10px] leading-snug text-muted-foreground">
2559+
{t('designer.field.lookup.referenceNameVisible.help', locale)}
2560+
</p>
2561+
</div>
2562+
);
2563+
}
2564+
24832565
/* ─────────────── Related list on the parent (relatedList*) ─────────────── */
24842566

24852567
/** The three states of `relatedList`, as the control names them. */

‎ui/packages/i18n/src/namespaces/studio/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ const STUDIO_EN: Record<string, string> = {
416416
'designer.field.lookup.searchHostFields': "Search this object's fields…",
417417
'designer.field.lookup.pageSize': 'Picker page size',
418418
'designer.field.lookup.allowCreate': 'Allow quick-create',
419+
'designer.field.lookup.referenceNameVisible': 'Show Name When Unreadable',
420+
'designer.field.lookup.referenceNameVisible.help': "Lets a reader of this record see the referenced record's name, as plain text with no link, even when they may not open that record. Nothing else of it is shown, and a field-level rule on the name still holds (the name then reads Record # and the id). Unset: such a reference shows as a restricted record.",
419421
'designer.field.relatedList.heading': 'Related list on the parent record',
420422
'designer.field.relatedList.shown': 'Show',
421423
'designer.field.relatedList.hidden': 'Don\'t show',

‎ui/packages/i18n/src/namespaces/studio/zh.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ const STUDIO_ZH: Record<string, string> = {
344344
'designer.field.lookup.searchHostFields': '搜索本对象的字段…',
345345
'designer.field.lookup.pageSize': '选择器分页大小',
346346
'designer.field.lookup.allowCreate': '允许快速创建',
347+
'designer.field.lookup.referenceNameVisible': '不可读时显示名称',
348+
'designer.field.lookup.referenceNameVisible.help': '允许此记录的读者看到被引用记录的名称(纯文本,不带链接),即使其无权打开该记录。该记录的其他内容一律不显示;名称字段上的字段级规则仍然生效(此时名称显示为 Record # 加 id)。未设置:此类引用显示为「受限记录」。',
347349
'designer.field.relatedList.heading': '父记录上的关联列表',
348350
'designer.field.relatedList.shown': '显示',
349351
'designer.field.relatedList.hidden': '不显示',

0 commit comments

Comments
 (0)