@frigear-nu/forms 0.0.6 → 0.0.8

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.
Files changed (185) hide show
  1. package/README.md +16 -1
  2. package/dist/module.d.mts +148 -8
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +65 -35
  5. package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
  6. package/dist/runtime/app/components/FormBuilder.vue +220 -0
  7. package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
  8. package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
  9. package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
  10. package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
  11. package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
  12. package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
  13. package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
  14. package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
  15. package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
  16. package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
  17. package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
  18. package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
  19. package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
  20. package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
  21. package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
  22. package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
  23. package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
  24. package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
  25. package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
  26. package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
  27. package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
  28. package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
  29. package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
  30. package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
  31. package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
  32. package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
  33. package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
  34. package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
  35. package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
  36. package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
  37. package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
  38. package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
  39. package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
  40. package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
  41. package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
  42. package/dist/runtime/app/components/FormBuilderText.vue +127 -0
  43. package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
  44. package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
  45. package/dist/runtime/app/components/FormCaptcha.vue +95 -0
  46. package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
  47. package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
  48. package/dist/runtime/app/components/FormEmbed.vue +113 -0
  49. package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
  50. package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
  51. package/dist/runtime/app/components/FormField.vue +35 -3
  52. package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
  53. package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
  54. package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
  55. package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
  56. package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
  57. package/dist/runtime/app/components/FormPreview.vue +244 -0
  58. package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
  59. package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
  60. package/dist/runtime/app/components/FormRenderer.vue +16 -7
  61. package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
  62. package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
  63. package/dist/runtime/app/composables/index.d.ts +4 -1
  64. package/dist/runtime/app/composables/index.js +4 -1
  65. package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
  66. package/dist/runtime/app/composables/useCaptcha.js +39 -0
  67. package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
  68. package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
  69. package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
  70. package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
  71. package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
  72. package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
  73. package/dist/runtime/app/composables/useForms.d.ts +18 -3
  74. package/dist/runtime/app/composables/useForms.js +38 -5
  75. package/dist/runtime/app/pages/admin-form.vue +112 -69
  76. package/dist/runtime/app/pages/admin-submission.vue +7 -16
  77. package/dist/runtime/app/pages/admin-submissions.vue +41 -46
  78. package/dist/runtime/app/pages/admin.vue +128 -44
  79. package/dist/runtime/app/pages/form.vue +71 -5
  80. package/dist/runtime/i18n/lang/da.json +231 -3
  81. package/dist/runtime/i18n/lang/en.json +231 -3
  82. package/dist/runtime/menus.js +10 -0
  83. package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
  84. package/dist/runtime/server/db/schema.sqlite.js +33 -1
  85. package/dist/runtime/server/mcp/schemas.d.ts +9 -0
  86. package/dist/runtime/server/mcp/schemas.js +8 -0
  87. package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
  88. package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
  89. package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
  90. package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
  91. package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
  92. package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
  93. package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
  94. package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
  95. package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
  96. package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
  97. package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
  98. package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
  99. package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
  100. package/dist/runtime/server/mcp/tools/get-form.js +12 -0
  101. package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
  102. package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
  103. package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
  104. package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
  105. package/dist/runtime/server/plugins/email-delivery.js +16 -11
  106. package/dist/runtime/server/plugins/encryption.d.ts +2 -0
  107. package/dist/runtime/server/plugins/encryption.js +4 -0
  108. package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
  109. package/dist/runtime/server/plugins/staff-scopes.js +1 -0
  110. package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
  111. package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
  112. package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
  113. package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
  114. package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
  115. package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
  116. package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
  117. package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
  118. package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
  119. package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
  120. package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
  121. package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
  122. package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
  123. package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
  124. package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
  125. package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
  126. package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
  127. package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
  128. package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
  129. package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
  130. package/dist/runtime/server/routes/admin/form.get.js +5 -1
  131. package/dist/runtime/server/routes/admin/form.patch.js +3 -12
  132. package/dist/runtime/server/routes/admin/forms.post.js +1 -6
  133. package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
  134. package/dist/runtime/server/routes/public/access.get.js +3 -0
  135. package/dist/runtime/server/routes/public/form.get.js +3 -1
  136. package/dist/runtime/server/routes/public/submit.post.js +2 -2
  137. package/dist/runtime/server/utils/access.d.ts +27 -0
  138. package/dist/runtime/server/utils/access.js +63 -0
  139. package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
  140. package/dist/runtime/server/utils/auth.d.ts +7 -7
  141. package/dist/runtime/server/utils/auth.js +4 -4
  142. package/dist/runtime/server/utils/captcha.d.ts +10 -0
  143. package/dist/runtime/server/utils/captcha.js +25 -0
  144. package/dist/runtime/server/utils/crypto.d.ts +18 -0
  145. package/dist/runtime/server/utils/crypto.js +48 -0
  146. package/dist/runtime/server/utils/forms.d.ts +6 -1
  147. package/dist/runtime/server/utils/forms.js +28 -0
  148. package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
  149. package/dist/runtime/server/utils/mcp-access.js +8 -0
  150. package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
  151. package/dist/runtime/server/utils/rate-limit.js +19 -0
  152. package/dist/runtime/server/utils/render.d.ts +2 -11
  153. package/dist/runtime/server/utils/render.js +1 -26
  154. package/dist/runtime/server/utils/secrets.d.ts +28 -0
  155. package/dist/runtime/server/utils/secrets.js +60 -0
  156. package/dist/runtime/server/utils/submit.d.ts +12 -1
  157. package/dist/runtime/server/utils/submit.js +18 -4
  158. package/dist/runtime/utils/builder.d.ts +73 -0
  159. package/dist/runtime/utils/builder.js +220 -0
  160. package/dist/runtime/utils/captcha.d.ts +9 -0
  161. package/dist/runtime/utils/captcha.js +7 -0
  162. package/dist/runtime/utils/conditions.d.ts +3 -1
  163. package/dist/runtime/utils/conditions.js +4 -0
  164. package/dist/runtime/utils/definition.d.ts +116 -6
  165. package/dist/runtime/utils/definition.js +53 -4
  166. package/dist/runtime/utils/embed.d.ts +8 -0
  167. package/dist/runtime/utils/embed.js +7 -0
  168. package/dist/runtime/utils/index.d.ts +16 -3
  169. package/dist/runtime/utils/index.js +66 -3
  170. package/dist/runtime/utils/markdown.d.ts +1 -5
  171. package/dist/runtime/utils/markdown.js +1 -5
  172. package/dist/runtime/utils/phone.d.ts +29 -0
  173. package/dist/runtime/utils/phone.js +294 -0
  174. package/dist/runtime/utils/preview.d.ts +12 -0
  175. package/dist/runtime/utils/preview.js +17 -0
  176. package/dist/runtime/utils/schedule.d.ts +1 -0
  177. package/dist/runtime/utils/schedule.js +1 -0
  178. package/dist/runtime/utils/schema.d.ts +2 -0
  179. package/dist/runtime/utils/schema.js +12 -4
  180. package/dist/runtime/utils/template.d.ts +57 -0
  181. package/dist/runtime/utils/template.js +147 -0
  182. package/dist/runtime/utils/text.d.ts +0 -3
  183. package/dist/runtime/utils/text.js +0 -5
  184. package/dist/types.d.mts +5 -1
  185. package/package.json +13 -5
