Skip to content

Commit 923a0fb

Browse files
committed
feat: add ObjectKanban and ObjectTimeline components with data fetching and rendering capabilities; include MSW tests for calendar and kanban plugins
1 parent e5e899d commit 923a0fb

11 files changed

Lines changed: 478 additions & 4 deletions

File tree

packages/plugin-calendar/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@types/react": "^19.2.9",
4747
"@types/react-dom": "^19.2.3",
4848
"@vitejs/plugin-react": "^4.2.1",
49+
"@object-ui/data-objectstack": "workspace:*",
4950
"typescript": "^5.9.3",
5051
"vite": "^7.3.1",
5152
"vite-plugin-dts": "^4.5.4"
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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://test-api.com';
11+
12+
// --- Mock Data ---
13+
14+
const mockEvents = {
15+
value: [
16+
{
17+
_id: '1',
18+
title: 'Meeting with Client',
19+
start: new Date().toISOString(),
20+
end: new Date(Date.now() + 3600000).toISOString(),
21+
type: 'business'
22+
},
23+
{
24+
_id: '2',
25+
title: 'Team Lunch',
26+
start: new Date(Date.now() + 86400000).toISOString(), // Tomorrow
27+
type: 'personal'
28+
}
29+
]
30+
};
31+
32+
// --- MSW Setup ---
33+
34+
const handlers = [
35+
http.get(`${BASE_URL}/api/v1`, () => {
36+
return HttpResponse.json({ status: 'ok', version: '1.0.0' });
37+
}),
38+
39+
// Data Query: GET /api/v1/data/events
40+
http.get(`${BASE_URL}/api/v1/data/events`, () => {
41+
return HttpResponse.json(mockEvents);
42+
})
43+
];
44+
45+
const server = setupServer(...handlers);
46+
47+
// --- Test Suite ---
48+
49+
describe('ObjectCalendar with MSW', () => {
50+
if (!process.env.OBJECTSTACK_API_URL) {
51+
beforeAll(() => server.listen());
52+
afterEach(() => server.resetHandlers());
53+
afterAll(() => server.close());
54+
}
55+
56+
const dataSource = new ObjectStackAdapter({
57+
baseUrl: BASE_URL,
58+
});
59+
60+
it('fetches events and renders them', async () => {
61+
render(
62+
<ObjectCalendar
63+
schema={{
64+
type: 'calendar',
65+
objectName: 'events',
66+
// Calendar specific config usually goes into 'calendar' prop or implicit mapping
67+
calendar: {
68+
dateField: 'start',
69+
endDateField: 'end',
70+
titleField: 'title'
71+
}
72+
}}
73+
dataSource={dataSource}
74+
/>
75+
);
76+
77+
// Verify events appear
78+
await waitFor(() => {
79+
expect(screen.getByText('Meeting with Client')).toBeInTheDocument();
80+
});
81+
82+
// Check subsequent events (might need navigation depending on view,
83+
// but default month view usually shows current month dates)
84+
// Note: If today is near end of month, tomorrow might be in next month view.
85+
// However, the test event is "now", so it should be visible.
86+
87+
// We can just assert the first event for now.
88+
});
89+
});

