@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.
- package/CHANGELOG.md +31 -0
- package/README.md +20 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1041 -751
- package/dist/index.umd.cjs +31 -6
- package/dist/packages/plugin-timeline/src/ObjectTimeline.d.ts.map +1 -0
- package/dist/packages/plugin-timeline/src/ObjectTimeline.stories.d.ts.map +1 -0
- package/dist/packages/plugin-timeline/src/index.d.ts.map +1 -0
- package/dist/packages/plugin-timeline/src/renderer.d.ts.map +1 -0
- package/package.json +36 -13
- package/.turbo/turbo-build.log +0 -22
- package/dist/src/ObjectTimeline.d.ts.map +0 -1
- package/dist/src/ObjectTimeline.stories.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/renderer.d.ts.map +0 -1
- package/src/ObjectTimeline.msw.test.tsx +0 -80
- package/src/ObjectTimeline.stories.tsx +0 -113
- package/src/ObjectTimeline.test.tsx +0 -138
- package/src/ObjectTimeline.tsx +0 -241
- package/src/__tests__/ObjectTimeline.spec-config.test.tsx +0 -232
- package/src/__tests__/renderer.test.tsx +0 -295
- package/src/__tests__/timeline-variants.test.tsx +0 -150
- package/src/index.test.tsx +0 -24
- package/src/index.tsx +0 -334
- package/src/renderer.tsx +0 -477
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -63
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-timeline/src}/ObjectTimeline.d.ts +0 -0
- /package/dist/{src → packages/plugin-timeline/src}/ObjectTimeline.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-timeline/src}/index.d.ts +0 -0
- /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
|
-
});
|
package/src/ObjectTimeline.tsx
DELETED
|
@@ -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
|
-
}
|