@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,220 @@
1
+ <script setup>
2
+ import { fieldTypes } from "../../utils";
3
+ import { useCaptchaSecrets } from "../composables/useCaptchaSecrets";
4
+ import { useFormBuilderEditor } from "../composables/useFormBuilderEditor";
5
+ import { fieldTypeIcons, useFormBuilderLabels } from "../composables/useFormBuilderLabels";
6
+ const definition = defineModel({ type: Object, ...{ required: true } });
7
+ const props = defineProps({
8
+ formId: { type: String, required: false }
9
+ });
10
+ const t = useModuleT("forms");
11
+ const { typeLabel } = useFormBuilderLabels();
12
+ const {
13
+ selection,
14
+ selectedField,
15
+ selectedStep,
16
+ isSelected,
17
+ isStepSelected,
18
+ selectField,
19
+ selectStep,
20
+ startFieldDrag,
21
+ startStepDrag,
22
+ endDrag,
23
+ overField,
24
+ overStepBody,
25
+ overStep,
26
+ overStepSlot,
27
+ drop,
28
+ isFieldDragged,
29
+ isStepDragged,
30
+ showLine,
31
+ showStepLine,
32
+ addField,
33
+ addStep,
34
+ removeAtField,
35
+ copyField,
36
+ nudge,
37
+ nudgeStep,
38
+ deleteStep,
39
+ patchField,
40
+ renameKey,
41
+ patchStep,
42
+ moveToStep,
43
+ setMode,
44
+ conditionFields
45
+ } = useFormBuilderEditor(definition);
46
+ const secrets = useCaptchaSecrets(() => props.formId, selectedField);
47
+ const stepped = computed(() => definition.value.mode === "stepped");
48
+ const modeItems = computed(() => [
49
+ { value: "single", label: t("admin.single", "Single page"), icon: "i-lucide-file" },
50
+ { value: "stepped", label: t("admin.stepped", "Stepped"), icon: "i-lucide-layers" }
51
+ ]);
52
+ const insertItems = (at) => [fieldTypes.map((type) => ({
53
+ label: typeLabel(type),
54
+ icon: fieldTypeIcons[type],
55
+ onSelect: () => addField(type, at)
56
+ }))];
57
+ </script>
58
+
59
+ <template>
60
+ <div class="grid items-start gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,20rem)] xl:grid-cols-[minmax(0,1fr)_minmax(0,28rem)]">
61
+ <!-- Canvas -->
62
+ <div class="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-3">
63
+ <div class="flex flex-wrap items-center justify-between gap-2">
64
+ <h2 class="text-sm font-semibold">
65
+ {{ t("builder.fieldsTitle", "Form fields") }}
66
+ </h2>
67
+ <UFieldGroup size="xs">
68
+ <UButton
69
+ v-for="item in modeItems"
70
+ :key="item.value"
71
+ color="neutral"
72
+ :variant="definition.mode === item.value ? 'solid' : 'outline'"
73
+ :icon="item.icon"
74
+ :label="item.label"
75
+ :aria-label="`${t('builder.layout', 'Layout')}: ${item.label}`"
76
+ @click="setMode(item.value)"
77
+ />
78
+ </UFieldGroup>
79
+ </div>
80
+
81
+ <div
82
+ class="space-y-3"
83
+ @dragover.prevent
84
+ @drop="drop"
85
+ >
86
+ <template
87
+ v-for="(step, stepIndex) in definition.steps"
88
+ :key="step.id"
89
+ >
90
+ <FormBuilderInsertRow
91
+ v-if="stepped"
92
+ :label="t('builder.addStep', 'Add step')"
93
+ @click="addStep(stepIndex)"
94
+ @dragover="overStepSlot($event, stepIndex)"
95
+ />
96
+ <div class="min-w-0">
97
+ <div
98
+ v-if="stepped && showStepLine(stepIndex)"
99
+ class="mb-2 h-1 rounded bg-primary"
100
+ />
101
+ <section
102
+ class="rounded-lg border bg-default"
103
+ :class="[
104
+ isStepSelected(stepIndex) ? 'border-primary ring-1 ring-primary' : 'border-default',
105
+ isStepDragged(stepIndex) ? 'opacity-40' : ''
106
+ ]"
107
+ @dragover="overStep($event, stepIndex)"
108
+ >
109
+ <FormBuilderStepHeader
110
+ v-if="stepped"
111
+ :step="step"
112
+ :index="stepIndex"
113
+ :is-first="stepIndex === 0"
114
+ :is-last="stepIndex === definition.steps.length - 1"
115
+ :is-only="definition.steps.length === 1"
116
+ @select="selectStep(stepIndex)"
117
+ @dragstart="startStepDrag($event, stepIndex)"
118
+ @dragend="endDrag"
119
+ @up="nudgeStep(stepIndex, -1)"
120
+ @down="nudgeStep(stepIndex, 1)"
121
+ @remove="deleteStep(stepIndex)"
122
+ />
123
+
124
+ <FormBuilderStepSettings
125
+ v-if="stepped && selectedStep && selection?.kind === 'step' && selection.index === stepIndex"
126
+ :step="selectedStep"
127
+ :condition-fields="conditionFields(selection.index, 0)"
128
+ @patch="patchStep"
129
+ />
130
+
131
+ <div
132
+ class="min-h-12 p-2"
133
+ @dragover="overStepBody($event, stepIndex)"
134
+ >
135
+ <template
136
+ v-for="(field, fieldIndex) in step.fields"
137
+ :key="fieldIndex"
138
+ >
139
+ <FormBuilderInsertRow
140
+ dot
141
+ :items="insertItems({ step: stepIndex, index: fieldIndex })"
142
+ :aria-label="t('builder.insertField', 'Insert field here')"
143
+ :line="showLine(stepIndex, fieldIndex)"
144
+ />
145
+ <FormBuilderFieldRow
146
+ :field="field"
147
+ :selected="isSelected(stepIndex, fieldIndex)"
148
+ :dragging="isFieldDragged(stepIndex, fieldIndex)"
149
+ @select="selectField({ step: stepIndex, index: fieldIndex })"
150
+ @dragstart="startFieldDrag($event, { step: stepIndex, index: fieldIndex })"
151
+ @dragend="endDrag"
152
+ @dragover="overField($event, stepIndex, fieldIndex)"
153
+ @up="nudge({ step: stepIndex, index: fieldIndex }, -1)"
154
+ @down="nudge({ step: stepIndex, index: fieldIndex }, 1)"
155
+ @duplicate="copyField({ step: stepIndex, index: fieldIndex })"
156
+ @remove="removeAtField({ step: stepIndex, index: fieldIndex })"
157
+ />
158
+ <FormBuilderFieldSettings
159
+ v-if="selectedField && isSelected(stepIndex, fieldIndex) && selection?.kind === 'field'"
160
+ :field="selectedField"
161
+ :at="selection.at"
162
+ :steps="stepped ? definition.steps : void 0"
163
+ :condition-fields="conditionFields(selection.at.step, selection.at.index)"
164
+ :form-id="formId"
165
+ :secrets="secrets"
166
+ @patch="patchField"
167
+ @rename="renameKey"
168
+ @move-to-step="moveToStep"
169
+ />
170
+ </template>
171
+ <FormBuilderInsertRow
172
+ :dot="step.fields.length > 0"
173
+ :visible="step.fields.length === 0"
174
+ :items="insertItems({ step: stepIndex, index: step.fields.length })"
175
+ :aria-label="t('builder.insertField', 'Insert field here')"
176
+ :label="step.fields.length ? void 0 : t('builder.addField', 'Add field')"
177
+ :line="showLine(stepIndex, step.fields.length)"
178
+ />
179
+ </div>
180
+ </section>
181
+ </div>
182
+ </template>
183
+ <div
184
+ v-if="stepped && showStepLine(definition.steps.length)"
185
+ class="h-1 rounded bg-primary"
186
+ />
187
+
188
+ <div
189
+ v-if="stepped"
190
+ class="flex justify-center"
191
+ >
192
+ <UButton
193
+ color="neutral"
194
+ variant="soft"
195
+ size="xs"
196
+ icon="i-lucide-plus"
197
+ :label="t('builder.addStep', 'Add step')"
198
+ @click="addStep()"
199
+ @dragover="overStepSlot($event, definition.steps.length)"
200
+ />
201
+ </div>
202
+ </div>
203
+
204
+ <p
205
+ v-if="!selection"
206
+ class="text-center text-xs text-muted"
207
+ >
208
+ {{ t("builder.selectHint", "Select a field or step to edit its settings.") }}
209
+ </p>
210
+ </div>
211
+
212
+ <!-- Live preview -->
213
+ <div class="min-w-0 space-y-2 lg:sticky lg:top-4">
214
+ <h2 class="text-sm font-semibold">
215
+ {{ t("admin.preview", "Preview") }}
216
+ </h2>
217
+ <slot name="preview" />
218
+ </div>
219
+ </div>
220
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { FormDefinition } from '../../../types.js';
2
+ /** The saved form; enables per-form secrets (captcha secret key), which are stored right away, not with the definition. */
3
+ type __VLS_Props = {
4
+ formId?: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue: FormDefinition;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare var __VLS_82: {};
11
+ type __VLS_Slots = {} & {
12
+ preview?: (props: typeof __VLS_82) => any;
13
+ };
14
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
15
+ "update:modelValue": (value: FormDefinition) => any;
16
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
+ "onUpdate:modelValue"?: ((value: FormDefinition) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
20
+ declare const _default: typeof __VLS_export;
21
+ export default _default;
22
+ type __VLS_WithSlots<T, S> = T & {
23
+ new (): {
24
+ $slots: S;
25
+ };
26
+ };
@@ -0,0 +1,16 @@
1
+ import type { FormDefinition } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ formId: string;
4
+ slug: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue: FormDefinition;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:modelValue": (value: FormDefinition) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: FormDefinition) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,404 @@
1
+ <script setup>
2
+ import { resolveAccess, setAccess } from "../../utils";
3
+ const definition = defineModel({ type: Object, ...{ required: true } });
4
+ const props = defineProps({
5
+ formId: { type: String, required: true },
6
+ slug: { type: String, required: true }
7
+ });
8
+ const t = useModuleT("forms");
9
+ const toast = useToast();
10
+ const base = computed(() => `/api/forms/admin/forms/${props.formId}`);
11
+ const access = computed(() => resolveAccess(definition.value));
12
+ const availabilities = computed(() => [
13
+ { value: "web", label: t("access.availabilityWeb", "Web page and API") },
14
+ { value: "api", label: t("access.availabilityApi", "API only") }
15
+ ]);
16
+ const authModes = computed(() => [
17
+ { value: "none", label: t("access.authNone", "Open to everyone") },
18
+ { value: "user", label: t("access.authUser", "Signed-in users") },
19
+ { value: "token", label: t("access.authToken", "API token") },
20
+ { value: "password", label: t("access.authPassword", "Shared password") }
21
+ ]);
22
+ const schedule = computed(() => access.value.schedule ?? { type: "always" });
23
+ const scheduleTypes = computed(() => [
24
+ { value: "always", label: t("schedule.always", "Always open") },
25
+ { value: "yearly", label: t("schedule.yearly", "Every year (a season)") },
26
+ { value: "range", label: t("schedule.range", "One fixed period") }
27
+ ]);
28
+ const months = computed(() => Array.from({ length: 12 }, (_, i) => ({
29
+ value: i + 1,
30
+ label: new Intl.DateTimeFormat(void 0, { month: "long" }).format(new Date(2e3, i, 1))
31
+ })));
32
+ const setSchedule = (next) => {
33
+ definition.value = setAccess(definition.value, { schedule: next.type === "always" ? void 0 : next });
34
+ };
35
+ function changeScheduleType(type) {
36
+ if (type === "yearly") setSchedule({ type, from: { month: 1, day: 1, time: "00:00" }, to: { month: 2, day: 1, time: "00:00" }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone });
37
+ else if (type === "range") setSchedule({ type });
38
+ else setSchedule({ type: "always" });
39
+ }
40
+ const toLocalInput = (iso) => {
41
+ if (!iso) return "";
42
+ const d = new Date(iso);
43
+ return new Date(d.getTime() - d.getTimezoneOffset() * 6e4).toISOString().slice(0, 16);
44
+ };
45
+ const fromLocalInput = (value) => value ? new Date(value).toISOString() : void 0;
46
+ function patchMoment(side, patch) {
47
+ if (schedule.value.type === "yearly") setSchedule({ ...schedule.value, [side]: { ...schedule.value[side], ...patch } });
48
+ }
49
+ const fail = (cause) => toast.add({
50
+ title: cause.statusMessage ?? t("error.generic", "Something went wrong. Please try again."),
51
+ color: "error"
52
+ });
53
+ const { data: tokens, refresh: refreshTokens } = await useFetch(() => `${base.value}/tokens`, { default: () => [] });
54
+ const tokenName = ref("");
55
+ const creating = ref(false);
56
+ const revealed = ref();
57
+ async function createToken() {
58
+ creating.value = true;
59
+ try {
60
+ revealed.value = await $fetch(`${base.value}/tokens`, { method: "POST", body: { name: tokenName.value } });
61
+ tokenName.value = "";
62
+ await refreshTokens();
63
+ } catch (cause) {
64
+ fail(cause);
65
+ } finally {
66
+ creating.value = false;
67
+ }
68
+ }
69
+ async function revoke(token) {
70
+ try {
71
+ await $fetch(`${base.value}/tokens/${token.id}`, { method: "DELETE" });
72
+ await refreshTokens();
73
+ } catch (cause) {
74
+ fail(cause);
75
+ }
76
+ }
77
+ async function copy(text) {
78
+ try {
79
+ await navigator.clipboard.writeText(text);
80
+ toast.add({ title: t("preview.copied", "Copied to clipboard."), color: "success" });
81
+ } catch (cause) {
82
+ fail(cause);
83
+ }
84
+ }
85
+ const { data: passwordStatus, refresh: refreshPassword } = await useFetch(() => `${base.value}/password`, { default: () => ({ set: false }) });
86
+ const password = ref("");
87
+ const savingPassword = ref(false);
88
+ async function savePassword() {
89
+ savingPassword.value = true;
90
+ try {
91
+ await $fetch(`${base.value}/password`, { method: "PUT", body: { password: password.value } });
92
+ password.value = "";
93
+ await refreshPassword();
94
+ toast.add({ title: t("access.passwordSaved", "Password saved."), color: "success" });
95
+ } catch (cause) {
96
+ fail(cause);
97
+ } finally {
98
+ savingPassword.value = false;
99
+ }
100
+ }
101
+ async function clearPassword() {
102
+ try {
103
+ await $fetch(`${base.value}/password`, { method: "DELETE" });
104
+ await refreshPassword();
105
+ } catch (cause) {
106
+ fail(cause);
107
+ }
108
+ }
109
+ const origin = useRequestURL().origin;
110
+ const submitUrl = computed(() => `${origin}/api/forms/${props.slug}/submit`);
111
+ const snippet = computed(() => {
112
+ const headers = {
113
+ token: " -H 'Authorization: Bearer TOKEN' \\\n",
114
+ password: " -H 'x-form-password: <password>' \\\n",
115
+ user: " -H 'Cookie: <session cookie of a signed-in user>' \\\n",
116
+ none: ""
117
+ }[access.value.auth];
118
+ return `curl -X POST '${submitUrl.value}' \\
119
+ ${headers} -H 'content-type: application/json' \\
120
+ -d '{"answers":{"field":"value"},"locale":"en"}'`;
121
+ });
122
+ </script>
123
+
124
+ <template>
125
+ <div class="max-w-2xl space-y-8">
126
+ <section class="space-y-4">
127
+ <h2 class="font-semibold">
128
+ {{ t("access.title", "Who can use this form") }}
129
+ </h2>
130
+ <UFormField
131
+ :label="t('access.availability', 'Availability')"
132
+ :description="access.availability === 'api' ? t('access.availabilityApiHint', 'No public page: the form can only be read and submitted through the API.') : t('access.availabilityWebHint', 'The public page and the API are both available.')"
133
+ >
134
+ <USelect
135
+ :model-value="access.availability"
136
+ :items="availabilities"
137
+ :disabled="access.auth === 'token'"
138
+ class="w-full"
139
+ @update:model-value="definition = setAccess(definition, { availability: $event })"
140
+ />
141
+ </UFormField>
142
+
143
+ <UFormField
144
+ :label="t('access.auth', 'Authentication')"
145
+ :description="access.auth === 'token' ? t('access.authTokenHint', 'Token forms are API only: a token cannot be typed into a page.') : void 0"
146
+ >
147
+ <USelect
148
+ :model-value="access.auth"
149
+ :items="authModes"
150
+ class="w-full"
151
+ @update:model-value="definition = setAccess(definition, { auth: $event })"
152
+ />
153
+ </UFormField>
154
+ <UFormField
155
+ :label="t('schedule.title', 'Open period')"
156
+ :description="t('schedule.hint', 'Outside the period the form shows a closed notice and rejects submissions. A season whose end is before its start wraps over new year.')"
157
+ >
158
+ <USelect
159
+ :model-value="schedule.type"
160
+ :items="scheduleTypes"
161
+ class="w-full"
162
+ @update:model-value="changeScheduleType($event)"
163
+ />
164
+ </UFormField>
165
+
166
+ <div
167
+ v-if="schedule.type === 'range'"
168
+ class="grid gap-4 sm:grid-cols-2"
169
+ >
170
+ <UFormField :label="t('schedule.opens', 'Opens')">
171
+ <UInput
172
+ type="datetime-local"
173
+ class="w-full"
174
+ :model-value="toLocalInput(schedule.from)"
175
+ @update:model-value="setSchedule({ ...schedule, from: fromLocalInput($event) })"
176
+ />
177
+ </UFormField>
178
+ <UFormField :label="t('schedule.closes', 'Closes')">
179
+ <UInput
180
+ type="datetime-local"
181
+ class="w-full"
182
+ :model-value="toLocalInput(schedule.to)"
183
+ @update:model-value="setSchedule({ ...schedule, to: fromLocalInput($event) })"
184
+ />
185
+ </UFormField>
186
+ </div>
187
+
188
+ <div
189
+ v-else-if="schedule.type === 'yearly'"
190
+ class="space-y-4"
191
+ >
192
+ <div
193
+ v-for="side in ['from', 'to']"
194
+ :key="side"
195
+ class="grid gap-4 sm:grid-cols-3"
196
+ >
197
+ <UFormField :label="side === 'from' ? t('schedule.opens', 'Opens') : t('schedule.closes', 'Closes')">
198
+ <USelect
199
+ :model-value="schedule[side].month"
200
+ :items="months"
201
+ class="w-full"
202
+ @update:model-value="patchMoment(side, { month: Number($event) })"
203
+ />
204
+ </UFormField>
205
+ <UFormField :label="t('schedule.day', 'Day')">
206
+ <UInput
207
+ type="number"
208
+ min="1"
209
+ max="31"
210
+ class="w-full"
211
+ :model-value="schedule[side].day"
212
+ @update:model-value="patchMoment(side, { day: Number($event) })"
213
+ />
214
+ </UFormField>
215
+ <UFormField :label="t('schedule.time', 'Time')">
216
+ <UInput
217
+ type="time"
218
+ class="w-full"
219
+ :model-value="schedule[side].time"
220
+ @update:model-value="patchMoment(side, { time: String($event) })"
221
+ />
222
+ </UFormField>
223
+ </div>
224
+ <UFormField :label="t('schedule.timezone', 'Timezone')">
225
+ <UInput
226
+ class="w-full"
227
+ :model-value="schedule.timezone"
228
+ @update:model-value="setSchedule({ ...schedule, timezone: String($event) })"
229
+ />
230
+ </UFormField>
231
+ </div>
232
+
233
+ <p class="text-sm text-muted">
234
+ {{ t("access.saveHint", "Availability and authentication are saved with the form. Tokens and the password take effect immediately.") }}
235
+ </p>
236
+ </section>
237
+
238
+ <section class="space-y-4">
239
+ <h2 class="font-semibold">
240
+ {{ t("access.tokensTitle", "API tokens") }}
241
+ </h2>
242
+ <p class="text-sm text-muted">
243
+ {{ t("access.tokensHint", "Send a token as Authorization: Bearer TOKEN or in the x-form-token header. It is shown only once.") }}
244
+ </p>
245
+ <UAlert
246
+ v-if="access.auth !== 'token'"
247
+ color="neutral"
248
+ variant="subtle"
249
+ :title="t('access.tokensInactive', 'Tokens are only checked when authentication is set to API token.')"
250
+ />
251
+
252
+ <form
253
+ class="flex gap-2"
254
+ @submit.prevent="createToken"
255
+ >
256
+ <UInput
257
+ v-model="tokenName"
258
+ :placeholder="t('access.tokenName', 'Token name, e.g. Mobile app')"
259
+ class="flex-1"
260
+ />
261
+ <UButton
262
+ type="submit"
263
+ :loading="creating"
264
+ :disabled="!tokenName.trim()"
265
+ :label="t('access.createToken', 'Create token')"
266
+ />
267
+ </form>
268
+
269
+ <UAlert
270
+ v-if="revealed"
271
+ color="success"
272
+ variant="subtle"
273
+ :title="t('access.tokenCreated', 'Copy this token now. You will not see it again.')"
274
+ :close="true"
275
+ @update:open="revealed = void 0"
276
+ >
277
+ <template #description>
278
+ <div class="mt-2 flex items-center gap-2">
279
+ <code
280
+ data-testid="revealed-token"
281
+ class="break-all rounded bg-elevated px-2 py-1 text-xs"
282
+ >{{ revealed.token }}</code>
283
+ <UButton
284
+ color="neutral"
285
+ variant="outline"
286
+ size="xs"
287
+ icon="i-lucide-copy"
288
+ :label="t('preview.copy', 'Copy')"
289
+ @click="copy(revealed.token)"
290
+ />
291
+ </div>
292
+ </template>
293
+ </UAlert>
294
+
295
+ <ul
296
+ v-if="tokens?.length"
297
+ class="divide-y divide-default rounded-md border border-default"
298
+ >
299
+ <li
300
+ v-for="token in tokens"
301
+ :key="token.id"
302
+ class="flex items-center justify-between gap-3 p-3"
303
+ >
304
+ <div class="min-w-0">
305
+ <p class="truncate font-medium">
306
+ {{ token.name }}
307
+ <UBadge
308
+ v-if="token.revokedAt"
309
+ color="error"
310
+ variant="subtle"
311
+ size="sm"
312
+ :label="t('access.revoked', 'Revoked')"
313
+ />
314
+ </p>
315
+ <p class="text-xs text-muted">
316
+ <code>{{ token.prefix }}…</code>
317
+ ·
318
+ {{ token.lastUsedAt ? t("access.lastUsed", "Last used {date}", { date: new Date(token.lastUsedAt).toLocaleString() }) : t("access.neverUsed", "Never used") }}
319
+ </p>
320
+ </div>
321
+ <UButton
322
+ v-if="!token.revokedAt"
323
+ color="error"
324
+ variant="outline"
325
+ size="xs"
326
+ :label="t('access.revoke', 'Revoke')"
327
+ @click="revoke(token)"
328
+ />
329
+ </li>
330
+ </ul>
331
+ <p
332
+ v-else
333
+ class="text-sm text-muted"
334
+ >
335
+ {{ t("access.noTokens", "No tokens yet.") }}
336
+ </p>
337
+ </section>
338
+
339
+ <section class="space-y-4">
340
+ <h2 class="font-semibold">
341
+ {{ t("access.passwordTitle", "Shared password") }}
342
+ </h2>
343
+ <UAlert
344
+ v-if="access.auth === 'password' && !passwordStatus?.set"
345
+ color="warning"
346
+ variant="subtle"
347
+ :title="t('access.passwordMissing', 'No password is set, so nobody can open this form yet.')"
348
+ />
349
+ <p
350
+ v-else
351
+ class="text-sm text-muted"
352
+ >
353
+ {{ passwordStatus?.set ? t("access.passwordIsSet", "A password is set.") : t("access.passwordIsNotSet", "No password is set.") }}
354
+ {{ access.auth !== "password" ? t("access.passwordInactive", "It is only checked when authentication is set to Shared password.") : "" }}
355
+ </p>
356
+ <form
357
+ class="flex gap-2"
358
+ @submit.prevent="savePassword"
359
+ >
360
+ <UInput
361
+ v-model="password"
362
+ type="password"
363
+ autocomplete="new-password"
364
+ :placeholder="passwordStatus?.set ? t('access.newPassword', 'New password') : t('access.setPassword', 'Password (min. 6 characters)')"
365
+ class="flex-1"
366
+ />
367
+ <UButton
368
+ type="submit"
369
+ :loading="savingPassword"
370
+ :disabled="password.length < 6"
371
+ :label="passwordStatus?.set ? t('access.changePassword', 'Change') : t('access.savePassword', 'Set password')"
372
+ />
373
+ <UButton
374
+ v-if="passwordStatus?.set"
375
+ type="button"
376
+ color="error"
377
+ variant="outline"
378
+ :label="t('access.clearPassword', 'Clear')"
379
+ @click="clearPassword"
380
+ />
381
+ </form>
382
+ </section>
383
+
384
+ <section class="space-y-3">
385
+ <h2 class="font-semibold">
386
+ {{ t("access.apiTitle", "API") }}
387
+ </h2>
388
+ <p class="text-sm text-muted">
389
+ {{ t("access.apiHint", "Submit with POST (JSON, or multipart with file:KEY parts). The definition is available with GET on /api/forms/{slug}.", { slug: props.slug }) }}
390
+ </p>
391
+ <div class="flex items-start gap-2">
392
+ <pre class="flex-1 overflow-x-auto rounded-md bg-elevated p-3 text-xs">{{ snippet }}</pre>
393
+ <UButton
394
+ color="neutral"
395
+ variant="outline"
396
+ size="xs"
397
+ icon="i-lucide-copy"
398
+ :label="t('preview.copy', 'Copy')"
399
+ @click="copy(snippet)"
400
+ />
401
+ </div>
402
+ </section>
403
+ </div>
404
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { FormDefinition } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ formId: string;
4
+ slug: string;
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue: FormDefinition;
8
+ };
9
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
+ "update:modelValue": (value: FormDefinition) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: FormDefinition) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;