Merge pull request #3448 from aerodeval/fix-ui-issues

fix: UI bugs
This commit is contained in:
Sydney Gomes
2026-06-12 00:57:19 +05:30
committed by GitHub
9 changed files with 120 additions and 37 deletions
+8
View File
@@ -151,6 +151,7 @@ const htmlContent = computed(
font-weight: 700;
height: 12px;
line-height: 0.1;
color: #383838;
background: #e8eaed;
width: 23px;
justify-content: center;
@@ -159,6 +160,13 @@ const htmlContent = computed(
.replied-content .collapse:hover {
background: #dadce0;
}
[data-theme='dark'] .replied-content .collapse {
color: #e2e2e2;
background: #424242;
}
[data-theme='dark'] .replied-content .collapse:hover {
background: #525252;
}
.replied-content .collapse + input {
display: none;
}
@@ -193,18 +193,25 @@ const onPriorityChange = () => {
setAssignmentRuleValue("priority", props.data.priority);
};
const onToggle = () => {
const onToggle = (enabled: boolean) => {
if (!props.data.users_exists && props.data.disabled) {
toast.error(__("Cannot enable rule without adding users in it"));
return;
}
setAssignmentRuleValue("disabled", !props.data.disabled, "status");
// Optimistically flip so the switch reflects the new state immediately;
// revert if the backend update fails.
const previous = props.data.disabled;
props.data.disabled = enabled ? 0 : 1;
setAssignmentRuleValue("disabled", props.data.disabled, "status", () => {
props.data.disabled = previous;
});
};
const setAssignmentRuleValue = (
key: string,
value: any,
fieldName?: string
fieldName?: string,
onError?: () => void
) => {
createResource({
url: "frappe.client.set_value",
@@ -215,11 +222,16 @@ const setAssignmentRuleValue = (
value: value,
},
onSuccess: () => {
assignmentRulesListData?.reload();
toast.success(
__("Assignment rule {0} updated successfully.", fieldName || key)
);
},
onError: () => {
onError?.();
toast.error(
__("Failed to update assignment rule {0}.", fieldName || key)
);
},
auto: true,
});
};
+11 -1
View File
@@ -195,7 +195,7 @@ import { useNotificationStore } from "@/stores/notification";
import { useSidebarStore } from "@/stores/sidebar";
import { capture } from "@/telemetry";
import { isCustomerPortal } from "@/utils";
import { call, toast } from "frappe-ui";
import { call, toast, useTheme } from "frappe-ui";
import {
GettingStartedBanner,
HelpModal,
@@ -224,6 +224,8 @@ 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 LucideMoon from "~icons/lucide/moon";
import LucideSun from "~icons/lucide/sun";
import LucideMail from "~icons/lucide/mail";
import MailOpen from "~icons/lucide/mail-open";
import MessageCircle from "~icons/lucide/message-circle";
@@ -254,6 +256,13 @@ const showShortcutsModal = ref(false);
const showCommandPalette = ref(false);
const { pinnedViews, publicViews } = useView();
const { currentTheme, toggleTheme } = useTheme();
const themeMenuItem = computed(() => ({
label: __("Toggle theme"),
icon: currentTheme.value === "dark" ? LucideSun : LucideMoon,
onClick: () => toggleTheme(),
}));
const isFCSite = ref(window.is_fc_site);
@@ -326,6 +335,7 @@ function parseViews(views) {
}
const customerPortalDropdown = computed(() => [
themeMenuItem.value,
{
group: __("Danger"),
hideLabel: true,
+10 -6
View File
@@ -122,12 +122,16 @@
>
<UserAvatar :name="agent.value" size="sm" />
</Tooltip>
<div
class="absolute bottom-0 -right-0.5 size-2 rounded-full outline outline-white outline-1.5"
:class="
agentStatusStore.statusColor(agent.availability || '')
"
/>
<span
class="absolute block translate-x-1/2 translate-y-1/2 transform rounded-full bottom-0.5 right-0.5"
>
<span
class="block h-2 w-2 rounded-full border border-slate-2"
:class="
agentStatusStore.statusColor(agent.availability || '')
"
/>
</span>
</div>
<span class="text-ink-gray-7 flex-1 text-start truncate">
{{ agent.label }}
@@ -211,8 +211,7 @@ function handleDeleteTicket() {
iconLeft: "trash-2",
variant: "solid",
onClick({ close }) {
call("frappe.client.delete", {
doctype: "HD Ticket",
call("helpdesk.api.ticket.delete_ticket", {
name: ticket?.value?.doc.name,
})
.then(() => {
+4 -1
View File
@@ -1,10 +1,11 @@
<template>
<div
v-if="showHeader"
class="mx-6 md:mx-10 md:my-2 flex items-center justify-between text-lg font-medium mb-4 !mt-6 md:h-8 md:text-xl md:font-semibold md:text-ink-gray-8"
>
Activity
</div>
<div class="overflow-auto px-6 md:px-10 grow">
<div class="overflow-auto px-5 md:px-10 grow">
<div
v-for="(c, i) in communications"
:id="c.name"
@@ -52,10 +53,12 @@ import { ITicket } from "./symbols";
interface P {
focus?: string;
showHeader?: boolean;
}
const props = withDefaults(defineProps<P>(), {
focus: "",
showHeader: true,
});
const route = useRoute();
const ticket = inject(ITicket);
+24 -3
View File
@@ -38,11 +38,24 @@
>
</Alert>
</div>
<!-- show for only mobile -->
<TicketCustomerTemplateFields v-if="isMobileView" />
<!-- Mobile: Activity / Details tabs -->
<Tabs
v-if="isMobileView"
v-model="activeTab"
:tabs="tabs"
class="[&_[role='tablist']]:px-5"
>
<template #tab-panel="{ tab }">
<TicketCustomerTemplateFields v-if="tab.name === 'details'" />
<TicketConversation v-else :show-header="false" class="grow" />
</template>
</Tabs>
<!-- Desktop: conversation -->
<TicketConversation v-else class="grow" />
<TicketConversation class="grow" />
<div
v-if="!isMobileView || activeTab === 0"
class="w-full p-5"
@keydown.ctrl.enter.capture.stop="sendEmail"
@keydown.meta.enter.capture.stop="sendEmail"
@@ -92,12 +105,14 @@ import { globalStore } from "@/stores/globalStore";
import { useTicketStatusStore } from "@/stores/ticketStatus";
import { __ } from "@/translation";
import { isContentEmpty, isCustomerPortal, uploadFunction } from "@/utils";
import { ActivityIcon, DetailsIcon } from "@/components/icons";
import {
Alert,
Breadcrumbs,
Button,
call,
createResource,
Tabs,
toast,
} from "frappe-ui";
import {
@@ -162,6 +177,12 @@ const { isMobileView } = useScreenSize();
const { $dialog, $socket } = globalStore();
const isDismissed = ref(false);
const activeTab = ref(0);
const tabs = computed(() => [
{ name: "activity", label: __("Activity"), icon: ActivityIcon },
{ name: "details", label: __("Details"), icon: DetailsIcon },
]);
function getTodayKey() {
return new Date().toISOString().split("T")[0];
}
@@ -1,44 +1,59 @@
<template>
<div class="grid grid-cols-3 md:grid-cols-1 gap-4 border-b px-5 py-2.5">
<div class="space-y-1.5">
<span class="block text-sm text-ink-gray-7"> Status </span>
<span class="block break-words text-base font-medium text-ink-gray-9">
<div class="space-y-2 px-6 py-3.5 border-b">
<div class="flex items-center gap-4">
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">Status</span>
<span
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
>
{{ ticket.data.status }}
</span>
</div>
<div class="space-y-1.5">
<span class="block text-sm text-ink-gray-7"> Priority </span>
<span class="block break-words text-base font-medium text-ink-gray-9">
<div class="flex items-center gap-4">
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">Priority</span>
<span
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
>
{{ ticket.data.priority }}
</span>
</div>
<div v-for="data in slaData" :key="data.label" class="space-y-1.5">
<div
v-for="data in slaData"
:key="data.label"
class="flex items-center gap-4"
>
<Tooltip :text="dayjs(data.value).format('LLLL')">
<span class="block text-sm text-ink-gray-7">{{ data.title }}</span>
<span class="w-[160px] shrink-0 text-sm text-ink-gray-5">{{
data.title
}}</span>
</Tooltip>
<span class="block break-words text-base font-medium text-ink-gray-9">
<span
class="flex-1 rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium text-ink-gray-9"
>
<Badge
v-if="data.showSla"
:label="data.label"
:theme="data.theme"
variant="outline"
/>
<span v-else>
{{ dayjs.tz(data.value).fromNow() }}
</span>
<span v-else>{{ dayjs.tz(data.value).fromNow() }}</span>
</span>
</div>
<div
v-for="field in customFields"
:key="field.fieldname"
class="space-y-1.5"
class="flex items-center gap-4"
>
<span class="block text-sm text-ink-gray-7">
{{ field.label }}
</span>
<span class="w-[150px] shrink-0 text-sm text-ink-gray-5">{{
field.label
}}</span>
<span
class="block break-words text-base font-medium text-ink-gray-9"
:class="!ticket.data[field.fieldname] && 'text-ink-gray-4'"
class="flex-1 break-words rounded border border-outline-gray-2 bg-surface-white px-2 py-1 text-base font-medium"
:class="
ticket.data[field.fieldname] ? 'text-ink-gray-9' : 'text-ink-gray-4'
"
>
{{ ticket.data[field.fieldname] || "—" }}
</span>
+12 -1
View File
@@ -1,7 +1,7 @@
import frappe
from frappe import _
from helpdesk.utils import agent_only
from helpdesk.utils import agent_only, is_admin
@frappe.whitelist()
@@ -66,3 +66,14 @@ def assign_ticket_to_agent(ticket_id, agent_id=None):
ticket_doc.assign_agent(agent_id)
return ticket_doc
@frappe.whitelist()
def delete_ticket(name: str):
if not is_admin():
frappe.throw(
msg=_("Only administrators can delete tickets."),
title=_("Not Allowed"),
exc=frappe.PermissionError,
)
frappe.delete_doc("HD Ticket", name, force=True, ignore_permissions=True)