Skip to content

Commit 4be0868

Browse files
huangyiireneclaude
andauthored
fix(plugin-sharing): recipient_id declares object_name, the second sibling its picker reads (#19505)
Fixes #19258 Clause-②: no `sys_sharing_rule.recipient_id` declared `dependsOn: ['recipient_type']` while its `recipient-picker` widget reads **two** siblings. It now declares both. ## The three readings, taken first-hand on today's head (`4045781fa`, worktree base) | # | reading | value | |:--|:--|:--| | 1 | `recipient_id.dependsOn` before this PR — `packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts:197` | `['recipient_type']` | | 2 | **control, same file**, `criteria_json.dependsOn` at line 156, for its `filter-condition` widget | `['object_name']` | | 3 | the widget's sibling reads — objectui `packages/fields/src/widgets/RecipientPickerField.tsx:150,152` | `dependentValues.recipient_type` **and** `dependentValues.object_name` | Reading 2 is the judgement: the key is live and used correctly a few lines up, for the very same `object_name` dependency, by a widget in the same form. So this was an **omission**, not a key nobody uses. Reading 3 was taken in objectui at `befd40cc` and re-taken at this repo's pinned `.objectui-sha` `87af769e` — the picker reads `object_name` at the pin too, so the dependency is live in the console this repo actually ships. ## Why nothing was broken today, and why that is the point objectui `packages/components/src/renderers/form/form.tsx:3004` passes `dependentValues: ruleRecord` — the **whole** watched form record, not a `dependsOn`-scoped slice. That is what masked the under-declaration. A renderer that ever scoped it — which is exactly what this key asks for — would drop the object name and degrade the `field` recipient mode to a plain text input **in silence**: no error, no empty state, just an admin typing a column name by hand again. The declaration is the only thing that survives that change, so it is what gets fixed. ## The second half of the fix: the docblock parenthesis, now verified unblocked The `recipient_id` docblock said the picker "has no mapping for that kind and degrades to its text input". That was conditional on objectui#10049, so it was **checked rather than assumed**: - objectui#10049 landed as commit `23b99585`, 2026-09-20, `feat(fields): give RecipientPickerField a picker mode for the field sharing recipient (objectui#7613)`. - `git merge-base --is-ancestor 23b99585 87af769e` exits **0** — this repo's pinned console **includes** it. (Exit 0 is self-proving on a shallow checkout; the control leg, an older known-ancestor commit, also exits 0.) So the parenthesis is false for the console this repo ships, and the docblock now says what the picker actually does: it offers the shared object's user-valued columns, using a "holds users" predicate that is a clause-for-clause copy of this plugin's own `fieldHoldsUsers`. ## Tests `packages/plugins/plugin-sharing/src/field-recipient.test.ts` gains three pins in the existing authoring-seams block (which already asserts declaration facts about this same field): - `recipient_id.dependsOn` contains `recipient_type`; - `recipient_id.dependsOn` contains `object_name`; - control: `criteria_json.dependsOn` still contains `object_name`. **Reverse verification** — predicted direction: red, and sharply. The fix was committed first (`5e794538b`), then `dependsOn` was reverted on disk to `['recipient_type']` through `scripts/ablation-replace.mjs`, which proved the mutation landed by anchor counts and blob hash (`3b86f89cc43e` to `158df1894eb5`) before running anything: ``` grep count "dependsOn: ['recipient_type']" -> 1 (mutation on disk) grep count "dependsOn: ['recipient_type', 'object_name']" -> 0 Tests 1 failed | 2 passed | 57 skipped (60) FAIL ... names `object_name` — the sibling the `field` mode reads for its candidate columns ``` Exactly one pin failed — the `object_name` one. The `recipient_type` pin and the `criteria_json` control stayed green, so the new pin is sharp rather than tautological. Restore verified the way a restore has to be: blob back to `3b86f89cc43e` == HEAD, and `git diff HEAD` empty. Other readings, exit codes captured before any pipe: | run | verdict | |:--|:--| | `pnpm --filter @objectstack/plugin-sharing exec vitest run src/field-recipient.test.ts` | exit 0 — 60 passed | | `pnpm --filter @objectstack/plugin-sharing build && ... typecheck` | exit 0 (test layer compiles; debt ledger held) | | `pnpm --filter '@objectstack/plugin-sharing^...' build` (dependency closure) | exit 0 | | `pnpm lint` (repo-wide, `eslint . --no-inline-config`) | exit 0 — no narrowing claimed | | `scripts/pm/dispatch-gates.mjs --commands` derived families, all 62 run | 60 exit 0 | | `pnpm check:i18n` | exit 0 after building its declared 10-package prerequisite closure — 9 packages in sync | | `pnpm check:dual-build-cjs-loads`, `pnpm check:type-check-debt` | **NOT MEASURED** — both exit **3**, PREREQUISITE NOT MET (each needs a whole-repo `dist`, which is CI's build). Not a red and not a green. | `dispatch-gates --ran` with those exit codes reconciles: 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN. ## Changeset `patch` on `@objectstack/plugin-sharing`. Measured rather than assumed: after `pnpm --filter @objectstack/plugin-sharing build`, the changed declaration is present in the published `files[]` output — `dist/index.js` carries `dependsOn: ["recipient_type", "object_name"]`, with `recipient-picker` as the positive control (1 hit). So the publish surface moves and `skip-changeset` does not apply. ## Acceptance notes - **Same-shape sweep, negative result.** `widget` is live in this repo on the `sys_sharing_rule` trio plus `sys_permission_set`'s `permission-facet-link`. Of the others: `object_name`'s `object-ref` widget reads no siblings (`ObjectRefField.tsx` contains no `dependentValues` at all), `criteria_json`'s `filter-condition` widget reads exactly the `object_name` it already declares, and `permission-facet-link` reads no siblings either. So `recipient_id` was the only instance of this defect — nothing else to file. - No gate or test in this repo pinned `dependsOn` on this object before this PR; the three added pins are the first, and they sit in the file that already owns this field's declaration assertions rather than in a new verification surface. - Nothing in `packages/spec` is touched, no renderer behaviour is changed, and no `dependentValues` plumbing is changed — this PR moves a declaration and the comment that describes it. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f34dda6 commit 4be0868

3 files changed

Lines changed: 59 additions & 9 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-sharing": patch
3+
---
4+
5+
`sys_sharing_rule.recipient_id` now declares `dependsOn: ['recipient_type', 'object_name']` — every sibling field its `recipient-picker` widget actually reads.
6+
7+
The picker reads two siblings, not one: `recipient_type` picks the mode (a record picker over `sys_user` / `sys_team` / `sys_business_unit` / `sys_position`), and for the `field` recipient kind (#15072) it reads `object_name` to offer that object's user-valued columns. The declaration named only the first. The neighbouring `criteria_json` field already declares `dependsOn: ['object_name']` for its own `filter-condition` widget, so the key is live and correctly used a few lines up — the omission was an omission.
8+
9+
Nothing was broken at runtime: the form renderer hands widgets the WHOLE watched record as `dependentValues` rather than a `dependsOn`-scoped slice, which masked the under-declaration. A renderer that ever scoped it — which is exactly what this key asks for — would drop the object name and degrade the `field` recipient mode to a plain text input **in silence**, with no error anywhere. This is the declaration catching up with what is read, so the scoping change can never be the one that breaks it.
10+
11+
The same commit corrects the `recipient_id` docblock: the picker no longer "has no mapping for that kind and degrades to its text input" — the pinned console (`.objectui-sha` 87af769e, which includes objectui#10049 / commit 23b99585) offers the shared object's user-valued columns for the `field` kind, using a "holds users" predicate that is a clause-for-clause copy of this plugin's own `fieldHoldsUsers`.
12+
13+
Authors and stored rows are unaffected: no key is added, removed or renamed, no value is newly accepted or refused, and no wire byte moves.

‎packages/plugins/plugin-sharing/src/field-recipient.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,33 @@ describe('#15072 authoring seams', () => {
816816
const help = String((SysSharingRule as any).fields.recipient_id.description);
817817
expect(help).toMatch(/field/i);
818818
});
819+
820+
/**
821+
* [#19258] `dependsOn` is the declaration of WHICH siblings the widget
822+
* reads, and `recipient-picker` reads two: `recipient_type` picks the mode,
823+
* and the `field` mode reads `object_name` to offer that object's
824+
* user-valued columns. Nothing breaks today only because the form renderer
825+
* hands widgets the whole watched record rather than a `dependsOn`-scoped
826+
* slice — so a scoped renderer would degrade `field` mode to a plain text
827+
* input with no error anywhere. These pin the declaration, which is the
828+
* only thing that would survive that change.
829+
*/
830+
describe('[#19258] `recipient_id.dependsOn` names every sibling the picker reads', () => {
831+
const dependsOn = (field: string): string[] =>
832+
((SysSharingRule as any).fields[field].dependsOn as string[]) ?? [];
833+
834+
it('names `recipient_type` — the sibling that picks the picker mode', () => {
835+
expect(dependsOn('recipient_id')).toContain('recipient_type');
836+
});
837+
838+
it('names `object_name` — the sibling the `field` mode reads for its candidate columns', () => {
839+
expect(dependsOn('recipient_id')).toContain('object_name');
840+
});
841+
842+
it('control: `criteria_json` declares the same `object_name` dependency for its own widget', () => {
843+
expect(dependsOn('criteria_json')).toContain('object_name');
844+
});
845+
});
819846
});
820847
});
821848