packages/plugin-kanban/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"@types/react": "^19.2.9",
4848
"@types/react-dom": "^19.2.3",
4949
"@vitejs/plugin-react": "^4.2.1",
50+
"@object-ui/data-objectstack": "workspace:*",
5051
"typescript": "^5.9.3",
5152
"vite": "^7.3.1",
5253
"vite-plugin-dts": "^4.5.4"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 { ObjectKanban } from './ObjectKanban';
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+
// Register layout components (if needed by cards)
11+
// registerLayout();
12+
13+
const BASE_URL = 'http://localhost';
14+
15+
// --- Mock Data ---
16+
17+
const mockTasks = {
18+
value: [
19+
{ _id: '1', title: 'Task 1', status: 'todo', description: 'Description 1' },
20+
{ _id: '2', title: 'Task 2', status: 'done', description: 'Description 2' },
21+
{ _id: '3', title: 'Task 3', status: 'todo', description: 'Description 3' }
22+
]
23+
};
24+
25+
// --- MSW Setup ---
26+
27+
const handlers = [
28+
// OPTIONS handler for CORS preflight checks
29+
http.options('*', () => {
30+
return new HttpResponse(null, { status: 200 });
31+
}),
32+
33+
// Health check
34+
http.get(`${BASE_URL}/api/v1`, () => {
35+
return HttpResponse.json({ status: 'ok', version: '1.0.0' });
36+
}),
37+
38+
// Data Query: GET /api/v1/data/tasks
39+
http.get(`${BASE_URL}/api/v1/data/tasks`, () => {
40+
return HttpResponse.json(mockTasks);
41+
})
42+
];
43+
44+
const server = setupServer(...handlers);
45+
46+
// --- Test Suite ---
47+
48+
describe('ObjectKanban with MSW', () => {
49+
if (!process.env.OBJECTSTACK_API_URL) {
50+
beforeAll(() => server.listen());
51+
afterEach(() => server.resetHandlers());
52+
afterAll(() => server.close());
53+
}
54+
55+
const dataSource = new ObjectStackAdapter({
56+
baseUrl: BASE_URL,
57+
});
58+
59+
it('fetches tasks and renders them in columns based on groupBy', async () => {
60+
render(
61+
<ObjectKanban
62+
schema={{
63+
type: 'kanban',
64+
objectName: 'tasks',
65+
groupBy: 'status',
66+
columns: [
67+
{ id: 'todo', title: 'To Do', cards: [] },
68+
{ id: 'done', title: 'Done', cards: [] }
69+
]
70+
}}
71+
dataSource={dataSource}
72+
/>
73+
);
74+
75+
// Initial state might show Skeleton, wait for data
76+
await waitFor(() => {
77+
expect(screen.getByText('Task 1')).toBeInTheDocument();
78+
});
79+
80+
// Check classification
81+
// Task 1 (todo) and Task 3 (todo) should be in To Do column.
82+
// Task 2 (done) should be in Done column.
83+
84+
// We can verify "Task 1" is present.
85+
expect(screen.getByText('Task 2')).toBeInTheDocument();
86+
expect(screen.getByText('Task 3')).toBeInTheDocument();
87+
88+
// Check descriptions
89+
expect(screen.getByText('Description 1')).toBeInTheDocument();
90+
});
91+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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+
import React, { useEffect, useState } from 'react';
10+
import type { DataSource } from '@object-ui/types';
11+
import { useDataScope } from '@object-ui/react';
12+
import { KanbanRenderer } from './index';
13+
import { KanbanSchema } from './types';
14+
15+
export interface ObjectKanbanProps {
16+
schema: KanbanSchema;
17+
dataSource?: DataSource;
18+
className?: string; // Allow override
19+
}
20+
21+
export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
22+
schema,
23+
dataSource,
24+
className,
25+
}) => {
26+
const [fetchedData, setFetchedData] = useState<any[]>([]);
27+
// loading state
28+
const [loading, setLoading] = useState(false);
29+
const [error, setError] = useState<Error | null>(null);
30+
31+
// Resolve bound data if 'bind' property exists
32+
const boundData = useDataScope(schema.bind);
33+
34+
useEffect(() => {
35+
const fetchData = async () => {
36+
if (!dataSource || !schema.objectName) return;
37+
setLoading(true);
38+
try {
39+
// Simple find for now, usually we might want filters
40+
// Using a large limit or pagination would be needed for real apps,
41+
// for now, we assume a reasonable default.
42+
const results = await dataSource.find(schema.objectName, {
43+
options: { $top: 100 } // Fetch up to 100 cards
44+
});
45+
// Handle { value: [] } OData shape or direct array
46+
const data = (results as any).value || results;
47+
console.log("ObjectKanban fetched:", JSON.stringify(data));
48+
if (Array.isArray(data)) {
49+
setFetchedData(data);
50+
}
51+
} catch (e) {
52+
console.error(e);
53+
setError(e as Error);
54+
} finally {
55+
setLoading(false);
56+
}
57+
};
58+
59+
// Trigger fetch if we have an objectName AND verify no inline/bound data overrides it
60+
if (schema.objectName && !boundData && !schema.data) {
61+
fetchData();
62+
}
63+
}, [schema.objectName, dataSource, boundData, schema.data]);
64+
65+
// Determine which data to use: bound -> inline -> fetched
66+
const effectiveData = boundData || schema.data || fetchedData;
67+
68+
// Clone schema to inject data and className
69+
const effectiveSchema = {
70+
...schema,
71+
data: effectiveData,
72+
className: className || schema.className
73+
};
74+
75+
if (error) {
76+
return (
77+
<div className="p-4 border border-destructive/50 rounded bg-destructive/10 text-destructive">
78+
Error loading kanban data: {error.message}
79+
</div>
80+
);
81+
}
82+
83+
// Pass through to the renderer
84+
return <KanbanRenderer schema={effectiveSchema} />;
85+
}

