* fix: add 404 not found page Closes #320 - Add custom 404 page matching the app's design system - Include PatternedBackground and proper dark mode support - Provide navigation links to homepage and boards - Add i18n support with @lingui/react * fix: add 404 redirects for non-existent boards and cards Add useEffect hooks to redirect to /404 page when: - Board does not exist in board view - Card does not exist in card view This addresses the cases mentioned in maintainer feedback where the static 404 page wasn't being used for dynamic routes. Addresses review feedback on PR #349 * feat: redirect to 404 for non-existent public workspaces and cards Per maintainer feedback: - Add redirect logic to public boards view when workspace doesn't exist - Add redirect logic to public card modal when card doesn't exist - Uses error?.data?.code === 'NOT_FOUND' check as suggested - Closes modal before redirecting for card modal
661 lines
23 KiB
TypeScript
661 lines
23 KiB
TypeScript
import type { DropResult } from "react-beautiful-dnd";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import { useRouter } from "next/router";
|
|
import { t } from "@lingui/core/macro";
|
|
import { keepPreviousData } from "@tanstack/react-query";
|
|
import { useEffect, useState } from "react";
|
|
import { DragDropContext, Draggable } from "react-beautiful-dnd";
|
|
import { useForm } from "react-hook-form";
|
|
import {
|
|
HiOutlinePlusSmall,
|
|
HiOutlineRectangleStack,
|
|
HiOutlineSquare3Stack3D,
|
|
} from "react-icons/hi2";
|
|
|
|
import type { UpdateBoardInput } from "@kan/api/types";
|
|
|
|
import Button from "~/components/Button";
|
|
import { DeleteLabelConfirmation } from "~/components/DeleteLabelConfirmation";
|
|
import { LabelForm } from "~/components/LabelForm";
|
|
import Modal from "~/components/modal";
|
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
|
import { PageHead } from "~/components/PageHead";
|
|
import PatternedBackground from "~/components/PatternedBackground";
|
|
import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable";
|
|
import { Tooltip } from "~/components/Tooltip";
|
|
import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal";
|
|
import { useDragToScroll } from "~/hooks/useDragToScroll";
|
|
import { useScrollRestore } from "~/hooks/useScrollRestore";
|
|
import { usePermissions } from "~/hooks/usePermissions";
|
|
import { useKeyboardShortcut } from "~/providers/keyboard-shortcuts";
|
|
import { useModal } from "~/providers/modal";
|
|
import { usePopup } from "~/providers/popup";
|
|
import { useWorkspace } from "~/providers/workspace";
|
|
import { api } from "~/utils/api";
|
|
import { formatToArray } from "~/utils/helpers";
|
|
import BoardDropdown from "./components/BoardDropdown";
|
|
import Card from "./components/Card";
|
|
import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation";
|
|
import { DeleteListConfirmation } from "./components/DeleteListConfirmation";
|
|
import Filters from "./components/Filters";
|
|
import List from "./components/List";
|
|
import { NewCardForm } from "./components/NewCardForm";
|
|
import { NewListForm } from "./components/NewListForm";
|
|
import { NewTemplateForm } from "./components/NewTemplateForm";
|
|
import UpdateBoardSlugButton from "./components/UpdateBoardSlugButton";
|
|
import { UpdateBoardSlugForm } from "./components/UpdateBoardSlugForm";
|
|
import VisibilityButton from "./components/VisibilityButton";
|
|
|
|
type PublicListId = string;
|
|
|
|
export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|
const params = useParams() as { boardId: string | string[] } | null;
|
|
const router = useRouter();
|
|
const utils = api.useUtils();
|
|
const { showPopup } = usePopup();
|
|
const { workspace } = useWorkspace();
|
|
const { openModal, modalContentType, entityId, isOpen } = useModal();
|
|
const [selectedPublicListId, setSelectedPublicListId] =
|
|
useState<PublicListId>("");
|
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
|
|
|
const { ref: scrollRef, onMouseDown } = useDragToScroll({
|
|
enabled: true,
|
|
direction: "horizontal",
|
|
});
|
|
|
|
const { canCreateList, canEditList, canEditCard, canEditBoard } =
|
|
usePermissions();
|
|
|
|
const { tooltipContent: createListShortcutTooltipContent } =
|
|
useKeyboardShortcut({
|
|
type: "PRESS",
|
|
stroke: { key: "C" },
|
|
action: () => boardId && canCreateList && openNewListForm(boardId),
|
|
description: t`Create new list`,
|
|
group: "ACTIONS",
|
|
});
|
|
|
|
const boardId = params?.boardId
|
|
? Array.isArray(params.boardId)
|
|
? params.boardId[0]
|
|
: params.boardId
|
|
: null;
|
|
|
|
const updateBoard = api.board.update.useMutation();
|
|
|
|
const { register, handleSubmit, setValue } = useForm<UpdateBoardInput>({
|
|
values: {
|
|
boardPublicId: boardId ?? "",
|
|
name: "",
|
|
},
|
|
});
|
|
|
|
const onSubmit = (values: UpdateBoardInput) => {
|
|
updateBoard.mutate({
|
|
boardPublicId: values.boardPublicId,
|
|
name: values.name,
|
|
});
|
|
};
|
|
|
|
const semanticFilters = formatToArray(router.query.dueDate) as (
|
|
| "overdue"
|
|
| "today"
|
|
| "tomorrow"
|
|
| "next-week"
|
|
| "next-month"
|
|
| "no-due-date"
|
|
)[];
|
|
|
|
const boardType: "regular" | "template" = isTemplate ? "template" : "regular";
|
|
|
|
const queryParams = {
|
|
boardPublicId: boardId ?? "",
|
|
members: formatToArray(router.query.members),
|
|
labels: formatToArray(router.query.labels),
|
|
lists: formatToArray(router.query.lists),
|
|
...(semanticFilters.length > 0 && {
|
|
dueDateFilters: semanticFilters,
|
|
}),
|
|
type: boardType,
|
|
};
|
|
|
|
const {
|
|
data: boardData,
|
|
isSuccess,
|
|
isLoading: isQueryLoading,
|
|
error,
|
|
} = api.board.byId.useQuery(queryParams, {
|
|
enabled: !!boardId,
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
|
|
// Redirect to 404 if board doesn't exist
|
|
useEffect(() => {
|
|
if (router.isReady && boardId && !isQueryLoading) {
|
|
if (error?.data?.code === "NOT_FOUND" || (!boardData && !isQueryLoading)) {
|
|
router.replace("/404");
|
|
}
|
|
}
|
|
}, [router, boardId, isQueryLoading, error, boardData]);
|
|
|
|
const refetchBoard = async () => {
|
|
if (boardId) await utils.board.byId.refetch({ boardPublicId: boardId });
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (boardId) {
|
|
setIsInitialLoading(false);
|
|
}
|
|
}, [boardId]);
|
|
|
|
const isLoading = isInitialLoading || isQueryLoading;
|
|
|
|
useScrollRestore(boardId, scrollRef, router, !isLoading && (boardData?.lists.length ?? 0) > 0);
|
|
|
|
const updateListMutation = api.list.update.useMutation({
|
|
onMutate: async (args) => {
|
|
await utils.board.byId.cancel();
|
|
|
|
const currentState = utils.board.byId.getData(queryParams);
|
|
|
|
utils.board.byId.setData(queryParams, (oldBoard) => {
|
|
if (!oldBoard) return oldBoard;
|
|
|
|
const updatedLists = Array.from(oldBoard.lists);
|
|
|
|
const sourceList = updatedLists.find(
|
|
(list) => list.publicId === args.listPublicId,
|
|
);
|
|
|
|
const currentIndex = sourceList?.index;
|
|
|
|
if (currentIndex === undefined) return oldBoard;
|
|
|
|
const removedList = updatedLists.splice(currentIndex, 1)[0];
|
|
|
|
if (removedList && args.index !== undefined) {
|
|
updatedLists.splice(args.index, 0, removedList);
|
|
|
|
return {
|
|
...oldBoard,
|
|
lists: updatedLists,
|
|
};
|
|
}
|
|
});
|
|
|
|
return { previousState: currentState };
|
|
},
|
|
onError: (_error, _newList, context) => {
|
|
utils.board.byId.setData(queryParams, context?.previousState);
|
|
showPopup({
|
|
header: t`Unable to update list`,
|
|
message: t`Please try again later, or contact customer support.`,
|
|
icon: "error",
|
|
});
|
|
},
|
|
onSettled: async () => {
|
|
await utils.board.byId.invalidate(queryParams);
|
|
},
|
|
});
|
|
|
|
const updateCardMutation = api.card.update.useMutation({
|
|
onMutate: async (args) => {
|
|
await utils.board.byId.cancel();
|
|
|
|
const currentState = utils.board.byId.getData(queryParams);
|
|
|
|
utils.board.byId.setData(queryParams, (oldBoard) => {
|
|
if (!oldBoard) return oldBoard;
|
|
|
|
const updatedLists = Array.from(oldBoard.lists);
|
|
|
|
const sourceList = updatedLists.find((list) =>
|
|
list.cards.some((card) => card.publicId === args.cardPublicId),
|
|
);
|
|
const destinationList = updatedLists.find(
|
|
(list) => list.publicId === args.listPublicId,
|
|
);
|
|
|
|
const cardToMove = sourceList?.cards.find(
|
|
(card) => card.publicId === args.cardPublicId,
|
|
);
|
|
|
|
if (!cardToMove) return oldBoard;
|
|
|
|
const removedCard = sourceList?.cards.splice(cardToMove.index, 1)[0];
|
|
|
|
if (
|
|
sourceList &&
|
|
destinationList &&
|
|
removedCard &&
|
|
args.index !== undefined
|
|
) {
|
|
destinationList.cards.splice(args.index, 0, removedCard);
|
|
|
|
return {
|
|
...oldBoard,
|
|
lists: updatedLists,
|
|
};
|
|
}
|
|
});
|
|
|
|
return { previousState: currentState };
|
|
},
|
|
onError: (_error, _newList, context) => {
|
|
utils.board.byId.setData(queryParams, context?.previousState);
|
|
showPopup({
|
|
header: t`Unable to update card`,
|
|
message: t`Please try again later, or contact customer support.`,
|
|
icon: "error",
|
|
});
|
|
},
|
|
onSettled: async () => {
|
|
await utils.board.byId.invalidate(queryParams);
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isSuccess && boardData) {
|
|
setValue("name", boardData.name || "");
|
|
}
|
|
}, [isSuccess, boardData, setValue]);
|
|
|
|
const openNewListForm = (publicBoardId: string) => {
|
|
openModal("NEW_LIST");
|
|
setSelectedPublicListId(publicBoardId);
|
|
};
|
|
|
|
const onDragEnd = ({
|
|
source: _source,
|
|
destination,
|
|
draggableId,
|
|
type,
|
|
}: DropResult): void => {
|
|
if (!destination) {
|
|
return;
|
|
}
|
|
|
|
if (type === "LIST" && canEditList) {
|
|
updateListMutation.mutate({
|
|
listPublicId: draggableId,
|
|
index: destination.index,
|
|
});
|
|
}
|
|
|
|
if (type === "CARD" && canEditCard) {
|
|
updateCardMutation.mutate({
|
|
cardPublicId: draggableId,
|
|
|
|
listPublicId: destination.droppableId,
|
|
index: destination.index,
|
|
});
|
|
}
|
|
};
|
|
|
|
const renderModalContent = () => {
|
|
return (
|
|
<>
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "DELETE_BOARD"}
|
|
>
|
|
<DeleteBoardConfirmation
|
|
isTemplate={!!isTemplate}
|
|
boardPublicId={boardId ?? ""}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "DELETE_LIST"}
|
|
>
|
|
<DeleteListConfirmation
|
|
listPublicId={selectedPublicListId}
|
|
queryParams={queryParams}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="md"
|
|
isVisible={isOpen && modalContentType === "NEW_CARD"}
|
|
>
|
|
<NewCardForm
|
|
isTemplate={!!isTemplate}
|
|
boardPublicId={boardId ?? ""}
|
|
listPublicId={selectedPublicListId}
|
|
queryParams={queryParams}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "NEW_LIST"}
|
|
>
|
|
<NewListForm
|
|
boardPublicId={boardId ?? ""}
|
|
queryParams={queryParams}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
|
|
>
|
|
<NewWorkspaceForm />
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "NEW_LABEL"}
|
|
>
|
|
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchBoard} />
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "EDIT_LABEL"}
|
|
>
|
|
<LabelForm
|
|
boardPublicId={boardId ?? ""}
|
|
refetch={refetchBoard}
|
|
isEdit
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "DELETE_LABEL"}
|
|
>
|
|
<DeleteLabelConfirmation
|
|
refetch={refetchBoard}
|
|
labelPublicId={entityId}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "UPDATE_BOARD_SLUG"}
|
|
>
|
|
<UpdateBoardSlugForm
|
|
boardPublicId={boardId ?? ""}
|
|
workspaceSlug={workspace.slug ?? ""}
|
|
boardSlug={boardData?.slug ?? ""}
|
|
queryParams={queryParams}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "CREATE_TEMPLATE"}
|
|
>
|
|
<NewTemplateForm
|
|
workspacePublicId={workspace.publicId ?? ""}
|
|
sourceBoardPublicId={boardId ?? ""}
|
|
sourceBoardName={boardData?.name ?? ""}
|
|
/>
|
|
</Modal>
|
|
|
|
<Modal
|
|
modalSize="sm"
|
|
isVisible={isOpen && modalContentType === "EDIT_YOUTUBE"}
|
|
>
|
|
<EditYouTubeModal />
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageHead
|
|
title={`${boardData?.name ?? (isTemplate ? t`Board` : t`Template`)} | ${workspace.name ?? t`Workspace`}`}
|
|
/>
|
|
<div className="relative flex h-full flex-col">
|
|
<PatternedBackground />
|
|
<div className="z-10 flex w-full flex-col justify-between p-6 md:flex-row md:p-8">
|
|
{isLoading && !boardData && (
|
|
<div className="flex space-x-2">
|
|
<div className="h-[2.3rem] w-[150px] animate-pulse rounded-[5px] bg-light-200 dark:bg-dark-100" />
|
|
</div>
|
|
)}
|
|
{boardData && (
|
|
<form
|
|
onSubmit={handleSubmit(onSubmit)}
|
|
className="order-2 focus-visible:outline-none md:order-1"
|
|
>
|
|
<input
|
|
id="name"
|
|
type="text"
|
|
{...register("name")}
|
|
onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined}
|
|
readOnly={!canEditBoard}
|
|
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none disabled:cursor-not-allowed dark:text-dark-1000 sm:text-[1.2rem]"
|
|
/>
|
|
</form>
|
|
)}
|
|
{!boardData && !isLoading && (
|
|
<p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1">
|
|
{t`${isTemplate ? "Template" : "Board"} not found`}
|
|
</p>
|
|
)}
|
|
<div className="order-1 mb-4 flex items-center justify-end space-x-2 md:order-2 md:mb-0">
|
|
{isTemplate && (
|
|
<div className="inline-flex cursor-default items-center justify-center whitespace-nowrap rounded-md border-[1px] border-light-300 bg-light-50 px-3 py-2 text-sm font-semibold text-light-950 shadow-sm dark:border-dark-300 dark:bg-dark-50 dark:text-dark-950">
|
|
<span className="mr-2">
|
|
<HiOutlineRectangleStack />
|
|
</span>
|
|
{t`Template`}
|
|
</div>
|
|
)}
|
|
{!isTemplate && (
|
|
<>
|
|
<UpdateBoardSlugButton
|
|
handleOnClick={() => openModal("UPDATE_BOARD_SLUG")}
|
|
isLoading={isLoading}
|
|
workspaceSlug={workspace.slug ?? ""}
|
|
boardSlug={boardData?.slug ?? ""}
|
|
boardPublicId={boardId ?? ""}
|
|
visibility={boardData?.visibility ?? "private"}
|
|
canEdit={canEditBoard}
|
|
/>
|
|
<VisibilityButton
|
|
visibility={boardData?.visibility ?? "private"}
|
|
boardPublicId={boardId ?? ""}
|
|
boardSlug={boardData?.slug ?? ""}
|
|
queryParams={queryParams}
|
|
isLoading={!boardData}
|
|
isAdmin={workspace.role === "admin"}
|
|
/>
|
|
{boardData && (
|
|
<Filters
|
|
labels={boardData.labels}
|
|
members={boardData.workspace.members.filter(
|
|
(member) => member.user !== null,
|
|
)}
|
|
lists={boardData.allLists}
|
|
position="left"
|
|
isLoading={!boardData}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
<Tooltip
|
|
content={
|
|
!canCreateList
|
|
? t`You don't have permission`
|
|
: createListShortcutTooltipContent
|
|
}
|
|
>
|
|
<Button
|
|
iconLeft={
|
|
<HiOutlinePlusSmall
|
|
className="-mr-0.5 h-5 w-5"
|
|
aria-hidden="true"
|
|
/>
|
|
}
|
|
onClick={() => {
|
|
if (boardId && canCreateList) openNewListForm(boardId);
|
|
}}
|
|
disabled={!boardData || !canCreateList}
|
|
>
|
|
{t`New list`}
|
|
</Button>
|
|
</Tooltip>
|
|
<BoardDropdown
|
|
isTemplate={!!isTemplate}
|
|
isLoading={!boardData}
|
|
boardPublicId={boardId ?? ""}
|
|
isArchived={boardData?.isArchived ?? false}
|
|
isFavorite={boardData?.favorite}
|
|
boardName={boardData?.name}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
ref={scrollRef}
|
|
onMouseDown={onMouseDown}
|
|
className={`scrollbar-w-none scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] z-0 flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300`}
|
|
>
|
|
{isLoading ? (
|
|
<div className="ml-[2rem] flex">
|
|
<div className="0 mr-5 h-[500px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
|
<div className="0 mr-5 h-[275px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
|
<div className="0 mr-5 h-[375px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
|
</div>
|
|
) : boardData ? (
|
|
<>
|
|
{boardData.lists.length === 0 ? (
|
|
<div className="z-10 flex h-full w-full flex-col items-center justify-center space-y-8 pb-[150px]">
|
|
<div className="flex flex-col items-center">
|
|
<HiOutlineSquare3Stack3D className="h-10 w-10 text-light-800 dark:text-dark-800" />
|
|
<p className="mb-2 mt-4 text-[14px] font-bold text-light-1000 dark:text-dark-950">
|
|
{t`No lists`}
|
|
</p>
|
|
<p className="text-[14px] text-light-900 dark:text-dark-900">
|
|
{canCreateList
|
|
? t`Get started by creating a new list`
|
|
: t`No lists have been created yet`}
|
|
</p>
|
|
</div>
|
|
<Tooltip
|
|
content={
|
|
!canCreateList ? t`You don't have permission` : undefined
|
|
}
|
|
>
|
|
<Button
|
|
onClick={() => {
|
|
if (boardId && canCreateList) openNewListForm(boardId);
|
|
}}
|
|
disabled={!canCreateList}
|
|
>
|
|
{t`Create new list`}
|
|
</Button>
|
|
</Tooltip>
|
|
</div>
|
|
) : (
|
|
<DragDropContext onDragEnd={onDragEnd}>
|
|
<Droppable
|
|
droppableId="all-lists"
|
|
direction="horizontal"
|
|
type="LIST"
|
|
>
|
|
{(provided) => (
|
|
<div
|
|
className="flex"
|
|
ref={provided.innerRef}
|
|
{...provided.droppableProps}
|
|
>
|
|
<div className="min-w-[2rem]" />
|
|
{boardData.lists.map((list, index) => (
|
|
<List
|
|
index={index}
|
|
key={index}
|
|
list={list}
|
|
setSelectedPublicListId={(publicListId) =>
|
|
setSelectedPublicListId(publicListId)
|
|
}
|
|
>
|
|
<Droppable
|
|
droppableId={`${list.publicId}`}
|
|
type="CARD"
|
|
>
|
|
{(provided) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.droppableProps}
|
|
className="scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px] z-10 h-full max-h-[calc(100vh-225px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-600"
|
|
>
|
|
{list.cards.map((card, index) => (
|
|
<Draggable
|
|
key={card.publicId}
|
|
draggableId={card.publicId}
|
|
index={index}
|
|
isDragDisabled={!canEditCard}
|
|
>
|
|
{(provided) => (
|
|
<Link
|
|
onClick={(e) => {
|
|
if (
|
|
card.publicId.startsWith(
|
|
"PLACEHOLDER",
|
|
)
|
|
)
|
|
e.preventDefault();
|
|
}}
|
|
key={card.publicId}
|
|
href={
|
|
isTemplate
|
|
? `/templates/${boardId}/cards/${card.publicId}`
|
|
: `/cards/${card.publicId}`
|
|
}
|
|
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
|
|
"PLACEHOLDER",
|
|
)
|
|
? "pointer-events-none"
|
|
: ""
|
|
}`}
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
{...provided.dragHandleProps}
|
|
>
|
|
<Card
|
|
title={card.title}
|
|
labels={card.labels}
|
|
members={card.members}
|
|
checklists={card.checklists ?? []}
|
|
description={
|
|
card.description ?? null
|
|
}
|
|
comments={card.comments ?? []}
|
|
attachments={card.attachments}
|
|
dueDate={card.dueDate ?? null}
|
|
/>
|
|
</Link>
|
|
)}
|
|
</Draggable>
|
|
))}
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</List>
|
|
))}
|
|
<div className="min-w-[0.75rem]" />
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</div>
|
|
{renderModalContent()}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|