@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,15 @@
1
+ import type { FormField } from '../../../types.js';
2
+ import type { CaptchaSecrets } from '../composables/useCaptchaSecrets.js';
3
+ type __VLS_Props = {
4
+ field: FormField;
5
+ /** The saved form; enables the per-form secret key. */
6
+ formId?: string;
7
+ secrets: CaptchaSecrets;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ patch: (patch: Partial<import("../../utils/index.js").CaptchaConfig>) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onPatch?: ((patch: Partial<import("../../utils/index.js").CaptchaConfig>) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,126 @@
1
+ <script setup>
2
+ import { captchaProviders } from "../../utils";
3
+ import { captchaSiteKey } from "../composables/useCaptcha";
4
+ const props = defineProps({
5
+ field: { type: Object, required: true },
6
+ formId: { type: String, required: false },
7
+ secrets: { type: null, required: true }
8
+ });
9
+ const emit = defineEmits(["patch"]);
10
+ const t = useModuleT("forms");
11
+ const providerItems = computed(() => captchaProviders.map((value) => ({ value, label: t(`captcha.providers.${value}`, value) })));
12
+ const themeItems = computed(() => [
13
+ { value: "auto", label: t("captcha.themes.auto", "Automatic") },
14
+ { value: "light", label: t("captcha.themes.light", "Light") },
15
+ { value: "dark", label: t("captcha.themes.dark", "Dark") }
16
+ ]);
17
+ const sizeItems = computed(() => [
18
+ { value: "normal", label: t("captcha.sizes.normal", "Normal") },
19
+ { value: "compact", label: t("captcha.sizes.compact", "Compact") }
20
+ ]);
21
+ const keyMissing = computed(() => !captchaSiteKey(props.field));
22
+ </script>
23
+
24
+ <template>
25
+ <template v-if="secrets.captcha">
26
+ <UFormField :label="t('captcha.provider', 'Provider')">
27
+ <USelect
28
+ :model-value="secrets.captcha.provider"
29
+ :items="providerItems"
30
+ class="w-full"
31
+ @update:model-value="emit('patch', { provider: $event })"
32
+ />
33
+ </UFormField>
34
+ <UAlert
35
+ v-if="keyMissing"
36
+ color="warning"
37
+ variant="subtle"
38
+ icon="i-lucide-triangle-alert"
39
+ :title="t('captcha.missingKey', 'The {provider} captcha has no site key configured.', { provider: t(`captcha.providers.${secrets.captcha.provider}`, secrets.captcha.provider) })"
40
+ :ui="{ description: 'break-words' }"
41
+ :description="t('captcha.missingKeyHint', 'Set the site key below or the NUXT_PUBLIC_FORMS_CAPTCHA_PROVIDER_SITE_KEY environment variable. Submissions also need the matching secret key on the server.')"
42
+ />
43
+ <UFormField
44
+ :label="t('captcha.siteKey', 'Site key')"
45
+ :description="t('captcha.siteKeyHint', 'Public key of this site. Leave empty to use the one configured for the provider.')"
46
+ >
47
+ <UInput
48
+ :model-value="secrets.captcha.siteKey"
49
+ class="w-full font-mono"
50
+ @update:model-value="emit('patch', { siteKey: String($event).trim() })"
51
+ />
52
+ </UFormField>
53
+ <UFormField
54
+ v-if="formId"
55
+ :label="t('captcha.secretLabel', 'Secret key')"
56
+ :description="t('captcha.secretHint', 'Server-side key used to verify this form. Stored encrypted and never shown again. Without one, the environment variable is used.')"
57
+ >
58
+ <p
59
+ class="mb-2 flex gap-1 text-sm"
60
+ data-testid="captcha-secret-status"
61
+ >
62
+ <span class="text-muted">{{ t("captcha.secretStatus", "Secret key") }}:</span>
63
+ <span :class="secrets.source === 'unset' ? 'text-warning' : 'text-success'">{{ secrets.statusLabel }}</span>
64
+ </p>
65
+ <UAlert
66
+ v-if="secrets.secrets?.unreadable"
67
+ color="warning"
68
+ variant="subtle"
69
+ icon="i-lucide-triangle-alert"
70
+ class="mb-2"
71
+ :title="t('captcha.secretUnreadable', 'The stored secrets of this form cannot be decrypted because the encryption key changed. Enter them again.')"
72
+ />
73
+ <form
74
+ class="flex gap-2"
75
+ @submit.prevent="secrets.save()"
76
+ >
77
+ <UInput
78
+ :model-value="secrets.value"
79
+ type="password"
80
+ autocomplete="new-password"
81
+ class="min-w-0 flex-1 font-mono"
82
+ :placeholder="secrets.source === 'form' ? t('captcha.secretPlaceholderSet', 'Enter a new secret key to replace it') : t('captcha.secretPlaceholder', 'Paste the secret key')"
83
+ @update:model-value="secrets.setValue(String($event ?? ''))"
84
+ />
85
+ <UButton
86
+ type="submit"
87
+ :loading="secrets.saving"
88
+ :disabled="!secrets.value.trim()"
89
+ :label="t('captcha.secretSave', 'Save secret')"
90
+ />
91
+ <UButton
92
+ v-if="secrets.source === 'form'"
93
+ type="button"
94
+ color="error"
95
+ variant="outline"
96
+ :label="t('captcha.secretRemove', 'Remove')"
97
+ @click="secrets.remove()"
98
+ />
99
+ </form>
100
+ <p class="mt-1 text-xs text-muted">
101
+ {{ t("captcha.secretImmediate", "Saved immediately, independent of the form Save.") }}
102
+ </p>
103
+ </UFormField>
104
+ <div class="grid grid-cols-2 gap-2">
105
+ <UFormField :label="t('captcha.theme', 'Theme')">
106
+ <USelect
107
+ :model-value="secrets.captcha.theme ?? 'auto'"
108
+ :items="themeItems"
109
+ class="w-full"
110
+ @update:model-value="emit('patch', { theme: $event === 'auto' ? void 0 : $event })"
111
+ />
112
+ </UFormField>
113
+ <UFormField :label="t('captcha.size', 'Size')">
114
+ <USelect
115
+ :model-value="secrets.captcha.size ?? 'normal'"
116
+ :items="sizeItems"
117
+ class="w-full"
118
+ @update:model-value="emit('patch', { size: $event === 'normal' ? void 0 : $event })"
119
+ />
120
+ </UFormField>
121
+ </div>
122
+ <p class="text-sm text-muted">
123
+ {{ t("captcha.hint", "Always required while visible. The token is checked on the server and never stored. Submissions with an API token skip it.") }}
124
+ </p>
125
+ </template>
126
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { FormField } from '../../../types.js';
2
+ import type { CaptchaSecrets } from '../composables/useCaptchaSecrets.js';
3
+ type __VLS_Props = {
4
+ field: FormField;
5
+ /** The saved form; enables the per-form secret key. */
6
+ formId?: string;
7
+ secrets: CaptchaSecrets;
8
+ };
9
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ patch: (patch: Partial<import("../../utils/index.js").CaptchaConfig>) => any;
11
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
+ onPatch?: ((patch: Partial<import("../../utils/index.js").CaptchaConfig>) => any) | undefined;
13
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import type { Condition } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ /** Keys of the fields (before this one) a condition may look at. */
4
+ fields: string[];
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue?: Condition | undefined;
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: Condition | undefined) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: Condition | undefined) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ fields: { type: Array, required: true }
4
+ });
5
+ const model = defineModel({ type: null });
6
+ const t = useModuleT("forms");
7
+ const ops = ["eq", "neq", "in", "empty", "notEmpty"];
8
+ const opNames = { eq: "is equal to", neq: "is not equal to", in: "is one of", empty: "is empty", notEmpty: "has an answer", truthy: "has an answer" };
9
+ const withoutValue = (op) => op === "empty" || op === "notEmpty" || op === "truthy";
10
+ const text = computed(() => {
11
+ const value = model.value?.value;
12
+ return Array.isArray(value) ? value.join(", ") : String(value ?? "");
13
+ });
14
+ function patch(next) {
15
+ const current = model.value ?? { field: props.fields[0] ?? "", op: "eq" };
16
+ model.value = { ...current, ...next };
17
+ }
18
+ function setValue(raw) {
19
+ patch({ value: model.value?.op === "in" ? raw.split(",").map((part) => part.trim()).filter(Boolean) : raw });
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <div class="space-y-2">
25
+ <USwitch
26
+ :model-value="!!model"
27
+ :disabled="!fields.length"
28
+ :label="t('builder.conditional', 'Show only if\u2026')"
29
+ @update:model-value="model = $event ? { field: fields[0], op: 'eq', value: '' } : void 0"
30
+ />
31
+ <p
32
+ v-if="!fields.length"
33
+ class="text-xs text-muted"
34
+ >
35
+ {{ t("builder.noEarlierFields", "Add a field before this one to use as a condition.") }}
36
+ </p>
37
+ <div
38
+ v-else-if="model"
39
+ class="grid gap-2"
40
+ >
41
+ <USelect
42
+ :model-value="model.field"
43
+ :items="fields"
44
+ class="w-full"
45
+ @update:model-value="patch({ field: String($event) })"
46
+ />
47
+ <USelect
48
+ :model-value="model.op === 'truthy' ? 'notEmpty' : model.op"
49
+ :items="ops.map((value) => ({ value, label: t(`builder.ops.${value}`, opNames[value]) }))"
50
+ class="w-full"
51
+ @update:model-value="patch({ op: $event, value: void 0 })"
52
+ />
53
+ <UInput
54
+ v-if="!withoutValue(model.op)"
55
+ :model-value="text"
56
+ :placeholder="model.op === 'in' ? t('builder.valuesHint', 'a, b, c') : t('builder.value', 'Value')"
57
+ class="w-full"
58
+ @update:model-value="setValue(String($event))"
59
+ />
60
+ </div>
61
+ </div>
62
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { Condition } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ /** Keys of the fields (before this one) a condition may look at. */
4
+ fields: string[];
5
+ };
6
+ type __VLS_ModelProps = {
7
+ modelValue?: Condition | undefined;
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: Condition | undefined) => any;
12
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
13
+ "onUpdate:modelValue"?: ((value: Condition | undefined) => any) | undefined;
14
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,27 @@
1
+ import type { FormField } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ field: FormField;
4
+ selected: boolean;
5
+ dragging: boolean;
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ select: () => any;
9
+ dragover: (event: DragEvent) => any;
10
+ dragstart: (event: DragEvent) => any;
11
+ dragend: () => any;
12
+ up: () => any;
13
+ down: () => any;
14
+ remove: () => any;
15
+ duplicate: () => any;
16
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
+ onSelect?: (() => any) | undefined;
18
+ onDragover?: ((event: DragEvent) => any) | undefined;
19
+ onDragstart?: ((event: DragEvent) => any) | undefined;
20
+ onDragend?: (() => any) | undefined;
21
+ onUp?: (() => any) | undefined;
22
+ onDown?: (() => any) | undefined;
23
+ onRemove?: (() => any) | undefined;
24
+ onDuplicate?: (() => any) | undefined;
25
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
+ declare const _default: typeof __VLS_export;
27
+ export default _default;
@@ -0,0 +1,87 @@
1
+ <script setup>
2
+ import { fieldTypeIcons, useFormBuilderLabels } from "../composables/useFormBuilderLabels";
3
+ defineProps({
4
+ field: { type: Object, required: true },
5
+ selected: { type: Boolean, required: true },
6
+ dragging: { type: Boolean, required: true }
7
+ });
8
+ const emit = defineEmits(["select", "dragstart", "dragend", "dragover", "up", "down", "duplicate", "remove"]);
9
+ const t = useModuleT("forms");
10
+ const { typeLabel, summary } = useFormBuilderLabels();
11
+ </script>
12
+
13
+ <template>
14
+ <div
15
+ draggable="true"
16
+ class="group flex cursor-pointer items-center gap-2 rounded-md border px-2 py-2 hover:bg-elevated"
17
+ :class="[
18
+ selected ? 'border-primary bg-elevated' : 'border-default',
19
+ dragging ? 'opacity-40' : ''
20
+ ]"
21
+ @click="emit('select')"
22
+ @dragstart.stop="emit('dragstart', $event)"
23
+ @dragend="emit('dragend')"
24
+ @dragover="emit('dragover', $event)"
25
+ >
26
+ <UIcon
27
+ name="i-lucide-grip-vertical"
28
+ class="size-4 shrink-0 cursor-grab text-muted"
29
+ />
30
+ <UIcon
31
+ :name="fieldTypeIcons[field.type]"
32
+ class="size-4 shrink-0 text-muted"
33
+ />
34
+ <div class="min-w-0 flex-1">
35
+ <p class="truncate text-sm font-medium">
36
+ {{ summary(field) }}<span
37
+ v-if="field.required"
38
+ class="text-error"
39
+ > *</span>
40
+ </p>
41
+ <p class="truncate text-xs text-muted">
42
+ {{ typeLabel(field.type) }} · {{ field.key }}
43
+ </p>
44
+ </div>
45
+ <UBadge
46
+ v-if="field.showIf"
47
+ color="neutral"
48
+ variant="subtle"
49
+ size="sm"
50
+ :label="t('builder.conditionalBadge', 'Conditional')"
51
+ />
52
+ <div class="flex shrink-0 opacity-0 focus-within:opacity-100 group-hover:opacity-100">
53
+ <UButton
54
+ color="neutral"
55
+ variant="ghost"
56
+ size="xs"
57
+ icon="i-lucide-chevron-up"
58
+ :aria-label="t('builder.moveUp', 'Move up')"
59
+ @click.stop="emit('up')"
60
+ />
61
+ <UButton
62
+ color="neutral"
63
+ variant="ghost"
64
+ size="xs"
65
+ icon="i-lucide-chevron-down"
66
+ :aria-label="t('builder.moveDown', 'Move down')"
67
+ @click.stop="emit('down')"
68
+ />
69
+ <UButton
70
+ color="neutral"
71
+ variant="ghost"
72
+ size="xs"
73
+ icon="i-lucide-copy"
74
+ :aria-label="t('builder.duplicate', 'Duplicate')"
75
+ @click.stop="emit('duplicate')"
76
+ />
77
+ <UButton
78
+ color="error"
79
+ variant="ghost"
80
+ size="xs"
81
+ icon="i-lucide-trash-2"
82
+ :aria-label="t('builder.delete', 'Delete')"
83
+ @click.stop="emit('remove')"
84
+ />
85
+ </div>
86
+ </div>
87
+ </template>
@@ -0,0 +1,27 @@
1
+ import type { FormField } from '../../../types.js';
2
+ type __VLS_Props = {
3
+ field: FormField;
4
+ selected: boolean;
5
+ dragging: boolean;
6
+ };
7
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
+ select: () => any;
9
+ dragover: (event: DragEvent) => any;
10
+ dragstart: (event: DragEvent) => any;
11
+ dragend: () => any;
12
+ up: () => any;
13
+ down: () => any;
14
+ remove: () => any;
15
+ duplicate: () => any;
16
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
+ onSelect?: (() => any) | undefined;
18
+ onDragover?: ((event: DragEvent) => any) | undefined;
19
+ onDragstart?: ((event: DragEvent) => any) | undefined;
20
+ onDragend?: (() => any) | undefined;
21
+ onUp?: (() => any) | undefined;
22
+ onDown?: (() => any) | undefined;
23
+ onRemove?: (() => any) | undefined;
24
+ onDuplicate?: (() => any) | undefined;
25
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
+ declare const _default: typeof __VLS_export;
27
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import type { FormField, FormStep } from '../../../types.js';
2
+ import type { CaptchaSecrets } from '../composables/useCaptchaSecrets.js';
3
+ import type { FieldPosition } from '../../utils/index.js';
4
+ type __VLS_Props = {
5
+ field: FormField;
6
+ at: FieldPosition;
7
+ /** The titles of the steps the field can move to; only given for stepped forms. */
8
+ steps?: FormStep[];
9
+ /** Keys of the fields the field's condition may look at. */
10
+ conditionFields: string[];
11
+ /** The saved form; enables per-form secrets (captcha secret key). */
12
+ formId?: string;
13
+ secrets: CaptchaSecrets;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ patch: (patch: Partial<FormField>) => any;
17
+ moveToStep: (step: number) => any;
18
+ rename: (key: string) => any;
19
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
20
+ onPatch?: ((patch: Partial<FormField>) => any) | undefined;
21
+ onMoveToStep?: ((step: number) => any) | undefined;
22
+ onRename?: ((key: string) => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,170 @@
1
+ <script setup>
2
+ import { translate } from "../../utils";
3
+ import { useFormBuilderLabels } from "../composables/useFormBuilderLabels";
4
+ import { useFormsLocale } from "../composables/useForms";
5
+ const props = defineProps({
6
+ field: { type: Object, required: true },
7
+ at: { type: Object, required: true },
8
+ steps: { type: Array, required: false },
9
+ conditionFields: { type: Array, required: true },
10
+ formId: { type: String, required: false },
11
+ secrets: { type: null, required: true }
12
+ });
13
+ const emit = defineEmits(["patch", "rename", "moveToStep"]);
14
+ const t = useModuleT("forms");
15
+ const locale = useFormsLocale();
16
+ const { typeLabel } = useFormBuilderLabels();
17
+ const hasOptions = computed(() => props.field.type === "select" || props.field.type === "radio");
18
+ const hasPlaceholder = computed(() => ["text", "email", "tel", "number", "textarea", "select"].includes(props.field.type));
19
+ const hasLength = computed(() => ["text", "email", "tel", "textarea"].includes(props.field.type));
20
+ function setValidation(key, value) {
21
+ const { [key]: _old, ...rest } = props.field.validation ?? {};
22
+ const validation = value === null || value === void 0 || Number.isNaN(value) ? rest : { ...rest, [key]: value };
23
+ emit("patch", { validation: Object.keys(validation).length ? validation : void 0 });
24
+ }
25
+ function patchCaptcha(patch) {
26
+ const next = Object.fromEntries(Object.entries({ ...props.field.captcha, ...patch }).filter(([, value]) => value !== void 0 && value !== ""));
27
+ emit("patch", { captcha: next });
28
+ }
29
+ </script>
30
+
31
+ <template>
32
+ <aside class="my-1 space-y-4 rounded-md border border-primary/40 bg-elevated/40 p-3">
33
+ <h3 class="font-semibold">
34
+ {{ typeLabel(field.type) }}
35
+ </h3>
36
+
37
+ <UFormField
38
+ v-if="field.type === 'markdown'"
39
+ :label="t('builder.content', 'Text (Markdown)')"
40
+ >
41
+ <FormBuilderText
42
+ :model-value="field.content"
43
+ multiline
44
+ @update:model-value="emit('patch', { content: $event })"
45
+ />
46
+ </UFormField>
47
+ <template v-else-if="field.type === 'captcha' && secrets.captcha">
48
+ <FormBuilderCaptchaSettings
49
+ :field="field"
50
+ :form-id="formId"
51
+ :secrets="secrets"
52
+ @patch="patchCaptcha"
53
+ />
54
+ </template>
55
+ <template v-else>
56
+ <UFormField :label="t('builder.label', 'Label')">
57
+ <FormBuilderText
58
+ :model-value="field.label"
59
+ @update:model-value="emit('patch', { label: $event })"
60
+ />
61
+ </UFormField>
62
+ <UFormField :label="t('builder.description', 'Help text')">
63
+ <FormBuilderText
64
+ :model-value="field.description"
65
+ @update:model-value="emit('patch', { description: $event })"
66
+ />
67
+ </UFormField>
68
+ <UFormField
69
+ v-if="hasPlaceholder"
70
+ :label="t('builder.placeholder', 'Placeholder')"
71
+ >
72
+ <FormBuilderText
73
+ :model-value="field.placeholder"
74
+ @update:model-value="emit('patch', { placeholder: $event })"
75
+ />
76
+ </UFormField>
77
+ <USwitch
78
+ :model-value="!!field.required"
79
+ :label="t('builder.required', 'Required')"
80
+ @update:model-value="emit('patch', { required: $event === true || void 0 })"
81
+ />
82
+ <FormBuilderPhoneSettings
83
+ v-if="field.type === 'tel'"
84
+ :field="field"
85
+ @patch="emit('patch', $event)"
86
+ />
87
+ <USwitch
88
+ v-if="field.type === 'file'"
89
+ :model-value="!!field.multiple"
90
+ :label="t('builder.multiple', 'Allow several files')"
91
+ @update:model-value="emit('patch', { multiple: $event === true || void 0 })"
92
+ />
93
+ </template>
94
+
95
+ <UFormField
96
+ :label="t('builder.key', 'Key')"
97
+ :description="t('builder.keyHint', 'Identifies the answer. Letters, digits and underscores.')"
98
+ >
99
+ <UInput
100
+ :model-value="field.key"
101
+ class="w-full font-mono"
102
+ @update:model-value="emit('rename', String($event))"
103
+ />
104
+ </UFormField>
105
+
106
+ <FormBuilderOptions
107
+ v-if="hasOptions"
108
+ :options="field.options"
109
+ @update:options="emit('patch', { options: $event })"
110
+ />
111
+
112
+ <div
113
+ v-if="hasLength || field.type === 'number'"
114
+ class="grid grid-cols-2 gap-2"
115
+ >
116
+ <template v-if="field.type === 'number'">
117
+ <UFormField :label="t('builder.min', 'Minimum')">
118
+ <UInputNumber
119
+ :model-value="field.validation?.min"
120
+ class="w-full"
121
+ @update:model-value="setValidation('min', $event)"
122
+ />
123
+ </UFormField>
124
+ <UFormField :label="t('builder.max', 'Maximum')">
125
+ <UInputNumber
126
+ :model-value="field.validation?.max"
127
+ class="w-full"
128
+ @update:model-value="setValidation('max', $event)"
129
+ />
130
+ </UFormField>
131
+ </template>
132
+ <template v-else>
133
+ <UFormField :label="t('builder.minLength', 'Min length')">
134
+ <UInputNumber
135
+ :model-value="field.validation?.minLength"
136
+ :min="0"
137
+ class="w-full"
138
+ @update:model-value="setValidation('minLength', $event)"
139
+ />
140
+ </UFormField>
141
+ <UFormField :label="t('builder.maxLength', 'Max length')">
142
+ <UInputNumber
143
+ :model-value="field.validation?.maxLength"
144
+ :min="1"
145
+ class="w-full"
146
+ @update:model-value="setValidation('maxLength', $event)"
147
+ />
148
+ </UFormField>
149
+ </template>
150
+ </div>
151
+
152
+ <UFormField
153
+ v-if="steps"
154
+ :label="t('builder.step', 'Step')"
155
+ >
156
+ <USelect
157
+ :model-value="at.step"
158
+ :items="steps.map((s, i) => ({ value: i, label: `${i + 1}. ${translate(s.title, locale) || s.id}` }))"
159
+ class="w-full"
160
+ @update:model-value="emit('moveToStep', Number($event))"
161
+ />
162
+ </UFormField>
163
+
164
+ <FormBuilderCondition
165
+ :model-value="field.showIf"
166
+ :fields="conditionFields"
167
+ @update:model-value="emit('patch', { showIf: $event })"
168
+ />
169
+ </aside>
170
+ </template>
@@ -0,0 +1,25 @@
1
+ import type { FormField, FormStep } from '../../../types.js';
2
+ import type { CaptchaSecrets } from '../composables/useCaptchaSecrets.js';
3
+ import type { FieldPosition } from '../../utils/index.js';
4
+ type __VLS_Props = {
5
+ field: FormField;
6
+ at: FieldPosition;
7
+ /** The titles of the steps the field can move to; only given for stepped forms. */
8
+ steps?: FormStep[];
9
+ /** Keys of the fields the field's condition may look at. */
10
+ conditionFields: string[];
11
+ /** The saved form; enables per-form secrets (captcha secret key). */
12
+ formId?: string;
13
+ secrets: CaptchaSecrets;
14
+ };
15
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
16
+ patch: (patch: Partial<FormField>) => any;
17
+ moveToStep: (step: number) => any;
18
+ rename: (key: string) => any;
19
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
20
+ onPatch?: ((patch: Partial<FormField>) => any) | undefined;
21
+ onMoveToStep?: ((step: number) => any) | undefined;
22
+ onRename?: ((key: string) => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import type { DropdownMenuItem } from '@nuxt/ui';
2
+ type __VLS_Props = {
3
+ items?: DropdownMenuItem[][];
4
+ /** A small round dot instead of a regular button. */
5
+ dot?: boolean;
6
+ /** Keeps the button fully visible instead of only on hover. */
7
+ visible?: boolean;
8
+ label?: string;
9
+ ariaLabel?: string;
10
+ line?: boolean;
11
+ };
12
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
13
+ click: () => any;
14
+ dragover: (event: DragEvent) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onClick?: (() => any) | undefined;
17
+ onDragover?: ((event: DragEvent) => any) | undefined;
18
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;