client/src/itemHoverMenu.tsx (147 lines of code) (raw):
import { css } from "@emotion/react";
import React, { useContext, useEffect } from "react";
import { palette, space } from "@guardian/source-foundations";
import ReplyIcon from "../icons/reply.svg";
import PencilIcon from "../icons/pencil.svg";
import BinIcon from "../icons/bin.svg";
import { useConfirmModal } from "./modal";
import { scrollbarsCss } from "./styling";
import { composer } from "../colours";
import { useMutation } from "@apollo/client";
import { gqlDeleteItem } from "../gql";
import { Item } from "shared/graphql/graphql";
import { PINBOARD_TELEMETRY_TYPE, TelemetryContext } from "./types/Telemetry";
import { useTourProgress } from "./tour/tourState";
import { demoPinboardData } from "./tour/tourConstants";
export const ITEM_HOVER_MENU_CLASS_NAME = "item-hover-menu";
interface ItemHoverMenuProps {
item: Item;
isMutable: boolean;
enterEditMode: () => void;
setMaybeDeleteItemModalElement: (element: JSX.Element | null) => void;
setMaybeReplyingToItemId: (itemId: string | null) => void;
}
export const ItemHoverMenu = ({
item,
isMutable,
enterEditMode,
setMaybeDeleteItemModalElement,
setMaybeReplyingToItemId,
}: ItemHoverMenuProps) => {
const [deleteConfirmModalElement, confirmDelete] = useConfirmModal(
<React.Fragment>
<div
css={css`
font-weight: bold;
`}
>
Are you sure you want
<br /> to delete this item?
</div>
<div
css={css`
padding: ${space[2]}px;
font-style: italic;
max-height: 55px;
overflow-y: auto;
${scrollbarsCss(composer.primary["100"])}
`}
>
{item.message}
</div>
</React.Fragment>
);
useEffect(
() => setMaybeDeleteItemModalElement(deleteConfirmModalElement),
[deleteConfirmModalElement]
);
const [deleteItem] = useMutation(gqlDeleteItem, {
onError: (error) => {
console.error(error);
alert(`failed to delete item`);
},
});
const sendTelemetryEvent = useContext(TelemetryContext);
const tourProgress = useTourProgress();
const onClickDeleteItem = () => {
confirmDelete().then((confirmed) => {
const telemetryPayload = {
pinboardId: item.pinboardId,
itemId: item.id,
};
if (confirmed) {
// TODO show spinner whilst deleting
if (tourProgress.isRunning) {
setTimeout(() => tourProgress.deleteItem(item.id), 50);
} else if (item.pinboardId === demoPinboardData.id) {
throw new Error(
"Demo/Tour NOT running, but delete attempt on 'demo' pinboard"
);
} else {
deleteItem({ variables: { itemId: item.id } });
sendTelemetryEvent?.(
PINBOARD_TELEMETRY_TYPE.DELETE_ITEM,
telemetryPayload
);
}
} else if (!tourProgress.isRunning) {
sendTelemetryEvent?.(
PINBOARD_TELEMETRY_TYPE.CANCEL_DELETE_ITEM,
telemetryPayload
);
}
});
};
return (
<div
className={ITEM_HOVER_MENU_CLASS_NAME}
css={css`
display: none; // :hover in ItemDisplay sets this to 'display: flex'
position: absolute;
top: 0;
right: 0;
background: ${palette.neutral["86"]};
border-radius: 12px;
padding: ${space[1] / 2}px;
gap: ${space[1] / 2}px;
button {
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
line-height: 11px;
width: 18px;
height: 18px;
border: none;
border-radius: 50%;
padding: ${space[1] / 2}px;
cursor: pointer;
background: ${palette.neutral["86"]};
&:hover {
background: ${palette.neutral["60"]};
svg {
fill: ${palette.neutral[10]};
}
}
svg {
fill: ${palette.neutral[20]};
}
}
`}
>
{/*TODO starred messages*/}
<button onClick={() => setMaybeReplyingToItemId(item.id)} title="Reply">
<ReplyIcon />
</button>
{isMutable && (
<>
<button onClick={enterEditMode} title="Edit">
<PencilIcon />
</button>
<button onClick={onClickDeleteItem} title="Delete">
<BinIcon />
</button>
</>
)}
</div>
);
};