Files
helpdesk/desk/src/composables/useTicket.ts

129 lines
4.1 KiB
TypeScript

import type { TicketAnalytics } from "@/components/ticket-agent/analytics/types";
import type { CommentExtras } from "@/components/ticket-agent/timeline/TimelineCommentRow.vue";
import { __ } from "@/translation";
import type {
DocumentResource,
RecentSimilarTicket,
Resource,
TicketAssignee,
TicketContact,
} from "@/types";
import type { HDTicket } from "@/types/doctypes";
import { createDocumentResource, createResource, toast } from "frappe-ui";
import { reactive } from "vue";
interface MapValue {
ticket: DocumentResource<HDTicket>;
assignees: Resource<TicketAssignee[]>;
contact: Resource<TicketContact>;
recentSimilarTickets: Resource<RecentSimilarTicket>;
analytics: Resource<TicketAnalytics>;
// shared by every timeline tab instance, fetched once per ticket
calls: Resource<Record<string, any>[]>;
commentExtras: Resource<TicketCommentExtras>;
// lent by the mounted timeline; see registerTicketFeed
reloadFeed?: () => void;
}
export interface TicketCommentExtras {
comments: Record<string, CommentExtras>;
// oldest first, so a pin's position is its "N:" in the pinned bar
pinned_comments: string[];
}
export interface PinnedComment {
name: string;
content: string;
}
const ticketMap: Record<string, MapValue> = reactive({});
export const useTicket = (ticketId: string): MapValue => {
if (!ticketMap[ticketId]) {
ticketMap[ticketId] = {
ticket: createDocumentResource<HDTicket>({
doctype: "HD Ticket",
name: ticketId,
whitelistedMethods: {
markSeen: "mark_seen",
},
setValue: {
onSuccess: () => {
toast.success(__("Ticket updated successfully."));
},
onError: (error) => {
const msg = error.exc_type
? (error.messages || error.message || []).join(", ")
: error.message;
toast.error(msg);
},
},
}),
assignees: createResource({
url: "helpdesk.helpdesk.doctype.hd_ticket.api.get_ticket_assignees",
params: { ticket: ticketId },
auto: true,
}),
contact: createResource({
url: "helpdesk.helpdesk.doctype.hd_ticket.api.get_ticket_contact",
params: { ticket: ticketId },
auto: true,
}),
recentSimilarTickets: createResource({
url: "helpdesk.helpdesk.doctype.hd_ticket.api.get_recent_similar_tickets",
params: { ticket: ticketId },
auto: true,
}),
// fetched by the analytics tab, not on ticket open
analytics: createResource({
url: "helpdesk.api.ticket_analytics.get_ticket_analytics",
params: { ticket: ticketId },
cache: ["Ticket", ticketId, "analytics"],
}),
calls: createResource({
url: "helpdesk.api.timeline.get_ticket_calls",
params: { ticket: ticketId },
auto: true,
}),
commentExtras: createResource({
url: "helpdesk.api.timeline.get_comment_extras",
params: { ticket: ticketId },
auto: true,
}),
};
}
return ticketMap[ticketId];
};
export function reloadTicket(ticketId: string) {
const ticketData = ticketMap[ticketId];
if (!ticketData) return;
ticketData.ticket.reload();
ticketData.assignees.reload();
}
// The timeline owns its feed through useActivityTimeline, so anything outside it
// (a saved reply applying actions, say) reloads through the mounted component.
export function registerTicketFeed(ticketId: string, reload: () => void) {
const ticketData = ticketMap[ticketId];
if (!ticketData) return () => {};
ticketData.reloadFeed = reload;
// identity check: on ticket switch the new instance mounts before the old unmounts
return () => {
if (ticketData.reloadFeed === reload) delete ticketData.reloadFeed;
};
}
export function reloadTicketFeed(ticketId: string) {
ticketMap[ticketId]?.reloadFeed?.();
}
// Refresh a ticket that may have gone stale
export function revalidateTicket(ticketId: string | number) {
const ticketData = ticketMap[ticketId];
if (ticketData?.ticket.get.fetched && !ticketData.ticket.get.loading) {
reloadTicket(ticketId as string);
}
}