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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
33 changes: 33 additions & 0 deletions .changeset/6875-grid-relational-meta-derive.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
'@object-ui/plugin-grid': patch
'@object-ui/plugin-dashboard': patch
---

A lookup cell in `ObjectGrid` now honours the author's `displayField`
(objectui#6875).

`ObjectGrid` copies a set of relational keys off the object-schema field def
onto each column's `fieldMeta`, and that bag is what the lookup cell renderer
and the inline picker receive. The set was hand-kept and had become a strict
SUBSET of what those two consumers read — `displayField`, `descriptionField`
and `lookupColumns` were read on the grid's own path and never copied.

They are the spellings that matter. `@objectstack/spec` 17.2.0's `FieldSchema`
is strict and declares `displayField` / `descriptionField` / `lookupColumns` /
`lookupFilters` / `reference`, and none of the snake_case twins the copy set
mostly carried — those parse to `unrecognized_keys`, so a spec-compliant
producer cannot emit them. Nothing renames anything on the way in either: the
adapter's `getObjectSchema` choke point rewrites only the `reference` ⇄
`reference_to` pair. So an author who declared `displayField: 'project_code'`
got a grid cell showing the referenced record's generic `.name` instead.

- The copy set is now DERIVED, in `plugin-grid/src/relationalMetaKeys.ts`, from
a table that classifies every key the consumers read off this bag. A gate
re-extracts that read set from the consumer sources on each run and fails on
any unclassified spelling or orphan, so the two cannot drift apart again.
- `reference_field` and `lookup_columns` — the other two never-copied keys —
stay out on purpose: `FieldSchema` declares neither, so no producer can fill
them. The gate proves that against the installed spec rather than asserting it
in prose.
- `plugin-dashboard`'s `CELL_RELATIONAL_META_KEYS` had the same omission in the
same fallback chain and gains `displayField` too.
Original file line numberDiff line numberDiff line change
Expand Up@@ -260,6 +260,14 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
reference_to: 'project',
reference: 'project',
display_field: 'project_code',
// The SPEC spelling of the same pointer (objectui#6875). `FieldSchema`
// declares `displayField` and none of the snake twins, so this is the leg a
// live `getObjectSchema` actually serves — it must be copied.
displayField: 'project_code',
// The chain's third leg. Read by `LookupCellRenderer`, but `FieldSchema`
// refuses it with `unrecognized_keys`, so no producer can emit it and
// copying it would reach nothing (objectui#6711's reasoning). NOT copied.
reference_field: 'x',
// Six keys with no reader on this path. FOUR of them the grid still copies
// (its picker-only keys); the other two it has since retired as well —
// `reference_to_field` (objectui#6711) and `titleFormat` (objectui#6874).
Expand All@@ -273,18 +281,24 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
titleFormat: '{project_code}',
};

