Skip to content

Commit 3f0b0cd

Browse files
feat(plugin-form): derive the per-row form's fields and its offer through the spec rule deriveInlineRowFormFields / isInlineRowFormOffered (objectui#11428) (#11756)
Fixes #11428 Clause-②: no ## What this PR is The per-row expand form of an inline master-detail collection now takes both of its rules from `@objectstack/spec/data`, as the collection's grid columns already do (objectui#11345): - **Which fields.** `deriveFormFields` returns `deriveInlineRowFormFields(childSchema, { relationshipField, exclude })`. objectui's own per-field rendering over the returned names is unchanged: the row form still renders each name through `ObjectForm`. - **Whether a row offers the form.** `MasterDetailForm` replaces its hand-written offer expression with `isInlineRowFormOffered({ inlineMode, formFields, columns })`, asked with the collection's resolved values, authored or derived. Deleted from `packages/plugin-form/src/deriveMasterDetail.ts`: `SYSTEM_FIELDS` and `NON_INPUT_TYPES`, which only `deriveFormFields` read. `SORT_FIELD_NAMES` stays, because the sort-field pick in `deriveDetail` still reads it. The direction is triage's first grade (comment 5944853069), unlocked at 17.7.0 (comment 6029278659). The default record form in `ObjectForm.tsx` is not touched; it waits on objectstack-ai/objectstack#21281. ## The fence - `deriveFormFields` keeps its signature. The built `dist/deriveMasterDetail.d.ts` before (BASE `c0862c1`) and after differ only in that function's doc comment, and `dist/MasterDetailForm.d.ts` is byte-identical. No export, prop, type member or i18n key is added to any `@object-ui/*` package, which is why `Clause-②: no` stands. - The output does not change on any input the evidence below reached. ## Landing site, and one addition to the claimed file surface The landing sites are the claimed ones: `deriveMasterDetail.ts`, `MasterDetailForm.tsx`, two tests beside them, and `.changeset/11428-inline-row-form-spec-rule.md`. **Outside the claim's declared surface**, as the unlock comment directs ("any other importer raises its own floor, and the PR says so"): `packages/plugin-form/package.json` (`@objectstack/spec` goes from `^17.6.0` to `^17.7.0`) and that importer's one specifier line in `pnpm-lock.yaml`. The resolved version stays 17.7.0. They are commit `a62959a`, kept separate so it can be judged on its own. The reason: the published `dist/index.js` now imports `deriveInlineRowFormFields` and `isInlineRowFormOffered` from `@objectstack/spec/data`, and 17.6.x exports neither. ## Evidence (head `328a381`) **The installed exports.** `@objectstack/spec` 17.7.0 `dist/data/index.d.mts` declares both, beside `deriveInlineGridColumns`: - `deriveInlineRowFormFields(def: unknown, opts?: { relationshipField?: string; exclude?: readonly string[] }): string[]` - `isInlineRowFormOffered(opts: { inlineMode?: 'grid' | 'form'; formFields?: readonly unknown[]; columns?: readonly unknown[] }): boolean` **The pins.** - `deriveMasterDetail.inlineRowFormFields-11428.test.ts`, the twin of the objectui#11345 corpus: one 15-entry corpus, two answers per entry. The names equal (`toStrictEqual`) the spec's `deriveInlineRowFormFields` answer, and they equal the arrays the replaced local filter returned. The second leg was run against the replaced filter, before the swap: `Tests 30 passed (30)`. - `inlineRowForm.specSource-11428.test.tsx`: both exports behind pass-through spies (a file-scoped `vi.mock` that spreads the real module, with a factory-ran probe). The pass-through rows are the existing behaviour: the three authored offer arms of `masterDetailDetailsMembers-8071` row 3, plus a fully derived collection. The stubbed rows hand back answers the local rules could never give: a stubbed field list comes back from `deriveFormFields` unchanged and drives `deriveDetail`'s `formFields` and its smart default; an inverted offer answer inverts every collection's offer, while list mode and its Add stay. Run against the BASE implementation, before the swap: `Tests 4 failed | 1 passed (5)`, the one green being the mock-installed check. - The existing pins are unchanged and green: `deriveFormFields (per-row expand form)` in `deriveMasterDetail.test.ts`, and `masterDetailDetailsMembers-8071` row 3. **Ablations on committed code (`328a381`).** Each ran through objectstack's `scripts/ablation-replace.mjs`. In each, the anchor went from 1 hit to 0, the blob changed, and the restore was proven (blob equals HEAD, `git diff HEAD` empty). - A, the order's leg: the offer reverted to the hand-written expression. Predicted 2 red (the two offer pins) with `masterDetailDetailsMembers-8071` green. Observed `Tests 2 failed | 12 passed (14)`. Row 3 of 8071 stays green under this ablation, which is why an equality pin cannot stand in for the spy. - B: `deriveFormFields` reverted to the replaced local filter. Predicted 2 red (the two derive spy pins) with the corpus green. Observed `Tests 2 failed | 33 passed (35)`. **One-off differential.** Not committed; a historical reading at `7ae2f6b`, whose source is the head's. It compares BASE `c0862c1` with this branch. - `deriveFormFields`, 100,000 random cases: system, sort-position and prototype-named keys; random types and flags, truthy and falsy; falsy and primitive definitions; array, null and non-object field maps; random `relationshipField` and `exclude`. Compared by deep equality plus the `util.inspect` rendering. **0 mismatches.** Control leg (the BASE filter plus a `readonly` drop): 14,118 mismatches, so the instrument can fire. - The offer, 300,000 random cases: `inlineMode` grid, form, absent or junk; `formFields` and `columns` absent, null or of random length. **0 mismatches.** Control leg (greater-or-equal in place of strictly greater): 62,517 mismatches. **Floor.** objectui's `scripts/check-spec-range-floors.mjs` verdict through its own `analyze()`, scoped to `@object-ui/plugin-form` over its built dist: - at `^17.7.0`: 0 findings; - at `^17.6.0` (in memory): `floor-too-low` on exactly `deriveInlineRowFormFields` and `isInlineRowFormOffered`. | Run (from the repo root, head `328a381`) | Exit | |:--|:--| | `pnpm exec vitest run --maxWorkers=2 packages/plugin-form/` | 0: `Test Files 167 passed (167)`, `Tests 1970 passed / 1 skipped (1971)` | | `pnpm --filter @object-ui/plugin-form type-check`, after `turbo run build --filter=@object-ui/plugin-form`. Both `tsc` legs ran, and `--listFiles` shows the test program reads both new files | 0 | | `pnpm --filter @object-ui/plugin-form lint` | 0, with 0 errors | | `check:phantom-deps`, `check:unused-deps`, `check:esm-specifiers`, `check:self-import`, `check:spec-symbols`, `check:unreferenced-sources`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:lockfile-integrity`, `check:lockfile-dedupe`, `check:installed-pin-claims`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:new-line-citations`, `check:control-bytes`, `check:element-data-source-declaration`, `check:handler-key-reads`, `check:metadata-write-doors`, `check:published-tsconfig-exclude` | 0 each | | `scripts/check-changeset-presence.mjs`, `scripts/check-changeset-no-major.mjs`, `scripts/check-changeset-fixed.mjs`, `scripts/check-lint-coverage.mjs`, `scripts/check-type-check-coverage.mjs` | 0 each | | `scripts/check-governed-queue-guard.mjs --test` over the seven changed paths | NOT GOVERNED | **NOT MEASURED locally.** These need every published package built, and CI or the release path runs them: - the full `check:spec-floors` (the scoped verdict above stands in); - `check:readme-exports` (this diff touches no README and no export); - `check:dist-completeness`, the load leg of `check:node-esm-load`, and `check:published-dist`; - the root `pnpm lint` and `pnpm test`. ## Acceptance notes - The sort-position names are now read in two places that hold the same set: the spec's module-private `INLINE_GRID_SORT_FIELDS` (through both derive exports) and objectui's `SORT_FIELD_NAMES`, which `deriveDetail` reads to pick the line's sort field. `@objectstack/spec/data` does not export the set. An observation only; the sort-field pick is not this card's rule. Carrier: none. ## Changeset `.changeset/11428-inline-row-form-spec-rule.md` declares `@object-ui/plugin-form` `patch`, the level measured above (the output is unchanged), and carries the floor sentence. Session: `https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju` (os-dev, dispatched by the `domain:ui` seat 2). --- _Generated by [Claude Code](https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a80fef7 commit 3f0b0cd

