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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | 1x 1x 1x 1x 1x 98x 98x 98x 98x 98x 98x 24x 98x 98x 98x 98x 98x 89x 98x | import { useCallback, useEffect, useState } from "react";
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import { Stack } from "@mui/material";
import GiveBaseModal from "@shared/modals/GiveBaseModal";
import GiveSearchBar from "@shared/SearchBar/GiveSearchBar";
import { useTeamPermissions } from "@features/Permissions/AccessControl/hooks";
import { useCustomThemeV2 } from "@theme/hooks/useCustomThemeV2";
import AssignHostModalBody from "./AssignHostModalBody";
import { useAssignableHosts } from "./useAssignableHosts";
export const ASSIGN_HOST_TITLE = "Assign Host";
/**
* The header is absolutely positioned at 64px and the modal has no footer, so
* the content's own top inset clears it and the mockup's 20px gutter does the
* rest (search at y84, list at y144).
*/
const CONTENT_PADDING = "84px 20px 20px";
/** The mockup's states, from the tallest list down to the message-only case. */
const DESKTOP_WIDTH = "560px";
const LIST_HEIGHT = "560px";
type Props = {
eventId?: string;
};
/**
* "Assign Host" (frames 8181-61190, 8136-65947, 8122-59649, 8119-113344).
*
* The merchant's existing hosts, with this event's already-assigned ones at the
* top. One control per row does both directions — see AssignHostRow — so this
* modal is where an event's roster is both grown and trimmed, and it commits on
* click rather than behind a Save: there is no footer to press.
*/
const AssignHostModal = NiceModal.create(({ eventId }: Props) => {
const modal = useModal();
const { isMobileView } = useCustomThemeV2();
const [search, setSearch] = useState("");
// Assigning an event to a host writes the member's assignment set, which is
// the same `member:access-policy update` Settings → Team's per-member event
// list is gated on.
const { isUpdateAccessPolicyAllowed, isListAllowed } = useTeamPermissions();
const {
rows,
isLoading,
isRefetching,
isError,
isEmpty,
toggleAssignment,
pendingId,
} = useAssignableHosts(eventId, search, {
// The bench is a member-list read; without `member:list` there is nothing
// to ask for.
enabled: modal.visible && isListAllowed,
});
// NiceModal keeps the component mounted across hide/show, so without this the
// previous visit's query greets the next open.
useEffect(() => {
if (modal.visible) setSearch("");
}, [modal.visible]);
const clearSearch = useCallback(() => setSearch(""), []);
const hasQuery = Boolean(search.trim());
// Frame 8119-113344 — with nothing on the bench there is nothing to search,
// so the field goes with the list and the modal collapses to its message.
const showSearch = !isEmpty && !isError && isListAllowed;
// The list scrolls inside a definite height rather than growing the modal:
// the mockup's list state on desktop, the whole sheet on mobile (where the
// paper is given its height below). The message-only state opts out, which is
// what makes it the short modal in frame 8119-113344.
const getContentHeight = () => {
if (!showSearch) return undefined;
return isMobileView ? "100%" : LIST_HEIGHT;
};
return (
<GiveBaseModal
open={modal.visible}
title={ASSIGN_HOST_TITLE}
width={DESKTOP_WIDTH}
// Only honoured on mobile, where the mockup's sheet runs to the bottom of
// the screen whichever state it is in (frames 8124-64185 / 8124-60544).
height={isMobileView ? "100%" : undefined}
showFooter={false}
customContentPadding={CONTENT_PADDING}
onClose={modal.hide}
// `hide` only closes it; the unmount is what clears the search and the
// scroll position for the next open.
TransitionProps={{ onExited: () => modal.remove() }}
>
<Stack gap="24px" height={getContentHeight()}>
{showSearch && (
<GiveSearchBar
value={search}
handleChange={setSearch}
placeholder="Search"
// The modal unmounts on exit, so the bar's own reset-on-close would
// fire a redundant change into an unmounting tree.
resetOnClose={false}
/>
)}
{/* The list takes the remaining height — to scroll in, or to centre its
empty states in. */}
<Stack
flex={1}
minHeight={0}
justifyContent={hasQuery ? "center" : "flex-start"}
// `keepPreviousData` keeps the previous answer on screen while the
// next one loads, so without this a search reads as having done
// nothing. Dimmed rather than replaced by a spinner, which would
// flash the list away on every keystroke.
sx={{
opacity: isRefetching ? 0.5 : 1,
transition: "opacity 150ms ease",
pointerEvents: isRefetching ? "none" : undefined,
}}
>
<AssignHostModalBody
isError={isError}
isLoading={isLoading}
isListAllowed={isListAllowed}
isEmpty={isEmpty}
rows={rows}
search={search}
onClearSearch={clearSearch}
pendingId={pendingId}
canAssign={isUpdateAccessPolicyAllowed}
onToggle={toggleAssignment}
/>
</Stack>
</Stack>
</GiveBaseModal>
);
});
export default AssignHostModal;
|