‎packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,26 @@ export const SysSharingRule = ObjectSchema.create({
185185
label: 'Recipient',
186186
required: true,
187187
maxLength: 200,
188-
// Rendered as a record picker whose target object follows recipient_type
189-
// (dependsOn: recipient_type): sys_user / sys_team / sys_business_unit /
190-
// sys_position. Stores the value the evaluator matches on — a record id
191-
// for user/team/business_unit, the position NAME for `position`, and for
192-
// `field` (#15072) the NAME of a user-typed field of the shared object
193-
// (the picker has no mapping for that kind and degrades to its text
194-
// input, which is the right input for a field name). Falls back to a
195-
// text input when the widget is unavailable.
188+
// Rendered as a record picker whose target object follows recipient_type:
189+
// sys_user / sys_team / sys_business_unit / sys_position. Stores the
190+
// value the evaluator matches on — a record id for user/team/
191+
// business_unit, the position NAME for `position`, and for `field`
192+
// (#15072) the NAME of a user-typed field of the shared object, which the
193+
// picker offers from the object named in the sibling `object_name`: its
194+
// "holds users" predicate is a clause-for-clause copy of this plugin's
195+
// own `fieldHoldsUsers`. Falls back to a text input when the widget is
196+
// unavailable.
197+
//
198+
// `dependsOn` names BOTH siblings the widget reads — `recipient_type`
199+
// picks the mode, and in `field` mode `object_name` decides whose columns
200+
// are offered, the same dependency `criteria_json` declares a few lines
201+
// up. Declaring only the first was masked by the form renderer handing
202+
// widgets the WHOLE watched record instead of a `dependsOn`-scoped slice;
203+
// a renderer that ever scoped it — which is what this key asks for —
204+
// would drop the object name and degrade `field` mode to a plain text
205+
// input in silence.
196206
widget: 'recipient-picker',
197-
dependsOn: ['recipient_type'],
207+
dependsOn: ['recipient_type', 'object_name'],
198208
description: 'The specific user, team, business unit or position that receives access — or, for the "Field" recipient type, the name of the record field that holds the user or users to share with.',
199209
group: 'Recipient',
200210
}),

0 commit comments

Comments
 (0)