From 1fc5c8dbecd5f0b10af2bb47aa8d42055a036021 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Tue, 20 Aug 2024 15:10:03 -0700 Subject: [PATCH 1/5] feat(Support): add captured URL to new ticket notif --- server/api/mail.js | 6 ++++-- server/api/support.ts | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/server/api/mail.js b/server/api/mail.js index 2775f57b..4806e1ff 100644 --- a/server/api/mail.js +++ b/server/api/mail.js @@ -787,8 +787,9 @@ const sendSupportTicketCreateConfirmation = (recipientAddress, ticketID, params) * @param {string} ticketAuthor - the ticket's author * @param {string} ticketCategory - the ticket's category * @param {string} ticketPriority - the ticket's priority + * @param {string | undefined} capturedURL - the URL of the page where the ticket was created or provided by the user */ -const sendSupportTicketCreateInternalNotification = (recipientAddresses, ticketID, ticketTitle, ticketBody, ticketAuthor, ticketCategory, ticketPriority) => { +const sendSupportTicketCreateInternalNotification = (recipientAddresses, ticketID, ticketTitle, ticketBody, ticketAuthor, ticketCategory, ticketPriority, capturedURL) => { return mailgun.messages.create(process.env.MAILGUN_DOMAIN, { from: 'LibreTexts Support ', to: recipientAddresses, @@ -800,8 +801,9 @@ const sendSupportTicketCreateInternalNotification = (recipientAddresses, ticketI

Author: ${ticketAuthor}

Category: ${ticketCategory}

Priority: ${ticketPriority}

+ ${capturedURL ? `

Related URL: ${capturedURL}

` : ''}
-

Body: ${ticketBody}

+

Description: ${ticketBody}


You can view the ticket at https://commons.libretexts.org/support/ticket/${ticketID}.

Sincerely,

