From a4889687783f972471c72c72cf96dea153cc9208 Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 12 Aug 2025 22:21:11 +0100 Subject: [PATCH] feat: show checklists progress on cards --- apps/web/src/utils/helpers.ts | 6 +- apps/web/src/views/board/components/Card.tsx | 65 +++++++++++++++---- apps/web/src/views/board/index.tsx | 37 ++++++----- .../card/components/NewChecklistForm.tsx | 55 +++++++++------- packages/db/src/repository/board.repo.ts | 23 +++++++ 5 files changed, 131 insertions(+), 55 deletions(-) diff --git a/apps/web/src/utils/helpers.ts b/apps/web/src/utils/helpers.ts index 9db6af73..b57b36d3 100644 --- a/apps/web/src/utils/helpers.ts +++ b/apps/web/src/utils/helpers.ts @@ -45,10 +45,12 @@ export const formatMemberDisplayName = ( return localPart.replace(/[_-]/g, "."); }; -export const getAvatarUrl = (imageOrKey: string) => { +export const getAvatarUrl = (imageOrKey: string | null) => { + if (!imageOrKey) return ""; + if (imageOrKey.startsWith("http://") || imageOrKey.startsWith("https://")) { return imageOrKey; } - + return `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${imageOrKey}`; }; diff --git a/apps/web/src/views/board/components/Card.tsx b/apps/web/src/views/board/components/Card.tsx index 2d5d48b1..08a1eaff 100644 --- a/apps/web/src/views/board/components/Card.tsx +++ b/apps/web/src/views/board/components/Card.tsx @@ -1,5 +1,6 @@ import Avatar from "~/components/Avatar"; import Badge from "~/components/Badge"; +import CircularProgress from "~/components/CircularProgress"; import LabelIcon from "~/components/LabelIcon"; import { getAvatarUrl } from "~/utils/helpers"; @@ -7,6 +8,7 @@ const Card = ({ title, labels, members, + checklists, }: { title: string; labels: { name: string; colourCode: string | null }[]; @@ -15,7 +17,28 @@ const Card = ({ email: string; user: { name: string | null; email: string; image: string | null } | null; }[]; + checklists: { + publicId: string; + name: string; + items: { + publicId: string; + title: string; + completed: boolean; + index: number; + }[]; + }[]; }) => { + const completedItems = checklists.reduce((acc, checklist) => { + return acc + checklist.items.filter((item) => item.completed).length; + }, 0); + + const totalItems = checklists.reduce((acc, checklist) => { + return acc + checklist.items.length; + }, 0); + + const progress = + totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0; + return (
{title} @@ -29,21 +52,37 @@ const Card = ({ /> ))}
-
- {members.map(({ user, email }) => { - const avatarUrl = user?.image - ? getAvatarUrl(user.image) - : undefined; - - return ( - + {checklists.length > 0 && ( +
+ - ); - })} + + {completedItems}/{totalItems} + +
+ )} + {members.length > 0 && ( +
+ {members.map(({ user, email }) => { + const avatarUrl = user?.image + ? getAvatarUrl(user.image) + : undefined; + + return ( + + ); + })} +
+ )}
) : null} diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 26d3fd87..0e4ce5a2 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -238,15 +238,15 @@ export default function BoardPage() { const renderModalContent = () => { return ( <> - - - - - - - - - )} diff --git a/apps/web/src/views/card/components/NewChecklistForm.tsx b/apps/web/src/views/card/components/NewChecklistForm.tsx index e76ed4b5..4d15865d 100644 --- a/apps/web/src/views/card/components/NewChecklistForm.tsx +++ b/apps/web/src/views/card/components/NewChecklistForm.tsx @@ -22,7 +22,7 @@ export function NewChecklistForm({ cardPublicId }: { cardPublicId: string }) { const utils = api.useUtils(); - const { register, handleSubmit, reset, setValue, watch } = + const { register, handleSubmit, reset, watch } = useForm({ defaultValues: { name: "Checklist", @@ -32,34 +32,45 @@ export function NewChecklistForm({ cardPublicId }: { cardPublicId: string }) { const createChecklist = api.checklist.create.useMutation({ onMutate: async (args) => { - // await utils.board.byId.cancel(); - // const currentState = utils.board.byId.getData(queryParams); - // utils.board.byId.setData(queryParams, (oldBoard) => { - // if (!oldBoard) return oldBoard; - // const newList = { - // publicId: generateUID(), - // name: args.name, - // boardId: 1, - // boardPublicId, - // cards: [], - // index: oldBoard.lists.length, - // }; - // const updatedLists = [...oldBoard.lists, newList]; - // return { ...oldBoard, lists: updatedLists }; - // }); - // return { previousState: currentState }; + await utils.card.byId.cancel({ cardPublicId: args.cardPublicId }); + const previous = utils.card.byId.getData({ + cardPublicId: args.cardPublicId, + }); + utils.card.byId.setData({ cardPublicId: args.cardPublicId }, (old) => { + if (!old) return old as any; + const placeholderChecklist = { + publicId: `PLACEHOLDER_${generateUID()}`, + name: args.name, + index: old.checklists.length, + items: [] as { + publicId: string; + title: string; + completed: boolean; + index: number; + }[], + }; + return { + ...old, + checklists: [...old.checklists, placeholderChecklist], + } as typeof old; + }); + return { previous }; }, - onError: (_error, _newList, context) => { - // utils.board.byId.setData(queryParams, context?.previousState); + onError: (_error, vars, ctx) => { + if (ctx?.previous) + utils.card.byId.setData( + { cardPublicId: vars.cardPublicId }, + ctx.previous, + ); showPopup({ header: t`Unable to create checklist`, message: t`Please try again later, or contact customer support.`, icon: "error", }); }, - // onSettled: async () => { - // await utils.board.byId.invalidate(queryParams); - // }, + onSettled: async (_data, _error, vars) => { + await utils.card.byId.invalidate({ cardPublicId: vars.cardPublicId }); + }, }); useEffect(() => { diff --git a/packages/db/src/repository/board.repo.ts b/packages/db/src/repository/board.repo.ts index 6e4a5057..42219ec8 100644 --- a/packages/db/src/repository/board.repo.ts +++ b/packages/db/src/repository/board.repo.ts @@ -7,6 +7,8 @@ import { cards, cardsToLabels, cardToWorkspaceMembers, + checklistItems, + checklists, labels, lists, workspaceMembers, @@ -164,6 +166,27 @@ export const getByPublicId = async ( }, }, }, + checklists: { + columns: { + publicId: true, + name: true, + index: true, + }, + where: isNull(checklists.deletedAt), + orderBy: asc(checklists.index), + with: { + items: { + columns: { + publicId: true, + title: true, + completed: true, + index: true, + }, + where: isNull(checklistItems.deletedAt), + orderBy: asc(checklistItems.index), + }, + }, + }, }, where: and( cardIds.length > 0 ? inArray(cards.publicId, cardIds) : undefined,