Skip to content

Commit 7fa022b

Browse files
committed
fix(plugin-sharing,plugin-webhooks): by_object lists sort by object instead of grouping on its API name
sys_sharing_rule.by_object and sys_webhook.by_object grouped on object_name, and the console heads a group with the stored value, so each group read an object API name. Both views keep their object_name sort, which holds one object's rows together, and their Object column, whose object-ref cell draws the object's label. A pin beside each object refuses a grouping on a field that stores an object API name. Claude-Session: https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s Co-authored-by: Claude <noreply@anthropic.com>
1 parent d20bf9b commit 7fa022b

5 files changed

Lines changed: 158 additions & 2 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+
'@objectstack/plugin-webhooks': patch
4+
---
5+
6+
fix(plugin-sharing, plugin-webhooks): the `by_object` lists of sharing rules and webhooks no longer head each group with an object API name
7+
8+
Clause-②: no
9+
10+
- **`sys_sharing_rule.by_object` and `sys_webhook.by_object` sort by object instead of grouping on it.** Both grouped on `object_name`, and a group header draws its value as stored text, so each group was headed by an API name such as `crm_account`. The sort on `object_name`, then `label`, still keeps one object's rows together, and the Object column still draws each object's icon and label.
11+
- **Unchanged:** `object_name` stays a declared field on both objects, with the same widget, and no stored column, index or query changes.
12+
13+
A patch: one list view key is removed on each of two shipped platform objects. No export, type, accepted value, stored column or authorable key changes.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { SysSharingRule } from './sys-sharing-rule.object.js';
5+
import * as SharingObjects from './index.js';
6+
7+
/**
8+
* [#24701] No sharing list view groups on a field whose stored value is an
9+
* object's API name.
10+
*
11+
* The console (objectui at this repository's `.objectui-sha`) heads each group
12+
* with the grouping value as text: its group formatter turns a select value
13+
* into its option label and a boolean into Yes / No, and draws anything else
14+
* as stored. An `object-ref` field's CELL draws the object's icon and label,
15+
* but its group header drew the API name (`crm_account`), so `by_object` read
16+
* one machine name per group. #24611 removed the same grouping from
17+
* `sys_record_share.by_object`, where the field is a pointer pair's type half.
18+
*
19+
* 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
21+
* pointer pair. Enumerated over every object this package ships
22+
* (`objects/index.ts`), so a view added later is judged without anyone
23+
* listing it here.
24+
*/
25+
describe('sharing list views — no grouping on a field that stores an object API name (#24701)', () => {
26+
type ViewDef = {
27+
columns?: ReadonlyArray<string | { field: string }>;
28+
sort?: ReadonlyArray<{ field: string; order?: string }>;
29+
grouping?: { fields?: ReadonlyArray<{ field: string }> };
30+
};
31+
type ObjectDef = {
32+
name: string;
33+
fields: Record<string, { widget?: unknown; referenceVia?: unknown } | undefined>;
34+
listViews?: Record<string, ViewDef>;
35+
};
36+
37+
const objects = Object.values(SharingObjects) as unknown as ObjectDef[];
38+
39+
/** The fields of `object` whose stored value is an object's API name. */
40+
function apiNameFields(object: ObjectDef): string[] {
41+
const names = new Set<string>();
42+
for (const [name, def] of Object.entries(object.fields)) {
43+
if (def?.widget === 'object-ref') names.add(name);
44+
if (typeof def?.referenceVia === 'string') names.add(def.referenceVia);
45+
}
46+
return [...names].sort();
47+
}
48+
49+
it('finds the API-name field on every sharing object, so the case below cannot pass on an empty set', () => {
50+
const found = Object.fromEntries(objects.map((o) => [o.name, apiNameFields(o)]));
51+
expect(found).toEqual({
52+
sys_record_share: ['object_name'],
53+
sys_sharing_rule: ['object_name'],
54+
sys_share_link: ['object_name'],
55+
});
56+
});
57+
58+
it('no list view groups on a field that stores an object API name', () => {
59+
const hits = objects.flatMap((o) => {
60+
const apiNames = new Set(apiNameFields(o));
61+
return Object.entries(o.listViews ?? {}).flatMap(([view, def]) =>
62+
(def.grouping?.fields ?? [])
63+
.filter((g) => apiNames.has(g.field))
64+
.map((g) => `${o.name}.${view}: ${g.field}`));
65+
});
66+
expect(hits).toEqual([]);
67+
});
68+
69+
it('control: sys_sharing_rule.by_object still keeps one object\'s rules together and shows the Object column', () => {
70+
const view = (SysSharingRule.listViews as Record<string, ViewDef> | undefined)?.by_object;
71+
expect(view?.sort?.[0]).toEqual({ field: 'object_name', order: 'asc' });
72+
expect(view?.columns?.[0]).toBe('object_name');
73+
const fields = SysSharingRule.fields as Record<string, { widget?: unknown }>;
74+
expect(fields.object_name.widget).toBe('object-ref');
75+
});
76+
});

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ export const SysSharingRule = ObjectSchema.create({
7070
label: 'By Object',
7171
data: { provider: 'object', object: 'sys_sharing_rule' },
7272
columns: ['object_name', 'label', 'recipient_type', 'access_level', 'active'],
73+
// [#24701] Ordered by the target object, not grouped on it: a group header
74+
// draws `object_name` as its stored API name, while the Object cell draws
75+
// the object's icon and label (its `object-ref` widget). No column stores
76+
// the label, so no grouping key could carry it. The sort keeps one
77+
// object's rules together (`sys-sharing-rule.by-object.test.ts`).
7378
sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],
74-
grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },
7579
pagination: { pageSize: 100 },
7680
},
7781
all_rules: {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import { SysWebhook } from './sys-webhook.object.js';
5+
6+
/**
7+
* [#24701] No `sys_webhook` list view groups on a field whose stored value is
8+
* an object's API name.
9+
*
10+
* The console (objectui at this repository's `.objectui-sha`) heads each group
11+
* with the grouping value as text: its group formatter turns a select value
12+
* into its option label and a boolean into Yes / No, and draws anything else
13+
* as stored. `object_name` declares the `object-ref` widget, so its CELL draws
14+
* the object's icon and label, but its group header drew the API name
15+
* (`crm_account`), so `by_object` read one machine name per group.
16+
*
17+
* A field stores an object's API name when it declares the `object-ref`
18+
* widget (the object picker) or when it is the `referenceVia` sibling of a
19+
* pointer pair. Enumerated over every view the object declares, so a view
20+
* added later is judged without anyone listing it here.
21+
*/
22+
describe('sys_webhook list views — no grouping on a field that stores an object API name (#24701)', () => {
23+
type ViewDef = {
24+
columns?: ReadonlyArray<string | { field: string }>;
25+
sort?: ReadonlyArray<{ field: string; order?: string }>;
26+
grouping?: { fields?: ReadonlyArray<{ field: string }> };
27+
};
28+
29+
const fields = SysWebhook.fields as Record<string, { widget?: unknown; referenceVia?: unknown } | undefined>;
30+
const views = (SysWebhook.listViews ?? {}) as Record<string, ViewDef>;
31+
32+
/** The fields whose stored value is an object's API name. */
33+
function apiNameFields(): string[] {
34+
const names = new Set<string>();
35+
for (const [name, def] of Object.entries(fields)) {
36+
if (def?.widget === 'object-ref') names.add(name);
37+
if (typeof def?.referenceVia === 'string') names.add(def.referenceVia);
38+
}
39+
return [...names].sort();
40+
}
41+
42+
it('finds the API-name field, so the case below cannot pass on an empty set', () => {
43+
expect(apiNameFields()).toEqual(['object_name']);
44+
});
45+
46+
it('no list view groups on a field that stores an object API name', () => {
47+
const apiNames = new Set(apiNameFields());
48+
const hits = Object.entries(views).flatMap(([view, def]) =>
49+
(def.grouping?.fields ?? [])
50+
.filter((g) => apiNames.has(g.field))
51+
.map((g) => `sys_webhook.${view}: ${g.field}`));
52+
expect(hits).toEqual([]);
53+
});
54+
55+
it('control: by_object still keeps one object\'s webhooks together and shows the Object column', () => {
56+
expect(views.by_object?.sort?.[0]).toEqual({ field: 'object_name', order: 'asc' });
57+
expect(views.by_object?.columns?.[0]).toBe('object_name');
58+
});
59+
});

‎packages/plugins/plugin-webhooks/src/sys-webhook.object.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,12 @@ export const SysWebhook = ObjectSchema.create({
8585
label: 'By Object',
8686
data: { provider: 'object', object: 'sys_webhook' },
8787
columns: ['object_name', 'label', 'url', 'active', 'updated_at'],
88+
// [#24701] Ordered by the target object, not grouped on it: a group header
89+
// draws `object_name` as its stored API name, while the Object cell draws
90+
// the object's icon and label (its `object-ref` widget). No column stores
91+
// the label, so no grouping key could carry it. The sort keeps one
92+
// object's webhooks together (`sys-webhook.by-object.test.ts`).
8893
sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],
89-
grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },
9094
pagination: { pageSize: 100 },
9195
},
9296
all_webhooks: {

0 commit comments

Comments
 (0)