@evanschleret/formforgeclient 1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/module.cjs +112 -0
- package/dist/module.d.cts +20 -0
- package/dist/module.d.mts +20 -0
- package/dist/module.d.ts +20 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +109 -0
- package/dist/runtime/api/categories.d.ts +9 -0
- package/dist/runtime/api/categories.js +83 -0
- package/dist/runtime/api/client.d.ts +45 -0
- package/dist/runtime/api/client.js +148 -0
- package/dist/runtime/api/drafts.d.ts +6 -0
- package/dist/runtime/api/drafts.js +77 -0
- package/dist/runtime/api/http.d.ts +3 -0
- package/dist/runtime/api/http.js +138 -0
- package/dist/runtime/api/index.d.ts +9 -0
- package/dist/runtime/api/index.js +11 -0
- package/dist/runtime/api/management.d.ts +19 -0
- package/dist/runtime/api/management.js +180 -0
- package/dist/runtime/api/request.d.ts +8 -0
- package/dist/runtime/api/request.js +52 -0
- package/dist/runtime/api/responses.d.ts +6 -0
- package/dist/runtime/api/responses.js +61 -0
- package/dist/runtime/api/schema.d.ts +7 -0
- package/dist/runtime/api/schema.js +56 -0
- package/dist/runtime/api/submission.d.ts +11 -0
- package/dist/runtime/api/submission.js +47 -0
- package/dist/runtime/api/upload.d.ts +8 -0
- package/dist/runtime/api/upload.js +37 -0
- package/dist/runtime/composables/index.d.ts +31 -0
- package/dist/runtime/composables/index.js +16 -0
- package/dist/runtime/composables/useFormForgeApi.d.ts +3 -0
- package/dist/runtime/composables/useFormForgeApi.js +4 -0
- package/dist/runtime/composables/useFormForgeBuilder.d.ts +57 -0
- package/dist/runtime/composables/useFormForgeBuilder.js +515 -0
- package/dist/runtime/composables/useFormForgeCategory.d.ts +61 -0
- package/dist/runtime/composables/useFormForgeCategory.js +248 -0
- package/dist/runtime/composables/useFormForgeClient.d.ts +3 -0
- package/dist/runtime/composables/useFormForgeClient.js +200 -0
- package/dist/runtime/composables/useFormForgeDrafts.d.ts +20 -0
- package/dist/runtime/composables/useFormForgeDrafts.js +78 -0
- package/dist/runtime/composables/useFormForgeForm.d.ts +26 -0
- package/dist/runtime/composables/useFormForgeForm.js +114 -0
- package/dist/runtime/composables/useFormForgeGetForm.d.ts +22 -0
- package/dist/runtime/composables/useFormForgeGetForm.js +36 -0
- package/dist/runtime/composables/useFormForgeI18n.d.ts +250 -0
- package/dist/runtime/composables/useFormForgeI18n.js +324 -0
- package/dist/runtime/composables/useFormForgeManagement.d.ts +40 -0
- package/dist/runtime/composables/useFormForgeManagement.js +153 -0
- package/dist/runtime/composables/useFormForgeResolver.d.ts +28 -0
- package/dist/runtime/composables/useFormForgeResolver.js +88 -0
- package/dist/runtime/composables/useFormForgeResponses.d.ts +45 -0
- package/dist/runtime/composables/useFormForgeResponses.js +206 -0
- package/dist/runtime/composables/useFormForgeSchema.d.ts +24 -0
- package/dist/runtime/composables/useFormForgeSchema.js +69 -0
- package/dist/runtime/composables/useFormForgeSubmission.d.ts +12 -0
- package/dist/runtime/composables/useFormForgeSubmission.js +4 -0
- package/dist/runtime/composables/useFormForgeSubmit.d.ts +29 -0
- package/dist/runtime/composables/useFormForgeSubmit.js +291 -0
- package/dist/runtime/composables/useFormForgeUploads.d.ts +21 -0
- package/dist/runtime/composables/useFormForgeUploads.js +37 -0
- package/dist/runtime/composables/useFormForgeWizard.d.ts +20 -0
- package/dist/runtime/composables/useFormForgeWizard.js +83 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.js +14 -0
- package/dist/runtime/plugin.d.ts +3 -0
- package/dist/runtime/plugin.js +175 -0
- package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +40 -0
- package/dist/runtime/renderers/default/FormForgeBuilder.vue +1159 -0
- package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +40 -0
- package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.d.vue.ts +16 -0
- package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue +129 -0
- package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue.d.ts +16 -0
- package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +72 -0
- package/dist/runtime/renderers/default/FormForgeRenderer.vue +1188 -0
- package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +72 -0
- package/dist/runtime/renderers/default/FormForgeResponse.d.vue.ts +18 -0
- package/dist/runtime/renderers/default/FormForgeResponse.vue +744 -0
- package/dist/runtime/renderers/default/FormForgeResponse.vue.d.ts +18 -0
- package/dist/runtime/renderers/default/index.d.ts +5 -0
- package/dist/runtime/renderers/default/index.js +4 -0
- package/dist/runtime/renderers/index.d.ts +2 -0
- package/dist/runtime/renderers/index.js +1 -0
- package/dist/runtime/types/api.d.ts +129 -0
- package/dist/runtime/types/api.js +0 -0
- package/dist/runtime/types/category.d.ts +42 -0
- package/dist/runtime/types/category.js +0 -0
- package/dist/runtime/types/errors.d.ts +16 -0
- package/dist/runtime/types/errors.js +0 -0
- package/dist/runtime/types/index.d.ts +8 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/runtime/types/json.d.ts +6 -0
- package/dist/runtime/types/json.js +0 -0
- package/dist/runtime/types/management.d.ts +46 -0
- package/dist/runtime/types/management.js +0 -0
- package/dist/runtime/types/nuxt.d.ts +13 -0
- package/dist/runtime/types/nuxt.js +1 -0
- package/dist/runtime/types/schema.d.ts +93 -0
- package/dist/runtime/types/schema.js +0 -0
- package/dist/runtime/utils/category.d.ts +5 -0
- package/dist/runtime/utils/category.js +101 -0
- package/dist/runtime/utils/form-data.d.ts +8 -0
- package/dist/runtime/utils/form-data.js +64 -0
- package/dist/runtime/utils/object.d.ts +8 -0
- package/dist/runtime/utils/object.js +43 -0
- package/dist/runtime/utils/schema.d.ts +3 -0
- package/dist/runtime/utils/schema.js +309 -0
- package/dist/runtime/utils/submission.d.ts +4 -0
- package/dist/runtime/utils/submission.js +45 -0
- package/dist/runtime/validation/errors.d.ts +5 -0
- package/dist/runtime/validation/errors.js +130 -0
- package/dist/runtime/validation/zod.d.ts +6 -0
- package/dist/runtime/validation/zod.js +203 -0
- package/dist/runtime.cjs +16 -0
- package/dist/runtime.d.cts +1 -0
- package/dist/runtime.d.mts +1 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.mjs +1 -0
- package/dist/types.d.mts +3 -0
- package/package.json +60 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, useRoute, watch } from "#imports";
|
|
3
|
+
import { useFormForgeGetForm } from "../../composables/useFormForgeGetForm";
|
|
4
|
+
import { useFormForgeI18n } from "../../composables/useFormForgeI18n";
|
|
5
|
+
import { useFormForgeResponses } from "../../composables/useFormForgeResponses";
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
responseUuid: { type: String, required: true },
|
|
8
|
+
formKey: { type: String, required: false, default: void 0 },
|
|
9
|
+
endpoint: { type: String, required: false, default: void 0 },
|
|
10
|
+
layout: { type: String, required: false, default: "line" },
|
|
11
|
+
clientConfig: { type: Object, required: false, default: void 0 }
|
|
12
|
+
});
|
|
13
|
+
const route = useRoute();
|
|
14
|
+
const { t } = useFormForgeI18n({
|
|
15
|
+
locale: () => props.clientConfig?.locale
|
|
16
|
+
});
|
|
17
|
+
const responseResource = ref(null);
|
|
18
|
+
const localError = ref(null);
|
|
19
|
+
const formLoadError = ref(null);
|
|
20
|
+
const previewImage = ref(null);
|
|
21
|
+
const resolvedFormKey = computed(() => {
|
|
22
|
+
if (typeof props.formKey === "string" && props.formKey.trim() !== "") {
|
|
23
|
+
return props.formKey.trim();
|
|
24
|
+
}
|
|
25
|
+
const routeForm = route.params.form;
|
|
26
|
+
if (typeof routeForm === "string" && routeForm.trim() !== "") {
|
|
27
|
+
return routeForm.trim();
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(routeForm) && typeof routeForm[0] === "string" && routeForm[0].trim() !== "") {
|
|
30
|
+
return routeForm[0].trim();
|
|
31
|
+
}
|
|
32
|
+
return "";
|
|
33
|
+
});
|
|
34
|
+
const resolvedResponseUuid = computed(() => {
|
|
35
|
+
return typeof props.responseUuid === "string" ? props.responseUuid.trim() : "";
|
|
36
|
+
});
|
|
37
|
+
const responses = useFormForgeResponses({
|
|
38
|
+
key: resolvedFormKey.value === "" ? "__missing_form_key__" : resolvedFormKey.value,
|
|
39
|
+
immediate: false,
|
|
40
|
+
endpoint: props.endpoint,
|
|
41
|
+
querySync: {
|
|
42
|
+
enabled: false
|
|
43
|
+
},
|
|
44
|
+
clientConfig: props.clientConfig
|
|
45
|
+
});
|
|
46
|
+
const formSchema = useFormForgeGetForm({
|
|
47
|
+
endpoint: props.endpoint,
|
|
48
|
+
clientConfig: props.clientConfig
|
|
49
|
+
});
|
|
50
|
+
const loading = computed(() => responses.loading.value || formSchema.loading.value);
|
|
51
|
+
const isLineLayout = computed(() => props.layout === "line");
|
|
52
|
+
const isPreviewOpen = computed(() => previewImage.value !== null);
|
|
53
|
+
const error = computed(() => {
|
|
54
|
+
return localError.value ?? responses.error.value;
|
|
55
|
+
});
|
|
56
|
+
const payload = computed(() => {
|
|
57
|
+
if (responseResource.value === null) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
const responsePayload = responseResource.value.payload;
|
|
61
|
+
if (isRecord(responsePayload)) {
|
|
62
|
+
return responsePayload;
|
|
63
|
+
}
|
|
64
|
+
return {};
|
|
65
|
+
});
|
|
66
|
+
function isRecord(value) {
|
|
67
|
+
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
function pickString(value, keys) {
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
const candidate = value[key];
|
|
75
|
+
if (typeof candidate === "string" && candidate.trim() !== "") {
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
function basename(path) {
|
|
82
|
+
const value = path.split("/").at(-1);
|
|
83
|
+
if (typeof value === "string" && value !== "") {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return path;
|
|
87
|
+
}
|
|
88
|
+
function looksLikeImage(name, mimeType) {
|
|
89
|
+
if (typeof mimeType === "string" && mimeType.startsWith("image/")) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return /\.(png|jpe?g|gif|webp|svg|bmp|ico|avif)$/i.test(name);
|
|
93
|
+
}
|
|
94
|
+
function looksLikeFileReference(value) {
|
|
95
|
+
const trimmed = value.trim();
|
|
96
|
+
if (trimmed === "") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("/")) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.includes("/")) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return /\.(pdf|docx?|xlsx?|csv|txt|zip|png|jpe?g|gif|webp|svg|bmp|ico|avif)(\?.*)?$/i.test(trimmed);
|
|
106
|
+
}
|
|
107
|
+
function normalizeFileItem(value, index) {
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
const trimmed = value.trim();
|
|
110
|
+
if (!looksLikeFileReference(trimmed)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const name2 = basename(trimmed);
|
|
114
|
+
return {
|
|
115
|
+
id: `file-${index}`,
|
|
116
|
+
name: name2,
|
|
117
|
+
url: trimmed,
|
|
118
|
+
isImage: looksLikeImage(name2)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (!isRecord(value)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if ("start" in value || "end" in value) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const url = pickString(value, ["url", "download_url", "preview_url", "signed_url", "public_url", "path"]) ?? null;
|
|
128
|
+
const mimeType = pickString(value, ["mime_type", "mimeType"]);
|
|
129
|
+
const name = pickString(value, ["original_name", "file_name", "filename", "name"]) ?? (url !== null ? basename(url) : t("response.file.unnamed"));
|
|
130
|
+
const hasFileSignal = url !== null || mimeType !== void 0 || "disk" in value || "size" in value || "extension" in value;
|
|
131
|
+
if (!hasFileSignal) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
id: `file-${index}`,
|
|
136
|
+
name,
|
|
137
|
+
url,
|
|
138
|
+
mimeType,
|
|
139
|
+
isImage: looksLikeImage(name, mimeType)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function collectFiles(value, allowFiles) {
|
|
143
|
+
if (!allowFiles) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
const files = [];
|
|
148
|
+
for (const [index, item] of value.entries()) {
|
|
149
|
+
const normalized = normalizeFileItem(item, index);
|
|
150
|
+
if (normalized !== null) {
|
|
151
|
+
files.push(normalized);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return files;
|
|
155
|
+
}
|
|
156
|
+
const single = normalizeFileItem(value, 0);
|
|
157
|
+
if (single !== null) {
|
|
158
|
+
return [single];
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
function formatAnswerText(value) {
|
|
163
|
+
if (value === void 0 || value === null) {
|
|
164
|
+
return t("response.answer.empty");
|
|
165
|
+
}
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
const trimmed = value.trim();
|
|
168
|
+
return trimmed === "" ? t("response.answer.empty") : trimmed;
|
|
169
|
+
}
|
|
170
|
+
if (typeof value === "number") {
|
|
171
|
+
return String(value);
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === "boolean") {
|
|
174
|
+
return value ? t("response.answer.yes") : t("response.answer.no");
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
if (value.length === 0) {
|
|
178
|
+
return t("response.answer.empty");
|
|
179
|
+
}
|
|
180
|
+
return value.map((item) => formatAnswerText(item)).join(", ");
|
|
181
|
+
}
|
|
182
|
+
if (isRecord(value)) {
|
|
183
|
+
const start = value.start;
|
|
184
|
+
const end = value.end;
|
|
185
|
+
if (start !== void 0 || end !== void 0) {
|
|
186
|
+
const startText = formatAnswerText(start);
|
|
187
|
+
const endText = formatAnswerText(end);
|
|
188
|
+
if (startText === t("response.answer.empty") && endText === t("response.answer.empty")) {
|
|
189
|
+
return t("response.answer.empty");
|
|
190
|
+
}
|
|
191
|
+
if (startText === t("response.answer.empty")) {
|
|
192
|
+
return endText;
|
|
193
|
+
}
|
|
194
|
+
if (endText === t("response.answer.empty")) {
|
|
195
|
+
return startText;
|
|
196
|
+
}
|
|
197
|
+
return `${startText} - ${endText}`;
|
|
198
|
+
}
|
|
199
|
+
return JSON.stringify(value);
|
|
200
|
+
}
|
|
201
|
+
return t("response.answer.empty");
|
|
202
|
+
}
|
|
203
|
+
function makeAnswer(value, allowFiles) {
|
|
204
|
+
const files = collectFiles(value, allowFiles);
|
|
205
|
+
if (files.length > 0) {
|
|
206
|
+
return {
|
|
207
|
+
kind: "files",
|
|
208
|
+
value: files
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
kind: "text",
|
|
213
|
+
value: formatAnswerText(value)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function toOptionKey(value) {
|
|
217
|
+
if (value === null) {
|
|
218
|
+
return "null";
|
|
219
|
+
}
|
|
220
|
+
if (typeof value === "string") {
|
|
221
|
+
return `string:${value}`;
|
|
222
|
+
}
|
|
223
|
+
if (typeof value === "number") {
|
|
224
|
+
return `number:${value}`;
|
|
225
|
+
}
|
|
226
|
+
if (typeof value === "boolean") {
|
|
227
|
+
return `boolean:${value ? "true" : "false"}`;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function normalizeOptionLabels(options) {
|
|
232
|
+
if (!Array.isArray(options)) {
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
const labels = {};
|
|
236
|
+
for (const option of options) {
|
|
237
|
+
if (option === null || typeof option === "string" || typeof option === "number" || typeof option === "boolean") {
|
|
238
|
+
const key2 = toOptionKey(option);
|
|
239
|
+
if (key2 !== null) {
|
|
240
|
+
labels[key2] = String(option ?? "");
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!isRecord(option)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const optionValue = option.value;
|
|
248
|
+
const key = toOptionKey(optionValue);
|
|
249
|
+
if (key === null) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const label = typeof option.label === "string" ? option.label : String(optionValue ?? "");
|
|
253
|
+
labels[key] = label;
|
|
254
|
+
}
|
|
255
|
+
return labels;
|
|
256
|
+
}
|
|
257
|
+
function mapOptionValue(optionLabels, value) {
|
|
258
|
+
const optionKey = toOptionKey(value);
|
|
259
|
+
if (optionKey === null) {
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
return optionLabels[optionKey] ?? value;
|
|
263
|
+
}
|
|
264
|
+
function mapFieldAnswerValue(field, value) {
|
|
265
|
+
const optionLabelCount = Object.keys(field.optionLabels).length;
|
|
266
|
+
if (optionLabelCount === 0) {
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
return value.map((entry) => mapOptionValue(field.optionLabels, entry));
|
|
271
|
+
}
|
|
272
|
+
return mapOptionValue(field.optionLabels, value);
|
|
273
|
+
}
|
|
274
|
+
function normalizeField(value, index) {
|
|
275
|
+
if (!isRecord(value)) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const fieldKey = typeof value.field_key === "string" && value.field_key !== "" ? value.field_key : `field_${index + 1}`;
|
|
279
|
+
const name = typeof value.name === "string" && value.name !== "" ? value.name : "";
|
|
280
|
+
const label = typeof value.label === "string" ? value.label : void 0;
|
|
281
|
+
const type = typeof value.type === "string" ? value.type : void 0;
|
|
282
|
+
const optionLabels = normalizeOptionLabels(value.options);
|
|
283
|
+
if (name === "") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
fieldKey,
|
|
288
|
+
name,
|
|
289
|
+
type,
|
|
290
|
+
label,
|
|
291
|
+
optionLabels
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function normalizePages(schemaValue) {
|
|
295
|
+
if (!isRecord(schemaValue)) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
const schemaPages = schemaValue.pages;
|
|
299
|
+
if (Array.isArray(schemaPages) && schemaPages.length > 0) {
|
|
300
|
+
const pages = [];
|
|
301
|
+
for (const [pageIndex, pageValue] of schemaPages.entries()) {
|
|
302
|
+
if (!isRecord(pageValue)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const pageKey = typeof pageValue.page_key === "string" && pageValue.page_key !== "" ? pageValue.page_key : `page_${pageIndex + 1}`;
|
|
306
|
+
const title = typeof pageValue.title === "string" ? pageValue.title : t("response.page.fallback", { index: pageIndex + 1 });
|
|
307
|
+
const description = typeof pageValue.description === "string" && pageValue.description.trim() !== "" ? pageValue.description : void 0;
|
|
308
|
+
const rawFields2 = Array.isArray(pageValue.fields) ? pageValue.fields : [];
|
|
309
|
+
const fields2 = [];
|
|
310
|
+
for (const [fieldIndex, fieldValue] of rawFields2.entries()) {
|
|
311
|
+
const field = normalizeField(fieldValue, fieldIndex);
|
|
312
|
+
if (field !== null) {
|
|
313
|
+
fields2.push(field);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
pages.push({
|
|
317
|
+
pageKey,
|
|
318
|
+
title,
|
|
319
|
+
description,
|
|
320
|
+
fields: fields2
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (pages.length > 0) {
|
|
324
|
+
return pages;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const rawFields = Array.isArray(schemaValue.fields) ? schemaValue.fields : [];
|
|
328
|
+
const fields = [];
|
|
329
|
+
for (const [fieldIndex, fieldValue] of rawFields.entries()) {
|
|
330
|
+
const field = normalizeField(fieldValue, fieldIndex);
|
|
331
|
+
if (field !== null) {
|
|
332
|
+
fields.push(field);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (fields.length === 0) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
return [
|
|
339
|
+
{
|
|
340
|
+
pageKey: "page_1",
|
|
341
|
+
title: t("response.page.fallback", { index: 1 }),
|
|
342
|
+
fields
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
function createPagesFromSchema(schemaValue) {
|
|
347
|
+
const pages = normalizePages(schemaValue);
|
|
348
|
+
return pages.map((page, pageIndex) => ({
|
|
349
|
+
id: page.pageKey,
|
|
350
|
+
title: page.title.trim() !== "" ? page.title : t("response.page.fallback", { index: pageIndex + 1 }),
|
|
351
|
+
description: page.description,
|
|
352
|
+
items: page.fields.map((field, fieldIndex) => ({
|
|
353
|
+
id: field.fieldKey,
|
|
354
|
+
question: typeof field.label === "string" && field.label.trim() !== "" ? field.label : t("response.question.fallback", { index: fieldIndex + 1 }),
|
|
355
|
+
answer: makeAnswer(
|
|
356
|
+
mapFieldAnswerValue(field, payload.value[field.name]),
|
|
357
|
+
field.type === "file"
|
|
358
|
+
)
|
|
359
|
+
}))
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
function createPagesFromPayload() {
|
|
363
|
+
const entries = Object.entries(payload.value);
|
|
364
|
+
if (entries.length === 0) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
return [
|
|
368
|
+
{
|
|
369
|
+
id: "page_1",
|
|
370
|
+
title: t("response.page.fallback", { index: 1 }),
|
|
371
|
+
items: entries.map(([key, value], index) => ({
|
|
372
|
+
id: `payload-${key}`,
|
|
373
|
+
question: key !== "" ? key : t("response.question.fallback", { index: index + 1 }),
|
|
374
|
+
answer: makeAnswer(value, true)
|
|
375
|
+
}))
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
const responsePages = computed(() => {
|
|
380
|
+
const schema = formSchema.form.value;
|
|
381
|
+
if (schema !== null) {
|
|
382
|
+
return createPagesFromSchema(schema);
|
|
383
|
+
}
|
|
384
|
+
return createPagesFromPayload();
|
|
385
|
+
});
|
|
386
|
+
function openImagePreview(file) {
|
|
387
|
+
if (file.url === null) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
previewImage.value = file;
|
|
391
|
+
}
|
|
392
|
+
function closeImagePreview() {
|
|
393
|
+
previewImage.value = null;
|
|
394
|
+
}
|
|
395
|
+
async function downloadFile(file) {
|
|
396
|
+
if (file.url === null || import.meta.server) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const response = await fetch(file.url, {
|
|
401
|
+
credentials: "include"
|
|
402
|
+
});
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
405
|
+
}
|
|
406
|
+
const blob = await response.blob();
|
|
407
|
+
const objectUrl = window.URL.createObjectURL(blob);
|
|
408
|
+
const link = document.createElement("a");
|
|
409
|
+
link.href = objectUrl;
|
|
410
|
+
link.download = file.name;
|
|
411
|
+
link.rel = "noopener";
|
|
412
|
+
document.body.appendChild(link);
|
|
413
|
+
link.click();
|
|
414
|
+
document.body.removeChild(link);
|
|
415
|
+
window.URL.revokeObjectURL(objectUrl);
|
|
416
|
+
} catch {
|
|
417
|
+
const link = document.createElement("a");
|
|
418
|
+
link.href = file.url;
|
|
419
|
+
link.download = file.name;
|
|
420
|
+
link.target = "_blank";
|
|
421
|
+
link.rel = "noopener noreferrer";
|
|
422
|
+
document.body.appendChild(link);
|
|
423
|
+
link.click();
|
|
424
|
+
document.body.removeChild(link);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function loadResponseView() {
|
|
428
|
+
localError.value = null;
|
|
429
|
+
formLoadError.value = null;
|
|
430
|
+
responseResource.value = null;
|
|
431
|
+
previewImage.value = null;
|
|
432
|
+
formSchema.clear();
|
|
433
|
+
if (resolvedResponseUuid.value === "") {
|
|
434
|
+
localError.value = t("response.error.missingResponseUuid");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (resolvedFormKey.value === "") {
|
|
438
|
+
localError.value = t("response.error.missingFormKey");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
responseResource.value = await responses.getResponse(resolvedResponseUuid.value);
|
|
443
|
+
} catch (caughtError) {
|
|
444
|
+
localError.value = caughtError instanceof Error ? caughtError.message : t("response.error.load");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await formSchema.getForm({
|
|
449
|
+
key: resolvedFormKey.value
|
|
450
|
+
});
|
|
451
|
+
} catch (caughtError) {
|
|
452
|
+
formLoadError.value = caughtError instanceof Error ? caughtError.message : t("response.error.loadForm");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
watch(
|
|
456
|
+
() => [resolvedFormKey.value, resolvedResponseUuid.value],
|
|
457
|
+
() => {
|
|
458
|
+
loadResponseView().catch(() => {
|
|
459
|
+
});
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
immediate: true
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
</script>
|
|
466
|
+
|
|
467
|
+
<template>
|
|
468
|
+
<div class="w-full">
|
|
469
|
+
<p
|
|
470
|
+
v-if="loading"
|
|
471
|
+
class="text-sm text-neutral-500"
|
|
472
|
+
>
|
|
473
|
+
{{ t("response.loading") }}
|
|
474
|
+
</p>
|
|
475
|
+
|
|
476
|
+
<p
|
|
477
|
+
v-else-if="error !== null"
|
|
478
|
+
class="text-sm text-red-600"
|
|
479
|
+
>
|
|
480
|
+
{{ error }}
|
|
481
|
+
</p>
|
|
482
|
+
|
|
483
|
+
<p
|
|
484
|
+
v-else-if="responsePages.length === 0"
|
|
485
|
+
class="text-sm text-neutral-500"
|
|
486
|
+
>
|
|
487
|
+
{{ t("response.empty") }}
|
|
488
|
+
</p>
|
|
489
|
+
|
|
490
|
+
<div
|
|
491
|
+
v-else
|
|
492
|
+
class="space-y-12"
|
|
493
|
+
>
|
|
494
|
+
<p
|
|
495
|
+
v-if="formLoadError !== null"
|
|
496
|
+
class="text-sm text-amber-700"
|
|
497
|
+
>
|
|
498
|
+
{{ formLoadError }}
|
|
499
|
+
</p>
|
|
500
|
+
|
|
501
|
+
<section
|
|
502
|
+
v-for="(page, pageIndex) in responsePages"
|
|
503
|
+
:key="page.id"
|
|
504
|
+
class="space-y-4"
|
|
505
|
+
>
|
|
506
|
+
<div class="space-y-1">
|
|
507
|
+
<h3 class="text-base font-semibold text-neutral-900">
|
|
508
|
+
{{ page.title || t("response.page.fallback", { index: pageIndex + 1 }) }}
|
|
509
|
+
</h3>
|
|
510
|
+
<p
|
|
511
|
+
v-if="typeof page.description === 'string' && page.description.trim() !== ''"
|
|
512
|
+
class="text-sm text-neutral-500"
|
|
513
|
+
>
|
|
514
|
+
{{ page.description }}
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div class="space-y-5">
|
|
519
|
+
<article
|
|
520
|
+
v-for="item in page.items"
|
|
521
|
+
:key="item.id"
|
|
522
|
+
class="space-y-2"
|
|
523
|
+
>
|
|
524
|
+
<div
|
|
525
|
+
v-if="isLineLayout"
|
|
526
|
+
class="grid grid-cols-1 gap-2 md:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] md:gap-6"
|
|
527
|
+
>
|
|
528
|
+
<p class="whitespace-pre-wrap text-sm font-medium text-neutral-900">
|
|
529
|
+
{{ item.question }}
|
|
530
|
+
</p>
|
|
531
|
+
|
|
532
|
+
<div
|
|
533
|
+
v-if="item.answer.kind === 'text'"
|
|
534
|
+
class="whitespace-pre-wrap text-sm text-neutral-700"
|
|
535
|
+
>
|
|
536
|
+
{{ item.answer.value }}
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<div
|
|
540
|
+
v-else
|
|
541
|
+
class="space-y-2"
|
|
542
|
+
>
|
|
543
|
+
<div
|
|
544
|
+
v-for="file in item.answer.value"
|
|
545
|
+
:key="file.id"
|
|
546
|
+
class="flex items-center gap-3"
|
|
547
|
+
>
|
|
548
|
+
<button
|
|
549
|
+
v-if="file.isImage && file.url !== null"
|
|
550
|
+
type="button"
|
|
551
|
+
class="inline-flex h-12 w-12 items-center justify-center overflow-hidden rounded border border-neutral-200 bg-white"
|
|
552
|
+
@click="openImagePreview(file)"
|
|
553
|
+
>
|
|
554
|
+
<img
|
|
555
|
+
:src="file.url"
|
|
556
|
+
:alt="file.name"
|
|
557
|
+
class="h-full w-full object-cover"
|
|
558
|
+
>
|
|
559
|
+
</button>
|
|
560
|
+
|
|
561
|
+
<UIcon
|
|
562
|
+
v-else
|
|
563
|
+
name="i-lucide-file"
|
|
564
|
+
class="h-5 w-5 shrink-0 text-neutral-500"
|
|
565
|
+
/>
|
|
566
|
+
|
|
567
|
+
<div class="min-w-0 flex-1">
|
|
568
|
+
<p class="truncate text-sm text-neutral-700">
|
|
569
|
+
{{ file.name }}
|
|
570
|
+
</p>
|
|
571
|
+
<p
|
|
572
|
+
v-if="file.mimeType"
|
|
573
|
+
class="text-xs text-neutral-500"
|
|
574
|
+
>
|
|
575
|
+
{{ file.mimeType }}
|
|
576
|
+
</p>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
<div class="flex items-center gap-1">
|
|
580
|
+
<UButton
|
|
581
|
+
v-if="file.url !== null"
|
|
582
|
+
:href="file.url"
|
|
583
|
+
target="_blank"
|
|
584
|
+
rel="noopener noreferrer"
|
|
585
|
+
variant="ghost"
|
|
586
|
+
color="neutral"
|
|
587
|
+
icon="i-lucide-external-link"
|
|
588
|
+
size="xs"
|
|
589
|
+
>
|
|
590
|
+
{{ t("response.action.preview") }}
|
|
591
|
+
</UButton>
|
|
592
|
+
|
|
593
|
+
<UButton
|
|
594
|
+
v-if="file.url !== null"
|
|
595
|
+
type="button"
|
|
596
|
+
variant="ghost"
|
|
597
|
+
color="neutral"
|
|
598
|
+
icon="i-lucide-download"
|
|
599
|
+
size="xs"
|
|
600
|
+
@click="downloadFile(file)"
|
|
601
|
+
>
|
|
602
|
+
{{ t("response.action.download") }}
|
|
603
|
+
</UButton>
|
|
604
|
+
|
|
605
|
+
<UTooltip
|
|
606
|
+
v-else
|
|
607
|
+
:text="t('response.file.missingUrlHint')"
|
|
608
|
+
>
|
|
609
|
+
<span class="inline-flex h-7 w-7 items-center justify-center text-amber-600">
|
|
610
|
+
<UIcon
|
|
611
|
+
name="i-lucide-triangle-alert"
|
|
612
|
+
class="h-4 w-4"
|
|
613
|
+
/>
|
|
614
|
+
</span>
|
|
615
|
+
</UTooltip>
|
|
616
|
+
</div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<div
|
|
622
|
+
v-else
|
|
623
|
+
class="space-y-1"
|
|
624
|
+
>
|
|
625
|
+
<p class="whitespace-pre-wrap text-sm font-medium text-neutral-900">
|
|
626
|
+
{{ item.question }}
|
|
627
|
+
</p>
|
|
628
|
+
|
|
629
|
+
<p
|
|
630
|
+
v-if="item.answer.kind === 'text'"
|
|
631
|
+
class="whitespace-pre-wrap text-sm text-neutral-700"
|
|
632
|
+
>
|
|
633
|
+
{{ item.answer.value }}
|
|
634
|
+
</p>
|
|
635
|
+
|
|
636
|
+
<div
|
|
637
|
+
v-else
|
|
638
|
+
class="space-y-2"
|
|
639
|
+
>
|
|
640
|
+
<div
|
|
641
|
+
v-for="file in item.answer.value"
|
|
642
|
+
:key="file.id"
|
|
643
|
+
class="flex items-center gap-3"
|
|
644
|
+
>
|
|
645
|
+
<button
|
|
646
|
+
v-if="file.isImage && file.url !== null"
|
|
647
|
+
type="button"
|
|
648
|
+
class="inline-flex h-12 w-12 items-center justify-center overflow-hidden rounded border border-neutral-200 bg-white"
|
|
649
|
+
@click="openImagePreview(file)"
|
|
650
|
+
>
|
|
651
|
+
<img
|
|
652
|
+
:src="file.url"
|
|
653
|
+
:alt="file.name"
|
|
654
|
+
class="h-full w-full object-cover"
|
|
655
|
+
>
|
|
656
|
+
</button>
|
|
657
|
+
|
|
658
|
+
<UIcon
|
|
659
|
+
v-else
|
|
660
|
+
name="i-lucide-file"
|
|
661
|
+
class="h-5 w-5 shrink-0 text-neutral-500"
|
|
662
|
+
/>
|
|
663
|
+
|
|
664
|
+
<div class="min-w-0 flex-1">
|
|
665
|
+
<p class="truncate text-sm text-neutral-700">
|
|
666
|
+
{{ file.name }}
|
|
667
|
+
</p>
|
|
668
|
+
<p
|
|
669
|
+
v-if="file.mimeType"
|
|
670
|
+
class="text-xs text-neutral-500"
|
|
671
|
+
>
|
|
672
|
+
{{ file.mimeType }}
|
|
673
|
+
</p>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<div class="flex items-center gap-1">
|
|
677
|
+
<UButton
|
|
678
|
+
v-if="file.url !== null"
|
|
679
|
+
:href="file.url"
|
|
680
|
+
target="_blank"
|
|
681
|
+
rel="noopener noreferrer"
|
|
682
|
+
variant="ghost"
|
|
683
|
+
color="neutral"
|
|
684
|
+
icon="i-lucide-external-link"
|
|
685
|
+
size="xs"
|
|
686
|
+
>
|
|
687
|
+
{{ t("response.action.preview") }}
|
|
688
|
+
</UButton>
|
|
689
|
+
|
|
690
|
+
<UButton
|
|
691
|
+
v-if="file.url !== null"
|
|
692
|
+
type="button"
|
|
693
|
+
variant="ghost"
|
|
694
|
+
color="neutral"
|
|
695
|
+
icon="i-lucide-download"
|
|
696
|
+
size="xs"
|
|
697
|
+
@click="downloadFile(file)"
|
|
698
|
+
>
|
|
699
|
+
{{ t("response.action.download") }}
|
|
700
|
+
</UButton>
|
|
701
|
+
|
|
702
|
+
<UTooltip
|
|
703
|
+
v-else
|
|
704
|
+
:text="t('response.file.missingUrlHint')"
|
|
705
|
+
>
|
|
706
|
+
<span class="inline-flex h-7 w-7 items-center justify-center text-amber-600">
|
|
707
|
+
<UIcon
|
|
708
|
+
name="i-lucide-triangle-alert"
|
|
709
|
+
class="h-4 w-4"
|
|
710
|
+
/>
|
|
711
|
+
</span>
|
|
712
|
+
</UTooltip>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
</article>
|
|
718
|
+
</div>
|
|
719
|
+
</section>
|
|
720
|
+
</div>
|
|
721
|
+
|
|
722
|
+
<UModal
|
|
723
|
+
:open="isPreviewOpen"
|
|
724
|
+
@update:open="(open) => !open && closeImagePreview()"
|
|
725
|
+
>
|
|
726
|
+
<template #content>
|
|
727
|
+
<div class="space-y-3 p-3">
|
|
728
|
+
<img
|
|
729
|
+
v-if="previewImage?.url"
|
|
730
|
+
:src="previewImage.url"
|
|
731
|
+
:alt="previewImage.name"
|
|
732
|
+
class="max-h-[80vh] w-full rounded object-contain"
|
|
733
|
+
>
|
|
734
|
+
<p
|
|
735
|
+
v-if="previewImage"
|
|
736
|
+
class="truncate text-sm text-neutral-600"
|
|
737
|
+
>
|
|
738
|
+
{{ previewImage.name }}
|
|
739
|
+
</p>
|
|
740
|
+
</div>
|
|
741
|
+
</template>
|
|
742
|
+
</UModal>
|
|
743
|
+
</div>
|
|
744
|
+
</template>
|