@object-ui/plugin-timeline 2.0.0 β 3.0.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +32 -0
- package/dist/index.js +1240 -1210
- package/dist/index.umd.cjs +14 -14
- package/dist/src/ObjectTimeline.d.ts +2 -0
- package/dist/src/ObjectTimeline.d.ts.map +1 -1
- package/dist/src/ObjectTimeline.stories.d.ts +24 -0
- package/dist/src/ObjectTimeline.stories.d.ts.map +1 -0
- package/package.json +11 -10
- package/src/ObjectTimeline.stories.tsx +113 -0
- package/src/ObjectTimeline.test.tsx +15 -2
- package/src/ObjectTimeline.tsx +62 -7
- package/src/renderer.tsx +11 -11
- package/vite.config.ts +3 -1
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
};
|
|
@@ -4,9 +4,22 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
4
4
|
import { ObjectTimeline } from './ObjectTimeline';
|
|
5
5
|
import { DataSource } from '@object-ui/types';
|
|
6
6
|
|
|
7
|
-
// Mock useDataScope
|
|
7
|
+
// Mock useDataScope and useNavigationOverlay
|
|
8
8
|
vi.mock('@object-ui/react', () => ({
|
|
9
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
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@object-ui/components', () => ({
|
|
22
|
+
NavigationOverlay: () => null,
|
|
10
23
|
}));
|
|
11
24
|
|
|
12
25
|
vi.mock('./renderer', () => ({
|
|
@@ -47,7 +60,7 @@ describe('ObjectTimeline', () => {
|
|
|
47
60
|
});
|
|
48
61
|
|
|
49
62
|
it('fetches data when objectName is provided', async () => {
|
|
50
|
-
(mockDataSource.find as any).mockResolvedValue(
|
|
63
|
+
(mockDataSource.find as any).mockResolvedValue(mockData);
|
|
51
64
|
|
|
52
65
|
const schema: any = {
|
|
53
66
|
type: 'timeline',
|
package/src/ObjectTimeline.tsx
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect, useState } from 'react';
|
|
9
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
10
10
|
import type { DataSource, TimelineSchema } from '@object-ui/types';
|
|
11
|
-
import { useDataScope } from '@object-ui/react';
|
|
11
|
+
import { useDataScope, useNavigationOverlay } from '@object-ui/react';
|
|
12
|
+
import { NavigationOverlay } from '@object-ui/components';
|
|
13
|
+
import { usePullToRefresh } from '@object-ui/mobile';
|
|
12
14
|
import { z } from 'zod';
|
|
13
15
|
import { TimelineRenderer } from './renderer';
|
|
14
16
|
|
|
@@ -43,17 +45,22 @@ export interface ObjectTimelineProps {
|
|
|
43
45
|
};
|
|
44
46
|
dataSource?: DataSource;
|
|
45
47
|
className?: string;
|
|
48
|
+
onRowClick?: (record: any) => void;
|
|
49
|
+
onItemClick?: (record: any) => void;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
|
|
49
53
|
schema,
|
|
50
54
|
dataSource,
|
|
51
55
|
className,
|
|
56
|
+
onRowClick,
|
|
57
|
+
onItemClick,
|
|
52
58
|
...props
|
|
53
59
|
}) => {
|
|
54
60
|
const [fetchedData, setFetchedData] = useState<any[]>([]);
|
|
55
61
|
const [loading, setLoading] = useState(false);
|
|
56
62
|
const [error, setError] = useState<Error | null>(null);
|
|
63
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
57
64
|
|
|
58
65
|
useEffect(() => {
|
|
59
66
|
const result = TimelineExtensionSchema.safeParse(schema);
|
|
@@ -73,8 +80,8 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
|
|
|
73
80
|
options: { $top: 100 }
|
|
74
81
|
});
|
|
75
82
|
let data = results;
|
|
76
|
-
if ((results as any).
|
|
77
|
-
data = (results as any).
|
|
83
|
+
if ((results as any).records) {
|
|
84
|
+
data = (results as any).records;
|
|
78
85
|
} else if ((results as any).data) {
|
|
79
86
|
data = (results as any).data;
|
|
80
87
|
}
|
|
@@ -93,7 +100,7 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
|
|
|
93
100
|
if (schema.objectName && !boundData && !schema.items && !(props as any).data) {
|
|
94
101
|
fetchData();
|
|
95
102
|
}
|
|
96
|
-
}, [schema.objectName, dataSource, boundData, schema.items, (props as any).data]);
|
|
103
|
+
}, [schema.objectName, dataSource, boundData, schema.items, (props as any).data, refreshKey]);
|
|
97
104
|
|
|
98
105
|
const rawData = (props as any).data || boundData || fetchedData;
|
|
99
106
|
|
|
@@ -119,10 +126,30 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
|
|
|
119
126
|
}));
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
const handleRefresh = useCallback(async () => {
|
|
130
|
+
setRefreshKey(k => k + 1);
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
|
|
134
|
+
onRefresh: handleRefresh,
|
|
135
|
+
enabled: !!schema.objectName && !!dataSource,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const navigation = useNavigationOverlay({
|
|
139
|
+
navigation: (schema as any).navigation,
|
|
140
|
+
objectName: schema.objectName,
|
|
141
|
+
onRowClick: onRowClick ?? onItemClick,
|
|
142
|
+
});
|
|
143
|
+
|
|
122
144
|
const effectiveSchema = {
|
|
123
145
|
...schema,
|
|
124
146
|
items: effectiveItems || [],
|
|
125
|
-
className: className || schema.className
|
|
147
|
+
className: className || schema.className,
|
|
148
|
+
onItemClick: (item: any) => {
|
|
149
|
+
const record = item._data || item;
|
|
150
|
+
navigation.handleClick(record);
|
|
151
|
+
onItemClick?.(record);
|
|
152
|
+
},
|
|
126
153
|
};
|
|
127
154
|
|
|
128
155
|
if (error) {
|
|
@@ -133,5 +160,33 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
|
|
|
133
160
|
);
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
return
|
|
163
|
+
return (
|
|
164
|
+
<div ref={pullRef} className="relative overflow-auto h-full">
|
|
165
|
+
{pullDistance > 0 && (
|
|
166
|
+
<div
|
|
167
|
+
className="flex items-center justify-center text-xs text-muted-foreground"
|
|
168
|
+
style={{ height: pullDistance }}
|
|
169
|
+
>
|
|
170
|
+
{isRefreshing ? 'Refreshingβ¦' : 'Pull to refresh'}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
<TimelineRenderer schema={effectiveSchema} />
|
|
174
|
+
{navigation.isOverlay && (
|
|
175
|
+
<NavigationOverlay {...navigation} title="Timeline Item">
|
|
176
|
+
{(record) => (
|
|
177
|
+
<div className="space-y-3">
|
|
178
|
+
{Object.entries(record).map(([key, value]) => (
|
|
179
|
+
<div key={key} className="flex flex-col">
|
|
180
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
181
|
+
{key.replace(/_/g, ' ')}
|
|
182
|
+
</span>
|
|
183
|
+
<span className="text-sm">{String(value ?? 'β')}</span>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</NavigationOverlay>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
137
192
|
}
|
package/src/renderer.tsx
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
TimelineGanttBar,
|
|
28
28
|
TimelineGanttBarContent,
|
|
29
29
|
} from './index';
|
|
30
|
-
import { renderChildren } from '@object-ui/components';
|
|
30
|
+
import { renderChildren, cn } from '@object-ui/components';
|
|
31
31
|
|
|
32
32
|
// Constants
|
|
33
33
|
const MILLISECONDS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -98,7 +98,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
98
98
|
return (
|
|
99
99
|
<Timeline className={className} {...props}>
|
|
100
100
|
{items.map((item: any, index: number) => (
|
|
101
|
-
<TimelineItem key={index} className={item.className}>
|
|
101
|
+
<TimelineItem key={index} className={cn(item.className, onItemClick && 'cursor-pointer')} onClick={() => onItemClick?.(item)}>
|
|
102
102
|
<TimelineMarker variant={item.variant || 'default'}>
|
|
103
103
|
{item.icon && <span className="text-xs">{item.icon}</span>}
|
|
104
104
|
</TimelineMarker>
|
|
@@ -110,7 +110,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
110
110
|
)}
|
|
111
111
|
{item.title && <TimelineTitle>{item.title}</TimelineTitle>}
|
|
112
112
|
{item.description && (
|
|
113
|
-
<TimelineDescription>{item.description}</TimelineDescription>
|
|
113
|
+
<TimelineDescription className="line-clamp-2 sm:line-clamp-none">{item.description}</TimelineDescription>
|
|
114
114
|
)}
|
|
115
115
|
{item.content && renderChildren(item.content)}
|
|
116
116
|
</TimelineContent>
|
|
@@ -123,9 +123,9 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
123
123
|
// Horizontal Timeline
|
|
124
124
|
if (variant === 'horizontal') {
|
|
125
125
|
return (
|
|
126
|
-
<TimelineHorizontal className={className} {...props}>
|
|
126
|
+
<TimelineHorizontal className={cn("overflow-x-auto [-webkit-overflow-scrolling:touch]", className)} {...props}>
|
|
127
127
|
{items.map((item: any, index: number) => (
|
|
128
|
-
<TimelineHorizontalItem key={index} className={item.className}>
|
|
128
|
+
<TimelineHorizontalItem key={index} className={cn(item.className, onItemClick && 'cursor-pointer')} onClick={() => onItemClick?.(item)}>
|
|
129
129
|
<div className="flex flex-col items-center">
|
|
130
130
|
<TimelineMarker variant={item.variant || 'default'}>
|
|
131
131
|
{item.icon && <span className="text-xs">{item.icon}</span>}
|
|
@@ -138,7 +138,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
138
138
|
)}
|
|
139
139
|
{item.title && <TimelineTitle>{item.title}</TimelineTitle>}
|
|
140
140
|
{item.description && (
|
|
141
|
-
<TimelineDescription className="text-center">
|
|
141
|
+
<TimelineDescription className="text-center line-clamp-2 sm:line-clamp-none">
|
|
142
142
|
{item.description}
|
|
143
143
|
</TimelineDescription>
|
|
144
144
|
)}
|
|
@@ -208,11 +208,11 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
208
208
|
const timeHeaders = generateTimeHeaders();
|
|
209
209
|
|
|
210
210
|
return (
|
|
211
|
-
<TimelineGantt className={className} {...props}>
|
|
211
|
+
<TimelineGantt className={cn("overflow-x-auto [-webkit-overflow-scrolling:touch]", className)} {...props}>
|
|
212
212
|
{/* Header */}
|
|
213
213
|
<TimelineGanttHeader>
|
|
214
|
-
<TimelineGanttRowLabels className="flex items-center px-4 py-3">
|
|
215
|
-
<span className="font-semibold text-sm">
|
|
214
|
+
<TimelineGanttRowLabels className="flex items-center px-2 sm:px-4 py-2 sm:py-3">
|
|
215
|
+
<span className="font-semibold text-xs sm:text-sm">
|
|
216
216
|
{schema.rowLabel || 'Items'}
|
|
217
217
|
</span>
|
|
218
218
|
</TimelineGanttRowLabels>
|
|
@@ -221,7 +221,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
221
221
|
{timeHeaders.map((header, index) => (
|
|
222
222
|
<div
|
|
223
223
|
key={index}
|
|
224
|
-
className="flex-1 px-2 py-3 border-r text-xs font-medium text-center"
|
|
224
|
+
className="flex-1 px-1 sm:px-2 py-2 sm:py-3 border-r text-xs font-medium text-center"
|
|
225
225
|
>
|
|
226
226
|
{header}
|
|
227
227
|
</div>
|
|
@@ -236,7 +236,7 @@ export const TimelineRenderer = ({ schema, className, ...props }: { schema: Time
|
|
|
236
236
|
<TimelineGanttRowLabels>
|
|
237
237
|
{items.map((row: any, rowIndex: number) => (
|
|
238
238
|
<TimelineGanttRow key={rowIndex}>
|
|
239
|
-
<TimelineGanttLabel title={row.label}>
|
|
239
|
+
<TimelineGanttLabel title={row.label} className="truncate">
|
|
240
240
|
{row.label}
|
|
241
241
|
</TimelineGanttLabel>
|
|
242
242
|
</TimelineGanttRow>
|
package/vite.config.ts
CHANGED
|
@@ -29,6 +29,7 @@ export default defineConfig({
|
|
|
29
29
|
'@object-ui/react': resolve(__dirname, '../react/src'),
|
|
30
30
|
'@object-ui/components': resolve(__dirname, '../components/src'),
|
|
31
31
|
'@object-ui/fields': resolve(__dirname, '../fields/src'),
|
|
32
|
+
'@object-ui/mobile': resolve(__dirname, '../mobile/src'),
|
|
32
33
|
'@object-ui/plugin-dashboard': resolve(__dirname, '../plugin-dashboard/src'),
|
|
33
34
|
'@object-ui/plugin-grid': resolve(__dirname, '../plugin-grid/src'),
|
|
34
35
|
},
|
|
@@ -40,7 +41,7 @@ export default defineConfig({
|
|
|
40
41
|
fileName: 'index',
|
|
41
42
|
},
|
|
42
43
|
rollupOptions: {
|
|
43
|
-
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react'],
|
|
44
|
+
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', '@object-ui/mobile'],
|
|
44
45
|
output: {
|
|
45
46
|
globals: {
|
|
46
47
|
react: 'React',
|
|
@@ -48,6 +49,7 @@ export default defineConfig({
|
|
|
48
49
|
'@object-ui/components': 'ObjectUIComponents',
|
|
49
50
|
'@object-ui/core': 'ObjectUICore',
|
|
50
51
|
'@object-ui/react': 'ObjectUIReact',
|
|
52
|
+
'@object-ui/mobile': 'ObjectUIMobile',
|
|
51
53
|
},
|
|
52
54
|
},
|
|
53
55
|
},
|