Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .storybook/datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* DataSource Helper for Storybook
*
* Creates an ObjectStack data source adapter that connects to the
* MSW-backed API endpoints running in Storybook.
*/

import { ObjectStackAdapter } from '@object-ui/data-objectstack';

/**
* Create a DataSource for use in Storybook stories.
* This DataSource connects to the MSW mock server at /api/v1.
*/
export function createStorybookDataSource() {
return new ObjectStackAdapter({
baseUrl: '/api/v1',
// No token needed for MSW
});
}
5 changes: 5 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ const config: StorybookConfig = {
return mergeConfig(config, {
resolve: {
alias: {
// Alias for .storybook directory to allow imports from stories
'@storybook-config': path.resolve(__dirname, '.'),
// Alias components package to source to avoid circular dependency during build
'@object-ui/core': path.resolve(__dirname, '../packages/core/src/index.ts'),
'@object-ui/react': path.resolve(__dirname, '../packages/react/src/index.ts'),
'@object-ui/components': path.resolve(__dirname, '../packages/components/src/index.ts'),
'@object-ui/fields': path.resolve(__dirname, '../packages/fields/src/index.tsx'),
'@object-ui/layout': path.resolve(__dirname, '../packages/layout/src/index.ts'),
'@object-ui/data-objectstack': path.resolve(__dirname, '../packages/data-objectstack/src/index.ts'),
// Alias example packages for Storybook to resolve them from workspace
'@object-ui/example-crm': path.resolve(__dirname, '../examples/crm/src/index.ts'),
// Alias plugin packages for Storybook to resolve them from workspace
'@object-ui/plugin-aggrid': path.resolve(__dirname, '../packages/plugin-aggrid/src/index.tsx'),
'@object-ui/plugin-calendar': path.resolve(__dirname, '../packages/plugin-calendar/src/index.tsx'),
Expand Down
85 changes: 16 additions & 69 deletions .storybook/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,22 @@
// .storybook/mocks.ts
import { http, HttpResponse } from 'msw';
import { ObjectStackServer } from '@objectstack/plugin-msw';

export const protocol = {
objects: [
{
name: "contact",
fields: {
name: { type: "text" },
email: { type: "email" },
title: { type: "text" },
company: { type: "text" },
status: { type: "select", options: ["Active", "Lead", "Customer"] }
}
},
{
name: "opportunity",
fields: {
name: { type: "text" },
amount: { type: "currency" },
stage: { type: "select" },
closeDate: { type: "date" },
accountId: { type: "lookup", reference_to: "account" }
}
},
{
name: "account",
fields: {
name: { type: "text" },
industry: { type: "text" }
}
}
]
};

// Initialize the mock server
// @ts-ignore
ObjectStackServer.init(protocol);

// Seed basic data
(async () => {
await ObjectStackServer.createData('contact', { id: '1', name: 'John Doe', title: 'Developer', company: 'Tech', status: 'Active' });
await ObjectStackServer.createData('contact', { id: '2', name: 'Jane Smith', title: 'Manager', company: 'Corp', status: 'Customer' });
await ObjectStackServer.createData('account', { id: '1', name: 'Big Corp', industry: 'Finance' });
await ObjectStackServer.createData('opportunity', { id: '1', name: 'Big Deal', amount: 50000, stage: 'Negotiation', accountId: '1' });
})();
/**
* MSW Handlers for Storybook
*
* Note: The main MSW runtime with ObjectStack kernel is initialized in msw-browser.ts
* These handlers are additional story-specific handlers that can be used
* via the msw parameter in individual stories.
*
* The ObjectStack kernel handles standard CRUD operations automatically via MSWPlugin.
*/

export const handlers = [
// Standard CRUD handlers using ObjectStackServer
http.get('/api/v1/data/:object', async ({ params }) => {
const { object } = params;
const result = await ObjectStackServer.findData(object as string);
return HttpResponse.json({ value: result.data });
}),

http.get('/api/v1/data/:object/:id', async ({ params }) => {
const { object, id } = params;
const result = await ObjectStackServer.getData(object as string, id as string);
return HttpResponse.json(result.data);
}),

http.post('/api/v1/data/:object', async ({ params, request }) => {
const { object } = params;
const body = await request.json();
const result = await ObjectStackServer.createData(object as string, body);
return HttpResponse.json(result.data);
}),

// Custom bootstrap if needed
http.get('/api/bootstrap', async () => {
const contacts = (await ObjectStackServer.findData('contact')).data;
return HttpResponse.json({ contacts });
})
// Additional custom handlers can be added here for specific story needs
// The ObjectStack MSW runtime already handles:
// - /api/v1/data/:object (GET, POST)
// - /api/v1/data/:object/:id (GET, PUT, DELETE)
// - /api/v1/metadata/*
// - /api/v1/index.json (for ObjectStackClient.connect())
// - /api/bootstrap
];
108 changes: 108 additions & 0 deletions .storybook/msw-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* MSW Browser Setup for Storybook
*
* This file integrates the ObjectStack runtime with MSW in browser mode
* for use within Storybook stories. Based on the pattern from examples/crm-app.
*/

import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { MSWPlugin } from '@objectstack/plugin-msw';
import { config as crmConfig } from '@object-ui/example-crm';
import { http, HttpResponse } from 'msw';

let kernel: ObjectKernel | null = null;

export async function startMockServer() {
if (kernel) return kernel;

console.log('[Storybook MSW] Starting ObjectStack Runtime (Browser Mode)...');
console.log('[Storybook MSW] Loaded Config:', crmConfig ? 'Found' : 'Missing', crmConfig?.apps?.length);

if (crmConfig && crmConfig.objects) {
console.log('[Storybook MSW] Objects in Config:', crmConfig.objects.map(o => o.name));
} else {
console.error('[Storybook MSW] No objects found in config!');
}

const driver = new InMemoryDriver();
kernel = new ObjectKernel();

try {
kernel
.use(new ObjectQLPlugin())
.use(new DriverPlugin(driver, 'memory'));

if (crmConfig) {
kernel.use(new AppPlugin(crmConfig));
} else {
console.error('❌ CRM Config is missing! Skipping AppPlugin.');
}

kernel.use(new MSWPlugin({
enableBrowser: true,
baseUrl: '/api/v1',
logRequests: true,
customHandlers: [
// Handle /api/v1/index.json for ObjectStackClient.connect()
http.get('/api/v1/index.json', async () => {
return HttpResponse.json({
version: '1.0',
objects: ['contact', 'opportunity', 'account'],
endpoints: {
data: '/api/v1/data',
metadata: '/api/v1/metadata'
}
});
}),
// Explicitly handle all metadata requests to prevent pass-through
http.get('/api/v1/metadata/*', async () => {
return HttpResponse.json({});
}),
http.get('/api/bootstrap', async () => {
const contacts = await driver.find('contact', { object: 'contact' });
const stats = { revenue: 125000, leads: 45, deals: 12 };
return HttpResponse.json({
user: { name: "Demo User", role: "admin" },
stats,
contacts: contacts || []
});
})
]
}));

console.log('[Storybook MSW] Bootstrapping kernel...');
await kernel.bootstrap();
console.log('[Storybook MSW] Bootstrap Complete');

// Seed Data
if (crmConfig) {
await initializeMockData(driver);
}
} catch (err: any) {
console.error('❌ Storybook Mock Server Start Failed:', err);
throw err;
}

return kernel;
}

// Helper to seed data into the in-memory driver
async function initializeMockData(driver: InMemoryDriver) {
console.log('[Storybook MSW] Initializing mock data...');
// @ts-ignore
const manifest = crmConfig.manifest;
if (manifest && manifest.data) {
for (const dataSet of manifest.data) {
console.log(`[Storybook MSW] Seeding ${dataSet.object}...`);
if (dataSet.records) {
for (const record of dataSet.records) {
await driver.create(dataSet.object, record);
}
}
}
}
}

export { kernel };
9 changes: 9 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Preview } from '@storybook/react-vite'
import { initialize, mswLoader } from 'msw-storybook-addon';
import { handlers } from './mocks';
import { startMockServer } from './msw-browser';
import '../packages/components/src/index.css';
import { ComponentRegistry } from '@object-ui/core';
import * as components from '../packages/components/src/index';
Expand All @@ -10,6 +11,14 @@ initialize({
onUnhandledRequest: 'bypass'
});

// Start MSW runtime with ObjectStack kernel
// This must be called during Storybook initialization
if (typeof window !== 'undefined') {
startMockServer().catch(err => {
console.error('Failed to start MSW runtime:', err);
});
}

// Register all base components for Storybook
Object.values(components);

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
"devDependencies": {
"@changesets/cli": "^2.29.8",
"@eslint/js": "^9.39.1",
"@objectstack/core": "^0.6.1",
"@objectstack/driver-memory": "^0.6.1",
"@objectstack/objectql": "^0.6.1",
"@objectstack/plugin-msw": "^0.6.1",
"@objectstack/runtime": "^0.6.1",
"@storybook/addon-essentials": "^8.6.14",
"@storybook/addon-interactions": "^8.6.14",
"@storybook/addon-links": "^8.6.15",
Expand Down
60 changes: 56 additions & 4 deletions packages/components/src/stories-json/object-form.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SchemaRenderer } from '../SchemaRenderer';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
import type { BaseSchema } from '@object-ui/types';
import { createStorybookDataSource } from '@storybook-config/datasource';

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

const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
// Create a DataSource instance that connects to MSW
const dataSource = createStorybookDataSource();

const renderStory = (args: any) => (
<SchemaRendererProvider dataSource={dataSource}>
<SchemaRenderer schema={args as unknown as BaseSchema} />
</SchemaRendererProvider>
);

export const BasicSchema: Story = {
render: renderStory,
args: {
type: 'object-form',
objectName: 'User',
fields: [
customFields: [
{ name: 'firstName', label: 'First Name', type: 'text', required: true },
{ name: 'lastName', label: 'Last Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
Expand Down Expand Up @@ -66,7 +74,7 @@ export const ComplexFields: Story = {
args: {
type: 'object-form',
objectName: 'Product',
fields: [
customFields: [
{ name: 'name', label: 'Product Name', type: 'text', required: true },
{ name: 'category', label: 'Category', type: 'select', options: ['Electronics', 'Clothing', 'Food'], required: true },
{ name: 'price', label: 'Price', type: 'number', required: true },
Expand All @@ -76,3 +84,47 @@ export const ComplexFields: Story = {
className: 'w-full max-w-2xl'
} as any,
};

/**
* Contact Form - Uses MSW-backed schema from ObjectStack runtime
*
* This story demonstrates integration with the MSW plugin runtime mode.
* The form schema is fetched from /api/v1/metadata/contact via the ObjectStack kernel.
*/
export const ContactForm: Story = {
render: renderStory,
args: {
type: 'object-form',
objectName: 'contact',
customFields: [
{ name: 'name', label: 'Name', type: 'text', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'phone', label: 'Phone', type: 'tel' },
{ name: 'title', label: 'Title', type: 'text' },
{ name: 'company', label: 'Company', type: 'text' },
{ name: 'status', label: 'Status', type: 'select', options: ['Active', 'Lead', 'Customer'] }
],
className: 'w-full max-w-2xl'
} as any,
};

/**
* Opportunity Form - Uses MSW-backed schema from ObjectStack runtime
*
* This story demonstrates creating/editing opportunity records via MSW runtime.
*/
export const OpportunityForm: Story = {
render: renderStory,
args: {
type: 'object-form',
objectName: 'opportunity',
customFields: [
{ name: 'name', label: 'Opportunity Name', type: 'text', required: true },
{ name: 'amount', label: 'Amount', type: 'number', required: true },
{ name: 'stage', label: 'Stage', type: 'select', options: ['Prospecting', 'Qualification', 'Proposal', 'Negotiation', 'Closed Won', 'Closed Lost'] },
{ name: 'closeDate', label: 'Close Date', type: 'date' },
{ name: 'description', label: 'Description', type: 'textarea', rows: 4 }
],
className: 'w-full max-w-2xl'
} as any,
};
12 changes: 10 additions & 2 deletions packages/components/src/stories-json/object-gantt.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SchemaRenderer } from '../SchemaRenderer';
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
import type { BaseSchema } from '@object-ui/types';
import { createStorybookDataSource } from '@storybook-config/datasource';

const meta = {
title: 'Views/Gantt',
Expand All @@ -20,7 +21,14 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
// Create a DataSource instance that connects to MSW
const dataSource = createStorybookDataSource();

const renderStory = (args: any) => (
<SchemaRendererProvider dataSource={dataSource}>
<SchemaRenderer schema={args as unknown as BaseSchema} />
</SchemaRendererProvider>
);

export const ProjectSchedule: Story = {
render: renderStory,
Expand Down
Loading
Loading