Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 10x 10x 10x 10x 10x 63x 63x 63x 63x 102x 63x 102x 63x 63x 63x 63x 4x 3x 3x 3x 3x 3x 3x 3x 3x 63x 5x 4x 4x 63x | import { useIsMutating, useMutation, useQueryClient } from "react-query";
import { showMessage } from "@common/Toast";
import { QKEY_EVENT_TICKETS } from "@constants/queryKeys";
import { useAppSelector } from "@redux/hooks";
import { sortingKey } from "@redux/slices/fundraisers";
import { selectQueryString } from "@redux/slices/search";
import { customInstance } from "@services/api";
import { buildEventTicketsExportUrl } from "@services/api/products/tickets";
import { useFormattedFilters } from "@shared/GiveFilter/hooks/useFormattedFilters";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import { DEFAULT_SORTING } from "./useEventTickets";
/** Toast copy is spec — frame 7423-217395. */
export const EXPORT_TOAST_TITLE = "Exporting guest list";
export const EXPORT_TOAST_BODY =
"Your export is being prepared. This may take a few minutes.";
const EXPORT_FILENAME = "tickets.csv";
const EXPORT_MUTATION_KEY = "event-tickets-export";
/**
* Downloads `.../ticket-holders.csv` under the list's own sort, search and filter.
*
* A hook rather than logic inside the button, because the desktop toolbar, the
* mobile overflow sheet (frame 7397-90578) and the compact banner's (⋮) all
* trigger it. The in-flight state is a react-query mutation keyed on the event,
* so those instances share one `isExporting` — a separate `useState` per
* instance let two of them fire the same export at once, and left a setState to
* land after the compact banner had been unmounted by a scroll back up.
*
* A synchronous download — the endpoint streams the CSV, and the
* `Content-Disposition` filename comes from the server. The frame's toast fires on
* click, which is honest either way: for a large event the count-then-stream
* request really can take a while. If product wants a true background job (a
* queued task that emails a link, the way `transactions.csv?async=true` works),
* that is a server-side change — this would then show the same toast and poll
* instead of holding the request open.
*/
export const useTicketsExport = (eventId?: string) => {
const { isMobileView } = useCustomThemeV2();
const queryClient = useQueryClient();
const mutationKey = [EXPORT_MUTATION_KEY, eventId];
// Read the sort, search and filter from the same redux state `useEventTickets`
// reads, so the file can never drift from the list on screen.
const searchQuery = useAppSelector((state) =>
selectQueryString(state, QKEY_EVENT_TICKETS),
);
const storedSorting = useAppSelector((state) =>
sortingKey(state, QKEY_EVENT_TICKETS),
);
const sorting = storedSorting || DEFAULT_SORTING;
const { formattedFilterString } = useFormattedFilters({
queryKey: QKEY_EVENT_TICKETS,
});
const isExporting = useIsMutating(mutationKey) > 0;
const { mutate } = useMutation(
mutationKey,
async () => {
const data = await customInstance({
method: "GET",
url: buildEventTicketsExportUrl(eventId as string, {
sorting,
searchQuery,
filter: formattedFilterString,
}),
// Without this axios defaults to "json" and the CSV only survives
// because JSON.parse fails — a coincidentally-parseable body would
// arrive as an object and corrupt the file.
responseType: "blob",
});
const href = window.URL.createObjectURL(new Blob([data]));
const link = document.createElement("a");
link.href = href;
link.setAttribute("download", EXPORT_FILENAME);
document.body.appendChild(link);
link.click();
Eif (document.body.contains(link)) document.body.removeChild(link);
window.URL.revokeObjectURL(href);
},
// The axios interceptor already surfaces the failure toast; the mutation
// only has to not reject unhandled.
{ onError: () => undefined },
);
const exportTickets = () => {
// Read the cache rather than the rendered flag: that one only catches up on
// the cache's next notification, so two clicks in one tick both pass it.
if (!eventId || queryClient.isMutating({ mutationKey }) > 0) return;
showMessage("Info", EXPORT_TOAST_BODY, !isMobileView, EXPORT_TOAST_TITLE);
mutate();
};
return { exportTickets, isExporting };
};
|