Files
kan/apps/web/src/views/board/components/CardContextMembersModal.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

74 lines
2.3 KiB
TypeScript

import { t } from "@lingui/core/macro";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
import Avatar from "~/components/Avatar";
import MemberSelector from "~/views/card/components/MemberSelector";
export function CardContextMembersModal() {
const { entityId: cardPublicId, closeModal } = useModal();
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardPublicId ?? "" },
{ enabled: !!cardPublicId && cardPublicId.length >= 12 },
);
const board = card?.list?.board;
const workspaceMembers = board?.workspace?.members ?? [];
const selectedMembers = card?.members ?? [];
const formattedMembers = workspaceMembers.map((member) => {
const isSelected = selectedMembers.some(
(m) => m.publicId === member.publicId,
);
return {
key: member.publicId,
value: formatMemberDisplayName(
member.user?.name ?? null,
member.user?.email ?? member.email,
),
imageUrl: member.user?.image ? getAvatarUrl(member.user.image) : undefined,
selected: isSelected,
leftIcon: (
<Avatar
size="xs"
name={member.user?.name ?? ""}
imageUrl={
member.user?.image ? getAvatarUrl(member.user.image) : undefined
}
email={member.user?.email ?? member.email}
/>
),
};
});
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`Manage members`}
</h2>
{isLoading ? (
<div className="h-10 w-full animate-pulse rounded bg-light-200 dark:bg-dark-300" />
) : (
<MemberSelector
cardPublicId={cardPublicId}
members={formattedMembers}
isLoading={isLoading}
/>
)}
<div className="mt-4 flex justify-end">
<button
type="button"
onClick={closeModal}
className="rounded-md border border-light-300 bg-light-50 px-3 py-1.5 text-sm font-medium text-light-1000 hover:bg-light-200 dark:border-dark-400 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300"
>
{t`Done`}
</button>
</div>
</div>
);
}