Skip to content

Commit 22ce77c

Browse files
Copilothotlong
andcommitted
fix: support nested view config for calendar, kanban, gantt, etc. in console ObjectView
The console ObjectView's renderListView callback now correctly reads nested config properties (e.g., viewDef.calendar.startDateField) in addition to flat properties (e.g., viewDef.startDateField). This fixes the CRM calendar view which uses the @objectstack/spec nested format. Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent e8440c3 commit 22ce77c

2 files changed

Lines changed: 167 additions & 34 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import { ObjectView } from '../components/ObjectView';
4+
import { SchemaRendererProvider } from '@object-ui/react';
5+
import '@object-ui/plugin-grid';
6+
import '@object-ui/plugin-kanban';
7+
import '@object-ui/plugin-calendar';
8+
import '@object-ui/plugin-list';
9+
10+
vi.mock('@object-ui/components', async () => {
11+
const actual = await vi.importActual('@object-ui/components');
12+
return { ...actual };
13+
});
14+
15+
vi.mock('@object-ui/react', async (importOriginal) => {
16+
const actual = await importOriginal<any>();
17+
return {
18+
...actual,
19+
useDataScope: () => undefined,
20+
};
21+
});
22+
23+
const mockSetSearchParams = vi.fn();
24+
let mockSearchParams = new URLSearchParams();
25+
26+
vi.mock('react-router-dom', () => ({
27+
useParams: () => ({ objectName: 'event', viewId: 'calendar' }),
28+
useSearchParams: () => [mockSearchParams, mockSetSearchParams],
29+
useNavigate: () => vi.fn(),
30+
}));
31+
32+
// Use dates in current month so events show on the visible calendar
33+
const now = new Date();
34+
const year = now.getFullYear();
35+
const month = String(now.getMonth() + 1).padStart(2, '0');
36+
37+
const mockDataSource = {
38+
find: vi.fn().mockResolvedValue([
39+
{ _id: 'e1', subject: 'Weekly Standup', start: `${year}-${month}-05T09:00:00`, end: `${year}-${month}-05T10:00:00`, type: 'Meeting' },
40+
{ _id: 'e2', subject: 'Client Call', start: `${year}-${month}-06T14:00:00`, end: `${year}-${month}-06T15:00:00`, type: 'Call' }
41+
]),
42+
findOne: vi.fn().mockResolvedValue({}),
43+
create: vi.fn().mockResolvedValue({}),
44+
update: vi.fn().mockResolvedValue({}),
45+
delete: vi.fn().mockResolvedValue(true),
46+
getObjectSchema: vi.fn().mockResolvedValue({
47+
name: 'event',
48+
label: 'Event',
49+
fields: {
50+
subject: { label: 'Subject', type: 'text' },
51+
start: { label: 'Start', type: 'datetime' },
52+
end: { label: 'End', type: 'datetime' },
53+
type: { label: 'Type', type: 'select' },
54+
}
55+
})
56+
};
57+
58+
// CRM-style nested calendar config (same structure as objectstack.config.ts)
59+
const mockObjects = [
60+
{
61+
name: 'event',
62+
label: 'Event',
63+
fields: {
64+
subject: { label: 'Subject', type: 'text' },
65+
start: { label: 'Start', type: 'datetime' },
66+
end: { label: 'End', type: 'datetime' },
67+
type: { label: 'Type', type: 'select' },
68+
},
69+
listViews: {
70+
all_events: {
71+
name: 'all_events',
72+
label: 'All Events',
73+
type: 'grid',
74+
columns: ['subject', 'start', 'end', 'type'],
75+
},
76+
calendar: {
77+
name: 'calendar',
78+
label: 'Calendar',
79+
type: 'calendar',
80+
columns: ['subject', 'start', 'end', 'type'],
81+
calendar: {
82+
startDateField: 'start',
83+
endDateField: 'end',
84+
titleField: 'subject',
85+
},
86+
},
87+
},
88+
},
89+
];
90+
91+
describe('CRM Calendar View with Nested Config', () => {
92+
beforeEach(() => {
93+
vi.clearAllMocks();
94+
mockSearchParams = new URLSearchParams();
95+
});
96+
97+
it('renders calendar view with events using nested calendar config', async () => {
98+
render(
99+
<SchemaRendererProvider dataSource={mockDataSource}>
100+
<ObjectView dataSource={mockDataSource} objects={mockObjects} onEdit={vi.fn()} />
101+
</SchemaRendererProvider>
102+
);
103+
104+
// Calendar region should render
105+
await waitFor(() => {
106+
const calendarRegion = document.querySelector('[aria-label="Calendar"]');
107+
expect(calendarRegion).toBeInTheDocument();
108+
}, { timeout: 5000 });
109+
110+
// Event titles should display correctly using titleField: 'subject'
111+
await waitFor(() => {
112+
expect(screen.getByText('Weekly Standup')).toBeInTheDocument();
113+
}, { timeout: 5000 });
114+
});
115+
116+
it('does not show "Untitled" events when nested calendar config provides titleField', async () => {
117+
render(
118+
<SchemaRendererProvider dataSource={mockDataSource}>
119+
<ObjectView dataSource={mockDataSource} objects={mockObjects} onEdit={vi.fn()} />
120+
</SchemaRendererProvider>
121+
);
122+
123+
await waitFor(() => {
124+
expect(mockDataSource.find).toHaveBeenCalled();
125+
}, { timeout: 3000 });
126+
127+
// Wait for rendering
128+
await new Promise(r => setTimeout(r, 500));
129+
130+
// Should NOT show "Untitled" - that would mean titleField defaulted to 'name'
131+
expect(screen.queryByText('Untitled')).not.toBeInTheDocument();
132+
});
133+
});

