import Link from "next/link"; import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { HiXMark } from "react-icons/hi2"; import { IoChevronForwardSharp } from "react-icons/io5"; import { authClient } from "@kan/auth/client"; import Avatar from "~/components/Avatar"; import Editor from "~/components/Editor"; import FeedbackModal from "~/components/FeedbackModal"; import { LabelForm } from "~/components/LabelForm"; import LabelIcon from "~/components/LabelIcon"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal"; import { usePermissions } from "~/hooks/usePermissions"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; import { invalidateCard } from "~/utils/cardInvalidation"; import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers"; import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation"; import ActivityList from "./components/ActivityList"; import { AttachmentThumbnails } from "./components/AttachmentThumbnails"; import { AttachmentUpload } from "./components/AttachmentUpload"; import Checklists from "./components/Checklists"; import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation"; import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation"; import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation"; import Dropdown from "./components/Dropdown"; import { DueDateSelector } from "./components/DueDateSelector"; import LabelSelector from "./components/LabelSelector"; import ListSelector from "./components/ListSelector"; import MemberSelector from "./components/MemberSelector"; import { NewChecklistForm } from "./components/NewChecklistForm"; import NewCommentForm from "./components/NewCommentForm"; interface FormValues { cardId: string; title: string; description: string; } export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) { const router = useRouter(); const { canEditCard } = usePermissions(); const { data: session } = authClient.useSession(); const cardId = Array.isArray(router.query.cardId) ? router.query.cardId[0] : router.query.cardId; const { data: card } = api.card.byId.useQuery( { cardPublicId: cardId ?? "" }, { enabled: !!cardId && cardId.length >= 12 }, ); const isCreator = card?.createdBy && session?.user.id === card.createdBy; const canEdit = canEditCard || isCreator; const board = card?.list.board; const labels = board?.labels; const workspaceMembers = board?.workspace.members; const selectedLabels = card?.labels; const selectedMembers = card?.members; const formattedLabels = labels?.map((label) => { const isSelected = selectedLabels?.some( (selectedLabel) => selectedLabel.publicId === label.publicId, ); return { key: label.publicId, value: label.name, selected: isSelected ?? false, leftIcon: , }; }) ?? []; const formattedLists = board?.lists.map((list) => ({ key: list.publicId, value: list.name, selected: list.publicId === card?.list.publicId, })) ?? []; const formattedMembers = workspaceMembers?.map((member) => { const isSelected = selectedMembers?.some( (assignedMember) => assignedMember.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 ?? false, leftIcon: ( ), }; }) ?? []; return (

{t`List`}

{t`Labels`}

{!isTemplate && (

{t`Members`}

)}

{t`Due date`}

); } export default function CardPage({ isTemplate }: { isTemplate?: boolean }) { const router = useRouter(); const utils = api.useUtils(); const { modalContentType, entityId, getModalState, clearModalState, isOpen, modalStates, } = useModal(); const { showPopup } = usePopup(); const { workspace } = useWorkspace(); const { canEditCard } = usePermissions(); const { data: session } = authClient.useSession(); const [activeChecklistForm, setActiveChecklistForm] = useState( null, ); const cardId = Array.isArray(router.query.cardId) ? router.query.cardId[0] : router.query.cardId; const { data: card, isLoading, error } = api.card.byId.useQuery( { cardPublicId: cardId ?? "" }, { enabled: !!cardId && cardId.length >= 12 }, ); // Redirect to 404 if card doesn't exist useEffect(() => { if (router.isReady && cardId && !isLoading) { if (error?.data?.code === "NOT_FOUND" || (!card && !isLoading)) { router.replace("/404"); } } }, [router, cardId, isLoading, error, card]); const isCreator = card?.createdBy && session?.user.id === card.createdBy; const canEdit = canEditCard || isCreator; const refetchCard = async () => { if (cardId) await utils.card.byId.refetch({ cardPublicId: cardId }); }; const board = card?.list.board; const workspaceMembers = board?.workspace.members; const boardId = board?.publicId; const editorWorkspaceMembers = workspaceMembers ?.filter((member) => member.email) .map((member) => ({ publicId: member.publicId, email: member.email, user: member.user ? { id: member.user.id, name: member.user.name ?? null, image: member.user.image ?? null, } : null, })) ?? []; const updateCard = api.card.update.useMutation({ onError: () => { showPopup({ header: t`Unable to update card`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, onSettled: async () => { if (cardId) await invalidateCard(utils, cardId); }, }); const addOrRemoveLabel = api.card.addOrRemoveLabel.useMutation({ onError: () => { showPopup({ header: t`Unable to add label`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, onSettled: async () => { if (cardId) { await utils.card.byId.invalidate({ cardPublicId: cardId }); } }, }); const { register, handleSubmit, setValue, watch } = useForm({ values: { cardId: cardId ?? "", title: card?.title ?? "", description: card?.description ?? "", }, }); const onSubmit = (values: FormValues) => { updateCard.mutate({ cardPublicId: values.cardId, title: values.title, description: values.description, }); }; // this adds the new created label to selected labels useEffect(() => { const newLabelId = modalStates.NEW_LABEL_CREATED; if (newLabelId && cardId) { const isAlreadyAdded = card?.labels.some( (label) => label.publicId === newLabelId, ); if (!isAlreadyAdded) { addOrRemoveLabel.mutate({ cardPublicId: cardId, labelPublicId: newLabelId, }); } clearModalState("NEW_LABEL_CREATED"); } }, [modalStates.NEW_LABEL_CREATED, card, cardId]); // Open the new item form after creating a new checklist useEffect(() => { if (!card) return; const state = getModalState("ADD_CHECKLIST"); const createdId: string | undefined = state?.createdChecklistId; if (createdId) { setActiveChecklistForm(createdId); clearModalState("ADD_CHECKLIST"); } }, [card, getModalState, clearModalState]); // Auto-resize title textarea useEffect(() => { const titleTextarea = document.getElementById( "title", ) as HTMLTextAreaElement; if (titleTextarea) { titleTextarea.style.height = "auto"; titleTextarea.style.height = `${titleTextarea.scrollHeight}px`; } }, [card]); if (!cardId) return <>; return ( <>
{/* Full-width top strip with board link and dropdown */}
{!card && isLoading && (
)} {card && ( <>
{workspace.name} {board?.name}
)} {!card && !isLoading && (

{t`Card not found`}

)}
{!card && isLoading && (
)} {card && (