@frigear-nu/forms 0.0.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +7 -5
  3. package/dist/runtime/app/components/FormBuilderAccess.vue +6 -13
  4. package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +2 -2
  5. package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +2 -2
  6. package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +2 -2
  7. package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +2 -2
  8. package/dist/runtime/app/components/FormCreateSlideover.d.vue.ts +8 -0
  9. package/dist/runtime/app/components/FormCreateSlideover.vue +73 -0
  10. package/dist/runtime/app/components/FormCreateSlideover.vue.d.ts +8 -0
  11. package/dist/runtime/app/components/FormPreview.vue +2 -8
  12. package/dist/runtime/app/composables/index.d.ts +1 -0
  13. package/dist/runtime/app/composables/index.js +1 -0
  14. package/dist/runtime/app/composables/useFormActions.d.ts +26 -0
  15. package/dist/runtime/app/composables/useFormActions.js +103 -0
  16. package/dist/runtime/app/composables/useForms.d.ts +1 -1
  17. package/dist/runtime/app/composables/useForms.js +2 -9
  18. package/dist/runtime/app/pages/admin-form.vue +84 -82
  19. package/dist/runtime/app/pages/admin-submission.vue +60 -29
  20. package/dist/runtime/app/pages/admin-submissions.vue +81 -47
  21. package/dist/runtime/app/pages/admin.vue +152 -233
  22. package/dist/runtime/app/pages/form.vue +47 -45
  23. package/dist/runtime/i18n/lang/da.json +31 -10
  24. package/dist/runtime/i18n/lang/en.json +31 -10
  25. package/dist/runtime/menus.js +0 -11
  26. package/dist/runtime/server/db/schema.postgresql.d.ts +741 -0
  27. package/dist/runtime/server/db/schema.postgresql.js +78 -0
  28. package/package.json +17 -17
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/forms",
3
3
  "configKey": "forms",
4
- "version": "0.0.8",
4
+ "version": "0.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -23,10 +23,10 @@ const module$1 = defineNuxtModule({
23
23
  "@frigear-nu/notify": { version: ">=0.0.1" },
24
24
  "@frigear-nu/ui": { version: ">=0.0.1" },
25
25
  "@frigear-nu/menu": { version: ">=0.0.1", optional: true },
26
- // Provides the shared page components of the admin pages (`AdminPage`, `AdminTable`, ...).
26
+ // The admin pages only exist with the admin shell; its page components come from `@frigear-nu/ui`.
27
27
  "@frigear-nu/admin": { version: ">=0.0.0", optional: true }
28
28
  },
29
- async setup(options, nuxt) {
29
+ setup(options, nuxt) {
30
30
  registerModule(MODULE_NAME, nuxt);
31
31
  addAuthScope(
32
32
  nuxt,
@@ -71,7 +71,7 @@ const module$1 = defineNuxtModule({
71
71
  defaultProvider: options.captcha?.defaultProvider ?? "turnstile"
72
72
  }
73
73
  };
74
- await registerDatabaseTable(nuxt, {
74
+ registerDatabaseTable(nuxt, {
75
75
  resolver,
76
76
  schema: (dialect) => `./runtime/server/db/schema.${dialect}`
77
77
  });
@@ -114,7 +114,9 @@ const module$1 = defineNuxtModule({
114
114
  { name: "useFormsLocales", from: resolver.resolve("./runtime/app/composables/useForms") },
115
115
  { name: "useFormsPaths", from: resolver.resolve("./runtime/app/composables/useForms") },
116
116
  { name: "useFormLink", from: resolver.resolve("./runtime/app/composables/useForms") },
117
- { name: "useFormStepper", from: resolver.resolve("./runtime/app/composables/useFormStepper") }
117
+ { name: "useFormStepper", from: resolver.resolve("./runtime/app/composables/useFormStepper") },
118
+ { name: "useFormActions", from: resolver.resolve("./runtime/app/composables/useFormActions") },
119
+ { name: "FORMS_LIST_KEY", from: resolver.resolve("./runtime/app/composables/useFormActions") }
118
120
  ]);
119
121
  addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
120
122
  addComponent({ name: "FormEmbed", filePath: resolver.resolve("./runtime/app/components/FormEmbed.vue"), global: true });
@@ -125,7 +127,7 @@ const module$1 = defineNuxtModule({
125
127
  { name: "forms-admin-submissions", path: adminSubmissionsPath, file: "./runtime/app/pages/admin-submissions.vue" },
126
128
  { name: "forms-admin-submission", path: adminSubmissionPath, file: "./runtime/app/pages/admin-submission.vue" }
127
129
  ]);
128
- addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "dashboard.account" });
130
+ addOptionalMenuSource(nuxt, { file: resolver.resolve("./runtime/menus"), menu: "admin.nav" });
129
131
  }
130
132
  });