@@ -0,0 +1,49 @@
1
+ import type { FormDefinition, FormField, FormStep, FieldType } from '../../../types.js';
2
+ import type { FieldPosition } from '../../utils/index.js';
3
+ export type BuilderSelection = {
4
+ kind: 'field';
5
+ at: FieldPosition;
6
+ } | {
7
+ kind: 'step';
8
+ index: number;
9
+ };
10
+ /**
11
+ * State and edit actions of the form builder: what is selected, drag and drop reordering and every change to
12
+ * the definition. All edits go through the `definition` model, so the builder stays a pure v-model component.
13
+ */
14
+ export declare function useFormBuilderEditor(definition: Ref<FormDefinition>): {
15
+ selection: any;
16
+ selectedField: any;
17
+ selectedStep: any;
18
+ isSelected: (step: number, index: number) => boolean;
19
+ isStepSelected: (index: number) => boolean;
20
+ selectField: (at: FieldPosition) => void;
21
+ selectStep: (index: number) => void;
22
+ dragged: any;
23
+ slot: any;
24
+ startFieldDrag: (event: DragEvent, from: FieldPosition) => void;
25
+ startStepDrag: (event: DragEvent, index: number) => void;
26
+ endDrag: () => void;
27
+ overField: (event: DragEvent, step: number, index: number) => void;
28
+ overStepBody: (event: DragEvent, step: number) => void;
29
+ overStep: (event: DragEvent, index: number) => void;
30
+ overStepSlot: (event: DragEvent, index: number) => void;
31
+ drop: (event: DragEvent) => void;
32
+ isFieldDragged: (step: number, index: number) => boolean;
33
+ isStepDragged: (index: number) => boolean;
34
+ showLine: (step: number, index: number) => boolean;
35
+ showStepLine: (index: number) => boolean;
36
+ addField: (type: FieldType, at: FieldPosition) => void;
37
+ addStep: (index?: any) => void;
38
+ removeAtField: (at: FieldPosition) => void;
39
+ copyField: (at: FieldPosition) => void;
40
+ nudge: (at: FieldPosition, delta: -1 | 1) => void;
41
+ nudgeStep: (index: number, delta: -1 | 1) => void;
42
+ deleteStep: (index: number) => void;
43
+ patchField: (patch: Partial<FormField>) => void;
44
+ renameKey: (key: string) => void;
45
+ patchStep: (patch: Partial<FormStep>) => void;
46
+ moveToStep: (step: number) => void;
47
+ setMode: (mode: FormDefinition["mode"]) => Promise<void>;
48
+ conditionFields: (step: number, index: number) => any;
49
+ };
@@ -0,0 +1,210 @@
1
+ import {
2
+ changeMode,
3
+ createField,
4
+ createStep,
5
+ dropIndex,
6
+ duplicateField,
7
+ insertField,
8
+ insertStep,
9
+ moveField,
10
+ moveStep,
11
+ removeField,
12
+ removeStep,
13
+ renameFieldKey,
14
+ updateField,
15
+ updateStep
16
+ } from "../../utils/index.js";
17
+ import { useConfirmDialog } from "@frigear-nu/ui/composables";
18
+ export function useFormBuilderEditor(definition) {
19
+ const t = useModuleT("forms");
20
+ const confirm = useConfirmDialog();
21
+ const selection = ref();
22
+ const selectedField = computed(() => selection.value?.kind === "field" ? definition.value.steps[selection.value.at.step]?.fields[selection.value.at.index] : void 0);
23
+ const selectedStep = computed(() => selection.value?.kind === "step" ? definition.value.steps[selection.value.index] : void 0);
24
+ watch(definition, () => {
25
+ if (selection.value && !selectedField.value && !selectedStep.value) selection.value = void 0;
26
+ });
27
+ const isSelected = (step, index) => selection.value?.kind === "field" && selection.value.at.step === step && selection.value.at.index === index;
28
+ const isStepSelected = (index) => selection.value?.kind === "step" && selection.value.index === index;
29
+ const selectField = (at) => {
30
+ selection.value = { kind: "field", at };
31
+ };
32
+ const selectStep = (index) => {
33
+ selection.value = { kind: "step", index };
34
+ };
35
+ const dragged = ref();
36
+ const slot = ref();
37
+ function startFieldDrag(event, from) {
38
+ startDrag(event, { kind: "field", from });
39
+ }
40
+ function startStepDrag(event, index) {
41
+ startDrag(event, { kind: "step", index });
42
+ }
43
+ function startDrag(event, payload) {
44
+ dragged.value = payload;
45
+ if (!event.dataTransfer) return;
46
+ event.dataTransfer.effectAllowed = "move";
47
+ event.dataTransfer.setData("text/plain", payload.kind);
48
+ }
49
+ function endDrag() {
50
+ dragged.value = void 0;
51
+ slot.value = void 0;
52
+ }
53
+ const lowerHalf = (event) => {
54
+ const rect = event.currentTarget.getBoundingClientRect();
55
+ return event.clientY > rect.top + rect.height / 2;
56
+ };
57
+ function overField(event, step, index) {
58
+ if (!dragged.value || dragged.value.kind === "step") return;
59
+ event.preventDefault();
60
+ event.stopPropagation();
61
+ slot.value = { kind: "field", step, index: index + (lowerHalf(event) ? 1 : 0) };
62
+ }
63
+ function overStepBody(event, step) {
64
+ if (!dragged.value || dragged.value.kind === "step") return;
65
+ event.preventDefault();
66
+ if (slot.value?.kind !== "field" || slot.value.step !== step) {
67
+ slot.value = { kind: "field", step, index: definition.value.steps[step]?.fields.length ?? 0 };
68
+ }
69
+ }
70
+ function overStep(event, index) {
71
+ if (dragged.value?.kind !== "step") return;
72
+ event.preventDefault();
73
+ slot.value = { kind: "step", index: index + (lowerHalf(event) ? 1 : 0) };
74
+ }
75
+ function overStepSlot(event, index) {
76
+ if (dragged.value?.kind !== "step") return;
77
+ event.preventDefault();
78
+ slot.value = { kind: "step", index };
79
+ }
80
+ function drop(event) {
81
+ event.preventDefault();
82
+ const from = dragged.value;
83
+ const to = slot.value;
84
+ endDrag();
85
+ if (!from || !to) return;
86
+ if (from.kind === "step" && to.kind === "step") {
87
+ definition.value = moveStep(definition.value, from.index, from.index < to.index ? to.index - 1 : to.index);
88
+ selection.value = void 0;
89
+ } else if (to.kind === "field" && from.kind === "field") {
90
+ const target = { step: to.step, index: dropIndex(from.from, to) };
91
+ definition.value = moveField(definition.value, from.from, target);
92
+ selection.value = { kind: "field", at: target };
93
+ }
94
+ }
95
+ const isFieldDragged = (step, index) => dragged.value?.kind === "field" && dragged.value.from.step === step && dragged.value.from.index === index;
96
+ const isStepDragged = (index) => dragged.value?.kind === "step" && dragged.value.index === index;
97
+ const showLine = (step, index) => slot.value?.kind === "field" && slot.value.step === step && slot.value.index === index;
98
+ const showStepLine = (index) => slot.value?.kind === "step" && slot.value.index === index;
99
+ function addField(type, at) {
100
+ const field = createField(definition.value, type);
101
+ const next = insertField(definition.value, at, field);
102
+ definition.value = next;
103
+ const index = next.steps[at.step]?.fields.findIndex((candidate) => candidate.key === field.key) ?? -1;
104
+ if (index >= 0) selection.value = { kind: "field", at: { step: at.step, index } };
105
+ }
106
+ function addStep(index = definition.value.steps.length) {
107
+ const next = insertStep(definition.value, index, createStep(definition.value));
108
+ definition.value = next;
109
+ selection.value = { kind: "step", index: Math.min(index, next.steps.length - 1) };
110
+ }
111
+ function removeAtField(at) {
112
+ definition.value = removeField(definition.value, at);
113
+ selection.value = void 0;
114
+ }
115
+ function copyField(at) {
116
+ definition.value = duplicateField(definition.value, at);
117
+ selection.value = { kind: "field", at: { step: at.step, index: at.index + 1 } };
118
+ }
119
+ function nudge(at, delta) {
120
+ const steps = definition.value.steps;
121
+ let target = { step: at.step, index: at.index + delta };
122
+ if (target.index < 0) target = at.step > 0 ? { step: at.step - 1, index: steps[at.step - 1].fields.length } : at;
123
+ else if (target.index >= steps[at.step].fields.length) {
124
+ target = at.step < steps.length - 1 ? { step: at.step + 1, index: 0 } : at;
125
+ }
126
+ if (target === at) return;
127
+ definition.value = moveField(definition.value, at, target);
128
+ selection.value = { kind: "field", at: target };
129
+ }
130
+ function nudgeStep(index, delta) {
131
+ if (index + delta < 0 || index + delta >= definition.value.steps.length) return;
132
+ definition.value = moveStep(definition.value, index, index + delta);
133
+ selection.value = { kind: "step", index: index + delta };
134
+ }
135
+ function deleteStep(index) {
136
+ const hasFields = !!definition.value.steps[index]?.fields.length;
137
+ definition.value = removeStep(definition.value, index, { keepFields: hasFields });
138
+ selection.value = void 0;
139
+ }
140
+ function patchField(patch) {
141
+ if (selection.value?.kind !== "field") return;
142
+ definition.value = updateField(definition.value, selection.value.at, patch);
143
+ }
144
+ function renameKey(key) {
145
+ if (selection.value?.kind !== "field") return;
146
+ definition.value = renameFieldKey(definition.value, selection.value.at, key);
147
+ }
148
+ function patchStep(patch) {
149
+ if (selection.value?.kind !== "step") return;
150
+ definition.value = updateStep(definition.value, selection.value.index, patch);
151
+ }
152
+ function moveToStep(step) {
153
+ if (selection.value?.kind !== "field" || step === selection.value.at.step) return;
154
+ const target = { step, index: definition.value.steps[step].fields.length };
155
+ definition.value = moveField(definition.value, selection.value.at, target);
156
+ selection.value = { kind: "field", at: target };
157
+ }
158
+ async function setMode(mode) {
159
+ if (mode === definition.value.mode) return;
160
+ if (mode === "single" && definition.value.steps.length > 1) {
161
+ const ok = await confirm({
162
+ title: t("builder.singleConfirmTitle", "Switch to a single page?"),
163
+ description: t("builder.singleConfirmDescription", "All steps are merged into one page. Step titles, descriptions and step conditions are lost, and switching back to steps will not restore them."),
164
+ confirmText: t("builder.singleConfirmButton", "Switch to single page")
165
+ });
166
+ if (!ok) return;
167
+ }
168
+ definition.value = changeMode(definition.value, mode);
169
+ selection.value = void 0;
170
+ }
171
+ function conditionFields(step, index) {
172
+ return definition.value.steps.flatMap((s, si) => si > step ? [] : s.fields.slice(0, si === step ? index : void 0).filter((f) => f.type !== "markdown" && f.type !== "captcha").map((f) => f.key));
173
+ }
174
+ return {
175
+ selection,
176
+ selectedField,
177
+ selectedStep,
178
+ isSelected,
179
+ isStepSelected,
180
+ selectField,
181
+ selectStep,
182
+ dragged,
183
+ slot,
184
+ startFieldDrag,
185
+ startStepDrag,
186
+ endDrag,
187
+ overField,
188
+ overStepBody,
189
+ overStep,
190
+ overStepSlot,
191
+ drop,
192
+ isFieldDragged,
193
+ isStepDragged,
194
+ showLine,
195
+ showStepLine,
196
+ addField,
197
+ addStep,
198
+ removeAtField,
199
+ copyField,
200
+ nudge,
201
+ nudgeStep,
202
+ deleteStep,
203
+ patchField,
204
+ renameKey,
205
+ patchStep,
206
+ moveToStep,
207
+ setMode,
208
+ conditionFields
209
+ };
210
+ }
@@ -0,0 +1,8 @@
1
+ import type { FieldType, FormField } from '../../../types.js';
2
+ /** Icon per field type, shown in the builder canvas and the insertion menus. */
3
+ export declare const fieldTypeIcons: Record<FieldType, string>;
4
+ /** Translated field type names and the one-line canvas summary of a field. */
5
+ export declare function useFormBuilderLabels(): {
6
+ typeLabel: (type: FieldType) => any;
7
+ summary: (field: FormField) => any;
8
+ };
@@ -0,0 +1,41 @@
1
+ import { translate } from "../../utils/index.js";
2
+ import { useFormsLocale } from "./useForms.js";
3
+ export const fieldTypeIcons = {
4
+ text: "i-lucide-text-cursor-input",
5
+ textarea: "i-lucide-align-left",
6
+ email: "i-lucide-mail",
7
+ tel: "i-lucide-phone",
8
+ number: "i-lucide-hash",
9
+ date: "i-lucide-calendar",
10
+ select: "i-lucide-list",
11
+ radio: "i-lucide-circle-dot",
12
+ checkbox: "i-lucide-square-check",
13
+ file: "i-lucide-paperclip",
14
+ markdown: "i-lucide-type",
15
+ captcha: "i-lucide-shield-check"
16
+ };
17
+ const fieldTypeNames = {
18
+ text: "Short text",
19
+ textarea: "Long text",
20
+ email: "Email",
21
+ tel: "Phone",
22
+ number: "Number",
23
+ date: "Date",
24
+ select: "Dropdown",
25
+ radio: "Single choice",
26
+ checkbox: "Checkbox",
27
+ file: "File upload",
28
+ markdown: "Text block",
29
+ captcha: "Captcha"
30
+ };
31
+ export function useFormBuilderLabels() {
32
+ const t = useModuleT("forms");
33
+ const locale = useFormsLocale();
34
+ const typeLabel = (type) => t(`builder.types.${type}`, fieldTypeNames[type]);
35
+ const summary = (field) => {
36
+ const text = translate(field.type === "markdown" ? field.content : field.label, locale.value) || (field.type === "captcha" ? typeLabel("captcha") : field.key);
37
+ const line = text.replace(/[*_`#>~[]|\]\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
38
+ return line.length > 80 ? `${line.slice(0, 80).trimEnd()}\u2026` : line;
39
+ };
40
+ return { typeLabel, summary };
41
+ }
@@ -1,12 +1,21 @@
1
1
  import type { FormDefinition, FormField } from '../../../types.js';
2
2
  /** Locale for translated form content: `@nuxtjs/i18n`'s when installed, otherwise `en`. */
3
3
  export declare function useFormsLocale(): any;
4
- /** Loads a published form definition by slug. */
5
- export declare function useForm(slug: MaybeRefOrGetter<string>): any;
4
+ /** Locales content can be translated into: `@nuxtjs/i18n`'s configured locales when installed, otherwise just `en`. */
5
+ export declare function useFormsLocales(): any;
6
+ /** Header carrying the shared password of a password-protected form. */
7
+ export declare const FORM_PASSWORD_HEADER = "x-form-password";
8
+ /** Loads a published form definition by slug; `password` unlocks a password-protected form. */
9
+ export declare function useForm(slug: MaybeRefOrGetter<string>, password?: MaybeRefOrGetter<string | undefined>): any;
10
+ /** Loads what the public page needs to choose between the form, a password/sign-in gate and "not found". */
11
+ export declare function useFormAccess(slug: MaybeRefOrGetter<string>): any;
6
12
  /** Initial answers: empty arrays for files, `false` for checkboxes, everything else unset. */
7
13
  export declare function emptyAnswers(definition: Pick<FormDefinition, 'steps'>): Record<string, unknown>;
8
14
  /** Posts the answers: JSON for forms without files, multipart (answers + `file:<key>` parts) otherwise. */
9
- export declare function submitAnswers(slug: string, definition: Pick<FormDefinition, 'steps'>, answers: Record<string, unknown>, locale: string): Promise<any>;
15
+ export declare function submitAnswers(slug: string, definition: Pick<FormDefinition, 'steps'>, answers: Record<string, unknown>, locale: string, password?: string): Promise<{
16
+ ok: true;
17
+ id: string | null;
18
+ }>;
10
19
  declare const DEFAULT_PATHS: {
11
20
  readonly form: "/forms/:slug";
12
21
  readonly admin: "/admin/forms";
@@ -16,6 +25,12 @@ declare const DEFAULT_PATHS: {
16
25
  };
17
26
  /** Resolves the (configurable) page paths, filling in params. */
18
27
  export declare function useFormsPaths(): (name: keyof typeof DEFAULT_PATHS, params?: Record<string, string>) => string;
28
+ /** Absolute public address of a form and a clipboard copier; `enabled` is false when the form page route is off. */
29
+ export declare function useFormLink(): {
30
+ enabled: any;
31
+ url: (slug: string) => string;
32
+ copy: (slug: string) => Promise<void>;
33
+ };
19
34
  /** Label of a field in the given locale, falling back to its key. */
20
35
  export declare const fieldLabel: (field: FormField, locale: string) => string;
21
36
  export {};
@@ -3,8 +3,22 @@ export function useFormsLocale() {
3
3
  const nuxtApp = useNuxtApp();
4
4
  return computed(() => nuxtApp.$i18n?.locale?.value ?? "en");
5
5
  }
6
- export function useForm(slug) {
7
- return useFetch(() => `/api/forms/${toValue(slug)}`, { key: `forms:${toValue(slug)}` });
6
+ export function useFormsLocales() {
7
+ const nuxtApp = useNuxtApp();
8
+ return computed(() => {
9
+ const codes = (nuxtApp.$i18n?.locales?.value ?? []).map((locale) => typeof locale === "string" ? locale : locale.code);
10
+ return codes.length ? codes : ["en"];
11
+ });
12
+ }
13
+ export const FORM_PASSWORD_HEADER = "x-form-password";
14
+ export function useForm(slug, password) {
15
+ return useFetch(() => `/api/forms/${toValue(slug)}`, {
16
+ key: `forms:${toValue(slug)}${toValue(password) ? ":pw" : ""}`,
17
+ headers: computed(() => toValue(password) ? { [FORM_PASSWORD_HEADER]: toValue(password) } : void 0)
18
+ });
19
+ }
20
+ export function useFormAccess(slug) {
21
+ return useFetch(() => `/api/forms/${toValue(slug)}/access`, { key: `forms:access:${toValue(slug)}` });
8
22
  }
9
23
  export function emptyAnswers(definition) {
10
24
  const state = {};
@@ -17,7 +31,8 @@ export function emptyAnswers(definition) {
17
31
  }
18
32
  return state;
19
33
  }
20
- export async function submitAnswers(slug, definition, answers, locale) {
34
+ export async function submitAnswers(slug, definition, answers, locale, password) {
35
+ const headers = password ? { [FORM_PASSWORD_HEADER]: password } : void 0;
21
36
  const files = [];
22
37
  const plain = {};
23
38
  const visible = new Set(resolveVisibility(definition, answers).flatMap((step) => step.fields.map((field) => field.key)));
@@ -30,13 +45,13 @@ export async function submitAnswers(slug, definition, answers, locale) {
30
45
  plain[key] = value;
31
46
  }
32
47
  if (!files.length) {
33
- return $fetch(`/api/forms/${slug}/submit`, { method: "POST", body: { answers: plain, locale } });
48
+ return $fetch(`/api/forms/${slug}/submit`, { method: "POST", body: { answers: plain, locale }, headers });
34
49
  }
35
50
  const body = new FormData();
36
51
  body.append("answers", JSON.stringify(plain));
37
52
  body.append("locale", locale);
38
53
  for (const { key, file } of files) body.append(`file:${key}`, file, file.name);
39
- return $fetch(`/api/forms/${slug}/submit`, { method: "POST", body });
54
+ return $fetch(`/api/forms/${slug}/submit`, { method: "POST", body, headers });
40
55
  }
41
56
  const DEFAULT_PATHS = {
42
57
  form: "/forms/:slug",
@@ -49,4 +64,22 @@ export function useFormsPaths() {
49
64
  const pages = useRuntimeConfig().public.forms?.pages ?? {};
50
65
  return (name, params = {}) => Object.entries(params).reduce((path, [key, value]) => path.replace(`:${key}`, encodeURIComponent(value)), pages[name] ?? DEFAULT_PATHS[name]);
51
66
  }
67
+ export function useFormLink() {
68
+ const paths = useFormsPaths();
69
+ const origin = useRequestURL().origin;
70
+ const toast = useToast();
71
+ const t = useModuleT("forms");
72
+ const pages = useRuntimeConfig().public.forms?.pages;
73
+ const enabled = computed(() => !!pages?.form);
74
+ const url = (slug) => `${origin}${paths("form", { slug })}`;
75
+ async function copy(slug) {
76
+ try {
77
+ await navigator.clipboard.writeText(url(slug));
78
+ toast.add({ title: t("admin.linkCopied", "Link copied."), color: "success" });
79
+ } catch {
80
+ toast.add({ title: t("error.generic", "Something went wrong. Please try again."), color: "error" });
81
+ }
82
+ }
83
+ return { enabled, url, copy };
84
+ }
52
85
  export const fieldLabel = (field, locale) => translate(field.label, locale) || field.key;
@@ -4,18 +4,41 @@ const t = useModuleT("forms");
4
4
  const route = useRoute();
5
5
  const paths = useFormsPaths();
6
6
  const toast = useToast();
7
+ const link = useFormLink();
7
8
  const id = computed(() => String(route.params.id));
8
9
  const { data: form, error, refresh } = await useFetch(() => `/api/forms/admin/forms/${id.value}`);
9
10
  const draft = reactive({ name: form.value?.name ?? "", slug: form.value?.slug ?? "", status: form.value?.status ?? "draft" });
10
- const json = ref(form.value ? JSON.stringify(form.value.definition, null, 2) : "");
11
- const checked = computed(() => {
11
+ const definition = ref(structuredClone(toRaw(form.value?.definition ?? { mode: "single", storage: "db", steps: [] })));
12
+ const tab = ref("builder");
13
+ const json = ref("");
14
+ const jsonError = ref();
15
+ watch(tab, (next) => {
16
+ if (next === "json") {
17
+ json.value = JSON.stringify(definition.value, null, 2);
18
+ jsonError.value = void 0;
19
+ }
20
+ }, { immediate: true });
21
+ function onJson(text) {
22
+ json.value = text;
12
23
  try {
13
- const result = formDefinitionSchema.safeParse(JSON.parse(json.value));
14
- return result.success ? { definition: result.data, issues: [] } : { definition: void 0, issues: result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`) };
24
+ const result = formDefinitionSchema.safeParse(JSON.parse(text));
25
+ jsonError.value = void 0;
26
+ definition.value = result.success ? result.data : JSON.parse(text);
15
27
  } catch {
16
- return { definition: void 0, issues: [t("admin.invalidJson", "The JSON is not valid.")] };
28
+ jsonError.value = t("admin.invalidJson", "The JSON is not valid.");
17
29
  }
30
+ }
31
+ const checked = computed(() => {
32
+ if (jsonError.value) return { definition: void 0, issues: [jsonError.value] };
33
+ const result = formDefinitionSchema.safeParse(definition.value);
34
+ return result.success ? { definition: result.data, issues: [] } : { definition: void 0, issues: result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`) };
18
35
  });
36
+ const tabs = computed(() => [
37
+ { value: "builder", label: t("admin.builder", "Builder"), icon: "i-lucide-layout-list" },
38
+ { value: "settings", label: t("admin.settings", "Delivery & storage"), icon: "i-lucide-settings" },
39
+ { value: "access", label: t("admin.access", "Access"), icon: "i-lucide-lock" },
40
+ { value: "json", label: t("admin.json", "JSON"), icon: "i-lucide-braces" }
41
+ ]);
19
42
  const previewKey = ref(0);
20
43
  const saving = ref(false);
21
44
  const statuses = ["draft", "published", "archived"];
@@ -36,7 +59,7 @@ async function save() {
36
59
  </script>
37
60
 
38
61
  <template>
39
- <UContainer class="space-y-6 py-10">
62
+ <AdminPage>
40
63
  <UAlert
41
64
  v-if="error || !form"
42
65
  color="error"
@@ -45,21 +68,24 @@ async function save() {
45
68
  />
46
69
 
47
70
  <template v-else>
48
- <div class="flex flex-wrap items-center justify-between gap-2">
49
- <div>
50
- <NuxtLink
51
- :to="paths('admin')"
52
- class="text-sm text-muted hover:underline"
53
- >
54
- ← {{ t("admin.title", "Forms") }}
55
- </NuxtLink>
56
- <h1 class="text-3xl font-bold">
57
- {{ form.name }} <span class="text-base font-normal text-muted">v{{ form.version }}</span>
58
- </h1>
59
- </div>
60
- <div class="flex gap-2">
71
+ <AdminPageHeader
72
+ :title="form.name"
73
+ :back="{ label: t('admin.title', 'Forms'), to: paths('admin') }"
74
+ >
75
+ <template #title>
76
+ {{ form.name }} <span class="text-base font-normal text-muted">v{{ form.version }}</span>
77
+ </template>
78
+ <template #actions>
79
+ <UButton
80
+ v-if="form.status === 'published' && link.enabled.value"
81
+ color="neutral"
82
+ variant="outline"
83
+ icon="i-lucide-link"
84
+ :label="t('admin.copyLink', 'Copy link')"
85
+ @click="link.copy(form.slug)"
86
+ />
61
87
  <UButton
62
- v-if="form.definition.storage === 'db'"
88
+ v-if="form.definition.storage === 'db' || (form.submissionCount ?? 0) > 0"
63
89
  color="neutral"
64
90
  variant="outline"
65
91
  :to="paths('adminSubmissions', { id: form.id })"
@@ -71,8 +97,8 @@ async function save() {
71
97
  :label="t('admin.save', 'Save')"
72
98
  @click="save"
73
99
  />
74
- </div>
75
- </div>
100
+ </template>
101
+ </AdminPageHeader>
76
102
 
77
103
  <div class="grid gap-4 sm:grid-cols-3">
78
104
  <UFormField :label="t('admin.name', 'Name')">
@@ -96,55 +122,72 @@ async function save() {
96
122
  </UFormField>
97
123
  </div>
98
124
 
99
- <div class="grid gap-6 lg:grid-cols-2">
100
- <div class="space-y-3">
101
- <h2 class="font-semibold">
102
- {{ t("admin.definition", "Definition (JSON)") }}
103
- </h2>
104
- <UTextarea
105
- v-model="json"
106
- :rows="30"
107
- class="w-full font-mono text-xs"
125
+ <UTabs
126
+ v-model="tab"
127
+ :items="tabs"
128
+ :content="false"
129
+ class="w-full"
130
+ />
131
+
132
+ <FormBuilder
133
+ v-if="tab === 'builder'"
134
+ v-model="definition"
135
+ :form-id="form.id"
136
+ >
137
+ <template #preview>
138
+ <FormPreview
139
+ :slug="draft.slug"
140
+ :name="draft.name"
141
+ :definition="checked.definition"
142
+ :content-key="previewKey"
108
143
  />
109
- <UAlert
110
- v-if="checked.issues.length"
111
- color="error"
112
- variant="subtle"
113
- :title="t('admin.invalidDefinition', 'The definition has problems')"
114
- >
115
- <template #description>
116
- <ul class="list-disc pl-4">
117
- <li
118
- v-for="issue in checked.issues"
119
- :key="issue"
120
- >
121
- {{ issue }}
122
- </li>
123
- </ul>
124
- </template>
125
- </UAlert>
126
- </div>
144
+ </template>
145
+ </FormBuilder>
127
146
 
128
- <div class="space-y-3">
129
- <h2 class="font-semibold">
130
- {{ t("admin.preview", "Preview") }}
131
- </h2>
132
- <div class="rounded-lg border border-default p-6">
133
- <FormRenderer
134
- v-if="checked.definition"
135
- :key="previewKey + json.length"
136
- :form="{ slug: draft.slug, definition: checked.definition }"
137
- preview
138
- />
139
- <p
140
- v-else
141
- class="text-muted"
142
- >
143
- {{ t("admin.previewUnavailable", "Fix the definition to see a preview.") }}
144
- </p>
145
- </div>
146
- </div>
147
+ <FormBuilderSettings
148
+ v-else-if="tab === 'settings'"
149
+ v-model="definition"
150
+ />
151
+
152
+ <FormBuilderAccess
153
+ v-else-if="tab === 'access'"
154
+ v-model="definition"
155
+ :form-id="form.id"
156
+ :slug="form.slug"
157
+ />
158
+
159
+ <div
160
+ v-else-if="tab === 'json'"
161
+ class="space-y-3"
162
+ >
163
+ <h2 class="font-semibold">
164
+ {{ t("admin.definition", "Definition (JSON)") }}
165
+ </h2>
166
+ <UTextarea
167
+ :model-value="json"
168
+ :rows="30"
169
+ class="w-full font-mono text-xs"
170
+ @update:model-value="onJson(String($event))"
171
+ />
147
172
  </div>
173
+
174
+ <UAlert
175
+ v-if="checked.issues.length"
176
+ color="error"
177
+ variant="subtle"
178
+ :title="t('admin.invalidDefinition', 'The definition has problems')"
179
+ >
180
+ <template #description>
181
+ <ul class="list-disc pl-4">
182
+ <li
183
+ v-for="issue in checked.issues"
184
+ :key="issue"
185
+ >
186
+ {{ issue }}
187
+ </li>
188
+ </ul>
189
+ </template>
190
+ </UAlert>
148
191
  </template>
149
- </UContainer>
192
+ </AdminPage>
150
193
  </template>