Step 6: Add inventory forms
The last piece is a create/edit workflow that reuses the same validators and API you built earlier.
1. Create form definitions
mkdir -p apps/saasframe/src/modules/inventory/backend/inventory/create
touch apps/saasframe/src/modules/inventory/backend/inventory/create/page.tsx
touch apps/saasframe/src/modules/inventory/backend/inventory/create/page.meta.ts
apps/saasframe/src/modules/inventory/backend/inventory/create/page.tsx
import { CrudForm } from '@saasframe/ui/backend/CrudForm';
import type { CrudFormSchema } from '@saasframe/ui/backend/CrudForm/types';
import { readApiResultOrThrow } from '@saasframe/ui/backend/utils/apiCall';
import type { UpsertInventoryItemInput } from '../../data/validators';
const schema: CrudFormSchema<UpsertInventoryItemInput> = {
title: 'Create Inventory Item',
fields: [
{ id: 'sku', label: 'SKU', component: 'text', required: true },
{ id: 'name', label: 'Name', component: 'text', required: true },
{ id: 'quantity', label: 'Quantity', component: 'number', required: true, min: 0 },
{ id: 'location', label: 'Location', component: 'text' },
],
};
async function submit(data: UpsertInventoryItemInput) {
return readApiResultOrThrow('/api/items', {
method: 'POST',
body: JSON.stringify(data),
}, { errorMessage: 'Failed to create inventory item' });
}
const InventoryCreatePage = () => {
return <CrudForm schema={schema} onSubmit={submit} redirectTo="/backend/inventory/list" />;
};
export default InventoryCreatePage;
apps/saasframe/src/modules/inventory/backend/inventory/create/page.meta.ts
import type { PageMetadata } from '@saasframe/shared/modules/registry';
export const metadata: PageMetadata = {
title: 'Add item',
group: 'Operations',
order: 21,
requireAuth: true,
requireFeatures: ['inventory.create'],
};
2. Reuse the form for edits
Create an edit route that fetches row data and submits via PATCH:
mkdir -p apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit
touch apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.tsx
touch apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.meta.ts
apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.tsx
import { CrudForm } from '@saasframe/ui/backend/CrudForm';
import type { CrudFormSchema } from '@saasframe/ui/backend/CrudForm/types';
import { readApiResultOrThrow } from '@saasframe/ui/backend/utils/apiCall';
import type { UpsertInventoryItemInput } from '../../../data/validators';
const schema: CrudFormSchema<UpsertInventoryItemInput> = {
title: 'Edit Inventory Item',
fields: [
{ id: 'sku', label: 'SKU', component: 'text', required: true },
{ id: 'name', label: 'Name', component: 'text', required: true },
{ id: 'quantity', label: 'Quantity', component: 'number', required: true, min: 0 },
{ id: 'location', label: 'Location', component: 'text' },
],
};
async function fetchItem(id: string) {
return readApiResultOrThrow(`/api/items/${id}`, undefined, { errorMessage: 'Inventory item not found' });
}
async function submit(id: string, data: UpsertInventoryItemInput) {
return readApiResultOrThrow(`/api/items/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
}, { errorMessage: 'Failed to update inventory item' });
}
type Props = { params: { id: string } };
const InventoryEditPage = async ({ params }: Props) => {
const item = await fetchItem(params.id);
return (
<CrudForm
schema={schema}
initialValues={item}
onSubmit={(data) => submit(params.id, data)}
redirectTo="/backend/inventory/list"
/>
);
};
export default InventoryEditPage;
apps/saasframe/src/modules/inventory/backend/inventory/[id]/edit/page.meta.ts
import type { PageMetadata } from '@saasframe/shared/modules/registry';
export const metadata: PageMetadata = {
title: 'Edit item',
group: 'Operations',
order: 22,
requireAuth: true,
requireFeatures: ['inventory.edit'],
};
3. Link actions from the grid
Update the list page to surface "Create" and "Edit" buttons -- either via DataTable row actions or links in your JSX. Users with the correct features can now manage inventory end to end.
After adding the new pages, regenerate:
yarn generate
yarn saasframe configs cache structural --all-tenants
That completes the tutorial sequence. You now have a fully working inventory module with:
- Module metadata, RBAC features, and default role assignments (
index.ts,acl.ts,setup.ts) - A MikroORM entity with multi-tenant scoping and soft deletes (
data/entities.ts) - Input validation with zod (
data/validators.ts) - A REST API powered by the CRUD factory (
api/items/route.ts) - A data table list page (
backend/inventory/list/page.tsx) - Create and edit forms (
backend/inventory/create/page.tsx,backend/inventory/[id]/edit/page.tsx)
Next steps
In the rest of the documentation you will dive deeper into the framework primitives that power these steps:
- Custom fields -- extend your entity with admin-configurable attributes
- CRUD Factory reference -- advanced filters, search integration, caching, and event emission
- DataTable reference -- row actions, bulk actions, export, and custom-field filters
- CrudForm reference -- field types, custom renderers, and validation
- Events and subscribers -- react to CRUD lifecycle events
- Widget injection -- inject UI into other modules without coupling