7 files changed

Lines changed: 469 additions & 37 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@object-ui/plugin-form': patch
3+
---
4+
5+
The per-row expand form of an inline master-detail collection now takes both of its rules from `@objectstack/spec` (objectui#11428), as the collection's grid columns already do. `deriveFormFields`, the form's fields when its author listed none, returns the spec's `deriveInlineRowFormFields` answer, and `MasterDetailForm` offers a row the form when the spec's `isInlineRowFormOffered` says so. The rules are the spec's now, so objectstack's `field-no-consumers` lint credits exactly the fields this form draws.
6+
7+
The output does not change. `deriveFormFields` keeps its signature and returns the same names in the same order, and a row is offered the form exactly when it was before: always in `form` mode, and in `grid` mode only when the form has more fields than the grid has columns. How each named field renders is still decided here.
8+
9+
`@object-ui/plugin-form` raises its `@objectstack/spec` floor from `^17.6.0` to `^17.7.0`, because its published entry now imports `deriveInlineRowFormFields` and `isInlineRowFormOffered`, which the spec first exports in 17.7.0.

‎packages/plugin-form/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@object-ui/permissions": "workspace:*",
2929
"@object-ui/react": "workspace:*",
3030
"@object-ui/types": "workspace:*",
31-
"@objectstack/spec": "^17.6.0",
31+
"@objectstack/spec": "^17.7.0",
3232
"lucide-react": "^1.43.0"
3333
},
3434
"peerDependencies": {

‎packages/plugin-form/src/MasterDetailForm.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ import {
6060
} from './masterDetailTx';
6161
import { isSameStoredValue } from './sanitize';
6262
import { deriveDetail } from './deriveMasterDetail';
63+
// objectui#11428 — the per-row form's offer test is the spec's rule.
64+
import { isInlineRowFormOffered } from '@objectstack/spec/data';
6365
// objectui#11396 — one `details` entry, by reference: the spec's own type for
6466
// `ComponentPropsMap['object-master-detail-form'].details[]`. Type-only.
6567
import type { ObjectMasterDetailFormProps } from '@objectstack/spec/ui';
@@ -877,8 +879,10 @@ const MasterDetailLines: React.FC<MasterDetailLinesProps> = ({
877879
// always in form mode (it IS the editor), and in grid mode only
878880
// when the full form has fields the grid omits. A thin grid whose
879881
// columns already cover every field (e.g. invoice lines) shows no
880-
// redundant expand button.
881-
{...((d.inlineMode === 'form' || (d.formFields?.length ?? 0) > (d.columns?.length ?? 0))
882+
// redundant expand button. The test is `@objectstack/spec`'s
883+
// `isInlineRowFormOffered` (objectui#11428), asked with the
884+
// collection's RESOLVED form factor and lists, authored or derived.
885+
{...(isInlineRowFormOffered({ inlineMode: d.inlineMode, formFields: d.formFields, columns: d.columns })
882886
? { onRowExpand: (rowIdx: number) => onRowExpand(entry.id, rowIdx) }
883887
: {})}
884888
displayMode={d.inlineMode === 'form' ? 'list' : 'grid'}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* objectui#11428 — `deriveFormFields`, the field names of an inline
3+
* master-detail collection's per-row expand form, takes them from
4+
* `@objectstack/spec`'s `deriveInlineRowFormFields`. It is the row-form twin of
5+
* `deriveMasterDetail.inlineGridColumns-11345.test.ts` (the grid columns).
6+
*
7+
* Two pins over one corpus, and they guard different things:
8+
*
9+
* 1. The names and their order ARE the spec's answer. This is what lets
10+
* objectstack's `field-no-consumers` lint credit exactly the fields this
11+
* form draws: a local filter creeping back in would let the two disagree,
12+
* and this pin goes red the moment one changes an answer.
13+
* 2. The output is what `deriveFormFields` returned BEFORE the swap. The
14+
* expected arrays below are the answers of the local filter this card
15+
* replaced (its own system-name, sort-name and non-input-type sets), and
16+
* `deriveFormFields` is a public export of `@object-ui/plugin-form`: the
17+
* swap must not move a single name. The corpus covers the relationship
18+
* field and `exclude` alone, together and absent; every system and
19+
* sort-position name; the `system` and `hidden` flags; `readonly`, which the
20+
* form KEEPS where the grid drops it; every non-input type; the rich types
21+
* the grid omits; falsy and primitive field definitions; prototype-named
22+
* keys; and the shapes with no field map.
23+
*
24+
* That `deriveFormFields` reads the spec's export at all, rather than agreeing
25+
* with it by coincidence, is pinned where a module mock is file-scoped:
26+
* `inlineRowForm.specSource-11428.test.tsx`.
27+
*
28+
* `toStrictEqual` on purpose, as in the twin.
29+
*/
30+
import { describe, it, expect } from 'vitest';
31+
import { deriveInlineRowFormFields } from '@objectstack/spec/data';
32+
import { deriveFormFields, type ChildObjectSchemaLike } from './deriveMasterDetail';
33+
34+
type Opts = { relationshipField?: string; exclude?: string[] };
35+
36+
const line = {
37+
name: { type: 'text', label: 'Name' },
38+
qty: { type: 'number', label: 'Qty' },
39+
unit_price: { type: 'currency', label: 'Unit price' },
40+
status: { type: 'select', options: ['open', 'closed'] },
41+
memo: { type: 'textarea' },
42+
order: { type: 'master_detail', reference: 'order', required: true },
43+
};
44+
45+
const CORPUS: Array<{ title: string; schema: ChildObjectSchemaLike | undefined; opts?: Opts; before: string[] }> = [
46+
{
47+
title: 'the relationship field is dropped',
48+
schema: { name: 'line', fields: line },
49+
opts: { relationshipField: 'order' },
50+
before: ['name', 'qty', 'unit_price', 'status', 'memo'],
51+
},
52+
{
53+
title: '`exclude` alone: the unexcluded relationship field stays',
54+
schema: { name: 'line', fields: line },
55+
opts: { exclude: ['memo'] },
56+
before: ['name', 'qty', 'unit_price', 'status', 'order'],
57+
},
58+
{
59+
title: '`exclude` and the relationship field together, with a name the map does not hold',
60+
schema: { name: 'line', fields: line },
61+
opts: { relationshipField: 'order', exclude: ['qty', 'qty', 'nope'] },
62+
before: ['name', 'unit_price', 'status', 'memo'],
63+
},
64+
{
65+
title: 'no options: every business field, the relationship field included',
66+
schema: { name: 'line', fields: line },
67+
before: ['name', 'qty', 'unit_price', 'status', 'memo', 'order'],
68+
},
69+
{
70+
title: 'an empty relationship field excludes nothing',
71+
schema: { name: 'line', fields: line },
72+
opts: { relationshipField: '' },
73+
before: ['name', 'qty', 'unit_price', 'status', 'memo', 'order'],
74+
},
75+
{
76+
title: 'every system / audit name and every sort-position name is dropped',
77+
schema: {
78+
name: 'line',
79+
fields: {
80+
id: { type: 'text' },
81+
_id: {},
82+
recordId: {},
83+
created_at: { type: 'datetime' },
84+
updated_at: { type: 'datetime' },
85+
created_by: { type: 'lookup', reference: 'user' },
86+
updated_by: { type: 'lookup', reference: 'user' },
87+
createdAt: {},
88+
updatedAt: {},
89+
createdBy: {},
90+
updatedBy: {},
91+
organization_id: {},
92+
tenant_id: {},
93+
space: {},
94+
owner: { type: 'lookup', reference: 'user' },
95+
position: { type: 'number' },
96+
sort_order: { type: 'number' },
97+
sequence: { type: 'number' },
98+
line_no: { type: 'number' },
99+
line_number: { type: 'number' },
100+
sort: { type: 'number' },
101+
title: { type: 'text', required: true },
102+
},
103+
},
104+
before: ['title'],
105+
},
106+
{
107+
title: 'flags: `system` and `hidden` drop a field, `readonly` and `required` do not',
108+
schema: {
109+
name: 'line',
110+
fields: {
111+
secret: { type: 'text', hidden: true },
112+
internal: { type: 'text', system: true },
113+
locked: { type: 'text', readonly: true },
114+
must: { type: 'text', required: true },
115+
shown: { type: 'text', hidden: false, system: false },
116+
truthy_hidden: { type: 'text', hidden: 1 },
117+
},
118+
},
119+
before: ['locked', 'must', 'shown'],
120+
},
121+
{
122+
title: 'types: the non-input types are dropped, the rich types the grid omits are kept',
123+
schema: {
124+
name: 'invoice_line',
125+
fields: {
126+
total: { type: 'formula', expression: 'qty * unit_price' },
127+
sum: { type: 'summary' },
128+
roll: { type: 'rollup' },
129+
seq: { type: 'autonumber' },
130+
seq2: { type: 'auto_number' },
131+
notes: { type: 'textarea' },
132+
body: { type: 'richtext' },
133+
blob: { type: 'json' },
134+
place: { type: 'location' },
135+
html: { type: 'html' },
136+
md: { type: 'markdown' },
137+
cover: { type: 'image' },
138+
attachment: { type: 'file', multiple: true },
139+
face: { type: 'avatar' },
140+
product: { type: 'lookup', reference: 'product' },
141+
amount: { type: 'currency', expression: 'qty * unit_price' },
142+
typed_oddly: { type: 7 },
143+
untyped: {},
144+
},
145+
},
146+
before: ['notes', 'body', 'blob', 'place', 'html', 'md', 'cover', 'attachment', 'face', 'product', 'amount', 'typed_oddly', 'untyped'],
147+
},
148+
{
149+
title: 'falsy and primitive field definitions are kept, in field order',
150+
schema: {
151+
name: 'line',
152+
fields: {
153+
a_null: null,
154+
b_undefined: undefined,
155+
c_zero: 0,
156+
d_empty: '',
157+
e_false: false,
158+
f_string: 'text',
159+
g_number: 1,
160+
h_true: true,
161+
name: { type: 'text', label: 'Name' },
162+
},
163+
},
164+
before: ['a_null', 'b_undefined', 'c_zero', 'd_empty', 'e_false', 'f_string', 'g_number', 'h_true', 'name'],
165+
},
166+
{
167+
title: 'prototype-named keys are ordinary field names',
168+
schema: {
169+
name: 'line',
170+
fields: {
171+
constructor: { type: 'text' },
172+
toString: { type: 'text' },
173+
hasOwnProperty: { type: 'text', hidden: true },
174+
valueOf: { type: 'formula' },
175+
},
176+
},
177+
opts: { exclude: ['toString'] },
178+
before: ['constructor'],
179+
},
180+
{ title: 'no schema', schema: undefined, before: [] },
181+
{ title: 'no field map', schema: { name: 'line' }, before: [] },
182+
{ title: 'an empty field map', schema: { name: 'line', fields: {} }, before: [] },
183+
{ title: 'a null field map', schema: { name: 'line', fields: null as unknown as Record<string, unknown> }, before: [] },
184+
{ title: 'a field map that is not an object', schema: { name: 'line', fields: 'nope' as unknown as Record<string, unknown> }, before: [] },
185+
];
186+
187+
describe('deriveFormFields derives its names through the spec rule (objectui#11428)', () => {
188+
it.each(CORPUS)("$title: the names and their order are the spec's deriveInlineRowFormFields", ({ schema, opts }) => {
189+
expect(deriveFormFields(schema, opts)).toStrictEqual(deriveInlineRowFormFields(schema, opts));
190+
});
191+
192+
it.each(CORPUS)('$title: the output is unchanged from the local filter it replaced', ({ schema, opts, before }) => {
193+
expect(deriveFormFields(schema, opts)).toStrictEqual(before);
194+
});
195+
});

‎packages/plugin-form/src/deriveMasterDetail.ts‎

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
* `reportCurrencyColumnScale` (objectui#10783).
1616
*/
1717

18-
import { DEFAULT_MAX_INLINE_GRID_COLUMNS, deriveInlineGridColumns } from '@objectstack/spec/data';
18+
import {
19+
DEFAULT_MAX_INLINE_GRID_COLUMNS,
20+
deriveInlineGridColumns,
21+
deriveInlineRowFormFields,
22+
} from '@objectstack/spec/data';
1923
import type { GridColumn } from '@object-ui/fields';
2024

2125
/**
@@ -43,17 +47,10 @@ export interface ChildObjectSchemaLike {
4347
fields?: Record<string, any>;
4448
}
4549

46-
/** System / audit fields never offered in a line's row form. */
47-
const SYSTEM_FIELDS = new Set([
48-
'id', '_id', 'recordId',
49-
'created_at', 'updated_at', 'created_by', 'updated_by',
50-
'createdAt', 'updatedAt', 'createdBy', 'updatedBy',
51-
'organization_id', 'tenant_id', 'space', 'owner',
52-
]);
53-
54-
/** Field names that hold a line's sort position — excluded from the row form
55-
* (the grid stamps them on drag-reorder instead), and the names `deriveDetail`
56-
* picks the line's sort field from. */
50+
/** Field names that hold a line's sort position — the names `deriveDetail`
51+
* picks the line's sort field from (the grid stamps it on drag-reorder). The
52+
* grid columns and the row form leave these names out by the spec's own rule
53+
* (`deriveInlineGridColumns`, `deriveInlineRowFormFields`). */
5754
const SORT_FIELD_NAMES = new Set(['position', 'sort_order', 'sequence', 'line_no', 'line_number', 'sort']);
5855

5956
/** Map an ObjectQL field type to a LineItems grid column type. */
@@ -347,33 +344,34 @@ export function hydrateColumns(
347344
});
348345
}
349346

350-
/** Computed / non-input field types — excluded from the row form (read-only,
351-
* server-derived). Unlike grid columns we DO keep rich inputs (textarea,
352-
* richtext, file, image, json…) since the row form has room for them. */
353-
const NON_INPUT_TYPES = new Set(['formula', 'summary', 'rollup', 'autonumber', 'auto_number']);
354-
355347
/**
356-
* Field names for a child's full "row form" (the per-row expand editor) — every
357-
* editable business field, skipping system/audit fields, the back-reference FK,
358-
* and computed types. Broader than {@link deriveColumns} (which only returns
359-
* grid-friendly types): the form has room for textarea/richtext/file/etc.
348+
* Field names for a child's full "row form" (the per-row expand editor) — the
349+
* form an inline master-detail collection opens when its author listed no
350+
* `formFields`.
351+
*
352+
* WHICH fields, in what order, is `@objectstack/spec`'s rule,
353+
* `deriveInlineRowFormFields` (objectui#11428), the row-form twin of the
354+
* `deriveInlineGridColumns` rule {@link deriveColumns} reads: it skips system /
355+
* audit fields, sort-position fields, fields flagged `system` or `hidden`, the
356+
* computed types, the back-reference FK to the parent and any `exclude`d name,
357+
* and keeps every other field in the child's field order. So it is broader than
358+
* the grid: `readonly` fields and the rich types a grid cell omits (textarea,
359+
* richtext, json, file, …) stay, since the form has room for them. The rule
360+
* lives in the spec so that an author-time tool (objectstack's
361+
* `field-no-consumers` lint) credits exactly the fields this form draws.
362+
*
363+
* WHETHER a row offers the form at all is the spec's `isInlineRowFormOffered`,
364+
* which `MasterDetailForm` asks. How each named field renders is the form's own
365+
* per-field builder, over these names.
360366
*/
361367
export function deriveFormFields(
362368
childSchema: ChildObjectSchemaLike | undefined,
363369
opts: { relationshipField?: string; exclude?: string[] } = {},
364370
): string[] {
365-
const fields = childSchema?.fields;
366-
if (!fields || typeof fields !== 'object') return [];
367-
const exclude = new Set([...(opts.exclude ?? []), ...(opts.relationshipField ? [opts.relationshipField] : [])]);
368-
const out: string[] = [];
369-
for (const [name, def] of Object.entries(fields)) {
370-
const d = def as any;
371-
if (SYSTEM_FIELDS.has(name) || exclude.has(name) || SORT_FIELD_NAMES.has(name)) continue;
372-
if (d?.system || d?.hidden) continue;
373-
if (NON_INPUT_TYPES.has(d?.type)) continue;
374-
out.push(name);
375-
}
376-
return out;
371+
return deriveInlineRowFormFields(childSchema, {
372+
relationshipField: opts.relationshipField,
373+
exclude: opts.exclude,
374+
});
377375
}
378376

379377
/** Inline-edit form factor. */

0 commit comments

Comments
 (0)