apps/console/src/components/ObjectView.tsx

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -165,52 +165,52 @@ export function ObjectView({ dataSource, objects, onEdit, onRowClick }: any) {
165165
...listSchema,
166166
options: {
167167
kanban: {
168-
groupBy: viewDef.groupBy || viewDef.groupField || 'status',
169-
groupField: viewDef.groupBy || viewDef.groupField || 'status',
170-
titleField: viewDef.titleField || objectDef.titleField || 'name',
171-
cardFields: viewDef.columns || viewDef.cardFields,
168+
groupBy: viewDef.kanban?.groupByField || viewDef.kanban?.groupField || viewDef.groupBy || viewDef.groupField || 'status',
169+
groupField: viewDef.kanban?.groupByField || viewDef.kanban?.groupField || viewDef.groupBy || viewDef.groupField || 'status',
170+
titleField: viewDef.kanban?.titleField || viewDef.titleField || objectDef.titleField || 'name',
171+
cardFields: viewDef.kanban?.columns || viewDef.columns || viewDef.cardFields,
172172
},
173173
calendar: {
174-
startDateField: viewDef.startDateField || viewDef.dateField || 'due_date',
175-
endDateField: viewDef.endDateField || viewDef.endField,
176-
titleField: viewDef.titleField || viewDef.subjectField || 'name',
177-
colorField: viewDef.colorField,
178-
allDayField: viewDef.allDayField,
179-
defaultView: viewDef.defaultView,
174+
startDateField: viewDef.calendar?.startDateField || viewDef.startDateField || viewDef.dateField || 'due_date',
175+
endDateField: viewDef.calendar?.endDateField || viewDef.endDateField || viewDef.endField,
176+
titleField: viewDef.calendar?.titleField || viewDef.titleField || viewDef.subjectField || 'name',
177+
colorField: viewDef.calendar?.colorField || viewDef.colorField,
178+
allDayField: viewDef.calendar?.allDayField || viewDef.allDayField,
179+
defaultView: viewDef.calendar?.defaultView || viewDef.defaultView,
180180
},
181181
timeline: {
182-
dateField: viewDef.dateField || viewDef.startDateField || 'due_date',
183-
titleField: viewDef.titleField || objectDef.titleField || 'name',
184-
descriptionField: viewDef.descriptionField,
182+
dateField: viewDef.timeline?.dateField || viewDef.dateField || viewDef.startDateField || 'due_date',
183+
titleField: viewDef.timeline?.titleField || viewDef.titleField || objectDef.titleField || 'name',
184+
descriptionField: viewDef.timeline?.descriptionField || viewDef.descriptionField,
185185
},
186186
map: {
187-
locationField: viewDef.locationField,
188-
titleField: viewDef.titleField || objectDef.titleField || 'name',
189-
latitudeField: viewDef.latitudeField,
190-
longitudeField: viewDef.longitudeField,
191-
zoom: viewDef.zoom,
192-
center: viewDef.center,
187+
locationField: viewDef.map?.locationField || viewDef.locationField,
188+
titleField: viewDef.map?.titleField || viewDef.titleField || objectDef.titleField || 'name',
189+
latitudeField: viewDef.map?.latitudeField || viewDef.latitudeField,
190+
longitudeField: viewDef.map?.longitudeField || viewDef.longitudeField,
191+
zoom: viewDef.map?.zoom || viewDef.zoom,
192+
center: viewDef.map?.center || viewDef.center,
193193
},
194194
gallery: {
195-
imageField: viewDef.imageField || 'image',
196-
titleField: viewDef.titleField || objectDef.titleField || 'name',
197-
subtitleField: viewDef.subtitleField,
195+
imageField: viewDef.gallery?.imageField || viewDef.imageField || 'image',
196+
titleField: viewDef.gallery?.titleField || viewDef.titleField || objectDef.titleField || 'name',
197+
subtitleField: viewDef.gallery?.subtitleField || viewDef.subtitleField,
198198
},
199199
gantt: {
200-
startDateField: viewDef.startDateField || 'start_date',
201-
endDateField: viewDef.endDateField || 'end_date',
202-
titleField: viewDef.titleField || 'name',
203-
progressField: viewDef.progressField,
204-
dependenciesField: viewDef.dependenciesField,
205-
colorField: viewDef.colorField,
200+
startDateField: viewDef.gantt?.startDateField || viewDef.startDateField || 'start_date',
201+
endDateField: viewDef.gantt?.endDateField || viewDef.endDateField || 'end_date',
202+
titleField: viewDef.gantt?.titleField || viewDef.titleField || 'name',
203+
progressField: viewDef.gantt?.progressField || viewDef.progressField,
204+
dependenciesField: viewDef.gantt?.dependenciesField || viewDef.dependenciesField,
205+
colorField: viewDef.gantt?.colorField || viewDef.colorField,
206206
},
207207
chart: {
208-
chartType: viewDef.chartType,
209-
xAxisField: viewDef.xAxisField,
210-
yAxisFields: viewDef.yAxisFields,
211-
aggregation: viewDef.aggregation,
212-
series: viewDef.series,
213-
config: viewDef.config,
208+
chartType: viewDef.chart?.chartType || viewDef.chartType,
209+
xAxisField: viewDef.chart?.xAxisField || viewDef.xAxisField,
210+
yAxisFields: viewDef.chart?.yAxisFields || viewDef.yAxisFields,
211+
aggregation: viewDef.chart?.aggregation || viewDef.aggregation,
212+
series: viewDef.chart?.series || viewDef.series,
213+
config: viewDef.chart?.config || viewDef.config,
214214
},
215215
},
216216
};

0 commit comments

Comments
 (0)