feat: show checklists progress on cards
This commit is contained in:
@@ -45,10 +45,12 @@ export const formatMemberDisplayName = (
|
|||||||
return localPart.replace(/[_-]/g, ".");
|
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://")) {
|
if (imageOrKey.startsWith("http://") || imageOrKey.startsWith("https://")) {
|
||||||
return imageOrKey;
|
return imageOrKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${imageOrKey}`;
|
return `${env("NEXT_PUBLIC_STORAGE_URL")}/${env("NEXT_PUBLIC_AVATAR_BUCKET_NAME")}/${imageOrKey}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Avatar from "~/components/Avatar";
|
import Avatar from "~/components/Avatar";
|
||||||
import Badge from "~/components/Badge";
|
import Badge from "~/components/Badge";
|
||||||
|
import CircularProgress from "~/components/CircularProgress";
|
||||||
import LabelIcon from "~/components/LabelIcon";
|
import LabelIcon from "~/components/LabelIcon";
|
||||||
import { getAvatarUrl } from "~/utils/helpers";
|
import { getAvatarUrl } from "~/utils/helpers";
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ const Card = ({
|
|||||||
title,
|
title,
|
||||||
labels,
|
labels,
|
||||||
members,
|
members,
|
||||||
|
checklists,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
labels: { name: string; colourCode: string | null }[];
|
labels: { name: string; colourCode: string | null }[];
|
||||||
@@ -15,7 +17,28 @@ const Card = ({
|
|||||||
email: string;
|
email: string;
|
||||||
user: { name: string | null; email: string; image: string | null } | null;
|
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 (
|
return (
|
||||||
<div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
|
<div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
@@ -29,21 +52,37 @@ const Card = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
|
<div className="flex items-center justify-end gap-1">
|
||||||
{members.map(({ user, email }) => {
|
{checklists.length > 0 && (
|
||||||
const avatarUrl = user?.image
|
<div className="flex items-center gap-1 rounded-full border-[1px] border-light-300 px-2 py-1 dark:border-dark-600">
|
||||||
? getAvatarUrl(user.image)
|
<CircularProgress
|
||||||
: undefined;
|
progress={progress || 2}
|
||||||
|
|
||||||
return (
|
|
||||||
<Avatar
|
|
||||||
name={user?.name ?? ""}
|
|
||||||
email={user?.email ?? email}
|
|
||||||
imageUrl={avatarUrl}
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
);
|
<span className="text-[10px] text-light-900 dark:text-dark-950">
|
||||||
})}
|
{completedItems}/{totalItems}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{members.length > 0 && (
|
||||||
|
<div className="isolate flex justify-end -space-x-1 overflow-hidden">
|
||||||
|
{members.map(({ user, email }) => {
|
||||||
|
const avatarUrl = user?.image
|
||||||
|
? getAvatarUrl(user.image)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
name={user?.name ?? ""}
|
||||||
|
email={user?.email ?? email}
|
||||||
|
imageUrl={avatarUrl}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -238,15 +238,15 @@ export default function BoardPage() {
|
|||||||
const renderModalContent = () => {
|
const renderModalContent = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "DELETE_BOARD"}
|
isVisible={isOpen && modalContentType === "DELETE_BOARD"}
|
||||||
>
|
>
|
||||||
<DeleteBoardConfirmation boardPublicId={boardId ?? ""} />
|
<DeleteBoardConfirmation boardPublicId={boardId ?? ""} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "DELETE_LIST"}
|
isVisible={isOpen && modalContentType === "DELETE_LIST"}
|
||||||
>
|
>
|
||||||
<DeleteListConfirmation
|
<DeleteListConfirmation
|
||||||
@@ -255,8 +255,8 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="md"
|
modalSize="md"
|
||||||
isVisible={isOpen && modalContentType === "NEW_CARD"}
|
isVisible={isOpen && modalContentType === "NEW_CARD"}
|
||||||
>
|
>
|
||||||
<NewCardForm
|
<NewCardForm
|
||||||
@@ -266,8 +266,8 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "NEW_LIST"}
|
isVisible={isOpen && modalContentType === "NEW_LIST"}
|
||||||
>
|
>
|
||||||
<NewListForm
|
<NewListForm
|
||||||
@@ -276,22 +276,22 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
|
isVisible={isOpen && modalContentType === "NEW_WORKSPACE"}
|
||||||
>
|
>
|
||||||
<NewWorkspaceForm />
|
<NewWorkspaceForm />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "NEW_LABEL"}
|
isVisible={isOpen && modalContentType === "NEW_LABEL"}
|
||||||
>
|
>
|
||||||
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchBoard} />
|
<LabelForm boardPublicId={boardId ?? ""} refetch={refetchBoard} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "EDIT_LABEL"}
|
isVisible={isOpen && modalContentType === "EDIT_LABEL"}
|
||||||
>
|
>
|
||||||
<LabelForm
|
<LabelForm
|
||||||
@@ -301,8 +301,8 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "DELETE_LABEL"}
|
isVisible={isOpen && modalContentType === "DELETE_LABEL"}
|
||||||
>
|
>
|
||||||
<DeleteLabelConfirmation
|
<DeleteLabelConfirmation
|
||||||
@@ -311,8 +311,8 @@ export default function BoardPage() {
|
|||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
modalSize="sm"
|
modalSize="sm"
|
||||||
isVisible={isOpen && modalContentType === "UPDATE_BOARD_SLUG"}
|
isVisible={isOpen && modalContentType === "UPDATE_BOARD_SLUG"}
|
||||||
>
|
>
|
||||||
<UpdateBoardSlugForm
|
<UpdateBoardSlugForm
|
||||||
@@ -492,6 +492,7 @@ export default function BoardPage() {
|
|||||||
title={card.title}
|
title={card.title}
|
||||||
labels={card.labels}
|
labels={card.labels}
|
||||||
members={card.members}
|
members={card.members}
|
||||||
|
checklists={card.checklists}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function NewChecklistForm({ cardPublicId }: { cardPublicId: string }) {
|
|||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { register, handleSubmit, reset, setValue, watch } =
|
const { register, handleSubmit, reset, watch } =
|
||||||
useForm<NewChecklistFormInput>({
|
useForm<NewChecklistFormInput>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "Checklist",
|
name: "Checklist",
|
||||||
@@ -32,34 +32,45 @@ export function NewChecklistForm({ cardPublicId }: { cardPublicId: string }) {
|
|||||||
|
|
||||||
const createChecklist = api.checklist.create.useMutation({
|
const createChecklist = api.checklist.create.useMutation({
|
||||||
onMutate: async (args) => {
|
onMutate: async (args) => {
|
||||||
// await utils.board.byId.cancel();
|
await utils.card.byId.cancel({ cardPublicId: args.cardPublicId });
|
||||||
// const currentState = utils.board.byId.getData(queryParams);
|
const previous = utils.card.byId.getData({
|
||||||
// utils.board.byId.setData(queryParams, (oldBoard) => {
|
cardPublicId: args.cardPublicId,
|
||||||
// if (!oldBoard) return oldBoard;
|
});
|
||||||
// const newList = {
|
utils.card.byId.setData({ cardPublicId: args.cardPublicId }, (old) => {
|
||||||
// publicId: generateUID(),
|
if (!old) return old as any;
|
||||||
// name: args.name,
|
const placeholderChecklist = {
|
||||||
// boardId: 1,
|
publicId: `PLACEHOLDER_${generateUID()}`,
|
||||||
// boardPublicId,
|
name: args.name,
|
||||||
// cards: [],
|
index: old.checklists.length,
|
||||||
// index: oldBoard.lists.length,
|
items: [] as {
|
||||||
// };
|
publicId: string;
|
||||||
// const updatedLists = [...oldBoard.lists, newList];
|
title: string;
|
||||||
// return { ...oldBoard, lists: updatedLists };
|
completed: boolean;
|
||||||
// });
|
index: number;
|
||||||
// return { previousState: currentState };
|
}[],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
checklists: [...old.checklists, placeholderChecklist],
|
||||||
|
} as typeof old;
|
||||||
|
});
|
||||||
|
return { previous };
|
||||||
},
|
},
|
||||||
onError: (_error, _newList, context) => {
|
onError: (_error, vars, ctx) => {
|
||||||
// utils.board.byId.setData(queryParams, context?.previousState);
|
if (ctx?.previous)
|
||||||
|
utils.card.byId.setData(
|
||||||
|
{ cardPublicId: vars.cardPublicId },
|
||||||
|
ctx.previous,
|
||||||
|
);
|
||||||
showPopup({
|
showPopup({
|
||||||
header: t`Unable to create checklist`,
|
header: t`Unable to create checklist`,
|
||||||
message: t`Please try again later, or contact customer support.`,
|
message: t`Please try again later, or contact customer support.`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// onSettled: async () => {
|
onSettled: async (_data, _error, vars) => {
|
||||||
// await utils.board.byId.invalidate(queryParams);
|
await utils.card.byId.invalidate({ cardPublicId: vars.cardPublicId });
|
||||||
// },
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
cards,
|
cards,
|
||||||
cardsToLabels,
|
cardsToLabels,
|
||||||
cardToWorkspaceMembers,
|
cardToWorkspaceMembers,
|
||||||
|
checklistItems,
|
||||||
|
checklists,
|
||||||
labels,
|
labels,
|
||||||
lists,
|
lists,
|
||||||
workspaceMembers,
|
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(
|
where: and(
|
||||||
cardIds.length > 0 ? inArray(cards.publicId, cardIds) : undefined,
|
cardIds.length > 0 ? inArray(cards.publicId, cardIds) : undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user