feat: optimisic card updates

This commit is contained in:
Henry
2025-02-02 10:33:32 +00:00
parent 14b4d73fb6
commit eba5d9335c
6 changed files with 122 additions and 19 deletions

View File

@@ -24,7 +24,10 @@ interface CheckboxDropdownProps {
createNewItemLabel?: string;
menuSpacing?: "sm" | "md" | "lg";
position?: "left" | "right";
handleSelect: (groupKey: string | null, item: { key: string }) => void;
handleSelect: (
groupKey: string | null,
item: { key: string; value: string },
) => void;
handleEdit?: (key: string) => void;
handleCreate?: () => void;
asChild?: boolean;
@@ -58,7 +61,7 @@ export default function CheckboxDropdown({
className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
handleSelect(groupKey, { key: item.key });
handleSelect(groupKey, { key: item.key, value: item.value });
}}
>
<input
@@ -67,7 +70,9 @@ export default function CheckboxDropdown({
type="checkbox"
className="h-[14px] w-[14px] rounded bg-transparent"
onClick={(event) => event.stopPropagation()}
onChange={() => handleSelect(groupKey, { key: item.key })}
onChange={() =>
handleSelect(groupKey, { key: item.key, value: item.value })
}
checked={item.selected}
/>
{item.leftIcon && (

View File

@@ -9,7 +9,7 @@ import {
HiOutlineUserPlus,
} from "react-icons/hi2";
import { type GetCardByIdOutput } from "@kan/api/types";
import type { GetCardByIdOutput } from "@kan/api/types";
import Avatar from "~/components/Avatar";
import Comment from "./Comment";
@@ -148,7 +148,7 @@ const ActivityList = ({
}) => {
return (
<div className="flex flex-col space-y-4 pt-4">
{activities?.map((activity, index) => {
{activities.map((activity, index) => {
const activityText = getActivityText({
type: activity.type,
toTitle: activity.toTitle,

View File

@@ -14,12 +14,14 @@ interface LabelSelectorProps {
selected: boolean;
leftIcon: React.ReactNode;
}[];
handleSelectLabel: (labelPublicId: string) => void;
isLoading: boolean;
}
export default function LabelSelector({
cardPublicId,
labels,
handleSelectLabel,
isLoading,
}: LabelSelectorProps) {
const { openModal } = useModal();
@@ -44,9 +46,10 @@ export default function LabelSelector({
) : (
<CheckboxDropdown
items={labels}
handleSelect={(_, label) =>
addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key })
}
handleSelect={(_, label) => {
handleSelectLabel(label.key);
addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key });
}}
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
handleCreate={() => openModal("NEW_LABEL")}
createNewItemLabel="Create new label"

View File

@@ -10,12 +10,14 @@ interface ListSelectorProps {
value: string;
selected: boolean;
}[];
handleChangeList: (newListPublicId: string, newListName: string) => void;
isLoading: boolean;
}
export default function ListSelector({
cardPublicId,
lists,
handleChangeList,
isLoading,
}: ListSelectorProps) {
const utils = api.useUtils();
@@ -39,12 +41,13 @@ export default function ListSelector({
) : (
<CheckboxDropdown
items={lists}
handleSelect={(_, member) =>
handleSelect={(_, member) => {
handleChangeList(member.key, member.value);
updateCardList.mutate({
cardPublicId,
newListPublicId: member.key,
})
}
});
}}
asChild
>
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 py-1 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">

View File

@@ -5,6 +5,7 @@ import { HiMiniPlus } from "react-icons/hi2";
import Avatar from "~/components/Avatar";
import CheckboxDropdown from "~/components/CheckboxDropdown";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
interface MemberSelectorProps {
@@ -16,16 +17,19 @@ interface MemberSelectorProps {
leftIcon: React.ReactNode;
imageUrl: string | undefined;
}[];
handleSelectMember: (memberPublicId: string) => void;
isLoading: boolean;
}
export default function MemberSelector({
cardPublicId,
members,
handleSelectMember,
isLoading,
}: MemberSelectorProps) {
const router = useRouter();
const { openModal } = useModal();
const { showPopup } = usePopup();
const utils = api.useUtils();
const refetchCard = () => utils.card.byId.refetch({ cardPublicId });
@@ -34,6 +38,14 @@ export default function MemberSelector({
onSuccess: async () => {
await refetchCard();
},
onError: async () => {
await refetchCard();
showPopup({
header: "Unable to update members",
message: "Please try again later, or contact customer support.",
icon: "error",
});
},
});
const selectedMembers = members.filter((member) => member.selected);
@@ -52,12 +64,13 @@ export default function MemberSelector({
) : (
<CheckboxDropdown
items={members}
handleSelect={(_, member) =>
handleSelect={(_, member) => {
handleSelectMember(member.key);
addOrRemoveMember.mutate({
cardPublicId,
workspaceMemberPublicId: member.key,
})
}
});
}}
handleCreate={handleInviteMember}
createNewItemLabel="Invite member"
asChild

View File

@@ -1,9 +1,12 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5";
import type { GetCardByIdOutput } from "@kan/api/types";
import Avatar from "~/components/Avatar";
import LabelIcon from "~/components/LabelIcon";
import Modal from "~/components/modal";
@@ -35,6 +38,7 @@ export default function CardPage() {
const utils = api.useUtils();
const { modalContentType, entityId } = useModal();
const { showPopup } = usePopup();
const [card, setCard] = useState<GetCardByIdOutput>();
const cardId = Array.isArray(router.query.cardId)
? router.query.cardId[0]
@@ -44,13 +48,85 @@ export default function CardPage() {
cardPublicId: cardId ?? "",
});
const board = data?.list?.board;
useEffect(() => {
setCard(data);
}, [data]);
const board = card?.list?.board;
const boardId = board?.publicId;
const labels = board?.labels;
const activities = data?.activities;
const activities = card?.activities;
const workspaceMembers = board?.workspace?.members;
const selectedLabels = data?.labels;
const selectedMembers = data?.members;
const selectedLabels = card?.labels;
const selectedMembers = card?.members;
const handleChangeList = (newListPublicId: string) => {
if (!card) return;
setCard({
...card,
list: {
...card.list,
publicId: newListPublicId,
name: card.list?.name ?? "",
board: card.list?.board ?? null,
},
});
};
const handleSelectLabel = (labelPublicId: string) => {
if (!card) return;
const isSelected = card.labels.some(
(label) => label.publicId === labelPublicId,
);
const updatedLabels = isSelected
? card.labels.filter((label) => label.publicId !== labelPublicId)
: [
...(card.labels ?? []),
labels?.find((label) => label.publicId === labelPublicId),
].filter(Boolean);
setCard({
...card,
labels: updatedLabels as {
publicId: string;
name: string;
colourCode: string | null;
}[],
});
};
const handleSelectMember = (memberPublicId: string) => {
if (!card) return;
const isSelected = card.members.some(
(member) => member.publicId === memberPublicId,
);
const updatedMembers = isSelected
? card.members.filter((member) => member.publicId !== memberPublicId)
: [
...(card.members ?? []),
workspaceMembers?.find(
(member) => member.publicId === memberPublicId,
),
];
setCard({
...card,
members: updatedMembers.filter(Boolean) as {
publicId: string;
user: {
id: string;
name: string | null;
email: string;
image: string | null;
} | null;
}[],
});
};
const formattedLabels =
labels?.map((label) => {
@@ -70,7 +146,7 @@ export default function CardPage() {
board?.lists.map((list) => ({
key: list.publicId,
value: list.name,
selected: list.publicId === data?.list?.publicId,
selected: list.publicId === card?.list?.publicId,
})) ?? [];
const formattedMembers =
@@ -219,6 +295,7 @@ export default function CardPage() {
<ListSelector
cardPublicId={cardId}
lists={formattedLists}
handleChangeList={handleChangeList}
isLoading={isLoading}
/>
</div>
@@ -227,6 +304,7 @@ export default function CardPage() {
<LabelSelector
cardPublicId={cardId}
labels={formattedLabels}
handleSelectLabel={handleSelectLabel}
isLoading={isLoading}
/>
</div>
@@ -235,6 +313,7 @@ export default function CardPage() {
<MemberSelector
cardPublicId={cardId}
members={formattedMembers}
handleSelectMember={handleSelectMember}
isLoading={isLoading}
/>
</div>