packages/plugin-kanban/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ export interface KanbanColumn {
3737
export interface KanbanSchema extends BaseSchema {
3838
type: 'kanban';
3939

40+
/**
41+
* Object name to fetch data from.
42+
*/
43+
objectName?: string;
44+
45+
/**
46+
* Field to group records by (maps to column IDs).
47+
*/
48+
groupBy?: string;
49+
50+
/**
51+
* Static data or bound data.
52+
*/
53+
data?: any[];
54+
4055
/**
4156
* Array of columns to display in the kanban board.
4257
* Each column contains an array of cards.

packages/plugin-timeline/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@types/react": "^19.2.9",
4545
"@types/react-dom": "^19.2.3",
4646
"@vitejs/plugin-react": "^4.2.1",
47+
"@object-ui/data-objectstack": "workspace:*",
4748
"typescript": "^5.9.3",
4849
"vite": "^7.3.1",
4950
"vite-plugin-dts": "^4.5.4"
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import '@testing-library/jest-dom';
4+
import { ObjectTimeline } from './ObjectTimeline';
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://test-api.com';
11+
12+
// --- Mock Data ---
13+
14+
const mockMilestones = {
15+
value: [
16+
{ _id: '1', title: 'Start Project', due_date: '2023-01-01', description: 'Initial Kickoff' },
17+
{ _id: '2', title: 'Beta Launch', due_date: '2023-06-01', description: 'Public Beta' },
18+
{ _id: '3', title: 'Release', due_date: '2023-12-01', description: 'v1.0 Release' }
19+
]
20+
};
21+
22+
// --- MSW Setup ---
23+
24+
const handlers = [
25+
http.get(`${BASE_URL}/api/v1/data/milestones`, () => {
26+
return HttpResponse.json(mockMilestones);
27+
})
28+
];
29+
30+
const server = setupServer(...handlers);
31+
32+
// --- Test Suite ---
33+
34+
describe('ObjectTimeline with MSW', () => {
35+
beforeAll(() => server.listen());
36+
afterEach(() => server.resetHandlers());
37+
afterAll(() => server.close());
38+
39+
it('fetches and renders timeline items from object data', async () => {
40+
const adapter = new ObjectStackAdapter({
41+
baseUrl: BASE_URL,
42+
bucket: 'test-bucket'
43+
});
44+
45+
const schema = {
46+
type: 'timeline',
47+
objectName: 'milestones',
48+
variant: 'vertical',
49+
titleField: 'title',
50+
dateField: 'due_date',
51+
descriptionField: 'description'
52+
};
53+
54+
render(
55+
<ObjectTimeline
56+
// @ts-ignore
57+
schema={schema}
58+
dataSource={adapter}
59+
/>
60+
);
61+
62+
// Wait for items to appear
63+
await waitFor(() => {
64+
expect(screen.getByText('Start Project')).toBeInTheDocument();
65+
});
66+
67+
expect(screen.getByText('Beta Launch')).toBeInTheDocument();
68+
expect(screen.getByText('Release')).toBeInTheDocument();
69+
70+
// Check descriptions if rendered
71+
expect(screen.getByText('Initial Kickoff')).toBeInTheDocument();
72+
});
73+
});

0 commit comments

Comments
 (0)