@svgrid/enterprise 2.4.0 → 2.5.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/cdn/svgrid-enterprise.svelte-external.js +5917 -5545
- package/dist/designer/assets/{index-okIcRP9P.js → index-qL7ir9C7.js} +3 -3
- package/dist/designer/assets/{src-CIla2aVG.js → src-96d31O63.js} +259 -228
- package/dist/designer/assets/{src-BvsJ00AO.css → src-D-O2F805.css} +1 -1
- package/dist/designer/index.html +3 -3
- package/dist/node/studio.js +681 -84
- package/package.json +3 -3
- package/src/SvGridEditPanel.dom.test.ts +174 -0
- package/src/SvGridEditPanel.svelte +209 -46
- package/src/edit-panel.test.ts +145 -36
- package/src/edit-panel.ts +167 -16
- package/src/expressions/evaluate.ts +1 -1
- package/src/expressions/parse.ts +3 -3
- package/src/index.ts +8 -0
- package/src/license-core.ts +77 -5
- package/src/license-expiry.test.ts +91 -0
- package/src/license.ts +31 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js +3875 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js +21 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js +1938 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js +205 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js +18 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js +264 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js +697 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js +4938 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js +495 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js +190 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js +8 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js.map +1 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js +138 -0
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js.map +1 -0
- package/src/schema.ts +63 -0
- package/src/studio/emit-project.test.ts +191 -0
- package/src/studio/emit-project.ts +237 -27
- package/src/studio/emit-schema.ts +4 -1
- package/src/studio/index.ts +2 -0
- package/src/studio/init-flow.test.ts +150 -0
- package/src/studio/init-flow.ts +169 -22
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +178 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +3875 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +21 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +1938 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +205 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +18 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +264 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +697 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +3 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +134 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +55 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +2 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +46843 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +237 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +2 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +4 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +7 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +4 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +5 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +5 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +5 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +6 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +83 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +559 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +24 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +122 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +2 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +89 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +241 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +4938 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +495 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +190 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +8 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +1 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +138 -0
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +1 -0
- package/src/studio/project.test.ts +30 -0
- package/src/studio/project.ts +72 -8
- package/src/studio/samples/crm.ts +11 -2
- package/src/studio/samples/insurance.ts +10 -4
- package/src/studio/samples/samples.test.ts +36 -0
- package/src/studio/samples/shared.ts +14 -0
- package/src/studio/samples/support.ts +12 -5
- package/src/studio/screen-suites.test.ts +43 -0
- package/src/studio/screen-suites.ts +5 -2
- package/src/studio/user-error.ts +31 -0
- package/src/upgrade-prompt.ts +23 -10
- package/src/version.test.ts +17 -0
- package/src/version.ts +13 -0
|
@@ -17,6 +17,8 @@ import { blockColumns, blockStyleCss, blockClassName, sanitizeClassName, compone
|
|
|
17
17
|
import { uiComponentSpec, gridApiSettableProps, STANDARD_UI_EVENTS } from './ui-components.js'
|
|
18
18
|
import { resolveThemeTokens, resolveThemeTokensFor, isDarkTheme } from './themes.js'
|
|
19
19
|
import type { EntityField, EntitySchema } from '../schema.js'
|
|
20
|
+
import { hasFieldConditions } from '../edit-panel.js'
|
|
21
|
+
import { SVGRID_VERSION } from '../version.js'
|
|
20
22
|
import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles, type NavItem } from './emit-schema.js'
|
|
21
23
|
|
|
22
24
|
const has = (blocks: Block[], kind: Block['config']['kind']) => blocks.some((b) => b.config.kind === kind)
|
|
@@ -2249,7 +2251,10 @@ function screenChildEntities(blocks: Block[], resolve: (name: string) => EntityS
|
|
|
2249
2251
|
/** `+page.server.ts` for a read-only SSR screen (data-viz / detail / master-
|
|
2250
2252
|
* detail): a `load` that returns the full dataset (+ each child collection) -
|
|
2251
2253
|
* the page renders real SSR HTML from `data.*`. No actions (read-only). */
|
|
2252
|
-
|
|
2254
|
+
/** Sources a screen can render on the server. See `ssrScreenShape`. */
|
|
2255
|
+
type SsrSourceKind = 'memory' | 'sql' | 'rest'
|
|
2256
|
+
|
|
2257
|
+
function ssrReadServerFile(schema: EntitySchema, screen: Screen, sourceKind: SsrSourceKind, accessEnabled: boolean, screenIds: string[], resolve: (name: string) => EntitySchema | undefined): GeneratedFile {
|
|
2253
2258
|
const n = namesFor(schema)
|
|
2254
2259
|
const isSql = sourceKind === 'sql'
|
|
2255
2260
|
const children = screenChildEntities(screen.blocks, resolve)
|
|
@@ -2264,8 +2269,8 @@ function ssrReadServerFile(schema: EntitySchema, screen: Screen, sourceKind: 'me
|
|
|
2264
2269
|
contents: `import type { PageServerLoad } from './$types'
|
|
2265
2270
|
import type { ServerRequest } from '@svgrid/grid'
|
|
2266
2271
|
${isSql ? "import { createKitDataSource } from '@svgrid/enterprise'\n" : ''}${rbac ? "import { error } from '@sveltejs/kit'\nimport { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${memImports.length ? `import { ${memImports.join(', ')} } from '$lib/data'\n` : ''}
|
|
2267
|
-
//
|
|
2268
|
-
//
|
|
2272
|
+
// This screen renders on the server: real SSR HTML, not just a server-side
|
|
2273
|
+
// load. Stated explicitly so it holds whichever way the root layout is set.
|
|
2269
2274
|
export const ssr = true
|
|
2270
2275
|
${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}\n` : ''}
|
|
2271
2276
|
const PAGE: ServerRequest = { startRow: 0, endRow: 1000, pageIndex: 0, pageSize: 1000, sortModel: [], filterModel: {} }
|
|
@@ -2325,26 +2330,61 @@ function ssrQueryHelperFile(): GeneratedFile {
|
|
|
2325
2330
|
}
|
|
2326
2331
|
|
|
2327
2332
|
const htmlEsc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
2333
|
+
/** Escape a value going inside a double-quoted HTML attribute. */
|
|
2334
|
+
const attrEsc = (s: string) => htmlEsc(s).replace(/"/g, '"')
|
|
2335
|
+
|
|
2336
|
+
/**
|
|
2337
|
+
* The field's own constraints as native HTML attributes, so a server-rendered
|
|
2338
|
+
* form validates in the browser too. The server still re-checks every one of
|
|
2339
|
+
* them: these only save a round-trip, they never decide anything.
|
|
2340
|
+
*/
|
|
2341
|
+
function attrs(f: EntityField): string {
|
|
2342
|
+
const out: string[] = []
|
|
2343
|
+
if (f.input?.placeholder) out.push(`placeholder="${attrEsc(f.input.placeholder)}"`)
|
|
2344
|
+
if (f.type === 'number') {
|
|
2345
|
+
if (f.min != null) out.push(`min="${f.min}"`)
|
|
2346
|
+
if (f.max != null) out.push(`max="${f.max}"`)
|
|
2347
|
+
if (f.input?.step != null) out.push(`step="${f.input.step}"`)
|
|
2348
|
+
} else {
|
|
2349
|
+
if (f.minLength != null) out.push(`minlength="${f.minLength}"`)
|
|
2350
|
+
if (f.maxLength != null) out.push(`maxlength="${f.maxLength}"`)
|
|
2351
|
+
// `format` already picks the input type, which brings its own checking.
|
|
2352
|
+
if (f.pattern && !f.format) out.push(`pattern="${attrEsc(f.pattern)}"`)
|
|
2353
|
+
}
|
|
2354
|
+
return out.length ? ' ' + out.join(' ') : ''
|
|
2355
|
+
}
|
|
2328
2356
|
|
|
2329
2357
|
/** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
|
|
2330
|
-
function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind:
|
|
2358
|
+
function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind: SsrSourceKind, accessEnabled: boolean, screenIds: string[], byName: Map<string, EntitySchema>): GeneratedFile[] {
|
|
2331
2359
|
const n = namesFor(schema)
|
|
2332
2360
|
const isSql = sourceKind === 'sql'
|
|
2361
|
+
// REST reads like the memory path - the source is imported from $lib/data and
|
|
2362
|
+
// called directly - but the remote API owns the ids, so a create posts the
|
|
2363
|
+
// values alone, as sql does.
|
|
2364
|
+
const isRest = sourceKind === 'rest'
|
|
2333
2365
|
const relSchemaOf = (f: EntityField) => byName.get(f.relation!.entity)!
|
|
2334
2366
|
const relIdOf = (rs: EntitySchema) => rs.idField ?? rs.fields.find((x) => x.primaryKey)?.field ?? 'id'
|
|
2335
|
-
const grid = screen.blocks
|
|
2367
|
+
const grid = screen.blocks.find((b) => b.config.kind === 'grid')!.config as GridConfig
|
|
2368
|
+
const facetPanel = screen.blocks.find((b) => b.config.kind === 'filter')?.config as FilterPanelConfig | undefined
|
|
2336
2369
|
const pageSize = grid.pageSize && grid.pageSize > 0 ? grid.pageSize : 25
|
|
2337
2370
|
const wantsFilter = grid.filterable !== false
|
|
2338
2371
|
const idField = schema.idField ?? schema.fields.find((f) => f.primaryKey)?.field ?? 'id'
|
|
2339
2372
|
const isFormHidden = (f: EntityField) => f.hidden === true || (typeof f.hidden === 'object' && !!f.hidden.form)
|
|
2340
|
-
const
|
|
2373
|
+
const editable = schema.fields.filter(
|
|
2341
2374
|
(f) => f.field !== idField && !f.primaryKey && !f.readonly && !f.computed && !f.formula && !isFormHidden(f),
|
|
2342
2375
|
)
|
|
2376
|
+
// The grid block's form settings win over the schema's, matching how the edit
|
|
2377
|
+
// panel resolves them - so a layout arranged in the designer renders the same
|
|
2378
|
+
// here instead of the server-rendered form quietly ignoring it.
|
|
2379
|
+
const formFields = grid.formFields?.length
|
|
2380
|
+
? (grid.formFields.map((name) => editable.find((f) => f.field === name)).filter(Boolean) as EntityField[])
|
|
2381
|
+
: editable
|
|
2343
2382
|
const normType = (t: string): 'text' | 'number' | 'boolean' => (t === 'number' ? 'number' : t === 'boolean' ? 'boolean' : 'text')
|
|
2344
2383
|
const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(', ')} }`
|
|
2345
2384
|
|
|
2346
|
-
// Relation fields render as a native <select> whose options are prefetched in
|
|
2347
|
-
//
|
|
2385
|
+
// Relation fields render as a native <select> whose options are prefetched in
|
|
2386
|
+
// load - from the in-process source on memory, or the related entity's /api
|
|
2387
|
+
// route on sql (see relSource below).
|
|
2348
2388
|
const isRel = (f: EntityField) => f.type === 'relation' && !!f.relation && byName.has(f.relation!.entity)
|
|
2349
2389
|
const relFields = formFields.filter(isRel)
|
|
2350
2390
|
// The $lib/data import only needs the related sources on the memory path; on sql
|
|
@@ -2371,14 +2411,25 @@ function emitSsrGridScreen(schema: EntitySchema, screen: Screen, sourceKind: 'me
|
|
|
2371
2411
|
// once, in that route's createKitHandlers - not duplicated here.
|
|
2372
2412
|
const src = isSql ? 'source(fetch)' : n.sourceVar
|
|
2373
2413
|
const fetchArg = isSql ? ', fetch' : ''
|
|
2374
|
-
|
|
2375
|
-
|
|
2414
|
+
// A schema with value-driven fields posts through `stripHiddenValues`, so a
|
|
2415
|
+
// field the form was hiding is never written from a submitted FormData - the
|
|
2416
|
+
// same fields the client panel would have sent.
|
|
2417
|
+
const conditional = hasFieldConditions(schema)
|
|
2418
|
+
const readValues = (fd: string) => (conditional ? `stripHiddenValues(${n.schemaVar}, formToValues(${fd}))` : `formToValues(${fd})`)
|
|
2419
|
+
const enterpriseImports = [
|
|
2420
|
+
'validateAll',
|
|
2421
|
+
...(conditional ? ['stripHiddenValues'] : []),
|
|
2422
|
+
...(isSql ? ['createKitDataSource'] : []),
|
|
2423
|
+
].join(', ')
|
|
2424
|
+
const sourceImport = isSql
|
|
2425
|
+
? ''
|
|
2426
|
+
: `import { ${[...new Set([n.sourceVar, ...relSourceVars]), ...(isRest ? [] : ['nextId'])].join(', ')} } from '$lib/data'\n`
|
|
2376
2427
|
const schemaImport = `import { ${n.schemaVar}${isSql ? `, type ${n.type}` : ''} } from '$lib/schemas'`
|
|
2377
|
-
const idConst = isSql ? '' : `\nconst ID_FIELD = ${jsStr(idField)}`
|
|
2428
|
+
const idConst = isSql || isRest ? '' : `\nconst ID_FIELD = ${jsStr(idField)}`
|
|
2378
2429
|
const srcHelper = isSql
|
|
2379
2430
|
? `\n// Same-origin client over the connected /api/${n.route} route (that route runs\n// validation, RBAC, triggers + audit via createKitHandlers).\nconst source = (fetch: typeof globalThis.fetch) => createKitDataSource<${n.type}>({ endpoint: ${jsStr('/api/' + n.route)}, fetch })\n`
|
|
2380
2431
|
: ''
|
|
2381
|
-
const createCall = isSql
|
|
2432
|
+
const createCall = isSql || isRest
|
|
2382
2433
|
? `await ${src}.createRow(values)`
|
|
2383
2434
|
: `await ${n.sourceVar}.createRow({ [ID_FIELD]: nextId(${jsStr(n.idPrefix)}), ...values })`
|
|
2384
2435
|
|
|
@@ -2395,8 +2446,8 @@ import { ${enterpriseImports} } from '@svgrid/enterprise'
|
|
|
2395
2446
|
${rbac ? "import { authorizeAction, getServerRole } from '$lib/access'\n" : ''}${sourceImport}${schemaImport}
|
|
2396
2447
|
import { planFromSearchParams } from '$lib/server/query'
|
|
2397
2448
|
|
|
2398
|
-
//
|
|
2399
|
-
//
|
|
2449
|
+
// This screen renders on the server: real SSR HTML, not just a server-side
|
|
2450
|
+
// load. Stated explicitly so it holds whichever way the root layout is set.
|
|
2400
2451
|
export const ssr = true
|
|
2401
2452
|
${idConst}${rbac ? `\nconst SCREEN_IDS = ${JSON.stringify(screenIds)}` : ''}
|
|
2402
2453
|
const FIELD_TYPES: Record<string, 'text' | 'number' | 'boolean'> = ${fieldTypesLit}
|
|
@@ -2422,7 +2473,7 @@ ${relPrefetch ? relPrefetch + '\n' : ''} return { rows, total: rowCount, page:
|
|
|
2422
2473
|
|
|
2423
2474
|
export const actions: Actions = {
|
|
2424
2475
|
create: async ({ request${fetchArg}${localsArg} }) => {
|
|
2425
|
-
${writeGuard('create')} const values =
|
|
2476
|
+
${writeGuard('create')} const values = ${readValues('await request.formData()')}
|
|
2426
2477
|
const errors = await validateAll(${n.schemaVar}, values)
|
|
2427
2478
|
if (Object.keys(errors).length) return fail(422, { errors, values })
|
|
2428
2479
|
${createCall}
|
|
@@ -2431,7 +2482,7 @@ ${writeGuard('create')} const values = formToValues(await request.formData())
|
|
|
2431
2482
|
update: async ({ request${fetchArg}${localsArg} }) => {
|
|
2432
2483
|
const fd = await request.formData()
|
|
2433
2484
|
${writeGuard('update')} const id = String(fd.get('__id') ?? '')
|
|
2434
|
-
const values =
|
|
2485
|
+
const values = ${readValues('fd')}
|
|
2435
2486
|
const errors = await validateAll(${n.schemaVar}, values)
|
|
2436
2487
|
if (Object.keys(errors).length) return fail(422, { errors, values })
|
|
2437
2488
|
await ${src}.updateRow(id, values)
|
|
@@ -2445,6 +2496,78 @@ ${writeGuard('delete')} const fd = await request.formData()
|
|
|
2445
2496
|
}
|
|
2446
2497
|
`
|
|
2447
2498
|
|
|
2499
|
+
// The facet panel becomes a plain GET form: each control is named for the
|
|
2500
|
+
// param `planFromSearchParams` reads, so submitting navigates to a filtered
|
|
2501
|
+
// URL and the server load does the rest. No JavaScript required, and the
|
|
2502
|
+
// resulting URL is shareable. A GET submit drops `page`, which is what we want
|
|
2503
|
+
// (filtering returns you to the first page); `sort` is carried in a hidden
|
|
2504
|
+
// input so it survives.
|
|
2505
|
+
// Which control types the panel actually renders, so the stylesheet below can
|
|
2506
|
+
// skip rules for controls this screen has none of (svelte-check flags an unused
|
|
2507
|
+
// selector, and a generated app should not ship warnings).
|
|
2508
|
+
const facetControls = { select: false, input: false }
|
|
2509
|
+
const facetForm = (() => {
|
|
2510
|
+
if (!facetPanel) return ''
|
|
2511
|
+
const fields = filterFieldsOf(schema, facetPanel)
|
|
2512
|
+
if (!fields.length) return ''
|
|
2513
|
+
// Parenthesised: `===` binds tighter than `??`, so an unwrapped
|
|
2514
|
+
// `get(x) ?? '' === ''` would compare the empty strings and hand `selected`
|
|
2515
|
+
// the raw param value - marking every option selected at once.
|
|
2516
|
+
const cur = (f: EntityField) => `(page.url.searchParams.get(${jsStr('f_' + f.field)}) ?? '')`
|
|
2517
|
+
// `selected` per option rather than `value` on the <select>: the value form
|
|
2518
|
+
// is applied client-side, so a server-rendered page would show "Any" while
|
|
2519
|
+
// the URL said otherwise.
|
|
2520
|
+
const optionList = (f: EntityField, opts: { value: string; label: string }[]) =>
|
|
2521
|
+
[`<option value="" selected={${cur(f)} === ''}>Any</option>`]
|
|
2522
|
+
.concat(opts.map((o) => `<option value=${jsStr(o.value)} selected={${cur(f)} === ${jsStr(o.value)}}>${htmlEsc(o.label)}</option>`))
|
|
2523
|
+
.join('\n ')
|
|
2524
|
+
const controls = fields
|
|
2525
|
+
.map((f) => {
|
|
2526
|
+
const key = jsStr('f_' + f.field)
|
|
2527
|
+
const opts = enumOpts(f)
|
|
2528
|
+
let control: string
|
|
2529
|
+
if (opts.length) {
|
|
2530
|
+
facetControls.select = true
|
|
2531
|
+
control = `<select name=${key}>\n ${optionList(f, opts)}\n </select>`
|
|
2532
|
+
} else if (f.type === 'boolean') {
|
|
2533
|
+
facetControls.select = true
|
|
2534
|
+
control = `<select name=${key}>\n ${optionList(f, [{ value: 'true', label: 'Yes' }, { value: 'false', label: 'No' }])}\n </select>`
|
|
2535
|
+
} else {
|
|
2536
|
+
facetControls.input = true
|
|
2537
|
+
control = `<input name=${key} value={${cur(f)}} placeholder="Any" />`
|
|
2538
|
+
}
|
|
2539
|
+
return ` <label class="sk-facet">\n <span>${htmlEsc(f.label ?? f.field)}</span>\n ${control}\n </label>`
|
|
2540
|
+
})
|
|
2541
|
+
.join('\n')
|
|
2542
|
+
return `
|
|
2543
|
+
<form method="GET" class="sk-facets">
|
|
2544
|
+
<input type="hidden" name="sort" value={page.url.searchParams.get('sort') ?? ''} />
|
|
2545
|
+
${controls}
|
|
2546
|
+
<div class="sk-facet-actions">
|
|
2547
|
+
<button type="submit">Filter</button>
|
|
2548
|
+
<a href={page.url.pathname}>Clear</a>
|
|
2549
|
+
</div>
|
|
2550
|
+
</form>
|
|
2551
|
+
`
|
|
2552
|
+
})()
|
|
2553
|
+
|
|
2554
|
+
const facetCss = !facetForm
|
|
2555
|
+
? ''
|
|
2556
|
+
: [
|
|
2557
|
+
' .sk-facets { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; margin-bottom: 14px; padding: 12px 14px; border: 1px solid var(--sg-border, #cbd5e1); border-radius: 10px; background: var(--sg-surface, #f8fafc); }',
|
|
2558
|
+
' .sk-facet { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }',
|
|
2559
|
+
' .sk-facet span { color: var(--sg-muted, #64748b); }',
|
|
2560
|
+
...(facetControls.select && facetControls.input
|
|
2561
|
+
? [' .sk-facet input, .sk-facet select { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']
|
|
2562
|
+
: facetControls.select
|
|
2563
|
+
? [' .sk-facet select { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']
|
|
2564
|
+
: [' .sk-facet input { font: inherit; padding: 6px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }']),
|
|
2565
|
+
' .sk-facet-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }',
|
|
2566
|
+
' .sk-facet-actions button { font: inherit; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--sg-accent, #4f46e5); background: var(--sg-accent, #4f46e5); color: #fff; cursor: pointer; }',
|
|
2567
|
+
' .sk-facet-actions a { font-size: 13px; color: var(--sg-muted, #64748b); }',
|
|
2568
|
+
'',
|
|
2569
|
+
].join('\n')
|
|
2570
|
+
|
|
2448
2571
|
const row = `(editing as Record<string, unknown>)`
|
|
2449
2572
|
const fieldBlocks = formFields
|
|
2450
2573
|
.map((f) => {
|
|
@@ -2461,17 +2584,57 @@ ${writeGuard('delete')} const fd = await request.formData()
|
|
|
2461
2584
|
} else if (isRel(f)) {
|
|
2462
2585
|
// Relation: options prefetched in load (data.<field>Options), current FK selected.
|
|
2463
2586
|
input = `<select name=${key}${req}>\n <option value="">-</option>\n {#each data.${f.field}Options as o (o.value)}<option value={o.value} selected={!isCreate && String(${row}[${key}] ?? '') === o.value}>{o.label}</option>{/each}\n </select>`
|
|
2587
|
+
} else if (f.input?.editorType === 'textarea' || f.type === 'json') {
|
|
2588
|
+
input = `<textarea name=${key}${req}${attrs(f)}>{isCreate ? '' : (${row}[${key}] ?? '')}</textarea>`
|
|
2464
2589
|
} else {
|
|
2465
|
-
|
|
2466
|
-
|
|
2590
|
+
// The browser enforces the same constraints the server will, so an
|
|
2591
|
+
// obvious mistake is caught before a round-trip - and still caught after
|
|
2592
|
+
// one, because `validateAll` runs in the action regardless.
|
|
2593
|
+
const t = f.format === 'email' ? 'email'
|
|
2594
|
+
: f.format === 'url' ? 'url'
|
|
2595
|
+
: normType(f.type) === 'number' ? 'number'
|
|
2596
|
+
: f.type === 'date' || f.type === 'dateString' ? 'date'
|
|
2597
|
+
: 'text'
|
|
2598
|
+
input = `<input type="${t}" name=${key} value={isCreate ? '' : (${row}[${key}] ?? '')}${req}${attrs(f)} />`
|
|
2467
2599
|
}
|
|
2468
|
-
|
|
2600
|
+
const hint = f.input?.help
|
|
2601
|
+
return ` <label class="sk-field${f.input?.span === 2 ? ' sk-field--wide' : ''}">
|
|
2469
2602
|
<span>${htmlEsc(f.label ?? f.field)}${f.required ? ' *' : ''}</span>
|
|
2470
2603
|
${input}
|
|
2471
|
-
{#if form?.errors?.[${key}]}<em class="sk-err">{form.errors[${key}]}</em>{/if}
|
|
2604
|
+
${hint ? ` {#if !form?.errors?.[${key}]}<small class="sk-hint">${htmlEsc(hint)}</small>{/if}\n` : ''} {#if form?.errors?.[${key}]}<em class="sk-err">{form.errors[${key}]}</em>{/if}
|
|
2472
2605
|
</label>`
|
|
2473
2606
|
})
|
|
2474
|
-
|
|
2607
|
+
|
|
2608
|
+
// Group the inputs the way the schema's form layout says, so the server-
|
|
2609
|
+
// rendered form matches the one the edit panel draws instead of being a flat
|
|
2610
|
+
// fallback. Fields outside every section keep rendering, in a trailing group.
|
|
2611
|
+
const blockFor = (name: string) => {
|
|
2612
|
+
const i = formFields.findIndex((f) => f.field === name)
|
|
2613
|
+
return i === -1 ? null : fieldBlocks[i]!
|
|
2614
|
+
}
|
|
2615
|
+
const layout = {
|
|
2616
|
+
columns: grid.formColumns ?? schema.form?.columns,
|
|
2617
|
+
sections: grid.formSections?.length ? grid.formSections : schema.form?.sections,
|
|
2618
|
+
}
|
|
2619
|
+
const ssrSections = layout?.sections?.length
|
|
2620
|
+
? (() => {
|
|
2621
|
+
const assigned = new Set<string>()
|
|
2622
|
+
const groups = layout.sections!.map((s) => {
|
|
2623
|
+
const items = s.fields.map((name) => { assigned.add(name); return blockFor(name) }).filter(Boolean) as string[]
|
|
2624
|
+
return { title: s.title, description: s.description, columns: s.columns, items }
|
|
2625
|
+
}).filter((g) => g.items.length)
|
|
2626
|
+
const rest = formFields.filter((f) => !assigned.has(f.field)).map((f) => blockFor(f.field)!).filter(Boolean)
|
|
2627
|
+
return rest.length ? [...groups, { title: undefined, description: undefined, columns: undefined, items: rest }] : groups
|
|
2628
|
+
})()
|
|
2629
|
+
: null
|
|
2630
|
+
const formCols = layout?.columns ?? 1
|
|
2631
|
+
const fieldsMarkup = ssrSections
|
|
2632
|
+
? ssrSections
|
|
2633
|
+
.map((g) => ` <fieldset class="sk-group" style="--sk-cols: ${g.columns ?? formCols}">
|
|
2634
|
+
${g.title ? ` <legend>${htmlEsc(g.title)}</legend>\n` : ''}${g.description ? ` <p class="sk-group__desc">${htmlEsc(g.description)}</p>\n` : ''}${g.items.join('\n')}
|
|
2635
|
+
</fieldset>`)
|
|
2636
|
+
.join('\n')
|
|
2637
|
+
: ` <div class="sk-group" style="--sk-cols: ${formCols}">\n${fieldBlocks.join('\n')}\n </div>`
|
|
2475
2638
|
|
|
2476
2639
|
const page = `<script lang="ts">
|
|
2477
2640
|
import { SvGrid, renderSnippet, type ColumnDef, type CellContext } from '@svgrid/grid'
|
|
@@ -2541,7 +2704,7 @@ ${wantsFilter ? `
|
|
|
2541
2704
|
<h1>{TITLE}</h1>
|
|
2542
2705
|
<button type="button" class="sk-btn sk-btn--primary" onclick={() => (editing = 'create')}>{NEW_LABEL}</button>
|
|
2543
2706
|
</header>
|
|
2544
|
-
|
|
2707
|
+
${facetForm}
|
|
2545
2708
|
<SvGrid
|
|
2546
2709
|
data={data.rows}
|
|
2547
2710
|
{columns}
|
|
@@ -2560,7 +2723,7 @@ ${wantsFilter ? `
|
|
|
2560
2723
|
<form method="POST" action={isCreate ? '?/create' : '?/update'} class="sk-form" use:enhance={onSubmit}>
|
|
2561
2724
|
<h2>{isCreate ? NEW_LABEL : 'Edit ${htmlEsc(n.label)}'}</h2>
|
|
2562
2725
|
{#if !isCreate}<input type="hidden" name="__id" value={${row}[ID_FIELD] as string} />{/if}
|
|
2563
|
-
${
|
|
2726
|
+
${fieldsMarkup}
|
|
2564
2727
|
<div class="sk-form__actions">
|
|
2565
2728
|
<button type="button" class="sk-btn" onclick={() => (editing = null)}>Cancel</button>
|
|
2566
2729
|
<button type="submit" class="sk-btn sk-btn--primary">Save</button>
|
|
@@ -2574,12 +2737,19 @@ ${fieldBlocks}
|
|
|
2574
2737
|
.sk-head h1 { margin: 0; font-size: 20px; }
|
|
2575
2738
|
.sk-btn { font: inherit; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer; }
|
|
2576
2739
|
.sk-btn--primary { background: var(--sg-accent, #4f46e5); border-color: var(--sg-accent, #4f46e5); color: #fff; }
|
|
2577
|
-
.sk-rowact { display: flex; gap: 10px; }
|
|
2740
|
+
${facetCss} .sk-rowact { display: flex; gap: 10px; }
|
|
2578
2741
|
.sk-link { background: none; border: none; padding: 0; font: inherit; color: var(--sg-accent, #4f46e5); cursor: pointer; }
|
|
2579
2742
|
.sk-danger { color: #dc2626; }
|
|
2580
2743
|
.sk-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); display: grid; place-items: center; z-index: 50; }
|
|
2581
2744
|
.sk-form { width: min(480px, 92vw); max-height: 90vh; overflow: auto; background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); border-radius: 12px; padding: 22px; display: flex; flex-direction: column; gap: 12px; box-shadow: 0 24px 60px -20px rgba(15, 23, 42, 0.5); }
|
|
2582
2745
|
.sk-form h2 { margin: 0 0 4px; font-size: 16px; }
|
|
2746
|
+
.sk-group { display: grid; grid-template-columns: repeat(var(--sk-cols, 1), minmax(0, 1fr)); gap: 12px; border: 0; padding: 0; margin: 0; min-width: 0; }
|
|
2747
|
+
.sk-group + .sk-group { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--sg-border, #e2e8f0); }
|
|
2748
|
+
.sk-group legend { grid-column: 1 / -1; padding: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.02em; text-transform: uppercase; color: var(--sg-muted, #64748b); }
|
|
2749
|
+
.sk-group__desc { grid-column: 1 / -1; margin: 0; font-size: 12px; color: var(--sg-muted, #64748b); }
|
|
2750
|
+
.sk-field--wide { grid-column: 1 / -1; }
|
|
2751
|
+
.sk-hint { color: var(--sg-muted, #64748b); font-size: 11.5px; }
|
|
2752
|
+
@media (max-width: 560px) { .sk-group { grid-template-columns: 1fr; } }
|
|
2583
2753
|
.sk-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
|
|
2584
2754
|
.sk-field span { color: var(--sg-muted, #64748b); }
|
|
2585
2755
|
.sk-field input, .sk-field select { font: inherit; padding: 7px 9px; border-radius: 8px; border: 1px solid var(--sg-border, #cbd5e1); background: var(--sg-input-bg, #fff); color: var(--sg-fg, #0f172a); }
|
|
@@ -2669,7 +2839,8 @@ export function emitStudioProject(project: StudioProject): GeneratedFile[] {
|
|
|
2669
2839
|
// driven sort/filter/page; a read-only screen (data-viz / detail) gets a
|
|
2670
2840
|
// load-only server file, and the SAME page markup renders from data.*.
|
|
2671
2841
|
if (isSsrScreen(project, screen)) {
|
|
2672
|
-
const
|
|
2842
|
+
const kind = sources[screen.entity]?.kind
|
|
2843
|
+
const srcKind: SsrSourceKind = kind === 'sql' ? 'sql' : kind === 'rest' ? 'rest' : 'memory'
|
|
2673
2844
|
if (ssrScreenShape(project, screen) === 'grid') {
|
|
2674
2845
|
pages.push(...emitSsrGridScreen(schema, screen, srcKind, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName))
|
|
2675
2846
|
} else {
|
|
@@ -2682,6 +2853,20 @@ export function emitStudioProject(project: StudioProject): GeneratedFile[] {
|
|
|
2682
2853
|
}
|
|
2683
2854
|
pages.push(screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, false, sources[screen.entity]))
|
|
2684
2855
|
}
|
|
2856
|
+
// Once the root layout leaves SSR on, every client-only screen has to say so
|
|
2857
|
+
// itself: these pages fetch their rows in the browser, so rendering them on the
|
|
2858
|
+
// server would run that fetch there and hand back an empty first paint.
|
|
2859
|
+
const anySsr = project.screens.some((s) => isSsrScreen(project, s))
|
|
2860
|
+
if (anySsr) {
|
|
2861
|
+
for (const screen of project.screens) {
|
|
2862
|
+
if (isSsrScreen(project, screen) || !screen.route) continue
|
|
2863
|
+
pages.push({
|
|
2864
|
+
path: `src/routes/${screen.route}/+page.ts`,
|
|
2865
|
+
description: `${screen.title} renders in the browser (client data source).`,
|
|
2866
|
+
contents: `// This screen loads its rows in the browser, so it opts out of the root\n// layout's server rendering.\nexport const ssr = false\n`,
|
|
2867
|
+
})
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2685
2870
|
// Any SSR screen needs the shared URL-params -> query helper (server-only).
|
|
2686
2871
|
const ssrHelpers = project.screens.some((s) => isSsrScreen(project, s)) ? [ssrQueryHelperFile()] : []
|
|
2687
2872
|
const actionRouteFiles = [...actionsById.values()].map(({ action, screenId }) => actionRouteFile(action, screenId, accessEnabled))
|
|
@@ -4516,6 +4701,26 @@ main().then(() => process.exit(0)).catch((err) => { console.error(err); process.
|
|
|
4516
4701
|
const appSlug = (title: string): string =>
|
|
4517
4702
|
(title || 'studio-app').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'studio-app'
|
|
4518
4703
|
|
|
4704
|
+
/**
|
|
4705
|
+
* The root `+layout.ts`.
|
|
4706
|
+
*
|
|
4707
|
+
* An app with server-rendered screens leaves SvelteKit's own default in place
|
|
4708
|
+
* (SSR on) and lets each client-only screen opt out in its own `+page.ts` - so
|
|
4709
|
+
* the app reads the way a hand-written SvelteKit app does, instead of announcing
|
|
4710
|
+
* itself as a SPA in the first file anyone opens. With nothing to server-render,
|
|
4711
|
+
* the whole app stays a SPA as before.
|
|
4712
|
+
*/
|
|
4713
|
+
function rootLayoutModule(project: StudioProject): GeneratedFile {
|
|
4714
|
+
const anySsr = project.screens.some((s) => isSsrScreen(project, s))
|
|
4715
|
+
return {
|
|
4716
|
+
path: 'src/routes/+layout.ts',
|
|
4717
|
+
description: anySsr ? 'Root layout options (SSR on; client-only screens opt out per page).' : 'Client SPA (in-memory sources persist across navigation).',
|
|
4718
|
+
contents: anySsr
|
|
4719
|
+
? `// Server rendering stays on (SvelteKit's default). Screens that have to run in\n// the browser - anything on an in-memory source, or a shape that can't render on\n// the server - opt out in their own +page.ts.\nexport const prerender = false\n`
|
|
4720
|
+
: `// In-memory sources are module singletons, so render as a client SPA. Move an\n// entity to SQL / Supabase and its /api route still runs server-side.\nexport const ssr = false\nexport const prerender = false\n`,
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4519
4724
|
/** The static SvelteKit + Vite scaffolding around the generated screens. */
|
|
4520
4725
|
const SCAFFOLD_STATIC: ReadonlyArray<GeneratedFile> = [
|
|
4521
4726
|
// watch.ignored: `studio dev` runs this app's Vite next to the designer, which
|
|
@@ -4525,7 +4730,6 @@ const SCAFFOLD_STATIC: ReadonlyArray<GeneratedFile> = [
|
|
|
4525
4730
|
{ path: 'tsconfig.json', description: 'TypeScript config.', contents: `{\n "extends": "./.svelte-kit/tsconfig.json",\n "compilerOptions": {\n "allowJs": true,\n "checkJs": true,\n "esModuleInterop": true,\n "forceConsistentCasingInFileNames": true,\n "resolveJsonModule": true,\n "skipLibCheck": true,\n "sourceMap": true,\n "strict": true,\n "moduleResolution": "bundler"\n }\n}\n` },
|
|
4526
4731
|
{ path: 'src/app.html', description: 'HTML shell.', contents: `<!doctype html>\n<html lang="en">\n <head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n %sveltekit.head%\n </head>\n <body data-sveltekit-preload-data="hover">\n <div style="display: contents">%sveltekit.body%</div>\n </body>\n</html>\n` },
|
|
4527
4732
|
{ path: 'src/app.d.ts', description: 'SvelteKit app types.', contents: `declare global {\n namespace App {}\n}\n\nexport {}\n` },
|
|
4528
|
-
{ path: 'src/routes/+layout.ts', description: 'Client SPA (in-memory sources persist across navigation).', contents: `// In-memory sources are module singletons, so render as a client SPA. Move an\n// entity to SQL / Supabase and its /api route still runs server-side.\nexport const ssr = false\nexport const prerender = false\n` },
|
|
4529
4733
|
// engine-strict=false: a mismatched Node `engines` range only warns, never hard-fails
|
|
4530
4734
|
// `npm install` - important for sandboxes (StackBlitz WebContainer) whose Node version
|
|
4531
4735
|
// may not satisfy every transitive dep. (npm 7+ installs peers by default, so no
|
|
@@ -4883,7 +5087,12 @@ function svelteConfig(plan: DeployPlan): string {
|
|
|
4883
5087
|
/** The runtime npm deps the generated source imports, keyed by package -> range.
|
|
4884
5088
|
* One source of truth for package.json AND the fragment-mode dependency report. */
|
|
4885
5089
|
export function runtimeDeps(project: StudioProject, allSource: string): Record<string, string> {
|
|
4886
|
-
|
|
5090
|
+
// Pin to the generator's own version, not `latest`: the emitted code uses the
|
|
5091
|
+
// APIs of the SvGrid it was written by, so the app must install a runtime at
|
|
5092
|
+
// least that new. `latest` also made a scaffold non-reproducible - an app could
|
|
5093
|
+
// resolve a different runtime tomorrow, or break on an unrelated publish.
|
|
5094
|
+
const svgrid = `^${SVGRID_VERSION}`
|
|
5095
|
+
const dependencies: Record<string, string> = { '@svgrid/grid': svgrid, '@svgrid/enterprise': svgrid }
|
|
4887
5096
|
if (allSource.includes("from '@supabase/supabase-js'")) dependencies['@supabase/supabase-js'] = '^2.45.0'
|
|
4888
5097
|
if (allSource.includes("import pg from 'pg'")) dependencies['pg'] = '^8.11.0'
|
|
4889
5098
|
if (allSource.includes("from 'mysql2/promise'")) dependencies['mysql2'] = '^3.9.0'
|
|
@@ -5088,6 +5297,7 @@ export function emitStudioAppBundle(project: StudioProject): GeneratedFile[] {
|
|
|
5088
5297
|
{ path: 'src/lib/schemas.test.ts', description: 'Smoke tests: every entity renders + round-trips through its data source.', contents: smokeTestFile(project) },
|
|
5089
5298
|
...plan.files,
|
|
5090
5299
|
...cronScheduleFiles(project, plan),
|
|
5300
|
+
rootLayoutModule(project),
|
|
5091
5301
|
...SCAFFOLD_STATIC,
|
|
5092
5302
|
...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource)! }] : []),
|
|
5093
5303
|
...(envDotFile(allSource) ? [{ path: '.env', description: 'Local env (git-ignored): a real random SESSION_SECRET is pre-filled so sessions are secure out of the box; fill in the rest.', contents: envDotFile(allSource)! }] : []),
|
|
@@ -88,7 +88,10 @@ export const lookupVar = (schema: EntitySchema, field: string) => `${camel(schem
|
|
|
88
88
|
function rowType(schema: EntitySchema, derived: Set<string>): string {
|
|
89
89
|
// Runtime-filled columns are optional in the row type: relation labels (`derived`)
|
|
90
90
|
// and computed / no-code-formula fields (not present on seed rows / inserts).
|
|
91
|
-
|
|
91
|
+
// So is a field the form only asks for under a condition - a row that never met
|
|
92
|
+
// the condition simply does not carry it (a won deal has no "lost reason").
|
|
93
|
+
const optional = (f: EntityField) =>
|
|
94
|
+
derived.has(f.field) || !!f.formula || !!f.computed || !!f.when?.visible
|
|
92
95
|
const lines = schema.fields.map((f) => ` ${f.field}${optional(f) ? '?' : ''}: ${tsType(f)}`)
|
|
93
96
|
return `export type ${pascal(schema.name)} = {\n${lines.join('\n')}\n}`
|
|
94
97
|
}
|
package/src/studio/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
type InitFlags,
|
|
55
55
|
type InitResult,
|
|
56
56
|
} from './init-flow.js'
|
|
57
|
+
export { UserError, isUserError } from './user-error.js'
|
|
57
58
|
export {
|
|
58
59
|
crudSuiteScreens,
|
|
59
60
|
addCrudSuite,
|
|
@@ -171,6 +172,7 @@ export {
|
|
|
171
172
|
clickSlot,
|
|
172
173
|
ssrScreenShape,
|
|
173
174
|
ssrEligible,
|
|
175
|
+
withSsrDefaults,
|
|
174
176
|
setScreenRenderMode,
|
|
175
177
|
eventSlot,
|
|
176
178
|
rowSelectSlot,
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
2
|
import { runStudioInit, parseSelection, type DbGateway, type PromptIO } from './init-flow.js'
|
|
3
|
+
import { isUserError } from './user-error.js'
|
|
3
4
|
import type { StudioIO } from './cli.js'
|
|
4
5
|
import { parseProject, validateProject, type GridConfig } from './project.js'
|
|
5
6
|
|
|
7
|
+
/** Run the flow and hand back whatever it threw. */
|
|
8
|
+
async function thrownBy(run: Promise<unknown>): Promise<unknown> {
|
|
9
|
+
try {
|
|
10
|
+
await run
|
|
11
|
+
throw new Error('expected the flow to throw')
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return err
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
6
17
|
/** A scripted terminal: answers are consumed in order, defaults fill the rest. */
|
|
7
18
|
function scriptedPrompts(answers: string[]) {
|
|
8
19
|
const asked: string[] = []
|
|
@@ -352,6 +363,145 @@ describe('runStudioInit', () => {
|
|
|
352
363
|
expect(result.project.theme?.mode).toBe('dark')
|
|
353
364
|
})
|
|
354
365
|
|
|
366
|
+
describe('mistakes are reported as mistakes, not crashes', () => {
|
|
367
|
+
// The CLI writes a bug report and prints a "report this" link for anything
|
|
368
|
+
// that isn't a UserError, so a typo must never come back as an Error.
|
|
369
|
+
it('flags a bad dataset id, an empty table pick and a missing driver as user errors', async () => {
|
|
370
|
+
const fs = memoryIo()
|
|
371
|
+
const badDataset = await thrownBy(
|
|
372
|
+
runStudioInit({ yes: true, dataset: 'nope' }, scriptedPrompts([]).io, null, fs.io),
|
|
373
|
+
)
|
|
374
|
+
expect(isUserError(badDataset)).toBe(true)
|
|
375
|
+
expect((badDataset as { hint?: string }).hint).toContain('customers-orders')
|
|
376
|
+
|
|
377
|
+
const noTables = await thrownBy(
|
|
378
|
+
runStudioInit({ db: 'postgres', url: 'postgres://x' }, scriptedPrompts(['none']).io, fakeDb(), fs.io),
|
|
379
|
+
)
|
|
380
|
+
expect(isUserError(noTables)).toBe(true)
|
|
381
|
+
|
|
382
|
+
const db: DbGateway = { ...fakeDb(), ensureDriver: async () => ({ ok: false, message: 'pg is not installed' }) }
|
|
383
|
+
const noDriver = await thrownBy(
|
|
384
|
+
runStudioInit({ db: 'postgres', url: 'postgres://x' }, scriptedPrompts(['all']).io, db, fs.io),
|
|
385
|
+
)
|
|
386
|
+
expect(isUserError(noDriver)).toBe(true)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('blames the connection string when the database cannot be reached', async () => {
|
|
390
|
+
const fs = memoryIo()
|
|
391
|
+
const db: DbGateway = {
|
|
392
|
+
ensureDriver: async () => ({ ok: true }),
|
|
393
|
+
connect: async () => { throw new Error('getaddrinfo ENOTFOUND base') },
|
|
394
|
+
}
|
|
395
|
+
const err = await thrownBy(
|
|
396
|
+
runStudioInit({ db: 'postgres', url: 'not-a-real-conn' }, scriptedPrompts([]).io, db, fs.io),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
expect(isUserError(err)).toBe(true)
|
|
400
|
+
// The driver's own wording survives - it is the diagnostic - but the hint
|
|
401
|
+
// points at the thing the user can actually fix.
|
|
402
|
+
expect((err as Error).message).toContain('ENOTFOUND')
|
|
403
|
+
expect((err as { hint?: string }).hint).toContain('postgres://')
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('explains a REST endpoint that is not an absolute URL instead of throwing "Invalid URL"', async () => {
|
|
407
|
+
const prompts = scriptedPrompts(['5', './rows.json'])
|
|
408
|
+
const fs = memoryIo()
|
|
409
|
+
const rows = JSON.stringify([{ id: 1, name: 'Widget' }])
|
|
410
|
+
const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io, async () => rows))
|
|
411
|
+
|
|
412
|
+
expect(isUserError(err)).toBe(true)
|
|
413
|
+
expect((err as Error).message).not.toMatch(/Invalid URL/)
|
|
414
|
+
expect((err as Error).message).toContain('not a full URL')
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('explains a REST endpoint that does not return JSON', async () => {
|
|
418
|
+
const prompts = scriptedPrompts(['5', 'https://api.example.com/widgets'])
|
|
419
|
+
const fs = memoryIo()
|
|
420
|
+
const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io, async () => '<html>nope</html>'))
|
|
421
|
+
|
|
422
|
+
expect(isUserError(err)).toBe(true)
|
|
423
|
+
expect((err as Error).message).toContain('did not return JSON')
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('target folder guard', () => {
|
|
428
|
+
/** A folder that already holds somebody else's SvelteKit app. */
|
|
429
|
+
const existingApp = () => {
|
|
430
|
+
const fs = memoryIo()
|
|
431
|
+
fs.files.set('package.json', '{ "name": "their-app" }')
|
|
432
|
+
fs.files.set('svelte.config.js', 'export default {}')
|
|
433
|
+
return fs
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
it('asks before overwriting an app that is already there', async () => {
|
|
437
|
+
const fs = existingApp()
|
|
438
|
+
const prompts = scriptedPrompts(['1', '1', 'y', '1', '1', 'n', 'Mine', '.', 'y'])
|
|
439
|
+
await runStudioInit({}, prompts.io, null, fs.io)
|
|
440
|
+
|
|
441
|
+
expect(prompts.asked).toContain('Overwrite? (y/N)')
|
|
442
|
+
expect(fs.files.has('studio.config.json')).toBe(true)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('writes nothing when the answer is no', async () => {
|
|
446
|
+
const fs = existingApp()
|
|
447
|
+
const prompts = scriptedPrompts(['1', '1', 'y', '1', '1', 'n', 'Mine', '.', 'n'])
|
|
448
|
+
const err = await thrownBy(runStudioInit({}, prompts.io, null, fs.io))
|
|
449
|
+
|
|
450
|
+
expect(isUserError(err)).toBe(true)
|
|
451
|
+
expect(fs.files.get('package.json')).toBe('{ "name": "their-app" }')
|
|
452
|
+
expect(fs.files.has('studio.config.json')).toBe(false)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('refuses rather than silently clobbering when nothing can be asked', async () => {
|
|
456
|
+
const fs = existingApp()
|
|
457
|
+
const err = await thrownBy(runStudioInit({ yes: true }, scriptedPrompts([]).io, null, fs.io))
|
|
458
|
+
|
|
459
|
+
expect(isUserError(err)).toBe(true)
|
|
460
|
+
expect((err as { hint?: string }).hint).toContain('--force')
|
|
461
|
+
expect(fs.files.get('package.json')).toBe('{ "name": "their-app" }')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('goes ahead with --force', async () => {
|
|
465
|
+
const fs = existingApp()
|
|
466
|
+
await runStudioInit({ yes: true, force: true }, scriptedPrompts([]).io, null, fs.io)
|
|
467
|
+
expect(fs.files.get('package.json')).not.toBe('{ "name": "their-app" }')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('treats a folder Studio generated as a regenerate, not a clobber', async () => {
|
|
471
|
+
const fs = memoryIo()
|
|
472
|
+
await runStudioInit({ yes: true, out: 'app' }, scriptedPrompts([]).io, null, fs.io)
|
|
473
|
+
// studio.config.json is there now, so a second run asks nothing.
|
|
474
|
+
const second = scriptedPrompts([])
|
|
475
|
+
await runStudioInit({ yes: true, out: 'app' }, second.io, null, fs.io)
|
|
476
|
+
expect(second.asked).toEqual([])
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
it('ignores files Studio itself creates mid-run', async () => {
|
|
480
|
+
// Picking a database installs a driver into the target folder first, which
|
|
481
|
+
// writes a package.json. The guard must not then trip over it.
|
|
482
|
+
const fs = memoryIo()
|
|
483
|
+
const db: DbGateway = {
|
|
484
|
+
...fakeDb(),
|
|
485
|
+
ensureDriver: async () => {
|
|
486
|
+
await fs.io.writeFile('package.json', '{ "dependencies": { "pg": "^8" } }')
|
|
487
|
+
return { ok: true }
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
const prompts = scriptedPrompts([])
|
|
491
|
+
const result = await runStudioInit({ db: 'postgres', url: 'postgres://x', yes: true }, prompts.io, db, fs.io)
|
|
492
|
+
|
|
493
|
+
expect(prompts.asked).toEqual([])
|
|
494
|
+
expect(result.written.some((p) => p.endsWith('studio.config.json'))).toBe(true)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('leaves an empty folder alone', async () => {
|
|
498
|
+
const fs = memoryIo()
|
|
499
|
+
const prompts = scriptedPrompts([])
|
|
500
|
+
await runStudioInit({ yes: true }, prompts.io, null, fs.io)
|
|
501
|
+
expect(prompts.asked).toEqual([])
|
|
502
|
+
})
|
|
503
|
+
})
|
|
504
|
+
|
|
355
505
|
it('keeps user-owned files that already exist', async () => {
|
|
356
506
|
const fs = memoryIo()
|
|
357
507
|
await runStudioInit({ yes: true }, scriptedPrompts([]).io, null, fs.io)
|