feat: toggle checklist item completed state
This commit is contained in:
172
apps/web/src/views/card/components/ChecklistItemRow.tsx
Normal file
172
apps/web/src/views/card/components/ChecklistItemRow.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="group relative flex h-9 items-center gap-3 rounded-md pl-4 hover:bg-light-100 dark:hover:bg-dark-100">
|
||||||
|
<label className="relative inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-pointer items-center justify-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={completed}
|
||||||
|
onChange={handleToggleCompleted}
|
||||||
|
className="peer h-[16px] w-[16px] appearance-none rounded-full border border-light-500 bg-transparent outline-none ring-0 checked:bg-indigo-600 hover:border-light-500 hover:bg-transparent focus:outline-none focus:ring-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className="pointer-events-none absolute left-1/2 top-1/2 hidden h-[12px] w-[12px] -translate-x-1/2 -translate-y-1/2 text-white peer-checked:block"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5 10.5l3 3 7-7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 pr-7">
|
||||||
|
<ContentEditable
|
||||||
|
html={title}
|
||||||
|
onChange={(e) => 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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="absolute right-1 top-1/2 hidden -translate-y-1/2 rounded-md p-1 text-light-900 group-hover:block hover:bg-light-200 dark:text-dark-700 dark:hover:bg-dark-200"
|
||||||
|
>
|
||||||
|
<HiXMark size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import ContentEditable from "react-contenteditable";
|
import ContentEditable from "react-contenteditable";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import { generateUID } from "@kan/shared/utils";
|
||||||
|
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ const NewChecklistItemForm = ({
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
|
||||||
const { handleSubmit, setValue, watch, reset } = useForm<FormValues>({
|
const { setValue, watch, reset, getValues } = useForm<FormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
title: "",
|
||||||
},
|
},
|
||||||
@@ -32,8 +34,58 @@ const NewChecklistItemForm = ({
|
|||||||
|
|
||||||
const title = watch("title");
|
const title = watch("title");
|
||||||
|
|
||||||
|
const editableRef = useRef<HTMLElement | null>(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({
|
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({
|
showPopup({
|
||||||
header: t`Unable to add checklist item`,
|
header: t`Unable to add checklist item`,
|
||||||
message: t`Please try again later, or contact customer support.`,
|
message: t`Please try again later, or contact customer support.`,
|
||||||
@@ -43,57 +95,81 @@ const NewChecklistItemForm = ({
|
|||||||
onSettled: async () => {
|
onSettled: async () => {
|
||||||
await utils.card.byId.invalidate({ cardPublicId });
|
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(/<br\s*\/?>(\n)?/gi, "\n")
|
||||||
|
.replace(/<div><br\s*\/?><\/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({
|
addChecklistItemMutation.mutate({
|
||||||
checklistPublicId,
|
checklistPublicId,
|
||||||
title: data.title,
|
title: plain,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<form
|
refocusEditable();
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
}, []);
|
||||||
className="mt-2 w-full rounded-xl border border-light-600 bg-light-100 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000"
|
|
||||||
>
|
|
||||||
<div className="mb-3">
|
|
||||||
<ContentEditable
|
|
||||||
placeholder={t`Add an item...`}
|
|
||||||
html={title}
|
|
||||||
disabled={false}
|
|
||||||
onChange={(e) => setValue("title", e.target.value)}
|
|
||||||
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
|
||||||
onKeyDown={async (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
await handleSubmit(onSubmit)();
|
|
||||||
}
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault();
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
return (
|
||||||
<div className="flex space-x-2">
|
<form onSubmit={(e) => e.preventDefault()} className="mt-1">
|
||||||
<Button size="xs" type="button" variant="ghost" onClick={onCancel}>
|
<div className="group relative flex h-9 items-center gap-3 rounded-md pl-4 hover:bg-light-100 dark:hover:bg-dark-100">
|
||||||
{t`Cancel`}
|
<label className="relative inline-flex h-[16px] w-[16px] flex-shrink-0 cursor-default items-center justify-center">
|
||||||
</Button>
|
<input
|
||||||
<Button
|
type="checkbox"
|
||||||
size="xs"
|
disabled
|
||||||
type="submit"
|
className="peer h-[16px] w-[16px] appearance-none rounded-full border border-light-500 bg-transparent outline-none ring-0 hover:border-light-500 hover:bg-transparent focus:outline-none focus:ring-0 focus-visible:outline-none dark:border-dark-500 dark:hover:border-dark-500"
|
||||||
variant="secondary"
|
/>
|
||||||
disabled={title.length === 0 || addChecklistItemMutation.isPending}
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className="pointer-events-none absolute left-1/2 top-1/2 hidden h-[12px] w-[12px] -translate-x-1/2 -translate-y-1/2 text-white peer-checked:block"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
{t`Add`}
|
<path
|
||||||
</Button>
|
d="M5 10.5l3 3 7-7"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</label>
|
||||||
|
<div className="flex-1 pr-7">
|
||||||
|
<ContentEditable
|
||||||
|
placeholder={t`Add an item...`}
|
||||||
|
html={title}
|
||||||
|
disabled={false}
|
||||||
|
onChange={(e) => setValue("title", e.target.value)}
|
||||||
|
className="m-0 min-h-[20px] w-full p-0 text-sm leading-5 text-light-900 outline-none focus-visible:outline-none dark:text-dark-1000"
|
||||||
|
onBlur={() => submitIfNotEmpty(false)}
|
||||||
|
onKeyDown={async (e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
submitIfNotEmpty(true);
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
innerRef={(el) => {
|
||||||
|
editableRef.current = (el as unknown as HTMLElement) ?? null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { api } from "~/utils/api";
|
|||||||
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
|
import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers";
|
||||||
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
|
import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation";
|
||||||
import ActivityList from "./components/ActivityList";
|
import ActivityList from "./components/ActivityList";
|
||||||
|
import ChecklistItemRow from "./components/ChecklistItemRow";
|
||||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||||
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
||||||
import Dropdown from "./components/Dropdown";
|
import Dropdown from "./components/Dropdown";
|
||||||
@@ -273,11 +274,9 @@ export default function CardPage() {
|
|||||||
100
|
100
|
||||||
: 2;
|
: 2;
|
||||||
|
|
||||||
console.log({ checklist });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={checklist.publicId}>
|
<div key={checklist.publicId} className="mb-4">
|
||||||
<div className="text-md mb-4 flex items-center justify-between font-medium text-light-900 dark:text-dark-1000">
|
<div className="text-md mb-2 flex items-center justify-between font-medium text-light-900 dark:text-dark-1000">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{checklist.name}</span>
|
<span>{checklist.name}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,12 +307,29 @@ export default function CardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-1">
|
||||||
|
{checklist.items.map((item) => (
|
||||||
|
<ChecklistItemRow
|
||||||
|
key={item.publicId}
|
||||||
|
item={{
|
||||||
|
publicId: item.publicId,
|
||||||
|
title: item.title,
|
||||||
|
completed: item.completed,
|
||||||
|
}}
|
||||||
|
cardPublicId={cardId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{activeChecklistForm === checklist.publicId && (
|
{activeChecklistForm === checklist.publicId && (
|
||||||
<NewChecklistItemForm
|
<div className="ml-1">
|
||||||
checklistPublicId={checklist.publicId}
|
<NewChecklistItemForm
|
||||||
cardPublicId={cardId}
|
checklistPublicId={checklist.publicId}
|
||||||
onCancel={() => setActiveChecklistForm(null)}
|
cardPublicId={cardId}
|
||||||
/>
|
onCancel={() => setActiveChecklistForm(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,14 +71,6 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
});
|
});
|
||||||
|
|
||||||
// await cardActivityRepo.create(ctx.db, {
|
|
||||||
// type: "card.updated.checklist.added" as const,
|
|
||||||
// cardId: card.id,
|
|
||||||
// checklistId: newChecklist.id,
|
|
||||||
// toChecklist: newChecklist.title,
|
|
||||||
// createdBy: userId,
|
|
||||||
// });
|
|
||||||
|
|
||||||
return newChecklist;
|
return newChecklist;
|
||||||
}),
|
}),
|
||||||
createItem: protectedProcedure
|
createItem: protectedProcedure
|
||||||
@@ -139,6 +131,116 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return newChecklistItem;
|
return newChecklistItem;
|
||||||
}),
|
}),
|
||||||
|
updateItem: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update a checklist item",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/checklists/items/{checklistItemPublicId}",
|
||||||
|
description: "Updates a checklist item (title/completed)",
|
||||||
|
tags: ["Cards"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
checklistItemPublicId: z.string().length(12),
|
||||||
|
title: z.string().min(1).max(500).optional(),
|
||||||
|
completed: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(checklistItemSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `User not authenticated`,
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await checklistRepo.getChecklistItemByPublicIdWithChecklist(
|
||||||
|
ctx.db,
|
||||||
|
input.checklistItemPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!item)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Checklist item with public ID ${input.checklistItemPublicId} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertUserInWorkspace(
|
||||||
|
ctx.db,
|
||||||
|
userId,
|
||||||
|
item.checklist.card.list.board.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updated = await checklistRepo.updateItemById(ctx.db, {
|
||||||
|
id: item.id,
|
||||||
|
title: input.title,
|
||||||
|
completed: input.completed,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to update checklist item`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}),
|
||||||
|
deleteItem: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Delete a checklist item",
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/checklists/items/{checklistItemPublicId}",
|
||||||
|
description: "Deletes a checklist item",
|
||||||
|
tags: ["Cards"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.object({ checklistItemPublicId: z.string().length(12) }))
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
if (!userId)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `User not authenticated`,
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
|
||||||
|
const item = await checklistRepo.getChecklistItemByPublicIdWithChecklist(
|
||||||
|
ctx.db,
|
||||||
|
input.checklistItemPublicId,
|
||||||
|
);
|
||||||
|
if (!item)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Checklist item with public ID ${input.checklistItemPublicId} not found`,
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertUserInWorkspace(
|
||||||
|
ctx.db,
|
||||||
|
userId,
|
||||||
|
item.checklist.card.list.board.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleted = await checklistRepo.softDeleteItemById(ctx.db, {
|
||||||
|
id: item.id,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
deletedBy: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deleted)
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `Failed to delete item`,
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
// update: protectedProcedure
|
// update: protectedProcedure
|
||||||
// .meta({
|
// .meta({
|
||||||
// openapi: {
|
// openapi: {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cards,
|
cards,
|
||||||
cardsToLabels,
|
cardsToLabels,
|
||||||
cardToWorkspaceMembers,
|
cardToWorkspaceMembers,
|
||||||
|
checklistItems,
|
||||||
checklists,
|
checklists,
|
||||||
labels,
|
labels,
|
||||||
lists,
|
lists,
|
||||||
@@ -316,6 +317,7 @@ export const getWithListAndMembersByPublicId = async (
|
|||||||
completed: true,
|
completed: true,
|
||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
|
where: isNull(checklistItems.deletedAt),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,3 +106,68 @@ export const getChecklistByPublicId = async (
|
|||||||
|
|
||||||
return checklist;
|
return checklist;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getChecklistItemByPublicIdWithChecklist = async (
|
||||||
|
db: dbClient,
|
||||||
|
checklistItemPublicId: string,
|
||||||
|
) => {
|
||||||
|
const item = await db.query.checklistItems.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(checklistItems.publicId, checklistItemPublicId),
|
||||||
|
isNull(checklistItems.deletedAt),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
checklist: {
|
||||||
|
with: {
|
||||||
|
card: {
|
||||||
|
with: {
|
||||||
|
list: {
|
||||||
|
with: {
|
||||||
|
board: {
|
||||||
|
with: { workspace: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateItemById = async (
|
||||||
|
db: dbClient,
|
||||||
|
args: { id: number; title?: string; completed?: boolean },
|
||||||
|
) => {
|
||||||
|
const [result] = await db
|
||||||
|
.update(checklistItems)
|
||||||
|
.set({
|
||||||
|
...(args.title !== undefined ? { title: args.title } : {}),
|
||||||
|
...(args.completed !== undefined ? { completed: args.completed } : {}),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(checklistItems.id, args.id))
|
||||||
|
.returning({
|
||||||
|
publicId: checklistItems.publicId,
|
||||||
|
title: checklistItems.title,
|
||||||
|
completed: checklistItems.completed,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const softDeleteItemById = async (
|
||||||
|
db: dbClient,
|
||||||
|
args: { id: number; deletedAt: Date; deletedBy: string },
|
||||||
|
) => {
|
||||||
|
const [result] = await db
|
||||||
|
.update(checklistItems)
|
||||||
|
.set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
|
||||||
|
.where(eq(checklistItems.id, args.id))
|
||||||
|
.returning({ id: checklistItems.id });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user