Skip to content

Commit a1d5b38

Browse files
Copilothotlong
andcommitted
feat: ObjectTimeline accepts nested timeline config; ListView passes timeline as nested object
- ObjectTimeline now supports `schema.timeline.*` nested config with fallback to flat props - ObjectTimeline maps `timeline.scale` to `timeScale` for the renderer - ListView timeline case mirrors gallery pattern: passes nested `timeline` config - Bridge uses strong ListViewGalleryConfig/ListViewTimelineConfig types - Added 13 ObjectTimeline spec config tests (startDateField, endDateField, groupByField, colorField, scale, backward compat, spec priority) - Enhanced GalleryTimelineSpecConfig tests with cardSize/coverFit/co-existence coverage Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 19b015c commit a1d5b38

5 files changed

Lines changed: 328 additions & 20 deletions

File tree

packages/plugin-list/src/ListView.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -789,19 +789,26 @@ export const ListView: React.FC<ListViewProps> = ({
789789
...(groupingConfig ? { grouping: groupingConfig } : {}),
790790
};
791791
}
792-
case 'timeline':
792+
case 'timeline': {
793+
// Merge spec config over legacy options into nested timeline prop
794+
const mergedTimeline = {
795+
...(schema.options?.timeline || {}),
796+
...(schema.timeline || {}),
797+
};
793798
return {
794799
type: 'object-timeline',
795800
...baseProps,
801+
// Nested timeline config (spec-compliant, used by ObjectTimeline)
802+
timeline: Object.keys(mergedTimeline).length > 0 ? mergedTimeline : undefined,
803+
// Deprecated top-level props for backward compat
796804
startDateField: schema.timeline?.startDateField || schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || 'created_at',
797805
titleField: schema.timeline?.titleField || schema.options?.timeline?.titleField || 'name',
798806
...(schema.timeline?.endDateField ? { endDateField: schema.timeline.endDateField } : {}),
799807
...(schema.timeline?.groupByField ? { groupByField: schema.timeline.groupByField } : {}),
800808
...(schema.timeline?.colorField ? { colorField: schema.timeline.colorField } : {}),
801809
...(schema.timeline?.scale ? { scale: schema.timeline.scale } : {}),
802-
...(schema.options?.timeline || {}),
803-
...(schema.timeline || {}),
804810
};
811+
}
805812
case 'gantt':
806813
return {
807814
type: 'object-gantt',

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ describe('Gallery/Timeline Spec Config Types', () => {
3939
expect(schema.gallery?.visibleFields).toEqual(['status', 'price']);
4040
});
4141

42+
it('accepts all cardSize values', () => {
43+
const sizes = ['small', 'medium', 'large'] as const;
44+
sizes.forEach((cardSize) => {
45+
const schema: ListViewSchema = {
46+
type: 'list-view',
47+
objectName: 'products',
48+
viewType: 'gallery',
49+
fields: ['name'],
50+
gallery: { cardSize },
51+
};
52+
expect(schema.gallery?.cardSize).toBe(cardSize);
53+
});
54+
});
55+
56+
it('accepts all coverFit values', () => {
57+
const fits = ['cover', 'contain', 'fill'] as const;
58+
fits.forEach((coverFit) => {
59+
const schema: ListViewSchema = {
60+
type: 'list-view',
61+
objectName: 'products',
62+
viewType: 'gallery',
63+
fields: ['name'],
64+
gallery: { coverFit },
65+
};
66+
expect(schema.gallery?.coverFit).toBe(coverFit);
67+
});
68+
});
69+
4270
it('accepts legacy imageField and subtitleField alongside spec fields', () => {
4371
const schema: ListViewSchema = {
4472
type: 'list-view',
@@ -143,4 +171,33 @@ describe('Gallery/Timeline Spec Config Types', () => {
143171
expect(schema.options?.timeline?.dateField).toBe('created_at');
144172
});
145173
});
174+
175+
describe('spec config co-existence', () => {
176+
it('gallery and timeline configs can coexist on the same ListViewSchema', () => {
177+
const schema: ListViewSchema = {
178+
type: 'list-view',
179+
objectName: 'projects',
180+
viewType: 'grid',
181+
fields: ['name', 'date', 'photo'],
182+
gallery: {
183+
coverField: 'photo',
184+
cardSize: 'medium',
185+
titleField: 'name',
186+
visibleFields: ['status'],
187+
},
188+
timeline: {
189+
startDateField: 'start_date',
190+
titleField: 'name',
191+
scale: 'quarter',
192+
groupByField: 'team',
193+
},
194+
};
195+
196+
expect(schema.gallery?.coverField).toBe('photo');
197+
expect(schema.gallery?.cardSize).toBe('medium');
198+
expect(schema.timeline?.startDateField).toBe('start_date');
199+
expect(schema.timeline?.scale).toBe('quarter');
200+
expect(schema.timeline?.groupByField).toBe('team');
201+
});
202+
});
146203
});