it('copies reference_to / reference / display_field', () => {
it('copies reference_to / reference / display_field / displayField', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
expect(meta.reference_to).toBe('project');
expect(meta.reference).toBe('project');
expect(meta.display_field).toBe('project_code');
// objectui#6875 — the spec-declared spelling, previously dropped here and in
// `ObjectGrid` at the same time.
expect(meta.displayField).toBe('project_code');
});

it('does NOT copy the picker-only keys', () => {
const meta = buildFieldMeta({ accessorKey: 'project', label: 'Project', def }) as any;
for (const k of [
'reference_to_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters', 'titleFormat',
// Read by `LookupCellRenderer`, but unproducible under the strict
// `FieldSchema` — objectui#6875 measured it and left it out on purpose.
'reference_field',
]) {
expect(meta).not.toHaveProperty(k);
}
Expand All@@ -294,7 +308,7 @@ describe('objectui#6694 — buildFieldMeta copies the cell-read relational keys
const meta = buildFieldMeta({
accessorKey: 'amount', label: 'Amount', def: { type: 'currency' },
}) as any;
for (const k of ['reference_to', 'reference', 'display_field']) {
for (const k of ['reference_to', 'reference', 'display_field', 'displayField']) {
expect(meta).not.toHaveProperty(k);
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/plugin-dashboard/src/recordFields.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -112,8 +112,27 @@ export const NUMERIC_FIELD_TYPES = new Set([
* the module `getCellRenderer` dispatches into — the complete set of relational
* keys read off a cell's `field` prop is:
*
* - `reference_to`, `reference`, `display_field` — read by
* - `reference_to`, `reference`, `display_field`, `displayField` — read by
* `LookupCellRenderer` itself. ✅ COPIED.
*
* ⭐ `displayField` ARRIVED with objectui#6875. The enumeration above used
* to name three keys, because it was written from the FIRST leg of each
* chain rather than from the whole chain: `LookupCellRenderer` resolves the
* display pointer as `display_field || displayField || reference_field`, and
* the two extra spellings in that one chain were missed here and in the
* grid's own list at the same time. `displayField` is the spelling
* `@objectstack/spec` 17.2.0's strict `FieldSchema` DECLARES — so on a live
* path served through `getObjectSchema` it is the only one that can arrive,
* and a lookup cell here rendered the referenced record's generic `.name`
* instead of the author's pointer. The grid's twin of this defect is pinned
* behaviourally in `plugin-grid/src/__tests__/lookupDisplayFieldSpelling-6875.test.tsx`.
*
* - `reference_field` — the chain's third leg, and still ⛔ NOT copied.
* `FieldSchema` does not declare it (it parses to `unrecognized_keys`) and
* the producer repo has zero occurrences of the identifier, against a
* `displayField` control that hits 68 files. Copying it would write a member
* from the def on every call that no producer can fill — objectui#6711's
* reasoning, unchanged.
* - `id_field`, `description_field`, `lookup_filters`, `lookupFilters` — ZERO
* mentions in that module; read only by `fields/src/widgets/LookupField.tsx`
* and `UserField.tsx`, both EDITORS. ⛔ NOT copied.
Expand All@@ -136,7 +155,7 @@ export const NUMERIC_FIELD_TYPES = new Set([
* picker keys. The boundary is pinned in
* `__tests__/lookupRelationalMeta-6694.test.tsx`.
*/
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field'] as const;
const CELL_RELATIONAL_META_KEYS = ['reference_to', 'reference', 'display_field', 'displayField'] as const;

/**
* Copy {@link CELL_RELATIONAL_META_KEYS} off a schema field def, with
Expand DownExpand Up@@ -228,6 +247,13 @@ export interface FieldMeta {
reference?: string;
/** Author-declared display field on the lookup — beats every resolver in the cell. */
display_field?: string;
/**
* Same pointer, SPEC spelling (`FieldSchema.displayField`) — the second leg of
* `LookupCellRenderer`'s `display_field || displayField || reference_field`
* chain, and the only leg a spec-compliant producer can actually emit
* (objectui#6875).
*/
displayField?: string;
}

/**
Expand Down
95 changes: 8 additions & 87 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { useColumnSummary } from './useColumnSummary';
import { resolveRowCrudAffordances, resolveRowRecordCrudAffordance } from './rowCrudAffordances';
import { useRecordCrudVerdicts } from './hooks/useRecordCrudVerdicts';
import { resolveLegacyRowActions } from './resolveLegacyRowActions';
import { applyRelationalMeta } from './relationalMetaKeys';
import { resolveBulkActions } from './resolveBulkActions';
import { partitionBulkRows } from './bulkEligibility';
import { resolvesToDataColumn, describeUnresolvedColumns } from './columnSpellingDiagnostics';
Expand DownExpand Up@@ -421,84 +422,14 @@ function getDataConfig(schema: ObjectGridSchema): ViewData | null {
}

/**
* Relational field metadata that a lookup / master_detail / user cell needs to
* (a) resolve a bare foreign-key id to a display name (LookupCellRenderer →
* `field.reference_to`) and (b) drive the inline picker's query (LookupField
* reads reference_to/reference, display_field, id_field, description_field,
* lookup_filters). These are dropped if we only copy the scalar-display props
* (label/currency/precision/…), which is why an inline-edited lookup showed the
* raw id after moving to another row. Copy them from the object-schema field
* definition onto the built `fieldMeta` for every column-building path.
*
* ## ⛔ Two keys were in this list and are RETIRED
*
* Every key here has to have a measured reader on this grid's own render path —
* the cell renderers and inline editors in `@object-ui/fields` that
* `getCellRenderer` dispatches into. Two keys had none, for two different
* reasons, and each retirement was its own adjudication.
*
* ### `reference_to_field` — objectui#6711
*
* Swept across `packages/` and `apps/` (and again across the producer repo), the
* only occurrences of the identifier anywhere were this array literal — the
* write — and prose recording that nothing reads it. No member access, no
* destructuring, no bracket read. `@objectstack/spec`'s FieldSchema does not
* declare it either, so nothing authorable produces it.
*
* ### `titleFormat` — objectui#6874
*
* A zero of a different kind, and a stronger one. `titleFormat` is a real, live
* key with plenty of readers — it simply has no FIELD-meta reader. The sweep did
* not fail to find readers; it found every member read of the identifier across
* `packages/` and `apps/` (tests included) and classified each one by receiver:
*
* - `objectDef` / `objectSchema` / `objSchema` — `core/utils/record-title.ts`,
* `components/.../containers.tsx`, `plugin-detail/DetailView.tsx`,
* `ObjectKanban.tsx`, `ObjectCalendar.tsx`, `react/hooks/useRecordSearch.ts`.
* OBJECT schema, every one.
* - `refObjectSchema?.titleFormat` — `fields/widgets/LookupField.tsx`: the
* REFERENCED object's schema, fetched by `getSchema(referenceTo)`. Also an
* OBJECT schema, and the one that matters here — it is what this grid's own
* inline picker reads.
* - `param.titleFormat` — `app-shell/utils/paramToField.ts`, off a resolved
* `ActionParamDef`; the field-def read next to it is `field.title_format`,
* a different spelling on a different surface.
*
* `RecordPickerDialog` and `lookupColumnDisplay` receive it as a PROP, and the
* repo's single `titleFormat=` pass is `titleFormat={refTitleFormat}` —
* object-schema sourced. ⇒ copying `reference_to` is what makes `titleFormat`
* work on this path; copying `titleFormat` onto the meta reached nothing.
* `plugin-dashboard/src/recordFields.tsx` recorded this same measurement first
* and declined to copy the key, so it was a measured no-op in two seams and had
* been retired from only one.
*
* ### The control that makes both zeros a reading
*
* Not an artefact of how the sweep was written: the same sweep over the
* surviving list-mates finds a real FIELD-meta reader for every one of them —
* `reference_to` / `reference` / `display_field` off the cell's `field` prop in
* `LookupCellRenderer` (`fields/src/index.tsx`), and `id_field` /
* `description_field` / `lookup_filters` / `lookupFilters` off `fieldMeta?.…`
* in `LookupField` / `UserField`. There is no third reader-less key: all seven
* survivors are read off a field meta.
*
* ⚠️ The sweep bounds these two repos. A host application outside them could
* still be reading either key off `fieldMeta`; the repo's own contract is what
* these retirements are about.
*
* ⛔ Do not re-add a key for symmetry with the object-schema field def. A
* member written from the def on every column build and read by nothing is
* exactly what objectui#6625 (`decimals`) and objectui#6597 (`referenceTo`)
* retired from the sibling producer. Add a key when a reader on THIS path is
* measured, not before. Both absences are pinned, at all three call sites —
* `__tests__/relationalMetaCopySet-6711.test.tsx` and
* `__tests__/relationalMetaCopySet-6874.test.tsx`.
* The relational copy set and `applyRelationalMeta` moved to
* `./relationalMetaKeys` for objectui#6875. The list there is DERIVED from a
* table classifying every key the grid's own cell renderer and inline picker
* read off this bag, and a gate re-derives that read set from the consumer
* sources — so the copy set can no longer drift into being a strict subset of
* what its consumers read, which is what it had silently become. Read that
* file's docblock before adding, removing or re-spelling a key.
*/
const RELATIONAL_META_KEYS = [
'reference_to', 'reference',
'display_field', 'id_field', 'description_field',
'lookup_filters', 'lookupFilters',
] as const;

/**
* Content signature of a host's find-params, used as the query-change signal for
Expand All@@ -517,16 +448,6 @@ function findParamsSignature(params: Record<string, unknown> | null | undefined)
);
}

function applyRelationalMeta(
fieldMeta: Record<string, any>,
fieldDef: Record<string, any> | undefined | null,
): void {
if (!fieldDef) return;
for (const key of RELATIONAL_META_KEYS) {
if (fieldDef[key] !== undefined) fieldMeta[key] = fieldDef[key];
}
}

/**
* Helper to normalize columns configuration
* Handles both string[] and ListColumn[] formats
Expand Down
Loading
Loading