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`}