Skip to content

Commit 3b2e4d9

Browse files
os-zhuangclaude
andauthored
fix(list): route remaining system-field groupings through shared classifier (#2706)
Follow-up to the owner_id default-column fix. Consolidate the display-oriented system-field exclusions onto the shared isSystemManagedField / SYSTEM_MANAGED_FIELD_NAMES helper so the framework-injected owner_id is treated consistently across the grid, record picker, and detail drawer: - ObjectGrid record-detail drawer (renderRecordDetail): split business fields vs. the muted meta section via the shared classifier, so owner_id and other injected system fields land in the meta section, not the business body. - deriveLookupColumns (record picker): drop the local SYSTEM_FIELDS name set for the shared classifier — now flag-aware (field.system), not just name-based. - RecordDetailDrawer: derive its default systemFields set from the shared SYSTEM_MANAGED_FIELD_NAMES; the systemFields prop override is preserved. deriveRelatedLists' narrow audit-FK set and plugin-detail's inline-edit never-editable set are intentionally left distinct (different semantics). Adds a flag-awareness test to deriveLookupColumns and a changeset. Claude-Session: https://claude.ai/code/session_01NMcdh2cNs165TSF4ub61oy Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7abe4cd commit 3b2e4d9

5 files changed

Lines changed: 56 additions & 26 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@object-ui/plugin-grid": patch
3+
"@object-ui/fields": patch
4+
"@object-ui/plugin-detail": patch
5+
---
6+
7+
fix(list): route remaining system-field groupings through the shared classifier
8+
9+
Follow-up to the `owner_id` default-column fix: consolidate the display-oriented
10+
system-field exclusions onto the shared `isSystemManagedField` /
11+
`SYSTEM_MANAGED_FIELD_NAMES` (from `@object-ui/types`) so the framework-injected
12+
`owner_id` is treated consistently across the grid, record picker, and detail
13+
drawer.
14+
15+
- `ObjectGrid` record-detail drawer: the business-fields vs. muted meta-section
16+
split now uses the shared classifier, so `owner_id` (and other injected system
17+
fields) land in the meta section instead of the business body.
18+
- `deriveLookupColumns` (record picker): drops its local name set for the shared
19+
classifier — now flag-aware (`field.system`), not just name-based.
20+
- `RecordDetailDrawer`: its default `systemFields` set is derived from the shared
21+
`SYSTEM_MANAGED_FIELD_NAMES`; the `systemFields` prop override is preserved.
22+
23+
`deriveRelatedLists`' narrow "audit FK on every object" set and plugin-detail's
24+
inline-edit "never editable" set are intentionally left distinct — different
25+
semantics (the latter deliberately keeps `owner_id` editable).

packages/fields/src/widgets/deriveLookupColumns.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ describe('deriveLookupColumns', () => {
5050
expect(deriveLookupColumns(accountSchema, { displayField: 'name', max: 2 })).toHaveLength(2);
5151
});
5252

53+
it('skips fields flagged `system: true` even when the name is not a known audit column', () => {
54+
const schema = {
55+
fields: {
56+
name: { type: 'text', label: 'Name' },
57+
// Non-canonical name, but stamped system by the framework — must be skipped.
58+
custom_ref: { type: 'lookup', label: 'Custom Ref', system: true } as any,
59+
code: { type: 'text', label: 'Code' },
60+
},
61+
};
62+
const cols = deriveLookupColumns(schema, { displayField: 'name', max: 10 });
63+
expect(cols.map((c) => c.field)).toEqual(['name', 'code']);
64+
});
65+
5366
it('honours an object-level displayFields list, with the display field first', () => {
5467
const schema = {
5568
fields: {

packages/fields/src/widgets/deriveLookupColumns.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
import type { LookupColumnDef } from '@object-ui/types';
2-
3-
/**
4-
* System / audit fields that carry no disambiguating value in a record picker.
5-
* Excluded from auto-derived columns so the picker shows business data, not
6-
* bookkeeping.
7-
*/
8-
const SYSTEM_FIELDS = new Set<string>([
9-
'id', '_id',
10-
'created', 'modified', 'created_at', 'updated_at',
11-
'created_by', 'updated_by', 'modified_by',
12-
'owner_id', 'organization_id', 'space', 'company_id',
13-
'instance_state', 'locked', 'is_deleted', 'deleted',
14-
]);
2+
import { isSystemManagedField } from '@object-ui/types';
153

164
/**
175
* Field types that don't render usefully as a compact picker column (large
@@ -31,7 +19,7 @@ export interface DeriveColumnsOptions {
3119
}
3220

3321
interface ObjectSchemaLike {
34-
fields?: Record<string, { type?: string; label?: string; hidden?: boolean }>;
22+
fields?: Record<string, { type?: string; label?: string; hidden?: boolean; system?: boolean }>;
3523
/**
3624
* ADR-0085 semantic role: the object's most important fields. The single
3725
* source for "how to list this object" — shared with the detail-page related
@@ -106,8 +94,10 @@ export function deriveLookupColumns(
10694
// 3. Derive from the field set in declaration order.
10795
const candidates = Object.keys(fields).filter((name) => {
10896
if (name === displayField) return false;
109-
if (SYSTEM_FIELDS.has(name)) return false;
11097
const f = fieldDef(name);
98+
// Skip framework-managed system/audit/ownership columns (incl. owner_id) —
99+
// they carry no disambiguating value in a record picker.
100+
if (isSystemManagedField(name, f)) return false;
111101
if (f.hidden) return false;
112102
if (f.type && NON_TABULAR_TYPES.has(f.type)) return false;
113103
return true;

packages/plugin-detail/src/RecordDetailDrawer.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
SheetTitle,
2929
} from '@object-ui/components';
3030
import type { DataSource } from '@object-ui/types';
31+
import { SYSTEM_MANAGED_FIELD_NAMES } from '@object-ui/types';
3132
import { InlineEditProvider } from '@object-ui/react';
3233
import { DetailView } from './DetailView';
3334
import { InlineEditSaveBar } from './InlineEditSaveBar';
@@ -42,15 +43,12 @@ import { useDetailTranslation } from './useDetailTranslation';
4243
* `<RecordMetaFooter>` (single-line, muted) rather than a heavy panel, so
4344
* users can still see who/when created or last touched a record.
4445
*
45-
* Keep this in sync with the audit fields auto-injected by the
46-
* framework's `applySystemFields` (created_at/created_by/updated_at/
47-
* updated_by) plus the legacy/tenant identifiers.
46+
* Derived from the shared `SYSTEM_MANAGED_FIELD_NAMES` — the same set the grid's
47+
* default-column derivation uses — so it stays in lockstep with the fields
48+
* `applySystemFields` injects (audit `*_at`/`*_by`, the ownership/tenant FKs,
49+
* soft-delete bookkeeping). Callers can still override via the `systemFields` prop.
4850
*/
49-
const DEFAULT_SYSTEM_FIELDS = new Set([
50-
'id', '_id', '__v', 'created_at', 'updated_at', 'createdAt', 'updatedAt',
51-
'created_by', 'updated_by', 'organization_id', 'tenant_id', 'owner_id',
52-
'deleted_at', 'is_deleted',
53-
]);
51+
const DEFAULT_SYSTEM_FIELDS = new Set(SYSTEM_MANAGED_FIELD_NAMES);
5452

5553
export interface RecordDetailDrawerProps {
5654
/** Whether the drawer is currently open. */

packages/plugin-grid/src/ObjectGrid.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,14 +1956,18 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
19561956
// this same type-aware renderer instead of the card view falling back to
19571957
// a raw `String(value)` dump (which showed "[object Object]" for lookups).
19581958
const renderRecordDetail = (record: any) => {
1959-
const systemFields = ['_id', 'id', 'created_at', 'updated_at', 'created_by', 'updated_by'];
19601959
const entries = Object.entries(record);
19611960
// Honor `hidden: true` on the schema field def — internal/system fields
19621961
// (e.g. database_url, environment_id, is_system) shouldn't leak into the
19631962
// grid's record-detail drawer just because they're in the record payload.
19641963
const isHidden = (key: string) => objectSchema?.fields?.[key]?.hidden === true;
1965-
const regularFields = entries.filter(([key]) => !systemFields.includes(key) && !isHidden(key));
1966-
const metaFields = entries.filter(([key]) => systemFields.includes(key) && key !== '_id' && key !== 'id');
1964+
// Split business fields from framework-managed system/audit/ownership
1965+
// columns via the shared classifier (branches on `field.system`), so the
1966+
// injected `owner_id` and friends land in the muted meta section rather than
1967+
// the business body — consistent with the grid's default-column derivation.
1968+
const isSystem = (key: string) => isSystemManagedField(key, objectSchema?.fields?.[key]);
1969+
const regularFields = entries.filter(([key]) => !isSystem(key) && !isHidden(key));
1970+
const metaFields = entries.filter(([key]) => isSystem(key) && key !== '_id' && key !== 'id' && !isHidden(key));
19671971

19681972
const formatFieldLabel = (key: string): string =>
19691973
key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ');

0 commit comments

Comments
 (0)