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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/components/shared/FieldRenderer
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-component
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Low-level field rendering primitive. Given a `FieldDefinition` and a
|
|
8
|
+
* current value, renders the appropriate input widget (edit mode) or
|
|
9
|
+
* display element (read-only mode).
|
|
10
|
+
*
|
|
11
|
+
* **Render type resolution** (`resolveRenderType`):
|
|
12
|
+
* 1. If `displayType` is set (from the view config), it maps to a canonical
|
|
13
|
+
* render type first — this lets a view override how a field looks without
|
|
14
|
+
* changing the underlying `FieldDefinition`.
|
|
15
|
+
* 2. Falls back to `field.data_type`.
|
|
16
|
+
*
|
|
17
|
+
* **Supported render types:**
|
|
18
|
+
* `text`, `textarea`, `email`, `phone`, `url`, `number`, `date`,
|
|
19
|
+
* `datetime`, `select`, `multiselect`, `radio`, `checkbox`, `json`,
|
|
20
|
+
* `file`, `color`, `range`
|
|
21
|
+
*
|
|
22
|
+
* **Error display:** when `error` is set, a red helper text is shown below
|
|
23
|
+
* the field and `border-red-500` is applied where applicable.
|
|
24
|
+
*
|
|
25
|
+
* **Label + description:** rendered above / below the input respectively,
|
|
26
|
+
* with a red `*` suffix when `field.required` is true.
|
|
27
|
+
*
|
|
28
|
+
* @seeAlso src/components/shared/SchemaFields.tsx (mounts this component)
|
|
29
|
+
* @seeAlso src/types/types.ts (FieldDefinition)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import React from 'react'
|
|
33
|
+
import { FieldDefinition } from '../../types/types'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Props for `FieldRenderer`.
|
|
37
|
+
*
|
|
38
|
+
* @prop field - Full field definition including data_type, options, validation
|
|
39
|
+
* @prop value - Current controlled value
|
|
40
|
+
* @prop onChange - Value change callback (omit or pass `undefined` for read-only)
|
|
41
|
+
* @prop onBlur - Optional blur callback for validation triggers
|
|
42
|
+
* @prop readonly - If true, renders a display element instead of an input
|
|
43
|
+
* @prop error - Validation error message to display below the field
|
|
44
|
+
* @prop displayType - View-config widget override (e.g. `'textarea'`, `'select'`)
|
|
45
|
+
*/
|
|
46
|
+
interface FieldRendererProps {
|
|
47
|
+
field: FieldDefinition
|
|
48
|
+
value: any
|
|
49
|
+
onChange?: (value: any) => void
|
|
50
|
+
onBlur?: () => void
|
|
51
|
+
readonly?: boolean
|
|
52
|
+
error?: string
|
|
53
|
+
displayType?: string // From view config — controls rendering without polluting field data contract
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the canonical render type for a field.
|
|
58
|
+
*
|
|
59
|
+
* `displayType` (from view config) takes precedence over `field.data_type`
|
|
60
|
+
* so view authors can choose a different widget without changing the
|
|
61
|
+
* underlying schema.
|
|
62
|
+
*
|
|
63
|
+
* @param field - Field definition
|
|
64
|
+
* @param displayType - Optional view-config widget override
|
|
65
|
+
* @returns Canonical render type string (e.g. `'text'`, `'select'`, `'checkbox'`)
|
|
66
|
+
*/
|
|
67
|
+
function resolveRenderType(field: FieldDefinition, displayType?: string): string {
|
|
68
|
+
// displayType from view config overrides data_type for rendering decisions
|
|
69
|
+
if (displayType) {
|
|
70
|
+
switch (displayType) {
|
|
71
|
+
case 'textarea':
|
|
72
|
+
case 'rich_text':
|
|
73
|
+
return 'textarea'
|
|
74
|
+
case 'select':
|
|
75
|
+
return 'select'
|
|
76
|
+
case 'multiselect':
|
|
77
|
+
return 'multiselect'
|
|
78
|
+
case 'radio':
|
|
79
|
+
return 'radio'
|
|
80
|
+
case 'checkbox':
|
|
81
|
+
case 'switch':
|
|
82
|
+
return 'checkbox'
|
|
83
|
+
case 'date_picker':
|
|
84
|
+
return 'date'
|
|
85
|
+
case 'datetime_picker':
|
|
86
|
+
return 'datetime'
|
|
87
|
+
case 'color_picker':
|
|
88
|
+
return 'color'
|
|
89
|
+
case 'range_slider':
|
|
90
|
+
return 'range'
|
|
91
|
+
case 'file_upload':
|
|
92
|
+
case 'image_upload':
|
|
93
|
+
return 'file'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Fall back to data_type
|
|
97
|
+
switch (field.data_type) {
|
|
98
|
+
case 'textarea':
|
|
99
|
+
case 'rich_text':
|
|
100
|
+
return 'textarea'
|
|
101
|
+
case 'email':
|
|
102
|
+
return 'email'
|
|
103
|
+
case 'phone':
|
|
104
|
+
return 'phone'
|
|
105
|
+
case 'url':
|
|
106
|
+
return 'url'
|
|
107
|
+
case 'number':
|
|
108
|
+
case 'currency':
|
|
109
|
+
case 'range':
|
|
110
|
+
return 'number'
|
|
111
|
+
case 'date':
|
|
112
|
+
return 'date'
|
|
113
|
+
case 'datetime':
|
|
114
|
+
return 'datetime'
|
|
115
|
+
case 'boolean':
|
|
116
|
+
case 'checkbox':
|
|
117
|
+
return 'checkbox'
|
|
118
|
+
case 'select':
|
|
119
|
+
return 'select'
|
|
120
|
+
case 'multiselect':
|
|
121
|
+
return 'multiselect'
|
|
122
|
+
case 'radio':
|
|
123
|
+
return 'radio'
|
|
124
|
+
case 'color':
|
|
125
|
+
return 'color'
|
|
126
|
+
case 'file':
|
|
127
|
+
case 'image':
|
|
128
|
+
return 'file'
|
|
129
|
+
case 'json':
|
|
130
|
+
return 'json'
|
|
131
|
+
default:
|
|
132
|
+
return 'text'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Renders a single schema field as an input widget or read-only display.
|
|
138
|
+
*
|
|
139
|
+
* @param props - `FieldRendererProps`
|
|
140
|
+
* @returns Label + field widget + description + error message
|
|
141
|
+
* @sideEffects none (delegates changes to `onChange`)
|
|
142
|
+
*/
|
|
143
|
+
export function FieldRenderer({ field, value, onChange, readonly = false, error, displayType }: FieldRendererProps) {
|
|
144
|
+
const renderType = resolveRenderType(field, displayType)
|
|
145
|
+
|
|
146
|
+
const renderField = () => {
|
|
147
|
+
switch (renderType) {
|
|
148
|
+
case 'text':
|
|
149
|
+
return readonly ? (
|
|
150
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
151
|
+
{value || <span className="text-slate-400 italic">—</span>}
|
|
152
|
+
</div>
|
|
153
|
+
) : (
|
|
154
|
+
<input
|
|
155
|
+
type="text"
|
|
156
|
+
value={value || ''}
|
|
157
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
158
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
159
|
+
placeholder={field.placeholder}
|
|
160
|
+
/>
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
case 'textarea':
|
|
164
|
+
return readonly ? (
|
|
165
|
+
<div className={`text-slate-900 whitespace-pre-wrap ${error ? 'border-red-500' : ''}`}>
|
|
166
|
+
{value || <span className="text-slate-400 italic">—</span>}
|
|
167
|
+
</div>
|
|
168
|
+
) : (
|
|
169
|
+
<textarea
|
|
170
|
+
value={value || ''}
|
|
171
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
172
|
+
className={`textarea ${error ? 'border-red-500' : ''}`}
|
|
173
|
+
placeholder={field.placeholder}
|
|
174
|
+
rows={field.rows || 3}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
case 'email':
|
|
179
|
+
return readonly ? (
|
|
180
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
181
|
+
{value ? <a href={`mailto:${value}`} className="text-blue-600 hover:text-blue-800">{value}</a> : <span className="text-slate-400 italic">—</span>}
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
<input
|
|
185
|
+
type="email"
|
|
186
|
+
value={value || ''}
|
|
187
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
188
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
189
|
+
placeholder={field.placeholder}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
case 'phone':
|
|
194
|
+
return readonly ? (
|
|
195
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
196
|
+
{value || <span className="text-slate-400 italic">—</span>}
|
|
197
|
+
</div>
|
|
198
|
+
) : (
|
|
199
|
+
<input
|
|
200
|
+
type="tel"
|
|
201
|
+
value={value || ''}
|
|
202
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
203
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
204
|
+
placeholder={field.placeholder}
|
|
205
|
+
/>
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
case 'number':
|
|
209
|
+
return (
|
|
210
|
+
<input
|
|
211
|
+
type="number"
|
|
212
|
+
value={value ?? ''}
|
|
213
|
+
onChange={(e) => onChange?.(e.target.value ? Number(e.target.value) : null)}
|
|
214
|
+
readOnly={readonly}
|
|
215
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
216
|
+
placeholder={field.placeholder}
|
|
217
|
+
min={field.min ?? field.validation?.min}
|
|
218
|
+
max={field.max ?? field.validation?.max}
|
|
219
|
+
step={field.step ?? field.validation?.step}
|
|
220
|
+
/>
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
case 'date':
|
|
224
|
+
return (
|
|
225
|
+
<input
|
|
226
|
+
type="date"
|
|
227
|
+
value={value ? new Date(value).toISOString().split('T')[0] : ''}
|
|
228
|
+
onChange={(e) => onChange?.(e.target.value || null)}
|
|
229
|
+
readOnly={readonly}
|
|
230
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
case 'datetime':
|
|
235
|
+
return (
|
|
236
|
+
<input
|
|
237
|
+
type="datetime-local"
|
|
238
|
+
value={value ? new Date(value).toISOString().slice(0, 16) : ''}
|
|
239
|
+
onChange={(e) => onChange?.(e.target.value || null)}
|
|
240
|
+
readOnly={readonly}
|
|
241
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
242
|
+
/>
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
case 'select':
|
|
246
|
+
return readonly ? (
|
|
247
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
248
|
+
{(() => {
|
|
249
|
+
if (!value) return <span className="text-slate-400 italic">—</span>
|
|
250
|
+
const option = field.options?.find((opt) => {
|
|
251
|
+
const optVal = typeof opt === 'string' ? opt : opt.value
|
|
252
|
+
return optVal === value
|
|
253
|
+
})
|
|
254
|
+
return typeof option === 'string' ? option : option?.label || value
|
|
255
|
+
})()}
|
|
256
|
+
</div>
|
|
257
|
+
) : (
|
|
258
|
+
<select
|
|
259
|
+
value={value || ''}
|
|
260
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
261
|
+
className={`select ${error ? 'border-red-500' : ''}`}
|
|
262
|
+
>
|
|
263
|
+
<option value="">Select...</option>
|
|
264
|
+
{field.options?.map((option) => {
|
|
265
|
+
const optVal = typeof option === 'string' ? option : option.value
|
|
266
|
+
const optLabel = typeof option === 'string' ? option : option.label
|
|
267
|
+
return (
|
|
268
|
+
<option key={optVal} value={optVal}>
|
|
269
|
+
{optLabel}
|
|
270
|
+
</option>
|
|
271
|
+
)
|
|
272
|
+
})}
|
|
273
|
+
</select>
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
case 'multiselect':
|
|
277
|
+
return (
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
{field.options?.map((option) => {
|
|
280
|
+
const optionValue = typeof option === 'string' ? option : option.value
|
|
281
|
+
const optionLabel = typeof option === 'string' ? option : option.label
|
|
282
|
+
return (
|
|
283
|
+
<label key={optionValue} className="flex items-center">
|
|
284
|
+
<input
|
|
285
|
+
type="checkbox"
|
|
286
|
+
checked={Array.isArray(value) && value.includes(optionValue)}
|
|
287
|
+
onChange={(e) => {
|
|
288
|
+
const currentValue = Array.isArray(value) ? value : []
|
|
289
|
+
if (e.target.checked) {
|
|
290
|
+
onChange?.([...currentValue, optionValue])
|
|
291
|
+
} else {
|
|
292
|
+
onChange?.(currentValue.filter((v: string) => v !== optionValue))
|
|
293
|
+
}
|
|
294
|
+
}}
|
|
295
|
+
disabled={readonly}
|
|
296
|
+
className="mr-2"
|
|
297
|
+
/>
|
|
298
|
+
<span>{optionLabel}</span>
|
|
299
|
+
</label>
|
|
300
|
+
)
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
case 'radio':
|
|
306
|
+
return (
|
|
307
|
+
<div className="space-y-2">
|
|
308
|
+
{field.options?.map((option) => {
|
|
309
|
+
const optionValue = typeof option === 'string' ? option : option.value
|
|
310
|
+
const optionLabel = typeof option === 'string' ? option : option.label
|
|
311
|
+
return (
|
|
312
|
+
<label key={optionValue} className="flex items-center">
|
|
313
|
+
<input
|
|
314
|
+
type="radio"
|
|
315
|
+
name={field.name}
|
|
316
|
+
value={optionValue}
|
|
317
|
+
checked={value === optionValue}
|
|
318
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
319
|
+
disabled={readonly}
|
|
320
|
+
className="mr-2"
|
|
321
|
+
/>
|
|
322
|
+
<span>{optionLabel}</span>
|
|
323
|
+
</label>
|
|
324
|
+
)
|
|
325
|
+
})}
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
case 'checkbox':
|
|
330
|
+
return readonly ? (
|
|
331
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
332
|
+
{Boolean(value) ? 'Yes' : 'No'}
|
|
333
|
+
</div>
|
|
334
|
+
) : (
|
|
335
|
+
<label className="flex items-center">
|
|
336
|
+
<input
|
|
337
|
+
type="checkbox"
|
|
338
|
+
checked={Boolean(value)}
|
|
339
|
+
onChange={(e) => onChange?.(e.target.checked)}
|
|
340
|
+
className="mr-2"
|
|
341
|
+
/>
|
|
342
|
+
<span>{field.label}</span>
|
|
343
|
+
</label>
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
case 'json':
|
|
347
|
+
return readonly ? (
|
|
348
|
+
<pre className="bg-slate-100 p-3 rounded-md text-sm overflow-x-auto">
|
|
349
|
+
{JSON.stringify(value, null, 2)}
|
|
350
|
+
</pre>
|
|
351
|
+
) : (
|
|
352
|
+
<textarea
|
|
353
|
+
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
|
|
354
|
+
onChange={(e) => {
|
|
355
|
+
try {
|
|
356
|
+
onChange?.(JSON.parse(e.target.value))
|
|
357
|
+
} catch {
|
|
358
|
+
// Invalid JSON, don't update
|
|
359
|
+
}
|
|
360
|
+
}}
|
|
361
|
+
className={`textarea font-mono text-sm ${error ? 'border-red-500' : ''}`}
|
|
362
|
+
rows={field.rows || 6}
|
|
363
|
+
placeholder="Enter valid JSON"
|
|
364
|
+
/>
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
case 'url':
|
|
368
|
+
return readonly ? (
|
|
369
|
+
<div className={`text-slate-900 ${error ? 'border-red-500' : ''}`}>
|
|
370
|
+
{value ? <a href={value} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800">{value}</a> : <span className="text-slate-400 italic">—</span>}
|
|
371
|
+
</div>
|
|
372
|
+
) : (
|
|
373
|
+
<input
|
|
374
|
+
type="url"
|
|
375
|
+
value={value || ''}
|
|
376
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
377
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
378
|
+
placeholder={field.placeholder || 'https://example.com'}
|
|
379
|
+
/>
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
case 'file':
|
|
383
|
+
return readonly ? (
|
|
384
|
+
value ? (
|
|
385
|
+
<div className="text-sm">
|
|
386
|
+
<a href={value} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:text-blue-800">
|
|
387
|
+
View File
|
|
388
|
+
</a>
|
|
389
|
+
</div>
|
|
390
|
+
) : (
|
|
391
|
+
<span className="text-slate-500">No file</span>
|
|
392
|
+
)
|
|
393
|
+
) : (
|
|
394
|
+
<input
|
|
395
|
+
type="file"
|
|
396
|
+
onChange={(e) => {
|
|
397
|
+
const file = e.target.files?.[0]
|
|
398
|
+
if (file) onChange?.(file.name)
|
|
399
|
+
}}
|
|
400
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
401
|
+
/>
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
case 'color':
|
|
405
|
+
return (
|
|
406
|
+
<div className="flex items-center space-x-2">
|
|
407
|
+
<input
|
|
408
|
+
type="color"
|
|
409
|
+
value={value || '#000000'}
|
|
410
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
411
|
+
disabled={readonly}
|
|
412
|
+
className="h-10 w-20"
|
|
413
|
+
/>
|
|
414
|
+
<input
|
|
415
|
+
type="text"
|
|
416
|
+
value={value || ''}
|
|
417
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
418
|
+
readOnly={readonly}
|
|
419
|
+
className={`input flex-1 ${error ? 'border-red-500' : ''}`}
|
|
420
|
+
placeholder="#000000"
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
case 'range':
|
|
426
|
+
return (
|
|
427
|
+
<div className="space-y-2">
|
|
428
|
+
<input
|
|
429
|
+
type="range"
|
|
430
|
+
value={value ?? field.min ?? field.validation?.min ?? 0}
|
|
431
|
+
onChange={(e) => onChange?.(Number(e.target.value))}
|
|
432
|
+
disabled={readonly}
|
|
433
|
+
className="w-full"
|
|
434
|
+
min={field.min ?? field.validation?.min}
|
|
435
|
+
max={field.max ?? field.validation?.max}
|
|
436
|
+
step={field.step ?? field.validation?.step}
|
|
437
|
+
/>
|
|
438
|
+
<div className="text-sm text-slate-600">
|
|
439
|
+
Value: {value ?? field.min ?? 0}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
default:
|
|
445
|
+
return (
|
|
446
|
+
<input
|
|
447
|
+
type="text"
|
|
448
|
+
value={value || ''}
|
|
449
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
450
|
+
readOnly={readonly}
|
|
451
|
+
className={`input ${error ? 'border-red-500' : ''}`}
|
|
452
|
+
placeholder={field.placeholder}
|
|
453
|
+
/>
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return (
|
|
459
|
+
<div className="space-y-1">
|
|
460
|
+
{field.label && (
|
|
461
|
+
<label className="label">
|
|
462
|
+
{field.label}
|
|
463
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
464
|
+
</label>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{renderField()}
|
|
468
|
+
|
|
469
|
+
{field.description && (
|
|
470
|
+
<p className="text-xs text-slate-500">{field.description}</p>
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
{error && (
|
|
474
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
475
|
+
)}
|
|
476
|
+
</div>
|
|
477
|
+
)
|
|
478
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module src/components/shared/SchemaFields
|
|
3
|
+
* @audience installer
|
|
4
|
+
* @layer frontend-component
|
|
5
|
+
* @stability stable
|
|
6
|
+
*
|
|
7
|
+
* Schema-driven field grid. Renders an ordered list of `FieldDefinition`
|
|
8
|
+
* entries via `FieldRenderer`, managing the name→value mapping and
|
|
9
|
+
* propagating `onChange` calls back to the parent.
|
|
10
|
+
*
|
|
11
|
+
* **Value source resolution** (per field):
|
|
12
|
+
* - `system` flag set → `data[name]` (top-level column)
|
|
13
|
+
* - `system` flag unset → `data.data?.[name] ?? data[name]` (JSONB field
|
|
14
|
+
* with column fallback)
|
|
15
|
+
*
|
|
16
|
+
* **Layout:** two-column responsive grid by default (`twoColumn=true`);
|
|
17
|
+
* pass `twoColumn=false` for a single-column stacked layout.
|
|
18
|
+
*
|
|
19
|
+
* **Exports:**
|
|
20
|
+
* - `SchemaFields` — full editable/read-only field grid
|
|
21
|
+
* - `SchemaFieldDisplay` — read-only single-field key:value display row
|
|
22
|
+
* - `SchemaField` (unexported) — internal wrapper binding name to value
|
|
23
|
+
*
|
|
24
|
+
* @seeAlso src/components/shared/FieldRenderer.tsx
|
|
25
|
+
* @seeAlso src/types/types.ts (FieldDefinition)
|
|
26
|
+
* @seeAlso src/components/runtime/SchemaDetailForm.tsx (primary consumer)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import React from 'react'
|
|
30
|
+
import { FieldDefinition } from '../../types/types'
|
|
31
|
+
import { FieldRenderer } from './FieldRenderer'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Props for `SchemaFields`.
|
|
35
|
+
*
|
|
36
|
+
* @prop fields - Ordered array of field definitions to render
|
|
37
|
+
* @prop data - Record containing current field values; may be flat or
|
|
38
|
+
* nested under a `.data` key for JSONB fields
|
|
39
|
+
* @prop onChange - `(name, value)` callback; omit for pure read-only display
|
|
40
|
+
* @prop readonly - Passes read-only mode down to every `FieldRenderer`
|
|
41
|
+
* @prop errors - Per-field validation error messages
|
|
42
|
+
* @prop twoColumn - Two-column responsive grid (default: `true`)
|
|
43
|
+
* @prop displayTypes - Widget override map keyed by field name (from view config)
|
|
44
|
+
*/
|
|
45
|
+
interface SchemaFieldsProps {
|
|
46
|
+
fields: FieldDefinition[]
|
|
47
|
+
data: Record<string, any>
|
|
48
|
+
onChange?: (name: string, value: any) => void
|
|
49
|
+
readonly?: boolean
|
|
50
|
+
errors?: Record<string, string>
|
|
51
|
+
/** Render fields in a two-column grid (default: true) */
|
|
52
|
+
twoColumn?: boolean
|
|
53
|
+
/** display_type per field key, sourced from view config — never from field definitions */
|
|
54
|
+
displayTypes?: Record<string, string>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Renders a full schema field grid.
|
|
59
|
+
*
|
|
60
|
+
* @param props - `SchemaFieldsProps`
|
|
61
|
+
* @returns Two-column (or single-column) field grid, or an empty-state message
|
|
62
|
+
* @sideEffects none (delegates changes to `onChange`)
|
|
63
|
+
*/
|
|
64
|
+
export function SchemaFields({
|
|
65
|
+
fields,
|
|
66
|
+
data,
|
|
67
|
+
onChange,
|
|
68
|
+
readonly = false,
|
|
69
|
+
errors = {},
|
|
70
|
+
twoColumn = true,
|
|
71
|
+
displayTypes = {}
|
|
72
|
+
}: SchemaFieldsProps) {
|
|
73
|
+
if (!fields || fields.length === 0) {
|
|
74
|
+
return (
|
|
75
|
+
<p className="text-sm text-slate-500 italic">No schema fields defined for this type.</p>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={twoColumn ? 'grid grid-cols-1 md:grid-cols-2 gap-4' : 'space-y-4'}>
|
|
81
|
+
{fields.filter(f => !!f.name).map((field) => {
|
|
82
|
+
const name = field.name!
|
|
83
|
+
return (
|
|
84
|
+
<SchemaField
|
|
85
|
+
key={name}
|
|
86
|
+
field={field}
|
|
87
|
+
value={field.system ? data[name] : (data.data?.[name] ?? data[name])}
|
|
88
|
+
onChange={onChange}
|
|
89
|
+
readonly={readonly || field.readonly}
|
|
90
|
+
error={errors[name]}
|
|
91
|
+
displayType={displayTypes[name]}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Internal props for the `SchemaField` name-binding wrapper. */
|
|
100
|
+
interface SchemaFieldProps {
|
|
101
|
+
field: FieldDefinition
|
|
102
|
+
value: any
|
|
103
|
+
onChange?: (name: string, value: any) => void
|
|
104
|
+
readonly?: boolean
|
|
105
|
+
error?: string
|
|
106
|
+
displayType?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function SchemaField({ field, value, onChange, readonly, error, displayType }: SchemaFieldProps) {
|
|
110
|
+
return (
|
|
111
|
+
<FieldRenderer
|
|
112
|
+
field={field}
|
|
113
|
+
value={value}
|
|
114
|
+
onChange={readonly ? undefined : (val) => field.name && onChange?.(field.name, val)}
|
|
115
|
+
readonly={readonly}
|
|
116
|
+
error={error}
|
|
117
|
+
displayType={displayType}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read-only display of a single schema field value as a label:value row.
|
|
124
|
+
* Useful for compact detail views outside the full form layout.
|
|
125
|
+
*
|
|
126
|
+
* @param field - Field definition (used for label and data_type formatting)
|
|
127
|
+
* @param value - Raw value to display
|
|
128
|
+
* @returns A `<div>` with label on the left and formatted value on the right
|
|
129
|
+
* @sideEffects none (pure rendering)
|
|
130
|
+
*/
|
|
131
|
+
export function SchemaFieldDisplay({
|
|
132
|
+
field,
|
|
133
|
+
value
|
|
134
|
+
}: {
|
|
135
|
+
field: FieldDefinition
|
|
136
|
+
value: any
|
|
137
|
+
}) {
|
|
138
|
+
const displayValue = formatFieldValue(field, value)
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="flex justify-between items-start py-2">
|
|
142
|
+
<dt className="text-xs text-slate-500 font-medium flex-shrink-0 mr-4">
|
|
143
|
+
{field.label || field.name}:
|
|
144
|
+
</dt>
|
|
145
|
+
<dd className="text-sm text-slate-900 text-right">
|
|
146
|
+
{displayValue}
|
|
147
|
+
</dd>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Formats a raw field value for read-only display, applying type-specific
|
|
154
|
+
* transformations:
|
|
155
|
+
* - `boolean`/`checkbox` → `'Yes'` / `'No'`
|
|
156
|
+
* - `date` / `datetime` → locale string
|
|
157
|
+
* - `select` → resolves option label from `field.options`
|
|
158
|
+
* - `multiselect` → comma-joined option labels
|
|
159
|
+
* - `json` → `<pre>` code block
|
|
160
|
+
* - `url` → `<a>` link
|
|
161
|
+
* - Null/undefined/empty → em-dash placeholder
|
|
162
|
+
*
|
|
163
|
+
* @param field - Field definition
|
|
164
|
+
* @param value - Raw value
|
|
165
|
+
* @returns Formatted `ReactNode`
|
|
166
|
+
*/
|
|
167
|
+
function formatFieldValue(field: FieldDefinition, value: any): React.ReactNode {
|
|
168
|
+
if (value === null || value === undefined || value === '') {
|
|
169
|
+
return <span className="text-slate-400 italic">—</span>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
switch (field.data_type) {
|
|
173
|
+
case 'boolean':
|
|
174
|
+
case 'checkbox':
|
|
175
|
+
return value ? 'Yes' : 'No'
|
|
176
|
+
|
|
177
|
+
case 'date':
|
|
178
|
+
try {
|
|
179
|
+
return new Date(value).toLocaleDateString()
|
|
180
|
+
} catch {
|
|
181
|
+
return String(value)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'datetime':
|
|
185
|
+
try {
|
|
186
|
+
return new Date(value).toLocaleString()
|
|
187
|
+
} catch {
|
|
188
|
+
return String(value)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'select': {
|
|
192
|
+
const option = field.options?.find(o => typeof o === 'object' && o.value === value)
|
|
193
|
+
return option && typeof option === 'object' ? option.label : String(value)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case 'multiselect': {
|
|
197
|
+
if (!Array.isArray(value)) return String(value)
|
|
198
|
+
return value.map(v => {
|
|
199
|
+
const option = field.options?.find(o => typeof o === 'object' && o.value === v)
|
|
200
|
+
return option && typeof option === 'object' ? option.label : v
|
|
201
|
+
}).join(', ')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case 'json':
|
|
205
|
+
return (
|
|
206
|
+
<pre className="text-xs bg-slate-50 rounded p-2 max-w-xs overflow-x-auto">
|
|
207
|
+
{JSON.stringify(value, null, 2)}
|
|
208
|
+
</pre>
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
case 'url':
|
|
212
|
+
return (
|
|
213
|
+
<a
|
|
214
|
+
href={value}
|
|
215
|
+
target="_blank"
|
|
216
|
+
rel="noopener noreferrer"
|
|
217
|
+
className="text-blue-600 hover:text-blue-800 underline"
|
|
218
|
+
>
|
|
219
|
+
{value}
|
|
220
|
+
</a>
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
default:
|
|
224
|
+
return String(value)
|
|
225
|
+
}
|
|
226
|
+
}
|