Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support #352

Merged
merged 5 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -82,6 +84,7 @@ const ManageUserModal: React.FC<ManageUserModalProps> = ({

// Hooks and Error Handling
const { handleGlobalError } = useGlobalError();
const { addNotification } = useNotifications();
const { control, formState, reset, watch, getValues, setValue } =
useForm<CentralIdentityUser>({
defaultValues: {
Expand Down Expand Up @@ -536,17 +539,23 @@ const ManageUserModal: React.FC<ManageUserModalProps> = ({
<p>
<strong>UUID: </strong>
{getValues("uuid")}
<Icon
name="copy"
color="blue"
className="pl-2p"
style={{ cursor: "pointer" }}
onClick={async () => {
await copyToClipboard(
getValues("uuid") ?? "Unknown"
);
}}
/>
<CopyButton val={getValues("uuid") ?? "unknown"}>
{({ copied, copy }) => (
<Icon
name="copy"
color={copied ? "green" : "blue"}
className="pl-2p"
style={{ cursor: "pointer" }}
onClick={() => {
copy();
addNotification({
message: "UUID copied to clipboard!",
type: "success",
});
}}
/>
)}
</CopyButton>
</p>
</div>
<div className="flex-row-div mb-2p">
Expand All @@ -562,16 +571,21 @@ const ManageUserModal: React.FC<ManageUserModalProps> = ({
<strong>Time of Last Access: </strong>
{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"}
</p>
</div>
<div className="flex-row-div">
<p>
<strong>Time of Last Password Change: </strong>
{getValues("last_password_change") ?? "Never"}
{getValues("last_password_change") ?
format(
parseISO(getValues("last_password_change") as string),
"MM/dd/yyyy hh:mm aa"
) : "Unknown"
}
</p>
</div>
</div>
Expand Down
32 changes: 26 additions & 6 deletions client/src/components/support/StaffDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ 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";
import { Link } from "react-router-dom";
const AssignTicketModal = lazy(() => import("./AssignTicketModal"));
const SupportCenterSettingsModal = lazy(
() => import("./SupportCenterSettingsModal")
Expand All @@ -25,7 +28,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<number>(1);
const [activeSort, setActiveSort] = useState<string>("opened");
Expand Down Expand Up @@ -186,10 +191,6 @@ const StaffDashboard = () => {
}
}

function openTicket(uuid: string) {
window.open(`/support/ticket/${uuid}`, "_blank");
}

function openAssignModal(ticketId: string) {
setSelectedTicketId(ticketId);
setShowAssignModal(true);
Expand Down Expand Up @@ -390,7 +391,24 @@ const StaffDashboard = () => {
{!isFetching &&
openTickets?.map((ticket) => (
<Table.Row key={ticket.uuid}>
<Table.Cell>{ticket.uuid.slice(-7)}</Table.Cell>
<Table.Cell>{ticket.uuid.slice(-7)}
<CopyButton val={ticket.uuid}>
{({ copied, copy }) => (
<Icon name="copy"
className="cursor-pointer !ml-1"
onClick={() => {
copy()
addNotification({
message: "Ticket ID copied to clipboard",
type: "success",
duration: 2000,
});
}}
color={copied ? "green" : "blue"}
/>
)}
</CopyButton>
</Table.Cell>
<Table.Cell>
{format(parseISO(ticket.timeOpened), "MM/dd/yyyy hh:mm aa")}
</Table.Cell>
Expand All @@ -414,7 +432,9 @@ const StaffDashboard = () => {
<Button
color="blue"
size="tiny"
onClick={() => openTicket(ticket.uuid)}
to={`/support/ticket/${ticket.uuid}`}
target="_blank"
as={Link}
>
<Icon name="eye" />
View
Expand Down
17 changes: 14 additions & 3 deletions client/src/components/support/TicketDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button } from "semantic-ui-react";
import { Button, Label } from "semantic-ui-react";
import { SupportTicket } from "../../types";
import { format, parseISO } from "date-fns";
import { getPrettySupportTicketCategory } from "../../utils/supportHelpers";
Expand Down Expand Up @@ -46,8 +46,19 @@ const TicketDetails: React.FC<TicketDetailsProps> = ({ ticket }) => {
</Button>
</>
)}
{ticket.guest &&
`${ticket.guest.firstName} ${ticket.guest.lastName} (${ticket.guest.email})`}
{ticket.guest && (
<div className="flex flex-row justify-center ml-1 text-xl">
{ticket.guest.firstName} {ticket.guest.lastName} ({ticket.guest.email})

<Label
className="!ml-2 !p-2 !cursor-default"
basic
color="yellow"
size="mini"
>
Guest
</Label>
</div>)}
</div>
<p className="2xl:text-xl">
<span className="font-semibold">Category:</span>{" "}
Expand Down
2 changes: 1 addition & 1 deletion client/src/providers/NotificationsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const NotificationsProvider = ({ children }: { children: React.ReactNode }) => {
}}
>
{children}
<div className="!fixed bottom-16 right-0 p-4">
<div className="!fixed bottom-16 right-0 p-4 z-[9999]">
{notifications.map((notification) => (
<div
key={notification.id}
Expand Down
25 changes: 13 additions & 12 deletions client/src/screens/conductor/support/Ticket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
Expand Down Expand Up @@ -80,7 +81,7 @@ const SupportTicketView = () => {
});

const updateTicketPriorityMutation = useMutation({
mutationFn: (priority: "high" | "medium" | "low") =>
mutationFn: (priority: SupportTicketPriority) =>
updateTicket({ priority }),
onSuccess: () => {
queryClient.invalidateQueries(["ticket", id]);
Expand Down Expand Up @@ -142,7 +143,7 @@ const SupportTicketView = () => {
autoCloseSilenced,
}: {
status?: "open" | "in_progress" | "closed";
priority?: "high" | "medium" | "low";
priority?: SupportTicketPriority;
autoCloseSilenced?: boolean;
}) {
try {
Expand Down Expand Up @@ -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<SupportTicketPriority, number> = {
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]);

Expand Down
4 changes: 3 additions & 1 deletion client/src/types/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 13 additions & 3 deletions client/src/utils/supportHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ import { GenericKeyTextValueObj, User } from "../types";
export const SupportTicketPriorityOptions: GenericKeyTextValueObj<string>[] = [
{
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<string>[] = [
Expand All @@ -24,6 +29,11 @@ export const SupportTicketCategoryOptions: GenericKeyTextValueObj<string>[] = [
text: "General Inquiry",
value: "general",
},
{
key: "adaptcode",
text: "ADAPT Access Code Request",
value: "adaptcode",
},
{
key: "technical",
text: "Technical Issue (Bug, Error, etc.)",
Expand Down
6 changes: 4 additions & 2 deletions server/api/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>',
to: recipientAddresses,
Expand All @@ -800,8 +801,9 @@ const sendSupportTicketCreateInternalNotification = (recipientAddresses, ticketI
<p><strong>Author:</strong> ${ticketAuthor}</p>
<p><strong>Category:</strong> ${ticketCategory}</p>
<p><strong>Priority:</strong> ${ticketPriority}</p>
${capturedURL ? `<p><strong>Related URL:</strong> ${capturedURL}</p>` : ''}
<br />
<p><strong>Body:</strong> ${ticketBody}</p>
<p><strong>Description:</strong> ${ticketBody}</p>
<br />
<p>You can view the ticket at <a href="https://commons.libretexts.org/support/ticket/${ticketID}" target="_blank" rel="noopener noreferrer">https://commons.libretexts.org/support/ticket/${ticketID}</a>.</p>
<p>Sincerely,</p>
Expand Down
13 changes: 7 additions & 6 deletions server/api/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};
Expand Down
8 changes: 5 additions & 3 deletions server/api/validators/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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(),
Expand All @@ -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(),
}),
Expand All @@ -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(),
}),
});

Expand Down
Loading
Loading