@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
@@ -21,7 +21,7 @@ async function redeliver() {
21
21
  </script>
22
22
 
23
23
  <template>
24
- <UContainer class="max-w-3xl space-y-6 py-10">
24
+ <AdminPage>
25
25
  <UAlert
26
26
  v-if="error || !submission"
27
27
  color="error"
@@ -30,20 +30,11 @@ async function redeliver() {
30
30
  />
31
31
 
32
32
  <template v-else>
33
- <div>
34
- <NuxtLink
35
- :to="paths('adminSubmissions', { id })"
36
- class="text-sm text-muted hover:underline"
37
- >
38
- ← {{ t("admin.submissionsTitle", "Submissions") }}
39
- </NuxtLink>
40
- <h1 class="text-3xl font-bold">
41
- {{ new Date(submission.createdAt).toLocaleString() }}
42
- </h1>
43
- <p class="text-sm text-muted">
44
- {{ submission.formSlug }} · v{{ submission.formVersion }} · {{ submission.locale }}
45
- </p>
46
- </div>
33
+ <AdminPageHeader
34
+ :title="new Date(submission.createdAt).toLocaleString()"
35
+ :description="`${submission.formSlug} \xB7 v${submission.formVersion} \xB7 ${submission.locale}`"
36
+ :back="{ label: t('admin.submissionsTitle', 'Submissions'), to: paths('adminSubmissions', { id }) }"
37
+ />
47
38
 
48
39
  <UAlert
49
40
  :color="submission.deliveryStatus === 'failed' ? 'error' : 'neutral'"
@@ -55,5 +46,5 @@ async function redeliver() {
55
46
 
56
47
  <FormSubmissionView :submission="submission" />
57
48
  </template>
58
- </UContainer>
49
+ </AdminPage>
59
50
  </template>
@@ -5,22 +5,20 @@ const paths = useFormsPaths();
5
5
  const id = computed(() => String(route.params.id));
6
6
  const { data: submissions, error } = await useFetch(() => `/api/forms/admin/forms/${id.value}/submissions`);
7
7
  const color = { none: "neutral", pending: "warning", delivered: "success", failed: "error" };
8
+ const columns = [
9
+ { accessorKey: "createdAt", header: t("admin.submittedAt", "Submitted") },
10
+ { id: "preview", header: t("admin.answers", "Answers") },
11
+ { accessorKey: "deliveryStatus", header: t("admin.delivery", "Delivery") }
12
+ ];
8
13
  const format = (iso) => new Date(iso).toLocaleString();
9
14
  </script>
10
15
 
11
16
  <template>
12
- <UContainer class="space-y-6 py-10">
13
- <div>
14
- <NuxtLink
15
- :to="paths('adminForm', { id })"
16
- class="text-sm text-muted hover:underline"
17
- >
18
- ← {{ t("admin.edit", "Edit") }}
19
- </NuxtLink>
20
- <h1 class="text-3xl font-bold">
21
- {{ t("admin.submissionsTitle", "Submissions") }}
22
- </h1>
23
- </div>
17
+ <AdminPage>
18
+ <AdminPageHeader
19
+ :title="t('admin.submissionsTitle', 'Submissions')"
20
+ :back="{ label: t('admin.edit', 'Edit'), to: paths('adminForm', { id }) }"
21
+ />
24
22
 
25
23
  <UAlert
26
24
  v-if="error"
@@ -28,44 +26,41 @@ const format = (iso) => new Date(iso).toLocaleString();
28
26
  variant="subtle"
29
27
  :title="t('admin.forbidden', 'You are not allowed to manage forms.')"
30
28
  />
31
- <UEmpty
32
- v-else-if="!submissions?.length"
33
- icon="i-lucide-inbox"
34
- :title="t('admin.noSubmissions', 'No submissions yet.')"
35
- />
36
-
37
- <div
29
+ <AdminTable
38
30
  v-else
39
- class="divide-y divide-default rounded-lg border border-default"
31
+ :data="submissions ?? []"
32
+ :columns="columns"
33
+ @select="(_, row2) => navigateTo(paths('adminSubmission', { id, submissionId: row2.original.id }))"
40
34
  >
41
- <NuxtLink
42
- v-for="submission in submissions"
43
- :key="submission.id"
44
- :to="paths('adminSubmission', { id, submissionId: submission.id })"
45
- class="flex flex-wrap items-center justify-between gap-3 p-4 hover:bg-elevated"
46
- >
47
- <div class="min-w-0 space-y-1">
48
- <p class="text-sm text-muted">
49
- {{ format(submission.createdAt) }}
50
- </p>
51
- <p class="truncate">
35
+ <template #createdAt-cell="{ row }">
36
+ <span class="text-muted">{{ format(row.original.createdAt) }}</span>
37
+ </template>
38
+ <template #preview-cell="{ row }">
39
+ <p class="max-w-md truncate">
40
+ <span
41
+ v-for="(answer, index) in row.original.preview"
42
+ :key="answer.label"
43
+ >
52
44
  <span
53
- v-for="(answer, index) in submission.preview"
54
- :key="answer.label"
55
- >
56
- <span
57
- v-if="index"
58
- class="text-muted"
59
- > · </span>{{ answer.value }}
60
- </span>
61
- </p>
62
- </div>
45
+ v-if="index"
46
+ class="text-muted"
47
+ > · </span>{{ answer.value }}
48
+ </span>
49
+ </p>
50
+ </template>
51
+ <template #deliveryStatus-cell="{ row }">
63
52
  <UBadge
64
- :color="color[submission.deliveryStatus]"
53
+ :color="color[row.original.deliveryStatus]"
65
54
  variant="subtle"
66
- :label="t(`delivery.${submission.deliveryStatus}`, submission.deliveryStatus)"
55
+ :label="t(`delivery.${row.original.deliveryStatus}`, row.original.deliveryStatus)"
56
+ />
57
+ </template>
58
+ <template #empty>
59
+ <AdminEmpty
60
+ icon="i-lucide-inbox"
61
+ :title="t('admin.noSubmissions', 'No submissions yet.')"
67
62
  />
68
- </NuxtLink>
69
- </div>
70
- </UContainer>
63
+ </template>
64
+ </AdminTable>
65
+ </AdminPage>
71
66
  </template>
@@ -3,6 +3,7 @@ import { defineForm } from "../../utils";
3
3
  const t = useModuleT("forms");
4
4
  const paths = useFormsPaths();
5
5
  const toast = useToast();
6
+ const link = useFormLink();
6
7
  const { data: forms, error, refresh } = await useFetch("/api/forms/admin/forms");
7
8
  const starter = defineForm({
8
9
  mode: "single",
@@ -31,6 +32,14 @@ async function create() {
31
32
  toast.add({ title: cause.statusMessage ?? t("error.generic", "Something went wrong. Please try again."), color: "error" });
32
33
  }
33
34
  }
35
+ async function clone(form) {
36
+ try {
37
+ const copy = await $fetch(`/api/forms/admin/forms/${form.id}/clone`, { method: "POST" });
38
+ await navigateTo(paths("adminForm", { id: copy.id }));
39
+ } catch (cause) {
40
+ toast.add({ title: cause.statusMessage ?? t("error.generic", "Something went wrong. Please try again."), color: "error" });
41
+ }
42
+ }
34
43
  async function runImport() {
35
44
  try {
36
45
  const parsed = JSON.parse(importJson.value);
@@ -46,16 +55,32 @@ async function runImport() {
46
55
  toast.add({ title: cause.statusMessage ?? t("admin.invalidJson", "The JSON is not valid."), color: "error" });
47
56
  }
48
57
  }
58
+ const columns = [
59
+ { accessorKey: "name", header: t("admin.name", "Name") },
60
+ { accessorKey: "status", header: t("admin.status", "Status") },
61
+ { id: "actions", header: "" }
62
+ ];
49
63
  const statusColor = { draft: "neutral", published: "success", archived: "warning" };
64
+ const statuses = ["published", "draft", "archived"];
65
+ const tab = ref("published");
66
+ const search = ref("");
67
+ const tabs = computed(() => statuses.map((status) => ({
68
+ value: status,
69
+ label: t(`status.${status}`, status),
70
+ badge: (forms.value ?? []).filter((form) => form.status === status).length
71
+ })));
72
+ const visibleForms = computed(() => {
73
+ const query = search.value.trim().toLowerCase();
74
+ return (forms.value ?? []).filter(
75
+ (form) => form.status === tab.value && (!query || form.name.toLowerCase().includes(query) || form.slug.toLowerCase().includes(query))
76
+ );
77
+ });
50
78
  </script>
51
79
 
52
80
  <template>
53
- <UContainer class="space-y-6 py-10">
54
- <div class="flex items-center justify-between gap-2">
55
- <h1 class="text-3xl font-bold">
56
- {{ t("admin.title", "Forms") }}
57
- </h1>
58
- <div class="flex gap-2">
81
+ <AdminPage>
82
+ <AdminPageHeader :title="t('admin.title', 'Forms')">
83
+ <template #actions>
59
84
  <UButton
60
85
  color="neutral"
61
86
  variant="outline"
@@ -68,8 +93,8 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
68
93
  :label="t('admin.new', 'New form')"
69
94
  @click="creating = true"
70
95
  />
71
- </div>
72
- </div>
96
+ </template>
97
+ </AdminPageHeader>
73
98
 
74
99
  <UAlert
75
100
  v-if="error"
@@ -78,61 +103,98 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
78
103
  :title="t('admin.forbidden', 'You are not allowed to manage forms.')"
79
104
  />
80
105
 
81
- <UEmpty
82
- v-else-if="!forms?.length"
83
- icon="i-lucide-clipboard-list"
84
- :title="t('admin.empty', 'No forms yet.')"
85
- />
86
-
87
106
  <div
88
107
  v-else
89
- class="divide-y divide-default rounded-lg border border-default"
108
+ class="flex flex-wrap items-center justify-between gap-3"
90
109
  >
91
- <div
92
- v-for="form in forms"
93
- :key="form.id"
94
- class="flex flex-wrap items-center justify-between gap-3 p-4"
95
- >
96
- <div class="space-y-1">
97
- <NuxtLink
98
- :to="paths('adminForm', { id: form.id })"
99
- class="font-medium hover:underline"
100
- >
101
- {{ form.name }}
102
- </NuxtLink>
110
+ <UTabs
111
+ v-model="tab"
112
+ :items="tabs"
113
+ :content="false"
114
+ class="w-auto"
115
+ />
116
+ <UInput
117
+ v-model="search"
118
+ class="w-full sm:w-72"
119
+ icon="i-lucide-search"
120
+ :placeholder="t('admin.search', 'Search forms')"
121
+ :aria-label="t('admin.search', 'Search forms')"
122
+ />
123
+ </div>
124
+
125
+ <AdminTable
126
+ v-if="!error"
127
+ :data="visibleForms"
128
+ :columns="columns"
129
+ @select="(_, row2) => navigateTo(paths('adminForm', { id: row2.original.id }))"
130
+ >
131
+ <template #name-cell="{ row }">
132
+ <div class="space-y-0.5">
133
+ <p class="font-medium">
134
+ {{ row.original.name }}
135
+ </p>
103
136
  <p class="text-sm text-muted">
104
- /{{ form.slug }} · {{ form.mode === "stepped" ? t("admin.stepped", "Stepped") : t("admin.single", "Single page") }}
105
- · v{{ form.version }}
106
- <template v-if="form.storage === 'none'">
137
+ /{{ row.original.slug }} · {{ row.original.mode === "stepped" ? t("admin.stepped", "Stepped") : t("admin.single", "Single page") }}
138
+ · v{{ row.original.version }}
139
+ <template v-if="row.original.storage === 'none'">
107
140
  · {{ t("admin.notStored", "Not stored") }}
108
141
  </template>
109
142
  </p>
110
143
  </div>
111
- <div class="flex items-center gap-3">
112
- <UBadge
113
- :color="statusColor[form.status]"
114
- variant="subtle"
115
- :label="t(`status.${form.status}`, form.status)"
116
- />
144
+ </template>
145
+ <template #status-cell="{ row }">
146
+ <UBadge
147
+ :color="statusColor[row.original.status]"
148
+ variant="subtle"
149
+ :label="t(`status.${row.original.status}`, row.original.status)"
150
+ />
151
+ </template>
152
+ <template #actions-cell="{ row }">
153
+ <div class="flex items-center justify-end gap-2">
117
154
  <UButton
118
- v-if="form.storage === 'db'"
155
+ v-if="row.original.storage === 'db' || row.original.submissionCount > 0"
119
156
  color="neutral"
120
157
  variant="outline"
121
158
  size="sm"
122
- :to="paths('adminSubmissions', { id: form.id })"
123
- :label="t('admin.submissions', 'Submissions ({count})', { count: form.submissionCount })"
159
+ :to="paths('adminSubmissions', { id: row.original.id })"
160
+ :label="t('admin.submissions', 'Submissions ({count})', { count: row.original.submissionCount })"
161
+ @click.stop
162
+ />
163
+ <UButton
164
+ v-if="row.original.status === 'published' && link.enabled.value"
165
+ color="neutral"
166
+ variant="ghost"
167
+ size="sm"
168
+ icon="i-lucide-link"
169
+ :aria-label="t('admin.copyLink', 'Copy link')"
170
+ @click.stop="link.copy(row.original.slug)"
171
+ />
172
+ <UButton
173
+ color="neutral"
174
+ variant="ghost"
175
+ size="sm"
176
+ icon="i-lucide-copy"
177
+ :aria-label="t('admin.clone', 'Duplicate')"
178
+ @click="clone(form)"
124
179
  />
125
180
  <UButton
126
181
  color="neutral"
127
182
  variant="ghost"
128
183
  size="sm"
129
184
  icon="i-lucide-pencil"
130
- :to="paths('adminForm', { id: form.id })"
185
+ :to="paths('adminForm', { id: row.original.id })"
131
186
  :aria-label="t('admin.edit', 'Edit')"
187
+ @click.stop
132
188
  />
133
189
  </div>
134
- </div>
135
- </div>
190
+ </template>
191
+ <template #empty>
192
+ <AdminEmpty
193
+ icon="i-lucide-clipboard-list"
194
+ :title="forms?.length ? t('admin.noMatches', 'No forms match.') : t('admin.empty', 'No forms yet.')"
195
+ />
196
+ </template>
197
+ </AdminTable>
136
198
 
137
199
  <UModal
138
200
  v-model:open="creating"
@@ -140,6 +202,7 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
140
202
  >
141
203
  <template #body>
142
204
  <form
205
+ id="admin-form-create"
143
206
  class="space-y-4"
144
207
  @submit.prevent="create"
145
208
  >
@@ -159,12 +222,23 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
159
222
  class="w-full"
160
223
  />
161
224
  </UFormField>
225
+ </form>
226
+ </template>
227
+ <template #footer>
228
+ <div class="flex w-full justify-end gap-2">
229
+ <UButton
230
+ variant="subtle"
231
+ color="neutral"
232
+ :label="t('admin.cancel', 'Cancel')"
233
+ @click="creating = false"
234
+ />
162
235
  <UButton
163
236
  type="submit"
237
+ form="admin-form-create"
164
238
  :label="t('admin.create', 'Create')"
165
239
  :disabled="!draft.name || !draft.slug"
166
240
  />
167
- </form>
241
+ </div>
168
242
  </template>
169
243
  </UModal>
170
244
 
@@ -182,6 +256,16 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
182
256
  :rows="12"
183
257
  class="w-full font-mono text-xs"
184
258
  />
259
+ </div>
260
+ </template>
261
+ <template #footer>
262
+ <div class="flex w-full justify-end gap-2">
263
+ <UButton
264
+ variant="subtle"
265
+ color="neutral"
266
+ :label="t('admin.cancel', 'Cancel')"
267
+ @click="importing = false"
268
+ />
185
269
  <UButton
186
270
  :label="t('admin.import', 'Import')"
187
271
  :disabled="!importJson.trim()"
@@ -190,5 +274,5 @@ const statusColor = { draft: "neutral", published: "success", archived: "warning
190
274
  </div>
191
275
  </template>
192
276
  </UModal>
193
- </UContainer>
277
+ </AdminPage>
194
278
  </template>
@@ -1,18 +1,84 @@
1
1
  <script setup>
2
2
  const route = useRoute();
3
+ const t = useModuleT("forms");
3
4
  const slug = computed(() => String(route.params.slug));
4
- const { data: form } = await useForm(slug);
5
- useHead({ title: computed(() => form.value?.name ?? "") });
5
+ const { data: access } = await useFormAccess(slug);
6
+ if (access.value && (access.value.access.availability === "api" || access.value.access.auth === "token")) {
7
+ throw createError({ statusCode: 404, statusMessage: "Form not found.", fatal: true });
8
+ }
9
+ const needsSignIn = computed(() => access.value?.access.auth === "user" && !access.value.signedIn);
10
+ if (needsSignIn.value) {
11
+ const loginPath = useAuthPages().login;
12
+ await navigateTo({ path: loginPath, query: { redirect: route.path } });
13
+ }
14
+ const gated = computed(() => access.value?.access.auth === "password");
15
+ const password = ref("");
16
+ const unlocked = ref("");
17
+ const checking = ref(false);
18
+ const wrong = ref(false);
19
+ async function unlock() {
20
+ checking.value = true;
21
+ wrong.value = false;
22
+ try {
23
+ await $fetch(`/api/forms/${slug.value}`, { headers: { [FORM_PASSWORD_HEADER]: password.value } });
24
+ unlocked.value = password.value;
25
+ } catch {
26
+ wrong.value = true;
27
+ } finally {
28
+ checking.value = false;
29
+ }
30
+ }
31
+ useHead({ title: computed(() => access.value?.name ?? "") });
6
32
  </script>
7
33
 
8
34
  <template>
9
35
  <UContainer class="max-w-2xl py-10">
10
36
  <h1
11
- v-if="form"
37
+ v-if="access"
12
38
  class="mb-6 text-3xl font-bold"
13
39
  >
14
- {{ form.name }}
40
+ {{ access.name }}
15
41
  </h1>
16
- <FormRenderer :slug="slug" />
42
+
43
+ <UAlert
44
+ v-if="access && !access.open"
45
+ icon="i-lucide-calendar-x"
46
+ color="neutral"
47
+ variant="subtle"
48
+ :title="t('schedule.closed', 'This form is closed.')"
49
+ :description="access.opensAt ? t('schedule.opensAt', 'It opens again {date}.', { date: new Date(access.opensAt).toLocaleString() }) : void 0"
50
+ />
51
+
52
+ <UCard v-else-if="gated && !unlocked">
53
+ <form
54
+ class="space-y-4"
55
+ @submit.prevent="unlock"
56
+ >
57
+ <UFormField
58
+ :label="t('access.passwordLabel', 'Password')"
59
+ :description="t('access.passwordHint', 'This form is protected. Enter the password you were given.')"
60
+ :error="wrong ? t('access.passwordWrong', 'Wrong password.') : void 0"
61
+ >
62
+ <UInput
63
+ v-model="password"
64
+ type="password"
65
+ autocomplete="off"
66
+ class="w-full"
67
+ />
68
+ </UFormField>
69
+ <UButton
70
+ type="submit"
71
+ :loading="checking"
72
+ :disabled="!password"
73
+ :label="t('access.unlock', 'Continue')"
74
+ />
75
+ </form>
76
+ </UCard>
77
+
78
+ <FormRenderer
79
+ v-else-if="!needsSignIn"
80
+ :slug="slug"
81
+ :password="unlocked || void 0"
82
+ />
17
83
  </UContainer>
18
84
  </template>