1<template>
2 <Dialog v-model:open="open" :title="__('Reply')" size="2xl">
3 <template #default>
4 <div class="flex flex-col gap-4">
5 <p class="text-p-sm">
6 <CompactEditor
7 ref="editorRef"
8 v-model="content"
9 v-model:attachments="attachments"
10 :show-signature="true"
11 :show-attachments="true"
12 :placeholder="__('Write your reply...')"
13 :min-height="'min-h-[200px]'"
14 :max-height="'max-h-[300px]'"
15 :upload-fn="handleFileUpload"
16 @keydown="handleKeydown"
17 />
18 </p>
19 </div>
20 </template>
21 <template #actions>
22 <div class="flex items-center justify-end gap-2">
23 <Button :label="__('Discard')" @click="handleDiscard" />
24 <Button
25 variant="solid"
26 :loading="bulkReplyResource.loading"
27 :disabled="editorRef?.isUploading"
28 @click="handleSubmit"
29 :label="
30 props.selections.size === 1
31 ? __('Send Reply')
32 : __('Send to {0} tickets', String(props.selections.size))
33 "
34 />
35 </div>
36 </template>
37 </Dialog>
38</template>
39
40<script setup lang="ts">
41import CompactEditor from "@/components/CompactEditor.vue";
42import { __ } from "@/translation";
43import { Resource } from "@/types";
44import { uploadFunction } from "@/utils";
45import { useStorage } from "@vueuse/core";
46import { createResource, Dialog, toast, UploadedFile } from "frappe-ui";
47import { ref } from "vue";
48
49const open = defineModel<boolean>();
50
51const props = defineProps<{
52 selections: Set<string>;
53}>();
54const emit = defineEmits<{
55 (e: "success"): void;
56}>();
57
58const content = useStorage<string>("bulk-reply", "");
59const attachments = useStorage<UploadedFile[]>("bulk-attachments", []);
60const editorRef = ref<InstanceType<typeof CompactEditor> | null>(null);
61
62const bulkReplyResource: Resource = createResource({
63 url: "opendesk.api.ticket.bulk_reply",
64 onSuccess() {
65 clearDraft();
66 open.value = false;
67 },
68});
69
70function clearDraft() {
71 content.value = "";
72 editorRef.value?.reset();
73}
74
75async function handleFileUpload(file: File, options?: any) {
76 const uploads = await Promise.all(
77 Array.from(props.selections).map((ticketId) =>
78 uploadFunction(file, "OD Ticket", ticketId, true, options)
79 )
80 );
81
82 const uploaded = uploads[0];
83 if (!uploaded) throw new Error(__("No tickets selected"));
84 return uploaded;
85}
86
87function handleDiscard() {
88 open.value = false;
89 clearDraft();
90}
91
92function handleSubmit() {
93 if (editorRef.value?.isEmpty()) return;
94 bulkReplyResource.submit(
95 {
96 ticket_ids: Array.from(props.selections),
97 message: content.value,
98 attachments: (attachments.value ?? []).map((a) => a.name),
99 },
100 {
101 onSuccess() {
102 const msg =
103 props.selections.size === 1
104 ? __("Bulk reply sent successfully to 1 ticket.")
105 : __(
106 "Bulk reply sent successfully to {0} tickets.",
107 String(props.selections.size)
108 );
109 toast.success(msg);
110 emit("success");
111 },
112 }
113 );
114}
115
116function handleKeydown(e: KeyboardEvent) {
117 if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
118 e.preventDefault();
119 handleSubmit();
120 }
121}
122</script>
123