@object-ui/plugin-gantt 3.0.3 → 3.1.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 +11 -0
- package/dist/index.js +2745 -2030
- package/dist/index.umd.cjs +20 -20
- package/dist/src/GanttView.d.ts +4 -1
- package/dist/src/GanttView.d.ts.map +1 -1
- package/dist/src/ObjectGantt.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/GanttView.tsx +89 -13
- package/src/ObjectGantt.tsx +7 -13
- package/src/__tests__/GanttView.test.tsx +69 -0
- package/src/index.tsx +1 -1
package/dist/src/GanttView.d.ts
CHANGED
|
@@ -22,9 +22,12 @@ export interface GanttViewProps {
|
|
|
22
22
|
startDate?: Date;
|
|
23
23
|
endDate?: Date;
|
|
24
24
|
onTaskClick?: (task: GanttTask) => void;
|
|
25
|
+
onTaskUpdate?: (task: GanttTask, changes: Partial<Pick<GanttTask, 'title' | 'start' | 'end' | 'progress'>>) => void;
|
|
25
26
|
onViewChange?: (view: GanttViewMode) => void;
|
|
26
27
|
onAddClick?: () => void;
|
|
27
28
|
className?: string;
|
|
29
|
+
/** Enable inline editing of task fields */
|
|
30
|
+
inlineEdit?: boolean;
|
|
28
31
|
}
|
|
29
|
-
export declare function GanttView({ tasks, viewMode, startDate, endDate, onTaskClick, onViewChange, onAddClick, className }: GanttViewProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export declare function GanttView({ tasks, viewMode, startDate, endDate, onTaskClick, onTaskUpdate, onViewChange, onAddClick, className, inlineEdit, }: GanttViewProps): import("react/jsx-runtime").JSX.Element;
|
|
30
33
|
//# sourceMappingURL=GanttView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GanttView.d.ts","sourceRoot":"","sources":["../../src/GanttView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"GanttView.d.ts","sourceRoot":"","sources":["../../src/GanttView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA0CH,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,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC,CAAC,KAAK,IAAI,CAAA;IACnH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAA;IAC5C,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,QAAkB,EAClB,SAAS,EACT,OAAO,EACP,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAkB,GACnB,EAAE,cAAc,2CAqVhB"}
|
|
@@ -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;
|
|
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;AAO5F,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,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACnC,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,CAkMlD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-gantt",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Gantt chart plugin for Object UI",
|
|
@@ -24,20 +24,20 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@objectstack/spec": "^3.
|
|
28
|
-
"lucide-react": "^0.
|
|
29
|
-
"@object-ui/components": "3.
|
|
30
|
-
"@object-ui/core": "3.
|
|
31
|
-
"@object-ui/fields": "3.
|
|
32
|
-
"@object-ui/react": "3.
|
|
33
|
-
"@object-ui/types": "3.
|
|
27
|
+
"@objectstack/spec": "^3.2.1",
|
|
28
|
+
"lucide-react": "^0.576.0",
|
|
29
|
+
"@object-ui/components": "3.1.1",
|
|
30
|
+
"@object-ui/core": "3.1.1",
|
|
31
|
+
"@object-ui/fields": "3.1.1",
|
|
32
|
+
"@object-ui/react": "3.1.1",
|
|
33
|
+
"@object-ui/types": "3.1.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": "^18.0.0 || ^19.0.0",
|
|
37
37
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/react": "19.2.
|
|
40
|
+
"@types/react": "19.2.14",
|
|
41
41
|
"@types/react-dom": "19.2.3",
|
|
42
42
|
"@vitejs/plugin-react": "^5.1.4",
|
|
43
43
|
"typescript": "^5.9.3",
|
package/src/GanttView.tsx
CHANGED
|
@@ -39,6 +39,13 @@ function getResponsiveColumnWidth() {
|
|
|
39
39
|
return 60;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function getResponsiveTaskListWidth() {
|
|
43
|
+
const w = typeof window !== 'undefined' ? window.innerWidth : 1024;
|
|
44
|
+
if (w < 640) return 120;
|
|
45
|
+
if (w < 1024) return 200;
|
|
46
|
+
return 300;
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
export interface GanttTask {
|
|
43
50
|
id: string | number
|
|
44
51
|
title: string
|
|
@@ -58,9 +65,12 @@ export interface GanttViewProps {
|
|
|
58
65
|
startDate?: Date
|
|
59
66
|
endDate?: Date
|
|
60
67
|
onTaskClick?: (task: GanttTask) => void
|
|
68
|
+
onTaskUpdate?: (task: GanttTask, changes: Partial<Pick<GanttTask, 'title' | 'start' | 'end' | 'progress'>>) => void
|
|
61
69
|
onViewChange?: (view: GanttViewMode) => void
|
|
62
70
|
onAddClick?: () => void
|
|
63
71
|
className?: string
|
|
72
|
+
/** Enable inline editing of task fields */
|
|
73
|
+
inlineEdit?: boolean
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
export function GanttView({
|
|
@@ -69,15 +79,19 @@ export function GanttView({
|
|
|
69
79
|
startDate,
|
|
70
80
|
endDate,
|
|
71
81
|
onTaskClick,
|
|
82
|
+
onTaskUpdate,
|
|
72
83
|
onViewChange,
|
|
73
84
|
onAddClick,
|
|
74
|
-
className
|
|
85
|
+
className,
|
|
86
|
+
inlineEdit = false,
|
|
75
87
|
}: GanttViewProps) {
|
|
76
88
|
const [currentDate, setCurrentDate] = React.useState(new Date());
|
|
77
89
|
const [rowHeight, setRowHeight] = React.useState(
|
|
78
90
|
typeof window !== 'undefined' && window.innerWidth < 640 ? 32 : 40
|
|
79
91
|
);
|
|
80
92
|
const [columnWidth, setColumnWidth] = React.useState(getResponsiveColumnWidth());
|
|
93
|
+
const [editingTask, setEditingTask] = React.useState<string | number | null>(null);
|
|
94
|
+
const [editValues, setEditValues] = React.useState<Record<string, string>>({});
|
|
81
95
|
|
|
82
96
|
React.useEffect(() => {
|
|
83
97
|
const handleResize = () => {
|
|
@@ -131,7 +145,7 @@ export function GanttView({
|
|
|
131
145
|
return cols;
|
|
132
146
|
}, [timelineRange]);
|
|
133
147
|
|
|
134
|
-
const taskListWidth =
|
|
148
|
+
const taskListWidth = getResponsiveTaskListWidth();
|
|
135
149
|
|
|
136
150
|
const headerRef = React.useRef<HTMLDivElement>(null);
|
|
137
151
|
const listRef = React.useRef<HTMLDivElement>(null);
|
|
@@ -163,7 +177,7 @@ export function GanttView({
|
|
|
163
177
|
};
|
|
164
178
|
|
|
165
179
|
return (
|
|
166
|
-
<div className={cn("flex flex-col h-full bg-background border rounded-lg overflow-hidden", className)}>
|
|
180
|
+
<div className={cn("flex flex-col h-full bg-background border rounded-lg overflow-hidden min-w-0", className)}>
|
|
167
181
|
{/* Toolbar */}
|
|
168
182
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 p-2 border-b bg-card">
|
|
169
183
|
<div className="flex items-center gap-2">
|
|
@@ -173,10 +187,10 @@ export function GanttView({
|
|
|
173
187
|
<span className="sm:hidden">New</span>
|
|
174
188
|
</Button>
|
|
175
189
|
<div className="h-4 w-px bg-border mx-1 sm:mx-2" />
|
|
176
|
-
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
190
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Previous period">
|
|
177
191
|
<ChevronLeft className="h-4 w-4" />
|
|
178
192
|
</Button>
|
|
179
|
-
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
193
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Next period">
|
|
180
194
|
<ChevronRight className="h-4 w-4" />
|
|
181
195
|
</Button>
|
|
182
196
|
<span className="font-semibold text-xs sm:text-sm">
|
|
@@ -197,10 +211,10 @@ export function GanttView({
|
|
|
197
211
|
</SelectContent>
|
|
198
212
|
</Select>
|
|
199
213
|
<div className="flex bg-muted rounded-md p-1">
|
|
200
|
-
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.max(15, prev - 10))}>
|
|
214
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.max(15, prev - 10))} aria-label="Zoom out">
|
|
201
215
|
<ZoomOut className="h-3 w-3" />
|
|
202
216
|
</Button>
|
|
203
|
-
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.min(120, prev + 10))}>
|
|
217
|
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setColumnWidth(prev => Math.min(120, prev + 10))} aria-label="Zoom in">
|
|
204
218
|
<ZoomIn className="h-3 w-3" />
|
|
205
219
|
</Button>
|
|
206
220
|
</div>
|
|
@@ -251,28 +265,90 @@ export function GanttView({
|
|
|
251
265
|
ref={listRef}
|
|
252
266
|
style={{ width: taskListWidth, minWidth: taskListWidth }}
|
|
253
267
|
>
|
|
254
|
-
{tasks.map((task) =>
|
|
268
|
+
{tasks.map((task) => {
|
|
269
|
+
const isEditing = inlineEdit && editingTask === task.id;
|
|
270
|
+
return (
|
|
255
271
|
<div
|
|
256
272
|
key={task.id}
|
|
257
273
|
className="flex items-center border-b px-2 sm:px-4 hover:bg-accent/50 cursor-pointer transition-colors touch-manipulation"
|
|
258
274
|
style={{ height: rowHeight }}
|
|
259
|
-
onClick={() => onTaskClick?.(task)}
|
|
275
|
+
onClick={() => !isEditing && onTaskClick?.(task)}
|
|
276
|
+
onDoubleClick={() => {
|
|
277
|
+
if (inlineEdit && onTaskUpdate) {
|
|
278
|
+
setEditingTask(task.id);
|
|
279
|
+
setEditValues({
|
|
280
|
+
title: task.title,
|
|
281
|
+
start: task.start.toLocaleDateString('en-CA'),
|
|
282
|
+
end: task.end.toLocaleDateString('en-CA'),
|
|
283
|
+
progress: String(task.progress),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}}
|
|
260
287
|
>
|
|
261
288
|
<div className="flex-1 truncate font-medium text-xs sm:text-sm flex items-center gap-2">
|
|
262
289
|
<div
|
|
263
290
|
className="w-2 h-2 rounded-full shrink-0"
|
|
264
291
|
style={{ backgroundColor: task.color || '#3b82f6' }}
|
|
265
292
|
/>
|
|
266
|
-
{
|
|
293
|
+
{isEditing ? (
|
|
294
|
+
<input
|
|
295
|
+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
|
|
296
|
+
value={editValues.title || ''}
|
|
297
|
+
onChange={(e) => setEditValues(prev => ({ ...prev, title: e.target.value }))}
|
|
298
|
+
onKeyDown={(e) => {
|
|
299
|
+
if (e.key === 'Enter') {
|
|
300
|
+
onTaskUpdate?.(task, {
|
|
301
|
+
title: editValues.title,
|
|
302
|
+
start: new Date(editValues.start),
|
|
303
|
+
end: new Date(editValues.end),
|
|
304
|
+
progress: Number(editValues.progress) || 0,
|
|
305
|
+
});
|
|
306
|
+
setEditingTask(null);
|
|
307
|
+
} else if (e.key === 'Escape') {
|
|
308
|
+
setEditingTask(null);
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
onClick={(e) => e.stopPropagation()}
|
|
312
|
+
autoFocus
|
|
313
|
+
/>
|
|
314
|
+
) : (
|
|
315
|
+
<span className="flex flex-col min-w-0">
|
|
316
|
+
<span className="truncate">{task.title}</span>
|
|
317
|
+
<span className="text-[10px] text-muted-foreground sm:hidden">
|
|
318
|
+
{task.start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })} → {task.end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })}
|
|
319
|
+
</span>
|
|
320
|
+
</span>
|
|
321
|
+
)}
|
|
267
322
|
</div>
|
|
268
323
|
<div className="w-16 sm:w-20 text-right text-xs text-muted-foreground hidden sm:block">
|
|
269
|
-
{
|
|
324
|
+
{isEditing ? (
|
|
325
|
+
<input
|
|
326
|
+
type="date"
|
|
327
|
+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
|
|
328
|
+
value={editValues.start || ''}
|
|
329
|
+
onChange={(e) => setEditValues(prev => ({ ...prev, start: e.target.value }))}
|
|
330
|
+
onClick={(e) => e.stopPropagation()}
|
|
331
|
+
/>
|
|
332
|
+
) : (
|
|
333
|
+
task.start.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })
|
|
334
|
+
)}
|
|
270
335
|
</div>
|
|
271
336
|
<div className="w-16 sm:w-20 text-right text-xs text-muted-foreground hidden sm:block">
|
|
272
|
-
{
|
|
337
|
+
{isEditing ? (
|
|
338
|
+
<input
|
|
339
|
+
type="date"
|
|
340
|
+
className="border rounded px-1 py-0.5 text-xs w-full bg-background"
|
|
341
|
+
value={editValues.end || ''}
|
|
342
|
+
onChange={(e) => setEditValues(prev => ({ ...prev, end: e.target.value }))}
|
|
343
|
+
onClick={(e) => e.stopPropagation()}
|
|
344
|
+
/>
|
|
345
|
+
) : (
|
|
346
|
+
task.end.toLocaleDateString(undefined, { month: 'numeric', day: 'numeric' })
|
|
347
|
+
)}
|
|
273
348
|
</div>
|
|
274
349
|
</div>
|
|
275
|
-
|
|
350
|
+
);
|
|
351
|
+
})}
|
|
276
352
|
</div>
|
|
277
353
|
|
|
278
354
|
{/* Right Side: Timeline */}
|
package/src/ObjectGantt.tsx
CHANGED
|
@@ -27,6 +27,7 @@ import type { ObjectGridSchema, DataSource, ViewData, GanttConfig } from '@objec
|
|
|
27
27
|
import { GanttConfigSchema } from '@objectstack/spec/ui';
|
|
28
28
|
import { useNavigationOverlay } from '@object-ui/react';
|
|
29
29
|
import { NavigationOverlay } from '@object-ui/components';
|
|
30
|
+
import { extractRecords, buildExpandFields } from '@object-ui/core';
|
|
30
31
|
import { GanttView, type GanttTask } from './GanttView';
|
|
31
32
|
|
|
32
33
|
export interface ObjectGanttProps {
|
|
@@ -167,27 +168,20 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
|
|
|
167
168
|
return;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
if (!dataSource) {
|
|
171
|
+
if (!dataSource || typeof dataSource.find !== 'function') {
|
|
171
172
|
throw new Error('DataSource required for object/api providers');
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
if (dataConfig?.provider === 'object') {
|
|
175
176
|
const objectName = dataConfig.object;
|
|
177
|
+
// Auto-inject $expand for lookup/master_detail fields
|
|
178
|
+
const expand = buildExpandFields(objectSchema?.fields);
|
|
176
179
|
const result = await dataSource.find(objectName, {
|
|
177
180
|
$filter: schema.filter,
|
|
178
181
|
$orderby: convertSortToQueryParams(schema.sort),
|
|
182
|
+
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
179
183
|
});
|
|
180
|
-
|
|
181
|
-
let items: any[] = [];
|
|
182
|
-
if (Array.isArray(result)) {
|
|
183
|
-
items = result;
|
|
184
|
-
} else if (result && typeof result === 'object') {
|
|
185
|
-
if (Array.isArray((result as any).data)) {
|
|
186
|
-
items = (result as any).data;
|
|
187
|
-
} else if (Array.isArray((result as any).records)) {
|
|
188
|
-
items = (result as any).records;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
184
|
+
let items: any[] = extractRecords(result);
|
|
191
185
|
setData(items);
|
|
192
186
|
} else if (dataConfig?.provider === 'api') {
|
|
193
187
|
console.warn('API provider not yet implemented for ObjectGantt');
|
|
@@ -202,7 +196,7 @@ export const ObjectGantt: React.FC<ObjectGanttProps> = ({
|
|
|
202
196
|
};
|
|
203
197
|
|
|
204
198
|
fetchData();
|
|
205
|
-
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]);
|
|
199
|
+
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema]);
|
|
206
200
|
|
|
207
201
|
// Fetch object schema for field metadata
|
|
208
202
|
useEffect(() => {
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -20,7 +20,7 @@ export type { GanttViewProps, GanttTask, GanttViewMode } from './GanttView';
|
|
|
20
20
|
|
|
21
21
|
// Register component
|
|
22
22
|
export const ObjectGanttRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
23
|
-
const { dataSource } = useSchemaContext();
|
|
23
|
+
const { dataSource } = useSchemaContext() || {};
|
|
24
24
|
return <ObjectGantt schema={schema} dataSource={dataSource} />;
|
|
25
25
|
};
|
|
26
26
|
|