… recipient
`sys_sharing_rule.recipient_type` carries a sixth value, `field` — the
record-relative recipient (maintainer ruling objectstack#14103, executor
objectstack#15072). Its `recipient_id` stores a field NAME, not a record
id, so the kind has no row in `TYPE_TO_OBJECT` and fell through to the
plain text input the header documents for unknown types: the stored value
was already correct, but the admin had to know and type the machine name
of the column by hand, with no list and no signal that the name existed.
The widget now reads the shared object from the sibling `object_name`
(the same dependency the `filter-condition` widget already reads), loads
its schema through `dataSource.getObjectSchema`, and offers that object's
user-valued columns, storing the column's name.
The offered set is exactly the set the evaluator honours: the `user`
type, or a `lookup` / `master_detail` whose `reference` is `sys_user`.
Anything else the evaluator reads as "grants nobody" plus one warning per
rule, so a wider picker would produce a rule that looks configured and
authorises nobody. `hidden` is deliberately not filtered (the evaluator
honours a hidden user column) and `reference_to` is deliberately not read
(the protocol refuses that spelling by name, and so does the evaluator).
Three states the mode names instead of leaving to be inferred: it asks
for the shared object while `object_name` is unset; an object with no
user columns says so rather than rendering a search-flavoured "No
matches"; and a stored name that is not on the offered list stays visible
and is marked, because the evaluator grants nobody for it.
Unknown types keep degrading to the text input, and so does `field`
itself when the data source cannot enumerate columns.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HrVaotisyhgmot9o2MLRq
Fixes #7613
Clause-②: no
sys_sharing_rule.recipient_typecarries a sixth value,field— the record-relative recipient (maintainer ruling objectstack#14103, executor objectstack#15072). Itsrecipient_idstores a field NAME (assignees,owner_manager) and not a record id, so the kind has no row inTYPE_TO_OBJECTand fell through to the plain text input the widget header documents for unknown types. The value that input stored was already the right one; what was missing was any list of the object's user-valued columns, and any signal that a hand-typed name exists.When
recipient_typeisfieldthe widget now reads the shared object from the siblingobject_name— the same dependency thefilter-conditionwidget already reads — loads that object's schema throughdataSource.getObjectSchema, and offers its user-valued columns, storing the column's name.The one hard agreement — checked at the runtime, not taken from the card
The card's filter and the runtime's judgement agree, and this was verified against the executor's source in the
objectstackcheckout rather than assumed.packages/plugins/plugin-sharing/src/sharing-rule-service.tsdeclares afieldHoldsUsershelper whose rule is, in words: a declared field holds users when itstypeisuser, or when itstypeislookupormaster_detailand itsreferenceissys_user. Its callerfieldRecipientColumnIsUsableanswersfalsefor anything else and emits the one-per-rule warning that begins "active field-recipient rule grants NOBODY". That is the card's sentence, word for word in behaviour.So the picker's predicate is the same predicate, and two corollaries follow from the agreement rather than from taste:
hiddenis deliberately NOT filtered, unlike the criteria builder's own field derivation. The evaluator honours a hidden user column exactly like a visible one, so withholding it would break the agreement in the other direction — an authorable, working configuration the picker refuses to offer.reference_tois deliberately NOT read. The protocol refuses that spelling by name (objectui#6837) and the evaluator readsreferenceonly, so alookupdeclaringreference_to: sys_useris not a user column on either side.Three states the mode names out loud
object_namestill unset: it asks for the shared object instead of rendering a list it cannot fill.Unknown recipient types keep degrading to the plain text input, and so does
fielditself when the data source cannot enumerate columns (nogetObjectSchema): a hand-typed name is worse than a list, and better than a list that can never fill.Why
dependentValues.object_nameis actually reachableChecked, because the card asserts it and the upstream metadata does not obviously grant it:
sys-sharing-rule.object.tsdeclaresrecipient_idwithdependsOn: ['recipient_type']and does not nameobject_name. That turns out not to matter for this widget: the form renderer passesdependentValues: ruleRecord, andruleRecordis the WHOLE watched form record (every declared field seeded, the persisted record under it, liveform.watch()values over the top) rather than adependsOn-scoped slice, anddependsOnitself is stripped from the widget's props. Soobject_nameis present and live today. Widening the upstream declaration is still worth doing for honesty, and is named in the acceptance notes below rather than done here.i18n
Three keys, added to the provider-less defaults table in
packages/fieldsand to all ten packs inpackages/i18n:fields.recipient.selectField,fields.recipient.noUserFields,fields.recipient.fieldNotUserTyped(one hole, spelled the one way the provider-less fallback resolves).The "select an object first" gate reuses the criteria builder's existing sentence rather than adding a
fields.recipient.*twin. It is the same sentence, in the same role, on the same form, gating on the same sibling field, and this repo's own defaults table records the measured reason not to twin such a sentence — two copies of one deliberately shared sentence are how they come to read differently in a locale. Flagged here because it is a judgement call a reviewer may want to reverse; reversing it is three more rows in ten packs and nothing else.Tests — red before, green after
New:
packages/fields/src/widgets/__tests__/RecipientPickerField.fieldRecipient-7613.test.tsx, 13 pins. The schema fixture carries three columns the evaluator honours and four it refuses in one object, so every zero in the file travels with a same-subject control lit by the same command.Three ablations, each run through
ablation-replaceso the mutation is proven on disk (anchor count, replacement count and thegit hash-objectblob all move) and the restore is proven againstHEAD(blob equality plus an emptygit diff HEAD):d.reference === 'sys_user'replaced)a90a0e95f380tofac0c726efb2if (canListObjectFields)gated off)a90a0e95f380to51830498522de86445f57a90a0e95f380to062ac5b30875, equal to the base blobThe 13th pin in that last row is green on base by construction and is reported as such rather than glossed: it is the "degrades to the plain text input when the data source cannot list columns" pin, i.e. the pre-existing behaviour this change preserves. Every ablation restored to the committed blob
a90a0e95f380withgit diff HEADempty.Commands and exit codes
All measurements taken after the last edit, on
d547d4217. Heavy runs went through the shared verify lock in the sibling checkout.pnpm exec vitest run packages/fields/ packages/i18n/— 241 files, 4102 tests passedpnpm --filter @object-ui/fields --filter @object-ui/i18n run type-checkpnpm --filter @object-ui/fields --filter @object-ui/i18n run lint— 0 errorspnpm check:i18n-keyspnpm check:i18n-drift— "0 en value(s) changed (3 key(s) added, 0 removed)"pnpm check:i18n-dead-keyspnpm check:control-bytespnpm check:new-line-citations— 0 new citationsnode scripts/check-changeset-presence.mjs— 12 source files, 1 changesetnode scripts/check-changeset-no-major.mjsnode scripts/check-governed-queue-guard.mjs --test ...— NOT GOVERNED, 14 pathsEvery exit code was captured by redirecting to a file first and reading the status before any pipe.
Declared narrowing: the repo-wide
turbo run lint/turbo run type-check/ whole-suitepnpm testruns were not taken locally; they are CI's. What was taken locally is the two affected packages' own lint, type-check and full test suites, plus the gate scripts above.@object-ui/fields' published surface gains two exports (fieldHoldsUsers,deriveUserFields, both marked internal-for-tests in the same style the siblingFilterConditionFieldalready uses) and loses none, so no importer's behaviour changes.Acceptance notes — out of scope, not fixed here
sys-sharing-rule.object.tsdeclaresrecipient_idwithdependsOn: ['recipient_type']only. The widget readsobject_nametoo, and gets it because the renderer hands over the whole watched record rather than a scoped slice. The declaration is therefore an honest-metadata gap, not a defect: it understates what the widget reads, and a future renderer that did scopedependentValuesbydependsOnwould break this mode silently. It belongs in theobjectstackrepo and is named as a card to file, not carried here.object_namewhilerecipient_typeisfieldleaves the stored column name in place. It is now visible and marked when it is not a user column of the new object, which is the safe half; auto-clearing it the way arecipient_typeswitch does was weighed and not taken, because the same widget's own history has a wipe-on-hydration regression (a stored recipient cleared the moment an existing rule was opened for editing) and a second value-clearing effect on a second dependency is the shape that caused it. Noted, not filed: the marking covers the authoring hazard, and the carrier for a change here would be this same widget.deriveFilterFieldsinFilterConditionFieldandderiveUserFieldshere both normalise the two shapes offields(array and name-keyed map) with the same eight lines. A shared helper is a reasonable cleanup for whichever PR next touches both; carrier today: none.Parallel work: objectui#9954 is in flight on
packages/app-shell/src/, disjoint from this file face. The dependency edge runsapp-shellto@object-ui/fieldsand not back, so if this lands first that card owes a re-verification and this one owes it nothing.This PR is left as a draft on purpose; the claiming seat lands it.
Session:
session_018HrVaotisyhgmot9o2MLRqGenerated by Claude Code