@evanschleret/formforgeclient 1.2.3 → 2.0.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.
- package/README.md +10 -0
- package/dist/module.cjs +1 -0
- package/dist/module.d.cts +1 -0
- package/dist/module.d.mts +1 -0
- package/dist/module.d.ts +1 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -0
- package/dist/runtime/api/client.js +4 -2
- package/dist/runtime/api/request.d.ts +1 -0
- package/dist/runtime/api/schema.js +4 -4
- package/dist/runtime/assets/formforge.css +1 -0
- package/dist/runtime/composables/index.d.ts +1 -1
- package/dist/runtime/composables/useFormForgeBuilder.d.ts +24 -2
- package/dist/runtime/composables/useFormForgeBuilder.js +299 -43
- package/dist/runtime/composables/useFormForgeClient.js +3 -5
- package/dist/runtime/composables/useFormForgeForm.js +15 -5
- package/dist/runtime/composables/useFormForgeI18n.d.ts +245 -19
- package/dist/runtime/composables/useFormForgeI18n.js +245 -19
- package/dist/runtime/composables/useFormForgeSubmit.js +31 -9
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +21 -2
- package/dist/runtime/renderers/default/FormForgeBuilder.vue +689 -738
- package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +21 -2
- package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.d.vue.ts +17 -0
- package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue +32 -0
- package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue.d.ts +17 -0
- package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +3 -4
- package/dist/runtime/renderers/default/FormForgeRenderer.vue +344 -294
- package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +3 -4
- package/dist/runtime/renderers/default/FormForgeRendererField.d.vue.ts +22 -0
- package/dist/runtime/renderers/default/FormForgeRendererField.vue +237 -0
- package/dist/runtime/renderers/default/FormForgeRendererField.vue.d.ts +22 -0
- package/dist/runtime/renderers/default/FormForgeRendererPage.d.vue.ts +18 -0
- package/dist/runtime/renderers/default/FormForgeRendererPage.vue +31 -0
- package/dist/runtime/renderers/default/FormForgeRendererPage.vue.d.ts +18 -0
- package/dist/runtime/renderers/default/FormForgeResponse.vue +4 -3
- package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.d.vue.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue +118 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue.d.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.d.vue.ts +46 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue +205 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue.d.ts +46 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.d.vue.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue +37 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue.d.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.d.vue.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue +195 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue.d.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.d.vue.ts +14 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue +91 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue.d.ts +14 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.d.vue.ts +13 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue +387 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue.d.ts +13 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.d.vue.ts +44 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue +328 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue.d.ts +44 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.d.vue.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue +47 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue.d.ts +11 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.d.vue.ts +14 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue +595 -0
- package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue.d.ts +14 -0
- package/dist/runtime/renderers/default/builder/builderFieldHelpers.d.ts +3 -0
- package/dist/runtime/renderers/default/builder/builderFieldHelpers.js +4 -0
- package/dist/runtime/types/index.d.ts +1 -1
- package/dist/runtime/types/management.d.ts +12 -0
- package/dist/runtime/types/schema.d.ts +72 -4
- package/dist/runtime/utils/defaults.d.ts +7 -0
- package/dist/runtime/utils/defaults.js +86 -0
- package/dist/runtime/utils/page-logic.d.ts +24 -0
- package/dist/runtime/utils/page-logic.js +351 -0
- package/dist/runtime/utils/rich-text.d.ts +3 -0
- package/dist/runtime/utils/rich-text.js +72 -0
- package/dist/runtime/utils/schema.d.ts +1 -1
- package/dist/runtime/utils/schema.js +70 -16
- package/dist/runtime/utils/temporal.d.ts +10 -0
- package/dist/runtime/utils/temporal.js +28 -0
- package/dist/runtime/utils/validation.d.ts +5 -0
- package/dist/runtime/utils/validation.js +36 -0
- package/dist/runtime/validation/zod.d.ts +5 -2
- package/dist/runtime/validation/zod.js +563 -54
- package/dist/types.d.mts +2 -0
- package/package.json +18 -14
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, nextTick,
|
|
2
|
+
import { computed, nextTick, ref, watch } from "#imports";
|
|
3
|
+
import { parseDate, parseTime } from "@internationalized/date";
|
|
3
4
|
import { useOverlay } from "@nuxt/ui/composables/useOverlay";
|
|
4
5
|
import { useToast } from "@nuxt/ui/composables/useToast";
|
|
5
6
|
import Draggable from "vuedraggable";
|
|
6
7
|
import {
|
|
7
|
-
FORM_FORGE_BUILDER_CONDITION_ACTIONS,
|
|
8
|
-
FORM_FORGE_BUILDER_CONDITION_MATCHES,
|
|
9
|
-
FORM_FORGE_BUILDER_CONDITION_OPERATORS,
|
|
10
8
|
FORM_FORGE_BUILDER_FIELD_TYPES,
|
|
11
9
|
useFormForgeBuilder
|
|
12
10
|
} from "../../composables/useFormForgeBuilder";
|
|
13
11
|
import { useFormForgeCategory, useFormForgeCategoryOptions } from "../../composables/useFormForgeCategory";
|
|
14
12
|
import { useFormForgeI18n } from "../../composables/useFormForgeI18n";
|
|
13
|
+
import {
|
|
14
|
+
createDefaultAddressFields,
|
|
15
|
+
resolveDefaultConsentLabel
|
|
16
|
+
} from "../../utils/defaults";
|
|
17
|
+
import {
|
|
18
|
+
ensurePageLogic
|
|
19
|
+
} from "../../utils/page-logic";
|
|
15
20
|
import FormForgeCategoryCreateModal from "./FormForgeCategoryCreateModal.vue";
|
|
21
|
+
import FormForgeBuilderBlockCard from "./builder/FormForgeBuilderBlockCard.vue";
|
|
16
22
|
const props = defineProps({
|
|
17
23
|
formUuid: { type: String, required: false, default: void 0 },
|
|
18
24
|
formKey: { type: String, required: false, default: void 0 },
|
|
@@ -29,24 +35,29 @@ const props = defineProps({
|
|
|
29
35
|
disableTitleInput: { type: Boolean, required: false, default: false },
|
|
30
36
|
disableCategoryControl: { type: Boolean, required: false, default: false },
|
|
31
37
|
disablePublishAction: { type: Boolean, required: false, default: false },
|
|
32
|
-
|
|
38
|
+
disableSettingsTab: { type: Boolean, required: false, default: false },
|
|
39
|
+
hideSettings: { type: Boolean, required: false, default: false },
|
|
40
|
+
autoPublishOnSave: { type: Boolean, required: false, default: false },
|
|
41
|
+
defaultPublished: { type: Boolean, required: false, default: false },
|
|
42
|
+
standalone: { type: Boolean, required: false, default: false }
|
|
33
43
|
});
|
|
34
|
-
const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error"]);
|
|
44
|
+
const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error", "selection-change"]);
|
|
35
45
|
const builderOptions = {
|
|
36
46
|
formUuid: props.formUuid,
|
|
37
47
|
formKey: props.formKey,
|
|
38
48
|
endpoint: props.endpoint,
|
|
49
|
+
locale: props.locale,
|
|
39
50
|
initial: props.modelValue,
|
|
40
51
|
autosave: props.autosave,
|
|
41
|
-
autosaveDelay: props.autosaveDelay
|
|
52
|
+
autosaveDelay: props.autosaveDelay,
|
|
53
|
+
autoPublishOnSave: props.autoPublishOnSave
|
|
42
54
|
};
|
|
43
55
|
const builder = useFormForgeBuilder(builderOptions);
|
|
44
|
-
const { t } = useFormForgeI18n({
|
|
56
|
+
const { t, locale } = useFormForgeI18n({
|
|
45
57
|
locale: () => props.locale
|
|
46
58
|
});
|
|
47
59
|
const toast = useToast();
|
|
48
60
|
const draft = builder.draft;
|
|
49
|
-
const isClientReady = ref(false);
|
|
50
61
|
const saving = builder.saving;
|
|
51
62
|
const publishing = builder.publishing;
|
|
52
63
|
const publishable = builder.publishable;
|
|
@@ -66,14 +77,10 @@ const categoryOptions = useFormForgeCategoryOptions({
|
|
|
66
77
|
includeInactive: true
|
|
67
78
|
});
|
|
68
79
|
const CATEGORY_NONE_VALUE = "__formforge_no_category__";
|
|
69
|
-
const fieldElements = /* @__PURE__ */ new Map();
|
|
70
|
-
const builderRootElement = ref(null);
|
|
71
|
-
const builderColumnElement = ref(null);
|
|
72
|
-
const railTop = ref(120);
|
|
73
|
-
const railLeft = ref(0);
|
|
74
80
|
const loadingRemoteForm = ref(false);
|
|
75
81
|
const isPublished = ref(props.defaultPublished);
|
|
76
82
|
let loadRequestId = 0;
|
|
83
|
+
const settingsHidden = computed(() => props.hideSettings || props.disableSettingsTab);
|
|
77
84
|
const safePages = computed(() => {
|
|
78
85
|
const pages = draft.value?.pages;
|
|
79
86
|
if (!Array.isArray(pages)) {
|
|
@@ -81,13 +88,6 @@ const safePages = computed(() => {
|
|
|
81
88
|
}
|
|
82
89
|
return pages.filter((page) => page !== void 0 && page !== null);
|
|
83
90
|
});
|
|
84
|
-
const safeConditions = computed(() => {
|
|
85
|
-
const conditions = draft.value?.conditions;
|
|
86
|
-
if (!Array.isArray(conditions)) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
return conditions.filter((condition) => condition !== void 0 && condition !== null);
|
|
90
|
-
});
|
|
91
91
|
const draftTitle = computed({
|
|
92
92
|
get() {
|
|
93
93
|
return typeof draft.value?.title === "string" ? draft.value.title : "";
|
|
@@ -153,8 +153,8 @@ const draftMutationIdentifier = computed(() => {
|
|
|
153
153
|
}
|
|
154
154
|
return null;
|
|
155
155
|
});
|
|
156
|
-
const
|
|
157
|
-
return
|
|
156
|
+
const builderLayoutClass = computed(() => {
|
|
157
|
+
return props.standalone ? "grid gap-4" : "grid gap-4 lg:grid-cols-[minmax(0,1fr)_4rem]";
|
|
158
158
|
});
|
|
159
159
|
function twoDigits(value) {
|
|
160
160
|
return String(value).padStart(2, "0");
|
|
@@ -181,6 +181,239 @@ const formattedLastSavedAt = computed(() => {
|
|
|
181
181
|
const minutes = twoDigits(parsed.getMinutes());
|
|
182
182
|
return `${day}.${month}.${year} \xE0 ${hours}:${minutes}`;
|
|
183
183
|
});
|
|
184
|
+
function parseDateTimeValue(value) {
|
|
185
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
186
|
+
return {
|
|
187
|
+
date: null,
|
|
188
|
+
time: null
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const parsed = new Date(value);
|
|
192
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
193
|
+
return {
|
|
194
|
+
date: null,
|
|
195
|
+
time: null
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
date: parseDate([
|
|
200
|
+
parsed.getFullYear(),
|
|
201
|
+
twoDigits(parsed.getMonth() + 1),
|
|
202
|
+
twoDigits(parsed.getDate())
|
|
203
|
+
].join("-")),
|
|
204
|
+
time: parseTime([
|
|
205
|
+
twoDigits(parsed.getHours()),
|
|
206
|
+
twoDigits(parsed.getMinutes()),
|
|
207
|
+
twoDigits(parsed.getSeconds())
|
|
208
|
+
].join(":"))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function serializeDateTimeValue(date, time) {
|
|
212
|
+
if (date === null) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const year = date.year;
|
|
216
|
+
const month = twoDigits(date.month);
|
|
217
|
+
const day = twoDigits(date.day);
|
|
218
|
+
const hours = time !== null ? twoDigits(time.hour) : "00";
|
|
219
|
+
const minutes = time !== null ? twoDigits(time.minute) : "00";
|
|
220
|
+
const seconds = time !== null ? twoDigits(time.second) : "00";
|
|
221
|
+
return (/* @__PURE__ */ new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`)).toISOString();
|
|
222
|
+
}
|
|
223
|
+
function currentDateTimeValue() {
|
|
224
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
225
|
+
}
|
|
226
|
+
const selectedTab = ref("builder");
|
|
227
|
+
const publishAtParts = computed(() => parseDateTimeValue(draft.value?.publish_at));
|
|
228
|
+
const pauseAtParts = computed(() => parseDateTimeValue(draft.value?.pause_at));
|
|
229
|
+
const publishAtDate = computed({
|
|
230
|
+
get() {
|
|
231
|
+
return publishAtParts.value.date;
|
|
232
|
+
},
|
|
233
|
+
set(value) {
|
|
234
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
draft.value.publish_at = serializeDateTimeValue(value, publishAtParts.value.time);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const publishAtTime = computed({
|
|
241
|
+
get() {
|
|
242
|
+
return publishAtParts.value.time;
|
|
243
|
+
},
|
|
244
|
+
set(value) {
|
|
245
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
draft.value.publish_at = serializeDateTimeValue(publishAtParts.value.date, value);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
const pauseAtDate = computed({
|
|
252
|
+
get() {
|
|
253
|
+
return pauseAtParts.value.date;
|
|
254
|
+
},
|
|
255
|
+
set(value) {
|
|
256
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
draft.value.pause_at = serializeDateTimeValue(value, pauseAtParts.value.time);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
const pauseAtTime = computed({
|
|
263
|
+
get() {
|
|
264
|
+
return pauseAtParts.value.time;
|
|
265
|
+
},
|
|
266
|
+
set(value) {
|
|
267
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
draft.value.pause_at = serializeDateTimeValue(pauseAtParts.value.date, value);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const responseLimitValue = computed({
|
|
274
|
+
get() {
|
|
275
|
+
return typeof draft.value?.response_limit === "number" ? draft.value.response_limit : null;
|
|
276
|
+
},
|
|
277
|
+
set(value) {
|
|
278
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
draft.value.response_limit = typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const openingEnabled = computed({
|
|
285
|
+
get() {
|
|
286
|
+
return draft.value?.publish_at !== null && draft.value?.publish_at !== void 0;
|
|
287
|
+
},
|
|
288
|
+
set(value) {
|
|
289
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (value && (draft.value.publish_at === null || draft.value.publish_at === void 0)) {
|
|
293
|
+
draft.value.publish_at = currentDateTimeValue();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!value) {
|
|
297
|
+
draft.value.publish_at = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const closingEnabled = computed({
|
|
302
|
+
get() {
|
|
303
|
+
return draft.value?.pause_at !== null && draft.value?.pause_at !== void 0;
|
|
304
|
+
},
|
|
305
|
+
set(value) {
|
|
306
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (value && (draft.value.pause_at === null || draft.value.pause_at === void 0)) {
|
|
310
|
+
draft.value.pause_at = currentDateTimeValue();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!value) {
|
|
314
|
+
draft.value.pause_at = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
const responseLimitEnabled = computed({
|
|
319
|
+
get() {
|
|
320
|
+
return draft.value?.response_limit !== null && draft.value?.response_limit !== void 0;
|
|
321
|
+
},
|
|
322
|
+
set(value) {
|
|
323
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (value && (draft.value.response_limit === null || draft.value.response_limit === void 0)) {
|
|
327
|
+
draft.value.response_limit = 1;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (!value) {
|
|
331
|
+
draft.value.response_limit = null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
const submissionPinEnabled = computed({
|
|
336
|
+
get() {
|
|
337
|
+
return draft.value?.submission_code_required === true;
|
|
338
|
+
},
|
|
339
|
+
set(value) {
|
|
340
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
draft.value.submission_code_required = value;
|
|
344
|
+
if (!value) {
|
|
345
|
+
draft.value.submission_code = null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
const submissionPinValue = computed({
|
|
350
|
+
get() {
|
|
351
|
+
return typeof draft.value?.submission_code === "string" ? draft.value.submission_code : "";
|
|
352
|
+
},
|
|
353
|
+
set(value) {
|
|
354
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
draft.value.submission_code = value;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
const publicUrlValue = computed(() => {
|
|
361
|
+
return typeof draft.value?.public_url === "string" ? draft.value.public_url : "";
|
|
362
|
+
});
|
|
363
|
+
function ensureSubmissionApi() {
|
|
364
|
+
if (draft.value === void 0 || draft.value === null) {
|
|
365
|
+
return {};
|
|
366
|
+
}
|
|
367
|
+
if (typeof draft.value.api !== "object" || draft.value.api === null || Array.isArray(draft.value.api)) {
|
|
368
|
+
draft.value.api = {};
|
|
369
|
+
}
|
|
370
|
+
const api = draft.value.api;
|
|
371
|
+
if (typeof api.submission !== "object" || api.submission === null || Array.isArray(api.submission)) {
|
|
372
|
+
api.submission = {};
|
|
373
|
+
}
|
|
374
|
+
return api.submission;
|
|
375
|
+
}
|
|
376
|
+
const submissionIsPublic = computed({
|
|
377
|
+
get() {
|
|
378
|
+
const api = draft.value?.api;
|
|
379
|
+
if (typeof api !== "object" || api === null || Array.isArray(api)) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
const submission = api.submission;
|
|
383
|
+
if (typeof submission !== "object" || submission === null || Array.isArray(submission)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
if (typeof submission.public === "boolean") {
|
|
387
|
+
return submission.public;
|
|
388
|
+
}
|
|
389
|
+
if (typeof submission.auth === "string") {
|
|
390
|
+
return submission.auth === "public";
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
set(value) {
|
|
395
|
+
const submission = ensureSubmissionApi();
|
|
396
|
+
submission.public = value;
|
|
397
|
+
submission.auth = value ? "public" : "required";
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
const builderTabs = computed(() => {
|
|
401
|
+
const items = [
|
|
402
|
+
{
|
|
403
|
+
label: "Builder",
|
|
404
|
+
icon: "i-lucide-layout-grid",
|
|
405
|
+
slot: "builder"
|
|
406
|
+
}
|
|
407
|
+
];
|
|
408
|
+
if (!settingsHidden.value) {
|
|
409
|
+
items.push({
|
|
410
|
+
label: "Settings",
|
|
411
|
+
icon: "i-lucide-settings-2",
|
|
412
|
+
slot: "settings"
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return items;
|
|
416
|
+
});
|
|
184
417
|
const selectedPageKey = ref(safePages.value[0]?.page_key ?? null);
|
|
185
418
|
const selectedFieldKey = ref(safePages.value[0]?.fields[0]?.field_key ?? null);
|
|
186
419
|
const fieldTypeMeta = computed(() => ({
|
|
@@ -192,13 +425,13 @@ const fieldTypeMeta = computed(() => ({
|
|
|
192
425
|
select_menu: { label: t("builder.fieldType.select_menu"), icon: "i-lucide-list-filter" },
|
|
193
426
|
radio: { label: t("builder.fieldType.radio"), icon: "i-lucide-circle-dot" },
|
|
194
427
|
checkbox: { label: t("builder.fieldType.checkbox"), icon: "i-lucide-check-square" },
|
|
428
|
+
consent: { label: t("builder.fieldType.consent"), icon: "i-lucide-badge-check" },
|
|
195
429
|
checkbox_group: { label: t("builder.fieldType.checkbox_group"), icon: "i-lucide-list-checks" },
|
|
430
|
+
address: { label: t("builder.fieldType.address"), icon: "i-lucide-house" },
|
|
196
431
|
switch: { label: t("builder.fieldType.switch"), icon: "i-lucide-toggle-left" },
|
|
432
|
+
temporal: { label: t("builder.fieldType.temporal"), icon: "i-lucide-calendar-clock" },
|
|
197
433
|
date: { label: t("builder.fieldType.date"), icon: "i-lucide-calendar" },
|
|
198
434
|
time: { label: t("builder.fieldType.time"), icon: "i-lucide-clock-3" },
|
|
199
|
-
datetime: { label: t("builder.fieldType.datetime"), icon: "i-lucide-calendar-clock" },
|
|
200
|
-
date_range: { label: t("builder.fieldType.date_range"), icon: "i-lucide-calendar-range" },
|
|
201
|
-
datetime_range: { label: t("builder.fieldType.datetime_range"), icon: "i-lucide-calendar-range" },
|
|
202
435
|
file: { label: t("builder.fieldType.file"), icon: "i-lucide-paperclip" }
|
|
203
436
|
}));
|
|
204
437
|
const fieldTypeItems = computed(() => FORM_FORGE_BUILDER_FIELD_TYPES.map((type) => ({
|
|
@@ -206,88 +439,25 @@ const fieldTypeItems = computed(() => FORM_FORGE_BUILDER_FIELD_TYPES.map((type)
|
|
|
206
439
|
value: type,
|
|
207
440
|
icon: fieldTypeMeta.value[type].icon
|
|
208
441
|
})));
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
show: t("builder.condition.action.show"),
|
|
212
|
-
hide: t("builder.condition.action.hide"),
|
|
213
|
-
skip: t("builder.condition.action.skip"),
|
|
214
|
-
require: t("builder.condition.action.require"),
|
|
215
|
-
disable: t("builder.condition.action.disable")
|
|
216
|
-
};
|
|
217
|
-
return labels[action];
|
|
442
|
+
function isChoiceFieldType(type) {
|
|
443
|
+
return type === "radio" || type === "checkbox_group";
|
|
218
444
|
}
|
|
219
|
-
function
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
445
|
+
function createChoiceOption(index) {
|
|
446
|
+
return {
|
|
447
|
+
label: "",
|
|
448
|
+
value: `option_${index + 1}`
|
|
223
449
|
};
|
|
224
|
-
return labels[match];
|
|
225
450
|
}
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
neq: t("builder.condition.operator.neq"),
|
|
230
|
-
in: t("builder.condition.operator.in"),
|
|
231
|
-
not_in: t("builder.condition.operator.not_in"),
|
|
232
|
-
gt: t("builder.condition.operator.gt"),
|
|
233
|
-
gte: t("builder.condition.operator.gte"),
|
|
234
|
-
lt: t("builder.condition.operator.lt"),
|
|
235
|
-
lte: t("builder.condition.operator.lte"),
|
|
236
|
-
contains: t("builder.condition.operator.contains"),
|
|
237
|
-
not_contains: t("builder.condition.operator.not_contains"),
|
|
238
|
-
is_empty: t("builder.condition.operator.is_empty"),
|
|
239
|
-
not_empty: t("builder.condition.operator.not_empty")
|
|
240
|
-
};
|
|
241
|
-
return labels[operator];
|
|
242
|
-
}
|
|
243
|
-
const conditionActionItems = computed(() => FORM_FORGE_BUILDER_CONDITION_ACTIONS.map((action) => ({
|
|
244
|
-
label: conditionActionLabel(action),
|
|
245
|
-
value: action
|
|
246
|
-
})));
|
|
247
|
-
const conditionMatchItems = computed(() => FORM_FORGE_BUILDER_CONDITION_MATCHES.map((match) => ({
|
|
248
|
-
label: conditionMatchLabel(match),
|
|
249
|
-
value: match
|
|
250
|
-
})));
|
|
251
|
-
const conditionOperatorItems = computed(() => FORM_FORGE_BUILDER_CONDITION_OPERATORS.map((operator) => ({
|
|
252
|
-
label: conditionOperatorLabel(operator),
|
|
253
|
-
value: operator
|
|
254
|
-
})));
|
|
255
|
-
const targetTypeItems = computed(() => [
|
|
256
|
-
{ label: t("builder.targetType.page"), value: "page" },
|
|
257
|
-
{ label: t("builder.targetType.field"), value: "field" }
|
|
258
|
-
]);
|
|
259
|
-
const pageTargetItems = computed(() => {
|
|
260
|
-
const items = [];
|
|
261
|
-
for (const maybePage of safePages.value) {
|
|
262
|
-
const title = typeof maybePage.title === "string" ? maybePage.title : "";
|
|
263
|
-
items.push({
|
|
264
|
-
label: title === "" ? maybePage.page_key : `${title} (${maybePage.page_key})`,
|
|
265
|
-
value: maybePage.page_key
|
|
266
|
-
});
|
|
451
|
+
function normalizeLoadedPageLogic(pages) {
|
|
452
|
+
if (!Array.isArray(pages)) {
|
|
453
|
+
return;
|
|
267
454
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const fieldTargetItems = computed(() => {
|
|
271
|
-
const items = [];
|
|
272
|
-
for (const maybePage of safePages.value) {
|
|
273
|
-
if (!Array.isArray(maybePage.fields)) {
|
|
455
|
+
for (const page of pages) {
|
|
456
|
+
if (page === void 0 || page === null) {
|
|
274
457
|
continue;
|
|
275
458
|
}
|
|
276
|
-
|
|
277
|
-
const label = maybeField.label === void 0 || maybeField.label === "" ? maybeField.field_key : `${maybeField.label} (${maybeField.field_key})`;
|
|
278
|
-
items.push({
|
|
279
|
-
label,
|
|
280
|
-
value: maybeField.field_key
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return items;
|
|
285
|
-
});
|
|
286
|
-
function selectedPage() {
|
|
287
|
-
if (selectedPageKey.value === null) {
|
|
288
|
-
return safePages.value[0];
|
|
459
|
+
ensurePageLogic(page);
|
|
289
460
|
}
|
|
290
|
-
return safePages.value.find((page) => page.page_key === selectedPageKey.value) ?? safePages.value[0];
|
|
291
461
|
}
|
|
292
462
|
function syncSelectionWithDraft(pages) {
|
|
293
463
|
if (pages.length === 0) {
|
|
@@ -302,123 +472,29 @@ function syncSelectionWithDraft(pages) {
|
|
|
302
472
|
selectedFieldKey.value = null;
|
|
303
473
|
return;
|
|
304
474
|
}
|
|
305
|
-
|
|
306
|
-
selectedFieldKey.value = nextField.field_key;
|
|
307
|
-
}
|
|
308
|
-
function resolveHTMLElement(element) {
|
|
309
|
-
if (element instanceof HTMLElement) {
|
|
310
|
-
return element;
|
|
311
|
-
}
|
|
312
|
-
if (typeof element !== "object" || element === null || !("$el" in element)) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
const componentElement = element.$el;
|
|
316
|
-
return componentElement instanceof HTMLElement ? componentElement : null;
|
|
317
|
-
}
|
|
318
|
-
function registerFieldElement(fieldKey, element) {
|
|
319
|
-
const htmlElement = resolveHTMLElement(element);
|
|
320
|
-
if (htmlElement !== null) {
|
|
321
|
-
fieldElements.set(fieldKey, htmlElement);
|
|
322
|
-
if (fieldKey === selectedFieldKey.value) {
|
|
323
|
-
nextTick(() => {
|
|
324
|
-
updateRailPosition();
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
fieldElements.delete(fieldKey);
|
|
330
|
-
}
|
|
331
|
-
function updateRailPosition() {
|
|
332
|
-
if (!isClientReady.value || typeof window === "undefined") {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
const rootElement = builderRootElement.value;
|
|
336
|
-
if (rootElement === null) {
|
|
475
|
+
if (selectedFieldKey.value === null) {
|
|
337
476
|
return;
|
|
338
477
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
const selectedElement = selectedFieldKey.value === null ? void 0 : fieldElements.get(selectedFieldKey.value);
|
|
342
|
-
const selectedRect = selectedElement?.getBoundingClientRect();
|
|
343
|
-
if (window.innerWidth <= 1024) {
|
|
344
|
-
railLeft.value = 0;
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const railWidth = 68;
|
|
348
|
-
const horizontalGap = 14;
|
|
349
|
-
const minLeft = 8;
|
|
350
|
-
const maxLeft = window.innerWidth - railWidth - 8;
|
|
351
|
-
const horizontalAnchor = selectedRect?.right ?? columnRect.right;
|
|
352
|
-
const desiredLeft = horizontalAnchor + horizontalGap;
|
|
353
|
-
railLeft.value = Math.min(Math.max(desiredLeft, minLeft), maxLeft);
|
|
354
|
-
const railHeight = 122;
|
|
355
|
-
const minTop = Math.max(88, columnRect.top + 8);
|
|
356
|
-
const maxTop = Math.max(minTop, Math.min(window.innerHeight - railHeight - 12, columnRect.bottom - railHeight - 8));
|
|
357
|
-
if (selectedRect === void 0) {
|
|
358
|
-
railTop.value = minTop;
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
const desiredTop = selectedRect.top + selectedRect.height / 2 - railHeight / 2;
|
|
362
|
-
railTop.value = Math.min(Math.max(desiredTop, minTop), maxTop);
|
|
478
|
+
const nextField = fields.find((field) => field.field_key === selectedFieldKey.value) ?? fields[0];
|
|
479
|
+
selectedFieldKey.value = nextField.field_key;
|
|
363
480
|
}
|
|
364
|
-
onMounted(() => {
|
|
365
|
-
isClientReady.value = true;
|
|
366
|
-
window.addEventListener("scroll", updateRailPosition, { passive: true });
|
|
367
|
-
window.addEventListener("resize", updateRailPosition);
|
|
368
|
-
nextTick(() => {
|
|
369
|
-
syncSelectionWithDraft(safePages.value);
|
|
370
|
-
updateRailPosition();
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
onBeforeUnmount(() => {
|
|
374
|
-
window.removeEventListener("scroll", updateRailPosition);
|
|
375
|
-
window.removeEventListener("resize", updateRailPosition);
|
|
376
|
-
fieldElements.clear();
|
|
377
|
-
});
|
|
378
481
|
function selectField(page, fieldKey) {
|
|
379
482
|
selectedPageKey.value = page.page_key;
|
|
380
483
|
selectedFieldKey.value = fieldKey;
|
|
381
|
-
nextTick(() => {
|
|
382
|
-
updateRailPosition();
|
|
383
|
-
});
|
|
384
484
|
}
|
|
385
|
-
function
|
|
485
|
+
function findPage(pageKey) {
|
|
486
|
+
return safePages.value.find((page) => page.page_key === pageKey);
|
|
487
|
+
}
|
|
488
|
+
function selectFieldByKey(pageKey, fieldKey) {
|
|
489
|
+
const page = findPage(pageKey);
|
|
386
490
|
if (page === void 0) {
|
|
387
491
|
return;
|
|
388
492
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (page === void 0 || page === null) {
|
|
393
|
-
return "";
|
|
493
|
+
if (selectedPageKey.value === pageKey && selectedFieldKey.value === fieldKey) {
|
|
494
|
+
selectedFieldKey.value = null;
|
|
495
|
+
return;
|
|
394
496
|
}
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
function pageOrder(page) {
|
|
398
|
-
const index = safePages.value.findIndex((item) => item.page_key === page.page_key);
|
|
399
|
-
return index < 0 ? 1 : index + 1;
|
|
400
|
-
}
|
|
401
|
-
function canMergeWithPreviousPage(page) {
|
|
402
|
-
return safePages.value.length > 1 && pageOrder(page) > 1;
|
|
403
|
-
}
|
|
404
|
-
function mergePageWithPrevious(page) {
|
|
405
|
-
builder.mergePageWithPrevious(page.page_key);
|
|
406
|
-
nextTick(() => {
|
|
407
|
-
syncSelectionWithDraft(safePages.value);
|
|
408
|
-
updateRailPosition();
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
function isFieldSelected(fieldKey) {
|
|
412
|
-
return selectedFieldKey.value === fieldKey;
|
|
413
|
-
}
|
|
414
|
-
function showInlinePreview(field) {
|
|
415
|
-
return field.type === "text" || field.type === "textarea" || field.type === "email";
|
|
416
|
-
}
|
|
417
|
-
function fieldTypeLabel(type) {
|
|
418
|
-
return fieldTypeMeta.value[type]?.label ?? type;
|
|
419
|
-
}
|
|
420
|
-
function fieldTypeIcon(type) {
|
|
421
|
-
return fieldTypeMeta.value[type]?.icon ?? "i-lucide-square";
|
|
497
|
+
selectField(page, fieldKey);
|
|
422
498
|
}
|
|
423
499
|
function isUuidLike(value) {
|
|
424
500
|
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
@@ -445,12 +521,21 @@ function applyLoadedForm(schema) {
|
|
|
445
521
|
draft.value = {
|
|
446
522
|
uuid: nextUuid,
|
|
447
523
|
key: schema.key,
|
|
524
|
+
schema_version: schema.schema_version ?? 1,
|
|
448
525
|
title: schema.title,
|
|
526
|
+
publish_at: schema.publish_at ?? null,
|
|
527
|
+
pause_at: schema.pause_at ?? null,
|
|
528
|
+
response_limit: schema.response_limit ?? null,
|
|
529
|
+
submission_code_required: schema.submission_code_required === true,
|
|
530
|
+
submission_code: null,
|
|
531
|
+
public_url: schema.public_url ?? null,
|
|
449
532
|
category: schema.category ?? null,
|
|
450
533
|
pages: cloneValue(schema.pages),
|
|
451
534
|
conditions: cloneValue(schema.conditions),
|
|
452
|
-
drafts: cloneValue(schema.drafts)
|
|
535
|
+
drafts: cloneValue(schema.drafts),
|
|
536
|
+
api: cloneValue(schema.api ?? {})
|
|
453
537
|
};
|
|
538
|
+
normalizeLoadedPageLogic(draft.value.pages);
|
|
454
539
|
isPublished.value = schema.is_published === true;
|
|
455
540
|
}
|
|
456
541
|
async function loadFormIntoBuilder(key, version) {
|
|
@@ -464,7 +549,6 @@ async function loadFormIntoBuilder(key, version) {
|
|
|
464
549
|
applyLoadedForm(schema);
|
|
465
550
|
nextTick(() => {
|
|
466
551
|
syncSelectionWithDraft(safePages.value);
|
|
467
|
-
updateRailPosition();
|
|
468
552
|
});
|
|
469
553
|
} catch (caughtError) {
|
|
470
554
|
const message = caughtError instanceof Error ? caughtError.message : t("builder.error.loadForm");
|
|
@@ -486,22 +570,32 @@ async function loadFormRouteIntoBuilder(routeKey, version) {
|
|
|
486
570
|
}
|
|
487
571
|
await loadFormIntoBuilder(key, version);
|
|
488
572
|
}
|
|
489
|
-
function addField(
|
|
490
|
-
builder.addField(
|
|
573
|
+
function addField(pageKey, type) {
|
|
574
|
+
builder.addField(pageKey, type);
|
|
491
575
|
builder.normalizeFieldLocations();
|
|
492
|
-
const
|
|
493
|
-
|
|
576
|
+
const page = safePages.value.find((candidate) => candidate.page_key === pageKey);
|
|
577
|
+
const nextField = page?.fields.at(-1);
|
|
578
|
+
if (page !== void 0 && nextField !== void 0) {
|
|
494
579
|
selectField(page, nextField.field_key);
|
|
495
580
|
}
|
|
496
581
|
}
|
|
497
582
|
function onFieldTypeChange(field, nextType) {
|
|
498
583
|
field.type = nextType;
|
|
499
|
-
if (nextType
|
|
500
|
-
if (field.options ===
|
|
501
|
-
field.options = [];
|
|
584
|
+
if (isChoiceFieldType(nextType)) {
|
|
585
|
+
if (!Array.isArray(field.options) || field.options.length === 0) {
|
|
586
|
+
field.options = nextType === "checkbox_group" ? [createChoiceOption(0), createChoiceOption(1), createChoiceOption(2)] : [createChoiceOption(0), createChoiceOption(1)];
|
|
587
|
+
} else if (field.options.length < (nextType === "checkbox_group" ? 2 : 1)) {
|
|
588
|
+
const minimumOptionCount = nextType === "checkbox_group" ? 2 : 1;
|
|
589
|
+
const nextOptions = [...field.options];
|
|
590
|
+
for (let index = nextOptions.length; index < minimumOptionCount; index += 1) {
|
|
591
|
+
nextOptions.push(createChoiceOption(index));
|
|
592
|
+
}
|
|
593
|
+
field.options = nextOptions;
|
|
502
594
|
}
|
|
595
|
+
field.display = nextType === "radio" || nextType === "checkbox_group" ? "list" : "menu";
|
|
503
596
|
} else {
|
|
504
597
|
field.options = void 0;
|
|
598
|
+
field.display = void 0;
|
|
505
599
|
}
|
|
506
600
|
if (nextType === "file") {
|
|
507
601
|
field.multiple = false;
|
|
@@ -511,54 +605,26 @@ function onFieldTypeChange(field, nextType) {
|
|
|
511
605
|
});
|
|
512
606
|
}
|
|
513
607
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (field.options === void 0) {
|
|
517
|
-
field.options = [];
|
|
518
|
-
}
|
|
519
|
-
field.options.push({
|
|
520
|
-
label: t("builder.optionDefaultLabel"),
|
|
521
|
-
value: `option_${field.options.length + 1}`
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
function removeOption(field, index) {
|
|
525
|
-
if (field.options === void 0) {
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
field.options.splice(index, 1);
|
|
529
|
-
}
|
|
530
|
-
function optionLabel(option) {
|
|
531
|
-
if (option === void 0 || option === null) {
|
|
532
|
-
return "";
|
|
533
|
-
}
|
|
534
|
-
if (typeof option === "object" && "label" in option) {
|
|
535
|
-
return typeof option.label === "string" ? option.label : "";
|
|
608
|
+
if (nextType === "consent" && typeof field.consent_label !== "string") {
|
|
609
|
+
field.consent_label = resolveDefaultConsentLabel(locale.value);
|
|
536
610
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
field.options[optionIndex] = {
|
|
549
|
-
...option,
|
|
550
|
-
label: value
|
|
611
|
+
if (nextType === "address") {
|
|
612
|
+
if (!Array.isArray(field.address_fields) || field.address_fields.length === 0) {
|
|
613
|
+
field.address_fields = createDefaultAddressFields(locale.value);
|
|
614
|
+
}
|
|
615
|
+
field.default = {
|
|
616
|
+
line1: null,
|
|
617
|
+
line2: null,
|
|
618
|
+
city: null,
|
|
619
|
+
state: null,
|
|
620
|
+
zip: null,
|
|
621
|
+
country: null
|
|
551
622
|
};
|
|
552
|
-
return;
|
|
553
623
|
}
|
|
554
|
-
field.options[optionIndex] = {
|
|
555
|
-
label: value,
|
|
556
|
-
value: option
|
|
557
|
-
};
|
|
558
624
|
}
|
|
559
625
|
async function save() {
|
|
560
626
|
try {
|
|
561
|
-
const shouldAutoPublish = props.defaultPublished;
|
|
627
|
+
const shouldAutoPublish = props.defaultPublished || props.autoPublishOnSave;
|
|
562
628
|
await builder.save({
|
|
563
629
|
autoPublish: shouldAutoPublish
|
|
564
630
|
});
|
|
@@ -640,12 +706,6 @@ const canTogglePublish = computed(() => {
|
|
|
640
706
|
}
|
|
641
707
|
return true;
|
|
642
708
|
});
|
|
643
|
-
const toolbarActionsClass = computed(() => {
|
|
644
|
-
if (showTopControls.value) {
|
|
645
|
-
return ["builder-toolbar-actions"];
|
|
646
|
-
}
|
|
647
|
-
return ["builder-toolbar-actions", "builder-toolbar-actions--compact"];
|
|
648
|
-
});
|
|
649
709
|
async function togglePublishState() {
|
|
650
710
|
if (!props.defaultPublished && isPublished.value) {
|
|
651
711
|
await unpublish();
|
|
@@ -659,24 +719,41 @@ function removeField(page, fieldKey) {
|
|
|
659
719
|
selectedFieldKey.value = null;
|
|
660
720
|
}
|
|
661
721
|
}
|
|
662
|
-
function
|
|
663
|
-
const page =
|
|
722
|
+
function removeFieldByKey(pageKey, fieldKey) {
|
|
723
|
+
const page = findPage(pageKey);
|
|
664
724
|
if (page === void 0) {
|
|
665
|
-
builder.addPage();
|
|
666
|
-
nextTick(() => {
|
|
667
|
-
syncSelectionWithDraft(safePages.value);
|
|
668
|
-
updateRailPosition();
|
|
669
|
-
});
|
|
670
725
|
return;
|
|
671
726
|
}
|
|
672
|
-
|
|
727
|
+
removeField(page, fieldKey);
|
|
673
728
|
}
|
|
674
|
-
function
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
729
|
+
function duplicateFieldByKey(pageKey, fieldKey) {
|
|
730
|
+
const page = findPage(pageKey);
|
|
731
|
+
if (page === void 0) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
duplicateField(page, fieldKey);
|
|
735
|
+
}
|
|
736
|
+
function moveFieldByKey(pageKey, fieldKey, direction) {
|
|
737
|
+
builder.moveField(pageKey, fieldKey, direction);
|
|
738
|
+
}
|
|
739
|
+
function changeFieldTypeByKey(pageKey, fieldKey, nextType) {
|
|
740
|
+
const page = findPage(pageKey);
|
|
741
|
+
if (page === void 0) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const field = page.fields.find((item) => item.field_key === fieldKey);
|
|
745
|
+
if (field === void 0) {
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
onFieldTypeChange(field, nextType);
|
|
749
|
+
}
|
|
750
|
+
function addFieldBelowByKey(pageKey, fieldKey, type) {
|
|
751
|
+
builder.insertFieldAfter(pageKey, fieldKey, type);
|
|
752
|
+
builder.normalizeFieldLocations();
|
|
753
|
+
}
|
|
754
|
+
function moveFieldToBlockByKey(pageKey, fieldKey, targetPageKey) {
|
|
755
|
+
builder.moveFieldToPage(pageKey, fieldKey, targetPageKey);
|
|
756
|
+
builder.normalizeFieldLocations();
|
|
680
757
|
}
|
|
681
758
|
async function openCategoryCreateModal() {
|
|
682
759
|
if (props.readonly) {
|
|
@@ -748,6 +825,11 @@ watch(() => props.autosaveDelay, (value) => {
|
|
|
748
825
|
}, {
|
|
749
826
|
immediate: true
|
|
750
827
|
});
|
|
828
|
+
watch(() => props.autoPublishOnSave, (value) => {
|
|
829
|
+
builder.autoPublishOnSave.value = value;
|
|
830
|
+
}, {
|
|
831
|
+
immediate: true
|
|
832
|
+
});
|
|
751
833
|
watch(() => [props.loadFormKey, props.loadFormVersion], ([key, version]) => {
|
|
752
834
|
if (typeof key !== "string" || key === "") {
|
|
753
835
|
return;
|
|
@@ -769,523 +851,392 @@ watch(() => [props.formRouteKey, props.loadFormKey, props.loadFormVersion], ([ro
|
|
|
769
851
|
});
|
|
770
852
|
watch(() => safePages.value, (pages) => {
|
|
771
853
|
syncSelectionWithDraft(pages);
|
|
772
|
-
nextTick(() => {
|
|
773
|
-
updateRailPosition();
|
|
774
|
-
});
|
|
775
854
|
}, {
|
|
776
855
|
deep: true,
|
|
777
856
|
immediate: true
|
|
778
857
|
});
|
|
779
|
-
watch(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
858
|
+
watch(
|
|
859
|
+
() => [selectedPageKey.value, selectedFieldKey.value],
|
|
860
|
+
([pageKey, fieldKey]) => {
|
|
861
|
+
emit("selection-change", pageKey, fieldKey);
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
immediate: true
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
watch(settingsHidden, (value) => {
|
|
868
|
+
if (value) {
|
|
869
|
+
selectedTab.value = "builder";
|
|
870
|
+
}
|
|
871
|
+
}, {
|
|
872
|
+
immediate: true
|
|
783
873
|
});
|
|
874
|
+
const builderExpose = {
|
|
875
|
+
save,
|
|
876
|
+
publish,
|
|
877
|
+
unpublish,
|
|
878
|
+
togglePublishState
|
|
879
|
+
};
|
|
880
|
+
defineExpose(builderExpose);
|
|
784
881
|
</script>
|
|
785
882
|
|
|
786
883
|
<template>
|
|
787
884
|
<div
|
|
788
|
-
|
|
789
|
-
ref="builderRootElement"
|
|
790
|
-
class="builder-root"
|
|
885
|
+
class="w-full"
|
|
791
886
|
>
|
|
792
|
-
<div class="
|
|
887
|
+
<div :class="builderLayoutClass">
|
|
793
888
|
<div
|
|
794
|
-
|
|
795
|
-
class="builder-column"
|
|
889
|
+
class="grid min-w-0 gap-6"
|
|
796
890
|
>
|
|
797
|
-
<
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
<USelect
|
|
817
|
-
v-model="draftCategorySelectValue"
|
|
818
|
-
:items="categorySelectItems"
|
|
819
|
-
:disabled="readonly"
|
|
820
|
-
:placeholder="t('builder.categoryPlaceholder')"
|
|
821
|
-
/>
|
|
822
|
-
<UTooltip :text="t('builder.tooltip.addCategory')">
|
|
823
|
-
<UButton
|
|
824
|
-
color="neutral"
|
|
825
|
-
variant="soft"
|
|
826
|
-
icon="i-lucide-folder-plus"
|
|
827
|
-
:disabled="readonly"
|
|
828
|
-
@click="openCategoryCreateModal"
|
|
829
|
-
/>
|
|
830
|
-
</UTooltip>
|
|
831
|
-
</div>
|
|
832
|
-
</div>
|
|
833
|
-
|
|
834
|
-
<div :class="toolbarActionsClass">
|
|
835
|
-
<div class="builder-status">
|
|
836
|
-
<span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
|
|
837
|
-
<span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
|
|
838
|
-
<span
|
|
839
|
-
v-if="builderError !== null"
|
|
840
|
-
class="builder-error"
|
|
841
|
-
>{{ builderError }}</span>
|
|
842
|
-
</div>
|
|
843
|
-
|
|
844
|
-
<div class="builder-actions">
|
|
845
|
-
<UButton
|
|
846
|
-
:loading="saving"
|
|
847
|
-
:disabled="readonly"
|
|
848
|
-
@click="save"
|
|
849
|
-
>
|
|
850
|
-
{{ t("builder.save") }}
|
|
851
|
-
</UButton>
|
|
852
|
-
<UButton
|
|
853
|
-
v-if="!disablePublishAction && !defaultPublished"
|
|
854
|
-
:color="publishButtonColor"
|
|
855
|
-
:variant="publishButtonVariant"
|
|
856
|
-
:loading="publishing"
|
|
857
|
-
:disabled="!canTogglePublish"
|
|
858
|
-
@click="togglePublishState"
|
|
891
|
+
<template v-if="!settingsHidden">
|
|
892
|
+
<UTabs
|
|
893
|
+
v-model="selectedTab"
|
|
894
|
+
:items="builderTabs"
|
|
895
|
+
value-key="slot"
|
|
896
|
+
class="w-full"
|
|
897
|
+
:ui="{
|
|
898
|
+
list: 'w-full',
|
|
899
|
+
trigger: 'flex-1 justify-center'
|
|
900
|
+
}"
|
|
901
|
+
>
|
|
902
|
+
<template #builder>
|
|
903
|
+
<div class="grid gap-6">
|
|
904
|
+
<Draggable
|
|
905
|
+
v-model="draftPages"
|
|
906
|
+
item-key="page_key"
|
|
907
|
+
handle=".page-drag-handle"
|
|
908
|
+
group="formforge-pages"
|
|
909
|
+
class="grid gap-6"
|
|
859
910
|
>
|
|
860
|
-
{
|
|
861
|
-
|
|
911
|
+
<template #item="{ element: page, index }">
|
|
912
|
+
<FormForgeBuilderBlockCard
|
|
913
|
+
:page="page"
|
|
914
|
+
:pages="safePages"
|
|
915
|
+
:page-index="index"
|
|
916
|
+
:total-pages="safePages.length"
|
|
917
|
+
:selected-field-key="selectedFieldKey"
|
|
918
|
+
:readonly="readonly"
|
|
919
|
+
:field-type-items="fieldTypeItems"
|
|
920
|
+
@select-field="selectFieldByKey"
|
|
921
|
+
@move-page="builder.movePage"
|
|
922
|
+
@duplicate-page="builder.duplicatePage"
|
|
923
|
+
@remove-page="builder.removePage"
|
|
924
|
+
@add-question="addField"
|
|
925
|
+
@move-field="moveFieldByKey"
|
|
926
|
+
@duplicate-field="duplicateFieldByKey"
|
|
927
|
+
@remove-field="removeFieldByKey"
|
|
928
|
+
@change-field-type="changeFieldTypeByKey"
|
|
929
|
+
@add-field-below="addFieldBelowByKey"
|
|
930
|
+
@move-field-to-block="moveFieldToBlockByKey"
|
|
931
|
+
/>
|
|
932
|
+
</template>
|
|
933
|
+
</Draggable>
|
|
862
934
|
</div>
|
|
863
|
-
</
|
|
864
|
-
</div>
|
|
865
|
-
</UCard>
|
|
935
|
+
</template>
|
|
866
936
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
<UCard
|
|
880
|
-
variant="soft"
|
|
881
|
-
class="builder-card page-meta-card"
|
|
882
|
-
>
|
|
883
|
-
<div class="page-chip-row">
|
|
884
|
-
<UBadge
|
|
885
|
-
color="primary"
|
|
886
|
-
variant="subtle"
|
|
887
|
-
>
|
|
888
|
-
{{ t("builder.pageCounter", { current: pageOrder(page), total: safePages.length }) }}
|
|
889
|
-
</UBadge>
|
|
890
|
-
</div>
|
|
937
|
+
<template #settings>
|
|
938
|
+
<UCard variant="subtle">
|
|
939
|
+
<template #header>
|
|
940
|
+
<div class="space-y-1">
|
|
941
|
+
<p class="text-sm font-semibold text-default">
|
|
942
|
+
Settings
|
|
943
|
+
</p>
|
|
944
|
+
<p class="text-sm text-muted">
|
|
945
|
+
Configure the lifecycle, access, and publication settings for this form.
|
|
946
|
+
</p>
|
|
947
|
+
</div>
|
|
948
|
+
</template>
|
|
891
949
|
|
|
892
|
-
<div class="
|
|
893
|
-
<
|
|
894
|
-
|
|
950
|
+
<div class="space-y-6">
|
|
951
|
+
<div
|
|
952
|
+
v-if="!disableTitleInput || !disableCategoryControl"
|
|
953
|
+
class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]"
|
|
954
|
+
>
|
|
895
955
|
<UInput
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
class="grow"
|
|
899
|
-
:placeholder="t('builder.pageTitlePlaceholder')"
|
|
900
|
-
@update:model-value="(value) => updatePageTitle(page, value)"
|
|
901
|
-
/>
|
|
902
|
-
<UTextarea
|
|
903
|
-
v-model="page.description"
|
|
904
|
-
:rows="2"
|
|
956
|
+
v-if="!disableTitleInput"
|
|
957
|
+
v-model="draftTitle"
|
|
905
958
|
:disabled="readonly"
|
|
906
|
-
|
|
959
|
+
placeholder="Form title"
|
|
960
|
+
:ui="{ base: 'w-full' }"
|
|
907
961
|
/>
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
:disabled="readonly
|
|
916
|
-
|
|
962
|
+
<div
|
|
963
|
+
v-if="!disableCategoryControl"
|
|
964
|
+
class="flex flex-row items-center gap-2"
|
|
965
|
+
>
|
|
966
|
+
<USelect
|
|
967
|
+
v-model="draftCategorySelectValue"
|
|
968
|
+
:items="categorySelectItems"
|
|
969
|
+
:disabled="readonly"
|
|
970
|
+
placeholder="Category"
|
|
971
|
+
:ui="{
|
|
972
|
+
base: 'w-full'
|
|
973
|
+
}"
|
|
917
974
|
/>
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
975
|
+
<UTooltip :text="t('builder.tooltip.addCategory')">
|
|
976
|
+
<UButton
|
|
977
|
+
color="neutral"
|
|
978
|
+
variant="soft"
|
|
979
|
+
icon="i-lucide-folder-plus"
|
|
980
|
+
:disabled="readonly"
|
|
981
|
+
@click="openCategoryCreateModal"
|
|
982
|
+
/>
|
|
983
|
+
</UTooltip>
|
|
984
|
+
</div>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<div class="grid gap-6">
|
|
988
|
+
<div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
|
|
989
|
+
<div class="space-y-1">
|
|
990
|
+
<p class="text-sm font-semibold text-default">
|
|
991
|
+
Survey opening
|
|
992
|
+
</p>
|
|
993
|
+
<p class="text-sm text-muted">
|
|
994
|
+
Choose when the form becomes available.
|
|
995
|
+
</p>
|
|
996
|
+
</div>
|
|
997
|
+
<USwitch
|
|
998
|
+
v-model="openingEnabled"
|
|
999
|
+
:disabled="readonly"
|
|
926
1000
|
/>
|
|
927
|
-
</
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
<div
|
|
1004
|
+
v-if="openingEnabled"
|
|
1005
|
+
class="grid gap-4 sm:grid-cols-2"
|
|
1006
|
+
>
|
|
1007
|
+
<UFormField label="Opening date">
|
|
1008
|
+
<UInputDate
|
|
1009
|
+
v-model="publishAtDate"
|
|
1010
|
+
:disabled="readonly"
|
|
1011
|
+
:ui="{ base: 'w-full' }"
|
|
1012
|
+
/>
|
|
1013
|
+
</UFormField>
|
|
1014
|
+
<UFormField label="Opening time">
|
|
1015
|
+
<UInputTime
|
|
1016
|
+
v-model="publishAtTime"
|
|
1017
|
+
:disabled="readonly"
|
|
1018
|
+
:hour-cycle="24"
|
|
1019
|
+
:ui="{ base: 'w-full' }"
|
|
1020
|
+
/>
|
|
1021
|
+
</UFormField>
|
|
1022
|
+
</div>
|
|
928
1023
|
</div>
|
|
929
|
-
</div>
|
|
930
|
-
</UCard>
|
|
931
1024
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
<
|
|
953
|
-
<
|
|
954
|
-
|
|
955
|
-
v-model="field.label"
|
|
1025
|
+
<div class="grid gap-6">
|
|
1026
|
+
<div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
|
|
1027
|
+
<div class="space-y-1">
|
|
1028
|
+
<p class="text-sm font-semibold text-default">
|
|
1029
|
+
Survey closing
|
|
1030
|
+
</p>
|
|
1031
|
+
<p class="text-sm text-muted">
|
|
1032
|
+
Choose when the form stops accepting responses.
|
|
1033
|
+
</p>
|
|
1034
|
+
</div>
|
|
1035
|
+
<USwitch
|
|
1036
|
+
v-model="closingEnabled"
|
|
1037
|
+
:disabled="readonly"
|
|
1038
|
+
/>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
1041
|
+
<div
|
|
1042
|
+
v-if="closingEnabled"
|
|
1043
|
+
class="grid gap-4 sm:grid-cols-2"
|
|
1044
|
+
>
|
|
1045
|
+
<UFormField label="Closing date">
|
|
1046
|
+
<UInputDate
|
|
1047
|
+
v-model="pauseAtDate"
|
|
956
1048
|
:disabled="readonly"
|
|
957
|
-
:
|
|
1049
|
+
:ui="{ base: 'w-full' }"
|
|
958
1050
|
/>
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
:
|
|
965
|
-
:
|
|
1051
|
+
</UFormField>
|
|
1052
|
+
<UFormField label="Closing time">
|
|
1053
|
+
<UInputTime
|
|
1054
|
+
v-model="pauseAtTime"
|
|
1055
|
+
:disabled="readonly"
|
|
1056
|
+
:hour-cycle="24"
|
|
1057
|
+
:ui="{ base: 'w-full' }"
|
|
1058
|
+
/>
|
|
1059
|
+
</UFormField>
|
|
1060
|
+
</div>
|
|
1061
|
+
</div>
|
|
1062
|
+
|
|
1063
|
+
<div class="grid gap-6">
|
|
1064
|
+
<div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
|
|
1065
|
+
<div class="space-y-1">
|
|
1066
|
+
<p class="text-sm font-semibold text-default">
|
|
1067
|
+
Submission limit
|
|
1068
|
+
</p>
|
|
1069
|
+
<p class="text-sm text-muted">
|
|
1070
|
+
The form closes automatically after this many responses.
|
|
1071
|
+
</p>
|
|
1072
|
+
</div>
|
|
1073
|
+
<USwitch
|
|
1074
|
+
v-model="responseLimitEnabled"
|
|
1075
|
+
:disabled="readonly"
|
|
1076
|
+
/>
|
|
1077
|
+
</div>
|
|
1078
|
+
|
|
1079
|
+
<UFormField
|
|
1080
|
+
v-if="responseLimitEnabled"
|
|
1081
|
+
label="Maximum responses"
|
|
1082
|
+
>
|
|
1083
|
+
<UInputNumber
|
|
1084
|
+
v-model="responseLimitValue"
|
|
1085
|
+
:disabled="readonly"
|
|
1086
|
+
:min="1"
|
|
1087
|
+
:ui="{ base: 'w-full' }"
|
|
1088
|
+
/>
|
|
1089
|
+
</UFormField>
|
|
1090
|
+
</div>
|
|
1091
|
+
|
|
1092
|
+
<div class="grid gap-6">
|
|
1093
|
+
<div class="space-y-1">
|
|
1094
|
+
<p class="text-sm font-semibold text-default">
|
|
1095
|
+
Access
|
|
1096
|
+
</p>
|
|
1097
|
+
<p class="text-sm text-muted">
|
|
1098
|
+
Control who can submit the form and whether a PIN is required.
|
|
1099
|
+
</p>
|
|
1100
|
+
</div>
|
|
1101
|
+
|
|
1102
|
+
<div class="grid gap-4">
|
|
1103
|
+
<div class="flex items-start justify-between gap-4">
|
|
1104
|
+
<div class="space-y-1">
|
|
1105
|
+
<p class="text-sm font-medium text-default">
|
|
1106
|
+
Public form
|
|
1107
|
+
</p>
|
|
1108
|
+
<p class="text-sm text-muted">
|
|
1109
|
+
Allow anyone with the link to submit responses.
|
|
1110
|
+
</p>
|
|
1111
|
+
</div>
|
|
1112
|
+
<USwitch
|
|
1113
|
+
v-model="submissionIsPublic"
|
|
966
1114
|
:disabled="readonly"
|
|
967
|
-
@update:model-value="(value) => onFieldTypeChange(field, value)"
|
|
968
1115
|
/>
|
|
969
1116
|
</div>
|
|
970
1117
|
|
|
971
|
-
<div
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
variant="ghost"
|
|
979
|
-
icon="i-lucide-copy"
|
|
980
|
-
:disabled="readonly"
|
|
981
|
-
@click.stop="duplicateField(page, field.field_key)"
|
|
982
|
-
/>
|
|
983
|
-
</UTooltip>
|
|
984
|
-
<UTooltip :text="t('builder.tooltip.deleteQuestion')">
|
|
985
|
-
<UButton
|
|
986
|
-
color="error"
|
|
987
|
-
variant="ghost"
|
|
988
|
-
icon="i-lucide-trash-2"
|
|
989
|
-
:disabled="readonly || page.fields.length <= 1"
|
|
990
|
-
@click.stop="removeField(page, field.field_key)"
|
|
991
|
-
/>
|
|
992
|
-
</UTooltip>
|
|
993
|
-
<div class="field-required-switch">
|
|
994
|
-
<span>{{ t("builder.required") }}</span>
|
|
995
|
-
<USwitch
|
|
996
|
-
v-model="field.required"
|
|
997
|
-
:disabled="readonly"
|
|
1118
|
+
<div v-if="submissionIsPublic">
|
|
1119
|
+
<UFormField label="Public link">
|
|
1120
|
+
<UInput
|
|
1121
|
+
:model-value="publicUrlValue"
|
|
1122
|
+
disabled
|
|
1123
|
+
placeholder="Save the form to generate a link"
|
|
1124
|
+
:ui="{ base: 'w-full' }"
|
|
998
1125
|
/>
|
|
999
|
-
</
|
|
1126
|
+
</UFormField>
|
|
1000
1127
|
</div>
|
|
1001
1128
|
|
|
1002
|
-
<div
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
v-if="field.type === 'textarea'"
|
|
1011
|
-
class="field-inline-preview-textarea"
|
|
1012
|
-
>
|
|
1013
|
-
<span />
|
|
1014
|
-
<span />
|
|
1129
|
+
<div class="flex items-start justify-between gap-4 border-t border-muted pt-4">
|
|
1130
|
+
<div class="space-y-1">
|
|
1131
|
+
<p class="text-sm font-medium text-default">
|
|
1132
|
+
PIN protection
|
|
1133
|
+
</p>
|
|
1134
|
+
<p class="text-sm text-muted">
|
|
1135
|
+
Require a PIN before the form can be submitted.
|
|
1136
|
+
</p>
|
|
1015
1137
|
</div>
|
|
1016
|
-
<
|
|
1017
|
-
v-
|
|
1018
|
-
|
|
1138
|
+
<USwitch
|
|
1139
|
+
v-model="submissionPinEnabled"
|
|
1140
|
+
:disabled="readonly"
|
|
1019
1141
|
/>
|
|
1020
1142
|
</div>
|
|
1021
1143
|
|
|
1022
|
-
<div
|
|
1023
|
-
|
|
1024
|
-
class="field-options"
|
|
1025
|
-
>
|
|
1026
|
-
<div
|
|
1027
|
-
v-for="(option, optionIndex) in field.options"
|
|
1028
|
-
:key="optionIndex"
|
|
1029
|
-
class="field-option-row"
|
|
1030
|
-
>
|
|
1144
|
+
<div v-if="submissionPinEnabled">
|
|
1145
|
+
<UFormField label="PIN">
|
|
1031
1146
|
<UInput
|
|
1032
|
-
|
|
1033
|
-
:disabled="readonly"
|
|
1034
|
-
:placeholder="t('builder.optionLabelPlaceholder')"
|
|
1035
|
-
@update:model-value="(value) => setOptionLabel(field, optionIndex, value)"
|
|
1036
|
-
/>
|
|
1037
|
-
<UButton
|
|
1038
|
-
color="neutral"
|
|
1039
|
-
variant="ghost"
|
|
1040
|
-
icon="i-lucide-x"
|
|
1147
|
+
v-model="submissionPinValue"
|
|
1041
1148
|
:disabled="readonly"
|
|
1042
|
-
|
|
1149
|
+
type="password"
|
|
1150
|
+
placeholder="Enter PIN"
|
|
1151
|
+
:ui="{ base: 'w-full' }"
|
|
1043
1152
|
/>
|
|
1044
|
-
</
|
|
1045
|
-
<UButton
|
|
1046
|
-
color="neutral"
|
|
1047
|
-
variant="soft"
|
|
1048
|
-
icon="i-lucide-plus"
|
|
1049
|
-
:disabled="readonly"
|
|
1050
|
-
@click="addOption(field)"
|
|
1051
|
-
>
|
|
1052
|
-
{{ t("builder.addOption") }}
|
|
1053
|
-
</UButton>
|
|
1153
|
+
</UFormField>
|
|
1054
1154
|
</div>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1055
1157
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
>
|
|
1060
|
-
|
|
1061
|
-
|
|
1158
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-muted pt-4">
|
|
1159
|
+
<div class="flex flex-wrap gap-2 text-sm text-muted">
|
|
1160
|
+
<span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
|
|
1161
|
+
<span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
|
|
1162
|
+
<span
|
|
1163
|
+
v-if="builderError !== null"
|
|
1164
|
+
class="text-error"
|
|
1165
|
+
>{{ builderError }}</span>
|
|
1166
|
+
</div>
|
|
1062
1167
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1168
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
1169
|
+
<UButton
|
|
1170
|
+
:loading="saving"
|
|
1171
|
+
:disabled="readonly"
|
|
1172
|
+
@click="save"
|
|
1066
1173
|
>
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
v-model="field.help_text"
|
|
1080
|
-
:disabled="readonly"
|
|
1081
|
-
:rows="2"
|
|
1082
|
-
:placeholder="t('builder.helpTextPlaceholder')"
|
|
1083
|
-
/>
|
|
1084
|
-
</div>
|
|
1085
|
-
|
|
1086
|
-
<div
|
|
1087
|
-
v-if="field.type === 'number'"
|
|
1088
|
-
class="field-numbers"
|
|
1089
|
-
>
|
|
1090
|
-
<UInput
|
|
1091
|
-
v-model="field.min"
|
|
1092
|
-
:disabled="readonly"
|
|
1093
|
-
:placeholder="t('builder.minPlaceholder')"
|
|
1094
|
-
/>
|
|
1095
|
-
<UInput
|
|
1096
|
-
v-model="field.max"
|
|
1097
|
-
:disabled="readonly"
|
|
1098
|
-
:placeholder="t('builder.maxPlaceholder')"
|
|
1099
|
-
/>
|
|
1100
|
-
<UInput
|
|
1101
|
-
v-model="field.step"
|
|
1102
|
-
:disabled="readonly"
|
|
1103
|
-
:placeholder="t('builder.stepPlaceholder')"
|
|
1104
|
-
/>
|
|
1105
|
-
</div>
|
|
1106
|
-
|
|
1107
|
-
<div
|
|
1108
|
-
v-if="field.type === 'file'"
|
|
1109
|
-
class="field-file"
|
|
1110
|
-
>
|
|
1111
|
-
<UCheckbox
|
|
1112
|
-
v-model="field.multiple"
|
|
1113
|
-
:disabled="readonly"
|
|
1114
|
-
:label="t('builder.multiple')"
|
|
1115
|
-
/>
|
|
1116
|
-
<UInput
|
|
1117
|
-
:model-value="field.accept?.join(',') ?? ''"
|
|
1118
|
-
:disabled="readonly"
|
|
1119
|
-
:placeholder="t('builder.acceptedExtensionsPlaceholder')"
|
|
1120
|
-
@update:model-value="(value) => {
|
|
1121
|
-
field.accept = value.split(',').map((item) => item.trim()).filter((item) => item !== '');
|
|
1122
|
-
}"
|
|
1123
|
-
/>
|
|
1124
|
-
</div>
|
|
1125
|
-
</div>
|
|
1126
|
-
</details>
|
|
1174
|
+
{{ t("builder.save") }}
|
|
1175
|
+
</UButton>
|
|
1176
|
+
<UButton
|
|
1177
|
+
v-if="!disablePublishAction && !defaultPublished"
|
|
1178
|
+
:color="publishButtonColor"
|
|
1179
|
+
:variant="publishButtonVariant"
|
|
1180
|
+
:loading="publishing"
|
|
1181
|
+
:disabled="!canTogglePublish"
|
|
1182
|
+
@click="togglePublishState"
|
|
1183
|
+
>
|
|
1184
|
+
{{ publishButtonLabel }}
|
|
1185
|
+
</UButton>
|
|
1127
1186
|
</div>
|
|
1128
|
-
</
|
|
1129
|
-
</
|
|
1130
|
-
</
|
|
1131
|
-
</
|
|
1132
|
-
</
|
|
1133
|
-
</
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</UCard>
|
|
1190
|
+
</template>
|
|
1191
|
+
</UTabs>
|
|
1192
|
+
</template>
|
|
1134
1193
|
|
|
1135
|
-
<
|
|
1136
|
-
|
|
1137
|
-
class="
|
|
1194
|
+
<div
|
|
1195
|
+
v-else
|
|
1196
|
+
class="grid gap-6"
|
|
1138
1197
|
>
|
|
1139
|
-
<
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
variant="soft"
|
|
1146
|
-
icon="i-lucide-plus"
|
|
1147
|
-
:disabled="readonly"
|
|
1148
|
-
@click="builder.addCondition()"
|
|
1149
|
-
>
|
|
1150
|
-
{{ t("builder.addCondition") }}
|
|
1151
|
-
</UButton>
|
|
1152
|
-
</div>
|
|
1153
|
-
|
|
1154
|
-
<div
|
|
1155
|
-
v-for="condition in safeConditions"
|
|
1156
|
-
:key="condition.condition_key"
|
|
1157
|
-
class="condition-card"
|
|
1198
|
+
<Draggable
|
|
1199
|
+
v-model="draftPages"
|
|
1200
|
+
item-key="page_key"
|
|
1201
|
+
handle=".page-drag-handle"
|
|
1202
|
+
group="formforge-pages"
|
|
1203
|
+
class="grid gap-6"
|
|
1158
1204
|
>
|
|
1159
|
-
<
|
|
1160
|
-
<
|
|
1161
|
-
|
|
1162
|
-
:
|
|
1163
|
-
:
|
|
1205
|
+
<template #item="{ element: page, index }">
|
|
1206
|
+
<FormForgeBuilderBlockCard
|
|
1207
|
+
:page="page"
|
|
1208
|
+
:pages="safePages"
|
|
1209
|
+
:page-index="index"
|
|
1210
|
+
:total-pages="safePages.length"
|
|
1211
|
+
:selected-field-key="selectedFieldKey"
|
|
1212
|
+
:readonly="readonly"
|
|
1213
|
+
:field-type-items="fieldTypeItems"
|
|
1214
|
+
@select-field="selectFieldByKey"
|
|
1215
|
+
@move-page="builder.movePage"
|
|
1216
|
+
@duplicate-page="builder.duplicatePage"
|
|
1217
|
+
@remove-page="builder.removePage"
|
|
1218
|
+
@add-question="addField"
|
|
1219
|
+
@move-field="moveFieldByKey"
|
|
1220
|
+
@duplicate-field="duplicateFieldByKey"
|
|
1221
|
+
@remove-field="removeFieldByKey"
|
|
1222
|
+
@change-field-type="changeFieldTypeByKey"
|
|
1223
|
+
@add-field-below="addFieldBelowByKey"
|
|
1224
|
+
@move-field-to-block="moveFieldToBlockByKey"
|
|
1164
1225
|
/>
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
v-model="condition.target_key"
|
|
1168
|
-
:items="pageTargetItems"
|
|
1169
|
-
:disabled="readonly"
|
|
1170
|
-
/>
|
|
1171
|
-
<USelect
|
|
1172
|
-
v-else
|
|
1173
|
-
v-model="condition.target_key"
|
|
1174
|
-
:items="fieldTargetItems"
|
|
1175
|
-
:disabled="readonly"
|
|
1176
|
-
/>
|
|
1177
|
-
<USelect
|
|
1178
|
-
v-model="condition.action"
|
|
1179
|
-
:items="conditionActionItems"
|
|
1180
|
-
:disabled="readonly"
|
|
1181
|
-
/>
|
|
1182
|
-
<USelect
|
|
1183
|
-
v-model="condition.match"
|
|
1184
|
-
:items="conditionMatchItems"
|
|
1185
|
-
:disabled="readonly"
|
|
1186
|
-
/>
|
|
1187
|
-
</div>
|
|
1188
|
-
|
|
1189
|
-
<div class="space-y-2">
|
|
1190
|
-
<div
|
|
1191
|
-
v-for="(clause, clauseIndex) in condition.when"
|
|
1192
|
-
:key="`${condition.condition_key}-${clauseIndex}`"
|
|
1193
|
-
class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_180px_1fr_auto]"
|
|
1194
|
-
>
|
|
1195
|
-
<USelect
|
|
1196
|
-
v-model="clause.field_key"
|
|
1197
|
-
:items="fieldTargetItems"
|
|
1198
|
-
:disabled="readonly"
|
|
1199
|
-
/>
|
|
1200
|
-
<USelect
|
|
1201
|
-
v-model="clause.operator"
|
|
1202
|
-
:items="conditionOperatorItems"
|
|
1203
|
-
:disabled="readonly"
|
|
1204
|
-
/>
|
|
1205
|
-
<UInput
|
|
1206
|
-
v-if="clause.operator !== 'is_empty' && clause.operator !== 'not_empty'"
|
|
1207
|
-
:model-value="typeof clause.value === 'string' ? clause.value : String(clause.value ?? '')"
|
|
1208
|
-
:disabled="readonly"
|
|
1209
|
-
:placeholder="t('builder.valuePlaceholder')"
|
|
1210
|
-
@update:model-value="(value) => {
|
|
1211
|
-
clause.value = value;
|
|
1212
|
-
}"
|
|
1213
|
-
/>
|
|
1214
|
-
<div v-else />
|
|
1215
|
-
<UButton
|
|
1216
|
-
color="neutral"
|
|
1217
|
-
variant="soft"
|
|
1218
|
-
:disabled="readonly || condition.when.length <= 1"
|
|
1219
|
-
@click="builder.removeConditionClause(condition.condition_key, clauseIndex)"
|
|
1220
|
-
>
|
|
1221
|
-
{{ t("builder.remove") }}
|
|
1222
|
-
</UButton>
|
|
1223
|
-
</div>
|
|
1224
|
-
</div>
|
|
1225
|
-
|
|
1226
|
-
<div class="condition-actions">
|
|
1227
|
-
<UButton
|
|
1228
|
-
color="neutral"
|
|
1229
|
-
variant="soft"
|
|
1230
|
-
:disabled="readonly"
|
|
1231
|
-
@click="builder.addConditionClause(condition.condition_key)"
|
|
1232
|
-
>
|
|
1233
|
-
{{ t("builder.addClause") }}
|
|
1234
|
-
</UButton>
|
|
1235
|
-
<UButton
|
|
1236
|
-
color="error"
|
|
1237
|
-
variant="soft"
|
|
1238
|
-
:disabled="readonly"
|
|
1239
|
-
@click="builder.removeCondition(condition.condition_key)"
|
|
1240
|
-
>
|
|
1241
|
-
{{ t("builder.deleteCondition") }}
|
|
1242
|
-
</UButton>
|
|
1243
|
-
</div>
|
|
1244
|
-
</div>
|
|
1245
|
-
</UCard>
|
|
1246
|
-
</div>
|
|
1247
|
-
|
|
1248
|
-
<aside class="builder-rail-column">
|
|
1249
|
-
<div
|
|
1250
|
-
class="builder-rail"
|
|
1251
|
-
:style="{ top: `${railTop}px`, left: `${railLeft}px` }"
|
|
1252
|
-
>
|
|
1253
|
-
<UTooltip :text="t('builder.rail.addQuestion')">
|
|
1254
|
-
<UButton
|
|
1255
|
-
color="neutral"
|
|
1256
|
-
variant="soft"
|
|
1257
|
-
icon="i-lucide-circle-plus"
|
|
1258
|
-
class="rail-action"
|
|
1259
|
-
:disabled="readonly"
|
|
1260
|
-
@click="addQuestionFromRail"
|
|
1261
|
-
/>
|
|
1262
|
-
</UTooltip>
|
|
1263
|
-
<UTooltip :text="t('builder.rail.addPage')">
|
|
1264
|
-
<UButton
|
|
1265
|
-
color="neutral"
|
|
1266
|
-
variant="soft"
|
|
1267
|
-
icon="i-lucide-file-plus-2"
|
|
1268
|
-
class="rail-action"
|
|
1269
|
-
:disabled="readonly"
|
|
1270
|
-
@click="addPageFromRail"
|
|
1271
|
-
/>
|
|
1272
|
-
</UTooltip>
|
|
1226
|
+
</template>
|
|
1227
|
+
</Draggable>
|
|
1273
1228
|
</div>
|
|
1274
|
-
</
|
|
1229
|
+
</div>
|
|
1275
1230
|
</div>
|
|
1276
1231
|
</div>
|
|
1277
1232
|
<div
|
|
1278
|
-
v-
|
|
1233
|
+
v-if="loadingRemoteForm"
|
|
1279
1234
|
class="space-y-6"
|
|
1280
1235
|
>
|
|
1281
1236
|
<UCard>
|
|
1282
|
-
<div class="text-sm text-
|
|
1237
|
+
<div class="text-sm text-gray-500">
|
|
1283
1238
|
{{ t("builder.loadingBuilder") }}
|
|
1284
1239
|
</div>
|
|
1285
1240
|
</UCard>
|
|
1286
1241
|
</div>
|
|
1287
1242
|
</template>
|
|
1288
|
-
|
|
1289
|
-
<style scoped>
|
|
1290
|
-
.builder-root{width:100%}.builder-layout{align-items:start;display:grid;gap:1rem;grid-template-columns:minmax(0,1fr) auto;width:100%}.builder-column{display:grid;gap:1.5rem;min-width:0;width:100%}.builder-card{border-radius:16px}.builder-toolbar{display:grid;gap:1rem}.builder-toolbar-grid{display:grid;gap:.85rem;grid-template-columns:1fr}.builder-category-control{align-items:center;display:grid;gap:.5rem;grid-template-columns:minmax(0,1fr) auto}.builder-toolbar-actions{align-items:center;border-top:1px solid var(--ui-border-muted);display:flex;flex-wrap:wrap;gap:.75rem;justify-content:space-between;padding-top:.85rem}.builder-toolbar-actions--compact{border-top:none;padding-top:0}.builder-status{color:var(--ui-text-muted);font-size:.82rem}.builder-error{color:var(--ui-error);margin-left:.5rem}.builder-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-stack{display:grid;gap:1rem}.pages-stack{gap:2.25rem}.drag-handle{color:var(--ui-text-muted);cursor:grab;padding-top:.4rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.page-block{display:grid;gap:1rem}.page-meta-card{border-radius:16px}.page-chip-row{margin-bottom:.65rem}.page-header{align-items:start;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) auto}.page-header-main{display:grid;gap:.6rem;min-width:0}.page-actions{align-items:center;display:inline-flex;gap:.25rem}.page-questions-stack{gap:1rem}.field-card{border-left:4px solid transparent;border-radius:14px;cursor:pointer}.field-card--active{border-left-color:var(--ui-primary)}.field-shell{display:grid;gap:.85rem}.field-head{align-items:center;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) minmax(220px,280px)}.field-controls{align-items:center;display:flex;flex-wrap:wrap;gap:.35rem}.field-required-switch{align-items:center;color:var(--ui-text-toned);display:inline-flex;font-size:.82rem;gap:.5rem;margin-left:auto}.field-preview{color:var(--ui-text-muted);font-size:.84rem;margin:0}.field-inline-preview{background:var(--ui-bg-muted);border-radius:10px;display:grid;gap:.5rem;padding:.75rem}.field-inline-preview-label{color:var(--ui-text-muted);font-size:.8rem;margin:0}.field-inline-preview-line{border-bottom:1px dashed var(--ui-border-accented);height:1.4rem;width:min(32rem,100%)}.field-inline-preview-textarea{display:grid;gap:.5rem;width:min(32rem,100%)}.field-inline-preview-textarea>span{border-bottom:1px dashed var(--ui-border-accented);display:block;height:1.2rem}.field-advanced{border-top:1px solid var(--ui-border-muted);padding-top:.75rem}.field-advanced-summary{color:var(--ui-text-toned);cursor:pointer;font-size:.82rem;font-weight:500}.field-advanced-body{background:var(--ui-bg-elevated);border-radius:12px;display:grid;gap:.75rem;margin-top:.75rem;padding:.75rem}.field-advanced-grid{display:grid;gap:.65rem}.field-toggle-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.field-options,.field-toggle-grid{display:grid;gap:.6rem}.field-option-row{align-items:center;display:grid;gap:.45rem;grid-template-columns:minmax(0,1fr) auto}.field-numbers{grid-template-columns:repeat(3,minmax(0,1fr))}.field-file,.field-numbers{display:grid;gap:.55rem}.conditions-card{border-radius:16px}.builder-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.conditions-title{font-size:.95rem;font-weight:600}.condition-card{border:1px solid var(--ui-border-muted);border-radius:12px;display:grid;gap:.75rem;padding:.75rem}.condition-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-rail-column{display:flex;justify-content:center;width:4.25rem}.builder-rail{background:var(--ui-bg);border:1px solid var(--ui-border-muted);border-radius:14px;box-shadow:0 8px 24px color-mix(in srgb,var(--ui-text) 8%,transparent);display:flex;flex-direction:column;gap:.5rem;padding:.5rem;position:fixed;transition:top .12s ease,left .12s ease;z-index:20}.rail-action{height:2.75rem;justify-content:center;width:2.75rem}@media (min-width:1024px){.builder-toolbar-grid{grid-template-columns:1.2fr .8fr}}@media (max-width:1024px){.builder-layout{display:block}.builder-rail{bottom:.85rem;flex-direction:row;left:auto!important;position:fixed;right:.85rem;top:auto!important}}@media (max-width:768px){.page-header{grid-template-columns:minmax(0,1fr) auto}.page-drag-handle{display:none}.field-head{grid-template-columns:minmax(0,1fr)}.field-drag-handle{display:none}.field-numbers,.field-toggle-grid{grid-template-columns:1fr}.field-required-switch{margin-left:0}}
|
|
1291
|
-
</style>
|