@svgrid/enterprise 2.3.2 → 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-oYok52zd.css +1 -0
- package/dist/designer/assets/{index-DVrv9dH6.js → index-qL7ir9C7.js} +89 -89
- package/dist/designer/assets/src-96d31O63.js +2873 -0
- package/dist/designer/assets/src-D-O2F805.css +1 -0
- package/dist/designer/index.html +6 -6
- 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
- package/dist/designer/assets/index-DjPp1mn7.css +0 -1
- package/dist/designer/assets/src-BRKu53tV.js +0 -2842
- package/dist/designer/assets/src-DseB1VRN.css +0 -1
package/src/edit-panel.test.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
buildInitialValues,
|
|
5
5
|
controlKind,
|
|
6
6
|
editMode,
|
|
7
|
+
evalValidationRules,
|
|
8
|
+
fieldState,
|
|
7
9
|
rowId,
|
|
8
10
|
toSubmitValues,
|
|
9
11
|
validateAll,
|
|
@@ -13,7 +15,9 @@ import {
|
|
|
13
15
|
fromNumberValue,
|
|
14
16
|
toSliderValue,
|
|
15
17
|
toTags,
|
|
18
|
+
visibleFormFields,
|
|
16
19
|
} from './edit-panel'
|
|
20
|
+
import { schemaToFormFields, type FormFieldDescriptor } from './schema'
|
|
17
21
|
|
|
18
22
|
type Customer = {
|
|
19
23
|
id: string
|
|
@@ -175,39 +179,144 @@ describe('rowId', () => {
|
|
|
175
179
|
expect(rowId(schema, { id: 99 } as unknown as Customer)).toBe('99')
|
|
176
180
|
})
|
|
177
181
|
})
|
|
178
|
-
|
|
179
|
-
describe('editor value coercion', () => {
|
|
180
|
-
it('formats a Date to a local yyyy-MM-dd (no UTC shift)', () => {
|
|
181
|
-
// Local midnight - toISOString would shift the day in negative-offset zones; local formatting must not.
|
|
182
|
-
const d = new Date(2024, 0, 2, 0, 0) // 2 Jan 2024, local
|
|
183
|
-
expect(toDateString(d)).toBe('2024-01-02')
|
|
184
|
-
expect(toDateString(null)).toBe('')
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('formats a Date to a local yyyy-MM-ddTHH:mm', () => {
|
|
188
|
-
expect(toDateTimeString(new Date(2024, 2, 4, 9, 5))).toBe('2024-03-04T09:05')
|
|
189
|
-
expect(toDateTimeString(null)).toBe('')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('coerces numbers in/out with the empty-string convention', () => {
|
|
193
|
-
expect(toNumberValue(1200)).toBe(1200)
|
|
194
|
-
expect(toNumberValue('1200')).toBeNull() // strings are not numbers
|
|
195
|
-
expect(toNumberValue(NaN)).toBeNull()
|
|
196
|
-
expect(fromNumberValue(null)).toBe('')
|
|
197
|
-
expect(fromNumberValue(0)).toBe(0) // 0 is a real value, not empty
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('slider falls back to min when unset', () => {
|
|
201
|
-
expect(toSliderValue(42, 0)).toBe(42)
|
|
202
|
-
expect(toSliderValue('', 10)).toBe(10)
|
|
203
|
-
expect(toSliderValue(undefined, 5)).toBe(5)
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('coerces tags from arrays, comma strings, or empty', () => {
|
|
207
|
-
expect(toTags(['a', 'b'])).toEqual(['a', 'b'])
|
|
208
|
-
expect(toTags('a, b ,c')).toEqual(['a', 'b', 'c'])
|
|
209
|
-
expect(toTags('')).toEqual([])
|
|
210
|
-
expect(toTags(null)).toEqual([])
|
|
211
|
-
expect(toTags([1, 2])).toEqual(['1', '2'])
|
|
212
|
-
})
|
|
213
|
-
})
|
|
182
|
+
|
|
183
|
+
describe('editor value coercion', () => {
|
|
184
|
+
it('formats a Date to a local yyyy-MM-dd (no UTC shift)', () => {
|
|
185
|
+
// Local midnight - toISOString would shift the day in negative-offset zones; local formatting must not.
|
|
186
|
+
const d = new Date(2024, 0, 2, 0, 0) // 2 Jan 2024, local
|
|
187
|
+
expect(toDateString(d)).toBe('2024-01-02')
|
|
188
|
+
expect(toDateString(null)).toBe('')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('formats a Date to a local yyyy-MM-ddTHH:mm', () => {
|
|
192
|
+
expect(toDateTimeString(new Date(2024, 2, 4, 9, 5))).toBe('2024-03-04T09:05')
|
|
193
|
+
expect(toDateTimeString(null)).toBe('')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('coerces numbers in/out with the empty-string convention', () => {
|
|
197
|
+
expect(toNumberValue(1200)).toBe(1200)
|
|
198
|
+
expect(toNumberValue('1200')).toBeNull() // strings are not numbers
|
|
199
|
+
expect(toNumberValue(NaN)).toBeNull()
|
|
200
|
+
expect(fromNumberValue(null)).toBe('')
|
|
201
|
+
expect(fromNumberValue(0)).toBe(0) // 0 is a real value, not empty
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('slider falls back to min when unset', () => {
|
|
205
|
+
expect(toSliderValue(42, 0)).toBe(42)
|
|
206
|
+
expect(toSliderValue('', 10)).toBe(10)
|
|
207
|
+
expect(toSliderValue(undefined, 5)).toBe(5)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('coerces tags from arrays, comma strings, or empty', () => {
|
|
211
|
+
expect(toTags(['a', 'b'])).toEqual(['a', 'b'])
|
|
212
|
+
expect(toTags('a, b ,c')).toEqual(['a', 'b', 'c'])
|
|
213
|
+
expect(toTags('')).toEqual([])
|
|
214
|
+
expect(toTags(null)).toEqual([])
|
|
215
|
+
expect(toTags([1, 2])).toEqual(['1', '2'])
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// --- the form builder: value-driven fields -----------------------------------
|
|
220
|
+
|
|
221
|
+
/** `field <op> value` as a serializable predicate leaf. */
|
|
222
|
+
const cmp = (column: string, op: string, value: unknown) =>
|
|
223
|
+
({ kind: 'cmp', column, op, value }) as never
|
|
224
|
+
|
|
225
|
+
type Order = { id: string; reason: string; other: string; total: number; approver: string }
|
|
226
|
+
|
|
227
|
+
const orders: EntitySchema<Order> = {
|
|
228
|
+
name: 'orders',
|
|
229
|
+
idField: 'id',
|
|
230
|
+
fields: [
|
|
231
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
232
|
+
{ field: 'reason', type: 'enum', options: [{ value: 'damaged', label: 'Damaged' }, { value: 'other', label: 'Other' }] },
|
|
233
|
+
// Only asked for, and only demanded, when the reason is "other".
|
|
234
|
+
{
|
|
235
|
+
field: 'other',
|
|
236
|
+
type: 'text',
|
|
237
|
+
when: { visible: cmp('reason', 'equals', 'other'), required: cmp('reason', 'equals', 'other') },
|
|
238
|
+
},
|
|
239
|
+
{ field: 'total', type: 'number' },
|
|
240
|
+
// Locked once the order is large enough to need sign-off.
|
|
241
|
+
{ field: 'approver', type: 'text', when: { disabled: cmp('total', 'lessThan', 1000) } },
|
|
242
|
+
],
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
describe('conditional fields', () => {
|
|
246
|
+
const specOf = (name: string) => schemaToFormFields(orders).find((f) => f.field === name)!
|
|
247
|
+
|
|
248
|
+
it('shows and demands a field only while its condition holds', () => {
|
|
249
|
+
const hidden = fieldState(specOf('other'), { reason: 'damaged', other: '' })
|
|
250
|
+
expect(hidden.visible).toBe(false)
|
|
251
|
+
expect(hidden.required).toBe(false)
|
|
252
|
+
|
|
253
|
+
const shown = fieldState(specOf('other'), { reason: 'other', other: '' })
|
|
254
|
+
expect(shown.visible).toBe(true)
|
|
255
|
+
expect(shown.required).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('locks a field while its disabled condition holds, and readonly always wins', () => {
|
|
259
|
+
expect(fieldState(specOf('approver'), { total: 10 }).disabled).toBe(true)
|
|
260
|
+
expect(fieldState(specOf('approver'), { total: 5000 }).disabled).toBe(false)
|
|
261
|
+
expect(fieldState(specOf('id'), { total: 5000 }).disabled).toBe(true)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('lists only the fields the form is currently showing', () => {
|
|
265
|
+
expect(visibleFormFields(orders, { reason: 'damaged' }).map((f) => f.field)).not.toContain('other')
|
|
266
|
+
expect(visibleFormFields(orders, { reason: 'other' }).map((f) => f.field)).toContain('other')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('never blocks a save on a field the user cannot see', async () => {
|
|
270
|
+
// `other` is required-when-visible, but the reason is "damaged", so it is
|
|
271
|
+
// neither shown nor demanded.
|
|
272
|
+
expect(await validateAll(orders, { reason: 'damaged', other: '', total: 1 })).toEqual({})
|
|
273
|
+
expect(await validateAll(orders, { reason: 'other', other: '', total: 1 })).toEqual({
|
|
274
|
+
other: 'Other is required',
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('leaves a hidden field out of the payload, so a stale value is never written', () => {
|
|
279
|
+
const hidden = toSubmitValues(orders, { reason: 'damaged', other: 'stale text', total: 1, approver: '' })
|
|
280
|
+
expect(hidden).not.toHaveProperty('other')
|
|
281
|
+
const shown = toSubmitValues(orders, { reason: 'other', other: 'kept', total: 1, approver: '' })
|
|
282
|
+
expect(shown.other).toBe('kept')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('falls back to showing a field when its condition is malformed', () => {
|
|
286
|
+
// A broken rule must never hide data or block a save.
|
|
287
|
+
const broken: FormFieldDescriptor = {
|
|
288
|
+
field: 'x', label: 'X', type: 'text', editorType: 'text',
|
|
289
|
+
required: false, readonly: false, span: 1,
|
|
290
|
+
when: { visible: { kind: 'nonsense' } as never },
|
|
291
|
+
}
|
|
292
|
+
expect(fieldState(broken, {}).visible).toBe(true)
|
|
293
|
+
expect(fieldState(broken, {}).disabled).toBe(false)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('no-code validation rules at runtime', () => {
|
|
298
|
+
it('applies each operator the generated server applies', () => {
|
|
299
|
+
const values = { name: 'ab', qty: 5, max: 3 }
|
|
300
|
+
expect(evalValidationRules([{ field: 'name', op: 'minLen', value: 3, message: 'Too short' }], values))
|
|
301
|
+
.toEqual({ name: 'Too short' })
|
|
302
|
+
expect(evalValidationRules([{ field: 'name', op: 'minLen', value: 2, message: 'Too short' }], values)).toEqual({})
|
|
303
|
+
expect(evalValidationRules([{ field: 'qty', op: 'lte', compareTo: 'max', message: 'Over the max' }], values))
|
|
304
|
+
.toEqual({ qty: 'Over the max' })
|
|
305
|
+
expect(evalValidationRules([{ field: 'missing', op: 'required', message: 'Needed' }], values))
|
|
306
|
+
.toEqual({ missing: 'Needed' })
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('runs as part of validateAll, so a rule fires in the form, not just on the server', async () => {
|
|
310
|
+
const withRules: EntitySchema<{ id: string; code: string }> = {
|
|
311
|
+
name: 'things',
|
|
312
|
+
idField: 'id',
|
|
313
|
+
fields: [
|
|
314
|
+
{ field: 'id', type: 'text', primaryKey: true, readonly: true },
|
|
315
|
+
{ field: 'code', type: 'text' },
|
|
316
|
+
],
|
|
317
|
+
validations: [{ field: 'code', op: 'minLen', value: 4, message: 'Code needs 4+ characters' }],
|
|
318
|
+
}
|
|
319
|
+
expect(await validateAll(withRules, { code: 'ab' })).toEqual({ code: 'Code needs 4+ characters' })
|
|
320
|
+
expect(await validateAll(withRules, { code: 'abcd' })).toEqual({})
|
|
321
|
+
})
|
|
322
|
+
})
|
package/src/edit-panel.ts
CHANGED
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
validateField,
|
|
14
14
|
type EntitySchema,
|
|
15
15
|
type FormFieldDescriptor,
|
|
16
|
-
|
|
16
|
+
type ValidationRuleSpec,
|
|
17
|
+
} from './schema.js'
|
|
18
|
+
import { evaluatePredicate } from './expressions/evaluate.js'
|
|
19
|
+
import type { PredicateExpr } from './expressions/expression-types.js'
|
|
17
20
|
|
|
18
21
|
export type EditMode = 'create' | 'edit'
|
|
19
22
|
|
|
@@ -136,6 +139,117 @@ export function builtInError(field: FormFieldDescriptor, value: unknown): string
|
|
|
136
139
|
return null
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Evaluate one of a field's `when` conditions against the values currently in
|
|
144
|
+
* the form. A malformed condition falls back rather than throwing: a bad rule
|
|
145
|
+
* should not make the form unusable, and the safe answer differs per flag
|
|
146
|
+
* (a field stays visible, stays enabled, keeps its static required flag).
|
|
147
|
+
*/
|
|
148
|
+
function test(expr: PredicateExpr | undefined, values: Record<string, unknown>, fallback: boolean): boolean {
|
|
149
|
+
if (!expr) return fallback
|
|
150
|
+
try {
|
|
151
|
+
const result = evaluatePredicate(expr, { row: values })
|
|
152
|
+
// An unrecognised node evaluates to undefined rather than throwing, so a
|
|
153
|
+
// non-boolean answer counts as "no opinion" and takes the fallback too.
|
|
154
|
+
return typeof result === 'boolean' ? result : fallback
|
|
155
|
+
} catch {
|
|
156
|
+
return fallback
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Whether a field is shown, editable, and demanded, for the current values. */
|
|
161
|
+
export type FieldState = { visible: boolean; disabled: boolean; required: boolean }
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve a field's live state from its `when` conditions. Read-only always
|
|
165
|
+
* wins over a `disabled` condition, and `when.required` replaces the static
|
|
166
|
+
* `required` flag rather than adding to it, so a rule can make a normally
|
|
167
|
+
* required field optional as well as the other way round.
|
|
168
|
+
*/
|
|
169
|
+
export function fieldState(field: FormFieldDescriptor, values: Record<string, unknown>): FieldState {
|
|
170
|
+
return {
|
|
171
|
+
visible: test(field.when?.visible, values, true),
|
|
172
|
+
disabled: field.readonly || test(field.when?.disabled, values, false),
|
|
173
|
+
required: field.when?.required ? test(field.when.required, values, false) : field.required,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Whether a form section's `visibleWhen` holds for the current values. */
|
|
178
|
+
export function sectionVisible(expr: PredicateExpr, values: Record<string, unknown>): boolean {
|
|
179
|
+
return test(expr, values, true)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The fields the form currently shows, in order. */
|
|
183
|
+
export function visibleFormFields<TData extends RowData>(
|
|
184
|
+
schema: EntitySchema<TData>,
|
|
185
|
+
values: Record<string, unknown>,
|
|
186
|
+
): FormFieldDescriptor[] {
|
|
187
|
+
return schemaToFormFields(schema).filter((f) => fieldState(f, values).visible)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** True when any field's behaviour depends on the values (i.e. has a `when`). */
|
|
191
|
+
export function hasFieldConditions<TData extends RowData>(schema: EntitySchema<TData>): boolean {
|
|
192
|
+
return schema.fields.some((f) => !!f.when && (!!f.when.visible || !!f.when.disabled || !!f.when.required))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Drop the values of fields hidden by a `when.visible` condition.
|
|
197
|
+
*
|
|
198
|
+
* The edit panel already leaves them out via `toSubmitValues`; a server that
|
|
199
|
+
* builds its own payload (the generated SSR route reads a `FormData`) runs it
|
|
200
|
+
* through this so both paths write exactly the same fields. Without it, a field
|
|
201
|
+
* the user could not see would still be saved from a posted form.
|
|
202
|
+
*/
|
|
203
|
+
export function stripHiddenValues<TData extends RowData>(
|
|
204
|
+
schema: EntitySchema<TData>,
|
|
205
|
+
values: Record<string, unknown>,
|
|
206
|
+
): Record<string, unknown> {
|
|
207
|
+
if (!hasFieldConditions(schema)) return values
|
|
208
|
+
const out: Record<string, unknown> = {}
|
|
209
|
+
const shown = new Set(visibleFormFields(schema, values).map((f) => f.field))
|
|
210
|
+
for (const [field, value] of Object.entries(values)) {
|
|
211
|
+
if (!schema.fields.some((f) => f.field === field) || shown.has(field)) out[field] = value
|
|
212
|
+
}
|
|
213
|
+
return out
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run the schema's no-code validation rules (`EntitySchema.validations`).
|
|
218
|
+
*
|
|
219
|
+
* These already compile into the generated app's server-side `hooks.validate`;
|
|
220
|
+
* this is the matching interpreter, so the same rule also fires live in the edit
|
|
221
|
+
* panel and the designer preview instead of only after a round-trip to a
|
|
222
|
+
* generated server. The comparisons mirror `compileValidation` in
|
|
223
|
+
* `studio/emit-schema.ts` exactly - string equality for eq/ne, numeric ordering
|
|
224
|
+
* for lt/lte/gt/gte, length for minLen/maxLen - so a rule cannot mean one thing
|
|
225
|
+
* in the form and another on the server.
|
|
226
|
+
*/
|
|
227
|
+
export function evalValidationRules(rules: ReadonlyArray<ValidationRuleSpec>, values: Record<string, unknown>): Record<string, string> {
|
|
228
|
+
const errors: Record<string, string> = {}
|
|
229
|
+
for (const rule of rules) {
|
|
230
|
+
if (errors[rule.field]) continue // first failing rule per field wins
|
|
231
|
+
const left = values[rule.field]
|
|
232
|
+
const num = () => (rule.compareTo ? Number(values[rule.compareTo]) : Number(rule.value))
|
|
233
|
+
const str = () => (rule.compareTo ? String(values[rule.compareTo]) : String(rule.value ?? ''))
|
|
234
|
+
const len = String(left ?? '').length
|
|
235
|
+
let holds: boolean
|
|
236
|
+
switch (rule.op) {
|
|
237
|
+
case 'eq': holds = String(left) === str(); break
|
|
238
|
+
case 'ne': holds = String(left) !== str(); break
|
|
239
|
+
case 'lt': holds = Number(left) < num(); break
|
|
240
|
+
case 'lte': holds = Number(left) <= num(); break
|
|
241
|
+
case 'gt': holds = Number(left) > num(); break
|
|
242
|
+
case 'gte': holds = Number(left) >= num(); break
|
|
243
|
+
case 'required': holds = left != null && left !== ''; break
|
|
244
|
+
case 'maxLen': holds = len <= Number(rule.value); break
|
|
245
|
+
case 'minLen': holds = len >= Number(rule.value); break
|
|
246
|
+
default: holds = true
|
|
247
|
+
}
|
|
248
|
+
if (!holds) errors[rule.field] = rule.message
|
|
249
|
+
}
|
|
250
|
+
return errors
|
|
251
|
+
}
|
|
252
|
+
|
|
139
253
|
/**
|
|
140
254
|
* Seed the form's editable state. Uses the row's value when editing, else the
|
|
141
255
|
* field's `defaultValue`, else a type-appropriate empty. Covers every field the
|
|
@@ -156,6 +270,48 @@ export function buildInitialValues<TData extends RowData>(
|
|
|
156
270
|
return values
|
|
157
271
|
}
|
|
158
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The error for one field, or null. Required-but-empty first, then the field's
|
|
275
|
+
* own constraints, then its Standard Schema validator. Read-only and
|
|
276
|
+
* condition-hidden fields never produce one: the user cannot act on them, so an
|
|
277
|
+
* error there would be a dead end.
|
|
278
|
+
*
|
|
279
|
+
* Shared by whole-form validation and by the panel's on-blur check, so a field
|
|
280
|
+
* cannot pass one and fail the other.
|
|
281
|
+
*/
|
|
282
|
+
async function fieldError(f: FormFieldDescriptor, values: Record<string, unknown>): Promise<string | null> {
|
|
283
|
+
if (f.readonly) return null
|
|
284
|
+
const state = fieldState(f, values)
|
|
285
|
+
if (!state.visible) return null
|
|
286
|
+
const value = values[f.field]
|
|
287
|
+
if (state.required && f.type !== 'boolean' && isEmpty(value)) return `${f.label} is required`
|
|
288
|
+
const builtIn = builtInError(f, value)
|
|
289
|
+
if (builtIn) return builtIn
|
|
290
|
+
if (!isEmpty(value)) return (await validateField(f, value)) ?? null
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validate a single field, for checking as the user leaves it rather than making
|
|
296
|
+
* them submit to find out. Includes the schema-level rules that name this field,
|
|
297
|
+
* so a cross-field rule surfaces on the field it blames.
|
|
298
|
+
*/
|
|
299
|
+
export async function validateOne<TData extends RowData>(
|
|
300
|
+
schema: EntitySchema<TData>,
|
|
301
|
+
field: string,
|
|
302
|
+
values: Record<string, unknown>,
|
|
303
|
+
): Promise<string | null> {
|
|
304
|
+
const descriptor = schemaToFormFields(schema).find((f) => f.field === field)
|
|
305
|
+
if (!descriptor) return null
|
|
306
|
+
const own = await fieldError(descriptor, values)
|
|
307
|
+
if (own) return own
|
|
308
|
+
if (schema.validations?.length) {
|
|
309
|
+
const byRule = evalValidationRules(schema.validations, values)[field]
|
|
310
|
+
if (byRule) return byRule
|
|
311
|
+
}
|
|
312
|
+
return (await validateEntity(schema, values as Partial<TData>))[field] ?? null
|
|
313
|
+
}
|
|
314
|
+
|
|
159
315
|
/**
|
|
160
316
|
* Validate every editable field: required-but-empty first, then the field's
|
|
161
317
|
* Standard Schema validator (if any), then the schema-level cross-field
|
|
@@ -169,23 +325,15 @@ export async function validateAll<TData extends RowData>(
|
|
|
169
325
|
): Promise<Record<string, string>> {
|
|
170
326
|
const errors: Record<string, string> = {}
|
|
171
327
|
for (const f of schemaToFormFields(schema)) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (builtIn) {
|
|
180
|
-
errors[f.field] = builtIn
|
|
181
|
-
continue
|
|
182
|
-
}
|
|
183
|
-
if (!isEmpty(value)) {
|
|
184
|
-
const msg = await validateField(f, value)
|
|
185
|
-
if (msg) errors[f.field] = msg
|
|
328
|
+
const msg = await fieldError(f, values)
|
|
329
|
+
if (msg) errors[f.field] = msg
|
|
330
|
+
}
|
|
331
|
+
// No-code rules, then the cross-field hook. Neither overwrites a per-field error.
|
|
332
|
+
if (schema.validations?.length) {
|
|
333
|
+
for (const [field, message] of Object.entries(evalValidationRules(schema.validations, values))) {
|
|
334
|
+
if (!errors[field]) errors[field] = message
|
|
186
335
|
}
|
|
187
336
|
}
|
|
188
|
-
// Cross-field rules run last, and never overwrite a per-field error.
|
|
189
337
|
const crossField = await validateEntity(schema, values as Partial<TData>)
|
|
190
338
|
for (const [field, message] of Object.entries(crossField)) {
|
|
191
339
|
if (!errors[field]) errors[field] = message
|
|
@@ -216,6 +364,9 @@ export function toSubmitValues<TData extends RowData>(
|
|
|
216
364
|
const out: Record<string, unknown> = {}
|
|
217
365
|
for (const f of schemaToFormFields(schema)) {
|
|
218
366
|
if (f.readonly) continue
|
|
367
|
+
// A field hidden by its `when.visible` condition is left out entirely, so a
|
|
368
|
+
// value the user can no longer see is never written back.
|
|
369
|
+
if (!fieldState(f, values).visible) continue
|
|
219
370
|
out[f.field] = coerceOut(f, values[f.field])
|
|
220
371
|
}
|
|
221
372
|
return out as Partial<TData>
|
package/src/expressions/parse.ts
CHANGED
|
@@ -28,13 +28,13 @@ import type {
|
|
|
28
28
|
ComparisonOp,
|
|
29
29
|
PredicateExpr,
|
|
30
30
|
ScalarExpr,
|
|
31
|
-
} from './expression-types'
|
|
31
|
+
} from './expression-types.js'
|
|
32
32
|
import {
|
|
33
33
|
columnRefText,
|
|
34
34
|
resolveColumnRef,
|
|
35
35
|
type ExprColumn,
|
|
36
|
-
} from './expression-columns'
|
|
37
|
-
import { isBuiltinFunction } from './evaluate'
|
|
36
|
+
} from './expression-columns.js'
|
|
37
|
+
import { isBuiltinFunction } from './evaluate.js'
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
// Tokenizer
|
package/src/index.ts
CHANGED
|
@@ -181,6 +181,8 @@ export {
|
|
|
181
181
|
type EntityFieldType,
|
|
182
182
|
type EntityHooks,
|
|
183
183
|
type FormFieldDescriptor,
|
|
184
|
+
type FormLayout,
|
|
185
|
+
type FormSection,
|
|
184
186
|
type StudioEditorType,
|
|
185
187
|
type StandardSchemaV1,
|
|
186
188
|
type StandardSchemaResult,
|
|
@@ -274,11 +276,17 @@ export {
|
|
|
274
276
|
buildInitialValues,
|
|
275
277
|
controlKind,
|
|
276
278
|
editMode,
|
|
279
|
+
evalValidationRules,
|
|
280
|
+
fieldState,
|
|
281
|
+
hasFieldConditions,
|
|
277
282
|
rowId,
|
|
283
|
+
stripHiddenValues,
|
|
278
284
|
toSubmitValues,
|
|
279
285
|
validateAll,
|
|
286
|
+
visibleFormFields,
|
|
280
287
|
type EditMode,
|
|
281
288
|
type ControlKind,
|
|
289
|
+
type FieldState,
|
|
282
290
|
} from './edit-panel'
|
|
283
291
|
export { default as SvGridEditPanel } from './SvGridEditPanel.svelte'
|
|
284
292
|
export { default as SvLookupInput } from './SvLookupInput.svelte'
|
package/src/license-core.ts
CHANGED
|
@@ -22,14 +22,86 @@ export type LicenseInfo = {
|
|
|
22
22
|
status: LicenseStatus
|
|
23
23
|
/** True when the key permits use (dev / eval / licensed). */
|
|
24
24
|
valid: boolean
|
|
25
|
+
/** Expiry encoded in the key, when it carries one. Undefined otherwise. */
|
|
26
|
+
expiresAt?: Date
|
|
27
|
+
/**
|
|
28
|
+
* True once `expiresAt` has passed. Deliberately does NOT clear `valid`:
|
|
29
|
+
* an expired trial keeps working and the callers nudge instead. Turning a
|
|
30
|
+
* lapsed key into a hard stop would break a running app on a date boundary,
|
|
31
|
+
* which is exactly what a soft gate exists to avoid.
|
|
32
|
+
*/
|
|
33
|
+
expired?: boolean
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Pull the expiry out of a key.
|
|
38
|
+
*
|
|
39
|
+
* Keys are `SVENTERPRISE[-KIND]-<SLUG>-<SEATS>-<DATE>-<RANDOM>`, so the date is
|
|
40
|
+
* always the second-to-last segment regardless of whether a KIND is present.
|
|
41
|
+
* Two widths are accepted:
|
|
42
|
+
*
|
|
43
|
+
* YYYYMM - subscription granularity; expires at the end of that month.
|
|
44
|
+
* YYYYMMDD - trial granularity; expires at the end of that day.
|
|
45
|
+
*
|
|
46
|
+
* The 6-digit form came first, so every key issued before day-precision trials
|
|
47
|
+
* existed still parses. Anything else (including the older keys whose slug sits
|
|
48
|
+
* in that position) yields null, which reads as "no expiry encoded".
|
|
49
|
+
*
|
|
50
|
+
* Boundaries are UTC so a key does not lapse at a different instant per
|
|
51
|
+
* timezone. Kept in sync with `tools/issue-license.mjs`.
|
|
52
|
+
*/
|
|
53
|
+
export function parseLicenseExpiry(key: string | null | undefined): Date | null {
|
|
54
|
+
if (!key) return null
|
|
55
|
+
const segments = key.split('-')
|
|
56
|
+
if (segments.length < 3) return null
|
|
57
|
+
const stamp = segments[segments.length - 2]
|
|
58
|
+
if (stamp == null) return null
|
|
59
|
+
|
|
60
|
+
if (/^\d{6}$/.test(stamp)) {
|
|
61
|
+
const year = Number(stamp.slice(0, 4))
|
|
62
|
+
const month = Number(stamp.slice(4, 6))
|
|
63
|
+
if (month < 1 || month > 12) return null
|
|
64
|
+
// Date.UTC's month is 0-based, so passing the 1-based month lands on the
|
|
65
|
+
// first of the FOLLOWING month; one millisecond earlier is the last instant
|
|
66
|
+
// of the month the key names.
|
|
67
|
+
return new Date(Date.UTC(year, month, 1) - 1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (/^\d{8}$/.test(stamp)) {
|
|
71
|
+
const year = Number(stamp.slice(0, 4))
|
|
72
|
+
const month = Number(stamp.slice(4, 6))
|
|
73
|
+
const day = Number(stamp.slice(6, 8))
|
|
74
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return null
|
|
75
|
+
const end = new Date(Date.UTC(year, month - 1, day, 23, 59, 59, 999))
|
|
76
|
+
// Reject a date that rolled over (e.g. 20260231), which would otherwise
|
|
77
|
+
// silently grant a few extra days.
|
|
78
|
+
if (end.getUTCMonth() !== month - 1) return null
|
|
79
|
+
return end
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Classify a license key string. Pure and side-effect-free.
|
|
86
|
+
* `now` is injectable so expiry behaviour is testable without faking clocks. */
|
|
87
|
+
export function checkLicenseKey(
|
|
88
|
+
key: string | null | undefined,
|
|
89
|
+
now: Date = new Date(),
|
|
90
|
+
): LicenseInfo {
|
|
29
91
|
if (key == null || key === '') return { status: 'unset', valid: false }
|
|
30
92
|
if (!key.startsWith(VALID_PREFIX)) return { status: 'invalid', valid: false }
|
|
31
93
|
if (REVOKED_KEYS.has(key)) return { status: 'revoked', valid: false }
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
94
|
+
|
|
95
|
+
const expiresAt = parseLicenseExpiry(key) ?? undefined
|
|
96
|
+
// Left undefined rather than false when the key encodes no date, so callers
|
|
97
|
+
// can tell "not expired" apart from "no expiry to check".
|
|
98
|
+
const expired = expiresAt ? now.getTime() > expiresAt.getTime() : undefined
|
|
99
|
+
|
|
100
|
+
const status: LicenseStatus = key.startsWith('SVENTERPRISE-DEV')
|
|
101
|
+
? 'dev'
|
|
102
|
+
: key.startsWith('SVENTERPRISE-EVAL')
|
|
103
|
+
? 'eval'
|
|
104
|
+
: 'licensed'
|
|
105
|
+
|
|
106
|
+
return { status, valid: true, expiresAt, expired }
|
|
35
107
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { checkLicenseKey, parseLicenseExpiry } from './license-core'
|
|
4
|
+
import {
|
|
5
|
+
assertEnterpriseLicensed,
|
|
6
|
+
clearLicenseKey,
|
|
7
|
+
getLicenseExpiry,
|
|
8
|
+
isLicenseExpired,
|
|
9
|
+
setLicenseKey,
|
|
10
|
+
} from './license'
|
|
11
|
+
import { dismissUpgradePrompt } from './upgrade-prompt'
|
|
12
|
+
|
|
13
|
+
const KEY = 'SVENTERPRISE-EVAL-GUNEI-5-20260904-RGY2TN'
|
|
14
|
+
const CARD = '[data-svgrid-enterprise-upgrade]'
|
|
15
|
+
|
|
16
|
+
describe('trial key expiry parsing', () => {
|
|
17
|
+
it('unlocks Enterprise today', () => {
|
|
18
|
+
const info = checkLicenseKey(KEY, new Date('2026-08-21T12:00:00Z'))
|
|
19
|
+
expect(info).toMatchObject({ status: 'eval', valid: true, expired: false })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('expires end of 2026-09-04, not end of September', () => {
|
|
23
|
+
expect(parseLicenseExpiry(KEY)?.toISOString()).toBe('2026-09-04T23:59:59.999Z')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('still valid on the final day', () => {
|
|
27
|
+
expect(checkLicenseKey(KEY, new Date('2026-09-04T23:00:00Z')).expired).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('expired the next day, but still permitted', () => {
|
|
31
|
+
const info = checkLicenseKey(KEY, new Date('2026-09-05T00:30:00Z'))
|
|
32
|
+
expect(info.expired).toBe(true)
|
|
33
|
+
expect(info.valid).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('keeps parsing month-granularity keys issued before day precision', () => {
|
|
37
|
+
expect(parseLicenseExpiry('SVENTERPRISE-ACME-5-202705-ABC123')?.toISOString()).toBe(
|
|
38
|
+
'2027-05-31T23:59:59.999Z',
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('a key with no encoded date has no expiry', () => {
|
|
43
|
+
const info = checkLicenseKey('SVENTERPRISE-EVAL-acme-2026')
|
|
44
|
+
expect(info.expiresAt).toBeUndefined()
|
|
45
|
+
expect(info.expired).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('what happens after the trial ends', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.useRealTimers()
|
|
52
|
+
clearLicenseKey()
|
|
53
|
+
dismissUpgradePrompt()
|
|
54
|
+
document.body.innerHTML = ''
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('during the trial: no card, and nothing thrown', () => {
|
|
58
|
+
vi.useFakeTimers({ now: new Date('2026-08-25T10:00:00Z') })
|
|
59
|
+
setLicenseKey(KEY)
|
|
60
|
+
expect(isLicenseExpired()).toBe(false)
|
|
61
|
+
expect(() => assertEnterpriseLicensed('Export')).not.toThrow()
|
|
62
|
+
expect(document.querySelector(CARD)).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('after the trial: pops the message, and STILL does not throw', () => {
|
|
66
|
+
vi.useFakeTimers({ now: new Date('2026-09-20T10:00:00Z') })
|
|
67
|
+
setLicenseKey(KEY)
|
|
68
|
+
expect(isLicenseExpired()).toBe(true)
|
|
69
|
+
|
|
70
|
+
expect(() => assertEnterpriseLicensed('Export')).not.toThrow()
|
|
71
|
+
|
|
72
|
+
const card = document.querySelector(CARD)
|
|
73
|
+
expect(card).not.toBeNull()
|
|
74
|
+
expect(card!.textContent).toContain('Your trial has ended')
|
|
75
|
+
expect(card!.textContent).toContain('everything still works')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('exposes the expiry date so a host can render its own banner', () => {
|
|
79
|
+
vi.useFakeTimers({ now: new Date('2026-09-20T10:00:00Z') })
|
|
80
|
+
setLicenseKey(KEY)
|
|
81
|
+
expect(getLicenseExpiry()?.toISOString()).toBe('2026-09-04T23:59:59.999Z')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('a paid key never shows the card', () => {
|
|
85
|
+
vi.useFakeTimers({ now: new Date('2026-09-20T10:00:00Z') })
|
|
86
|
+
setLicenseKey('SVENTERPRISE-ACME-5-202705-ABC123')
|
|
87
|
+
expect(isLicenseExpired()).toBe(false)
|
|
88
|
+
assertEnterpriseLicensed('Export')
|
|
89
|
+
expect(document.querySelector(CARD)).toBeNull()
|
|
90
|
+
})
|
|
91
|
+
})
|