diff --git a/apps/web/src/utils/api.ts b/apps/web/src/utils/api.ts index 1e16e22f..7a511bc1 100644 --- a/apps/web/src/utils/api.ts +++ b/apps/web/src/utils/api.ts @@ -1,11 +1,12 @@ -import type { CreateTRPCClientOptions, TRPCLink } from "@trpc/client"; +import type { TRPCLink } from "@trpc/client"; +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; +import { QueryClient } from "@tanstack/react-query"; import { httpBatchLink, loggerLink } from "@trpc/client"; import { createTRPCNext } from "@trpc/next"; -import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import superjson from "superjson"; -import { type AppRouter } from "@kan/api/root"; +import type { AppRouter } from "@kan/api/root"; /** * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which @@ -42,6 +43,8 @@ const getBaseUrl = () => { return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost }; +const queryClient = new QueryClient(); + // @ts-expect-error export const api = createTRPCNext({ config() { @@ -58,6 +61,7 @@ export const api = createTRPCNext({ transformer: superjson, }), ], + queryClient: queryClient, }; }, ssr: false, diff --git a/apps/web/src/views/card/components/LabelSelector.tsx b/apps/web/src/views/card/components/LabelSelector.tsx index 51c7eca9..f1b8c746 100644 --- a/apps/web/src/views/card/components/LabelSelector.tsx +++ b/apps/web/src/views/card/components/LabelSelector.tsx @@ -15,33 +15,67 @@ interface LabelSelectorProps { selected: boolean; leftIcon: React.ReactNode; }[]; - refetchCard: () => Promise; - handleSelectLabel: (labelPublicId: string) => void; isLoading: boolean; } export default function LabelSelector({ cardPublicId, labels, - refetchCard, - handleSelectLabel, isLoading, }: LabelSelectorProps) { + const utils = api.useUtils(); const { openModal } = useModal(); const { showPopup } = usePopup(); const addOrRemoveLabel = api.card.addOrRemoveLabel.useMutation({ - onSuccess: async () => { - await refetchCard(); + onMutate: async (update) => { + await utils.card.byId.cancel(); + + const previousCard = utils.card.byId.getData({ cardPublicId }); + + utils.card.byId.setData({ cardPublicId }, (oldCard) => { + if (!oldCard) return oldCard; + + const hasLabel = oldCard.labels.some( + (label) => label.publicId === update.labelPublicId, + ); + + const labelToAdd = oldCard.labels.find( + (label) => label.publicId === update.labelPublicId, + ); + + const updatedLabels = hasLabel + ? oldCard.labels.filter( + (label) => label.publicId !== update.labelPublicId, + ) + : [ + ...oldCard.labels, + { + publicId: update.labelPublicId, + name: labelToAdd?.name ?? "", + colourCode: labelToAdd?.colourCode ?? "", + }, + ]; + + return { + ...oldCard, + labels: updatedLabels, + }; + }); + + return { previousCard }; }, - onError: async () => { - await refetchCard(); + onError: (_error, _newList, context) => { + utils.card.byId.setData({ cardPublicId }, context?.previousCard); showPopup({ header: "Unable to update labels", message: "Please try again later, or contact customer support.", icon: "error", }); }, + onSettled: async () => { + await utils.card.byId.invalidate({ cardPublicId }); + }, }); const selectedLabels = labels.filter((label) => label.selected); @@ -56,7 +90,6 @@ export default function LabelSelector({ { - handleSelectLabel(label.key); addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key }); }} handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)} diff --git a/apps/web/src/views/card/components/ListSelector.tsx b/apps/web/src/views/card/components/ListSelector.tsx index 99a0ae57..99b4b7fe 100644 --- a/apps/web/src/views/card/components/ListSelector.tsx +++ b/apps/web/src/views/card/components/ListSelector.tsx @@ -11,32 +11,51 @@ interface ListSelectorProps { value: string; selected: boolean; }[]; - refetchCard: () => Promise; - handleChangeList: (newListPublicId: string, newListName: string) => void; isLoading: boolean; } export default function ListSelector({ cardPublicId, lists, - handleChangeList, - refetchCard, isLoading, }: ListSelectorProps) { + const utils = api.useUtils(); + const { showPopup } = usePopup(); const updateCardList = api.card.reorder.useMutation({ - onSuccess: async () => { - await refetchCard(); + onMutate: async (newList) => { + await utils.card.byId.cancel(); + + const previousCard = utils.card.byId.getData({ cardPublicId }); + + utils.card.byId.setData({ cardPublicId }, (oldCard) => { + if (!oldCard) return oldCard; + + return { + ...oldCard, + list: { + ...oldCard.list, + publicId: newList.newListPublicId, + name: oldCard.list?.name ?? "", + board: oldCard.list?.board ?? null, + }, + }; + }); + + return { previousCard }; }, - onError: async () => { - await refetchCard(); + onError: (_error, _newList, context) => { + utils.card.byId.setData({ cardPublicId }, context?.previousCard); showPopup({ header: "Unable to update list", message: "Please try again later, or contact customer support.", icon: "error", }); }, + onSettled: async () => { + await utils.card.byId.invalidate({ cardPublicId }); + }, }); const selectedList = lists.find((list) => list.selected); @@ -51,7 +70,6 @@ export default function ListSelector({ { - handleChangeList(member.key, member.value); updateCardList.mutate({ cardPublicId, newListPublicId: member.key, diff --git a/apps/web/src/views/card/components/MemberSelector.tsx b/apps/web/src/views/card/components/MemberSelector.tsx index 8a159640..6813ddff 100644 --- a/apps/web/src/views/card/components/MemberSelector.tsx +++ b/apps/web/src/views/card/components/MemberSelector.tsx @@ -17,34 +17,70 @@ interface MemberSelectorProps { leftIcon: React.ReactNode; imageUrl: string | undefined; }[]; - handleSelectMember: (memberPublicId: string) => void; - refetchCard: () => Promise; isLoading: boolean; } export default function MemberSelector({ cardPublicId, members, - handleSelectMember, - refetchCard, isLoading, }: MemberSelectorProps) { const router = useRouter(); + const utils = api.useUtils(); const { openModal } = useModal(); const { showPopup } = usePopup(); const addOrRemoveMember = api.card.addOrRemoveMember.useMutation({ - onSuccess: async () => { - await refetchCard(); + onMutate: async (update) => { + await utils.card.byId.cancel(); + + const previousCard = utils.card.byId.getData({ cardPublicId }); + + utils.card.byId.setData({ cardPublicId }, (oldCard) => { + if (!oldCard) return oldCard; + + const hasMember = oldCard.members.some( + (member) => member.publicId === update.workspaceMemberPublicId, + ); + + const memberToAdd = oldCard.members.find( + (member) => member.publicId === update.workspaceMemberPublicId, + ); + + const updatedMembers = hasMember + ? oldCard.members.filter( + (member) => member.publicId !== update.workspaceMemberPublicId, + ) + : [ + ...oldCard.members, + { + publicId: update.workspaceMemberPublicId, + user: { + id: memberToAdd?.user?.id ?? "", + name: memberToAdd?.user?.name ?? "", + }, + }, + ]; + + return { + ...oldCard, + members: updatedMembers, + }; + }); + + return { previousCard }; }, - onError: async () => { - await refetchCard(); + onError: (_error, _newList, context) => { + utils.card.byId.setData({ cardPublicId }, context?.previousCard); showPopup({ header: "Unable to update members", message: "Please try again later, or contact customer support.", icon: "error", }); }, + onSettled: async () => { + await utils.card.byId.invalidate({ cardPublicId }); + }, }); const selectedMembers = members.filter((member) => member.selected); @@ -64,7 +100,6 @@ export default function MemberSelector({ { - handleSelectMember(member.key); addOrRemoveMember.mutate({ cardPublicId, workspaceMemberPublicId: member.key, diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index ac64a16e..f30e3ba2 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -1,12 +1,9 @@ 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"; @@ -38,30 +35,15 @@ export default function CardPage() { const utils = api.useUtils(); const { modalContentType, entityId } = useModal(); const { showPopup } = usePopup(); - const [card, setCard] = useState(); const cardId = Array.isArray(router.query.cardId) ? router.query.cardId[0] : router.query.cardId; - const { data, isLoading, refetch } = api.card.byId.useQuery({ + const { data: card, isLoading } = api.card.byId.useQuery({ cardPublicId: cardId ?? "", }); - const refetchCard = async () => { - try { - const { data: updatedCard } = await refetch(); - - if (updatedCard) setCard(updatedCard); - } catch (error) { - console.error({ error }); - } - }; - - useEffect(() => { - setCard(data); - }, [data]); - const board = card?.list?.board; const boardId = board?.publicId; const labels = board?.labels; @@ -70,74 +52,6 @@ export default function CardPage() { 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) => { const isSelected = selectedLabels?.some( @@ -189,25 +103,23 @@ export default function CardPage() { }) ?? []; const updateCard = api.card.update.useMutation({ - onSuccess: async () => { - await refetchCard(); - }, - onError: async () => { - await refetchCard(); - + onError: () => { showPopup({ header: "Unable to update card", message: "Please try again later, or contact customer support.", icon: "error", }); }, + onSettled: async () => { + await utils.card.byId.invalidate({ cardPublicId: cardId }); + }, }); const { register, handleSubmit, setValue, watch } = useForm({ values: { cardId: cardId ?? "", - title: data?.title ?? "", - description: data?.description ?? "", + title: card?.title ?? "", + description: card?.description ?? "", }, }); @@ -224,7 +136,7 @@ export default function CardPage() { return ( <>
@@ -307,8 +219,6 @@ export default function CardPage() {
@@ -317,8 +227,6 @@ export default function CardPage() {
@@ -327,8 +235,6 @@ export default function CardPage() { diff --git a/packages/db/src/repository/card.repo.ts b/packages/db/src/repository/card.repo.ts index d4ee5c01..ef3ccece 100644 --- a/packages/db/src/repository/card.repo.ts +++ b/packages/db/src/repository/card.repo.ts @@ -408,10 +408,7 @@ export const hardDeleteCardMemberRelationship = async ( .delete() .eq("cardId", args.cardId) .eq("workspaceMemberId", args.memberId) - .select() - .order("cardId", { ascending: true }) - .limit(1) - .single(); + .select(); return { success: !error }; };