Skip to content

Commit b94fe25

Browse files
committed
feat: update plugin-calendar dependency to workspace and enhance ObjectCalendar and ObjectKanban components
- Changed dependency for @object-ui/plugin-calendar in package.json to use workspace protocol. - Enhanced ObjectCalendar to support flat properties for schema. - Improved ObjectKanban to fetch object definitions and generate columns based on metadata. - Added comprehensive integration tests for ObjectCalendar and ObjectKanban components.
1 parent 967c221 commit b94fe25

5 files changed

Lines changed: 253 additions & 174 deletions

File tree

apps/console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@object-ui/example-todo": "workspace:*",
3131
"@object-ui/fields": "workspace:*",
3232
"@object-ui/layout": "workspace:*",
33-
"@object-ui/plugin-calendar": "^0.3.1",
33+
"@object-ui/plugin-calendar": "workspace:*",
3434
"@object-ui/plugin-dashboard": "workspace:*",
3535
"@object-ui/plugin-form": "workspace:*",
3636
"@object-ui/plugin-grid": "workspace:*",
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { ObjectCalendar } from '@object-ui/plugin-calendar';
5+
import { ObjectKanban } from '@object-ui/plugin-kanban';
6+
7+
// Mock KanbanImpl to avoid dnd-kit issues in test environment
8+
vi.mock('@object-ui/plugin-kanban', async (importOriginal) => {
9+
const actual = await importOriginal();
10+
return {
11+
...(actual as any),
12+
// Overwrite the lazy loaded component (if exposed) or mock the whole module logic related to it
13+
// Since ObjectKanban uses KanbanRenderer which lazy loads KanbanImpl,
14+
// we essentially need to mock KanbanRenderer or ensure KanbanImpl is mockable.
15+
// But KanbanImpl is NOT exported by the package main entry point usually.
16+
// So we mock the KanbanRenderer component directly which is simpler.
17+
KanbanRenderer: ({ schema }: any) => {
18+
// Replicate the mapping logic approximately for visual test
19+
const { columns = [], data, groupBy } = schema;
20+
// If we have flat data and a grouping key, distribute items into columns
21+
let displayColumns = columns;
22+
if (data && groupBy && Array.isArray(data)) {
23+
const groups = data.reduce((acc: any, item: any) => {
24+
const key = item[groupBy];
25+
if (!acc[key]) acc[key] = [];
26+
acc[key].push(item);
27+
return acc;
28+
}, {});
29+
displayColumns = columns.map((col: any) => ({
30+
...col,
31+
cards: [
32+
...(col.cards || []),
33+
...(groups[col.id] || [])
34+
]
35+
}));
36+
}
37+
38+
return (
39+
<div data-testid="kanban-impl">
40+
{displayColumns.map((col: any) => (
41+
<div key={col.id} data-testid={`column-${col.id}`}>
42+
<h3>{col.title}</h3>
43+
<div data-testid={`cards-${col.id}`}>
44+
{col.cards.map((card: any) => (
45+
<div key={card.id}>{card.title || card.name}</div>
46+
))}
47+
</div>
48+
</div>
49+
))}
50+
</div>
51+
);
52+
}
53+
};
54+
});
55+
56+
describe('Plugins Integration Test', () => {
57+
58+
// Mock DataSource
59+
const mockSchema = {
60+
name: 'todo_task',
61+
fields: {
62+
status: {
63+
type: 'select',
64+
options: [
65+
{ label: 'Not Started', value: 'new' },
66+
{ label: 'In Progress', value: 'working' },
67+
{ label: 'Done', value: 'done' }
68+
]
69+
}
70+
}
71+
};
72+
73+
const mockData = [
74+
{ id: '1', name: 'Task 1', status: 'new', due_date: '2026-02-15' },
75+
{ id: '2', name: 'Task 2', status: 'working', due_date: '2026-02-16' },
76+
{ id: '3', name: 'Task 3', status: 'done', due_date: '2026-02-17' }
77+
];
78+
79+
const mockDataSource = {
80+
find: vi.fn().mockResolvedValue(mockData),
81+
getObject: vi.fn().mockResolvedValue(mockSchema),
82+
getObjectSchema: vi.fn().mockResolvedValue(mockSchema)
83+
};
84+
85+
beforeEach(() => {
86+
vi.clearAllMocks();
87+
});
88+
89+
describe('ObjectCalendar', () => {
90+
it('renders events using flat schema properties (ObjectView style)', async () => {
91+
render(
92+
<ObjectCalendar
93+
schema={{
94+
type: 'calendar',
95+
objectName: 'todo_task',
96+
dateField: 'due_date',
97+
titleField: 'name'
98+
} as any}
99+
dataSource={mockDataSource as any}
100+
/>
101+
);
102+
103+
// Expect mockData items to be rendered text
104+
// ObjectCalendar renders events. If generic implementation works, we should see 'Task 1'
105+
await waitFor(() => {
106+
expect(screen.getByText('Task 1')).toBeInTheDocument();
107+
});
108+
expect(screen.getByText('Task 2')).toBeInTheDocument();
109+
});
110+
});
111+
112+
describe('ObjectKanban', () => {
113+
it('automatically generates columns from picklist metadata', async () => {
114+
render(
115+
<ObjectKanban
116+
schema={{
117+
type: 'kanban',
118+
objectName: 'todo_task',
119+
groupBy: 'status'
120+
} as any}
121+
dataSource={mockDataSource as any}
122+
/>
123+
);
124+
125+
// Wait for schema fetch and rendering
126+
await waitFor(() => {
127+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('todo_task');
128+
});
129+
130+
// Expect columns to be generated from schema options
131+
await waitFor(() => {
132+
expect(screen.getByText('Not Started')).toBeInTheDocument();
133+
});
134+
expect(screen.getByText('In Progress')).toBeInTheDocument();
135+
expect(screen.getByText('Done')).toBeInTheDocument();
136+
137+
// Expect cards to be distributed
138+
expect(screen.getByText('Task 1')).toBeInTheDocument(); // in 'new' column
139+
});
140+
});
141+
});

