spine-framework 0.1.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/.framework/README.md +129 -0
- package/.framework/cli/bin.cjs +14 -0
- package/.framework/cli/commands/agents.ts +153 -0
- package/.framework/cli/commands/auth.ts +94 -0
- package/.framework/cli/commands/create-app.ts +185 -0
- package/.framework/cli/commands/dev.ts +295 -0
- package/.framework/cli/commands/doctor.ts +442 -0
- package/.framework/cli/commands/generate.ts +332 -0
- package/.framework/cli/commands/init.ts +272 -0
- package/.framework/cli/commands/install-app.ts +391 -0
- package/.framework/cli/commands/items.ts +253 -0
- package/.framework/cli/commands/migrations.ts +141 -0
- package/.framework/cli/commands/pipelines.ts +166 -0
- package/.framework/cli/commands/status.ts +197 -0
- package/.framework/cli/commands/system.ts +184 -0
- package/.framework/cli/commands/test.ts +227 -0
- package/.framework/cli/commands/uninstall-app.ts +166 -0
- package/.framework/cli/context.ts +268 -0
- package/.framework/cli/env-loader.ts +36 -0
- package/.framework/cli/index.ts +106 -0
- package/.framework/cli/welcome.cjs +45 -0
- package/.framework/docs/API.md +384 -0
- package/.framework/docs/STABILITY.md +52 -0
- package/.framework/docs/admin-routes.md +76 -0
- package/.framework/docs/api-docs-progress.md +38 -0
- package/.framework/docs/api-governance.md +146 -0
- package/.framework/docs/api-testing-results.md +212 -0
- package/.framework/docs/apis/admin-configs.md +567 -0
- package/.framework/docs/apis/admin-data.md +272 -0
- package/.framework/docs/apis/index.md +231 -0
- package/.framework/docs/apis/internal.md +295 -0
- package/.framework/docs/apis/runtime.md +537 -0
- package/.framework/docs/assembly-launch-guide.md +138 -0
- package/.framework/docs/audit-results.md +590 -0
- package/.framework/docs/authorization-model.md +170 -0
- package/.framework/docs/db-api-inventory.md +95 -0
- package/.framework/docs/examples/custom-app/README.md +77 -0
- package/.framework/docs/examples/custom-function/README.md +27 -0
- package/.framework/docs/examples/custom-function/handler.ts +48 -0
- package/.framework/docs/examples/custom-webhook/README.md +68 -0
- package/.framework/docs/gap-remediation-backlog.md +103 -0
- package/.framework/docs/guides/cli-guide.md +224 -0
- package/.framework/docs/guides/getting-started.md +103 -0
- package/.framework/docs/guides/import-guide.md +193 -0
- package/.framework/docs/guides/testing-guide.md +229 -0
- package/.framework/docs/permission-examples.md +326 -0
- package/.framework/docs/ui-adoption-verification.md +111 -0
- package/.framework/docs/ui-api-coverage.md +84 -0
- package/.framework/docs/v2-compatibility-audit.md +228 -0
- package/.framework/functions/.gitkeep +1 -0
- package/.framework/functions/_shared/agent-runner.ts +1097 -0
- package/.framework/functions/_shared/app-manifest.ts +184 -0
- package/.framework/functions/_shared/audit.ts +150 -0
- package/.framework/functions/_shared/db.ts +174 -0
- package/.framework/functions/_shared/index.ts +382 -0
- package/.framework/functions/_shared/middleware.ts +490 -0
- package/.framework/functions/_shared/permissions.ts +1325 -0
- package/.framework/functions/_shared/pipeline-runner.ts +731 -0
- package/.framework/functions/_shared/principal.ts +760 -0
- package/.framework/functions/_shared/schema-utils.ts +967 -0
- package/.framework/functions/_shared/testing.ts +258 -0
- package/.framework/functions/_shared/trigger-engine.ts +425 -0
- package/.framework/functions/_shared/webhook-registration.ts +168 -0
- package/.framework/functions/_shared/webhook-registry.ts +129 -0
- package/.framework/functions/account-nodes.ts +111 -0
- package/.framework/functions/admin-data.ts +606 -0
- package/.framework/functions/ai-agents.ts +323 -0
- package/.framework/functions/api-keys.ts +376 -0
- package/.framework/functions/apps.ts +483 -0
- package/.framework/functions/auth.ts +196 -0
- package/.framework/functions/debug-auth.ts +107 -0
- package/.framework/functions/embeddings.ts +556 -0
- package/.framework/functions/integration-routes.ts +523 -0
- package/.framework/functions/integrations.ts +319 -0
- package/.framework/functions/item-progress.ts +272 -0
- package/.framework/functions/logs.ts +438 -0
- package/.framework/functions/observability.ts +275 -0
- package/.framework/functions/pipeline-executions.ts +494 -0
- package/.framework/functions/pipelines.ts +485 -0
- package/.framework/functions/prompt-configs.ts +339 -0
- package/.framework/functions/roles.ts +387 -0
- package/.framework/functions/system-cron.ts +742 -0
- package/.framework/functions/system.ts +323 -0
- package/.framework/functions/tests.ts +119 -0
- package/.framework/functions/timers.ts +357 -0
- package/.framework/functions/triggers.ts +563 -0
- package/.framework/functions/types.ts +604 -0
- package/.framework/migrations/000_foundation.sql +1256 -0
- package/.framework/migrations/001_seed.sql +92 -0
- package/.framework/migrations/002_seed_constraints.sql +13 -0
- package/.framework/migrations/003_auth_user_trigger.sql +59 -0
- package/.framework/src/App.tsx +126 -0
- package/.framework/src/apps/admin/index.tsx +173 -0
- package/.framework/src/components/AppWrapper.tsx +56 -0
- package/.framework/src/components/CustomAppLoader.tsx +116 -0
- package/.framework/src/components/admin/AdminListPage.tsx +151 -0
- package/.framework/src/components/admin/AdminSidebar.tsx +166 -0
- package/.framework/src/components/admin/AdminStatsCard.tsx +62 -0
- package/.framework/src/components/admin/SortableTableHeader.tsx +42 -0
- package/.framework/src/components/app-shell/GenericAppShell.tsx +181 -0
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +200 -0
- package/.framework/src/components/app-shell/GenericListPage.tsx +116 -0
- package/.framework/src/components/app-sidebar.tsx +228 -0
- package/.framework/src/components/auth/ProtectedRoute.tsx +88 -0
- package/.framework/src/components/layout/AppShell.tsx +91 -0
- package/.framework/src/components/layout/Header.tsx +88 -0
- package/.framework/src/components/layout/Layout.tsx +95 -0
- package/.framework/src/components/layout/Sidebar.tsx +329 -0
- package/.framework/src/components/runtime/DataDetailHeader.tsx +77 -0
- package/.framework/src/components/runtime/DataDetailPage.tsx +171 -0
- package/.framework/src/components/runtime/DataFilters.tsx +91 -0
- package/.framework/src/components/runtime/DataHeader.tsx +68 -0
- package/.framework/src/components/runtime/DataListPage.tsx +124 -0
- package/.framework/src/components/runtime/DataStats.tsx +70 -0
- package/.framework/src/components/runtime/DataTable.tsx +174 -0
- package/.framework/src/components/runtime/SchemaDetailForm.tsx +134 -0
- package/.framework/src/components/runtime/index.ts +18 -0
- package/.framework/src/components/search-form.tsx +29 -0
- package/.framework/src/components/shared/AgentView.tsx +213 -0
- package/.framework/src/components/shared/FieldRenderer.tsx +478 -0
- package/.framework/src/components/shared/SchemaFields.tsx +226 -0
- package/.framework/src/components/ui/DataTable.tsx +343 -0
- package/.framework/src/components/ui/Form.tsx +281 -0
- package/.framework/src/components/ui/ItemCard.tsx +296 -0
- package/.framework/src/components/ui/ItemListView.tsx +308 -0
- package/.framework/src/components/ui/LoadingSpinner.tsx +52 -0
- package/.framework/src/components/ui/Modal.tsx +61 -0
- package/.framework/src/components/ui/RichTextEditor.tsx +210 -0
- package/.framework/src/components/ui/accordion.tsx +82 -0
- package/.framework/src/components/ui/alert-dialog.tsx +197 -0
- package/.framework/src/components/ui/alert.tsx +76 -0
- package/.framework/src/components/ui/aspect-ratio.tsx +11 -0
- package/.framework/src/components/ui/avatar.tsx +110 -0
- package/.framework/src/components/ui/badge.tsx +49 -0
- package/.framework/src/components/ui/breadcrumb.tsx +122 -0
- package/.framework/src/components/ui/button-group.tsx +83 -0
- package/.framework/src/components/ui/button.tsx +65 -0
- package/.framework/src/components/ui/calendar.tsx +222 -0
- package/.framework/src/components/ui/card.tsx +100 -0
- package/.framework/src/components/ui/carousel.tsx +240 -0
- package/.framework/src/components/ui/chart.tsx +373 -0
- package/.framework/src/components/ui/checkbox.tsx +31 -0
- package/.framework/src/components/ui/collapsible.tsx +33 -0
- package/.framework/src/components/ui/combobox.tsx +299 -0
- package/.framework/src/components/ui/command.tsx +193 -0
- package/.framework/src/components/ui/context-menu.tsx +261 -0
- package/.framework/src/components/ui/dialog.tsx +165 -0
- package/.framework/src/components/ui/direction.tsx +22 -0
- package/.framework/src/components/ui/drawer.tsx +132 -0
- package/.framework/src/components/ui/dropdown-menu.tsx +269 -0
- package/.framework/src/components/ui/empty.tsx +104 -0
- package/.framework/src/components/ui/field.tsx +238 -0
- package/.framework/src/components/ui/hover-card.tsx +42 -0
- package/.framework/src/components/ui/input-group.tsx +153 -0
- package/.framework/src/components/ui/input-otp.tsx +87 -0
- package/.framework/src/components/ui/input.tsx +19 -0
- package/.framework/src/components/ui/item.tsx +196 -0
- package/.framework/src/components/ui/kbd.tsx +26 -0
- package/.framework/src/components/ui/label.tsx +22 -0
- package/.framework/src/components/ui/menubar.tsx +277 -0
- package/.framework/src/components/ui/native-select.tsx +61 -0
- package/.framework/src/components/ui/navigation-menu.tsx +164 -0
- package/.framework/src/components/ui/pagination.tsx +129 -0
- package/.framework/src/components/ui/popover.tsx +87 -0
- package/.framework/src/components/ui/progress.tsx +31 -0
- package/.framework/src/components/ui/radio-group.tsx +42 -0
- package/.framework/src/components/ui/resizable.tsx +50 -0
- package/.framework/src/components/ui/scroll-area.tsx +53 -0
- package/.framework/src/components/ui/select.tsx +195 -0
- package/.framework/src/components/ui/separator.tsx +26 -0
- package/.framework/src/components/ui/sheet.tsx +145 -0
- package/.framework/src/components/ui/sidebar.tsx +706 -0
- package/.framework/src/components/ui/skeleton.tsx +13 -0
- package/.framework/src/components/ui/slider.tsx +59 -0
- package/.framework/src/components/ui/sonner.tsx +47 -0
- package/.framework/src/components/ui/spinner.tsx +10 -0
- package/.framework/src/components/ui/switch.tsx +33 -0
- package/.framework/src/components/ui/table-primitives.tsx +141 -0
- package/.framework/src/components/ui/table.tsx +114 -0
- package/.framework/src/components/ui/tabs.tsx +90 -0
- package/.framework/src/components/ui/textarea.tsx +18 -0
- package/.framework/src/components/ui/toggle-group.tsx +89 -0
- package/.framework/src/components/ui/toggle.tsx +45 -0
- package/.framework/src/components/ui/tooltip.tsx +57 -0
- package/.framework/src/contexts/AppContext.tsx +133 -0
- package/.framework/src/contexts/AuthContext.tsx +371 -0
- package/.framework/src/hooks/use-mobile.ts +19 -0
- package/.framework/src/hooks/useApi.ts +526 -0
- package/.framework/src/hooks/useApps.ts +114 -0
- package/.framework/src/hooks/useEntityList.ts +190 -0
- package/.framework/src/hooks/useEntityRecord.ts +308 -0
- package/.framework/src/hooks/useForm.ts +307 -0
- package/.framework/src/hooks/useListSchema.ts +264 -0
- package/.framework/src/hooks/useSchemaRecord.ts +223 -0
- package/.framework/src/index.css +128 -0
- package/.framework/src/lib/api.ts +156 -0
- package/.framework/src/lib/supabase.ts +94 -0
- package/.framework/src/lib/utils.ts +317 -0
- package/.framework/src/main.tsx +27 -0
- package/.framework/src/pages/DashboardPage.tsx +181 -0
- package/.framework/src/pages/NotFoundPage.tsx +39 -0
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +161 -0
- package/.framework/src/pages/admin/AIAgentsPage.tsx +318 -0
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +199 -0
- package/.framework/src/pages/admin/APIKeysPage.tsx +303 -0
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +523 -0
- package/.framework/src/pages/admin/AppDetailPage.tsx +493 -0
- package/.framework/src/pages/admin/AppsPage.tsx +355 -0
- package/.framework/src/pages/admin/DesignedPage.tsx +491 -0
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +534 -0
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +424 -0
- package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +176 -0
- package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +109 -0
- package/.framework/src/pages/admin/IntegratedDashboard.tsx +402 -0
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +187 -0
- package/.framework/src/pages/admin/IntegrationsPage.tsx +301 -0
- package/.framework/src/pages/admin/LogsPage.tsx +283 -0
- package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +85 -0
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +470 -0
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +183 -0
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +279 -0
- package/.framework/src/pages/admin/PipelinesPage.tsx +390 -0
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +299 -0
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +292 -0
- package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +434 -0
- package/.framework/src/pages/admin/RoleDetailPage.tsx +273 -0
- package/.framework/src/pages/admin/RolesPage.tsx +292 -0
- package/.framework/src/pages/admin/SelectTestPage.tsx +61 -0
- package/.framework/src/pages/admin/ShadcnTestPage.tsx +588 -0
- package/.framework/src/pages/admin/SimpleDashboard.tsx +387 -0
- package/.framework/src/pages/admin/TestRunDetailPage.tsx +172 -0
- package/.framework/src/pages/admin/TestingDashboard.tsx +257 -0
- package/.framework/src/pages/admin/TimerDetailPage.tsx +151 -0
- package/.framework/src/pages/admin/TimersPage.tsx +376 -0
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +149 -0
- package/.framework/src/pages/admin/TriggersPage.tsx +381 -0
- package/.framework/src/pages/admin/TypeDetailPage.tsx +694 -0
- package/.framework/src/pages/admin/TypesPage.tsx +295 -0
- package/.framework/src/pages/auth/LoginPage.tsx +188 -0
- package/.framework/src/pages/auth/RegisterPage.tsx +163 -0
- package/.framework/src/pages/spine-framework/APIPage.tsx +17 -0
- package/.framework/src/pages/spine-framework/CLIPage.tsx +25 -0
- package/.framework/src/types/auth.ts +125 -0
- package/.framework/src/types/types.ts +407 -0
- package/STRUCTURE.md +150 -0
- package/config/components.json +25 -0
- package/config/deno.lock +108 -0
- package/config/package-lock.json +17183 -0
- package/config/postcss.config.cjs +10 -0
- package/config/tailwind.config.cjs +78 -0
- package/config/tsconfig.build.json +32 -0
- package/config/tsconfig.cli.json +18 -0
- package/config/tsconfig.json +41 -0
- package/config/tsconfig.node.json +17 -0
- package/config/tsconfig.node.tsbuildinfo +1 -0
- package/config/tsconfig.tsbuildinfo +1 -0
- package/config/typedoc.json +16 -0
- package/config/vite.config.d.ts +2 -0
- package/config/vite.config.ts +72 -0
- package/dist/cli/commands/agents.d.ts +39 -0
- package/dist/cli/commands/agents.d.ts.map +1 -0
- package/dist/cli/commands/auth.d.ts +36 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/create-app.d.ts +23 -0
- package/dist/cli/commands/create-app.d.ts.map +1 -0
- package/dist/cli/commands/dev.d.ts +39 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/doctor.d.ts +42 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/generate.d.ts +36 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/init.d.ts +30 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/install-app.d.ts +30 -0
- package/dist/cli/commands/install-app.d.ts.map +1 -0
- package/dist/cli/commands/items.d.ts +45 -0
- package/dist/cli/commands/items.d.ts.map +1 -0
- package/dist/cli/commands/migrations.d.ts +41 -0
- package/dist/cli/commands/migrations.d.ts.map +1 -0
- package/dist/cli/commands/pipelines.d.ts +40 -0
- package/dist/cli/commands/pipelines.d.ts.map +1 -0
- package/dist/cli/commands/status.d.ts +23 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/system.d.ts +29 -0
- package/dist/cli/commands/system.d.ts.map +1 -0
- package/dist/cli/commands/test.d.ts +46 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/uninstall-app.d.ts +23 -0
- package/dist/cli/commands/uninstall-app.d.ts.map +1 -0
- package/dist/cli/context.d.ts +88 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/env-loader.d.ts +14 -0
- package/dist/cli/env-loader.d.ts.map +1 -0
- package/dist/cli/index.d.ts +41 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/functions/_shared/agent-runner.d.ts +156 -0
- package/dist/functions/_shared/agent-runner.d.ts.map +1 -0
- package/dist/functions/_shared/app-manifest.d.ts +68 -0
- package/dist/functions/_shared/app-manifest.d.ts.map +1 -0
- package/dist/functions/_shared/audit.d.ts +91 -0
- package/dist/functions/_shared/audit.d.ts.map +1 -0
- package/dist/functions/_shared/db.d.ts +125 -0
- package/dist/functions/_shared/db.d.ts.map +1 -0
- package/dist/functions/_shared/index.d.ts +298 -0
- package/dist/functions/_shared/index.d.ts.map +1 -0
- package/dist/functions/_shared/middleware.d.ts +315 -0
- package/dist/functions/_shared/middleware.d.ts.map +1 -0
- package/dist/functions/_shared/permissions.d.ts +626 -0
- package/dist/functions/_shared/permissions.d.ts.map +1 -0
- package/dist/functions/_shared/pipeline-runner.d.ts +124 -0
- package/dist/functions/_shared/pipeline-runner.d.ts.map +1 -0
- package/dist/functions/_shared/principal.d.ts +284 -0
- package/dist/functions/_shared/principal.d.ts.map +1 -0
- package/dist/functions/_shared/schema-utils.d.ts +181 -0
- package/dist/functions/_shared/schema-utils.d.ts.map +1 -0
- package/dist/functions/_shared/testing.d.ts +172 -0
- package/dist/functions/_shared/testing.d.ts.map +1 -0
- package/dist/functions/_shared/trigger-engine.d.ts +140 -0
- package/dist/functions/_shared/trigger-engine.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registration.d.ts +81 -0
- package/dist/functions/_shared/webhook-registration.d.ts.map +1 -0
- package/dist/functions/_shared/webhook-registry.d.ts +57 -0
- package/dist/functions/_shared/webhook-registry.d.ts.map +1 -0
- package/dist/functions/account-nodes.d.ts +48 -0
- package/dist/functions/account-nodes.d.ts.map +1 -0
- package/dist/functions/admin-data.d.ts +178 -0
- package/dist/functions/admin-data.d.ts.map +1 -0
- package/dist/functions/ai-agents.d.ts +125 -0
- package/dist/functions/ai-agents.d.ts.map +1 -0
- package/dist/functions/api-keys.d.ts +140 -0
- package/dist/functions/api-keys.d.ts.map +1 -0
- package/dist/functions/apps.d.ts +163 -0
- package/dist/functions/apps.d.ts.map +1 -0
- package/dist/functions/auth.d.ts +74 -0
- package/dist/functions/auth.d.ts.map +1 -0
- package/dist/functions/debug-auth.d.ts +33 -0
- package/dist/functions/debug-auth.d.ts.map +1 -0
- package/dist/functions/embeddings.d.ts +205 -0
- package/dist/functions/embeddings.d.ts.map +1 -0
- package/dist/functions/integration-routes.d.ts +45 -0
- package/dist/functions/integration-routes.d.ts.map +1 -0
- package/dist/functions/integrations.d.ts +124 -0
- package/dist/functions/integrations.d.ts.map +1 -0
- package/dist/functions/item-progress.d.ts +41 -0
- package/dist/functions/item-progress.d.ts.map +1 -0
- package/dist/functions/logs.d.ts +162 -0
- package/dist/functions/logs.d.ts.map +1 -0
- package/dist/functions/observability.d.ts +123 -0
- package/dist/functions/observability.d.ts.map +1 -0
- package/dist/functions/pipeline-executions.d.ts +190 -0
- package/dist/functions/pipeline-executions.d.ts.map +1 -0
- package/dist/functions/pipelines.d.ts +171 -0
- package/dist/functions/pipelines.d.ts.map +1 -0
- package/dist/functions/prompt-configs.d.ts +125 -0
- package/dist/functions/prompt-configs.d.ts.map +1 -0
- package/dist/functions/roles.d.ts +118 -0
- package/dist/functions/roles.d.ts.map +1 -0
- package/dist/functions/system-cron.d.ts +65 -0
- package/dist/functions/system-cron.d.ts.map +1 -0
- package/dist/functions/system.d.ts +29 -0
- package/dist/functions/system.d.ts.map +1 -0
- package/dist/functions/tests.d.ts +28 -0
- package/dist/functions/tests.d.ts.map +1 -0
- package/dist/functions/timers.d.ts +139 -0
- package/dist/functions/timers.d.ts.map +1 -0
- package/dist/functions/triggers.d.ts +203 -0
- package/dist/functions/triggers.d.ts.map +1 -0
- package/dist/functions/types.d.ts +151 -0
- package/dist/functions/types.d.ts.map +1 -0
- package/dist/src/types/types.d.ts +364 -0
- package/dist/src/types/types.d.ts.map +1 -0
- package/package.json +192 -0
- package/scripts/app-install-cli.ts +286 -0
- package/scripts/assemble-frontend.sh +79 -0
- package/scripts/assemble-functions.sh +62 -0
- package/scripts/assemble.sh +35 -0
- package/scripts/boundary-check.sh +106 -0
- package/scripts/build-manifest.sh +80 -0
- package/scripts/check-core-integrity.sh +82 -0
- package/scripts/ingest-chunks.cjs +202 -0
- package/scripts/kb-chunk-parser.cjs +312 -0
- package/scripts/kb-chunk-parser.ts +330 -0
- package/scripts/load-test-app-install.ts +484 -0
- package/scripts/netlify-dev-wrapper.sh +22 -0
- package/scripts/verify-integrity.sh +69 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/hooks/useForm
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Schema-aware form state hook. Manages field values, touched state,
|
|
8
|
+
* per-field validation (required, email, url, number range, text
|
|
9
|
+
* length/pattern), and form submission with async support.
|
|
10
|
+
*
|
|
11
|
+
* Designed to work directly with `FieldDefinition[]` arrays produced
|
|
12
|
+
* by `useSchemaRecord` or static field arrays in admin forms.
|
|
13
|
+
*
|
|
14
|
+
* **Validation order per field:**
|
|
15
|
+
* 1. Required check
|
|
16
|
+
* 2. Type-specific rule (email regex, URL parse, number range, text length/pattern)
|
|
17
|
+
* 3. Custom `validate` function (whole-form)
|
|
18
|
+
*
|
|
19
|
+
* @seeAlso src/types/types.ts (FieldDefinition — field schema shape)
|
|
20
|
+
* @seeAlso src/hooks/useSchemaRecord.ts (produces FieldDefinition[] for this hook)
|
|
21
|
+
* @seeAlso src/components/shared/SchemaFields.tsx (renders fields from FieldDefinition[])
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
25
|
+
import { FieldDefinition, FormState, ValidationError } from '../types/types'
|
|
26
|
+
|
|
27
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
interface UseFormOptions {
|
|
30
|
+
initialValues: Record<string, any>
|
|
31
|
+
fields: FieldDefinition[]
|
|
32
|
+
onSubmit?: (values: Record<string, any>) => void | Promise<void>
|
|
33
|
+
validate?: (values: Record<string, any>) => Record<string, string>
|
|
34
|
+
onChange?: (values: Record<string, any>) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return value of `useForm`.
|
|
39
|
+
*
|
|
40
|
+
* @prop data - Current field values keyed by field name
|
|
41
|
+
* @prop errors - Per-field error messages (empty string = cleared, absent = untouched)
|
|
42
|
+
* @prop touched - Fields the user has interacted with
|
|
43
|
+
* @prop isSubmitting - True while `onSubmit` promise is pending
|
|
44
|
+
* @prop isValid - True when `errors` is empty
|
|
45
|
+
* @prop isDirty - True when any touched field differs from `initialValues`
|
|
46
|
+
* @prop handleChange - Update a field value (clears error, marks touched)
|
|
47
|
+
* @prop handleBlur - Mark a field touched and run field-level validation
|
|
48
|
+
* @prop handleSubmit - Validate all fields, then call `onSubmit` if valid
|
|
49
|
+
* @prop resetForm - Restore all state to `initialValues`
|
|
50
|
+
* @prop setFieldValue - Programmatic field update (same as handleChange)
|
|
51
|
+
* @prop setFieldError - Inject a server-side error for a field
|
|
52
|
+
* @prop clearErrors - Clear all errors (e.g. after navigation)
|
|
53
|
+
*/
|
|
54
|
+
interface UseFormReturn {
|
|
55
|
+
data: Record<string, any>
|
|
56
|
+
errors: Record<string, string>
|
|
57
|
+
touched: Record<string, boolean>
|
|
58
|
+
isSubmitting: boolean
|
|
59
|
+
isValid: boolean
|
|
60
|
+
isDirty: boolean
|
|
61
|
+
handleChange: (field: string, value: any) => void
|
|
62
|
+
handleBlur: (field: string) => void
|
|
63
|
+
handleSubmit: (e?: React.FormEvent) => void
|
|
64
|
+
resetForm: () => void
|
|
65
|
+
setFieldValue: (field: string, value: any) => void
|
|
66
|
+
setFieldError: (field: string, error: string) => void
|
|
67
|
+
clearErrors: () => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── HOOK ────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Schema-driven form state and validation hook.
|
|
74
|
+
*
|
|
75
|
+
* @param options.initialValues - Seed values for all fields
|
|
76
|
+
* @param options.fields - `FieldDefinition[]` from schema or static config
|
|
77
|
+
* @param options.onSubmit - Async submit handler; called only if validation passes
|
|
78
|
+
* @param options.validate - Optional whole-form custom validator returning
|
|
79
|
+
* `Record<fieldName, errorMessage>`
|
|
80
|
+
* @param options.onChange - Called on every field change with the full values map
|
|
81
|
+
*
|
|
82
|
+
* @returns `UseFormReturn` — see interface for full description
|
|
83
|
+
*
|
|
84
|
+
* @inputSpec fields[].data_type: 'email'|'url'|'number'|'text'|'textarea'
|
|
85
|
+
* drives built-in validation rules
|
|
86
|
+
* @inputSpec fields[].validation.minLength/maxLength/pattern: text constraints
|
|
87
|
+
* @sideEffects React state mutations only
|
|
88
|
+
* @calledBy SchemaDetailForm.tsx, all admin detail pages
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* const form = useForm({
|
|
93
|
+
* initialValues: { name: '', email: '' },
|
|
94
|
+
* fields: [{ name: 'email', data_type: 'email', required: true, label: 'Email' }],
|
|
95
|
+
* onSubmit: async (values) => await save(values)
|
|
96
|
+
* })
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export function useForm({
|
|
100
|
+
initialValues,
|
|
101
|
+
fields,
|
|
102
|
+
onSubmit,
|
|
103
|
+
validate,
|
|
104
|
+
onChange
|
|
105
|
+
}: UseFormOptions): UseFormReturn {
|
|
106
|
+
const [data, setData] = useState<Record<string, any>>(initialValues)
|
|
107
|
+
const [errors, setErrors] = useState<Record<string, string>>({})
|
|
108
|
+
const [touched, setTouched] = useState<Record<string, boolean>>({})
|
|
109
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
110
|
+
const [submitCount, setSubmitCount] = useState(0)
|
|
111
|
+
|
|
112
|
+
// Validate a single field
|
|
113
|
+
const validateField = useCallback((field: string, value: any): string | null => {
|
|
114
|
+
const fieldDef = fields.find(f => f.name === field)
|
|
115
|
+
if (!fieldDef) return null
|
|
116
|
+
|
|
117
|
+
// Required validation
|
|
118
|
+
if (fieldDef.required && (!value || value === '')) {
|
|
119
|
+
return `${fieldDef.label || field} is required`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip validation for empty optional fields
|
|
123
|
+
if (!fieldDef.required && (!value || value === '')) {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Type-specific validation
|
|
128
|
+
switch (fieldDef.data_type) {
|
|
129
|
+
case 'email':
|
|
130
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
131
|
+
if (!emailRegex.test(value)) {
|
|
132
|
+
return 'Please enter a valid email address'
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
case 'url':
|
|
137
|
+
try {
|
|
138
|
+
new URL(value)
|
|
139
|
+
} catch {
|
|
140
|
+
return 'Please enter a valid URL'
|
|
141
|
+
}
|
|
142
|
+
break
|
|
143
|
+
|
|
144
|
+
case 'number':
|
|
145
|
+
const num = Number(value)
|
|
146
|
+
if (isNaN(num)) {
|
|
147
|
+
return 'Please enter a valid number'
|
|
148
|
+
}
|
|
149
|
+
if (fieldDef.min !== undefined && num < fieldDef.min) {
|
|
150
|
+
return `Value must be at least ${fieldDef.min}`
|
|
151
|
+
}
|
|
152
|
+
if (fieldDef.max !== undefined && num > fieldDef.max) {
|
|
153
|
+
return `Value must be at most ${fieldDef.max}`
|
|
154
|
+
}
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
case 'text':
|
|
158
|
+
case 'textarea':
|
|
159
|
+
if (fieldDef.validation?.minLength && value.length < fieldDef.validation.minLength) {
|
|
160
|
+
return `Must be at least ${fieldDef.validation.minLength} characters`
|
|
161
|
+
}
|
|
162
|
+
if (fieldDef.validation?.maxLength && value.length > fieldDef.validation.maxLength) {
|
|
163
|
+
return `Must be at most ${fieldDef.validation.maxLength} characters`
|
|
164
|
+
}
|
|
165
|
+
if (fieldDef.validation?.pattern && !new RegExp(fieldDef.validation.pattern).test(value)) {
|
|
166
|
+
return 'Invalid format'
|
|
167
|
+
}
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null
|
|
172
|
+
}, [fields])
|
|
173
|
+
|
|
174
|
+
// Validate all fields
|
|
175
|
+
const validateForm = useCallback((values: Record<string, any>): Record<string, string> => {
|
|
176
|
+
const newErrors: Record<string, string> = {}
|
|
177
|
+
|
|
178
|
+
// Run field-level validation
|
|
179
|
+
fields.forEach(field => {
|
|
180
|
+
if (!field.name) return
|
|
181
|
+
const fieldErr = validateField(field.name, values[field.name])
|
|
182
|
+
if (fieldErr) {
|
|
183
|
+
newErrors[field.name] = fieldErr
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Run custom validation
|
|
188
|
+
if (validate) {
|
|
189
|
+
const customErrors = validate(values)
|
|
190
|
+
Object.assign(newErrors, customErrors)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return newErrors
|
|
194
|
+
}, [fields, validate, validateField])
|
|
195
|
+
|
|
196
|
+
// Handle field change
|
|
197
|
+
const handleChange = useCallback((field: string, value: any) => {
|
|
198
|
+
const newData = { ...data, [field]: value }
|
|
199
|
+
setData(newData)
|
|
200
|
+
|
|
201
|
+
// Clear error for this field when user starts typing
|
|
202
|
+
if (errors[field]) {
|
|
203
|
+
setErrors(prev => {
|
|
204
|
+
const newErrors = { ...prev }
|
|
205
|
+
delete newErrors[field]
|
|
206
|
+
return newErrors
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Mark field as touched
|
|
211
|
+
setTouched(prev => ({ ...prev, [field]: true }))
|
|
212
|
+
|
|
213
|
+
// Call onChange callback
|
|
214
|
+
onChange?.(newData)
|
|
215
|
+
}, [data, errors, onChange])
|
|
216
|
+
|
|
217
|
+
// Handle field blur
|
|
218
|
+
const handleBlur = useCallback((field: string) => {
|
|
219
|
+
setTouched(prev => ({ ...prev, [field]: true }))
|
|
220
|
+
|
|
221
|
+
// Validate field on blur
|
|
222
|
+
const error = validateField(field, data[field])
|
|
223
|
+
setErrors(prev => ({
|
|
224
|
+
...prev,
|
|
225
|
+
...(error ? { [field]: error } : { [field]: '' })
|
|
226
|
+
}))
|
|
227
|
+
}, [data, validateField])
|
|
228
|
+
|
|
229
|
+
// Handle form submission
|
|
230
|
+
const handleSubmit = useCallback(async (e?: React.FormEvent) => {
|
|
231
|
+
e?.preventDefault()
|
|
232
|
+
|
|
233
|
+
// Mark all fields as touched
|
|
234
|
+
const allTouched = fields.reduce((acc, field) => {
|
|
235
|
+
if (field.name) acc[field.name] = true
|
|
236
|
+
return acc
|
|
237
|
+
}, {} as Record<string, boolean>)
|
|
238
|
+
setTouched(allTouched)
|
|
239
|
+
|
|
240
|
+
// Validate form
|
|
241
|
+
const newErrors = validateForm(data)
|
|
242
|
+
setErrors(newErrors)
|
|
243
|
+
|
|
244
|
+
const isValid = Object.keys(newErrors).length === 0
|
|
245
|
+
if (!isValid) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setIsSubmitting(true)
|
|
250
|
+
setSubmitCount(prev => prev + 1)
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await onSubmit?.(data)
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('Form submission error:', error)
|
|
256
|
+
} finally {
|
|
257
|
+
setIsSubmitting(false)
|
|
258
|
+
}
|
|
259
|
+
}, [data, fields, validateForm, onSubmit])
|
|
260
|
+
|
|
261
|
+
// Reset form
|
|
262
|
+
const resetForm = useCallback(() => {
|
|
263
|
+
setData(initialValues)
|
|
264
|
+
setErrors({})
|
|
265
|
+
setTouched({})
|
|
266
|
+
setIsSubmitting(false)
|
|
267
|
+
setSubmitCount(0)
|
|
268
|
+
}, [initialValues])
|
|
269
|
+
|
|
270
|
+
// Set field value
|
|
271
|
+
const setFieldValue = useCallback((field: string, value: any) => {
|
|
272
|
+
handleChange(field, value)
|
|
273
|
+
}, [handleChange])
|
|
274
|
+
|
|
275
|
+
// Set field error
|
|
276
|
+
const setFieldError = useCallback((field: string, error: string) => {
|
|
277
|
+
setErrors(prev => ({
|
|
278
|
+
...prev,
|
|
279
|
+
[field]: error
|
|
280
|
+
}))
|
|
281
|
+
}, [])
|
|
282
|
+
|
|
283
|
+
// Clear all errors
|
|
284
|
+
const clearErrors = useCallback(() => {
|
|
285
|
+
setErrors({})
|
|
286
|
+
}, [])
|
|
287
|
+
|
|
288
|
+
// Calculate derived state
|
|
289
|
+
const isValid = Object.keys(errors).length === 0
|
|
290
|
+
const isDirty = Object.keys(touched).some(field => data[field] !== initialValues[field])
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
data,
|
|
294
|
+
errors,
|
|
295
|
+
touched,
|
|
296
|
+
isSubmitting,
|
|
297
|
+
isValid,
|
|
298
|
+
isDirty,
|
|
299
|
+
handleChange,
|
|
300
|
+
handleBlur,
|
|
301
|
+
handleSubmit,
|
|
302
|
+
resetForm,
|
|
303
|
+
setFieldValue,
|
|
304
|
+
setFieldError,
|
|
305
|
+
clearErrors
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/hooks/useListSchema
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-hook
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Resolves the `DesignSchema` and target `View` for a list page. Because
|
|
8
|
+
* the `design_schema` is stamp-copied onto records, this hook uses a
|
|
9
|
+
* two-stage resolution strategy to handle stale or missing stamps:
|
|
10
|
+
*
|
|
11
|
+
* **Resolution order:**
|
|
12
|
+
* 1. Fetch one sample record via `admin-data?action=list&limit=1`
|
|
13
|
+
* 2. If the sample record's `design_schema` has the requested view, use it
|
|
14
|
+
* 3. Otherwise, fall back to the canonical `types` table entry for the entity kind
|
|
15
|
+
* 4. If no records exist, return a minimal fallback schema so the page renders
|
|
16
|
+
*
|
|
17
|
+
* **Auth retry:** Auth errors (JWT not ready) trigger a 300 ms retry instead
|
|
18
|
+
* of silently falling back to an empty schema (which would render a blank list).
|
|
19
|
+
*
|
|
20
|
+
* **Entity → kind mapping** (used for the types fallback):
|
|
21
|
+
* `accounts→account`, `people→person`, `items→item`, `threads→thread`,
|
|
22
|
+
* `messages→message`, `links→link`, `attachments→attachment`, `watchers→watcher`
|
|
23
|
+
*
|
|
24
|
+
* @seeAlso src/lib/api.ts (apiFetch)
|
|
25
|
+
* @seeAlso src/types/types.ts (DesignSchema, View)
|
|
26
|
+
* @seeAlso src/components/runtime/DataListPage.tsx (primary consumer)
|
|
27
|
+
* @seeAlso functions/admin-data.ts (list action endpoint)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { useState, useEffect } from 'react'
|
|
31
|
+
import { apiFetch } from '../lib/api'
|
|
32
|
+
import { DesignSchema, View } from '../types/types'
|
|
33
|
+
|
|
34
|
+
// ─── TYPES ───────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for `useListSchema`.
|
|
38
|
+
*
|
|
39
|
+
* @prop entity - Entity table name (e.g. `'accounts'`, `'items'`)
|
|
40
|
+
* @prop viewSlug - View key in `design_schema.views`; defaults to `'default_list'`
|
|
41
|
+
*/
|
|
42
|
+
interface UseListSchemaOptions {
|
|
43
|
+
entity: string
|
|
44
|
+
viewSlug?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return value of `useListSchema`.
|
|
49
|
+
*
|
|
50
|
+
* @prop schema - Resolved `DesignSchema` or null while loading
|
|
51
|
+
* @prop view - The specific `View` for `viewSlug`, or null while loading
|
|
52
|
+
* @prop loading - True during initial fetch and auth retry
|
|
53
|
+
* @prop error - Error message if schema resolution fails
|
|
54
|
+
* @prop refetch - Manually re-run schema resolution
|
|
55
|
+
*/
|
|
56
|
+
interface UseListSchemaResult {
|
|
57
|
+
schema: DesignSchema | null
|
|
58
|
+
view: View | null
|
|
59
|
+
loading: boolean
|
|
60
|
+
error: string | null
|
|
61
|
+
refetch: () => void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── HOOK ────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the `DesignSchema` and target `View` for a list page, using a
|
|
68
|
+
* two-stage fallback strategy (stamped record → canonical type → minimal fallback).
|
|
69
|
+
*
|
|
70
|
+
* @param options.entity - Entity name, e.g. `'accounts'`
|
|
71
|
+
* @param options.viewSlug - View key; defaults to `'default_list'`
|
|
72
|
+
* @returns `UseListSchemaResult` — schema, view, loading, error, refetch
|
|
73
|
+
*
|
|
74
|
+
* @inputSpec options.entity: string — must be non-empty or an error is set
|
|
75
|
+
* @outputSpec schema: DesignSchema — minimal fallback if no records exist
|
|
76
|
+
* @outputSpec view: View — matches `viewSlug` from the resolved schema
|
|
77
|
+
* @throws never (all errors are caught and stored in `error` state)
|
|
78
|
+
* @sideEffects Network requests (1–2 calls); React state mutations; 300ms setTimeout on auth retry
|
|
79
|
+
* @calledBy DataListPage.tsx
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```tsx
|
|
83
|
+
* const { schema, view, loading } = useListSchema({ entity: 'accounts' })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function useListSchema(options: UseListSchemaOptions): UseListSchemaResult {
|
|
87
|
+
const { entity, viewSlug = 'default_list' } = options
|
|
88
|
+
|
|
89
|
+
const [schema, setSchema] = useState<DesignSchema | null>(null)
|
|
90
|
+
const [view, setView] = useState<View | null>(null)
|
|
91
|
+
const [loading, setLoading] = useState(true)
|
|
92
|
+
const [error, setError] = useState<string | null>(null)
|
|
93
|
+
|
|
94
|
+
const fetchSchema = async () => {
|
|
95
|
+
let cancelled = false
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
setLoading(true)
|
|
99
|
+
setError(null)
|
|
100
|
+
|
|
101
|
+
// Fetch a sample record to get its design_schema
|
|
102
|
+
const response = await apiFetch(`/api/admin-data?action=list&entity=${entity}&limit=1`)
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`Failed to fetch sample record: ${response.statusText}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const data = await response.json()
|
|
109
|
+
|
|
110
|
+
if (data.error) {
|
|
111
|
+
// Auth errors (session not yet ready) should not silently fall back to
|
|
112
|
+
// an empty schema — that causes the list to render with no columns.
|
|
113
|
+
// Retry once after a short delay to allow the session to hydrate.
|
|
114
|
+
if (
|
|
115
|
+
data.error.includes('Invalid authentication') ||
|
|
116
|
+
data.error.includes('Unauthorized') ||
|
|
117
|
+
data.error.includes('JWT')
|
|
118
|
+
) {
|
|
119
|
+
if (cancelled) return
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 300))
|
|
121
|
+
if (!cancelled) fetchSchema()
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
throw new Error(data.error || 'Failed to fetch records')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No records yet — use a minimal fallback schema so the list page still renders
|
|
128
|
+
if (!data.data || data.data.length === 0) {
|
|
129
|
+
if (cancelled) return
|
|
130
|
+
const fallback: DesignSchema = {
|
|
131
|
+
fields: {},
|
|
132
|
+
record_permissions: {},
|
|
133
|
+
views: {
|
|
134
|
+
[viewSlug]: {
|
|
135
|
+
type: 'list',
|
|
136
|
+
label: 'Default List',
|
|
137
|
+
fields: {
|
|
138
|
+
id: { sortable: false, display_type: 'text' },
|
|
139
|
+
created_at: { sortable: true, display_type: 'timestamp' }
|
|
140
|
+
},
|
|
141
|
+
display: 'table',
|
|
142
|
+
default_sort: { field: 'created_at', direction: 'desc' }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
setSchema(fallback)
|
|
147
|
+
setView(fallback.views![viewSlug]!)
|
|
148
|
+
setLoading(false)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sampleRecord = data.data[0]
|
|
153
|
+
|
|
154
|
+
// If the sample record has a design_schema with the requested view, use it directly
|
|
155
|
+
const stampedSchema = sampleRecord.design_schema as DesignSchema | undefined
|
|
156
|
+
const stampedView = stampedSchema?.views?.[viewSlug]
|
|
157
|
+
|
|
158
|
+
if (stampedSchema && stampedView) {
|
|
159
|
+
if (cancelled) return
|
|
160
|
+
setSchema(stampedSchema)
|
|
161
|
+
setView(stampedView)
|
|
162
|
+
} else {
|
|
163
|
+
// Schema missing or stale — fetch current design_schema from the types table
|
|
164
|
+
const kindMap: Record<string, string> = { accounts: 'account', people: 'person', items: 'item', threads: 'thread', messages: 'message', links: 'link', attachments: 'attachment', watchers: 'watcher', item_progress: 'progress' }
|
|
165
|
+
const kind = kindMap[entity]
|
|
166
|
+
let resolvedSchema: DesignSchema | null = null
|
|
167
|
+
|
|
168
|
+
if (kind) {
|
|
169
|
+
const typeResp = await apiFetch(`/api/types?kind=${kind}&limit=1`)
|
|
170
|
+
if (typeResp.ok) {
|
|
171
|
+
const typeData = await typeResp.json()
|
|
172
|
+
const typeRecord = typeData.data?.[0]
|
|
173
|
+
if (typeRecord?.design_schema) {
|
|
174
|
+
resolvedSchema = typeRecord.design_schema as DesignSchema
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (resolvedSchema) {
|
|
180
|
+
const resolvedView = resolvedSchema.views?.[viewSlug]
|
|
181
|
+
if (resolvedView) {
|
|
182
|
+
if (cancelled) return
|
|
183
|
+
setSchema(resolvedSchema)
|
|
184
|
+
setView(resolvedView)
|
|
185
|
+
} else {
|
|
186
|
+
// Schema found but view key missing — use minimal fallback
|
|
187
|
+
const fallback: DesignSchema = {
|
|
188
|
+
fields: {},
|
|
189
|
+
record_permissions: {},
|
|
190
|
+
views: {
|
|
191
|
+
[viewSlug]: {
|
|
192
|
+
type: 'list',
|
|
193
|
+
label: entity.charAt(0).toUpperCase() + entity.slice(1),
|
|
194
|
+
fields: {
|
|
195
|
+
id: { sortable: false, display_type: 'text' },
|
|
196
|
+
created_at: { sortable: true, display_type: 'timestamp' }
|
|
197
|
+
},
|
|
198
|
+
display: 'table',
|
|
199
|
+
default_sort: { field: 'created_at', direction: 'desc' }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (cancelled) return
|
|
204
|
+
setSchema(fallback)
|
|
205
|
+
setView(fallback.views![viewSlug]!)
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// No type found in registry — use minimal fallback
|
|
209
|
+
const fallback: DesignSchema = {
|
|
210
|
+
fields: {},
|
|
211
|
+
record_permissions: {},
|
|
212
|
+
views: {
|
|
213
|
+
[viewSlug]: {
|
|
214
|
+
type: 'list',
|
|
215
|
+
label: entity.charAt(0).toUpperCase() + entity.slice(1),
|
|
216
|
+
fields: {
|
|
217
|
+
id: { sortable: false, display_type: 'text' },
|
|
218
|
+
created_at: { sortable: true, display_type: 'timestamp' }
|
|
219
|
+
},
|
|
220
|
+
display: 'table',
|
|
221
|
+
default_sort: { field: 'created_at', direction: 'desc' }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (cancelled) return
|
|
226
|
+
setSchema(fallback)
|
|
227
|
+
setView(fallback.views![viewSlug]!)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (!cancelled) {
|
|
233
|
+
setError(err instanceof Error ? err.message : 'Failed to load schema')
|
|
234
|
+
setSchema(null)
|
|
235
|
+
setView(null)
|
|
236
|
+
}
|
|
237
|
+
} finally {
|
|
238
|
+
if (!cancelled) {
|
|
239
|
+
setLoading(false)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return () => {
|
|
244
|
+
cancelled = true
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (entity) {
|
|
250
|
+
fetchSchema()
|
|
251
|
+
} else {
|
|
252
|
+
setError('Entity is required')
|
|
253
|
+
setLoading(false)
|
|
254
|
+
}
|
|
255
|
+
}, [entity, viewSlug])
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
schema,
|
|
259
|
+
view,
|
|
260
|
+
loading,
|
|
261
|
+
error,
|
|
262
|
+
refetch: fetchSchema
|
|
263
|
+
}
|
|
264
|
+
}
|