From 84dd981e16b2cccdb775b7d16c53c5dbcdd29937 Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Wed, 15 May 2024 23:15:27 -0700 Subject: [PATCH 1/3] fix(SupportCenter): render multiline comments --- .../src/components/support/TicketComment.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/src/components/support/TicketComment.tsx b/client/src/components/support/TicketComment.tsx index 30af1fed..5699a51a 100644 --- a/client/src/components/support/TicketComment.tsx +++ b/client/src/components/support/TicketComment.tsx @@ -15,6 +15,21 @@ interface TicketCommentProps { } const TicketComment: React.FC = ({ msg }) => { + const formatMessage = (message: string) => { + return message.split("\n").map((line, index) => { + // ignore trailing empty line + if (index === message.split("\n").length - 1 && line === "") { + return null; + } + return ( + + {line} +
+
+ ); + }); + }; + return ( = ({ msg }) => {

- {msg.message} + + {formatMessage(msg.message)} +
); From 01c140857700b4363dc9dc11ff9d3c4978e518d2 Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Wed, 15 May 2024 23:54:59 -0700 Subject: [PATCH 2/3] feat(Support): subscribe all commenters --- server/api/mail.js | 4 +- server/api/support.ts | 122 ++++++++++++++++++++++++++++++++---------- 2 files changed, 97 insertions(+), 29 deletions(-) diff --git a/server/api/mail.js b/server/api/mail.js index d9b6a562..fcf7495e 100644 --- a/server/api/mail.js +++ b/server/api/mail.js @@ -826,7 +826,7 @@ const sendNewTicketMessageNotification = (recipientAddresses, ticketID, message, subject: `New Message on Support Ticket (ID #${ticketID.slice(-7)})`, html: `

Hi,

-

A new message has been posted to your support ticket.

+

A new message has been posted a support ticket you have subscribed to.

${messageSender} said:

${message}

You can respond to this message at https://commons.libretexts.org/support/ticket/${ticketID}${params ? `?${params}` : ''}.

@@ -882,7 +882,7 @@ const sendNewInternalTicketMessageAssignedStaffNotification = (recipientAddresse subject: `New Internal Message on Support Ticket (P: ${priority}) (ID #${ticketID.slice(-7)})`, html: `

Hi,

-

A new internal message has been posted to a support ticket you are assigned to: "${subject}"

+

A new internal message has been posted to a support ticket you have subscribed to: "${subject}"


${messageSender} said:

${message}

diff --git a/server/api/support.ts b/server/api/support.ts index 4d933180..327d06a7 100644 --- a/server/api/support.ts +++ b/server/api/support.ts @@ -48,6 +48,7 @@ import { ZodReqWithFiles } from "../types/Express"; import { getSignedUrl } from "@aws-sdk/cloudfront-signer"; import base64 from "base-64"; import Organization from "../models/organization.js"; +import auth from "./auth"; export const SUPPORT_FILES_S3_CLIENT_CONFIG: S3ClientConfig = { credentials: { @@ -852,42 +853,75 @@ async function createGeneralMessage( type: "general", }); - // If user was found and user is not the ticket author, send a notification to the ticket author - if (foundUser && foundUser.uuid !== ticket.userUUID) { - const emailToNotify = await _getTicketAuthorEmail(ticket); - if (!emailToNotify) return conductor500Err(res); + // Notify all users who have previously commented on the ticket, are assigned to the ticket, or the ticket author + const previousCommenters = await SupportTicketMessage.find({ + ticket: ticket.uuid, + }).distinct("senderUUID"); + + const allUUIDs = [ + ...new Set([ + ...(ticket.assignedUUIDs ?? []), + ...previousCommenters, + ticket.userUUID, + ]), + ]; + + const emailsToNotify = await _getEmails(allUUIDs); + + // Remove the comment author from the list of emails to notify + if (foundUser && foundUser.uuid) { + const index = emailsToNotify.indexOf(foundUser.email); + if (index > -1) { + emailsToNotify.splice(index, 1); + } + } + + // If the user was not found and the ticket has a guest, assume the guest is the comment author + // if (!foundUser && ticket.guest?.email) { + // const index = emailsToNotify.indexOf(ticket.guest.email); + // if (index > -1) { + // emailsToNotify.splice(index, 1); + // } + // } + + // Filter null or undefined emails + const filteredEmails = emailsToNotify.filter((e) => e); + if (!filteredEmails) return conductor500Err(res); + + // If the ticket has a guest, but the commenter was found, send a notification to the guest + // otherwise, we assume the commenter is the guest + const senderName = foundUser + ? `${foundUser?.firstName} ${foundUser?.lastName}` + : `${ticket.guest?.firstName} ${ticket.guest?.lastName}`; + if (ticket.guest?.email && foundUser?.uuid) { const params = new URLSearchParams(); params.append("accessKey", ticket.guestAccessKey); - const addParams = !foundUser?.uuid ? true : false; // if guest, append access key to ticket path - - const senderName = `${foundUser.firstName} ${foundUser.lastName}`; await mailAPI.sendNewTicketMessageNotification( - [emailToNotify], + [ticket.guest.email], ticket.uuid, message, senderName, - addParams ? params.toString() : "" + params.toString() ); } - // If user was found and user is the ticket author, send a notification to assigned staff - if ((foundUser && foundUser.uuid === ticket.userUUID) || !foundUser) { - const teamToNotify = await _getAssignedStaffEmails(ticket.assignedUUIDs); - if (teamToNotify.length > 0) { - await mailAPI.sendNewTicketMessageAssignedStaffNotification( - teamToNotify, - ticket.uuid, - message, - foundUser - ? `${foundUser.firstName} ${foundUser.lastName}` - : `${ticket.guest?.firstName} ${ticket.guest?.lastName}`, - capitalizeFirstLetter(ticket.priority), - ticket.title - ); - } + // If no other emails to notify, return (ie ticket author commented on their own ticket before ticket was assigned to staff) + if (filteredEmails.length === 0) { + return res.send({ + err: false, + message: ticketMessage, + }); } + await mailAPI.sendNewTicketMessageNotification( + filteredEmails, + ticket.uuid, + message, + senderName, + "" + ); + return res.send({ err: false, message: ticketMessage, @@ -945,8 +979,20 @@ async function createInternalMessage( type: "internal", }); - const allTeamEmails = await _getAssignedStaffEmails(ticket.assignedUUIDs); - const teamToNotify = allTeamEmails.filter((e) => e !== foundUser.email); // remove the sender from the list of emails to notify + const previousCommenters = await SupportTicketMessage.find({ + ticket: ticket.uuid, + }).distinct("senderUUID"); + + const allUUIDs = [ + ...new Set([ + ...(ticket.assignedUUIDs ?? []), + ...previousCommenters, + ticket.userUUID, + ]), + ]; + + const emailsToNotify = await _getEmails(allUUIDs, true); // only notify staff + const teamToNotify = emailsToNotify.filter((e) => e !== foundUser.email); // remove the sender from the list of emails to notify if (teamToNotify.length > 0) { await mailAPI.sendNewInternalTicketMessageAssignedStaffNotification( teamToNotify, @@ -954,7 +1000,7 @@ async function createInternalMessage( message, foundUser ? `${foundUser.firstName} ${foundUser.lastName}` - : `${ticket.guest?.firstName} ${ticket.guest?.lastName}`, + : "Unknown Commenter", capitalizeFirstLetter(ticket.priority), ticket.title ); @@ -1065,6 +1111,28 @@ const _getTicketAuthorEmail = async ( return undefined; }; +const _getEmails = async ( + uuids: string[], + staffOnly = false +): Promise => { + try { + if (!uuids || uuids.length === 0) return []; + const users = await User.find({ + uuid: { $in: uuids }, + }); + + if (staffOnly) { + return users + .filter((u) => auth.checkHasRole(u, "libretexts", "support", true)) + .map((u) => u.email); + } + + return users.map((u) => u.email); + } catch (err) { + throw err; + } +}; + const _getTicketAuthorString = ( emailToNotify: string, foundUser?: UserInterface, From 40226b6f23f4c147c459764a805e53394e744002 Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Thu, 16 May 2024 00:05:12 -0700 Subject: [PATCH 3/3] feat(Support): minor improvements --- .../src/components/support/StaffDashboard.tsx | 2 +- client/src/components/support/TicketFeed.tsx | 2 +- server/api/support.ts | 20 +++++++++++++++++-- server/api/validators/support.ts | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/client/src/components/support/StaffDashboard.tsx b/client/src/components/support/StaffDashboard.tsx index af4f89dc..f7864d8c 100644 --- a/client/src/components/support/StaffDashboard.tsx +++ b/client/src/components/support/StaffDashboard.tsx @@ -275,7 +275,7 @@ const StaffDashboard = () => { title="Open/In Progress Tickets" /> = ({ ticket }) => { return (
-
+

Activity Feed

{ticket.feed?.length === 0 && ( diff --git a/server/api/support.ts b/server/api/support.ts index 327d06a7..92b65812 100644 --- a/server/api/support.ts +++ b/server/api/support.ts @@ -412,11 +412,27 @@ async function assignTicket( .select("firstName lastName") .orFail(); + const ticket = await SupportTicket.findOne({ uuid }).orFail(); + + if (!assigned || assigned.length === 0) { + // If no assignees, remove all assignees and set status to open + await SupportTicket.updateOne( + { uuid }, + { + assignedUUIDs: [], + status: "in_progress", + } + ).orFail(); + + return res.send({ + err: false, + ticket, + }); + } + const assignees = await User.find({ uuid: { $in: assigned } }).orFail(); const assigneeEmails = assignees.map((a) => a.email); - const ticket = await SupportTicket.findOne({ uuid }).orFail(); - // Check that ticket is open or in progress if (!["open", "in_progress"].includes(ticket.status)) { return res.status(400).send({ diff --git a/server/api/validators/support.ts b/server/api/validators/support.ts index 32c780d6..a73a6ee8 100644 --- a/server/api/validators/support.ts +++ b/server/api/validators/support.ts @@ -76,7 +76,7 @@ export const GetClosedTicketsValidator = z.object({ export const AssignTicketValidator = z .object({ body: z.object({ - assigned: z.array(z.string().uuid()).min(1).max(25), + assigned: z.array(z.string().uuid()).min(0).max(25), }), }) .merge(TicketUUIDParams);