* fix: update checklist when pressing enter on edit * feat: open new checklist item form on enter
246 lines
9.0 KiB
TypeScript
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>
|
|
);
|
|
}
|