Skip to content

Commit cb78eb1

Browse files
Copilothuangyiirene
andcommitted
Add Airtable-style calendar view component
- Created CalendarView UI component with month/week/day views - Created calendar-view renderer for ComponentRegistry - Added support for data field mapping (title, dates, colors) - Integrated with existing component system Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
1 parent 230e147 commit cb78eb1

4 files changed

Lines changed: 713 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { ComponentRegistry } from '@object-ui/core';
2+
import { CalendarView, type CalendarEvent } from '@/ui';
3+
import React from 'react';
4+
5+
// Calendar View Renderer - Airtable-style calendar for displaying records as events
6+
ComponentRegistry.register('calendar-view',
7+
({ schema, className, onAction, ...props }) => {
8+
// Transform schema data to CalendarEvent format
9+
const events: CalendarEvent[] = React.useMemo(() => {
10+
if (!schema.data || !Array.isArray(schema.data)) return [];
11+
12+
return schema.data.map((record: any, index: number) => {
13+
// Get field values based on field mappings
14+
const titleField = schema.titleField || 'title';
15+
const startField = schema.startDateField || 'start';
16+
const endField = schema.endDateField || 'end';
17+
const colorField = schema.colorField || 'color';
18+
const allDayField = schema.allDayField || 'allDay';
19+
20+
const title = record[titleField] || 'Untitled';
21+
const start = record[startField] ? new Date(record[startField]) : new Date();
22+
const end = record[endField] ? new Date(record[endField]) : undefined;
23+
const allDay = record[allDayField] !== undefined ? record[allDayField] : false;
24+
25+
// Handle color mapping
26+
let color = record[colorField];
27+
if (color && schema.colorMapping && schema.colorMapping[color]) {
28+
color = schema.colorMapping[color];
29+
}
30+
31+
return {
32+
id: record.id || record._id || index,
33+
title,
34+
start,
35+
end,
36+
allDay,
37+
color,
38+
data: record,
39+
};
40+
});
41+
}, [schema.data, schema.titleField, schema.startDateField, schema.endDateField, schema.colorField, schema.allDayField, schema.colorMapping]);
42+
43+
const handleEventClick = React.useCallback((event: CalendarEvent) => {
44+
if (onAction) {
45+
onAction({
46+
type: 'event_click',
47+
payload: { event: event.data, eventId: event.id }
48+
});
49+
}
50+
if (schema.onEventClick) {
51+
schema.onEventClick(event.data);
52+
}
53+
}, [onAction, schema]);
54+
55+
const handleDateClick = React.useCallback((date: Date) => {
56+
if (onAction) {
57+
onAction({
58+
type: 'date_click',
59+
payload: { date }
60+
});
61+
}
62+
if (schema.onDateClick) {
63+
schema.onDateClick(date);
64+
}
65+
}, [onAction, schema]);
66+
67+
const handleViewChange = React.useCallback((view: "month" | "week" | "day") => {
68+
if (onAction) {
69+
onAction({
70+
type: 'view_change',
71+
payload: { view }
72+
});
73+
}
74+
if (schema.onViewChange) {
75+
schema.onViewChange(view);
76+
}
77+
}, [onAction, schema]);
78+
79+
const handleNavigate = React.useCallback((date: Date) => {
80+
if (onAction) {
81+
onAction({
82+
type: 'navigate',
83+
payload: { date }
84+
});
85+
}
86+
if (schema.onNavigate) {
87+
schema.onNavigate(date);
88+
}
89+
}, [onAction, schema]);
90+
91+
return (
92+
<CalendarView
93+
events={events}
94+
view={schema.view || schema.defaultView || 'month'}
95+
currentDate={schema.currentDate ? new Date(schema.currentDate) : undefined}
96+
onEventClick={handleEventClick}
97+
onDateClick={schema.allowCreate || schema.onDateClick ? handleDateClick : undefined}
98+
onViewChange={handleViewChange}
99+
onNavigate={handleNavigate}
100+
className={className}
101+
{...props}
102+
/>
103+
);
104+
},
105+
{
106+
label: 'Calendar View',
107+
inputs: [
108+
{
109+
name: 'data',
110+
type: 'array',
111+
label: 'Data',
112+
description: 'Array of record objects to display as events'
113+
},
114+
{
115+
name: 'titleField',
116+
type: 'string',
117+
label: 'Title Field',
118+
defaultValue: 'title',
119+
description: 'Field name to use for event title'
120+
},
121+
{
122+
name: 'startDateField',
123+
type: 'string',
124+
label: 'Start Date Field',
125+
defaultValue: 'start',
126+
description: 'Field name for event start date'
127+
},
128+
{
129+
name: 'endDateField',
130+
type: 'string',
131+
label: 'End Date Field',
132+
defaultValue: 'end',
133+
description: 'Field name for event end date (optional)'
134+
},
135+
{
136+
name: 'allDayField',
137+
type: 'string',
138+
label: 'All Day Field',
139+
defaultValue: 'allDay',
140+
description: 'Field name for all-day flag'
141+
},
142+
{
143+
name: 'colorField',
144+
type: 'string',
145+
label: 'Color Field',
146+
defaultValue: 'color',
147+
description: 'Field name for event color'
148+
},
149+
{
150+
name: 'colorMapping',
151+
type: 'object',
152+
label: 'Color Mapping',
153+
description: 'Map field values to colors (e.g., {meeting: "blue", deadline: "red"})'
154+
},
155+
{
156+
name: 'view',
157+
type: 'enum',
158+
enum: ['month', 'week', 'day'],
159+
defaultValue: 'month',
160+
label: 'Default View'
161+
},
162+
{
163+
name: 'defaultView',
164+
type: 'enum',
165+
enum: ['month', 'week', 'day'],
166+
defaultValue: 'month',
167+
label: 'Default View (alias)'
168+
},
169+
{
170+
name: 'currentDate',
171+
type: 'string',
172+
label: 'Current Date',
173+
description: 'ISO date string for initial calendar date'
174+
},
175+
{
176+
name: 'allowCreate',
177+
type: 'boolean',
178+
label: 'Allow Create',
179+
defaultValue: false,
180+
description: 'Allow creating events by clicking on dates'
181+
},
182+
{ name: 'className', type: 'string', label: 'CSS Class' }
183+
],
184+
defaultProps: {
185+
view: 'month',
186+
titleField: 'title',
187+
startDateField: 'start',
188+
endDateField: 'end',
189+
allDayField: 'allDay',
190+
colorField: 'color',
191+
allowCreate: false,
192+
data: [
193+
{
194+
id: 1,
195+
title: 'Team Meeting',
196+
start: new Date(new Date().setHours(10, 0, 0, 0)).toISOString(),
197+
end: new Date(new Date().setHours(11, 0, 0, 0)).toISOString(),
198+
color: '#3b82f6',
199+
allDay: false
200+
},
201+
{
202+
id: 2,
203+
title: 'Project Deadline',
204+
start: new Date(new Date().setDate(new Date().getDate() + 3)).toISOString(),
205+
color: '#ef4444',
206+
allDay: true
207+
},
208+
{
209+
id: 3,
210+
title: 'Conference',
211+
start: new Date(new Date().setDate(new Date().getDate() + 7)).toISOString(),
212+
end: new Date(new Date().setDate(new Date().getDate() + 9)).toISOString(),
213+
color: '#10b981',
214+
allDay: true
215+
}
216+
],
217+
className: 'h-[600px] border rounded-lg'
218+
}
219+
}
220+
);

packages/components/src/renderers/complex/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ import './carousel';
22
import './scroll-area';
33
import './resizable';
44
import './table';
5+
import './calendar-view';

0 commit comments

Comments
 (0)