@antelopejs/dms-saas 0.3.1 → 0.3.2

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 (98) hide show
  1. package/README.md +0 -86
  2. package/dist/config/runtime.d.ts +0 -1
  3. package/dist/config/runtime.js +0 -7
  4. package/dist/config/runtime.js.map +1 -1
  5. package/dist/data-api/platformOwner/index.d.ts +0 -1
  6. package/dist/data-api/platformOwner/index.js +0 -1
  7. package/dist/data-api/platformOwner/index.js.map +1 -1
  8. package/dist/data-api/tenant/index.d.ts +0 -1
  9. package/dist/data-api/tenant/index.js +0 -1
  10. package/dist/data-api/tenant/index.js.map +1 -1
  11. package/dist/notifications/subjects.d.ts +0 -2
  12. package/dist/notifications/subjects.js +1 -13
  13. package/dist/notifications/subjects.js.map +1 -1
  14. package/dist/pages/platform/categories.d.ts +0 -1
  15. package/dist/pages/platform/categories.js +1 -8
  16. package/dist/pages/platform/categories.js.map +1 -1
  17. package/dist/pages/platform/index.d.ts +0 -1
  18. package/dist/pages/platform/index.js +0 -1
  19. package/dist/pages/platform/index.js.map +1 -1
  20. package/dist/pages/tenant/index.d.ts +0 -1
  21. package/dist/pages/tenant/index.js +0 -1
  22. package/dist/pages/tenant/index.js.map +1 -1
  23. package/dist/routes/platformOwner/index.d.ts +0 -1
  24. package/dist/routes/platformOwner/index.js +0 -1
  25. package/dist/routes/platformOwner/index.js.map +1 -1
  26. package/dist/routes/tenant/index.d.ts +0 -1
  27. package/dist/routes/tenant/index.js +0 -1
  28. package/dist/routes/tenant/index.js.map +1 -1
  29. package/dist/types/config.d.ts +0 -1
  30. package/frontend-vue/i18n/locales/saas-en-GB.json +1 -142
  31. package/frontend-vue/i18n/locales/saas-fr-FR.json +1 -142
  32. package/package.json +2 -4
  33. package/dist/data-api/platformOwner/support.d.ts +0 -58
  34. package/dist/data-api/platformOwner/support.js +0 -111
  35. package/dist/data-api/platformOwner/support.js.map +0 -1
  36. package/dist/data-api/support-fields.d.ts +0 -49
  37. package/dist/data-api/support-fields.js +0 -166
  38. package/dist/data-api/support-fields.js.map +0 -1
  39. package/dist/data-api/support-routes.d.ts +0 -10
  40. package/dist/data-api/support-routes.js +0 -71
  41. package/dist/data-api/support-routes.js.map +0 -1
  42. package/dist/data-api/tenant/support.d.ts +0 -39
  43. package/dist/data-api/tenant/support.js +0 -53
  44. package/dist/data-api/tenant/support.js.map +0 -1
  45. package/dist/pages/platform/support.d.ts +0 -9
  46. package/dist/pages/platform/support.js +0 -29
  47. package/dist/pages/platform/support.js.map +0 -1
  48. package/dist/pages/tenant/support.d.ts +0 -9
  49. package/dist/pages/tenant/support.js +0 -27
  50. package/dist/pages/tenant/support.js.map +0 -1
  51. package/dist/routes/platformOwner/support.d.ts +0 -21
  52. package/dist/routes/platformOwner/support.js +0 -180
  53. package/dist/routes/platformOwner/support.js.map +0 -1
  54. package/dist/routes/tenant/support.d.ts +0 -27
  55. package/dist/routes/tenant/support.js +0 -185
  56. package/dist/routes/tenant/support.js.map +0 -1
  57. package/dist/support/admissions.d.ts +0 -36
  58. package/dist/support/admissions.js +0 -136
  59. package/dist/support/admissions.js.map +0 -1
  60. package/dist/support/attachments.d.ts +0 -12
  61. package/dist/support/attachments.js +0 -56
  62. package/dist/support/attachments.js.map +0 -1
  63. package/dist/support/config.d.ts +0 -19
  64. package/dist/support/config.js +0 -75
  65. package/dist/support/config.js.map +0 -1
  66. package/dist/support/index.d.ts +0 -6
  67. package/dist/support/index.js +0 -23
  68. package/dist/support/index.js.map +0 -1
  69. package/dist/support/operations.d.ts +0 -51
  70. package/dist/support/operations.js +0 -109
  71. package/dist/support/operations.js.map +0 -1
  72. package/dist/support/plans.d.ts +0 -6
  73. package/dist/support/plans.js +0 -130
  74. package/dist/support/plans.js.map +0 -1
  75. package/dist/support/publications.d.ts +0 -8
  76. package/dist/support/publications.js +0 -106
  77. package/dist/support/publications.js.map +0 -1
  78. package/dist/support/recovery.d.ts +0 -7
  79. package/dist/support/recovery.js +0 -43
  80. package/dist/support/recovery.js.map +0 -1
  81. package/dist/support/service.d.ts +0 -51
  82. package/dist/support/service.js +0 -143
  83. package/dist/support/service.js.map +0 -1
  84. package/dist/support/transitions.d.ts +0 -4
  85. package/dist/support/transitions.js +0 -29
  86. package/dist/support/transitions.js.map +0 -1
  87. package/dist/support/uploads.d.ts +0 -39
  88. package/dist/support/uploads.js +0 -115
  89. package/dist/support/uploads.js.map +0 -1
  90. package/dist/support/validation.d.ts +0 -15
  91. package/dist/support/validation.js +0 -54
  92. package/dist/support/validation.js.map +0 -1
  93. package/frontend-vue/app/components/PlatformSupport.vue +0 -461
  94. package/frontend-vue/app/components/SupportAttachmentUpload.vue +0 -140
  95. package/frontend-vue/app/components/SupportThreadHistory.vue +0 -139
  96. package/frontend-vue/app/components/TenantSupport.vue +0 -415
  97. package/frontend-vue/app/composables/useSupportRequest.ts +0 -74
  98. package/frontend-vue/app/types/support.ts +0 -123
