@object-ui/plugin-timeline 3.1.5 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +20 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1041 -751
  5. package/dist/index.umd.cjs +31 -6
  6. package/dist/packages/plugin-timeline/src/ObjectTimeline.d.ts.map +1 -0
  7. package/dist/packages/plugin-timeline/src/ObjectTimeline.stories.d.ts.map +1 -0
  8. package/dist/packages/plugin-timeline/src/index.d.ts.map +1 -0
  9. package/dist/packages/plugin-timeline/src/renderer.d.ts.map +1 -0
  10. package/package.json +36 -13
  11. package/.turbo/turbo-build.log +0 -22
  12. package/dist/src/ObjectTimeline.d.ts.map +0 -1
  13. package/dist/src/ObjectTimeline.stories.d.ts.map +0 -1
  14. package/dist/src/index.d.ts.map +0 -1
  15. package/dist/src/renderer.d.ts.map +0 -1
  16. package/src/ObjectTimeline.msw.test.tsx +0 -80
  17. package/src/ObjectTimeline.stories.tsx +0 -113
  18. package/src/ObjectTimeline.test.tsx +0 -138
  19. package/src/ObjectTimeline.tsx +0 -241
  20. package/src/__tests__/ObjectTimeline.spec-config.test.tsx +0 -232
  21. package/src/__tests__/renderer.test.tsx +0 -295
  22. package/src/__tests__/timeline-variants.test.tsx +0 -150
  23. package/src/index.test.tsx +0 -24
  24. package/src/index.tsx +0 -334
  25. package/src/renderer.tsx +0 -477
  26. package/tsconfig.json +0 -18
  27. package/vite.config.ts +0 -63
  28. package/vitest.config.ts +0 -13
  29. package/vitest.setup.ts +0 -1
  30. /package/dist/{src → packages/plugin-timeline/src}/ObjectTimeline.d.ts +0 -0
  31. /package/dist/{src → packages/plugin-timeline/src}/ObjectTimeline.stories.d.ts +0 -0
  32. /package/dist/{src → packages/plugin-timeline/src}/index.d.ts +0 -0
  33. /package/dist/{src → packages/plugin-timeline/src}/renderer.d.ts +0 -0
