Skip to content

Commit 118bee4

Browse files
feat(app-shell): Delete Behavior row in the Studio field panel on lookup, user and master_detail fields (#29853)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbYG8enByZpuMcJGBafucYrK)_ Fixes #29807 Clause-②: no Metadata-protocol: none Claim: 6596731216 (amended 6596763338) ## 维护者速读 - 改了什么:Studio 对象设计器的字段面板(`ObjectFieldInspector`)在 lookup、user 与 master_detail 字段上新增「删除行为」下拉框(`deleteBehavior`)。lookup 与 user 可选 置空 / 级联 / 限制;master_detail 只有 级联 / 限制(规范拒绝在主从字段上写 `set_null`)。首项「— 无 —」把该键从字段定义里删掉。字段改成没有该行的类型时删除该键,改成 master_detail 时删除已写的 `set_null`。 - 为什么改:删除行为决定被引用记录删除时本记录怎么办,在 user 字段上「不声明」本身就是一种生效的结果。此前 Studio 里看不到也设不了,只能改原始元数据。 - 风险与回滚:只动字段面板的类型专属区与类型切换;text、tree 等类型不出现该行(有测试钉住)。回滚即 revert 本 PR。 - 先看:`ObjectFieldInspector.tsx` 的 `offeredDeleteBehaviors`(哪种类型给哪些值)、`changeType` 里的 `dropsDeleteBehavior`、`DeleteBehaviorField` 的 `onCommit`(选值写入 / 选「无」走 `unsetDefKey`)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.test.tsx`,预期 54 个用例全部通过(其中 9 个是本 PR 新增的删除行为用例)。 - 需要你做的:无,席位落地。 ## What changes The Studio object designer's field panel can now set and clear `FieldSchema.deleteBehavior`. Until now only the generic `field` / `object` metadata forms (PR #29712) offered it, and the object editor draws the designer and its inspectors, not those forms. | Field in the panel | Before | Now | |---|---|---| | `lookup` | picker config, name toggle, related list; no Delete Behavior | a Delete Behavior select: — None —, Set Null, Cascade, Restrict | | `user` | the name toggle only | the same select with the same four items | | `master_detail` | picker config, related list | the select with — None —, Cascade, Restrict (the spec refuses an authored `set_null` there) | | `text`, `tree` and every other type | unchanged | unchanged, no row | - **Values follow the generic form's rows** (`field.form.ts`): all three on `lookup` and `user`, `cascade` / `restrict` on `master_detail`. One helper, `offeredDeleteBehaviors(type)`, decides both the row and its values. - **Unset removes the key.** The select leads with the designer's existing "— None —" item, using the inspector's own absent-key convention: `InspectorSelectField`'s `''` option mapped to `unsetDefKey`, the shape the `valueDomain` row already uses. The field then carries no `deleteBehavior` at all, never `''` or `null`. On a `lookup` that means the spec default; on a `user` field it keeps the field out of the delete walk. - **Retype.** `changeType` now removes `deleteBehavior` when the next type draws no row, `tree` included, and removes an authored `set_null` on a retype to `master_detail`. `cascade` and `restrict` survive a retype to `master_detail`. - **Wording.** The label, help text and option labels are five new Studio namespace keys (`designer.field.deleteBehavior`, `.help`, `.set_null`, `.cascade`, `.restrict`) in `en` and `zh`, the two locales that namespace carries. The label and help text match the form row; the Cascade and Restrict labels are `field.form.ts`'s `master_detail` option labels. ## Tests `ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.test.tsx`, a new top-level describe in the shared harness (9 cases, over the real inspector with a host that feeds each patch back as Studio's draft does): - The row is drawn on `lookup` and `user` with None plus three values, and on `master_detail` with None plus two. - CONTROL: no row on a `text` or a `tree` field. - Picking Cascade on a `user` field writes `deleteBehavior: 'cascade'`. - Picking None on a `lookup` holding `restrict` leaves the key absent (`hasOwnProperty` false), with `reference` kept. - A retype from `lookup` to Text drops the key. - A retype from `lookup` to Master-Detail drops `set_null`, and keeps `cascade`. Ablations through `scripts/ablation-replace.mjs`, each restored to the HEAD blob with `git diff HEAD` empty, each red: | Mutation | Result | |---|---| | A1 the row's render guard prefixed with `false &&` | 5 failed (the three draw cases, set, unset) | | A2 the retype's `delete nextDef.deleteBehavior` removed | 2 failed (both retype cases) | ## Acceptance notes - Surface: exactly the amended claim's five files. `ObjectFieldInspector.tsx` (a type helper, `changeType`, the row and its component), its shared test harness, `ui/packages/i18n/src/namespaces/studio/{en,zh}.ts` (five keys each), and `.changeset/29807-field-panel-delete-behavior.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 and the claim's `Clause-②: no` holds. - PR #29805 (#29733) does not touch this file; its "— None —" item is private to `SchemaForm`. This row reuses the field panel's own absent-key path instead, so the two PRs share no code (claim amendment 6596763338). #29805 has since merged and is in this branch through the main merge. - A stored value the type does not offer (an authored `set_null` on a `master_detail`) is drawn flagged by `InspectorSelectField`, as every select in the panel does, so the author sees what the save refuses. - The help text sits under the select as plain text. `InspectorSelectField` takes no description, so it is not wired as the control's accessible description, unlike the name toggle's checkbox. - The dead-keys report lists `designer.field.deleteBehavior.` among the dynamic prefixes it keeps alive (3 keys, one call site), the same as `designer.field.summary.fn.`. ## Verification Real backend (`objectstack dev --seed-admin --fresh` in `examples/app-crm`) and the source Console on Vite, as the admin, on head `e646fdc1` (this PR's code is unchanged by the later main merge). Screenshots and scripts are in the project folder `issue-29807/` (`shots/`, `setup.mjs`, `measure.cjs`, `read.mjs`), not on GitHub. <details><summary>Before, set, publish, unset: the definition carries the key, then drops it</summary> - A scratch package `com.example.dbx` over REST: `dbx_account`, and `dbx_ticket` with `title` (text), `parent_ticket` (lookup), `assignee` (user) and `account` (master_detail), none declaring the key. - Before (`ObjectFieldInspector.tsx` from `origin/main` swapped in under the same Vite server, then restored to the HEAD blob): no Delete Behavior row on any of the four fields (`before-*.png`). - After, at `/apps/setup/metadata/object/dbx_ticket`: the row is on `parent_ticket` and `assignee` with — None —, Set Null (clear the reference), Cascade (delete children), Restrict (block the delete), and on `account` with — None —, Cascade, Restrict; `title` has none (`set-*.png`). Picking Cascade, Restrict and Restrict autosaved the draft (`PUT /api/v1/meta/object/dbx_ticket?mode=draft` 200, three times), Publish answered `POST …/dbx_ticket/publish` 200, and `GET /api/v1/meta/object/dbx_ticket` then served `parent_ticket.deleteBehavior` `cascade`, `assignee` `restrict`, `account` `restrict`, and no key on `title`. After a reload the selects read those values (`set-reloaded-*.png`). - Picking — None — on `parent_ticket` and `assignee`: the last draft PUT body carried neither key on those two fields (`account` kept `restrict`), Publish answered 200, and the served definition had no `deleteBehavior` on `parent_ticket` or `assignee` (`unset-*.png`). </details> <details><summary>Local gates and suites</summary> - `dispatch-gates --commands` derives no tier mandate (default tier). Changed lines 229 (+228 / -1), 5 files. - On the merged head `eab8960c` (main merged, which brought in PR #29805), after `pnpm install --frozen-lockfile` and a full `pnpm build` (117/117): `turbo run type-check` for `@object-ui/app-shell` and `@object-ui/i18n` 66/66; 128 test files (all 31 `ObjectFieldInspector*` files, the zh base-term census, `block-config-i18n`, the whole `@object-ui/i18n` package) at 2 workers under the verify lock, 128/128 files, 1800 passed, 13 skipped. - On `eab8960c`: the derived set, 35 commands, every one exit 0, including `check-sdui-manifest` (manifest intact, 106 components) and `pnpm --dir ui exec vitest run scripts/` under the lock (170 files passed, 2 skipped); `--ran` reconciles 35 derived, 35 run, 0 NOT-MEASURED. The same set was also all exit 0 on `e646fdc1` before the merge. - ui `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:designer-field-key-parity`, `check:phantom-deps`, `check:spec-symbols` exit 0 on `eab8960c`; `eslint` on the four touched source files exit 0 (warnings only, all on lines this PR does not touch). </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jS7iTtMx3iqedtUAyz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 589049b commit 118bee4

5 files changed

Lines changed: 228 additions & 1 deletion

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 object designer's field panel offers a Delete Behavior row on lookup, user and master_detail fields
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The field panel in the Studio object designer now draws a Delete Behavior select, so an author can set or clear `deleteBehavior` without editing raw metadata. It follows the generic `field` form's rows for the same key: a `lookup` or `user` field offers Set Null, Cascade and Restrict, and a `master_detail` field offers Cascade and Restrict only, because the spec refuses an authored `set_null` there. The leading "— None —" item removes the key, so the field declares nothing (the spec default on a `lookup`; no part in the delete walk on a `user` field). Changing a field to a type without the row removes the key, and changing it to `master_detail` removes an authored `set_null`. The labels are new Studio namespace keys (`designer.field.deleteBehavior.*`) in English and Chinese.

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import * as React from 'react';
44
import { describe, it, expect, vi, afterEach } from 'vitest';
55
import { render, screen, fireEvent, cleanup, waitFor, within } from '@testing-library/react';
6+
import userEvent from '@testing-library/user-event';
67
import { FieldSchema } from '@objectstack/spec/data';
78

89
// The inspector loads the object list (published + drafts) for the lookup
@@ -32,6 +33,7 @@ vi.mock('../previews/useObjectFields', () => ({
3233

3334
import { ObjectFieldInspector } from './ObjectFieldInspector';
3435
import { __setCelFormulaLoader } from '../celAuthoring';
36+
import { t } from '../i18n';
3537

3638
afterEach(() => {
3739
cleanup();
@@ -667,3 +669,126 @@ describe('ObjectFieldInspector — summary roll-up editor', () => {
667669
expect(screen.queryByText('Filter 1')).not.toBeInTheDocument();
668670
});
669671
});
672+
673+
describe('ObjectFieldInspector — delete behavior (objectstack#29807)', () => {
674+
// Radix Select reads pointer capture and scrollIntoView, which jsdom lacks.
675+
const proto = Element.prototype as unknown as Record<string, unknown>;
676+
for (const m of ['hasPointerCapture', 'setPointerCapture', 'releasePointerCapture']) {
677+
if (!proto[m]) proto[m] = m === 'hasPointerCapture' ? () => false : () => {};
678+
}
679+
if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {};
680+
681+
type Def = Record<string, unknown>;
682+
const LABEL = t('designer.field.deleteBehavior', 'en-US');
683+
const NONE = t('engine.form.none', 'en-US');
684+
const SET_NULL = t('designer.field.deleteBehavior.set_null', 'en-US');
685+
const CASCADE = t('designer.field.deleteBehavior.cascade', 'en-US');
686+
const RESTRICT = t('designer.field.deleteBehavior.restrict', 'en-US');
687+
const LOOKUP: Def = { type: 'lookup', label: 'Parent', reference: 'account' };
688+
const USER: Def = { type: 'user', label: 'Assignee' };
689+
const MASTER: Def = { type: 'master_detail', label: 'Master', reference: 'account' };
690+
const has = (o: object, key: string) => Object.prototype.hasOwnProperty.call(o, key);
691+
692+
/** A host that feeds each patch back into the draft, as Studio's editor does. */
693+
function renderStateful(def: Def) {
694+
const onPatch = vi.fn();
695+
function Host() {
696+
const [draft, setDraft] = React.useState<Record<string, unknown>>({
697+
name: 'ticket',
698+
fields: { title: { type: 'text', label: 'Title' }, ref: def },
699+
});
700+
return (
701+
<ObjectFieldInspector
702+
type="object"
703+
name="ticket"
704+
draft={draft}
705+
selection={{ kind: 'field', id: 'ref' }}
706+
onPatch={(patch: Record<string, unknown>) => {
707+
onPatch(patch);
708+
setDraft((d) => ({ ...d, ...patch }));
709+
}}
710+
onClearSelection={vi.fn()}
711+
onSelectionChange={vi.fn()}
712+
readOnly={false}
713+
locale={'en-US'}
714+
/>
715+
);
716+
}
717+
render(<Host />);
718+
/** The `ref` definition the editor's Save would persist after the last edit. */
719+
const saved = () => (onPatch.mock.calls.at(-1)![0].fields as Record<string, Def>).ref;
720+
return { onPatch, saved };
721+
}
722+
723+
const control = () => screen.queryByRole('combobox', { name: LABEL });
724+
725+
async function optionNames(): Promise<string[]> {
726+
await userEvent.click(control()!);
727+
const list = await screen.findByRole('listbox');
728+
return within(list).getAllByRole('option').map((o) => o.textContent ?? '');
729+
}
730+
731+
async function pick(name: string) {
732+
await userEvent.click(control()!);
733+
const list = await screen.findByRole('listbox');
734+
await userEvent.click(within(list).getByRole('option', { name }));
735+
}
736+
737+
async function retypeTo(typeLabel: string) {
738+
await userEvent.click(screen.getByRole('combobox', { name: t('designer.field.type', 'en-US') }));
739+
const list = await screen.findByRole('listbox', { name: t('designer.field.type', 'en-US') });
740+
await userEvent.click(within(list).getByRole('option', { name: typeLabel }));
741+
}
742+
743+
it.each([
744+
['lookup', LOOKUP, [NONE, SET_NULL, CASCADE, RESTRICT]],
745+
['user', USER, [NONE, SET_NULL, CASCADE, RESTRICT]],
746+
// The spec refuses an authored `set_null` on a master_detail.
747+
['master_detail', MASTER, [NONE, CASCADE, RESTRICT]],
748+
] as const)('draws the row on a %s field with the values the spec accepts there', async (_type, def, expected) => {
749+
renderStateful(def);
750+
expect(screen.getByTestId('field-delete-behavior')).toBeTruthy();
751+
expect(await optionNames()).toEqual(expected);
752+
});
753+
754+
it.each([
755+
['text', { type: 'text', label: 'Title' }],
756+
['tree', { type: 'tree', label: 'Parent' }],
757+
] as const)('CONTROL: draws no row on a %s field', (_type, def) => {
758+
renderStateful(def);
759+
expect(screen.queryByTestId('field-delete-behavior')).toBeNull();
760+
expect(control()).toBeNull();
761+
});
762+
763+
it('picking a value writes it to the draft', async () => {
764+
const { saved } = renderStateful(USER);
765+
await pick(CASCADE);
766+
expect(saved().deleteBehavior).toBe('cascade');
767+
});
768+
769+
it('picking None removes the key outright (absent, never empty or null)', async () => {
770+
const { saved } = renderStateful({ ...LOOKUP, deleteBehavior: 'restrict' });
771+
await pick(NONE);
772+
expect(has(saved(), 'deleteBehavior')).toBe(false);
773+
expect(saved().reference).toBe('account');
774+
});
775+
776+
it('a retype to text drops the key', async () => {
777+
const { saved } = renderStateful({ ...LOOKUP, deleteBehavior: 'cascade' });
778+
await retypeTo(t('engine.fieldType.text', 'en-US'));
779+
expect(saved().type).toBe('text');
780+
expect(has(saved(), 'deleteBehavior')).toBe(false);
781+
});
782+
783+
it('a retype to master_detail drops set_null and keeps cascade', async () => {
784+
const first = renderStateful({ ...LOOKUP, deleteBehavior: 'set_null' });
785+
await retypeTo(t('engine.fieldType.master_detail', 'en-US'));
786+
expect(first.saved().type).toBe('master_detail');
787+
expect(has(first.saved(), 'deleteBehavior')).toBe(false);
788+
cleanup();
789+
790+
const second = renderStateful({ ...LOOKUP, deleteBehavior: 'cascade' });
791+
await retypeTo(t('engine.fieldType.master_detail', 'en-US'));
792+
expect(second.saved().deleteBehavior).toBe('cascade');
793+
});
794+
});

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,23 @@ function offersReferenceNameVisible(type: string): boolean {
499499
return type === 'lookup' || type === 'user';
500500
}
501501

502+
/**
503+
* The `deleteBehavior` values the field panel offers on `type`: the rows the
504+
* generic `field` form draws for the key (`field.form.ts`), so the two
505+
* authoring surfaces agree. All three on a `lookup`, and on a `user` field,
506+
* where a declared value takes effect when the user is deleted (objectstack#29253)
507+
* and declaring nothing keeps the field out of the delete walk. Only
508+
* `cascade` / `restrict` on a `master_detail`: `FieldSchema` refuses an
509+
* authored `set_null` there, since a detail row cannot outlive its master.
510+
* Empty on every other type, `tree` included: no row is drawn, and a retype
511+
* to one drops the key.
512+
*/
513+
function offeredDeleteBehaviors(type: string): ReadonlyArray<string> {
514+
if (type === 'lookup' || type === 'user') return ['set_null', 'cascade', 'restrict'];
515+
if (type === 'master_detail') return ['cascade', 'restrict'];
516+
return [];
517+
}
518+
502519
/**
503520
* objectui#11786 — what the object write guard holds this field for: its
504521
* options (a choice type with no option source) or its target (a relationship
@@ -1098,7 +1115,15 @@ export function ObjectFieldInspector({
10981115
// The spec refuses `referenceNameVisible` off `lookup` / `user`, `false`
10991116
// included, so a retype to any other type removes the key outright.
11001117
const dropsNameVisible = 'referenceNameVisible' in def && !offersReferenceNameVisible(nextType);
1101-
if (keepsPicklist && !dropsKinds && !dropsNameVisible) {
1118+
// objectstack#29807 — the Delete Behavior row lives only where it is
1119+
// drawn, so a retype to a type without it removes the key, and a retype to
1120+
// `master_detail` removes an authored `set_null` the spec refuses there.
1121+
const nextDeleteBehaviors = offeredDeleteBehaviors(nextType);
1122+
const dropsDeleteBehavior =
1123+
'deleteBehavior' in def
1124+
&& (nextDeleteBehaviors.length === 0
1125+
|| (def.deleteBehavior === 'set_null' && !nextDeleteBehaviors.includes('set_null')));
1126+
if (keepsPicklist && !dropsKinds && !dropsNameVisible && !dropsDeleteBehavior) {
11021127
patchDef(requireMaster ? { type: nextType, required: true } : { type: nextType });
11031128
return;
11041129
}
@@ -1107,6 +1132,7 @@ export function ObjectFieldInspector({
11071132
if (!keepsPicklist) delete nextDef.picklist;
11081133
if (dropsKinds) nextDef.options = withoutOptionKinds(def.options as unknown[]);
11091134
if (dropsNameVisible) delete nextDef.referenceNameVisible;
1135+
if (dropsDeleteBehavior) delete nextDef.deleteBehavior;
11101136
const nextEntries = [...view.entries];
11111137
nextEntries[idx] = { ...entry, def: nextDef };
11121138
writeView({ shape: view.shape, entries: nextEntries });
@@ -1568,6 +1594,17 @@ export function ObjectFieldInspector({
15681594
)}
15691595
{/* A `user` field's target is always `sys_user`: no picker config, only this key. */}
15701596
{type === 'user' && nameVisibleToggle}
1597+
{offeredDeleteBehaviors(type).length > 0 && (
1598+
<DisplayPatchLock>
1599+
<DeleteBehaviorField
1600+
value={typeof def.deleteBehavior === 'string' ? def.deleteBehavior : ''}
1601+
offered={offeredDeleteBehaviors(type)}
1602+
onCommit={(v) => (v ? patchDef({ deleteBehavior: v }) : unsetDefKey('deleteBehavior'))}
1603+
disabled={locked}
1604+
locale={locale}
1605+
/>
1606+
</DisplayPatchLock>
1607+
)}
15711608
{type === 'formula' && (
15721609
<DisplayPatchLock>
15731610
<CelPredicateField
@@ -2590,6 +2627,50 @@ function ReferenceNameVisibleField({
25902627
);
25912628
}
25922629

2630+
/* ─────────────── Delete behavior (deleteBehavior) ─────────────── */
2631+
2632+
/**
2633+
* objectstack#29807 — what deleting the referenced record does to this one.
2634+
* The label, help text and the `master_detail` option labels are the generic
2635+
* `field` form's rows for the same key. The leading "— None —" item writes the
2636+
* key as ABSENT (`unsetDefKey`), never `''` or `null`: "declares nothing" is
2637+
* its own outcome (the spec default on a `lookup`, staying out of the delete
2638+
* walk on a `user` field), not a blank value. A stored value the type does not
2639+
* offer (`set_null` authored on a `master_detail`) is drawn flagged by
2640+
* {@link InspectorSelectField}, so the author sees what the save refuses.
2641+
*/
2642+
function DeleteBehaviorField({
2643+
value,
2644+
offered,
2645+
onCommit,
2646+
disabled,
2647+
locale,
2648+
}: {
2649+
value: string;
2650+
offered: ReadonlyArray<string>;
2651+
onCommit: (v: string) => void;
2652+
disabled?: boolean;
2653+
locale?: string;
2654+
}) {
2655+
return (
2656+
<div className="space-y-1 border-t pt-2.5" data-testid="field-delete-behavior">
2657+
<InspectorSelectField
2658+
label={t('designer.field.deleteBehavior', locale)}
2659+
value={value}
2660+
options={[
2661+
{ value: '', label: t('engine.form.none', locale) },
2662+
...offered.map((v) => ({ value: v, label: t(`designer.field.deleteBehavior.${v}`, locale) })),
2663+
]}
2664+
onCommit={onCommit}
2665+
disabled={disabled}
2666+
/>
2667+
<p className="text-[10px] leading-snug text-muted-foreground">
2668+
{t('designer.field.deleteBehavior.help', locale)}
2669+
</p>
2670+
</div>
2671+
);
2672+
}
2673+
25932674
/* ─────────────── Related list on the parent (relatedList*) ─────────────── */
25942675

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

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,11 @@ const STUDIO_EN: Record<string, string> = {
431431
'designer.field.lookup.allowCreate': 'Allow quick-create',
432432
'designer.field.lookup.referenceNameVisible': 'Show Name When Unreadable',
433433
'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.",
434+
'designer.field.deleteBehavior': 'Delete Behavior',
435+
'designer.field.deleteBehavior.help': 'What happens when the referenced record is deleted',
436+
'designer.field.deleteBehavior.set_null': 'Set Null (clear the reference)',
437+
'designer.field.deleteBehavior.cascade': 'Cascade (delete children)',
438+
'designer.field.deleteBehavior.restrict': 'Restrict (block the delete)',
434439
'designer.field.relatedList.heading': 'Related list on the parent record',
435440
'designer.field.relatedList.shown': 'Show',
436441
'designer.field.relatedList.hidden': 'Don\'t show',

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ const STUDIO_ZH: Record<string, string> = {
357357
'designer.field.lookup.allowCreate': '允许快速创建',
358358
'designer.field.lookup.referenceNameVisible': '不可读时显示名称',
359359
'designer.field.lookup.referenceNameVisible.help': '允许此记录的读者看到被引用记录的名称(纯文本,不带链接),即使其无权打开该记录。该记录的其他内容一律不显示;名称字段上的字段级规则仍然生效(此时名称显示为 Record # 加 id)。未设置:此类引用显示为「受限记录」。',
360+
'designer.field.deleteBehavior': '删除行为',
361+
'designer.field.deleteBehavior.help': '被引用记录删除时的处理方式',
362+
'designer.field.deleteBehavior.set_null': '置空(清除引用)',
363+
'designer.field.deleteBehavior.cascade': '级联(删除子记录)',
364+
'designer.field.deleteBehavior.restrict': '限制(阻止删除)',
360365
'designer.field.relatedList.heading': '父记录上的关联列表',
361366
'designer.field.relatedList.shown': '显示',
362367
'designer.field.relatedList.hidden': '不显示',

0 commit comments

Comments
 (0)