@object-ui/plugin-calendar 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.
- package/CHANGELOG.md +37 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -17
- package/dist/index.umd.cjs +2 -2
- package/dist/packages/plugin-calendar/src/CalendarView.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/ObjectCalendar.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/calendar-view-renderer.d.ts.map +1 -0
- package/dist/packages/plugin-calendar/src/index.d.ts.map +1 -0
- package/package.json +37 -14
- package/.turbo/turbo-build.log +0 -22
- package/dist/src/CalendarView.d.ts.map +0 -1
- package/dist/src/ObjectCalendar.d.ts.map +0 -1
- package/dist/src/calendar-view-renderer.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/src/CalendarView.test.tsx +0 -118
- package/src/CalendarView.tsx +0 -821
- package/src/ObjectCalendar.msw.test.tsx +0 -100
- package/src/ObjectCalendar.stories.tsx +0 -82
- package/src/ObjectCalendar.tsx +0 -420
- package/src/__tests__/accessibility.test.tsx +0 -290
- package/src/__tests__/calendar-bugfixes.test.tsx +0 -230
- package/src/__tests__/calendar-optimizations.test.tsx +0 -178
- package/src/__tests__/performance-benchmark.test.tsx +0 -227
- package/src/__tests__/view-states.test.tsx +0 -377
- package/src/calendar-view-renderer.tsx +0 -181
- package/src/index.tsx +0 -50
- package/src/registration.test.tsx +0 -41
- package/test/setup.ts +0 -32
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-calendar/src}/CalendarView.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/ObjectCalendar.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/calendar-view-renderer.d.ts +0 -0
- /package/dist/{src → packages/plugin-calendar/src}/index.d.ts +0 -0
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
|
|
3
|
-
import type { BaseSchema } from '@object-ui/types';
|
|
4
|
-
import { createStorybookDataSource } from '@storybook-config/datasource';
|
|
5
|
-
|
|
6
|
-
const meta = {
|
|
7
|
-
title: 'Plugins/ObjectCalendar',
|
|
8
|
-
component: SchemaRenderer,
|
|
9
|
-
parameters: {
|
|
10
|
-
layout: 'padded',
|
|
11
|
-
},
|
|
12
|
-
tags: ['autodocs'],
|
|
13
|
-
argTypes: {
|
|
14
|
-
schema: { table: { disable: true } },
|
|
15
|
-
},
|
|
16
|
-
} satisfies Meta<any>;
|
|
17
|
-
|
|
18
|
-
export default meta;
|
|
19
|
-
type Story = StoryObj<typeof meta>;
|
|
20
|
-
|
|
21
|
-
const dataSource = createStorybookDataSource();
|
|
22
|
-
|
|
23
|
-
const renderStory = (args: any) => (
|
|
24
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
25
|
-
<SchemaRenderer schema={args as unknown as BaseSchema} />
|
|
26
|
-
</SchemaRendererProvider>
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
export const Default: Story = {
|
|
30
|
-
render: renderStory,
|
|
31
|
-
args: {
|
|
32
|
-
type: 'object-grid',
|
|
33
|
-
objectName: 'Event',
|
|
34
|
-
viewType: 'calendar',
|
|
35
|
-
calendar: {
|
|
36
|
-
startDateField: 'startDate',
|
|
37
|
-
endDateField: 'endDate',
|
|
38
|
-
titleField: 'title',
|
|
39
|
-
},
|
|
40
|
-
columns: [
|
|
41
|
-
{ field: 'title', header: 'Title' },
|
|
42
|
-
{ field: 'startDate', header: 'Start Date' },
|
|
43
|
-
{ field: 'endDate', header: 'End Date' },
|
|
44
|
-
{ field: 'category', header: 'Category' },
|
|
45
|
-
],
|
|
46
|
-
data: [
|
|
47
|
-
{ id: 1, title: 'Team Standup', startDate: '2024-03-04T09:00:00', endDate: '2024-03-04T09:30:00', category: 'Meeting' },
|
|
48
|
-
{ id: 2, title: 'Sprint Planning', startDate: '2024-03-05T10:00:00', endDate: '2024-03-05T12:00:00', category: 'Meeting' },
|
|
49
|
-
{ id: 3, title: 'Design Review', startDate: '2024-03-06T14:00:00', endDate: '2024-03-06T15:00:00', category: 'Review' },
|
|
50
|
-
{ id: 4, title: 'Product Demo', startDate: '2024-03-07T16:00:00', endDate: '2024-03-07T17:00:00', category: 'Demo' },
|
|
51
|
-
{ id: 5, title: 'Retrospective', startDate: '2024-03-08T11:00:00', endDate: '2024-03-08T12:00:00', category: 'Meeting' },
|
|
52
|
-
],
|
|
53
|
-
className: 'w-full',
|
|
54
|
-
} as any,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const MonthlyEvents: Story = {
|
|
58
|
-
render: renderStory,
|
|
59
|
-
args: {
|
|
60
|
-
type: 'object-grid',
|
|
61
|
-
objectName: 'Appointment',
|
|
62
|
-
viewType: 'calendar',
|
|
63
|
-
calendar: {
|
|
64
|
-
startDateField: 'date',
|
|
65
|
-
titleField: 'name',
|
|
66
|
-
},
|
|
67
|
-
columns: [
|
|
68
|
-
{ field: 'name', header: 'Name' },
|
|
69
|
-
{ field: 'date', header: 'Date' },
|
|
70
|
-
{ field: 'type', header: 'Type' },
|
|
71
|
-
],
|
|
72
|
-
data: [
|
|
73
|
-
{ id: 1, name: 'Board Meeting', date: '2024-03-01T10:00:00', type: 'Corporate' },
|
|
74
|
-
{ id: 2, name: 'Client Call', date: '2024-03-05T14:00:00', type: 'Sales' },
|
|
75
|
-
{ id: 3, name: 'Team Lunch', date: '2024-03-12T12:00:00', type: 'Social' },
|
|
76
|
-
{ id: 4, name: 'Quarterly Review', date: '2024-03-15T09:00:00', type: 'Corporate' },
|
|
77
|
-
{ id: 5, name: 'Workshop', date: '2024-03-20T13:00:00', type: 'Training' },
|
|
78
|
-
{ id: 6, name: 'Release Day', date: '2024-03-25T08:00:00', type: 'Engineering' },
|
|
79
|
-
],
|
|
80
|
-
className: 'w-full',
|
|
81
|
-
} as any,
|
|
82
|
-
};
|
package/src/ObjectCalendar.tsx
DELETED
|
@@ -1,420 +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
|
-
* 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, useRef } from 'react';
|
|
26
|
-
import type { ObjectGridSchema, DataSource, ViewData, CalendarConfig } from '@object-ui/types';
|
|
27
|
-
import { CalendarView, type CalendarEvent } from './CalendarView';
|
|
28
|
-
import { usePullToRefresh } from '@object-ui/mobile';
|
|
29
|
-
import { useNavigationOverlay } from '@object-ui/react';
|
|
30
|
-
import { NavigationOverlay } from '@object-ui/components';
|
|
31
|
-
import { extractRecords, buildExpandFields } from '@object-ui/core';
|
|
32
|
-
|
|
33
|
-
export interface CalendarSchema {
|
|
34
|
-
type: 'calendar';
|
|
35
|
-
objectName?: string;
|
|
36
|
-
dateField?: string;
|
|
37
|
-
endField?: string;
|
|
38
|
-
titleField?: string;
|
|
39
|
-
colorField?: string;
|
|
40
|
-
filter?: any;
|
|
41
|
-
sort?: any;
|
|
42
|
-
/** Initial view mode */
|
|
43
|
-
defaultView?: 'month' | 'week' | 'day';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ObjectCalendarProps {
|
|
47
|
-
schema: ObjectGridSchema | CalendarSchema;
|
|
48
|
-
dataSource?: DataSource;
|
|
49
|
-
className?: string;
|
|
50
|
-
/** Pre-fetched records passed by a parent (e.g. ObjectView). When provided, skips internal data fetching. */
|
|
51
|
-
data?: any[];
|
|
52
|
-
/** Loading state propagated from a parent. Respected only when `data` is also provided. */
|
|
53
|
-
loading?: boolean;
|
|
54
|
-
onEventClick?: (record: any) => void;
|
|
55
|
-
onRowClick?: (record: any) => void;
|
|
56
|
-
onDateClick?: (date: Date) => void;
|
|
57
|
-
onEdit?: (record: any) => void;
|
|
58
|
-
onDelete?: (record: any) => void;
|
|
59
|
-
onNavigate?: (date: Date) => void;
|
|
60
|
-
onViewChange?: (view: 'month' | 'week' | 'day') => void;
|
|
61
|
-
onEventDrop?: (record: any, newStart: Date, newEnd?: Date) => void;
|
|
62
|
-
locale?: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Helper to get data configuration from schema
|
|
67
|
-
*/
|
|
68
|
-
function getDataConfig(schema: ObjectGridSchema | CalendarSchema): ViewData | null {
|
|
69
|
-
if ('data' in schema && schema.data) {
|
|
70
|
-
return schema.data;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if ('staticData' in schema && schema.staticData) {
|
|
74
|
-
return {
|
|
75
|
-
provider: 'value',
|
|
76
|
-
items: schema.staticData,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (schema.objectName) {
|
|
81
|
-
return {
|
|
82
|
-
provider: 'object',
|
|
83
|
-
object: schema.objectName,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Helper to convert sort config to QueryParams format
|
|
92
|
-
*/
|
|
93
|
-
function convertSortToQueryParams(sort: string | any[] | undefined): Record<string, 'asc' | 'desc'> | undefined {
|
|
94
|
-
if (!sort) return undefined;
|
|
95
|
-
|
|
96
|
-
// If it's a string like "name desc"
|
|
97
|
-
if (typeof sort === 'string') {
|
|
98
|
-
const parts = sort.split(' ');
|
|
99
|
-
const field = parts[0];
|
|
100
|
-
const order = (parts[1]?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc';
|
|
101
|
-
return { [field]: order };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// If it's an array of SortConfig objects
|
|
105
|
-
if (Array.isArray(sort)) {
|
|
106
|
-
return sort.reduce((acc, item) => {
|
|
107
|
-
if (item.field && item.order) {
|
|
108
|
-
acc[item.field] = item.order;
|
|
109
|
-
}
|
|
110
|
-
return acc;
|
|
111
|
-
}, {} as Record<string, 'asc' | 'desc'>);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Helper to get calendar configuration from schema
|
|
119
|
-
*/
|
|
120
|
-
function getCalendarConfig(schema: ObjectGridSchema | CalendarSchema): CalendarConfig | null {
|
|
121
|
-
// Check if schema has calendar configuration
|
|
122
|
-
if ('filter' in schema && schema.filter && typeof schema.filter === 'object' && 'calendar' in schema.filter) {
|
|
123
|
-
return (schema.filter as any).calendar as CalendarConfig;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// For backward compatibility, check if schema has calendar config at root
|
|
127
|
-
if ((schema as any).calendar) {
|
|
128
|
-
return (schema as any).calendar as CalendarConfig;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Check for flat properties (used by ObjectView)
|
|
132
|
-
if ((schema as any).startDateField || (schema as any).dateField) {
|
|
133
|
-
return {
|
|
134
|
-
startDateField: (schema as any).startDateField || (schema as any).dateField,
|
|
135
|
-
endDateField: (schema as any).endDateField || (schema as any).endField,
|
|
136
|
-
titleField: (schema as any).titleField || 'name',
|
|
137
|
-
colorField: (schema as any).colorField,
|
|
138
|
-
allDayField: (schema as any).allDayField
|
|
139
|
-
} as CalendarConfig;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
|
|
146
|
-
schema,
|
|
147
|
-
dataSource,
|
|
148
|
-
className,
|
|
149
|
-
data: externalData,
|
|
150
|
-
loading: externalLoading,
|
|
151
|
-
onEventClick,
|
|
152
|
-
onRowClick,
|
|
153
|
-
onDateClick,
|
|
154
|
-
onNavigate,
|
|
155
|
-
onViewChange,
|
|
156
|
-
onEventDrop,
|
|
157
|
-
locale,
|
|
158
|
-
}) => {
|
|
159
|
-
// When the parent (e.g. ObjectView) pre-fetches data and passes it via the `data` prop,
|
|
160
|
-
// we must not trigger a second fetch. Detect external data by checking for an array.
|
|
161
|
-
const hasExternalData = Array.isArray(externalData);
|
|
162
|
-
|
|
163
|
-
const [data, setData] = useState<any[]>(hasExternalData ? externalData! : []);
|
|
164
|
-
const [loading, setLoading] = useState(hasExternalData ? (externalLoading ?? false) : true);
|
|
165
|
-
const [error, setError] = useState<Error | null>(null);
|
|
166
|
-
const [objectSchema, setObjectSchema] = useState<any>(null);
|
|
167
|
-
const [currentDate, setCurrentDate] = useState(new Date());
|
|
168
|
-
const [view, setView] = useState<'month' | 'week' | 'day'>('month');
|
|
169
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
170
|
-
|
|
171
|
-
const handlePullRefresh = useCallback(async () => {
|
|
172
|
-
setRefreshKey(k => k + 1);
|
|
173
|
-
}, []);
|
|
174
|
-
|
|
175
|
-
const { ref: pullRef, isRefreshing, pullDistance } = usePullToRefresh<HTMLDivElement>({
|
|
176
|
-
onRefresh: handlePullRefresh,
|
|
177
|
-
enabled: !!dataSource && !!schema.objectName,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
const dataConfig = useMemo(() => getDataConfig(schema), [
|
|
181
|
-
(schema as any).data,
|
|
182
|
-
(schema as any).staticData,
|
|
183
|
-
schema.objectName,
|
|
184
|
-
]);
|
|
185
|
-
const calendarConfig = useMemo(() => getCalendarConfig(schema), [
|
|
186
|
-
schema.filter,
|
|
187
|
-
(schema as any).calendar,
|
|
188
|
-
(schema as any).dateField,
|
|
189
|
-
(schema as any).endField,
|
|
190
|
-
(schema as any).titleField,
|
|
191
|
-
(schema as any).colorField
|
|
192
|
-
]);
|
|
193
|
-
const hasInlineData = dataConfig?.provider === 'value';
|
|
194
|
-
|
|
195
|
-
// Use ref for objectSchema to avoid double-fetch on mount
|
|
196
|
-
const objectSchemaRef = useRef<any>(null);
|
|
197
|
-
objectSchemaRef.current = objectSchema;
|
|
198
|
-
|
|
199
|
-
// Sync external data/loading changes from parent (e.g. ObjectView re-fetches after filter change)
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
if (hasExternalData) {
|
|
202
|
-
setData(externalData!);
|
|
203
|
-
}
|
|
204
|
-
}, [externalData, hasExternalData]);
|
|
205
|
-
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (hasExternalData && externalLoading !== undefined) {
|
|
208
|
-
setLoading(externalLoading);
|
|
209
|
-
}
|
|
210
|
-
}, [externalLoading, hasExternalData]);
|
|
211
|
-
|
|
212
|
-
// Fetch data based on provider
|
|
213
|
-
useEffect(() => {
|
|
214
|
-
// Skip internal fetch when data is managed by a parent component
|
|
215
|
-
if (hasExternalData) return;
|
|
216
|
-
|
|
217
|
-
let isMounted = true;
|
|
218
|
-
const fetchData = async () => {
|
|
219
|
-
try {
|
|
220
|
-
if (!isMounted) return;
|
|
221
|
-
setLoading(true);
|
|
222
|
-
|
|
223
|
-
if (hasInlineData && dataConfig?.provider === 'value') {
|
|
224
|
-
if (isMounted) {
|
|
225
|
-
setData(dataConfig.items as any[]);
|
|
226
|
-
setLoading(false);
|
|
227
|
-
}
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!dataSource || typeof dataSource.find !== 'function') {
|
|
232
|
-
throw new Error('DataSource required for object/api providers');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (dataConfig?.provider === 'object') {
|
|
236
|
-
const objectName = dataConfig.object;
|
|
237
|
-
// Auto-inject $expand for lookup/master_detail fields
|
|
238
|
-
const expand = buildExpandFields(objectSchemaRef.current?.fields);
|
|
239
|
-
const result = await dataSource.find(objectName, {
|
|
240
|
-
$filter: schema.filter,
|
|
241
|
-
$orderby: convertSortToQueryParams(schema.sort),
|
|
242
|
-
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
let items: any[] = extractRecords(result);
|
|
246
|
-
|
|
247
|
-
if (isMounted) {
|
|
248
|
-
setData(items);
|
|
249
|
-
}
|
|
250
|
-
} else if (dataConfig?.provider === 'api') {
|
|
251
|
-
console.warn('API provider not yet implemented for ObjectCalendar');
|
|
252
|
-
if (isMounted) setData([]);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (isMounted) setLoading(false);
|
|
256
|
-
} catch (err) {
|
|
257
|
-
console.error('[ObjectCalendar] Error fetching data:', err);
|
|
258
|
-
if (isMounted) {
|
|
259
|
-
setError(err as Error);
|
|
260
|
-
setLoading(false);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
fetchData();
|
|
266
|
-
return () => { isMounted = false; };
|
|
267
|
-
}, [hasExternalData, dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, refreshKey]);
|
|
268
|
-
|
|
269
|
-
// Fetch object schema for field metadata
|
|
270
|
-
useEffect(() => {
|
|
271
|
-
const fetchObjectSchema = async () => {
|
|
272
|
-
try {
|
|
273
|
-
if (!dataSource) return;
|
|
274
|
-
|
|
275
|
-
const objectName = dataConfig?.provider === 'object'
|
|
276
|
-
? dataConfig.object
|
|
277
|
-
: schema.objectName;
|
|
278
|
-
|
|
279
|
-
if (!objectName) return;
|
|
280
|
-
|
|
281
|
-
const schemaData = await dataSource.getObjectSchema(objectName);
|
|
282
|
-
setObjectSchema(schemaData);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error('Failed to fetch object schema:', err);
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
if (!hasInlineData && dataSource) {
|
|
289
|
-
fetchObjectSchema();
|
|
290
|
-
}
|
|
291
|
-
}, [schema.objectName, dataSource, hasInlineData, dataConfig]);
|
|
292
|
-
|
|
293
|
-
// Transform data to calendar events
|
|
294
|
-
const events = useMemo(() => {
|
|
295
|
-
if (!calendarConfig || !data.length) {
|
|
296
|
-
return [];
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const { startDateField, endDateField, titleField, colorField } = calendarConfig;
|
|
300
|
-
|
|
301
|
-
return data.map((record, index) => {
|
|
302
|
-
const startDate = record[startDateField];
|
|
303
|
-
const endDate = endDateField ? record[endDateField] : null;
|
|
304
|
-
const title = record[titleField] || 'Untitled';
|
|
305
|
-
const color = colorField ? record[colorField] : undefined;
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
id: record.id || record._id || `event-${index}`,
|
|
309
|
-
title,
|
|
310
|
-
start: startDate ? new Date(startDate) : new Date(),
|
|
311
|
-
end: endDate ? new Date(endDate) : undefined,
|
|
312
|
-
color,
|
|
313
|
-
allDay: !endDate, // If no end date, treat as all-day event
|
|
314
|
-
data: record,
|
|
315
|
-
};
|
|
316
|
-
}).filter(event => !isNaN(event.start.getTime())); // Filter out invalid dates
|
|
317
|
-
}, [data, calendarConfig]);
|
|
318
|
-
|
|
319
|
-
// Get days in current month view - REMOVED (Handled by CalendarView)
|
|
320
|
-
|
|
321
|
-
const handleCreate = useCallback(() => {
|
|
322
|
-
// Standard "Create" action trigger
|
|
323
|
-
const today = new Date();
|
|
324
|
-
onDateClick?.(today);
|
|
325
|
-
}, [onDateClick]);
|
|
326
|
-
|
|
327
|
-
// --- NavigationConfig support ---
|
|
328
|
-
// Must be called before any early returns to satisfy React hooks rules
|
|
329
|
-
const navigation = useNavigationOverlay({
|
|
330
|
-
navigation: (schema as any).navigation,
|
|
331
|
-
objectName: schema.objectName,
|
|
332
|
-
onRowClick,
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (loading) {
|
|
336
|
-
return (
|
|
337
|
-
<div className={className}>
|
|
338
|
-
<div className="flex items-center justify-center h-96">
|
|
339
|
-
<div className="text-muted-foreground">Loading calendar...</div>
|
|
340
|
-
</div>
|
|
341
|
-
</div>
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (error) {
|
|
346
|
-
return (
|
|
347
|
-
<div className={className}>
|
|
348
|
-
<div className="flex items-center justify-center h-96">
|
|
349
|
-
<div className="text-destructive">Error: {error.message}</div>
|
|
350
|
-
</div>
|
|
351
|
-
</div>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (!calendarConfig) {
|
|
356
|
-
return (
|
|
357
|
-
<div className={className}>
|
|
358
|
-
<div className="flex items-center justify-center h-96">
|
|
359
|
-
<div className="text-muted-foreground">
|
|
360
|
-
Calendar configuration required. Please specify startDateField and titleField.
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
</div>
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<div ref={pullRef} className={className}>
|
|
369
|
-
{pullDistance > 0 && (
|
|
370
|
-
<div
|
|
371
|
-
className="flex items-center justify-center text-xs text-muted-foreground"
|
|
372
|
-
style={{ height: pullDistance }}
|
|
373
|
-
>
|
|
374
|
-
{isRefreshing ? 'Refreshing…' : 'Pull to refresh'}
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
<div className="border rounded-lg bg-background h-[calc(100vh-120px)] sm:h-[calc(100vh-160px)] md:h-[calc(100vh-200px)] min-h-[400px] sm:min-h-[600px]">
|
|
378
|
-
<CalendarView
|
|
379
|
-
events={events}
|
|
380
|
-
currentDate={currentDate}
|
|
381
|
-
view={(schema as any).defaultView || 'month'}
|
|
382
|
-
locale={locale}
|
|
383
|
-
onEventClick={(event) => {
|
|
384
|
-
navigation.handleClick(event.data);
|
|
385
|
-
onEventClick?.(event.data);
|
|
386
|
-
}}
|
|
387
|
-
onDateClick={onDateClick}
|
|
388
|
-
onNavigate={(date) => {
|
|
389
|
-
setCurrentDate(date);
|
|
390
|
-
onNavigate?.(date);
|
|
391
|
-
}}
|
|
392
|
-
onViewChange={(v) => {
|
|
393
|
-
setView(v);
|
|
394
|
-
onViewChange?.(v);
|
|
395
|
-
}}
|
|
396
|
-
onAddClick={undefined}
|
|
397
|
-
onEventDrop={onEventDrop ? (event, newStart, newEnd) => {
|
|
398
|
-
onEventDrop(event.data, newStart, newEnd);
|
|
399
|
-
} : undefined}
|
|
400
|
-
/>
|
|
401
|
-
</div>
|
|
402
|
-
{navigation.isOverlay && (
|
|
403
|
-
<NavigationOverlay {...navigation} title="Event Details">
|
|
404
|
-
{(record) => (
|
|
405
|
-
<div className="space-y-3">
|
|
406
|
-
{Object.entries(record).map(([key, value]) => (
|
|
407
|
-
<div key={key} className="flex flex-col">
|
|
408
|
-
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
409
|
-
{key.replace(/_/g, ' ')}
|
|
410
|
-
</span>
|
|
411
|
-
<span className="text-sm">{String(value ?? '—')}</span>
|
|
412
|
-
</div>
|
|
413
|
-
))}
|
|
414
|
-
</div>
|
|
415
|
-
)}
|
|
416
|
-
</NavigationOverlay>
|
|
417
|
-
)}
|
|
418
|
-
</div>
|
|
419
|
-
);
|
|
420
|
-
};
|