-
-
-
editor.attachments.push(file)">
-
-
-
-
-
+ editor.attachments.push(item)"
+ @attachment-removed="(item) => removeAttachment(item)"
+ >
+
+
+
+
-
+
-
-
diff --git a/desk/src/pages/desk/ticket/editor/TopSection.vue b/desk/src/pages/desk/ticket/editor/TopSection.vue
index add630d04..16b7bad15 100644
--- a/desk/src/pages/desk/ticket/editor/TopSection.vue
+++ b/desk/src/pages/desk/ticket/editor/TopSection.vue
@@ -71,9 +71,10 @@
diff --git a/desk/src/pages/onboarding/SetupFavicon.vue b/desk/src/pages/onboarding/SetupFavicon.vue
new file mode 100644
index 000000000..c3b930df8
--- /dev/null
+++ b/desk/src/pages/onboarding/SetupFavicon.vue
@@ -0,0 +1,53 @@
+
+
+
+ {{ help }}
+
+
![]()
+
update(file)">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/SetupLogo.vue b/desk/src/pages/onboarding/SetupLogo.vue
new file mode 100644
index 000000000..c264c7db7
--- /dev/null
+++ b/desk/src/pages/onboarding/SetupLogo.vue
@@ -0,0 +1,52 @@
+
+
+
+ {{ help }}
+
+
![]()
+
update(file)">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/SetupName.vue b/desk/src/pages/onboarding/SetupName.vue
new file mode 100644
index 000000000..5dd02c1d6
--- /dev/null
+++ b/desk/src/pages/onboarding/SetupName.vue
@@ -0,0 +1,57 @@
+
+
+ {{ text }}
+
+
+
+
+
+ {{ subText }}
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/SetupSkipEmail.vue b/desk/src/pages/onboarding/SetupSkipEmail.vue
new file mode 100644
index 000000000..1215fc5e1
--- /dev/null
+++ b/desk/src/pages/onboarding/SetupSkipEmail.vue
@@ -0,0 +1,45 @@
+
+
+ {{ query }}
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/SimpleOnboarding.vue b/desk/src/pages/onboarding/SimpleOnboarding.vue
new file mode 100644
index 000000000..e8a940883
--- /dev/null
+++ b/desk/src/pages/onboarding/SimpleOnboarding.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+ {{ steps[step]["title"] }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/SuccessMessage.vue b/desk/src/pages/onboarding/SuccessMessage.vue
new file mode 100644
index 000000000..cd6b39544
--- /dev/null
+++ b/desk/src/pages/onboarding/SuccessMessage.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+ If you find any bugs, report them at the issue tracker
+
+
+ {{ issues }}
+
+
+
+
+ For any queries or support, reach out to our support portal
+
+
+ {{ support }}
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/EmailCredentials.vue b/desk/src/pages/onboarding/email/EmailCredentials.vue
new file mode 100644
index 000000000..47b410d16
--- /dev/null
+++ b/desk/src/pages/onboarding/email/EmailCredentials.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/EmailIntro.vue b/desk/src/pages/onboarding/email/EmailIntro.vue
new file mode 100644
index 000000000..b649c8eb5
--- /dev/null
+++ b/desk/src/pages/onboarding/email/EmailIntro.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/SelectService.vue b/desk/src/pages/onboarding/email/SelectService.vue
new file mode 100644
index 000000000..9c409544f
--- /dev/null
+++ b/desk/src/pages/onboarding/email/SelectService.vue
@@ -0,0 +1,92 @@
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/SetupEmail.vue b/desk/src/pages/onboarding/email/SetupEmail.vue
new file mode 100644
index 000000000..8af36829a
--- /dev/null
+++ b/desk/src/pages/onboarding/email/SetupEmail.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/SuccessMessage.vue b/desk/src/pages/onboarding/email/SuccessMessage.vue
new file mode 100644
index 000000000..aff318395
--- /dev/null
+++ b/desk/src/pages/onboarding/email/SuccessMessage.vue
@@ -0,0 +1,22 @@
+
+
+
+ {{ emoji }}
+
+
+ {{ message }}
+
+
+
+
+
diff --git a/desk/src/pages/onboarding/email/data.ts b/desk/src/pages/onboarding/email/data.ts
new file mode 100644
index 000000000..fbfdda461
--- /dev/null
+++ b/desk/src/pages/onboarding/email/data.ts
@@ -0,0 +1,17 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+
+export const useOnboardingEmailStore = defineStore("onboarding", () => {
+ const step = ref(0);
+ const service = ref("");
+
+ function next() {
+ step.value++;
+ }
+
+ return {
+ next,
+ service,
+ step,
+ };
+});
diff --git a/desk/src/pages/portal/CustomerRoot.vue b/desk/src/pages/portal/CustomerRoot.vue
new file mode 100644
index 000000000..b7e2e20b5
--- /dev/null
+++ b/desk/src/pages/portal/CustomerRoot.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+ {{ configStore.helpdeskName }}
+
+
+
+
+
+
+ {{ authStore.userName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/portal/Portal.vue b/desk/src/pages/portal/Portal.vue
deleted file mode 100644
index 10ec99760..000000000
--- a/desk/src/pages/portal/Portal.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
diff --git a/desk/src/pages/portal/TicketList.vue b/desk/src/pages/portal/TicketList.vue
new file mode 100644
index 000000000..28ba69f78
--- /dev/null
+++ b/desk/src/pages/portal/TicketList.vue
@@ -0,0 +1,178 @@
+
+
+
+
+ handleRowClick(ticketId)"
+ >
+
+
+
+ {{ data.subject }}
+
+
+
+ {{ data.name }}
+
+
+
+
+ {{ transformStatus(data.status) }}
+
+
+ {{ dayjs(data.creation).fromNow() }}
+
+
+
+
+
+ 📭 No tickets
+
+
+
+
+
diff --git a/desk/src/pages/portal/TicketNew.vue b/desk/src/pages/portal/TicketNew.vue
new file mode 100644
index 000000000..09447fcbb
--- /dev/null
+++ b/desk/src/pages/portal/TicketNew.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
{{ field.label }}
+
+ (customFields[field.fieldname] = v.value)"
+ />
+
+
+
(customFields[field.fieldname] = v.value)"
+ />
+
+
+
+
+
+ 📚 Did you know? These articles might cover what you looking for!
+
+
+ {{ article.title }}
+ →
+
+
+
searchArticles(v)"
+ />
+
(description = v)"
+ @attachment-added="(item) => attachments.push(item)"
+ @attachment-removed="(item) => removeAttachment(item)"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/portal/TicketSingle.vue b/desk/src/pages/portal/TicketSingle.vue
new file mode 100644
index 000000000..21c31c1f3
--- /dev/null
+++ b/desk/src/pages/portal/TicketSingle.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+ {{ ticket.doc?.subject }}
+
+
+
+ {{ ticket.doc?.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dayShort(communication.creation) }}
+
+
+
+
+
+
+
+
(editorContent = v)"
+ @attachment-added="(item) => attachments.add(item)"
+ @attachment-removed="(item) => attachments.delete(item)"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/desk/src/pages/portal/ticketing/Impersonate.vue b/desk/src/pages/portal/ticketing/Impersonate.vue
deleted file mode 100644
index 78bd876f8..000000000
--- a/desk/src/pages/portal/ticketing/Impersonate.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
diff --git a/desk/src/pages/portal/ticketing/NewTicket.vue b/desk/src/pages/portal/ticketing/NewTicket.vue
deleted file mode 100644
index f26557364..000000000
--- a/desk/src/pages/portal/ticketing/NewTicket.vue
+++ /dev/null
@@ -1,444 +0,0 @@
-
-
-
-
-
- {{
- `New Ticket ${
- template.template_name != "Default"
- ? `(${template.template_name})`
- : ""
- }`
- }}
-
-
-
-
-
-
- {
- validateField(field, data);
- }
- "
- />
-
-
- {
- validateField(field, data);
- }
- "
- />
-
-
-
- {{ field.label }}
-
-
{
- validateField(field, val);
- }
- "
- >
-
-
-
-
-
-
-
- {{ field.label }}
-
-
{
- validateField(field, data);
- }
- "
- >
-
-
-
-
-
-
-
-
-
- {{ field.label }}
-
-
{
- validateField(field, data);
- }
- "
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
attachments.push(file)">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/portal/ticketing/Ticket.vue b/desk/src/pages/portal/ticketing/Ticket.vue
deleted file mode 100644
index ebd19ae3d..000000000
--- a/desk/src/pages/portal/ticketing/Ticket.vue
+++ /dev/null
@@ -1,576 +0,0 @@
-
-
-
-
-
-
Home
-
-
Tickets
-
-
{{ ticket.name }}
-
-
-
-
-
-
-
- {{ ticket.subject }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- This ticket has been closed.
-
-
-
{
- $resources.ticket.setValue.submit({
- status: 'Open',
- })
- }
- "
- >
- Reopen ticket
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ authStore.full_name }}
-
-
-
{
- content = val
- }
- "
- @click="
- $refs.replyEditor.editor.commands.focus()
- "
- placeholder="Type a response"
- class="border border-gray-300 rounded-[8px] p-[12px]"
- >
-
-
-
-
-
-
-
-
-
- {
- showTextFormattingMenu =
- !showTextFormattingMenu
- }
- "
- />
-
- attachments.push(
- file
- )
- "
- >
-
-
-
-
-
-
- {
- content =
- ''
- attachments =
- []
- editing = false
- }
- "
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/portal/ticketing/Ticketing.vue b/desk/src/pages/portal/ticketing/Ticketing.vue
deleted file mode 100644
index e3c6c1c1b..000000000
--- a/desk/src/pages/portal/ticketing/Ticketing.vue
+++ /dev/null
@@ -1,154 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/pages/portal/ticketing/Tickets.vue b/desk/src/pages/portal/ticketing/Tickets.vue
deleted file mode 100644
index 8f7979ed8..000000000
--- a/desk/src/pages/portal/ticketing/Tickets.vue
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ this.ticketFilter }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/desk/src/router.js b/desk/src/router.js
index 1d9636f1e..e800bf797 100644
--- a/desk/src/router.js
+++ b/desk/src/router.js
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from "vue-router";
import { call } from "frappe-ui";
import { useAuthStore } from "@/stores/auth";
+import { init as initTelemetry } from "./telemetry";
export const WEBSITE_ROOT = "Website Root";
@@ -8,16 +9,20 @@ export const LOGIN = "Login";
export const SIGNUP = "Signup";
export const VERIFY = "Verify Account";
export const AUTH_ROUTES = [LOGIN, SIGNUP, VERIFY];
+export const ONBOARDING_PAGE = "Setup";
export const CUSTOMER_PORTAL_LANDING = "PortalTickets";
export const CUSTOMER_PORTAL_NEW_TICKET = "DefaultNewTicket";
-export const KNOWLEDGE_BASE_PUBLIC = "Knowledge Base";
+export const CUSTOMER_PORTAL_TICKET = "PortalTicket";
export const AGENT_PORTAL_DASHBOARD = "DeskDashboard";
export const AGENT_PORTAL_TICKET_LIST = "DeskTickets";
export const AGENT_PORTAL_TICKET = "DeskTicket";
export const AGENT_PORTAL_LANDING = AGENT_PORTAL_DASHBOARD;
+export const KB_PUBLIC = "Knowledge Base";
+export const KB_PUBLIC_ARTICLE = "PortalKBArticle";
+
const routes = [
{
path: "",
@@ -44,84 +49,74 @@ const routes = [
props: true,
},
{
- path: "",
- name: "PortalRoot",
- component: () => import("@/pages/portal/Portal.vue"),
+ path: "/onboarding",
+ name: ONBOARDING_PAGE,
+ component: () => import("@/pages/onboarding/SimpleOnboarding.vue"),
+ },
+ {
+ path: "/knowledge-base",
+ component: () => import("@/pages/portal/kb/KnowledgeBase.vue"),
children: [
{
- path: "my-tickets",
- name: "Ticketing",
- component: () => import("@/pages/portal/ticketing/Ticketing.vue"),
- children: [
- {
- path: "",
- name: CUSTOMER_PORTAL_LANDING,
- component: () => import("@/pages/portal/ticketing/Tickets.vue"),
- },
- {
- path: ":ticketId",
- name: "PortalTicket",
- component: () => import("@/pages/portal/ticketing/Ticket.vue"),
- props: true,
- },
- {
- path: "new/:templateId",
- name: "TemplatedNewTicket",
- component: () => import("@/pages/portal/ticketing/NewTicket.vue"),
- props: true,
- },
- {
- path: "new",
- name: CUSTOMER_PORTAL_NEW_TICKET,
- component: () => import("@/pages/portal/ticketing/NewTicket.vue"),
- },
- {
- path: "impersonate",
- name: "Impersonate",
- component: () => import("@/pages/portal/ticketing/Impersonate.vue"),
- },
- ],
+ path: "",
+ name: KB_PUBLIC,
+ component: () =>
+ // shows root categories and faqs
+ import("@/pages/common/kb/Category.vue"),
+ meta: {
+ editable: false,
+ isRoot: true,
+ },
},
{
- path: "knowledge-base",
- component: () => import("@/pages/portal/kb/KnowledgeBase.vue"),
- children: [
- {
- path: "",
- name: KNOWLEDGE_BASE_PUBLIC,
- component: () =>
- // shows root categories and faqs
- import("@/pages/common/kb/Category.vue"),
- meta: {
- editable: false,
- isRoot: true,
- },
- },
- {
- path: "categories/:categoryId",
- name: "PortalKBCategory", // Category Page
- component: () =>
- // shows sub categories and articles
- import("@/pages/common/kb/Category.vue"),
- props: true,
- meta: {
- editable: false,
- },
- },
- {
- path: "articles/:articleId/:articleTitleSlug",
- name: "PortalKBArticle",
- component: () => import("@/pages/common/kb/Article.vue"),
- props: true,
- meta: {
- editable: false,
- },
- },
- ],
+ path: "categories/:categoryId",
+ name: "PortalKBCategory", // Category Page
+ component: () =>
+ // shows sub categories and articles
+ import("@/pages/common/kb/Category.vue"),
+ props: true,
+ meta: {
+ editable: false,
+ },
+ },
+ {
+ path: "articles/:articleId/:articleTitleSlug",
+ name: KB_PUBLIC_ARTICLE,
+ component: () => import("@/pages/common/kb/Article.vue"),
+ props: true,
+ meta: {
+ editable: false,
+ },
+ },
+ ],
+ },
+ {
+ path: "/my-tickets",
+ component: () => import("@/pages/portal/CustomerRoot.vue"),
+ children: [
+ {
+ path: "",
+ name: CUSTOMER_PORTAL_LANDING,
+ component: () => import("@/pages/portal/TicketList.vue"),
+ },
+ {
+ path: ":ticketId",
+ name: CUSTOMER_PORTAL_TICKET,
+ component: () => import("@/pages/portal/TicketSingle.vue"),
+ props: true,
+ },
+ {
+ path: "new",
+ name: CUSTOMER_PORTAL_NEW_TICKET,
+ component: () => import("@/pages/portal/TicketNew.vue"),
+ },
+ {
+ path: "new/:templateId",
+ component: () => import("@/pages/portal/TicketNew.vue"),
+ props: true,
},
],
},
-
{
path: "",
name: "AgentRoot",
@@ -135,7 +130,7 @@ const routes = [
{
path: "tickets",
name: AGENT_PORTAL_TICKET_LIST,
- component: () => import("@/pages/desk/Tickets.vue"),
+ component: () => import("@/pages/desk/ticket-list/TicketList.vue"),
},
{
path: "tickets/:ticketId",
@@ -392,7 +387,9 @@ router.beforeEach(async (to) => {
const authStore = useAuthStore();
try {
+ await initTelemetry();
await authStore.init();
+
if (isAuthRoute) {
router.replace({ name: WEBSITE_ROOT });
}
diff --git a/desk/src/stores/agent.ts b/desk/src/stores/agent.ts
index 460847693..00a6aa7c6 100644
--- a/desk/src/stores/agent.ts
+++ b/desk/src/stores/agent.ts
@@ -13,6 +13,7 @@ export const useAgentStore = defineStore("agent", () => {
doctype: "HD Agent",
fields: ["*"],
auto: true,
+ pageLength: 99999,
});
const options: ComputedRef
> = computed(
diff --git a/desk/src/stores/auth.ts b/desk/src/stores/auth.ts
index 4ce42bbe7..a9a9f40ee 100644
--- a/desk/src/stores/auth.ts
+++ b/desk/src/stores/auth.ts
@@ -40,6 +40,9 @@ export const useAuthStore = defineStore("auth", () => {
const resLogin = createResource({
url: URI_LOGIN,
+ onError() {
+ throw new Error("Invalid email or password");
+ },
onSuccess() {
router.replace({ path: "/" });
},
diff --git a/desk/src/stores/config.ts b/desk/src/stores/config.ts
index 8541c121b..16848828f 100644
--- a/desk/src/stores/config.ts
+++ b/desk/src/stores/config.ts
@@ -20,13 +20,16 @@ export const useConfigStore = defineStore("config", () => {
const helpdeskName: ComputedRef = computed(
() => config.value.helpdesk_name || DEFAULT_TITLE
);
- const suppressEmailToast: ComputedRef = computed(
- () => config.value.suppress_default_email_toast ?? true
+ const isSetupComplete: ComputedRef = computed(
+ () => config.value.is_setup_complete
+ );
+ const skipEmailWorkflow: ComputedRef = computed(
+ () => config.value.skip_email_workflow
);
const pageTitle = ref(null);
const windowTitle = computed(() =>
pageTitle.value
- ? `${pageTitle.value} | ${helpdeskName.value}`
+ ? `${pageTitle.value} • ${helpdeskName.value}`
: helpdeskName.value
);
@@ -40,11 +43,11 @@ export const useConfigStore = defineStore("config", () => {
socket.on("helpdesk:settings-updated", () => configRes.reload());
return {
- config,
brandLogo,
+ config,
helpdeskName,
- suppressEmailToast,
-
+ isSetupComplete,
setTitle,
+ skipEmailWorkflow,
};
});
diff --git a/desk/src/stores/contact.ts b/desk/src/stores/contact.ts
index 347e251e4..73adf6583 100644
--- a/desk/src/stores/contact.ts
+++ b/desk/src/stores/contact.ts
@@ -15,6 +15,7 @@ export const useContactStore = defineStore("contact", () => {
doctype: "Contact",
fields: ["*"],
auto: true,
+ pageLength: 99999,
});
const options: ComputedRef> = computed(
diff --git a/desk/src/stores/keymap.ts b/desk/src/stores/keymap.ts
index ade96fe5b..d50175c7c 100644
--- a/desk/src/stores/keymap.ts
+++ b/desk/src/stores/keymap.ts
@@ -1,4 +1,4 @@
-import { ComputedRef, ref, Ref, watch } from "vue";
+import { ComputedRef, ref, Ref } from "vue";
import { defineStore } from "pinia";
import { useMagicKeys } from "@vueuse/core";
import { isEqual } from "lodash";
@@ -33,7 +33,16 @@ class Shortcut {
}
export const useKeymapStore = defineStore("keymap", () => {
- const keys = useMagicKeys();
+ const keys = useMagicKeys({
+ passive: false,
+ onEventFired(e) {
+ const k = items.value.find((item) => item.isActive);
+ if (!k) return;
+
+ e.preventDefault();
+ k.handler();
+ },
+ });
const items: Ref> = ref([]);
const isOpen = ref(false);
@@ -54,10 +63,6 @@ export const useKeymapStore = defineStore("keymap", () => {
isOpen.value = open ?? !isOpen.value;
}
- watch(items, (v) => v.filter((s) => s.isActive).forEach((s) => s.handler()), {
- deep: true,
- });
-
return {
add,
isOpen,
diff --git a/desk/src/stores/team.ts b/desk/src/stores/team.ts
index 525c66b2f..88a6df063 100644
--- a/desk/src/stores/team.ts
+++ b/desk/src/stores/team.ts
@@ -10,6 +10,7 @@ export const useTeamStore = defineStore("team", () => {
const d__ = createListResource({
doctype: "HD Team",
auto: true,
+ pageLength: 99999,
});
const options: ComputedRef> = computed(
diff --git a/desk/src/stores/ticketPriority.ts b/desk/src/stores/ticketPriority.ts
index 31757eb0c..ceac3ad21 100644
--- a/desk/src/stores/ticketPriority.ts
+++ b/desk/src/stores/ticketPriority.ts
@@ -12,6 +12,7 @@ export const useTicketPriorityStore = defineStore("ticketPriority", () => {
doctype: "HD Ticket Priority",
orderBy: "integer_value desc",
auto: true,
+ pageLength: 99999,
});
const options: ComputedRef> = computed(
diff --git a/desk/src/stores/ticketType.ts b/desk/src/stores/ticketType.ts
index 1bd74f81a..19a2bae3e 100644
--- a/desk/src/stores/ticketType.ts
+++ b/desk/src/stores/ticketType.ts
@@ -13,6 +13,7 @@ export const useTicketTypeStore = defineStore("ticketType", () => {
doctype: "HD Ticket Type",
fields: ["*"],
auto: true,
+ pageLength: 99999,
});
const options: ComputedRef> = computed(
diff --git a/desk/src/telemetry.ts b/desk/src/telemetry.ts
new file mode 100644
index 000000000..9a32f929b
--- /dev/null
+++ b/desk/src/telemetry.ts
@@ -0,0 +1,57 @@
+import { useStorage } from "@vueuse/core";
+import { call } from "frappe-ui";
+import "../../../frappe/frappe/public/js/lib/posthog.js";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+declare const posthog: any;
+
+const APP = "helpdesk";
+const SITENAME = window.location.hostname;
+
+const telemetry = useStorage("telemetry", {
+ enabled: false,
+ project_id: "",
+ host: "",
+});
+
+export async function init() {
+ await set_enabled();
+ if (!telemetry.value.enabled) return;
+ try {
+ await set_credentials();
+ posthog.init(telemetry.value.project_id, {
+ api_host: telemetry.value.host,
+ autocapture: false,
+ capture_pageview: false,
+ capture_pageleave: false,
+ advanced_disable_decide: true,
+ });
+ posthog.identify(SITENAME);
+ } catch (e) {
+ console.trace("Failed to initialize telemetry", e);
+ telemetry.value.enabled = false;
+ }
+}
+
+async function set_enabled() {
+ if (telemetry.value.enabled) return;
+
+ await call("helpdesk.api.telemetry.is_enabled").then((res) => {
+ telemetry.value.enabled = res;
+ });
+}
+
+async function set_credentials() {
+ if (!telemetry.value.enabled) return;
+ if (telemetry.value.project_id && telemetry.value.host) return;
+
+ await call("helpdesk.api.telemetry.get_credentials").then((res) => {
+ telemetry.value.project_id = res.project_id;
+ telemetry.value.host = res.telemetry_host;
+ });
+}
+
+export function capture(event: string) {
+ if (!telemetry.value.enabled) return;
+ posthog.capture(`${APP}_${event}`);
+}
diff --git a/desk/src/utils/editor.ts b/desk/src/utils/editor.ts
index 6c91b9ff4..d6f1028af 100644
--- a/desk/src/utils/editor.ts
+++ b/desk/src/utils/editor.ts
@@ -12,5 +12,5 @@ export function strip(content: string) {
* with empty strings.
*/
export function isEmpty(content: string) {
- return isEmpty_(strip(content).trim());
+ return isEmpty_(strip(content).trim());
}
diff --git a/desk/vite.config.js b/desk/vite.config.js
index 40488ace1..e8a5a7f09 100644
--- a/desk/vite.config.js
+++ b/desk/vite.config.js
@@ -39,14 +39,18 @@ export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
+ "tailwind.config.js": path.resolve(__dirname, "tailwind.config.js"),
},
},
build: {
outDir: `../helpdesk/public/desk`,
emptyOutDir: true,
target: "es2021",
+ commonjsOptions: {
+ include: [/tailwind.config.js/, /node_modules/],
+ },
},
optimizeDeps: {
- include: ["feather-icons", "showdown"],
+ include: ["feather-icons", "showdown", "tailwind.config.js"],
},
});
diff --git a/desk/yarn.lock b/desk/yarn.lock
index ff1213900..af72b3df1 100644
--- a/desk/yarn.lock
+++ b/desk/yarn.lock
@@ -1221,10 +1221,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.105:
- version "0.0.105"
- resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.0.105.tgz#173acd4dbf8e14f1d072c1a19a4dd597af0d7833"
- integrity sha512-/CbbpAgEHgOXQ6L69hg1hW99rHsc6Q5VKccaBSCZbczU9iWNXvnm+YZ3LggnwNiFpiJBjNsy1TqMUy448ry+oQ==
+frappe-ui@^0.0.112:
+ version "0.0.112"
+ resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.0.112.tgz#2246c8739ffdb913eb92cba4a7290a997d3c7b8c"
+ integrity sha512-6hu7GwXQh1W4O6wKOjwSgl0RwszGsIgh+IM9/hOOsYHLOsb58pQkSnWNb8F2A70qGbE9u07zqX0knFtzI381kQ==
dependencies:
"@headlessui/vue" "^1.5.0"
"@popperjs/core" "^2.11.2"
@@ -1307,7 +1307,7 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
-htmlparser2@^8.0.1:
+htmlparser2@^8.0.0, htmlparser2@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
@@ -1374,6 +1374,11 @@ is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-plain-object@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
+ integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -1561,6 +1566,11 @@ nanoid@^3.3.4:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
+nanoid@^3.3.6:
+ version "3.3.6"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
+ integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
+
node-fetch@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
@@ -1649,6 +1659,11 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
+parse-srcset@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
+ integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
+
parse5-htmlparser2-tree-adapter@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1"
@@ -1773,6 +1788,15 @@ postcss@^8.0.9, postcss@^8.1.10, postcss@^8.4.21, postcss@^8.4.5:
picocolors "^1.0.0"
source-map-js "^1.0.2"
+postcss@^8.3.11:
+ version "8.4.23"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"
+ integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==
+ dependencies:
+ nanoid "^3.3.6"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.2"
+
prosemirror-changeset@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/prosemirror-changeset/-/prosemirror-changeset-2.2.0.tgz#22c05da271a118be40d3e339fa2cace789b1254b"
@@ -2007,6 +2031,18 @@ safari-14-idb-fix@^3.0.0:
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-3.0.0.tgz#450fc049b996ec7f3fd9ca2f89d32e0761583440"
integrity sha512-eBNFLob4PMq8JA1dGyFn6G97q3/WzNtFK4RnzT1fnLq+9RyrGknzYiM/9B12MnKAxuj1IXr7UKYtTNtjyKMBog==
+sanitize-html@^2.10.0:
+ version "2.10.0"
+ resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.10.0.tgz#74d28848dfcf72c39693139131895c78900ab452"
+ integrity sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ==
+ dependencies:
+ deepmerge "^4.2.2"
+ escape-string-regexp "^4.0.0"
+ htmlparser2 "^8.0.0"
+ is-plain-object "^5.0.0"
+ parse-srcset "^1.0.2"
+ postcss "^8.3.11"
+
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
diff --git a/helpdesk/__init__.py b/helpdesk/__init__.py
index fa3ddd8c5..3e2f46a3a 100644
--- a/helpdesk/__init__.py
+++ b/helpdesk/__init__.py
@@ -1 +1 @@
-__version__ = "0.8.4"
+__version__ = "0.9.0"
diff --git a/helpdesk/api/config.py b/helpdesk/api/config.py
index f71a28a57..0986ecfbc 100644
--- a/helpdesk/api/config.py
+++ b/helpdesk/api/config.py
@@ -3,16 +3,12 @@
@frappe.whitelist(allow_guest=True)
def get_config():
- brand_logo = frappe.db.get_single_value("HD Settings", "brand_logo")
- brand_favicon = frappe.db.get_single_value("HD Settings", "brand_favicon")
- helpdesk_name = frappe.db.get_single_value("HD Settings", "helpdesk_name")
- suppress_default_email_toast = frappe.db.get_single_value(
- "HD Settings", "suppress_default_email_toast"
- )
+ settings = frappe.get_single("HD Settings")
return {
- "brand_logo": brand_logo,
- "brand_favicon": brand_favicon,
- "helpdesk_name": helpdesk_name,
- "suppress_default_email_toast": suppress_default_email_toast,
+ "brand_logo": settings.brand_logo,
+ "brand_favicon": settings.brand_favicon,
+ "helpdesk_name": settings.helpdesk_name,
+ "is_setup_complete": settings.setup_complete,
+ "skip_email_workflow": settings.skip_email_workflow,
}
diff --git a/helpdesk/api/dashboard.py b/helpdesk/api/dashboard.py
index 47faadf1b..c9326baa6 100644
--- a/helpdesk/api/dashboard.py
+++ b/helpdesk/api/dashboard.py
@@ -90,7 +90,7 @@ def new_tickets():
res = frappe.db.get_list(
"HD Ticket",
- fields=["COUNT(name) as value", "DATE_FORMAT(creation, '%d/%m/%Y') as name"],
+ fields=["COUNT(name) as value", "DATE(creation) as name"],
filters=filters,
group_by="DATE(creation)",
order_by="DATE(creation)",
@@ -144,7 +144,7 @@ def ticket_activity():
res = frappe.db.get_list(
"HD Ticket Activity",
- fields=["COUNT(name) as value", "DATE_FORMAT(creation, '%d/%m/%Y') as name"],
+ fields=["COUNT(name) as value", "DATE(creation) as name"],
filters=filters,
group_by="DATE(creation)",
order_by="DATE(creation)",
diff --git a/helpdesk/api/settings.py b/helpdesk/api/settings.py
deleted file mode 100644
index e0224d273..000000000
--- a/helpdesk/api/settings.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import frappe
-
-
-@frappe.whitelist()
-def update_helpdesk_name(name):
- doc = frappe.get_doc("HD Settings")
- doc.helpdesk_name = name
- doc.save(ignore_permissions=True)
-
- return doc.helpdesk_name
-
-
-@frappe.whitelist()
-def skip_helpdesk_name_setup():
- doc = frappe.get_doc("HD Settings")
- doc.initial_helpdesk_name_setup_skipped = True
- doc.save(ignore_permissions=True)
-
- return doc
diff --git a/helpdesk/api/telemetry.py b/helpdesk/api/telemetry.py
new file mode 100644
index 000000000..7fd31a3e0
--- /dev/null
+++ b/helpdesk/api/telemetry.py
@@ -0,0 +1,18 @@
+import frappe
+
+
+@frappe.whitelist()
+def is_enabled():
+ return bool(
+ frappe.get_system_settings("enable_telemetry")
+ and frappe.conf.get("posthog_host")
+ and frappe.conf.get("posthog_project_id")
+ )
+
+
+@frappe.whitelist()
+def get_credentials():
+ return {
+ "project_id": frappe.conf.get("posthog_project_id"),
+ "telemetry_host": frappe.conf.get("posthog_host"),
+ }
diff --git a/helpdesk/extends/client.py b/helpdesk/extends/client.py
index 67ce57a1c..72ad92f02 100644
--- a/helpdesk/extends/client.py
+++ b/helpdesk/extends/client.py
@@ -6,7 +6,6 @@
import frappe
from frappe.model.base_document import get_controller
-from frappe.query_builder import Query
from frappe.query_builder.functions import Count
from .doc import apply_sort
@@ -20,7 +19,7 @@ def get_list(
filters=None,
order_by=None,
start=0,
- limit=20,
+ limit=None,
group_by=None,
parent=None,
debug=False,
diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
index b541be565..d648b802c 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py
@@ -16,7 +16,6 @@
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Case, DocType, Order
from frappe.query_builder.functions import Count
-from frappe.realtime import get_website_room
from frappe.utils import date_diff, get_datetime, now_datetime, time_diff_in_seconds
from frappe.utils.user import is_website_user
@@ -27,6 +26,7 @@
default_outgoing_email_account,
default_ticket_outgoing_email_account,
)
+from helpdesk.utils import publish_event, capture_event
class HDTicket(Document):
@@ -110,6 +110,10 @@ def by_priority(query: Query, direction: Order):
"Last modified on": "modified",
}
+ def publish_update(self):
+ publish_event("helpdesk:ticket-update", {"name": self.name})
+ capture_event("ticket_updated")
+
def autoname(self):
return self.name
@@ -128,10 +132,12 @@ def before_insert(self):
def after_insert(self):
log_ticket_activity(self.name, "created")
+ capture_event("ticket_created")
def on_update(self):
self.handle_ticket_activity_update()
self.remove_assignment_if_not_in_team()
+ self.publish_update()
def handle_ticket_activity_update(self):
"""
@@ -157,11 +163,6 @@ def handle_ticket_activity_update(self):
self.name, f"{field_maps[field]} set to {self.as_dict()[field]}"
)
- event = "helpdesk:ticket-update"
- room = get_website_room()
- data = {"name": self.name}
- frappe.publish_realtime(event, message=data, room=room, after_commit=True)
-
def remove_assignment_if_not_in_team(self):
"""
Removes the assignment if the agent is not in the team.
@@ -298,10 +299,7 @@ def assign_agent(self, agent):
agent_name = frappe.get_value("HD Agent", agent, "agent_name")
log_ticket_activity(self.name, f"assigned to {agent_name}")
- event = "helpdesk:ticket-assignee-update"
- data = {"name": self.name}
- room = get_website_room()
- frappe.publish_realtime(event, message=data, room=room, after_commit=True)
+ publish_event("helpdesk:ticket-assignee-update", {"name": self.name})
def get_assigned_agent(self):
if self._assign:
@@ -436,6 +434,19 @@ def reply_via_agent(
communication.insert(ignore_permissions=True)
+ # Mark status, unconditionally.
+ self.reload()
+ self.status = "Replied"
+ self.save()
+
+ capture_event("agent_replied")
+
+ if skip_email_workflow:
+ return
+
+ if not sender_email:
+ frappe.throw(_("Can not send email. No sender email set up!"))
+
_attachments = []
for attachment in attachments:
@@ -445,12 +456,6 @@ def reply_via_agent(
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"
@@ -492,6 +497,40 @@ def reply_via_agent(
except Exception as e:
frappe.throw(_(e))
+ @frappe.whitelist()
+ def create_communication_via_contact(self, message, attachments=[]):
+ ticket_doc = frappe.get_doc("HD Ticket", self.name)
+
+ if ticket_doc.status == "Replied":
+ ticket_doc.status = "Open"
+ log_ticket_activity(self.name, f"status set to Open")
+ ticket_doc.save(ignore_permissions=True)
+
+ communication = frappe.new_doc("Communication")
+ communication.update(
+ {
+ "communication_type": "Communication",
+ "communication_medium": "Email",
+ "sent_or_received": "Received",
+ "email_status": "Open",
+ "subject": "Re: " + ticket_doc.subject,
+ "sender": ticket_doc.raised_by,
+ "content": message,
+ "status": "Linked",
+ "reference_doctype": "HD Ticket",
+ "reference_name": ticket_doc.name,
+ }
+ )
+ communication.ignore_permissions = True
+ communication.ignore_mandatory = True
+ communication.save(ignore_permissions=True)
+
+ 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)
+
@frappe.whitelist()
def mark_seen(self):
self.add_seen()
@@ -622,6 +661,22 @@ def get_comments(self):
return l
+ @frappe.whitelist()
+ def reopen(self):
+ if self.status != "Resolved":
+ frappe.throw(_("Only resolved tickets can be reopened"))
+
+ self.status = "Open"
+ self.save()
+
+ @frappe.whitelist()
+ def resolve(self):
+ if self.status == "Closed":
+ frappe.throw(_("Closed tickets cannot be resolved"))
+
+ self.status = "Resolved"
+ self.save()
+
def set_descritption_from_communication(doc, type):
if doc.reference_doctype == "HD Ticket":
@@ -918,7 +973,9 @@ def calculate_first_response_time(ticket, first_responded_on):
ticket_creation_date = ticket.creation
ticket_creation_time = get_time_in_seconds(ticket_creation_date)
first_responded_on_in_seconds = get_time_in_seconds(first_responded_on)
- support_hours = frappe.get_cached_doc("SLA", ticket.sla).support_and_resolution
+ support_hours = frappe.get_cached_doc(
+ "HD Service Level Agreement", ticket.sla
+ ).support_and_resolution
if ticket_creation_date.day == first_responded_on.day:
if is_work_day(ticket_creation_date, support_hours):
diff --git a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
index 5b48fa493..2d1734a34 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py
@@ -1,46 +1,25 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt
-import frappe
from frappe.model.document import Document
-from frappe.realtime import get_website_room
-from frappe.utils import get_fullname
-from helpdesk.utils import extract_mentions
+from helpdesk.utils import capture_event, publish_event
class HDTicketComment(Document):
- def on_change(self):
- mentions = extract_mentions(self.content)
-
- for mention in mentions:
- values = frappe._dict(
- from_user=self.commented_by,
- to_user=mention.email,
- ticket=self.reference_ticket,
- comment=self.name,
- )
-
- if frappe.db.exists("HD Notification", values):
- continue
-
- notification = frappe.get_doc(doctype="HD 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):
event = "helpdesk:new-ticket-comment"
data = {"ticket_id": self.reference_ticket}
- room = get_website_room()
+ telemetry_event = "ticket_comment_added"
- frappe.publish_realtime(event, message=data, room=room, after_commit=True)
+ publish_event(event, data)
+ capture_event(telemetry_event)
def after_delete(self):
event = "helpdesk:delete-ticket-comment"
data = {"ticket_id": self.reference_ticket}
- room = get_website_room()
+ telemetry_event = "ticket_comment_deleted"
+
+ publish_event(event, data)
+ capture_event(telemetry_event)
- frappe.publish_realtime(event, message=data, room=room, after_commit=True)
diff --git a/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.json b/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.json
index f433c7b2d..58bfad6a7 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.json
+++ b/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.json
@@ -22,8 +22,9 @@
},
{
"fieldname": "about",
- "fieldtype": "Text Editor",
- "label": "About"
+ "fieldtype": "Code",
+ "label": "About",
+ "options": "HTML"
},
{
"fieldname": "template_route",
@@ -40,7 +41,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-03-27 00:28:50.972089",
+ "modified": "2023-06-02 13:34:05.696001",
"modified_by": "Administrator",
"module": "Helpdesk",
"name": "HD Ticket Template",
@@ -72,4 +73,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
+}
\ No newline at end of file
diff --git a/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py b/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py
index 93d4dae80..5aac732fa 100644
--- a/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py
+++ b/helpdesk/helpdesk/doctype/hd_ticket_template/hd_ticket_template.py
@@ -4,17 +4,12 @@
import frappe
from frappe.model.document import Document
from frappe.website.utils import cleanup_page_name
-from frappe.utils.safe_exec import safe_exec
class HDTicketTemplate(Document):
def validate(self):
allowed_field_types = [
- "Data",
- "Custom Link",
"Link",
- "Long Text",
- "Text Editor",
"Select",
]
@@ -25,24 +20,10 @@ def validate(self):
)
if not field.fieldname:
field.fieldname = cleanup_page_name(field.label)
+
if field.fieldname == "description" and field.fieldtype != "Text Editor":
frappe.throw(f"field type for description field should be Text Editor")
- required_fields_not_added = []
- for fieldname in ["subject", "description"]:
- if not next(
- (
- field
- for field in self.fields
- if field.fieldname == fieldname and field.reqd == True
- ),
- None,
- ):
- required_fields_not_added.append(fieldname)
-
- if len(required_fields_not_added) > 0:
- frappe.throw(f"template mandetory fields {required_fields_not_added} are not added")
-
def before_save(self):
self.template_route = cleanup_page_name(self.template_name)
diff --git a/helpdesk/patches.txt b/helpdesk/patches.txt
index bf27357d6..203392e6a 100644
--- a/helpdesk/patches.txt
+++ b/helpdesk/patches.txt
@@ -7,3 +7,4 @@ helpdesk.patches.naming_autoincrement
[post_model_sync]
execute:frappe.delete_doc("Workspace", "Frappe Desk", force=True)
helpdesk.patches.add_priority_integer
+helpdesk.patches.template_remove_default_fields
diff --git a/helpdesk/patches/template_remove_default_fields.py b/helpdesk/patches/template_remove_default_fields.py
new file mode 100644
index 000000000..eba4935c3
--- /dev/null
+++ b/helpdesk/patches/template_remove_default_fields.py
@@ -0,0 +1,9 @@
+import frappe
+from frappe.query_builder import Case
+
+
+def execute():
+ QBDocField = frappe.qb.DocType("HD Ticket Template DocField")
+ case = Case.any([QBDocField.fieldname == "Subject", QBDocField.fieldname == "Description"])
+ frappe.qb.from_(QBDocField).delete().where(case).run()
+ frappe.db.commit()
diff --git a/helpdesk/setup/default_template.py b/helpdesk/setup/default_template.py
new file mode 100644
index 000000000..bbbce7d6f
--- /dev/null
+++ b/helpdesk/setup/default_template.py
@@ -0,0 +1,21 @@
+import frappe
+
+
+def create_default_template():
+ if frappe.db.exists("HD Ticket Template", "Default"):
+ return
+
+ template = frappe.new_doc("HD Ticket Template")
+ template.template_name = "Default"
+ template.append(
+ "fields",
+ {
+ "label": "Type",
+ "fieldname": "ticket_type",
+ "fieldtype": "Link",
+ "options": "HD Ticket Type",
+ "reqd": False,
+ },
+ )
+
+ template.insert()
diff --git a/helpdesk/setup/install.py b/helpdesk/setup/install.py
index 4ea204e1b..7d2613363 100644
--- a/helpdesk/setup/install.py
+++ b/helpdesk/setup/install.py
@@ -4,6 +4,7 @@
from frappe.permissions import add_permission
from .welcome_ticket import create_welcome_ticket
+from .default_template import create_default_template
def before_install():
@@ -16,12 +17,12 @@ def after_install():
add_default_ticket_priorities()
add_default_sla()
add_on_ticket_create_script()
- add_default_ticket_template()
add_default_agent_groups()
update_agent_role_permissions()
add_default_assignment_rule()
add_system_preset_filters()
create_welcome_ticket()
+ create_default_template()
def add_support_redirect_to_tickets():
@@ -182,39 +183,6 @@ def enable_track_service_level_agreement_in_support_settings():
frappe.db.commit()
-def add_default_ticket_template():
- if frappe.db.exists("HD Ticket Template", "Default"):
- return
-
- template = frappe.new_doc("HD Ticket Template")
-
- template.template_name = "Default"
- template.append(
- "fields",
- {
- "label": "Type",
- "fieldname": "ticket_type",
- "fieldtype": "Link",
- "options": "HD Ticket Type",
- "reqd": False,
- },
- )
- template.append(
- "fields",
- {"label": "Subject", "fieldname": "subject", "fieldtype": "Data", "reqd": True},
- )
- template.append(
- "fields",
- {
- "label": "Description",
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "reqd": True,
- },
- )
-
- template.insert()
-
def add_default_ticket_types():
ticket_types = ["Question", "Bug", "Incident"]
diff --git a/helpdesk/utils.py b/helpdesk/utils.py
index 4f30009da..d6ffd57f9 100644
--- a/helpdesk/utils.py
+++ b/helpdesk/utils.py
@@ -1,7 +1,19 @@
-from bs4 import BeautifulSoup
-import frappe
import re
+import frappe
+from bs4 import BeautifulSoup
+from frappe.realtime import get_website_room
+from frappe.utils.telemetry import capture as _capture
+
+
+def publish_event(event: str, data: dict):
+ room = get_website_room()
+ frappe.publish_realtime(event, message=data, room=room, after_commit=True)
+
+
+def capture_event(event: str):
+ return _capture(event, "helpdesk")
+
def extract_mentions(html):
if not html:
diff --git a/logo.png b/logo.png
new file mode 100644
index 000000000..67936464c
Binary files /dev/null and b/logo.png differ