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"; interface ChecklistItemRowProps { item: { publicId: string; title: string; completed: boolean; }; cardPublicId: string; viewOnly?: boolean; } export default function ChecklistItemRow({ item, cardPublicId, viewOnly = false, }: ChecklistItemRowProps) { const utils = api.useUtils(); const { showPopup } = usePopup(); const [title, setTitle] = useState(""); const [completed, setCompleted] = useState(false); const updateItem = 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 as any; const updatedChecklists = old.checklists.map((cl) => ({ ...cl, items: cl.items.map((ci) => ci.publicId === item.publicId ? { ...ci, ...(vars.title !== undefined ? { title: vars.title } : {}), ...(vars.completed !== undefined ? { completed: vars.completed } : {}), } : ci, ), })); 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 update checklist item`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, onSettled: async () => { await utils.card.byId.invalidate({ cardPublicId }); }, }); const deleteItem = api.checklist.deleteItem.useMutation({ onMutate: async () => { await utils.card.byId.cancel({ cardPublicId }); const previous = utils.card.byId.getData({ cardPublicId }); utils.card.byId.setData({ cardPublicId }, (old) => { if (!old) return old as any; const updatedChecklists = old.checklists.map((cl) => ({ ...cl, items: cl.items.filter((ci) => ci.publicId !== item.publicId), })); 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 delete checklist item`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, onSettled: async () => { await utils.card.byId.invalidate({ cardPublicId }); }, }); // Only resync from props when switching items to avoid clobbering edits useEffect(() => { setTitle(item.title); setCompleted(item.completed); // eslint-disable-next-line react-hooks/exhaustive-deps }, [item.publicId]); const sanitizeHtmlToPlainText = (html: string): string => html .replace(/(\n)?/gi, "\n") .replace(/
<\/div>/gi, "") .replace(/<[^>]*>/g, "") .replace(/ /g, " ") .trim(); const handleToggleCompleted = () => { if (viewOnly) return; setCompleted((prev) => !prev); updateItem.mutate({ checklistItemPublicId: item.publicId, completed: !completed, }); }; const commitTitle = (rawHtml: string) => { if (viewOnly) return; const plain = sanitizeHtmlToPlainText(rawHtml); if (!plain || plain === item.title) { setTitle(item.title); return; } setTitle(plain); updateItem.mutate({ checklistItemPublicId: item.publicId, title: plain, }); }; const handleDelete = () => { if (viewOnly) return; deleteItem.mutate({ checklistItemPublicId: item.publicId }); }; return (
setTitle(e.target.value)} // @ts-expect-error - valid event onBlur={(e: Event) => commitTitle(e.target.innerHTML as string)} 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...`} onKeyDown={(e) => { if (viewOnly) return; if (e.key === "Enter") { e.preventDefault(); commitTitle(title); } if (e.key === "Escape") { e.preventDefault(); setTitle(item.title); } }} />
{!viewOnly && ( )}
); }