Skip to content

Commit 8f80661

Browse files
Merge pull request #3103 from frappe/main-hotfix
chore(release): hotfix to main
2 parents 1a8bf43 + 077a1c0 commit 8f80661

12 files changed

Lines changed: 276 additions & 164 deletions

File tree

desk/src/components/EmailContent.vue

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -171,31 +171,6 @@ const htmlContent = computed(
171171
.email-content {
172172
word-break: break-word;
173173
}
174-
.email-content :is(:where(table):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
175-
table-layout: auto;
176-
}
177-
.email-content :where(table):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
178-
width: unset;
179-
table-layout: auto;
180-
text-align: unset;
181-
margin-top: unset;
182-
margin-bottom: unset;
183-
font-size: unset;
184-
line-height: unset;
185-
}
186-
.email-content :where(tbody tr):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
187-
border-bottom-width: 0;
188-
border-bottom-color: transparent;
189-
}
190-
.email-content :is(:where(td):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
191-
position: unset;
192-
border-width: 0;
193-
border-color: transparent;
194-
padding: 0;
195-
}
196-
.email-content :where(tbody td):not(:where([class~='not-prose'], [class~='not-prose'] *)) {
197-
vertical-align: revert;
198-
}
199174
.email-content :is(:where(img):not(:where([class~='not-prose'], [class~='not-prose'] *))) {
200175
border-width: 0;
201176
}

desk/src/components/EmailEditor.vue

