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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | 12x 204x 204x 204x 204x 204x 204x 204x 204x 204x 204x 204x 204x 2x 2x 204x 204x 204x 1x 204x 204x 204x | import NiceModal from "@ebay/nice-modal-react";
import {
ArrowUpRightIcon,
EyeIcon,
PencilSimpleIcon,
ShareFatIcon,
TrashIcon,
} from "@phosphor-icons/react";
import { useNavigate } from "react-router-dom";
import { showMessage } from "@common/Toast";
import { useDeleteMutation } from "@components/common/Modal/DeleteModal/useDeleteMutation";
import { usePublicFormUrl } from "@components/DonationList/DonationsBanner";
import { DELETE_DENY_MESSAGE, EDIT_DENY_MESSAGE } from "@constants/permissions";
import {
QKEY_GET_CAMPAGIN_STATS,
QKEY_GET_PRODUCT_TYPES,
QKEY_LIST_EVENTS,
} from "@constants/queryKeys";
import { DonationType } from "@customTypes/products/fundraiser.types";
import GiveConfirmationPopUp from "@features/Merchants/MerchantSidePanel/Modals/GiveConfirmationPopUp";
import { useProductPermission } from "@features/Permissions/AccessControl/hooks";
import { useAppDispatch } from "@redux/hooks";
import { setSelectedPaymentFormID } from "@redux/slices/products";
import PublicURLShareModal from "@sections/PayBuilder/LaunchStep/PublicURLShare/PublicURLShareModal";
import { useExpandedActions } from "@sections/PayBuilder/hooks/useExpandedActions";
import { buildMerchantEndpoints } from "@services/api/utils.api";
import { ContextualMenuOptionProps } from "@shared/ContextualMenu/ContextualMenu.types";
import { checkEnd } from "@utils/date.helpers";
import GiveText from "@shared/Text/GiveText";
import { EDIT_EVENT_MODAL, NEW_PEEK_MODE_MODAL } from "modals/modal_names";
import { useMenuIcon } from "./useMenuIcon";
/**
* PayBuilder 032-b AC001–AC005 — the five actions behind the (…) button in the
* Event Detail header.
*
* Every action re-uses the flow the legacy `EventsBanner` already shipped, so
* the redesign changes where the actions live, not what they do:
* - Edit Event → PayBuilder, pre-loaded with the event (AC002)
* - Share Event → the existing share modal (AC003)
* - Preview Event Page → the peek-mode preview overlay (AC004.1/AC004.2)
* - Open Live Page → the published page, new tab (AC004.3)
* - Delete Event → the shared delete-confirmation modal (AC005)
*/
export const useEventMenuActions = (
event?: DonationType,
/**
* The compact banner is rendered by `MobileNavBar`, outside the route tree —
* `useParams` gives it no `:id` there, so every action would run on
* `undefined`. Callers that can be mounted outside the route pass it in.
*/
eventIdFromProps?: string,
) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { id: idFromRoute } = usePublicFormUrl();
const id = eventIdFromProps ?? idFromRoute;
// Not the hook's own `composedUrl`: that one is route-derived, and its path is
// assigned in an effect so it reads as bare origin on the first render.
const composedUrl = `${window.location.origin}/${id}`;
const { editPaymentForm } = useExpandedActions(
event as DonationType,
EDIT_EVENT_MODAL,
);
const { isEditProductAllowed, isDeleteProductAllowed } =
useProductPermission();
// An event is editable until it ends, and deletable only once it has.
// `checkEnd` returns 0 when the event has no dates — which reads as "ended"
// and would enable Delete while the event is still LOADING (`event`
// undefined). Guard on the event being present so the safe, blocked state is
// what renders until the data arrives. Edit needs the same guard for the
// opposite reason: "not ended" is what the loading state reads as, and
// `editPaymentForm` must never open PayBuilder on a missing product.
const hasEnded =
Boolean(event) &&
checkEnd(event?.startsAt, event?.endsAt ?? null, event?.includeTime) <
Date.now();
const isEditDisabled = !event || !isEditProductAllowed || hasEnded;
const isDeleteDisabled = !isDeleteProductAllowed || !hasEnded;
// Always the redesigned share modal (frame 7397-80460). The legacy
// `EventsBanner` fell back to the old `MERCHANT_PORTAL_SHARE_MODAL` when the
// typeName failed the rebranded-types check — on this page the subject is an
// event by definition, and that fallback silently served the old design
// whenever the loaded product's typeName didn't match.
const shareEvent = () =>
NiceModal.show(PublicURLShareModal, {
idFromProps: id,
typeFromProps: "event",
});
// AC004.1/AC004.2 — the full-screen peek overlay (Preview Receipt / Open
// Live Page / X), the same one the campaign side panel and the legacy
// banner's Preview button open. Navigating to `/{id}` instead lands the
// merchant in the live customer purchase flow — for an event, the
// ticket-buying page. The dispatched id is what the modal's data hooks read:
// rendered outside the route tree, `useParams` gives them nothing.
const previewEventPage = () => {
dispatch(setSelectedPaymentFormID(String(id)));
NiceModal.show(NEW_PEEK_MODE_MODAL, {
previewLink: `events/${id}`,
// The subject is an event by definition on this page (see shareEvent).
state: { preview: true, campaignType: "event" },
title: event?.name,
// Undefined while the event is still loading — this row has no `!event`
// guard — which drops the overlay onto the default surface rather than
// the form's background. Harmless, and visible to the type checker.
backgroundColor: event?.backgroundColor,
});
};
const openLivePage = () =>
window.open(composedUrl, "_blank", "noopener,noreferrer");
const { handleDelete } = useDeleteMutation({
url: buildMerchantEndpoints(`products/${id}`),
refetchQuerieKeys: [
QKEY_LIST_EVENTS,
QKEY_GET_CAMPAGIN_STATS,
QKEY_GET_PRODUCT_TYPES,
],
onSuccess: () => {
showMessage(
"Success",
<>
Event <span style={{ fontWeight: 600 }}>“{event?.name}”</span> has
been deleted successfully
</>,
);
// AC005.3 — back to the Events list once the event is gone.
navigate("/merchant/events");
},
});
// AC005 / frame 7397-80586 — the rebranded confirmation popup (red trash
// badge, Cancel + "Yes, Delete"), replacing the legacy DeleteConfirmationModal.
// Shown by component reference: the GIVE_CONFIRMATION_POP_UP name is only
// registered for the acquirer portal.
const deleteEvent = () =>
NiceModal.show(GiveConfirmationPopUp, {
modalType: "delete",
title: "Delete Event Confirmation",
description: (
<GiveText variant="bodyS" color="secondary">
Are you sure you want to delete the{" "}
<GiveText
component="span"
variant="bodyS"
color="primary"
sx={{ fontWeight: 500 }}
>
{event?.name}
</GiveText>
? this event will be permanently deleted.
</GiveText>
),
actions: {
handleSuccess: { onClick: handleDelete },
},
});
const withIcon = useMenuIcon();
// AC001.2 — order is part of the acceptance criteria, keep it.
const menuOptions: ContextualMenuOptionProps[] = [
{
text: "Edit Event",
...withIcon(PencilSimpleIcon),
onClick: editPaymentForm,
disabled: isEditDisabled,
tooltipTitle: !isEditProductAllowed ? EDIT_DENY_MESSAGE : undefined,
disableTooltip: isEditProductAllowed,
},
{
text: "Share Event",
...withIcon(ShareFatIcon),
onClick: shareEvent,
},
{
text: "Preview Event Page",
...withIcon(EyeIcon),
onClick: previewEventPage,
},
{
text: "Open Live Page",
...withIcon(ArrowUpRightIcon),
onClick: openLivePage,
},
// Frame 7397-80327 rules off the destructive action on desktop as well.
{ type: "divider" as const, text: "" },
{
// AC001.2 — destructive, and disabled while the event is still running
// (no helper caption under the label: the row shows only "Delete Event").
// The mockups render the blocked row in grey (7397-80327/80885) and only
// the actionable state in red (7397-80966), so the destructive styling
// applies only when enabled.
text: "Delete Event",
type: isDeleteDisabled ? "default" : "destructive",
...withIcon(TrashIcon),
onClick: deleteEvent,
disabled: isDeleteDisabled,
tooltipTitle: !isDeleteProductAllowed ? DELETE_DENY_MESSAGE : undefined,
disableTooltip: isDeleteProductAllowed,
},
];
return {
menuOptions,
editPaymentForm,
isEditDisabled,
// The header's tooltip needs the reason, not just the verdict: Edit can be
// disabled by ACM (deny message applies) or by the event's lifecycle (it
// doesn't).
isEditAllowed: isEditProductAllowed,
hasEnded,
};
};
|