diff --git a/server/api/support.ts b/server/api/support.ts index ddb77b79..69165013 100644 --- a/server/api/support.ts +++ b/server/api/support.ts @@ -180,9 +180,9 @@ async function getOpenInProgressTickets( .populate("assignedUsers") .populate("user") .exec()) as (SupportTicketInterface & { - assignedUsers?: UserInterface[]; - user?: UserInterface; - })[]; + assignedUsers?: UserInterface[]; + user?: UserInterface; + })[]; // We have to sort the tickets in memory because we can only alphabetically sort by priority in query if (req.query.sort === "priority") { @@ -628,7 +628,8 @@ async function createTicket( ticket.description, authorString, capitalizeFirstLetter(ticket.category), - capitalizeFirstLetter(ticket.priority) + capitalizeFirstLetter(ticket.priority), + ticket.capturedURL ?? undefined ); if (teamToNotify.length > 0) emailPromises.push(teamPromise); @@ -1325,8 +1326,8 @@ const _getTicketAuthorString = ( const ticketAuthor = foundUser ? `${foundUser.firstName} ${foundUser.lastName}` : guest - ? `${guest.firstName} ${guest.lastName}` - : "Unknown"; + ? `${guest.firstName} ${guest.lastName}` + : "Unknown"; const authorString = `${ticketAuthor} (${emailToNotify})`; return authorString; }; From 0e9aa65fc1cfc26e81478c5611fbe03bd7afd964 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Tue, 20 Aug 2024 15:16:39 -0700 Subject: [PATCH 2/5] feat(Support): add copy ID button --- .../src/components/support/StaffDashboard.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/client/src/components/support/StaffDashboard.tsx b/client/src/components/support/StaffDashboard.tsx index 5c6aa1ae..974e0278 100644 --- a/client/src/components/support/StaffDashboard.tsx +++ b/client/src/components/support/StaffDashboard.tsx @@ -12,6 +12,8 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import LoadingSpinner from "../LoadingSpinner"; import { capitalizeFirstLetter } from "../util/HelperFunctions"; import { getPrettySupportTicketCategory } from "../../utils/supportHelpers"; +import { useNotifications } from "../../context/NotificationContext"; +import CopyButton from "../util/CopyButton"; const AssignTicketModal = lazy(() => import("./AssignTicketModal")); const SupportCenterSettingsModal = lazy( () => import("./SupportCenterSettingsModal") @@ -25,7 +27,9 @@ type SupportMetrics = { const StaffDashboard = () => { const { handleGlobalError } = useGlobalError(); + const { addNotification } = useNotifications(); const user = useTypedSelector((state) => state.user); + const [loading, setLoading] = useState(false); const [activePage, setActivePage] = useState(1); const [activeSort, setActiveSort] = useState("opened"); @@ -390,7 +394,24 @@ const StaffDashboard = () => { {!isFetching && openTickets?.map((ticket) => ( - {ticket.uuid.slice(-7)} + {ticket.uuid.slice(-7)} + + {({ copied, copy }) => ( + { + copy() + addNotification({ + message: "Ticket ID copied to clipboard", + type: "success", + duration: 2000, + }); + }} + color={copied ? "green" : "blue"} + /> + )} + + {format(parseISO(ticket.timeOpened), "MM/dd/yyyy hh:mm aa")} From 90042fba93fa4dc20ffd0ae2c129d03704ea387c Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Tue, 20 Aug 2024 15:25:55 -0700 Subject: [PATCH 3/5] fix(CentralIdentity): minor UI tweaks --- .../CentralIdentity/ManageUserModal.tsx | 46 ++++++++++++------- .../src/providers/NotificationsProvider.tsx | 2 +- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/client/src/components/controlpanel/CentralIdentity/ManageUserModal.tsx b/client/src/components/controlpanel/CentralIdentity/ManageUserModal.tsx index d1a90353..eabbb965 100644 --- a/client/src/components/controlpanel/CentralIdentity/ManageUserModal.tsx +++ b/client/src/components/controlpanel/CentralIdentity/ManageUserModal.tsx @@ -27,10 +27,12 @@ import CtlCheckbox from "../../ControlledInputs/CtlCheckbox"; import { isCentralIdentityUserProperty } from "../../../utils/typeHelpers"; import axios from "axios"; import useGlobalError from "../../error/ErrorHooks"; -import { copyToClipboard, dirtyValues } from "../../../utils/misc"; +import { dirtyValues } from "../../../utils/misc"; import LoadingSpinner from "../../LoadingSpinner"; import { CentralIdentityApp } from "../../../types/CentralIdentity"; import { format, parseISO } from "date-fns"; +import CopyButton from "../../util/CopyButton"; +import { useNotifications } from "../../../context/NotificationContext"; const AddUserAppModal = lazy(() => import("./AddUserAppModal")); const AddUserOrgModal = lazy(() => import("./AddUserOrgModal")); const ConfirmRemoveOrgOrAppModal = lazy( @@ -82,6 +84,7 @@ const ManageUserModal: React.FC = ({ // Hooks and Error Handling const { handleGlobalError } = useGlobalError(); + const { addNotification } = useNotifications(); const { control, formState, reset, watch, getValues, setValue } = useForm({ defaultValues: { @@ -536,17 +539,23 @@ const ManageUserModal: React.FC = ({

UUID: {getValues("uuid")} - { - await copyToClipboard( - getValues("uuid") ?? "Unknown" - ); - }} - /> + + {({ copied, copy }) => ( + { + copy(); + addNotification({ + message: "UUID copied to clipboard!", + type: "success", + }); + }} + /> + )} +

