Skip to content

Commit 534a287

Browse files
feat(console): the sharing-rule object picker lists only shareable objects, business objects first (#31274)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XBy2QatVq72fKr8HLn4oAbE)_ Fixes #31217 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:新建共享规则时,「对象」下拉框改为只列「规则存得进、也能生效」的对象,业务对象在前、平台对象在后,顺序照服务器 `GET /api/v1/sharing/shareable-objects` 给的原样显示。新增字段控件 `shareable-object-ref`,`sys_sharing_rule.object_name` 的控件提示从 `object-ref` 改为它(同一个 PR 注册、同一个 PR 切换)。 - 为什么改:原来下拉框列出全部 86 个对象,多数是 `sys_*`,还包括存了会被 `SHARING_NOT_ENABLED` 拒绝的对象和会话、凭据类对象;#30209 第 5 行方案 B 的控制台半边,服务器半边 PR #31207 已合并。 - 风险与回滚:接口答 403(不是共享规则管理员)、501(本部署没跑共享规则)或任何读不出列表的情况,字段退回原来的 `object-ref` 全量列表,不会出现空下拉框;保存时服务器仍是最终裁决。已存的规则对象不在列表里时仍保留为选项,编辑不会丢值。列表格子和只读面沿用 `object-ref` 的对象名标签。回滚即 revert。 - 先看:`ui/packages/fields/src/widgets/ShareableObjectRefField.tsx`(读接口与退回逻辑),`ui/packages/plugin-form/src/__tests__/objectFormShareableObjectRef-31217.test.tsx`(在新建表单上点选、保存、403/501 退回),`packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.object.ts`(控件提示)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-form/src/__tests__/objectFormShareableObjectRef-31217.test.tsx`,预期 `Tests 5 passed (5)`。 - 需要你做的:无,席位落地。 ## What changes - **The widget** (`@object-ui/fields`, new `widgets/ShareableObjectRefField.tsx`, registered as `field:shareable-object-ref`). It reads `GET /api/v1/sharing/shareable-objects` through the host's authenticated fetch (`SchemaRendererContext.apiFetch`, the route the `notification-topic` picker reads its door by) and offers `data.objects` in the order the door sends: business objects, then platform objects, each by label. Options use `objectRefOptionLabel`, the `Label (name)` spelling `object-ref` uses, and a pick writes the object's machine name. A stored name the list does not hold is kept as its own leading option, so editing a rule on an object that no longer takes one never drops the value. No new translation key: the loading, placeholder, search and empty strings are `object-ref`'s. - **The fallback.** When the door does not answer a readable list, the field renders `ObjectRefField` unchanged: every registered object from `dataSource.getObjects()`. That covers the two states the card names, `403 PERMISSION_DENIED` (not a sharing-rule administrator) and `501 NOT_IMPLEMENTED` (sharing rules not running), and also any other non-2xx, a body that is not the door's shape, a network failure, and a host with no `apiFetch`. Never an empty picker. The read-only face is also `object-ref`'s, so a record page does not call an admin-only door. - **The declaration** (`@objectstack/plugin-sharing`). `sys_sharing_rule.object_name` declares `widget: 'shareable-object-ref'`, flipped in the PR that registers the widget. The `by_object` pin moves with it, and the census of fields that store an object API name (`sys-sharing-rule.by-object.test.ts`) counts the new widget beside `object-ref`, so the no-grouping rule still reaches this field. - **The other faces of the key**, each the row `object-ref` already has: the list cell promotes to the same object label face (`WIDGET_CELL_RENDERER_TYPES`, the default text face in `@object-ui/fields` and the console's `ObjectRefCell` in `@object-ui/app-shell`); the record form threads `dataSource` to it (`DATA_SOURCE_ONLY_WIDGET_TYPES` in `@object-ui/components`, which the fallback reads); it is not edited inline in a grid (`FieldEditWidget`); it is a widget-hint-only string in `paramValueShape`; and `field:shareable-object-ref` joins the known schema types of `@object-ui/cli`. - **Registry censuses** that enumerate every registered widget gain the row: `widget-aria-invalid-registry-e2e`, `widget-dom-leak-e2e`, `cellRenderers.objectLiteral-8596`, `summaryChip.badgeFitCensus-8464` (fit side 41 to 42), `inlineEditTypeCoverage`, and the `className` readers in `form-field-channel-census-12130`. - **Changeset** `.changeset/31217-shareable-object-picker.md`, patch on the five shipping packages, both declaration lines. ### Declaration lines, as measured - `Clause-②: yes (widening)`. The claim started at `no`; the diff adds a registered widget key, which widens the published `RegisteredFieldWidgetType` union of `@object-ui/fields` and the known schema types of `@object-ui/cli` (the precedent is `.changeset/31062-notification-topic-channel-pickers.md`). No translation key is added, removed or reworded; no export is added (the widget is reached only through the lazy registry, not the package barrel); nothing is narrowed. - `Metadata-protocol: none`. The spec's `FieldSchema.widget` is an open string, so no authorable key or accepted value changes. The flip changes the value one server object declares, which only picks the console control. ## Tests - New `objectFormShareableObjectRef-31217.test.tsx` drives `ObjectForm` in create mode inside `SchemaRendererProvider` (as the console mounts it), opening the real combobox and submitting the real form: (1) the door's list in its order and nothing else (no `sys_session`, no `sys_api_key`, no object the door leaves out), the registry list never read; (2) CONTROL: a shareable object is selectable and the form saves `object_name: 'crm_opportunity'`; (3) a stored name the door does not list stays an option; (4, 5) a 403 and a 501 give the `object-ref` list of every registered object, and a pick from it saves. - Ablations on the final head, each through `scripts/ablation-replace.mjs` (anchor hit, restore proved by blob equality and an empty `git diff HEAD`): ignoring the door's list (`objects: null`) reds tests 1 and 3; answering a refused door with an empty list (`return []`) reds tests 4 and 5. ## Acceptance notes - The door withholds localization (its header: "the list is the same in every language"), so the picker shows the labels the server declares. `object-ref`'s list (`GET /meta/object`) carries declared labels too, so no language loses a translation here. - A failure other than 403/501 also falls back rather than showing a notice. The save doors stay the authority (`SHARING_NOT_ENABLED`), so the wider list costs a refusal at save, never a wrong rule, and adding a notice would add translation keys this card did not ask for. ## Verification Head `309f44a0`; every count below is read off the tool's own verdict line. - `pnpm --dir ui exec vitest run` over `packages/fields/`, `packages/plugin-form/`, `packages/components/src/renderers/form/`, `packages/app-shell/src/views/widget-ref-cells/`, `packages/app-shell/src/utils/`, `packages/cli/src/__tests__/` and the census files above (under `os-verify-lock`): `Test Files 696 passed | 2 skipped`, with 6 census files red on the first run, each the missing row for the new key; after adding the rows the 7 touched files pass, `Tests 456 passed (456)`, re-run on `309f44a0`. - `pnpm --filter @objectstack/plugin-sharing test`: `Test Files 70 passed (70)`, `Tests 1464 passed (1464)`. - `type-check` for `@object-ui/fields`, `@object-ui/components`, `@object-ui/plugin-form`, `@object-ui/plugin-detail`, `@object-ui/app-shell`, and `pnpm --filter @objectstack/plugin-sharing typecheck`: all exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `309f44a0`: 67 derived families, all 67 run with exit 0 (codes captured before any pipe); `--ran`: "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED". The branch merges cleanly onto `origin/main` `abc7a940` (`git merge-tree`). `pnpm check:new-line-citations` (ui): 0 new citations. --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5ntiCZev2cDL6r5VLw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebc2cdc commit 534a287

17 files changed

Lines changed: 379 additions & 13 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/fields': patch
3+
'@object-ui/components': patch
4+
'@object-ui/app-shell': patch
5+
'@object-ui/cli': patch
6+
'@objectstack/plugin-sharing': patch
7+
---
8+
9+
feat(console): the sharing-rule object picker lists only the objects a rule can take effect on, business objects first
10+
11+
Clause-②: yes (widening) — `field:shareable-object-ref` joins `@object-ui/fields`' registered widget keys (`RegisteredFieldWidgetType`) and the console's known schema types; nothing is narrowed and no translation key is added.
12+
Metadata-protocol: none
13+
14+
The object field of a new sharing rule listed every registered object, most of them platform `sys_*` objects, including ones a save refuses with `SHARING_NOT_ENABLED` and the credential and session objects. It now lists only what `GET /api/v1/sharing/shareable-objects` answers: the objects a rule saved on would take effect on, business objects first and then platform objects, each group by label, in the order the server sends.
15+
16+
- **The widget (`@object-ui/fields`).** `field:shareable-object-ref` reads the list through the host's authenticated fetch (`SchemaRendererContext.apiFetch`) and stores the object's name, as `object-ref` does, with the same `Label (name)` spelling. A stored name the list does not hold stays on screen as its own option, so editing a rule never drops it. The read-only face is `object-ref`'s.
17+
- **When the list is not served.** A `403` (the caller is not a sharing-rule administrator), a `501` (sharing rules do not run on this deployment), any other failed or unreadable answer, and a host with no authenticated fetch all give the `object-ref` picker over every registered object, as before. The field is never an empty picker; the save doors still decide which object a rule may name.
18+
- **The declaration (`@objectstack/plugin-sharing`).** `sys_sharing_rule.object_name` declares `widget: 'shareable-object-ref'` (was `object-ref`). A console without the widget draws the field as a text box, as with any unregistered widget.
19+
- **The list cell, the record form and the grid.** The new key draws the same object label cell as `object-ref` (`@object-ui/app-shell`), the record form hands it the data source its fallback reads (`@object-ui/components`), and it is not edited inline in a grid cell, like the other widget-hint pickers.

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import * as SharingObjects from './index.js';
1717
* `sys_record_share.by_object`, where the field is a pointer pair's type half.
1818
*
1919
* A field stores an object's API name when it declares the `object-ref`
20-
* widget (the object picker) or when it is the `referenceVia` sibling of a
20+
* widget (the object picker) or `shareable-object-ref` (the sharing-rule object
21+
* picker, the same stored name) or when it is the `referenceVia` sibling of a
2122
* pointer pair. Enumerated over every object this package ships
2223
* (`objects/index.ts`), so a view added later is judged without anyone
2324
* listing it here.
@@ -40,7 +41,7 @@ describe('sharing list views — no grouping on a field that stores an object AP
4041
function apiNameFields(object: ObjectDef): string[] {
4142
const names = new Set<string>();
4243
for (const [name, def] of Object.entries(object.fields)) {
43-
if (def?.widget === 'object-ref') names.add(name);
44+
if (def?.widget === 'object-ref' || def?.widget === 'shareable-object-ref') names.add(name);
4445
if (typeof def?.referenceVia === 'string') names.add(def.referenceVia);
4546
}
4647
return [...names].sort();
@@ -77,6 +78,6 @@ describe('sharing list views — no grouping on a field that stores an object AP
7778
expect(view?.sort?.[0]).toEqual({ field: 'object_name', order: 'asc' });
7879
expect(view?.columns?.[0]).toBe('object_name');
7980
const fields = SysSharingRule.fields as Record<string, { widget?: unknown }>;
80-
expect(fields.object_name.widget).toBe('object-ref');
81+
expect(fields.object_name.widget).toBe('shareable-object-ref');
8182
});
8283
});

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const SysSharingRule = ObjectSchema.create({
7171
columns: ['object_name', 'label', 'recipient_type', 'access_level', 'active'],
7272
// [#24701] Ordered by the target object, not grouped on it: a group header
7373
// draws `object_name` as its stored API name, while the Object cell draws
74-
// the object's icon and label (its `object-ref` widget). No column stores
74+
// the object's icon and label (its object-picker widget). No column stores
7575
// the label, so no grouping key could carry it. The sort keeps one
7676
// object's rules together (`sys-sharing-rule.by-object.test.ts`).
7777
sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],
@@ -132,10 +132,12 @@ export const SysSharingRule = ObjectSchema.create({
132132
label: 'Object',
133133
required: true,
134134
maxLength: 100,
135-
// Rendered as an object picker (choose a registered object by name)
136-
// instead of a free-text machine-name input. Falls back to a text input
137-
// when the `field:object-ref` widget is unavailable.
138-
widget: 'object-ref',
135+
// Rendered as the sharing-rule object picker: only the objects a rule
136+
// can take effect on (`GET /api/v1/sharing/shareable-objects`, the save
137+
// doors' own verdict), business objects first. It is the `object-ref`
138+
// picker when that door does not answer the list, and a text input when
139+
// the `field:shareable-object-ref` widget is unavailable.
140+
widget: 'shareable-object-ref',
139141
description: 'The object whose records this rule shares.',
140142
group: 'Target',
141143
}),

‎ui/examples/schema-catalog/test/form-field-channel-census-12130.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ const DECLARED_BY_12130: Readonly<Record<string, readonly string[]>> = {
368368
// recorder sees. The effects file measures where it lands, type by type.
369369
className: [
370370
'checkboxes', 'currency', 'filter-condition', 'multiselect', 'object-ref', 'password',
371-
'percent', 'radio', 'rating', 'recipient-picker', 'tags',
371+
'percent', 'radio', 'rating', 'recipient-picker', 'shareable-object-ref', 'tags',
372372
],
373373
};
374374

‎ui/packages/app-shell/src/utils/paramValueShape.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export const PARAM_VALUE_SHAPES: Readonly<Record<string, ParamValueShapeSpec>> =
175175
// Widget-hint-only pickers — reached via a field `widget:` override, never a
176176
// bare param `type`. Present for total FORM_FIELD_TYPES coverage only.
177177
'object-ref': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Object name string. Widget-hint only — not a declarable param type.' },
178+
'shareable-object-ref': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Object name string (an object a sharing rule can take effect on). Widget-hint only — not a declarable param type.' },
178179
'filter-condition': { base: 'object', cardinality: 'scalar', widgetHintOnly: true, note: 'Filter/criteria object. Widget-hint only — not a declarable param type.' },
179180
'recipient-picker': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Recipient id string. Widget-hint only — not a declarable param type.' },
180181
'position-ref': { base: 'string', cardinality: 'scalar', widgetHintOnly: true, note: 'Position name string. Widget-hint only — not a declarable param type.' },

‎ui/packages/app-shell/src/views/widget-ref-cells/widget-ref-cell-renderers.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,6 @@ export function RecipientCell(props: CellRendererProps) {
4747
}
4848

4949
registerFieldRenderer('object-ref', ObjectRefCell);
50+
// The sharing-rule object picker stores an object name too: the same face.
51+
registerFieldRenderer('shareable-object-ref', ObjectRefCell);
5052
registerFieldRenderer('recipient-picker', RecipientCell);

‎ui/packages/cli/src/utils/known-schema-types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ export const KNOWN_SCHEMA_TYPES: readonly string[] = [
163163
'field:recipient-picker',
164164
'field:richtext',
165165
'field:select',
166+
'field:shareable-object-ref',
166167
'field:signature',
167168
'field:slider',
168169
'field:summary',

‎ui/packages/components/src/renderers/form/form.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,9 @@ function reportUnrecognizedRulesOnce(fieldName: string, unrecognized: string[]):
740740
*/
741741
const DATA_SOURCE_ONLY_WIDGET_TYPES = new Set([
742742
'object-ref', 'filter-condition', 'recipient-picker',
743+
// The sharing-rule object picker is `object-ref` when its door does not
744+
// answer, and that list is read through `dataSource.getObjects`.
745+
'shareable-object-ref',
743746
// objectui#7611: the registry-name pickers read their catalog through
744747
// `dataSource.listMetadataItems`.
745748
'position-ref', 'permission-ref',

‎ui/packages/fields/src/FieldEditWidget.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export const INLINE_EXCLUDED_FIELD_TYPES = new Set<string>([
188188
'object', 'grid', 'vector',
189189
// Widget-hint-only pickers — authored in the record form (they depend on
190190
// sibling fields / a loaded object catalog), not inline in a grid cell.
191-
'object-ref', 'filter-condition', 'recipient-picker',
191+
'object-ref', 'shareable-object-ref', 'filter-condition', 'recipient-picker',
192192
'position-ref', 'permission-ref',
193193
'notification-topic', 'notification-channel',
194194
]);

‎ui/packages/fields/src/__tests__/cellRenderers.objectLiteral-8596.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ const OBJECT_LITERAL_CENSUS: ReadonlyArray<readonly [type: string, text: string,
297297
// the text cell's face for it, exactly as `text` does.
298298
['object-ref', '[Object]', false],
299299
['recipient-picker', '[Object]', false],
300+
// objectstack#31217: the sharing-rule object picker's key, the same default
301+
// text face as `object-ref`.
302+
['shareable-object-ref', '[Object]', false],
300303
// ── objectstack#27861: the registry-name read faces ─────────────────────
301304
// `{}` names no item, so nothing is asked and both draw the text cell's
302305
// face for it, exactly as `text` does.
@@ -364,6 +367,7 @@ const POPULATED_CENSUS: ReadonlyArray<readonly [type: string, value: unknown]> =
364367
['grid', [[1]]],
365368
['record_pointer_card', { objectName: 'invoice', recordId: 'inv_1' }],
366369
['object-ref', 'crm_account'],
370+
['shareable-object-ref', 'crm_account'],
367371
['recipient-picker', 'sales_manager'],
368372
['position-ref', 'sales_manager'],
369373
['permission-ref', 'admin_full_access'],

0 commit comments

Comments
 (0)