Files
kan/apps/web/src/views/card/components/Checklists.tsx
Henry a55c8cd52b fix: update checklist when pressing enter on edit (#292)
* fix: update checklist when pressing enter on edit

* feat: open new checklist item form on enter
2025-12-19 21:40:24 +00:00

246 lines
9.0 KiB
TypeScript

import type { DropResult } from "react-beautiful-dnd";
import { t } from "@lingui/core/macro";
import { DragDropContext, Draggable } from "react-beautiful-dnd";
import { HiPlus, HiXMark } from "react-icons/hi2";
import CircularProgress from "~/components/CircularProgress";
import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
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();
const { showPopup } = usePopup();
const utils = api.useUtils();
const reorderItemMutation = api.checklist.updateItem.useMutation({
onMutate: async (vars) => {
await utils.card.byId.cancel({ cardPublicId });
const previous = utils.card.byId.getData({ cardPublicId });
utils.card.byId.setData({ cardPublicId }, (old) => {
if (!old) return old;
const updatedChecklists = old.checklists.map((cl) => {
const itemIndex = cl.items.findIndex(
(item) => item.publicId === vars.checklistItemPublicId,
);
if (itemIndex === -1 || vars.index === undefined) return cl;
const newIndex = vars.index;
const items = Array.from(cl.items);
const [movedItem] = items.splice(itemIndex, 1);
if (!movedItem) return cl;
items.splice(newIndex, 0, movedItem);
return { ...cl, items };
});
return { ...old, checklists: updatedChecklists } as typeof old;
});
return { previous };
},
onError: (_err, _vars, ctx) => {
if (ctx?.previous)
utils.card.byId.setData({ cardPublicId }, ctx.previous);
showPopup({
header: t`Unable to reorder checklist item`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
onSettled: async () => {
await utils.card.byId.invalidate({ cardPublicId });
},
});
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
const { source, destination, draggableId } = result;
if (source.droppableId !== destination.droppableId) return;
if (source.index === destination.index) return;
reorderItemMutation.mutate({
checklistItemPublicId: draggableId,
index: destination.index,
});
};
if (checklists.length === 0) return null;
return (
<DragDropContext onDragEnd={onDragEnd}>
<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>
<Droppable
droppableId={checklist.publicId}
type="CHECKLIST_ITEM"
isDropDisabled={viewOnly}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className="ml-1"
>
{checklist.items.map((item, index) => (
<Draggable
key={item.publicId}
draggableId={item.publicId}
index={index}
isDragDisabled={viewOnly}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={{
...provided.draggableProps.style,
opacity: snapshot.isDragging ? 0.8 : 1,
}}
>
<ChecklistItemRow
item={{
publicId: item.publicId,
title: item.title,
completed: item.completed,
}}
cardPublicId={cardPublicId}
onCreateNewItem={() =>
setActiveChecklistForm?.(checklist.publicId)
}
viewOnly={viewOnly}
dragHandleProps={provided.dragHandleProps}
isDragging={snapshot.isDragging}
/>
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
{activeChecklistForm === checklist.publicId && !viewOnly && (
<div className="ml-1">
<NewChecklistItemForm
checklistPublicId={checklist.publicId}
cardPublicId={cardPublicId}
onCancel={() => setActiveChecklistForm?.(null)}
readOnly={viewOnly}
/>
</div>
)}
</div>
);
})}
</div>
</div>
</DragDropContext>
);
}