Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/system-field-classifier-consolidation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@object-ui/plugin-grid": patch
"@object-ui/fields": patch
"@object-ui/plugin-detail": patch
---

fix(list): route remaining system-field groupings through the shared classifier

Follow-up to the `owner_id` default-column fix: consolidate the display-oriented
system-field exclusions onto the shared `isSystemManagedField` /
`SYSTEM_MANAGED_FIELD_NAMES` (from `@object-ui/types`) so the framework-injected
`owner_id` is treated consistently across the grid, record picker, and detail
drawer.

- `ObjectGrid` record-detail drawer: the business-fields vs. muted meta-section
split now uses the shared classifier, so `owner_id` (and other injected system
fields) land in the meta section instead of the business body.
- `deriveLookupColumns` (record picker): drops its local name set for the shared
classifier — now flag-aware (`field.system`), not just name-based.
- `RecordDetailDrawer`: its default `systemFields` set is derived from the shared
`SYSTEM_MANAGED_FIELD_NAMES`; the `systemFields` prop override is preserved.

`deriveRelatedLists`' narrow "audit FK on every object" set and plugin-detail's
inline-edit "never editable" set are intentionally left distinct — different
semantics (the latter deliberately keeps `owner_id` editable).
13 changes: 13 additions & 0 deletions packages/fields/src/widgets/deriveLookupColumns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ describe('deriveLookupColumns', () => {
expect(deriveLookupColumns(accountSchema, { displayField: 'name', max: 2 })).toHaveLength(2);
});

it('skips fields flagged `system: true` even when the name is not a known audit column', () => {
const schema = {
fields: {
name: { type: 'text', label: 'Name' },
// Non-canonical name, but stamped system by the framework — must be skipped.
custom_ref: { type: 'lookup', label: 'Custom Ref', system: true } as any,
code: { type: 'text', label: 'Code' },
},
};
const cols = deriveLookupColumns(schema, { displayField: 'name', max: 10 });
expect(cols.map((c) => c.field)).toEqual(['name', 'code']);
});

it('honours an object-level displayFields list, with the display field first', () => {
const schema = {
fields: {
Expand Down
20 changes: 5 additions & 15 deletions packages/fields/src/widgets/deriveLookupColumns.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import type { LookupColumnDef } from '@object-ui/types';

/**
* System / audit fields that carry no disambiguating value in a record picker.
* Excluded from auto-derived columns so the picker shows business data, not
* bookkeeping.
*/
const SYSTEM_FIELDS = new Set<string>([
'id', '_id',
'created', 'modified', 'created_at', 'updated_at',
'created_by', 'updated_by', 'modified_by',
'owner_id', 'organization_id', 'space', 'company_id',
'instance_state', 'locked', 'is_deleted', 'deleted',
]);
import { isSystemManagedField } from '@object-ui/types';

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

interface ObjectSchemaLike {
fields?: Record<string, { type?: string; label?: string; hidden?: boolean }>;
fields?: Record<string, { type?: string; label?: string; hidden?: boolean; system?: boolean }>;
/**
* ADR-0085 semantic role: the object's most important fields. The single
* source for "how to list this object" — shared with the detail-page related
Expand Down Expand Up @@ -106,8 +94,10 @@ export function deriveLookupColumns(
// 3. Derive from the field set in declaration order.
const candidates = Object.keys(fields).filter((name) => {
if (name === displayField) return false;
if (SYSTEM_FIELDS.has(name)) return false;
const f = fieldDef(name);
// Skip framework-managed system/audit/ownership columns (incl. owner_id) —
// they carry no disambiguating value in a record picker.
if (isSystemManagedField(name, f)) return false;
if (f.hidden) return false;
if (f.type && NON_TABULAR_TYPES.has(f.type)) return false;
return true;
Expand Down
14 changes: 6 additions & 8 deletions packages/plugin-detail/src/RecordDetailDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SheetTitle,
} from '@object-ui/components';
import type { DataSource } from '@object-ui/types';
import { SYSTEM_MANAGED_FIELD_NAMES } from '@object-ui/types';
import { InlineEditProvider } from '@object-ui/react';
import { DetailView } from './DetailView';
import { InlineEditSaveBar } from './InlineEditSaveBar';
Expand All @@ -42,15 +43,12 @@ import { useDetailTranslation } from './useDetailTranslation';
* `<RecordMetaFooter>` (single-line, muted) rather than a heavy panel, so
* users can still see who/when created or last touched a record.
*
* Keep this in sync with the audit fields auto-injected by the
* framework's `applySystemFields` (created_at/created_by/updated_at/
* updated_by) plus the legacy/tenant identifiers.
* Derived from the shared `SYSTEM_MANAGED_FIELD_NAMES` — the same set the grid's
* default-column derivation uses — so it stays in lockstep with the fields
* `applySystemFields` injects (audit `*_at`/`*_by`, the ownership/tenant FKs,
* soft-delete bookkeeping). Callers can still override via the `systemFields` prop.
*/
const DEFAULT_SYSTEM_FIELDS = new Set([
'id', '_id', '__v', 'created_at', 'updated_at', 'createdAt', 'updatedAt',
'created_by', 'updated_by', 'organization_id', 'tenant_id', 'owner_id',
'deleted_at', 'is_deleted',
]);
const DEFAULT_SYSTEM_FIELDS = new Set(SYSTEM_MANAGED_FIELD_NAMES);

export interface RecordDetailDrawerProps {
/** Whether the drawer is currently open. */
Expand Down
10 changes: 7 additions & 3 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1956,14 +1956,18 @@ export const ObjectGrid: React.FC<ObjectGridProps> = ({
// this same type-aware renderer instead of the card view falling back to
// a raw `String(value)` dump (which showed "[object Object]" for lookups).
const renderRecordDetail = (record: any) => {
const systemFields = ['_id', 'id', 'created_at', 'updated_at', 'created_by', 'updated_by'];
const entries = Object.entries(record);
// Honor `hidden: true` on the schema field def — internal/system fields
// (e.g. database_url, environment_id, is_system) shouldn't leak into the
// grid's record-detail drawer just because they're in the record payload.
const isHidden = (key: string) => objectSchema?.fields?.[key]?.hidden === true;
const regularFields = entries.filter(([key]) => !systemFields.includes(key) && !isHidden(key));
const metaFields = entries.filter(([key]) => systemFields.includes(key) && key !== '_id' && key !== 'id');
// Split business fields from framework-managed system/audit/ownership
// columns via the shared classifier (branches on `field.system`), so the
// injected `owner_id` and friends land in the muted meta section rather than
// the business body — consistent with the grid's default-column derivation.
const isSystem = (key: string) => isSystemManagedField(key, objectSchema?.fields?.[key]);
const regularFields = entries.filter(([key]) => !isSystem(key) && !isHidden(key));
const metaFields = entries.filter(([key]) => isSystem(key) && key !== '_id' && key !== 'id' && !isHidden(key));

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