Files
kan/apps/web/src/views/board/components/CardContextMoveListModal.tsx
Charity 3a44957662 feat: add card context menu and duplication functionality (#381)
* 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
2026-04-01 14:32:56 +01:00

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>
);
}