@frigear-nu/forms 0.0.5 → 0.0.7
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/README.md +16 -1
- package/dist/module.d.mts +148 -8
- package/dist/module.json +1 -1
- package/dist/module.mjs +64 -35
- package/dist/runtime/app/components/FormBuilder.d.vue.ts +26 -0
- package/dist/runtime/app/components/FormBuilder.vue +220 -0
- package/dist/runtime/app/components/FormBuilder.vue.d.ts +26 -0
- package/dist/runtime/app/components/FormBuilderAccess.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue +404 -0
- package/dist/runtime/app/components/FormBuilderAccess.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.d.vue.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue +126 -0
- package/dist/runtime/app/components/FormBuilderCaptchaSettings.vue.d.ts +15 -0
- package/dist/runtime/app/components/FormBuilderCondition.d.vue.ts +16 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue +62 -0
- package/dist/runtime/app/components/FormBuilderCondition.vue.d.ts +16 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue +87 -0
- package/dist/runtime/app/components/FormBuilderFieldRow.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.d.vue.ts +25 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue +170 -0
- package/dist/runtime/app/components/FormBuilderFieldSettings.vue.d.ts +25 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue +51 -0
- package/dist/runtime/app/components/FormBuilderInsertRow.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormBuilderOptions.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue +81 -0
- package/dist/runtime/app/components/FormBuilderOptions.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue +63 -0
- package/dist/runtime/app/components/FormBuilderPhoneSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.d.vue.ts +11 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue +233 -0
- package/dist/runtime/app/components/FormBuilderSettings.vue.d.ts +11 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.d.vue.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue +71 -0
- package/dist/runtime/app/components/FormBuilderStepHeader.vue.d.ts +27 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.d.vue.ts +13 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue +33 -0
- package/dist/runtime/app/components/FormBuilderStepSettings.vue.d.ts +13 -0
- package/dist/runtime/app/components/FormBuilderText.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormBuilderText.vue +127 -0
- package/dist/runtime/app/components/FormBuilderText.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.d.vue.ts +19 -0
- package/dist/runtime/app/components/FormCaptcha.vue +95 -0
- package/dist/runtime/app/components/FormCaptcha.vue.d.ts +19 -0
- package/dist/runtime/app/components/FormEmbed.d.vue.ts +17 -0
- package/dist/runtime/app/components/FormEmbed.vue +113 -0
- package/dist/runtime/app/components/FormEmbed.vue.d.ts +17 -0
- package/dist/runtime/app/components/FormField.d.vue.ts +4 -0
- package/dist/runtime/app/components/FormField.vue +35 -3
- package/dist/runtime/app/components/FormField.vue.d.ts +4 -0
- package/dist/runtime/app/components/FormPhoneInput.d.vue.ts +20 -0
- package/dist/runtime/app/components/FormPhoneInput.vue +56 -0
- package/dist/runtime/app/components/FormPhoneInput.vue.d.ts +20 -0
- package/dist/runtime/app/components/FormPreview.d.vue.ts +12 -0
- package/dist/runtime/app/components/FormPreview.vue +244 -0
- package/dist/runtime/app/components/FormPreview.vue.d.ts +12 -0
- package/dist/runtime/app/components/FormRenderer.d.vue.ts +6 -0
- package/dist/runtime/app/components/FormRenderer.vue +16 -7
- package/dist/runtime/app/components/FormRenderer.vue.d.ts +6 -0
- package/dist/runtime/app/components/FormSubmissionView.vue +2 -2
- package/dist/runtime/app/composables/index.d.ts +4 -1
- package/dist/runtime/app/composables/index.js +4 -1
- package/dist/runtime/app/composables/useCaptcha.d.ts +22 -0
- package/dist/runtime/app/composables/useCaptcha.js +39 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.d.ts +7 -0
- package/dist/runtime/app/composables/useCaptchaSecrets.js +69 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.d.ts +49 -0
- package/dist/runtime/app/composables/useFormBuilderEditor.js +210 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.d.ts +8 -0
- package/dist/runtime/app/composables/useFormBuilderLabels.js +41 -0
- package/dist/runtime/app/composables/useForms.d.ts +18 -3
- package/dist/runtime/app/composables/useForms.js +38 -5
- package/dist/runtime/app/pages/admin-form.vue +112 -69
- package/dist/runtime/app/pages/admin-submission.vue +7 -16
- package/dist/runtime/app/pages/admin-submissions.vue +41 -46
- package/dist/runtime/app/pages/admin.vue +128 -44
- package/dist/runtime/app/pages/form.vue +71 -5
- package/dist/runtime/i18n/lang/da.json +237 -3
- package/dist/runtime/i18n/lang/en.json +237 -3
- package/dist/runtime/menus.js +12 -0
- package/dist/runtime/server/db/schema.sqlite.d.ts +283 -0
- package/dist/runtime/server/db/schema.sqlite.js +33 -1
- package/dist/runtime/server/mcp/schemas.d.ts +9 -0
- package/dist/runtime/server/mcp/schemas.js +8 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/create-form.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-form-definition.js +14 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/get-submission.js +26 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-all-forms.js +21 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/list-submissions.js +22 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/admin/update-form.js +24 -0
- package/dist/runtime/server/mcp/tools/get-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/get-form.js +12 -0
- package/dist/runtime/server/mcp/tools/list-forms.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/list-forms.js +11 -0
- package/dist/runtime/server/mcp/tools/submit-form.d.ts +2 -0
- package/dist/runtime/server/mcp/tools/submit-form.js +17 -0
- package/dist/runtime/server/plugins/email-delivery.js +16 -11
- package/dist/runtime/server/plugins/encryption.d.ts +2 -0
- package/dist/runtime/server/plugins/encryption.js +4 -0
- package/dist/runtime/server/plugins/staff-scopes.d.ts +2 -0
- package/dist/runtime/server/plugins/staff-scopes.js +1 -0
- package/dist/runtime/server/routes/admin/form-clone.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-clone.post.js +17 -0
- package/dist/runtime/server/routes/admin/form-password.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-password.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-password.put.js +14 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secret.delete.js +8 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.get.js +5 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-secrets.put.js +12 -0
- package/dist/runtime/server/routes/admin/form-token.delete.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-token.delete.js +12 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.get.js +8 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.d.ts +2 -0
- package/dist/runtime/server/routes/admin/form-tokens.post.js +12 -0
- package/dist/runtime/server/routes/admin/form.get.js +5 -1
- package/dist/runtime/server/routes/admin/form.patch.js +3 -12
- package/dist/runtime/server/routes/admin/forms.post.js +1 -6
- package/dist/runtime/server/routes/public/access.get.d.ts +2 -0
- package/dist/runtime/server/routes/public/access.get.js +3 -0
- package/dist/runtime/server/routes/public/form.get.js +3 -1
- package/dist/runtime/server/routes/public/submit.post.js +2 -2
- package/dist/runtime/server/utils/access.d.ts +27 -0
- package/dist/runtime/server/utils/access.js +63 -0
- package/dist/runtime/server/utils/admin-schemas.d.ts +219 -15
- package/dist/runtime/server/utils/auth.d.ts +7 -7
- package/dist/runtime/server/utils/auth.js +4 -4
- package/dist/runtime/server/utils/captcha.d.ts +10 -0
- package/dist/runtime/server/utils/captcha.js +25 -0
- package/dist/runtime/server/utils/crypto.d.ts +18 -0
- package/dist/runtime/server/utils/crypto.js +48 -0
- package/dist/runtime/server/utils/forms.d.ts +6 -1
- package/dist/runtime/server/utils/forms.js +28 -0
- package/dist/runtime/server/utils/mcp-access.d.ts +9 -0
- package/dist/runtime/server/utils/mcp-access.js +8 -0
- package/dist/runtime/server/utils/rate-limit.d.ts +3 -0
- package/dist/runtime/server/utils/rate-limit.js +19 -0
- package/dist/runtime/server/utils/render.d.ts +2 -11
- package/dist/runtime/server/utils/render.js +1 -26
- package/dist/runtime/server/utils/secrets.d.ts +28 -0
- package/dist/runtime/server/utils/secrets.js +60 -0
- package/dist/runtime/server/utils/submit.d.ts +12 -1
- package/dist/runtime/server/utils/submit.js +18 -4
- package/dist/runtime/utils/builder.d.ts +73 -0
- package/dist/runtime/utils/builder.js +220 -0
- package/dist/runtime/utils/captcha.d.ts +9 -0
- package/dist/runtime/utils/captcha.js +7 -0
- package/dist/runtime/utils/conditions.d.ts +3 -1
- package/dist/runtime/utils/conditions.js +4 -0
- package/dist/runtime/utils/definition.d.ts +116 -6
- package/dist/runtime/utils/definition.js +53 -4
- package/dist/runtime/utils/embed.d.ts +8 -0
- package/dist/runtime/utils/embed.js +7 -0
- package/dist/runtime/utils/index.d.ts +16 -3
- package/dist/runtime/utils/index.js +66 -3
- package/dist/runtime/utils/markdown.d.ts +1 -5
- package/dist/runtime/utils/markdown.js +1 -5
- package/dist/runtime/utils/phone.d.ts +29 -0
- package/dist/runtime/utils/phone.js +294 -0
- package/dist/runtime/utils/preview.d.ts +12 -0
- package/dist/runtime/utils/preview.js +17 -0
- package/dist/runtime/utils/schedule.d.ts +1 -0
- package/dist/runtime/utils/schedule.js +1 -0
- package/dist/runtime/utils/schema.d.ts +2 -0
- package/dist/runtime/utils/schema.js +12 -4
- package/dist/runtime/utils/template.d.ts +57 -0
- package/dist/runtime/utils/template.js +147 -0
- package/dist/runtime/utils/text.d.ts +0 -3
- package/dist/runtime/utils/text.js +0 -5
- package/dist/types.d.mts +5 -1
- package/package.json +14 -5
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { composePhone, countryName, defaultPhoneCountry, parsePhone, phoneCountries } from "../../utils";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
config: { type: Object, required: false },
|
|
5
|
+
locale: { type: String, required: true },
|
|
6
|
+
placeholder: { type: String, required: false },
|
|
7
|
+
countryLabel: { type: String, required: true }
|
|
8
|
+
});
|
|
9
|
+
const model = defineModel({ type: null });
|
|
10
|
+
const country = ref(parsePhone(model.value ?? "", props.config).country);
|
|
11
|
+
const national = ref(parsePhone(model.value ?? "", props.config, country.value).national);
|
|
12
|
+
const countries = computed(() => phoneCountries(props.config));
|
|
13
|
+
const items = computed(() => countries.value.map(({ code, dial, flag }) => ({
|
|
14
|
+
value: code,
|
|
15
|
+
label: `${flag} ${countryName(code, props.locale)} (+${dial})`,
|
|
16
|
+
short: `${flag} +${dial}`
|
|
17
|
+
})));
|
|
18
|
+
const current = computed(() => items.value.find((item) => item.value === country.value));
|
|
19
|
+
watch(model, (value) => {
|
|
20
|
+
if ((value ?? "") === composePhone(country.value, national.value)) return;
|
|
21
|
+
const parsed = parsePhone(value ?? "", props.config, country.value);
|
|
22
|
+
country.value = parsed.country;
|
|
23
|
+
national.value = parsed.national;
|
|
24
|
+
});
|
|
25
|
+
watch(() => props.config, () => {
|
|
26
|
+
if (!countries.value.some((item) => item.code === country.value)) country.value = defaultPhoneCountry(props.config);
|
|
27
|
+
}, { deep: true });
|
|
28
|
+
watch([country, national], () => {
|
|
29
|
+
model.value = composePhone(country.value, national.value) || void 0;
|
|
30
|
+
});
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<UFieldGroup class="w-full">
|
|
35
|
+
<USelectMenu
|
|
36
|
+
v-model="country"
|
|
37
|
+
:items="items"
|
|
38
|
+
value-key="value"
|
|
39
|
+
:search-input="{ placeholder: countryLabel }"
|
|
40
|
+
:aria-label="countryLabel"
|
|
41
|
+
:ui="{ base: 'w-28 shrink-0', content: 'min-w-72' }"
|
|
42
|
+
>
|
|
43
|
+
<template #default>
|
|
44
|
+
<span class="truncate">{{ current?.short }}</span>
|
|
45
|
+
</template>
|
|
46
|
+
</USelectMenu>
|
|
47
|
+
<UInput
|
|
48
|
+
v-model="national"
|
|
49
|
+
type="tel"
|
|
50
|
+
inputmode="tel"
|
|
51
|
+
autocomplete="tel-national"
|
|
52
|
+
:placeholder="placeholder"
|
|
53
|
+
class="w-full"
|
|
54
|
+
/>
|
|
55
|
+
</UFieldGroup>
|
|
56
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PhoneConfig } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
config?: PhoneConfig;
|
|
4
|
+
locale: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
/** Name of the country selector for assistive technology. */
|
|
7
|
+
countryLabel: string;
|
|
8
|
+
};
|
|
9
|
+
type __VLS_ModelProps = {
|
|
10
|
+
/** The single merged value, e.g. `+4512345678`. */
|
|
11
|
+
modelValue?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
14
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
15
|
+
"update:modelValue": (value: string | undefined) => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
17
|
+
"onUpdate:modelValue"?: ((value: string | undefined) => any) | undefined;
|
|
18
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormDefinition } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
slug: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
/** `undefined` when the definition is invalid. */
|
|
6
|
+
definition?: Omit<FormDefinition, 'delivery'>;
|
|
7
|
+
/** Bump to remount the renderer (e.g. after saving). */
|
|
8
|
+
contentKey?: string | number;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { PREVIEW_WIDTHS, previewScale } from "../../utils";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
slug: { type: String, required: true },
|
|
5
|
+
name: { type: String, required: false },
|
|
6
|
+
definition: { type: Object, required: false },
|
|
7
|
+
contentKey: { type: [String, Number], required: false }
|
|
8
|
+
});
|
|
9
|
+
const t = useModuleT("forms");
|
|
10
|
+
const toast = useToast();
|
|
11
|
+
const paths = useFormsPaths();
|
|
12
|
+
const path = computed(() => paths("form", { slug: props.slug }));
|
|
13
|
+
const STORAGE_KEY = "forms:preview-device";
|
|
14
|
+
const device = ref("mobile");
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
try {
|
|
17
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
18
|
+
if (stored === "mobile" || stored === "desktop") device.value = stored;
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
watch(device, (value) => {
|
|
23
|
+
try {
|
|
24
|
+
localStorage.setItem(STORAGE_KEY, value);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const devices = computed(() => [
|
|
29
|
+
{ value: "mobile", label: t("preview.mobile", "Mobile"), icon: "i-lucide-smartphone" },
|
|
30
|
+
{ value: "desktop", label: t("preview.desktop", "Desktop"), icon: "i-lucide-monitor" }
|
|
31
|
+
]);
|
|
32
|
+
const width = computed(() => PREVIEW_WIDTHS[device.value]);
|
|
33
|
+
const renderKey = computed(() => `${props.contentKey ?? ""}-${props.definition ? JSON.stringify(props.definition).length : "x"}`);
|
|
34
|
+
const resetKey = ref(0);
|
|
35
|
+
const hostRef = useTemplateRef("hostRef");
|
|
36
|
+
const innerRef = useTemplateRef("innerRef");
|
|
37
|
+
const available = ref(0);
|
|
38
|
+
const innerHeight = ref(0);
|
|
39
|
+
const scale = computed(() => previewScale(available.value, width.value));
|
|
40
|
+
let observer;
|
|
41
|
+
onMounted(() => {
|
|
42
|
+
observer = new ResizeObserver(() => {
|
|
43
|
+
available.value = hostRef.value?.clientWidth ?? 0;
|
|
44
|
+
innerHeight.value = innerRef.value?.offsetHeight ?? 0;
|
|
45
|
+
});
|
|
46
|
+
if (hostRef.value) observer.observe(hostRef.value);
|
|
47
|
+
if (innerRef.value) observer.observe(innerRef.value);
|
|
48
|
+
});
|
|
49
|
+
onBeforeUnmount(() => observer?.disconnect());
|
|
50
|
+
const open = ref(false);
|
|
51
|
+
const modalKey = ref(0);
|
|
52
|
+
const result = ref(null);
|
|
53
|
+
const json = computed(() => JSON.stringify(result.value, null, 2));
|
|
54
|
+
function tryAgain() {
|
|
55
|
+
result.value = null;
|
|
56
|
+
modalKey.value++;
|
|
57
|
+
}
|
|
58
|
+
watch(open, (value) => {
|
|
59
|
+
if (value) tryAgain();
|
|
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
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div class="min-w-0 space-y-3">
|
|
72
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
73
|
+
<UFieldGroup :aria-label="t('preview.device', 'Device')">
|
|
74
|
+
<UButton
|
|
75
|
+
v-for="item in devices"
|
|
76
|
+
:key="item.value"
|
|
77
|
+
size="xs"
|
|
78
|
+
color="neutral"
|
|
79
|
+
:variant="device === item.value ? 'solid' : 'outline'"
|
|
80
|
+
:icon="item.icon"
|
|
81
|
+
:label="item.label"
|
|
82
|
+
:aria-pressed="device === item.value"
|
|
83
|
+
@click="device = item.value"
|
|
84
|
+
/>
|
|
85
|
+
</UFieldGroup>
|
|
86
|
+
<div class="flex items-center gap-1">
|
|
87
|
+
<UButton
|
|
88
|
+
size="xs"
|
|
89
|
+
color="neutral"
|
|
90
|
+
variant="ghost"
|
|
91
|
+
icon="i-lucide-rotate-ccw"
|
|
92
|
+
:label="t('preview.reset', 'Reset')"
|
|
93
|
+
@click="resetKey++"
|
|
94
|
+
/>
|
|
95
|
+
<UButton
|
|
96
|
+
size="xs"
|
|
97
|
+
color="neutral"
|
|
98
|
+
variant="outline"
|
|
99
|
+
icon="i-lucide-maximize-2"
|
|
100
|
+
:label="t('preview.tryIt', 'Try it')"
|
|
101
|
+
:disabled="!definition"
|
|
102
|
+
@click="open = true"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="rounded-lg bg-elevated/50 p-3">
|
|
108
|
+
<div
|
|
109
|
+
:class="[
|
|
110
|
+
'mx-auto overflow-hidden bg-default shadow-sm ring-1 ring-default',
|
|
111
|
+
device === 'mobile' ? 'rounded-[1.75rem] border-[5px] border-inverted/80' : 'rounded-lg'
|
|
112
|
+
]"
|
|
113
|
+
:style="{ width: `${Math.round(width * scale) + (device === 'mobile' ? 10 : 0)}px`, maxWidth: '100%' }"
|
|
114
|
+
>
|
|
115
|
+
<div
|
|
116
|
+
v-if="device === 'desktop'"
|
|
117
|
+
class="flex items-center gap-1.5 border-b border-default bg-elevated px-2 py-1.5"
|
|
118
|
+
>
|
|
119
|
+
<span class="size-2 rounded-full bg-error/70" />
|
|
120
|
+
<span class="size-2 rounded-full bg-warning/70" />
|
|
121
|
+
<span class="size-2 rounded-full bg-success/70" />
|
|
122
|
+
<span class="ml-2 min-w-0 flex-1 truncate rounded bg-default px-2 py-0.5 text-[10px] text-muted">{{ path }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div
|
|
125
|
+
v-else
|
|
126
|
+
class="mx-auto mt-1.5 h-1 w-10 rounded-full bg-inverted/20"
|
|
127
|
+
/>
|
|
128
|
+
<div
|
|
129
|
+
ref="hostRef"
|
|
130
|
+
class="max-h-[calc(100vh-14rem)] overflow-y-auto overflow-x-hidden"
|
|
131
|
+
>
|
|
132
|
+
<div :style="{ height: `${innerHeight * scale}px` }">
|
|
133
|
+
<div
|
|
134
|
+
ref="innerRef"
|
|
135
|
+
:style="{ width: `${width}px`, transform: `scale(${scale})`, transformOrigin: 'top left' }"
|
|
136
|
+
>
|
|
137
|
+
<UContainer class="max-w-2xl py-10">
|
|
138
|
+
<h1
|
|
139
|
+
v-if="name"
|
|
140
|
+
class="mb-6 text-3xl font-bold"
|
|
141
|
+
>
|
|
142
|
+
{{ name }}
|
|
143
|
+
</h1>
|
|
144
|
+
<FormRenderer
|
|
145
|
+
v-if="definition"
|
|
146
|
+
:key="`${renderKey}-${resetKey}`"
|
|
147
|
+
:form="{ slug, definition }"
|
|
148
|
+
preview
|
|
149
|
+
/>
|
|
150
|
+
<p
|
|
151
|
+
v-else
|
|
152
|
+
class="text-muted"
|
|
153
|
+
>
|
|
154
|
+
{{ t("admin.previewUnavailable", "Fix the definition to see a preview.") }}
|
|
155
|
+
</p>
|
|
156
|
+
</UContainer>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<UModal
|
|
164
|
+
v-model:open="open"
|
|
165
|
+
:title="t('preview.tryIt', 'Try it')"
|
|
166
|
+
:ui="{ content: 'max-w-5xl' }"
|
|
167
|
+
>
|
|
168
|
+
<template #actions>
|
|
169
|
+
<UFieldGroup :aria-label="t('preview.device', 'Device')">
|
|
170
|
+
<UButton
|
|
171
|
+
v-for="item in devices"
|
|
172
|
+
:key="item.value"
|
|
173
|
+
size="xs"
|
|
174
|
+
color="neutral"
|
|
175
|
+
:variant="device === item.value ? 'solid' : 'outline'"
|
|
176
|
+
:icon="item.icon"
|
|
177
|
+
:label="item.label"
|
|
178
|
+
@click="device = item.value"
|
|
179
|
+
/>
|
|
180
|
+
</UFieldGroup>
|
|
181
|
+
</template>
|
|
182
|
+
<template #body>
|
|
183
|
+
<div
|
|
184
|
+
class="mx-auto max-w-full rounded-lg border border-default"
|
|
185
|
+
:style="{ width: `${width}px` }"
|
|
186
|
+
>
|
|
187
|
+
<UContainer class="max-w-2xl py-8">
|
|
188
|
+
<div
|
|
189
|
+
v-if="result"
|
|
190
|
+
class="space-y-3"
|
|
191
|
+
>
|
|
192
|
+
<h2 class="text-lg font-semibold">
|
|
193
|
+
{{ t("preview.result", "Result") }}
|
|
194
|
+
</h2>
|
|
195
|
+
<p class="text-sm text-muted">
|
|
196
|
+
{{ t("preview.resultHint", "This is what would be sent. Nothing was submitted.") }}
|
|
197
|
+
</p>
|
|
198
|
+
<pre
|
|
199
|
+
data-testid="preview-result"
|
|
200
|
+
class="max-h-96 overflow-auto rounded-md bg-elevated p-3 text-xs"
|
|
201
|
+
>{{ json }}</pre>
|
|
202
|
+
<div class="flex flex-wrap justify-end gap-2">
|
|
203
|
+
<UButton
|
|
204
|
+
color="neutral"
|
|
205
|
+
variant="outline"
|
|
206
|
+
icon="i-lucide-copy"
|
|
207
|
+
:label="t('preview.copy', 'Copy')"
|
|
208
|
+
@click="copy"
|
|
209
|
+
/>
|
|
210
|
+
<UButton
|
|
211
|
+
color="neutral"
|
|
212
|
+
variant="outline"
|
|
213
|
+
icon="i-lucide-rotate-ccw"
|
|
214
|
+
:label="t('preview.tryAgain', 'Try again')"
|
|
215
|
+
@click="tryAgain"
|
|
216
|
+
/>
|
|
217
|
+
<UButton
|
|
218
|
+
:label="t('preview.close', 'Close')"
|
|
219
|
+
@click="open = false"
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<FormRenderer
|
|
224
|
+
v-else-if="definition"
|
|
225
|
+
:key="`${renderKey}-${modalKey}`"
|
|
226
|
+
:form="{ slug, definition }"
|
|
227
|
+
preview
|
|
228
|
+
@previewed="result = $event"
|
|
229
|
+
/>
|
|
230
|
+
</UContainer>
|
|
231
|
+
</div>
|
|
232
|
+
</template>
|
|
233
|
+
<template #footer>
|
|
234
|
+
<UButton
|
|
235
|
+
color="neutral"
|
|
236
|
+
variant="ghost"
|
|
237
|
+
icon="i-lucide-rotate-ccw"
|
|
238
|
+
:label="t('preview.reset', 'Reset')"
|
|
239
|
+
@click="tryAgain"
|
|
240
|
+
/>
|
|
241
|
+
</template>
|
|
242
|
+
</UModal>
|
|
243
|
+
</div>
|
|
244
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FormDefinition } from '../../../types.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
slug: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
/** `undefined` when the definition is invalid. */
|
|
6
|
+
definition?: Omit<FormDefinition, 'delivery'>;
|
|
7
|
+
/** Bump to remount the renderer (e.g. after saving). */
|
|
8
|
+
contentKey?: string | number;
|
|
9
|
+
};
|
|
10
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
|
+
declare const _default: typeof __VLS_export;
|
|
12
|
+
export default _default;
|
|
@@ -7,12 +7,18 @@ type __VLS_Props = {
|
|
|
7
7
|
slug?: string;
|
|
8
8
|
/** A definition to render directly (admin preview, tests). */
|
|
9
9
|
form?: FormShape;
|
|
10
|
+
/** Shared password of a password-protected form (sent as `x-form-password`). */
|
|
11
|
+
password?: string;
|
|
10
12
|
/** Validates and steps like normal, but never posts. */
|
|
11
13
|
preview?: boolean;
|
|
14
|
+
/** Overrides the locale of translated form content (defaults to the app's locale). */
|
|
15
|
+
locale?: string;
|
|
12
16
|
};
|
|
13
17
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
previewed: (answers: Record<string, unknown>) => any;
|
|
14
19
|
submitted: (id: string | null) => any;
|
|
15
20
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
|
+
onPreviewed?: ((answers: Record<string, unknown>) => any) | undefined;
|
|
16
22
|
onSubmitted?: ((id: string | null) => any) | undefined;
|
|
17
23
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
24
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { translate } from "../../utils";
|
|
2
|
+
import { previewPayload, translate } from "../../utils";
|
|
3
3
|
import { emptyAnswers, submitAnswers, useFormsLocale } from "../composables/useForms";
|
|
4
4
|
import { useFormStepper } from "../composables/useFormStepper";
|
|
5
5
|
const props = defineProps({
|
|
6
6
|
slug: { type: String, required: false },
|
|
7
7
|
form: { type: Object, required: false },
|
|
8
|
-
|
|
8
|
+
password: { type: String, required: false },
|
|
9
|
+
preview: { type: Boolean, required: false },
|
|
10
|
+
locale: { type: String, required: false }
|
|
9
11
|
});
|
|
10
|
-
const emit = defineEmits(["submitted"]);
|
|
12
|
+
const emit = defineEmits(["submitted", "previewed"]);
|
|
13
|
+
const listening = !!getCurrentInstance()?.vnode.props?.onPreviewed;
|
|
11
14
|
const t = useModuleT("forms");
|
|
12
|
-
const
|
|
15
|
+
const appLocale = useFormsLocale();
|
|
16
|
+
const locale = computed(() => props.locale || appLocale.value);
|
|
13
17
|
const toast = useToast();
|
|
14
|
-
const loaded = props.slug ? await useForm(props.slug) : void 0;
|
|
18
|
+
const loaded = props.slug ? await useForm(props.slug, () => props.password) : void 0;
|
|
15
19
|
const form = computed(() => props.form ?? loaded?.data.value ?? void 0);
|
|
16
20
|
const definition = computed(() => form.value?.definition);
|
|
17
21
|
const state = reactive(form.value ? emptyAnswers(form.value.definition) : {});
|
|
@@ -22,18 +26,20 @@ watch(() => form.value?.definition, (next) => {
|
|
|
22
26
|
const stepper = useFormStepper(definition, state, { t: () => t, locale });
|
|
23
27
|
const formRef = useTemplateRef("formRef");
|
|
24
28
|
const submitting = ref(false);
|
|
29
|
+
const captchaResetKey = ref(0);
|
|
25
30
|
const done = ref(false);
|
|
26
31
|
const submitSettings = computed(() => definition.value?.submit);
|
|
27
32
|
const title = (value) => translate(value, locale.value);
|
|
28
33
|
async function onSubmit() {
|
|
29
34
|
if (!stepper.isLast.value) return stepper.next();
|
|
30
35
|
if (props.preview) {
|
|
31
|
-
|
|
36
|
+
if (listening) emit("previewed", previewPayload(definition.value, state));
|
|
37
|
+
else toast.add({ title: t("preview.notSent", "Preview: nothing was sent."), color: "info" });
|
|
32
38
|
return;
|
|
33
39
|
}
|
|
34
40
|
submitting.value = true;
|
|
35
41
|
try {
|
|
36
|
-
const result = await submitAnswers(form.value.slug, definition.value, state, locale.value);
|
|
42
|
+
const result = await submitAnswers(form.value.slug, definition.value, state, locale.value, props.password);
|
|
37
43
|
done.value = true;
|
|
38
44
|
emit("submitted", result.id);
|
|
39
45
|
const redirect = submitSettings.value?.redirect;
|
|
@@ -47,6 +53,7 @@ async function onSubmit() {
|
|
|
47
53
|
toast.add({ title: t("error.generic", "Something went wrong. Please try again."), color: "error" });
|
|
48
54
|
}
|
|
49
55
|
} finally {
|
|
56
|
+
captchaResetKey.value++;
|
|
50
57
|
submitting.value = false;
|
|
51
58
|
}
|
|
52
59
|
}
|
|
@@ -122,6 +129,8 @@ function sendAnother() {
|
|
|
122
129
|
v-model="state[field.key]"
|
|
123
130
|
:field="field"
|
|
124
131
|
:locale="locale"
|
|
132
|
+
:preview="preview"
|
|
133
|
+
:captcha-reset-key="captchaResetKey"
|
|
125
134
|
/>
|
|
126
135
|
</template>
|
|
127
136
|
|
|
@@ -7,12 +7,18 @@ type __VLS_Props = {
|
|
|
7
7
|
slug?: string;
|
|
8
8
|
/** A definition to render directly (admin preview, tests). */
|
|
9
9
|
form?: FormShape;
|
|
10
|
+
/** Shared password of a password-protected form (sent as `x-form-password`). */
|
|
11
|
+
password?: string;
|
|
10
12
|
/** Validates and steps like normal, but never posts. */
|
|
11
13
|
preview?: boolean;
|
|
14
|
+
/** Overrides the locale of translated form content (defaults to the app's locale). */
|
|
15
|
+
locale?: string;
|
|
12
16
|
};
|
|
13
17
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
|
+
previewed: (answers: Record<string, unknown>) => any;
|
|
14
19
|
submitted: (id: string | null) => any;
|
|
15
20
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
21
|
+
onPreviewed?: ((answers: Record<string, unknown>) => any) | undefined;
|
|
16
22
|
onSubmitted?: ((id: string | null) => any) | undefined;
|
|
17
23
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
18
24
|
declare const _default: typeof __VLS_export;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { isAnswerField, resolveVisibility, translate } from "../../utils";
|
|
3
3
|
const props = defineProps({
|
|
4
4
|
submission: { type: Object, required: true }
|
|
5
5
|
});
|
|
@@ -8,7 +8,7 @@ const locale = useFormsLocale();
|
|
|
8
8
|
const sections = computed(() => resolveVisibility(props.submission.definition, props.submission.answers).map(({ step, fields }) => ({
|
|
9
9
|
id: step.id,
|
|
10
10
|
title: translate(step.title, locale.value),
|
|
11
|
-
rows: fields.filter(
|
|
11
|
+
rows: fields.filter(isAnswerField).map((field) => {
|
|
12
12
|
const value = props.submission.answers[field.key];
|
|
13
13
|
const option = (v) => translate(field.options?.find((o) => o.value === v)?.label, locale.value) || v;
|
|
14
14
|
return {
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export { useForm, useFormsLocale, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from './useForms.js';
|
|
1
|
+
export { useForm, useFormAccess, useFormsLocale, useFormsLocales, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from './useForms.js';
|
|
2
2
|
export { useFormStepper } from './useFormStepper.js';
|
|
3
|
+
export { useFormBuilderEditor } from './useFormBuilderEditor.js';
|
|
4
|
+
export { useFormBuilderLabels, fieldTypeIcons } from './useFormBuilderLabels.js';
|
|
5
|
+
export { useCaptchaSecrets } from './useCaptchaSecrets.js';
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
export { useForm, useFormsLocale, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from "./useForms.js";
|
|
1
|
+
export { useForm, useFormAccess, useFormsLocale, useFormsLocales, useFormsPaths, emptyAnswers, submitAnswers, fieldLabel } from "./useForms.js";
|
|
2
2
|
export { useFormStepper } from "./useFormStepper.js";
|
|
3
|
+
export { useFormBuilderEditor } from "./useFormBuilderEditor.js";
|
|
4
|
+
export { useFormBuilderLabels, fieldTypeIcons } from "./useFormBuilderLabels.js";
|
|
5
|
+
export { useCaptchaSecrets } from "./useCaptchaSecrets.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CaptchaProvider, FormField } from '../../../types.js';
|
|
2
|
+
type WidgetOptions = {
|
|
3
|
+
'sitekey': string;
|
|
4
|
+
'theme'?: string;
|
|
5
|
+
'size'?: string;
|
|
6
|
+
'callback': (token: string) => void;
|
|
7
|
+
'expired-callback': () => void;
|
|
8
|
+
'error-callback': () => void;
|
|
9
|
+
};
|
|
10
|
+
/** The part of the Turnstile / reCAPTCHA / hCaptcha browser APIs this module uses (they share one shape). */
|
|
11
|
+
export type CaptchaApi = {
|
|
12
|
+
render: (container: HTMLElement, options: WidgetOptions) => string | number;
|
|
13
|
+
reset: (id?: string | number) => void;
|
|
14
|
+
remove?: (id?: string | number) => void;
|
|
15
|
+
};
|
|
16
|
+
/** Loads the provider's script once, in the browser only, and resolves with its global API. */
|
|
17
|
+
export declare function loadCaptchaScript(provider: CaptchaProvider): Promise<CaptchaApi>;
|
|
18
|
+
/** Provider of a captcha field with none set: `runtimeConfig.public.forms.captcha.defaultProvider` (env `NUXT_PUBLIC_FORMS_CAPTCHA_DEFAULT_PROVIDER`). */
|
|
19
|
+
export declare function captchaDefaultProvider(): CaptchaProvider;
|
|
20
|
+
/** Public site key for a captcha field: its own override, else the module's `public.forms.captcha.<provider>.siteKey`. */
|
|
21
|
+
export declare function captchaSiteKey(field: Pick<FormField, 'captcha'>): string;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { normalizeCaptchaProvider, resolveCaptcha } from "../../utils/index.js";
|
|
2
|
+
const SCRIPTS = {
|
|
3
|
+
turnstile: { src: "https://challenges.cloudflare.com/turnstile/v0/api.js", global: "turnstile" },
|
|
4
|
+
recaptcha: { src: "https://www.google.com/recaptcha/api.js", global: "grecaptcha" },
|
|
5
|
+
hcaptcha: { src: "https://js.hcaptcha.com/1/api.js", global: "hcaptcha" }
|
|
6
|
+
};
|
|
7
|
+
const loading = /* @__PURE__ */ new Map();
|
|
8
|
+
export function loadCaptchaScript(provider) {
|
|
9
|
+
if (import.meta.server) return Promise.reject(new Error("Captcha scripts only load in the browser."));
|
|
10
|
+
const existing = loading.get(provider);
|
|
11
|
+
if (existing) return existing;
|
|
12
|
+
const { src, global } = SCRIPTS[provider];
|
|
13
|
+
const callback = `__formsCaptchaLoaded_${provider}`;
|
|
14
|
+
const promise = new Promise((resolve, reject) => {
|
|
15
|
+
const scope = window;
|
|
16
|
+
scope[callback] = () => resolve(scope[global]);
|
|
17
|
+
const script = document.createElement("script");
|
|
18
|
+
script.src = `${src}?render=explicit&onload=${callback}`;
|
|
19
|
+
script.async = true;
|
|
20
|
+
script.defer = true;
|
|
21
|
+
script.onerror = () => {
|
|
22
|
+
loading.delete(provider);
|
|
23
|
+
script.remove();
|
|
24
|
+
reject(new Error(`Could not load the ${provider} script.`));
|
|
25
|
+
};
|
|
26
|
+
document.head.appendChild(script);
|
|
27
|
+
});
|
|
28
|
+
loading.set(provider, promise);
|
|
29
|
+
return promise;
|
|
30
|
+
}
|
|
31
|
+
export function captchaDefaultProvider() {
|
|
32
|
+
return normalizeCaptchaProvider(useRuntimeConfig().public.forms?.captcha?.defaultProvider);
|
|
33
|
+
}
|
|
34
|
+
export function captchaSiteKey(field) {
|
|
35
|
+
const { provider, siteKey } = resolveCaptcha(field, captchaDefaultProvider());
|
|
36
|
+
if (siteKey) return siteKey;
|
|
37
|
+
const config = useRuntimeConfig().public.forms;
|
|
38
|
+
return config?.captcha?.[provider]?.siteKey ?? "";
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FormField } from '../../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The write-only captcha secret key of a saved form. It is stored right away through the admin secrets API,
|
|
4
|
+
* not with the definition. `field` is the currently selected field; the state only applies when it is a captcha.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useCaptchaSecrets(formId: MaybeRefOrGetter<string | undefined>, field: MaybeRefOrGetter<FormField | undefined>): any;
|
|
7
|
+
export type CaptchaSecrets = ReturnType<typeof useCaptchaSecrets>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { resolveCaptcha } from "../../utils/index.js";
|
|
2
|
+
import { captchaDefaultProvider } from "./useCaptcha.js";
|
|
3
|
+
export function useCaptchaSecrets(formId, field) {
|
|
4
|
+
const t = useModuleT("forms");
|
|
5
|
+
const toast = useToast();
|
|
6
|
+
const captcha = computed(() => {
|
|
7
|
+
const selected = toValue(field);
|
|
8
|
+
return selected?.type === "captcha" ? resolveCaptcha(selected, captchaDefaultProvider()) : void 0;
|
|
9
|
+
});
|
|
10
|
+
const secrets = ref();
|
|
11
|
+
const url = computed(() => toValue(formId) ? `/api/forms/admin/forms/${toValue(formId)}/secrets` : void 0);
|
|
12
|
+
const value = ref("");
|
|
13
|
+
const saving = ref(false);
|
|
14
|
+
const source = computed(() => captcha.value ? secrets.value?.captcha[captcha.value.provider]?.source : void 0);
|
|
15
|
+
const statusLabel = computed(() => ({
|
|
16
|
+
form: t("captcha.secretForm", "Configured via form"),
|
|
17
|
+
env: t("captcha.secretEnv", "Configured via environment"),
|
|
18
|
+
unset: t("captcha.secretUnset", "Not set")
|
|
19
|
+
})[source.value ?? "unset"]);
|
|
20
|
+
async function load() {
|
|
21
|
+
if (!url.value) return;
|
|
22
|
+
try {
|
|
23
|
+
secrets.value = await $fetch(url.value);
|
|
24
|
+
} catch {
|
|
25
|
+
secrets.value = void 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
onMounted(load);
|
|
29
|
+
watch(() => captcha.value?.provider, () => {
|
|
30
|
+
value.value = "";
|
|
31
|
+
});
|
|
32
|
+
async function save() {
|
|
33
|
+
if (!url.value || !captcha.value || !value.value.trim()) return;
|
|
34
|
+
saving.value = true;
|
|
35
|
+
try {
|
|
36
|
+
secrets.value = await $fetch(url.value, { method: "PUT", body: { key: `captcha.${captcha.value.provider}.secretKey`, value: value.value.trim() } });
|
|
37
|
+
value.value = "";
|
|
38
|
+
toast.add({ title: t("captcha.secretSaved", "Secret key saved."), color: "success" });
|
|
39
|
+
} catch {
|
|
40
|
+
toast.add({ title: t("captcha.secretFailed", "The secret key could not be saved."), color: "error" });
|
|
41
|
+
} finally {
|
|
42
|
+
saving.value = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function remove() {
|
|
46
|
+
if (!url.value || !captcha.value) return;
|
|
47
|
+
try {
|
|
48
|
+
secrets.value = await $fetch(`${url.value}/captcha.${captcha.value.provider}.secretKey`, { method: "DELETE" });
|
|
49
|
+
toast.add({ title: t("captcha.secretRemoved", "Secret key removed."), color: "success" });
|
|
50
|
+
} catch {
|
|
51
|
+
toast.add({ title: t("captcha.secretFailed", "The secret key could not be saved."), color: "error" });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return reactive({
|
|
55
|
+
/** The resolved captcha settings of the selected field. */
|
|
56
|
+
captcha,
|
|
57
|
+
secrets,
|
|
58
|
+
/** What the admin is typing, not saved yet. */
|
|
59
|
+
value,
|
|
60
|
+
saving,
|
|
61
|
+
source,
|
|
62
|
+
statusLabel,
|
|
63
|
+
setValue: (next) => {
|
|
64
|
+
value.value = next;
|
|
65
|
+
},
|
|
66
|
+
save,
|
|
67
|
+
remove
|
|
68
|
+
});
|
|
69
|
+
}
|