@mettlecast/domain-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +133 -0
- package/dist/builder/build-registry.d.ts +20 -0
- package/dist/builder/build-registry.js +178 -0
- package/dist/builder/build-types.d.ts +10 -0
- package/dist/builder/build-types.js +32 -0
- package/dist/builder/load-domain-module-config.d.ts +12 -0
- package/dist/builder/load-domain-module-config.js +34 -0
- package/dist/builder/load-module.d.ts +26 -0
- package/dist/builder/load-module.js +114 -0
- package/dist/builder/zod-to-json.d.ts +10 -0
- package/dist/builder/zod-to-json.js +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +338 -0
- package/dist/commands/add-api.d.ts +32 -0
- package/dist/commands/add-api.js +106 -0
- package/dist/commands/add-component.d.ts +15 -0
- package/dist/commands/add-component.js +78 -0
- package/dist/commands/add-domain.d.ts +21 -0
- package/dist/commands/add-domain.js +71 -0
- package/dist/commands/add-flow.d.ts +14 -0
- package/dist/commands/add-flow.js +108 -0
- package/dist/commands/add-hook.d.ts +15 -0
- package/dist/commands/add-hook.js +65 -0
- package/dist/commands/add-migration.d.ts +26 -0
- package/dist/commands/add-migration.js +93 -0
- package/dist/commands/add-module.d.ts +7 -0
- package/dist/commands/add-module.js +261 -0
- package/dist/commands/add-page.d.ts +13 -0
- package/dist/commands/add-page.js +170 -0
- package/dist/commands/add-seed-page.d.ts +5 -0
- package/dist/commands/add-seed-page.js +251 -0
- package/dist/commands/add-subscriber.d.ts +30 -0
- package/dist/commands/add-subscriber.js +87 -0
- package/dist/commands/add-test.d.ts +22 -0
- package/dist/commands/add-test.js +285 -0
- package/dist/commands/audit.d.ts +39 -0
- package/dist/commands/audit.js +171 -0
- package/dist/commands/build-catalog.d.ts +68 -0
- package/dist/commands/build-catalog.js +105 -0
- package/dist/commands/build-flows.d.ts +26 -0
- package/dist/commands/build-flows.js +174 -0
- package/dist/commands/build.d.ts +17 -0
- package/dist/commands/build.js +39 -0
- package/dist/commands/check-hashes.d.ts +38 -0
- package/dist/commands/check-hashes.js +97 -0
- package/dist/commands/create-project.d.ts +5 -0
- package/dist/commands/create-project.js +272 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +27 -0
- package/dist/commands/doctor.d.ts +48 -0
- package/dist/commands/doctor.js +1301 -0
- package/dist/commands/explain.d.ts +34 -0
- package/dist/commands/explain.js +167 -0
- package/dist/commands/power-tune.d.ts +1 -0
- package/dist/commands/power-tune.js +87 -0
- package/dist/commands/reseed-page.d.ts +6 -0
- package/dist/commands/reseed-page.js +297 -0
- package/dist/commands/show-dns.d.ts +15 -0
- package/dist/commands/show-dns.js +101 -0
- package/dist/commands/show.d.ts +46 -0
- package/dist/commands/show.js +135 -0
- package/dist/commands/test.d.ts +18 -0
- package/dist/commands/test.js +56 -0
- package/dist/commands/upgrade-backend.d.ts +9 -0
- package/dist/commands/upgrade-backend.js +46 -0
- package/dist/commands/upgrade-event.d.ts +22 -0
- package/dist/commands/upgrade-event.js +111 -0
- package/dist/commands/upgrade-frontend.d.ts +5 -0
- package/dist/commands/upgrade-frontend.js +99 -0
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +495 -0
- package/dist/commands/validate.d.ts +30 -0
- package/dist/commands/validate.js +148 -0
- package/dist/runtime-stubs/hydrate-local-ctx.d.ts +49 -0
- package/dist/runtime-stubs/hydrate-local-ctx.js +45 -0
- package/dist/runtime-stubs/load-secrets.d.ts +11 -0
- package/dist/runtime-stubs/load-secrets.js +37 -0
- package/dist/server/api-server.d.ts +21 -0
- package/dist/server/api-server.js +14 -0
- package/dist/server/mount-routes.d.ts +19 -0
- package/dist/server/mount-routes.js +42 -0
- package/dist/templates/api-skeleton.d.ts +18 -0
- package/dist/templates/api-skeleton.js +69 -0
- package/dist/templates/claude-md.d.ts +4 -0
- package/dist/templates/claude-md.js +7 -0
- package/dist/templates/dashboard-pages/index.d.ts +101 -0
- package/dist/templates/dashboard-pages/index.js +22 -0
- package/dist/templates/domain-config.d.ts +10 -0
- package/dist/templates/domain-config.js +19 -0
- package/dist/templates/events-skeleton.d.ts +9 -0
- package/dist/templates/events-skeleton.js +23 -0
- package/dist/templates/patterns/api/create-with-event.d.ts +5 -0
- package/dist/templates/patterns/api/create-with-event.js +69 -0
- package/dist/templates/patterns/api/idempotent-mutation.d.ts +5 -0
- package/dist/templates/patterns/api/idempotent-mutation.js +99 -0
- package/dist/templates/patterns/api/paginated-list.d.ts +5 -0
- package/dist/templates/patterns/api/paginated-list.js +71 -0
- package/dist/templates/patterns/api/simple-crud.d.ts +6 -0
- package/dist/templates/patterns/api/simple-crud.js +95 -0
- package/dist/templates/patterns/api/system-admin.d.ts +5 -0
- package/dist/templates/patterns/api/system-admin.js +98 -0
- package/dist/templates/patterns/api/webhook-receiver-style.d.ts +5 -0
- package/dist/templates/patterns/api/webhook-receiver-style.js +125 -0
- package/dist/templates/patterns/component/index.d.ts +25 -0
- package/dist/templates/patterns/component/index.js +814 -0
- package/dist/templates/patterns/flow/autonomous-remediation.d.ts +5 -0
- package/dist/templates/patterns/flow/autonomous-remediation.js +98 -0
- package/dist/templates/patterns/flow/fan-out-reaction.d.ts +5 -0
- package/dist/templates/patterns/flow/fan-out-reaction.js +53 -0
- package/dist/templates/patterns/flow/human-in-loop.d.ts +5 -0
- package/dist/templates/patterns/flow/human-in-loop.js +90 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.d.ts +5 -0
- package/dist/templates/patterns/flow/multi-step-orchestration.js +67 -0
- package/dist/templates/patterns/hook/index.d.ts +7 -0
- package/dist/templates/patterns/hook/index.js +378 -0
- package/dist/templates/patterns/migration/column-add.d.ts +5 -0
- package/dist/templates/patterns/migration/column-add.js +52 -0
- package/dist/templates/patterns/migration/event-store.d.ts +6 -0
- package/dist/templates/patterns/migration/event-store.js +97 -0
- package/dist/templates/patterns/migration/index.d.ts +5 -0
- package/dist/templates/patterns/migration/index.js +52 -0
- package/dist/templates/patterns/migration/rls-policy.d.ts +5 -0
- package/dist/templates/patterns/migration/rls-policy.js +78 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.d.ts +6 -0
- package/dist/templates/patterns/migration/tenant-scoped-table.js +81 -0
- package/dist/templates/patterns/page/dashboard.d.ts +4 -0
- package/dist/templates/patterns/page/dashboard.js +164 -0
- package/dist/templates/patterns/page/detail.d.ts +4 -0
- package/dist/templates/patterns/page/detail.js +162 -0
- package/dist/templates/patterns/page/form.d.ts +4 -0
- package/dist/templates/patterns/page/form.js +192 -0
- package/dist/templates/patterns/page/index.d.ts +8 -0
- package/dist/templates/patterns/page/index.js +14 -0
- package/dist/templates/patterns/page/list.d.ts +4 -0
- package/dist/templates/patterns/page/list.js +138 -0
- package/dist/templates/patterns/page/settings.d.ts +4 -0
- package/dist/templates/patterns/page/settings.js +318 -0
- package/dist/templates/patterns/page/wizard.d.ts +4 -0
- package/dist/templates/patterns/page/wizard.js +293 -0
- package/dist/templates/patterns/subscriber/audit-relay.d.ts +5 -0
- package/dist/templates/patterns/subscriber/audit-relay.js +51 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.d.ts +5 -0
- package/dist/templates/patterns/subscriber/cascade-deletion.js +60 -0
- package/dist/templates/patterns/subscriber/single-step-projection.d.ts +5 -0
- package/dist/templates/patterns/subscriber/single-step-projection.js +36 -0
- package/dist/templates/readme.d.ts +4 -0
- package/dist/templates/readme.js +7 -0
- package/dist/templates/subscriber-skeleton.d.ts +11 -0
- package/dist/templates/subscriber-skeleton.js +23 -0
- package/dist/utils/checksum.d.ts +18 -0
- package/dist/utils/checksum.js +39 -0
- package/dist/utils/file-helpers.d.ts +35 -0
- package/dist/utils/file-helpers.js +59 -0
- package/dist/utils/header-inject.d.ts +15 -0
- package/dist/utils/header-inject.js +93 -0
- package/dist/utils/install-file.d.ts +17 -0
- package/dist/utils/install-file.js +78 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/manifest.d.ts +28 -0
- package/dist/utils/manifest.js +93 -0
- package/dist/utils/s3-fetch.d.ts +25 -0
- package/dist/utils/s3-fetch.js +52 -0
- package/dist/utils/scaffold-config.d.ts +95 -0
- package/dist/utils/scaffold-config.js +53 -0
- package/dist/utils/token-substitution.d.ts +35 -0
- package/dist/utils/token-substitution.js +52 -0
- package/dist/utils/zod-extractor.d.ts +10 -0
- package/dist/utils/zod-extractor.js +28 -0
- package/package.json +43 -0
- package/src/__tests__/build-registry.test.ts +115 -0
- package/src/__tests__/build-types.test.ts +100 -0
- package/src/__tests__/builder/walkDomainDir.test.ts +140 -0
- package/src/__tests__/commands/add-domain.test.ts +174 -0
- package/src/__tests__/commands/add-flow.test.ts +69 -0
- package/src/__tests__/commands/add-module.test.ts +207 -0
- package/src/__tests__/commands/build-flows.test.ts +154 -0
- package/src/__tests__/commands/check-hashes.test.ts +142 -0
- package/src/__tests__/commands/create-project.test.ts +712 -0
- package/src/__tests__/commands/upgrade.test.ts +762 -0
- package/src/__tests__/doctor.test.ts +291 -0
- package/src/__tests__/hydrate-local-ctx.test.ts +71 -0
- package/src/__tests__/load-domain-module-config.test.ts +10 -0
- package/src/__tests__/mount-routes.test.ts +100 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +46 -0
- package/src/__tests__/scripts/package-scaffold.test.ts +45 -0
- package/src/__tests__/utils/checksum.test.ts +90 -0
- package/src/__tests__/utils/header-inject.test.ts +89 -0
- package/src/__tests__/utils/install-file.test.ts +207 -0
- package/src/__tests__/utils/manifest.test.ts +229 -0
- package/src/__tests__/utils/token-substitution.test.ts +134 -0
- package/src/__tests__/validate.test.ts +18 -0
- package/src/builder/build-registry.ts +237 -0
- package/src/builder/build-types.ts +36 -0
- package/src/builder/load-domain-module-config.ts +43 -0
- package/src/builder/load-module.ts +133 -0
- package/src/builder/zod-to-json.ts +16 -0
- package/src/cli.ts +387 -0
- package/src/commands/add-api.ts +145 -0
- package/src/commands/add-component.ts +116 -0
- package/src/commands/add-domain.ts +100 -0
- package/src/commands/add-flow.ts +136 -0
- package/src/commands/add-hook.ts +100 -0
- package/src/commands/add-migration.ts +129 -0
- package/src/commands/add-module.ts +347 -0
- package/src/commands/add-page.ts +242 -0
- package/src/commands/add-seed-page.ts +338 -0
- package/src/commands/add-subscriber.ts +123 -0
- package/src/commands/add-test.ts +328 -0
- package/src/commands/audit.ts +217 -0
- package/src/commands/build-catalog.ts +181 -0
- package/src/commands/build-flows.ts +198 -0
- package/src/commands/build.ts +59 -0
- package/src/commands/check-hashes.ts +150 -0
- package/src/commands/create-project.ts +367 -0
- package/src/commands/dev.ts +47 -0
- package/src/commands/doctor.ts +1362 -0
- package/src/commands/explain.ts +209 -0
- package/src/commands/power-tune.ts +112 -0
- package/src/commands/reseed-page.ts +410 -0
- package/src/commands/show-dns.ts +139 -0
- package/src/commands/show.ts +179 -0
- package/src/commands/test.ts +91 -0
- package/src/commands/upgrade-backend.ts +72 -0
- package/src/commands/upgrade-event.ts +175 -0
- package/src/commands/upgrade-frontend.ts +135 -0
- package/src/commands/upgrade.ts +668 -0
- package/src/commands/validate.ts +208 -0
- package/src/runtime-stubs/hydrate-local-ctx.ts +102 -0
- package/src/runtime-stubs/load-secrets.ts +40 -0
- package/src/server/api-server.ts +34 -0
- package/src/server/mount-routes.ts +68 -0
- package/src/templates/api-skeleton.ts +72 -0
- package/src/templates/claude-md.ts +7 -0
- package/src/templates/dashboard-pages/account/api-keys.tsx +107 -0
- package/src/templates/dashboard-pages/account/audit-log.tsx +60 -0
- package/src/templates/dashboard-pages/account/index.ts +5 -0
- package/src/templates/dashboard-pages/account/members.tsx +107 -0
- package/src/templates/dashboard-pages/account/profile.tsx +53 -0
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +64 -0
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +62 -0
- package/src/templates/dashboard-pages/auth/choose-org.tsx +66 -0
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +69 -0
- package/src/templates/dashboard-pages/auth/index.ts +10 -0
- package/src/templates/dashboard-pages/auth/login.tsx +75 -0
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +56 -0
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +49 -0
- package/src/templates/dashboard-pages/auth/reset-password.tsx +64 -0
- package/src/templates/dashboard-pages/auth/sign-out.tsx +20 -0
- package/src/templates/dashboard-pages/auth/signup.tsx +71 -0
- package/src/templates/dashboard-pages/auth/verify-email.tsx +53 -0
- package/src/templates/dashboard-pages/index.ts +22 -0
- package/src/templates/dashboard-pages/shell/dashboard-home.tsx +41 -0
- package/src/templates/dashboard-pages/shell/forbidden.tsx +46 -0
- package/src/templates/dashboard-pages/shell/index.ts +5 -0
- package/src/templates/dashboard-pages/shell/maintenance.tsx +27 -0
- package/src/templates/dashboard-pages/shell/not-found.tsx +34 -0
- package/src/templates/dashboard-pages/shell/server-error.tsx +44 -0
- package/src/templates/domain-config.ts +20 -0
- package/src/templates/events-skeleton.ts +24 -0
- package/src/templates/patterns/api/create-with-event.ts +72 -0
- package/src/templates/patterns/api/idempotent-mutation.ts +102 -0
- package/src/templates/patterns/api/paginated-list.ts +74 -0
- package/src/templates/patterns/api/simple-crud.ts +100 -0
- package/src/templates/patterns/api/system-admin.ts +101 -0
- package/src/templates/patterns/api/webhook-receiver-style.ts +128 -0
- package/src/templates/patterns/component/index.ts +833 -0
- package/src/templates/patterns/flow/autonomous-remediation.ts +101 -0
- package/src/templates/patterns/flow/fan-out-reaction.ts +56 -0
- package/src/templates/patterns/flow/human-in-loop.ts +93 -0
- package/src/templates/patterns/flow/multi-step-orchestration.ts +70 -0
- package/src/templates/patterns/hook/index.ts +382 -0
- package/src/templates/patterns/migration/column-add.ts +54 -0
- package/src/templates/patterns/migration/event-store.ts +99 -0
- package/src/templates/patterns/migration/index.ts +54 -0
- package/src/templates/patterns/migration/rls-policy.ts +80 -0
- package/src/templates/patterns/migration/tenant-scoped-table.ts +83 -0
- package/src/templates/patterns/page/dashboard.ts +164 -0
- package/src/templates/patterns/page/detail.ts +162 -0
- package/src/templates/patterns/page/form.ts +192 -0
- package/src/templates/patterns/page/index.ts +17 -0
- package/src/templates/patterns/page/list.ts +138 -0
- package/src/templates/patterns/page/settings.ts +318 -0
- package/src/templates/patterns/page/wizard.ts +293 -0
- package/src/templates/patterns/subscriber/audit-relay.ts +54 -0
- package/src/templates/patterns/subscriber/cascade-deletion.ts +63 -0
- package/src/templates/patterns/subscriber/single-step-projection.ts +39 -0
- package/src/templates/readme.ts +7 -0
- package/src/templates/subscriber-skeleton.ts +28 -0
- package/src/utils/checksum.ts +43 -0
- package/src/utils/file-helpers.ts +91 -0
- package/src/utils/header-inject.ts +126 -0
- package/src/utils/install-file.ts +99 -0
- package/src/utils/logger.ts +26 -0
- package/src/utils/manifest.ts +137 -0
- package/src/utils/s3-fetch.ts +91 -0
- package/src/utils/scaffold-config.ts +172 -0
- package/src/utils/token-substitution.ts +80 -0
- package/src/utils/zod-extractor.ts +31 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant-scoped-table migration pattern template.
|
|
3
|
+
* Produces: CREATE TABLE with tenancy trio (tenant_id, workspace_id, org_id),
|
|
4
|
+
* RLS policy, updated_at trigger, and standard indexes.
|
|
5
|
+
*/
|
|
6
|
+
export function tenantScopedTableTemplate(domain, name) {
|
|
7
|
+
const tableName = name.replace(/-/g, '_');
|
|
8
|
+
const timestamp = new Date().toISOString().replace(/[-:T.]/g, '').slice(0, 14);
|
|
9
|
+
return `-- Migration: ${timestamp}_${name}
|
|
10
|
+
-- Domain: ${domain}
|
|
11
|
+
-- Pattern: tenant-scoped-table
|
|
12
|
+
-- Creates a tenant-scoped table with RLS, audit columns, and updated_at trigger.
|
|
13
|
+
|
|
14
|
+
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
15
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
16
|
+
tenant_id UUID NOT NULL,
|
|
17
|
+
workspace_id UUID,
|
|
18
|
+
org_id UUID,
|
|
19
|
+
|
|
20
|
+
-- Domain-specific columns
|
|
21
|
+
name VARCHAR(255) NOT NULL,
|
|
22
|
+
status VARCHAR(50) NOT NULL DEFAULT 'active',
|
|
23
|
+
metadata JSONB DEFAULT '{}'::jsonb,
|
|
24
|
+
|
|
25
|
+
-- Audit columns
|
|
26
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
27
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
28
|
+
created_by UUID,
|
|
29
|
+
updated_by UUID,
|
|
30
|
+
deleted_at TIMESTAMPTZ
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- ── Indexes ──────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_tenant_id
|
|
36
|
+
ON ${tableName}(tenant_id);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_workspace_id
|
|
39
|
+
ON ${tableName}(workspace_id)
|
|
40
|
+
WHERE workspace_id IS NOT NULL;
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_status
|
|
43
|
+
ON ${tableName}(tenant_id, status);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_${tableName}_created_at
|
|
46
|
+
ON ${tableName}(created_at DESC);
|
|
47
|
+
|
|
48
|
+
-- ── Row-Level Security ───────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
ALTER TABLE ${tableName} ENABLE ROW LEVEL SECURITY;
|
|
51
|
+
|
|
52
|
+
CREATE POLICY ${tableName}_tenant_isolation
|
|
53
|
+
ON ${tableName}
|
|
54
|
+
FOR ALL
|
|
55
|
+
TO authenticated
|
|
56
|
+
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
|
57
|
+
|
|
58
|
+
-- ── Updated-at trigger ───────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
CREATE OR REPLACE FUNCTION update_${tableName}_updated_at()
|
|
61
|
+
RETURNS TRIGGER AS $$
|
|
62
|
+
BEGIN
|
|
63
|
+
NEW.updated_at = now();
|
|
64
|
+
RETURN NEW;
|
|
65
|
+
END;
|
|
66
|
+
$$ LANGUAGE plpgsql;
|
|
67
|
+
|
|
68
|
+
CREATE TRIGGER trg_${tableName}_updated_at
|
|
69
|
+
BEFORE UPDATE ON ${tableName}
|
|
70
|
+
FOR EACH ROW
|
|
71
|
+
EXECUTE FUNCTION update_${tableName}_updated_at();
|
|
72
|
+
|
|
73
|
+
-- ── Soft-delete policy ───────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
CREATE POLICY ${tableName}_soft_delete
|
|
76
|
+
ON ${tableName}
|
|
77
|
+
FOR SELECT
|
|
78
|
+
TO authenticated
|
|
79
|
+
USING (deleted_at IS NULL);
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard page pattern template — dashboard page with widget grid layout.
|
|
3
|
+
*/
|
|
4
|
+
export function dashboardPageTemplate(pageName) {
|
|
5
|
+
const componentName = `${pageName}Page`;
|
|
6
|
+
return `import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useTib } from '@/hooks/useTib';
|
|
8
|
+
import { Page } from '@/components/layout/Page';
|
|
9
|
+
import { PageHeader } from '@/components/layout/PageHeader';
|
|
10
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
11
|
+
import { ErrorState } from '@/components/state/ErrorState';
|
|
12
|
+
import { LoadingState } from '@/components/state/LoadingState';
|
|
13
|
+
import { EmptyState } from '@/components/state/EmptyState';
|
|
14
|
+
import { BarChart3, Activity, Users, Clock } from 'lucide-react';
|
|
15
|
+
|
|
16
|
+
interface ${componentName}Props {
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DashboardStats {
|
|
21
|
+
totalItems: number;
|
|
22
|
+
activeItems: number;
|
|
23
|
+
recentActivity: number;
|
|
24
|
+
avgCompletion: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface RecentItem {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
status: string;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ${componentName}({ workspaceId }: ${componentName}Props) {
|
|
35
|
+
const tib = useTib();
|
|
36
|
+
|
|
37
|
+
const { data: stats, isLoading: statsLoading, isError: statsError, refetch: refetchStats } = useQuery({
|
|
38
|
+
queryKey: ['dashboard-stats', workspaceId],
|
|
39
|
+
queryFn: () => tib.dashboard.get() as Promise<DashboardStats>,
|
|
40
|
+
staleTime: 30_000,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const { data: recent, isLoading: recentLoading } = useQuery({
|
|
44
|
+
queryKey: ['dashboard-recent', workspaceId],
|
|
45
|
+
queryFn: () => tib.data.list({ workspaceId, limit: 5 }) as Promise<{ items: RecentItem[] }>,
|
|
46
|
+
staleTime: 30_000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const isLoading = statsLoading || recentLoading;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Page title="Dashboard" description="Overview of your workspace">
|
|
53
|
+
<Page.Header
|
|
54
|
+
breadcrumb={[{ label: '${pageName}' }]}
|
|
55
|
+
/>
|
|
56
|
+
<Page.Body>
|
|
57
|
+
{statsError ? (
|
|
58
|
+
<ErrorState
|
|
59
|
+
message="Failed to load dashboard data"
|
|
60
|
+
onRetry={() => refetchStats()}
|
|
61
|
+
/>
|
|
62
|
+
) : isLoading ? (
|
|
63
|
+
<LoadingState variant="page" />
|
|
64
|
+
) : !stats ? (
|
|
65
|
+
<EmptyState
|
|
66
|
+
icon={BarChart3}
|
|
67
|
+
title="No data yet"
|
|
68
|
+
description="Start creating items to see your dashboard."
|
|
69
|
+
/>
|
|
70
|
+
) : (
|
|
71
|
+
<div className="space-y-6">
|
|
72
|
+
{/* Stat cards grid */}
|
|
73
|
+
<div className="layout-cards">
|
|
74
|
+
<Card>
|
|
75
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
76
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
77
|
+
Total Items
|
|
78
|
+
</CardTitle>
|
|
79
|
+
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
|
80
|
+
</CardHeader>
|
|
81
|
+
<CardContent>
|
|
82
|
+
<p className="text-2xl font-semibold">{stats.totalItems}</p>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
|
|
86
|
+
<Card>
|
|
87
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
88
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
89
|
+
Active
|
|
90
|
+
</CardTitle>
|
|
91
|
+
<Activity className="h-4 w-4 text-muted-foreground" />
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent>
|
|
94
|
+
<p className="text-2xl font-semibold">{stats.activeItems}</p>
|
|
95
|
+
</CardContent>
|
|
96
|
+
</Card>
|
|
97
|
+
|
|
98
|
+
<Card>
|
|
99
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
100
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
101
|
+
Recent Activity
|
|
102
|
+
</CardTitle>
|
|
103
|
+
<Users className="h-4 w-4 text-muted-foreground" />
|
|
104
|
+
</CardHeader>
|
|
105
|
+
<CardContent>
|
|
106
|
+
<p className="text-2xl font-semibold">{stats.recentActivity}</p>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
|
|
110
|
+
<Card>
|
|
111
|
+
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
112
|
+
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
113
|
+
Avg Completion
|
|
114
|
+
</CardTitle>
|
|
115
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
116
|
+
</CardHeader>
|
|
117
|
+
<CardContent>
|
|
118
|
+
<p className="text-2xl font-semibold">{stats.avgCompletion}</p>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Recent items */}
|
|
124
|
+
<Card>
|
|
125
|
+
<CardHeader>
|
|
126
|
+
<CardTitle className="text-lg font-semibold">Recent Items</CardTitle>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardContent>
|
|
129
|
+
{!recent?.items?.length ? (
|
|
130
|
+
<EmptyState
|
|
131
|
+
icon={BarChart3}
|
|
132
|
+
title="No recent items"
|
|
133
|
+
description="Items you create will appear here."
|
|
134
|
+
/>
|
|
135
|
+
) : (
|
|
136
|
+
<div className="space-y-3">
|
|
137
|
+
{recent.items.map((item) => (
|
|
138
|
+
<div
|
|
139
|
+
key={item.id}
|
|
140
|
+
className="flex items-center justify-between rounded-lg border border-border p-3"
|
|
141
|
+
>
|
|
142
|
+
<div>
|
|
143
|
+
<p className="text-sm font-medium">{item.name}</p>
|
|
144
|
+
<p className="text-xs text-muted-foreground">
|
|
145
|
+
Updated {new Date(item.updatedAt).toLocaleDateString()}
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
<span className="text-xs text-muted-foreground">{item.status}</span>
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</CardContent>
|
|
154
|
+
</Card>
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</Page.Body>
|
|
158
|
+
</Page>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export default ${componentName};
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail page pattern template — single entity detail page with tabs for related data.
|
|
3
|
+
*/
|
|
4
|
+
export function detailPageTemplate(pageName) {
|
|
5
|
+
const componentName = `${pageName}Page`;
|
|
6
|
+
return `import { useSearchParams } from 'react-router-dom';
|
|
7
|
+
import { ArrowLeft, Pencil, Trash2 } from 'lucide-react';
|
|
8
|
+
import { useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useTib } from '@/hooks/useTib';
|
|
10
|
+
import { Page } from '@/components/layout/Page';
|
|
11
|
+
import { PageHeader } from '@/components/layout/PageHeader';
|
|
12
|
+
import { Button } from '@/components/ui/button';
|
|
13
|
+
import { Badge } from '@/components/ui/badge';
|
|
14
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
15
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
16
|
+
import { ErrorState } from '@/components/state/ErrorState';
|
|
17
|
+
import { LoadingState } from '@/components/state/LoadingState';
|
|
18
|
+
import { EmptyState } from '@/components/state/EmptyState';
|
|
19
|
+
import { useNavigate } from 'react-router-dom';
|
|
20
|
+
|
|
21
|
+
interface ${componentName}Props {
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
entityId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface EntityDetail {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
status: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ${componentName}({ workspaceId, entityId }: ${componentName}Props) {
|
|
36
|
+
const tib = useTib();
|
|
37
|
+
const navigate = useNavigate();
|
|
38
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
39
|
+
const activeTab = searchParams.get('tab') ?? 'overview';
|
|
40
|
+
|
|
41
|
+
const { data, isLoading, isError, error, refetch } = useQuery({
|
|
42
|
+
queryKey: ['entity-detail', workspaceId, entityId],
|
|
43
|
+
queryFn: () => tib.data.get({ workspaceId, id: entityId }) as Promise<EntityDetail>,
|
|
44
|
+
staleTime: 10_000,
|
|
45
|
+
enabled: !!entityId,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const setTab = (tab: string) => setSearchParams({ tab });
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Page title={data?.name ?? 'Detail'} description={data?.description ?? 'View entity details'}>
|
|
52
|
+
<Page.Header
|
|
53
|
+
breadcrumb={[
|
|
54
|
+
{ label: '${pageName}', to: '..' },
|
|
55
|
+
{ label: data?.name ?? entityId },
|
|
56
|
+
]}
|
|
57
|
+
status={data && (
|
|
58
|
+
<Badge variant={data.status === 'active' ? 'success' : 'neutral'}>
|
|
59
|
+
{data.status}
|
|
60
|
+
</Badge>
|
|
61
|
+
)}
|
|
62
|
+
actions={
|
|
63
|
+
<>
|
|
64
|
+
<Button variant="outline" size="sm" onClick={() => navigate(-1)}>
|
|
65
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
66
|
+
Back
|
|
67
|
+
</Button>
|
|
68
|
+
<Button variant="outline" size="sm">
|
|
69
|
+
<Pencil className="h-4 w-4 mr-1" />
|
|
70
|
+
Edit
|
|
71
|
+
</Button>
|
|
72
|
+
<Button variant="destructive" size="sm">
|
|
73
|
+
<Trash2 className="h-4 w-4 mr-1" />
|
|
74
|
+
Delete
|
|
75
|
+
</Button>
|
|
76
|
+
</>
|
|
77
|
+
}
|
|
78
|
+
/>
|
|
79
|
+
<Page.Tabs items={[
|
|
80
|
+
{ value: 'overview', label: 'Overview', to: 'overview' },
|
|
81
|
+
{ value: 'related', label: 'Related', to: 'related' },
|
|
82
|
+
{ value: 'activity', label: 'Activity', to: 'activity' },
|
|
83
|
+
]} />
|
|
84
|
+
<Page.Body>
|
|
85
|
+
{isLoading ? (
|
|
86
|
+
<LoadingState variant="page" />
|
|
87
|
+
) : isError ? (
|
|
88
|
+
<ErrorState
|
|
89
|
+
message={error instanceof Error ? error.message : 'Failed to load entity'}
|
|
90
|
+
onRetry={() => refetch()}
|
|
91
|
+
/>
|
|
92
|
+
) : !data ? (
|
|
93
|
+
<EmptyState
|
|
94
|
+
icon={Pencil}
|
|
95
|
+
title="Entity not found"
|
|
96
|
+
description="The requested entity could not be found."
|
|
97
|
+
/>
|
|
98
|
+
) : (
|
|
99
|
+
<Tabs value={activeTab} onValueChange={setTab}>
|
|
100
|
+
<TabsContent value="overview">
|
|
101
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
102
|
+
<Card>
|
|
103
|
+
<CardHeader>
|
|
104
|
+
<CardTitle className="text-lg font-semibold">Details</CardTitle>
|
|
105
|
+
</CardHeader>
|
|
106
|
+
<CardContent className="space-y-3">
|
|
107
|
+
<div>
|
|
108
|
+
<p className="text-sm text-muted-foreground">Name</p>
|
|
109
|
+
<p className="text-sm font-medium">{data.name}</p>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<p className="text-sm text-muted-foreground">Status</p>
|
|
113
|
+
<Badge variant={data.status === 'active' ? 'success' : 'neutral'}>
|
|
114
|
+
{data.status}
|
|
115
|
+
</Badge>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<p className="text-sm text-muted-foreground">Created</p>
|
|
119
|
+
<p className="text-sm">{new Date(data.createdAt).toLocaleDateString()}</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<p className="text-sm text-muted-foreground">Updated</p>
|
|
123
|
+
<p className="text-sm">{new Date(data.updatedAt).toLocaleDateString()}</p>
|
|
124
|
+
</div>
|
|
125
|
+
</CardContent>
|
|
126
|
+
</Card>
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader>
|
|
129
|
+
<CardTitle className="text-lg font-semibold">Description</CardTitle>
|
|
130
|
+
</CardHeader>
|
|
131
|
+
<CardContent>
|
|
132
|
+
<p className="text-sm text-muted-foreground">
|
|
133
|
+
{data.description ?? 'No description provided.'}
|
|
134
|
+
</p>
|
|
135
|
+
</CardContent>
|
|
136
|
+
</Card>
|
|
137
|
+
</div>
|
|
138
|
+
</TabsContent>
|
|
139
|
+
<TabsContent value="related">
|
|
140
|
+
<EmptyState
|
|
141
|
+
icon={Pencil}
|
|
142
|
+
title="No related data"
|
|
143
|
+
description="Related items will appear here."
|
|
144
|
+
/>
|
|
145
|
+
</TabsContent>
|
|
146
|
+
<TabsContent value="activity">
|
|
147
|
+
<EmptyState
|
|
148
|
+
icon={Pencil}
|
|
149
|
+
title="No recent activity"
|
|
150
|
+
description="Activity for this entity will appear here."
|
|
151
|
+
/>
|
|
152
|
+
</TabsContent>
|
|
153
|
+
</Tabs>
|
|
154
|
+
)}
|
|
155
|
+
</Page.Body>
|
|
156
|
+
</Page>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default ${componentName};
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form page pattern template — create/edit form with Zod validation.
|
|
3
|
+
*/
|
|
4
|
+
export function formPageTemplate(pageName) {
|
|
5
|
+
const componentName = `${pageName}Page`;
|
|
6
|
+
return `import { useForm } from 'react-hook-form';
|
|
7
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
import { ArrowLeft, Save } from 'lucide-react';
|
|
11
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
12
|
+
import { useTib } from '@/hooks/useTib';
|
|
13
|
+
import { Page } from '@/components/layout/Page';
|
|
14
|
+
import { PageHeader } from '@/components/layout/PageHeader';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
16
|
+
import { Input } from '@/components/ui/input';
|
|
17
|
+
import { Label } from '@/components/ui/label';
|
|
18
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
19
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
20
|
+
import { ErrorState } from '@/components/state/ErrorState';
|
|
21
|
+
import { LoadingState } from '@/components/state/LoadingState';
|
|
22
|
+
import { toast } from 'sonner';
|
|
23
|
+
|
|
24
|
+
const formSchema = z.object({
|
|
25
|
+
name: z.string().min(1, 'Name is required').max(100, 'Name must be 100 characters or less'),
|
|
26
|
+
description: z.string().max(500).optional(),
|
|
27
|
+
status: z.enum(['active', 'inactive']),
|
|
28
|
+
type: z.string().min(1, 'Type is required'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
type FormValues = z.infer<typeof formSchema>;
|
|
32
|
+
|
|
33
|
+
interface ${componentName}Props {
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
entityId?: string; // undefined = create mode, defined = edit mode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ${componentName}({ workspaceId, entityId }: ${componentName}Props) {
|
|
39
|
+
const tib = useTib();
|
|
40
|
+
const navigate = useNavigate();
|
|
41
|
+
const queryClient = useQueryClient();
|
|
42
|
+
const isEdit = !!entityId;
|
|
43
|
+
|
|
44
|
+
const { data: existing, isLoading: isLoadingExisting } = useQuery({
|
|
45
|
+
queryKey: ['entity-detail', workspaceId, entityId],
|
|
46
|
+
queryFn: () => tib.data.get({ workspaceId, id: entityId! }) as Promise<FormValues>,
|
|
47
|
+
enabled: isEdit,
|
|
48
|
+
staleTime: 30_000,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const {
|
|
52
|
+
register,
|
|
53
|
+
handleSubmit,
|
|
54
|
+
formState: { errors, isSubmitting },
|
|
55
|
+
setValue,
|
|
56
|
+
watch,
|
|
57
|
+
} = useForm<FormValues>({
|
|
58
|
+
resolver: zodResolver(formSchema),
|
|
59
|
+
defaultValues: {
|
|
60
|
+
name: '',
|
|
61
|
+
description: '',
|
|
62
|
+
status: 'active',
|
|
63
|
+
type: '',
|
|
64
|
+
},
|
|
65
|
+
values: existing,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const mutation = useMutation({
|
|
69
|
+
mutationFn: (values: FormValues) =>
|
|
70
|
+
isEdit
|
|
71
|
+
? tib.data.update({ workspaceId, id: entityId!, ...values })
|
|
72
|
+
: tib.data.create({ workspaceId, ...values }),
|
|
73
|
+
onSuccess: () => {
|
|
74
|
+
toast.success(isEdit ? '\${pageName} updated' : '\${pageName} created');
|
|
75
|
+
queryClient.invalidateQueries({ queryKey: ['data-list', workspaceId] });
|
|
76
|
+
navigate('..');
|
|
77
|
+
},
|
|
78
|
+
onError: (err) => {
|
|
79
|
+
toast.error('Failed to save', {
|
|
80
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const onSubmit = (values: FormValues) => mutation.mutate(values);
|
|
86
|
+
|
|
87
|
+
if (isEdit && isLoadingExisting) {
|
|
88
|
+
return <LoadingState variant="page" />;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Page title={isEdit ? 'Edit ${pageName}' : 'Create ${pageName}'} description={isEdit ? 'Update existing entity' : 'Add a new entity'}>
|
|
93
|
+
<Page.Header
|
|
94
|
+
breadcrumb={[
|
|
95
|
+
{ label: '${pageName}', to: '..' },
|
|
96
|
+
{ label: isEdit ? 'Edit' : 'Create' },
|
|
97
|
+
]}
|
|
98
|
+
actions={
|
|
99
|
+
<Button variant="outline" size="sm" onClick={() => navigate(-1)}>
|
|
100
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
101
|
+
Back
|
|
102
|
+
</Button>
|
|
103
|
+
}
|
|
104
|
+
/>
|
|
105
|
+
<Page.Body>
|
|
106
|
+
<form onSubmit={handleSubmit(onSubmit)} className="max-w-2xl space-y-6">
|
|
107
|
+
<div className="space-y-2">
|
|
108
|
+
<Label htmlFor="name">Name</Label>
|
|
109
|
+
<Input
|
|
110
|
+
id="name"
|
|
111
|
+
{...register('name')}
|
|
112
|
+
placeholder="Enter name"
|
|
113
|
+
aria-invalid={!!errors.name}
|
|
114
|
+
aria-describedby={errors.name ? 'name-error' : undefined}
|
|
115
|
+
/>
|
|
116
|
+
{errors.name && (
|
|
117
|
+
<p id="name-error" className="text-xs text-destructive">{errors.name.message}</p>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="space-y-2">
|
|
122
|
+
<Label htmlFor="description">Description</Label>
|
|
123
|
+
<Textarea
|
|
124
|
+
id="description"
|
|
125
|
+
{...register('description')}
|
|
126
|
+
placeholder="Optional description"
|
|
127
|
+
rows={3}
|
|
128
|
+
/>
|
|
129
|
+
{errors.description && (
|
|
130
|
+
<p className="text-xs text-destructive">{errors.description.message}</p>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="space-y-2">
|
|
135
|
+
<Label htmlFor="type">Type</Label>
|
|
136
|
+
<Input
|
|
137
|
+
id="type"
|
|
138
|
+
{...register('type')}
|
|
139
|
+
placeholder="Enter type"
|
|
140
|
+
aria-invalid={!!errors.type}
|
|
141
|
+
aria-describedby={errors.type ? 'type-error' : undefined}
|
|
142
|
+
/>
|
|
143
|
+
{errors.type && (
|
|
144
|
+
<p id="type-error" className="text-xs text-destructive">{errors.type.message}</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<Label htmlFor="status">Status</Label>
|
|
150
|
+
<Select
|
|
151
|
+
value={watch('status')}
|
|
152
|
+
onValueChange={(value) => setValue('status', value as 'active' | 'inactive')}
|
|
153
|
+
>
|
|
154
|
+
<SelectTrigger id="status">
|
|
155
|
+
<SelectValue placeholder="Select status" />
|
|
156
|
+
</SelectTrigger>
|
|
157
|
+
<SelectContent>
|
|
158
|
+
<SelectItem value="active">Active</SelectItem>
|
|
159
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
160
|
+
</SelectContent>
|
|
161
|
+
</Select>
|
|
162
|
+
{errors.status && (
|
|
163
|
+
<p className="text-xs text-destructive">{errors.status.message}</p>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div className="flex items-center gap-3 pt-4">
|
|
168
|
+
<Button
|
|
169
|
+
type="submit"
|
|
170
|
+
variant="teal"
|
|
171
|
+
disabled={isSubmitting || mutation.isPending}
|
|
172
|
+
>
|
|
173
|
+
<Save className="h-4 w-4 mr-1" />
|
|
174
|
+
{mutation.isPending ? 'Saving...' : isEdit ? 'Update' : 'Create'}
|
|
175
|
+
</Button>
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
onClick={() => navigate(-1)}
|
|
180
|
+
>
|
|
181
|
+
Cancel
|
|
182
|
+
</Button>
|
|
183
|
+
</div>
|
|
184
|
+
</form>
|
|
185
|
+
</Page.Body>
|
|
186
|
+
</Page>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default ${componentName};
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { listPageTemplate } from './list.js';
|
|
2
|
+
export { detailPageTemplate } from './detail.js';
|
|
3
|
+
export { formPageTemplate } from './form.js';
|
|
4
|
+
export { wizardPageTemplate } from './wizard.js';
|
|
5
|
+
export { dashboardPageTemplate } from './dashboard.js';
|
|
6
|
+
export { settingsPageTemplate } from './settings.js';
|
|
7
|
+
export type PagePattern = 'list' | 'detail' | 'form' | 'wizard' | 'dashboard' | 'settings';
|
|
8
|
+
export declare const PAGE_PATTERNS: readonly PagePattern[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { listPageTemplate } from './list.js';
|
|
2
|
+
export { detailPageTemplate } from './detail.js';
|
|
3
|
+
export { formPageTemplate } from './form.js';
|
|
4
|
+
export { wizardPageTemplate } from './wizard.js';
|
|
5
|
+
export { dashboardPageTemplate } from './dashboard.js';
|
|
6
|
+
export { settingsPageTemplate } from './settings.js';
|
|
7
|
+
export const PAGE_PATTERNS = [
|
|
8
|
+
'list',
|
|
9
|
+
'detail',
|
|
10
|
+
'form',
|
|
11
|
+
'wizard',
|
|
12
|
+
'dashboard',
|
|
13
|
+
'settings',
|
|
14
|
+
];
|