Skip to content

Commit 75a5c3e

Browse files
fix(plugin-list): the sort picker leaves out a field the object marks hidden, as the filter list does (#30384)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg7b913SptrRMtzTBi127R7U)_ domain:ui#2 · os-bill · session_01Utp6Cb5R6JDdfyvtj3dPR2 ## 维护者速读 - **改了什么:** 列表的「排序」选择器不再列出对象定义里标了 `hidden: true` 的字段(队列、未翻译的 Search Index、组织等系统列),与「筛选」面板的字段列表同一条规则;视图或已存排序本来就按这类字段排的,那一行照旧显示,标为不可选、可删除。 - **为什么改:** 销售添加排序时会看到「队列」和英文的 "Search Index",对他们毫无意义,按搜索索引排序得到的顺序也无从解释;筛选面板早已把这些字段藏起,只有排序漏了。 - **风险与回滚:** 只改 Console 前端的排序字段列表,不改存储形状、不加元数据键;服务端排序能力不变。已按隐藏字段排序的视图照常加载和排序。回滚即还原本 PR。 - **先看:** `ui/packages/plugin-list/src/ListView.tsx`(新的 `isDeclaredHidden` 与 `sortFields` 循环开头的 `offered` 判定)。 - **两分钟验证:** `pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx`,预期 6 个用例全绿,其中 `hidden:` 一条钉住隐藏字段只在当前排序点名时以不可选形式出现。 - **需要你做的:** 无,席位落地。 Fixes #30037 Clause-②: no Metadata-protocol: none ## What changes `ui/packages/plugin-list/src/ListView.tsx`, one rule shared by three field lists: - **`isDeclaredHidden(defs, field)`**, a module-level predicate beside `canReadField`: the object definition marks the field `hidden: true`. The Filter panel's list (`filterFields`) and the hide-fields lists' addable fields (`addableFields`) already asked this question inline; both now call the predicate, unchanged in behaviour. - **The Sort picker (`sortFields`) asks it too.** A field is offered only when the user may read it and the definition does not mark it hidden. A hidden field the current sort already names is kept by the existing in-use exception, listed removable only (`disabled`), exactly as an unreadable field is: its row shows its label and can be removed, no other row and no "Add sort" can choose it. A hidden lookup (the platform's `organization_id`) no longer raises the relational hint under the rows, since the picker never offered it; a visible lookup still does. - The memo's dependency list gains `objectDef`, which the loop now reads. What is persisted and requested is unchanged: a hidden field is sortable on the server, so a stored sort naming one keeps being sent and saved (the #6455 persist boundary filters platform-refused entries only). ## Acceptance The card's list, verbatim, each item pinned: > 1. The sort picker leaves out a field declared `hidden`, unless the current sort already names it. That row stays visible and removable, as today's `inUse` path does for other fields. Pinned by `ListView.sortRemovableOnly-11943.test.tsx`: `FIELD_DEFS` gains a hidden `search_index` ("Search Index", standing in for `__search` and `queue`), so every case's exact option list now asserts it is absent; the new `hidden:` case declares it first and sorts by it, and asserts it is listed disabled, its row shows it, "Add sort" skips it, the new row offers it only as unavailable, and removing its row drops it from the list. > 2. The hide-fields, group and row-color lists are read for the same gap, and any that share it are fixed here, with the list of what was read in the PR. Read on this head, no gap found, nothing changed in them: - **Hide-fields popover and View settings section, column list:** `allFields`, built from the view's declared `columns` only. A hidden field appears there only when the view's author declares it as a column, and then the grid draws it (`effectiveFields` drops unreadable and user-hidden columns, not definition-hidden ones), so listing it is consistent with the grid. - **Hide-fields lists, fields a reader may add:** `addableFields` already leaves out a definition-hidden field; it now asks the shared predicate. - **Group editor** (`GroupingEditor fieldOptions`) and **Row color select**: both read `allFields`, the same declared-columns list as above. > 3. A test pins the sort picker. Item 1's pins, plus `ListView.sortFieldRead-11943.test.tsx` (new top-level `describe`): a hidden lookup is not offered and raises no relational hint, while the same lookup unhidden raises it. ## Acceptance notes The card's "To check while here" names two Studio lists. Both live in `ui/packages/app-shell/**`, outside this order's file surface, so they were read, not measured in a running Studio and not changed: - **Formula 插入字段** (`ObjectFieldInspector`, the `fieldOptions` it hands `CelPredicateField`): every entry of the object draft's field list except the field itself, with no hidden check. Whether the draft carries the injected `queue` / `__search` columns was not measured here. A formula may legitimately read some hidden columns (`queue`, `owner`), so this is an implementer's judgement, not a mechanical copy of this rule. - **Validation 参与字段** (`ObjectValidationsPanel`, the `cross_field` checkbox list): every field of the list it is handed. The panel already reads `hidden` (its new-rule default skips hidden fields) but the checkbox list does not filter on it. ## Tests Red first, on `origin/main` code with the new pins (`ListView.tsx` restored to its base, commit `91cec45d`): Test Files 2 failed (2), Tests 6 failed, 6 passed (12). Each failure reads "Search Index" listed (the exact option lists) or the relational hint drawn for the hidden lookup. ## Verification All readings on objectstack-ai/objectstack. Final head `26c80600` (after merging `origin/main` at `fdad1786`); the full-package run was on `c3b69a91`, before the merge, whose six incoming commits touch no `ui/` path. <details><summary>Tests</summary> - Green after the fix, final head: `pnpm --dir ui exec vitest run --maxWorkers=2` on `ListView.sortRemovableOnly-11943`, `ListView.sortFieldRead-11943`, `ListView.filterFieldList-11810`, `ListView.fieldListRead-11984`: Test Files 4 passed (4), Tests 50 passed (50); lock VERDICT command-exit 0. - Every importer of `ListView.tsx` (the whole `plugin-list` package, 145 files import it): `pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-list/` on `c3b69a91`: Test Files 189 passed (189), Tests 1813 passed (1813); lock VERDICT command-exit 0. - `pnpm exec turbo run type-check --filter=@object-ui/plugin-list` (`tsc --noEmit && tsc -p tsconfig.test.json`), final head: Tasks 53 successful, 53 total. The first run, before `26c80600`, was red on TS6133 (an unused `def` binding this PR left in `addableFields`), fixed by that commit. - `pnpm --filter @object-ui/plugin-list lint` exit 0 (0 errors), and root eslint `--no-inline-config` on the three touched files exit 0. </details> <details><summary>Gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on `26c80600` derived 35 commands; all 35 run, each exit 0 (among them `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run scripts/`: Test Files 170 passed, 2 skipped (172); Tests 4985 passed, 2 skipped). `--ran` reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Also `pnpm check:changeset-fixed` exit 0, `pnpm --dir ui check:new-line-citations` 0 new citations, `node scripts/check-nul-bytes.mjs` exit 0. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CY1ikAq74XRbNdsyUC)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f18adce commit 75a5c3e

4 files changed

Lines changed: 68 additions & 11 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-list': patch
3+
---
4+
5+
fix(plugin-list): the list's sort picker no longer offers a field the object marks hidden, such as the queue column or the search index
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
**Visible behaviour change on a released page.** A list's 排序 / Sort picker offered every field of the object, including the injected system columns the platform marks `hidden: true` (the queue column, the `__search` companion shown as an untranslated "Search Index", `organization_id`). The Filter panel's field list already left those out; the sort picker now follows the same rule.
11+
12+
- A field the object definition marks `hidden: true` is no longer listed in the sort picker, and "Add sort" never starts on one.
13+
- A view or saved sort that already orders by such a field keeps working: the field stays listed in that row as removable only (disabled), as the picker already does for an unreadable or refused field, so the row is not blank and can be removed.
14+
- A hidden lookup (such as `organization_id`) no longer raises the "relational fields cannot be sorted" hint under the rows, since the picker never offered it.

‎ui/packages/plugin-list/src/ListView.tsx‎

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,22 @@ function canReadField(
16901690
return !perms?.isLoaded || !objectName || perms.checkField(objectName, field, 'read');
16911691
}
16921692

1693+
/**
1694+
* Whether the object definition marks a field `hidden: true`: the injected
1695+
* system columns `@objectstack/spec` hides from pickers (`organization_id`,
1696+
* `queue`, the `__search` companion). The toolbar's field lists ask this one
1697+
* predicate. The Filter panel's list and the Sort picker's list leave such a
1698+
* field out unless the current filter or sort already names it, so that row
1699+
* is not blank and can be removed; the hide-fields lists never offer one to
1700+
* add. A plain function of the definition's field map, like `canReadField`.
1701+
*/
1702+
function isDeclaredHidden(
1703+
defs: Record<string, { hidden?: unknown } | undefined> | undefined,
1704+
field: string,
1705+
): boolean {
1706+
return defs?.[field]?.hidden === true;
1707+
}
1708+
16931709
/** The item a value none of a picker's options carries is shown by. */
16941710
const OUTSIDE_OPTIONS = 'outside';
16951711

@@ -5173,7 +5189,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
51735189
*/
51745190
const filterFields = React.useMemo(() => {
51755191
const defs: Record<string, { hidden?: unknown; system?: boolean } | undefined> | undefined = objectDef?.fields;
5176-
const isHidden = (name: string) => defs?.[name]?.hidden === true;
5192+
const isHidden = (name: string) => isDeclaredHidden(defs, name);
51775193
const held = new Set((currentFilters.conditions ?? []).map((c) => c.field));
51785194
const columnRank = new Map<string, number>();
51795195
for (const column of effectiveFields) {
@@ -5257,14 +5273,19 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
52575273
// field the current sort already names (a stored or URL sort) stays listed,
52585274
// so its row is not blank and can be removed.
52595275
//
5276+
// A field the object definition marks `hidden: true` (`isDeclaredHidden`,
5277+
// the rule the Filter panel's list follows) is withheld the same way as an
5278+
// unreadable one: not offered, no relational hint, and kept by the in-use
5279+
// exception alone.
5280+
//
52605281
// An entry the exception alone keeps is listed REMOVABLE ONLY
52615282
// (objectui#11943): it carries `disabled`, which `SortBuilder` renders as an
52625283
// unavailable option. Its own row still shows its label and can be changed
52635284
// to another field or removed, but no row's dropdown offers it as a choice
52645285
// and "Add sort" never seeds it. That holds for each reason the exception
5265-
// keeps a field: unreadable, relational, or refused by the platform (or by
5266-
// the type read when no projection is served). A field the two rules list
5267-
// anyway carries no flag, whether or not the sort names it.
5286+
// keeps a field: unreadable, hidden, relational, or refused by the platform
5287+
// (or by the type read when no projection is served). A field the two rules
5288+
// list anyway carries no flag, whether or not the sort names it.
52685289
//
52695290
// Both legs below — the list this picker renders, and the sort it emits for
52705291
// a host to persist — read `platformSortability`, the ONE read of the served
@@ -5279,16 +5300,17 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
52795300
// stored ID: said in one line under the rows, only while it is true.
52805301
let picked = false;
52815302
const fields: Array<{ value: string; label: string; disabled?: boolean; type?: string }> = [];
5303+
const defs: Record<string, { hidden?: unknown } | undefined> | undefined = objectDef?.fields;
52825304
for (const field of candidateFields) {
5283-
const readable = canReadField(perms, schema.objectName, field.value);
5284-
if (!readable && !inUse.has(field.value)) continue;
5305+
const offered = canReadField(perms, schema.objectName, field.value) && !isDeclaredHidden(defs, field.value);
5306+
if (!offered && !inUse.has(field.value)) continue;
52855307
const relational = EXPANDABLE_FIELD_TYPES.has(field.type);
52865308
const platformSortable = platformSortability
52875309
? isPlatformSortableField(platformSortability, field.value)
52885310
: !UNMATERIALIZED_FIELD_TYPES.has(field.type);
52895311
// `type` lets the picker name a date's direction as time
52905312
// (objectstack#27520): "Oldest → Newest", not "A → Z".
5291-
if (readable && !relational && platformSortable) {
5313+
if (offered && !relational && platformSortable) {
52925314
fields.push({ value: field.value, label: field.label, type: field.type });
52935315
continue;
52945316
}
@@ -5306,7 +5328,7 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
53065328
if (relational) excluded = true;
53075329
}
53085330
return { sortFields: fields, sortHasRelationalField: excluded, sortPicksRelationalField: picked };
5309-
}, [candidateFields, currentSort, t, platformSortability, perms, schema.objectName]);
5331+
}, [candidateFields, currentSort, t, platformSortability, perms, schema.objectName, objectDef]);
53105332

53115333
/**
53125334
* [#6455] THE persist boundary: what this picker LISTS is not what it
@@ -5669,8 +5691,8 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
56695691
if (onAddedFieldsChange && objectDef?.fields) {
56705692
const defs = objectDef.fields as Record<string, { label?: string; hidden?: unknown; system?: boolean } | undefined>;
56715693
const listed = new Set(allFields.map((f) => f.name));
5672-
const candidates = Object.entries(defs).filter(([name, def]) =>
5673-
!listed.has(name) && def?.hidden !== true && canReadField(perms, schema.objectName, name));
5694+
const candidates = Object.entries(defs).filter(([name]) =>
5695+
!listed.has(name) && !isDeclaredHidden(defs, name) && canReadField(perms, schema.objectName, name));
56745696
for (const tier of [false, true]) {
56755697
for (const [name, def] of candidates) {
56765698
if (isSystemManagedField(name, def) !== tier) continue;

‎ui/packages/plugin-list/src/__tests__/ListView.sortFieldRead-11943.test.tsx‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,19 @@ describe('the Sort picker field list asks the column read check (objectui#11943)
222222
expect(screen.getByTestId('sort-relational-info')).toBeInTheDocument();
223223
});
224224
});
225+
226+
describe('a lookup the definition marks hidden does not raise the relational hint (objectstack#30037)', () => {
227+
it('withheld like an unreadable lookup: not offered and no hint; a visible lookup still raises it', async () => {
228+
// `organization_id` is the platform's hidden lookup: the picker never
229+
// offers it, so the hint has nothing to explain.
230+
const hiddenLookup = { ...FIELDS, org: { type: 'lookup', label: 'Organization', reference: 'sys_org', hidden: true } };
231+
mount({ perms: FULL_READ, fields: hiddenLookup });
232+
expect(await openSortOptions()).toEqual(FULL_READ_LABELS);
233+
expect(screen.queryByTestId('sort-relational-info')).toBeNull();
234+
cleanup();
235+
const visibleLookup = { ...FIELDS, org: { type: 'lookup', label: 'Organization', reference: 'sys_org' } };
236+
mount({ perms: FULL_READ, fields: visibleLookup });
237+
await openSortOptions();
238+
expect(screen.getByTestId('sort-relational-info')).toBeInTheDocument();
239+
});
240+
});

‎ui/packages/plugin-list/src/__tests__/ListView.sortRemovableOnly-11943.test.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
* - UNREADABLE: field-level read denies it (`canReadField`, objectui#11943).
1919
* - PLATFORM-REFUSED: the served projection refuses to order by it (#6455).
2020
* - RELATIONAL: a lookup, listed as ordering by ID (objectui#4243).
21+
* - HIDDEN: the object definition marks it `hidden: true` (objectstack#30037),
22+
* the rule the Filter panel's list already follows. `search_index` stands
23+
* in for the platform's `__search` companion and its `queue` column.
2124
*
2225
* In each case the field is declared FIRST on the object, so an "Add sort" that
2326
* still seeded the first entry would seed it. The controls: a field the picker
@@ -44,6 +47,7 @@ const FIELD_DEFS: Record<string, Record<string, unknown>> = {
4447
owner: { type: 'lookup', label: 'Owner', reference: 'sys_user' },
4548
title: { type: 'text', label: 'Title' },
4649
priority: { type: 'number', label: 'Priority' },
50+
search_index: { type: 'text', label: 'Search Index', hidden: true },
4751
};
4852

4953
/** The object definition with `first` declared first, the rest in `FIELD_DEFS` order. */
@@ -74,7 +78,7 @@ function attachProjection(def: ReturnType<typeof definition>) {
7478
const resolved = resolveObjectSortability(def) as { fields: Record<string, unknown> };
7579
const fields: Record<string, unknown> = { ...resolved.fields };
7680
// The base the refusal is measured against: every other field is sortable.
77-
for (const name of ['secret_note', 'owner', 'title', 'priority']) {
81+
for (const name of ['secret_note', 'owner', 'title', 'priority', 'search_index']) {
7882
expect(fields[name]).toEqual({ sortable: true });
7983
}
8084
fields.remote_status = { sortable: false };
@@ -175,6 +179,7 @@ const CASES = [
175179
{ name: 'unreadable', field: 'secret_note', label: 'Secret Note' },
176180
{ name: 'platform-refused', field: 'remote_status', label: 'Remote Status' },
177181
{ name: 'relational', field: 'owner', label: 'Owner (by ID)' },
182+
{ name: 'hidden', field: 'search_index', label: 'Search Index' },
178183
] as const;
179184

180185
describe('a field the Sort picker keeps only for the current sort is removable only (objectui#11943)', () => {

0 commit comments

Comments
 (0)