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
27 changes: 27 additions & 0 deletions .changeset/10868-list-view-leftovers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@object-ui/core': patch
'@object-ui/types': patch
---

fix(core): `normalizeListViewSchema`'s density fold tests an own key, not `in` (objectui#10868)

The fold turned a legacy `densityMode` into `rowHeight` when the value was `in`
`DENSITY_MODE_TO_ROW_HEIGHT`, and `in` walks the prototype chain. So
`densityMode: 'toString'` came back with `rowHeight` set to
`Object.prototype.toString`, a function, and the key was dropped. The fold now
uses the same own-key test that `rowHeightToDensityMode` uses in the read
direction. An inherited key such as `'toString'` or `'constructor'` is now
treated like any other unrecognized density, such as `'cozy'`: nothing is
folded, `rowHeight` stays unset, and `densityMode` is kept. The three densities
`compact`, `comfortable` and `spacious` fold exactly as before.

Such a value reaches the fold only on a render path that does not validate.
`@object-ui/types`' zod schemas already refuse it: the `list-view` node declares
`densityMode` as a three-value enum, and a named view refuses `densityMode` by
name.

docs(types): `NamedListView`'s docblock no longer says the type is used in
`ObjectViewSchema.listViews`. Since objectui#7928 that member has been the
protocol's `ObjectListViewSchema`, by reference. The docblock now says what the
type is: exported, and the type of no member of the contract. The type itself is
unchanged.
29 changes: 29 additions & 0 deletions packages/core/src/utils/__tests__/normalize-list-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ describe('normalizeListViewSchema (#2890)', () => {
expect(out.densityMode).toBe('cozy');
});

// objectui#10868 — the fold's membership test is an OWN-key test, the same
// trap `rowHeightToDensityMode` guards in the read direction: `in` walks the
// prototype chain, so `'toString'` used to fold to `Object.prototype.toString`
// (a FUNCTION in `rowHeight`) and the key was dropped. An inherited key is
// an unrecognized density like `'cozy'`, and gets exactly its treatment.
it.each(['toString', 'constructor', 'hasOwnProperty', 'valueOf', '__proto__'])(
'does not read the inherited Object.prototype key %j as a density',
(inherited) => {
const out = normalizeListViewSchema({ densityMode: inherited }) as Record<string, unknown>;
expect(out.rowHeight).toBeUndefined();
expect(out.densityMode).toBe(inherited);
// With a canonical `viewType` there is nothing else to fold, so the
// input comes back by reference: the density fold did not fire.
const canonical = { viewType: 'grid', densityMode: inherited };
expect(normalizeListViewSchema(canonical)).toBe(canonical);
},
);

it('keeps the unrecognized-density CONTROL and the recognized fold beside the inherited keys', () => {
// `'cozy'` was already unfolded before the own-key test; `'compact'` is an
// own key of the table and still folds. Both hold on either side of it.
const cozy = normalizeListViewSchema({ densityMode: 'cozy' }) as Record<string, unknown>;
expect(cozy.rowHeight).toBeUndefined();
expect(cozy.densityMode).toBe('cozy');
const compact = normalizeListViewSchema({ densityMode: 'compact' }) as Record<string, unknown>;
expect(compact.rowHeight).toBe('compact');
expect('densityMode' in compact).toBe(false);
});

it('round-trips every density through the widening and back', () => {
// The fold widens 3 values onto 5 and the renderer narrows them back, so
// a folded view must render the density the author picked.
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/utils/normalize-list-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,13 @@ export function normalizeListViewSchema<T>(schema: T): T {
const legacyFields = s.fields;
const foldColumns = Array.isArray(legacyFields);
const legacyDensity = s.densityMode;
const foldRowHeight = typeof legacyDensity === 'string' && legacyDensity in DENSITY_MODE_TO_ROW_HEIGHT;
// `hasOwnProperty`, not `in` — same trap as {@link rowHeightToDensityMode},
// in the fold direction: `in` walks the prototype chain, so `'toString'` used
// to fold into `rowHeight` as `Object.prototype.toString`, a FUNCTION, and
// drop the key (objectui#10868). An inherited key is an unrecognized density.
const foldRowHeight =
typeof legacyDensity === 'string' &&
Object.prototype.hasOwnProperty.call(DENSITY_MODE_TO_ROW_HEIGHT, legacyDensity);
const legacyFilters = s.filters;
const foldFilter = Array.isArray(legacyFilters);
const legacyFlags = Object.keys(SHOW_FLAG_TO_USER_ACTION).filter((k) => typeof s[k] === 'boolean');
Expand Down
10 changes: 7 additions & 3 deletions packages/plugin-view/src/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1749,7 +1749,11 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
{ ...legacyViewOptions } as Record<string, any>,
);

// Dev-mode warning for flat property access violations
// Dev-mode warning for flat property access violations. The place it names
// is the view's own top-level `KIND` block — the one `viewOptions[viewType]`
// reads. ⛔ Not `options.KIND`: this component reads no `options` bag since
// objectui#7928, and a named view refuses `options` by name, so that text
// sent an author to a place that is ignored or refused (objectui#10868).
if (process.env.NODE_ENV === 'development') {
const flatKeys = ['startDateField', 'endDateField', 'dateField', 'groupBy', 'groupField',
'locationField', 'imageField', 'dependenciesField', 'progressField', 'titleField',
Expand All @@ -1758,8 +1762,8 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
const found = flatKeys.filter(k => k in viewOptions && !(k in nestedConfig));
if (found.length > 0) {
console.warn(
`[Spec Compliance] View options use flat properties ${JSON.stringify(found)}. ` +
`Move them under options.${viewType} per @objectstack/spec protocol.`
`[Spec Compliance] The view uses flat properties ${JSON.stringify(found)}. ` +
`Move them under its top-level ${viewType} block ({ ${viewType}: { ... } }) per @objectstack/spec protocol.`
);
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/types/src/objectql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2236,8 +2236,15 @@ export interface ViewTabBarConfig {
}

/**
* Named List View Definition
* Used in ObjectViewSchema.listViews for named views (e.g., "All", "My Records").
* Named List View Definition — objectui's own hand-written named-view shape.
*
* No member of this package's contract is typed by it: since objectui#7928,
* `ObjectViewSchema.listViews` is the protocol's `ObjectListViewSchema` by
* reference (see that member), and the pins
* `_ListViewsIsNoLongerTheLocalRecord` / `_ListViewsIsTheSpecRecord` in
* `__tests__/object-view-unmirrored-keys-7779.test.ts` hold that. It stays
* exported from `@object-ui/types`; whether it is retired or narrowed follows
* objectui#7924.
*/
export interface NamedListView {
/** View display label */
Expand Down
Loading