@svgrid/enterprise 2.5.2 → 2.6.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/dist/SvAlertRuleEditor.svelte.d.ts +13 -0
- package/dist/SvAlertsManager.svelte.d.ts +14 -0
- package/dist/SvAlertsPanel.svelte.d.ts +9 -0
- package/dist/SvAuthGate.svelte.d.ts +22 -0
- package/dist/SvBoard.svelte.d.ts +35 -0
- package/dist/SvExportMenu.svelte.d.ts +61 -0
- package/dist/SvExpressionEditor.svelte.d.ts +12 -0
- package/dist/SvFileInput.svelte.d.ts +20 -0
- package/dist/SvGridAlerts.svelte.d.ts +77 -0
- package/dist/SvGridBoard.svelte.d.ts +30 -0
- package/dist/SvGridEditPanel.svelte.d.ts +73 -0
- package/dist/SvGridMasterDetail.svelte.d.ts +49 -0
- package/dist/SvGridScheduler.svelte.d.ts +30 -0
- package/dist/SvImportDialog.svelte.d.ts +88 -0
- package/dist/SvLookupInput.svelte.d.ts +21 -0
- package/dist/SvPivotDesigner.svelte.d.ts +176 -0
- package/dist/SvRecordDetail.svelte.d.ts +58 -0
- package/dist/SvSchedule.svelte.d.ts +29 -0
- package/dist/SvSchemaChart.svelte.d.ts +61 -0
- package/dist/SvSchemaDashboard.svelte.d.ts +46 -0
- package/dist/alerts/alert-engine-attach.d.ts +79 -0
- package/dist/alerts/alert-engine-attach.js +106 -0
- package/dist/alerts/alert-engine.d.ts +42 -0
- package/dist/alerts/alert-engine.js +196 -0
- package/dist/alerts/alert-formats.d.ts +26 -0
- package/dist/alerts/alert-formats.js +58 -0
- package/dist/alerts/alert-observer.d.ts +54 -0
- package/dist/alerts/alert-observer.js +156 -0
- package/dist/alerts/alert-scheduler.d.ts +30 -0
- package/dist/alerts/alert-scheduler.js +54 -0
- package/dist/alerts/alert-storage.d.ts +31 -0
- package/dist/alerts/alert-storage.js +92 -0
- package/dist/alerts/alert-store.svelte.d.ts +24 -0
- package/dist/alerts/alert-store.svelte.js +69 -0
- package/dist/alerts/alert-types.d.ts +86 -0
- package/dist/alerts/alert-types.js +1 -0
- package/dist/alerts.d.ts +2 -0
- package/{src/alerts.ts → dist/alerts.js} +9 -9
- package/dist/board.d.ts +7 -0
- package/{src/board.ts → dist/board.js} +13 -14
- package/dist/cdn/svgrid-enterprise.svelte-external.js +2987 -2981
- package/dist/designer/assets/{index-CM5xvaOy.js → index-BF0UH638.js} +3 -3
- package/dist/designer/assets/{src-D-ET9Mmo.js → src-BPJruwB9.js} +184 -184
- package/dist/designer/index.html +2 -2
- package/dist/edit-panel.d.ts +105 -0
- package/dist/edit-panel.js +350 -0
- package/dist/export-conditional.d.ts +31 -0
- package/dist/export-conditional.js +65 -0
- package/dist/export-ooxml.d.ts +93 -0
- package/dist/export-ooxml.js +385 -0
- package/dist/export-pdf.d.ts +107 -0
- package/dist/export-pdf.js +144 -0
- package/dist/export-print.d.ts +55 -0
- package/dist/export-print.js +91 -0
- package/{src/export-serialize.ts → dist/export-serialize.d.ts} +11 -24
- package/dist/export-serialize.js +11 -0
- package/dist/export-xls.d.ts +26 -0
- package/dist/export-xls.js +157 -0
- package/dist/export.d.ts +323 -0
- package/dist/export.js +1021 -0
- package/dist/expressions/evaluate.d.ts +17 -0
- package/dist/expressions/evaluate.js +214 -0
- package/dist/expressions/expression-columns.d.ts +61 -0
- package/dist/expressions/expression-columns.js +72 -0
- package/dist/expressions/expression-types.d.ts +125 -0
- package/dist/expressions/expression-types.js +1 -0
- package/dist/expressions/parse.d.ts +25 -0
- package/dist/expressions/parse.js +537 -0
- package/dist/import.d.ts +232 -0
- package/dist/import.js +846 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +91 -0
- package/dist/install.d.ts +70 -0
- package/dist/install.js +61 -0
- package/dist/license-core.d.ts +37 -0
- package/dist/license-core.js +81 -0
- package/dist/license.d.ts +30 -0
- package/dist/license.js +139 -0
- package/dist/master-detail.d.ts +23 -0
- package/dist/master-detail.js +35 -0
- package/dist/node/studio.js +1 -1
- package/dist/pivot-chart.d.ts +28 -0
- package/dist/pivot-chart.js +71 -0
- package/dist/pivot-designer.d.ts +69 -0
- package/dist/pivot-designer.js +38 -0
- package/dist/pivot-enable.d.ts +6 -0
- package/{src/pivot-enable.ts → dist/pivot-enable.js} +13 -18
- package/dist/pivot.d.ts +111 -0
- package/dist/pivot.js +353 -0
- package/dist/print.d.ts +33 -0
- package/dist/print.js +61 -0
- package/dist/revoked.d.ts +1 -0
- package/{src/revoked.ts → dist/revoked.js} +5 -6
- package/dist/scheduler-assignments.d.ts +64 -0
- package/dist/scheduler-assignments.js +92 -0
- package/dist/scheduler-axis.d.ts +81 -0
- package/dist/scheduler-axis.js +180 -0
- package/{src/scheduler-booking.ts → dist/scheduler-booking.d.ts} +10 -43
- package/dist/scheduler-booking.js +50 -0
- package/dist/scheduler-config.d.ts +177 -0
- package/dist/scheduler-config.js +1 -0
- package/dist/scheduler-dependencies.d.ts +89 -0
- package/dist/scheduler-dependencies.js +179 -0
- package/dist/scheduler-freebusy.d.ts +19 -0
- package/{src/scheduler-freebusy.ts → dist/scheduler-freebusy.js} +9 -19
- package/{src/scheduler-heatmap.ts → dist/scheduler-heatmap.d.ts} +12 -35
- package/dist/scheduler-heatmap.js +41 -0
- package/dist/scheduler-resource-tree.d.ts +48 -0
- package/dist/scheduler-resource-tree.js +78 -0
- package/dist/scheduler-slots.d.ts +37 -0
- package/dist/scheduler-slots.js +80 -0
- package/dist/scheduler-summary.d.ts +41 -0
- package/dist/scheduler-summary.js +54 -0
- package/dist/scheduler.d.ts +7 -0
- package/{src/scheduler.ts → dist/scheduler.js} +13 -14
- package/dist/scheduling.d.ts +132 -0
- package/dist/scheduling.js +228 -0
- package/dist/schema-designer.d.ts +42 -0
- package/dist/schema-designer.js +117 -0
- package/dist/schema.d.ts +369 -0
- package/dist/schema.js +216 -0
- package/dist/smart-shim.d.ts +62 -0
- package/dist/smart-shim.js +34 -0
- package/dist/sources/aggregate.d.ts +43 -0
- package/dist/sources/aggregate.js +60 -0
- package/dist/sources/auth-supabase.d.ts +69 -0
- package/dist/sources/auth-supabase.js +39 -0
- package/dist/sources/dashboard.d.ts +65 -0
- package/dist/sources/dashboard.js +95 -0
- package/dist/sources/field-inference.d.ts +23 -0
- package/dist/sources/field-inference.js +52 -0
- package/dist/sources/filters.d.ts +35 -0
- package/dist/sources/filters.js +52 -0
- package/dist/sources/index.d.ts +18 -0
- package/dist/sources/index.js +18 -0
- package/dist/sources/introspect-supabase.d.ts +30 -0
- package/dist/sources/introspect-supabase.js +134 -0
- package/dist/sources/realtime-supabase.d.ts +37 -0
- package/dist/sources/realtime-supabase.js +52 -0
- package/dist/sources/relation-lookup.d.ts +50 -0
- package/dist/sources/relation-lookup.js +77 -0
- package/dist/sources/rest-adapters.d.ts +48 -0
- package/dist/sources/rest-adapters.js +85 -0
- package/dist/sources/rest.d.ts +38 -0
- package/dist/sources/rest.js +86 -0
- package/dist/sources/schema-from-columns.d.ts +37 -0
- package/dist/sources/schema-from-columns.js +48 -0
- package/dist/sources/supabase.d.ts +33 -0
- package/dist/sources/supabase.js +74 -0
- package/dist/sources/with-entity-rules.d.ts +25 -0
- package/dist/sources/with-entity-rules.js +47 -0
- package/dist/sources/with-relation-labels.d.ts +26 -0
- package/dist/sources/with-relation-labels.js +38 -0
- package/dist/staged-editing.d.ts +85 -0
- package/dist/staged-editing.js +0 -0
- package/dist/studio/bug-report.d.ts +61 -0
- package/dist/studio/bug-report.js +126 -0
- package/dist/studio/cli.d.ts +56 -0
- package/dist/studio/cli.js +70 -0
- package/dist/studio/copilot-core.d.ts +22 -0
- package/{src/studio/copilot-core.ts → dist/studio/copilot-core.js} +34 -38
- package/dist/studio/csv.d.ts +36 -0
- package/dist/studio/csv.js +173 -0
- package/dist/studio/db-connect-string.d.ts +45 -0
- package/dist/studio/db-connect-string.js +113 -0
- package/dist/studio/deploy-cli.d.ts +35 -0
- package/dist/studio/deploy-cli.js +81 -0
- package/dist/studio/emit-project.d.ts +77 -0
- package/dist/studio/emit-project.js +5389 -0
- package/dist/studio/emit-schema.d.ts +110 -0
- package/dist/studio/emit-schema.js +1128 -0
- package/dist/studio/index.d.ts +35 -0
- package/dist/studio/index.js +38 -0
- package/dist/studio/init-flow.d.ts +58 -0
- package/dist/studio/init-flow.js +451 -0
- package/dist/studio/introspect-db.d.ts +47 -0
- package/dist/studio/introspect-db.js +255 -0
- package/dist/studio/introspect-openapi.d.ts +19 -0
- package/dist/studio/introspect-openapi.js +244 -0
- package/dist/studio/introspect-prisma.d.ts +25 -0
- package/dist/studio/introspect-prisma.js +164 -0
- package/dist/studio/introspect.d.ts +35 -0
- package/dist/studio/introspect.js +306 -0
- package/dist/studio/project.d.ts +1327 -0
- package/dist/studio/project.js +1798 -0
- package/dist/studio/sample-data.d.ts +19 -0
- package/dist/studio/sample-data.js +207 -0
- package/dist/studio/samples/ats.d.ts +2 -0
- package/dist/studio/samples/ats.js +194 -0
- package/dist/studio/samples/clinic.d.ts +2 -0
- package/dist/studio/samples/clinic.js +171 -0
- package/dist/studio/samples/crm.d.ts +2 -0
- package/dist/studio/samples/crm.js +280 -0
- package/dist/studio/samples/datasets.d.ts +38 -0
- package/dist/studio/samples/datasets.js +296 -0
- package/dist/studio/samples/ecommerce.d.ts +2 -0
- package/dist/studio/samples/ecommerce.js +178 -0
- package/dist/studio/samples/events.d.ts +2 -0
- package/dist/studio/samples/events.js +192 -0
- package/dist/studio/samples/fleet.d.ts +2 -0
- package/dist/studio/samples/fleet.js +203 -0
- package/dist/studio/samples/gym.d.ts +2 -0
- package/dist/studio/samples/gym.js +205 -0
- package/dist/studio/samples/hr.d.ts +2 -0
- package/dist/studio/samples/hr.js +186 -0
- package/dist/studio/samples/index.d.ts +13 -0
- package/dist/studio/samples/index.js +44 -0
- package/dist/studio/samples/insurance.d.ts +2 -0
- package/dist/studio/samples/insurance.js +220 -0
- package/dist/studio/samples/inventory.d.ts +2 -0
- package/dist/studio/samples/inventory.js +204 -0
- package/dist/studio/samples/invoicing.d.ts +2 -0
- package/dist/studio/samples/invoicing.js +154 -0
- package/dist/studio/samples/library.d.ts +2 -0
- package/dist/studio/samples/library.js +171 -0
- package/dist/studio/samples/live-data.d.ts +5 -0
- package/dist/studio/samples/live-data.js +261 -0
- package/dist/studio/samples/projects.d.ts +2 -0
- package/dist/studio/samples/projects.js +203 -0
- package/dist/studio/samples/realestate.d.ts +2 -0
- package/dist/studio/samples/realestate.js +187 -0
- package/dist/studio/samples/restaurant.d.ts +2 -0
- package/dist/studio/samples/restaurant.js +180 -0
- package/dist/studio/samples/school.d.ts +2 -0
- package/dist/studio/samples/school.js +188 -0
- package/dist/studio/samples/shared.d.ts +142 -0
- package/dist/studio/samples/shared.js +164 -0
- package/dist/studio/samples/starter.d.ts +4 -0
- package/dist/studio/samples/starter.js +237 -0
- package/dist/studio/samples/subscriptions.d.ts +2 -0
- package/dist/studio/samples/subscriptions.js +175 -0
- package/dist/studio/samples/support.d.ts +2 -0
- package/dist/studio/samples/support.js +209 -0
- package/dist/studio/scaffold-app.d.ts +22 -0
- package/{src/studio/scaffold-app.ts → dist/studio/scaffold-app.js} +45 -63
- package/dist/studio/scaffold.d.ts +63 -0
- package/dist/studio/scaffold.js +346 -0
- package/dist/studio/screen-suites.d.ts +193 -0
- package/dist/studio/screen-suites.js +297 -0
- package/dist/studio/themes.d.ts +34 -0
- package/dist/studio/themes.js +41 -0
- package/dist/studio/ui-components.d.ts +89 -0
- package/dist/studio/ui-components.generated.d.ts +29 -0
- package/dist/studio/ui-components.generated.js +7139 -0
- package/dist/studio/ui-components.js +673 -0
- package/dist/studio/user-error.d.ts +23 -0
- package/{src/studio/user-error.ts → dist/studio/user-error.js} +10 -13
- package/dist/studio/verify.d.ts +34 -0
- package/dist/studio/verify.js +47 -0
- package/dist/sveltekit/in-memory.d.ts +14 -0
- package/dist/sveltekit/in-memory.js +113 -0
- package/dist/sveltekit/index.d.ts +6 -0
- package/dist/sveltekit/index.js +5 -0
- package/dist/sveltekit/query-plan.d.ts +44 -0
- package/dist/sveltekit/query-plan.js +76 -0
- package/dist/sveltekit/sql-source.d.ts +47 -0
- package/dist/sveltekit/sql-source.js +106 -0
- package/dist/sveltekit/sql.d.ts +48 -0
- package/dist/sveltekit/sql.js +73 -0
- package/dist/sveltekit/transport.d.ts +209 -0
- package/dist/sveltekit/transport.js +219 -0
- package/{src/sveltekit/types.ts → dist/sveltekit/types.d.ts} +8 -11
- package/dist/sveltekit/types.js +1 -0
- package/dist/upgrade-prompt.d.ts +16 -0
- package/dist/upgrade-prompt.js +141 -0
- package/dist/version.d.ts +13 -0
- package/{src/version.ts → dist/version.js} +1 -1
- package/dist/watermark.d.ts +2 -0
- package/dist/watermark.js +146 -0
- package/package.json +26 -24
- package/src/SvBoard.dom.test.ts +0 -67
- package/src/SvGridAlerts.dom.test.ts +0 -113
- package/src/SvGridEditPanel.dom.test.ts +0 -320
- package/src/SvRecordDetail.dom.test.ts +0 -137
- package/src/SvSchedule.dom.test.ts +0 -57
- package/src/ai-export-pdf.dom.test.ts +0 -77
- package/src/ai-export-xlsx.dom.test.ts +0 -90
- package/src/ai-export.dom.test.ts +0 -114
- package/src/alerts/alert-engine-attach.ts +0 -165
- package/src/alerts/alert-engine.test.ts +0 -135
- package/src/alerts/alert-engine.ts +0 -260
- package/src/alerts/alert-formats.test.ts +0 -87
- package/src/alerts/alert-formats.ts +0 -77
- package/src/alerts/alert-observer.test.ts +0 -189
- package/src/alerts/alert-observer.ts +0 -208
- package/src/alerts/alert-scheduler.ts +0 -96
- package/src/alerts/alert-storage.test.ts +0 -54
- package/src/alerts/alert-storage.ts +0 -116
- package/src/alerts/alert-store.svelte.ts +0 -80
- package/src/alerts/alert-types.ts +0 -94
- package/src/board.dom.test.ts +0 -941
- package/src/edit-panel.test.ts +0 -322
- package/src/edit-panel.ts +0 -379
- package/src/export-conditional.test.ts +0 -60
- package/src/export-conditional.ts +0 -94
- package/src/export-ooxml.test.ts +0 -152
- package/src/export-ooxml.ts +0 -489
- package/src/export-pdf.test.ts +0 -138
- package/src/export-pdf.ts +0 -245
- package/src/export-print.test.ts +0 -66
- package/src/export-print.ts +0 -132
- package/src/export-serialize.test.ts +0 -56
- package/src/export-xls.ts +0 -199
- package/src/export-xlsx-roundtrip.test.ts +0 -120
- package/src/export-xlsx.test.ts +0 -150
- package/src/export.test.ts +0 -349
- package/src/export.ts +0 -1464
- package/src/expressions/evaluate.test.ts +0 -111
- package/src/expressions/evaluate.ts +0 -237
- package/src/expressions/expression-columns.ts +0 -133
- package/src/expressions/expression-types.ts +0 -84
- package/src/expressions/parse.test.ts +0 -106
- package/src/expressions/parse.ts +0 -610
- package/src/import-automap.test.ts +0 -131
- package/src/import-hardening.test.ts +0 -158
- package/src/import.test.ts +0 -415
- package/src/import.ts +0 -1044
- package/src/index.ts +0 -569
- package/src/install.ts +0 -154
- package/src/license-core.test.ts +0 -23
- package/src/license-core.ts +0 -107
- package/src/license-expiry.test.ts +0 -127
- package/src/license.ts +0 -137
- package/src/master-detail.test.ts +0 -61
- package/src/master-detail.ts +0 -42
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -178
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js +0 -3875
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js +0 -21
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js +0 -1938
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js +0 -205
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js +0 -18
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js +0 -264
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js +0 -697
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -134
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -55
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -46843
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -237
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -4
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -7
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -4
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -6
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -83
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -559
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -24
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -122
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -89
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -241
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js +0 -4938
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js +0 -495
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js +0 -190
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js +0 -8
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js +0 -138
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js.map +0 -1
- package/src/pivot-chart.test.ts +0 -86
- package/src/pivot-chart.ts +0 -112
- package/src/pivot-designer.ts +0 -101
- package/src/pivot.test.ts +0 -335
- package/src/pivot.ts +0 -578
- package/src/print.ts +0 -107
- package/src/scheduler-assignments.test.ts +0 -97
- package/src/scheduler-assignments.ts +0 -134
- package/src/scheduler-axis.test.ts +0 -108
- package/src/scheduler-axis.ts +0 -238
- package/src/scheduler-booking.test.ts +0 -57
- package/src/scheduler-config.ts +0 -179
- package/src/scheduler-dependencies.test.ts +0 -155
- package/src/scheduler-dependencies.ts +0 -223
- package/src/scheduler-freebusy.test.ts +0 -41
- package/src/scheduler-heatmap.test.ts +0 -39
- package/src/scheduler-resource-tree.test.ts +0 -84
- package/src/scheduler-resource-tree.ts +0 -107
- package/src/scheduler-slots.test.ts +0 -53
- package/src/scheduler-slots.ts +0 -94
- package/src/scheduler-summary.test.ts +0 -47
- package/src/scheduler-summary.ts +0 -81
- package/src/scheduling.test.ts +0 -194
- package/src/scheduling.ts +0 -293
- package/src/schema-designer.test.ts +0 -121
- package/src/schema-designer.ts +0 -142
- package/src/schema.test.ts +0 -268
- package/src/schema.ts +0 -562
- package/src/smart-shim.ts +0 -107
- package/src/sources/aggregate.test.ts +0 -79
- package/src/sources/aggregate.ts +0 -102
- package/src/sources/auth-supabase.test.ts +0 -80
- package/src/sources/auth-supabase.ts +0 -87
- package/src/sources/dashboard.kpi.test.ts +0 -50
- package/src/sources/dashboard.test.ts +0 -61
- package/src/sources/dashboard.ts +0 -136
- package/src/sources/field-inference.test.ts +0 -60
- package/src/sources/field-inference.ts +0 -63
- package/src/sources/filters.test.ts +0 -58
- package/src/sources/filters.ts +0 -70
- package/src/sources/index.ts +0 -67
- package/src/sources/introspect-supabase.test.ts +0 -112
- package/src/sources/introspect-supabase.ts +0 -139
- package/src/sources/realtime-supabase.test.ts +0 -93
- package/src/sources/realtime-supabase.ts +0 -99
- package/src/sources/relation-lookup.test.ts +0 -99
- package/src/sources/relation-lookup.ts +0 -128
- package/src/sources/rest-adapters.test.ts +0 -95
- package/src/sources/rest-adapters.ts +0 -120
- package/src/sources/rest.test.ts +0 -104
- package/src/sources/rest.ts +0 -129
- package/src/sources/schema-from-columns.test.ts +0 -106
- package/src/sources/schema-from-columns.ts +0 -88
- package/src/sources/supabase.test.ts +0 -110
- package/src/sources/supabase.ts +0 -99
- package/src/sources/with-entity-rules.test.ts +0 -104
- package/src/sources/with-entity-rules.ts +0 -78
- package/src/sources/with-relation-labels.test.ts +0 -75
- package/src/sources/with-relation-labels.ts +0 -73
- package/src/staged-editing.ts +0 -0
- package/src/studio/bug-report.test.ts +0 -101
- package/src/studio/bug-report.ts +0 -165
- package/src/studio/cli.test.ts +0 -187
- package/src/studio/cli.ts +0 -117
- package/src/studio/copilot-core.test.ts +0 -45
- package/src/studio/csv.test.ts +0 -90
- package/src/studio/csv.ts +0 -165
- package/src/studio/db-connect-string.test.ts +0 -94
- package/src/studio/db-connect-string.ts +0 -128
- package/src/studio/deploy-cli.test.ts +0 -56
- package/src/studio/deploy-cli.ts +0 -100
- package/src/studio/emit-project.test.ts +0 -3324
- package/src/studio/emit-project.ts +0 -5377
- package/src/studio/emit-schema.test.ts +0 -111
- package/src/studio/emit-schema.ts +0 -1181
- package/src/studio/index.ts +0 -380
- package/src/studio/init-flow.test.ts +0 -514
- package/src/studio/init-flow.ts +0 -556
- package/src/studio/introspect-db.test.ts +0 -186
- package/src/studio/introspect-db.ts +0 -312
- package/src/studio/introspect-openapi.test.ts +0 -84
- package/src/studio/introspect-openapi.ts +0 -252
- package/src/studio/introspect-prisma.test.ts +0 -99
- package/src/studio/introspect-prisma.ts +0 -175
- package/src/studio/introspect.test.ts +0 -172
- package/src/studio/introspect.ts +0 -310
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -1
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -3
- package/src/studio/pipeline.test.ts +0 -42
- package/src/studio/project-robust.test.ts +0 -157
- package/src/studio/project.test.ts +0 -929
- package/src/studio/project.ts +0 -2394
- package/src/studio/sample-data.test.ts +0 -58
- package/src/studio/sample-data.ts +0 -203
- package/src/studio/samples/ats.ts +0 -202
- package/src/studio/samples/clinic.ts +0 -179
- package/src/studio/samples/crm.ts +0 -291
- package/src/studio/samples/datasets.test.ts +0 -84
- package/src/studio/samples/datasets.ts +0 -340
- package/src/studio/samples/ecommerce.ts +0 -187
- package/src/studio/samples/events.ts +0 -201
- package/src/studio/samples/fleet.ts +0 -213
- package/src/studio/samples/gym.ts +0 -215
- package/src/studio/samples/hr.ts +0 -195
- package/src/studio/samples/index.ts +0 -55
- package/src/studio/samples/insurance.ts +0 -229
- package/src/studio/samples/inventory.ts +0 -213
- package/src/studio/samples/invoicing.ts +0 -162
- package/src/studio/samples/library.ts +0 -180
- package/src/studio/samples/live-data.test.ts +0 -99
- package/src/studio/samples/live-data.ts +0 -306
- package/src/studio/samples/projects.ts +0 -212
- package/src/studio/samples/realestate.ts +0 -196
- package/src/studio/samples/restaurant.ts +0 -189
- package/src/studio/samples/samples.test.ts +0 -304
- package/src/studio/samples/school.ts +0 -197
- package/src/studio/samples/seed-floor.test.ts +0 -25
- package/src/studio/samples/shared.ts +0 -237
- package/src/studio/samples/starter.ts +0 -251
- package/src/studio/samples/subscriptions.ts +0 -183
- package/src/studio/samples/support.ts +0 -216
- package/src/studio/scaffold-app.test.ts +0 -91
- package/src/studio/scaffold.test.ts +0 -192
- package/src/studio/scaffold.ts +0 -390
- package/src/studio/screen-suites.test.ts +0 -269
- package/src/studio/screen-suites.ts +0 -448
- package/src/studio/themes.ts +0 -62
- package/src/studio/ui-components-surface.test.ts +0 -185
- package/src/studio/ui-components.generated.ts +0 -7154
- package/src/studio/ui-components.ts +0 -742
- package/src/studio/verify.test.ts +0 -47
- package/src/studio/verify.ts +0 -79
- package/src/sveltekit/in-memory.test.ts +0 -104
- package/src/sveltekit/in-memory.ts +0 -129
- package/src/sveltekit/index.ts +0 -24
- package/src/sveltekit/query-plan.test.ts +0 -109
- package/src/sveltekit/query-plan.ts +0 -125
- package/src/sveltekit/sql-source.test.ts +0 -198
- package/src/sveltekit/sql-source.ts +0 -171
- package/src/sveltekit/sql.test.ts +0 -80
- package/src/sveltekit/sql.ts +0 -131
- package/src/sveltekit/transport-scope.test.ts +0 -125
- package/src/sveltekit/transport.test.ts +0 -201
- package/src/sveltekit/transport.ts +0 -373
- package/src/upgrade-prompt.test.ts +0 -71
- package/src/upgrade-prompt.ts +0 -162
- package/src/version.test.ts +0 -17
- package/src/watermark.test.ts +0 -97
- package/src/watermark.ts +0 -156
- /package/{src → dist}/SvAlertRuleEditor.svelte +0 -0
- /package/{src → dist}/SvAlertsManager.svelte +0 -0
- /package/{src → dist}/SvAlertsPanel.svelte +0 -0
- /package/{src → dist}/SvAuthGate.svelte +0 -0
- /package/{src → dist}/SvBoard.svelte +0 -0
- /package/{src → dist}/SvExportMenu.svelte +0 -0
- /package/{src → dist}/SvExpressionEditor.svelte +0 -0
- /package/{src → dist}/SvFileInput.svelte +0 -0
- /package/{src → dist}/SvGridAlerts.svelte +0 -0
- /package/{src → dist}/SvGridBoard.svelte +0 -0
- /package/{src → dist}/SvGridEditPanel.svelte +0 -0
- /package/{src → dist}/SvGridMasterDetail.svelte +0 -0
- /package/{src → dist}/SvGridScheduler.svelte +0 -0
- /package/{src → dist}/SvImportDialog.svelte +0 -0
- /package/{src → dist}/SvLookupInput.svelte +0 -0
- /package/{src → dist}/SvPivotDesigner.svelte +0 -0
- /package/{src → dist}/SvRecordDetail.svelte +0 -0
- /package/{src → dist}/SvSchedule.svelte +0 -0
- /package/{src → dist}/SvSchemaChart.svelte +0 -0
- /package/{src → dist}/SvSchemaDashboard.svelte +0 -0
- /package/{src → dist}/pdfmake-shims.d.ts +0 -0
- /package/{src → dist}/smart.export.d.ts +0 -0
- /package/{src → dist}/smart.export.js +0 -0
- /package/{src → dist}/studio/HANDLERS-DESIGN.md +0 -0
- /package/{src → dist/studio}/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +0 -0
- /package/{src → dist/studio}/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +0 -0
|
@@ -0,0 +1,1798 @@
|
|
|
1
|
+
import { collectColumnRefs } from '../expressions/parse.js';
|
|
2
|
+
import { uiComponentSpec } from './ui-components.js';
|
|
3
|
+
/** Block kinds allowed inside a Tabs / Accordion container: the controller-free,
|
|
4
|
+
* `allRows`-driven display blocks (no grid / form / master-detail, which need the
|
|
5
|
+
* screen controller). `component` covers any UI-kit component added by key. */
|
|
6
|
+
export const TAB_CHILD_KINDS = ['chart', 'kpi', 'gauge', 'dashboard', 'pivot', 'tree', 'component'];
|
|
7
|
+
/** Alias: the same set applies to Accordion sections. */
|
|
8
|
+
export const CONTAINER_CHILD_KINDS = TAB_CHILD_KINDS;
|
|
9
|
+
/** Display-block child kinds (no `component`) - what the container "add block" menu offers;
|
|
10
|
+
* components are added separately, by registry key, via `addContainerComponent`. */
|
|
11
|
+
export const CONTAINER_DISPLAY_KINDS = ['chart', 'kpi', 'gauge', 'dashboard', 'pivot', 'tree'];
|
|
12
|
+
/** All blocks on a screen, flattened to include the children nested in Tabs
|
|
13
|
+
* containers - used to detect kinds for imports / data loading. */
|
|
14
|
+
export function flattenBlocks(blocks) {
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const b of blocks) {
|
|
17
|
+
out.push(b);
|
|
18
|
+
if (b.config.kind === 'tabs')
|
|
19
|
+
for (const t of b.config.tabs)
|
|
20
|
+
out.push(...flattenBlocks(t.blocks));
|
|
21
|
+
if (b.config.kind === 'accordion')
|
|
22
|
+
for (const s of b.config.sections)
|
|
23
|
+
out.push(...flattenBlocks(s.blocks));
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Sanitize a user-typed class list to a safe value (letters/digits/_/- and spaces) so
|
|
28
|
+
* it can never break out of a `class="..."` attribute. Shared by block/screen/app. */
|
|
29
|
+
export function sanitizeClassName(raw) {
|
|
30
|
+
return (raw ?? '').replace(/[^a-zA-Z0-9 _-]/g, '').trim().replace(/\s+/g, ' ').slice(0, 120);
|
|
31
|
+
}
|
|
32
|
+
/** The sanitized extra class(es) for a block's wrapper. */
|
|
33
|
+
export function blockClassName(block) {
|
|
34
|
+
return sanitizeClassName(block.className);
|
|
35
|
+
}
|
|
36
|
+
/** The number of columns (1-12) a block occupies in the 12-col layout. */
|
|
37
|
+
export const blockColumns = (b) =>
|
|
38
|
+
// Default to full width when a block sets neither colSpan nor span (Copilot /
|
|
39
|
+
// hand-authored configs may omit both) - never emit `span NaN` into the HTML.
|
|
40
|
+
Math.max(1, Math.min(12, Math.round(b.colSpan ?? (b.span != null ? b.span * 4 : 12))));
|
|
41
|
+
/** Restrict a user-typed color to a safe subset so it can't break out of an inline
|
|
42
|
+
* `style="..."` attribute (hex, rgb()/hsl(), named colors, css vars). */
|
|
43
|
+
const safeColor = (c) => c.replace(/[^#a-zA-Z0-9(),.%\s_-]/g, '').slice(0, 64);
|
|
44
|
+
/** The CSS declarations for a block's style overrides, for its wrapper element - shared
|
|
45
|
+
* by codegen and the designer so the preview matches the generated app. Empty when no
|
|
46
|
+
* overrides. Numeric fields are safe; the background color is sanitized. */
|
|
47
|
+
export function blockStyleCss(style) {
|
|
48
|
+
if (!style)
|
|
49
|
+
return '';
|
|
50
|
+
const d = [];
|
|
51
|
+
if (style.border === true)
|
|
52
|
+
d.push('border: 1px solid var(--sg-border, #e6e8ec)');
|
|
53
|
+
else if (style.border === false)
|
|
54
|
+
d.push('border: none');
|
|
55
|
+
if (style.shadow === true)
|
|
56
|
+
d.push('box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06)');
|
|
57
|
+
else if (style.shadow === false)
|
|
58
|
+
d.push('box-shadow: none');
|
|
59
|
+
if (typeof style.padding === 'number')
|
|
60
|
+
d.push(`padding: ${style.padding}px`);
|
|
61
|
+
if (typeof style.margin === 'number')
|
|
62
|
+
d.push(`margin: ${style.margin}px`);
|
|
63
|
+
if (style.background)
|
|
64
|
+
d.push(`background: ${safeColor(style.background)}`);
|
|
65
|
+
if (typeof style.radius === 'number')
|
|
66
|
+
d.push(`border-radius: ${style.radius}px`);
|
|
67
|
+
return d.join('; ');
|
|
68
|
+
}
|
|
69
|
+
/** Merge a partial style patch over a block's style, dropping keys set to undefined;
|
|
70
|
+
* returns undefined when nothing is left (keeps the model + serialized config clean). */
|
|
71
|
+
export function mergeBlockStyle(base, patch) {
|
|
72
|
+
const merged = { ...base, ...patch };
|
|
73
|
+
for (const k of Object.keys(merged))
|
|
74
|
+
if (merged[k] === undefined)
|
|
75
|
+
delete merged[k];
|
|
76
|
+
return Object.keys(merged).length ? merged : undefined;
|
|
77
|
+
}
|
|
78
|
+
/** The always-present page lifecycle handlers the Code view edits. Kept as named
|
|
79
|
+
* constants so codegen, the designer, and the manifest agree on the slot names.
|
|
80
|
+
* `onLoad` runs on mount (with the page context); `onDestroy` runs on unmount
|
|
81
|
+
* (cleanup: timers, subscriptions, aborts). */
|
|
82
|
+
export const ON_LOAD = 'onLoad';
|
|
83
|
+
export const ON_DESTROY = 'onDestroy';
|
|
84
|
+
/** Every lifecycle slot the Code view can edit, in display order. */
|
|
85
|
+
export const HANDLER_SLOTS = [ON_LOAD, ON_DESTROY];
|
|
86
|
+
/** The handler-steps key for a component block's click event. */
|
|
87
|
+
export const clickSlot = (blockId) => `click:${blockId}`;
|
|
88
|
+
/** The handler-steps key for a grid block's row-select (row click) event. The
|
|
89
|
+
* compiled steps get the clicked `row` in scope (use row-field values). */
|
|
90
|
+
export const rowSelectSlot = (blockId) => `rowSelect:${blockId}`;
|
|
91
|
+
/** The handler-steps key for a component block's change (value change) event. */
|
|
92
|
+
export const changeSlot = (blockId) => `change:${blockId}`;
|
|
93
|
+
/** Generic event slot - `click`/`change` produce the same keys as clickSlot/changeSlot,
|
|
94
|
+
* so existing projects keep their wiring; any other declared component event
|
|
95
|
+
* (focus, select, toggle, ...) gets its own `<event>:<blockId>` slot. */
|
|
96
|
+
export const eventSlot = (event, blockId) => `${event}:${blockId}`;
|
|
97
|
+
/** The handler-steps key for the screen's form-submit (record saved) event. The
|
|
98
|
+
* compiled steps get the submitted `row` (values) in scope. */
|
|
99
|
+
export const FORM_SUBMIT = 'formSubmit';
|
|
100
|
+
/** The Grid's real event surface (mirrors SvGrid's `on*` callbacks). Studio wires
|
|
101
|
+
* every one into `ctx.grid` so page code can subscribe with `ctx.grid.onCellClick =
|
|
102
|
+
* (e) => {}`. `params` is the handler's parameter list, with `Row` as the row-type
|
|
103
|
+
* placeholder (substituted per screen). `builtin` marks events a Studio feature may
|
|
104
|
+
* already use (sort/filter/paginate/edit/row-click) - codegen COMPOSES the user's
|
|
105
|
+
* handler onto the built-in rather than emitting a duplicate prop. Signatures are
|
|
106
|
+
* transcribed from packages/grid/src/SvGrid.types.ts. */
|
|
107
|
+
export const GRID_EVENTS = [
|
|
108
|
+
{ key: 'rowClick', method: 'onRowClick', prop: 'onRowClick', builtin: true, desc: 'A data row is single-clicked.', params: 'e: { rowIndex: number; columnId: string; row: Row }' },
|
|
109
|
+
{ key: 'rowDoubleClick', method: 'onRowDoubleClick', prop: 'onRowDoubleClick', builtin: true, desc: 'A data row is double-clicked.', params: 'e: { rowIndex: number; columnId: string; row: Row }' },
|
|
110
|
+
{ key: 'cellClick', method: 'onCellClick', prop: 'onCellClick', builtin: false, desc: 'A data cell is single-clicked.', params: 'e: { rowIndex: number; colIndex: number; columnId: string; value: unknown; row: Row }' },
|
|
111
|
+
{ key: 'cellDoubleClick', method: 'onCellDoubleClick', prop: 'onCellDoubleClick', builtin: false, desc: 'A data cell is double-clicked.', params: 'e: { rowIndex: number; colIndex: number; columnId: string; value: unknown; row: Row }' },
|
|
112
|
+
{ key: 'rowSelectionChange', method: 'onRowSelectionChange', prop: 'onRowSelectionChange', builtin: false, desc: 'The row selection changes.', params: 'selection: Record<string, boolean>, rows: Row[]' },
|
|
113
|
+
{ key: 'cellSelectionChange', method: 'onCellSelectionChange', prop: 'onCellSelectionChange', builtin: false, desc: 'The cell-selection rectangle changes.', params: 'ranges: Array<[number, number, number, number]>' },
|
|
114
|
+
{ key: 'activeCellChange', method: 'onActiveCellChange', prop: 'onActiveCellChange', builtin: false, desc: 'The active cell changes.', params: 'cell: { rowIndex: number; colIndex: number; columnId: string }' },
|
|
115
|
+
{ key: 'cellValueChange', method: 'onCellValueChange', prop: 'onCellValueChange', builtin: true, desc: 'An inline cell edit is committed.', params: 'e: { rowIndex: number; columnId: string; oldValue: unknown; newValue: unknown; row: Row }' },
|
|
116
|
+
{ key: 'sortingChange', method: 'onSortingChange', prop: 'onSortingChange', builtin: true, desc: 'The sort clauses change.', params: 'sorting: Array<{ id: string; desc: boolean }>' },
|
|
117
|
+
{ key: 'filtersChange', method: 'onFiltersChange', prop: 'onFiltersChange', builtin: true, desc: 'Any in-grid filter changes.', params: 'filters: { global: string; columns: Array<{ id: string; operator: string; value: string; valueTo?: string; selectedValues?: string[] }> }' },
|
|
118
|
+
{ key: 'paginationChange', method: 'onPaginationChange', prop: 'onPaginationChange', builtin: true, desc: 'The page or page size changes.', params: 'pagination: { pageIndex: number; pageSize: number }' },
|
|
119
|
+
{ key: 'columnOrderChange', method: 'onColumnOrderChange', prop: 'onColumnOrderChange', builtin: false, desc: 'The column order changes.', params: 'order: ReadonlyArray<string>' },
|
|
120
|
+
{ key: 'scrollBottomReached', method: 'onScrollBottomReached', prop: 'onScrollBottomReached', builtin: false, desc: 'The body scrolls near the bottom (lazy-load hook).', params: 'e: { scrollTop: number; scrollHeight: number; clientHeight: number }' },
|
|
121
|
+
{ key: 'noteChange', method: 'onNoteChange', prop: 'onNoteChange', builtin: false, desc: 'A cell note/comment is saved or removed (needs editable comments).', params: 'e: { rowId: string; columnId: string; note: string }' },
|
|
122
|
+
{ key: 'rowDragEnd', method: 'onRowDragEnd', prop: 'onRowDragEnd', builtin: false, desc: 'A managed row drag settles (needs row dragging enabled).', params: 'e: { row: Row; toIndex: number; sameGrid: boolean; fromGridId: number; toGridId: number }' },
|
|
123
|
+
// Data-layer events - fired by the screen's data controller after a write settles
|
|
124
|
+
// (form save, inline edit, delete action, or ctx.data.create/update/delete), not by
|
|
125
|
+
// the grid markup. Only fire on entity screens (which have a data source).
|
|
126
|
+
{ key: 'rowAdded', method: 'onRowAdded', prop: '', builtin: false, dataEvent: true, desc: 'A row was created (form / ctx.data.create).', params: 'row: Row' },
|
|
127
|
+
{ key: 'rowUpdated', method: 'onRowUpdated', prop: '', builtin: false, dataEvent: true, desc: 'A row was updated (form / inline edit / ctx.data.update).', params: 'row: Row' },
|
|
128
|
+
{ key: 'rowDeleted', method: 'onRowDeleted', prop: '', builtin: false, dataEvent: true, desc: 'A row was deleted (delete action / ctx.data.delete).', params: 'id: string' },
|
|
129
|
+
];
|
|
130
|
+
const identSafe = (s) => (s || '').replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^([0-9])/, '_$1') || 'v';
|
|
131
|
+
/** The `$state(...)` initializer for a variable's declared type. */
|
|
132
|
+
export function stateInitExpr(v) {
|
|
133
|
+
const raw = (v.initial ?? '').trim();
|
|
134
|
+
switch (v.type) {
|
|
135
|
+
case 'number': return raw === '' || Number.isNaN(Number(raw)) ? '0' : raw;
|
|
136
|
+
case 'boolean': return raw === 'true' ? 'true' : 'false';
|
|
137
|
+
case 'json': return raw || 'null';
|
|
138
|
+
case 'string': return q(v.initial ?? '');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** The TS type annotation for a variable. */
|
|
142
|
+
export function stateTsType(v) {
|
|
143
|
+
return v.type === 'number' ? 'number' : v.type === 'boolean' ? 'boolean' : v.type === 'json' ? 'unknown' : 'string';
|
|
144
|
+
}
|
|
145
|
+
export function compileValue(v, rowExpr = 'row') {
|
|
146
|
+
switch (v.kind) {
|
|
147
|
+
case 'state': return `ctx.state.${identSafe(v.name)}`;
|
|
148
|
+
case 'param': return `ctx.params[${q(v.name)}]`;
|
|
149
|
+
case 'field': return `${rowExpr}?.[${q(v.name)}]`;
|
|
150
|
+
case 'literal': return litValue(v.value);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function compileCondition(c, rowExpr = 'row') {
|
|
154
|
+
const L = compileValue(c.left, rowExpr);
|
|
155
|
+
const R = c.right ? compileValue(c.right, rowExpr) : "''";
|
|
156
|
+
switch (c.op) {
|
|
157
|
+
case 'eq': return `${L} === ${R}`;
|
|
158
|
+
case 'neq': return `${L} !== ${R}`;
|
|
159
|
+
case 'gt': return `Number(${L}) > Number(${R})`;
|
|
160
|
+
case 'lt': return `Number(${L}) < Number(${R})`;
|
|
161
|
+
case 'gte': return `Number(${L}) >= Number(${R})`;
|
|
162
|
+
case 'lte': return `Number(${L}) <= Number(${R})`;
|
|
163
|
+
case 'contains': return `String(${L}).includes(String(${R}))`;
|
|
164
|
+
case 'truthy': return `Boolean(${L})`;
|
|
165
|
+
case 'falsy': return `!(${L})`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const compileFields = (fs) => `{ ${fs.map((f) => `${identSafe(f.field)}: ${compileValue(f.value)}`).join(', ')} }`;
|
|
169
|
+
const q = (s) => `'${String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
170
|
+
/** Emit a value literal: true/false/number bare, else a quoted string. */
|
|
171
|
+
const litValue = (v) => {
|
|
172
|
+
const t = v.trim();
|
|
173
|
+
if (t === 'true' || t === 'false')
|
|
174
|
+
return t;
|
|
175
|
+
if (t !== '' && !Number.isNaN(Number(t)))
|
|
176
|
+
return t;
|
|
177
|
+
return q(v);
|
|
178
|
+
};
|
|
179
|
+
/** Compile one step to a `ctx` statement (a single line, no trailing newline). */
|
|
180
|
+
export function compileStep(step) {
|
|
181
|
+
switch (step.type) {
|
|
182
|
+
case 'navigate': return `ctx.goto(${q(step.to)})`;
|
|
183
|
+
case 'gridExport': {
|
|
184
|
+
const fmt = step.format ?? 'csv';
|
|
185
|
+
const fn = fmt === 'tsv' ? 'exportTsv' : fmt === 'json' ? 'exportJson' : 'exportCsv';
|
|
186
|
+
const mime = fmt === 'json' ? 'application/json' : fmt === 'tsv' ? 'text/tab-separated-values' : 'text/csv';
|
|
187
|
+
// A scoped block so the locals don't collide when there are two export steps.
|
|
188
|
+
return [
|
|
189
|
+
'{',
|
|
190
|
+
` const data = await ctx.grid.${fn}()`,
|
|
191
|
+
" const link = document.createElement('a')",
|
|
192
|
+
` link.href = URL.createObjectURL(new Blob([data], { type: ${q(mime)} }))`,
|
|
193
|
+
` link.download = ${q('export.' + fmt)}`,
|
|
194
|
+
' link.click()',
|
|
195
|
+
' URL.revokeObjectURL(link.href)',
|
|
196
|
+
'}',
|
|
197
|
+
].join('\n');
|
|
198
|
+
}
|
|
199
|
+
case 'gridCopy': return `ctx.grid.copyToClipboard()`;
|
|
200
|
+
case 'gridClear': return step.what === 'sort' ? `ctx.grid.clearSort()` : step.what === 'selection' ? `ctx.grid.clearRowSelection()` : `ctx.grid.clearAllFilters()`;
|
|
201
|
+
case 'gridSort': return `ctx.grid.setSort(${q(step.field)}, ${q(step.dir ?? 'asc')})`;
|
|
202
|
+
case 'gridFilter': return `ctx.grid.setFilter(${q(step.field)}, { operator: 'contains', value: ${q(step.value)} })`;
|
|
203
|
+
case 'reloadData': return `await ctx.data.reload()`;
|
|
204
|
+
case 'setProp': return `ctx.${step.target}.${step.prop} = ${litValue(step.value)}`;
|
|
205
|
+
case 'setText': return `ctx.${step.target}.text = ${litValue(step.value)}`;
|
|
206
|
+
case 'alert': return `alert(${q(step.message)})`;
|
|
207
|
+
case 'apiAction': return `await fetch(${q('/api/actions/' + step.actionId)}, { method: 'POST' })`;
|
|
208
|
+
case 'setVar': return `ctx.state.${identSafe(step.name)} = ${compileValue(step.value)}`;
|
|
209
|
+
case 'createRecord': return `await ctx.data.create(${compileFields(step.values)})`;
|
|
210
|
+
case 'updateRecord': return `await ctx.data.update(${compileValue(step.id)}, ${compileFields(step.values)})`;
|
|
211
|
+
case 'deleteRecord': return `await ctx.data.delete(${compileValue(step.id)})`;
|
|
212
|
+
case 'branch': {
|
|
213
|
+
const body = indentLines(compileHandlerSteps(step.then));
|
|
214
|
+
const elseB = step.else?.length ? ` else {\n${indentLines(compileHandlerSteps(step.else))}\n}` : '';
|
|
215
|
+
return `if (${compileCondition(step.condition)}) {\n${body}\n}${elseB}`;
|
|
216
|
+
}
|
|
217
|
+
case 'code': return step.code;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** Indent every line by 2 spaces (nested branch bodies). */
|
|
221
|
+
const indentLines = (s) => s.split('\n').map((l) => (l ? ' ' + l : l)).join('\n');
|
|
222
|
+
/** Compile a list of steps to a handler body (indented lines). */
|
|
223
|
+
export function compileHandlerSteps(steps) {
|
|
224
|
+
return steps.map((s) => compileStep(s)).join('\n');
|
|
225
|
+
}
|
|
226
|
+
/** All trigger events, in display order; before-hooks transform/validate, after-hooks are side effects. */
|
|
227
|
+
export const TRIGGER_EVENTS = ['beforeCreate', 'afterCreate', 'beforeUpdate', 'afterUpdate', 'beforeDelete', 'afterDelete'];
|
|
228
|
+
/** Compile one trigger step against the payload variable `rowExpr` (default `v`). */
|
|
229
|
+
export function compileTriggerStep(step, rowExpr = 'v') {
|
|
230
|
+
switch (step.type) {
|
|
231
|
+
case 'setField': return `${rowExpr}[${q(step.field)}] = ${compileValue(step.value, rowExpr)}`;
|
|
232
|
+
case 'requireField': return `if (${rowExpr}[${q(step.field)}] == null || ${rowExpr}[${q(step.field)}] === '') throw new Error(${q(step.message || step.field + ' is required')})`;
|
|
233
|
+
case 'reject': return `if (${compileCondition(step.condition, rowExpr)}) throw new Error(${q(step.message)})`;
|
|
234
|
+
case 'branch': {
|
|
235
|
+
const body = indentLines(compileTriggerSteps(step.then, rowExpr));
|
|
236
|
+
const elseB = step.else?.length ? ` else {\n${indentLines(compileTriggerSteps(step.else, rowExpr))}\n}` : '';
|
|
237
|
+
return `if (${compileCondition(step.condition, rowExpr)}) {\n${body}\n}${elseB}`;
|
|
238
|
+
}
|
|
239
|
+
case 'code': return step.code;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export function compileTriggerSteps(steps, rowExpr = 'v') {
|
|
243
|
+
return steps.map((s) => compileTriggerStep(s, rowExpr)).join('\n');
|
|
244
|
+
}
|
|
245
|
+
/** Layouts that arrange blocks as dockable / split panes (both back onto a
|
|
246
|
+
* serialized `DockManagerState`; `split` renders it locked = resize-only). */
|
|
247
|
+
export const PANE_LAYOUTS = ['split', 'dock'];
|
|
248
|
+
export function isPaneLayout(layout) { return PANE_LAYOUTS.includes(layout); }
|
|
249
|
+
export const CANVAS_COLS = 12;
|
|
250
|
+
/** Height of one canvas row in px (grid-auto-rows), shared by designer + codegen. */
|
|
251
|
+
export const CANVAS_ROW_PX = 40;
|
|
252
|
+
/** Gap between canvas cells in px. */
|
|
253
|
+
export const CANVAS_GAP_PX = 8;
|
|
254
|
+
/** `maxWidth: 0` (or absent) means full-bleed. */
|
|
255
|
+
export const GRID_OPT_DEFAULTS = { colGap: 16, rowGap: 16, maxWidth: 0, align: 'start', mobileBreakpoint: 720 };
|
|
256
|
+
export const STACK_OPT_DEFAULTS = { gap: 16, maxWidth: 0, align: 'left', dividers: false, minHeight: 160 };
|
|
257
|
+
export const SPLIT_OPT_DEFAULTS = { orientation: 'auto', minPaneSize: 80, persist: true };
|
|
258
|
+
export const DOCK_OPT_DEFAULTS = { allowPopout: false, hideSingleTab: false, headerPosition: 'top', persist: true };
|
|
259
|
+
export const CANVAS_OPT_DEFAULTS = { cols: CANVAS_COLS, rowHeight: CANVAS_ROW_PX, gap: CANVAS_GAP_PX, showGrid: false };
|
|
260
|
+
export const gridOpts = (s) => ({ ...GRID_OPT_DEFAULTS, ...s.layoutOpts?.grid });
|
|
261
|
+
export const stackOpts = (s) => ({ ...STACK_OPT_DEFAULTS, ...s.layoutOpts?.stack });
|
|
262
|
+
export const splitOpts = (s) => ({ ...SPLIT_OPT_DEFAULTS, ...s.layoutOpts?.split });
|
|
263
|
+
export const dockOpts = (s) => ({ ...DOCK_OPT_DEFAULTS, ...s.layoutOpts?.dock });
|
|
264
|
+
export const canvasOpts = (s) => ({ ...CANVAS_OPT_DEFAULTS, ...s.layoutOpts?.canvas });
|
|
265
|
+
/** Patch one mode's settings (merged over any existing values). */
|
|
266
|
+
export function setLayoutOpts(project, screenId, mode, patch) {
|
|
267
|
+
return mapScreen(project, screenId, (s) => ({ ...s, layoutOpts: { ...s.layoutOpts, [mode]: { ...s.layoutOpts?.[mode], ...patch } } }));
|
|
268
|
+
}
|
|
269
|
+
export const CRUD_ACTIONS = ['create', 'update', 'delete'];
|
|
270
|
+
/** Does a role's rules permit opening a screen? */
|
|
271
|
+
export const roleCanScreen = (r, screenId) => r.screens === '*' || r.screens.includes(screenId);
|
|
272
|
+
/** Does a role's rules permit a write action? */
|
|
273
|
+
export const roleCanAction = (r, action) => r.actions === '*' || r.actions.includes(action);
|
|
274
|
+
/** The render mode for a screen. `'ssr'` emits idiomatic SvelteKit (`+page.server.ts`
|
|
275
|
+
* with a `load` + form `actions`, SSR + progressive enhancement); `'spa'` (the
|
|
276
|
+
* default) emits the client data-source-controller page. Opt-in for now. */
|
|
277
|
+
export function screenRenderMode(_project, screen) {
|
|
278
|
+
return screen.renderMode === 'ssr' ? 'ssr' : 'spa';
|
|
279
|
+
}
|
|
280
|
+
/** Block kinds a read-only SSR screen can host: pure renders over server-loaded
|
|
281
|
+
* rows. Board/calendar/scheduler (client interaction runtimes), components
|
|
282
|
+
* (client bindings), grids-with-extras, and containers stay SPA. */
|
|
283
|
+
const SSR_READ_KINDS = new Set(['chart', 'pivot', 'dashboard', 'kpi', 'gauge', 'tree', 'detail', 'master-detail']);
|
|
284
|
+
/** How a screen would emit under SSR: 'grid' (single grid -> load + form actions,
|
|
285
|
+
* URL-driven sort/filter/page), 'read' (data-viz/detail blocks -> load only), or
|
|
286
|
+
* null when it must stay SPA. */
|
|
287
|
+
export function ssrScreenShape(project, screen) {
|
|
288
|
+
if (!screen.entity || screen.code)
|
|
289
|
+
return null;
|
|
290
|
+
const source = project.dataSources?.[screen.entity];
|
|
291
|
+
const kind = source?.kind ?? project.dataSource;
|
|
292
|
+
// memory runs the source in-process; sql reuses the connected /api route via
|
|
293
|
+
// event.fetch; rest calls the remote API straight from the server, which needs
|
|
294
|
+
// an absolute URL to resolve there. (supabase and pglite stay SPA: pglite only
|
|
295
|
+
// exists in the browser, and a Supabase read carries the signed-in user's token,
|
|
296
|
+
// which a server-side call with the anon key would silently drop.)
|
|
297
|
+
if (kind === 'rest') {
|
|
298
|
+
if (!(source?.kind === 'rest' && /^https?:\/\//i.test(source.baseUrl ?? '')))
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
else if (kind !== 'memory' && kind !== 'sql') {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const blocks = screen.blocks ?? [];
|
|
305
|
+
// A grid, optionally preceded by a facet panel. The panel is the reason this
|
|
306
|
+
// allowance exists: every list screen the generator produces is [filter, grid],
|
|
307
|
+
// so without it the shipped app shape could never render on the server. Facets
|
|
308
|
+
// map cleanly onto the URL params `planFromSearchParams` already reads.
|
|
309
|
+
const grids = blocks.filter((b) => b.config.kind === 'grid');
|
|
310
|
+
if (grids.length === 1 && blocks.every((b) => b.config.kind === 'grid' || b.config.kind === 'filter')) {
|
|
311
|
+
const g = grids[0].config;
|
|
312
|
+
if (g.treeData || g.scheduler)
|
|
313
|
+
return null;
|
|
314
|
+
return 'grid';
|
|
315
|
+
}
|
|
316
|
+
if (blocks.length >= 1 && blocks.every((b) => SSR_READ_KINDS.has(b.config.kind)))
|
|
317
|
+
return 'read';
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
/** Whether a screen can be emitted as SSR-native (any supported shape). */
|
|
321
|
+
export function ssrEligible(project, screen) {
|
|
322
|
+
return ssrScreenShape(project, screen) !== null;
|
|
323
|
+
}
|
|
324
|
+
/** Set a screen's render mode ('spa' clears back to the default). */
|
|
325
|
+
export function setScreenRenderMode(project, screenId, mode) {
|
|
326
|
+
return {
|
|
327
|
+
...project,
|
|
328
|
+
screens: project.screens.map((s) => (s.id === screenId ? { ...s, renderMode: mode === 'ssr' ? 'ssr' : undefined } : s)),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** True when the screen should actually emit as SSR (mode is 'ssr' AND eligible). */
|
|
332
|
+
export function isSsrScreen(project, screen) {
|
|
333
|
+
return screenRenderMode(project, screen) === 'ssr' && ssrEligible(project, screen);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Turn server rendering on for every screen that can support it, for entities
|
|
337
|
+
* backed by a real database. Applied when a project is first generated, so a new
|
|
338
|
+
* app reads as idiomatic SvelteKit (a `load` and form `actions`) rather than a
|
|
339
|
+
* client SPA. Screens that already state a mode are left alone, so this never
|
|
340
|
+
* overrides a choice somebody made.
|
|
341
|
+
*
|
|
342
|
+
* Deliberately limited to sources that hold one copy of the data. In-memory and
|
|
343
|
+
* PGlite sources are module singletons, so an SSR screen would read and write the
|
|
344
|
+
* *server's* copy of the rows while the app's remaining SPA screens read the
|
|
345
|
+
* browser's - create a row on one and the other never sees it. SQL and REST have
|
|
346
|
+
* no such split: every path goes to the same database or the same remote API.
|
|
347
|
+
*/
|
|
348
|
+
const SSR_DEFAULT_KINDS = new Set(['sql', 'rest']);
|
|
349
|
+
export function withSsrDefaults(project) {
|
|
350
|
+
return {
|
|
351
|
+
...project,
|
|
352
|
+
screens: project.screens.map((screen) => {
|
|
353
|
+
if (screen.renderMode || !screen.entity)
|
|
354
|
+
return screen;
|
|
355
|
+
const kind = project.dataSources?.[screen.entity]?.kind ?? project.dataSource;
|
|
356
|
+
if (!SSR_DEFAULT_KINDS.has(kind))
|
|
357
|
+
return screen;
|
|
358
|
+
return ssrEligible(project, screen) ? { ...screen, renderMode: 'ssr' } : screen;
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/** The triggers configured for an entity (or an empty object). */
|
|
363
|
+
export function triggersOf(project, entity) {
|
|
364
|
+
return project.triggers?.[entity] ?? {};
|
|
365
|
+
}
|
|
366
|
+
/** Set (or clear, with `null`) an entity's steps for one trigger event. */
|
|
367
|
+
export function setTrigger(project, entity, event, steps) {
|
|
368
|
+
const cur = project.triggers?.[entity] ?? {};
|
|
369
|
+
const nextEnt = { ...cur };
|
|
370
|
+
if (steps && steps.length)
|
|
371
|
+
nextEnt[event] = steps;
|
|
372
|
+
else
|
|
373
|
+
delete nextEnt[event];
|
|
374
|
+
const triggers = { ...project.triggers };
|
|
375
|
+
if (Object.keys(nextEnt).length)
|
|
376
|
+
triggers[entity] = nextEnt;
|
|
377
|
+
else
|
|
378
|
+
delete triggers[entity];
|
|
379
|
+
return { ...project, triggers: Object.keys(triggers).length ? triggers : undefined };
|
|
380
|
+
}
|
|
381
|
+
/** Turn multi-tenancy on/off and configure the scoping column. */
|
|
382
|
+
export function setTenancy(project, tenancy) {
|
|
383
|
+
if (!tenancy || !tenancy.enabled) {
|
|
384
|
+
const { tenancy: _drop, ...rest } = project;
|
|
385
|
+
return rest;
|
|
386
|
+
}
|
|
387
|
+
return { ...project, tenancy };
|
|
388
|
+
}
|
|
389
|
+
/** The tenant column name, defaulted. */
|
|
390
|
+
export const tenantField = (project) => project.tenancy?.field || 'tenantId';
|
|
391
|
+
/** True when `entity` is scoped to a tenant (i.e. tenancy is on and it is not
|
|
392
|
+
* listed as shared). Shared entities stay global reference data. */
|
|
393
|
+
export function isTenantScoped(project, entity) {
|
|
394
|
+
if (project.tenancy?.enabled !== true)
|
|
395
|
+
return false;
|
|
396
|
+
return !(project.tenancy.sharedEntities ?? []).includes(entity);
|
|
397
|
+
}
|
|
398
|
+
/** Add or replace a scheduled job (matched by `id`). Pass `null` to remove one. */
|
|
399
|
+
export function setJob(project, id, job) {
|
|
400
|
+
const rest = (project.jobs ?? []).filter((j) => j.id !== id);
|
|
401
|
+
const next = job ? [...rest, { ...job, id }] : rest;
|
|
402
|
+
return { ...project, jobs: next.length ? next : undefined };
|
|
403
|
+
}
|
|
404
|
+
/** The designer's block palette, in menu order. (Editing is a Grid property, so
|
|
405
|
+
* there's no standalone form block.) */
|
|
406
|
+
export const blockPalette = [
|
|
407
|
+
{ kind: 'grid', label: 'Grid' },
|
|
408
|
+
{ kind: 'chart', label: 'Chart', needs: 'measure' },
|
|
409
|
+
{ kind: 'pivot', label: 'Pivot', needs: 'measure' },
|
|
410
|
+
{ kind: 'dashboard', label: 'Dashboard' },
|
|
411
|
+
{ kind: 'kpi', label: 'KPI tile', needs: 'measure' },
|
|
412
|
+
{ kind: 'gauge', label: 'Gauge', needs: 'measure' },
|
|
413
|
+
{ kind: 'tree', label: 'Tree' },
|
|
414
|
+
{ kind: 'tabs', label: 'Tabs' },
|
|
415
|
+
{ kind: 'accordion', label: 'Accordion' },
|
|
416
|
+
{ kind: 'master-detail', label: 'Master / detail', needs: 'child' },
|
|
417
|
+
{ kind: 'board', label: 'Board' },
|
|
418
|
+
{ kind: 'calendar', label: 'Calendar' },
|
|
419
|
+
{ kind: 'detail', label: 'Detail page' },
|
|
420
|
+
{ kind: 'filter', label: 'Filter panel' },
|
|
421
|
+
{ kind: 'record', label: 'Record panel' },
|
|
422
|
+
{ kind: 'lookup', label: 'Lookup' },
|
|
423
|
+
];
|
|
424
|
+
// --- helpers ---------------------------------------------------------------
|
|
425
|
+
/** A free `${prefix}${n}` id not already in `taken`. */
|
|
426
|
+
function uid(prefix, taken) {
|
|
427
|
+
let n = 1;
|
|
428
|
+
while (taken.has(`${prefix}${n}`))
|
|
429
|
+
n++;
|
|
430
|
+
return `${prefix}${n}`;
|
|
431
|
+
}
|
|
432
|
+
export function entityOf(project, name) {
|
|
433
|
+
if (name == null)
|
|
434
|
+
return undefined;
|
|
435
|
+
return project.entities.find((e) => e.name === name);
|
|
436
|
+
}
|
|
437
|
+
const gridHidden = (f) => f.hidden === true || (typeof f.hidden === 'object' && f.hidden.grid === true);
|
|
438
|
+
function pickDimension(entity) {
|
|
439
|
+
const nonKey = entity.fields.filter((f) => !f.primaryKey);
|
|
440
|
+
// Prefer a low-cardinality dimension: enum, then boolean, then text.
|
|
441
|
+
return (nonKey.find((f) => f.type === 'enum')?.field ??
|
|
442
|
+
nonKey.find((f) => f.type === 'boolean')?.field ??
|
|
443
|
+
nonKey.find((f) => f.type === 'text')?.field ??
|
|
444
|
+
nonKey[0]?.field ??
|
|
445
|
+
entity.fields[0]?.field ??
|
|
446
|
+
'');
|
|
447
|
+
}
|
|
448
|
+
function pickMeasure(entity) {
|
|
449
|
+
return entity.fields.find((f) => f.type === 'number' && !f.primaryKey)?.field;
|
|
450
|
+
}
|
|
451
|
+
/** Sensible grid columns for an entity: every non-grid-hidden field, shown. */
|
|
452
|
+
export function gridColumns(entity) {
|
|
453
|
+
const pk = entity.idField ?? entity.fields.find((f) => f.primaryKey)?.field;
|
|
454
|
+
return entity.fields
|
|
455
|
+
.filter((f) => !gridHidden(f))
|
|
456
|
+
// Hide the raw primary-key column by default (a "co1 / dl2" id column reads
|
|
457
|
+
// as unfinished); everything else is visible. Users can re-enable it.
|
|
458
|
+
.map((f) => ({ field: f.field, show: f.field !== pk, header: f.label }));
|
|
459
|
+
}
|
|
460
|
+
/** A default config for a freshly-added block of `kind`, sized for the entity. */
|
|
461
|
+
export function defaultBlockConfig(kind, entity) {
|
|
462
|
+
switch (kind) {
|
|
463
|
+
case 'grid':
|
|
464
|
+
return { kind, columns: gridColumns(entity), pageSize: 10, selectable: true, sortable: true, filterable: false, editing: 'form', formPresentation: 'modal', density: 'normal', striped: false, cellSelection: false, rowSummaries: false, paginated: true, paginationPosition: 'bottom', pageSizeOptions: [10, 25, 50, 100] };
|
|
465
|
+
case 'form':
|
|
466
|
+
return { kind, presentation: 'modal' };
|
|
467
|
+
case 'chart': {
|
|
468
|
+
const measure = pickMeasure(entity);
|
|
469
|
+
return { kind, dimension: pickDimension(entity), measure, reduce: measure ? 'sum' : 'count', type: 'bar' };
|
|
470
|
+
}
|
|
471
|
+
case 'kpi': {
|
|
472
|
+
const measure = pickMeasure(entity);
|
|
473
|
+
return { kind, label: measure ? `Total ${measure}` : `Total ${entity.label ?? entity.name}`, measure, reduce: measure ? 'sum' : 'count' };
|
|
474
|
+
}
|
|
475
|
+
case 'gauge': {
|
|
476
|
+
const measure = pickMeasure(entity);
|
|
477
|
+
return { kind, label: measure ? `Avg ${measure}` : `${entity.label ?? entity.name} count`, measure, reduce: measure ? 'avg' : 'count', min: 0, max: 100 };
|
|
478
|
+
}
|
|
479
|
+
case 'tree': {
|
|
480
|
+
const label = entity.fields.find((f) => !f.primaryKey && f.type === 'text')?.field ?? entity.fields.find((f) => !f.primaryKey)?.field ?? entity.fields[0]?.field ?? '';
|
|
481
|
+
// A self-referential FK (relation back to this entity) is the natural parent link; else guess by name.
|
|
482
|
+
const parent = entity.fields.find((f) => f.type === 'relation' && f.relation?.entity === entity.name)?.field
|
|
483
|
+
?? entity.fields.find((f) => /parent/i.test(f.field))?.field ?? '';
|
|
484
|
+
return { kind, labelField: label, parentField: parent };
|
|
485
|
+
}
|
|
486
|
+
case 'tabs':
|
|
487
|
+
return { kind, tabs: [{ label: 'Overview', blocks: [] }, { label: 'Details', blocks: [] }] };
|
|
488
|
+
case 'accordion':
|
|
489
|
+
return { kind, sections: [{ label: 'Section 1', blocks: [] }, { label: 'Section 2', blocks: [] }], multiple: false };
|
|
490
|
+
case 'dashboard':
|
|
491
|
+
return { kind };
|
|
492
|
+
case 'master-detail':
|
|
493
|
+
return { kind, childEntity: '', foreignKey: '' };
|
|
494
|
+
case 'lookup':
|
|
495
|
+
return { kind, field: entity.fields.find((f) => f.type === 'relation')?.field ?? entity.fields[0]?.field ?? '' };
|
|
496
|
+
case 'pivot': {
|
|
497
|
+
const measure = pickMeasure(entity);
|
|
498
|
+
const dim = pickDimension(entity);
|
|
499
|
+
return { kind, rows: dim ? [dim] : [], cols: [], measure, aggregate: measure ? 'sum' : 'count' };
|
|
500
|
+
}
|
|
501
|
+
case 'filter':
|
|
502
|
+
return { kind, fields: pickFacetFields(entity) };
|
|
503
|
+
case 'record':
|
|
504
|
+
return { kind, editable: false };
|
|
505
|
+
case 'board': {
|
|
506
|
+
const enumField = entity.fields.find((f) => f.type === 'enum' && !f.primaryKey)?.field ?? '';
|
|
507
|
+
const titleF = entity.fields.find((f) => f.type === 'text' && !f.primaryKey)?.field ?? entity.fields.find((f) => !f.primaryKey)?.field ?? '';
|
|
508
|
+
const badge = pickMeasure(entity);
|
|
509
|
+
return { kind, groupBy: enumField, titleField: titleF, ...(badge ? { badgeField: badge } : {}) };
|
|
510
|
+
}
|
|
511
|
+
case 'calendar': {
|
|
512
|
+
const dateF = entity.fields.find((f) => (f.type === 'datetime' || f.type === 'date' || f.type === 'dateString') && !f.primaryKey)?.field ?? '';
|
|
513
|
+
const titleF = entity.fields.find((f) => f.type === 'text' && !f.primaryKey)?.field ?? entity.fields.find((f) => !f.primaryKey)?.field ?? '';
|
|
514
|
+
const colorF = entity.fields.find((f) => f.type === 'enum' && !f.primaryKey)?.field;
|
|
515
|
+
return { kind, dateField: dateF, titleField: titleF, ...(colorF ? { colorField: colorF } : {}) };
|
|
516
|
+
}
|
|
517
|
+
case 'detail': {
|
|
518
|
+
const nonKey = entity.fields.filter((f) => !f.primaryKey);
|
|
519
|
+
const titleF = nonKey.find((f) => f.type === 'text')?.field ?? nonKey[0]?.field ?? entity.fields[0]?.field ?? '';
|
|
520
|
+
const statusF = nonKey.find((f) => f.type === 'enum')?.field;
|
|
521
|
+
const subF = nonKey.find((f) => (f.type === 'text' || f.type === 'relation') && f.field !== titleF)?.field;
|
|
522
|
+
const metrics = nonKey.filter((f) => f.type === 'number').slice(0, 3).map((f) => f.field);
|
|
523
|
+
return { kind, titleField: titleF, ...(subF ? { subtitleField: subF } : {}), ...(statusF ? { statusField: statusF } : {}), ...(metrics.length ? { metricFields: metrics } : {}) };
|
|
524
|
+
}
|
|
525
|
+
case 'component':
|
|
526
|
+
// Entity-agnostic - built directly by `addComponentBlock`, never through here.
|
|
527
|
+
return { kind, component: '', props: {} };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/** Low-cardinality, filter-friendly fields (enum, boolean, then text), non-key.
|
|
531
|
+
* Used to seed the faceted Filter panel. Caps at 4 so the panel stays compact. */
|
|
532
|
+
export function pickFacetFields(entity) {
|
|
533
|
+
return entity.fields
|
|
534
|
+
.filter((f) => !f.primaryKey && (f.type === 'enum' || f.type === 'boolean' || f.type === 'text'))
|
|
535
|
+
.sort((a, b) => facetRank(a.type) - facetRank(b.type))
|
|
536
|
+
.slice(0, 4)
|
|
537
|
+
.map((f) => f.field);
|
|
538
|
+
}
|
|
539
|
+
const facetRank = (t) => (t === 'enum' ? 0 : t === 'boolean' ? 1 : 2);
|
|
540
|
+
const DEFAULT_SPAN = {
|
|
541
|
+
grid: 3, form: 1, chart: 2, dashboard: 3, kpi: 1, gauge: 1, tree: 2, tabs: 3, accordion: 3, 'master-detail': 3, lookup: 1, pivot: 3, filter: 1, record: 1, board: 3, calendar: 3, detail: 3, component: 1,
|
|
542
|
+
};
|
|
543
|
+
function makeBlock(kind, entity, taken) {
|
|
544
|
+
return { id: uid(`${kind}-`, taken), span: DEFAULT_SPAN[kind], config: defaultBlockConfig(kind, entity) };
|
|
545
|
+
}
|
|
546
|
+
// --- Tabs container ops (pure; the designer builds a new config + updateBlock) ---
|
|
547
|
+
/** Append a tab to a Tabs container. */
|
|
548
|
+
export function addTab(cfg, label) {
|
|
549
|
+
return { ...cfg, tabs: [...cfg.tabs, { label: label ?? `Tab ${cfg.tabs.length + 1}`, blocks: [] }] };
|
|
550
|
+
}
|
|
551
|
+
/** Remove a tab by index (keeps at least one). */
|
|
552
|
+
export function removeTab(cfg, index) {
|
|
553
|
+
if (cfg.tabs.length <= 1)
|
|
554
|
+
return cfg;
|
|
555
|
+
return { ...cfg, tabs: cfg.tabs.filter((_, i) => i !== index) };
|
|
556
|
+
}
|
|
557
|
+
/** Rename a tab by index. */
|
|
558
|
+
export function renameTab(cfg, index, label) {
|
|
559
|
+
return { ...cfg, tabs: cfg.tabs.map((t, i) => (i === index ? { ...t, label } : t)) };
|
|
560
|
+
}
|
|
561
|
+
/** Add a child block (default config for `kind`) to a tab. Only `TAB_CHILD_KINDS` are allowed. */
|
|
562
|
+
export function addTabBlock(cfg, index, kind, entity) {
|
|
563
|
+
if (!TAB_CHILD_KINDS.includes(kind))
|
|
564
|
+
return cfg;
|
|
565
|
+
const taken = new Set();
|
|
566
|
+
cfg.tabs.forEach((t) => flattenBlocks(t.blocks).forEach((b) => taken.add(b.id)));
|
|
567
|
+
const block = { id: uid(`${kind}-`, taken), span: DEFAULT_SPAN[kind], config: defaultBlockConfig(kind, entity) };
|
|
568
|
+
return { ...cfg, tabs: cfg.tabs.map((t, i) => (i === index ? { ...t, blocks: [...t.blocks, block] } : t)) };
|
|
569
|
+
}
|
|
570
|
+
/** Remove a child block from a tab by id. */
|
|
571
|
+
export function removeTabBlock(cfg, index, blockId) {
|
|
572
|
+
return { ...cfg, tabs: cfg.tabs.map((t, i) => (i === index ? { ...t, blocks: t.blocks.filter((b) => b.id !== blockId) } : t)) };
|
|
573
|
+
}
|
|
574
|
+
/** Build a UI-kit component child block for a container section (Tabs / Accordion),
|
|
575
|
+
* seeded with the registry's default props + content. Returns null for an unknown
|
|
576
|
+
* component key. Entity-agnostic - components need no entity. */
|
|
577
|
+
function makeComponentChild(componentKey, taken) {
|
|
578
|
+
const spec = uiComponentSpec(componentKey);
|
|
579
|
+
if (!spec)
|
|
580
|
+
return null;
|
|
581
|
+
const props = {};
|
|
582
|
+
for (const p of spec.props)
|
|
583
|
+
if (p.default != null)
|
|
584
|
+
props[p.key] = p.default;
|
|
585
|
+
if (spec.hasContent)
|
|
586
|
+
props._content = spec.contentDefault ?? spec.label;
|
|
587
|
+
const set = new Set(taken);
|
|
588
|
+
return { id: uid('component-', set), span: DEFAULT_SPAN.component, config: { kind: 'component', component: componentKey, props } };
|
|
589
|
+
}
|
|
590
|
+
/** Add a specific UI-kit component (by registry key) to a tab. */
|
|
591
|
+
export function addTabComponent(cfg, index, componentKey) {
|
|
592
|
+
const taken = new Set();
|
|
593
|
+
cfg.tabs.forEach((t) => flattenBlocks(t.blocks).forEach((b) => taken.add(b.id)));
|
|
594
|
+
const block = makeComponentChild(componentKey, taken);
|
|
595
|
+
if (!block)
|
|
596
|
+
return cfg;
|
|
597
|
+
return { ...cfg, tabs: cfg.tabs.map((t, i) => (i === index ? { ...t, blocks: [...t.blocks, block] } : t)) };
|
|
598
|
+
}
|
|
599
|
+
// --- Accordion container (mirrors the Tabs helpers) ------------------------
|
|
600
|
+
/** Append a section (collapsible panel) to an accordion. */
|
|
601
|
+
export function addAccordionSection(cfg, label) {
|
|
602
|
+
return { ...cfg, sections: [...cfg.sections, { label: label ?? `Section ${cfg.sections.length + 1}`, blocks: [] }] };
|
|
603
|
+
}
|
|
604
|
+
/** Remove a section by index (keeps at least one). */
|
|
605
|
+
export function removeAccordionSection(cfg, index) {
|
|
606
|
+
if (cfg.sections.length <= 1)
|
|
607
|
+
return cfg;
|
|
608
|
+
return { ...cfg, sections: cfg.sections.filter((_, i) => i !== index) };
|
|
609
|
+
}
|
|
610
|
+
/** Rename a section by index. */
|
|
611
|
+
export function renameAccordionSection(cfg, index, label) {
|
|
612
|
+
return { ...cfg, sections: cfg.sections.map((s, i) => (i === index ? { ...s, label } : s)) };
|
|
613
|
+
}
|
|
614
|
+
/** Toggle single- vs multiple-open behaviour. */
|
|
615
|
+
export function setAccordionMultiple(cfg, multiple) {
|
|
616
|
+
return { ...cfg, multiple };
|
|
617
|
+
}
|
|
618
|
+
/** Add a display-block child (default config for `kind`) to a section. Only CONTAINER_CHILD_KINDS. */
|
|
619
|
+
export function addAccordionBlock(cfg, index, kind, entity) {
|
|
620
|
+
if (!CONTAINER_CHILD_KINDS.includes(kind))
|
|
621
|
+
return cfg;
|
|
622
|
+
const taken = new Set();
|
|
623
|
+
cfg.sections.forEach((s) => flattenBlocks(s.blocks).forEach((b) => taken.add(b.id)));
|
|
624
|
+
const block = { id: uid(`${kind}-`, taken), span: DEFAULT_SPAN[kind], config: defaultBlockConfig(kind, entity) };
|
|
625
|
+
return { ...cfg, sections: cfg.sections.map((s, i) => (i === index ? { ...s, blocks: [...s.blocks, block] } : s)) };
|
|
626
|
+
}
|
|
627
|
+
/** Add a specific UI-kit component (by registry key) to a section. */
|
|
628
|
+
export function addAccordionComponent(cfg, index, componentKey) {
|
|
629
|
+
const taken = new Set();
|
|
630
|
+
cfg.sections.forEach((s) => flattenBlocks(s.blocks).forEach((b) => taken.add(b.id)));
|
|
631
|
+
const block = makeComponentChild(componentKey, taken);
|
|
632
|
+
if (!block)
|
|
633
|
+
return cfg;
|
|
634
|
+
return { ...cfg, sections: cfg.sections.map((s, i) => (i === index ? { ...s, blocks: [...s.blocks, block] } : s)) };
|
|
635
|
+
}
|
|
636
|
+
/** Remove a child block from a section by id. */
|
|
637
|
+
export function removeAccordionBlock(cfg, index, blockId) {
|
|
638
|
+
return { ...cfg, sections: cfg.sections.map((s, i) => (i === index ? { ...s, blocks: s.blocks.filter((b) => b.id !== blockId) } : s)) };
|
|
639
|
+
}
|
|
640
|
+
/** A default screen for an entity: a grid (editing via a popup form by default). */
|
|
641
|
+
export function defaultScreenFor(entity) {
|
|
642
|
+
const grid = makeBlock('grid', entity, new Set());
|
|
643
|
+
return { id: entity.name, entity: entity.name, title: entity.label ?? entity.name, route: entity.name, blocks: [grid] };
|
|
644
|
+
}
|
|
645
|
+
/** A new project from a set of entities: one default screen each, in-memory. */
|
|
646
|
+
export function createProject(entities, opts = {}) {
|
|
647
|
+
return {
|
|
648
|
+
title: opts.title ?? 'My Studio App',
|
|
649
|
+
entities: [...entities],
|
|
650
|
+
screens: entities.map(defaultScreenFor),
|
|
651
|
+
dataSource: opts.dataSource ?? 'memory',
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
// --- immutable ops ---------------------------------------------------------
|
|
655
|
+
function mapScreen(project, screenId, fn) {
|
|
656
|
+
return { ...project, screens: project.screens.map((s) => (s.id === screenId ? fn(s) : s)) };
|
|
657
|
+
}
|
|
658
|
+
export function addBlock(project, screenId, kind) {
|
|
659
|
+
return mapScreen(project, screenId, (s) => {
|
|
660
|
+
const entity = entityOf(project, s.entity);
|
|
661
|
+
if (!entity)
|
|
662
|
+
return s;
|
|
663
|
+
const taken = new Set(s.blocks.map((b) => b.id));
|
|
664
|
+
return { ...s, blocks: [...s.blocks, makeBlock(kind, entity, taken)] };
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
/** Insert a new block at `index` (drag-drop from the palette). Clamps to range. */
|
|
668
|
+
export function addBlockAt(project, screenId, kind, index) {
|
|
669
|
+
return mapScreen(project, screenId, (s) => {
|
|
670
|
+
const entity = entityOf(project, s.entity);
|
|
671
|
+
if (!entity)
|
|
672
|
+
return s;
|
|
673
|
+
const taken = new Set(s.blocks.map((b) => b.id));
|
|
674
|
+
const block = makeBlock(kind, entity, taken);
|
|
675
|
+
const blocks = [...s.blocks];
|
|
676
|
+
blocks.splice(Math.max(0, Math.min(index, blocks.length)), 0, block);
|
|
677
|
+
return { ...s, blocks };
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
/** Add a UI-kit component block (see `ui-components.ts`'s `UI_COMPONENT_REGISTRY`)
|
|
681
|
+
* to a screen. Unlike `addBlock`/`addBlockAt`, this needs no entity - it works on
|
|
682
|
+
* any screen, including a freestanding one, or mixed onto an entity-bound one. */
|
|
683
|
+
export function addComponentBlock(project, screenId, componentKey, defaultProps = {}, index) {
|
|
684
|
+
return mapScreen(project, screenId, (s) => {
|
|
685
|
+
const taken = new Set(flattenBlocks(s.blocks).map((b) => b.id));
|
|
686
|
+
const name = uniqueComponentName(componentKey, s);
|
|
687
|
+
const block = { id: uid('component-', taken), span: DEFAULT_SPAN.component, config: { kind: 'component', component: componentKey, props: { ...defaultProps }, name } };
|
|
688
|
+
const blocks = [...s.blocks];
|
|
689
|
+
blocks.splice(index != null ? Math.max(0, Math.min(index, blocks.length)) : blocks.length, 0, block);
|
|
690
|
+
return { ...s, blocks };
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
/** A valid, unique JS identifier for a component handle on a screen, e.g. `button1`.
|
|
694
|
+
* Handles are the named objects code reaches (btn.setLabel(...), btn.onclick = ...). */
|
|
695
|
+
export function componentHandleName(cfg) {
|
|
696
|
+
const raw = (cfg.name ?? cfg.component).trim();
|
|
697
|
+
const id = raw.replace(/[^A-Za-z0-9_$]/g, '_').replace(/^([0-9])/, '_$1');
|
|
698
|
+
return id || 'el';
|
|
699
|
+
}
|
|
700
|
+
function uniqueComponentName(componentKey, screen) {
|
|
701
|
+
const taken = new Set(flattenBlocks(screen.blocks)
|
|
702
|
+
.filter((b) => b.config.kind === 'component')
|
|
703
|
+
.map((b) => componentHandleName(b.config)));
|
|
704
|
+
let n = 1;
|
|
705
|
+
while (taken.has(`${componentKey}${n}`))
|
|
706
|
+
n++;
|
|
707
|
+
return `${componentKey}${n}`;
|
|
708
|
+
}
|
|
709
|
+
/** Rename a component's handle (the variable code uses to reach it). */
|
|
710
|
+
export function setComponentName(project, screenId, blockId, name) {
|
|
711
|
+
return mapScreen(project, screenId, (s) => ({
|
|
712
|
+
...s,
|
|
713
|
+
blocks: mapBlockTree(s.blocks, blockId, (b) => (b.config.kind === 'component' ? { ...b, config: { ...b.config, name: name.trim() || undefined } } : b)),
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
/** True when a component has at least one data-bound prop. */
|
|
717
|
+
export function componentHasBindings(cfg) {
|
|
718
|
+
return !!cfg.bindings && Object.keys(cfg.bindings).length > 0;
|
|
719
|
+
}
|
|
720
|
+
/** Bind (or, with `binding: null`, unbind) one component prop to the screen's data. */
|
|
721
|
+
export function setComponentBinding(project, screenId, blockId, propKey, binding) {
|
|
722
|
+
return mapScreen(project, screenId, (s) => ({
|
|
723
|
+
...s,
|
|
724
|
+
blocks: mapBlockTree(s.blocks, blockId, (b) => {
|
|
725
|
+
if (b.config.kind !== 'component')
|
|
726
|
+
return b;
|
|
727
|
+
const bindings = { ...(b.config.bindings ?? {}) };
|
|
728
|
+
if (binding)
|
|
729
|
+
bindings[propKey] = binding;
|
|
730
|
+
else
|
|
731
|
+
delete bindings[propKey];
|
|
732
|
+
return { ...b, config: { ...b.config, bindings: Object.keys(bindings).length ? bindings : undefined } };
|
|
733
|
+
}),
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
/** Apply `fn` to the block with `id` anywhere in the tree (top level or nested in a Tabs / Accordion container). */
|
|
737
|
+
function mapBlockTree(blocks, id, fn) {
|
|
738
|
+
return blocks.map((b) => {
|
|
739
|
+
if (b.id === id)
|
|
740
|
+
return fn(b);
|
|
741
|
+
if (b.config.kind === 'tabs') {
|
|
742
|
+
return { ...b, config: { ...b.config, tabs: b.config.tabs.map((t) => ({ ...t, blocks: mapBlockTree(t.blocks, id, fn) })) } };
|
|
743
|
+
}
|
|
744
|
+
if (b.config.kind === 'accordion') {
|
|
745
|
+
return { ...b, config: { ...b.config, sections: b.config.sections.map((s) => ({ ...s, blocks: mapBlockTree(s.blocks, id, fn) })) } };
|
|
746
|
+
}
|
|
747
|
+
return b;
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/** Remove the block with `id` anywhere in the tree (top level or nested in a Tabs / Accordion container). */
|
|
751
|
+
function removeBlockTree(blocks, id) {
|
|
752
|
+
return blocks
|
|
753
|
+
.filter((b) => b.id !== id)
|
|
754
|
+
.map((b) => b.config.kind === 'tabs' ? { ...b, config: { ...b.config, tabs: b.config.tabs.map((t) => ({ ...t, blocks: removeBlockTree(t.blocks, id) })) } }
|
|
755
|
+
: b.config.kind === 'accordion' ? { ...b, config: { ...b.config, sections: b.config.sections.map((s) => ({ ...s, blocks: removeBlockTree(s.blocks, id) })) } }
|
|
756
|
+
: b);
|
|
757
|
+
}
|
|
758
|
+
export function removeBlock(project, screenId, blockId) {
|
|
759
|
+
return mapScreen(project, screenId, (s) => ({ ...s, blocks: removeBlockTree(s.blocks, blockId) }));
|
|
760
|
+
}
|
|
761
|
+
/** Clone a block (config + width/height) with a fresh id, inserted right after it. */
|
|
762
|
+
export function duplicateBlock(project, screenId, blockId) {
|
|
763
|
+
return mapScreen(project, screenId, (s) => {
|
|
764
|
+
const idx = s.blocks.findIndex((b) => b.id === blockId);
|
|
765
|
+
if (idx < 0)
|
|
766
|
+
return s;
|
|
767
|
+
const src = s.blocks[idx];
|
|
768
|
+
// JSON clone (not structuredClone): the config is always JSON-safe, and this
|
|
769
|
+
// also unwraps any Svelte reactive $state proxy, which structuredClone rejects.
|
|
770
|
+
const clone = { ...src, id: uid(`${src.config.kind}-`, new Set(s.blocks.map((b) => b.id))), config: JSON.parse(JSON.stringify(src.config)) };
|
|
771
|
+
return { ...s, blocks: [...s.blocks.slice(0, idx + 1), clone, ...s.blocks.slice(idx + 1)] };
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
/** Move a block one slot up (`-1`) or down (`+1`). No-op at the ends. */
|
|
775
|
+
export function moveBlock(project, screenId, blockId, dir) {
|
|
776
|
+
return mapScreen(project, screenId, (s) => {
|
|
777
|
+
const i = s.blocks.findIndex((b) => b.id === blockId);
|
|
778
|
+
const j = i + dir;
|
|
779
|
+
if (i < 0 || j < 0 || j >= s.blocks.length)
|
|
780
|
+
return s;
|
|
781
|
+
const blocks = [...s.blocks];
|
|
782
|
+
[blocks[i], blocks[j]] = [blocks[j], blocks[i]];
|
|
783
|
+
return { ...s, blocks };
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/** Reorder a block to an explicit index (drag-drop). Clamps to range. */
|
|
787
|
+
export function reorderBlock(project, screenId, blockId, toIndex) {
|
|
788
|
+
return mapScreen(project, screenId, (s) => {
|
|
789
|
+
const from = s.blocks.findIndex((b) => b.id === blockId);
|
|
790
|
+
if (from < 0)
|
|
791
|
+
return s;
|
|
792
|
+
const blocks = [...s.blocks];
|
|
793
|
+
const [moved] = blocks.splice(from, 1);
|
|
794
|
+
blocks.splice(Math.max(0, Math.min(toIndex, blocks.length)), 0, moved);
|
|
795
|
+
return { ...s, blocks };
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/** Patch a block's config (merged), span/height, and/or appearance `style` (merged;
|
|
799
|
+
* keys set to undefined are cleared). */
|
|
800
|
+
export function updateBlock(project, screenId, blockId, patch) {
|
|
801
|
+
return mapScreen(project, screenId, (s) => ({
|
|
802
|
+
...s,
|
|
803
|
+
blocks: mapBlockTree(s.blocks, blockId, (b) => ({
|
|
804
|
+
...b,
|
|
805
|
+
span: patch.span ?? b.span,
|
|
806
|
+
colSpan: patch.colSpan ?? b.colSpan,
|
|
807
|
+
height: patch.height ?? b.height,
|
|
808
|
+
style: patch.style ? mergeBlockStyle(b.style, patch.style) : b.style,
|
|
809
|
+
className: patch.className !== undefined ? (patch.className.trim() || undefined) : b.className,
|
|
810
|
+
config: patch.config ? { ...b.config, ...patch.config } : b.config,
|
|
811
|
+
})),
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
/** Replace an entity's schema (e.g. after field edits), keeping its screens. */
|
|
815
|
+
export function updateEntity(project, name, schema) {
|
|
816
|
+
const entities = project.entities.map((e) => (e.name === name ? schema : e));
|
|
817
|
+
if (name === schema.name)
|
|
818
|
+
return { ...project, entities };
|
|
819
|
+
// Renamed: retarget its screens AND any master-detail block pointing at it.
|
|
820
|
+
const screens = project.screens.map((s) => {
|
|
821
|
+
const blocks = s.blocks.map((b) => b.config.kind === 'master-detail' && b.config.childEntity === name
|
|
822
|
+
? { ...b, config: { ...b.config, childEntity: schema.name } }
|
|
823
|
+
: b);
|
|
824
|
+
return { ...s, entity: s.entity === name ? schema.name : s.entity, blocks };
|
|
825
|
+
});
|
|
826
|
+
return { ...project, entities, screens };
|
|
827
|
+
}
|
|
828
|
+
/** Add an entity + its default screen. */
|
|
829
|
+
export function addEntity(project, schema) {
|
|
830
|
+
if (entityOf(project, schema.name))
|
|
831
|
+
return project;
|
|
832
|
+
return { ...project, entities: [...project.entities, schema], screens: [...project.screens, defaultScreenFor(schema)] };
|
|
833
|
+
}
|
|
834
|
+
/** Remove an entity and every screen bound to it. */
|
|
835
|
+
export function removeEntity(project, name) {
|
|
836
|
+
return {
|
|
837
|
+
...project,
|
|
838
|
+
entities: project.entities.filter((e) => e.name !== name),
|
|
839
|
+
screens: project.screens.filter((s) => s.entity !== name),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
/** Append a screen, making its id AND route unique against the existing set. */
|
|
843
|
+
function appendScreen(project, base, keyBase) {
|
|
844
|
+
const ids = new Set(project.screens.map((s) => s.id));
|
|
845
|
+
const routes = new Set(project.screens.map((s) => s.route));
|
|
846
|
+
const id = ids.has(base.id) ? uid(`${keyBase}-`, ids) : base.id;
|
|
847
|
+
const route = routes.has(base.route) ? uid(`${base.route}-`, routes) : base.route;
|
|
848
|
+
return { ...project, screens: [...project.screens, { ...base, id, route }] };
|
|
849
|
+
}
|
|
850
|
+
export function addScreen(project, entity) {
|
|
851
|
+
const schema = entityOf(project, entity);
|
|
852
|
+
if (!schema)
|
|
853
|
+
return project;
|
|
854
|
+
return appendScreen(project, defaultScreenFor(schema), entity);
|
|
855
|
+
}
|
|
856
|
+
export function removeScreen(project, screenId) {
|
|
857
|
+
return { ...project, screens: project.screens.filter((s) => s.id !== screenId) };
|
|
858
|
+
}
|
|
859
|
+
export function updateScreen(project, screenId,
|
|
860
|
+
// `renderMode` is patchable here so SSR output is reachable without the
|
|
861
|
+
// designer - the CLI and the MCP tools drive the model through this function.
|
|
862
|
+
patch) {
|
|
863
|
+
return mapScreen(project, screenId, (s) => ({ ...s, ...patch }));
|
|
864
|
+
}
|
|
865
|
+
// ---- Screen layout: 12-column grid (default) or a docking workspace ----------
|
|
866
|
+
/** The screen's layout engine. Defaults to `'grid'` (the 12-column responsive grid). */
|
|
867
|
+
export function screenLayoutOf(screen) {
|
|
868
|
+
return screen.layout ?? 'grid';
|
|
869
|
+
}
|
|
870
|
+
/** A short title for a block's dock pane. */
|
|
871
|
+
function dockPaneTitle(block) {
|
|
872
|
+
const c = block.config;
|
|
873
|
+
const byKind = {
|
|
874
|
+
grid: 'Grid', form: 'Form', chart: 'Chart', dashboard: 'Dashboard', tree: 'Tree', pivot: 'Pivot',
|
|
875
|
+
filter: 'Filters', record: 'Record', detail: 'Detail', board: 'Board', calendar: 'Calendar',
|
|
876
|
+
'master-detail': 'Master / detail', lookup: 'Lookup', tabs: 'Tabs', accordion: 'Accordion',
|
|
877
|
+
kpi: 'KPI', gauge: 'Gauge', component: 'Component',
|
|
878
|
+
};
|
|
879
|
+
return c.label || c.title || c.name || c.component || byKind[c.kind] || 'Panel';
|
|
880
|
+
}
|
|
881
|
+
/** Every pane id present anywhere in a dock workspace (main tree + floating + auto-hidden). */
|
|
882
|
+
export function dockPaneIds(state) {
|
|
883
|
+
const ids = new Set();
|
|
884
|
+
const walk = (n) => { if (n.type === 'tabs')
|
|
885
|
+
for (const p of n.panes)
|
|
886
|
+
ids.add(p.id);
|
|
887
|
+
else
|
|
888
|
+
for (const c of n.children)
|
|
889
|
+
walk(c); };
|
|
890
|
+
if (state.main)
|
|
891
|
+
walk(state.main);
|
|
892
|
+
for (const w of state.floating)
|
|
893
|
+
for (const p of w.leaf.panes)
|
|
894
|
+
ids.add(p.id);
|
|
895
|
+
for (const e of state.autoHide)
|
|
896
|
+
for (const p of e.leaf.panes)
|
|
897
|
+
ids.add(p.id);
|
|
898
|
+
return ids;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Smart auto-layout: seed a docking workspace from a screen's blocks by role -
|
|
902
|
+
* filters dock left, record / detail panels dock right, KPIs / gauges strip across
|
|
903
|
+
* the top, and the main content (grid / board / calendar / chart / ...) fills the
|
|
904
|
+
* centre. Pane ids equal block ids. Runtime float / pin / pop-out is layered on later.
|
|
905
|
+
*/
|
|
906
|
+
export function buildDockLayout(screen) {
|
|
907
|
+
const taken = new Set(screen.blocks.map((b) => b.id));
|
|
908
|
+
const genId = (p) => { const id = uid(p, taken); taken.add(id); return id; };
|
|
909
|
+
const paneFor = (b) => ({ id: b.id, title: dockPaneTitle(b), closable: true });
|
|
910
|
+
const roleOf = (b) => {
|
|
911
|
+
switch (b.config.kind) {
|
|
912
|
+
case 'filter': return 'left';
|
|
913
|
+
case 'record':
|
|
914
|
+
case 'detail': return 'right';
|
|
915
|
+
case 'kpi':
|
|
916
|
+
case 'gauge': return 'top';
|
|
917
|
+
default: return 'center';
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
const groupOf = (direction, children, sizes) => ({ type: 'group', id: genId('dg-'), direction, children, sizes });
|
|
921
|
+
const norm = (ws) => { const s = ws.reduce((a, b) => a + b, 0) || 1; return ws.map((w) => w / s); };
|
|
922
|
+
// Each block gets its OWN leaf, so split view can size every block independently
|
|
923
|
+
// (grouping them into one tabs leaf would trap them as tabs - unresizable when
|
|
924
|
+
// the manager is locked). Multiple blocks in a bucket stack along the given axis.
|
|
925
|
+
const oneLeaf = (b) => ({ type: 'tabs', id: genId('dt-'), panes: [paneFor(b)], active: 0 });
|
|
926
|
+
const stackOf = (bs, direction) => bs.length === 1 ? oneLeaf(bs[0]) : groupOf(direction, bs.map(oneLeaf), norm(bs.map(() => 1)));
|
|
927
|
+
// A forced split orientation (split-view setting) ignores roles and lays ALL
|
|
928
|
+
// blocks out as equal panes in one row / column.
|
|
929
|
+
const forced = splitOpts(screen).orientation;
|
|
930
|
+
if (forced !== 'auto' && screen.blocks.length) {
|
|
931
|
+
return { main: stackOf(screen.blocks, forced), floating: [], autoHide: [] };
|
|
932
|
+
}
|
|
933
|
+
const buckets = { left: [], center: [], right: [], top: [] };
|
|
934
|
+
for (const b of screen.blocks)
|
|
935
|
+
buckets[roleOf(b)].push(b);
|
|
936
|
+
const rowChildren = [];
|
|
937
|
+
const rowWeights = [];
|
|
938
|
+
if (buckets.left.length) {
|
|
939
|
+
rowChildren.push(stackOf(buckets.left, 'column'));
|
|
940
|
+
rowWeights.push(1);
|
|
941
|
+
}
|
|
942
|
+
if (buckets.center.length) {
|
|
943
|
+
rowChildren.push(stackOf(buckets.center, 'column'));
|
|
944
|
+
rowWeights.push(3);
|
|
945
|
+
}
|
|
946
|
+
if (buckets.right.length) {
|
|
947
|
+
rowChildren.push(stackOf(buckets.right, 'column'));
|
|
948
|
+
rowWeights.push(1);
|
|
949
|
+
}
|
|
950
|
+
const row = rowChildren.length === 0 ? null : rowChildren.length === 1 ? rowChildren[0] : groupOf('row', rowChildren, norm(rowWeights));
|
|
951
|
+
let main;
|
|
952
|
+
if (buckets.top.length && row)
|
|
953
|
+
main = groupOf('column', [stackOf(buckets.top, 'row'), row], [0.28, 0.72]);
|
|
954
|
+
else if (buckets.top.length)
|
|
955
|
+
main = stackOf(buckets.top, 'row');
|
|
956
|
+
else if (row)
|
|
957
|
+
main = row;
|
|
958
|
+
else
|
|
959
|
+
main = { type: 'tabs', id: genId('dt-'), panes: [], active: 0 };
|
|
960
|
+
return { main, floating: [], autoHide: [] };
|
|
961
|
+
}
|
|
962
|
+
/** Every group / tabs node id in a workspace (to keep freshly generated ids unique). */
|
|
963
|
+
function collectDockNodeIds(state) {
|
|
964
|
+
const ids = [];
|
|
965
|
+
const walk = (n) => { ids.push(n.id); if (n.type === 'group')
|
|
966
|
+
for (const c of n.children)
|
|
967
|
+
walk(c); };
|
|
968
|
+
if (state.main)
|
|
969
|
+
walk(state.main);
|
|
970
|
+
for (const w of state.floating) {
|
|
971
|
+
ids.push(w.id);
|
|
972
|
+
walk(w.leaf);
|
|
973
|
+
}
|
|
974
|
+
for (const e of state.autoHide) {
|
|
975
|
+
ids.push(e.id);
|
|
976
|
+
walk(e.leaf);
|
|
977
|
+
}
|
|
978
|
+
return ids;
|
|
979
|
+
}
|
|
980
|
+
/** Append a pane to the main area's first tabs leaf (creating one if the area is empty). */
|
|
981
|
+
function addDockPaneToMain(state, p, genId) {
|
|
982
|
+
const main = state.main;
|
|
983
|
+
if (!main)
|
|
984
|
+
return { ...state, main: { type: 'tabs', id: genId(), panes: [p], active: 0 } };
|
|
985
|
+
let added = false;
|
|
986
|
+
const add = (n) => {
|
|
987
|
+
if (added)
|
|
988
|
+
return n;
|
|
989
|
+
if (n.type === 'tabs') {
|
|
990
|
+
added = true;
|
|
991
|
+
return { ...n, panes: [...n.panes, p], active: n.panes.length };
|
|
992
|
+
}
|
|
993
|
+
return { ...n, children: n.children.map(add) };
|
|
994
|
+
};
|
|
995
|
+
const nextMain = add(main);
|
|
996
|
+
if (added)
|
|
997
|
+
return { ...state, main: nextMain };
|
|
998
|
+
// No tabs leaf anywhere: put main + a new tabs side by side.
|
|
999
|
+
return { ...state, main: { type: 'group', id: genId(), direction: 'row', children: [main, { type: 'tabs', id: genId(), panes: [p], active: 0 }], sizes: [0.7, 0.3] } };
|
|
1000
|
+
}
|
|
1001
|
+
/** Remove a pane anywhere in a workspace; collapse emptied tabs + single-child groups. */
|
|
1002
|
+
function stripDockPane(state, paneId) {
|
|
1003
|
+
const strip = (n) => {
|
|
1004
|
+
if (n.type === 'tabs') {
|
|
1005
|
+
const panes = n.panes.filter((p) => p.id !== paneId);
|
|
1006
|
+
return panes.length ? { ...n, panes, active: Math.min(n.active, panes.length - 1) } : null;
|
|
1007
|
+
}
|
|
1008
|
+
const children = n.children.map(strip).filter((c) => c !== null);
|
|
1009
|
+
if (children.length === 0)
|
|
1010
|
+
return null;
|
|
1011
|
+
if (children.length === 1)
|
|
1012
|
+
return children[0];
|
|
1013
|
+
return { ...n, children, sizes: children.map(() => 1 / children.length) };
|
|
1014
|
+
};
|
|
1015
|
+
const main = state.main ? strip(state.main) : null;
|
|
1016
|
+
const floating = state.floating.map((w) => ({ ...w, leaf: { ...w.leaf, panes: w.leaf.panes.filter((p) => p.id !== paneId) } })).filter((w) => w.leaf.panes.length > 0);
|
|
1017
|
+
const autoHide = state.autoHide.map((e) => ({ ...e, leaf: { ...e.leaf, panes: e.leaf.panes.filter((p) => p.id !== paneId) } })).filter((e) => e.leaf.panes.length > 0);
|
|
1018
|
+
return { ...state, main, floating, autoHide };
|
|
1019
|
+
}
|
|
1020
|
+
/** Incrementally reconcile a dock workspace to the screen's blocks - add a pane for each new
|
|
1021
|
+
* block, strip panes for removed blocks - WITHOUT rebuilding, so the user's arrangement
|
|
1022
|
+
* (splits / floats / pins) is preserved. Seeds a fresh workspace if none exists. */
|
|
1023
|
+
export function syncDockPanes(screen) {
|
|
1024
|
+
if (!isPaneLayout(screenLayoutOf(screen)))
|
|
1025
|
+
return screen;
|
|
1026
|
+
if (!screen.dock)
|
|
1027
|
+
return { ...screen, dock: buildDockLayout(screen) };
|
|
1028
|
+
const blockIds = new Set(screen.blocks.map((b) => b.id));
|
|
1029
|
+
const present = dockPaneIds(screen.dock);
|
|
1030
|
+
const missing = screen.blocks.filter((b) => !present.has(b.id));
|
|
1031
|
+
const extra = [...present].filter((id) => !blockIds.has(id));
|
|
1032
|
+
if (missing.length === 0 && extra.length === 0)
|
|
1033
|
+
return screen;
|
|
1034
|
+
let dock = screen.dock;
|
|
1035
|
+
for (const id of extra)
|
|
1036
|
+
dock = stripDockPane(dock, id);
|
|
1037
|
+
const taken = new Set([...blockIds, ...collectDockNodeIds(dock)]);
|
|
1038
|
+
const genId = () => { const id = uid('dt-', taken); taken.add(id); return id; };
|
|
1039
|
+
for (const b of missing)
|
|
1040
|
+
dock = addDockPaneToMain(dock, { id: b.id, title: dockPaneTitle(b), closable: true }, genId);
|
|
1041
|
+
return { ...screen, dock };
|
|
1042
|
+
}
|
|
1043
|
+
/** Rebuild the dock workspace if its panes no longer match the screen's blocks (a safety
|
|
1044
|
+
* net after block add / delete while the visual editor hasn't reconciled incrementally). */
|
|
1045
|
+
export function reconcileDock(screen) {
|
|
1046
|
+
if (!isPaneLayout(screenLayoutOf(screen)))
|
|
1047
|
+
return screen;
|
|
1048
|
+
const blockIds = new Set(screen.blocks.map((b) => b.id));
|
|
1049
|
+
const paneIds = screen.dock ? dockPaneIds(screen.dock) : new Set();
|
|
1050
|
+
const same = !!screen.dock && blockIds.size === paneIds.size && [...blockIds].every((id) => paneIds.has(id));
|
|
1051
|
+
return same ? screen : { ...screen, dock: buildDockLayout(screen) };
|
|
1052
|
+
}
|
|
1053
|
+
/** Switch a screen between the 12-column grid and a docking workspace. Switching to
|
|
1054
|
+
* `'dock'` seeds a workspace (smart auto-layout) if none exists yet. */
|
|
1055
|
+
export function setScreenLayout(project, screenId, layout) {
|
|
1056
|
+
// Both pane layouts (split / dock) back onto a DockManagerState; canvas seeds
|
|
1057
|
+
// explicit cell coordinates. Switching split<->dock preserves the existing
|
|
1058
|
+
// arrangement, but coming FROM a non-pane layout (grid / stack / canvas) seeds a
|
|
1059
|
+
// fresh one - so a stale arrangement never carries over into a new workspace.
|
|
1060
|
+
return mapScreen(project, screenId, (s) => {
|
|
1061
|
+
if (isPaneLayout(layout)) {
|
|
1062
|
+
const dock = isPaneLayout(screenLayoutOf(s)) && s.dock ? s.dock : buildDockLayout(s);
|
|
1063
|
+
return { ...s, layout, dock };
|
|
1064
|
+
}
|
|
1065
|
+
if (layout === 'canvas')
|
|
1066
|
+
return { ...s, layout, canvas: s.canvas ?? buildCanvasLayout(s) };
|
|
1067
|
+
return { ...s, layout };
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
// --- free-form canvas geometry ----------------------------------------------
|
|
1071
|
+
const clampInt = (n, lo, hi) => Math.max(lo, Math.min(hi, Math.round(n)));
|
|
1072
|
+
/** A sensible default row height (in canvas rows) for a block kind - tall for
|
|
1073
|
+
* data views, short for a single stat. */
|
|
1074
|
+
function defaultCanvasRows(block) {
|
|
1075
|
+
const kind = block.config.kind;
|
|
1076
|
+
if (kind === 'kpi' || kind === 'gauge')
|
|
1077
|
+
return 3;
|
|
1078
|
+
if (kind === 'filter')
|
|
1079
|
+
return 4;
|
|
1080
|
+
if (kind === 'chart' || kind === 'record' || kind === 'detail')
|
|
1081
|
+
return 6;
|
|
1082
|
+
return 8; // grid / board / pivot / tree / dashboard / master-detail / form / etc.
|
|
1083
|
+
}
|
|
1084
|
+
/** Seed canvas rects for every block: flow them two-up (half-width each) down the
|
|
1085
|
+
* page, so switching to canvas gives a reasonable starting arrangement. */
|
|
1086
|
+
export function buildCanvasLayout(screen) {
|
|
1087
|
+
const cols = canvasOpts(screen).cols;
|
|
1088
|
+
const out = {};
|
|
1089
|
+
let row = 0;
|
|
1090
|
+
let col = 0;
|
|
1091
|
+
let rowMax = 0;
|
|
1092
|
+
for (const b of screen.blocks) {
|
|
1093
|
+
const colSpan = Math.max(1, Math.round(cols / 2));
|
|
1094
|
+
const rowSpan = defaultCanvasRows(b);
|
|
1095
|
+
if (col + colSpan > cols) {
|
|
1096
|
+
col = 0;
|
|
1097
|
+
row += rowMax;
|
|
1098
|
+
rowMax = 0;
|
|
1099
|
+
}
|
|
1100
|
+
out[b.id] = { col, row, colSpan, rowSpan };
|
|
1101
|
+
col += colSpan;
|
|
1102
|
+
rowMax = Math.max(rowMax, rowSpan);
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
/** The stored rect for a block, or a computed default (so a block added after the
|
|
1107
|
+
* layout was seeded still lands somewhere sane). */
|
|
1108
|
+
export function canvasRectOf(screen, blockId) {
|
|
1109
|
+
const stored = screen.canvas?.[blockId];
|
|
1110
|
+
const cols = canvasOpts(screen).cols;
|
|
1111
|
+
if (stored) {
|
|
1112
|
+
// Clamp a stored rect to the current column count (it may have been placed
|
|
1113
|
+
// when the canvas had more columns).
|
|
1114
|
+
const colSpan = Math.max(1, Math.min(stored.colSpan, cols));
|
|
1115
|
+
return { ...stored, colSpan, col: Math.max(0, Math.min(stored.col, cols - colSpan)) };
|
|
1116
|
+
}
|
|
1117
|
+
return buildCanvasLayout(screen)[blockId] ?? { col: 0, row: 0, colSpan: Math.max(1, Math.round(cols / 2)), rowSpan: 8 };
|
|
1118
|
+
}
|
|
1119
|
+
/** Move / resize a block on the canvas. Values are clamped to the screen's column
|
|
1120
|
+
* count (col in-grid, colSpan 1..cols; row >= 0, rowSpan >= 1). */
|
|
1121
|
+
export function setCanvasRect(project, screenId, blockId, rect) {
|
|
1122
|
+
return mapScreen(project, screenId, (s) => {
|
|
1123
|
+
const cols = canvasOpts(s).cols;
|
|
1124
|
+
const cur = canvasRectOf(s, blockId);
|
|
1125
|
+
const colSpan = clampInt(rect.colSpan ?? cur.colSpan, 1, cols);
|
|
1126
|
+
const col = clampInt(rect.col ?? cur.col, 0, cols - colSpan);
|
|
1127
|
+
const rowSpan = Math.max(1, clampInt(rect.rowSpan ?? cur.rowSpan, 1, 999));
|
|
1128
|
+
const row = Math.max(0, clampInt(rect.row ?? cur.row, 0, 999));
|
|
1129
|
+
return { ...s, canvas: { ...s.canvas, [blockId]: { col, row, colSpan, rowSpan } } };
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const GRID_PRESET_LABELS = {
|
|
1133
|
+
full: 'Full width',
|
|
1134
|
+
'two-col': 'Two columns',
|
|
1135
|
+
'three-col': 'Three columns',
|
|
1136
|
+
sidebar: 'Sidebar + main',
|
|
1137
|
+
'kpi-row': 'KPI row + full',
|
|
1138
|
+
};
|
|
1139
|
+
export function gridPresetLabel(preset) { return GRID_PRESET_LABELS[preset]; }
|
|
1140
|
+
export const GRID_PRESETS = ['full', 'two-col', 'three-col', 'sidebar', 'kpi-row'];
|
|
1141
|
+
/** Small, wide KPI-like blocks that a "KPI row" preset lays out as a strip. */
|
|
1142
|
+
const KPI_ROW_KINDS = ['kpi', 'gauge'];
|
|
1143
|
+
/** Apply a grid preset: rewrite the top-level blocks' `colSpan` to the pattern.
|
|
1144
|
+
* Only touches the current screen's own blocks (nested container children keep
|
|
1145
|
+
* their spans). A no-op for a screen with no blocks. */
|
|
1146
|
+
export function applyGridPreset(project, screenId, preset) {
|
|
1147
|
+
return mapScreen(project, screenId, (s) => {
|
|
1148
|
+
if (s.blocks.length === 0)
|
|
1149
|
+
return s;
|
|
1150
|
+
const span = (i, b) => {
|
|
1151
|
+
switch (preset) {
|
|
1152
|
+
case 'full': return 12;
|
|
1153
|
+
case 'two-col': return 6;
|
|
1154
|
+
case 'three-col': return 4;
|
|
1155
|
+
// Narrow first block (filters / nav) beside a wide content column.
|
|
1156
|
+
case 'sidebar': return i === 0 ? 4 : 8;
|
|
1157
|
+
// KPI-ish blocks form a 4-across strip; everything else spans full.
|
|
1158
|
+
case 'kpi-row': return KPI_ROW_KINDS.includes(b.config.kind) ? 3 : 12;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
return { ...s, blocks: s.blocks.map((b, i) => ({ ...b, colSpan: span(i, b) })) };
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/** Persist a new dock workspace (user rearranged panes in the designer). */
|
|
1165
|
+
export function setScreenDock(project, screenId, dock) {
|
|
1166
|
+
return mapScreen(project, screenId, (s) => ({ ...s, dock }));
|
|
1167
|
+
}
|
|
1168
|
+
// --- screen state variables --------------------------------------------------
|
|
1169
|
+
/** Add a state variable (unique name; `state_N` if the name is taken / empty). */
|
|
1170
|
+
export function addStateVar(project, screenId, v = {}) {
|
|
1171
|
+
return mapScreen(project, screenId, (s) => {
|
|
1172
|
+
const taken = new Set((s.state ?? []).map((x) => x.name));
|
|
1173
|
+
let name = identSafe(v.name || 'value');
|
|
1174
|
+
if (taken.has(name)) {
|
|
1175
|
+
let i = 2;
|
|
1176
|
+
while (taken.has(`${name}${i}`))
|
|
1177
|
+
i++;
|
|
1178
|
+
name = `${name}${i}`;
|
|
1179
|
+
}
|
|
1180
|
+
const next = { name, type: v.type ?? 'string', initial: v.initial };
|
|
1181
|
+
return { ...s, state: [...(s.state ?? []), next] };
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
/** Patch a state variable by (old) name. Renames keep the identifier valid + unique. */
|
|
1185
|
+
export function updateStateVar(project, screenId, name, patch) {
|
|
1186
|
+
return mapScreen(project, screenId, (s) => {
|
|
1187
|
+
const cur = s.state ?? [];
|
|
1188
|
+
const taken = new Set(cur.filter((x) => x.name !== name).map((x) => x.name));
|
|
1189
|
+
return {
|
|
1190
|
+
...s,
|
|
1191
|
+
state: cur.map((x) => {
|
|
1192
|
+
if (x.name !== name)
|
|
1193
|
+
return x;
|
|
1194
|
+
let nm = patch.name !== undefined ? identSafe(patch.name) : x.name;
|
|
1195
|
+
if (nm !== x.name && taken.has(nm)) {
|
|
1196
|
+
let i = 2;
|
|
1197
|
+
while (taken.has(`${nm}${i}`))
|
|
1198
|
+
i++;
|
|
1199
|
+
nm = `${nm}${i}`;
|
|
1200
|
+
}
|
|
1201
|
+
return { ...x, ...patch, name: nm };
|
|
1202
|
+
}),
|
|
1203
|
+
};
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
/** Remove a state variable by name. */
|
|
1207
|
+
export function removeStateVar(project, screenId, name) {
|
|
1208
|
+
return mapScreen(project, screenId, (s) => ({ ...s, state: (s.state ?? []).filter((x) => x.name !== name) }));
|
|
1209
|
+
}
|
|
1210
|
+
/** Rebuild the pane arrangement from scratch (smart auto-layout, honoring the
|
|
1211
|
+
* current split orientation). Used when a setting invalidates the layout - e.g.
|
|
1212
|
+
* the user flips split orientation - or as a "reset arrangement" action. */
|
|
1213
|
+
export function reseedScreenDock(project, screenId) {
|
|
1214
|
+
return mapScreen(project, screenId, (s) => ({ ...s, dock: buildDockLayout(s) }));
|
|
1215
|
+
}
|
|
1216
|
+
/** The current title (tab text) of a block's dock pane, if the screen is docked. */
|
|
1217
|
+
export function dockPaneTitleOf(screen, paneId) {
|
|
1218
|
+
if (!screen.dock)
|
|
1219
|
+
return undefined;
|
|
1220
|
+
let found;
|
|
1221
|
+
const walk = (n) => { if (n.type === 'tabs') {
|
|
1222
|
+
const p = n.panes.find((x) => x.id === paneId);
|
|
1223
|
+
if (p)
|
|
1224
|
+
found = p.title;
|
|
1225
|
+
}
|
|
1226
|
+
else
|
|
1227
|
+
for (const c of n.children)
|
|
1228
|
+
walk(c); };
|
|
1229
|
+
if (screen.dock.main)
|
|
1230
|
+
walk(screen.dock.main);
|
|
1231
|
+
for (const w of screen.dock.floating) {
|
|
1232
|
+
const p = w.leaf.panes.find((x) => x.id === paneId);
|
|
1233
|
+
if (p)
|
|
1234
|
+
found = p.title;
|
|
1235
|
+
}
|
|
1236
|
+
for (const e of screen.dock.autoHide) {
|
|
1237
|
+
const p = e.leaf.panes.find((x) => x.id === paneId);
|
|
1238
|
+
if (p)
|
|
1239
|
+
found = p.title;
|
|
1240
|
+
}
|
|
1241
|
+
return found;
|
|
1242
|
+
}
|
|
1243
|
+
/** Rename a block's dock pane (the tab text) anywhere in the workspace. */
|
|
1244
|
+
export function setDockPaneTitle(project, screenId, paneId, title) {
|
|
1245
|
+
return mapScreen(project, screenId, (s) => {
|
|
1246
|
+
if (!s.dock)
|
|
1247
|
+
return s;
|
|
1248
|
+
const relabelPanes = (panes) => panes.map((p) => (p.id === paneId ? { ...p, title } : p));
|
|
1249
|
+
const relabel = (n) => (n.type === 'tabs' ? { ...n, panes: relabelPanes(n.panes) } : { ...n, children: n.children.map(relabel) });
|
|
1250
|
+
const main = s.dock.main ? relabel(s.dock.main) : null;
|
|
1251
|
+
const floating = s.dock.floating.map((w) => ({ ...w, leaf: { ...w.leaf, panes: relabelPanes(w.leaf.panes) } }));
|
|
1252
|
+
const autoHide = s.dock.autoHide.map((e) => ({ ...e, leaf: { ...e.leaf, panes: relabelPanes(e.leaf.panes) } }));
|
|
1253
|
+
return { ...s, dock: { ...s.dock, main, floating, autoHide } };
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
/** A freestanding screen: no bound entity, so no blocks/data binding - just a
|
|
1257
|
+
* title and (once actions are added) a toolbar. For a blank page the developer
|
|
1258
|
+
* builds on, or one that's purely a home for custom actions ("Run report"). */
|
|
1259
|
+
export function addFreestandingScreen(project, opts) {
|
|
1260
|
+
const id = 'screen';
|
|
1261
|
+
const route = opts.route ?? id;
|
|
1262
|
+
return appendScreen(project, { id, title: opts.title, route, blocks: [] }, 'screen');
|
|
1263
|
+
}
|
|
1264
|
+
/** Every custom action id already used project-wide (screen toolbars + row
|
|
1265
|
+
* actions) - action ids become `/api/actions/<id>` routes, so they must be
|
|
1266
|
+
* unique across the whole project, not just within one screen. */
|
|
1267
|
+
function takenActionIds(project) {
|
|
1268
|
+
const taken = new Set();
|
|
1269
|
+
for (const s of project.screens) {
|
|
1270
|
+
for (const a of s.actions ?? [])
|
|
1271
|
+
taken.add(a.id);
|
|
1272
|
+
for (const b of flattenBlocks(s.blocks)) {
|
|
1273
|
+
if (b.config.kind !== 'grid')
|
|
1274
|
+
continue;
|
|
1275
|
+
for (const a of b.config.rowActions ?? [])
|
|
1276
|
+
if (a.kind === 'custom' && a.id)
|
|
1277
|
+
taken.add(a.id);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return taken;
|
|
1281
|
+
}
|
|
1282
|
+
/** Add a toolbar-level custom action to a screen (works on a freestanding screen
|
|
1283
|
+
* too). Generates a stable id from `taken` project-wide ids; the generated app
|
|
1284
|
+
* gets a wired-up button + a stub `/api/actions/<id>` route to fill in. */
|
|
1285
|
+
export function addScreenAction(project, screenId, action) {
|
|
1286
|
+
const id = uid('action-', takenActionIds(project));
|
|
1287
|
+
return mapScreen(project, screenId, (s) => ({ ...s, actions: [...(s.actions ?? []), { id, ...action }] }));
|
|
1288
|
+
}
|
|
1289
|
+
export function removeScreenAction(project, screenId, actionId) {
|
|
1290
|
+
return mapScreen(project, screenId, (s) => ({ ...s, actions: (s.actions ?? []).filter((a) => a.id !== actionId) }));
|
|
1291
|
+
}
|
|
1292
|
+
/** Opt a screen into a user-owned `handlers.ts` companion (design + your own code).
|
|
1293
|
+
* The generator scaffolds the stub once and never rewrites it. */
|
|
1294
|
+
export function enableScreenCode(project, screenId) {
|
|
1295
|
+
return mapScreen(project, screenId, (s) => ({ ...s, code: true }));
|
|
1296
|
+
}
|
|
1297
|
+
/** Drop the code companion binding (leaves any file the user already wrote on disk;
|
|
1298
|
+
* the generator simply stops emitting/importing it). */
|
|
1299
|
+
export function disableScreenCode(project, screenId) {
|
|
1300
|
+
return mapScreen(project, screenId, (s) => ({ ...s, code: false, renderGrid: undefined }));
|
|
1301
|
+
}
|
|
1302
|
+
/** Toggle whether the page renders a Grid fed by `ctx.setRows` from `onLoad`.
|
|
1303
|
+
* Implies `code: true` (a Grid needs the handler to fill it). */
|
|
1304
|
+
export function setScreenRenderGrid(project, screenId, on) {
|
|
1305
|
+
return mapScreen(project, screenId, (s) => ({ ...s, code: on ? true : s.code, renderGrid: on || undefined }));
|
|
1306
|
+
}
|
|
1307
|
+
/** Set the body of one event handler (e.g. `load`) - the code inside the generated
|
|
1308
|
+
* function. This is what the designer's Code view edits per slot (the "single
|
|
1309
|
+
* onLoad block"). Empty clears it back to the stub default. Implies `code: true`. */
|
|
1310
|
+
export function setHandlerBody(project, screenId, handler, body) {
|
|
1311
|
+
return mapScreen(project, screenId, (s) => {
|
|
1312
|
+
const bodies = { ...(s.handlerBodies ?? {}) };
|
|
1313
|
+
if (body.trim())
|
|
1314
|
+
bodies[handler] = body;
|
|
1315
|
+
else
|
|
1316
|
+
delete bodies[handler];
|
|
1317
|
+
return { ...s, code: true, handlerBodies: Object.keys(bodies).length ? bodies : undefined };
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
/** Set the full source the designer writes into the screen's `handlers.ts` companion.
|
|
1321
|
+
* An advanced escape hatch that overrides the structured per-event bodies: when
|
|
1322
|
+
* present it is emitted verbatim. Empty string clears it. Implies `code: true`. */
|
|
1323
|
+
export function setScreenHandlersSource(project, screenId, source) {
|
|
1324
|
+
const trimmed = source.trim();
|
|
1325
|
+
return mapScreen(project, screenId, (s) => ({ ...s, code: true, handlersSource: trimmed || undefined }));
|
|
1326
|
+
}
|
|
1327
|
+
// --- visual methods (the Methods panel) ------------------------------------
|
|
1328
|
+
/** Replace the steps of one method slot (`onLoad`, `onDestroy`, or `click:<blockId>`).
|
|
1329
|
+
* An empty list clears the slot. Implies `code: true`. */
|
|
1330
|
+
export function setHandlerSteps(project, screenId, slot, steps) {
|
|
1331
|
+
return mapScreen(project, screenId, (s) => {
|
|
1332
|
+
const all = { ...(s.handlerSteps ?? {}) };
|
|
1333
|
+
if (steps.length)
|
|
1334
|
+
all[slot] = steps;
|
|
1335
|
+
else
|
|
1336
|
+
delete all[slot];
|
|
1337
|
+
return { ...s, code: true, handlerSteps: Object.keys(all).length ? all : undefined };
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
/** Append a step to a method slot. */
|
|
1341
|
+
export function addHandlerStep(project, screenId, slot, step) {
|
|
1342
|
+
const s = project.screens.find((x) => x.id === screenId);
|
|
1343
|
+
return setHandlerSteps(project, screenId, slot, [...(s?.handlerSteps?.[slot] ?? []), step]);
|
|
1344
|
+
}
|
|
1345
|
+
/** Replace / remove a step at `index` (null removes it). */
|
|
1346
|
+
export function updateHandlerStep(project, screenId, slot, index, step) {
|
|
1347
|
+
const s = project.screens.find((x) => x.id === screenId);
|
|
1348
|
+
const steps = [...(s?.handlerSteps?.[slot] ?? [])];
|
|
1349
|
+
if (index < 0 || index >= steps.length)
|
|
1350
|
+
return project;
|
|
1351
|
+
if (step === null)
|
|
1352
|
+
steps.splice(index, 1);
|
|
1353
|
+
else
|
|
1354
|
+
steps[index] = step;
|
|
1355
|
+
return setHandlerSteps(project, screenId, slot, steps);
|
|
1356
|
+
}
|
|
1357
|
+
/** Move a step within its slot (dir -1 up, +1 down). */
|
|
1358
|
+
export function moveHandlerStep(project, screenId, slot, index, dir) {
|
|
1359
|
+
const s = project.screens.find((x) => x.id === screenId);
|
|
1360
|
+
const steps = [...(s?.handlerSteps?.[slot] ?? [])];
|
|
1361
|
+
const j = index + dir;
|
|
1362
|
+
if (index < 0 || index >= steps.length || j < 0 || j >= steps.length)
|
|
1363
|
+
return project;
|
|
1364
|
+
[steps[index], steps[j]] = [steps[j], steps[index]];
|
|
1365
|
+
return setHandlerSteps(project, screenId, slot, steps);
|
|
1366
|
+
}
|
|
1367
|
+
/** Drop the visual steps for a slot into the raw code editor (compile once), so a
|
|
1368
|
+
* user can hand-edit from there. Moves `handlerSteps[slot]` -> `handlerBodies[slot]`. */
|
|
1369
|
+
export function stepsToCode(project, screenId, slot) {
|
|
1370
|
+
const s = project.screens.find((x) => x.id === screenId);
|
|
1371
|
+
const steps = s?.handlerSteps?.[slot];
|
|
1372
|
+
if (!steps?.length)
|
|
1373
|
+
return project;
|
|
1374
|
+
const withBody = setHandlerBody(project, screenId, slot, compileHandlerSteps(steps));
|
|
1375
|
+
return setHandlerSteps(withBody, screenId, slot, []);
|
|
1376
|
+
}
|
|
1377
|
+
/** Deep-clone `block` with fresh ids (recursing into Tabs children). */
|
|
1378
|
+
function freshBlockIds(blocks, taken) {
|
|
1379
|
+
return blocks.map((b) => {
|
|
1380
|
+
const config = JSON.parse(JSON.stringify(b.config));
|
|
1381
|
+
const id = uid(`${config.kind}-`, taken);
|
|
1382
|
+
taken.add(id);
|
|
1383
|
+
if (config.kind === 'tabs')
|
|
1384
|
+
config.tabs = config.tabs.map((t) => ({ ...t, blocks: freshBlockIds(t.blocks, taken) }));
|
|
1385
|
+
if (config.kind === 'accordion')
|
|
1386
|
+
config.sections = config.sections.map((s) => ({ ...s, blocks: freshBlockIds(s.blocks, taken) }));
|
|
1387
|
+
return { ...b, id, config };
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/** Insert a block (deep-cloned, fresh id) into a screen - the paste target. Appends by default. */
|
|
1391
|
+
export function insertBlock(project, screenId, block, index) {
|
|
1392
|
+
return mapScreen(project, screenId, (s) => {
|
|
1393
|
+
const taken = new Set(flattenBlocks(s.blocks).map((b) => b.id));
|
|
1394
|
+
const [clone] = freshBlockIds([block], taken);
|
|
1395
|
+
const blocks = [...s.blocks];
|
|
1396
|
+
blocks.splice(index ?? blocks.length, 0, clone);
|
|
1397
|
+
return { ...s, blocks };
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/** Duplicate a whole screen (fresh id / route / title + fresh block ids), inserted after it. */
|
|
1401
|
+
export function duplicateScreen(project, screenId) {
|
|
1402
|
+
const idx = project.screens.findIndex((s) => s.id === screenId);
|
|
1403
|
+
if (idx < 0)
|
|
1404
|
+
return project;
|
|
1405
|
+
const src = project.screens[idx];
|
|
1406
|
+
const ids = new Set(project.screens.map((s) => s.id));
|
|
1407
|
+
const routes = new Set(project.screens.map((s) => s.route));
|
|
1408
|
+
const id = uid(`${src.entity}-`, ids);
|
|
1409
|
+
const route = uid(`${src.route}-`, routes);
|
|
1410
|
+
// Seed the taken-ids set with the source's block ids so the clone's ids don't collide.
|
|
1411
|
+
const clone = { ...src, id, route, title: `${src.title} copy`, blocks: freshBlockIds(src.blocks, new Set(flattenBlocks(src.blocks).map((b) => b.id))) };
|
|
1412
|
+
const screens = [...project.screens];
|
|
1413
|
+
screens.splice(idx + 1, 0, clone);
|
|
1414
|
+
return { ...project, screens };
|
|
1415
|
+
}
|
|
1416
|
+
/** Move a screen to an explicit index (drag-reorder the tab strip). Clamps to range. */
|
|
1417
|
+
export function reorderScreen(project, screenId, toIndex) {
|
|
1418
|
+
const from = project.screens.findIndex((s) => s.id === screenId);
|
|
1419
|
+
if (from < 0)
|
|
1420
|
+
return project;
|
|
1421
|
+
const screens = [...project.screens];
|
|
1422
|
+
const [moved] = screens.splice(from, 1);
|
|
1423
|
+
screens.splice(Math.max(0, Math.min(toIndex, screens.length)), 0, moved);
|
|
1424
|
+
return { ...project, screens };
|
|
1425
|
+
}
|
|
1426
|
+
export function setDataSource(project, dataSource) {
|
|
1427
|
+
return { ...project, dataSource };
|
|
1428
|
+
}
|
|
1429
|
+
/** Set the deploy target (SvelteKit adapter + provider config the bundle emits). */
|
|
1430
|
+
export function setDeployTarget(project, deploy) {
|
|
1431
|
+
if (deploy === 'auto') {
|
|
1432
|
+
const { deploy: _drop, ...rest } = project;
|
|
1433
|
+
return rest;
|
|
1434
|
+
}
|
|
1435
|
+
return { ...project, deploy };
|
|
1436
|
+
}
|
|
1437
|
+
/** Enable / disable the authentication starter (login + session + hooks). */
|
|
1438
|
+
export function setAuth(project, patch) {
|
|
1439
|
+
if (!patch.enabled) {
|
|
1440
|
+
const { auth: _drop, ...rest } = project;
|
|
1441
|
+
return rest;
|
|
1442
|
+
}
|
|
1443
|
+
const prev = project.auth;
|
|
1444
|
+
const oauth = patch.oauth ?? prev?.oauth;
|
|
1445
|
+
const provider = patch.provider ?? prev?.provider;
|
|
1446
|
+
return {
|
|
1447
|
+
...project,
|
|
1448
|
+
auth: {
|
|
1449
|
+
enabled: true,
|
|
1450
|
+
...(provider && provider !== 'builtin' ? { provider } : {}),
|
|
1451
|
+
protect: patch.protect ?? prev?.protect ?? true,
|
|
1452
|
+
register: patch.register ?? prev?.register ?? false,
|
|
1453
|
+
userAdmin: patch.userAdmin ?? prev?.userAdmin ?? false,
|
|
1454
|
+
...(oauth && oauth.length ? { oauth } : {}),
|
|
1455
|
+
...((patch.twoFactor ?? prev?.twoFactor) ? { twoFactor: true } : {}),
|
|
1456
|
+
...((patch.email ?? prev?.email) ? { email: true } : {}),
|
|
1457
|
+
},
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
/** Enable / disable the typed Drizzle data layer (schema + repos + migrations). */
|
|
1461
|
+
export function setDataLayer(project, enabled) {
|
|
1462
|
+
if (!enabled) {
|
|
1463
|
+
const { dataLayer: _drop, ...rest } = project;
|
|
1464
|
+
return rest;
|
|
1465
|
+
}
|
|
1466
|
+
return { ...project, dataLayer: 'drizzle' };
|
|
1467
|
+
}
|
|
1468
|
+
export function seedUsers(project) {
|
|
1469
|
+
const roles = project.access?.enabled ? project.access.roles.map((r) => r.role) : [];
|
|
1470
|
+
if (!roles.length)
|
|
1471
|
+
return [{ email: 'admin@example.com', name: 'Admin', password: 'admin1234', role: 'admin' }];
|
|
1472
|
+
return roles.map((role) => ({
|
|
1473
|
+
email: `${role.replace(/[^a-z0-9]+/gi, '')}@example.com`.toLowerCase(),
|
|
1474
|
+
name: role.charAt(0).toUpperCase() + role.slice(1),
|
|
1475
|
+
role,
|
|
1476
|
+
password: `${role.replace(/[^a-z0-9]+/gi, '').toLowerCase()}1234`,
|
|
1477
|
+
}));
|
|
1478
|
+
}
|
|
1479
|
+
/** A skeleton binding for a kind, seeded with the entity's name as its table/path. */
|
|
1480
|
+
export function defaultEntitySource(kind, entityName) {
|
|
1481
|
+
switch (kind) {
|
|
1482
|
+
case 'rest': return { kind: 'rest', baseUrl: '', path: entityName, method: 'GET', params: [] };
|
|
1483
|
+
case 'sql': return { kind: 'sql', table: entityName };
|
|
1484
|
+
case 'supabase': return { kind: 'supabase', table: entityName };
|
|
1485
|
+
case 'pglite': return { kind: 'pglite', table: entityName };
|
|
1486
|
+
default: return { kind: 'memory' };
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/** Bind one entity to a data source (REST / SQL / Supabase / in-memory). */
|
|
1490
|
+
export function setEntityDataSource(project, entityName, source) {
|
|
1491
|
+
return { ...project, dataSources: { ...project.dataSources, [entityName]: source } };
|
|
1492
|
+
}
|
|
1493
|
+
/** The resolved source for an entity: its explicit per-entity binding, else a skeleton
|
|
1494
|
+
* for the project's default source kind (so the rail "Default source" actually applies
|
|
1495
|
+
* to unbound entities, and this agrees with `ssrScreenShape`'s fallback). */
|
|
1496
|
+
export function entityDataSource(project, entityName) {
|
|
1497
|
+
return project.dataSources?.[entityName] ?? defaultEntitySource(project.dataSource ?? 'memory', entityName);
|
|
1498
|
+
}
|
|
1499
|
+
export function setTheme(project, theme) {
|
|
1500
|
+
return { ...project, theme: { ...project.theme, ...theme } };
|
|
1501
|
+
}
|
|
1502
|
+
/** Apply a Studio theme preset: sets the preset id and clears any manual accent
|
|
1503
|
+
* override so the preset's own accent takes effect (the user can re-tune it
|
|
1504
|
+
* afterwards with the color picker). Pass `accent` to pin one explicitly. */
|
|
1505
|
+
export function setThemePreset(project, preset, accent) {
|
|
1506
|
+
const theme = { ...project.theme, preset };
|
|
1507
|
+
if (accent)
|
|
1508
|
+
theme.accent = accent;
|
|
1509
|
+
else
|
|
1510
|
+
delete theme.accent;
|
|
1511
|
+
return { ...project, theme };
|
|
1512
|
+
}
|
|
1513
|
+
/** Configure the generated app shell (layout style, brand, footer, nav position). */
|
|
1514
|
+
export function setShell(project, shell) {
|
|
1515
|
+
return { ...project, theme: { ...project.theme, shell: { ...project.theme?.shell, ...shell } } };
|
|
1516
|
+
}
|
|
1517
|
+
/** Build a screen for an entity from a template (a preset arrangement of blocks). */
|
|
1518
|
+
export function screenFromTemplate(entity, template, opts = {}) {
|
|
1519
|
+
const taken = new Set();
|
|
1520
|
+
const make = (kind, span, config) => {
|
|
1521
|
+
const b = makeBlock(kind, entity, taken);
|
|
1522
|
+
taken.add(b.id);
|
|
1523
|
+
if (span)
|
|
1524
|
+
b.span = span;
|
|
1525
|
+
if (config)
|
|
1526
|
+
b.config = config;
|
|
1527
|
+
return b;
|
|
1528
|
+
};
|
|
1529
|
+
const measure = pickMeasure(entity);
|
|
1530
|
+
const label = entity.label ?? entity.name;
|
|
1531
|
+
let blocks;
|
|
1532
|
+
if (template === 'empty') {
|
|
1533
|
+
blocks = [];
|
|
1534
|
+
}
|
|
1535
|
+
else if (template === 'dashboard') {
|
|
1536
|
+
const kpis = [make('kpi', 1, { kind: 'kpi', label: `Total ${label}`, reduce: 'count' })];
|
|
1537
|
+
if (measure)
|
|
1538
|
+
kpis.push(make('kpi', 1, { kind: 'kpi', label: `Total ${measure}`, measure, reduce: 'sum' }));
|
|
1539
|
+
blocks = [...kpis, make('chart', 2), make('grid', 3)];
|
|
1540
|
+
}
|
|
1541
|
+
else if (template === 'master-detail') {
|
|
1542
|
+
const md = make('master-detail', 3, {
|
|
1543
|
+
kind: 'master-detail',
|
|
1544
|
+
childEntity: opts.child?.name ?? '',
|
|
1545
|
+
foreignKey: opts.foreignKey ?? opts.child?.fields.find((f) => f.type === 'relation' && f.relation?.entity === entity.name)?.field ?? '',
|
|
1546
|
+
});
|
|
1547
|
+
blocks = [make('grid', 3), md];
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
blocks = [make('grid', 3)]; // crud (the grid edits via a popup form)
|
|
1551
|
+
}
|
|
1552
|
+
return { id: entity.name, entity: entity.name, title: label, route: entity.name, blocks };
|
|
1553
|
+
}
|
|
1554
|
+
/** Add a screen built from a template. `child` (for master-detail) is an entity name. */
|
|
1555
|
+
export function addScreenFromTemplate(project, entityName, template, opts = {}) {
|
|
1556
|
+
const entity = entityOf(project, entityName);
|
|
1557
|
+
if (!entity)
|
|
1558
|
+
return project;
|
|
1559
|
+
const base = screenFromTemplate(entity, template, {
|
|
1560
|
+
child: opts.child ? entityOf(project, opts.child) : undefined,
|
|
1561
|
+
foreignKey: opts.foreignKey,
|
|
1562
|
+
});
|
|
1563
|
+
return appendScreen(project, base, entityName);
|
|
1564
|
+
}
|
|
1565
|
+
// --- persistence: round-trippable studio.config -----------------------------
|
|
1566
|
+
/** `name`, or the first free `${name}_${n}` if taken. Records the result. */
|
|
1567
|
+
function makeUnique(name, taken) {
|
|
1568
|
+
if (!taken.has(name)) {
|
|
1569
|
+
taken.add(name);
|
|
1570
|
+
return name;
|
|
1571
|
+
}
|
|
1572
|
+
let i = 2;
|
|
1573
|
+
while (taken.has(`${name}_${i}`))
|
|
1574
|
+
i++;
|
|
1575
|
+
const u = `${name}_${i}`;
|
|
1576
|
+
taken.add(u);
|
|
1577
|
+
return u;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Enforce the structural invariants the designer + codegen rely on: unique
|
|
1581
|
+
* entity names, unique field names within an entity, unique screen ids + routes,
|
|
1582
|
+
* and unique block ids within a screen. In-app ops already keep these, but a
|
|
1583
|
+
* hand-edited / externally-produced `studio.config.json` may not - and a
|
|
1584
|
+
* duplicate key would crash a keyed `{#each}` list at render time. Idempotent:
|
|
1585
|
+
* a project that already satisfies the invariants is returned value-equal.
|
|
1586
|
+
*/
|
|
1587
|
+
export function sanitizeProject(project) {
|
|
1588
|
+
const entityNames = new Set();
|
|
1589
|
+
const entities = [];
|
|
1590
|
+
for (const e of project.entities) {
|
|
1591
|
+
if (entityNames.has(e.name))
|
|
1592
|
+
continue; // drop a duplicate-named entity; screens still resolve to the survivor
|
|
1593
|
+
entityNames.add(e.name);
|
|
1594
|
+
const fieldNames = new Set();
|
|
1595
|
+
entities.push({ ...e, fields: e.fields.map((f) => ({ ...f, field: makeUnique(f.field, fieldNames) })) });
|
|
1596
|
+
}
|
|
1597
|
+
const ids = new Set();
|
|
1598
|
+
const routes = new Set();
|
|
1599
|
+
const screens = project.screens.map((s) => {
|
|
1600
|
+
const blockIds = new Set();
|
|
1601
|
+
return {
|
|
1602
|
+
...s,
|
|
1603
|
+
id: makeUnique(s.id, ids),
|
|
1604
|
+
route: makeUnique(s.route, routes),
|
|
1605
|
+
blocks: s.blocks.map((b) => ({ ...b, id: makeUnique(b.id, blockIds) })),
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
const result = { ...project, entities, screens };
|
|
1609
|
+
// Drop per-entity source bindings for entities that no longer exist.
|
|
1610
|
+
if (project.dataSources) {
|
|
1611
|
+
const pruned = Object.fromEntries(Object.entries(project.dataSources).filter(([n]) => entityNames.has(n)));
|
|
1612
|
+
if (Object.keys(pruned).length)
|
|
1613
|
+
result.dataSources = pruned;
|
|
1614
|
+
else
|
|
1615
|
+
delete result.dataSources;
|
|
1616
|
+
}
|
|
1617
|
+
return result;
|
|
1618
|
+
}
|
|
1619
|
+
/** Serialize a project to a `studio.config.json` string (the persisted design). */
|
|
1620
|
+
export function serializeProject(project) {
|
|
1621
|
+
return JSON.stringify(project, null, 2);
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Parse a `studio.config.json` string back into a `StudioProject`, validating
|
|
1625
|
+
* the shape. Note: only the data model round-trips - any runtime functions on an
|
|
1626
|
+
* entity (`computed` / `validate` / `hooks`) are not part of the config.
|
|
1627
|
+
*/
|
|
1628
|
+
export function parseProject(json) {
|
|
1629
|
+
let raw;
|
|
1630
|
+
try {
|
|
1631
|
+
raw = JSON.parse(json);
|
|
1632
|
+
}
|
|
1633
|
+
catch {
|
|
1634
|
+
throw new Error('parseProject: not valid JSON');
|
|
1635
|
+
}
|
|
1636
|
+
if (!raw || typeof raw !== 'object')
|
|
1637
|
+
throw new Error('parseProject: expected a project object');
|
|
1638
|
+
const p = raw;
|
|
1639
|
+
// An empty `entities` array is a valid work-in-progress (a "start from zero"
|
|
1640
|
+
// project). Codegen is gated separately by validateProject / isProjectValid.
|
|
1641
|
+
if (!Array.isArray(p.entities))
|
|
1642
|
+
throw new Error('parseProject: missing "entities"');
|
|
1643
|
+
if (!Array.isArray(p.screens))
|
|
1644
|
+
throw new Error('parseProject: missing "screens"');
|
|
1645
|
+
return sanitizeProject({
|
|
1646
|
+
title: typeof p.title === 'string' ? p.title : 'My Studio App',
|
|
1647
|
+
entities: p.entities,
|
|
1648
|
+
screens: p.screens,
|
|
1649
|
+
dataSource: (p.dataSource ?? 'memory'),
|
|
1650
|
+
...(p.dataSources && typeof p.dataSources === 'object' ? { dataSources: p.dataSources } : {}),
|
|
1651
|
+
...(p.supabase && typeof p.supabase === 'object' ? { supabase: p.supabase } : {}),
|
|
1652
|
+
...(p.theme && typeof p.theme === 'object' ? { theme: p.theme } : {}),
|
|
1653
|
+
...(p.access && typeof p.access === 'object' ? { access: p.access } : {}),
|
|
1654
|
+
...(p.auth && typeof p.auth === 'object' && p.auth.enabled ? { auth: p.auth } : {}),
|
|
1655
|
+
...(p.dataLayer === 'drizzle' ? { dataLayer: 'drizzle' } : {}),
|
|
1656
|
+
...(typeof p.audit === 'boolean' ? { audit: p.audit } : {}),
|
|
1657
|
+
...(p.i18n && typeof p.i18n === 'object' ? { i18n: p.i18n } : {}),
|
|
1658
|
+
...(typeof p.deploy === 'string' && p.deploy !== 'auto' ? { deploy: p.deploy } : {}),
|
|
1659
|
+
...(p.triggers && typeof p.triggers === 'object' ? { triggers: p.triggers } : {}),
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
/** Validate a project. Errors block codegen; warnings are advisory. */
|
|
1663
|
+
export function validateProject(project) {
|
|
1664
|
+
const issues = [];
|
|
1665
|
+
if (project.entities.length === 0)
|
|
1666
|
+
issues.push({ level: 'error', message: 'Add at least one entity.' });
|
|
1667
|
+
if (project.screens.length === 0)
|
|
1668
|
+
issues.push({ level: 'warning', message: 'No screens yet.' });
|
|
1669
|
+
// Form conditions read other fields by name. A name that resolves to nothing
|
|
1670
|
+
// compares against undefined, so the field it guards would sit hidden or
|
|
1671
|
+
// locked forever with no clue why - worth catching before it ships.
|
|
1672
|
+
for (const entity of project.entities) {
|
|
1673
|
+
const names = new Set(entity.fields.map((f) => f.field));
|
|
1674
|
+
for (const field of entity.fields) {
|
|
1675
|
+
if (!field.when)
|
|
1676
|
+
continue;
|
|
1677
|
+
for (const [flag, expr] of Object.entries(field.when)) {
|
|
1678
|
+
if (!expr)
|
|
1679
|
+
continue;
|
|
1680
|
+
for (const ref of collectColumnRefs(expr)) {
|
|
1681
|
+
if (!names.has(ref)) {
|
|
1682
|
+
issues.push({
|
|
1683
|
+
level: 'error',
|
|
1684
|
+
message: `"${entity.name}.${field.field}" has a ${flag} condition on "${ref}", which is not a field on ${entity.name}.`,
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
const routes = new Set();
|
|
1692
|
+
for (const s of project.screens) {
|
|
1693
|
+
// `entity` is optional (a freestanding screen has none, by design) - only a
|
|
1694
|
+
// *dangling* reference (set but unresolvable) is an error.
|
|
1695
|
+
if (s.entity !== undefined && !entityOf(project, s.entity)) {
|
|
1696
|
+
issues.push({ level: 'error', message: `Screen "${s.title}" points at a missing entity "${s.entity}".`, screen: s.id });
|
|
1697
|
+
}
|
|
1698
|
+
if (routes.has(s.route))
|
|
1699
|
+
issues.push({ level: 'error', message: `Duplicate route "/${s.route}".`, screen: s.id });
|
|
1700
|
+
routes.add(s.route);
|
|
1701
|
+
if (s.entity !== undefined && s.blocks.length === 0) {
|
|
1702
|
+
issues.push({ level: 'warning', message: `Screen "${s.title}" has no blocks.`, screen: s.id });
|
|
1703
|
+
}
|
|
1704
|
+
else if (s.entity === undefined && !s.actions?.length && s.blocks.length === 0) {
|
|
1705
|
+
issues.push({ level: 'warning', message: `Screen "${s.title}" is empty - add an action or some content.`, screen: s.id });
|
|
1706
|
+
}
|
|
1707
|
+
for (const b of flattenBlocks(s.blocks)) {
|
|
1708
|
+
const at = (message, level = 'warning') => ({ level, message, screen: s.id, block: b.id });
|
|
1709
|
+
const c = b.config;
|
|
1710
|
+
if (c.kind === 'master-detail') {
|
|
1711
|
+
if (!c.childEntity || !c.foreignKey)
|
|
1712
|
+
issues.push(at('Master/detail needs a child entity + foreign key.'));
|
|
1713
|
+
else if (!entityOf(project, c.childEntity))
|
|
1714
|
+
issues.push(at(`Master/detail points at a missing child entity "${c.childEntity}".`));
|
|
1715
|
+
}
|
|
1716
|
+
else if (c.kind === 'tree') {
|
|
1717
|
+
if (!c.labelField || !c.parentField)
|
|
1718
|
+
issues.push(at('Tree needs a label field and a self-referential parent field.'));
|
|
1719
|
+
}
|
|
1720
|
+
else if (c.kind === 'lookup') {
|
|
1721
|
+
if (!c.field)
|
|
1722
|
+
issues.push(at('Lookup needs a relation field.'));
|
|
1723
|
+
}
|
|
1724
|
+
else if (c.kind === 'filter') {
|
|
1725
|
+
if (!c.fields.length)
|
|
1726
|
+
issues.push(at('Filter panel has no facets - pick fields to filter on.'));
|
|
1727
|
+
}
|
|
1728
|
+
else if (c.kind === 'chart') {
|
|
1729
|
+
if (!c.dimension)
|
|
1730
|
+
issues.push(at('Chart has no group-by dimension.'));
|
|
1731
|
+
}
|
|
1732
|
+
else if (c.kind === 'pivot') {
|
|
1733
|
+
if (!c.rows.length && !c.cols.length)
|
|
1734
|
+
issues.push(at('Pivot has no row or column dimensions.'));
|
|
1735
|
+
}
|
|
1736
|
+
else if (c.kind === 'tabs') {
|
|
1737
|
+
if (c.tabs.every((t) => t.blocks.length === 0))
|
|
1738
|
+
issues.push(at('Tabs container has no blocks in any tab.'));
|
|
1739
|
+
}
|
|
1740
|
+
else if (c.kind === 'accordion') {
|
|
1741
|
+
if (c.sections.every((s) => s.blocks.length === 0))
|
|
1742
|
+
issues.push(at('Accordion container has no blocks in any section.'));
|
|
1743
|
+
}
|
|
1744
|
+
else if (c.kind === 'component') {
|
|
1745
|
+
if (!c.component)
|
|
1746
|
+
issues.push(at('Component block has no component selected.'));
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
// Project-level deployment footguns (all advisory - the app still generates).
|
|
1751
|
+
const sources = Object.values(project.dataSources ?? {});
|
|
1752
|
+
const sqlSources = sources.filter((s) => s.kind === 'sql');
|
|
1753
|
+
if (sqlSources.length > 0 && project.dataLayer !== 'drizzle') {
|
|
1754
|
+
issues.push({
|
|
1755
|
+
level: 'warning',
|
|
1756
|
+
message: 'SQL entities have no migrations - their tables must already exist in the database. ' +
|
|
1757
|
+
'Enable the Drizzle data layer for typed migrations, or run the generated db/schema.sql against your database first.',
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
if (project.deploy === 'cloudflare' && sqlSources.some((s) => s.dialect === 'postgres' || s.dialect === 'supabase')) {
|
|
1761
|
+
issues.push({
|
|
1762
|
+
level: 'warning',
|
|
1763
|
+
message: 'Cloudflare Workers cannot run the socket "pg" Postgres driver. Swap the generated route to an HTTP driver ' +
|
|
1764
|
+
'(e.g. Neon serverless) or choose another deploy target.',
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
for (const [name, src] of Object.entries(project.dataSources ?? {})) {
|
|
1768
|
+
if (src.kind === 'supabase' && !(src.url ?? project.supabase?.url) && !(src.key ?? project.supabase?.key)) {
|
|
1769
|
+
issues.push({
|
|
1770
|
+
level: 'warning',
|
|
1771
|
+
message: `Supabase entity "${name}" has no URL/key - set the shared project connection (or this entity's own), or the generated app just reads PUBLIC_SUPABASE_URL / PUBLIC_SUPABASE_ANON_KEY from .env.`,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
// Supabase Auth needs a Supabase client to authenticate against: either the shared
|
|
1776
|
+
// project connection or at least one Supabase-bound entity (both emit connections.ts).
|
|
1777
|
+
if (project.auth?.enabled && project.auth.provider === 'supabase') {
|
|
1778
|
+
const hasSupabase = !!project.supabase?.url || sources.some((s) => s.kind === 'supabase');
|
|
1779
|
+
if (!hasSupabase) {
|
|
1780
|
+
issues.push({
|
|
1781
|
+
level: 'warning',
|
|
1782
|
+
message: 'Supabase Auth needs a Supabase connection. Set the shared project URL / anon key (in a data-source builder), or bind an entity to Supabase.',
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
// Supabase Auth is a client-side gate; it does not populate the server-side role
|
|
1786
|
+
// the RBAC route guard reads. Server enforcement must come from RLS instead.
|
|
1787
|
+
if (project.access?.enabled) {
|
|
1788
|
+
issues.push({
|
|
1789
|
+
level: 'warning',
|
|
1790
|
+
message: 'Supabase Auth signs in on the client, so it does not populate the server-side role that RBAC route guards check. Enforce per-user access with Row Level Security policies on your Supabase tables.',
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
return issues;
|
|
1795
|
+
}
|
|
1796
|
+
export function isProjectValid(project) {
|
|
1797
|
+
return !validateProject(project).some((i) => i.level === 'error');
|
|
1798
|
+
}
|