Repository navigation
Commit ea1751b
feat(fields): a lookup pick fills the fields its lookupAutofill declares, in the record form and the line-item grid (#28322)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoKfz877ZMAsxYB1FQDnMtZw)_
Fixes #28245
Clause-②: yes (widening: `@object-ui/fields` gains the widget prop
`onAutofill`, the `GridField` prop `lookupAutofill`, an optional fourth
argument on `lookupAutofillPatch`, and the exports
`declaredLookupAutofillPatch`, `lookupAutofillByField`,
`LookupAutofillPair`, `LookupAutofillByField`; `@object-ui/types`
`LookupFieldMetadata` gains `lookupAutofill`; measured on the built
declarations; no existing member, key or shape changes)
Metadata-protocol: none
## 维护者速读
- 改了什么:查找字段(lookup)选中一条记录后,按字段上声明的 `lookupAutofill`
立刻把对应字段填进表单;明细行网格里选产品时同样生效(例如报价明细选产品,单价立即带出产品的标价),不再要等保存后服务端钩子才填。
- 为什么改:#28244 已在 spec 里声明了 `lookupAutofill`,但界面上没有任何读取方,作者写了也不生效;本 PR
是它的 ui 一半,并把 liveness 行从 planned 改为 live。
- 风险与回滚:默认不覆盖用户自己填的值,只填空字段或上一次选择填进去的值;只有声明了 `overwrite: true`
才覆盖。没有声明该键的查找字段行为不变。网格里从「浏览全部」弹窗选记录现在也会走同名字段复制(之前只写 id)。回滚即 revert 本 PR。
-
先看:`ui/packages/fields/src/widgets/lookupAutofill.ts`(填充规则,唯一一处),`ui/packages/fields/src/widgets/LookupField.tsx`
的 `fillDeclaredTargets` 与
`handleDialogSelect`,`ui/packages/fields/src/widgets/GridField.tsx` 的
`applyLookupSelection`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx
packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx`,预期
2 个文件 17 passed。
- 需要你做的:无,席位落地。
## What changes
`FieldSchema.lookupAutofill` (`[{ target, source, overwrite? }]` on a
single-value `lookup` / `master_detail`, declared by #28244) gets its
reader in the record form and in the line-item grid.
- **One fill rule** —
`ui/packages/fields/src/widgets/lookupAutofill.ts#declaredLookupAutofillPatch`.
Each `target` takes the picked record's `source`. Without `overwrite:
true` a target is filled only while it is empty or still holds the value
the earlier pick filled in (the record the lookup held from an earlier
pick, read at the same `source`); a value the user typed, or one the
record already stored, stays. `overwrite: true` replaces on every pick.
A `source` the picked row does not carry (not served to this reader)
writes nothing. `lookupAutofillByField` reads an object's pairs by
lookup field name, single-value relationships only.
- **Record form** — `LookupField#fillDeclaredTargets` runs on every
single-value pick path the form owns: the dropdown, the keyboard,
quick-create and the browse-all dialog (the dialog commits its id
through `onChange`, then hands the row to `onSelectRecords`, where the
fill runs). Current values are the host's live record
(`dependentValues`); the earlier-pick memory is the widget's own
`pickedRecords`, so it is local to the form instance. The writer is a
new widget prop, `onAutofill(patch)`, declared on
`FieldWidgetComponentProps` and added to `toHostProps`' host-plumbing
list.
- **Form renderer** —
`ui/packages/components/src/renderers/form/form.tsx` hands `lookup` /
`master_detail` widgets `onAutofill`, which writes each target with
`form.setValue(..., { shouldDirty: true })` (re-validating a target
already in error), and passes it through `stripRegisteredFieldProps` for
those two types only. Nothing in the submit or save path changes.
- **Line-item grid** — `GridField` takes the child object's pairs
through a new host prop, `lookupAutofill` (an `InlineGridColumnSchema`
column is spec-strict and carries no field definition).
`MasterDetailForm` keeps them on each resolved entry beside `sortField`;
`LineItemsPanel` derives them from the child schema it already loads.
`lookupAutofillPatch` applies a declared pair whatever the column's
`autofill` says and takes its target out of the same-named copy, so a
target has one writer per pick. The earlier-pick memory is a per-grid
map keyed by column and the row's own lookup value, so adding, removing
or reordering rows cannot misattribute it.
- **Browse-all dialog in the grid** — `LookupField#handleDialogSelect`
holds back the dialog's bare-id `onSelect` when the host owns the pick
(`onSelectRecord`), and `handleDialogSelectRecords` hands the host the
record once, so the grid's autofill runs on that path too. Clearing and
multi-value picks commit through `onChange` as before. The PeoplePicker
path is unchanged.
- **Types** — `LookupFieldMetadata.lookupAutofill` is typed
`SpecField['lookupAutofill']`.
- **Liveness** — `packages/spec/liveness/field.json` `lookupAutofill`
(and its three children) moves `planned` → `live`, citing the read
points above and the two test files.
- **Changeset** — `.changeset/28245-lookup-autofill-fill.md`, `patch` on
the four `@object-ui/*` packages and `@objectstack/spec` (the ledger
ships in its `files[]`).
## Tests
- `ui/packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx`
— end to end through `ObjectForm` (schema with the pairs → form renderer
→ `LookupField`): a pick fills an empty target and the filled value is
saved; a typed value is kept while an `overwrite: true` pair replaces
its own; a value an earlier pick filled is replaced by the next pick and
kept once the user edits it; a browse-all dialog pick fills; control: a
lookup without the key fills nothing, not even a same-named field.
-
`ui/packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx`
— the pure patch (empty target from the declared source rather than the
same-named field; a written value kept because the declared target
leaves the same-named copy; `overwrite: true`; earlier pick replaced;
applies under `autofill: false`; control: no pair = the same-named copy,
unchanged) and the grid itself (a cell pick fills the row and the next
pick replaces it; a browse-all pick reaches the grid once and fills; a
stored line's own price is kept).
- **Ablation** (one-shot, at `0d887457`, `node
scripts/ablation-replace.mjs`): the write in
`declaredLookupAutofillPatch` (`patch[target] = picked[source]`)
replaced by a no-op → **10 failed | 7 passed** of the 17 (every fill
case red; the keep and control cases stay green, as they should);
restored, blob equal to HEAD, `git diff HEAD` empty.
- **Reverse type check**: a `lookupAutofill={{ product: [{ target:
'unit_price' }] }}` grid prop in `@object-ui/plugin-form`, compiled
against the rebuilt `@object-ui/fields` declarations → `TS2741: Property
'source' is missing`; file removed.
## Acceptance notes
- **Surface extension, agreed with the seat before editing.** The
order's surface named `ui/packages/plugin-form/**` as the form renderer,
but the record form's state lives in
`ui/packages/components/src/renderers/form/form.tsx` (react-hook-form),
and no widget could write a sibling field. That file carries only the
writer prop and its `stripRegisteredFieldProps` pass-through.
`ui/packages/types/src/field-types.ts` carries only the by-reference
`lookupAutofill` declaration.
- **Bound: a fully authored master-detail collection.**
`MasterDetailForm` skips the child-schema fetch for an entry whose
relationship and every column are authored, so such a grid sees no pairs
and fills only the same-named copy. Loading the child schema for every
such entry is a fetch-policy change outside this card. Derived, hydrated
and `LineItemsPanel` grids are covered.
- **Earlier-pick memory is per widget / grid instance.** A remount
forgets it, which errs toward keeping the target's value, never toward
overwriting it.
- **A target the caller may not edit is not saved.** Read in
`sanitizeFormData` (`stripUnwritable` with `canEdit`): the form's save
drops a field the caller may not edit, so a fill into such a target
never reaches the server. The fill does not check field-level security
itself.
- **Behaviour change on an existing path:** in a line-item grid, a pick
from the browse-all dialog used to write only the id; it now runs the
column's same-named copy as the dropdown always has.
- **#28246 (skills/docs) must merge after this PR, not with it.** It
edits `skills/**` (governed, Tier H), so riding along would make this PR
Tier H; its card already carries `Blocked-by: #28245`. Its text should
also state the fully authored master-detail bound above.
## Verification
All readings at head `12601c27d` unless a step names another commit.
- **New tests** — `pnpm --dir ui exec vitest run
packages/fields/src/widgets/GridField.lookupAutofill-28245.test.tsx
packages/plugin-form/src/ObjectForm.lookupAutofill-28245.test.tsx` →
Test Files 2 passed (2), Tests 17 passed (17).
- **Touched packages' suites** — `pnpm --dir ui exec vitest run
packages/fields/ packages/plugin-form/
packages/components/src/renderers/form/ packages/types/` (under
`os-verify-lock`, at `0d887457`; later commits touch only the liveness
ledger and changeset) → Test Files 973 passed | 1 skipped (974), Tests
17791 passed | 25 skipped (17816), VERDICT command-exit 0.
- **Type-check** — `pnpm --filter @object-ui/types --filter
@object-ui/fields --filter @object-ui/components --filter
@object-ui/plugin-form run type-check` against the rebuilt
`@object-ui/plugin-form^...` closure (full declarations) → VERDICT
command-exit 0.
- **Clause-② measure** — on the built declarations:
`ui/packages/fields/dist/widgets/types.d.ts` carries `onAutofill`,
`dist/widgets/GridField.d.ts` the `lookupAutofill` prop and the fourth
`lookupAutofillPatch` argument, `dist/widgets/lookupAutofill.d.ts` the
new exports; `ui/packages/types/dist/field-types.d.ts` carries
`lookupAutofill`. Additions only.
- **Lint** — `eslint --no-inline-config` over the 12 changed `ui/`
source and test files → 0 errors.
- **Derived gates** — `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`, every line run with stdin closed,
exit codes landed on disk first, reconciled with `--ran`: `70 derived
famil(ies) accounted for — 69 run, 1 NOT-MEASURED`, exit 0. Every run
family exits 0, including `check:liveness`, `check:generated`,
`check:sdui-manifest` (+ `--self-test`), `check:type-check-debt`,
`check:test-source-alias`, `check:cross-package-test-inputs` and `pnpm
--dir ui exec vitest run scripts/`.
- `check:liveness` first went red on the generated
`liveness/state-counts/field.md` shard (planned 9 → 6, live 100 → 103);
regenerated with `gen:liveness-counts` in `12601c27d`, then green.
- `check-plugin-teardown-shape --self-test` first answered exit 3 on the
shallow clone; after fetching its pinned fixture commit, 48 cases pass,
exit 0.
- NOT MEASURED: `check:dual-build-cjs-loads`, exit 3 (PREREQUISITE NOT
MET — it needs every package's `dist`, a full `pnpm build`); the diff
touches no `@objectstack/*` package entry. CI runs it.
- `pnpm --dir ui run check:new-line-citations` → 0 new citations, exit
0.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibzEPqxBYQbxn2d72r5)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5da3a8c commit ea1751b
17 files changed
Lines changed: 685 additions & 26 deletions
File tree
- .changeset
- packages/spec/liveness
- state-counts
- ui
- examples/schema-catalog/test
- packages
- components/src/renderers/form
- fields/src
- widgets
- plugin-form/src
- plugin-grid/src
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
382 | 382 | | |
383 | 383 | | |
384 | 384 | | |
385 | | - | |
386 | | - | |
| 385 | + | |
| 386 | + | |
387 | 387 | | |
388 | | - | |
389 | | - | |
| 388 | + | |
| 389 | + | |
390 | 390 | | |
391 | 391 | | |
392 | | - | |
393 | | - | |
394 | | - | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
395 | 395 | | |
396 | 396 | | |
397 | | - | |
398 | | - | |
399 | | - | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
400 | 400 | | |
401 | 401 | | |
402 | | - | |
403 | | - | |
404 | | - | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
326 | 326 | | |
327 | 327 | | |
328 | 328 | | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
329 | 334 | | |
330 | 335 | | |
331 | 336 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1246 | 1246 | | |
1247 | 1247 | | |
1248 | 1248 | | |
| 1249 | + | |
1249 | 1250 | | |
1250 | 1251 | | |
1251 | 1252 | | |
| |||
1286 | 1287 | | |
1287 | 1288 | | |
1288 | 1289 | | |
| 1290 | + | |
| 1291 | + | |
| 1292 | + | |
1289 | 1293 | | |
1290 | 1294 | | |
1291 | 1295 | | |
| 1296 | + | |
| 1297 | + | |
| 1298 | + | |
1292 | 1299 | | |
1293 | 1300 | | |
1294 | 1301 | | |
| |||
3904 | 3911 | | |
3905 | 3912 | | |
3906 | 3913 | | |
| 3914 | + | |
| 3915 | + | |
| 3916 | + | |
| 3917 | + | |
| 3918 | + | |
| 3919 | + | |
| 3920 | + | |
| 3921 | + | |
| 3922 | + | |
| 3923 | + | |
| 3924 | + | |
| 3925 | + | |
3907 | 3926 | | |
3908 | 3927 | | |
3909 | 3928 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5316 | 5316 | | |
5317 | 5317 | | |
5318 | 5318 | | |
| 5319 | + | |
5319 | 5320 | | |
5320 | 5321 | | |
5321 | 5322 | | |
| |||
0 commit comments