feat: delete checklist
This commit is contained in:
@@ -22,6 +22,9 @@ export default function ChecklistItemRow({
|
||||
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 });
|
||||
@@ -88,9 +91,6 @@ export default function ChecklistItemRow({
|
||||
},
|
||||
});
|
||||
|
||||
const [title, setTitle] = useState(item.title);
|
||||
const [completed, setCompleted] = useState(item.completed);
|
||||
|
||||
// Only resync from props when switching items to avoid clobbering edits
|
||||
useEffect(() => {
|
||||
setTitle(item.title);
|
||||
@@ -116,7 +116,10 @@ export default function ChecklistItemRow({
|
||||
|
||||
const commitTitle = (rawHtml: string) => {
|
||||
const plain = sanitizeHtmlToPlainText(rawHtml);
|
||||
if (!plain || plain === item.title) return;
|
||||
if (!plain || plain === item.title) {
|
||||
setTitle(item.title);
|
||||
return;
|
||||
}
|
||||
setTitle(plain);
|
||||
updateItem.mutate({
|
||||
checklistItemPublicId: item.publicId,
|
||||
@@ -142,7 +145,8 @@ export default function ChecklistItemRow({
|
||||
<ContentEditable
|
||||
html={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onBlur={() => commitTitle(title)}
|
||||
// @ts-expect-error - valid event
|
||||
onBlur={(e: Event) => commitTitle(e.target.innerHTML as string)}
|
||||
className="m-0 min-h-[20px] w-full p-0 text-sm leading-[20px] text-light-900 outline-none focus-visible:outline-none dark:text-dark-950"
|
||||
placeholder={t`Add details...`}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { t } from "@lingui/core/macro";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function DeleteChecklistConfirmation({
|
||||
cardPublicId,
|
||||
checklistPublicId,
|
||||
}: {
|
||||
cardPublicId: string;
|
||||
checklistPublicId: string;
|
||||
}) {
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const deleteChecklist = api.checklist.delete.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.filter(
|
||||
(cl) => cl.publicId !== checklistPublicId,
|
||||
);
|
||||
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`,
|
||||
message: t`Please try again later, or contact customer support.`,
|
||||
icon: "error",
|
||||
});
|
||||
},
|
||||
onSettled: async () => {
|
||||
closeModal();
|
||||
await utils.card.byId.invalidate({ cardPublicId });
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteChecklist.mutate({ checklistPublicId });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5">
|
||||
<div className="flex w-full flex-col justify-between pb-4">
|
||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||
{t`Are you sure you want to delete this checklist?`}
|
||||
</h2>
|
||||
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||
{t`This action can't be undone.`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
|
||||
<Button variant="secondary" onClick={() => closeModal()}>
|
||||
{t`Cancel`}
|
||||
</Button>
|
||||
<Button onClick={handleDelete} isLoading={deleteChecklist.isPending}>
|
||||
{t`Delete`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import ActivityList from "./components/ActivityList";
|
||||
import ChecklistItemRow from "./components/ChecklistItemRow";
|
||||
import ChecklistNameInput from "./components/ChecklistNameInput";
|
||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
|
||||
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
||||
import Dropdown from "./components/Dropdown";
|
||||
import LabelSelector from "./components/LabelSelector";
|
||||
@@ -139,7 +140,7 @@ export function CardRightPanel() {
|
||||
export default function CardPage() {
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
const { modalContentType, entityId } = useModal();
|
||||
const { modalContentType, entityId, openModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const { workspace } = useWorkspace();
|
||||
const [activeChecklistForm, setActiveChecklistForm] = useState<string | null>(
|
||||
@@ -298,7 +299,15 @@ export default function CardPage() {
|
||||
</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">
|
||||
<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
|
||||
@@ -397,6 +406,12 @@ export default function CardPage() {
|
||||
{modalContentType === "ADD_CHECKLIST" && (
|
||||
<NewChecklistForm cardPublicId={cardId} />
|
||||
)}
|
||||
{modalContentType === "DELETE_CHECKLIST" && (
|
||||
<DeleteChecklistConfirmation
|
||||
cardPublicId={cardId}
|
||||
checklistPublicId={entityId}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -118,6 +118,63 @@ export const checklistRouter = createTRPCRouter({
|
||||
|
||||
return updated;
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
summary: "Delete a checklist",
|
||||
method: "DELETE",
|
||||
path: "/checklists/{checklistPublicId}",
|
||||
description: "Deletes a checklist by its public ID",
|
||||
tags: ["Cards"],
|
||||
protect: true,
|
||||
},
|
||||
})
|
||||
.input(z.object({ checklistPublicId: 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 checklist = await checklistRepo.getChecklistByPublicId(
|
||||
ctx.db,
|
||||
input.checklistPublicId,
|
||||
);
|
||||
if (!checklist)
|
||||
throw new TRPCError({
|
||||
message: `Checklist with public ID ${input.checklistPublicId} not found`,
|
||||
code: "NOT_FOUND",
|
||||
});
|
||||
|
||||
await assertUserInWorkspace(
|
||||
ctx.db,
|
||||
userId,
|
||||
checklist.card.list.board.workspace.id,
|
||||
);
|
||||
|
||||
await checklistRepo.softDeleteAllItemsByChecklistId(ctx.db, {
|
||||
checklistId: checklist.id,
|
||||
deletedAt: new Date(),
|
||||
deletedBy: userId,
|
||||
});
|
||||
|
||||
const deleted = await checklistRepo.softDeleteById(ctx.db, {
|
||||
id: checklist.id,
|
||||
deletedAt: new Date(),
|
||||
deletedBy: userId,
|
||||
});
|
||||
|
||||
if (!deleted)
|
||||
throw new TRPCError({
|
||||
message: `Failed to delete checklist`,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
createItem: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
|
||||
@@ -172,6 +172,37 @@ export const softDeleteItemById = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const softDeleteAllItemsByChecklistId = async (
|
||||
db: dbClient,
|
||||
args: { checklistId: number; deletedAt: Date; deletedBy: string },
|
||||
) => {
|
||||
const result = await db
|
||||
.update(checklistItems)
|
||||
.set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
|
||||
.where(
|
||||
and(
|
||||
eq(checklistItems.checklistId, args.checklistId),
|
||||
isNull(checklistItems.deletedAt),
|
||||
),
|
||||
)
|
||||
.returning({ id: checklistItems.id });
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const softDeleteById = async (
|
||||
db: dbClient,
|
||||
args: { id: number; deletedAt: Date; deletedBy: string },
|
||||
) => {
|
||||
const [result] = await db
|
||||
.update(checklists)
|
||||
.set({ deletedAt: args.deletedAt, deletedBy: args.deletedBy })
|
||||
.where(eq(checklists.id, args.id))
|
||||
.returning({ id: checklists.id });
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const updateChecklistById = async (
|
||||
db: dbClient,
|
||||
args: { id: number; name: string },
|
||||
|
||||
Reference in New Issue
Block a user