@object-ui/plugin-gantt 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.
@@ -1,88 +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 { ObjectGantt } from './ObjectGantt';
5
- import { DataSource } from '@object-ui/types';
6
-
7
- // Mock GanttView to avoid complex rendering in tests
8
- vi.mock('./GanttView', () => ({
9
- GanttView: ({ tasks }: any) => (
10
- <div data-testid="gantt-view">
11
- {tasks.map((t: any) => (
12
- <div key={t.id} data-testid="gantt-task">{t.title}</div>
13
- ))}
14
- </div>
15
- ),
16
- }));
17
-
18
- const mockData = [
19
- { id: '1', name: 'Task 1', start_date: '2024-01-01', end_date: '2024-01-05', progress: 50 },
20
- { id: '2', name: 'Task 2', start_date: '2024-01-06', end_date: '2024-01-10', progress: 0 },
21
- ];
22
-
23
- const mockDataSource: DataSource = {
24
- find: vi.fn(),
25
- findOne: vi.fn(),
26
- create: vi.fn(),
27
- update: vi.fn(),
28
- delete: vi.fn(),
29
- getObjectSchema: vi.fn().mockResolvedValue({
30
- fields: {
31
- name: { type: 'text' },
32
- start_date: { type: 'date' },
33
- end_date: { type: 'date' }
34
- }
35
- }),
36
- };
37
-
38
- describe('ObjectGantt', () => {
39
- it('renders with static value provider', async () => {
40
- const schema: any = {
41
- type: 'gantt',
42
- gantt: {
43
- titleField: 'name',
44
- startDateField: 'start_date',
45
- endDateField: 'end_date',
46
- },
47
- data: {
48
- provider: 'value',
49
- items: mockData,
50
- },
51
- };
52
-
53
- render(<ObjectGantt schema={schema} />);
54
-
55
- // Check loading first if needed, or wait for tasks
56
- await waitFor(() => {
57
- expect(screen.getByTestId('gantt-view')).toBeDefined();
58
- });
59
-
60
- expect(screen.getAllByTestId('gantt-task')).toHaveLength(2);
61
- expect(screen.getByText('Task 1')).toBeDefined();
62
- });
63
-
64
- it('renders with object provider', async () => {
65
- (mockDataSource.find as any).mockResolvedValue({ data: mockData });
66
-
67
- const schema: any = {
68
- type: 'gantt',
69
- gantt: {
70
- titleField: 'name',
71
- startDateField: 'start_date',
72
- endDateField: 'end_date',
73
- },
74
- data: {
75
- provider: 'object',
76
- object: 'tasks',
77
- },
78
- };
79
-
80
- render(<ObjectGantt schema={schema} dataSource={mockDataSource} />);
81
-
82
- await waitFor(() => {
83
- expect(mockDataSource.find).toHaveBeenCalledWith('tasks', expect.any(Object));
84
- });
85
-
86
- expect(screen.getAllByTestId('gantt-task')).toHaveLength(2);
87
- });
88
- });
@@ -1,324 +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
- /**
10
- * ObjectGantt Component
11
- *
12
- * A specialized Gantt chart component that works with ObjectQL data sources.
13
- * Displays tasks with date ranges, progress, and dependencies.
14
- * Implements the gantt view type from @objectstack/spec view.zod ListView schema.
15
- *
16
- * Features:
17
- * - Gantt chart timeline visualization
18
- * - Task progress tracking (0-100%)
19
- * - Task dependencies visualization
20
- * - Date range display
21
- * - Auto-scrolling timeline
22
- * - Works with object/api/value data providers
23
- */
24
-
25
- import React, { useEffect, useState, useMemo } from 'react';
26
- import type { ObjectGridSchema, DataSource, ViewData, GanttConfig } from '@object-ui/types';
27
- import { GanttConfigSchema } from '@objectstack/spec/ui';
28
- import { useNavigationOverlay } from '@object-ui/react';
29
- import { NavigationOverlay } from '@object-ui/components';
30
- import { extractRecords, buildExpandFields } from '@object-ui/core';
31
- import { GanttView, type GanttTask } from './GanttView';
32
-
33
- export interface ObjectGanttProps {
34
- schema: ObjectGridSchema;
35
- dataSource?: DataSource;
36
- className?: string;
37
- onTaskClick?: (record: any) => void;
38
- onRowClick?: (record: any) => void;
39
- onEdit?: (record: any) => void;
40
- onDelete?: (record: any) => void;
41
- }
42
-
43
- /**
44
- * Helper to get data configuration from schema
45
- */
46
- function getDataConfig(schema: ObjectGridSchema): ViewData | null {
47
- if (schema.data) {
48
- return schema.data;
49
- }
50
-
51
- if (schema.staticData) {
52
- return {
53
- provider: 'value',
54
- items: schema.staticData,
55
- };
56
- }
57
-
58
- if (schema.objectName) {
59
- return {
60
- provider: 'object',
61
- object: schema.objectName,
62
- };
63
- }
64
-
65
- return null;
66
- }
67
-
68
- /**
69
- * Helper to convert sort config to QueryParams format
70
- */
71
- function convertSortToQueryParams(sort: string | any[] | undefined): Record<string, 'asc' | 'desc'> | undefined {
72
- if (!sort) return undefined;
73
-
74
- // If it's a string like "name desc"
75
- if (typeof sort === 'string') {
76
- const parts = sort.split(' ');
77
- const field = parts[0];
78
- const order = (parts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc';
79
- return { [field]: order };
80
- }
81
-
82
- // If it's an array of SortConfig objects
83
- if (Array.isArray(sort)) {
84
- return sort.reduce((acc, item) => {
85
- if (item.field && item.order) {
86
- acc[item.field] = item.order;
87
- }
88
- return acc;
89
- }, {} as Record<string, 'asc' | 'desc'>);
90
- }
91
-
92
- return undefined;
93
- }
94
-
95
- /**
96
- * Helper to get gantt configuration from schema
97
- */
98
- function getGanttConfig(schema: ObjectGridSchema | any): GanttConfig | null {
99
- let config: GanttConfig | null = null;
100
-
101
- // 1. Check top-level properties (ObjectGanttSchema style)
102
- if (schema.startDateField && schema.endDateField) {
103
- config = {
104
- startDateField: schema.startDateField,
105
- endDateField: schema.endDateField,
106
- titleField: schema.titleField || 'name',
107
- progressField: schema.progressField,
108
- dependenciesField: schema.dependenciesField || schema.dependencyField,
109
- colorField: schema.colorField
110
- };
111
- return config;
112
- }
113
-
114
- // 2. Check schema.gantt (ObjectGridSchema style)
115
- if (schema.gantt) {
116
- config = schema.gantt as GanttConfig;
117
- }
118
-
119
- if (config) {
120
- const result = GanttConfigSchema.safeParse(config);
121
- if (!result.success) {
122
- console.warn(`[ObjectGantt] Invalid gantt configuration:`, result.error.format());
123
- }
124
- return config;
125
- }
126
-
127
- return null;
128
- }
129
-
130
- export const ObjectGantt: React.FC<ObjectGanttProps> = ({
131
- schema,
132
- dataSource,
133
- className,
134
- onTaskClick,
135
- onRowClick,
136
- ...rest
137
- }) => {
138
- const [data, setData] = useState<any[]>([]);
139
- const [loading, setLoading] = useState(true);
140
- const [error, setError] = useState<Error | null>(null);
141
- const [objectSchema, setObjectSchema] = useState<any>(null);
142
-
143
- const rawDataConfig = getDataConfig(schema);
144
- // Memoize dataConfig using deep comparison to prevent infinite loops
145
- const dataConfig = useMemo(() => {
146
- return rawDataConfig;
147
- }, [JSON.stringify(rawDataConfig)]);
148
-
149
- const ganttConfig = getGanttConfig(schema);
150
- const hasInlineData = dataConfig?.provider === 'value';
151
-
152
- // Fetch data based on provider
153
- useEffect(() => {
154
- const fetchData = async () => {
155
- try {
156
- setLoading(true);
157
- // 1. Check for data prop (Unified ListView)
158
- if ((rest as any).data && Array.isArray((rest as any).data)) {
159
- setData((rest as any).data);
160
- setLoading(false);
161
- return;
162
- }
163
-
164
-
165
- if (hasInlineData && dataConfig?.provider === 'value') {
166
- setData(dataConfig.items as any[]);
167
- setLoading(false);
168
- return;
169
- }
170
-
171
- if (!dataSource || typeof dataSource.find !== 'function') {
172
- throw new Error('DataSource required for object/api providers');
173
- }
174
-
175
- if (dataConfig?.provider === 'object') {
176
- const objectName = dataConfig.object;
177
- // Auto-inject $expand for lookup/master_detail fields
178
- const expand = buildExpandFields(objectSchema?.fields);
179
- const result = await dataSource.find(objectName, {
180
- $filter: schema.filter,
181
- $orderby: convertSortToQueryParams(schema.sort),
182
- ...(expand.length > 0 ? { $expand: expand } : {}),
183
- });
184
- let items: any[] = extractRecords(result);
185
- setData(items);
186
- } else if (dataConfig?.provider === 'api') {
187
- console.warn('API provider not yet implemented for ObjectGantt');
188
- setData([]);
189
- }
190
-
191
- setLoading(false);
192
- } catch (err) {
193
- setError(err as Error);
194
- setLoading(false);
195
- }
196
- };
197
-
198
- fetchData();
199
- }, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema]);
200
-
201
- // Fetch object schema for field metadata
202
- useEffect(() => {
203
- const fetchObjectSchema = async () => {
204
- try {
205
- if (!dataSource) return;
206
-
207
- const objectName = dataConfig?.provider === 'object'
208
- ? dataConfig.object
209
- : schema.objectName;
210
-
211
- if (!objectName) return;
212
-
213
- const schemaData = await dataSource.getObjectSchema(objectName);
214
- setObjectSchema(schemaData);
215
- } catch (err) {
216
- console.error('Failed to fetch object schema:', err);
217
- }
218
- };
219
-
220
- if (!hasInlineData && dataSource) {
221
- fetchObjectSchema();
222
- }
223
- }, [schema.objectName, dataSource, hasInlineData, dataConfig]);
224
-
225
- // Transform data to gantt tasks
226
- const tasks = useMemo(() => {
227
- if (!ganttConfig || !data.length) {
228
- return [];
229
- }
230
-
231
- const { startDateField, endDateField, titleField, progressField, dependenciesField, colorField } = ganttConfig;
232
-
233
- return data.map((record, index) => {
234
- const startDate = record[startDateField];
235
- const endDate = record[endDateField];
236
- const title = record[titleField] || 'Untitled Task';
237
- const progress = progressField ? record[progressField] : 0;
238
- const dependencies = dependenciesField ? record[dependenciesField] : [];
239
- const color = colorField ? record[colorField] : undefined;
240
-
241
- return {
242
- id: record.id || record._id || `task-${index}`,
243
- title,
244
- start: startDate ? new Date(startDate) : new Date(),
245
- end: endDate ? new Date(endDate) : new Date(),
246
- progress: Math.min(100, Math.max(0, progress || 0)), // Clamp between 0-100
247
- dependencies: Array.isArray(dependencies) ? dependencies : [],
248
- color,
249
- data: record,
250
- };
251
- }).filter(task => !isNaN(task.start.getTime()) && !isNaN(task.end.getTime()));
252
- }, [data, ganttConfig]);
253
-
254
- const navigation = useNavigationOverlay({
255
- navigation: (schema as any).navigation,
256
- objectName: schema.objectName,
257
- onRowClick,
258
- });
259
-
260
- if (loading) {
261
- return (
262
- <div className={className}>
263
- <div className="flex items-center justify-center h-96">
264
- <div className="text-muted-foreground">Loading Gantt chart...</div>
265
- </div>
266
- </div>
267
- );
268
- }
269
-
270
- if (error) {
271
- return (
272
- <div className={className}>
273
- <div className="flex items-center justify-center h-96">
274
- <div className="text-destructive">Error: {error.message}</div>
275
- </div>
276
- </div>
277
- );
278
- }
279
-
280
- if (!ganttConfig) {
281
- return (
282
- <div className={className}>
283
- <div className="flex items-center justify-center h-96">
284
- <div className="text-muted-foreground">
285
- Gantt configuration required. Please specify startDateField, endDateField, and titleField.
286
- </div>
287
- </div>
288
- </div>
289
- );
290
- }
291
-
292
- return (
293
- <div className={className}>
294
- <div className="h-[calc(100vh-200px)] min-h-[600px]">
295
- <GanttView
296
- tasks={tasks}
297
- onTaskClick={(task) => {
298
- navigation.handleClick(task.data);
299
- onTaskClick?.(task.data);
300
- }}
301
- onAddClick={() => {
302
- // Placeholder for add action
303
- }}
304
- />
305
- </div>
306
- {navigation.isOverlay && (
307
- <NavigationOverlay {...navigation} title="Task Details">
308
- {(record) => (
309
- <div className="space-y-3">
310
- {Object.entries(record).map(([key, value]) => (
311
- <div key={key} className="flex flex-col">
312
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
313
- {key.replace(/_/g, ' ')}
314
- </span>
315
- <span className="text-sm">{String(value ?? '—')}</span>
316
- </div>
317
- ))}
318
- </div>
319
- )}
320
- </NavigationOverlay>
321
- )}
322
- </div>
323
- );
324
- };
@@ -1,69 +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 from 'react';
10
- import { describe, it, expect } from 'vitest';
11
- import { render, screen } from '@testing-library/react';
12
- import '@testing-library/jest-dom';
13
- import { GanttView, type GanttTask } from '../GanttView';
14
-
15
- const mockTasks: GanttTask[] = [
16
- {
17
- id: '1',
18
- title: 'Design Phase',
19
- start: new Date('2024-01-15'),
20
- end: new Date('2024-02-15'),
21
- progress: 75,
22
- },
23
- {
24
- id: '2',
25
- title: 'Development',
26
- start: new Date('2024-02-01'),
27
- end: new Date('2024-03-15'),
28
- progress: 30,
29
- },
30
- ];
31
-
32
- describe('GanttView accessibility', () => {
33
- it('renders aria-label on navigation buttons', () => {
34
- render(<GanttView tasks={mockTasks} />);
35
- expect(screen.getByLabelText('Previous period')).toBeInTheDocument();
36
- expect(screen.getByLabelText('Next period')).toBeInTheDocument();
37
- });
38
-
39
- it('renders aria-label on zoom buttons', () => {
40
- render(<GanttView tasks={mockTasks} />);
41
- expect(screen.getByLabelText('Zoom out')).toBeInTheDocument();
42
- expect(screen.getByLabelText('Zoom in')).toBeInTheDocument();
43
- });
44
-
45
- it('renders aria-label on create button', () => {
46
- render(<GanttView tasks={mockTasks} />);
47
- expect(screen.getByLabelText('Create new task')).toBeInTheDocument();
48
- });
49
- });
50
-
51
- describe('GanttView mobile date badge', () => {
52
- it('renders date range text below task title', () => {
53
- render(<GanttView tasks={mockTasks} />);
54
- // The mobile date badge shows start → end dates
55
- const startDate = mockTasks[0].start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });
56
- const endDate = mockTasks[0].end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });
57
- const dateText = `${startDate} → ${endDate}`;
58
- expect(screen.getByText(dateText)).toBeInTheDocument();
59
- });
60
-
61
- it('renders mobile date badge with sm:hidden class', () => {
62
- render(<GanttView tasks={mockTasks} />);
63
- const startDate = mockTasks[0].start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });
64
- const endDate = mockTasks[0].end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' });
65
- const dateText = `${startDate} → ${endDate}`;
66
- const badge = screen.getByText(dateText);
67
- expect(badge.className).toContain('sm:hidden');
68
- });
69
- });
@@ -1,25 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { render, screen } from '@testing-library/react';
3
- import React from 'react';
4
- import { ObjectGanttRenderer } from './index';
5
-
6
- // Mock dependencies
7
- vi.mock('@object-ui/react', () => ({
8
- useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })),
9
- }));
10
-
11
- vi.mock('./ObjectGantt', () => ({
12
- ObjectGantt: ({ dataSource }: any) => (
13
- <div data-testid="gantt-mock">
14
- {dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
15
- </div>
16
- )
17
- }));
18
-
19
- describe('Plugin Gantt Registration', () => {
20
- it('renderer passes dataSource from context', () => {
21
- // Note: We test the renderer directly to avoid singleton issues with ComponentRegistry in tests
22
- render(<ObjectGanttRenderer schema={{}} />);
23
- expect(screen.getByTestId('gantt-mock')).toHaveTextContent('DataSource: mock-datasource');
24
- });
25
- });
package/src/index.tsx DELETED
@@ -1,45 +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 from 'react';
10
- import { ComponentRegistry } from '@object-ui/core';
11
- import { useSchemaContext } from '@object-ui/react';
12
- import { ObjectGantt } from './ObjectGantt';
13
- import type { ObjectGanttProps } from './ObjectGantt';
14
-
15
- export { ObjectGantt };
16
- export type { ObjectGanttProps };
17
-
18
- export { GanttView } from './GanttView';
19
- export type { GanttViewProps, GanttTask, GanttViewMode } from './GanttView';
20
-
21
- // Register component
22
- export const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => {
23
- const { dataSource } = useSchemaContext() || {};
24
- return <ObjectGantt schema={schema} dataSource={dataSource} />;
25
- };
26
-
27
- ComponentRegistry.register('object-gantt', ObjectGanttRenderer, {
28
- namespace: 'plugin-gantt',
29
- label: 'Object Gantt',
30
- category: 'view',
31
- inputs: [
32
- { name: 'objectName', type: 'string', label: 'Object Name', required: true },
33
- { name: 'gantt', type: 'object', label: 'Gantt Config', description: 'startDateField, endDateField, titleField, progressField, percentageField, colorField, dependenciesField' },
34
- ],
35
- });
36
-
37
- ComponentRegistry.register('gantt', ObjectGanttRenderer, {
38
- namespace: 'view',
39
- label: 'Gantt View',
40
- category: 'view',
41
- inputs: [
42
- { name: 'objectName', type: 'string', label: 'Object Name', required: true },
43
- { name: 'gantt', type: 'object', label: 'Gantt Config', description: 'startDateField, endDateField, titleField, progressField, percentageField, colorField, dependenciesField' },
44
- ],
45
- });
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "jsx": "react-jsx",
6
- "baseUrl": ".",
7
- "paths": {
8
- "@/*": ["src/*"]
9
- },
10
- "noEmit": false,
11
- "declaration": true,
12
- "composite": true,
13
- "declarationMap": true,
14
- "skipLibCheck": true
15
- },
16
- "include": ["src"],
17
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx", "**/*.stories.tsx"]
18
- }
package/vite.config.ts DELETED
@@ -1,53 +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 { defineConfig } from 'vite';
10
- import react from '@vitejs/plugin-react';
11
- import dts from 'vite-plugin-dts';
12
- import { resolve } from 'path';
13
-
14
- export default defineConfig({
15
- plugins: [
16
- react(),
17
- dts({
18
- insertTypesEntry: true,
19
- include: ['src'],
20
- exclude: ['**/*.test.ts', '**/*.test.tsx', 'node_modules'],
21
- skipDiagnostics: true,
22
- }),
23
- ],
24
- resolve: {
25
- alias: {
26
- '@': resolve(__dirname, './src'),
27
- },
28
- },
29
- build: {
30
- lib: {
31
- entry: resolve(__dirname, 'src/index.tsx'),
32
- name: 'ObjectUIPluginGantt',
33
- fileName: 'index',
34
- },
35
- rollupOptions: {
36
- external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', '@object-ui/types', 'lucide-react'],
37
- output: {
38
- globals: {
39
- react: 'React',
40
- 'react-dom': 'ReactDOM',
41
- '@object-ui/components': 'ObjectUIComponents',
42
- '@object-ui/core': 'ObjectUICore',
43
- '@object-ui/react': 'ObjectUIReact',
44
- '@object-ui/types': 'ObjectUITypes',
45
- 'lucide-react': 'LucideReact',
46
- },
47
- },
48
- },
49
- },
50
- test: {
51
- passWithNoTests: true,
52
- },
53
- });
package/vitest.config.ts DELETED
@@ -1,13 +0,0 @@
1
- /// <reference types="vitest" />
2
- import { defineConfig } from 'vite';
3
- import react from '@vitejs/plugin-react';
4
- import path from 'path';
5
-
6
- export default defineConfig({
7
- plugins: [react()],
8
- test: {
9
- environment: 'happy-dom',
10
- globals: true,
11
- setupFiles: ['./vitest.setup.ts'],
12
- },
13
- });
package/vitest.setup.ts DELETED
@@ -1 +0,0 @@
1
- import '@testing-library/jest-dom';