@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.
@@ -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({ value: mockData });
63
+ (mockDataSource.find as any).mockResolvedValue(mockData);
51
64
 
52
65
  const schema: any = {
53
66
  type: 'timeline',
@@ -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).value) {
77
- data = (results as any).value;
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 <TimelineRenderer schema={effectiveSchema} />;
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
  },