packages/plugin-timeline/src/ObjectTimeline.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import React, { useEffect, useState, useCallback } from 'react';
10-
import type { DataSource, TimelineSchema } from '@object-ui/types';
10+
import type { DataSource, TimelineSchema, TimelineConfig } from '@object-ui/types';
1111
import { useDataScope, useNavigationOverlay } from '@object-ui/react';
1212
import { NavigationOverlay } from '@object-ui/components';
1313
import { usePullToRefresh } from '@object-ui/mobile';
@@ -38,19 +38,22 @@ const TimelineExtensionSchema = z.object({
3838
export interface ObjectTimelineProps {
3939
schema: TimelineSchema & {
4040
objectName?: string;
41+
/** Spec-compliant nested timeline config */
42+
timeline?: TimelineConfig;
43+
/** @deprecated Use timeline.titleField instead */
4144
titleField?: string;
42-
/** @deprecated Use startDateField instead */
45+
/** @deprecated Use timeline.startDateField instead */
4346
dateField?: string;
44-
/** Spec-compliant: field name for the start date */
47+
/** @deprecated Use timeline.startDateField instead */
4548
startDateField?: string;
46-
/** Spec-compliant: field name for the end date */
49+
/** @deprecated Use timeline.endDateField instead */
4750
endDateField?: string;
4851
descriptionField?: string;
49-
/** Spec-compliant: field name for grouping timeline items */
52+
/** @deprecated Use timeline.groupByField instead */
5053
groupByField?: string;
51-
/** Spec-compliant: field name for timeline item color */
54+
/** @deprecated Use timeline.colorField instead */
5255
colorField?: string;
53-
/** Spec-compliant: time scale for the timeline display */
56+
/** @deprecated Use timeline.scale instead */
5457
scale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
5558
// Map data fields to timeline item properties
5659
mapping?: {
@@ -125,14 +128,16 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
125128
let effectiveItems = schema.items;
126129

127130
if (!effectiveItems && rawData && Array.isArray(rawData)) {
128-
const titleField = schema.mapping?.title || schema.titleField || 'name';
129-
// Spec-compliant: prefer startDateField, fallback to dateField for backward compat
130-
const startDateField = schema.mapping?.date || schema.startDateField || schema.dateField || 'date';
131-
const endDateField = schema.endDateField || startDateField;
132-
const descField = schema.mapping?.description || schema.descriptionField || 'description';
133-
const variantField = schema.mapping?.variant || 'variant';
134-
const groupByField = schema.groupByField;
135-
const colorField = schema.colorField;
131+
// Resolve TimelineConfig with backwards-compatible fallbacks
132+
const tl = (schema as any).timeline as TimelineConfig | undefined;
133+
const titleField = tl?.titleField ?? schema.mapping?.title ?? schema.titleField ?? 'name';
134+
// Spec-compliant: prefer timeline.startDateField, fallback to flat props
135+
const startDateField = tl?.startDateField ?? schema.mapping?.date ?? schema.startDateField ?? schema.dateField ?? 'date';
136+
const endDateField = tl?.endDateField ?? schema.endDateField ?? startDateField;
137+
const descField = schema.mapping?.description ?? schema.descriptionField ?? 'description';
138+
const variantField = schema.mapping?.variant ?? 'variant';
139+
const groupByField = tl?.groupByField ?? schema.groupByField;
140+
const colorField = tl?.colorField ?? schema.colorField;
136141

137142
effectiveItems = rawData.map(item => ({
138143
title: item[titleField],
@@ -165,10 +170,16 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
165170
onRowClick: onRowClick ?? onItemClick,
166171
});
167172

173+
// Resolve scale: spec timeline.scale takes priority over flat schema.scale
174+
const tl = (schema as any).timeline as TimelineConfig | undefined;
175+
const resolvedScale = tl?.scale ?? schema.scale;
176+
168177
const effectiveSchema = {
169178
...schema,
170179
items: effectiveItems || [],
171180
className: className || schema.className,
181+
// Map spec 'scale' to renderer 'timeScale' (used by gantt variant)
182+
...(resolvedScale ? { timeScale: resolvedScale } : {}),
172183
onItemClick: (item: any) => {
173184
const record = item._data || item;
174185
navigation.handleClick(record);

0 commit comments

Comments
 (0)