diff --git a/apps/web/src/views/card/components/ChecklistItemRow.tsx b/apps/web/src/views/card/components/ChecklistItemRow.tsx index a4366dee..00cb048a 100644 --- a/apps/web/src/views/card/components/ChecklistItemRow.tsx +++ b/apps/web/src/views/card/components/ChecklistItemRow.tsx @@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro"; import { useEffect, useState } from "react"; import ContentEditable from "react-contenteditable"; import { HiXMark } from "react-icons/hi2"; +import { twMerge } from "tailwind-merge"; import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; @@ -13,11 +14,13 @@ interface ChecklistItemRowProps { completed: boolean; }; cardPublicId: string; + viewOnly?: boolean; } export default function ChecklistItemRow({ item, cardPublicId, + viewOnly = false, }: ChecklistItemRowProps) { const utils = api.useUtils(); const { showPopup } = usePopup(); @@ -107,6 +110,7 @@ export default function ChecklistItemRow({ .trim(); const handleToggleCompleted = () => { + if (viewOnly) return; setCompleted((prev) => !prev); updateItem.mutate({ checklistItemPublicId: item.publicId, @@ -115,6 +119,7 @@ export default function ChecklistItemRow({ }; const commitTitle = (rawHtml: string) => { + if (viewOnly) return; const plain = sanitizeHtmlToPlainText(rawHtml); if (!plain || plain === item.title) { setTitle(item.title); @@ -128,28 +133,45 @@ export default function ChecklistItemRow({ }; const handleDelete = () => { + if (viewOnly) return; deleteItem.mutate({ checklistItemPublicId: item.publicId }); }; return (
-
); } diff --git a/apps/web/src/views/card/components/ChecklistNameInput.tsx b/apps/web/src/views/card/components/ChecklistNameInput.tsx index bca9917c..a200df73 100644 --- a/apps/web/src/views/card/components/ChecklistNameInput.tsx +++ b/apps/web/src/views/card/components/ChecklistNameInput.tsx @@ -1,5 +1,6 @@ import { t } from "@lingui/core/macro"; import { useEffect, useRef, useState } from "react"; +import { twMerge } from "tailwind-merge"; import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; @@ -8,10 +9,12 @@ export default function ChecklistNameInput({ checklistPublicId, initialName, cardPublicId, + viewOnly = false, }: { checklistPublicId: string; initialName: string; cardPublicId: string; + viewOnly?: boolean; }) { const utils = api.useUtils(); const { showPopup } = usePopup(); @@ -50,6 +53,7 @@ export default function ChecklistNameInput({ }); const commit = () => { + if (viewOnly) return; const trimmed = name.trim(); if (!trimmed || trimmed === initialName) return; update.mutate({ checklistPublicId, name: trimmed }); @@ -60,9 +64,11 @@ export default function ChecklistNameInput({ ref={inputRef} type="text" value={name} + readOnly={viewOnly} onChange={(e) => setName(e.target.value)} onBlur={commit} onKeyDown={(e) => { + if (viewOnly) return; if (e.key === "Enter") { e.preventDefault(); commit(); @@ -75,7 +81,10 @@ export default function ChecklistNameInput({ } }} title={name} - className="text-md block w-full truncate border-0 bg-transparent p-0 py-0 font-medium text-light-1000 outline-none focus:ring-0 dark:text-dark-1000" + className={twMerge( + "text-md block w-full truncate border-0 bg-transparent p-0 py-0 font-medium text-light-1000 outline-none focus:ring-0 dark:text-dark-1000", + viewOnly && "cursor-default", + )} /> ); } diff --git a/apps/web/src/views/card/components/Checklists.tsx b/apps/web/src/views/card/components/Checklists.tsx new file mode 100644 index 00000000..34ed3280 --- /dev/null +++ b/apps/web/src/views/card/components/Checklists.tsx @@ -0,0 +1,142 @@ +import { HiPlus, HiXMark } from "react-icons/hi2"; + +import CircularProgress from "~/components/CircularProgress"; +import { useModal } from "~/providers/modal"; +import ChecklistItemRow from "./ChecklistItemRow"; +import ChecklistNameInput from "./ChecklistNameInput"; +import NewChecklistItemForm from "./NewChecklistItemForm"; + +interface ChecklistItem { + publicId: string; + title: string; + completed: boolean; +} + +interface Checklist { + publicId: string; + name: string; + items: ChecklistItem[]; +} + +interface ChecklistsProps { + checklists: Checklist[]; + cardPublicId: string; + activeChecklistForm?: string | null; + setActiveChecklistForm?: (id: string | null) => void; + viewOnly?: boolean; +} + +export default function Checklists({ + checklists, + cardPublicId, + activeChecklistForm, + setActiveChecklistForm, + viewOnly = false, +}: ChecklistsProps) { + const { openModal } = useModal(); + + if (!checklists || checklists.length === 0) return null; + + return ( +
+
+ {checklists.map((checklist) => { + const completedItems = checklist.items.filter( + (item) => item.completed, + ); + const progress = + checklist.items.length > 0 && completedItems.length > 0 + ? (completedItems.length / checklist.items.length) * 100 + : 2; + + return ( +
+
+
+ +
+ {!viewOnly && ( +
+
+ + + {completedItems.length}/{checklist.items.length} + +
+
+ + +
+
+ )} + {viewOnly && ( +
+
+ + + {completedItems.length}/{checklist.items.length} + +
+
+ )} +
+ +
+ {checklist.items.map((item) => ( + + ))} +
+ + {activeChecklistForm === checklist.publicId && !viewOnly && ( +
+ setActiveChecklistForm?.(null)} + readOnly={viewOnly} + /> +
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/apps/web/src/views/card/components/NewChecklistItemForm.tsx b/apps/web/src/views/card/components/NewChecklistItemForm.tsx index 4580ef3b..9e6de8c1 100644 --- a/apps/web/src/views/card/components/NewChecklistItemForm.tsx +++ b/apps/web/src/views/card/components/NewChecklistItemForm.tsx @@ -16,12 +16,14 @@ interface NewChecklistItemFormProps { checklistPublicId: string; cardPublicId: string; onCancel: () => void; + readOnly?: boolean; } const NewChecklistItemForm = ({ checklistPublicId, cardPublicId, onCancel, + readOnly = false, }: NewChecklistItemFormProps) => { const utils = api.useUtils(); const { showPopup } = usePopup(); @@ -40,6 +42,7 @@ const NewChecklistItemForm = ({ const refocusEditable = () => { const el = editableRef.current; if (!el) return; + if (readOnly) return; el.focus(); // hack to ensure the input is focused after creating new checklist item @@ -102,6 +105,7 @@ const NewChecklistItemForm = ({ }; const submitIfNotEmpty = (keepOpen: boolean) => { + if (readOnly) return; keepOpenRef.current = keepOpen; const currentHtml = getValues("title") ?? ""; const plain = sanitizeHtmlToPlainText(currentHtml); @@ -132,14 +136,15 @@ const NewChecklistItemForm = ({
setValue("title", e.target.value)} className="m-0 min-h-[20px] w-full p-0 text-sm leading-5 text-light-900 outline-none focus-visible:outline-none dark:text-dark-950" onBlur={() => submitIfNotEmpty(false)} onKeyDown={async (e) => { + if (readOnly) return; if (e.key === "Enter") { e.preventDefault(); submitIfNotEmpty(true); diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index 1461f518..e0d439ce 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -3,11 +3,9 @@ import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { HiPlus, HiXMark } from "react-icons/hi2"; import { IoChevronForwardSharp } from "react-icons/io5"; import Avatar from "~/components/Avatar"; -import CircularProgress from "~/components/CircularProgress"; import Editor from "~/components/Editor"; import FeedbackModal from "~/components/FeedbackModal"; import { LabelForm } from "~/components/LabelForm"; @@ -22,8 +20,7 @@ import { api } from "~/utils/api"; import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers"; import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation"; import ActivityList from "./components/ActivityList"; -import ChecklistItemRow from "./components/ChecklistItemRow"; -import ChecklistNameInput from "./components/ChecklistNameInput"; +import Checklists from "./components/Checklists"; import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation"; import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation"; import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation"; @@ -279,95 +276,12 @@ export default function CardPage() {
- {card.checklists.length > 0 && ( -
-
- {card.checklists.map((checklist) => { - const completedItems = checklist.items.filter( - (item) => item.completed, - ); - const progress = - checklist.items.length > 0 && - completedItems.length > 0 - ? (completedItems.length / checklist.items.length) * - 100 - : 2; - - return ( -
-
-
- -
-
-
- - - {completedItems.length}/ - {checklist.items.length} - -
-
- - -
-
-
- -
- {checklist.items.map((item) => ( - - ))} -
- - {activeChecklistForm === checklist.publicId && ( -
- setActiveChecklistForm(null)} - /> -
- )} -
- ); - })} -
-
- )} +

{t`Activity`} diff --git a/apps/web/src/views/public/board/CardModal.tsx b/apps/web/src/views/public/board/CardModal.tsx index 4b9baed3..b7476670 100644 --- a/apps/web/src/views/public/board/CardModal.tsx +++ b/apps/web/src/views/public/board/CardModal.tsx @@ -9,6 +9,7 @@ import LabelIcon from "~/components/LabelIcon"; import { useModal } from "~/providers/modal"; import { api } from "~/utils/api"; import ActivityList from "~/views/card/components/ActivityList"; +import Checklists from "~/views/card/components/Checklists"; export function CardModal({ cardPublicId, @@ -123,6 +124,13 @@ export function CardModal({

)} + {data?.checklists && data.checklists.length > 0 && ( + + )}

{t`Activity`}