packages/plugin-calendar/src/ObjectCalendar.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ function getCalendarConfig(schema: ObjectGridSchema): CalendarConfig | null {
101101
return (schema as any).calendar as CalendarConfig;
102102
}
103103

104+
// Check for flat properties (used by ObjectView)
105+
if ((schema as any).dateField) {
106+
return {
107+
startDateField: (schema as any).dateField,
108+
endDateField: (schema as any).endField,
109+
titleField: (schema as any).titleField || 'name',
110+
colorField: (schema as any).colorField
111+
} as CalendarConfig;
112+
}
113+
104114
return null;
105115
}
106116

@@ -111,6 +121,7 @@ export const ObjectCalendar: React.FC<ObjectCalendarProps> = ({
111121
onEventClick,
112122
onDateClick,
113123
}) => {
124+
console.log('ObjectCalendar Schema:', JSON.stringify(schema));
114125
const [data, setData] = useState<any[]>([]);
115126
const [loading, setLoading] = useState(true);
116127
const [error, setError] = useState<Error | null>(null);

packages/plugin-kanban/src/ObjectKanban.tsx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import React, { useEffect, useState } from 'react';
9+
import React, { useEffect, useState, useMemo } from 'react';
1010
import type { DataSource } from '@object-ui/types';
1111
import { useDataScope } from '@object-ui/react';
1212
import { KanbanRenderer } from './index';
@@ -24,13 +24,28 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
2424
className,
2525
}) => {
2626
const [fetchedData, setFetchedData] = useState<any[]>([]);
27+
const [objectDef, setObjectDef] = useState<any>(null);
2728
// loading state
2829
const [loading, setLoading] = useState(false);
2930
const [error, setError] = useState<Error | null>(null);
3031

3132
// Resolve bound data if 'bind' property exists
3233
const boundData = useDataScope(schema.bind);
3334

35+
// Fetch object definition for metadata (labels, options)
36+
useEffect(() => {
37+
const fetchMeta = async () => {
38+
if (!dataSource || !schema.objectName) return;
39+
try {
40+
const def = await dataSource.getObject(schema.objectName);
41+
setObjectDef(def);
42+
} catch (e) {
43+
console.warn("Failed to fetch object def", e);
44+
}
45+
};
46+
fetchMeta();
47+
}, [schema.objectName, dataSource]);
48+
3449
useEffect(() => {
3550
const fetchData = async () => {
3651
if (!dataSource || !schema.objectName) return;
@@ -68,12 +83,64 @@ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
6883
}, [schema.objectName, dataSource, boundData, schema.data]);
6984

7085
// Determine which data to use: bound -> inline -> fetched
71-
const effectiveData = boundData || schema.data || fetchedData;
86+
const rawData = boundData || schema.data || fetchedData;
87+
88+
// Enhance data with title mapping and ensure IDs
89+
const effectiveData = useMemo(() => {
90+
if (!Array.isArray(rawData)) return [];
91+
92+
// Support cardTitle property from schema (passed by ObjectView)
93+
// @ts-ignore - cardTitle might not be in KanbanSchema type definition yet
94+
const titleField = schema.cardTitle || (schema as any).titleField || 'name';
95+
96+
return rawData.map(item => ({
97+
...item,
98+
// Ensure id exists
99+
id: item.id || item._id,
100+
// Map title
101+
title: item[titleField] || item.title || 'Untitled',
102+
}));
103+
}, [rawData, schema]);
104+
105+
// Generate columns if missing but groupBy is present
106+
const effectiveColumns = useMemo(() => {
107+
// If columns exist, returns them (normalized)
108+
if (schema.columns && schema.columns.length > 0) {
109+
// If columns is array of strings, normalize to objects
110+
if (typeof schema.columns[0] === 'string') {
111+
return (schema.columns as unknown as string[]).map(val => ({
112+
id: val,
113+
title: val
114+
}));
115+
}
116+
return schema.columns;
117+
}
118+
119+
// Try to get options from metadata
120+
if (schema.groupBy && objectDef?.fields?.[schema.groupBy]?.options) {
121+
return objectDef.fields[schema.groupBy].options.map((opt: any) => ({
122+
id: opt.value,
123+
title: opt.label
124+
}));
125+
}
126+
127+
// If no columns, but we have groupBy and data, generate from data
128+
if (schema.groupBy && effectiveData.length > 0) {
129+
const groups = new Set(effectiveData.map(item => item[schema.groupBy!]));
130+
return Array.from(groups).map(g => ({
131+
id: String(g),
132+
title: String(g)
133+
}));
134+
}
135+
136+
return [];
137+
}, [schema.columns, schema.groupBy, effectiveData, objectDef]);
72138

73139
// Clone schema to inject data and className
74140
const effectiveSchema = {
75141
...schema,
76142
data: effectiveData,
143+
columns: effectiveColumns,
77144
className: className || schema.className
78145
};
79146

0 commit comments

Comments
 (0)