@object-ui/plugin-calendar 0.3.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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +946 -0
- package/dist/index.umd.cjs +6 -0
- package/dist/src/CalendarView.d.ts +29 -0
- package/dist/src/CalendarView.d.ts.map +1 -0
- package/dist/src/ObjectCalendar.d.ts +13 -0
- package/dist/src/ObjectCalendar.d.ts.map +1 -0
- package/dist/src/calendar-view-renderer.d.ts +9 -0
- package/dist/src/calendar-view-renderer.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/CalendarView.tsx +504 -0
- package/src/ObjectCalendar.tsx +385 -0
- package/src/calendar-view-renderer.tsx +231 -0
- package/src/index.tsx +37 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
* ObjectCalendar Component
|
|
11
|
+
*
|
|
12
|
+
* A specialized calendar component that works with ObjectQL data sources.
|
|
13
|
+
* Displays records as calendar events based on date field configuration.
|
|
14
|
+
* Implements the calendar view type from @objectstack/spec view.zod ListView schema.
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Month/week/day calendar views
|
|
18
|
+
* - Auto-mapping of records to calendar events
|
|
19
|
+
* - Date range filtering
|
|
20
|
+
* - Event click handling
|
|
21
|
+
* - Color coding support
|
|
22
|
+
* - Works with object/api/value data providers
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
26
|
+
import type { ObjectGridSchema, DataSource, ViewData, CalendarConfig } from '@object-ui/types';
|
|
27
|
+
|
|
28
|
+
export interface ObjectCalendarProps {
|
|
29
|
+
schema: ObjectGridSchema;
|
|
30
|
+
dataSource?: DataSource;
|
|
31
|
+
className?: string;
|
|
32
|
+
onEventClick?: (record: any) => void;
|
|
33
|
+
onDateClick?: (date: Date) => void;
|
|
34
|
+
onEdit?: (record: any) => void;
|
|
35
|
+
onDelete?: (record: any) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Helper to get data configuration from schema
|
|
40
|
+
*/
|
|
41
|
+
function getDataConfig(schema: ObjectGridSchema): ViewData | null {
|
|
42
|
+
if (schema.data) {
|
|
43
|
+
return schema.data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (schema.staticData) {
|
|
47
|
+
return {
|
|
48
|
+
provider: 'value',
|
|
49
|
+
items: schema.staticData,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (schema.objectName) {
|
|
54
|
+
return {
|
|
55
|
+
provider: 'object',
|
|
56
|
+
object: schema.objectName,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Helper to convert sort config to QueryParams format
|
|
65
|
+
*/
|
|
66
|
+
function convertSortToQueryParams(sort: string | any[] | undefined): Record<string, 'asc' | 'desc'> | undefined {
|
|
67
|
+
if (!sort) return undefined;
|
|
68
|
+
|
|
69
|
+
// If it's a string like "name desc"
|
|
70
|
+
if (typeof sort === 'string') {
|
|
71
|
+
const parts = sort.split(' ');
|
|
72
|
+
const field = parts[0];
|
|
73
|
+
const order = (parts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc';
|
|
74
|
+
return { [field]: order };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If it's an array of SortConfig objects
|
|
78
|
+
if (Array.isArray(sort)) {
|
|
79
|
+
return sort.reduce((acc, item) => {
|
|
80
|
+
if (item.field && item.order) {
|
|
81
|
+
acc[item.field] = item.order;
|
|
82
|
+
}
|
|
83
|
+
return acc;
|
|
84
|
+
}, {} as Record<string, 'asc' | 'desc'>);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Helper to get calendar configuration from schema
|
|
92
|
+
*/
|
|
93
|
+
function getCalendarConfig(schema: ObjectGridSchema): CalendarConfig | null {
|
|
94
|
+
// Check if schema has calendar configuration
|
|
95
|
+
if (schema.filter && typeof schema.filter === 'object' && 'calendar' in schema.filter) {
|
|
96
|
+
return (schema.filter as any).calendar as CalendarConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For backward compatibility, check if schema has calendar config at root
|
|
100
|
+
if ((schema as any).calendar) {
|
|
101
|
+
return (schema as any).calendar as CalendarConfig;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
108
|
+
schema,
|
|
109
|
+
dataSource,
|
|
110
|
+
className,
|
|
111
|
+
onEventClick,
|
|
112
|
+
onDateClick,
|
|
113
|
+
}) => {
|
|
114
|
+
const [data, setData] = useState<any[]>([]);
|
|
115
|
+
const [loading, setLoading] = useState(true);
|
|
116
|
+
const [error, setError] = useState<Error | null>(null);
|
|
117
|
+
const [objectSchema, setObjectSchema] = useState<any>(null);
|
|
118
|
+
const [currentDate, setCurrentDate] = useState(new Date());
|
|
119
|
+
const [view, setView] = useState<'month' | 'week' | 'day'>('month');
|
|
120
|
+
|
|
121
|
+
const dataConfig = getDataConfig(schema);
|
|
122
|
+
const calendarConfig = getCalendarConfig(schema);
|
|
123
|
+
const hasInlineData = dataConfig?.provider === 'value';
|
|
124
|
+
|
|
125
|
+
// Fetch data based on provider
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const fetchData = async () => {
|
|
128
|
+
try {
|
|
129
|
+
setLoading(true);
|
|
130
|
+
|
|
131
|
+
if (hasInlineData && dataConfig?.provider === 'value') {
|
|
132
|
+
setData(dataConfig.items as any[]);
|
|
133
|
+
setLoading(false);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!dataSource) {
|
|
138
|
+
throw new Error('DataSource required for object/api providers');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (dataConfig?.provider === 'object') {
|
|
142
|
+
const objectName = dataConfig.object;
|
|
143
|
+
const result = await dataSource.find(objectName, {
|
|
144
|
+
$filter: schema.filter,
|
|
145
|
+
$orderby: convertSortToQueryParams(schema.sort),
|
|
146
|
+
});
|
|
147
|
+
setData(result?.data || []);
|
|
148
|
+
} else if (dataConfig?.provider === 'api') {
|
|
149
|
+
console.warn('API provider not yet implemented for ObjectCalendar');
|
|
150
|
+
setData([]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setLoading(false);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
setError(err as Error);
|
|
156
|
+
setLoading(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
fetchData();
|
|
161
|
+
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]);
|
|
162
|
+
|
|
163
|
+
// Fetch object schema for field metadata
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const fetchObjectSchema = async () => {
|
|
166
|
+
try {
|
|
167
|
+
if (!dataSource) return;
|
|
168
|
+
|
|
169
|
+
const objectName = dataConfig?.provider === 'object'
|
|
170
|
+
? dataConfig.object
|
|
171
|
+
: schema.objectName;
|
|
172
|
+
|
|
173
|
+
if (!objectName) return;
|
|
174
|
+
|
|
175
|
+
const schemaData = await dataSource.getObjectSchema(objectName);
|
|
176
|
+
setObjectSchema(schemaData);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.error('Failed to fetch object schema:', err);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (!hasInlineData && dataSource) {
|
|
183
|
+
fetchObjectSchema();
|
|
184
|
+
}
|
|
185
|
+
}, [schema.objectName, dataSource, hasInlineData, dataConfig]);
|
|
186
|
+
|
|
187
|
+
// Transform data to calendar events
|
|
188
|
+
const events = useMemo(() => {
|
|
189
|
+
if (!calendarConfig || !data.length) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { startDateField, endDateField, titleField, colorField } = calendarConfig;
|
|
194
|
+
|
|
195
|
+
return data.map((record, index) => {
|
|
196
|
+
const startDate = record[startDateField];
|
|
197
|
+
const endDate = endDateField ? record[endDateField] : null;
|
|
198
|
+
const title = record[titleField] || 'Untitled';
|
|
199
|
+
const color = colorField ? record[colorField] : undefined;
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
id: record.id || record._id || `event-${index}`,
|
|
203
|
+
title,
|
|
204
|
+
start: startDate ? new Date(startDate) : new Date(),
|
|
205
|
+
end: endDate ? new Date(endDate) : undefined,
|
|
206
|
+
color,
|
|
207
|
+
allDay: !endDate, // If no end date, treat as all-day event
|
|
208
|
+
data: record,
|
|
209
|
+
};
|
|
210
|
+
}).filter(event => !isNaN(event.start.getTime())); // Filter out invalid dates
|
|
211
|
+
}, [data, calendarConfig]);
|
|
212
|
+
|
|
213
|
+
// Get days in current month view
|
|
214
|
+
const calendarDays = useMemo(() => {
|
|
215
|
+
const year = currentDate.getFullYear();
|
|
216
|
+
const month = currentDate.getMonth();
|
|
217
|
+
|
|
218
|
+
const firstDay = new Date(year, month, 1);
|
|
219
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
220
|
+
const startDay = new Date(firstDay);
|
|
221
|
+
startDay.setDate(startDay.getDate() - startDay.getDay()); // Start from Sunday
|
|
222
|
+
|
|
223
|
+
const days: Date[] = [];
|
|
224
|
+
const current = new Date(startDay);
|
|
225
|
+
|
|
226
|
+
// Get 6 weeks worth of days
|
|
227
|
+
for (let i = 0; i < 42; i++) {
|
|
228
|
+
days.push(new Date(current));
|
|
229
|
+
current.setDate(current.getDate() + 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return days;
|
|
233
|
+
}, [currentDate]);
|
|
234
|
+
|
|
235
|
+
// Get events for a specific day
|
|
236
|
+
const getEventsForDay = useCallback((day: Date) => {
|
|
237
|
+
return events.filter(event => {
|
|
238
|
+
const eventStart = new Date(event.start);
|
|
239
|
+
eventStart.setHours(0, 0, 0, 0);
|
|
240
|
+
const eventEnd = event.end ? new Date(event.end) : eventStart;
|
|
241
|
+
eventEnd.setHours(23, 59, 59, 999);
|
|
242
|
+
|
|
243
|
+
const checkDay = new Date(day);
|
|
244
|
+
checkDay.setHours(0, 0, 0, 0);
|
|
245
|
+
|
|
246
|
+
return checkDay >= eventStart && checkDay <= eventEnd;
|
|
247
|
+
});
|
|
248
|
+
}, [events]);
|
|
249
|
+
|
|
250
|
+
const navigateMonth = useCallback((direction: 'prev' | 'next') => {
|
|
251
|
+
setCurrentDate(prev => {
|
|
252
|
+
const newDate = new Date(prev);
|
|
253
|
+
newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
|
|
254
|
+
return newDate;
|
|
255
|
+
});
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
if (loading) {
|
|
259
|
+
return (
|
|
260
|
+
<div className={className}>
|
|
261
|
+
<div className="flex items-center justify-center h-96">
|
|
262
|
+
<div className="text-muted-foreground">Loading calendar...</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (error) {
|
|
269
|
+
return (
|
|
270
|
+
<div className={className}>
|
|
271
|
+
<div className="flex items-center justify-center h-96">
|
|
272
|
+
<div className="text-destructive">Error: {error.message}</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!calendarConfig) {
|
|
279
|
+
return (
|
|
280
|
+
<div className={className}>
|
|
281
|
+
<div className="flex items-center justify-center h-96">
|
|
282
|
+
<div className="text-muted-foreground">
|
|
283
|
+
Calendar configuration required. Please specify startDateField and titleField.
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
291
|
+
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
292
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div className={className}>
|
|
296
|
+
<div className="border rounded-lg bg-background">
|
|
297
|
+
{/* Calendar Header */}
|
|
298
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
299
|
+
<h2 className="text-xl font-semibold">
|
|
300
|
+
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
|
301
|
+
</h2>
|
|
302
|
+
<div className="flex gap-2">
|
|
303
|
+
<button
|
|
304
|
+
onClick={() => navigateMonth('prev')}
|
|
305
|
+
className="px-3 py-1 border rounded hover:bg-muted"
|
|
306
|
+
>
|
|
307
|
+
Previous
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => setCurrentDate(new Date())}
|
|
311
|
+
className="px-3 py-1 border rounded hover:bg-muted"
|
|
312
|
+
>
|
|
313
|
+
Today
|
|
314
|
+
</button>
|
|
315
|
+
<button
|
|
316
|
+
onClick={() => navigateMonth('next')}
|
|
317
|
+
className="px-3 py-1 border rounded hover:bg-muted"
|
|
318
|
+
>
|
|
319
|
+
Next
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Calendar Grid */}
|
|
325
|
+
<div className="p-4">
|
|
326
|
+
{/* Day Headers */}
|
|
327
|
+
<div className="grid grid-cols-7 gap-px mb-px">
|
|
328
|
+
{dayNames.map(day => (
|
|
329
|
+
<div
|
|
330
|
+
key={day}
|
|
331
|
+
className="text-center text-sm font-medium text-muted-foreground py-2"
|
|
332
|
+
>
|
|
333
|
+
{day}
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
|
|
338
|
+
{/* Calendar Days */}
|
|
339
|
+
<div className="grid grid-cols-7 gap-px bg-border">
|
|
340
|
+
{calendarDays.map((day, index) => {
|
|
341
|
+
const dayEvents = getEventsForDay(day);
|
|
342
|
+
const isCurrentMonth = day.getMonth() === currentDate.getMonth();
|
|
343
|
+
const isToday =
|
|
344
|
+
day.getDate() === new Date().getDate() &&
|
|
345
|
+
day.getMonth() === new Date().getMonth() &&
|
|
346
|
+
day.getFullYear() === new Date().getFullYear();
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div
|
|
350
|
+
key={index}
|
|
351
|
+
className={`min-h-24 bg-background p-2 ${
|
|
352
|
+
!isCurrentMonth ? 'text-muted-foreground bg-muted/30' : ''
|
|
353
|
+
} ${isToday ? 'ring-2 ring-primary' : ''}`}
|
|
354
|
+
onClick={() => onDateClick?.(day)}
|
|
355
|
+
>
|
|
356
|
+
<div className="text-sm font-medium mb-1">{day.getDate()}</div>
|
|
357
|
+
<div className="space-y-1">
|
|
358
|
+
{dayEvents.slice(0, 3).map(event => (
|
|
359
|
+
<div
|
|
360
|
+
key={event.id}
|
|
361
|
+
className="text-xs px-1 py-0.5 rounded bg-primary/10 hover:bg-primary/20 cursor-pointer truncate"
|
|
362
|
+
onClick={(e) => {
|
|
363
|
+
e.stopPropagation();
|
|
364
|
+
onEventClick?.(event.data);
|
|
365
|
+
}}
|
|
366
|
+
style={event.color ? { borderLeft: `3px solid ${event.color}` } : undefined}
|
|
367
|
+
>
|
|
368
|
+
{event.title}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
371
|
+
{dayEvents.length > 3 && (
|
|
372
|
+
<div className="text-xs text-muted-foreground">
|
|
373
|
+
+{dayEvents.length - 3} more
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
})}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
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 { ComponentRegistry } from '@object-ui/core';
|
|
10
|
+
import type { CalendarViewSchema, CalendarEvent } from '@object-ui/types';
|
|
11
|
+
import { CalendarView } from './CalendarView';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
// Calendar View Renderer - Airtable-style calendar for displaying records as events
|
|
15
|
+
ComponentRegistry.register('calendar-view',
|
|
16
|
+
({ schema, className, onAction, ...props }: { schema: CalendarViewSchema; className?: string; onAction?: (action: any) => void; [key: string]: any }) => {
|
|
17
|
+
// Transform schema data to CalendarEvent format
|
|
18
|
+
const events = React.useMemo(() => {
|
|
19
|
+
if (!schema.data || !Array.isArray(schema.data)) return [];
|
|
20
|
+
|
|
21
|
+
return schema.data.map((record: any, index: number) => {
|
|
22
|
+
/** Field name to use for event title display */
|
|
23
|
+
const titleField = schema.titleField || 'title';
|
|
24
|
+
/** Field name containing the event start date/time */
|
|
25
|
+
const startField = schema.startDateField || 'start';
|
|
26
|
+
/** Field name containing the event end date/time (optional) */
|
|
27
|
+
const endField = schema.endDateField || 'end';
|
|
28
|
+
/** Field name to determine event color or color category */
|
|
29
|
+
const colorField = schema.colorField || 'color';
|
|
30
|
+
/** Field name indicating if event is all-day */
|
|
31
|
+
const allDayField = schema.allDayField || 'allDay';
|
|
32
|
+
|
|
33
|
+
const title = record[titleField] || 'Untitled';
|
|
34
|
+
const start = record[startField] ? new Date(record[startField]) : new Date();
|
|
35
|
+
const end = record[endField] ? new Date(record[endField]) : undefined;
|
|
36
|
+
const allDay = record[allDayField] !== undefined ? record[allDayField] : false;
|
|
37
|
+
|
|
38
|
+
// Handle color mapping
|
|
39
|
+
let color = record[colorField];
|
|
40
|
+
if (color && schema.colorMapping && schema.colorMapping[color]) {
|
|
41
|
+
color = schema.colorMapping[color];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
id: String(record.id || record._id || index),
|
|
46
|
+
title,
|
|
47
|
+
start,
|
|
48
|
+
end,
|
|
49
|
+
allDay,
|
|
50
|
+
color,
|
|
51
|
+
data: record,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField, schema.colorMapping]);
|
|
55
|
+
|
|
56
|
+
const handleEventClick = React.useCallback((event: any) => {
|
|
57
|
+
if (onAction) {
|
|
58
|
+
onAction({
|
|
59
|
+
type: 'event_click',
|
|
60
|
+
payload: { event: event.data, eventId: event.id }
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (schema.onEventClick) {
|
|
64
|
+
schema.onEventClick(event.data);
|
|
65
|
+
}
|
|
66
|
+
}, [onAction, schema]);
|
|
67
|
+
|
|
68
|
+
const handleDateClick = React.useCallback((date: Date) => {
|
|
69
|
+
if (onAction) {
|
|
70
|
+
onAction({
|
|
71
|
+
type: 'date_click',
|
|
72
|
+
payload: { date }
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (schema.onDateClick) {
|
|
76
|
+
schema.onDateClick(date);
|
|
77
|
+
}
|
|
78
|
+
}, [onAction, schema]);
|
|
79
|
+
|
|
80
|
+
const handleViewChange = React.useCallback((view: "month" | "week" | "day") => {
|
|
81
|
+
if (onAction) {
|
|
82
|
+
onAction({
|
|
83
|
+
type: 'view_change',
|
|
84
|
+
payload: { view }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (schema.onViewChange) {
|
|
88
|
+
schema.onViewChange(view);
|
|
89
|
+
}
|
|
90
|
+
}, [onAction, schema]);
|
|
91
|
+
|
|
92
|
+
const handleNavigate = React.useCallback((date: Date) => {
|
|
93
|
+
if (onAction) {
|
|
94
|
+
onAction({
|
|
95
|
+
type: 'navigate',
|
|
96
|
+
payload: { date }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (schema.onNavigate) {
|
|
100
|
+
schema.onNavigate(date);
|
|
101
|
+
}
|
|
102
|
+
}, [onAction, schema]);
|
|
103
|
+
|
|
104
|
+
const validView = (schema.view && ['month', 'week', 'day'].includes(schema.view))
|
|
105
|
+
? (schema.view as "month" | "week" | "day")
|
|
106
|
+
: 'month';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<CalendarView
|
|
110
|
+
events={events}
|
|
111
|
+
view={validView}
|
|
112
|
+
currentDate={schema.currentDate ? new Date(schema.currentDate) : undefined}
|
|
113
|
+
onEventClick={handleEventClick}
|
|
114
|
+
onDateClick={schema.allowCreate || schema.onDateClick ? handleDateClick : undefined}
|
|
115
|
+
onViewChange={handleViewChange}
|
|
116
|
+
onNavigate={handleNavigate}
|
|
117
|
+
className={className}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: 'Calendar View',
|
|
124
|
+
inputs: [
|
|
125
|
+
{
|
|
126
|
+
name: 'data',
|
|
127
|
+
type: 'array',
|
|
128
|
+
label: 'Data',
|
|
129
|
+
description: 'Array of record objects to display as events'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'titleField',
|
|
133
|
+
type: 'string',
|
|
134
|
+
label: 'Title Field',
|
|
135
|
+
defaultValue: 'title',
|
|
136
|
+
description: 'Field name to use for event title'
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'startDateField',
|
|
140
|
+
type: 'string',
|
|
141
|
+
label: 'Start Date Field',
|
|
142
|
+
defaultValue: 'start',
|
|
143
|
+
description: 'Field name for event start date'
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'endDateField',
|
|
147
|
+
type: 'string',
|
|
148
|
+
label: 'End Date Field',
|
|
149
|
+
defaultValue: 'end',
|
|
150
|
+
description: 'Field name for event end date (optional)'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'allDayField',
|
|
154
|
+
type: 'string',
|
|
155
|
+
label: 'All Day Field',
|
|
156
|
+
defaultValue: 'allDay',
|
|
157
|
+
description: 'Field name for all-day flag'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'colorField',
|
|
161
|
+
type: 'string',
|
|
162
|
+
label: 'Color Field',
|
|
163
|
+
defaultValue: 'color',
|
|
164
|
+
description: 'Field name for event color'
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'colorMapping',
|
|
168
|
+
type: 'object',
|
|
169
|
+
label: 'Color Mapping',
|
|
170
|
+
description: 'Map field values to colors (e.g., {meeting: "blue", deadline: "red"})'
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'view',
|
|
174
|
+
type: 'enum',
|
|
175
|
+
enum: ['month', 'week', 'day'],
|
|
176
|
+
defaultValue: 'month',
|
|
177
|
+
label: 'View Mode',
|
|
178
|
+
description: 'Calendar view mode (month, week, or day)'
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'currentDate',
|
|
182
|
+
type: 'string',
|
|
183
|
+
label: 'Current Date',
|
|
184
|
+
description: 'ISO date string for initial calendar date'
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'allowCreate',
|
|
188
|
+
type: 'boolean',
|
|
189
|
+
label: 'Allow Create',
|
|
190
|
+
defaultValue: false,
|
|
191
|
+
description: 'Allow creating events by clicking on dates'
|
|
192
|
+
},
|
|
193
|
+
{ name: 'className', type: 'string', label: 'CSS Class' }
|
|
194
|
+
],
|
|
195
|
+
defaultProps: {
|
|
196
|
+
view: 'month',
|
|
197
|
+
titleField: 'title',
|
|
198
|
+
startDateField: 'start',
|
|
199
|
+
endDateField: 'end',
|
|
200
|
+
allDayField: 'allDay',
|
|
201
|
+
colorField: 'color',
|
|
202
|
+
allowCreate: false,
|
|
203
|
+
data: [
|
|
204
|
+
{
|
|
205
|
+
id: 1,
|
|
206
|
+
title: 'Team Meeting',
|
|
207
|
+
start: new Date(new Date().setHours(10, 0, 0, 0)).toISOString(),
|
|
208
|
+
end: new Date(new Date().setHours(11, 0, 0, 0)).toISOString(),
|
|
209
|
+
color: '#3b82f6',
|
|
210
|
+
allDay: false
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 2,
|
|
214
|
+
title: 'Project Deadline',
|
|
215
|
+
start: new Date(new Date().setDate(new Date().getDate() + 3)).toISOString(),
|
|
216
|
+
color: '#ef4444',
|
|
217
|
+
allDay: true
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 3,
|
|
221
|
+
title: 'Conference',
|
|
222
|
+
start: new Date(new Date().setDate(new Date().getDate() + 7)).toISOString(),
|
|
223
|
+
end: new Date(new Date().setDate(new Date().getDate() + 9)).toISOString(),
|
|
224
|
+
color: '#10b981',
|
|
225
|
+
allDay: true
|
|
226
|
+
}
|
|
227
|
+
],
|
|
228
|
+
className: 'h-[600px] border rounded-lg'
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
);
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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 { ObjectCalendar } from './ObjectCalendar';
|
|
12
|
+
import type { ObjectCalendarProps } from './ObjectCalendar';
|
|
13
|
+
|
|
14
|
+
// Export ObjectCalendar component
|
|
15
|
+
export { ObjectCalendar };
|
|
16
|
+
export type { ObjectCalendarProps };
|
|
17
|
+
|
|
18
|
+
// Export CalendarView component (merged from plugin-calendar-view)
|
|
19
|
+
export { CalendarView } from './CalendarView';
|
|
20
|
+
export type { CalendarViewProps, CalendarEvent } from './CalendarView';
|
|
21
|
+
|
|
22
|
+
// Import and register calendar-view renderer
|
|
23
|
+
import './calendar-view-renderer';
|
|
24
|
+
|
|
25
|
+
// Register object-calendar component
|
|
26
|
+
const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
27
|
+
return <ObjectCalendar schema={schema} dataSource={null as any} />;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ComponentRegistry.register('object-calendar', ObjectCalendarRenderer, {
|
|
31
|
+
label: 'Object Calendar',
|
|
32
|
+
category: 'plugin',
|
|
33
|
+
inputs: [
|
|
34
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
35
|
+
{ name: 'calendar', type: 'object', label: 'Calendar Config', description: 'startDateField, endDateField, titleField, colorField' },
|
|
36
|
+
],
|
|
37
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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"]
|
|
18
|
+
}
|