Skip to content

Commit 44686b9

Browse files
committed
fix(support): stop unnecessary assignable user fetching
1 parent 27eea58 commit 44686b9

8 files changed

Lines changed: 102 additions & 41 deletions

File tree

client/src/components/support/BulkChangeModal.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useSupportCenterContext } from "../../context/SupportCenterContext";
22
import useSupportQueues from "../../hooks/useSupportQueues";
3-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4-
import { User } from "../../types";
3+
import useAssignableUsers from "../../hooks/useAssignableUsers";
4+
import { useMutation, useQueryClient } from "@tanstack/react-query";
55
import api from "../../api";
66
import { useForm } from "react-hook-form";
77
import {
@@ -49,15 +49,7 @@ const BulkChangeModal: React.FC<BulkChangeModalProps> = ({
4949
withCount: false,
5050
});
5151

52-
const { data: assignableUsers } = useQuery<
53-
Pick<User, "uuid" | "firstName" | "lastName" | "email" | "avatar">[]
54-
>({
55-
queryKey: ["assignableUsers"],
56-
queryFn: async () => {
57-
const res = await api.getSupportAssignableUsers();
58-
return res.data.users;
59-
},
60-
});
52+
const { data: assignableUsers } = useAssignableUsers({ enabled: open });
6153

6254
useEffect(() => {
6355
if (open) {

client/src/components/support/QueueAutoAssignmentModal.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { IconCheck, IconX } from "@tabler/icons-react";
1111
import api from "../../api";
1212
import { useNotifications } from "../../context/NotificationContext";
1313
import { useSupportCenterContext } from "../../context/SupportCenterContext";
14-
import { SupportQueueAutoAssignConfig, User } from "../../types";
14+
import useAssignableUsers from "../../hooks/useAssignableUsers";
15+
import { SupportQueueAutoAssignConfig } from "../../types";
1516

1617
interface QueueAutoAssignmentModalProps {
1718
open: boolean;
@@ -39,16 +40,8 @@ const QueueAutoAssignmentModal: React.FC<QueueAutoAssignmentModalProps> = ({
3940
enabled: open,
4041
});
4142

42-
const { data: assignableUsers, isLoading: isAssignableUsersLoading } = useQuery<
43-
Pick<User, "uuid" | "firstName" | "lastName" | "email" | "avatar">[]
44-
>({
45-
queryKey: ["assignableUsers"],
46-
queryFn: async () => {
47-
const res = await api.getSupportAssignableUsers();
48-
return res.data.users;
49-
},
50-
enabled: open,
51-
});
43+
const { data: assignableUsers, isLoading: isAssignableUsersLoading } =
44+
useAssignableUsers({ enabled: open });
5245

5346
// Default the selected queue to the one active in the sidebar, falling back to the first.
5447
useEffect(() => {

client/src/components/support/TicketAssigneePicker.tsx

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { useMutation, useQueryClient } from "@tanstack/react-query";
33
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
44
import { Avatar, Button, Input, Text } from "@libretexts/davis-react";
55
import { IconCheck, IconPlus, IconSearch, IconX } from "@tabler/icons-react";
66
import api from "../../api";
7-
import { User } from "../../types";
7+
import { SupportTicket } from "../../types";
88
import { useNotifications } from "../../context/NotificationContext";
99
import { useTypedSelector } from "../../state/hooks";
1010
import useDebounce from "../../hooks/useDebounce";
11+
import useAssignableUsers, {
12+
AssignableUser,
13+
} from "../../hooks/useAssignableUsers";
1114

1215
interface TicketAssigneePickerProps {
1316
ticketId: string;
@@ -23,11 +26,6 @@ interface TicketAssigneePickerProps {
2326
triggerId?: string;
2427
}
2528

26-
type AssignableUser = Pick<
27-
User,
28-
"uuid" | "firstName" | "lastName" | "email" | "avatar"
29-
>;
30-
3129
const fullName = (u: AssignableUser) => `${u.firstName} ${u.lastName}`;
3230

3331
const TicketAssigneePicker: React.FC<TicketAssigneePickerProps> = ({
@@ -50,12 +48,7 @@ const TicketAssigneePicker: React.FC<TicketAssigneePickerProps> = ({
5048
setSelected(assignedUUIDs ?? []);
5149
}, [assignedUUIDs]);
5250

53-
const { data: assignableUsers } = useQuery<AssignableUser[]>({
54-
queryKey: ["assignableUsers"],
55-
queryFn: async () => {
56-
const res = await api.getSupportAssignableUsers();
57-
return res.data.users;
58-
},
51+
const { data: assignableUsers } = useAssignableUsers({
5952
enabled: !!ticketId && !disabled,
6053
});
6154

@@ -67,8 +60,31 @@ const TicketAssigneePicker: React.FC<TicketAssigneePickerProps> = ({
6760
throw new Error(res.data.errMsg);
6861
}
6962
},
70-
onSuccess: async () => {
71-
await queryClient.invalidateQueries(["ticket", ticketId]);
63+
onSuccess: (_data, assignees) => {
64+
// Patch the cached ticket so the UI settles immediately; the background
65+
// invalidation below reconciles with the server without blocking the user.
66+
// Additions resolve against the cached staff roster (falling back to the
67+
// records already on the ticket) so both chips and any assignedUsers-driven
68+
// UI stay correct even if the refetch is slow or fails.
69+
queryClient.setQueryData<SupportTicket | undefined>(
70+
["ticket", ticketId],
71+
(prev) => {
72+
if (!prev) return prev;
73+
const roster = assignableUsers ?? [];
74+
return {
75+
...prev,
76+
assignedUUIDs: assignees,
77+
assignedUsers: assignees
78+
.map(
79+
(uuid) =>
80+
prev.assignedUsers?.find((u) => u.uuid === uuid) ??
81+
roster.find((u) => u.uuid === uuid)
82+
)
83+
.filter((u): u is AssignableUser => !!u),
84+
};
85+
}
86+
);
87+
queryClient.invalidateQueries(["ticket", ticketId]);
7288
addNotification({
7389
type: "success",
7490
message: "Successfully updated assignees.",
@@ -92,7 +108,7 @@ const TicketAssigneePicker: React.FC<TicketAssigneePickerProps> = ({
92108
mutateRef.current = updateAssignedMutation.mutate;
93109

94110
const debouncedSave = useMemo(
95-
() => debounce((assignees: string[]) => mutateRef.current(assignees), 500),
111+
() => debounce((assignees: string[]) => mutateRef.current(assignees), 250),
96112
[]
97113
);
98114

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { AssignableUser } from "../types";
3+
import api from "../api";
4+
5+
export type { AssignableUser };
6+
7+
/**
8+
* Shared access to the support staff roster. The roster is small and near-static,
9+
* so it is cached aggressively — every surface that offers assignment (ticket
10+
* picker, bulk change, queue auto-assignment) reads the same cache entry instead
11+
* of issuing its own request.
12+
*/
13+
const useAssignableUsers = ({ enabled = true }: { enabled?: boolean } = {}) => {
14+
const QUERY_KEY = ['assignableUsers'];
15+
const queryClient = useQueryClient();
16+
17+
const queryObj = useQuery<AssignableUser[]>({
18+
queryKey: QUERY_KEY,
19+
queryFn: async () => {
20+
const res = await api.getSupportAssignableUsers();
21+
if (res.data.err) {
22+
throw new Error(res.data.errMsg);
23+
}
24+
return res.data.users;
25+
},
26+
enabled,
27+
staleTime: 5 * 60 * 1000, // 5 minutes
28+
cacheTime: 30 * 60 * 1000, // survives navigation between the dashboard and a ticket
29+
refetchOnWindowFocus: false,
30+
meta: {
31+
errorMessage: "Failed to fetch assignable users.",
32+
}
33+
})
34+
35+
const invalidate = () => {
36+
return queryClient.invalidateQueries({ queryKey: QUERY_KEY });
37+
}
38+
39+
return { ...queryObj, QUERY_KEY, invalidate };
40+
}
41+
42+
export default useAssignableUsers

client/src/screens/conductor/support/Ticket.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useDocumentTitle } from "usehooks-ts";
2323
import AuthHelper from "../../../components/util/AuthHelper";
2424
import { Button, Heading, Stack } from "@libretexts/davis-react";
2525
import { IconCheck, IconRefresh, IconTrash } from "@tabler/icons-react";
26+
import useAssignableUsers from "../../../hooks/useAssignableUsers";
2627

2728
const getIdFromURL = (url: string) => {
2829
if (!url) return "";
@@ -62,6 +63,12 @@ const SupportTicketView = () => {
6263
enabled: !!id,
6364
});
6465

66+
// Warm the staff roster cache while the ticket loads so the assignee picker
67+
// has data on hand the first time it is opened.
68+
useAssignableUsers({
69+
enabled: !!id && !!(user.isSupport || user.isHarvester),
70+
});
71+
6572
const updateTicketStatusMutation = useMutation({
6673
mutationFn: (status: "open" | "in_progress" | "closed") =>
6774
updateTicket({ status }),

client/src/types/User.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ export type AuthorizedApp = {
5252
icon: string;
5353
};
5454

55+
/**
56+
* The narrow projection of a user returned by the support staff roster
57+
* (`/support/assignable-users`). Assignment surfaces only ever need identity
58+
* and avatar, so this is the shape they share.
59+
*/
60+
export type AssignableUser = Pick<
61+
User,
62+
"uuid" | "firstName" | "lastName" | "email" | "avatar"
63+
>;
64+
5565
export type UserWCentralID = User & {
5666
centralID?: string;
5767
};

client/src/types/support.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { SupportQueue } from "./supportqueues";
2-
import { User, UserWCentralID } from "./User";
2+
import { AssignableUser, User, UserWCentralID } from "./User";
33

44
export type SupportTicketGuest = {
55
firstName: string;
@@ -30,7 +30,7 @@ export type SupportTicket = {
3030
category?: string;
3131
capturedURL?: string;
3232
assignedUUIDs?: string[]; // User uuids
33-
assignedUsers?: UserWCentralID[];
33+
assignedUsers?: AssignableUser[];
3434
user?: UserWCentralID;
3535
guest?: SupportTicketGuest;
3636
ccedEmails?: {

server/api/support.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,8 @@ async function _getAssignableUsersInternal(): Promise<Pick<UserInterface, "uuid"
459459
],
460460
})
461461
.select("uuid firstName lastName email avatar")
462-
.sort({ firstName: 1 });
462+
.sort({ firstName: 1 })
463+
.lean();
463464

464465
return users;
465466
} catch (err) {

0 commit comments

Comments
 (0)