feat: use plain text editor for checklist items

This commit is contained in:
Henry
2026-03-16 13:40:30 +00:00
parent 8b6e46fe63
commit 6852e83349

View File

@@ -1,11 +1,11 @@
import type { DraggableProvided } from "react-beautiful-dnd"; import type { DraggableProvided } from "react-beautiful-dnd";
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react"; import { useState } from "react";
import ContentEditable from "react-contenteditable";
import { HiXMark } from "react-icons/hi2"; import { HiXMark } from "react-icons/hi2";
import { RiDraggable } from "react-icons/ri"; import { RiDraggable } from "react-icons/ri";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import PlainTextEditor from "~/components/PlainTextEditor";
import { usePopup } from "~/providers/popup"; import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation"; import { invalidateCard } from "~/utils/cardInvalidation";
@@ -33,9 +33,7 @@ export default function ChecklistItemRow({
}: ChecklistItemRowProps) { }: ChecklistItemRowProps) {
const utils = api.useUtils(); const utils = api.useUtils();
const { showPopup } = usePopup(); const { showPopup } = usePopup();
const [completed, setCompleted] = useState(item.completed);
const [title, setTitle] = useState("");
const [completed, setCompleted] = useState(false);
const updateItem = api.checklist.updateItem.useMutation({ const updateItem = api.checklist.updateItem.useMutation({
onMutate: async (vars) => { onMutate: async (vars) => {
@@ -103,21 +101,6 @@ export default function ChecklistItemRow({
}, },
}); });
// 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(/<br\s*\/?>(\n)?/gi, "\n")
.replace(/<div><br\s*\/?><\/div>/gi, "")
.replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
const handleToggleCompleted = () => { const handleToggleCompleted = () => {
if (viewOnly) return; if (viewOnly) return;
setCompleted((prev) => !prev); setCompleted((prev) => !prev);
@@ -127,14 +110,8 @@ export default function ChecklistItemRow({
}); });
}; };
const commitTitle = (rawHtml: string) => { const commitTitle = (plain: string) => {
if (viewOnly) return; if (!plain || plain === item.title) return;
const plain = sanitizeHtmlToPlainText(rawHtml);
if (!plain || plain === item.title) {
setTitle(item.title);
return;
}
setTitle(plain);
updateItem.mutate({ updateItem.mutate({
checklistItemPublicId: item.publicId, checklistItemPublicId: item.publicId,
title: plain, title: plain,
@@ -183,36 +160,26 @@ export default function ChecklistItemRow({
)} )}
/> />
</label> </label>
<div className="flex-1 pr-7"> <div className="flex-1 pr-7">
<ContentEditable <PlainTextEditor
html={title} key={item.publicId}
disabled={viewOnly} content={item.title}
onChange={(e) => setTitle(e.target.value)} readOnly={viewOnly}
// @ts-expect-error - valid event placeholder={t`Add details...`}
onBlur={(e: Event) => { onBlur={commitTitle}
const innerHTML = (e.target as HTMLElement).innerHTML; onEnter={(plain) => {
commitTitle(innerHTML); commitTitle(plain);
onCreateNewItem?.();
}} }}
onEscape={() => undefined}
className={twMerge( 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", "m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-950 dark:text-dark-950",
viewOnly && "cursor-default", viewOnly && "cursor-default",
)} )}
placeholder={t`Add details...`}
onKeyDown={(e) => {
if (viewOnly) return;
if (e.key === "Enter") {
e.preventDefault();
const innerHTML = (e.currentTarget as HTMLElement).innerHTML;
commitTitle(innerHTML);
onCreateNewItem?.();
}
if (e.key === "Escape") {
e.preventDefault();
setTitle(item.title);
}
}}
/> />
</div> </div>
{!viewOnly && ( {!viewOnly && (
<button <button
type="button" type="button"