@object-ui/plugin-gantt 0.3.1 → 0.5.0

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,30 @@
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
+ export interface GanttTask {
9
+ id: string | number;
10
+ title: string;
11
+ start: Date;
12
+ end: Date;
13
+ progress: number;
14
+ color?: string;
15
+ data?: any;
16
+ dependencies?: (string | number)[];
17
+ }
18
+ export type GanttViewMode = 'day' | 'week' | 'month' | 'quarter';
19
+ export interface GanttViewProps {
20
+ tasks: GanttTask[];
21
+ viewMode?: GanttViewMode;
22
+ startDate?: Date;
23
+ endDate?: Date;
24
+ onTaskClick?: (task: GanttTask) => void;
25
+ onViewChange?: (view: GanttViewMode) => void;
26
+ onAddClick?: () => void;
27
+ className?: string;
28
+ }
29
+ export declare function GanttView({ tasks, viewMode, startDate, endDate, onTaskClick, onViewChange, onAddClick, className }: GanttViewProps): import("react/jsx-runtime").JSX.Element;
30
+ //# sourceMappingURL=GanttView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GanttView.d.ts","sourceRoot":"","sources":["../../src/GanttView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA6BH,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,IAAI,CAAA;IACX,GAAG,EAAE,IAAI,CAAA;IACT,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,GAAG,CAAA;IACV,YAAY,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;CACnC;AAED,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,SAAS,EAAE,CAAA;IAClB,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB,SAAS,CAAC,EAAE,IAAI,CAAA;IAChB,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IACvC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAkB,EAClB,SAAS,EACT,OAAO,EACP,WAAW,EACX,YAAY,EACZ,UAAU,EACV,SAAS,EACV,EAAE,cAAc,2CAwQhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ObjectGantt.d.ts","sourceRoot":"","sources":["../../src/ObjectGantt.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAyB,MAAM,kBAAkB,CAAC;AAE5F,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAuED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA8SlD,CAAC"}
1
+ {"version":3,"file":"ObjectGantt.d.ts","sourceRoot":"","sources":["../../src/ObjectGantt.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAyB,MAAM,kBAAkB,CAAC;AAI5F,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACpC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CAClC;AAyFD,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA+KlD,CAAC"}
@@ -1,4 +1,10 @@
1
+ import { default as React } from 'react';
1
2
  import { ObjectGantt, ObjectGanttProps } from './ObjectGantt';
2
3
  export { ObjectGantt };
3
4
  export type { ObjectGanttProps };
5
+ export { GanttView } from './GanttView';
6
+ export type { GanttViewProps, GanttTask, GanttViewMode } from './GanttView';
7
+ export declare const ObjectGanttRenderer: React.FC<{
8
+ schema: any;
9
+ }>;
4
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,WAAW,EAAE,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5E,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC;IAAE,MAAM,EAAE,GAAG,CAAA;CAAE,CAGzD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/plugin-gantt",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Gantt chart plugin for Object UI",
@@ -24,21 +24,22 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
+ "@objectstack/spec": "^0.9.2",
27
28
  "lucide-react": "^0.563.0",
28
- "@object-ui/components": "0.3.1",
29
- "@object-ui/core": "0.3.1",
30
- "@object-ui/fields": "0.3.1",
31
- "@object-ui/react": "0.3.1",
32
- "@object-ui/types": "0.3.1"
29
+ "@object-ui/components": "0.5.0",
30
+ "@object-ui/fields": "0.5.0",
31
+ "@object-ui/react": "0.5.0",
32
+ "@object-ui/core": "0.5.0",
33
+ "@object-ui/types": "0.5.0"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "react": "^18.0.0 || ^19.0.0",
36
37
  "react-dom": "^18.0.0 || ^19.0.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@types/react": "^19.2.9",
40
+ "@types/react": "^19.2.10",
40
41
  "@types/react-dom": "^19.2.3",
41
- "@vitejs/plugin-react": "^4.2.1",
42
+ "@vitejs/plugin-react": "^5.1.3",
42
43
  "typescript": "^5.9.3",
43
44
  "vite": "^7.3.1",
44
45
  "vite-plugin-dts": "^4.5.4"
@@ -0,0 +1,333 @@
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
+ "use client"
10
+
11
+ import * as React from "react"
12
+ import {
13
+ ChevronLeft,
14
+ ChevronRight,
15
+ ZoomIn,
16
+ ZoomOut,
17
+ Calendar as CalendarIcon,
18
+ MoreHorizontal,
19
+ Plus
20
+ } from "lucide-react"
21
+ import {
22
+ cn,
23
+ Button,
24
+ Select,
25
+ SelectContent,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ Separator
30
+ } from "@object-ui/components"
31
+
32
+ const HEADER_HEIGHT = 50;
33
+ const ROW_HEIGHT = 40;
34
+ const COLUMN_WIDTH = 100; // Time column width
35
+
36
+ export interface GanttTask {
37
+ id: string | number
38
+ title: string
39
+ start: Date
40
+ end: Date
41
+ progress: number
42
+ color?: string
43
+ data?: any
44
+ dependencies?: (string | number)[]
45
+ }
46
+
47
+ export type GanttViewMode = 'day' | 'week' | 'month' | 'quarter';
48
+
49
+ export interface GanttViewProps {
50
+ tasks: GanttTask[]
51
+ viewMode?: GanttViewMode
52
+ startDate?: Date
53
+ endDate?: Date
54
+ onTaskClick?: (task: GanttTask) => void
55
+ onViewChange?: (view: GanttViewMode) => void
56
+ onAddClick?: () => void
57
+ className?: string
58
+ }
59
+
60
+ export function GanttView({
61
+ tasks,
62
+ viewMode = 'month',
63
+ startDate,
64
+ endDate,
65
+ onTaskClick,
66
+ onViewChange,
67
+ onAddClick,
68
+ className
69
+ }: GanttViewProps) {
70
+ const [currentDate, setCurrentDate] = React.useState(new Date());
71
+ const [columnWidth, setColumnWidth] = React.useState(60);
72
+
73
+ // Calculate timeline range
74
+ const timelineRange = React.useMemo(() => {
75
+ let start = startDate ? new Date(startDate) : new Date();
76
+ let end = endDate ? new Date(endDate) : new Date();
77
+
78
+ if (!startDate && tasks.length > 0) {
79
+ // Find min start date
80
+ start = new Date(Math.min(...tasks.map(t => t.start.getTime())));
81
+ // Add padding
82
+ start.setDate(start.getDate() - 7);
83
+ }
84
+
85
+ if (!endDate && tasks.length > 0) {
86
+ // Find max end date
87
+ end = new Date(Math.max(...tasks.map(t => t.end.getTime())));
88
+ // Add padding
89
+ end.setDate(end.getDate() + 14);
90
+ }
91
+
92
+ // Normalize to start of day
93
+ start.setHours(0,0,0,0);
94
+ end.setHours(23,59,59,999);
95
+
96
+ return { start, end };
97
+ }, [startDate, endDate, tasks]);
98
+
99
+ // Generate timeline columns
100
+ const timeColumns = React.useMemo(() => {
101
+ const cols: { date: Date; label: string; isWeekend: boolean }[] = [];
102
+ const current = new Date(timelineRange.start);
103
+
104
+ while (current <= timelineRange.end) {
105
+ cols.push({
106
+ date: new Date(current),
107
+ label: current.getDate().toString(),
108
+ isWeekend: current.getDay() === 0 || current.getDay() === 6
109
+ });
110
+ current.setDate(current.getDate() + 1);
111
+ }
112
+
113
+ return cols;
114
+ }, [timelineRange]);
115
+
116
+ const taskListWidth = 300;
117
+
118
+ const headerRef = React.useRef<HTMLDivElement>(null);
119
+ const listRef = React.useRef<HTMLDivElement>(null);
120
+ const timelineRef = React.useRef<HTMLDivElement>(null);
121
+
122
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
123
+ // Sync horizontal scroll to header
124
+ if (headerRef.current) {
125
+ headerRef.current.scrollLeft = e.currentTarget.scrollLeft;
126
+ }
127
+ // Sync vertical scroll to task list
128
+ if (listRef.current) {
129
+ listRef.current.scrollTop = e.currentTarget.scrollTop;
130
+ }
131
+ };
132
+
133
+ const getTaskStyle = (task: GanttTask) => {
134
+ const totalDuration = timelineRange.end.getTime() - timelineRange.start.getTime();
135
+ const tickWidth = columnWidth; // px per day
136
+ const msPerDay = 1000 * 60 * 60 * 24;
137
+
138
+ const startOffsetMs = task.start.getTime() - timelineRange.start.getTime();
139
+ const durationMs = task.end.getTime() - task.start.getTime();
140
+
141
+ const left = (startOffsetMs / msPerDay) * tickWidth;
142
+ const width = Math.max((durationMs / msPerDay) * tickWidth, tickWidth); // Min 1 day width
143
+
144
+ return { left, width };
145
+ };
146
+
147
+ return (
148
+ <div className={cn("flex flex-col h-full bg-background border rounded-lg overflow-hidden", className)}>
149
+ {/* Toolbar */}
150
+ <div className="flex items-center justify-between p-2 border-b bg-card">
151
+ <div className="flex items-center gap-2">
152
+ <Button variant="outline" size="sm" onClick={() => onAddClick?.()}>
153
+ <Plus className="h-4 w-4 mr-2" />
154
+ New Task
155
+ </Button>
156
+ <div className="h-4 w-px bg-border mx-2" />
157
+ <Button variant="ghost" size="icon" className="h-8 w-8">
158
+ <ChevronLeft className="h-4 w-4" />
159
+ </Button>
160
+ <Button variant="ghost" size="icon" className="h-8 w-8">
161
+ <ChevronRight className="h-4 w-4" />
162
+ </Button>
163
+ <span className="font-semibold text-sm">
164
+ {timelineRange.start.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })}
165
+ </span>
166
+ </div>
167
+
168
+ <div className="flex items-center gap-2">
169
+ <Select value={viewMode} onValueChange={(v) => onViewChange?.(v as GanttViewMode)}>
170
+ <SelectTrigger className="w-[120px] h-8">
171
+ <SelectValue />
172
+ </SelectTrigger>
173
+ <SelectContent>
174
+ <SelectItem value="day">Day View</SelectItem>
175
+ <SelectItem value="week">Week View</SelectItem>
176
+ <SelectItem value="month">Month View</SelectItem>
177
+ <SelectItem value="quarter">Quarter View</SelectItem>
178
+ </SelectContent>
179
+ </Select>
180
+ <div className="flex bg-muted rounded-md p-1">
181
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.max(20, prev - 10))}>
182
+ <ZoomOut className="h-3 w-3" />
183
+ </Button>
184
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.min(100, prev + 10))}>
185
+ <ZoomIn className="h-3 w-3" />
186
+ </Button>
187
+ </div>
188
+ </div>
189
+ </div>
190
+
191
+ {/* Gantt Body */}
192
+ <div className="flex flex-col flex-1 overflow-hidden">
193
+ {/* Headers Row */}
194
+ <div className="flex border-b bg-muted/30 shrink-0 h-[50px]">
195
+ {/* List Header */}
196
+ <div
197
+ className="flex items-center font-medium text-xs text-muted-foreground px-4 border-r bg-card z-20 shadow-sm"
198
+ style={{ width: taskListWidth, minWidth: taskListWidth }}
199
+ >
200
+ <div className="flex-1">Task Name</div>
201
+ <div className="w-20 text-right">Start</div>
202
+ <div className="w-20 text-right">End</div>
203
+ </div>
204
+
205
+ {/* Timeline Header */}
206
+ <div className="flex-1 overflow-hidden" ref={headerRef}>
207
+ <div className="flex h-full" style={{ width: timeColumns.length * columnWidth }}>
208
+ {timeColumns.map((col, i) => (
209
+ <div
210
+ key={i}
211
+ className={cn(
212
+ "flex flex-col items-center justify-center border-r text-xs text-muted-foreground h-full",
213
+ col.isWeekend && "bg-muted/50"
214
+ )}
215
+ style={{ width: columnWidth, minWidth: columnWidth }}
216
+ >
217
+ <span className="font-medium text-foreground">{col.label}</span>
218
+ <span className="text-[10px] opacity-70">
219
+ {col.date.toLocaleDateString(undefined, { weekday: 'narrow' })}
220
+ </span>
221
+ </div>
222
+ ))}
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ {/* Content Row */}
228
+ <div className="flex flex-1 overflow-hidden">
229
+ {/* Left Side: Task List (Grid) */}
230
+ <div
231
+ className="overflow-hidden border-r bg-card z-10 shadow-sm"
232
+ ref={listRef}
233
+ style={{ width: taskListWidth, minWidth: taskListWidth }}
234
+ >
235
+ {tasks.map((task) => (
236
+ <div
237
+ key={task.id}
238
+ className="flex items-center border-b px-4 hover:bg-accent/50 cursor-pointer transition-colors"
239
+ style={{ height: ROW_HEIGHT }}
240
+ onClick={() => onTaskClick?.(task)}
241
+ >
242
+ <div className="flex-1 truncate font-medium text-sm flex items-center gap-2">
243
+ <div
244
+ className="w-2 h-2 rounded-full"
245
+ style={{ backgroundColor: task.color || '#3b82f6' }}
246
+ />
247
+ {task.title}
248
+ </div>
249
+ <div className="w-20 text-right text-xs text-muted-foreground">
250
+ {task.start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}
251
+ </div>
252
+ <div className="w-20 text-right text-xs text-muted-foreground">
253
+ {task.end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}
254
+ </div>
255
+ </div>
256
+ ))}
257
+ </div>
258
+
259
+ {/* Right Side: Timeline */}
260
+ <div
261
+ className="flex-1 overflow-auto bg-background/50 relative"
262
+ ref={timelineRef}
263
+ onScroll={handleScroll}
264
+ >
265
+ <div style={{ width: timeColumns.length * columnWidth }}>
266
+ {/* Timeline Task Rows */}
267
+ <div className="relative">
268
+ {/* Background Grid */}
269
+ <div className="absolute inset-0 flex pointer-events-none z-0">
270
+ {timeColumns.map((col, i) => (
271
+ <div
272
+ key={i}
273
+ className={cn(
274
+ "border-r h-full",
275
+ col.isWeekend && "bg-muted/20"
276
+ )}
277
+ style={{ width: columnWidth, minWidth: columnWidth }}
278
+ />
279
+ ))}
280
+ </div>
281
+
282
+ {/* Task Bars */}
283
+ {tasks.map((task) => {
284
+ const style = getTaskStyle(task);
285
+ return (
286
+ <div
287
+ key={task.id}
288
+ className="relative border-b hover:bg-black/5"
289
+ style={{ height: ROW_HEIGHT }}
290
+ >
291
+ <div
292
+ className="absolute top-2 h-[calc(100%-16px)] rounded-sm bg-primary border border-primary-foreground/20 shadow-sm cursor-pointer hover:brightness-110 flex items-center px-2 group"
293
+ style={{
294
+ left: style.left,
295
+ width: style.width,
296
+ backgroundColor: task.color || '#3b82f6'
297
+ }}
298
+ onClick={() => onTaskClick?.(task)}
299
+ >
300
+ {/* Progress Filter */}
301
+ {task.progress > 0 && (
302
+ <div
303
+ className="absolute left-0 top-0 bottom-0 bg-black/20 rounded-l-sm"
304
+ style={{ width: `${task.progress}%` }}
305
+ />
306
+ )}
307
+
308
+ {/* Hover Details */}
309
+ <span className="text-[10px] text-white font-medium truncate opacity-0 group-hover:opacity-100 transition-opacity">
310
+ {Math.round(task.progress)}%
311
+ </span>
312
+ </div>
313
+ </div>
314
+ )
315
+ })}
316
+
317
+ {/* Current Time Indicator */}
318
+ <div
319
+ className="absolute top-0 bottom-0 w-px bg-red-500 z-20 pointer-events-none"
320
+ style={{
321
+ left: (new Date().getTime() - timelineRange.start.getTime()) / (1000 * 60 * 60 * 24) * columnWidth
322
+ }}
323
+ >
324
+ <div className="w-2 h-2 rounded-full bg-red-500 -ml-[3px]" />
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ )
333
+ }
@@ -0,0 +1,88 @@
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
+ });