fix(components): a dependent lookup clears when its dependsOn parent changes or clears (objectui#11631) - #11644
Merged
objectstack-fleet[bot] merged 5 commits intoOct 5, 2026
Conversation
…changes or clears (objectui#11631) The form's cascade clear (#2284) tests a value against a static option set, so it never covered the reference family: a lookup scoped by `dependsOn` re-scoped its candidate query when the parent moved, but kept the selection already made, and the form saved an invoice whose contact belongs to a different account. A form.watch subscription now clears a dependent lookup (null, or [] for a multi-value lookup) when any parent that scopes it takes a different value. The parents are read from the same slot LookupField scopes by. A defaultValues reset (record landing, recordId swap, resetOnSubmit, cancel) re-bases the comparison instead of clearing, so a saved pair survives the record opening. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…ot a parent switch (objectui#11631) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…p clear (objectui#11631) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…est probe without any (objectui#11631) No behaviour change: the metadata carrier is read as Record<string, unknown> and the test probe declares its props. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11631-lookup-cascade-clear
branch
October 5, 2026 06:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11631
Clause-②: no
What changed
A lookup whose field-level
dependsOnnames a sibling field now drops its selection when that sibling changes or is cleared. Before, the picker re-scoped its candidate query to the new parent but kept the record already picked. So the form saved an invoice whose contact belongs to a different account, and the server, which checks only that a reference exists, accepted it.@object-ui/components,renderers/form/form.tsx:dependentLookupParents(module scope, not exported) answers which sibling fields scope a lookup. It reads the same slotLookupFieldscopes its query by: thefieldprop this renderer hands the widget (field.field || field), one nestedfieldunwrapped when it carriesreferenceortype, and only the array shape the spec declares at field level. The family isEXPANDABLE_FIELD_TYPES, the setneedsDataSourceWiringalready feedsdependentValuesto.CASCADE_OPTION_WIDGET_TYPESis untouched.form.watchsubscription (keyed on the primitivedependentLookupsKey, established only when the form has a dependent lookup) clears a lookup when any parent takes a different value. It writesnull, or[]for a multi-value lookup, the same sentinels the two existing clears write, so an edit overwrites the stored value instead of leaving it. A lookup holding nothing is never written to. A cleared lookup that scopes another lookup clears that one in the same pass. The field whose change started a pass is never cleared by it, so two lookups that name each other cannot wipe the pick the user just made.defaultValuesreset (a record landing after first paint, arecordIdswap in a mounted drawer, the input the reset carries across, read insideresetInFlightRef), and a notification that names no field (the bareform.reset()thatresetOnSubmitand Cancel call). Those re-base the comparison and clear nothing, so an existing record opens with its saved pair intact.The
Clause-②declaration holds: no export, prop, type member or i18n key is added to any@object-ui/*package.resolveDependsOnFieldswas already exported by@object-ui/coreand is only imported here.Landing site:
form.tsx, beside the reset, not inside the cascade-clear effectThe card names the cascade-clear effect. That effect is the right file but the wrong mechanism for a lookup, measured both ways:
CASCADE_OPTION_WIDGET_TYPESand every field with no staticoptions. A lookup fails both checks (A0 below: all seven clear rows red on the unchanged file).ruleRecord, and it validates a value against an option set. A lookup has no option set to test, so its rule has to be a transition (the parent moved). A passive effect after the commit sees two value sets and no record of which operation moved them. ThedefaultValuesreset that fills a parent and its lookup together runs inside the synchronousresetInFlightRefwindow, and only a subscriber notified inside that window can tell it from an edit. That is the same signal theonAction/onChangevalue channels in this file already read, so the new subscription sits beside them. The option effect's comment now names it.Measured
Unit pins (
form-dependent-lookup-clear-11631.test.tsx, 14 rows)Clear rows: create, parent switched; edit, parent emptied (explicit
nullsurvivesJSON.stringify); hand-written field carryingdependsOnitself; multi-value lookup to[]; two-level chain; two lookups naming each other.Keep rows: edit with the pair present at mount; record landing after first paint (then a real switch still clears);
recordIdswap; input carried across a defaults reset;resetOnSubmit; unrelated field change; empty lookup not written to; a bare-namedependsOnthe picker does not scope by.Reverse verification and ablations (fix committed first; every leg restored from HEAD and proved by blob hash and empty
git diff HEAD)Direction predicted before running: the clear rows go red in the STALE direction and the keep rows stay green; each guard ablation reds exactly the row that pins it. Observed exactly that:
form.tsxat the pre-fix baseprospect-18,b1or the two-id list) where an empty value was expectedresetInFlightRefre-baseresetOnSubmitdependsOnMutations went through
ablation-replace.mjs(anchor hit 1 to 0, blob changed); the run sits on the report comment.Live, card repro: before and after
Backend: objectstack
mainat75ddcd1b, showcase app, built in a private worktree,--freshdatabase on a private port. "Before" is the console from this branch's basec4c506b; "after" is the console from this branch. Same backend, same browser script (Chromium, headless)./meta/object/showcase_invoiceservescontactwithdependsOn: ["account"](field level). The contact candidate query under Northwind isfilter=["account","=",NORTHWIND_ID].accountis Northwind."contact": null; read back:contactnull.{"tax_rate":5}, and the read-back keeps the pair."contact": nulland the read-back is null.Other form paths: not changed here
Every host that draws a
type: 'form'schema shares this renderer:ObjectFormand its drawer, modal, split, tabbed and wizard variants in@object-ui/plugin-form. Two other paths keep the defect, by reading:apps/console/src/components/FormPage.tsx(the form-view page,/forms/:nameand/f/:slug) is a second renderer with its ownvaluesstate. It feeds lookupsdependentValuesbut never clears one. Not measured live: the only dependent lookup in the examples isshowcase_invoice.contact, and no form view renders it.packages/app-shell/src/views/ActionParamDialog.tsx: a lookup parameter withdependsOnis scoped by the dialog's values, andupdateValuewrites only the changed parameter. Not measured live.Acceptance notes
dependsOnon a lookup (the form-viewFormField.dependsOnthat@object-ui/plugin-formcopies onto the runtime field's top level) does not scopeLookupFieldwhen object metadata is stashed, so it does not drive this clear either. The clear follows the picker's scope by construction, and the boundary row pins the form side of that.Gates
On the final head
fea2341(after the merge ofmain):pnpm --filter @object-ui/components type-check(its test project lists the new file),pnpm --filter @object-ui/components lint(0 errors, no warning in the added hunks),pnpm exec vitest run packages/components/src/renderers/form/, the A0 to A6 legs above, and the root checkscheck:new-line-citations,check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:changeset-claims,check:pending-changeset-literals,check:shell-escape-residue,check:handler-key-reads,check:i18n-keys,check:unreferenced-sources,check-changeset-presence.mjsandcheck-changeset-no-major.mjs.Declared narrowing: the whole-package
pnpm exec vitest run packages/components/ran green ata3e25ff. After it came a types-only commit (the metadata carrier read as a record of unknown values instead ofany, the probe's props declared) and the merge, whose diff touches only@object-ui/app-shelland a changeset. The final head reran the form directory instead. Repo-widepnpm lintand the fullpnpm testare CI's.Changeset:
.changeset/11631-lookup-cascade-clear.md,@object-ui/componentspatch.Implemented in session
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL(dispatched by thedomain:uiseat 1 claim on the card).Generated by Claude Code