* feat: add card context menu and duplication functionality * Implemented a context menu for cards allowing actions such as copying links, duplicating cards, and managing members, labels, and due dates * Added modals for card duplication and context actions. * Updated API with a new endpoint for duplicating cards, including options for copying labels, members, and checklists * refactor: remove cardPublicId from context menu and related components * Removed cardPublicId prop from CardContextMenu and CardContextMembersModal for cleaner context handling * Updated scrollbar styling * feat: add delete card functionality to context menu
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
import { t } from "@lingui/core/macro";
|
|
|
|
import { useModal } from "~/providers/modal";
|
|
import { usePopup } from "~/providers/popup";
|
|
import { api } from "~/utils/api";
|
|
import { invalidateCard } from "~/utils/cardInvalidation";
|
|
|
|
export function CardContextMoveListModal() {
|
|
const { entityId: cardPublicId, closeModal } = useModal();
|
|
const { showPopup } = usePopup();
|
|
const utils = api.useUtils();
|
|
|
|
const { data: card, isLoading } = api.card.byId.useQuery(
|
|
{ cardPublicId: cardPublicId ?? "" },
|
|
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
|
|
);
|
|
|
|
const updateCardList = api.card.update.useMutation({
|
|
onMutate: async (vars) => {
|
|
if (!cardPublicId) return undefined;
|
|
await utils.card.byId.cancel();
|
|
const previous = utils.card.byId.getData({ cardPublicId });
|
|
utils.card.byId.setData({ cardPublicId }, (old) => {
|
|
if (!old) return old;
|
|
const list = old.list.board?.lists?.find(
|
|
(l) => l.publicId === vars.listPublicId,
|
|
);
|
|
if (!list) return old;
|
|
return {
|
|
...old,
|
|
list: { ...old.list, publicId: list.publicId, name: list.name },
|
|
};
|
|
});
|
|
return { previous };
|
|
},
|
|
onError: (_err, _vars, ctx) => {
|
|
if (cardPublicId && ctx?.previous) {
|
|
utils.card.byId.setData({ cardPublicId }, ctx.previous);
|
|
}
|
|
showPopup({
|
|
header: t`Unable to move card`,
|
|
message: t`Please try again.`,
|
|
icon: "error",
|
|
});
|
|
},
|
|
onSettled: async () => {
|
|
if (cardPublicId) {
|
|
await invalidateCard(utils, cardPublicId);
|
|
await utils.board.byId.invalidate();
|
|
}
|
|
},
|
|
});
|
|
|
|
const lists = card?.list?.board?.lists ?? [];
|
|
const currentListPublicId = card?.list?.publicId;
|
|
|
|
const handleSelectList = (listPublicId: string) => {
|
|
if (listPublicId === currentListPublicId || !cardPublicId) return;
|
|
updateCardList.mutate(
|
|
{ cardPublicId, listPublicId, index: 0 },
|
|
{ onSuccess: closeModal },
|
|
);
|
|
};
|
|
|
|
if (!cardPublicId) return null;
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<h2 className="mb-4 text-lg font-semibold text-light-1000 dark:text-dark-1000">
|
|
{t`Move to list`}
|
|
</h2>
|
|
{isLoading ? (
|
|
<div className="space-y-2">
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300"
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="max-h-[60vh] overflow-y-auto pr-1 scrollbar scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600">
|
|
<ul className="space-y-1">
|
|
{lists.map((list) => (
|
|
<li key={list.publicId}>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSelectList(list.publicId)}
|
|
disabled={list.publicId === currentListPublicId}
|
|
className="w-full rounded-md px-3 py-2 text-left text-sm hover:bg-light-200 disabled:opacity-50 dark:hover:bg-dark-400"
|
|
>
|
|
{list.name}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|