Skip to content

Commit d3d8ad8

Browse files
authored
Merge pull request #264 from objectstack-ai/copilot/integrate-msw-into-storybook
2 parents be54934 + 3f2c71e commit d3d8ad8

File tree

12 files changed

+339
-97
lines changed

12 files changed

+339
-97
lines changed

.storybook/datasource.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* DataSource Helper for Storybook
3+
*
4+
* Creates an ObjectStack data source adapter that connects to the
5+
* MSW-backed API endpoints running in Storybook.
6+
*/
7+
8+
import { ObjectStackAdapter } from '@object-ui/data-objectstack';
9+
10+
/**
11+
* Create a DataSource for use in Storybook stories.
12+
* This DataSource connects to the MSW mock server at /api/v1.
13+
*/
14+
export function createStorybookDataSource() {
15+
return new ObjectStackAdapter({
16+
baseUrl: '/api/v1',
17+
// No token needed for MSW
18+
});
19+
}

.storybook/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ const config: StorybookConfig = {
2121
return mergeConfig(config, {
2222
resolve: {
2323
alias: {
24+
// Alias for .storybook directory to allow imports from stories
25+
'@storybook-config': path.resolve(__dirname, '.'),
2426
// Alias components package to source to avoid circular dependency during build
2527
'@object-ui/core': path.resolve(__dirname, '../packages/core/src/index.ts'),
2628
'@object-ui/react': path.resolve(__dirname, '../packages/react/src/index.ts'),
2729
'@object-ui/components': path.resolve(__dirname, '../packages/components/src/index.ts'),
2830
'@object-ui/fields': path.resolve(__dirname, '../packages/fields/src/index.tsx'),
2931
'@object-ui/layout': path.resolve(__dirname, '../packages/layout/src/index.ts'),
32+
'@object-ui/data-objectstack': path.resolve(__dirname, '../packages/data-objectstack/src/index.ts'),
33+
// Alias example packages for Storybook to resolve them from workspace
34+
'@object-ui/example-crm': path.resolve(__dirname, '../examples/crm/src/index.ts'),
3035
// Alias plugin packages for Storybook to resolve them from workspace
3136
'@object-ui/plugin-aggrid': path.resolve(__dirname, '../packages/plugin-aggrid/src/index.tsx'),
3237
'@object-ui/plugin-calendar': path.resolve(__dirname, '../packages/plugin-calendar/src/index.tsx'),

.storybook/mocks.ts

Lines changed: 16 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,22 @@
11
// .storybook/mocks.ts
22
import { http, HttpResponse } from 'msw';
3-
import { ObjectStackServer } from '@objectstack/plugin-msw';
43

5-
export const protocol = {
6-
objects: [
7-
{
8-
name: "contact",
9-
fields: {
10-
name: { type: "text" },
11-
email: { type: "email" },
12-
title: { type: "text" },
13-
company: { type: "text" },
14-
status: { type: "select", options: ["Active", "Lead", "Customer"] }
15-
}
16-
},
17-
{
18-
name: "opportunity",
19-
fields: {
20-
name: { type: "text" },
21-
amount: { type: "currency" },
22-
stage: { type: "select" },
23-
closeDate: { type: "date" },
24-
accountId: { type: "lookup", reference_to: "account" }
25-
}
26-
},
27-
{
28-
name: "account",
29-
fields: {
30-
name: { type: "text" },
31-
industry: { type: "text" }
32-
}
33-
}
34-
]
35-
};
36-
37-
// Initialize the mock server
38-
// @ts-ignore
39-
ObjectStackServer.init(protocol);
40-
41-
// Seed basic data
42-
(async () => {
43-
await ObjectStackServer.createData('contact', { id: '1', name: 'John Doe', title: 'Developer', company: 'Tech', status: 'Active' });
44-
await ObjectStackServer.createData('contact', { id: '2', name: 'Jane Smith', title: 'Manager', company: 'Corp', status: 'Customer' });
45-
await ObjectStackServer.createData('account', { id: '1', name: 'Big Corp', industry: 'Finance' });
46-
await ObjectStackServer.createData('opportunity', { id: '1', name: 'Big Deal', amount: 50000, stage: 'Negotiation', accountId: '1' });
47-
})();
4+
/**
5+
* MSW Handlers for Storybook
6+
*
7+
* Note: The main MSW runtime with ObjectStack kernel is initialized in msw-browser.ts
8+
* These handlers are additional story-specific handlers that can be used
9+
* via the msw parameter in individual stories.
10+
*
11+
* The ObjectStack kernel handles standard CRUD operations automatically via MSWPlugin.
12+
*/
4813

4914
export const handlers = [
50-
// Standard CRUD handlers using ObjectStackServer
51-
http.get('/api/v1/data/:object', async ({ params }) => {
52-
const { object } = params;
53-
const result = await ObjectStackServer.findData(object as string);
54-
return HttpResponse.json({ value: result.data });
55-
}),
56-
57-
http.get('/api/v1/data/:object/:id', async ({ params }) => {
58-
const { object, id } = params;
59-
const result = await ObjectStackServer.getData(object as string, id as string);
60-
return HttpResponse.json(result.data);
61-
}),
62-
63-
http.post('/api/v1/data/:object', async ({ params, request }) => {
64-
const { object } = params;
65-
const body = await request.json();
66-
const result = await ObjectStackServer.createData(object as string, body);
67-
return HttpResponse.json(result.data);
68-
}),
69-
70-
// Custom bootstrap if needed
71-
http.get('/api/bootstrap', async () => {
72-
const contacts = (await ObjectStackServer.findData('contact')).data;
73-
return HttpResponse.json({ contacts });
74-
})
15+
// Additional custom handlers can be added here for specific story needs
16+
// The ObjectStack MSW runtime already handles:
17+
// - /api/v1/data/:object (GET, POST)
18+
// - /api/v1/data/:object/:id (GET, PUT, DELETE)
19+
// - /api/v1/metadata/*
20+
// - /api/v1/index.json (for ObjectStackClient.connect())
21+
// - /api/bootstrap
7522
];

.storybook/msw-browser.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* MSW Browser Setup for Storybook
3+
*
4+
* This file integrates the ObjectStack runtime with MSW in browser mode
5+
* for use within Storybook stories. Based on the pattern from examples/crm-app.
6+
*/
7+
8+
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
9+
import { ObjectQLPlugin } from '@objectstack/objectql';
10+
import { InMemoryDriver } from '@objectstack/driver-memory';
11+
import { MSWPlugin } from '@objectstack/plugin-msw';
12+
import { config as crmConfig } from '@object-ui/example-crm';
13+
import { http, HttpResponse } from 'msw';
14+
15+
let kernel: ObjectKernel | null = null;
16+
17+
export async function startMockServer() {
18+
if (kernel) return kernel;
19+
20+
console.log('[Storybook MSW] Starting ObjectStack Runtime (Browser Mode)...');
21+
console.log('[Storybook MSW] Loaded Config:', crmConfig ? 'Found' : 'Missing', crmConfig?.apps?.length);
22+
23+
if (crmConfig && crmConfig.objects) {
24+
console.log('[Storybook MSW] Objects in Config:', crmConfig.objects.map(o => o.name));
25+
} else {
26+
console.error('[Storybook MSW] No objects found in config!');
27+
}
28+
29+
const driver = new InMemoryDriver();
30+
kernel = new ObjectKernel();
31+
32+
try {
33+
kernel
34+
.use(new ObjectQLPlugin())
35+
.use(new DriverPlugin(driver, 'memory'));
36+
37+
if (crmConfig) {
38+
kernel.use(new AppPlugin(crmConfig));
39+
} else {
40+
console.error('❌ CRM Config is missing! Skipping AppPlugin.');
41+
}
42+
43+
kernel.use(new MSWPlugin({
44+
enableBrowser: true,
45+
baseUrl: '/api/v1',
46+
logRequests: true,
47+
customHandlers: [
48+
// Handle /api/v1/index.json for ObjectStackClient.connect()
49+
http.get('/api/v1/index.json', async () => {
50+
return HttpResponse.json({
51+
version: '1.0',
52+
objects: ['contact', 'opportunity', 'account'],
53+
endpoints: {
54+
data: '/api/v1/data',
55+
metadata: '/api/v1/metadata'
56+
}
57+
});
58+
}),
59+
// Explicitly handle all metadata requests to prevent pass-through
60+
http.get('/api/v1/metadata/*', async () => {
61+
return HttpResponse.json({});
62+
}),
63+
http.get('/api/bootstrap', async () => {
64+
const contacts = await driver.find('contact', { object: 'contact' });
65+
const stats = { revenue: 125000, leads: 45, deals: 12 };
66+
return HttpResponse.json({
67+
user: { name: "Demo User", role: "admin" },
68+
stats,
69+
contacts: contacts || []
70+
});
71+
})
72+
]
73+
}));
74+
75+
console.log('[Storybook MSW] Bootstrapping kernel...');
76+
await kernel.bootstrap();
77+
console.log('[Storybook MSW] Bootstrap Complete');
78+
79+
// Seed Data
80+
if (crmConfig) {
81+
await initializeMockData(driver);
82+
}
83+
} catch (err: any) {
84+
console.error('❌ Storybook Mock Server Start Failed:', err);
85+
throw err;
86+
}
87+
88+
return kernel;
89+
}
90+
91+
// Helper to seed data into the in-memory driver
92+
async function initializeMockData(driver: InMemoryDriver) {
93+
console.log('[Storybook MSW] Initializing mock data...');
94+
// @ts-ignore
95+
const manifest = crmConfig.manifest;
96+
if (manifest && manifest.data) {
97+
for (const dataSet of manifest.data) {
98+
console.log(`[Storybook MSW] Seeding ${dataSet.object}...`);
99+
if (dataSet.records) {
100+
for (const record of dataSet.records) {
101+
await driver.create(dataSet.object, record);
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
export { kernel };

.storybook/preview.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Preview } from '@storybook/react-vite'
22
import { initialize, mswLoader } from 'msw-storybook-addon';
33
import { handlers } from './mocks';
4+
import { startMockServer } from './msw-browser';
45
import '../packages/components/src/index.css';
56
import { ComponentRegistry } from '@object-ui/core';
67
import * as components from '../packages/components/src/index';
@@ -10,6 +11,14 @@ initialize({
1011
onUnhandledRequest: 'bypass'
1112
});
1213

14+
// Start MSW runtime with ObjectStack kernel
15+
// This must be called during Storybook initialization
16+
if (typeof window !== 'undefined') {
17+
startMockServer().catch(err => {
18+
console.error('Failed to start MSW runtime:', err);
19+
});
20+
}
21+
1322
// Register all base components for Storybook
1423
Object.values(components);
1524

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
"devDependencies": {
5151
"@changesets/cli": "^2.29.8",
5252
"@eslint/js": "^9.39.1",
53+
"@objectstack/core": "^0.6.1",
54+
"@objectstack/driver-memory": "^0.6.1",
55+
"@objectstack/objectql": "^0.6.1",
5356
"@objectstack/plugin-msw": "^0.6.1",
57+
"@objectstack/runtime": "^0.6.1",
5458
"@storybook/addon-essentials": "^8.6.14",
5559
"@storybook/addon-interactions": "^8.6.14",
5660
"@storybook/addon-links": "^8.6.15",

packages/components/src/stories-json/object-form.stories.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2-
import { SchemaRenderer } from '../SchemaRenderer';
2+
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
33
import type { BaseSchema } from '@object-ui/types';
4+
import { createStorybookDataSource } from '@storybook-config/datasource';
45

56
const meta = {
67
title: 'Views/Object Form',
@@ -17,14 +18,21 @@ const meta = {
1718
export default meta;
1819
type Story = StoryObj<typeof meta>;
1920

20-
const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21+
// Create a DataSource instance that connects to MSW
22+
const dataSource = createStorybookDataSource();
23+
24+
const renderStory = (args: any) => (
25+
<SchemaRendererProvider dataSource={dataSource}>
26+
<SchemaRenderer schema={args as unknown as BaseSchema} />
27+
</SchemaRendererProvider>
28+
);
2129

2230
export const BasicSchema: Story = {
2331
render: renderStory,
2432
args: {
2533
type: 'object-form',
2634
objectName: 'User',
27-
fields: [
35+
customFields: [
2836
{ name: 'firstName', label: 'First Name', type: 'text', required: true },
2937
{ name: 'lastName', label: 'Last Name', type: 'text', required: true },
3038
{ name: 'email', label: 'Email', type: 'email', required: true },
@@ -66,7 +74,7 @@ export const ComplexFields: Story = {
6674
args: {
6775
type: 'object-form',
6876
objectName: 'Product',
69-
fields: [
77+
customFields: [
7078
{ name: 'name', label: 'Product Name', type: 'text', required: true },
7179
{ name: 'category', label: 'Category', type: 'select', options: ['Electronics', 'Clothing', 'Food'], required: true },
7280
{ name: 'price', label: 'Price', type: 'number', required: true },
@@ -76,3 +84,47 @@ export const ComplexFields: Story = {
7684
className: 'w-full max-w-2xl'
7785
} as any,
7886
};
87+
88+
/**
89+
* Contact Form - Uses MSW-backed schema from ObjectStack runtime
90+
*
91+
* This story demonstrates integration with the MSW plugin runtime mode.
92+
* The form schema is fetched from /api/v1/metadata/contact via the ObjectStack kernel.
93+
*/
94+
export const ContactForm: Story = {
95+
render: renderStory,
96+
args: {
97+
type: 'object-form',
98+
objectName: 'contact',
99+
customFields: [
100+
{ name: 'name', label: 'Name', type: 'text', required: true },
101+
{ name: 'email', label: 'Email', type: 'email', required: true },
102+
{ name: 'phone', label: 'Phone', type: 'tel' },
103+
{ name: 'title', label: 'Title', type: 'text' },
104+
{ name: 'company', label: 'Company', type: 'text' },
105+
{ name: 'status', label: 'Status', type: 'select', options: ['Active', 'Lead', 'Customer'] }
106+
],
107+
className: 'w-full max-w-2xl'
108+
} as any,
109+
};
110+
111+
/**
112+
* Opportunity Form - Uses MSW-backed schema from ObjectStack runtime
113+
*
114+
* This story demonstrates creating/editing opportunity records via MSW runtime.
115+
*/
116+
export const OpportunityForm: Story = {
117+
render: renderStory,
118+
args: {
119+
type: 'object-form',
120+
objectName: 'opportunity',
121+
customFields: [
122+
{ name: 'name', label: 'Opportunity Name', type: 'text', required: true },
123+
{ name: 'amount', label: 'Amount', type: 'number', required: true },
124+
{ name: 'stage', label: 'Stage', type: 'select', options: ['Prospecting', 'Qualification', 'Proposal', 'Negotiation', 'Closed Won', 'Closed Lost'] },
125+
{ name: 'closeDate', label: 'Close Date', type: 'date' },
126+
{ name: 'description', label: 'Description', type: 'textarea', rows: 4 }
127+
],
128+
className: 'w-full max-w-2xl'
129+
} as any,
130+
};

packages/components/src/stories-json/object-gantt.stories.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Meta, StoryObj } from '@storybook/react';
2-
import { SchemaRenderer } from '../SchemaRenderer';
2+
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
33
import type { BaseSchema } from '@object-ui/types';
4+
import { createStorybookDataSource } from '@storybook-config/datasource';
45

56
const meta = {
67
title: 'Views/Gantt',
@@ -20,7 +21,14 @@ const meta = {
2021
export default meta;
2122
type Story = StoryObj<typeof meta>;
2223

23-
const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
24+
// Create a DataSource instance that connects to MSW
25+
const dataSource = createStorybookDataSource();
26+
27+
const renderStory = (args: any) => (
28+
<SchemaRendererProvider dataSource={dataSource}>
29+
<SchemaRenderer schema={args as unknown as BaseSchema} />
30+
</SchemaRendererProvider>
31+
);
2432

2533
export const ProjectSchedule: Story = {
2634
render: renderStory,

0 commit comments

Comments
 (0)