diff --git a/desk/package.json b/desk/package.json index b70c513f6..e8a3cf378 100644 --- a/desk/package.json +++ b/desk/package.json @@ -12,15 +12,18 @@ "dependencies": { "@headlessui/vue": "^1.7.12", "@tailwindcss/line-clamp": "^0.4.2", + "@vee-validate/zod": "^4.8.2", "@vitejs/plugin-vue": "^4.0.0", "autoprefixer": "^10.4.13", "dayjs": "^1.11.7", "echarts": "^5.4.1", - "frappe-ui": "^0.0.103", + "frappe-ui": "^0.0.105", + "vee-validate": "^4.8.2", "vue": "^3.2.47", "vue-echarts": "^6.5.4", "vue-router": "^4.1.6", - "vuedraggable": "^4.1.0" + "vuedraggable": "^4.1.0", + "zod": "^3.21.4" }, "devDependencies": { "tailwindcss": "^3.2.7", diff --git a/desk/src/App.vue b/desk/src/App.vue index d774fa782..ed23ed7d6 100644 --- a/desk/src/App.vue +++ b/desk/src/App.vue @@ -8,7 +8,7 @@ - - diff --git a/desk/src/components/desk/tickets/NewTicketDialog.vue b/desk/src/components/desk/tickets/NewTicketDialog.vue index 64896de5c..3f576637b 100644 --- a/desk/src/components/desk/tickets/NewTicketDialog.vue +++ b/desk/src/components/desk/tickets/NewTicketDialog.vue @@ -16,7 +16,7 @@ + + + diff --git a/desk/src/components/global/CustomAvatar.vue b/desk/src/components/global/CustomAvatar.vue index 40ffc7c75..4ddec6410 100644 --- a/desk/src/components/global/CustomAvatar.vue +++ b/desk/src/components/global/CustomAvatar.vue @@ -38,12 +38,6 @@ export default { default: "circle", validator(value) { const valid = validShapes.includes(value) - if (!valid) { - console.warn( - `shape property for must be one of `, - validShapes - ) - } return valid }, }, diff --git a/desk/src/components/global/FilterBoxItem.vue b/desk/src/components/global/FilterBoxItem.vue index d7485b70e..2dafc874d 100644 --- a/desk/src/components/global/FilterBoxItem.vue +++ b/desk/src/components/global/FilterBoxItem.vue @@ -178,7 +178,7 @@ export default { switch (filter.data_type) { case "Link": return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", inputMap: (query) => { return { doctype: filter.link_doctype, diff --git a/desk/src/components/global/ListManager.vue b/desk/src/components/global/ListManager.vue index 02e1e0b8d..8563760a7 100644 --- a/desk/src/components/global/ListManager.vue +++ b/desk/src/components/global/ListManager.vue @@ -37,7 +37,6 @@ export default { pageLength: props.options.limit || 20, start: 0, orderBy: props.options.order_by || "", - debug: true, }); const listResource = createListResource( @@ -53,7 +52,6 @@ export default { start: options.value.start, realtime: true, pageLength: 3, - debug: true, }, context ); diff --git a/desk/src/components/global/SaveFiltersDialog.vue b/desk/src/components/global/SaveFiltersDialog.vue index ee34b36cf..5fa69ad15 100644 --- a/desk/src/components/global/SaveFiltersDialog.vue +++ b/desk/src/components/global/SaveFiltersDialog.vue @@ -109,8 +109,8 @@ export default { onSuccess: (res) => { this.$toast({ title: "Filter Saved!", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }); this.close(); @@ -119,8 +119,8 @@ export default { this.$toast({ title: "Error Sending Invites!", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); this.close(); diff --git a/desk/src/components/global/TicketField.vue b/desk/src/components/global/TicketField.vue index cb714d0cc..261088cdb 100644 --- a/desk/src/components/global/TicketField.vue +++ b/desk/src/components/global/TicketField.vue @@ -106,16 +106,16 @@ export default { onSuccess() { this.$toast({ title: "Ticket updated successfully.", - appearance: "success", - customIcon: "circle-check", + icon: "check", + iconClasses: "text-green-500", }) }, onError(err) { this.$toast({ title: "Error while updating ticket", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, }, @@ -150,17 +150,17 @@ export default { url: "frappedesk.api.ticket.assign_ticket_to_agent", onSuccess: () => { this.$toast({ - title: "Agent assigned successfully.", - appearance: "success", - customIcon: "circle-check", + title: "Agent assigned successfully", + icon: "check", + iconClasses: "text-green-500", }) }, onError: (res) => { this.$toast({ title: "Error while assigning agent", text: res, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } @@ -182,8 +182,8 @@ export default { } else { this.$toast({ title: "Please fill all mandatory fields.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) } }, @@ -205,7 +205,7 @@ export default { } } return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", inputMap: (query) => { const filters = [ ...baseFilters, diff --git a/desk/src/components/global/Toast.vue b/desk/src/components/global/Toast.vue index 7f42b09e0..d9ca2448e 100644 --- a/desk/src/components/global/Toast.vue +++ b/desk/src/components/global/Toast.vue @@ -1,162 +1,87 @@ - diff --git a/desk/src/components/global/kb/ArticleMiniList.vue b/desk/src/components/global/kb/ArticleMiniList.vue index de3f134bc..e7e3f841a 100644 --- a/desk/src/components/global/kb/ArticleMiniList.vue +++ b/desk/src/components/global/kb/ArticleMiniList.vue @@ -157,16 +157,16 @@ export default { this.$toast({ title: "Articles updated!!", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }); }, onError: (err) => { this.$toast({ title: "Error while saving", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; diff --git a/desk/src/components/global/kb/CategoryCardList.vue b/desk/src/components/global/kb/CategoryCardList.vue index aa8fe065b..d8abd6da7 100644 --- a/desk/src/components/global/kb/CategoryCardList.vue +++ b/desk/src/components/global/kb/CategoryCardList.vue @@ -142,8 +142,8 @@ export default { this.$toast({ title: "Validation Error", text: "Please fix the errors before saving", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) return } @@ -243,16 +243,16 @@ export default { this.$toast({ title: "Categories updated!!", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }) }, onError: (err) => { this.$toast({ title: "Error while saving", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } diff --git a/desk/src/components/global/kb/SearchSection.vue b/desk/src/components/global/kb/SearchSection.vue index 7f31964ac..0438e283c 100644 --- a/desk/src/components/global/kb/SearchSection.vue +++ b/desk/src/components/global/kb/SearchSection.vue @@ -146,9 +146,6 @@ export default { resources: { searchResults: { url: "frappedesk.api.kb.search", - onSuccess: (res) => { - console.log(res) - }, }, }, } diff --git a/desk/src/components/global/toast.js b/desk/src/components/global/toast.js new file mode 100644 index 000000000..1a2f84134 --- /dev/null +++ b/desk/src/components/global/toast.js @@ -0,0 +1,98 @@ +import { h, reactive, TransitionGroup, ref, Teleport } from "vue"; +import Toast from "./Toast.vue"; + +let toasts = ref([]); + +export let Toasts = { + name: "Toasts", + created() { + if (typeof window === "undefined") return; + if (!document.getElementById("frappeui-toast-root")) { + const root = document.createElement("div"); + root.id = "frappeui-toast-root"; + root.style.position = "fixed"; + root.style.top = "16px"; + root.style.right = "16px"; + root.style.bottom = "16px"; + root.style.left = "16px"; + root.style.zIndex = "9999"; + root.style.pointerEvents = "none"; + document.body.appendChild(root); + } + }, + render() { + return h(Teleport, { to: "#frappeui-toast-root" }, [ + getToastsGroup("top-left"), + getToastsGroup("top-center"), + getToastsGroup("top-right"), + getToastsGroup("bottom-left"), + getToastsGroup("bottom-center"), + getToastsGroup("bottom-right"), + ]); + }, +}; + +function getToastsGroup(position) { + let transition = + "transition duration-[230ms] ease-[cubic-bezier(.21,1.02,.73,1)]"; + let classes = ["absolute"]; + if (position === "top-left") { + classes.push("top-0 left-0"); + } + if (position === "top-right") { + classes.push("top-0 right-0"); + } + if (position === "top-center") { + classes.push("top-0 left-1/2 -translate-x-1/2"); + } + if (position === "bottom-left") { + classes.push("bottom-0 left-0"); + } + if (position === "bottom-right") { + classes.push("bottom-0 right-0"); + } + if (position === "bottom-center") { + classes.push("bottom-0 left-1/2 -translate-x-1/2"); + } + + return h( + TransitionGroup, + { + tag: "div", + class: classes, + moveClass: transition, + enterActiveClass: transition, + enterFromClass: "translate-y-1 opacity-0", + enterToClass: "translate-y-0 opacity-100", + leaveActiveClass: `${transition} absolute`, + leaveFromClass: "translate-y-0 opacity-100", + leaveToClass: "translate-y-1 opacity-0", + }, + () => + toasts.value + .filter((toast) => toast.position === position) + .map((toast) => { + return h( + "div", + { key: toast.key, class: "pointer-events-auto flex" }, + h(Toast, { + ...toast, + onClose: () => { + toasts.value = toasts.value.filter((t) => t !== toast); + }, + }) + ); + }) + ); +} + +export function toast(options) { + let id = `toast-${Math.random().toString(36).slice(2, 9)}`; + let toast = reactive({ + key: id, + position: "top-center", + ...options, + }); + toasts.value.push(toast); + return id; +} diff --git a/desk/src/components/portal/kb/ArticleFeedback.vue b/desk/src/components/portal/kb/ArticleFeedback.vue index ad0f0f5c1..f94da9925 100644 --- a/desk/src/components/portal/kb/ArticleFeedback.vue +++ b/desk/src/components/portal/kb/ArticleFeedback.vue @@ -97,7 +97,7 @@ export default { return } return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "User Article Feedback", filters: { diff --git a/desk/src/main.js b/desk/src/main.js index 6d4de9808..ad91f85d3 100644 --- a/desk/src/main.js +++ b/desk/src/main.js @@ -15,6 +15,7 @@ import App from "./App.vue"; import "./index.css"; import { dayjs } from "@/utils"; import { createToast, clearToasts } from "@/utils/toasts"; +import { clipboardCopy } from '@/utils/clipboard'; import { event } from "@/utils/event"; import { socketio_port } from "../../../../sites/common_site_config.json"; @@ -38,9 +39,10 @@ app.component("Badge", Badge); app.config.unwrapInjectedRef = true; -app.config.globalProperties.$dayjs = dayjs; -app.config.globalProperties.$toast = createToast; app.config.globalProperties.$clearToasts = clearToasts; +app.config.globalProperties.$clipboardCopy = clipboardCopy; +app.config.globalProperties.$dayjs = dayjs; app.config.globalProperties.$event = event; +app.config.globalProperties.$toast = createToast; app.mount("#app"); diff --git a/desk/src/pages/auth/Login.vue b/desk/src/pages/auth/Login.vue index 9d0fa9053..d1f3021cd 100644 --- a/desk/src/pages/auth/Login.vue +++ b/desk/src/pages/auth/Login.vue @@ -133,7 +133,6 @@ export default { await this.resetPassword() } } catch (error) { - console.error(error) this.errorMessage = error.messages.join("\n") } finally { this.state = null diff --git a/desk/src/pages/common/kb/Article.vue b/desk/src/pages/common/kb/Article.vue index 317efbc01..5d17af82c 100644 --- a/desk/src/pages/common/kb/Article.vue +++ b/desk/src/pages/common/kb/Article.vue @@ -248,16 +248,16 @@ export default { onSuccess: () => { this.$toast({ title: "Article updated", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }); }, onError: (err) => { this.$toast({ title: "Error while updating article", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }, @@ -285,8 +285,8 @@ export default { this.$toast({ title: "Error while creating article", text: err, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; diff --git a/desk/src/pages/common/kb/Category.vue b/desk/src/pages/common/kb/Category.vue index 819e3ea6a..2aca80ec1 100644 --- a/desk/src/pages/common/kb/Category.vue +++ b/desk/src/pages/common/kb/Category.vue @@ -24,8 +24,8 @@ $toast({ title: 'Validation Error', text: 'Please fix the errors before saving', - customIcon: 'circle-fail', - appearance: 'danger', + icon: 'x', + iconClasses: 'text-red-500', }) } } diff --git a/desk/src/pages/desk/Desk.vue b/desk/src/pages/desk/Desk.vue index 2755791ce..08a287585 100644 --- a/desk/src/pages/desk/Desk.vue +++ b/desk/src/pages/desk/Desk.vue @@ -209,29 +209,33 @@ export default { } }, showHelpdeskNameSetupToast() { - console.log("showing helpdesk name setup toast"); this.$toast({ - title: "Setup Helpdesk Name", + title: "Set a name", + text: "What would you like to name your helpdesk?", + timeout: 0, + icon: "edit", + iconClasses: "text-blue-500", form: { + classes: "flex gap-1", inputs: [ { type: "text", - fieldname: "helpdesk_name", - placeholder: "eg: FDESK", + fieldname: "helpdeskName", + placeholder: "Frappe Helpdesk", }, ], onSubmit: (values) => { - if (values.helpdesk_name) { - this.$resources.setHelpdeskName.submit({ - name: values.helpdesk_name, - }); - } + const inputs = values.target?.elements; + const name = inputs?.helpdeskName?.value; + + if (!name) return; + + this.$resources.setHelpdeskName.submit({ + name, + }); }, }, - fixed: true, - appearance: "info", - position: "bottom-right", - onClose: () => { + actionOnClose: () => { this.$resources.skipHelpdeskNameSetup.submit(); }, }); @@ -240,36 +244,39 @@ export default { this.$toast({ title: "Default outgoing email account not added", text: "Please add a default outgoing email account in settings.", - appearance: "info", - icon: "info", - iconClasses: "stroke-blue-500 stroke-2", - fixed: true, - position: "bottom-right", - action: { - title: "Setup now", - onClick: () => { - this.$clearToasts(); - this.$router.push({ name: "Emails" }); + timeout: 0, + icon: "mail", + iconClasses: "text-red-500", + buttons: [ + { + title: "Setup now", + appearance: "primary", + iconRight: "arrow-right", + onClick: () => { + this.$router.push({ name: "Emails" }); + }, }, - }, + ], }); }, showAddAgentsToast() { this.$toast({ - title: "Add agents", - text: "Please add an agents from settings.", + title: "Add agent", + text: "Please add an agent from settings", + timeout: 0, appearance: "info", - icon: "info", - iconClasses: "stroke-blue-500 stroke-2", - fixed: true, - position: "bottom-right", - action: { - title: "Add now", - onClick: () => { - this.$clearToasts(); - this.$router.push({ name: "Agents" }); + icon: "users", + iconClasses: "text-red-500", + buttons: [ + { + title: "Add now", + appearance: "danger", + iconRight: "arrow-right", + onClick: () => { + this.$router.push({ name: "Agents" }); + }, }, - }, + ], }); }, }, @@ -287,8 +294,8 @@ export default { this.$toast({ title: "Something went wrong, while adding initial agent", text: "Please try again later.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; @@ -304,8 +311,8 @@ export default { this.$toast({ title: "Something went wrong, while creating a demo ticket", text: "Please try again later.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; @@ -318,16 +325,16 @@ export default { document.title = `Frappe Desk ${res ? ` | ${res}` : ""}`; this.$toast({ title: "Helpdesk name updated!!", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }); }, onError: (err) => { this.$toast({ title: "Something went wrong, updating helpdesk name", text: "Please try again later.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; @@ -350,8 +357,8 @@ export default { this.$toast({ title: "Something went wrong.", text: "Please try again later.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); }, }; @@ -391,8 +398,8 @@ export default { this.$toast({ title: "Error while creating ticket", text: error.messages.join(", "), - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); throw error; @@ -412,7 +419,7 @@ export default { }, types() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Ticket Type", pluck: "name", @@ -428,7 +435,7 @@ export default { }, priorities() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Ticket Priority", }, @@ -455,7 +462,7 @@ export default { }, contacts() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Contact", fields: ["*"], @@ -472,7 +479,7 @@ export default { }, agents() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Agent", fields: ["name", "agent_name", "user_image"], @@ -488,7 +495,7 @@ export default { }, agentGroups() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Agent Group", }, diff --git a/desk/src/pages/desk/Setup.vue b/desk/src/pages/desk/Setup.vue index de5d3e415..28b833393 100644 --- a/desk/src/pages/desk/Setup.vue +++ b/desk/src/pages/desk/Setup.vue @@ -293,8 +293,6 @@ export default { }, mounted() { this.$event.on("email-account-created", () => { - console.log("email account created successfully!!"); - if (this.inputValues.agentEmailList.length > 0) { this.$resources.sentInvites.submit({ emails: this.inputValues.agentEmailList, @@ -306,13 +304,11 @@ export default { }); this.$event.on("email-account-creation-failed", (error) => { - console.log("email account creation failed!!", error); - this.$toast({ title: "Email account creation failed", text: error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); this.submitInProgress = false; @@ -375,8 +371,6 @@ export default { } } } - } else { - console.log("input validation error!!!"); } }, skip() { @@ -498,8 +492,8 @@ export default { onSuccess: () => { this.$toast({ title: "Setup Complete", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }); this.submitInProgress = false; this.$router.push({ name: "DeskTickets" }); @@ -508,8 +502,8 @@ export default { this.$toast({ title: "Setup Failed", text: error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }); this.submitInProgress = false; }, diff --git a/desk/src/pages/desk/Ticket.vue b/desk/src/pages/desk/Ticket.vue index 78f9221a7..99cf92fa4 100644 --- a/desk/src/pages/desk/Ticket.vue +++ b/desk/src/pages/desk/Ticket.vue @@ -1,48 +1,43 @@ + diff --git a/desk/src/pages/desk/Tickets.vue b/desk/src/pages/desk/Tickets.vue index 67ef44bce..9ac2ee058 100644 --- a/desk/src/pages/desk/Tickets.vue +++ b/desk/src/pages/desk/Tickets.vue @@ -281,8 +281,8 @@ export default { this.$toast({ title: `Tickets marked as ${res.status}.`, - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }) this.$event.emit("update_ticket_list") @@ -290,8 +290,8 @@ export default { onError: () => { this.$toast({ title: "Unable to mark tickets as closed.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } @@ -304,9 +304,9 @@ export default { this.$refs.ticketList.manager.reload() this.$toast({ - title: "Tickets assigned to agent.", - customIcon: "circle-check", - appearance: "success", + title: "Tickets assigned to agent", + icon: "check", + iconClasses: "text-green-500", }) this.$event.emit("update_ticket_list") @@ -314,8 +314,8 @@ export default { onError: () => { this.$toast({ title: "Unable to assign tickets to agent.", - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } diff --git a/desk/src/pages/desk/consts.ts b/desk/src/pages/desk/consts.ts new file mode 100644 index 000000000..bda22c9d2 --- /dev/null +++ b/desk/src/pages/desk/consts.ts @@ -0,0 +1,39 @@ +export const TextEditorMenuButtons = [ + "Paragraph", + ["Heading 2", "Heading 3", "Heading 4", "Heading 5", "Heading 6"], + "Separator", + "Bold", + "Italic", + "Separator", + "Bullet List", + "Numbered List", + "Separator", + "Align Left", + "Align Center", + "Align Right", + "Separator", + "Image", + "Video", + "Link", + "Blockquote", + "Code", + "Horizontal Rule", + [ + "InsertTable", + "AddColumnBefore", + "AddColumnAfter", + "DeleteColumn", + "AddRowBefore", + "AddRowAfter", + "DeleteRow", + "MergeCells", + "SplitCell", + "ToggleHeaderColumn", + "ToggleHeaderRow", + "ToggleHeaderCell", + "DeleteTable", + ], + "Separator", + "Undo", + "Redo", +]; diff --git a/desk/src/pages/desk/settings/agent/Agents.vue b/desk/src/pages/desk/settings/agent/Agents.vue index 132e596f1..6b2b07970 100644 --- a/desk/src/pages/desk/settings/agent/Agents.vue +++ b/desk/src/pages/desk/settings/agent/Agents.vue @@ -133,8 +133,8 @@ export default { this.$toast({ title: "Error while deleting agents", text: err, - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-red-500", }) }, } diff --git a/desk/src/pages/desk/settings/canned_response/CannedResponses.vue b/desk/src/pages/desk/settings/canned_response/CannedResponses.vue index d2fbaa483..963ad9d24 100644 --- a/desk/src/pages/desk/settings/canned_response/CannedResponses.vue +++ b/desk/src/pages/desk/settings/canned_response/CannedResponses.vue @@ -127,8 +127,8 @@ export default { this.$toast({ title: "Error while deleting canned responses", text: err, - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-red-500", }); }, }; diff --git a/desk/src/pages/desk/settings/email/EmailAccount.vue b/desk/src/pages/desk/settings/email/EmailAccount.vue index 6720bd0cb..fd341b2c2 100644 --- a/desk/src/pages/desk/settings/email/EmailAccount.vue +++ b/desk/src/pages/desk/settings/email/EmailAccount.vue @@ -508,8 +508,8 @@ export default { this.$toast({ title: "Error getting Email Account.", text: this.errors[error] || error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } @@ -520,8 +520,8 @@ export default { onSuccess: () => { this.$toast({ title: "Email Account Created!!", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }) this.$clearToasts() this.$router.push({ @@ -532,8 +532,8 @@ export default { this.$toast({ title: "Error creating new Email Account.", text: this.errors[error] || error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } @@ -554,8 +554,8 @@ export default { } else { this.$toast({ title: "Email Account Updated.", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }) } }, @@ -563,8 +563,8 @@ export default { this.$toast({ title: "Error updating Email Account.", text: this.errors[error] || error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } @@ -579,8 +579,8 @@ export default { this.$toast({ title: "Error renaming Email Account.", text: this.errors[error] || error, - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } diff --git a/desk/src/pages/desk/settings/sla/SlaPolicy.vue b/desk/src/pages/desk/settings/sla/SlaPolicy.vue index bbf610697..83295e8df 100644 --- a/desk/src/pages/desk/settings/sla/SlaPolicy.vue +++ b/desk/src/pages/desk/settings/sla/SlaPolicy.vue @@ -403,17 +403,11 @@ export default { ) }) }, - onError: (error) => { - console.log(error) - }, } }, updateServicePolicy() { return { url: "frappe.client.set_value", - onError: (error) => { - console.log(error) - }, } }, createNewServicePolicy() { @@ -429,14 +423,11 @@ export default { renameServicePolicy() { return { url: "frappe.client.rename_doc", - onSuccess: (data) => { - console.log(data) - }, } }, getServiceHolidayList() { return { - url: "frappe.client.get_list", + url: "frappedesk.extends.client.get_list", params: { doctype: "Service Holiday List", fields: ["*"], @@ -615,8 +606,8 @@ export default { } this.$toast({ title: "Policy updated", - customIcon: "circle-check", - appearance: "success", + icon: "check", + iconClasses: "text-green-500", }) }) } diff --git a/desk/src/pages/portal/ticketing/NewTicket.vue b/desk/src/pages/portal/ticketing/NewTicket.vue index 22035f540..c8b185804 100644 --- a/desk/src/pages/portal/ticketing/NewTicket.vue +++ b/desk/src/pages/portal/ticketing/NewTicket.vue @@ -412,7 +412,7 @@ export default { break; case "frappe.get_list()": list = ( - await call("frappe.client.get_list", { + await call("frappedesk.extends.client.get_list", { doctype, filters: field.filters, }) diff --git a/desk/src/pages/portal/ticketing/Ticket.vue b/desk/src/pages/portal/ticketing/Ticket.vue index 94773a8f8..6c0ea10eb 100644 --- a/desk/src/pages/portal/ticketing/Ticket.vue +++ b/desk/src/pages/portal/ticketing/Ticket.vue @@ -100,7 +100,7 @@ @click=" () => { $resources.ticket.setValue.submit({ - status: 'Closed', + status: 'Open', }) } " diff --git a/desk/src/pages/portal/ticketing/Ticketing.vue b/desk/src/pages/portal/ticketing/Ticketing.vue index b76f84522..1e914d792 100644 --- a/desk/src/pages/portal/ticketing/Ticketing.vue +++ b/desk/src/pages/portal/ticketing/Ticketing.vue @@ -99,9 +99,6 @@ export default { this.tickets[data[i].name] = data[i] } }, - onError: (error) => { - console.log(`tickets error : ${error}`) - }, } }, ticket() { @@ -110,9 +107,6 @@ export default { onSuccess: (ticket) => { this.tickets[ticket.name] = ticket }, - onError: (error) => { - console.log(`ticket error : ${error}`) - }, } }, templates() { @@ -122,9 +116,6 @@ export default { onSuccess: (data) => { this.ticketTemplates = data }, - onError: (error) => { - console.log(`template error : ${error}`) - }, } }, assignTicketStatus() { @@ -133,9 +124,6 @@ export default { onSuccess: (ticket) => { this.ticketController.update(ticket.name) }, - onError: (error) => { - console.log(`assign status error : ${error}`) - }, } }, createTicket() { @@ -154,8 +142,8 @@ export default { this.$toast({ title: "Error while creating ticket", text: error.messages.join(' '), - customIcon: "circle-fail", - appearance: "danger", + icon: "x", + iconClasses: "text-red-500", }) }, } diff --git a/desk/src/router.js b/desk/src/router.js index f26996b7a..bf917afd0 100644 --- a/desk/src/router.js +++ b/desk/src/router.js @@ -385,7 +385,6 @@ router.beforeEach(async (to, from) => { { article_name: to.params.articleId } ); if (!articleIsPublished) { - console.log(to.params.articleId, " is not available"); return { name: "PortalKBHome" }; } } diff --git a/desk/src/utils/clipboard.ts b/desk/src/utils/clipboard.ts new file mode 100644 index 000000000..ce2297778 --- /dev/null +++ b/desk/src/utils/clipboard.ts @@ -0,0 +1,23 @@ +import { createToast } from './toasts'; + +export const clipboardCopy = (s: string) => { + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText + window + .navigator + .clipboard + .writeText(s) + .then(() => { + createToast({ + title: "Copied to clipboard", + icon: "check", + iconClasses: "text-green-500", + }); + }) + .catch(() => { + createToast({ + title: "Error copying to clipboard", + icon: "x", + iconClasses: "text-red-500", + }); + }); +} diff --git a/desk/src/utils/toasts.js b/desk/src/utils/toasts.js index 1622466e8..db1c9ab18 100644 --- a/desk/src/utils/toasts.js +++ b/desk/src/utils/toasts.js @@ -1,34 +1,15 @@ -import { h, reactive, ref } from "vue"; -import Toast from "@/components/global/Toast.vue"; +import { toast } from "../components/global/toast"; -let toasts = ref([]); - -export let Toasts = { - name: "Toasts", - render() { - return toasts.value.map((toast) => - h(Toast, { - ...toast, - modelValue: toast.show, - "onUpdate:modelValue": (val) => (toast.show = val), - }) - ); - }, -}; export function clearToasts() { const root = document.getElementById("frappeui-toast-root"); if (!root) return; root.innerHTML = ""; } -export function createToast(options) { - let toast = reactive({ - key: "toast-" + toasts.value.length, - show: false, +export function createToast(options = {}) { + toast({ + position: "bottom-right", + icon: options.customIcon, ...options, }); - setTimeout(() => { - toast.show = true; - }, 0); - toasts.value.push(toast); } diff --git a/desk/yarn.lock b/desk/yarn.lock index 914fac387..42b1d5c04 100644 --- a/desk/yarn.lock +++ b/desk/yarn.lock @@ -481,6 +481,15 @@ resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776" integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ== +"@vee-validate/zod@^4.8.2": + version "4.8.2" + resolved "https://registry.yarnpkg.com/@vee-validate/zod/-/zod-4.8.2.tgz#4ca5013b5e3f4dd63ed95029b52b979bd67d59e0" + integrity sha512-PKeahSiKNkNQTjJ6bKoldqqKqMThpxOtzyIo9ryPvqQ/cKbSfqYTkez76ZC+elCNthr7wJPPY59ZSyNBiV6meQ== + dependencies: + type-fest "^3.6.1" + vee-validate "^4.8.2" + zod "^3.21.4" + "@vitejs/plugin-vue@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz#93815beffd23db46288c787352a8ea31a0c03e5e" @@ -528,7 +537,7 @@ "@vue/compiler-dom" "3.2.47" "@vue/shared" "3.2.47" -"@vue/devtools-api@^6.4.5": +"@vue/devtools-api@^6.4.5", "@vue/devtools-api@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== @@ -883,10 +892,10 @@ fraction.js@^4.2.0: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== -frappe-ui@^0.0.103: - version "0.0.103" - resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.0.103.tgz#54be8966b4602f66b0eee36df94e2f0137889e4d" - integrity sha512-TEfAUsmJLzfIT28rFbl2eCA//apgOwMA12DIPOxOpUB8+mh0s6Ze5ADK1pE+/p3hJlPG9QSS8JxRXvH9t/lOKQ== +frappe-ui@^0.0.105: + version "0.0.105" + resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.0.105.tgz#173acd4dbf8e14f1d072c1a19a4dd597af0d7833" + integrity sha512-/CbbpAgEHgOXQ6L69hg1hW99rHsc6Q5VKccaBSCZbczU9iWNXvnm+YZ3LggnwNiFpiJBjNsy1TqMUy448ry+oQ== dependencies: "@headlessui/vue" "^1.5.0" "@popperjs/core" "^2.11.2" @@ -1559,6 +1568,11 @@ type-fest@^2.0.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== +type-fest@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.1.tgz#cf8025edeebfd6cf48de73573a5e1423350b9993" + integrity sha512-htXWckxlT6U4+ilVgweNliPqlsVSSucbxVexRYllyMVJDtf5rTjv6kF/s+qAd4QSL1BZcnJPEJavYBPQiWuZDA== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -1577,6 +1591,13 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +vee-validate@^4.8.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/vee-validate/-/vee-validate-4.8.2.tgz#2861fc2528819f499f4e6527a858ee18ee1dd45c" + integrity sha512-lqGzkUrH9dqrYjGgynW1m9nc3fTia3QVNVObINvHYzZTd73bv6J91yk3VVmTSMTKT9cV73vrYozkuNrN9PzU2A== + dependencies: + "@vue/devtools-api" "^6.5.0" + vite@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.4.tgz#170d93bcff97e0ebc09764c053eebe130bfe6ca0" @@ -1652,6 +1673,11 @@ yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +zod@^3.21.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" + integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + zrender@5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.4.1.tgz#892f864b885c71e1dc25dcb3c7a4ba42678d3f11" diff --git a/frappedesk/api/ticket.py b/frappedesk/api/ticket.py index e558dc016..44ceb9d34 100644 --- a/frappedesk/api/ticket.py +++ b/frappedesk/api/ticket.py @@ -8,7 +8,6 @@ from frappedesk.frappedesk.doctype.ticket.ticket import ( create_communication_via_contact, get_all_conversations, - create_communication_via_agent, ) from frappe.desk.form.assign_to import clear as clear_all_assignments from frappe.utils import datetime @@ -320,12 +319,6 @@ def get_agent_assigned_to_ticket(ticket_id): return agents -@frappe.whitelist() -def mark_ticket_as_seen(ticket_id): - if ticket_id: - return frappe.get_doc("Ticket", ticket_id).add_seen() - - @frappe.whitelist() def assign_ticket_to_agent(ticket_id, agent_id=None): if not ticket_id: @@ -379,6 +372,13 @@ def assign_ticket_status(ticket_id, status): log_ticket_activity(ticket_id, f"status set to {status}") return ticket_doc +@frappe.whitelist() +def update_ticket_status(ticket_id,status): + frappe.db.set_value('Ticket', ticket_id, 'status', status,update_modified=False) + + doc = frappe.get_doc('Ticket',ticket_id) + + return doc @frappe.whitelist() def set_ticket_notes(ticket_id, notes): @@ -453,11 +453,6 @@ def get_conversations(ticket_id): return get_all_conversations(ticket_id) -@frappe.whitelist() -def submit_conversation_via_agent(ticket_id, message,cc,bcc, attachments): - return create_communication_via_agent(ticket_id, message,cc,bcc, attachments) - - @frappe.whitelist() def submit_conversation_via_contact(ticket_id, message, attachments): return create_communication_via_contact(ticket_id, message, attachments) @@ -568,4 +563,4 @@ def get_custom_fields(view="Customer Portal"): @frappe.whitelist() def get_assignee(ticket_id): - return frappe.get_doc("Ticket", ticket_id).get_assigned_agent() \ No newline at end of file + return frappe.get_doc("Ticket", ticket_id).get_assigned_agent() diff --git a/frappedesk/frappedesk/doctype/agent/agent.json b/frappedesk/frappedesk/doctype/agent/agent.json index afda720cd..47523b050 100644 --- a/frappedesk/frappedesk/doctype/agent/agent.json +++ b/frappedesk/frappedesk/doctype/agent/agent.json @@ -1,87 +1,99 @@ { - "actions": [], - "allow_rename": 1, - "creation": "2022-02-24 22:45:45.019915", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "user", - "agent_name", - "user_image", - "is_active", - "groups" - ], - "fields": [ - { - "fieldname": "user", - "fieldtype": "Link", - "label": "User", - "options": "User" - }, - { - "fetch_from": "user.full_name", - "fieldname": "agent_name", - "fieldtype": "Data", - "hidden": 1, - "in_global_search": 1, - "in_standard_filter": 1, - "label": "Agent Name" - }, - { - "default": "1", - "fieldname": "is_active", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Is Active" - }, - { - "fieldname": "groups", - "fieldtype": "Table", - "label": "Groups", - "options": "Agent Group Item" - }, - { - "fetch_from": "user.user_image", - "fieldname": "user_image", - "fieldtype": "Data", - "label": "User Image" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2023-02-13 23:47:22.780889", - "modified_by": "Administrator", - "module": "FrappeDesk", - "name": "Agent", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "email": 1, - "export": 1, - "print": 1, - "report": 1, - "role": "All", - "select": 1, - "share": 1 - } - ], - "search_fields": "agent_name", - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "agent_name", - "track_changes": 1 -} + "actions": [], + "allow_rename": 1, + "creation": "2022-02-24 22:45:45.019915", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "agent_name", + "user_image", + "is_active", + "groups" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fetch_from": "user.full_name", + "fieldname": "agent_name", + "fieldtype": "Data", + "hidden": 1, + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Agent Name" + }, + { + "default": "1", + "fieldname": "is_active", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Active" + }, + { + "fieldname": "groups", + "fieldtype": "Table", + "label": "Groups", + "options": "Agent Group Item" + }, + { + "fetch_from": "user.user_image", + "fieldname": "user_image", + "fieldtype": "Data", + "label": "User Image" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-03-12 12:17:29.677422", + "modified_by": "Administrator", + "module": "FrappeDesk", + "name": "Agent", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "report": 1, + "role": "All", + "select": 1, + "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Agent", + "share": 1, + "write": 1 + } + ], + "search_fields": "agent_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "agent_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappedesk/frappedesk/doctype/agent/agent.py b/frappedesk/frappedesk/doctype/agent/agent.py index 3d4b06cf6..477568e3a 100644 --- a/frappedesk/frappedesk/doctype/agent/agent.py +++ b/frappedesk/frappedesk/doctype/agent/agent.py @@ -7,9 +7,11 @@ class Agent(Document): def before_save(self): - if self.name != self.user: - self.name = self.user - self.set_user_roles() + if self.name == self.user: + return + + self.name = self.user + self.set_user_roles() def set_user_roles(self): user = frappe.get_doc("User", self.user) @@ -28,7 +30,10 @@ def on_update(self): previous = self.get_doc_before_save() if previous: for group in previous.groups: - if not next((g for g in self.groups if g.agent_group == group.agent_group), None): + if not next( + (g for g in self.groups if g.agent_group == group.agent_group), + None, + ): self.remove_from_support_rotations(group.agent_group) self.add_to_support_rotations() @@ -37,12 +42,13 @@ def on_trash(self): self.remove_from_support_rotations() def add_to_support_rotations(self, group=None): - """Add the agent to the support rotation for the given group or all groups the agent belongs to - if agent already added to the support roatation for a group, skip + """ + Add the agent to the support rotation for the given group or all groups + the agent belongs to if agent already added to the support roatation + for a group, skip :param str group: Agent Group name, defaults to None. """ - rule_docs = [] if not group: # Add the agent to the base support rotation @@ -61,24 +67,36 @@ def add_to_support_rotations(self, group=None): agent_group_assignment_rule = frappe.get_doc( "Agent Group", group.agent_group ).get_assignment_rule() - rule_docs.append(frappe.get_doc("Assignment Rule", agent_group_assignment_rule,)) + rule_docs.append( + frappe.get_doc( + "Assignment Rule", + agent_group_assignment_rule, + ) + ) except frappe.DoesNotExistError: frappe.throw( - frappe._("Assignment Rule for Agent Group {0} does not exist").format( - group.agent_group - ) + frappe._( + "Assignment Rule for Agent Group {0} does not exist" + ).format(group.agent_group) ) else: # check if the group is in self.groups - if next((group for group in self.groups if group["group_name"] == group), None): + if next( + (group for group in self.groups if group["group_name"] == group), None + ): rule_docs.append( frappe.get_doc( - "Assignment Rule", frappe.get_doc("Agent Group", group).get_assignment_rule(), + "Assignment Rule", + frappe.get_doc("Agent Group", group).get_assignment_rule(), ) ) else: frappe.throw( - frappe._("Agent {0} does not belong to group {1}".format(self.agent_name, group)) + frappe._( + "Agent {0} does not belong to group {1}".format( + self.agent_name, group + ) + ) ) for rule_doc in rule_docs: @@ -86,13 +104,17 @@ def add_to_support_rotations(self, group=None): if rule_doc: if rule_doc.users and len(rule_doc.users) > 0: for user in rule_doc.users: - if user.user == self.user: # if the user is already in the rule, skip + if ( + user.user == self.user + ): # if the user is already in the rule, skip skip = True break if skip: continue - user_doc = frappe.get_doc({"doctype": "Assignment Rule User", "user": self.user}) + user_doc = frappe.get_doc( + {"doctype": "Assignment Rule User", "user": self.user} + ) rule_doc.append("users", user_doc) rule_doc.disabled = False # enable the rule if it is disabled rule_doc.save(ignore_permissions=True) @@ -104,7 +126,8 @@ def remove_from_support_rotations(self, group=None): # remove the agent from the support rotation for the given group rule_docs.append( frappe.get_doc( - "Assignment Rule", frappe.get_doc("Agent Group", group).get_assignment_rule(), + "Assignment Rule", + frappe.get_doc("Agent Group", group).get_assignment_rule(), ) ) @@ -122,7 +145,9 @@ def remove_from_support_rotations(self, group=None): rule_docs.append( frappe.get_doc( "Assignment Rule", - frappe.get_doc("Agent Group", group.agent_group).get_assignment_rule(), + frappe.get_doc( + "Agent Group", group.agent_group + ).get_assignment_rule(), ) ) @@ -131,12 +156,16 @@ def remove_from_support_rotations(self, group=None): for user in rule_doc.users: if user.user == self.user: if len(rule_doc.users) == 1: - rule_doc.disabled = True # disable the rule if there are no users left + rule_doc.disabled = ( + True # disable the rule if there are no users left + ) rule_doc.remove(user) rule_doc.save() def in_group(self, group): - """Check if the agent is in the given group""" + """ + Check if the agent is in the given group + """ if self.groups: return next((g for g in self.groups if g.agent_group == group), False) return False @@ -159,4 +188,6 @@ def create_agent(first_name, last_name, email, signature, team): user.send_welcome_mail_to_user() - return frappe.get_doc({"doctype": "Agent", "user": user.name, "group": team}).insert() + return frappe.get_doc( + {"doctype": "Agent", "user": user.name, "group": team} + ).insert() diff --git a/frappedesk/frappedesk/doctype/canned_response/canned_response.json b/frappedesk/frappedesk/doctype/canned_response/canned_response.json index 1432c5347..8d3cb7063 100644 --- a/frappedesk/frappedesk/doctype/canned_response/canned_response.json +++ b/frappedesk/frappedesk/doctype/canned_response/canned_response.json @@ -1,48 +1,63 @@ { - "actions": [], - "allow_rename": 1, - "autoname": "field:title", - "creation": "2022-11-03 17:19:47.135176", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": ["title", "message"], - "fields": [ - { - "fieldname": "title", - "fieldtype": "Data", - "label": "Title", - "unique": 1 - }, - { - "fieldname": "message", - "fieldtype": "Text Editor", - "label": "Message" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2022-11-03 17:20:21.807681", - "modified_by": "Administrator", - "module": "FrappeDesk", - "name": "Canned Response", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} + "actions": [], + "allow_rename": 1, + "autoname": "field:title", + "creation": "2022-11-03 17:19:47.135176", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "message" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "unique": 1 + }, + { + "fieldname": "message", + "fieldtype": "Text Editor", + "label": "Message" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-03-12 12:05:42.429658", + "modified_by": "Administrator", + "module": "FrappeDesk", + "name": "Canned Response", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Agent", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappedesk/frappedesk/doctype/fd_customer/fd_customer.json b/frappedesk/frappedesk/doctype/fd_customer/fd_customer.json index 3c5c6c8f9..2e02e8463 100644 --- a/frappedesk/frappedesk/doctype/fd_customer/fd_customer.json +++ b/frappedesk/frappedesk/doctype/fd_customer/fd_customer.json @@ -1,89 +1,101 @@ { - "actions": [], - "allow_rename": 1, - "autoname": "field:customer_name", - "creation": "2022-11-30 00:08:11.831313", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "image", - "customer_name", - "domain", - "contact_html", - "section_break_5", - "contact_count", - "ticket_count" - ], - "fields": [ - { - "fieldname": "image", - "fieldtype": "Attach Image", - "label": "Image" - }, - { - "fieldname": "customer_name", - "fieldtype": "Data", - "label": "Customer Name", - "unique": 1 - }, - { - "fieldname": "domain", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Domain" - }, - { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact" - }, - { - "fieldname": "section_break_5", - "fieldtype": "Section Break" - }, - { - "fieldname": "contact_count", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Contact Count", - "read_only": 1 - }, - { - "fieldname": "ticket_count", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Ticket Count", - "read_only": 1 - } - ], - "image_field": "image", - "index_web_pages_for_search": 1, - "links": [], - "modified": "2022-11-30 09:33:13.848020", - "modified_by": "Administrator", - "module": "FrappeDesk", - "name": "FD Customer", - "naming_rule": "Expression (old style)", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} + "actions": [], + "allow_rename": 1, + "autoname": "field:customer_name", + "creation": "2022-11-30 00:08:11.831313", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "image", + "customer_name", + "domain", + "contact_html", + "section_break_5", + "contact_count", + "ticket_count" + ], + "fields": [ + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "unique": 1 + }, + { + "fieldname": "domain", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Domain" + }, + { + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "contact_count", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Contact Count", + "read_only": 1 + }, + { + "fieldname": "ticket_count", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Ticket Count", + "read_only": 1 + } + ], + "image_field": "image", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-03-12 12:02:34.172337", + "modified_by": "Administrator", + "module": "FrappeDesk", + "name": "FD Customer", + "naming_rule": "Expression (old style)", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Agent", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappedesk/frappedesk/doctype/frappe_desk_comment/frappe_desk_comment.py b/frappedesk/frappedesk/doctype/frappe_desk_comment/frappe_desk_comment.py index f31a70e71..26d13c50b 100644 --- a/frappedesk/frappedesk/doctype/frappe_desk_comment/frappe_desk_comment.py +++ b/frappedesk/frappedesk/doctype/frappe_desk_comment/frappe_desk_comment.py @@ -9,9 +9,8 @@ class FrappeDeskComment(Document): def on_change(self): - print(f"\n\nFrappe Desk Comment created : {self.name}\n\n") mentions = extract_mentions(self.content) - print(f"\n\nMentions : {mentions}\n\n") + for mention in mentions: values = frappe._dict( from_user=self.commented_by, @@ -19,11 +18,18 @@ def on_change(self): ticket=self.reference_ticket, comment=self.name, ) + if frappe.db.exists("Frappe Desk Notification", values): continue + notification = frappe.get_doc(doctype="Frappe Desk Notification") notification.message = ( f"{get_fullname(self.owner)} mentioned you in Ticket #{self.reference_ticket}", ) notification.update(values) notification.insert(ignore_permissions=True) + + def after_insert(self): + frappe.publish_realtime( + "new_frappedesk_comment", {"ticket_id": self.reference_ticket} + ) diff --git a/frappedesk/frappedesk/doctype/frappe_desk_settings/frappe_desk_settings.json b/frappedesk/frappedesk/doctype/frappe_desk_settings/frappe_desk_settings.json index 4528bccc5..5c6090db3 100644 --- a/frappedesk/frappedesk/doctype/frappe_desk_settings/frappe_desk_settings.json +++ b/frappedesk/frappedesk/doctype/frappe_desk_settings/frappe_desk_settings.json @@ -22,6 +22,10 @@ "base_support_rotation", "knowledge_base_section", "suggest_articles_in_new_ticket_page", + "workflow_tab", + "skip_email_workflow", + "instantly_send_email", + "column_break_aomm", "misc_tab", "toasts_column", "suppress_default_email_toast", @@ -179,11 +183,34 @@ { "fieldname": "column_break_zxek", "fieldtype": "Column Break" + }, + { + "fieldname": "workflow_tab", + "fieldtype": "Tab Break", + "label": "Workflow" + }, + { + "default": "0", + "description": "This field is used to skip email-related workflows for tickets. If this field is checked, no emails will be sent related to tickets, such as new ticket creation, status updates, or notifications.", + "fieldname": "skip_email_workflow", + "fieldtype": "Check", + "label": "Skip e-mail workflow" + }, + { + "fieldname": "column_break_aomm", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "This field is used to send an email instantly without adding it into the queue. If this field is checked, the email will be sent immediately after clicking the \"Send\" button, instead of being added to the email queue for later processing.", + "fieldname": "instantly_send_email", + "fieldtype": "Check", + "label": "Instantly send e-mail" } ], "issingle": 1, "links": [], - "modified": "2023-03-06 17:05:10.302469", + "modified": "2023-03-17 12:14:28.728605", "modified_by": "Administrator", "module": "FrappeDesk", "name": "Frappe Desk Settings", diff --git a/frappedesk/frappedesk/doctype/ticket/ticket.json b/frappedesk/frappedesk/doctype/ticket/ticket.json index fd287cc08..26c512d9e 100644 --- a/frappedesk/frappedesk/doctype/ticket/ticket.json +++ b/frappedesk/frappedesk/doctype/ticket/ticket.json @@ -58,7 +58,11 @@ "feedback_submitted", "satisfaction_rating", "customer_feedback", - "feedback_status" + "feedback_status", + "computed_tab", + "dashboard_column", + "dashboard_uri", + "column_break_jwcv" ], "fields": [ { @@ -413,12 +417,32 @@ "fieldtype": "Select", "label": "Feedback Status", "options": "\nPositive\nNeutral\nNegative" + }, + { + "fieldname": "computed_tab", + "fieldtype": "Tab Break", + "label": "Computed" + }, + { + "fieldname": "dashboard_column", + "fieldtype": "Column Break", + "label": "Dashboard" + }, + { + "fieldname": "dashboard_uri", + "fieldtype": "Data", + "is_virtual": 1, + "label": "URI" + }, + { + "fieldname": "column_break_jwcv", + "fieldtype": "Column Break" } ], "icon": "fa fa-issue", "idx": 7, "links": [], - "modified": "2023-03-08 21:14:10.879459", + "modified": "2023-03-14 20:40:47.817434", "modified_by": "Administrator", "module": "FrappeDesk", "name": "Ticket", diff --git a/frappedesk/frappedesk/doctype/ticket/ticket.py b/frappedesk/frappedesk/doctype/ticket/ticket.py index ae4620ad9..0e4e44712 100644 --- a/frappedesk/frappedesk/doctype/ticket/ticket.py +++ b/frappedesk/frappedesk/doctype/ticket/ticket.py @@ -5,6 +5,7 @@ import json from datetime import timedelta +from typing import List import frappe from frappe import _ @@ -21,6 +22,10 @@ from frappedesk.frappedesk.doctype.ticket_activity.ticket_activity import ( log_ticket_activity, ) +from frappedesk.frappedesk.utils.email import ( + default_outgoing_email_account, + default_ticket_outgoing_email_account, +) class Ticket(Document): @@ -294,6 +299,169 @@ def verify_ticket_type(self): if not self.ticket_type: frappe.throw(_("Ticket type is mandatory")) + def skip_email_workflow(self): + skip: str = ( + frappe.get_value("Frappe Desk Settings", None, "skip_email_workflow") or "0" + ) + + return bool(int(skip)) + + def instantly_send_email(self): + check: str = ( + frappe.get_value("Frappe Desk Settings", None, "instantly_send_email") + or "0" + ) + + return bool(int(check)) + + @frappe.whitelist() + def last_communication(self): + filters = {"reference_doctype": "Ticket", "reference_name": ["=", self.name]} + + communication = frappe.get_last_doc( + "Communication", + filters=filters, + ) + + return communication + + def last_communication_email(self): + if not (communication := self.last_communication()): + return + + if not communication.email_account: + return + + email_account = frappe.get_doc("Email Account", communication.email_account) + + if not email_account.enable_outgoing: + return + + return email_account + + def sender_email(self): + """ + Find an email to use as sender. Fall back through multiple choices + + :return: `Email Account` + """ + if email_account := self.last_communication_email(): + return email_account + + if email_account := default_ticket_outgoing_email_account(): + return email_account + + if email_account := default_outgoing_email_account(): + return email_account + + @property + def dashboard_uri(self): + root_uri = frappe.utils.get_url() + return f"{root_uri}/frappedesk/tickets/{self.name}" + + @property + def portal_uri(self): + root_uri = frappe.utils.get_url() + return f"{root_uri}/support/tickets/{self.name}" + + @frappe.whitelist() + def reply_via_agent( + self, message: str, cc: str = None, bcc: str = None, attachments: List[str] = [] + ): + skip_email_workflow = self.skip_email_workflow() + medium = "" if skip_email_workflow else "Email" + subject = f"Re: {self.subject} {self.name}" + sender = frappe.session.user + recipients = self.raised_by + sender_email = None if skip_email_workflow else self.sender_email() + last_communication = self.last_communication() + + if last_communication: + bcc = bcc or last_communication.bcc + cc = cc or last_communication.cc + + if recipients == "Administrator": + admin_email = frappe.get_value("User", "Administrator", "email") + recipients = admin_email + + communication = frappe.get_doc( + { + "bcc": bcc, + "cc": cc, + "communication_medium": medium, + "communication_type": "Communication", + "content": message, + "doctype": "Communication", + "email_account": sender_email.name if sender_email else None, + "email_status": "Open", + "recipients": recipients, + "reference_doctype": "Ticket", + "reference_name": self.name, + "sender": sender, + "sent_or_received": "Sent", + "status": "Linked", + "subject": subject, + } + ) + + communication.insert(ignore_permissions=True) + + _attachments = [] + + for attachment in attachments: + file_doc = frappe.get_doc("File", attachment) + file_doc.attached_to_name = communication.name + file_doc.attached_to_doctype = "Communication" + file_doc.save(ignore_permissions=True) + _attachments.append({"file_url": file_doc.file_url}) + + if skip_email_workflow: + return + + if not sender_email: + frappe.throw(_("Can not send email. No sender email set up!")) + + reply_to_email = sender_email.email_id + template = "new_reply_on_customer_portal_notification" + args = { + "message": message, + "portal_link": self.portal_uri, + "ticket_id": self.name, + } + send_delayed = True + send_now = False + + if self.instantly_send_email(): + send_delayed = False + send_now = True + + try: + frappe.sendmail( + args=args, + attachments=_attachments, + bcc=bcc, + cc=cc, + communication=communication.name, + delayed=send_delayed, + expose_recipients="header", + message=message, + now=send_now, + recipients=recipients, + reference_doctype="Ticket", + reference_name=self.name, + reply_to=reply_to_email, + sender=reply_to_email, + subject=subject, + template=template, + with_container=True, + ) + except Exception as e: + frappe.throw(_(e)) + + @frappe.whitelist() + def mark_seen(self): + self.add_seen() + def set_descritption_from_communication(doc, type): if doc.reference_doctype == "Ticket": @@ -337,139 +505,6 @@ def create_communication_via_contact(ticket, message, attachments=[]): file_doc.save(ignore_permissions=True) -@frappe.whitelist(allow_guest=True) -def create_communication_via_agent(ticket, message, cc, bcc, attachments=None): - ticket_doc = frappe.get_doc("Ticket", ticket) - last_ticket_communication_doc = frappe.get_last_doc( - "Communication", filters={"reference_name": ["=", ticket_doc.name]} - ) - - sent_email = True # if not set email will not be sent - reply_email_account = None - - ticket_email_account = ( - last_ticket_communication_doc.email_account - if last_ticket_communication_doc - else None - ) - - default_ticket_outgoing_email_account = frappe.get_value( - "Email Account", - [ - ["use_imap", "=", 1], - ["IMAP Folder", "append_to", "=", "Ticket"], - ["default_outgoing", "=", 1], - ], - ) - default_outgoing_email_account = frappe.get_value( - "Email Account", [["Email Account", "default_outgoing", "=", 1]] - ) - - just_sent_email_notification = False - - # 1 if not via customer portal check if email account is set in ticket doc, else check if default outgoing is available, else throw error - if not ticket_doc.via_customer_portal: - if ( - ticket_email_account - and frappe.get_doc("Email Account", ticket_email_account).enable_outgoing - ): - reply_email_account = ticket_email_account - elif default_ticket_outgoing_email_account: - reply_email_account = default_ticket_outgoing_email_account - elif default_outgoing_email_account: - reply_email_account = default_outgoing_email_account - just_sent_email_notification = True - else: - return { - "status": "error", - "error_code": "No default outgoing email available", - } - else: - if default_ticket_outgoing_email_account: - # 2 if via customer portal, check if a default outgoing email with IMAP folder with ticket doctype is present, if so use that - reply_email_account = default_ticket_outgoing_email_account - elif default_outgoing_email_account: - # 3 if not check if default outgoing email is present, if then send the mail but it should say reply on the customer portal (as replying in the email will not trigger ticket updatee on desk) - reply_email_account = default_outgoing_email_account - just_sent_email_notification = True - else: - # 4 if via customer portal and no default outgoing email is present, throw error - sent_email = False - - communication = frappe.new_doc("Communication") - communication.update( - { - "communication_type": "Communication", - "communication_medium": "Email", - "sent_or_received": "Sent", - "email_status": "Open", - "subject": "Re: " + ticket_doc.subject + f" (#{ticket_doc.name})", - "sender": frappe.session.user, - "recipients": frappe.get_value("User", "Administrator", "email") - if ticket_doc.raised_by == "Administrator" - else ticket_doc.raised_by, - "cc": cc, - "bcc": bcc, - "content": message, - "status": "Linked", - "reference_doctype": "Ticket", - "reference_name": ticket_doc.name, - "email_account": reply_email_account, - } - ) - communication.ignore_permissions = True - communication.ignore_mandatory = True - communication.save(ignore_permissions=True) - - _attachments = [] - - for attachment in attachments: - file_doc = frappe.get_doc("File", attachment) - file_doc.attached_to_name = communication.name - file_doc.attached_to_doctype = "Communication" - file_doc.save(ignore_permissions=True) - - _attachments.append({"file_url": file_doc.file_url}) - - if sent_email: - reply_to_email = frappe.get_doc("Email Account", reply_email_account).email_id - try: - frappe.sendmail( - subject=f"Re: {ticket_doc.subject}", - sender=reply_to_email, - reply_to=reply_to_email, - recipients=[ticket_doc.raised_by], - cc=cc, - bcc=bcc, - reference_doctype="Ticket", - reference_name=ticket_doc.name, - communication=communication.name, - attachments=_attachments if len(_attachments) > 0 else None, - template="new_reply_on_customer_portal_notification" - if just_sent_email_notification - else None, - message=message if not just_sent_email_notification else None, - args={ - "ticket_id": ticket_doc.name, - "message": message, - "portal_link": f"{frappe.utils.get_url()}/support/tickets/{ticket_doc.name}", - } - if just_sent_email_notification - else None, - now=False, - ) - except: - frappe.throw( - "Either setup up support email account or there should be a default" - " outgoing email account" - ) - else: - return {"status": "error", "error_code": "No default outgoing email available"} - return { - "status": "success", - } - - @frappe.whitelist() def update_ticket_status_via_customer_portal(ticket, new_status): ticket_doc = frappe.get_doc("Ticket", ticket) @@ -635,7 +670,7 @@ def auto_close_tickets(): tickets = frappe.db.sql( """ select name from tabTicket where status='Replied' and - modified dict: + r = q.run(as_dict=True) + + if len(r) != 1: + return + + return r.pop() + + +def default_outgoing_email_account(): + QBEmailAccount = DocType("Email Account") + + r = ( + frappe.qb.from_(QBEmailAccount) + .select(QBEmailAccount.star) + .where(QBEmailAccount.use_imap == 1) + .where(QBEmailAccount.default_outgoing == 1) + .limit(1) + ) + + return query_get_one(r) + + +def default_ticket_outgoing_email_account(): + QBEmailAccount = DocType("Email Account") + QBImapFolder = DocType("IMAP Folder") + + r = ( + frappe.qb.from_(QBEmailAccount) + .select(QBEmailAccount.star) + .where(QBEmailAccount.use_imap == 1) + .where(QBEmailAccount.default_outgoing == 1) + .inner_join(QBImapFolder) + .on(QBImapFolder.parent == QBEmailAccount.name) + .where(QBImapFolder.append_to == "Ticket") + .limit(1) + ) + + return query_get_one(r) diff --git a/frappedesk/hooks.py b/frappedesk/hooks.py index 933b14d86..6273fab73 100644 --- a/frappedesk/hooks.py +++ b/frappedesk/hooks.py @@ -24,6 +24,7 @@ ], "after_insert": [ "frappedesk.frappedesk.doctype.ticket.ticket.set_descritption_from_communication", + "frappedesk.frappedesk.hooks.communication.after_insert", ], }, "Contact": { diff --git a/frappedesk/public/js/utils.js b/frappedesk/public/js/utils.js index c81974ea2..2a3416adf 100644 --- a/frappedesk/public/js/utils.js +++ b/frappedesk/public/js/utils.js @@ -15,7 +15,6 @@ window.FileAttachmentHandler = class FileAttachmentHandler { new frappe.ui.FileUploader({ folder: "Home/Attachments", on_success: (file_doc) => { - console.log(`File ${file_doc.name} uploaded`) if (!this.attachments) this.attachments = [] if (!this.save_paths) this.save_paths = {} this.attachments.push(file_doc) @@ -28,7 +27,6 @@ window.FileAttachmentHandler = class FileAttachmentHandler { } build_attachment_table() { - console.log("Here 10") var wrapper = $('
') wrapper.empty() diff --git a/frappedesk/templates/components/breadcrumbs/breadcrumbs.html b/frappedesk/templates/components/breadcrumbs/breadcrumbs.html index 84a2d3fd4..bfddbf681 100644 --- a/frappedesk/templates/components/breadcrumbs/breadcrumbs.html +++ b/frappedesk/templates/components/breadcrumbs/breadcrumbs.html @@ -23,8 +23,6 @@ let breadcrumbContainer = $(".breadcrumb-container") if (parents) { for (var i = 0; i < parents.length; i++) { - console.log(parents[i]["route"]) - console.log(parents[i]["label"]) breadcrumbContainer.append( i < parents.length - 1 ? `