From 21509d3c80d1bf3b2e3afb2f4e14e25a2a8e2a77 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 08:06:42 +0000 Subject: [PATCH] test(plugin-grid): render what the inline lookup picker actually receives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit objectui#7154 reported that `multiple`, `allowCreate`, `lookupPageSize` and `dependsOn` never reach `ObjectGrid`'s inline lookup picker, because none of them is on the relational copy set. Measured against the grid's own inline editor, all four already take effect: the copy set is not the route. `applyRelationalMeta` writes onto the `fieldMeta` handed to `` — the read-only cell, whose relational reader `LookupCellRenderer` reads none of the four. The inline editor is a different seam: `renderCellEditor` looks the field up in the object schema and spreads the whole def into the widget, so every key a def carries reaches `LookupField` regardless of this list. Both halves read `objectSchema.fields[name]`, so there is no shape where copying could rescue an editor the schema read did not already serve. New `__tests__/lookupPickerKeys-7154.test.tsx` renders each key against a control column differing only in that key. The four verdicts stay `deferred`, now with the measurement in their notes, and the docblocks and call-site comments that claimed the picker reads this bag are corrected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM --- .../7154-lookup-picker-keys-editor-seam.md | 29 ++ packages/plugin-grid/src/ObjectGrid.tsx | 26 +- .../__tests__/lookupPickerKeys-7154.test.tsx | 334 ++++++++++++++++++ .../relationalMetaCopySet.derivation.test.ts | 11 +- .../plugin-grid/src/relationalMetaKeys.ts | 46 ++- 5 files changed, 425 insertions(+), 21 deletions(-) create mode 100644 .changeset/7154-lookup-picker-keys-editor-seam.md create mode 100644 packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx diff --git a/.changeset/7154-lookup-picker-keys-editor-seam.md b/.changeset/7154-lookup-picker-keys-editor-seam.md new file mode 100644 index 000000000..cf3cf14d9 --- /dev/null +++ b/.changeset/7154-lookup-picker-keys-editor-seam.md @@ -0,0 +1,29 @@ +--- +--- + +Measurement-only change in `@object-ui/plugin-grid`: objectui#7154 reported that +`ObjectGrid`'s inline lookup picker never receives `multiple`, `allowCreate`, +`lookupPageSize` or `dependsOn` because they are not on the relational copy set. +Rendered against the grid's own inline editor, all four already take effect — +the copy set is not the route. + +`applyRelationalMeta` writes onto the `fieldMeta` handed to ``, +the read-only cell. The inline editor is a different seam: `renderCellEditor` +looks the field up in the object schema and spreads the whole def into the +widget, so every key a def carries reaches `LookupField` whether or not it is +copied. Both halves read `objectSchema.fields[name]`, so copying could never +rescue an editor the schema read did not already serve. + +- New `__tests__/lookupPickerKeys-7154.test.tsx` renders each key against a + control column differing only in that key: `multiple` accumulates two picks + ("2 selected") where the control replaces; `allowCreate: false` removes the + quick-create entry the control offers; `lookupPageSize: 3` scopes the picker + dialog to 3 rows against a control of 10; `dependsOn` arrives and gates. +- The four verdicts stay `deferred` — flipping them would write members onto a + bag whose consumer does not read them, the shape objectui#6711 and + objectui#6874 retired — with the measurement now in their notes. +- Corrected the docblocks and the three `applyRelationalMeta` call-site comments + that claimed the inline picker reads this bag; that claim is what the card was + filed against. + +No behaviour change. diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 83a0fa566..94798f011 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -424,11 +424,18 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null { /** * The relational copy set and `applyRelationalMeta` moved to * `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a - * table classifying every key the grid's own cell renderer and inline picker - * read off this bag, and a gate re-derives that read set from the consumer - * sources — so the copy set can no longer drift into being a strict subset of - * what its consumers read, which is what it had silently become. Read that - * file's docblock before adding, removing or re-spelling a key. + * table classifying every key swept off the three `@object-ui/fields` consumers, + * and a gate re-derives that read set from their sources — so the copy set can + * no longer drift into being a strict subset of what those consumers read, + * which is what it had silently become. Read that file's docblock before + * adding, removing or re-spelling a key. + * + * ⚠️ What this bag actually feeds is ONE of those three: the read-only cell. + * `fieldMeta` is handed to `` and nowhere else in this + * file. The inline picker is the OTHER seam — `renderCellEditor` below looks + * the field up in `objectSchema` and spreads the whole def into the widget, so + * a picker key reaches `LookupField` whether or not it is on the copy set. + * Measured in `__tests__/lookupPickerKeys-7154.test.tsx` (objectui#7154). */ /** @@ -2134,7 +2141,8 @@ export const ObjectGrid: React.FC = ({ if (objectDefField.options) fieldMeta.options = translateOptions(schema.objectName, col.field, objectDefField.options); } // Preserve relational metadata (reference_to, display_field, …) so - // lookup cells resolve ids to names and the inline picker can query. + // lookup CELLS resolve ids to names. ⛔ Not the inline picker — that + // reads the schema def directly, see `renderCellEditor` (objectui#7154). applyRelationalMeta(fieldMeta, objectDefField as any); // Auto-generate options from data for inferred select without existing options if (inferredType === 'select' && !fieldMeta.options) { @@ -2326,7 +2334,8 @@ export const ObjectGrid: React.FC = ({ if (fieldDef.options) fieldMeta.options = translateOptions(schema.objectName, fieldName, fieldDef.options); } // Preserve relational metadata (reference_to, display_field, …) so - // lookup cells resolve ids to names and the inline picker can query. + // lookup CELLS resolve ids to names. ⛔ Not the inline picker — that + // reads the schema def directly, see `renderCellEditor` (objectui#7154). applyRelationalMeta(fieldMeta, fieldDef as any); // Auto-generate select options from data when no options defined if (resolvedType === 'select' && !fieldMeta.options) { @@ -2483,7 +2492,8 @@ export const ObjectGrid: React.FC = ({ if (fieldDef.options) fieldMeta.options = translateOptions(schema.objectName, fieldName, fieldDef.options); } // Preserve relational metadata (reference_to, display_field, …) so - // lookup cells resolve ids to names and the inline picker can query. + // lookup CELLS resolve ids to names. ⛔ Not the inline picker — that + // reads the schema def directly, see `renderCellEditor` (objectui#7154). applyRelationalMeta(fieldMeta, fieldDef as any); // Auto-generate select options from data when no options defined if (resolvedType === 'select' && !fieldMeta.options) { diff --git a/packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx b/packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx new file mode 100644 index 000000000..229bbaf81 --- /dev/null +++ b/packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx @@ -0,0 +1,334 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#7154 — the four picker keys `multiple`, `allowCreate`, + * `lookupPageSize` and `dependsOn` DO reach `ObjectGrid`'s inline lookup + * picker, and they reach it WITHOUT being on the relational copy set. + * + * ## The card this file answers, and why it is not four verdict flips + * + * objectui#7154 was filed off the derivation objectui#6875 built: the extractor + * in `relationalMetaCopySet.derivation.test.ts` sweeps the three consumers for + * keys read off a field-meta bag, and these four came back read, spec-declared + * (`FieldSchema` 17.2.0 — 71 strict props, all four present, measured with + * `name`/`type`/`label` as the positive control) and absent from + * `RELATIONAL_META_KEYS`. The card concluded the values never arrive, and asked + * for four `deferred` → `spec` flips plus rendering proof. + * + * The rendering proof is what killed the premise. Every one of the four is + * ALREADY in effect in the grid's inline picker on an unmodified tree, because + * the picker is not fed the copied bag at all: + * + * - `applyRelationalMeta` writes onto `fieldMeta`, and `fieldMeta`'s only + * consumer is `` — the READ-ONLY cell + * (`ObjectGrid.tsx`, all three column-building paths). For a relational + * column that resolves to `LookupCellRenderer`, which reads exactly + * `reference_to`, `reference`, `display_field`, `displayField` and + * `reference_field` — none of the four. + * - The inline EDITOR is a different seam: `renderCellEditor` looks the field + * up in the object schema itself and spreads the WHOLE def into the widget + * — `let field: any = { name: ctx.column.accessorKey, ...fieldDef }` — so + * every key the def carries reaches `LookupField`, on the copy set or not. + * + * Both halves read `objectSchema?.fields?.[name]`, the same object, so there is + * no shape where the copy set could rescue an editor the schema read did not + * already serve: when that lookup misses, `renderCellEditor` returns `null` and + * `applyRelationalMeta` copies nothing, together. + * + * ⇒ Flipping the four verdicts would write four members onto a bag whose + * consumer does not read them — the defect class objectui#6711 + * (`reference_to_field`) and objectui#6874 (`titleFormat`) each retired. They + * stay `deferred`, now with the measurement in their notes instead of a promise. + * + * ## What each test renders + * + * One data source, one referenced object, two lookup columns whose field defs + * differ ONLY in the key under test — the shape objectui#6875's + * `lookupDisplayFieldSpelling-6875.test.tsx` established. The control column is + * load-bearing in both directions: it proves the picker path is reached and + * that the difference is the declared key rather than the fixture. + * + * ## ⚠️ `dependsOn` arrives and GATES — objectui#2215's grid-side residue + * + * objectui#2215 ("cascading lookup broken in forms; table picker bypasses the + * dependent filter") was closed COMPLETED by PR objectui#2216, which fixed two + * halves: the FORM renderer injects its live watched record as + * `dependentValues`, and every picker surface takes the `dependsOn` chain as a + * hard `baseFilter`. Only the second half is host-independent. `LookupField` + * resolves `dependentValues ?? ctx.formValues ?? ctx.data ?? {}`, and the grid's + * inline editor supplies none of the three — `renderCellEditor`'s context object + * is `{ column, row, value, stage, commit, cancel }` and nothing forwards `row`. + * + * So a `dependsOn` lookup column in an editable grid renders a PERMANENTLY + * gated, disabled trigger ("Select region first") even when the row carries the + * parent value. That is the current behaviour, it is pinned below as such, and + * it is a separate defect from this card — filed rather than fixed here, + * because whether the grid should feed the SAVED row or the row's in-flight + * staged edits is a design question objectui#2215's form fix answered one way + * (live values) that the grid cannot copy without a staged-value channel. + */ +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { render, screen, waitFor, fireEvent, within } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; + +import { ObjectGrid } from '../ObjectGrid'; +import { RELATIONAL_META_KEYS } from '../relationalMetaKeys'; +import { registerAllFields } from '@object-ui/fields'; +import { ActionProvider, SchemaRendererProvider } from '@object-ui/react'; + +registerAllFields(); + +const OBJECT = 'os_7154_task'; +const REF = 'os_7154_person'; + +/** Twelve candidates: more than the picker dialog's default page of 10. */ +const PEOPLE = Array.from({ length: 12 }, (_, i) => ({ + id: `p${i + 1}`, + name: `Person ${String(i + 1).padStart(2, '0')}`, + region: i < 6 ? 'north' : 'south', +})); + +beforeAll(() => { + if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = vi.fn() as any; + if (!(Element.prototype as any).hasPointerCapture) (Element.prototype as any).hasPointerCapture = () => false; + if (!(Element.prototype as any).setPointerCapture) (Element.prototype as any).setPointerCapture = () => {}; + if (!(Element.prototype as any).releasePointerCapture) (Element.prototype as any).releasePointerCapture = () => {}; +}); + +/** + * A data source whose referenced-object query honours `$top`/`$skip` and the + * `$filter` record, so a page size and a dependent filter are observable as + * rendered rows rather than only as call arguments. + */ +function makeDataSource(fields: Record, rows: any[]) { + const refQueries: any[] = []; + return { + refQueries, + find: vi.fn(async (objectName: string, params: any) => { + if (objectName === REF) { + refQueries.push(params); + let recs = PEOPLE; + const filter = params?.$filter; + if (filter && typeof filter === 'object' && filter.region) { + recs = recs.filter((p) => p.region === filter.region); + } + const top = params?.$top ?? 50; + const skip = params?.$skip ?? 0; + return { data: recs.slice(skip, skip + top), total: recs.length, hasMore: false, pageSize: top }; + } + return { data: rows, total: rows.length, hasMore: false, pageSize: 50 }; + }), + findOne: vi.fn(async (objectName: string, id: string) => + objectName === REF ? (PEOPLE.find((p) => p.id === id) ?? null) : null, + ), + update: vi.fn(async (_o: string, _id: string, changes: any) => changes), + getObjectSchema: async (name: string) => { + if (name === REF) { + return { name, fields: { id: { type: 'text' }, name: { type: 'text' }, region: { type: 'text' } } }; + } + return { name, fields: { id: { type: 'text' }, title: { type: 'text', label: 'Title' }, ...fields } }; + }, + } as any; +} + +function renderGrid(ds: any, rows: any[], columns: any[]) { + const schema: any = { + type: 'object-grid', + objectName: OBJECT, + editable: true, + singleClickEdit: true, + data: rows, + pagination: { pageSize: 50 }, + columns, + }; + return render( + + + + + , + ); +} + +/** The n-th DATA cell of the single row (`td[0]` is the row-number column). */ +function cellAt(container: HTMLElement, index: number): HTMLElement { + const row = container.querySelector('tbody tr') as HTMLElement; + const tds = Array.from(row.querySelectorAll('td')) as HTMLElement[]; + return tds[index + 1]; +} + +/** Single-click into a cell and hand back the widget's own trigger button. */ +async function openEditor(cell: HTMLElement): Promise { + fireEvent.click(cell); + return await waitFor(() => { + const btn = cell.querySelector('button'); + expect(btn).toBeTruthy(); + return btn as HTMLButtonElement; + }); +} + +describe('objectui#7154 — the four picker keys reach the grid’s inline picker off the field def', () => { + it('none of the four is on the relational copy set (the premise this file re-measures)', () => { + // Control: the copy set is populated and holds the key objectui#6875 added, + // so "does not contain" below is a reading and not an empty list. + expect(RELATIONAL_META_KEYS.length).toBeGreaterThan(5); + expect(RELATIONAL_META_KEYS).toContain('displayField'); + for (const key of ['multiple', 'allowCreate', 'lookupPageSize', 'dependsOn']) { + expect(RELATIONAL_META_KEYS).not.toContain(key); + } + }); + + it('`multiple` — the declared column accumulates two picks; the control replaces', async () => { + const rows = [{ id: 't1', title: 'Task one', owners: null, owner: null }]; + const ds = makeDataSource( + { + owners: { type: 'lookup', label: 'Owners', reference: REF, multiple: true }, + owner: { type: 'lookup', label: 'Owner', reference: REF }, + }, + rows, + ); + const { container } = renderGrid(ds, rows, [ + { field: 'title', label: 'Title', editable: false }, + { field: 'owners', label: 'Owners', type: 'lookup' }, + { field: 'owner', label: 'Owner', type: 'lookup' }, + ]); + await waitFor(() => expect(screen.getByText('Task one')).toBeInTheDocument()); + + // Declared `multiple: true` — both picks land, the popover stays open. + const multiCell = cellAt(container, 1); + fireEvent.click(await openEditor(multiCell)); + fireEvent.click(await waitFor(() => screen.getByText('Person 01'))); + fireEvent.click(await waitFor(() => screen.getByText('Person 02'))); + await waitFor(() => { + expect(multiCell.textContent).toMatch(/2 selected/); + }); + + // CONTROL — same reference, same records, no `multiple`: the second pick + // REPLACES the first and the compact trigger shows that one record. + const singleCell = cellAt(container, 2); + fireEvent.click(await openEditor(singleCell)); + fireEvent.click(await waitFor(() => screen.getByText('Person 01'))); + fireEvent.click(await openEditor(singleCell)); + fireEvent.click(await waitFor(() => screen.getByText('Person 02'))); + await waitFor(() => { + expect(within(singleCell).getByText('Person 02')).toBeInTheDocument(); + }); + expect(singleCell.textContent).not.toMatch(/selected/); + }); + + it('`allowCreate` — `false` removes the quick-create affordance the control offers', async () => { + const rows = [{ id: 't1', title: 'Task one', owner: null, fixed_owner: null }]; + const ds = makeDataSource( + { + owner: { type: 'lookup', label: 'Owner', reference: REF }, + fixed_owner: { type: 'lookup', label: 'Fixed owner', reference: REF, allowCreate: false }, + }, + rows, + ); + const { container } = renderGrid(ds, rows, [ + { field: 'title', label: 'Title', editable: false }, + { field: 'owner', label: 'Owner', type: 'lookup' }, + { field: 'fixed_owner', label: 'Fixed owner', type: 'lookup' }, + ]); + await waitFor(() => expect(screen.getByText('Task one')).toBeInTheDocument()); + + // CONTROL — nothing declared. `os_7154_person` is a user-facing reference, + // so inline quick-create is on by default and the entry renders. + const defaultCell = cellAt(container, 1); + fireEvent.click(await openEditor(defaultCell)); + await waitFor(() => expect(screen.getByText('Person 01')).toBeInTheDocument()); + expect(screen.getByText('Create new')).toBeInTheDocument(); + fireEvent.keyDown(document.body, { key: 'Escape' }); + await waitFor(() => expect(screen.queryByText('Create new')).not.toBeInTheDocument()); + + // Declared `allowCreate: false` — the author's opt-out is honoured. + const optedOutCell = cellAt(container, 2); + fireEvent.click(await openEditor(optedOutCell)); + await waitFor(() => expect(screen.getByText('Person 01')).toBeInTheDocument()); + expect(screen.queryByText('Create new')).not.toBeInTheDocument(); + }); + + it('`lookupPageSize` — the declared page size scopes the picker dialog’s query and rows', async () => { + const rows = [{ id: 't1', title: 'Task one', owner: null, paged_owner: null }]; + const ds = makeDataSource( + { + owner: { type: 'lookup', label: 'Owner', reference: REF }, + paged_owner: { type: 'lookup', label: 'Paged owner', reference: REF, lookupPageSize: 3 }, + }, + rows, + ); + const { container, unmount } = renderGrid(ds, rows, [ + { field: 'title', label: 'Title', editable: false }, + { field: 'owner', label: 'Owner', type: 'lookup' }, + { field: 'paged_owner', label: 'Paged owner', type: 'lookup' }, + ]); + await waitFor(() => expect(screen.getByText('Task one')).toBeInTheDocument()); + + const pagedCell = cellAt(container, 2); + await openEditor(pagedCell); + fireEvent.click(await waitFor(() => within(pagedCell).getByTestId('browse-all-records'))); + await waitFor(() => expect(screen.getByText('Person 01')).toBeInTheDocument()); + await waitFor(() => { + expect(document.querySelectorAll('[role="dialog"] tbody tr').length).toBe(3); + }); + expect(ds.refQueries.some((q: any) => q?.$top === 3)).toBe(true); + unmount(); + + // CONTROL — the sibling column declares no page size, so the same dialog + // over the same twelve records uses `RecordPickerDialog`'s default of 10. + const second = renderGrid(ds, rows, [ + { field: 'title', label: 'Title', editable: false }, + { field: 'owner', label: 'Owner', type: 'lookup' }, + { field: 'paged_owner', label: 'Paged owner', type: 'lookup' }, + ]); + await waitFor(() => expect(screen.getByText('Task one')).toBeInTheDocument()); + const defaultCell = cellAt(second.container, 1); + await openEditor(defaultCell); + fireEvent.click(await waitFor(() => within(defaultCell).getByTestId('browse-all-records'))); + await waitFor(() => { + expect(document.querySelectorAll('[role="dialog"] tbody tr').length).toBe(10); + }); + }); + + it('`dependsOn` — the declared column gates; the control does not (objectui#2215’s grid-side residue)', async () => { + const rows = [{ id: 't1', title: 'Task one', region: 'north', owner: null, regional_owner: null }]; + const ds = makeDataSource( + { + region: { type: 'text', label: 'Region' }, + owner: { type: 'lookup', label: 'Owner', reference: REF }, + regional_owner: { type: 'lookup', label: 'Regional owner', reference: REF, dependsOn: ['region'] }, + }, + rows, + ); + const { container } = renderGrid(ds, rows, [ + { field: 'title', label: 'Title', editable: false }, + { field: 'region', label: 'Region', editable: false }, + { field: 'owner', label: 'Owner', type: 'lookup' }, + { field: 'regional_owner', label: 'Regional owner', type: 'lookup' }, + ]); + await waitFor(() => expect(screen.getByText('Task one')).toBeInTheDocument()); + + // CONTROL — no `dependsOn`: an ordinary, usable trigger. + const plainTrigger = await openEditor(cellAt(container, 2)); + expect(plainTrigger.getAttribute('data-testid')).toBe('lookup-trigger-owner'); + expect(plainTrigger.disabled).toBe(false); + + // Declared `dependsOn: ['region']` — the key ARRIVES (the gate is proof it + // was read) and the picker is gated. The row carries `region: 'north'`, so + // the gate is not "the parent is empty": the grid feeds the widget no + // dependent values at all, which is why this state is permanent. + const gatedTrigger = await openEditor(cellAt(container, 3)); + expect(gatedTrigger.getAttribute('data-testid')).toBe('lookup-trigger-gated'); + expect(gatedTrigger.disabled).toBe(true); + expect(gatedTrigger.textContent).toMatch(/region/i); + // The browse-all button next to it is gated too (PR objectui#2216). + expect(within(cellAt(container, 3)).getByTestId('browse-all-records')).toBeDisabled(); + }); +}); diff --git a/packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts b/packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts index 2c4bc77ee..27867154b 100644 --- a/packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts +++ b/packages/plugin-grid/src/__tests__/relationalMetaCopySet.derivation.test.ts @@ -35,9 +35,14 @@ * `getCellRenderer` dispatches a relational column to `LookupCellRenderer` * (`@object-ui/fields/src/index.tsx`), which reads its keys through * `(field as { k?: T }).k` casts — the untyped-read shape this seam uses. The - * inline editor dispatches the same bag into `LookupField` (receiver - * `fieldMeta`) and `UserField` (receiver `meta`), which use optional-chained - * member reads. + * inline editor renders `LookupField` (receiver `fieldMeta`) and `UserField` + * (receiver `meta`), which use optional-chained member reads. + * + * ⚠️ Those two are swept, but they are NOT fed this bag — `renderCellEditor` + * spreads the schema def into them directly (objectui#7154, measured in + * `lookupPickerKeys-7154.test.tsx`). So a key that only they read is + * classified here without ever being a candidate for copying, and the verdict + * column is where that decision is recorded. * * ⚠️ `UserField` is swept even though it forwards its whole meta into * `LookupField` via a spread. A delegating consumer is exactly where a false diff --git a/packages/plugin-grid/src/relationalMetaKeys.ts b/packages/plugin-grid/src/relationalMetaKeys.ts index b9b046c2c..4469a85af 100644 --- a/packages/plugin-grid/src/relationalMetaKeys.ts +++ b/packages/plugin-grid/src/relationalMetaKeys.ts @@ -40,12 +40,34 @@ * `generateColumns()` hands `fieldMeta` to `CellRenderer` as the `field` prop, * and `getCellRenderer` dispatches a relational column into * `LookupCellRenderer` (`@object-ui/fields/src/index.tsx`). The grid's inline - * editor dispatches the same bag into `LookupField` and `UserField` - * (`@object-ui/fields/src/widgets/`). `UserField` reads a few keys itself and + * editor renders `LookupField` and `UserField` + * (`@object-ui/fields/src/widgets/`); `UserField` reads a few keys itself and * then spreads its whole meta into `LookupField` — so its own read set is a * subset and it adds nothing; it is swept anyway, because a key it read and * did NOT forward would otherwise be invisible here. * + * ## ⚠️ Only the FIRST of those three is fed by this bag — objectui#7154 + * + * This docblock used to say the inline editor "dispatches the same bag" into + * the two widgets. It does not, and the correction matters because it is what + * a whole class of card is filed against: `ObjectGrid.renderCellEditor` looks + * the field up in the object schema and spreads the WHOLE def into the widget + * (`{ name: ctx.column.accessorKey, ...fieldDef }`), so every key a def carries + * reaches `LookupField` regardless of this table. `fieldMeta` goes to + * `` and nowhere else. + * + * Both halves read `objectSchema?.fields?.[name]` — the same object — so there + * is no shape where copying could rescue an editor the schema read did not + * already serve: when that lookup misses, `renderCellEditor` returns `null` and + * `applyRelationalMeta` copies nothing, together. + * + * ⇒ A key whose only reader is one of the two EDITOR widgets gains nothing from + * being copied here, and copying it would write a member onto a bag its + * consumer does not read — the shape objectui#6711 and objectui#6874 retired. + * The four keys objectui#7154 asked about are measured arriving at the picker + * with this table unchanged, in + * `__tests__/lookupPickerKeys-7154.test.tsx`. + * * ## ⭐ Why a key can be READ and still not be worth copying * * `@objectstack/spec` 17.2.0's `FieldSchema` is a **strict** object of 71 @@ -179,14 +201,18 @@ export const RELATIONAL_META_READ_SET: Readonly