diff --git a/apps/web/src/views/card/components/ChecklistItemRow.tsx b/apps/web/src/views/card/components/ChecklistItemRow.tsx
new file mode 100644
index 00000000..e29132a0
--- /dev/null
+++ b/apps/web/src/views/card/components/ChecklistItemRow.tsx
@@ -0,0 +1,172 @@
+import { t } from "@lingui/core/macro";
+import { useEffect, useState } from "react";
+import ContentEditable from "react-contenteditable";
+import { HiXMark } from "react-icons/hi2";
+
+import { usePopup } from "~/providers/popup";
+import { api } from "~/utils/api";
+
+interface ChecklistItemRowProps {
+ item: {
+ publicId: string;
+ title: string;
+ completed: boolean;
+ };
+ cardPublicId: string;
+}
+
+export default function ChecklistItemRow({
+ item,
+ cardPublicId,
+}: ChecklistItemRowProps) {
+ const utils = api.useUtils();
+ const { showPopup } = usePopup();
+
+ 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 });
+ },
+ });
+
+ const [title, setTitle] = useState(item.title);
+ const [completed, setCompleted] = useState(item.completed);
+
+ useEffect(() => {
+ setTitle(item.title);
+ setCompleted(item.completed);
+ }, [item.publicId, item.title, item.completed]);
+
+ const handleToggleCompleted = () => {
+ setCompleted((prev) => !prev);
+ updateItem.mutate({
+ checklistItemPublicId: item.publicId,
+ completed: !completed,
+ });
+ };
+
+ const commitTitle = async () => {
+ const trimmed = title.trim();
+ if (!trimmed || trimmed === item.title) return;
+ updateItem.mutate({
+ checklistItemPublicId: item.publicId,
+ title: trimmed,
+ });
+ };
+
+ const handleDelete = () => {
+ deleteItem.mutate({ checklistItemPublicId: item.publicId });
+ };
+
+ return (
+
+
+
+ setTitle(e.target.value)}
+ onBlur={commitTitle}
+ className="m-0 min-h-[20px] w-full p-0 text-[14px] leading-[20px] text-light-900 outline-none focus-visible:outline-none dark:text-dark-1000"
+ placeholder={t`Add details...`}
+ onKeyDown={async (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ await commitTitle();
+ }
+ if (e.key === "Escape") {
+ e.preventDefault();
+ setTitle(item.title);
+ }
+ }}
+ />
+
+
+
+ );
+}
diff --git a/apps/web/src/views/card/components/NewChecklistItemForm.tsx b/apps/web/src/views/card/components/NewChecklistItemForm.tsx
index 539544c9..04511fa5 100644
--- a/apps/web/src/views/card/components/NewChecklistItemForm.tsx
+++ b/apps/web/src/views/card/components/NewChecklistItemForm.tsx
@@ -1,8 +1,10 @@
import { t } from "@lingui/core/macro";
+import { useEffect, useRef } from "react";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
-import Button from "~/components/Button";
+import { generateUID } from "@kan/shared/utils";
+
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
@@ -24,7 +26,7 @@ const NewChecklistItemForm = ({
const utils = api.useUtils();
const { showPopup } = usePopup();
- const { handleSubmit, setValue, watch, reset } = useForm({
+ const { setValue, watch, reset, getValues } = useForm({
defaultValues: {
title: "",
},
@@ -32,8 +34,58 @@ const NewChecklistItemForm = ({
const title = watch("title");
+ const editableRef = useRef(null);
+ const keepOpenRef = useRef(false);
+
+ const refocusEditable = () => {
+ const el = editableRef.current;
+ if (!el) return;
+ setTimeout(() => {
+ el.focus();
+ const range = document.createRange();
+ range.selectNodeContents(el);
+ range.collapse(false);
+ const sel = window.getSelection();
+ if (sel) {
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }, 0);
+ };
+
const addChecklistItemMutation = api.checklist.createItem.useMutation({
- onError: (_error, _newItem) => {
+ 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 placeholder = {
+ publicId: `PLACEHOLDER_${generateUID()}`,
+ title: vars.title,
+ completed: false,
+ };
+ const updatedChecklists = old.checklists.map((cl) =>
+ cl.publicId === checklistPublicId
+ ? { ...cl, items: [...cl.items, placeholder] }
+ : cl,
+ );
+ return { ...old, checklists: updatedChecklists } as typeof old;
+ });
+
+ if (keepOpenRef.current) {
+ reset({ title: "" });
+ if (editableRef.current) editableRef.current.innerHTML = "";
+ refocusEditable();
+ } else {
+ onCancel();
+ }
+
+ return { previous };
+ },
+ onError: (_err, _vars, ctx) => {
+ if (ctx?.previous)
+ utils.card.byId.setData({ cardPublicId }, ctx.previous);
showPopup({
header: t`Unable to add checklist item`,
message: t`Please try again later, or contact customer support.`,
@@ -43,57 +95,81 @@ const NewChecklistItemForm = ({
onSettled: async () => {
await utils.card.byId.invalidate({ cardPublicId });
},
- onSuccess: async () => {
- reset();
- await utils.card.byId.refetch({ cardPublicId });
- },
});
- const onSubmit = (data: FormValues) => {
+ const sanitizeHtmlToPlainText = (html: string): string => {
+ return html
+ .replace(/
(\n)?/gi, "\n")
+ .replace(/<\/div>/gi, "")
+ .replace(/<[^>]*>/g, "")
+ .replace(/ /g, " ")
+ .trim();
+ };
+
+ const submitIfNotEmpty = (keepOpen: boolean) => {
+ keepOpenRef.current = keepOpen;
+ const currentHtml = getValues("title") ?? "";
+ const plain = sanitizeHtmlToPlainText(currentHtml);
+ if (!plain) {
+ onCancel();
+ return;
+ }
addChecklistItemMutation.mutate({
checklistPublicId,
- title: data.title,
+ title: plain,
});
};
- return (
-