@object-ui/plugin-calendar 0.3.1 → 2.0.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/.turbo/turbo-build.log +21 -0
- package/CHANGELOG.md +15 -0
- package/dist/index.js +539 -560
- package/dist/index.umd.cjs +2 -2
- package/dist/src/CalendarView.d.ts +3 -1
- package/dist/src/CalendarView.d.ts.map +1 -1
- package/dist/src/ObjectCalendar.d.ts +15 -1
- package/dist/src/ObjectCalendar.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/CalendarView.test.tsx +118 -0
- package/src/CalendarView.tsx +95 -30
- package/src/ObjectCalendar.msw.test.tsx +100 -0
- package/src/ObjectCalendar.tsx +113 -149
- package/src/calendar-view-renderer.tsx +32 -76
- package/src/index.tsx +16 -3
- package/src/registration.test.tsx +25 -0
- package/test/setup.ts +32 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +13 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { ObjectCalendar } from './ObjectCalendar';
|
|
5
|
+
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
|
|
6
|
+
import { setupServer } from 'msw/node';
|
|
7
|
+
import { http, HttpResponse } from 'msw';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
10
|
+
const BASE_URL = 'http://localhost';
|
|
11
|
+
|
|
12
|
+
// --- Mock Data ---
|
|
13
|
+
|
|
14
|
+
// Create a stable date at noon to avoid day-spanning issues when tests run late at night
|
|
15
|
+
const todayAtNoon = new Date();
|
|
16
|
+
todayAtNoon.setHours(12, 0, 0, 0);
|
|
17
|
+
|
|
18
|
+
const mockEvents = {
|
|
19
|
+
value: [
|
|
20
|
+
{
|
|
21
|
+
_id: '1',
|
|
22
|
+
title: 'Meeting with Client',
|
|
23
|
+
start: todayAtNoon.toISOString(),
|
|
24
|
+
end: new Date(todayAtNoon.getTime() + 3600000).toISOString(),
|
|
25
|
+
type: 'business'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
_id: '2',
|
|
29
|
+
title: 'Team Lunch',
|
|
30
|
+
start: new Date(todayAtNoon.getTime() + 86400000).toISOString(), // Tomorrow
|
|
31
|
+
type: 'personal'
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// --- MSW Setup ---
|
|
37
|
+
|
|
38
|
+
const handlers = [
|
|
39
|
+
http.options('*', () => {
|
|
40
|
+
return new HttpResponse(null, { status: 200 });
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
http.get(`${BASE_URL}/api/v1`, () => {
|
|
44
|
+
return HttpResponse.json({ status: 'ok', version: '1.0.0' });
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// Data Query: GET /api/v1/data/events
|
|
48
|
+
http.get(`${BASE_URL}/api/v1/data/events`, () => {
|
|
49
|
+
return HttpResponse.json(mockEvents);
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
// Metadata Query
|
|
53
|
+
http.get(`${BASE_URL}/api/v1/metadata/object/events`, () => {
|
|
54
|
+
return HttpResponse.json({ fields: {} });
|
|
55
|
+
})
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const server = setupServer(...handlers);
|
|
59
|
+
|
|
60
|
+
// --- Test Suite ---
|
|
61
|
+
|
|
62
|
+
describe('ObjectCalendar with MSW', () => {
|
|
63
|
+
beforeAll(() => server.listen());
|
|
64
|
+
afterEach(() => server.resetHandlers());
|
|
65
|
+
afterAll(() => server.close());
|
|
66
|
+
|
|
67
|
+
it('fetches events and renders them', async () => {
|
|
68
|
+
const dataSource = new ObjectStackAdapter({
|
|
69
|
+
baseUrl: BASE_URL,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
render(
|
|
73
|
+
<ObjectCalendar
|
|
74
|
+
schema={{
|
|
75
|
+
type: 'calendar',
|
|
76
|
+
objectName: 'events',
|
|
77
|
+
// Calendar specific config usually goes into 'calendar' prop or implicit mapping
|
|
78
|
+
calendar: {
|
|
79
|
+
dateField: 'start',
|
|
80
|
+
endDateField: 'end',
|
|
81
|
+
titleField: 'title'
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
dataSource={dataSource}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Verify events appear
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByText('Meeting with Client')).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Check subsequent events (might need navigation depending on view,
|
|
94
|
+
// but default month view usually shows current month dates)
|
|
95
|
+
// Note: If today is near end of month, tomorrow might be in next month view.
|
|
96
|
+
// However, the test event is "now", so it should be visible.
|
|
97
|
+
|
|
98
|
+
// We can just assert the first event for now.
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/ObjectCalendar.tsx
CHANGED
|
@@ -24,26 +24,42 @@
|
|
|
24
24
|
|
|
25
25
|
import React, { useEffect, useState, useCallback, useMemo } from 'react';
|
|
26
26
|
import type { ObjectGridSchema, DataSource, ViewData, CalendarConfig } from '@object-ui/types';
|
|
27
|
+
import { CalendarView, type CalendarEvent } from './CalendarView';
|
|
28
|
+
|
|
29
|
+
export interface CalendarSchema {
|
|
30
|
+
type: 'calendar';
|
|
31
|
+
objectName?: string;
|
|
32
|
+
dateField?: string;
|
|
33
|
+
endField?: string;
|
|
34
|
+
titleField?: string;
|
|
35
|
+
colorField?: string;
|
|
36
|
+
filter?: any;
|
|
37
|
+
sort?: any;
|
|
38
|
+
/** Initial view mode */
|
|
39
|
+
defaultView?: 'month' | 'week' | 'day';
|
|
40
|
+
}
|
|
27
41
|
|
|
28
42
|
export interface ObjectCalendarProps {
|
|
29
|
-
schema: ObjectGridSchema;
|
|
43
|
+
schema: ObjectGridSchema | CalendarSchema;
|
|
30
44
|
dataSource?: DataSource;
|
|
31
45
|
className?: string;
|
|
32
46
|
onEventClick?: (record: any) => void;
|
|
33
47
|
onDateClick?: (date: Date) => void;
|
|
34
48
|
onEdit?: (record: any) => void;
|
|
35
49
|
onDelete?: (record: any) => void;
|
|
50
|
+
onNavigate?: (date: Date) => void;
|
|
51
|
+
onViewChange?: (view: 'month' | 'week' | 'day') => void;
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
/**
|
|
39
55
|
* Helper to get data configuration from schema
|
|
40
56
|
*/
|
|
41
|
-
function getDataConfig(schema: ObjectGridSchema): ViewData | null {
|
|
42
|
-
if (schema.data) {
|
|
57
|
+
function getDataConfig(schema: ObjectGridSchema | CalendarSchema): ViewData | null {
|
|
58
|
+
if ('data' in schema && schema.data) {
|
|
43
59
|
return schema.data;
|
|
44
60
|
}
|
|
45
61
|
|
|
46
|
-
if (schema.staticData) {
|
|
62
|
+
if ('staticData' in schema && schema.staticData) {
|
|
47
63
|
return {
|
|
48
64
|
provider: 'value',
|
|
49
65
|
items: schema.staticData,
|
|
@@ -90,9 +106,9 @@ function convertSortToQueryParams(sort: string | any[] | undefined): Record<stri
|
|
|
90
106
|
/**
|
|
91
107
|
* Helper to get calendar configuration from schema
|
|
92
108
|
*/
|
|
93
|
-
function getCalendarConfig(schema: ObjectGridSchema): CalendarConfig | null {
|
|
109
|
+
function getCalendarConfig(schema: ObjectGridSchema | CalendarSchema): CalendarConfig | null {
|
|
94
110
|
// Check if schema has calendar configuration
|
|
95
|
-
if (schema.filter && typeof schema.filter === 'object' && 'calendar' in schema.filter) {
|
|
111
|
+
if ('filter' in schema && schema.filter && typeof schema.filter === 'object' && 'calendar' in schema.filter) {
|
|
96
112
|
return (schema.filter as any).calendar as CalendarConfig;
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -101,6 +117,17 @@ function getCalendarConfig(schema: ObjectGridSchema): CalendarConfig | null {
|
|
|
101
117
|
return (schema as any).calendar as CalendarConfig;
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
// Check for flat properties (used by ObjectView)
|
|
121
|
+
if ((schema as any).startDateField || (schema as any).dateField) {
|
|
122
|
+
return {
|
|
123
|
+
startDateField: (schema as any).startDateField || (schema as any).dateField,
|
|
124
|
+
endDateField: (schema as any).endDateField || (schema as any).endField,
|
|
125
|
+
titleField: (schema as any).titleField || 'name',
|
|
126
|
+
colorField: (schema as any).colorField,
|
|
127
|
+
allDayField: (schema as any).allDayField
|
|
128
|
+
} as CalendarConfig;
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
return null;
|
|
105
132
|
}
|
|
106
133
|
|
|
@@ -110,6 +137,9 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
110
137
|
className,
|
|
111
138
|
onEventClick,
|
|
112
139
|
onDateClick,
|
|
140
|
+
onNavigate,
|
|
141
|
+
onViewChange,
|
|
142
|
+
...rest
|
|
113
143
|
}) => {
|
|
114
144
|
const [data, setData] = useState<any[]>([]);
|
|
115
145
|
const [loading, setLoading] = useState(true);
|
|
@@ -118,22 +148,47 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
118
148
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
119
149
|
const [view, setView] = useState<'month' | 'week' | 'day'>('month');
|
|
120
150
|
|
|
121
|
-
const dataConfig = getDataConfig(schema)
|
|
122
|
-
|
|
151
|
+
const dataConfig = useMemo(() => getDataConfig(schema), [
|
|
152
|
+
(schema as any).data,
|
|
153
|
+
(schema as any).staticData,
|
|
154
|
+
schema.objectName,
|
|
155
|
+
]);
|
|
156
|
+
const calendarConfig = useMemo(() => getCalendarConfig(schema), [
|
|
157
|
+
schema.filter,
|
|
158
|
+
(schema as any).calendar,
|
|
159
|
+
(schema as any).dateField,
|
|
160
|
+
(schema as any).endField,
|
|
161
|
+
(schema as any).titleField,
|
|
162
|
+
(schema as any).colorField
|
|
163
|
+
]);
|
|
123
164
|
const hasInlineData = dataConfig?.provider === 'value';
|
|
124
165
|
|
|
125
166
|
// Fetch data based on provider
|
|
126
167
|
useEffect(() => {
|
|
168
|
+
let isMounted = true;
|
|
127
169
|
const fetchData = async () => {
|
|
128
170
|
try {
|
|
171
|
+
if (!isMounted) return;
|
|
129
172
|
setLoading(true);
|
|
130
173
|
|
|
131
174
|
if (hasInlineData && dataConfig?.provider === 'value') {
|
|
132
|
-
|
|
133
|
-
|
|
175
|
+
if (isMounted) {
|
|
176
|
+
setData(dataConfig.items as any[]);
|
|
177
|
+
setLoading(false);
|
|
178
|
+
}
|
|
134
179
|
return;
|
|
135
180
|
}
|
|
136
181
|
|
|
182
|
+
// Prioritize data passed from parent (ListView)
|
|
183
|
+
if ((schema as any).data || (rest as any).data) {
|
|
184
|
+
const passedData = (schema as any).data || (rest as any).data;
|
|
185
|
+
if (Array.isArray(passedData)) {
|
|
186
|
+
setData(passedData);
|
|
187
|
+
setLoading(false);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
137
192
|
if (!dataSource) {
|
|
138
193
|
throw new Error('DataSource required for object/api providers');
|
|
139
194
|
}
|
|
@@ -144,20 +199,39 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
144
199
|
$filter: schema.filter,
|
|
145
200
|
$orderby: convertSortToQueryParams(schema.sort),
|
|
146
201
|
});
|
|
147
|
-
|
|
202
|
+
|
|
203
|
+
let items: any[] = [];
|
|
204
|
+
|
|
205
|
+
if (Array.isArray(result)) {
|
|
206
|
+
items = result;
|
|
207
|
+
} else if (result && typeof result === 'object') {
|
|
208
|
+
if (Array.isArray((result as any).data)) {
|
|
209
|
+
items = (result as any).data;
|
|
210
|
+
} else if (Array.isArray((result as any).value)) {
|
|
211
|
+
items = (result as any).value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isMounted) {
|
|
216
|
+
setData(items);
|
|
217
|
+
}
|
|
148
218
|
} else if (dataConfig?.provider === 'api') {
|
|
149
219
|
console.warn('API provider not yet implemented for ObjectCalendar');
|
|
150
|
-
setData([]);
|
|
220
|
+
if (isMounted) setData([]);
|
|
151
221
|
}
|
|
152
222
|
|
|
153
|
-
setLoading(false);
|
|
223
|
+
if (isMounted) setLoading(false);
|
|
154
224
|
} catch (err) {
|
|
155
|
-
|
|
156
|
-
|
|
225
|
+
console.error('[ObjectCalendar] Error fetching data:', err);
|
|
226
|
+
if (isMounted) {
|
|
227
|
+
setError(err as Error);
|
|
228
|
+
setLoading(false);
|
|
229
|
+
}
|
|
157
230
|
}
|
|
158
231
|
};
|
|
159
232
|
|
|
160
233
|
fetchData();
|
|
234
|
+
return () => { isMounted = false; };
|
|
161
235
|
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]);
|
|
162
236
|
|
|
163
237
|
// Fetch object schema for field metadata
|
|
@@ -210,50 +284,13 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
210
284
|
}).filter(event => !isNaN(event.start.getTime())); // Filter out invalid dates
|
|
211
285
|
}, [data, calendarConfig]);
|
|
212
286
|
|
|
213
|
-
// Get days in current month view
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
}, []);
|
|
287
|
+
// Get days in current month view - REMOVED (Handled by CalendarView)
|
|
288
|
+
|
|
289
|
+
const handleCreate = useCallback(() => {
|
|
290
|
+
// Standard "Create" action trigger
|
|
291
|
+
const today = new Date();
|
|
292
|
+
onDateClick?.(today);
|
|
293
|
+
}, [onDateClick]);
|
|
257
294
|
|
|
258
295
|
if (loading) {
|
|
259
296
|
return (
|
|
@@ -287,98 +324,25 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
|
287
324
|
);
|
|
288
325
|
}
|
|
289
326
|
|
|
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
327
|
return (
|
|
295
328
|
<div className={className}>
|
|
296
|
-
<div className="border rounded-lg bg-background">
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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>
|
|
329
|
+
<div className="border rounded-lg bg-background h-[calc(100vh-200px)] min-h-[600px]">
|
|
330
|
+
<CalendarView
|
|
331
|
+
events={events}
|
|
332
|
+
currentDate={currentDate}
|
|
333
|
+
view={(schema as any).defaultView || 'month'}
|
|
334
|
+
onEventClick={(event) => onEventClick?.(event.data)}
|
|
335
|
+
onDateClick={onDateClick}
|
|
336
|
+
onNavigate={(date) => {
|
|
337
|
+
setCurrentDate(date);
|
|
338
|
+
onNavigate?.(date);
|
|
339
|
+
}}
|
|
340
|
+
onViewChange={(v) => {
|
|
341
|
+
setView(v);
|
|
342
|
+
onViewChange?.(v);
|
|
343
|
+
}}
|
|
344
|
+
onAddClick={handleCreate}
|
|
345
|
+
/>
|
|
382
346
|
</div>
|
|
383
347
|
</div>
|
|
384
348
|
);
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ComponentRegistry } from '@object-ui/core';
|
|
10
|
-
import type { CalendarViewSchema
|
|
11
|
-
import { CalendarView } from './CalendarView';
|
|
10
|
+
import type { CalendarViewSchema } from '@object-ui/types';
|
|
11
|
+
import { CalendarView, type CalendarEvent } from './CalendarView';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
|
|
14
14
|
// Calendar View Renderer - Airtable-style calendar for displaying records as events
|
|
@@ -30,96 +30,52 @@ ComponentRegistry.register('calendar-view',
|
|
|
30
30
|
/** Field name indicating if event is all-day */
|
|
31
31
|
const allDayField = schema.allDayField || 'allDay';
|
|
32
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
33
|
return {
|
|
45
|
-
id:
|
|
46
|
-
title,
|
|
47
|
-
start,
|
|
48
|
-
end,
|
|
49
|
-
allDay,
|
|
50
|
-
color,
|
|
34
|
+
id: record._id || record.id || index,
|
|
35
|
+
title: record[titleField] || 'Untitled Event',
|
|
36
|
+
start: new Date(record[startField]),
|
|
37
|
+
end: record[endField] ? new Date(record[endField]) : undefined,
|
|
38
|
+
allDay: record[allDayField],
|
|
39
|
+
color: record[colorField],
|
|
51
40
|
data: record,
|
|
52
41
|
};
|
|
53
42
|
});
|
|
54
|
-
}, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField
|
|
43
|
+
}, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField]);
|
|
55
44
|
|
|
56
|
-
const handleEventClick =
|
|
57
|
-
if (onAction) {
|
|
58
|
-
onAction({
|
|
59
|
-
type: 'event_click',
|
|
60
|
-
payload: { event: event.data, eventId: event.id }
|
|
61
|
-
});
|
|
62
|
-
}
|
|
45
|
+
const handleEventClick = (event: CalendarEvent) => {
|
|
63
46
|
if (schema.onEventClick) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 }
|
|
47
|
+
// Dispatch configured action
|
|
48
|
+
// This would use the action runner in a real implementation
|
|
49
|
+
// For now we just call onAction if provided
|
|
50
|
+
onAction?.({
|
|
51
|
+
type: 'event-click',
|
|
52
|
+
payload: event
|
|
97
53
|
});
|
|
98
54
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleAddClick = () => {
|
|
58
|
+
// Standard "Create" action trigger
|
|
59
|
+
onAction?.({
|
|
60
|
+
type: 'create',
|
|
61
|
+
payload: {}
|
|
62
|
+
});
|
|
63
|
+
};
|
|
107
64
|
|
|
108
65
|
return (
|
|
109
|
-
<CalendarView
|
|
66
|
+
<CalendarView
|
|
67
|
+
className={className}
|
|
110
68
|
events={events}
|
|
111
|
-
view={validView}
|
|
112
|
-
currentDate={schema.currentDate ? new Date(schema.currentDate) : undefined}
|
|
113
69
|
onEventClick={handleEventClick}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
onNavigate={handleNavigate}
|
|
117
|
-
className={className}
|
|
70
|
+
onAddClick={handleAddClick}
|
|
71
|
+
// Pass validation or other props
|
|
118
72
|
{...props}
|
|
119
73
|
/>
|
|
120
74
|
);
|
|
121
|
-
}
|
|
75
|
+
}
|
|
76
|
+
,
|
|
122
77
|
{
|
|
78
|
+
namespace: 'plugin-calendar',
|
|
123
79
|
label: 'Calendar View',
|
|
124
80
|
inputs: [
|
|
125
81
|
{
|
package/src/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { ComponentRegistry } from '@object-ui/core';
|
|
11
|
+
import { useSchemaContext } from '@object-ui/react';
|
|
11
12
|
import { ObjectCalendar } from './ObjectCalendar';
|
|
12
13
|
import type { ObjectCalendarProps } from './ObjectCalendar';
|
|
13
14
|
|
|
@@ -23,13 +24,25 @@ export type { CalendarViewProps, CalendarEvent } from './CalendarView';
|
|
|
23
24
|
import './calendar-view-renderer';
|
|
24
25
|
|
|
25
26
|
// Register object-calendar component
|
|
26
|
-
const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
27
|
-
|
|
27
|
+
export const ObjectCalendarRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
28
|
+
const { dataSource } = useSchemaContext();
|
|
29
|
+
return <ObjectCalendar schema={schema} dataSource={dataSource} />;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
ComponentRegistry.register('object-calendar', ObjectCalendarRenderer, {
|
|
33
|
+
namespace: 'plugin-calendar',
|
|
31
34
|
label: 'Object Calendar',
|
|
32
|
-
category: '
|
|
35
|
+
category: 'view',
|
|
36
|
+
inputs: [
|
|
37
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
38
|
+
{ name: 'calendar', type: 'object', label: 'Calendar Config', description: 'startDateField, endDateField, titleField, colorField' },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
ComponentRegistry.register('calendar', ObjectCalendarRenderer, {
|
|
43
|
+
namespace: 'view',
|
|
44
|
+
label: 'Calendar View',
|
|
45
|
+
category: 'view',
|
|
33
46
|
inputs: [
|
|
34
47
|
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
35
48
|
{ name: 'calendar', type: 'object', label: 'Calendar Config', description: 'startDateField, endDateField, titleField, colorField' },
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ObjectCalendarRenderer } from './index';
|
|
5
|
+
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('@object-ui/react', () => ({
|
|
8
|
+
useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// Mock the implementation
|
|
12
|
+
vi.mock('./ObjectCalendar', () => ({
|
|
13
|
+
ObjectCalendar: ({ dataSource }: any) => (
|
|
14
|
+
<div data-testid="calendar-mock">
|
|
15
|
+
{dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('Plugin Calendar Registration', () => {
|
|
21
|
+
it('renderer passes dataSource from context', () => {
|
|
22
|
+
render(<ObjectCalendarRenderer schema={{ type: 'object-calendar' }} />);
|
|
23
|
+
expect(screen.getByTestId('calendar-mock')).toHaveTextContent('DataSource: mock-datasource');
|
|
24
|
+
});
|
|
25
|
+
});
|