Merge remote-tracking branch 'upstream/develop' into fix/agent-landing-page-bugs

This commit is contained in:
Pratik
2026-04-08 12:07:58 +05:30
63 changed files with 30234 additions and 12628 deletions
+2 -2
View File
@@ -18,7 +18,7 @@
"@vueuse/core": "^13.8.0",
"@vueuse/integrations": "^13.8.0",
"dayjs": "^1.11.7",
"frappe-ui": "0.1.269",
"frappe-ui": "0.1.271",
"gemoji": "^8.1.0",
"mime": "^3.0.0",
"pinia": "^2.0.33",
@@ -43,4 +43,4 @@
"cheerio": "1.0.0-rc.12",
"prosemirror-model": "1.25.4"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
View File
@@ -205,6 +205,13 @@ watch(iframeRef, (iframe) => {
iframe.style.height = parent.offsetHeight + 1 + "px";
// Clicks inside the iframe don't bubble to the parent document, popovers/dropdowns that close on outside-click never fire.
iframe.contentDocument?.addEventListener("pointerdown", () => {
document.dispatchEvent(
new PointerEvent("pointerdown", { bubbles: true })
);
});
const replyCollapsers = emailContent.querySelectorAll(".replyCollapser");
if (replyCollapsers.length) {
replyCollapsers.forEach((replyCollapser) => {
+3 -3
View File
@@ -442,9 +442,8 @@ onMounted(() => {
function handleSelectAll(e: KeyboardEvent) {
const active = document.activeElement;
const editorDom = editorRef.value?.editor?.view?.dom as
| HTMLElement
| undefined;
const editorContext = editorRef.value?.editor;
const editorDom = editorContext?.view?.dom as HTMLElement | undefined;
const quotedEl = quotedContentRef.value;
const sel = window.getSelection();
if (!sel || !editorDom) return;
@@ -452,6 +451,7 @@ function handleSelectAll(e: KeyboardEvent) {
return;
}
e.preventDefault();
editorContext?.commands.selectAll();
sel.removeAllRanges();
const range = document.createRange();
-10
View File
@@ -51,7 +51,6 @@
params: { [options.rowRoute?.prop]: row.name },
query: { view: route.query?.view },
}),
onRowClick: (row) => emit('rowClick', row.name),
emptyState,
}"
>
@@ -163,7 +162,6 @@ import {
import {
computed,
h,
nextTick,
onMounted,
provide,
reactive,
@@ -326,9 +324,6 @@ const list = createResource({
onSuccess: (data) => {
list.params = defaultParams;
columns.value = data.columns;
nextTick(() => {
document.querySelector(".list-rows")?.focus();
});
},
});
@@ -774,8 +769,3 @@ onMounted(async () => {
defineExpose(exposeFunctions);
</script>
<style scoped>
.list-rows:focus {
outline: none;
}
</style>
+8 -1
View File
@@ -45,7 +45,7 @@
</div>
</template>
<script setup>
import { ref } from "vue";
import { ref, watch } from "vue";
const props = defineProps({
label: {
@@ -81,6 +81,13 @@ const props = defineProps({
const hide = ref(props.hideLabel);
const opened = ref(props.opened);
watch(
() => props.opened,
(val) => {
opened.value = val;
}
);
function toggle() {
opened.value = !opened.value;
}
+14 -7
View File
@@ -10,7 +10,7 @@
class="flex w-52 shrink-0 flex-col bg-gray-50 p-1 overflow-y-auto hide-scrollbar"
>
<h1
class="h-7.5 px-2 py-[7px] my-[3px] flex cursor-pointer gap-1.5 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
class="h-7.5 px-2 py-[7px] my-[3px] flex cursor-pointer gap-1.5 text-base text-ink-gray-5 transition-all duration-300 ease-in-out truncate"
>
{{ __("My Settings") }}
</h1>
@@ -21,7 +21,9 @@
v-if="!tab.hideLabel"
class="h-7.5 px-2 py-[7px] my-[3px] flex cursor-pointer gap-1.5 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
<Tooltip :text="__(tab.label)" placement="right">
<span class="truncate">{{ __(tab.label) }}</span>
</Tooltip>
</div>
<nav class="space-y-[3px] px-1">
@@ -36,10 +38,15 @@
]"
@click="() => onTabChange(item)"
>
<component :is="item.icon" class="h-4 w-4 text-gray-700" />
<span class="text-p-sm text-gray-800">
{{ item.label }}
</span>
<component
:is="item.icon"
class="h-4 w-4 text-gray-700 shrink-0"
/>
<Tooltip :text="__(item.label)" placement="right">
<span class="text-p-sm text-gray-800 truncate">
{{ __(item.label) }}
</span>
</Tooltip>
</button>
</nav>
</div>
@@ -78,7 +85,7 @@
/>
</template>
<script setup lang="ts">
import { Dialog } from "frappe-ui";
import { Dialog, Tooltip } from "frappe-ui";
import { ModelRef, ref, watch } from "vue";
import ConfirmDialog from "@/components/ConfirmDialog.vue";
import {
+4 -2
View File
@@ -33,14 +33,16 @@
</span>
<div
class="-all ml-2 flex shrink-0 grow items-center justify-between text-sm duration-300 ease-in-out"
class="-all ml-2 flex min-w-0 items-center justify-between text-sm duration-300 ease-in-out w-full"
:class="{
'opacity-100': isExpanded,
'opacity-0': !isExpanded,
'-z-50': !isExpanded,
}"
>
{{ __(label) }}
<Tooltip :text="__(label)" placement="right">
<span class="truncate"> {{ __(label) }}</span>
</Tooltip>
<slot name="right" />
</div>
</div>
+1 -1
View File
@@ -11,7 +11,7 @@
<template #default="{ open }">
<Button
variant="ghost"
class="text-lg font-medium text-nowrap"
class="text-lg font-medium text-nowrap truncate max-w-[200px] sm:max-w-none"
:label="currentView.label"
>
<template #prefix>
@@ -18,12 +18,9 @@
:class="inputClasses"
@click="() => !disabled && togglePopover()"
>
<div class="flex items-center">
<div class="flex items-center min-w-0 flex-1">
<slot name="prefix" />
<span
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
v-if="selectedValue"
>
<span class="text-base leading-5 truncate" v-if="selectedValue">
{{ displayValue(selectedValue) }}
</span>
<span class="text-base leading-5 text-gray-500" v-else>
+23 -6
View File
@@ -50,7 +50,12 @@
</template>
</SidebarLink>
</div>
<div class="overflow-y-auto overflow-x-hidden">
<div
:class="[
'overflow-y-auto overflow-x-hidden',
!isExpanded && 'hide-scrollbar',
]"
>
<div v-for="view in allViews" :key="view.label">
<!-- <div
v-if="!view.hideLabel && !isExpanded && view.views?.length"
@@ -60,9 +65,9 @@
<Section
:label="view.label"
:hideLabel="view.hideLabel"
:opened="view.opened"
:opened="isSectionOpen(view.label, view.opened)"
>
<template #header="{ opened, hide, toggle }">
<template #header="{ opened, hide }">
<div
v-if="!hide"
class="flex cursor-pointer gap-1.5 px-2 text-base mx-2 font-medium text-ink-gray-5 transition-all duration-300 ease-in-out"
@@ -71,12 +76,12 @@
? 'ml-0 h-0 overflow-hidden opacity-0'
: 'pt-[11px] pb-2.5 w-auto opacity-100 '
"
@click="toggle()"
@click="toggleSection(view.label, view.opened)"
>
<FeatherIcon
name="chevron-right"
class="h-4 text-ink-gray-9 transition-all duration-300 ease-in-out"
:class="{ 'rotate-90': opened }"
:class="{ 'rotate-90': isSectionOpen(view.label, view.opened) }"
/>
<span>{{ __(view.label) }}</span>
</div>
@@ -212,7 +217,6 @@ import LucideBell from "~icons/lucide/bell";
import FileText from "~icons/lucide/file-text";
import Globe from "~icons/lucide/globe";
import LucideKeyboard from "~icons/lucide/keyboard";
import LucideLayoutDashboard from "~icons/lucide/layout-dashboard";
import LucideMail from "~icons/lucide/mail";
import MailOpen from "~icons/lucide/mail-open";
import MessageCircle from "~icons/lucide/message-circle";
@@ -245,6 +249,19 @@ const { pinnedViews, publicViews } = useView();
const isFCSite = ref(window.is_fc_site);
const sectionOpenState = ref<Record<string, boolean>>({});
function isSectionOpen(label: string, defaultOpen: boolean): boolean {
if (!isExpanded.value) return true;
if (label in sectionOpenState.value) return sectionOpenState.value[label];
return defaultOpen;
}
function toggleSection(label: string, defaultOpen: boolean) {
const current = isSectionOpen(label, defaultOpen);
sectionOpenState.value[label] = !current;
}
const allViews = computed(() => {
let items = isCustomerPortal.value
? customerPortalSidebarOptions
@@ -12,7 +12,7 @@
</div>
<!-- Optional Text -->
<div v-if="activity.feedback_extra">
<p class="mt-2 text-sm text-gray-700">{{ activity.feedback_extra }}</p>
<p class="mt-2 text-p-sm text-gray-700">{{ activity.feedback_extra }}</p>
</div>
</div>
</template>
@@ -94,7 +94,13 @@
class="text-base text-gray-800 flex-1"
:class="!field.value && 'text-ink-gray-4'"
>
<template v-if="field.value && dayjs(field.value).isValid()">
<template
v-if="
field.value &&
(field.fieldtype === 'Date' || field.fieldtype === 'Datetime') &&
dayjs(field.value).isValid()
"
>
{{ dateFormat(field.value, dateTooltipFormat) }}
</template>
<template v-else>
+6 -1
View File
@@ -173,6 +173,7 @@ import {
Tooltip,
} from "frappe-ui";
import { computed, h, inject } from "vue";
import { useDebounceFn } from "@vueuse/core";
const props = defineProps({
default_filters: {
@@ -492,7 +493,7 @@ function updateValue(value, filter) {
} else {
filter.value = value;
}
apply();
debouncedApply();
}
function updateOperator(event, filter) {
@@ -672,4 +673,8 @@ const timespanOptions = [
value: "next year",
},
];
const debouncedApply = useDebounceFn(() => {
apply();
}, 500);
</script>
@@ -5,11 +5,11 @@
type="checkbox"
:checked="props.value"
@change.stop="updateFilter(filter, $event.target.checked)"
class="w-44"
class="w-36"
/>
<FormControl
v-else-if="filter.type === 'Select'"
class="form-control cursor-pointer [&_select]:cursor-pointer w-44"
class="form-control cursor-pointer [&_select]:cursor-pointer w-36"
type="select"
:model-value="props.value"
:options="filter.options"
@@ -22,11 +22,11 @@
:doctype="filter.options"
:placeholder="filter.label"
@change="(data) => updateFilter(filter, data)"
class="w-44"
class="w-36"
/>
<component
v-else-if="['Date', 'Datetime'].includes(filter.type)"
class="border-none w-44"
class="border-none w-36"
:is="filter.type === 'Date' ? DatePicker : DateTimePicker"
:value="props.value"
@change="(v) => updateFilter(filter, v)"
@@ -1,6 +1,6 @@
<template>
<FadedScrollableDiv
class="flex flex-1 items-center -ml-1 overflow-x-scroll py-1 gap-2"
class="quick-filters flex flex-1 items-center -ml-1 overflow-x-auto py-1 gap-2 pr-4"
orientation="horizontal"
v-if="!quickFilters.loading"
>
@@ -79,4 +79,12 @@ function getValue(quickFilter, filters) {
}
</script>
<style lang="scss" scoped></style>
<style scoped>
.quick-filters {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.quick-filters::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
</style>
+29
View File
@@ -9,6 +9,7 @@
@apply break-normal max-w-none prose prose-code:break-all prose-code:whitespace-pre-wrap prose-img:border prose-img:rounded-lg prose-sm;
}
}
.tiptap input[placeholder="Add caption"] {
display: none !important;
}
@@ -17,3 +18,31 @@ html,
body {
color: var(--ink-gray-9);
}
/* Modern slim scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: var(--outline-gray-3) transparent;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: var(--outline-gray-3);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--outline-gray-4);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
+86
View File
@@ -0,0 +1,86 @@
import { existsSync } from "node:fs";
import path from "path";
import type { PluginOption } from "vite";
interface LocalFrappeUIDevConfigParams {
mode: string;
rootDir: string;
}
interface LocalFrappeUIDevConfig {
useLocalFrappeUI: boolean;
localFrappeUIPath: string;
localFrappeUIAliases: Record<string, string>;
}
interface ImportFrappeUIPluginParams {
useLocalFrappeUI: boolean;
}
type FrappeUIPluginFactory = (...args: any[]) => PluginOption;
type FrappeUIPluginModule = { default: FrappeUIPluginFactory };
async function loadFrappeUIPluginModule(
modulePath: string
): Promise<FrappeUIPluginModule> {
return (await import(modulePath)) as FrappeUIPluginModule;
}
export function getLocalFrappeUIDevConfig({
mode,
rootDir,
}: LocalFrappeUIDevConfigParams): LocalFrappeUIDevConfig {
const isDev = mode === "development";
const localFrappeUIPath = path.resolve(rootDir, "../frappe-ui");
const useLocalFrappeUI =
isDev && existsSync(path.join(localFrappeUIPath, "node_modules"));
if (isDev && existsSync(localFrappeUIPath) && !useLocalFrappeUI) {
console.warn("⚠️ Local frappe-ui found but dependencies not installed.");
console.warn(" Run: cd ../frappe-ui && yarn install");
}
const localFrappeUIAliases: Record<string, string> = useLocalFrappeUI
? {
"frappe-ui/style.css": path.resolve(
localFrappeUIPath,
"src",
"style.css"
),
"frappe-ui": localFrappeUIPath,
}
: {};
return {
useLocalFrappeUI,
localFrappeUIPath,
localFrappeUIAliases,
};
}
export async function importFrappeUIPlugin({
useLocalFrappeUI,
}: ImportFrappeUIPluginParams): Promise<FrappeUIPluginFactory> {
const npmModulePath = "frappe-ui/vite";
const modulePath = useLocalFrappeUI
? "../frappe-ui/vite/index.js"
: npmModulePath;
try {
const module = await loadFrappeUIPluginModule(modulePath);
return module.default as FrappeUIPluginFactory;
} catch (error) {
if (useLocalFrappeUI) {
console.warn(
"⚠️ Failed to import local frappe-ui plugin, falling back to npm package"
);
console.warn(
" Error:",
error instanceof Error ? error.message : String(error)
);
const fallbackModule = await loadFrappeUIPluginModule(npmModulePath);
return fallbackModule.default as FrappeUIPluginFactory;
}
throw error;
}
}
+108 -92
View File
@@ -1,102 +1,118 @@
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import frappeui from "frappe-ui/vite";
import path from "path";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import {
getLocalFrappeUIDevConfig,
importFrappeUIPlugin,
} from "./vite-helpers";
export default defineConfig({
plugins: [
frappeui({
frappeProxy: true,
lucideIcons: true,
jinjaBootData: true,
buildConfig: {
outDir: `../helpdesk/public/desk`,
emptyOutDir: true,
indexHtmlPath: "../helpdesk/www/helpdesk/index.html",
},
frappeTypes: {
input: {
helpdesk: [
"hd_ticket_status",
"hd_ticket",
"hd_service_holiday_list",
"hd_service_level_agreement",
"hd_agent",
],
frappe: ["assignment_rule"],
export default defineConfig(async ({ mode }) => {
const { useLocalFrappeUI, localFrappeUIAliases } = getLocalFrappeUIDevConfig({
mode,
rootDir: __dirname,
});
const frappeui = await importFrappeUIPlugin({ useLocalFrappeUI });
const config = {
plugins: [
frappeui({
frappeProxy: true,
lucideIcons: true,
jinjaBootData: true,
buildConfig: {
outDir: `../helpdesk/public/desk`,
emptyOutDir: true,
indexHtmlPath: "../helpdesk/www/helpdesk/index.html",
},
},
}),
frappeTypes: {
input: {
helpdesk: [
"hd_ticket_status",
"hd_ticket",
"hd_service_holiday_list",
"hd_service_level_agreement",
"hd_agent",
],
frappe: ["assignment_rule"],
},
},
}),
vue(),
vueJsx(),
VitePWA({
registerType: "autoUpdate",
devOptions: {
enabled: true,
},
workbox: {
cleanupOutdatedCaches: true,
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
},
manifest: {
display: "standalone",
name: "Frappe Helpdesk",
short_name: "Helpdesk",
start_url: "/helpdesk",
description:
"Modern, Streamlined, Free and Open Source Customer Service Software",
icons: [
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
}),
],
server: {
allowedHosts: true,
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"tailwind.config.js": path.resolve(__dirname, "tailwind.config.js"),
},
},
optimizeDeps: {
include: [
"feather-icons",
"tailwind.config.js",
"prosemirror-state",
"prosemirror-view",
"prosemirror-gapcursor",
"prosemirror-tables",
"lowlight",
"interactjs",
vue(),
vueJsx(),
VitePWA({
registerType: "autoUpdate",
devOptions: {
enabled: true,
},
workbox: {
cleanupOutdatedCaches: true,
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
},
manifest: {
display: "standalone",
name: "Frappe Helpdesk",
short_name: "Helpdesk",
start_url: "/helpdesk",
description:
"Modern, Streamlined, Free and Open Source Customer Service Software",
icons: [
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-192.maskable.png",
sizes: "192x192",
type: "image/png",
purpose: "maskable",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "/assets/helpdesk/desk/manifest/manifest-icon-512.maskable.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
}),
],
exclude: ["frappe-ui"],
},
server: {
allowedHosts: true,
fs: {
allow: [".."],
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"tailwind.config.js": path.resolve(__dirname, "tailwind.config.js"),
...localFrappeUIAliases,
},
},
optimizeDeps: {
include: [
"feather-icons",
"tailwind.config.js",
"prosemirror-state",
"prosemirror-view",
"prosemirror-gapcursor",
"prosemirror-tables",
"lowlight",
"interactjs",
],
exclude: ["frappe-ui"],
},
};
return config;
});
+4 -4
View File
@@ -2650,10 +2650,10 @@ fraction.js@^5.3.4:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a"
integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
[email protected]69:
version "0.1.269"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.269.tgz#d694cd23fe6eadbd7bb898bb3f46e0414c27f829"
integrity sha512-1D4AW8cPoC7OIRqEzYnF169/r+v0EfVBvfCi/ovmk76FBPKY+qIVOzL+Vx/9TcYvJubc1tSU19F/jX177e3axA==
[email protected]71:
version "0.1.271"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.271.tgz#206aa52f4faa541da0cfdb2b32ae845e622b08e1"
integrity sha512-H6FDKLynUCXxo3eIainJd6FjWrBtAZ3WZXVRM4tvLT3J7x7P5JjoNefB6YD8WZ+MQThKSwMC+/Kzg8vY2mc5XQ==
dependencies:
"@floating-ui/dom" "^1.7.4"
"@floating-ui/vue" "^1.1.6"
+1 -1
View File
@@ -1 +1 @@
__version__ = "1.21.3"
__version__ = "1.22.1"
+1 -7
View File
@@ -607,12 +607,7 @@ def apply_datetime_filter(query, field, filter_value):
@frappe.whitelist()
def remove_assignments(
doctype: str,
name: str | int,
assignees: list[str],
ignore_permissions: bool = False,
):
def remove_assignments(doctype: str, name: str | int, assignees: list[str]):
assignees = frappe.parse_json(assignees)
if not assignees:
@@ -625,5 +620,4 @@ def remove_assignments(
todo=None,
assign_to=assign_to,
status="Cancelled",
ignore_permissions=ignore_permissions,
)
@@ -43,6 +43,7 @@ def update_agent_role(user: str, new_role: str):
"""
Update the role of the user to Agent
"""
frappe.only_for(("Agent Manager", "System Manager"))
user_doc = frappe.get_doc("User", user)
@@ -1,9 +1,30 @@
# Copyright (c) 2022, Frappe Technologies and Contributors
# See license.txt
# import frappe
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from helpdesk.helpdesk.doctype.hd_agent.hd_agent import update_agent_role
class TestHDAgent(unittest.TestCase):
pass
class TestHDAgent(FrappeTestCase):
test_user = "[email protected]"
def setUp(self):
if not frappe.db.exists("User", self.test_user):
frappe.get_doc(
{
"doctype": "User",
"email": self.test_user,
"first_name": "Test User",
"send_welcome_email": 0,
}
).insert(ignore_permissions=True)
def test_unauthorized_role_update(self):
frappe.set_user(self.test_user)
with self.assertRaises(frappe.PermissionError):
update_agent_role(self.test_user, "System Manager")
frappe.set_user("Administrator")
@@ -686,6 +686,7 @@ def get_ticket_contact(ticket: str | int):
@frappe.whitelist()
def get_recent_similar_tickets(ticket: str | int):
frappe.has_permission("HD Ticket", "read", str(ticket), throw=True)
if not frappe.db.exists("HD Ticket", ticket):
return {"recent_tickets": [], "similar_tickets": []}
@@ -572,6 +572,10 @@ class HDTicket(Document):
bcc: str | None = None,
attachments: list[str] = [],
):
if not is_agent():
frappe.throw(
_("You are not permitted to reply as an agent"), frappe.PermissionError
)
skip_email_workflow = self.skip_email_workflow()
medium = "" if skip_email_workflow else "Email"
subject = f"Re: {self.subject}"
@@ -709,3 +709,33 @@ class TestHDTicket(IntegrationTestCase):
remove_holidays()
frappe.db.set_single_value("HD Settings", "default_ticket_status", "Open")
frappe.delete_doc("HD Ticket Status", "New", force=True)
def test_security_unauthorized_reply_via_agent(self):
ticket = make_ticket()
frappe.set_user(non_agent)
with self.assertRaises(frappe.PermissionError):
ticket.reply_via_agent(message="Test unauthorized reply")
frappe.set_user("Administrator")
def test_security_unauthorized_assign_agent(self):
ticket = make_ticket()
frappe.set_user(non_agent)
with self.assertRaises(frappe.PermissionError):
ticket.assign_agent(agent)
frappe.set_user("Administrator")
def test_security_info_disclosure_similar_tickets(self):
from helpdesk.helpdesk.doctype.hd_ticket.api import get_recent_similar_tickets
ticket = make_ticket()
frappe.set_user(non_agent)
with self.assertRaises(frappe.PermissionError):
get_recent_similar_tickets(ticket.name)
frappe.set_user("Administrator")
+1006 -396
View File
File diff suppressed because it is too large Load Diff
+1200 -590
View File
File diff suppressed because it is too large Load Diff
+1019 -409
View File
File diff suppressed because it is too large Load Diff
+1001 -391
View File
File diff suppressed because it is too large Load Diff
+1005 -395
View File
File diff suppressed because it is too large Load Diff
+1080 -470
View File
File diff suppressed because it is too large Load Diff
+1054 -444
View File
File diff suppressed because it is too large Load Diff
+1031 -421
View File
File diff suppressed because it is too large Load Diff
+1006 -396
View File
File diff suppressed because it is too large Load Diff
+1193 -583
View File
File diff suppressed because it is too large Load Diff
+1008 -398
View File
File diff suppressed because it is too large Load Diff
+1009 -399
View File
File diff suppressed because it is too large Load Diff
+1011 -401
View File
File diff suppressed because it is too large Load Diff
+696 -393
View File
File diff suppressed because it is too large Load Diff
+1001 -391
View File
File diff suppressed because it is too large Load Diff
+1003 -393
View File
File diff suppressed because it is too large Load Diff
+1014 -404
View File
File diff suppressed because it is too large Load Diff
+1004 -394
View File
File diff suppressed because it is too large Load Diff
+1009 -399
View File
File diff suppressed because it is too large Load Diff
+1016 -406
View File
File diff suppressed because it is too large Load Diff
+1079 -469
View File
File diff suppressed because it is too large Load Diff
+1019 -409
View File
File diff suppressed because it is too large Load Diff
+1075 -465
View File
File diff suppressed because it is too large Load Diff
+1075 -465
View File
File diff suppressed because it is too large Load Diff
+1080 -470
View File
File diff suppressed because it is too large Load Diff
+1009 -399
View File
File diff suppressed because it is too large Load Diff
+1008 -398
View File
File diff suppressed because it is too large Load Diff
+1038 -428
View File
File diff suppressed because it is too large Load Diff
+1034 -424
View File
File diff suppressed because it is too large Load Diff
+55 -12
View File
@@ -4,21 +4,50 @@ from frappe.desk.form.assign_to import add as add_assign
AUTHOR_EMAIl = "[email protected]"
AUTHOR_NAME = "John Doe"
CONTENT = """
<p>
Hi 👋
<div style="font-family: 'Segoe UI', sans-serif; font-size: 15px; line-height: 1.8; color: #374151; max-width: 560px; margin: 0 auto;">
Hey {{ first_name }} 👋,
<br><br>
I'm glad you decided to try Helpdesk! We're working hard to build a better way for teams
to communicate and serve customers well.
We thought we'd use this space to give you a quick walkthrough while you're here. 🙂
<br><br>
You can get started right away by setting up a support email. This will help you see what
your support will look like with Helpdesk!
This is a sample ticket we created to show you how Frappe Helpdesk works.
<br><br>
If you face any issues, please reach out to us via <a href="https://support.frappe.io/helpdesk">
https://support.frappe.io/helpdesk</a>
<br><br>
Best,
Getting started is easy. In the main <b>Ticket activity area</b>, when you reply to a ticket your response shows up directly below in the activity thread. This way you can keep track of all your ticket activities in one single place. ✨
<br>
Ritvik Sardana | Frappe Helpdesk.
<img src="/assets/helpdesk/desk/ticketActivity.png" style="width: 100%; max-width: 560px; margin: 16px 0; border-radius: 10px; display: block;">
<br>
When it's time to follow up with a user, just click <b>Reply</b>. Your message will be sent directly to their inbox, keeping the conversation flowing smoothly.
<br>
<video autoplay loop muted src="/assets/helpdesk/desk/videos/mailVideo.mp4" style="width: 100%; max-width: 560px; margin: 16px 0; border-radius: 10px; display: block;"></video>
<br>
Want to discuss a ticket with your team internally? Utilize the <b>Comment</b> feature to leave notes that are visible only to your colleagues, ensuring that internal communication remains clear, organized, and private.<br>
<video autoplay loop muted src="/assets/helpdesk/desk/videos/commentVideo.mp4" style="width: 100%; max-width: 560px; margin: 16px 0; border-radius: 10px; display: block;"></video>
<br>
In the <b>Ticket Sidebar</b>, you can manage the ticket. You can assign it to yourself or a teammate, set the priority, ticket type and also update the ticket status.
<br>
<video autoplay loop muted src="/assets/helpdesk/desk/videos/sidebarVideo.mp4" style="width: 100%; max-width: 560px; margin: 16px 0; border-radius: 10px; display: block;"></video>
<br>
Inside the <b>Ticket Header</b>, you can view the first response and resolution timers. Additionally, you can also see the source of the ticket, whether it came from email or the support portal.
<br>
<img src="/assets/helpdesk/desk/timers.png" style="width: 100%; max-width: 560px; margin: 16px 0; border-radius: 10px; display: block;">
<br><br>
That's pretty much how it works.
<br><br>
What you can do next:<br>
<ul style="padding-left: 20px; margin: 0;">
<li>Connect your support email to start receiving real tickets.</li>
<li>Invite your team and auto-assign tickets using assignment rules.</li>
<li>Set up SLAs to manage response times for your agents.</li>
<li>Add a Knowledge Base for self-serve support.</li>
<li>Check out our documentation at <a href="https://docs.frappe.io/helpdesk" style="color: #3b82f6;">https://docs.frappe.io/helpdesk</a> </li>
</ul>
<br>
Feel free to click around and explore our product.
<br><br>
If you need help, reach out to us here: <a href="https://support.frappe.io/helpdesk" style="color: #3b82f6;">https://support.frappe.io/helpdesk</a>
<br><br>
Cheers,<br>
Team Frappe
</div>
"""
@@ -31,9 +60,23 @@ def create_ticket():
if frappe.db.count("HD Ticket"):
return
# Render template with the current user's information
user_doc = frappe.get_doc("User", frappe.session.user)
if (user_doc.name or "").strip().lower() == "administrator":
first_name = "there"
else:
first_name = (user_doc.first_name or "").strip() or "there"
rendered_content = frappe.render_template(
CONTENT,
{
"first_name": first_name,
},
safe_render=True,
)
d = frappe.new_doc("HD Ticket")
d.subject = "Welcome to Helpdesk"
d.description = CONTENT
d.description = rendered_content
d.raised_by = AUTHOR_EMAIl
d.contact = AUTHOR_NAME
d.via_customer_portal = True
+1 -7
View File
@@ -3,14 +3,8 @@
"scripts": {
"postinstall": "cd desk && yarn install",
"dev": "cd desk && yarn dev",
"build": "cd desk && yarn build",
"disable-workspaces": "sed -i '' 's/\"workspaces\"/\"aworkspaces\"/g' package.json",
"enable-workspaces": "sed -i '' 's/\"aworkspaces\"/\"workspaces\"/g' package.json && rm -rf node_modules ./desk/node_modules/ frappe-ui/node_modules/ && yarn install"
"build": "cd desk && yarn build"
},
"workspaces": [
"desk",
"frappe-ui"
],
"resolutions": {
"cheerio": "1.0.0-rc.12",
"prosemirror-model": "1.25.4"
+13 -55
View File
@@ -1788,9 +1788,9 @@
integrity sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA==
"@tiptap/extension-gapcursor@^3.20.5":
version "3.20.5"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.5.tgz#0fe2ffb1d7669fc4f5541a0c66342da4107b08f8"
integrity sha512-H+bRr+mqU/DQq1vfoMlppK1o+RbfSKYBMIcAMHWOez+C96MWfj5bhooVU2HLtl4XGmQxKGr3oEOCKDPdtRNThg==
version "3.22.0"
resolved "https://registry.yarnpkg.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.0.tgz#dbc50979d451c8f775b23550bc59b578b66a695c"
integrity sha512-VsnaTU88PlA/eG9DtUvuB90z5gVZIaH6T/JVTxGasxR4CFsv0L4Zq5awwr0+SsYH9dKepRMgbanVU03c6k1SuA==
"@tiptap/extension-hard-break@^2.27.2":
version "2.27.2"
@@ -2632,14 +2632,6 @@ agent-base@6:
dependencies:
debug "4"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv@^8.6.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
@@ -2737,11 +2729,6 @@ arraybuffer.prototype.slice@^1.0.4:
get-intrinsic "^1.2.6"
is-array-buffer "^3.0.4"
[email protected], assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7"
@@ -3049,11 +3036,6 @@ classnames@^2.2.5:
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -3207,11 +3189,6 @@ core-js@^3.1.3:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.49.0.tgz#8b4d520ac034311fa21aa616f017ada0e0dbbddd"
integrity sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==
[email protected]:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
@@ -3794,16 +3771,6 @@ extend-shallow@^2.0.1:
dependencies:
is-extendable "^0.1.0"
[email protected]:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==
extsprintf@^1.2.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -3910,10 +3877,10 @@ fraction.js@^5.3.4:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a"
integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==
[email protected]69:
version "0.1.269"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.269.tgz#d694cd23fe6eadbd7bb898bb3f46e0414c27f829"
integrity sha512-1D4AW8cPoC7OIRqEzYnF169/r+v0EfVBvfCi/ovmk76FBPKY+qIVOzL+Vx/9TcYvJubc1tSU19F/jX177e3axA==
[email protected]71:
version "0.1.271"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.271.tgz#206aa52f4faa541da0cfdb2b32ae845e622b08e1"
integrity sha512-H6FDKLynUCXxo3eIainJd6FjWrBtAZ3WZXVRM4tvLT3J7x7P5JjoNefB6YD8WZ+MQThKSwMC+/Kzg8vY2mc5XQ==
dependencies:
"@floating-ui/dom" "^1.7.4"
"@floating-ui/vue" "^1.1.6"
@@ -4338,11 +4305,6 @@ ignore@^5.2.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -4717,7 +4679,7 @@ json-schema-traverse@^1.0.0:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
[email protected], json-schema@^0.4.0:
json-schema@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
@@ -6051,6 +6013,11 @@ set-proto@^1.0.0:
es-errors "^1.3.0"
es-object-atoms "^1.0.0"
shell-quote@^1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
shiki-es@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/shiki-es/-/shiki-es-0.2.0.tgz#ae5bced62dca0ba46ee81149e68d428565a3e6fb"
@@ -6883,15 +6850,6 @@ [email protected]:
"@vue/devtools-api" "^7.5.2"
type-fest "^4.8.3"
[email protected]:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
[email protected]:
version "2.1.9"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"