diff --git a/apps/web/src/providers/board.tsx b/apps/web/src/providers/board.tsx index 4e374f3f..5adb006c 100644 --- a/apps/web/src/providers/board.tsx +++ b/apps/web/src/providers/board.tsx @@ -1,13 +1,7 @@ import type { ReactNode } from "react"; import React, { createContext, useContext, useState } from "react"; -import type { - GetBoardByIdOutput, - NewCardInput, - NewListInput, - ReorderCardInput, - ReorderListInput, -} from "@kan/api/types"; +import type { GetBoardByIdOutput, NewListInput } from "@kan/api/types"; import { generateUID } from "@kan/shared/utils"; import { usePopup } from "~/providers/popup"; @@ -16,9 +10,6 @@ import { api } from "~/utils/api"; interface BoardContextProps { boardData: GetBoardByIdOutput; setBoardData: React.Dispatch>; - updateList: (params: ReorderListInput) => void; - updateCard: (params: ReorderCardInput) => void; - addCard: (params: NewCardInput) => void; addList: (params: NewListInput) => void; removeCard: (params: { cardPublicId: string }) => void; refetchBoard: () => Promise; @@ -60,78 +51,6 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({ } }; - const updateCardMutation = api.card.reorder.useMutation({ - onSuccess: async () => { - await refetchBoard(); - }, - onError: async () => { - await refetchBoard(); - showPopup({ - header: "Unable to update card", - message: "Please try again later, or contact customer support.", - icon: "error", - }); - }, - }); - - const updateListMutation = api.list.reorder.useMutation({ - onSuccess: async () => { - await refetchBoard(); - }, - onError: async () => { - await refetchBoard(); - showPopup({ - header: "Unable to update list", - message: "Please try again later, or contact customer support.", - icon: "error", - }); - }, - }); - - const addCard = ({ - title, - listPublicId, - labelPublicIds, - memberPublicIds, - position, - }: { - title: string; - listPublicId: string; - labelPublicIds: string[]; - memberPublicIds: string[]; - position: "start" | "end"; - }) => { - if (!boardData) return; - - const updatedLists = boardData.lists.map((list) => { - if (list.publicId === listPublicId) { - const newCard = { - publicId: `PLACEHOLDER_${generateUID()}`, - title, - listId: 2, - description: "", - labels: boardData.labels.filter((label) => - labelPublicIds.includes(label.publicId), - ), - members: - boardData.workspace?.members.filter((member) => - memberPublicIds.includes(member.publicId), - ) ?? [], - index: position === "start" ? 0 : list.cards.length, - }; - - const updatedCards = - position === "start" - ? [newCard, ...list.cards] - : [...list.cards, newCard]; - return { ...list, cards: updatedCards }; - } - return list; - }); - - setBoardData({ ...boardData, lists: updatedLists }); - }; - const addList = ({ name, boardPublicId }: NewListInput) => { if (!boardData) return; @@ -162,38 +81,11 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({ setBoardData({ ...boardData, lists: updatedLists }); }; - const updateList = ({ - listPublicId, - currentIndex, - newIndex, - }: ReorderListInput) => { - updateListMutation.mutate({ - listPublicId, - currentIndex, - newIndex, - }); - }; - - const updateCard = ({ - cardPublicId, - newListPublicId, - newIndex, - }: ReorderCardInput) => { - updateCardMutation.mutate({ - cardPublicId, - newListPublicId, - newIndex, - }); - }; - return ( ({ defaultValues: { @@ -52,19 +65,63 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) { const isCreateAnotherEnabled = watch("isCreateAnotherEnabled"); const position = watch("position"); + const { data: boardData } = api.board.byId.useQuery(queryParams, { + enabled: !!boardPublicId, + }); + const createCard = api.card.create.useMutation({ - onSuccess: async () => { - await refetchBoard(); + 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 updatedLists = oldBoard.lists.map((list) => { + if (list.publicId === listPublicId) { + const newCard = { + publicId: `PLACEHOLDER_${generateUID()}`, + title: args.title, + listId: 2, + description: "", + labels: oldBoard.labels.filter((label) => + labelPublicIds.includes(label.publicId), + ), + members: + oldBoard.workspace?.members.filter((member) => + memberPublicIds.includes(member.publicId), + ) ?? [], + _filteredLabels: labelPublicIds.map((id) => ({ publicId: id })), + _filteredMembers: memberPublicIds.map((id) => ({ publicId: id })), + index: position === "start" ? 0 : list.cards.length, + }; + + const updatedCards = + position === "start" + ? [newCard, ...list.cards] + : [...list.cards, newCard]; + return { ...list, cards: updatedCards }; + } + return list; + }); + + return { ...oldBoard, lists: updatedLists }; + }); + + return { previousState: currentState }; }, - onError: async () => { - closeModal(); - await refetchBoard(); + onError: (_error, _newList, context) => { + utils.board.byId.setData(queryParams, context?.previousState); showPopup({ header: "Unable to create card", message: "Please try again later, or contact customer support.", icon: "error", }); }, + onSettled: async () => { + await utils.board.byId.invalidate(queryParams); + }, }); useEffect(() => { @@ -108,7 +165,6 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) { })) ?? []; const onSubmit = (data: NewCardInput) => { - addCard(data); const isCreateAnotherEnabled = watch("isCreateAnotherEnabled"); if (!isCreateAnotherEnabled) closeModal(); reset({ diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index f937acee..283ce5c8 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -15,8 +15,8 @@ import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; import PatternedBackground from "~/components/PatternedBackground"; import { StrictModeDroppable as Droppable } from "~/components/StrictModeDroppable"; -import { useBoard } from "~/providers/board"; import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; import { formatToArray } from "~/utils/helpers"; @@ -36,7 +36,8 @@ type PublicListId = string; export default function BoardPage() { const params = useParams() as { boardId: string[] } | null; const router = useRouter(); - const { boardData, setBoardData, updateCard, updateList } = useBoard(); + const utils = api.useUtils(); + const { showPopup } = usePopup(); const { workspace } = useWorkspace(); const { openModal, modalContentType } = useModal(); const [selectedPublicListId, setSelectedPublicListId] = @@ -60,23 +61,110 @@ export default function BoardPage() { }); }; - const { data, isSuccess, isLoading } = api.board.byId.useQuery( - { - boardPublicId: boardId ?? "", - members: formatToArray(router.query.members), - labels: formatToArray(router.query.labels), + const queryParams = { + boardPublicId: boardId ?? "", + members: formatToArray(router.query.members), + labels: formatToArray(router.query.labels), + }; + + const { + data: boardData, + isSuccess, + isLoading, + } = api.board.byId.useQuery(queryParams, { + enabled: !!boardId, + }); + + const updateListMutation = api.list.reorder.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 updatedLists = Array.from(oldBoard.lists); + const removedList = updatedLists.splice(args.currentIndex, 1)[0]; + + if (removedList) { + updatedLists.splice(args.newIndex, 0, removedList); + + return { + ...oldBoard, + lists: updatedLists, + }; + } + }); + + return { previousState: currentState }; }, - { - enabled: !!boardId, + onError: (_error, _newList, context) => { + utils.board.byId.setData(queryParams, context?.previousState); + showPopup({ + header: "Unable to update list", + message: "Please try again later, or contact customer support.", + icon: "error", + }); }, - ); + onSettled: async () => { + await utils.board.byId.invalidate(queryParams); + }, + }); + + const updateCardMutation = api.card.reorder.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 updatedLists = Array.from(oldBoard.lists); + const sourceList = updatedLists.find( + (list) => list.publicId === args.currentListPublicId, + ); + const destinationList = updatedLists.find( + (list) => list.publicId === args.newListPublicId, + ); + const removedCard = sourceList?.cards.splice(args.currentIndex, 1)[0]; + + if ( + sourceList && + destinationList && + removedCard && + args.newIndex !== undefined + ) { + destinationList.cards.splice(args.newIndex, 0, removedCard); + + return { + ...oldBoard, + lists: updatedLists, + }; + } + }); + + return { previousState: currentState }; + }, + onError: (_error, _newList, context) => { + utils.board.byId.setData(queryParams, context?.previousState); + showPopup({ + header: "Unable to update card", + message: "Please try again later, or contact customer support.", + icon: "error", + }); + }, + onSettled: async () => { + await utils.board.byId.invalidate(queryParams); + }, + }); useEffect(() => { - if (isSuccess && data) { - setBoardData(data); - setValue("name", data.name || ""); + if (isSuccess && boardData) { + setValue("name", boardData.name || ""); } - }, [isSuccess, data, setBoardData, setValue]); + }, [isSuccess, boardData, setValue]); if (!boardId || !boardData) return <>; @@ -96,16 +184,7 @@ export default function BoardPage() { } if (type === "LIST") { - const updatedLists = Array.from(boardData.lists); - const removedList = updatedLists.splice(source.index, 1)[0]; - - if (removedList) { - updatedLists.splice(destination.index, 0, removedList); - - setBoardData({ ...boardData, lists: updatedLists }); - } - - updateList({ + updateListMutation.mutate({ listPublicId: draggableId, currentIndex: source.index, newIndex: destination.index, @@ -113,24 +192,11 @@ export default function BoardPage() { } if (type === "CARD") { - const updatedLists = Array.from(boardData.lists); - const sourceList = updatedLists.find( - (list) => list.publicId === source.droppableId, - ); - const destinationList = updatedLists.find( - (list) => list.publicId === destination.droppableId, - ); - const removedCard = sourceList?.cards.splice(source.index, 1)[0]; - - if (sourceList && destinationList && removedCard) { - destinationList.cards.splice(destination.index, 0, removedCard); - - setBoardData({ ...boardData, lists: updatedLists }); - } - - updateCard({ + updateCardMutation.mutate({ cardPublicId: draggableId, + currentListPublicId: source.droppableId, newListPublicId: destination.droppableId, + currentIndex: source.index, newIndex: destination.index, }); } @@ -277,7 +343,11 @@ export default function BoardPage() { )} {modalContentType === "NEW_CARD" && ( - + )} {modalContentType === "NEW_LIST" && ( diff --git a/packages/api/src/routers/card.ts b/packages/api/src/routers/card.ts index daa8f30d..73e7adf7 100644 --- a/packages/api/src/routers/card.ts +++ b/packages/api/src/routers/card.ts @@ -667,7 +667,9 @@ export const cardRouter = createTRPCRouter({ .input( z.object({ cardPublicId: z.string().min(12), + currentListPublicId: z.string().min(12), newListPublicId: z.string().min(12), + currentIndex: z.number(), newIndex: z.number().optional(), }), )