Lines changed: 141 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
<TextEditor
33
ref="editorRef"
44
:editor-class="[
5-
'prose-sm max-w-full mx-6 md:mx-10 max-h-[50vh] py-3',
6-
'min-h-[7rem]',
5+
'prose-sm max-w-full mx-6 md:mx-10 py-3',
76
getFontFamily(newEmail),
8-
editable && '!max-h-[35vh] overflow-y-auto',
97
'[&_p.reply-to-content]:hidden',
108
]"
119
:content="newEmail"
@@ -15,6 +13,7 @@
1513
@change="editable ? (newEmail = $event) : null"
1614
:extensions="[ComponentUtils, HandleExcelPaste]"
1715
:uploadFunction="(file:any)=>uploadFunction(file, doctype, ticketId)"
16+
@keydown.capture.stop="handleKeydown"
1817
>
1918
<template #top>
2019
<div class="mx-6 md:mx-10 flex items-center gap-2 border-y py-2.5">
@@ -65,12 +64,19 @@
6564
/>
6665
</div>
6766
</template>
68-
<!-- <template v-slot:editor="{ _editor }">
69-
<EditorContent
70-
:class="[editable && 'max-h-[35vh] overflow-y-auto']"
71-
:editor="_editor"
72-
/>
73-
</template> -->
67+
68+
<template #editor>
69+
<div class="overflow-y-auto min-h-[7rem] max-h-[30vh]">
70+
<EditorContent :editor="editor" />
71+
<div
72+
v-if="quotedContent"
73+
ref="quotedContentRef"
74+
contenteditable="true"
75+
class="prose !max-w-full mx-6 md:mx-10 my-2 border-l-4 border-gray-300 pl-4 text-sm focus:outline-none"
76+
@input="onQuotedInput"
77+
/>
78+
</div>
79+
</template>
7480
<template #bottom>
7581
<!-- Attachments -->
7682
<div class="flex flex-wrap gap-2 px-10">
@@ -165,9 +171,10 @@
165171
<script setup lang="ts">
166172
import {
167173
AttachmentItem,
168-
SavedRepliesSelectorModal,
169174
MultiSelectInput,
175+
SavedRepliesSelectorModal,
170176
} from "@/components";
177+
import { EditorContent } from "@tiptap/vue-3";
171178
import { AttachmentIcon } from "@/components/icons";
172179
import { useTyping } from "@/composables/realtime";
173180
import { useAuthStore } from "@/stores/auth";
@@ -180,7 +187,6 @@ import {
180187
uploadFunction,
181188
validateEmailWithZod,
182189
} from "@/utils";
183-
// import { EditorContent } from "@tiptap/vue-3";
184190
import { useStorage } from "@vueuse/core";
185191
import {
186192
FileUploader,
@@ -190,11 +196,19 @@ import {
190196
toast,
191197
} from "frappe-ui";
192198
import { useOnboarding } from "frappe-ui/frappe";
193-
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
199+
import {
200+
computed,
201+
nextTick,
202+
onBeforeUnmount,
203+
onMounted,
204+
ref,
205+
watch,
206+
} from "vue";
194207
import SavedReplyIcon from "./icons/SavedReplyIcon.vue";
195208
196209
const editorRef = ref(null);
197210
const showSavedRepliesSelectorModal = ref(false);
211+
const quotedContentRef = ref<HTMLElement | null>(null);
198212
199213
const props = defineProps({
200214
ticketId: {
@@ -241,6 +255,12 @@ const newEmail = useStorage<null | string>(
241255
"emailBoxContent" + props.ticketId,
242256
null
243257
);
258+
259+
const quotedContent = useStorage<null | string>(
260+
"quotedEmailBoxContent" + props.ticketId,
261+
null
262+
);
263+
244264
const { updateOnboardingStep } = useOnboarding("helpdesk");
245265
const { isManager } = useAuthStore();
246266
@@ -249,10 +269,11 @@ const { onUserType, cleanup } = useTyping(props.ticketId);
249269
250270
const attachments = ref([]);
251271
const isUploading = ref(false);
252-
253272
const isDisabled = computed(() => {
254273
return (
255-
isContentEmpty(newEmail.value) || sendMail.loading || isUploading.value
274+
(isContentEmpty(newEmail.value) && isContentEmpty(quotedContent.value)) ||
275+
sendMail.loading ||
276+
isUploading.value
256277
);
257278
});
258279
@@ -295,7 +316,11 @@ const sendMail = createResource({
295316
to: toEmailsClone.value.join(","),
296317
cc: ccEmailsClone.value?.join(","),
297318
bcc: bccEmailsClone.value?.join(","),
298-
message: newEmail.value,
319+
message:
320+
newEmail.value +
321+
(quotedContentRef.value
322+
? `<p class="reply-to-content"><p><blockquote>${quotedContentRef.value.innerHTML}</blockquote>`
323+
: ""),
299324
},
300325
}),
301326
onSuccess: () => {
@@ -310,7 +335,7 @@ const sendMail = createResource({
310335
});
311336
312337
function submitMail() {
313-
if (isContentEmpty(newEmail.value)) {
338+
if (isContentEmpty(newEmail.value) && isContentEmpty(quotedContent.value)) {
314339
return false;
315340
}
316341
if (!toEmailsClone.value.length) {
@@ -323,6 +348,21 @@ function submitMail() {
323348
sendMail.submit();
324349
}
325350
351+
watch(quotedContent, (newVal, oldVal) => {
352+
if (!oldVal && newVal) {
353+
nextTick(() => {
354+
if (quotedContentRef.value) {
355+
quotedContentRef.value.innerHTML = newVal;
356+
}
357+
});
358+
}
359+
});
360+
function onQuotedInput() {
361+
const el = quotedContentRef.value;
362+
if (!el) return;
363+
quotedContent.value = el.innerHTML || null;
364+
}
365+
326366
function toggleCC() {
327367
showCC.value = !showCC.value;
328368
@@ -354,15 +394,16 @@ function addToReply(
354394
toEmailsClone.value = toEmails;
355395
ccEmailsClone.value = ccEmails;
356396
bccEmailsClone.value = bccEmails;
357-
const repliedMessage = `<p class="reply-to-content"><p><blockquote>${body}</blockquote>`;
358-
editorRef.value.editor
359-
.chain()
360-
.clearContent()
361-
.insertContent(repliedMessage)
362-
.focus("all")
363-
.insertContentAt(0, { type: "paragraph" })
364-
.focus("start")
365-
.run();
397+
398+
if (body !== quotedContent.value) {
399+
//trigger change for watch when replied to body data is different from current quoted content
400+
quotedContent.value = null;
401+
nextTick(() => {
402+
quotedContent.value = body;
403+
});
404+
}
405+
406+
editorRef.value.editor.chain().clearContent().focus("start").run();
366407
nextTick(() => {
367408
newEmail.value = editorRef.value.editor.getHTML();
368409
});
@@ -371,21 +412,94 @@ function addToReply(
371412
function resetState() {
372413
newEmail.value = null;
373414
attachments.value = [];
415+
quotedContent.value = null;
374416
}
375417
376418
function handleDiscard() {
377419
attachments.value = [];
378420
newEmail.value = null;
379-
421+
quotedContent.value = null;
380422
ccEmailsClone.value = [];
381423
bccEmailsClone.value = [];
382-
ccEmailsClone.value = [];
383424
showCC.value = false;
384425
showBCC.value = false;
385426
386427
emit("discard");
387428
}
388429
430+
//on load set quoted content from storage
431+
onMounted(() => {
432+
if (quotedContent.value) {
433+
nextTick(() => {
434+
if (quotedContentRef.value) {
435+
quotedContentRef.value.innerHTML = quotedContent.value;
436+
}
437+
});
438+
}
439+
});
440+
441+
function handleSelectAll(e: KeyboardEvent) {
442+
const active = document.activeElement;
443+
const editorDom = editorRef.value?.editor?.view?.dom as
444+
| HTMLElement
445+
| undefined;
446+
const quotedEl = quotedContentRef.value;
447+
const sel = window.getSelection();
448+
if (!sel || !editorDom) return;
449+
if (!editorDom.contains(active) && !(quotedEl && quotedEl.contains(active))) {
450+
return;
451+
}
452+
e.preventDefault();
453+
sel.removeAllRanges();
454+
const range = document.createRange();
455+
456+
if (quotedEl) {
457+
range.setStartBefore(editorDom);
458+
range.setEndAfter(quotedEl);
459+
} else {
460+
range.selectNodeContents(editorDom);
461+
}
462+
sel.addRange(range);
463+
}
464+
465+
function handleDelete(e: KeyboardEvent) {
466+
const sel = window.getSelection();
467+
const quotedEl = quotedContentRef.value;
468+
const editorDom = editorRef.value?.editor?.view?.dom as
469+
| HTMLElement
470+
| undefined;
471+
472+
if (!sel || sel.isCollapsed || !quotedEl || !editorDom) return;
473+
474+
const isSelectingEntireEditor = sel.containsNode(editorDom, true);
475+
476+
const isSelectingEntireQuote = sel.containsNode(quotedEl, true);
477+
478+
if (isSelectingEntireEditor && isSelectingEntireQuote) {
479+
e.preventDefault();
480+
481+
editorRef.value?.editor?.commands?.clearContent();
482+
newEmail.value = null;
483+
quotedContent.value = null;
484+
485+
sel.removeAllRanges();
486+
}
487+
}
488+
489+
function handleKeydown(e: KeyboardEvent) {
490+
const key = e.key.toLowerCase();
491+
492+
if ((e.metaKey || e.ctrlKey) && key === "a") {
493+
handleSelectAll(e);
494+
return;
495+
}
496+
497+
if (key === "backspace" || key === "delete") {
498+
handleDelete(e);
499+
return;
500+
}
501+
}
502+
389503
const editor = computed(() => {
390504
return editorRef.value.editor;
391505
});

desk/src/components/SavedRepliesSelectorModal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ const selectedTemplate = ref({
201201
});
202202
203203
const scope = computed(() => {
204-
return filters.value.find((f) => f.label === activeFilter.value)?.value;
204+
return filters.value.find((f) => f.value === activeFilter.value)?.value;
205205
});
206206
207207
const savedReplyListResource = createListResource({

desk/src/index.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
@layer components {
88
.prose-f {
9-
@apply break-normal max-w-none prose prose-code:break-all prose-code:whitespace-pre-wrap prose-img:border prose-img:rounded-lg prose-sm prose-table:table-fixed prose-td:border prose-td:border-gray-300 prose-td:p-2 prose-td:relative prose-th:bg-gray-100 prose-th:border prose-th:border-gray-300 prose-th:p-2 prose-th:relative;
9+
@apply break-normal max-w-none prose prose-code:break-all prose-code:whitespace-pre-wrap prose-img:border prose-img:rounded-lg prose-sm;
1010
}
1111
}
1212
.tiptap input[placeholder="Add caption"] {
@@ -16,4 +16,4 @@
1616
html,
1717
body {
1818
color: var(--ink-gray-9);
19-
}
19+
}

desk/src/pages/ticket/TicketCommunication.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ function sanitize(html: string) {
6161
a: ["href"],
6262
video: ["src", "controls"],
6363
img: ["src"],
64+
table: ["border", "cellpadding", "cellspacing", "width", "data-type"],
65+
td: ["colspan", "rowspan", "width", "align", "valign"],
66+
th: ["colspan", "rowspan", "width", "align", "valign"],
6467
},
6568
});
6669
}

helpdesk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.21.1"
1+
__version__ = "1.21.2"

helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,10 +1167,8 @@ def parse_content(self, content):
11671167
tag["embed"] = tag.get("src")
11681168
tag["width"] = "80%"
11691169
tag["height"] = "80%"
1170-
del tag["src"]
11711170
elif tag.name == "video":
11721171
tag["embed"] = tag.get("src")
1173-
del tag["src"]
11741172

11751173
return str(soup)
11761174

0 commit comments

Comments
 (0)