@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.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/module.cjs +112 -0
  4. package/dist/module.d.cts +20 -0
  5. package/dist/module.d.mts +20 -0
  6. package/dist/module.d.ts +20 -0
  7. package/dist/module.json +12 -0
  8. package/dist/module.mjs +109 -0
  9. package/dist/runtime/api/categories.d.ts +9 -0
  10. package/dist/runtime/api/categories.js +83 -0
  11. package/dist/runtime/api/client.d.ts +45 -0
  12. package/dist/runtime/api/client.js +148 -0
  13. package/dist/runtime/api/drafts.d.ts +6 -0
  14. package/dist/runtime/api/drafts.js +77 -0
  15. package/dist/runtime/api/http.d.ts +3 -0
  16. package/dist/runtime/api/http.js +138 -0
  17. package/dist/runtime/api/index.d.ts +9 -0
  18. package/dist/runtime/api/index.js +11 -0
  19. package/dist/runtime/api/management.d.ts +19 -0
  20. package/dist/runtime/api/management.js +180 -0
  21. package/dist/runtime/api/request.d.ts +8 -0
  22. package/dist/runtime/api/request.js +52 -0
  23. package/dist/runtime/api/responses.d.ts +6 -0
  24. package/dist/runtime/api/responses.js +61 -0
  25. package/dist/runtime/api/schema.d.ts +7 -0
  26. package/dist/runtime/api/schema.js +56 -0
  27. package/dist/runtime/api/submission.d.ts +11 -0
  28. package/dist/runtime/api/submission.js +47 -0
  29. package/dist/runtime/api/upload.d.ts +8 -0
  30. package/dist/runtime/api/upload.js +37 -0
  31. package/dist/runtime/composables/index.d.ts +31 -0
  32. package/dist/runtime/composables/index.js +16 -0
  33. package/dist/runtime/composables/useFormForgeApi.d.ts +3 -0
  34. package/dist/runtime/composables/useFormForgeApi.js +4 -0
  35. package/dist/runtime/composables/useFormForgeBuilder.d.ts +57 -0
  36. package/dist/runtime/composables/useFormForgeBuilder.js +515 -0
  37. package/dist/runtime/composables/useFormForgeCategory.d.ts +61 -0
  38. package/dist/runtime/composables/useFormForgeCategory.js +248 -0
  39. package/dist/runtime/composables/useFormForgeClient.d.ts +3 -0
  40. package/dist/runtime/composables/useFormForgeClient.js +200 -0
  41. package/dist/runtime/composables/useFormForgeDrafts.d.ts +20 -0
  42. package/dist/runtime/composables/useFormForgeDrafts.js +78 -0
  43. package/dist/runtime/composables/useFormForgeForm.d.ts +26 -0
  44. package/dist/runtime/composables/useFormForgeForm.js +114 -0
  45. package/dist/runtime/composables/useFormForgeGetForm.d.ts +22 -0
  46. package/dist/runtime/composables/useFormForgeGetForm.js +36 -0
  47. package/dist/runtime/composables/useFormForgeI18n.d.ts +250 -0
  48. package/dist/runtime/composables/useFormForgeI18n.js +324 -0
  49. package/dist/runtime/composables/useFormForgeManagement.d.ts +40 -0
  50. package/dist/runtime/composables/useFormForgeManagement.js +153 -0
  51. package/dist/runtime/composables/useFormForgeResolver.d.ts +28 -0
  52. package/dist/runtime/composables/useFormForgeResolver.js +88 -0
  53. package/dist/runtime/composables/useFormForgeResponses.d.ts +45 -0
  54. package/dist/runtime/composables/useFormForgeResponses.js +206 -0
  55. package/dist/runtime/composables/useFormForgeSchema.d.ts +24 -0
  56. package/dist/runtime/composables/useFormForgeSchema.js +69 -0
  57. package/dist/runtime/composables/useFormForgeSubmission.d.ts +12 -0
  58. package/dist/runtime/composables/useFormForgeSubmission.js +4 -0
  59. package/dist/runtime/composables/useFormForgeSubmit.d.ts +29 -0
  60. package/dist/runtime/composables/useFormForgeSubmit.js +291 -0
  61. package/dist/runtime/composables/useFormForgeUploads.d.ts +21 -0
  62. package/dist/runtime/composables/useFormForgeUploads.js +37 -0
  63. package/dist/runtime/composables/useFormForgeWizard.d.ts +20 -0
  64. package/dist/runtime/composables/useFormForgeWizard.js +83 -0
  65. package/dist/runtime/index.d.ts +11 -0
  66. package/dist/runtime/index.js +14 -0
  67. package/dist/runtime/plugin.d.ts +3 -0
  68. package/dist/runtime/plugin.js +175 -0
  69. package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +40 -0
  70. package/dist/runtime/renderers/default/FormForgeBuilder.vue +1159 -0
  71. package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +40 -0
  72. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.d.vue.ts +16 -0
  73. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue +129 -0
  74. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue.d.ts +16 -0
  75. package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +72 -0
  76. package/dist/runtime/renderers/default/FormForgeRenderer.vue +1188 -0
  77. package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +72 -0
  78. package/dist/runtime/renderers/default/FormForgeResponse.d.vue.ts +18 -0
  79. package/dist/runtime/renderers/default/FormForgeResponse.vue +744 -0
  80. package/dist/runtime/renderers/default/FormForgeResponse.vue.d.ts +18 -0
  81. package/dist/runtime/renderers/default/index.d.ts +5 -0
  82. package/dist/runtime/renderers/default/index.js +4 -0
  83. package/dist/runtime/renderers/index.d.ts +2 -0
  84. package/dist/runtime/renderers/index.js +1 -0
  85. package/dist/runtime/types/api.d.ts +129 -0
  86. package/dist/runtime/types/api.js +0 -0
  87. package/dist/runtime/types/category.d.ts +42 -0
  88. package/dist/runtime/types/category.js +0 -0
  89. package/dist/runtime/types/errors.d.ts +16 -0
  90. package/dist/runtime/types/errors.js +0 -0
  91. package/dist/runtime/types/index.d.ts +8 -0
  92. package/dist/runtime/types/index.js +0 -0
  93. package/dist/runtime/types/json.d.ts +6 -0
  94. package/dist/runtime/types/json.js +0 -0
  95. package/dist/runtime/types/management.d.ts +46 -0
  96. package/dist/runtime/types/management.js +0 -0
  97. package/dist/runtime/types/nuxt.d.ts +13 -0
  98. package/dist/runtime/types/nuxt.js +1 -0
  99. package/dist/runtime/types/schema.d.ts +93 -0
  100. package/dist/runtime/types/schema.js +0 -0
  101. package/dist/runtime/utils/category.d.ts +5 -0
  102. package/dist/runtime/utils/category.js +101 -0
  103. package/dist/runtime/utils/form-data.d.ts +8 -0
  104. package/dist/runtime/utils/form-data.js +64 -0
  105. package/dist/runtime/utils/object.d.ts +8 -0
  106. package/dist/runtime/utils/object.js +43 -0
  107. package/dist/runtime/utils/schema.d.ts +3 -0
  108. package/dist/runtime/utils/schema.js +309 -0
  109. package/dist/runtime/utils/submission.d.ts +4 -0
  110. package/dist/runtime/utils/submission.js +45 -0
  111. package/dist/runtime/validation/errors.d.ts +5 -0
  112. package/dist/runtime/validation/errors.js +130 -0
  113. package/dist/runtime/validation/zod.d.ts +6 -0
  114. package/dist/runtime/validation/zod.js +203 -0
  115. package/dist/runtime.cjs +16 -0
  116. package/dist/runtime.d.cts +1 -0
  117. package/dist/runtime.d.mts +1 -0
  118. package/dist/runtime.d.ts +1 -0
  119. package/dist/runtime.mjs +1 -0
  120. package/dist/types.d.mts +3 -0
  121. 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>