perf: improve board optimistic updates
This commit is contained in:
@@ -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<React.SetStateAction<GetBoardByIdOutput>>;
|
||||
updateList: (params: ReorderListInput) => void;
|
||||
updateCard: (params: ReorderCardInput) => void;
|
||||
addCard: (params: NewCardInput) => void;
|
||||
addList: (params: NewListInput) => void;
|
||||
removeCard: (params: { cardPublicId: string }) => void;
|
||||
refetchBoard: () => Promise<void>;
|
||||
@@ -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 (
|
||||
<BoardContext.Provider
|
||||
value={{
|
||||
boardData,
|
||||
setBoardData,
|
||||
updateList,
|
||||
updateCard,
|
||||
addCard,
|
||||
addList,
|
||||
removeCard,
|
||||
refetchBoard,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import type { NewCardInput } from "@kan/api/types";
|
||||
import { generateUID } from "@kan/shared/utils";
|
||||
|
||||
import Avatar from "~/components/Avatar";
|
||||
import Button from "~/components/Button";
|
||||
@@ -14,7 +15,6 @@ import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||
import Input from "~/components/Input";
|
||||
import LabelIcon from "~/components/LabelIcon";
|
||||
import Toggle from "~/components/Toggle";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
@@ -25,15 +25,28 @@ type NewCardFormInput = NewCardInput & {
|
||||
isCreateAnotherEnabled: boolean;
|
||||
};
|
||||
|
||||
interface NewCardFormProps {
|
||||
listPublicId: string;
|
||||
interface QueryParams {
|
||||
boardPublicId: string;
|
||||
members: string[];
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
||||
const { boardData, addCard, refetchBoard } = useBoard();
|
||||
interface NewCardFormProps {
|
||||
boardPublicId: string;
|
||||
listPublicId: string;
|
||||
queryParams: QueryParams;
|
||||
}
|
||||
|
||||
export function NewCardForm({
|
||||
boardPublicId,
|
||||
listPublicId,
|
||||
queryParams,
|
||||
}: NewCardFormProps) {
|
||||
const { showPopup } = usePopup();
|
||||
const { closeModal } = useModal();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { register, handleSubmit, reset, setValue, watch } =
|
||||
useForm<NewCardFormInput>({
|
||||
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({
|
||||
|
||||
@@ -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() {
|
||||
<DeleteListConfirmation listPublicId={selectedPublicListId} />
|
||||
)}
|
||||
{modalContentType === "NEW_CARD" && (
|
||||
<NewCardForm listPublicId={selectedPublicListId} />
|
||||
<NewCardForm
|
||||
boardPublicId={boardId}
|
||||
listPublicId={selectedPublicListId}
|
||||
queryParams={queryParams}
|
||||
/>
|
||||
)}
|
||||
{modalContentType === "NEW_LIST" && (
|
||||
<NewListForm boardPublicId={boardId} />
|
||||
|
||||
Reference in New Issue
Block a user