Skip to content

Commit 8aae006

Browse files
os-zhuangclaude
andauthored
fix(views): the five per-view-type configs speak the spec vocabulary (#2231 phase 3) (#2892)
* fix(views): the five per-view-type configs speak the spec vocabulary (#2231 phase 3) `kanban`/`calendar`/`gantt`/`gallery`/`timeline` on `ListViewSchema` were the last hand-written forks after #2882 — and the fork was not cosmetic. objectui named the same concepts differently from `@objectstack/spec/ui`, and several read-sites only understood one of the two dialects. Two of those gaps were live bugs. Kanban lanes ignored the spec key. `ListView` gated the Kanban tab on `groupByField || groupField` but rendered lanes off `groupField` alone, so a config authored with the spec key — what the product's own `CreateViewDialog` emits — offered the tab and then grouped by whatever `detectStatusField()` guessed. The spec's `columns` (fields shown on each card) was also spread onto the board verbatim, where `columns` means LANES, so `ObjectKanban` built lanes with undefined id/title. `columns` now maps to `cardFields`, and the vocabulary keys are stripped from the passthrough (matching plugin-view's adapter, which already did this). Timeline lost every spec key in app-shell. `ObjectView`'s `timeline` branch was a three-key whitelist while its `gallery`/`gantt` siblings had already been fixed to spread-first, so a stored `timeline: { startDateField, endDateField, groupByField, colorField, scale }` arrived with only `titleField` and an axis pinned to the `'due_date'` fallback. Also: plugin-view's `ObjectView` now reads `gallery.coverField` and `timeline.startDateField` (it understood only the legacy aliases), the dead `gallery.subtitleField` is removed (three producers computed it; `ObjectGallery` never read it), and `defaultKanbanFromObject` no longer emits the legacy key alongside the spec one — that dual-write existed only to work around the render bug. Schema side: the four remaining configs derive from the spec via `.partial()` — the product authors partial configs while spec marks `columns`/`titleField`/ `startDateField` required — and `gantt` needs no local schema at all. `.passthrough()` is preserved so renderer-ahead knobs (calendar's `allDayField`) still reach the renderer. The pre-#2231 names (`groupField`, `cardFields`, `imageField`, `dateField`) stay accepted as deprecated aliases so stored views keep validating; the spec key wins wherever both appear. `calendar.defaultView` stays local — no spec counterpart. The drift guard now pins the derivation per config, and five ListView regression tests cover the kanban bugs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3EDGLKNvbcpDTLzyiAd3Q * fix(list): collect the fields named by the spec view-config keys into $select Third instance of the same root cause as the kanban lane bug. The view-config field collector — which decides what `$select`/`$expand` asks the server for — listed only the legacy objectui keys (`groupField`, `groupBy`, `cardFields`), so a spec-authored config named its lane and card fields in a vocabulary the collector could not see. The board then rendered from rows that never carried those fields, which reads as an empty board rather than a config error. Adds the spec counterparts (`groupByField`, `summarizeField`, and `columns` — the kanban card fields) ahead of the aliases, in both collectors. Pinned by a test that asserts the queried field set contains the lane and card fields of a spec-authored kanban; it fails without this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3EDGLKNvbcpDTLzyiAd3Q --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba45145 commit 8aae006

13 files changed

Lines changed: 391 additions & 74 deletions

File tree

.changeset/spec-view-configs.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@object-ui/types": patch
3+
"@object-ui/plugin-list": patch
4+
"@object-ui/plugin-view": patch
5+
"@object-ui/app-shell": patch
6+
---
7+
8+
fix(views): the five per-view-type configs speak the spec vocabulary (#2231 phase 3)
9+
10+
`kanban`/`calendar`/`gantt`/`gallery`/`timeline` on `ListViewSchema` were the last
11+
hand-written forks left after #2882 — and the fork was not cosmetic: objectui named
12+
the same concepts differently from `@objectstack/spec/ui`, and several read-sites
13+
only understood one of the two dialects. Two of those gaps were live bugs.
14+
15+
**Kanban lanes ignored the spec key.** `ListView` gated the Kanban tab on
16+
`groupByField || groupField` but rendered lanes off `groupField` alone. A config
17+
authored with the spec key — which is exactly what the product's own
18+
`CreateViewDialog` emits — offered the tab and then grouped by whatever
19+
`detectStatusField()` guessed. The spec's `columns` (the fields shown on each card)
20+
was also spread onto the board verbatim, where `columns` means *lanes*, so
21+
`ObjectKanban` built lanes with `undefined` id and title. `columns` now maps to
22+
`cardFields` and the vocabulary keys are stripped from the passthrough.
23+
24+
**Timeline lost every spec key in app-shell.** `ObjectView`'s `timeline` branch was
25+
a three-key whitelist while its `gallery`/`gantt` siblings had already been fixed to
26+
spread-first, so a stored `timeline: { startDateField, endDateField, groupByField,
27+
colorField, scale }` arrived with only `titleField` and an axis pinned to the
28+
`'due_date'` fallback.
29+
30+
Also: `plugin-view`'s `ObjectView` now reads `gallery.coverField` and
31+
`timeline.startDateField` (it only understood the legacy aliases), and the dead
32+
`gallery.subtitleField` is removed — three producers computed it and `ObjectGallery`
33+
never read it.
34+
35+
The schema side now derives from the spec configs (`.partial()`, since the product
36+
authors partial configs and spec marks `columns`/`titleField`/`startDateField`
37+
required). `gantt` needed no local schema at all. The pre-#2231 names
38+
(`groupField`, `cardFields`, `imageField`, `dateField`) remain accepted as deprecated
39+
aliases so stored views keep validating; the spec key wins wherever both appear.
40+
`calendar.defaultView` stays local — it has no spec counterpart.

packages/app-shell/src/views/InterfaceListPage.defaults.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ const taskObject = {
2828

2929
describe('InterfaceListPage default viz bindings', () => {
3030
it('kanban: picks the first select field as the group field (both aliases)', () => {
31-
expect(defaultKanbanFromObject(taskObject)).toEqual({ groupField: 'status', groupByField: 'status' });
31+
expect(defaultKanbanFromObject(taskObject)).toEqual({ groupByField: 'status' });
3232
});
3333

3434
it('kanban: falls back to a status-like field name when no select type exists', () => {
3535
const obj = { fields: { title: { type: 'text' }, stage: { type: 'text' } } };
36-
expect(defaultKanbanFromObject(obj)).toEqual({ groupField: 'stage', groupByField: 'stage' });
36+
expect(defaultKanbanFromObject(obj)).toEqual({ groupByField: 'stage' });
3737
});
3838

3939
it('kanban: undefined when nothing groupable', () => {
@@ -50,7 +50,7 @@ describe('InterfaceListPage default viz bindings', () => {
5050

5151
it('ignores hidden and system fields', () => {
5252
const obj = { fields: { created_at: { type: 'datetime' }, hidden_sel: { type: 'select', hidden: true }, real_sel: { type: 'select' } } };
53-
expect(defaultKanbanFromObject(obj)).toEqual({ groupField: 'real_sel', groupByField: 'real_sel' });
53+
expect(defaultKanbanFromObject(obj)).toEqual({ groupByField: 'real_sel' });
5454
});
5555
});
5656

packages/app-shell/src/views/InterfaceListPage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,14 @@ const SELECT_TYPES = new Set(['select', 'multiselect', 'radio', 'enum', 'boolean
132132
const DATE_TYPES = new Set(['date', 'datetime', 'time']);
133133
const IMAGE_TYPES = new Set(['image', 'file', 'attachment', 'avatar', 'photo']);
134134

135-
export function defaultKanbanFromObject(objectDef: any): { groupField: string; groupByField: string } | undefined {
135+
export function defaultKanbanFromObject(objectDef: any): { groupByField: string } | undefined {
136136
const field =
137137
firstFieldMatching(objectDef, (_n, f) => SELECT_TYPES.has(f.type)) ??
138138
firstFieldMatching(objectDef, (n) => /status|stage|state|priority|category|kind/i.test(n));
139-
// ListView reads `groupField` to render and `groupByField || groupField`
140-
// to decide the viz is available — set both so it resolves either way.
141-
return field ? { groupField: field, groupByField: field } : undefined;
139+
// `groupByField` is the spec key. This used to emit the legacy `groupField`
140+
// alongside it because ListView rendered off the alias only — that read-site
141+
// now prefers the spec key, so one key is enough.
142+
return field ? { groupByField: field } : undefined;
142143
}
143144

144145
function defaultDateField(objectDef: any): string | undefined {

packages/app-shell/src/views/ObjectView.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,13 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
14781478
defaultView: viewDef.calendar?.defaultView,
14791479
},
14801480
timeline: {
1481-
dateField: viewDef.timeline?.dateField || 'due_date',
1481+
// Spread the full view-defined timeline config first so the spec
1482+
// fields (startDateField/endDateField/groupByField/colorField/scale)
1483+
// survive; then layer the defaults. (Mirrors the gallery and gantt
1484+
// branches — a bare whitelist here was dropping every spec key and
1485+
// pinning the axis to the legacy `dateField` fallback.)
1486+
...(viewDef.timeline || {}),
1487+
startDateField: viewDef.timeline?.startDateField || viewDef.timeline?.dateField || 'due_date',
14821488
titleField: viewDef.timeline?.titleField || objectDef.titleField || 'name',
14831489
descriptionField: viewDef.timeline?.descriptionField,
14841490
},
@@ -1499,7 +1505,6 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
14991505
imageField: viewDef.gallery?.imageField || viewDef.gallery?.coverField || 'image',
15001506
coverField: viewDef.gallery?.coverField || viewDef.gallery?.imageField,
15011507
titleField: viewDef.gallery?.titleField || objectDef.titleField || 'name',
1502-
subtitleField: viewDef.gallery?.subtitleField,
15031508
},
15041509
gantt: {
15051510
// Spread the full view-defined gantt config first so the

packages/plugin-list/src/ListView.tsx

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -794,12 +794,16 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
794794
const collectViewFields = (v: any) => {
795795
if (!v) return;
796796
const candidates = [
797-
v.groupField, v.groupBy,
797+
// Spec keys first, then the legacy objectui aliases (#2231).
798+
v.groupByField, v.groupField, v.groupBy,
799+
v.summarizeField,
798800
v.titleField, v.cardTitle,
799801
v.startDateField, v.endDateField, v.dateField, v.endField,
800802
v.colorField, v.allDayField,
801-
v.coverField, v.imageField, v.subtitleField,
803+
v.coverField, v.imageField,
802804
v.swimlaneField, v.valueField,
805+
// Spec `columns` = the fields shown on each kanban card (legacy: cardFields).
806+
...(Array.isArray(v.columns) ? v.columns : []),
803807
...(Array.isArray(v.cardFields) ? v.cardFields : []),
804808
...(Array.isArray(v.visibleFields) ? v.visibleFields : []),
805809
...(Array.isArray(v.metaFields) ? v.metaFields : []),
@@ -1009,12 +1013,16 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
10091013
const collectViewFields = (v: any) => {
10101014
if (!v) return;
10111015
const candidates = [
1012-
v.groupField, v.groupBy,
1016+
// Spec keys first, then the legacy objectui aliases (#2231).
1017+
v.groupByField, v.groupField, v.groupBy,
1018+
v.summarizeField,
10131019
v.titleField, v.cardTitle,
10141020
v.startDateField, v.endDateField, v.dateField, v.endField,
10151021
v.colorField, v.allDayField,
1016-
v.coverField, v.imageField, v.subtitleField,
1022+
v.coverField, v.imageField,
10171023
v.swimlaneField, v.valueField,
1024+
// Spec `columns` = the fields shown on each kanban card (legacy: cardFields).
1025+
...(Array.isArray(v.columns) ? v.columns : []),
10181026
...(Array.isArray(v.cardFields) ? v.cardFields : []),
10191027
...(Array.isArray(v.visibleFields) ? v.visibleFields : []),
10201028
...(Array.isArray(v.metaFields) ? v.metaFields : []),
@@ -1353,28 +1361,34 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
13531361
...((schema as any).bulkActionDefs ? { bulkActionDefs: (schema as any).bulkActionDefs } : {}),
13541362
...(schema.options?.grid || {}),
13551363
};
1356-
case 'kanban':
1364+
case 'kanban': {
1365+
// The spec's lane field is `groupByField`; `groupField` is the legacy
1366+
// objectui alias. Read the canonical key FIRST — reading only the alias
1367+
// meant a spec-authored config (what CreateViewDialog emits) passed the
1368+
// capability gate above but rendered lanes from the detector instead.
1369+
// ADR-0085: with neither key set, fall back to the object's declared
1370+
// lifecycle (`stageField`, incl. strict-false suppression) via the shared
1371+
// detector — mirrors ObjectView's default so both entry paths agree.
1372+
// objectDef loads async: until it lands this stays undefined and the board
1373+
// re-derives lanes once it does.
1374+
const kanbanCfg = { ...(schema.options?.kanban || {}), ...(schema.kanban || {}) };
1375+
// `columns` is the spec's list of fields shown on each card. ObjectKanban's
1376+
// own `columns` prop is its LANES, so passing this through verbatim built
1377+
// lanes with undefined id/title. Map it to `cardFields` and strip the
1378+
// vocabulary keys from the passthrough (mirrors plugin-view's adapter).
1379+
const { columns: kanbanCardColumns, groupByField, groupField, cardFields, titleField, ...restKanban } = kanbanCfg as Record<string, any>;
1380+
const laneField = groupByField || groupField || detectStatusField(objectDef) || undefined;
13571381
return {
13581382
type: 'object-kanban',
13591383
...baseProps,
1360-
// ADR-0085: no explicit lane field → the object's declared
1361-
// lifecycle (`stageField`, incl. strict-false suppression) via the
1362-
// shared detector — mirrors ObjectView's default so a schema that
1363-
// omits groupField behaves the same on both entry paths. objectDef
1364-
// loads async: until it lands this stays undefined and the board
1365-
// re-derives lanes once it does.
1366-
groupBy: schema.kanban?.groupField || schema.options?.kanban?.groupField
1367-
|| detectStatusField(objectDef) || undefined,
1368-
groupField: schema.kanban?.groupField || schema.options?.kanban?.groupField
1369-
|| detectStatusField(objectDef) || undefined,
1370-
...(schema.kanban?.titleField || schema.options?.kanban?.titleField
1371-
? { titleField: schema.kanban?.titleField || schema.options?.kanban?.titleField }
1372-
: {}),
1373-
cardFields: schema.kanban?.cardFields || effectiveFields || [],
1384+
groupBy: laneField,
1385+
groupField: laneField,
1386+
...(titleField ? { titleField } : {}),
1387+
cardFields: cardFields || kanbanCardColumns || effectiveFields || [],
13741388
...(groupingConfig ? { grouping: groupingConfig } : {}),
1375-
...(schema.options?.kanban || {}),
1376-
...(schema.kanban || {}),
1389+
...restKanban,
13771390
};
1391+
}
13781392
case 'calendar':
13791393
return {
13801394
type: 'object-calendar',
@@ -1405,7 +1419,6 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
14051419
// Deprecated top-level props for backward compat
14061420
imageField: schema.gallery?.coverField || schema.gallery?.imageField || schema.options?.gallery?.imageField,
14071421
titleField: schema.gallery?.titleField || schema.options?.gallery?.titleField || 'name',
1408-
subtitleField: schema.gallery?.subtitleField || schema.options?.gallery?.subtitleField,
14091422
...(groupingConfig ? { grouping: groupingConfig } : {}),
14101423
};
14111424
}

packages/plugin-list/src/ObjectGallery.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export interface ObjectGalleryProps {
3030
imageField?: string;
3131
/** @deprecated Use gallery.titleField instead */
3232
titleField?: string;
33-
subtitleField?: string;
3433
};
3534
data?: Record<string, unknown>[];
3635
dataSource?: { find: (name: string, query: unknown) => Promise<unknown> };

packages/plugin-list/src/__tests__/ListView.test.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2549,3 +2549,130 @@ describe('ListView — gantt view fed by an api-provider ViewData', () => {
25492549
expect(mockDataSource.find).toHaveBeenCalled();
25502550
});
25512551
});
2552+
2553+
// ============================================================================
2554+
// Kanban — spec vocabulary reaches the board (#2231)
2555+
// ============================================================================
2556+
describe('ListView — kanban config speaks the spec vocabulary', () => {
2557+
// The spec lane key is `groupByField`; `groupField` is the legacy objectui
2558+
// alias. ListView used to gate the Kanban tab on `groupByField || groupField`
2559+
// but render lanes off `groupField` ALONE, so a spec-authored config — what
2560+
// CreateViewDialog emits — offered the tab and then grouped by the detector's
2561+
// guess. And because the whole config was spread onto the component props,
2562+
// the spec's `columns` (fields shown on each card) landed on ObjectKanban's
2563+
// `columns` prop, which is its LANES, producing lanes with undefined ids.
2564+
let prevObjectKanban: ReturnType<typeof ComponentRegistry.get>;
2565+
let kanbanCalls: any[];
2566+
2567+
beforeAll(() => {
2568+
prevObjectKanban = ComponentRegistry.get('object-kanban');
2569+
ComponentRegistry.register('object-kanban', (props: any) => {
2570+
kanbanCalls.push(props);
2571+
return <div data-testid="kanban-stub" />;
2572+
});
2573+
});
2574+
afterAll(() => {
2575+
if (prevObjectKanban) {
2576+
ComponentRegistry.register('object-kanban', prevObjectKanban);
2577+
} else {
2578+
ComponentRegistry.unregister('object-kanban');
2579+
}
2580+
});
2581+
beforeEach(() => {
2582+
kanbanCalls = [];
2583+
mockDataSource.find.mockClear();
2584+
});
2585+
2586+
const kanbanSchema = (kanban: Record<string, unknown>) => ({
2587+
type: 'list-view',
2588+
objectName: 'contacts',
2589+
viewType: 'kanban',
2590+
fields: ['name'],
2591+
kanban,
2592+
} as unknown as ListViewSchema);
2593+
2594+
it('groups by the spec `groupByField`', async () => {
2595+
renderWithProvider(<ListView schema={kanbanSchema({ groupByField: 'stage' })} dataSource={mockDataSource} />);
2596+
expect(await screen.findByTestId('kanban-stub')).toBeInTheDocument();
2597+
2598+
const last = kanbanCalls.at(-1);
2599+
expect(last?.schema?.groupBy).toBe('stage');
2600+
expect(last?.schema?.groupField).toBe('stage');
2601+
});
2602+
2603+
it('still honors the deprecated `groupField` alias', async () => {
2604+
renderWithProvider(<ListView schema={kanbanSchema({ groupField: 'priority' })} dataSource={mockDataSource} />);
2605+
expect(await screen.findByTestId('kanban-stub')).toBeInTheDocument();
2606+
2607+
expect(kanbanCalls.at(-1)?.schema?.groupBy).toBe('priority');
2608+
});
2609+
2610+
it('lets the spec key win when both are present', async () => {
2611+
renderWithProvider(
2612+
<ListView schema={kanbanSchema({ groupByField: 'stage', groupField: 'legacy' })} dataSource={mockDataSource} />,
2613+
);
2614+
expect(await screen.findByTestId('kanban-stub')).toBeInTheDocument();
2615+
2616+
expect(kanbanCalls.at(-1)?.schema?.groupBy).toBe('stage');
2617+
});
2618+
2619+
it('maps the spec `columns` to card fields instead of leaking it as lanes', async () => {
2620+
renderWithProvider(
2621+
<ListView
2622+
schema={kanbanSchema({ groupByField: 'stage', columns: ['name', 'amount'] })}
2623+
dataSource={mockDataSource}
2624+
/>,
2625+
);
2626+
expect(await screen.findByTestId('kanban-stub')).toBeInTheDocument();
2627+
2628+
const last = kanbanCalls.at(-1);
2629+
expect(last?.schema?.cardFields).toEqual(['name', 'amount']);
2630+
// `columns` on ObjectKanban means lanes — the view config's `columns` must
2631+
// not reach it, or the board renders lanes with undefined id/title.
2632+
expect(last?.schema?.columns).toBeUndefined();
2633+
});
2634+
2635+
it('does not leak the vocabulary keys onto the component schema', async () => {
2636+
renderWithProvider(
2637+
<ListView schema={kanbanSchema({ groupByField: 'stage', summarizeField: 'amount' })} dataSource={mockDataSource} />,
2638+
);
2639+
expect(await screen.findByTestId('kanban-stub')).toBeInTheDocument();
2640+
2641+
const last = kanbanCalls.at(-1);
2642+
expect(last?.schema?.groupByField).toBeUndefined();
2643+
// Unrecognized spec fields still pass through for the renderer to consume.
2644+
expect(last?.schema?.summarizeField).toBe('amount');
2645+
});
2646+
});
2647+
2648+
// ============================================================================
2649+
// Field collection covers the spec vocabulary (#2231)
2650+
// ============================================================================
2651+
describe('ListView — $select collects fields named by the spec vocabulary', () => {
2652+
// The per-view-type configs name fields the view must fetch. The collector
2653+
// only knew the legacy objectui keys, so a spec-authored config selected
2654+
// neither its lane field nor its card fields — the board then rendered from
2655+
// rows that never carried them.
2656+
beforeEach(() => {
2657+
mockDataSource.find.mockClear();
2658+
mockDataSource.find.mockResolvedValue([]);
2659+
});
2660+
2661+
it('selects the kanban lane and card fields named with spec keys', async () => {
2662+
const schema = {
2663+
type: 'list-view',
2664+
objectName: 'contacts',
2665+
viewType: 'kanban',
2666+
fields: ['name'],
2667+
kanban: { groupByField: 'stage', columns: ['owner', 'amount'], summarizeField: 'amount' },
2668+
} as unknown as ListViewSchema;
2669+
2670+
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
2671+
2672+
await vi.waitFor(() => expect(mockDataSource.find).toHaveBeenCalled());
2673+
const selected = JSON.stringify(mockDataSource.find.mock.calls);
2674+
for (const field of ['stage', 'owner', 'amount']) {
2675+
expect(selected).toContain(field);
2676+
}
2677+
});
2678+
});

packages/plugin-view/src/ObjectView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,16 +766,18 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
766766
return {
767767
type: 'object-gallery',
768768
...baseProps,
769-
imageField: viewOptions.gallery?.imageField,
769+
// `coverField` is the spec key; `imageField` is the legacy alias that
770+
// ObjectGallery still consults as a flat prop.
771+
imageField: viewOptions.gallery?.coverField || viewOptions.gallery?.imageField,
770772
titleField: viewOptions.gallery?.titleField || 'name',
771-
subtitleField: viewOptions.gallery?.subtitleField,
772773
...(viewOptions.gallery || {}),
773774
};
774775
case 'timeline':
775776
return {
776777
type: 'object-timeline',
777778
...baseProps,
778-
dateField: viewOptions.timeline?.dateField || 'created_at',
779+
// `startDateField` is the spec key; `dateField` is the legacy alias.
780+
startDateField: viewOptions.timeline?.startDateField || viewOptions.timeline?.dateField || 'created_at',
779781
titleField: viewOptions.timeline?.titleField || 'name',
780782
...(viewOptions.timeline || {}),
781783
};

0 commit comments

Comments
 (0)