131
133
 
@@ -7,6 +7,9 @@ const props = defineProps({
7
7
  });
8
8
  const t = useModuleT("forms");
9
9
  const toast = useToast();
10
+ const showError = useErrorToast();
11
+ const { copy: copyText } = useCopyToClipboard();
12
+ const format = useFormat();
10
13
  const base = computed(() => `/api/forms/admin/forms/${props.formId}`);
11
14
  const access = computed(() => resolveAccess(definition.value));
12
15
  const availabilities = computed(() => [
@@ -46,10 +49,7 @@ const fromLocalInput = (value) => value ? new Date(value).toISOString() : void 0
46
49
  function patchMoment(side, patch) {
47
50
  if (schedule.value.type === "yearly") setSchedule({ ...schedule.value, [side]: { ...schedule.value[side], ...patch } });
48
51
  }
49
- const fail = (cause) => toast.add({
50
- title: cause.statusMessage ?? t("error.generic", "Something went wrong. Please try again."),
51
- color: "error"
52
- });
52
+ const fail = (cause) => showError(cause, t("error.generic", "Something went wrong. Please try again."));
53
53
  const { data: tokens, refresh: refreshTokens } = await useFetch(() => `${base.value}/tokens`, { default: () => [] });
54
54
  const tokenName = ref("");
55
55
  const creating = ref(false);
@@ -74,14 +74,7 @@ async function revoke(token) {
74
74
  fail(cause);
75
75
  }
76
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
- }
77
+ const copy = (text) => copyText(text, t("preview.copied", "Copied to clipboard."));
85
78
  const { data: passwordStatus, refresh: refreshPassword } = await useFetch(() => `${base.value}/password`, { default: () => ({ set: false }) });
86
79
  const password = ref("");
87
80
  const savingPassword = ref(false);
@@ -315,7 +308,7 @@ ${headers} -H 'content-type: application/json' \\
315
308
  <p class="text-xs text-muted">
316
309
  <code>{{ token.prefix }}…</code>
317
310
  ·
318
- {{ token.lastUsedAt ? t("access.lastUsed", "Last used {date}", { date: new Date(token.lastUsedAt).toLocaleString() }) : t("access.neverUsed", "Never used") }}
311
+ {{ token.lastUsedAt ? t("access.lastUsed", "Last used {date}", { date: format.dateTime(token.lastUsedAt) }) : t("access.neverUsed", "Never used") }}
319
312
  </p>
320
313
  </div>
321
314
  <UButton
@@ -6,18 +6,18 @@ type __VLS_Props = {
6
6
  };
7
7
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
8
  select: () => any;
9
+ dragend: () => any;
9
10
  dragover: (event: DragEvent) => any;
10
11
  dragstart: (event: DragEvent) => any;
11
- dragend: () => any;
12
12
  up: () => any;
13
13
  down: () => any;
14
14
  remove: () => any;
15
15
  duplicate: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onSelect?: (() => any) | undefined;
18
+ onDragend?: (() => any) | undefined;
18
19
  onDragover?: ((event: DragEvent) => any) | undefined;
19
20
  onDragstart?: ((event: DragEvent) => any) | undefined;
20
- onDragend?: (() => any) | undefined;
21
21
  onUp?: (() => any) | undefined;
22
22
  onDown?: (() => any) | undefined;
23
23
  onRemove?: (() => any) | undefined;
@@ -6,18 +6,18 @@ type __VLS_Props = {
6
6
  };
7
7
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
8
  select: () => any;
9
+ dragend: () => any;
9
10
  dragover: (event: DragEvent) => any;
10
11
  dragstart: (event: DragEvent) => any;
11
- dragend: () => any;
12
12
  up: () => any;
13
13
  down: () => any;
14
14
  remove: () => any;
15
15
  duplicate: () => any;
16
16
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
17
  onSelect?: (() => any) | undefined;
18
+ onDragend?: (() => any) | undefined;
18
19
  onDragover?: ((event: DragEvent) => any) | undefined;
19
20
  onDragstart?: ((event: DragEvent) => any) | undefined;
20
- onDragend?: (() => any) | undefined;
21
21
  onUp?: (() => any) | undefined;
22
22
  onDown?: (() => any) | undefined;
23
23
  onRemove?: (() => any) | undefined;
@@ -10,15 +10,15 @@ type __VLS_Props = {
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  select: () => any;
13
- dragstart: (event: DragEvent) => any;
14
13
  dragend: () => any;
14
+ dragstart: (event: DragEvent) => any;
15
15
  up: () => any;
16
16
  down: () => any;
17
17
  remove: () => any;
18
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
19
19
  onSelect?: (() => any) | undefined;
20
- onDragstart?: ((event: DragEvent) => any) | undefined;
21
20
  onDragend?: (() => any) | undefined;
21
+ onDragstart?: ((event: DragEvent) => any) | undefined;
22
22
  onUp?: (() => any) | undefined;
23
23
  onDown?: (() => any) | undefined;
24
24
  onRemove?: (() => any) | undefined;
@@ -10,15 +10,15 @@ type __VLS_Props = {
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
12
  select: () => any;
13
- dragstart: (event: DragEvent) => any;
14
13
  dragend: () => any;
14
+ dragstart: (event: DragEvent) => any;
15
15
  up: () => any;
16
16
  down: () => any;
17
17
  remove: () => any;
18
18
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
19
19
  onSelect?: (() => any) | undefined;
20
- onDragstart?: ((event: DragEvent) => any) | undefined;
21
20
  onDragend?: (() => any) | undefined;
21
+ onDragstart?: ((event: DragEvent) => any) | undefined;
22
22
  onUp?: (() => any) | undefined;
23
23
  onDown?: (() => any) | undefined;
24
24
  onRemove?: (() => any) | undefined;
@@ -0,0 +1,8 @@
1
+ import type { FormAdminDto } from '../../../types.js';
2
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
3
+ close: (value: false | FormAdminDto) => any;
4
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
5
+ onClose?: ((value: false | FormAdminDto) => any) | undefined;
6
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -0,0 +1,73 @@
1
+ <script setup>
2
+ import { z } from "zod";
3
+ import { defineForm } from "../../utils";
4
+ const emit = defineEmits(["close"]);
5
+ const t = useModuleT("forms");
6
+ const { busy, run } = useAction();
7
+ const starter = defineForm({
8
+ mode: "single",
9
+ storage: "db",
10
+ steps: [{
11
+ id: "main",
12
+ fields: [
13
+ { key: "name", type: "text", label: "Name", required: true },
14
+ { key: "message", type: "textarea", label: "Message", required: true }
15
+ ]
16
+ }]
17
+ });
18
+ const state = reactive({ name: "", slug: "" });
19
+ const schema = z.object({
20
+ name: z.string().trim().min(1, t("error.invalid", "Please check the highlighted fields.")),
21
+ slug: z.string().trim().min(1, t("error.invalid", "Please check the highlighted fields."))
22
+ });
23
+ const slugify = (value) => value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
24
+ watch(() => state.name, (name, previous) => {
25
+ if (state.slug === slugify(previous)) state.slug = slugify(name);
26
+ });
27
+ async function save() {
28
+ const created = await run(
29
+ () => $fetch("/api/forms/admin/forms", { method: "POST", body: { ...state, definition: starter } }),
30
+ { success: t("admin.created", "Form created") }
31
+ );
32
+ if (created) emit("close", created);
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <FormSlideover
38
+ form-id="forms-create"
39
+ :title="t('admin.new', 'New form')"
40
+ :submit-label="t('admin.createForm', 'Create form')"
41
+ :loading="busy"
42
+ @close="emit('close', false)"
43
+ >
44
+ <UForm
45
+ id="forms-create"
46
+ :state="state"
47
+ :schema="schema"
48
+ class="flex flex-col gap-4"
49
+ @submit="save"
50
+ >
51
+ <UFormField
52
+ name="name"
53
+ :label="t('admin.name', 'Name')"
54
+ >
55
+ <UInput
56
+ v-model="state.name"
57
+ class="w-full"
58
+ autofocus
59
+ />
60
+ </UFormField>
61
+ <UFormField
62
+ name="slug"
63
+ :label="t('admin.slug', 'Slug')"
64
+ :description="t('admin.slugHint', 'Used in the public address.')"
65
+ >
66
+ <UInput
67
+ v-model="state.slug"
68
+ class="w-full"
69
+ />
70
+ </UFormField>
71
+ </UForm>
72
+ </FormSlideover>
73
+ </template>
@@ -0,0 +1,8 @@
1
+ import type { FormAdminDto } from '../../../types.js';
2
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
3
+ close: (value: false | FormAdminDto) => any;
4
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
5
+ onClose?: ((value: false | FormAdminDto) => any) | undefined;
6
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -7,7 +7,7 @@ const props = defineProps({
7
7
  contentKey: { type: [String, Number], required: false }
8
8
  });
9
9
  const t = useModuleT("forms");
10
- const toast = useToast();
10
+ const { copy: copyText } = useCopyToClipboard();
11
11
  const paths = useFormsPaths();
12
12
  const path = computed(() => paths("form", { slug: props.slug }));
13
13
  const STORAGE_KEY = "forms:preview-device";
@@ -58,13 +58,7 @@ function tryAgain() {
58
58
  watch(open, (value) => {
59
59
  if (value) tryAgain();
60
60
  });
61
- async function copy() {
62
- try {
63
- await navigator.clipboard.writeText(json.value);
64
- toast.add({ title: t("preview.copied", "Copied to clipboard."), color: "success" });
65
- } catch {
66
- }
67
- }
61
+ const copy = () => copyText(json.value, t("preview.copied", "Copied to clipboard."));
68
62
  </script>
69
63
 
70
64
  <template>
@@ -1,4 +1,5 @@
1
1
  export { useForm, useFormAccess, useFormsLocale, useFormsLocales, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from './useForms.js';
2
+ export { useFormActions, FORMS_LIST_KEY } from './useFormActions.js';
2
3
  export { useFormStepper } from './useFormStepper.js';
3
4
  export { useFormBuilderEditor } from './useFormBuilderEditor.js';
4
5
  export { useFormBuilderLabels, fieldTypeIcons } from './useFormBuilderLabels.js';
@@ -1,4 +1,5 @@
1
1
  export { useForm, useFormAccess, useFormsLocale, useFormsLocales, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from "./useForms.js";
2
+ export { useFormActions, FORMS_LIST_KEY } from "./useFormActions.js";
2
3
  export { useFormStepper } from "./useFormStepper.js";
3
4
  export { useFormBuilderEditor } from "./useFormBuilderEditor.js";
4
5
  export { useFormBuilderLabels, fieldTypeIcons } from "./useFormBuilderLabels.js";
@@ -0,0 +1,26 @@
1
+ import type { DropdownMenuItem } from '@nuxt/ui';
2
+ import type { FormAdminDto, FormStatus, FormStorage } from '../../../types.js';
3
+ /** What the actions need to know about a form: a list row or the loaded form fits. */
4
+ export interface FormActionTarget {
5
+ id: string;
6
+ name: string;
7
+ slug: string;
8
+ status: FormStatus;
9
+ storage?: FormStorage;
10
+ submissionCount?: number;
11
+ }
12
+ /** The cache key of the admin form list, so every action can refresh it. */
13
+ export declare const FORMS_LIST_KEY = "forms:admin:forms";
14
+ /** Create, duplicate, archive, import and redeliver for forms, plus the `⋯` menu every list and detail page uses. */
15
+ export declare function useFormActions(): {
16
+ busy: any;
17
+ create: () => Promise<FormAdminDto | false>;
18
+ clone: (form: Pick<FormActionTarget, "id">) => Promise<boolean>;
19
+ remove: (form: Pick<FormActionTarget, "id" | "name">) => Promise<boolean>;
20
+ importJson: (text: string) => Promise<boolean>;
21
+ redeliver: (submissionId: string) => Promise<boolean>;
22
+ menu: (form: FormActionTarget, options?: {
23
+ extra?: DropdownMenuItem[];
24
+ onRemoved?: () => void | Promise<void>;
25
+ }) => DropdownMenuItem[][];
26
+ };
@@ -0,0 +1,103 @@
1
+ import { LazyFormCreateSlideover } from "#components";
2
+ export const FORMS_LIST_KEY = "forms:admin:forms";
3
+ export function useFormActions() {
4
+ const t = useModuleT("forms");
5
+ const paths = useFormsPaths();
6
+ const link = useFormLink();
7
+ const overlay = useOverlay();
8
+ const confirm = useConfirmDialog();
9
+ const toast = useToast();
10
+ const showError = useErrorToast();
11
+ const { busy, run } = useAction();
12
+ const creator = overlay.create(LazyFormCreateSlideover, { destroyOnClose: true });
13
+ async function create() {
14
+ const created = await creator.open().result;
15
+ if (created) await navigateTo(paths("adminForm", { id: created.id }));
16
+ return created;
17
+ }
18
+ async function clone(form) {
19
+ const copy = await run(
20
+ () => $fetch(`/api/forms/admin/forms/${form.id}/clone`, { method: "POST" }),
21
+ { success: t("admin.cloned", "Form duplicated") }
22
+ );
23
+ if (!copy) return false;
24
+ await navigateTo(paths("adminForm", { id: copy.id }));
25
+ return true;
26
+ }
27
+ async function remove(form) {
28
+ const confirmed = await confirm({
29
+ title: t("admin.archiveTitle", "Archive {name}?", { name: form.name }),
30
+ description: t("admin.archiveBody", "It stops accepting submissions and disappears from the public site. Its submissions are kept."),
31
+ confirmText: t("admin.archive", "Archive form"),
32
+ destructive: true
33
+ });
34
+ if (!confirmed) return false;
35
+ const archived = await run(
36
+ () => $fetch(`/api/forms/admin/forms/${form.id}`, { method: "DELETE" }),
37
+ { success: t("admin.archived", "Form archived") }
38
+ );
39
+ if (!archived) return false;
40
+ await refreshNuxtData(FORMS_LIST_KEY);
41
+ return true;
42
+ }
43
+ async function importJson(text) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(text);
47
+ } catch {
48
+ toast.add({ title: t("admin.invalidJson", "The JSON is not valid."), color: "error" });
49
+ return false;
50
+ }
51
+ const result = await run(() => $fetch("/api/forms/admin/import", {
52
+ method: "POST",
53
+ body: { forms: Array.isArray(parsed) ? parsed : [parsed] }
54
+ }));
55
+ if (!result) return false;
56
+ toast.add({
57
+ title: t("admin.imported", "Imported {inserted}, skipped {skipped} existing.", { inserted: result.inserted.length, skipped: result.skipped.length }),
58
+ color: "success"
59
+ });
60
+ await refreshNuxtData(FORMS_LIST_KEY);
61
+ return true;
62
+ }
63
+ async function redeliver(submissionId) {
64
+ const result = await run(() => $fetch(
65
+ `/api/forms/admin/submissions/${submissionId}/redeliver`,
66
+ { method: "POST" }
67
+ ));
68
+ if (!result) return false;
69
+ if (result.deliveryStatus === "failed") {
70
+ showError(new Error(result.deliveryError ?? ""), t("delivery.failed", "Delivery failed"));
71
+ return false;
72
+ }
73
+ toast.add({ title: t("admin.redelivered", "Delivered again"), color: "success", icon: "i-lucide-check" });
74
+ return true;
75
+ }
76
+ function menu(form, options = {}) {
77
+ const items = [...options.extra ?? []];
78
+ if (form.storage === "db" || (form.submissionCount ?? 0) > 0) {
79
+ items.push({
80
+ label: t("admin.submissionsTitle", "Submissions"),
81
+ icon: "i-lucide-inbox",
82
+ to: paths("adminSubmissions", { id: form.id })
83
+ });
84
+ }
85
+ if (form.status === "published" && link.enabled.value) {
86
+ items.push({ label: t("admin.copyLink", "Copy link"), icon: "i-lucide-link", onSelect: () => link.copy(form.slug) });
87
+ }
88
+ items.push({ label: t("admin.clone", "Duplicate form"), icon: "i-lucide-copy", onSelect: () => clone(form) });
89
+ const groups = [items];
90
+ if (form.status !== "archived") {
91
+ groups.push([{
92
+ label: t("admin.archive", "Archive form"),
93
+ icon: "i-lucide-archive",
94
+ color: "error",
95
+ onSelect: async () => {
96
+ if (await remove(form)) await options.onRemoved?.();
97
+ }
98
+ }]);
99
+ }
100
+ return groups;
101
+ }
102
+ return { busy, create, clone, remove, importJson, redeliver, menu };
103
+ }
@@ -29,7 +29,7 @@ export declare function useFormsPaths(): (name: keyof typeof DEFAULT_PATHS, para
29
29
  export declare function useFormLink(): {
30
30
  enabled: any;
31
31
  url: (slug: string) => string;
32
- copy: (slug: string) => Promise<void>;
32
+ copy: (slug: string) => any;
33
33
  };
34
34
  /** Label of a field in the given locale, falling back to its key. */
35
35
  export declare const fieldLabel: (field: FormField, locale: string) => string;
@@ -67,19 +67,12 @@ export function useFormsPaths() {
67
67
  export function useFormLink() {
68
68
  const paths = useFormsPaths();
69
69
  const origin = useRequestURL().origin;
70
- const toast = useToast();
70
+ const { copy: copyText } = useCopyToClipboard();
71
71
  const t = useModuleT("forms");
72
72
  const pages = useRuntimeConfig().public.forms?.pages;
73
73
  const enabled = computed(() => !!pages?.form);
74
74
  const url = (slug) => `${origin}${paths("form", { slug })}`;
75
- async function copy(slug) {
76
- try {
77
- await navigator.clipboard.writeText(url(slug));
78
- toast.add({ title: t("admin.linkCopied", "Link copied."), color: "success" });
79
- } catch {
80
- toast.add({ title: t("error.generic", "Something went wrong. Please try again."), color: "error" });
81
- }
82
- }
75
+ const copy = (slug) => copyText(url(slug), t("admin.linkCopied", "Link copied."));
83
76
  return { enabled, url, copy };
84
77
  }
85
78
  export const fieldLabel = (field, locale) => translate(field.label, locale) || field.key;