@frigear-nu/forms 0.0.9 → 0.1.1
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.
- package/dist/module.json +1 -1
- package/dist/module.mjs +7 -5
- package/dist/runtime/app/components/FormBuilderAccess.vue +6 -13
- package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +2 -2
- package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +2 -2
- package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +2 -2
- package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +2 -2
- package/dist/runtime/app/components/FormCreateSlideover.d.vue.ts +8 -0
- package/dist/runtime/app/components/FormCreateSlideover.vue +73 -0
- package/dist/runtime/app/components/FormCreateSlideover.vue.d.ts +8 -0
- package/dist/runtime/app/components/FormPreview.vue +2 -8
- package/dist/runtime/app/composables/index.d.ts +1 -0
- package/dist/runtime/app/composables/index.js +1 -0
- package/dist/runtime/app/composables/useFormActions.d.ts +26 -0
- package/dist/runtime/app/composables/useFormActions.js +103 -0
- package/dist/runtime/app/composables/useForms.d.ts +1 -1
- package/dist/runtime/app/composables/useForms.js +2 -9
- package/dist/runtime/app/pages/admin-form.vue +84 -82
- package/dist/runtime/app/pages/admin-submission.vue +60 -29
- package/dist/runtime/app/pages/admin-submissions.vue +81 -47
- package/dist/runtime/app/pages/admin.vue +152 -233
- package/dist/runtime/app/pages/form.vue +47 -45
- package/dist/runtime/i18n/lang/da.json +31 -10
- package/dist/runtime/i18n/lang/en.json +31 -10
- package/dist/runtime/menus.js +0 -11
- package/package.json +17 -17
package/dist/module.json
CHANGED
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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) =>
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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;
|