feat: delete checklist
This commit is contained in:
@@ -22,6 +22,9 @@ export default function ChecklistItemRow({
|
|||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
|
||||||
|
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) => {
|
||||||
await utils.card.byId.cancel({ cardPublicId });
|
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
|
// Only resync from props when switching items to avoid clobbering edits
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitle(item.title);
|
setTitle(item.title);
|
||||||
@@ -116,7 +116,10 @@ export default function ChecklistItemRow({
|
|||||||
|
|
||||||
const commitTitle = (rawHtml: string) => {
|
const commitTitle = (rawHtml: string) => {
|
||||||
const plain = sanitizeHtmlToPlainText(rawHtml);
|
const plain = sanitizeHtmlToPlainText(rawHtml);
|
||||||
if (!plain || plain === item.title) return;
|
if (!plain || plain === item.title) {
|
||||||
|
setTitle(item.title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTitle(plain);
|
setTitle(plain);
|
||||||
updateItem.mutate({
|
updateItem.mutate({
|
||||||
checklistItemPublicId: item.publicId,
|
checklistItemPublicId: item.publicId,
|
||||||
@@ -142,7 +145,8 @@ export default function ChecklistItemRow({
|
|||||||
<ContentEditable
|
<ContentEditable
|
||||||
html={title}
|
html={title}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
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"
|
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...`}
|
placeholder={t`Add details...`}
|
||||||
onKeyDown={(e) => {
|
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 ChecklistItemRow from "./components/ChecklistItemRow";
|
||||||
import ChecklistNameInput from "./components/ChecklistNameInput";
|
import ChecklistNameInput from "./components/ChecklistNameInput";
|
||||||
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation";
|
||||||
|
import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation";
|
||||||
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
import { DeleteCommentConfirmation } from "./components/DeleteCommentConfirmation";
|
||||||
import Dropdown from "./components/Dropdown";
|
import Dropdown from "./components/Dropdown";
|
||||||
import LabelSelector from "./components/LabelSelector";
|
import LabelSelector from "./components/LabelSelector";
|
||||||
@@ -139,7 +140,7 @@ export function CardRightPanel() {
|
|||||||
export default function CardPage() {
|
export default function CardPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { modalContentType, entityId } = useModal();
|
const { modalContentType, entityId, openModal } = useModal();
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
const [activeChecklistForm, setActiveChecklistForm] = useState<string | null>(
|
const [activeChecklistForm, setActiveChecklistForm] = useState<string | null>(
|
||||||
@@ -298,7 +299,15 @@ export default function CardPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<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} />
|
<HiXMark size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -397,6 +406,12 @@ export default function CardPage() {
|
|||||||
{modalContentType === "ADD_CHECKLIST" && (
|
{modalContentType === "ADD_CHECKLIST" && (
|
||||||
<NewChecklistForm cardPublicId={cardId} />
|
<NewChecklistForm cardPublicId={cardId} />
|
||||||
)}
|
)}
|
||||||
|
{modalContentType === "DELETE_CHECKLIST" && (
|
||||||
|
<DeleteChecklistConfirmation
|
||||||
|
cardPublicId={cardId}
|
||||||
|
checklistPublicId={entityId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -118,6 +118,63 @@ export const checklistRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return updated;
|
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
|
createItem: protectedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
|
|||||||
@@ -172,6 +172,37 @@ export const softDeleteItemById = async (
|
|||||||
return result;
|
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 (
|
export const updateChecklistById = async (
|
||||||
db: dbClient,
|
db: dbClient,
|
||||||
args: { id: number; name: string },
|
args: { id: number; name: string },
|
||||||
|
|||||||
Reference in New Issue
Block a user