@@ -562,16 +571,21 @@ const ManageUserModal: React.FC = ({ Time of Last Access: {getValues("last_access") ? format( - parseISO(getValues("last_access") as string), - "MM/dd/yyyy hh:mm aa" - ) + parseISO(getValues("last_access") as string), + "MM/dd/yyyy hh:mm aa" + ) : "Unknown"}

Time of Last Password Change: - {getValues("last_password_change") ?? "Never"} + {getValues("last_password_change") ? + format( + parseISO(getValues("last_password_change") as string), + "MM/dd/yyyy hh:mm aa" + ) : "Unknown" + }

diff --git a/client/src/providers/NotificationsProvider.tsx b/client/src/providers/NotificationsProvider.tsx index 37f5ad28..e3c3b956 100644 --- a/client/src/providers/NotificationsProvider.tsx +++ b/client/src/providers/NotificationsProvider.tsx @@ -66,7 +66,7 @@ const NotificationsProvider = ({ children }: { children: React.ReactNode }) => { }} > {children} -
+
{notifications.map((notification) => (
Date: Tue, 20 Aug 2024 15:43:01 -0700 Subject: [PATCH 4/5] feat(Support): add severe priority and ADAPT code category --- .../src/screens/conductor/support/Ticket.tsx | 25 ++++++++++--------- client/src/types/support.ts | 4 ++- client/src/utils/supportHelpers.ts | 16 +++++++++--- server/api/validators/support.ts | 8 +++--- server/models/supporticket.ts | 4 +-- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/client/src/screens/conductor/support/Ticket.tsx b/client/src/screens/conductor/support/Ticket.tsx index 17ea36a8..e2feb327 100644 --- a/client/src/screens/conductor/support/Ticket.tsx +++ b/client/src/screens/conductor/support/Ticket.tsx @@ -25,6 +25,7 @@ import api from "../../../api"; import { capitalizeFirstLetter } from "../../../components/util/HelperFunctions"; import { ca, hi } from "date-fns/locale"; import TicketAutoCloseWarning from "../../../components/support/TicketAutoCloseWarning"; +import { SupportTicketPriority } from "../../../types/support"; const AssignTicketModal = lazy( () => import("../../../components/support/AssignTicketModal") ); @@ -80,7 +81,7 @@ const SupportTicketView = () => { }); const updateTicketPriorityMutation = useMutation({ - mutationFn: (priority: "high" | "medium" | "low") => + mutationFn: (priority: SupportTicketPriority) => updateTicket({ priority }), onSuccess: () => { queryClient.invalidateQueries(["ticket", id]); @@ -142,7 +143,7 @@ const SupportTicketView = () => { autoCloseSilenced, }: { status?: "open" | "in_progress" | "closed"; - priority?: "high" | "medium" | "low"; + priority?: SupportTicketPriority; autoCloseSilenced?: boolean; }) { try { @@ -195,23 +196,23 @@ const SupportTicketView = () => { } const changePriorityOptions = useMemo(() => { - const allOpts = ["high", "medium", "low"]; + const allOpts = ["high", "medium", "low", "severe"]; const currentPriority = ticket?.priority ?? "medium"; const allowed = allOpts.filter((opt) => opt !== currentPriority); - const higherOrLower = (priority: string) => { - if (currentPriority === "high") { - return priority === "medium" ? "lower" : "lower"; - } else if (currentPriority === "medium") { - return priority === "high" ? "higher" : "lower"; - } else { - return priority === "medium" ? "higher" : "higher"; - } + const higherOrLower = (priority: SupportTicketPriority) => { + const priorityMap: Record = { + severe: 4, + high: 3, + medium: 2, + low: 1, + }; + return priorityMap[priority] > priorityMap[currentPriority] ? "higher" : "lower"; }; return allowed.map((opt) => ({ value: capitalizeFirstLetter(opt), - icon: higherOrLower(opt) === "higher" ? "arrow up" : "arrow down", + icon: higherOrLower(opt as SupportTicketPriority) === "higher" ? "arrow up" : "arrow down", })); }, [ticket]); diff --git a/client/src/types/support.ts b/client/src/types/support.ts index a523b4b7..e78c60fe 100644 --- a/client/src/types/support.ts +++ b/client/src/types/support.ts @@ -7,13 +7,15 @@ export type SupportTicketGuest = { organization: string; }; +export type SupportTicketPriority = "low" | "medium" | "high" | "severe"; + export type SupportTicket = { uuid: string; title: string; description: string; apps?: number[]; // Central Identity app IDs attachments?: SupportTicketAttachment[]; - priority: "low" | "medium" | "high"; + priority: SupportTicketPriority; status: "open" | "in_progress" | "closed"; category: string; capturedURL?: string; diff --git a/client/src/utils/supportHelpers.ts b/client/src/utils/supportHelpers.ts index a67cc714..72c641dd 100644 --- a/client/src/utils/supportHelpers.ts +++ b/client/src/utils/supportHelpers.ts @@ -3,19 +3,24 @@ import { GenericKeyTextValueObj, User } from "../types"; export const SupportTicketPriorityOptions: GenericKeyTextValueObj[] = [ { key: "low", - text: "Low", + text: "Low (General Inquiries, Feature Requests)", value: "low", }, { key: "medium", - text: "Medium", + text: "Medium (Technical Issues, Account Issues, etc.)", value: "medium", }, { key: "high", - text: "High", + text: "High (Wide-Spread Issues, Time-Sensitive Requests)", value: "high", }, + { + key: "severe", + text: "Severe (Critical Issues, System-Wide Outages)", + value: "severe", + }, ]; export const SupportTicketCategoryOptions: GenericKeyTextValueObj[] = [ @@ -24,6 +29,11 @@ export const SupportTicketCategoryOptions: GenericKeyTextValueObj[] = [ text: "General Inquiry", value: "general", }, + { + key: "adaptcode", + text: "ADAPT Access Code Request", + value: "adaptcode", + }, { key: "technical", text: "Technical Issue (Bug, Error, etc.)", diff --git a/server/api/validators/support.ts b/server/api/validators/support.ts index 70c97a09..1fbb101b 100644 --- a/server/api/validators/support.ts +++ b/server/api/validators/support.ts @@ -6,6 +6,8 @@ const TicketUUIDParams = z.object({ }), }); +const TicketPriority = z.enum(["low", "medium", "high", "severe"]); + export const GetTicketValidator = TicketUUIDParams; export const DeleteTicketValidator = TicketUUIDParams; export const GetUserTicketsValidator = z.object({ @@ -22,7 +24,7 @@ export const CreateTicketValidator = z.object({ title: z.string().trim().min(1).max(200), description: z.string().trim().max(1000), apps: z.array(z.number()).optional(), - priority: z.enum(["low", "medium", "high"]), + priority: TicketPriority, category: z.string(), capturedURL: z.string().url().optional(), attachments: z.array(z.string()).optional(), @@ -42,7 +44,7 @@ export const AddTicketAttachementsValidator = TicketUUIDParams; export const UpdateTicketValidator = z .object({ body: z.object({ - priority: z.enum(["low", "medium", "high"]), + priority: TicketPriority, status: z.enum(["open", "in_progress", "closed"]), autoCloseSilenced: z.boolean().optional(), }), @@ -62,7 +64,7 @@ export const GetOpenTicketsValidator = z.object({ sort: z.enum(["opened", "priority", "status", "category"]).optional(), assignee: z.string().uuid().or(z.literal("")).optional(), category: z.string().or(z.literal("")).optional(), - priority: z.enum(["low", "medium", "high"]).or(z.literal("")).optional(), + priority: TicketPriority.or(z.literal("")).optional(), }), }); diff --git a/server/models/supporticket.ts b/server/models/supporticket.ts index 19aba3f7..4adddb27 100644 --- a/server/models/supporticket.ts +++ b/server/models/supporticket.ts @@ -32,7 +32,7 @@ export interface SupportTicketInterface extends Document { description: string; apps?: number[]; // Central Identity app IDs attachments?: SupportTicketAttachmentInterface[]; - priority: "low" | "medium" | "high"; + priority: "low" | "medium" | "high" | "severe"; status: "open" | "in_progress" | "closed"; category: string; guestAccessKey: string; @@ -88,7 +88,7 @@ const SupportTicketSchema = new Schema({ }, priority: { type: String, - enum: ["low", "medium", "high"], + enum: ["low", "medium", "high", "severe"], default: "low", }, status: { From d3b71d34300f217af1f1cb0cec3e53244a6be106 Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Tue, 20 Aug 2024 15:49:08 -0700 Subject: [PATCH 5/5] feat(Support): minor ui improvements --- .../src/components/support/StaffDashboard.tsx | 9 ++++----- client/src/components/support/TicketDetails.tsx | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/client/src/components/support/StaffDashboard.tsx b/client/src/components/support/StaffDashboard.tsx index 974e0278..b2bd6bda 100644 --- a/client/src/components/support/StaffDashboard.tsx +++ b/client/src/components/support/StaffDashboard.tsx @@ -14,6 +14,7 @@ import { capitalizeFirstLetter } from "../util/HelperFunctions"; import { getPrettySupportTicketCategory } from "../../utils/supportHelpers"; import { useNotifications } from "../../context/NotificationContext"; import CopyButton from "../util/CopyButton"; +import { Link } from "react-router-dom"; const AssignTicketModal = lazy(() => import("./AssignTicketModal")); const SupportCenterSettingsModal = lazy( () => import("./SupportCenterSettingsModal") @@ -190,10 +191,6 @@ const StaffDashboard = () => { } } - function openTicket(uuid: string) { - window.open(`/support/ticket/${uuid}`, "_blank"); - } - function openAssignModal(ticketId: string) { setSelectedTicketId(ticketId); setShowAssignModal(true); @@ -435,7 +432,9 @@ const StaffDashboard = () => { )} - {ticket.guest && - `${ticket.guest.firstName} ${ticket.guest.lastName} (${ticket.guest.email})`} + {ticket.guest && ( +
+ {ticket.guest.firstName} {ticket.guest.lastName} ({ticket.guest.email}) + + +
)}

Category:{" "}