@@ -1,461 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, onMounted, ref } from "vue";
3
- import { sendSupportRequest } from "../composables/useSupportRequest";
4
- import SupportThreadHistory from "./SupportThreadHistory.vue";
5
-
6
- const { $authFetch } = useAuthFetch();
7
- const { t } = useI18n();
8
- const toast = useToast();
9
- const { resolveApiError } = useApiErrorMessage();
10
-
11
- const FILTER_ALL = "all";
12
- const ASSIGNEE_UNASSIGNED = "unassigned";
13
-
14
- const inbox = ref<PlatformSupportTicketView[]>([]);
15
- const owners = ref<PlatformOwnerOptionView[]>([]);
16
- const selected = ref<PlatformSupportThreadView | null>(null);
17
- const statusFilter = ref<string>(FILTER_ALL);
18
- const priorityFilter = ref<string>(FILTER_ALL);
19
- const isLoading = ref(true);
20
- const isSubmitting = ref(false);
21
- const replyBody = ref("");
22
- const PAGE_SIZE = 20;
23
- const OWNER_PAGE_SIZE = 100;
24
- const listPage = ref(1);
25
- const messagesPage = ref(1);
26
- const eventsPage = ref(1);
27
- const inboxTotal = ref(0);
28
- let selectionRequest = 0;
29
- let messagesRequest = 0;
30
- let eventsRequest = 0;
31
- let inboxRequest = 0;
32
-
33
- const statusItems = [
34
- { value: FILTER_ALL, label: t("saas.support.filters.all_statuses") },
35
- ...["open", "in_progress", "waiting_customer", "resolved", "closed"].map(
36
- (value) => ({ value, label: t(`saas.support.status.${value}`) }),
37
- ),
38
- ];
39
- const priorityItems = [
40
- { value: FILTER_ALL, label: t("saas.support.filters.all_priorities") },
41
- ...["low", "normal", "high", "urgent"].map((value) => ({
42
- value,
43
- label: t(`saas.support.priority.${value}`),
44
- })),
45
- ];
46
- const assignmentItems = computed(() => [
47
- {
48
- value: ASSIGNEE_UNASSIGNED,
49
- label: t("saas.support.platform.unassigned"),
50
- },
51
- ...owners.value.map((owner) => ({
52
- value: owner._id,
53
- label: owner.name || owner.email || owner._id,
54
- })),
55
- ]);
56
- const assigneeLabels = computed<Record<string, string>>(() =>
57
- Object.fromEntries(
58
- owners.value.map((owner) => [
59
- owner._id,
60
- owner.name || owner.email || owner._id,
61
- ]),
62
- ),
63
- );
64
-
65
- function formatDate(value: string): string {
66
- return new Intl.DateTimeFormat(undefined, {
67
- dateStyle: "medium",
68
- timeStyle: "short",
69
- }).format(new Date(value));
70
- }
71
-
72
- function inboxQuery(): string {
73
- const query = new URLSearchParams({
74
- offset: String((listPage.value - 1) * PAGE_SIZE),
75
- limit: String(PAGE_SIZE),
76
- sortKey: "lastMessageAt",
77
- sortDirection: "desc",
78
- });
79
- if (statusFilter.value !== FILTER_ALL) {
80
- query.set("filter_status", `is:${statusFilter.value}`);
81
- }
82
- if (priorityFilter.value !== FILTER_ALL) {
83
- query.set("filter_priority", `is:${priorityFilter.value}`);
84
- }
85
- return query.toString();
86
- }
87
-
88
- function threadQuery(page: number): string {
89
- return new URLSearchParams({
90
- offset: String((page - 1) * PAGE_SIZE),
91
- limit: String(PAGE_SIZE),
92
- sortKey: "createdAt",
93
- sortDirection: "desc",
94
- }).toString();
95
- }
96
-
97
- async function loadInbox(): Promise<void> {
98
- const request = ++inboxRequest;
99
- isLoading.value = true;
100
- try {
101
- const suffix = inboxQuery();
102
- const page = await $authFetch<SupportPageView<PlatformSupportTicketView>>(
103
- `/api/saas/tables/support-tickets/list?${suffix}`,
104
- );
105
- if (request !== inboxRequest) return;
106
- inbox.value = page.results;
107
- inboxTotal.value = page.total;
108
- } catch (error) {
109
- showError(error, "saas.support.error.load");
110
- } finally {
111
- if (request === inboxRequest) isLoading.value = false;
112
- }
113
- }
114
-
115
- async function loadOwners(): Promise<void> {
116
- try {
117
- const loadedOwners: PlatformOwnerOptionView[] = [];
118
- let offset = 0;
119
- let total = 0;
120
- do {
121
- const query = new URLSearchParams({
122
- offset: String(offset),
123
- limit: String(OWNER_PAGE_SIZE),
124
- });
125
- const page = await $authFetch<SupportPageView<PlatformOwnerOptionView>>(
126
- `/api/saas/tables/support-owners/list?${query}`,
127
- );
128
- loadedOwners.push(...page.results);
129
- if (!page.results.length) break;
130
- offset += page.results.length;
131
- total = page.total;
132
- } while (offset < total);
133
- owners.value = loadedOwners;
134
- } catch (error) {
135
- showError(error, "saas.support.error.load");
136
- }
137
- }
138
-
139
- async function selectTicket(
140
- ticket: PlatformSupportTicketView,
141
- ): Promise<void> {
142
- messagesPage.value = 1;
143
- eventsPage.value = 1;
144
- const request = ++selectionRequest;
145
- const messageRequest = ++messagesRequest;
146
- const eventRequest = ++eventsRequest;
147
- const baseUrl = `/api/saas/support/platform/${ticket._instance}/${ticket._id}`;
148
- try {
149
- const [detail, messages, events] = await Promise.all([
150
- $authFetch<PlatformSupportTicketDetailView>(baseUrl),
151
- $authFetch<SupportPageView<SupportMessageView>>(
152
- `${baseUrl}/messages/list?${threadQuery(messagesPage.value)}`,
153
- ),
154
- $authFetch<SupportPageView<SupportTicketEventView>>(
155
- `${baseUrl}/events/list?${threadQuery(eventsPage.value)}`,
156
- ),
157
- ]);
158
- if (
159
- request === selectionRequest &&
160
- messageRequest === messagesRequest &&
161
- eventRequest === eventsRequest
162
- ) {
163
- selected.value = { ...detail, messages, events };
164
- }
165
- } catch (error) {
166
- showError(error, "saas.support.error.load");
167
- }
168
- }
169
-
170
- async function updateTicket(patch: SupportTicketPatch): Promise<void> {
171
- if (!selected.value) return;
172
- isSubmitting.value = true;
173
- const { tenantId, ticket } = selected.value;
174
- try {
175
- await sendSupportRequest(
176
- `platform:${tenantId}:${ticket._id}:update`,
177
- patch,
178
- (body) =>
179
- $authFetch(`/api/saas/support/platform/${tenantId}/${ticket._id}`, {
180
- method: "PUT",
181
- body,
182
- }),
183
- );
184
- eventsPage.value = 1;
185
- await Promise.all([
186
- loadInbox(),
187
- refreshDetail(),
188
- loadThreadPage("events"),
189
- ]);
190
- } catch (error) {
191
- showError(error, "saas.support.error.update");
192
- } finally {
193
- isSubmitting.value = false;
194
- }
195
- }
196
-
197
- function changeStatus(status: string): void {
198
- void updateTicket({ status: status as SupportStatus });
199
- }
200
-
201
- function changeAssignment(assignedTo: string): void {
202
- void updateTicket({
203
- assignedTo: assignedTo === ASSIGNEE_UNASSIGNED ? null : assignedTo,
204
- });
205
- }
206
-
207
- async function sendReply(): Promise<void> {
208
- if (!selected.value || !replyBody.value.trim()) return;
209
- const { tenantId, ticket } = selected.value;
210
- isSubmitting.value = true;
211
- try {
212
- await sendSupportRequest(
213
- `platform:${tenantId}:${ticket._id}:reply`,
214
- { body: replyBody.value, attachments: [] },
215
- (body) =>
216
- $authFetch(
217
- `/api/saas/support/platform/${tenantId}/${ticket._id}/messages`,
218
- { method: "POST", body },
219
- ),
220
- );
221
- replyBody.value = "";
222
- messagesPage.value = 1;
223
- eventsPage.value = 1;
224
- await Promise.all([
225
- loadInbox(),
226
- refreshDetail(),
227
- loadThreadPage("messages"),
228
- loadThreadPage("events"),
229
- ]);
230
- } catch (error) {
231
- showError(error, "saas.support.error.reply");
232
- } finally {
233
- isSubmitting.value = false;
234
- }
235
- }
236
-
237
- async function openAttachment(resourceKey: string): Promise<void> {
238
- if (!selected.value) return;
239
- const { tenantId, ticket } = selected.value;
240
- const query = new URLSearchParams({ resourceKey });
241
- try {
242
- const file = await $authFetch<SupportAttachmentView>(
243
- `/api/saas/support/platform/${tenantId}/${ticket._id}/attachment?${query}`,
244
- );
245
- window.open(file.url, "_blank", "noopener,noreferrer");
246
- } catch (error) {
247
- showError(error, "saas.support.error.attachment");
248
- }
249
- }
250
-
251
- function showError(error: unknown, fallback: string): void {
252
- toast.add({
253
- title: resolveApiError(error, fallback),
254
- color: "error",
255
- icon: "i-ph-warning-circle",
256
- });
257
- }
258
-
259
- onMounted(() => Promise.all([loadInbox(), loadOwners()]));
260
-
261
- function applyFilters(): void {
262
- listPage.value = 1;
263
- void loadInbox();
264
- }
265
-
266
- function changeListPage(page: number): void {
267
- listPage.value = page;
268
- void loadInbox();
269
- }
270
-
271
- async function refreshDetail(): Promise<void> {
272
- if (!selected.value) return Promise.resolve();
273
- const { tenantId, ticket } = selected.value;
274
- const request = ++selectionRequest;
275
- const detail = await $authFetch<PlatformSupportTicketDetailView>(
276
- `/api/saas/support/platform/${tenantId}/${ticket._id}`,
277
- );
278
- if (
279
- request === selectionRequest &&
280
- selected.value?.ticket._id === ticket._id &&
281
- selected.value?.tenantId === tenantId
282
- ) {
283
- Object.assign(selected.value, detail);
284
- }
285
- }
286
-
287
- function changeThreadPage(kind: "messages" | "events", page: number): void {
288
- const pages = { messages: messagesPage, events: eventsPage };
289
- pages[kind].value = page;
290
- void loadThreadPage(kind);
291
- }
292
-
293
- async function loadThreadPage(kind: "messages" | "events"): Promise<void> {
294
- if (!selected.value) return;
295
- const { tenantId, ticket } = selected.value;
296
- const request = kind === "messages" ? ++messagesRequest : ++eventsRequest;
297
- const pageNumber =
298
- kind === "messages" ? messagesPage.value : eventsPage.value;
299
- try {
300
- const page = await $authFetch<
301
- SupportPageView<SupportMessageView | SupportTicketEventView>
302
- >(
303
- `/api/saas/support/platform/${tenantId}/${ticket._id}/${kind}/list?${threadQuery(pageNumber)}`,
304
- );
305
- const currentRequest =
306
- kind === "messages" ? messagesRequest : eventsRequest;
307
- if (
308
- request !== currentRequest ||
309
- selected.value?.ticket._id !== ticket._id ||
310
- selected.value?.tenantId !== tenantId
311
- )
312
- return;
313
- if (kind === "messages") {
314
- selected.value.messages = page as SupportPageView<SupportMessageView>;
315
- } else {
316
- selected.value.events = page as SupportPageView<SupportTicketEventView>;
317
- }
318
- } catch (error) {
319
- showError(error, "saas.support.error.load");
320
- }
321
- }
322
- </script>
323
-
324
- <template>
325
- <div class="flex flex-col gap-5">
326
- <UCard>
327
- <div class="flex flex-wrap items-end gap-4">
328
- <UFormField
329
- :label="$t('saas.support.filters.status')"
330
- class="min-w-48"
331
- >
332
- <USelect
333
- v-model="statusFilter"
334
- :items="statusItems"
335
- class="w-full"
336
- />
337
- </UFormField>
338
- <UFormField
339
- :label="$t('saas.support.filters.priority')"
340
- class="min-w-48"
341
- >
342
- <USelect
343
- v-model="priorityFilter"
344
- :items="priorityItems"
345
- class="w-full"
346
- />
347
- </UFormField>
348
- <UButton color="neutral" variant="outline" @click="applyFilters">
349
- {{ $t("saas.support.filters.apply") }}
350
- </UButton>
351
- </div>
352
- </UCard>
353
-
354
- <div class="grid gap-6 xl:grid-cols-[24rem_1fr]">
355
- <div class="flex flex-col gap-3">
356
- <USkeleton v-if="isLoading" class="h-40" />
357
- <UCard v-else-if="!inbox.length">
358
- <p class="text-muted text-center text-sm">
359
- {{ $t("saas.support.platform.empty") }}
360
- </p>
361
- </UCard>
362
- <template v-else>
363
- <button
364
- v-for="ticket in inbox"
365
- :key="`${ticket._instance}:${ticket._id}`"
366
- class="border-default hover:bg-elevated rounded-lg border p-4 text-left"
367
- :class="{
368
- 'ring-primary ring-2':
369
- selected?.ticket._id === ticket._id &&
370
- selected?.tenantId === ticket._instance,
371
- }"
372
- @click="selectTicket(ticket)"
373
- >
374
- <div class="flex justify-between gap-2">
375
- <p class="font-medium">{{ ticket.subject }}</p>
376
- <UBadge size="sm" variant="subtle">
377
- {{ $t(`saas.support.priority.${ticket.priority}`) }}
378
- </UBadge>
379
- </div>
380
- <p class="text-muted mt-1 text-sm">{{ ticket.tenantName }}</p>
381
- <div class="text-muted mt-3 flex justify-between text-xs">
382
- <span>{{ $t(`saas.support.status.${ticket.status}`) }}</span>
383
- <span>{{ formatDate(ticket.lastMessageAt) }}</span>
384
- </div>
385
- </button>
386
- <UPagination
387
- v-if="inboxTotal > PAGE_SIZE"
388
- :page="listPage"
389
- :total="inboxTotal"
390
- :items-per-page="PAGE_SIZE"
391
- size="sm"
392
- class="self-center"
393
- @update:page="changeListPage"
394
- />
395
- </template>
396
- </div>
397
-
398
- <UCard v-if="selected">
399
- <template #header>
400
- <div>
401
- <p class="text-muted text-sm">{{ selected.tenantName }}</p>
402
- <h2 class="text-lg font-semibold">
403
- {{ selected.ticket.subject }}
404
- </h2>
405
- </div>
406
- </template>
407
- <div class="flex flex-col gap-5">
408
- <div class="grid gap-4 sm:grid-cols-2">
409
- <UFormField :label="$t('saas.support.filters.status')">
410
- <USelect
411
- :model-value="selected.ticket.status"
412
- :items="statusItems.slice(1)"
413
- class="w-full"
414
- :disabled="isSubmitting"
415
- @update:model-value="changeStatus"
416
- />
417
- </UFormField>
418
- <UFormField :label="$t('saas.support.platform.assignee')">
419
- <USelect
420
- :model-value="
421
- selected.ticket.assignedTo ?? ASSIGNEE_UNASSIGNED
422
- "
423
- :items="assignmentItems"
424
- class="w-full"
425
- :disabled="isSubmitting"
426
- @update:model-value="changeAssignment"
427
- />
428
- </UFormField>
429
- </div>
430
- <USeparator />
431
- <SupportThreadHistory
432
- :messages="selected.messages"
433
- :events="selected.events"
434
- :assignee-labels="assigneeLabels"
435
- @attachment="openAttachment"
436
- @messages-page="changeThreadPage('messages', $event)"
437
- @events-page="changeThreadPage('events', $event)"
438
- />
439
- <UTextarea
440
- v-model="replyBody"
441
- :rows="5"
442
- :placeholder="$t('saas.support.reply_placeholder')"
443
- class="w-full"
444
- />
445
- <div class="flex justify-end">
446
- <UButton :loading="isSubmitting" @click="sendReply">
447
- {{ $t("saas.support.send") }}
448
- </UButton>
449
- </div>
450
- </div>
451
- </UCard>
452
- <UCard v-else class="min-h-64">
453
- <div
454
- class="text-muted flex h-full items-center justify-center text-sm"
455
- >
456
- {{ $t("saas.support.platform.select") }}
457
- </div>
458
- </UCard>
459
- </div>
460
- </div>
461
- </template>
@@ -1,140 +0,0 @@
1
- <script setup lang="ts">
2
- import { computed, ref, watch } from "vue";
3
-
4
- interface AttachmentConstraints {
5
- maxSize?: number;
6
- allowedMimetypes?: string[];
7
- }
8
- interface UploadProps {
9
- modelValue: string[];
10
- multiple?: boolean;
11
- constraints?: AttachmentConstraints;
12
- disabled?: boolean;
13
- }
14
- interface SupportPresign {
15
- resourceKey: string;
16
- uploadUrl: string;
17
- headers: Record<string, string>;
18
- }
19
- const props = defineProps<UploadProps>();
20
- const emit = defineEmits<{
21
- "update:modelValue": [string[]];
22
- busy: [boolean];
23
- }>();
24
- const { $authFetch } = useAuthFetch();
25
- const { uploadWithProgress } = useUploadWithProgress();
26
- const { t } = useI18n();
27
- const selected = ref<File | File[] | null>(null);
28
- const names = ref(new Map<string, string>());
29
- const isUploading = ref(false);
30
- const progress = ref(0);
31
- const currentName = ref("");
32
- const error = ref("");
33
- const accept = computed(() => props.constraints?.allowedMimetypes?.join(","));
34
-
35
- function validate(file: File): boolean {
36
- const { maxSize, allowedMimetypes } = props.constraints ?? {};
37
- if (maxSize !== undefined && file.size > maxSize) {
38
- error.value = t("dms.form.file.rejected_size", {
39
- name: file.name,
40
- size: `${maxSize} B`,
41
- });
42
- return false;
43
- }
44
- if (
45
- allowedMimetypes?.length &&
46
- !allowedMimetypes.some((mime) =>
47
- mime.endsWith("/*")
48
- ? file.type.startsWith(mime.slice(0, -1))
49
- : mime === file.type,
50
- )
51
- ) {
52
- error.value = t("dms.form.file.rejected_type", { name: file.name });
53
- return false;
54
- }
55
- return true;
56
- }
57
-
58
- async function upload(file: File): Promise<void> {
59
- if (!validate(file)) return;
60
- currentName.value = file.name;
61
- progress.value = 0;
62
- const presign = await $authFetch<SupportPresign>(
63
- "/api/saas/support/uploads",
64
- {
65
- method: "POST",
66
- body: { filename: file.name, size: file.size, mimetype: file.type },
67
- },
68
- );
69
- await uploadWithProgress(presign, file, (value: number) => {
70
- progress.value = value;
71
- });
72
- names.value.set(presign.resourceKey, file.name);
73
- emit(
74
- "update:modelValue",
75
- props.multiple
76
- ? [...props.modelValue, presign.resourceKey]
77
- : [presign.resourceKey],
78
- );
79
- }
80
-
81
- watch(selected, async (value) => {
82
- if (!value || props.disabled || isUploading.value) return;
83
- isUploading.value = true;
84
- emit("busy", true);
85
- error.value = "";
86
- try {
87
- const files = Array.isArray(value) ? value : [value];
88
- for (const file of props.multiple ? files : files.slice(0, 1))
89
- await upload(file);
90
- } catch {
91
- error.value = t("saas.support.error.upload");
92
- } finally {
93
- selected.value = null;
94
- isUploading.value = false;
95
- emit("busy", false);
96
- }
97
- });
98
-
99
- function remove(key: string): void {
100
- if (props.disabled || isUploading.value) return;
101
- emit(
102
- "update:modelValue",
103
- props.modelValue.filter((value) => value !== key),
104
- );
105
- names.value.delete(key);
106
- }
107
- </script>
108
-
109
- <template>
110
- <div class="flex flex-col gap-2">
111
- <UFileUpload
112
- v-model="selected"
113
- :multiple="multiple"
114
- :accept="accept"
115
- :disabled="disabled || isUploading"
116
- />
117
- <div v-if="isUploading" role="status" class="bg-elevated rounded-md p-2">
118
- <span class="text-sm">{{ currentName }}</span>
119
- <UProgress :model-value="progress" />
120
- </div>
121
- <p v-if="error" role="alert" class="text-error text-sm">{{ error }}</p>
122
- <div
123
- v-for="key in modelValue"
124
- :key="key"
125
- class="bg-elevated flex items-center justify-between gap-2 rounded-md p-2"
126
- >
127
- <span class="min-w-0 truncate text-sm">
128
- {{ names.get(key) ?? $t("saas.support.attachments") }}
129
- </span>
130
- <UButton
131
- icon="i-lucide-x"
132
- color="neutral"
133
- variant="ghost"
134
- :aria-label="$t('saas.support.remove_attachment')"
135
- :disabled="disabled || isUploading"
136
- @click="remove(key)"
137
- />
138
- </div>
139
- </div>
140
- </template>