@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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings page pattern template — settings page with tabs (profile, security, notifications).
|
|
3
|
+
*/
|
|
4
|
+
export function settingsPageTemplate(pageName: string): string {
|
|
5
|
+
const componentName = `${pageName}Page`;
|
|
6
|
+
return `import { useState, useCallback } from 'react';
|
|
7
|
+
import { useSearchParams } from 'react-router-dom';
|
|
8
|
+
import { useForm } from 'react-hook-form';
|
|
9
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { Save, User, Shield, Bell } from 'lucide-react';
|
|
12
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
13
|
+
import { useTib } from '@/hooks/useTib';
|
|
14
|
+
import { Page } from '@/components/layout/Page';
|
|
15
|
+
import { PageHeader } from '@/components/layout/PageHeader';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { Input } from '@/components/ui/input';
|
|
18
|
+
import { Label } from '@/components/ui/label';
|
|
19
|
+
import { Switch } from '@/components/ui/switch';
|
|
20
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
21
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
22
|
+
import { ErrorState } from '@/components/state/ErrorState';
|
|
23
|
+
import { LoadingState } from '@/components/state/LoadingState';
|
|
24
|
+
import { toast } from 'sonner';
|
|
25
|
+
|
|
26
|
+
const profileSchema = z.object({
|
|
27
|
+
displayName: z.string().min(1, 'Display name is required'),
|
|
28
|
+
email: z.string().email('Invalid email'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const securitySchema = z.object({
|
|
32
|
+
mfaEnabled: z.boolean(),
|
|
33
|
+
sessionTimeout: z.string(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const notificationsSchema = z.object({
|
|
37
|
+
emailAlerts: z.boolean(),
|
|
38
|
+
pushNotifications: z.boolean(),
|
|
39
|
+
weeklyDigest: z.boolean(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
type ProfileValues = z.infer<typeof profileSchema>;
|
|
43
|
+
type SecurityValues = z.infer<typeof securitySchema>;
|
|
44
|
+
type NotificationsValues = z.infer<typeof notificationsSchema>;
|
|
45
|
+
|
|
46
|
+
interface ${componentName}Props {
|
|
47
|
+
workspaceId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function ${componentName}({ workspaceId }: ${componentName}Props) {
|
|
51
|
+
const tib = useTib();
|
|
52
|
+
const queryClient = useQueryClient();
|
|
53
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
54
|
+
const activeTab = searchParams.get('tab') ?? 'profile';
|
|
55
|
+
|
|
56
|
+
const { data: settings, isLoading, isError, refetch } = useQuery({
|
|
57
|
+
queryKey: ['settings', workspaceId],
|
|
58
|
+
queryFn: () => tib.settings.getProject({ projectShortcode: workspaceId }) as Promise<{
|
|
59
|
+
displayName: string;
|
|
60
|
+
email: string;
|
|
61
|
+
mfaEnabled: boolean;
|
|
62
|
+
sessionTimeout: string;
|
|
63
|
+
emailAlerts: boolean;
|
|
64
|
+
pushNotifications: boolean;
|
|
65
|
+
weeklyDigest: boolean;
|
|
66
|
+
}>,
|
|
67
|
+
staleTime: 60_000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const updateMutation = useMutation({
|
|
71
|
+
mutationFn: (values: Record<string, unknown>) =>
|
|
72
|
+
tib.settings.updateProject({ projectShortcode: workspaceId, ...values }),
|
|
73
|
+
onSuccess: () => {
|
|
74
|
+
toast.success('Settings saved');
|
|
75
|
+
queryClient.invalidateQueries({ queryKey: ['settings', workspaceId] });
|
|
76
|
+
},
|
|
77
|
+
onError: (err) => {
|
|
78
|
+
toast.error('Failed to save settings', {
|
|
79
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const setTab = (tab: string) => setSearchParams({ tab });
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Page title="Settings" description="Manage your account and preferences">
|
|
88
|
+
<Page.Header
|
|
89
|
+
breadcrumb={[{ label: '${pageName}' }]}
|
|
90
|
+
/>
|
|
91
|
+
<Page.Tabs items={[
|
|
92
|
+
{ value: 'profile', label: 'Profile', to: 'profile' },
|
|
93
|
+
{ value: 'security', label: 'Security', to: 'security' },
|
|
94
|
+
{ value: 'notifications', label: 'Notifications', to: 'notifications' },
|
|
95
|
+
]} />
|
|
96
|
+
<Page.Body>
|
|
97
|
+
{isLoading ? (
|
|
98
|
+
<LoadingState variant="page" />
|
|
99
|
+
) : isError ? (
|
|
100
|
+
<ErrorState
|
|
101
|
+
message="Failed to load settings"
|
|
102
|
+
onRetry={() => refetch()}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<Tabs value={activeTab} onValueChange={setTab}>
|
|
106
|
+
<TabsContent value="profile">
|
|
107
|
+
<SettingsProfileForm
|
|
108
|
+
defaultValues={settings}
|
|
109
|
+
onSave={(values) => updateMutation.mutate(values)}
|
|
110
|
+
isSaving={updateMutation.isPending}
|
|
111
|
+
/>
|
|
112
|
+
</TabsContent>
|
|
113
|
+
<TabsContent value="security">
|
|
114
|
+
<SettingsSecurityForm
|
|
115
|
+
defaultValues={settings}
|
|
116
|
+
onSave={(values) => updateMutation.mutate(values)}
|
|
117
|
+
isSaving={updateMutation.isPending}
|
|
118
|
+
/>
|
|
119
|
+
</TabsContent>
|
|
120
|
+
<TabsContent value="notifications">
|
|
121
|
+
<SettingsNotificationsForm
|
|
122
|
+
defaultValues={settings}
|
|
123
|
+
onSave={(values) => updateMutation.mutate(values)}
|
|
124
|
+
isSaving={updateMutation.isPending}
|
|
125
|
+
/>
|
|
126
|
+
</TabsContent>
|
|
127
|
+
</Tabs>
|
|
128
|
+
)}
|
|
129
|
+
</Page.Body>
|
|
130
|
+
</Page>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function SettingsProfileForm({
|
|
135
|
+
defaultValues,
|
|
136
|
+
onSave,
|
|
137
|
+
isSaving,
|
|
138
|
+
}: {
|
|
139
|
+
defaultValues?: ProfileValues;
|
|
140
|
+
onSave: (values: ProfileValues) => void;
|
|
141
|
+
isSaving: boolean;
|
|
142
|
+
}) {
|
|
143
|
+
const { register, handleSubmit, formState: { errors } } = useForm<ProfileValues>({
|
|
144
|
+
resolver: zodResolver(profileSchema),
|
|
145
|
+
defaultValues: defaultValues ?? { displayName: '', email: '' },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<form onSubmit={handleSubmit(onSave)} className="max-w-xl space-y-6">
|
|
150
|
+
<Card>
|
|
151
|
+
<CardHeader>
|
|
152
|
+
<CardTitle className="flex items-center gap-2">
|
|
153
|
+
<User className="h-5 w-5" />
|
|
154
|
+
Profile
|
|
155
|
+
</CardTitle>
|
|
156
|
+
<CardDescription>Update your profile information.</CardDescription>
|
|
157
|
+
</CardHeader>
|
|
158
|
+
<CardContent className="space-y-4">
|
|
159
|
+
<div className="space-y-2">
|
|
160
|
+
<Label htmlFor="displayName">Display Name</Label>
|
|
161
|
+
<Input
|
|
162
|
+
id="displayName"
|
|
163
|
+
{...register('displayName')}
|
|
164
|
+
aria-invalid={!!errors.displayName}
|
|
165
|
+
/>
|
|
166
|
+
{errors.displayName && (
|
|
167
|
+
<p className="text-xs text-destructive">{errors.displayName.message}</p>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
<div className="space-y-2">
|
|
171
|
+
<Label htmlFor="email">Email</Label>
|
|
172
|
+
<Input
|
|
173
|
+
id="email"
|
|
174
|
+
type="email"
|
|
175
|
+
{...register('email')}
|
|
176
|
+
aria-invalid={!!errors.email}
|
|
177
|
+
/>
|
|
178
|
+
{errors.email && (
|
|
179
|
+
<p className="text-xs text-destructive">{errors.email.message}</p>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
<Button type="submit" variant="teal" disabled={isSaving}>
|
|
183
|
+
<Save className="h-4 w-4 mr-1" />
|
|
184
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
185
|
+
</Button>
|
|
186
|
+
</CardContent>
|
|
187
|
+
</Card>
|
|
188
|
+
</form>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function SettingsSecurityForm({
|
|
193
|
+
defaultValues,
|
|
194
|
+
onSave,
|
|
195
|
+
isSaving,
|
|
196
|
+
}: {
|
|
197
|
+
defaultValues?: SecurityValues;
|
|
198
|
+
onSave: (values: SecurityValues) => void;
|
|
199
|
+
isSaving: boolean;
|
|
200
|
+
}) {
|
|
201
|
+
const { register, handleSubmit, setValue, watch } = useForm<SecurityValues>({
|
|
202
|
+
defaultValues: defaultValues ?? { mfaEnabled: false, sessionTimeout: '3600' },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<form onSubmit={handleSubmit(onSave)} className="max-w-xl space-y-6">
|
|
207
|
+
<Card>
|
|
208
|
+
<CardHeader>
|
|
209
|
+
<CardTitle className="flex items-center gap-2">
|
|
210
|
+
<Shield className="h-5 w-5" />
|
|
211
|
+
Security
|
|
212
|
+
</CardTitle>
|
|
213
|
+
<CardDescription>Manage your security settings.</CardDescription>
|
|
214
|
+
</CardHeader>
|
|
215
|
+
<CardContent className="space-y-4">
|
|
216
|
+
<div className="flex items-center justify-between">
|
|
217
|
+
<div>
|
|
218
|
+
<Label htmlFor="mfaEnabled">Two-Factor Authentication</Label>
|
|
219
|
+
<p className="text-xs text-muted-foreground">Require MFA for login.</p>
|
|
220
|
+
</div>
|
|
221
|
+
<Switch
|
|
222
|
+
id="mfaEnabled"
|
|
223
|
+
checked={watch('mfaEnabled')}
|
|
224
|
+
onCheckedChange={(checked) => setValue('mfaEnabled', checked)}
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
<div className="space-y-2">
|
|
228
|
+
<Label htmlFor="sessionTimeout">Session Timeout (seconds)</Label>
|
|
229
|
+
<Input
|
|
230
|
+
id="sessionTimeout"
|
|
231
|
+
{...register('sessionTimeout')}
|
|
232
|
+
type="number"
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
<Button type="submit" variant="teal" disabled={isSaving}>
|
|
236
|
+
<Save className="h-4 w-4 mr-1" />
|
|
237
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
238
|
+
</Button>
|
|
239
|
+
</CardContent>
|
|
240
|
+
</Card>
|
|
241
|
+
</form>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function SettingsNotificationsForm({
|
|
246
|
+
defaultValues,
|
|
247
|
+
onSave,
|
|
248
|
+
isSaving,
|
|
249
|
+
}: {
|
|
250
|
+
defaultValues?: NotificationsValues;
|
|
251
|
+
onSave: (values: NotificationsValues) => void;
|
|
252
|
+
isSaving: boolean;
|
|
253
|
+
}) {
|
|
254
|
+
const { handleSubmit, setValue, watch } = useForm<NotificationsValues>({
|
|
255
|
+
defaultValues: defaultValues ?? {
|
|
256
|
+
emailAlerts: true,
|
|
257
|
+
pushNotifications: false,
|
|
258
|
+
weeklyDigest: true,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<form onSubmit={handleSubmit(onSave)} className="max-w-xl space-y-6">
|
|
264
|
+
<Card>
|
|
265
|
+
<CardHeader>
|
|
266
|
+
<CardTitle className="flex items-center gap-2">
|
|
267
|
+
<Bell className="h-5 w-5" />
|
|
268
|
+
Notifications
|
|
269
|
+
</CardTitle>
|
|
270
|
+
<CardDescription>Configure how you receive notifications.</CardDescription>
|
|
271
|
+
</CardHeader>
|
|
272
|
+
<CardContent className="space-y-4">
|
|
273
|
+
<div className="flex items-center justify-between">
|
|
274
|
+
<div>
|
|
275
|
+
<Label htmlFor="emailAlerts">Email Alerts</Label>
|
|
276
|
+
<p className="text-xs text-muted-foreground">Receive email notifications.</p>
|
|
277
|
+
</div>
|
|
278
|
+
<Switch
|
|
279
|
+
id="emailAlerts"
|
|
280
|
+
checked={watch('emailAlerts')}
|
|
281
|
+
onCheckedChange={(checked) => setValue('emailAlerts', checked)}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
<div className="flex items-center justify-between">
|
|
285
|
+
<div>
|
|
286
|
+
<Label htmlFor="pushNotifications">Push Notifications</Label>
|
|
287
|
+
<p className="text-xs text-muted-foreground">Receive push notifications.</p>
|
|
288
|
+
</div>
|
|
289
|
+
<Switch
|
|
290
|
+
id="pushNotifications"
|
|
291
|
+
checked={watch('pushNotifications')}
|
|
292
|
+
onCheckedChange={(checked) => setValue('pushNotifications', checked)}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex items-center justify-between">
|
|
296
|
+
<div>
|
|
297
|
+
<Label htmlFor="weeklyDigest">Weekly Digest</Label>
|
|
298
|
+
<p className="text-xs text-muted-foreground">Receive a weekly summary email.</p>
|
|
299
|
+
</div>
|
|
300
|
+
<Switch
|
|
301
|
+
id="weeklyDigest"
|
|
302
|
+
checked={watch('weeklyDigest')}
|
|
303
|
+
onCheckedChange={(checked) => setValue('weeklyDigest', checked)}
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
<Button type="submit" variant="teal" disabled={isSaving}>
|
|
307
|
+
<Save className="h-4 w-4 mr-1" />
|
|
308
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
309
|
+
</Button>
|
|
310
|
+
</CardContent>
|
|
311
|
+
</Card>
|
|
312
|
+
</form>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export default ${componentName};
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wizard page pattern template — multi-step form with progress bar.
|
|
3
|
+
*/
|
|
4
|
+
export function wizardPageTemplate(pageName: string): string {
|
|
5
|
+
const componentName = `${pageName}Page`;
|
|
6
|
+
return `import { useState, useCallback } from 'react';
|
|
7
|
+
import { useNavigate } from 'react-router-dom';
|
|
8
|
+
import { useForm } from 'react-hook-form';
|
|
9
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { ArrowLeft, ArrowRight, Check, Save } from 'lucide-react';
|
|
12
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
13
|
+
import { useTib } from '@/hooks/useTib';
|
|
14
|
+
import { Page } from '@/components/layout/Page';
|
|
15
|
+
import { PageHeader } from '@/components/layout/PageHeader';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { Input } from '@/components/ui/input';
|
|
18
|
+
import { Label } from '@/components/ui/label';
|
|
19
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
20
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
21
|
+
import { Progress } from '@/components/ui/progress';
|
|
22
|
+
import { toast } from 'sonner';
|
|
23
|
+
|
|
24
|
+
interface ${componentName}Props {
|
|
25
|
+
workspaceId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Steps definition */
|
|
29
|
+
const STEPS = [
|
|
30
|
+
{ id: 'basics', label: 'Basics', description: 'Basic information' },
|
|
31
|
+
{ id: 'details', label: 'Details', description: 'Additional details' },
|
|
32
|
+
{ id: 'review', label: 'Review', description: 'Review and confirm' },
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
type StepId = (typeof STEPS)[number]['id'];
|
|
36
|
+
|
|
37
|
+
/** Full form schema across all steps */
|
|
38
|
+
const wizardSchema = z.object({
|
|
39
|
+
name: z.string().min(1, 'Name is required'),
|
|
40
|
+
description: z.string().optional(),
|
|
41
|
+
type: z.string().min(1, 'Type is required'),
|
|
42
|
+
status: z.enum(['active', 'inactive']),
|
|
43
|
+
notes: z.string().optional(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
type WizardValues = z.infer<typeof wizardSchema>;
|
|
47
|
+
|
|
48
|
+
export function ${componentName}({ workspaceId }: ${componentName}Props) {
|
|
49
|
+
const tib = useTib();
|
|
50
|
+
const navigate = useNavigate();
|
|
51
|
+
const queryClient = useQueryClient();
|
|
52
|
+
const [currentStep, setCurrentStep] = useState<number>(0);
|
|
53
|
+
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set([0]));
|
|
54
|
+
|
|
55
|
+
const form = useForm<WizardValues>({
|
|
56
|
+
resolver: zodResolver(wizardSchema),
|
|
57
|
+
defaultValues: {
|
|
58
|
+
name: '',
|
|
59
|
+
description: '',
|
|
60
|
+
type: '',
|
|
61
|
+
status: 'active',
|
|
62
|
+
notes: '',
|
|
63
|
+
},
|
|
64
|
+
mode: 'onChange',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { register, handleSubmit, formState: { errors }, setValue, watch, trigger } = form;
|
|
68
|
+
|
|
69
|
+
const mutation = useMutation({
|
|
70
|
+
mutationFn: (values: WizardValues) =>
|
|
71
|
+
tib.data.create({ workspaceId, ...values }),
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
toast.success('\${pageName} created successfully');
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: ['data-list', workspaceId] });
|
|
75
|
+
navigate('..');
|
|
76
|
+
},
|
|
77
|
+
onError: (err) => {
|
|
78
|
+
toast.error('Failed to create', {
|
|
79
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const progress = ((completedSteps.size) / STEPS.length) * 100;
|
|
85
|
+
|
|
86
|
+
const isFirstStep = currentStep === 0;
|
|
87
|
+
const isLastStep = currentStep === STEPS.length - 1;
|
|
88
|
+
|
|
89
|
+
const goNext = useCallback(async () => {
|
|
90
|
+
const fieldsToValidate = getFieldsForStep(currentStep);
|
|
91
|
+
const valid = await trigger(fieldsToValidate);
|
|
92
|
+
if (valid) {
|
|
93
|
+
const next = currentStep + 1;
|
|
94
|
+
setCompletedSteps(prev => new Set([...prev, next]));
|
|
95
|
+
setCurrentStep(next);
|
|
96
|
+
}
|
|
97
|
+
}, [currentStep, trigger]);
|
|
98
|
+
|
|
99
|
+
const goBack = useCallback(() => {
|
|
100
|
+
setCurrentStep(prev => Math.max(0, prev - 1));
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const onSubmit = (values: WizardValues) => mutation.mutate(values);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Page title="Create ${pageName}" description="Follow the steps to create a new entity">
|
|
107
|
+
<Page.Header
|
|
108
|
+
breadcrumb={[
|
|
109
|
+
{ label: '${pageName}', to: '..' },
|
|
110
|
+
{ label: 'Create' },
|
|
111
|
+
]}
|
|
112
|
+
/>
|
|
113
|
+
<Page.Body>
|
|
114
|
+
<div className="max-w-2xl mx-auto">
|
|
115
|
+
{/* Progress bar */}
|
|
116
|
+
<div className="mb-8">
|
|
117
|
+
<Progress value={progress} className="h-2" />
|
|
118
|
+
<div className="flex justify-between mt-2">
|
|
119
|
+
{STEPS.map((step, idx) => (
|
|
120
|
+
<div
|
|
121
|
+
key={step.id}
|
|
122
|
+
className={[
|
|
123
|
+
'flex items-center gap-2 text-sm',
|
|
124
|
+
idx <= currentStep ? 'text-primary' : 'text-muted-foreground',
|
|
125
|
+
].join(' ')}
|
|
126
|
+
>
|
|
127
|
+
<span className={[
|
|
128
|
+
'flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium',
|
|
129
|
+
completedSteps.has(idx)
|
|
130
|
+
? 'bg-primary text-primary-foreground'
|
|
131
|
+
: 'border border-muted-foreground text-muted-foreground',
|
|
132
|
+
].join(' ')}>
|
|
133
|
+
{completedSteps.has(idx) ? <Check className="h-3 w-3" /> : idx + 1}
|
|
134
|
+
</span>
|
|
135
|
+
<span className="hidden sm:inline">{step.label}</span>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
|
142
|
+
{/* Step 1: Basics */}
|
|
143
|
+
{currentStep === 0 && (
|
|
144
|
+
<div className="space-y-4">
|
|
145
|
+
<h2 className="text-lg font-semibold">Basic Information</h2>
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor="name">Name</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id="name"
|
|
150
|
+
{...register('name')}
|
|
151
|
+
placeholder="Enter name"
|
|
152
|
+
aria-invalid={!!errors.name}
|
|
153
|
+
/>
|
|
154
|
+
{errors.name && (
|
|
155
|
+
<p className="text-xs text-destructive">{errors.name.message}</p>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
<div className="space-y-2">
|
|
159
|
+
<Label htmlFor="description">Description</Label>
|
|
160
|
+
<Textarea
|
|
161
|
+
id="description"
|
|
162
|
+
{...register('description')}
|
|
163
|
+
placeholder="Optional description"
|
|
164
|
+
rows={3}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="space-y-2">
|
|
168
|
+
<Label htmlFor="type">Type</Label>
|
|
169
|
+
<Input
|
|
170
|
+
id="type"
|
|
171
|
+
{...register('type')}
|
|
172
|
+
placeholder="Enter type"
|
|
173
|
+
aria-invalid={!!errors.type}
|
|
174
|
+
/>
|
|
175
|
+
{errors.type && (
|
|
176
|
+
<p className="text-xs text-destructive">{errors.type.message}</p>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Step 2: Details */}
|
|
183
|
+
{currentStep === 1 && (
|
|
184
|
+
<div className="space-y-4">
|
|
185
|
+
<h2 className="text-lg font-semibold">Additional Details</h2>
|
|
186
|
+
<div className="space-y-2">
|
|
187
|
+
<Label htmlFor="wiz-status">Status</Label>
|
|
188
|
+
<Select
|
|
189
|
+
value={watch('status')}
|
|
190
|
+
onValueChange={(value) => setValue('status', value as 'active' | 'inactive')}
|
|
191
|
+
>
|
|
192
|
+
<SelectTrigger id="wiz-status">
|
|
193
|
+
<SelectValue placeholder="Select status" />
|
|
194
|
+
</SelectTrigger>
|
|
195
|
+
<SelectContent>
|
|
196
|
+
<SelectItem value="active">Active</SelectItem>
|
|
197
|
+
<SelectItem value="inactive">Inactive</SelectItem>
|
|
198
|
+
</SelectContent>
|
|
199
|
+
</Select>
|
|
200
|
+
</div>
|
|
201
|
+
<div className="space-y-2">
|
|
202
|
+
<Label htmlFor="notes">Notes</Label>
|
|
203
|
+
<Textarea
|
|
204
|
+
id="notes"
|
|
205
|
+
{...register('notes')}
|
|
206
|
+
placeholder="Add any notes"
|
|
207
|
+
rows={3}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{/* Step 3: Review */}
|
|
214
|
+
{currentStep === 2 && (
|
|
215
|
+
<div className="space-y-4">
|
|
216
|
+
<h2 className="text-lg font-semibold">Review & Confirm</h2>
|
|
217
|
+
<div className="rounded-lg border border-border p-4 space-y-3">
|
|
218
|
+
<div className="flex justify-between">
|
|
219
|
+
<span className="text-sm text-muted-foreground">Name</span>
|
|
220
|
+
<span className="text-sm font-medium">{watch('name') || '—'}</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div className="flex justify-between">
|
|
223
|
+
<span className="text-sm text-muted-foreground">Description</span>
|
|
224
|
+
<span className="text-sm font-medium">{watch('description') || '—'}</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="flex justify-between">
|
|
227
|
+
<span className="text-sm text-muted-foreground">Type</span>
|
|
228
|
+
<span className="text-sm font-medium">{watch('type') || '—'}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="flex justify-between">
|
|
231
|
+
<span className="text-sm text-muted-foreground">Status</span>
|
|
232
|
+
<span className="text-sm font-medium">{watch('status')}</span>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="flex justify-between">
|
|
235
|
+
<span className="text-sm text-muted-foreground">Notes</span>
|
|
236
|
+
<span className="text-sm font-medium">{watch('notes') || '—'}</span>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
<p className="text-xs text-muted-foreground">
|
|
240
|
+
Review the details above and click “Create” to finalize.
|
|
241
|
+
</p>
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Navigation buttons */}
|
|
246
|
+
<div className="flex items-center justify-between pt-4 border-t border-border">
|
|
247
|
+
<Button
|
|
248
|
+
type="button"
|
|
249
|
+
variant="outline"
|
|
250
|
+
size="sm"
|
|
251
|
+
onClick={goBack}
|
|
252
|
+
disabled={isFirstStep}
|
|
253
|
+
>
|
|
254
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
255
|
+
Back
|
|
256
|
+
</Button>
|
|
257
|
+
{!isLastStep ? (
|
|
258
|
+
<Button type="button" variant="teal" size="sm" onClick={goNext}>
|
|
259
|
+
Next
|
|
260
|
+
<ArrowRight className="h-4 w-4 ml-1" />
|
|
261
|
+
</Button>
|
|
262
|
+
) : (
|
|
263
|
+
<Button
|
|
264
|
+
type="submit"
|
|
265
|
+
variant="teal"
|
|
266
|
+
size="sm"
|
|
267
|
+
disabled={mutation.isPending}
|
|
268
|
+
>
|
|
269
|
+
<Save className="h-4 w-4 mr-1" />
|
|
270
|
+
{mutation.isPending ? 'Creating...' : 'Create'}
|
|
271
|
+
</Button>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</form>
|
|
275
|
+
</div>
|
|
276
|
+
</Page.Body>
|
|
277
|
+
</Page>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Returns the field names that belong to each step for validation. */
|
|
282
|
+
function getFieldsForStep(step: number): Array<keyof WizardValues> {
|
|
283
|
+
switch (step) {
|
|
284
|
+
case 0: return ['name', 'description', 'type'];
|
|
285
|
+
case 1: return ['status', 'notes'];
|
|
286
|
+
case 2: return []; // review step — already valid
|
|
287
|
+
default: return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export default ${componentName};
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-relay subscriber pattern template.
|
|
3
|
+
* Produces: subscriber that relays domain events to an audit event-store or external sink.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function camelCase(s: string): string {
|
|
7
|
+
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function auditRelayTemplate(domain: string, id: string, event: string): string {
|
|
11
|
+
const varName = camelCase(id);
|
|
12
|
+
|
|
13
|
+
return `import { defineSubscriber } from '@mettlecast/domain-runtime';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ${id} — Relays ${event} to the audit event-store for compliance.
|
|
17
|
+
*
|
|
18
|
+
* Captures every instance of ${event} and stores an immutable
|
|
19
|
+
* audit record including caller identity, timestamp, and event payload.
|
|
20
|
+
*/
|
|
21
|
+
export const ${varName} = defineSubscriber({
|
|
22
|
+
id: '${domain}.${id}',
|
|
23
|
+
event: '${event}',
|
|
24
|
+
semverRange: '^1',
|
|
25
|
+
handler: async (event, ctx) => {
|
|
26
|
+
const auditRecord = {
|
|
27
|
+
pk: 'AUDIT#' + event.data.tenantId,
|
|
28
|
+
sk: 'EVENT#' + event.id,
|
|
29
|
+
eventType: event.type,
|
|
30
|
+
eventVersion: event.version,
|
|
31
|
+
payload: event.data,
|
|
32
|
+
timestamp: event.timestamp,
|
|
33
|
+
source: event.source,
|
|
34
|
+
correlationId: event.correlationId,
|
|
35
|
+
ttl: Math.floor(Date.now() / 1000) + 90 * 86400, // 90-day retention
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ── Write to audit store ─────────────────────────────────
|
|
39
|
+
await ctx.db.put(auditRecord);
|
|
40
|
+
|
|
41
|
+
// ── Optionally forward to external sink ──────────────────
|
|
42
|
+
if (ctx.integrations?.auditSink) {
|
|
43
|
+
await ctx.fetch(ctx.integrations.auditSink, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
body: JSON.stringify(auditRecord),
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ctx.metrics?.putMetric('${id}.audit.relayed', 1, 'Count');
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
`;
|
|
54
|
+
}
|