@@ -1,80 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import { ObjectTimeline } from './ObjectTimeline';
4
- import { ObjectStackAdapter } from '@object-ui/data-objectstack';
5
- import { setupServer } from 'msw/node';
6
- import { http, HttpResponse } from 'msw';
7
- import React from 'react';
8
-
9
- const BASE_URL = 'http://localhost';
10
-
11
- // --- Mock Data ---
12
-
13
- const mockMilestones = {
14
- value: [
15
- { id: '1', title: 'Start Project', due_date: '2023-01-01', description: 'Initial Kickoff' },
16
- { id: '2', title: 'Beta Launch', due_date: '2023-06-01', description: 'Public Beta' },
17
- { id: '3', title: 'Release', due_date: '2023-12-01', description: 'v1.0 Release' }
18
- ]
19
- };
20
-
21
- // --- MSW Setup ---
22
-
23
- const handlers = [
24
- http.options('*', () => {
25
- return new HttpResponse(null, { status: 200 });
26
- }),
27
-
28
- http.get(`${BASE_URL}/api/v1`, () => {
29
- return HttpResponse.json({ status: 'ok', version: '1.0.0' });
30
- }),
31
-
32
- http.get(`${BASE_URL}/api/v1/data/milestones`, () => {
33
- return HttpResponse.json(mockMilestones);
34
- })
35
- ];
36
-
37
- const server = setupServer(...handlers);
38
-
39
- // --- Test Suite ---
40
-
41
- describe('ObjectTimeline with MSW', () => {
42
- beforeAll(() => server.listen());
43
- afterEach(() => server.resetHandlers());
44
- afterAll(() => server.close());
45
-
46
- it('fetches and renders timeline items from object data', async () => {
47
- const adapter = new ObjectStackAdapter({
48
- baseUrl: BASE_URL,
49
- bucket: 'test-bucket'
50
- });
51
-
52
- const schema = {
53
- type: 'timeline',
54
- objectName: 'milestones',
55
- variant: 'vertical',
56
- titleField: 'title',
57
- dateField: 'due_date',
58
- descriptionField: 'description'
59
- };
60
-
61
- render(
62
- <ObjectTimeline
63
- // @ts-expect-error - Mock schema type mismatch with TimelineSchema
64
- schema={schema}
65
- dataSource={adapter}
66
- />
67
- );
68
-
69
- // Wait for items to appear
70
- await waitFor(() => {
71
- expect(screen.getByText('Start Project')).toBeInTheDocument();
72
- });
73
-
74
- expect(screen.getByText('Beta Launch')).toBeInTheDocument();
75
- expect(screen.getByText('Release')).toBeInTheDocument();
76
-
77
- // Check descriptions if rendered
78
- expect(screen.getByText('Initial Kickoff')).toBeInTheDocument();
79
- });
80
- });
@@ -1,113 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { SchemaRenderer } from '@object-ui/react';
3
- import type { BaseSchema } from '@object-ui/types';
4
-
5
- const meta = {
6
- title: 'Plugins/ObjectTimeline',
7
- component: SchemaRenderer,
8
- parameters: {
9
- layout: 'padded',
10
- },
11
- tags: ['autodocs'],
12
- argTypes: {
13
- schema: { table: { disable: true } },
14
- },
15
- } satisfies Meta<any>;
16
-
17
- export default meta;
18
- type Story = StoryObj<typeof meta>;
19
-
20
- const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
-
22
- export const Default: Story = {
23
- render: renderStory,
24
- args: {
25
- type: 'timeline',
26
- variant: 'vertical',
27
- dateFormat: 'short',
28
- items: [
29
- {
30
- time: '2024-01-10',
31
- title: 'Project Kickoff',
32
- description: 'Initial planning and team formation',
33
- variant: 'success',
34
- icon: '🚀',
35
- },
36
- {
37
- time: '2024-02-15',
38
- title: 'Alpha Release',
39
- description: 'Internal alpha build delivered',
40
- variant: 'info',
41
- icon: '📦',
42
- },
43
- {
44
- time: '2024-03-20',
45
- title: 'Beta Launch',
46
- description: 'Public beta opened to early adopters',
47
- variant: 'warning',
48
- icon: '⚡',
49
- },
50
- {
51
- time: '2024-05-01',
52
- title: 'General Availability',
53
- description: 'Official public release',
54
- variant: 'success',
55
- icon: '🎉',
56
- },
57
- ],
58
- } as any,
59
- };
60
-
61
- export const Horizontal: Story = {
62
- render: renderStory,
63
- args: {
64
- type: 'timeline',
65
- variant: 'horizontal',
66
- dateFormat: 'short',
67
- items: [
68
- { time: '2024-01-01', title: 'Q1', description: 'Foundation phase', variant: 'default' },
69
- { time: '2024-04-01', title: 'Q2', description: 'Growth phase', variant: 'info' },
70
- { time: '2024-07-01', title: 'Q3', description: 'Scale phase', variant: 'warning' },
71
- { time: '2024-10-01', title: 'Q4', description: 'Optimization phase', variant: 'success' },
72
- ],
73
- } as any,
74
- };
75
-
76
- export const ActivityFeed: Story = {
77
- render: renderStory,
78
- args: {
79
- type: 'timeline',
80
- variant: 'vertical',
81
- dateFormat: 'long',
82
- items: [
83
- {
84
- time: '2024-03-15T14:30:00',
85
- title: 'Deal Updated',
86
- description: 'Stage changed from Proposal to Negotiation',
87
- variant: 'info',
88
- icon: '📝',
89
- },
90
- {
91
- time: '2024-03-14T10:00:00',
92
- title: 'Meeting Logged',
93
- description: 'Product demo with stakeholders',
94
- variant: 'default',
95
- icon: '📅',
96
- },
97
- {
98
- time: '2024-03-12T16:45:00',
99
- title: 'Email Sent',
100
- description: 'Follow-up proposal sent to client',
101
- variant: 'default',
102
- icon: '📧',
103
- },
104
- {
105
- time: '2024-03-10T09:00:00',
106
- title: 'Deal Created',
107
- description: 'New opportunity worth $150,000',
108
- variant: 'success',
109
- icon: '✨',
110
- },
111
- ],
112
- } as any,
113
- };
@@ -1,138 +0,0 @@
1
- import React from 'react';
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import { describe, it, expect, vi } from 'vitest';
4
- import { ObjectTimeline } from './ObjectTimeline';
5
- import { DataSource } from '@object-ui/types';
6
-
7
- // Mock useDataScope and useNavigationOverlay
8
- vi.mock('@object-ui/react', () => ({
9
- useDataScope: () => undefined,
10
- useNavigationOverlay: () => ({
11
- isOverlay: false,
12
- handleClick: vi.fn(),
13
- selectedRecord: null,
14
- isOpen: false,
15
- close: vi.fn(),
16
- setIsOpen: vi.fn(),
17
- mode: 'overlay',
18
- view: undefined,
19
- }),
20
- }));
21
-
22
- vi.mock('@object-ui/components', () => ({
23
- NavigationOverlay: () => null,
24
- }));
25
-
26
- vi.mock('./renderer', () => ({
27
- TimelineRenderer: ({ schema }: any) => (
28
- <div data-testid="timeline-view">
29
- {schema.items?.map((t: any, idx: number) => (
30
- <div key={idx} data-testid="timeline-item">{t.title}</div>
31
- ))}
32
- </div>
33
- ),
34
- }));
35
-
36
- const mockData = [
37
- { id: '1', name: 'Event 1', date: '2024-01-01', description: 'Desc 1' },
38
- ];
39
-
40
- const mockDataSource: DataSource = {
41
- find: vi.fn(),
42
- findOne: vi.fn(),
43
- create: vi.fn(),
44
- update: vi.fn(),
45
- delete: vi.fn(),
46
- getObjectSchema: vi.fn(),
47
- };
48
-
49
- describe('ObjectTimeline', () => {
50
- it('renders with static items', async () => {
51
- const schema: any = {
52
- type: 'timeline',
53
- items: [
54
- { title: 'Static Event', date: '2024-01-01' }
55
- ]
56
- };
57
- render(<ObjectTimeline schema={schema} />);
58
- await waitFor(() => {
59
- expect(screen.getByText('Static Event')).toBeDefined();
60
- });
61
- });
62
-
63
- it('fetches data when objectName is provided', async () => {
64
- (mockDataSource.find as any).mockResolvedValue(mockData);
65
-
66
- const schema: any = {
67
- type: 'timeline',
68
- objectName: 'events',
69
- titleField: 'name',
70
- dateField: 'date' // Backward-compat: legacy dateField still works
71
- };
72
-
73
- render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
74
-
75
- await waitFor(() => {
76
- expect(mockDataSource.find).toHaveBeenCalled();
77
- });
78
-
79
- await waitFor(() => {
80
- expect(screen.getByText('Event 1')).toBeDefined();
81
- });
82
- });
83
-
84
- it('uses spec-compliant startDateField property', async () => {
85
- const dataWithDates = [
86
- { id: '1', name: 'Sprint 1', start_date: '2024-01-01', end_date: '2024-01-14', team: 'Alpha' },
87
- ];
88
- (mockDataSource.find as any).mockResolvedValue(dataWithDates);
89
-
90
- const schema: any = {
91
- type: 'timeline',
92
- objectName: 'sprints',
93
- titleField: 'name',
94
- startDateField: 'start_date',
95
- endDateField: 'end_date',
96
- };
97
-
98
- render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
99
-
100
- await waitFor(() => {
101
- expect(screen.getByText('Sprint 1')).toBeDefined();
102
- });
103
- });
104
-
105
- it('supports groupByField and colorField properties', async () => {
106
- const dataWithGroups = [
107
- { id: '1', name: 'Task A', start_date: '2024-01-01', team: 'Alpha', priority: 'high' },
108
- ];
109
- (mockDataSource.find as any).mockResolvedValue(dataWithGroups);
110
-
111
- const schema: any = {
112
- type: 'timeline',
113
- objectName: 'tasks',
114
- titleField: 'name',
115
- startDateField: 'start_date',
116
- groupByField: 'team',
117
- colorField: 'priority',
118
- };
119
-
120
- render(<ObjectTimeline schema={schema} dataSource={mockDataSource} />);
121
-
122
- await waitFor(() => {
123
- expect(screen.getByText('Task A')).toBeDefined();
124
- });
125
- });
126
-
127
- it('supports scale property', () => {
128
- const schema: any = {
129
- type: 'timeline',
130
- items: [
131
- { title: 'Weekly Event', date: '2024-01-01' }
132
- ],
133
- scale: 'week',
134
- };
135
- render(<ObjectTimeline schema={schema} />);
136
- expect(screen.getByText('Weekly Event')).toBeDefined();
137
- });
138
- });
@@ -1,241 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import React, { useEffect, useState, useCallback } from 'react';
10
- import type { DataSource, TimelineSchema, TimelineConfig } from '@object-ui/types';
11
- import { useDataScope, useNavigationOverlay } from '@object-ui/react';
12
- import { NavigationOverlay } from '@object-ui/components';
13
- import { extractRecords, buildExpandFields } from '@object-ui/core';
14
- import { usePullToRefresh } from '@object-ui/mobile';
15
- import { z } from 'zod';
16
- import { TimelineRenderer } from './renderer';
17
-
18
- const TimelineMappingSchema = z.object({
19
- title: z.string().optional(),
20
- date: z.string().optional(),
21
- description: z.string().optional(),
22
- variant: z.string().optional(),
23
- });
24
-
25
- const TimelineExtensionSchema = z.object({
26
- mapping: TimelineMappingSchema.optional(),
27
- objectName: z.string().optional(),
28
- titleField: z.string().optional(),
29
- /** @deprecated Use startDateField instead */
30
- dateField: z.string().optional(),
31
- startDateField: z.string().optional(),
32
- endDateField: z.string().optional(),
33
- descriptionField: z.string().optional(),
34
- groupByField: z.string().optional(),
35
- colorField: z.string().optional(),
36
- scale: z.enum(['hour', 'day', 'week', 'month', 'quarter', 'year']).optional(),
37
- });
38
-
39
- export interface ObjectTimelineProps {
40
- schema: TimelineSchema & {
41
- objectName?: string;
42
- /** Spec-compliant nested timeline config */
43
- timeline?: TimelineConfig;
44
- /** @deprecated Use timeline.titleField instead */
45
- titleField?: string;
46
- /** @deprecated Use timeline.startDateField instead */
47
- dateField?: string;
48
- /** @deprecated Use timeline.startDateField instead */
49
- startDateField?: string;
50
- /** @deprecated Use timeline.endDateField instead */
51
- endDateField?: string;
52
- descriptionField?: string;
53
- /** @deprecated Use timeline.groupByField instead */
54
- groupByField?: string;
55
- /** @deprecated Use timeline.colorField instead */
56
- colorField?: string;
57
- /** @deprecated Use timeline.scale instead */
58
- scale?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
59
- // Map data fields to timeline item properties
60
- mapping?: {
61
- title?: string;
62
- date?: string;
63
- description?: string;
64
- variant?: string;
65
- }
66
- };
67
- dataSource?: DataSource;
68
- className?: string;
69
- onRowClick?: (record: any) => void;
70
- onItemClick?: (record: any) => void;
71
- }
72
-
73
- export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
74
- schema,
75
- dataSource,
76
- className,
77
- onRowClick,
78
- onItemClick,
79
- ...props
80
- }) => {
81
- const [fetchedData, setFetchedData] = useState<any[]>([]);
82
- const [loading, setLoading] = useState(false);
83
- const [error, setError] = useState<Error | null>(null);
84
- const [refreshKey, setRefreshKey] = useState(0);
85
- const [objectDef, setObjectDef] = useState<any>(null);
86
-
87
- // Resolve nested TimelineConfig (spec-compliant)
88
- const timelineConfig = schema.timeline;
89
-
90
- useEffect(() => {
91
- const result = TimelineExtensionSchema.safeParse(schema);
92
- if (!result.success) {
93
- console.warn(`[ObjectTimeline] Invalid timeline configuration:`, result.error.format());
94
- }
95
- }, [schema]);
96
-
97
- const boundData = useDataScope(schema.bind);
98
-
99
- // Fetch object definition for metadata
100
- useEffect(() => {
101
- let isMounted = true;
102
- const fetchMeta = async () => {
103
- if (!dataSource || typeof dataSource.getObjectSchema !== 'function' || !schema.objectName) return;
104
- try {
105
- const def = await dataSource.getObjectSchema(schema.objectName);
106
- if (isMounted) setObjectDef(def);
107
- } catch (e) {
108
- console.warn('Failed to fetch object def for ObjectTimeline', e);
109
- }
110
- };
111
- fetchMeta();
112
- return () => { isMounted = false; };
113
- }, [schema.objectName, dataSource]);
114
-
115
- useEffect(() => {
116
- const fetchData = async () => {
117
- if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
118
- setLoading(true);
119
- try {
120
- // Auto-inject $expand for lookup/master_detail fields
121
- const expand = buildExpandFields(objectDef?.fields);
122
- const results = await dataSource.find(schema.objectName, {
123
- options: { $top: 100 },
124
- ...(expand.length > 0 ? { $expand: expand } : {}),
125
- });
126
- const data = extractRecords(results);
127
- setFetchedData(data);
128
- } catch (e) {
129
- console.error(e);
130
- setError(e as Error);
131
- } finally {
132
- setLoading(false);
133
- }
134
- };
135
-
136
- if (schema.objectName && !boundData && !schema.items && !(props as any).data) {
137
- fetchData();
138
- }
139
- }, [schema.objectName, dataSource, boundData, schema.items, (props as any).data, refreshKey, objectDef]);
140
-
141
- const rawData = (props as any).data || boundData || fetchedData;
142
-
143
- // Transform data to items if we have raw data and no explicit items
144
- let effectiveItems = schema.items;
145
-
146
- if (!effectiveItems && rawData && Array.isArray(rawData)) {
147
- // Resolve TimelineConfig with backwards-compatible fallbacks
148
- const titleField = timelineConfig?.titleField ?? schema.mapping?.title ?? schema.titleField ?? 'name';
149
- // Spec-compliant: prefer timeline.startDateField, fallback to flat props
150
- const startDateField = timelineConfig?.startDateField ?? schema.mapping?.date ?? schema.startDateField ?? schema.dateField ?? 'date';
151
- const endDateField = timelineConfig?.endDateField ?? schema.endDateField ?? startDateField;
152
- const descField = schema.mapping?.description ?? schema.descriptionField ?? 'description';
153
- const variantField = schema.mapping?.variant ?? 'variant';
154
- const groupByField = timelineConfig?.groupByField ?? schema.groupByField;
155
- const colorField = timelineConfig?.colorField ?? schema.colorField;
156
-
157
- effectiveItems = rawData.map(item => ({
158
- title: item[titleField],
159
- // Support both 'time' (vertical) and 'startDate' (gantt)
160
- time: item[startDateField],
161
- startDate: item[startDateField],
162
- endDate: item[endDateField],
163
- description: item[descField],
164
- variant: item[variantField] || 'default',
165
- // Spec-compliant: group and color support
166
- ...(groupByField ? { group: item[groupByField] } : {}),
167
- ...(colorField ? { color: item[colorField] } : {}),
168
- // Pass original item for click handlers
169
- _data: item
170
- }));
171
- }
172
-
173
- const handleRefresh = useCallback(async () => {
174
- setRefreshKey(k => k + 1);
175
- }, []);
176
-
177
- const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
178
- onRefresh: handleRefresh,
179
- enabled: !!schema.objectName && !!dataSource,
180
- });
181
-
182
- const navigation = useNavigationOverlay({
183
- navigation: (schema as any).navigation,
184
- objectName: schema.objectName,
185
- onRowClick: onRowClick ?? onItemClick,
186
- });
187
-
188
- // Resolve scale: spec timeline.scale takes priority over flat schema.scale
189
- const resolvedScale = timelineConfig?.scale ?? schema.scale;
190
-
191
- const effectiveSchema = {
192
- ...schema,
193
- items: effectiveItems || [],
194
- className: className || schema.className,
195
- // Map spec 'scale' to renderer 'timeScale' (used by gantt variant)
196
- ...(resolvedScale ? { timeScale: resolvedScale } : {}),
197
- onItemClick: (item: any) => {
198
- const record = item._data || item;
199
- navigation.handleClick(record);
200
- onItemClick?.(record);
201
- },
202
- };
203
-
204
- if (error) {
205
- return (
206
- <div className="p-4 text-red-500">
207
- Error loading timeline: {error.message}
208
- </div>
209
- );
210
- }
211
-
212
- return (
213
- <div ref={pullRef} className="relative overflow-auto h-full min-w-0">
214
- {pullDistance > 0 && (
215
- <div
216
- className="flex items-center justify-center text-xs text-muted-foreground"
217
- style={{ height: pullDistance }}
218
- >
219
- {isRefreshing ? 'Refreshing…' : 'Pull to refresh'}
220
- </div>
221
- )}
222
- <TimelineRenderer schema={effectiveSchema} />
223
- {navigation.isOverlay && (
224
- <NavigationOverlay {...navigation} title="Timeline Item">
225
- {(record) => (
226
- <div className="space-y-3">
227
- {Object.entries(record).map(([key, value]) => (
228
- <div key={key} className="flex flex-col">
229
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
230
- {key.replace(/_/g, ' ')}
231
- </span>
232
- <span className="text-sm">{String(value ?? '—')}</span>
233
- </div>
234
- ))}
235
- </div>
236
- )}
237
- </NavigationOverlay>
238
- )}
239
- </div>
240
- );
241
- }