feat: display checklist items on public card modal
This commit is contained in:
@@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ContentEditable from "react-contenteditable";
|
import ContentEditable from "react-contenteditable";
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
@@ -13,11 +14,13 @@ interface ChecklistItemRowProps {
|
|||||||
completed: boolean;
|
completed: boolean;
|
||||||
};
|
};
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
|
viewOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChecklistItemRow({
|
export default function ChecklistItemRow({
|
||||||
item,
|
item,
|
||||||
cardPublicId,
|
cardPublicId,
|
||||||
|
viewOnly = false,
|
||||||
}: ChecklistItemRowProps) {
|
}: ChecklistItemRowProps) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
@@ -107,6 +110,7 @@ export default function ChecklistItemRow({
|
|||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
const handleToggleCompleted = () => {
|
const handleToggleCompleted = () => {
|
||||||
|
if (viewOnly) return;
|
||||||
setCompleted((prev) => !prev);
|
setCompleted((prev) => !prev);
|
||||||
updateItem.mutate({
|
updateItem.mutate({
|
||||||
checklistItemPublicId: item.publicId,
|
checklistItemPublicId: item.publicId,
|
||||||
@@ -115,6 +119,7 @@ export default function ChecklistItemRow({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const commitTitle = (rawHtml: string) => {
|
const commitTitle = (rawHtml: string) => {
|
||||||
|
if (viewOnly) return;
|
||||||
const plain = sanitizeHtmlToPlainText(rawHtml);
|
const plain = sanitizeHtmlToPlainText(rawHtml);
|
||||||
if (!plain || plain === item.title) {
|
if (!plain || plain === item.title) {
|
||||||
setTitle(item.title);
|
setTitle(item.title);
|
||||||
@@ -128,28 +133,45 @@ export default function ChecklistItemRow({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
|
if (viewOnly) return;
|
||||||
deleteItem.mutate({ checklistItemPublicId: item.publicId });
|
deleteItem.mutate({ checklistItemPublicId: item.publicId });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex items-start gap-3 rounded-md py-2 pl-4 hover:bg-light-100 dark:hover:bg-dark-100">
|
<div className="group relative flex items-start gap-3 rounded-md py-2 pl-4 hover:bg-light-100 dark:hover:bg-dark-100">
|
||||||
<label className="relative mt-[2px] inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-pointer items-center justify-center">
|
<label
|
||||||
|
className={`relative mt-[2px] inline-flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={completed}
|
checked={completed}
|
||||||
onChange={handleToggleCompleted}
|
onChange={(e) => {
|
||||||
className="h-[16px] w-[16px] cursor-pointer appearance-none rounded-md border border-light-500 bg-transparent outline-none ring-0 checked:bg-blue-600 focus:shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500"
|
if (viewOnly) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleToggleCompleted();
|
||||||
|
}}
|
||||||
|
className={twMerge(
|
||||||
|
"h-[16px] w-[16px] appearance-none rounded-md border border-light-500 bg-transparent outline-none ring-0 checked:bg-blue-600 focus:shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500",
|
||||||
|
viewOnly ? "cursor-default" : "cursor-pointer",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex-1 pr-7">
|
<div className="flex-1 pr-7">
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
html={title}
|
html={title}
|
||||||
|
disabled={viewOnly}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
// @ts-expect-error - valid event
|
// @ts-expect-error - valid event
|
||||||
onBlur={(e: Event) => commitTitle(e.target.innerHTML as string)}
|
onBlur={(e: Event) => commitTitle(e.target.innerHTML as string)}
|
||||||
className="m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 outline-none focus-visible:outline-none dark:text-dark-950"
|
className={twMerge(
|
||||||
|
"m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 outline-none focus-visible:outline-none dark:text-dark-950",
|
||||||
|
viewOnly && "cursor-default",
|
||||||
|
)}
|
||||||
placeholder={t`Add details...`}
|
placeholder={t`Add details...`}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
if (viewOnly) return;
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
commitTitle(title);
|
commitTitle(title);
|
||||||
@@ -161,13 +183,15 @@ export default function ChecklistItemRow({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{!viewOnly && (
|
||||||
type="button"
|
<button
|
||||||
onClick={handleDelete}
|
type="button"
|
||||||
className="absolute right-1 top-1/2 hidden -translate-y-1/2 rounded-md p-1 text-light-900 group-hover:block hover:bg-light-200 dark:text-dark-700 dark:hover:bg-dark-200"
|
onClick={handleDelete}
|
||||||
>
|
className="absolute right-1 top-1/2 hidden -translate-y-1/2 rounded-md p-1 text-light-900 group-hover:block hover:bg-light-200 dark:text-dark-700 dark:hover:bg-dark-200"
|
||||||
<HiXMark size={16} />
|
>
|
||||||
</button>
|
<HiXMark size={16} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
@@ -8,10 +9,12 @@ export default function ChecklistNameInput({
|
|||||||
checklistPublicId,
|
checklistPublicId,
|
||||||
initialName,
|
initialName,
|
||||||
cardPublicId,
|
cardPublicId,
|
||||||
|
viewOnly = false,
|
||||||
}: {
|
}: {
|
||||||
checklistPublicId: string;
|
checklistPublicId: string;
|
||||||
initialName: string;
|
initialName: string;
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
|
viewOnly?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
@@ -50,6 +53,7 @@ export default function ChecklistNameInput({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const commit = () => {
|
const commit = () => {
|
||||||
|
if (viewOnly) return;
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
if (!trimmed || trimmed === initialName) return;
|
if (!trimmed || trimmed === initialName) return;
|
||||||
update.mutate({ checklistPublicId, name: trimmed });
|
update.mutate({ checklistPublicId, name: trimmed });
|
||||||
@@ -60,9 +64,11 @@ export default function ChecklistNameInput({
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
|
readOnly={viewOnly}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
onBlur={commit}
|
onBlur={commit}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
if (viewOnly) return;
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
commit();
|
commit();
|
||||||
@@ -75,7 +81,10 @@ export default function ChecklistNameInput({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title={name}
|
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",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
142
apps/web/src/views/card/components/Checklists.tsx
Normal file
142
apps/web/src/views/card/components/Checklists.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="border-light-300 pb-4 dark:border-dark-300">
|
||||||
|
<div>
|
||||||
|
{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 (
|
||||||
|
<div key={checklist.publicId} className="mb-4">
|
||||||
|
<div className="mb-2 flex items-center font-medium text-light-1000 dark:text-dark-1000">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<ChecklistNameInput
|
||||||
|
checklistPublicId={checklist.publicId}
|
||||||
|
initialName={checklist.name}
|
||||||
|
cardPublicId={cardPublicId}
|
||||||
|
viewOnly={viewOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!viewOnly && (
|
||||||
|
<div className="ml-2 flex flex-shrink-0 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 rounded-full border-[1px] border-light-300 px-2 py-1 dark:border-dark-300">
|
||||||
|
<CircularProgress
|
||||||
|
progress={progress}
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-[11px] text-light-900 dark:text-dark-700">
|
||||||
|
{completedItems.length}/{checklist.items.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="rounded-md p-1 text-light-900 hover:bg-light-100 dark:text-dark-700 dark:hover:bg-dark-100"
|
||||||
|
onClick={() =>
|
||||||
|
openModal("DELETE_CHECKLIST", checklist.publicId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HiXMark size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setActiveChecklistForm?.(checklist.publicId)
|
||||||
|
}
|
||||||
|
className="rounded-md p-1 text-light-900 hover:bg-light-100 dark:text-dark-700 dark:hover:bg-dark-100"
|
||||||
|
>
|
||||||
|
<HiPlus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{viewOnly && (
|
||||||
|
<div className="ml-2 flex flex-shrink-0 items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 rounded-full border-[1px] border-light-300 px-2 py-1 dark:border-dark-300">
|
||||||
|
<CircularProgress
|
||||||
|
progress={progress}
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-[11px] text-light-900 dark:text-dark-700">
|
||||||
|
{completedItems.length}/{checklist.items.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-1">
|
||||||
|
{checklist.items.map((item) => (
|
||||||
|
<ChecklistItemRow
|
||||||
|
key={item.publicId}
|
||||||
|
item={{
|
||||||
|
publicId: item.publicId,
|
||||||
|
title: item.title,
|
||||||
|
completed: item.completed,
|
||||||
|
}}
|
||||||
|
cardPublicId={cardPublicId}
|
||||||
|
viewOnly={viewOnly}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeChecklistForm === checklist.publicId && !viewOnly && (
|
||||||
|
<div className="ml-1">
|
||||||
|
<NewChecklistItemForm
|
||||||
|
checklistPublicId={checklist.publicId}
|
||||||
|
cardPublicId={cardPublicId}
|
||||||
|
onCancel={() => setActiveChecklistForm?.(null)}
|
||||||
|
readOnly={viewOnly}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,12 +16,14 @@ interface NewChecklistItemFormProps {
|
|||||||
checklistPublicId: string;
|
checklistPublicId: string;
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewChecklistItemForm = ({
|
const NewChecklistItemForm = ({
|
||||||
checklistPublicId,
|
checklistPublicId,
|
||||||
cardPublicId,
|
cardPublicId,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
readOnly = false,
|
||||||
}: NewChecklistItemFormProps) => {
|
}: NewChecklistItemFormProps) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
@@ -40,6 +42,7 @@ const NewChecklistItemForm = ({
|
|||||||
const refocusEditable = () => {
|
const refocusEditable = () => {
|
||||||
const el = editableRef.current;
|
const el = editableRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
if (readOnly) return;
|
||||||
el.focus();
|
el.focus();
|
||||||
|
|
||||||
// hack to ensure the input is focused after creating new checklist item
|
// hack to ensure the input is focused after creating new checklist item
|
||||||
@@ -102,6 +105,7 @@ const NewChecklistItemForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const submitIfNotEmpty = (keepOpen: boolean) => {
|
const submitIfNotEmpty = (keepOpen: boolean) => {
|
||||||
|
if (readOnly) return;
|
||||||
keepOpenRef.current = keepOpen;
|
keepOpenRef.current = keepOpen;
|
||||||
const currentHtml = getValues("title") ?? "";
|
const currentHtml = getValues("title") ?? "";
|
||||||
const plain = sanitizeHtmlToPlainText(currentHtml);
|
const plain = sanitizeHtmlToPlainText(currentHtml);
|
||||||
@@ -132,14 +136,15 @@ const NewChecklistItemForm = ({
|
|||||||
<div className="flex-1 pr-7">
|
<div className="flex-1 pr-7">
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
id={`checklist-item-input-${checklistPublicId}`}
|
id={`checklist-item-input-${checklistPublicId}`}
|
||||||
tabIndex={0}
|
tabIndex={readOnly ? -1 : 0}
|
||||||
placeholder={t`Add an item...`}
|
placeholder={t`Add an item...`}
|
||||||
html={title}
|
html={title}
|
||||||
disabled={false}
|
disabled={readOnly}
|
||||||
onChange={(e) => setValue("title", e.target.value)}
|
onChange={(e) => 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"
|
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)}
|
onBlur={() => submitIfNotEmpty(false)}
|
||||||
onKeyDown={async (e) => {
|
onKeyDown={async (e) => {
|
||||||
|
if (readOnly) return;
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submitIfNotEmpty(true);
|
submitIfNotEmpty(true);
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { useRouter } from "next/router";
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { HiPlus, HiXMark } from "react-icons/hi2";
|
|
||||||
import { IoChevronForwardSharp } from "react-icons/io5";
|
import { IoChevronForwardSharp } from "react-icons/io5";
|
||||||
|
|
||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import CircularProgress from "~/components/CircularProgress";
|
|
||||||
import Editor from "~/components/Editor";
|
import Editor from "~/components/Editor";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
import FeedbackModal from "~/components/FeedbackModal";
|
||||||
import { LabelForm } from "~/components/LabelForm";
|
import { LabelForm } from "~/components/LabelForm";
|
||||||
@@ -22,8 +20,7 @@ import { api } from "~/utils/api";
|
|||||||
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
|
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
|
||||||
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
|
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
|
||||||
import ActivityList from "./components/ActivityList";
|
import ActivityList from "./components/ActivityList";
|
||||||
import ChecklistItemRow from "./components/ChecklistItemRow";
|
import Checklists from "./components/Checklists";
|
||||||
import ChecklistNameInput from "./components/ChecklistNameInput";
|
|
||||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||||
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
|
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
|
||||||
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
||||||
@@ -279,95 +276,12 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{card.checklists.length > 0 && (
|
<Checklists
|
||||||
<div className="border-light-300 pb-4 dark:border-dark-300">
|
checklists={card.checklists}
|
||||||
<div>
|
cardPublicId={cardId}
|
||||||
{card.checklists.map((checklist) => {
|
activeChecklistForm={activeChecklistForm}
|
||||||
const completedItems = checklist.items.filter(
|
setActiveChecklistForm={setActiveChecklistForm}
|
||||||
(item) => item.completed,
|
/>
|
||||||
);
|
|
||||||
const progress =
|
|
||||||
checklist.items.length > 0 &&
|
|
||||||
completedItems.length > 0
|
|
||||||
? (completedItems.length / checklist.items.length) *
|
|
||||||
100
|
|
||||||
: 2;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={checklist.publicId} className="mb-4">
|
|
||||||
<div className="mb-2 flex items-center font-medium text-light-1000 dark:text-dark-1000">
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<ChecklistNameInput
|
|
||||||
checklistPublicId={checklist.publicId}
|
|
||||||
initialName={checklist.name}
|
|
||||||
cardPublicId={cardId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ml-2 flex flex-shrink-0 items-center gap-2">
|
|
||||||
<div className="flex items-center gap-1 rounded-full border-[1px] border-light-300 px-2 py-1 dark:border-dark-300">
|
|
||||||
<CircularProgress
|
|
||||||
progress={progress}
|
|
||||||
size="sm"
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span className="text-[11px] text-light-900 dark:text-dark-700">
|
|
||||||
{completedItems.length}/
|
|
||||||
{checklist.items.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="rounded-md p-1 text-light-900 hover:bg-light-100 dark:text-dark-700 dark:hover:bg-dark-100"
|
|
||||||
onClick={() =>
|
|
||||||
openModal(
|
|
||||||
"DELETE_CHECKLIST",
|
|
||||||
checklist.publicId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<HiXMark size={16} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
setActiveChecklistForm(checklist.publicId)
|
|
||||||
}
|
|
||||||
className="rounded-md p-1 text-light-900 hover:bg-light-100 dark:text-dark-700 dark:hover:bg-dark-100"
|
|
||||||
>
|
|
||||||
<HiPlus size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-1">
|
|
||||||
{checklist.items.map((item) => (
|
|
||||||
<ChecklistItemRow
|
|
||||||
key={item.publicId}
|
|
||||||
item={{
|
|
||||||
publicId: item.publicId,
|
|
||||||
title: item.title,
|
|
||||||
completed: item.completed,
|
|
||||||
}}
|
|
||||||
cardPublicId={cardId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{activeChecklistForm === checklist.publicId && (
|
|
||||||
<div className="ml-1">
|
|
||||||
<NewChecklistItemForm
|
|
||||||
checklistPublicId={checklist.publicId}
|
|
||||||
cardPublicId={cardId}
|
|
||||||
onCancel={() => setActiveChecklistForm(null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="border-t-[1px] border-light-300 pt-12 dark:border-dark-300">
|
<div className="border-t-[1px] border-light-300 pt-12 dark:border-dark-300">
|
||||||
<h2 className="text-md pb-4 font-medium text-light-1000 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-light-1000 dark:text-dark-1000">
|
||||||
{t`Activity`}
|
{t`Activity`}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import LabelIcon from "~/components/LabelIcon";
|
|||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import ActivityList from "~/views/card/components/ActivityList";
|
import ActivityList from "~/views/card/components/ActivityList";
|
||||||
|
import Checklists from "~/views/card/components/Checklists";
|
||||||
|
|
||||||
export function CardModal({
|
export function CardModal({
|
||||||
cardPublicId,
|
cardPublicId,
|
||||||
@@ -123,6 +124,13 @@ export function CardModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{data?.checklists && data.checklists.length > 0 && (
|
||||||
|
<Checklists
|
||||||
|
checklists={data.checklists}
|
||||||
|
cardPublicId={cardPublicId ?? ""}
|
||||||
|
viewOnly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="border-t-[1px] border-light-600 pb-4 pt-12 dark:border-dark-400">
|
<div className="border-t-[1px] border-light-600 pb-4 pt-12 dark:border-dark-400">
|
||||||
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
|
||||||
{t`Activity`}
|
{t`Activity`}
|
||||||
|
|||||||
Reference in New Issue
Block a user