diff --git a/src/modules/data/board/Board.store.ts b/src/modules/data/board/Board.store.ts index 2942cba053..9419bdae99 100644 --- a/src/modules/data/board/Board.store.ts +++ b/src/modules/data/board/Board.store.ts @@ -5,20 +5,17 @@ import { useBoardRestApi } from "./boardActions/boardRestApi.composable"; import { useBoardSocketApi } from "./boardActions/boardSocketApi.composable"; import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; import { useSharedEditMode } from "./EditMode.composable"; -import { CardMove } from "@/types/board/DragAndDrop"; -import { ColumnResponse } from "@/serverApi/v3"; import { envConfigModule } from "@/store"; import { CreateCardRequestPayload, CreateCardSuccessPayload, CreateColumnRequestPayload, - CreateColumnSucccessPayload, + CreateColumnSuccessPayload, DeleteColumnRequestPayload, DeleteColumnSuccessPayload, DisconnectSocketRequestPayload, FetchBoardRequestPayload, FetchBoardSuccessPayload, - MoveCardRequestPayload, MoveCardSuccessPayload, MoveColumnRequestPayload, MoveColumnSuccessPayload, @@ -34,6 +31,7 @@ import { DeleteCardSuccessPayload } from "./cardActions/cardActionPayload"; export const useBoardStore = defineStore("boardStore", () => { const board = ref(undefined); const isLoading = ref(false); + const { setFocus } = useBoardFocusHandler(); const restApi = useBoardRestApi(); const isSocketEnabled = @@ -43,6 +41,8 @@ export const useBoardStore = defineStore("boardStore", () => { const { setEditModeId } = useSharedEditMode(); + const getLastColumnIndex = () => board.value!.columns.length - 1; + const getColumnIndex = (columnId: string | undefined): number => { if (columnId === undefined) return -1; if (board.value === undefined) return -1; @@ -52,8 +52,8 @@ export const useBoardStore = defineStore("boardStore", () => { }; const getColumnId = (columnIndex: number): string | undefined => { - if (board.value === undefined) return; - if (columnIndex === undefined) return; + if (board.value === undefined) return; // shouldn't happen because board presence is checked by callers + if (columnIndex === undefined) return; // shouldn't happen because columnIndex is always set by type definition if (columnIndex < 0) return; if (columnIndex > board.value.columns.length - 1) return; @@ -62,6 +62,20 @@ export const useBoardStore = defineStore("boardStore", () => { return board.value.columns[columnIndex].id; }; + const getCardLocation = (cardId: string) => { + if (!board.value) return; + + const columnIndex = board.value.columns.findIndex( + (column) => column.cards.find((c) => c.cardId === cardId) !== undefined + ); + if (columnIndex === -1) return undefined; + + const column = board.value.columns[columnIndex]; + const columnId = column.id; + const cardIndex = column.cards.findIndex((c) => c.cardId === cardId); + return { columnIndex, columnId, cardIndex }; + }; + const setBoard = (newBoard: Board | undefined) => { board.value = newBoard; }; @@ -78,8 +92,6 @@ export const useBoardStore = defineStore("boardStore", () => { if (!board.value) return; const { newCard } = payload; - const { setFocus } = useBoardFocusHandler(); - setFocus(newCard.id); const columnIndex = board.value.columns.findIndex( (column) => column.id === payload.columnId @@ -88,14 +100,17 @@ export const useBoardStore = defineStore("boardStore", () => { cardId: newCard.id, height: 120, }); - setEditModeId(newCard.id); + if (payload.isOwnAction === true) { + setFocus(newCard.id); + setEditModeId(newCard.id); + } }; const createColumnRequest = async (payload: CreateColumnRequestPayload) => { socketOrRest.createColumnRequest(payload); }; - const createColumnSuccess = (payload: CreateColumnSucccessPayload) => { + const createColumnSuccess = (payload: CreateColumnSuccessPayload) => { if (!board.value) return; board.value.columns.push(payload.newColumn); }; @@ -122,10 +137,9 @@ export const useBoardStore = defineStore("boardStore", () => { if (!board.value) return; const columnId = payload.columnId; const columnIndex = getColumnIndex(columnId); - if (columnIndex < 0) { - return; + if (columnIndex !== -1) { + board.value.columns.splice(columnIndex, 1); } - board.value.columns.splice(columnIndex, 1); }; const updateBoardTitleRequest = async ( @@ -152,7 +166,7 @@ export const useBoardStore = defineStore("boardStore", () => { if (!board.value) return; const { columnId, newTitle } = payload; const columnIndex = getColumnIndex(columnId); - if (columnIndex > -1) { + if (columnIndex !== -1) { board.value.columns[columnIndex].title = newTitle; } }; @@ -196,45 +210,26 @@ export const useBoardStore = defineStore("boardStore", () => { board.value.columns.splice(addedIndex, 0, column); }; - const getColumnIndices = ( - fromColumnId: string | undefined, - toColumnId: string | undefined, - columnDelta: number | undefined - ) => { - const fromColumnIndex = getColumnIndex(fromColumnId); - const newColumnId: string | undefined = - columnDelta === undefined - ? toColumnId - : getColumnId(fromColumnIndex + columnDelta); - - return { fromColumnIndex, toColumnIndex: getColumnIndex(newColumnId) }; - }; + const moveCardToNewColumn = async (cardId: string) => { + const cardLocation = getCardLocation(cardId); + if (cardLocation === undefined) return; - const isMoveValid = ( - payload: CardMove, - columns: Array, - targetColumnIndex: number, - fromColumnIndex: number - ) => { - const { cardId, newIndex, oldIndex } = payload; - if (cardId === undefined || targetColumnIndex === undefined) return false; // ensure values are set - - const movedInsideColumn = fromColumnIndex === targetColumnIndex; - if (movedInsideColumn) { - if ( - (newIndex === oldIndex && fromColumnIndex === targetColumnIndex) || // same position - newIndex < 0 || // first card - can't move up - newIndex > columns[fromColumnIndex].cards.length - 1 // last card - can't move down - ) - return false; - } + const { + columnIndex: fromColumnIndex, + columnId: fromColumnId, + cardIndex: oldIndex, + } = cardLocation; - return true; + await socketOrRest.moveCardRequest({ + cardId, + fromColumnId, + fromColumnIndex, + oldIndex, + newIndex: 0, + }); }; - const moveCardRequest = async (payload: MoveCardRequestPayload) => { - await socketOrRest.moveCardRequest(payload); - }; + const moveCardRequest = socketOrRest.moveCardRequest; const moveCardSuccess = async (payload: MoveCardSuccessPayload) => { if (!board.value) return; @@ -242,31 +237,11 @@ export const useBoardStore = defineStore("boardStore", () => { const { newIndex, oldIndex, - toColumnId, - fromColumnId, - columnDelta, forceNextTick, + fromColumnIndex, + toColumnIndex, } = payload; - const { fromColumnIndex, toColumnIndex } = getColumnIndices( - fromColumnId, - toColumnId, - columnDelta - ); - - const targetColumnIndex = toColumnIndex ?? board.value.columns.length - 1; - - if ( - !isMoveValid( - payload, - board.value.columns, - targetColumnIndex, - fromColumnIndex - ) - ) { - return; - } - const item = board.value.columns[fromColumnIndex].cards.splice( oldIndex, 1 @@ -280,9 +255,8 @@ export const useBoardStore = defineStore("boardStore", () => { await nextTick(); } - board.value.columns[targetColumnIndex].cards = - board.value.columns[targetColumnIndex].cards ?? []; - board.value.columns[targetColumnIndex].cards.splice(newIndex, 0, item); + const toColumn = board.value.columns[toColumnIndex]; + toColumn.cards.splice(newIndex, 0, item); }; const disconnectSocketRequest = (payload: DisconnectSocketRequestPayload) => { @@ -306,6 +280,9 @@ export const useBoardStore = defineStore("boardStore", () => { return { board, isLoading, + getColumnIndex, + getColumnId, + getLastColumnIndex, setBoard, setLoading, createCardRequest, @@ -316,6 +293,7 @@ export const useBoardStore = defineStore("boardStore", () => { deleteColumnRequest, deleteColumnSuccess, disconnectSocketRequest, + moveCardToNewColumn, moveCardRequest, moveCardSuccess, moveColumnRequest, diff --git a/src/modules/data/board/Board.store.unit.ts b/src/modules/data/board/Board.store.unit.ts index 1e32e1a830..8d74c166e8 100644 --- a/src/modules/data/board/Board.store.unit.ts +++ b/src/modules/data/board/Board.store.unit.ts @@ -1,5 +1,5 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; -import { CardMove, ColumnMove } from "@/types/board/DragAndDrop"; +import { ColumnMove } from "@/types/board/DragAndDrop"; import { boardResponseFactory, cardSkeletonResponseFactory, @@ -18,7 +18,6 @@ import { cardResponseFactory } from "@@/tests/test-utils/factory/cardResponseFac import setupStores from "@@/tests/test-utils/setupStores"; import { useSocketConnection } from "@data-board"; import { useI18n } from "vue-i18n"; -import { MoveCardRequestPayload } from "./boardActions/boardActionPayload"; import { useBoardRestApi } from "./boardActions/boardRestApi.composable"; import { useBoardSocketApi } from "./boardActions/boardSocketApi.composable"; @@ -115,6 +114,85 @@ describe("BoardStore", () => { jest.resetAllMocks(); }); + describe("getLastColumnIndex", () => { + it("should return last column index", () => { + const { boardStore } = setup(); + + const lastColumnIndex = boardStore.getLastColumnIndex(); + + expect(lastColumnIndex).toEqual(1); + }); + }); + + describe("getColumnIndex", () => { + it("should return -1 when columnId is undefined", () => { + const { boardStore } = setup(); + + const columnIndex = boardStore.getColumnIndex(undefined); + + expect(columnIndex).toEqual(-1); + }); + + it("should return -1 when board is undefined", () => { + const { boardStore } = setup({ createBoard: false }); + + const columnIndex = boardStore.getColumnIndex("columnId"); + + expect(columnIndex).toEqual(-1); + }); + + it("should return column index of first column", () => { + const { boardStore, firstColumn } = setup(); + + const columnIndex = boardStore.getColumnIndex(firstColumn.id); + + expect(columnIndex).toEqual(0); + }); + }); + + describe("getColumnId", () => { + it("should return undefined when board is undefined", () => { + const { boardStore } = setup({ createBoard: false }); + + const columnId = boardStore.getColumnId(0); + + expect(columnId).toBeUndefined(); + }); + + it("should return undefined when columnIndex is undefined", () => { + const { boardStore } = setup(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const columnId = boardStore.getColumnId(undefined as any); + + expect(columnId).toBeUndefined(); + }); + + it("should return undefined when columnIndex is negative", () => { + const { boardStore } = setup(); + + const columnId = boardStore.getColumnId(-1); + + expect(columnId).toBeUndefined(); + }); + + it("should return undefined when columnIndex is greater than columns length", () => { + const { boardStore } = setup(); + + const columnId = boardStore.getColumnId(2); + + expect(columnId).toBeUndefined(); + }); + + it("should return column id of first column", () => { + const { boardStore, firstColumn } = setup(); + + const columnId = boardStore.getColumnId(0); + + expect(columnId).toEqual(firstColumn.id); + }); + }); + describe("setBoard", () => { it("should set board", () => { const { boardStore } = setup(); @@ -146,6 +224,7 @@ describe("BoardStore", () => { boardStore.createCardSuccess({ newCard: NEW_CARD, columnId: firstColumn.id, + isOwnAction: true, }); expect(boardStore.board).toBe(undefined); @@ -157,6 +236,7 @@ describe("BoardStore", () => { boardStore.createCardSuccess({ newCard: NEW_CARD, columnId: firstColumn.id, + isOwnAction: true, }); expect(boardStore.board?.columns[0].cards[3]).toEqual({ @@ -168,9 +248,11 @@ describe("BoardStore", () => { it("should call setEditModeId", () => { const { boardStore, firstColumn } = setup(); + boardStore.createCardRequest({ columnId: firstColumn.id }); boardStore.createCardSuccess({ newCard: NEW_CARD, columnId: firstColumn.id, + isOwnAction: true, }); expect(setEditModeId).toHaveBeenCalled(); @@ -182,7 +264,10 @@ describe("BoardStore", () => { it("should not create Column when board value is undefined", () => { const { boardStore } = setup({ createBoard: false }); - boardStore.createColumnSuccess({ newColumn: NEW_COLUMN }); + boardStore.createColumnSuccess({ + newColumn: NEW_COLUMN, + isOwnAction: true, + }); expect(boardStore.board).toBe(undefined); }); @@ -190,7 +275,10 @@ describe("BoardStore", () => { it("should create a column", () => { const { boardStore } = setup(); - boardStore.createColumnSuccess({ newColumn: NEW_COLUMN }); + boardStore.createColumnSuccess({ + newColumn: NEW_COLUMN, + isOwnAction: true, + }); expect(boardStore.board?.columns[2]).toEqual(NEW_COLUMN); }); @@ -200,7 +288,10 @@ describe("BoardStore", () => { it("should not delete a card when a board is undefined", () => { const { boardStore, cards } = setup({ createBoard: false }); - boardStore.deleteCardSuccess({ cardId: cards[0].cardId }); + boardStore.deleteCardSuccess({ + cardId: cards[0].cardId, + isOwnAction: true, + }); expect(boardStore.board).toBe(undefined); }); @@ -208,7 +299,10 @@ describe("BoardStore", () => { it("should not delete a card if cardId does not exists", () => { const { boardStore } = setup(); - boardStore.deleteCardSuccess({ cardId: "unknown cardId" }); + boardStore.deleteCardSuccess({ + cardId: "unknown cardId", + isOwnAction: true, + }); expect(boardStore.board?.columns[0].cards.length).toEqual(3); }); @@ -219,7 +313,7 @@ describe("BoardStore", () => { const firstCardId = cards[0].cardId; const secondCardId = cards[1].cardId; - boardStore.deleteCardSuccess({ cardId: firstCardId }); + boardStore.deleteCardSuccess({ cardId: firstCardId, isOwnAction: true }); const firstCardIdAfterDeletion = boardStore.board?.columns[0].cards[0].cardId; @@ -233,7 +327,10 @@ describe("BoardStore", () => { it("should not delete a column when board value is undefined", () => { const { boardStore, firstColumn } = setup({ createBoard: false }); - boardStore.deleteColumnSuccess({ columnId: firstColumn.id }); + boardStore.deleteColumnSuccess({ + columnId: firstColumn.id, + isOwnAction: true, + }); expect(boardStore.board).toBe(undefined); }); @@ -241,7 +338,10 @@ describe("BoardStore", () => { it("should not delete a column when column id is unkown", () => { const { boardStore } = setup(); - boardStore.deleteColumnSuccess({ columnId: "unknownId" }); + boardStore.deleteColumnSuccess({ + columnId: "unknownId", + isOwnAction: true, + }); expect(boardStore.board?.columns.length).toEqual(2); }); @@ -249,7 +349,10 @@ describe("BoardStore", () => { it("should delete a column", () => { const { boardStore, firstColumn, secondColumn } = setup(); - boardStore.deleteColumnSuccess({ columnId: firstColumn.id }); + boardStore.deleteColumnSuccess({ + columnId: firstColumn.id, + isOwnAction: true, + }); expect(boardStore.board?.columns[0]).not.toEqual(firstColumn); expect(boardStore.board?.columns[0]).toEqual(secondColumn); @@ -265,6 +368,7 @@ describe("BoardStore", () => { boardStore.updateBoardTitleSuccess({ boardId: "boardId", newTitle: NEW_TITLE, + isOwnAction: true, }); expect(boardStore.board).toBe(undefined); @@ -276,6 +380,7 @@ describe("BoardStore", () => { boardStore.updateBoardTitleSuccess({ boardId: "boardId", newTitle: NEW_TITLE, + isOwnAction: true, }); expect(boardStore.board?.title).toStrictEqual(NEW_TITLE); @@ -290,6 +395,7 @@ describe("BoardStore", () => { boardStore.updateColumnTitleSuccess({ columnId: firstColumn.id, newTitle: NEW_TITLE, + isOwnAction: true, }); expect(boardStore.board).toBe(undefined); @@ -301,6 +407,7 @@ describe("BoardStore", () => { boardStore.updateColumnTitleSuccess({ columnId: firstColumn.id, newTitle: NEW_TITLE, + isOwnAction: true, }); expect(boardStore.board?.columns[0].title).toStrictEqual(NEW_TITLE); @@ -314,6 +421,7 @@ describe("BoardStore", () => { boardStore.updateBoardVisibilitySuccess({ boardId: "boardId", isVisible: true, + isOwnAction: true, }); expect(boardStore.board).toBe(undefined); }); @@ -323,6 +431,7 @@ describe("BoardStore", () => { boardStore.updateBoardVisibilitySuccess({ boardId: "boardId", isVisible: true, + isOwnAction: true, }); expect(boardStore.board?.isVisible).toStrictEqual(true); @@ -342,6 +451,7 @@ describe("BoardStore", () => { await boardStore.moveColumnSuccess({ columnMove, byKeyboard: false, + isOwnAction: true, }); expect(boardStore.board).toBe(undefined); @@ -359,6 +469,7 @@ describe("BoardStore", () => { await boardStore.moveColumnSuccess({ columnMove, byKeyboard: false, + isOwnAction: true, }); expect(boardStore.board?.columns).toEqual([secondColumn, firstColumn]); @@ -376,202 +487,174 @@ describe("BoardStore", () => { await boardStore.moveColumnSuccess({ columnMove, byKeyboard: true, + isOwnAction: true, }); expect(boardStore.board?.columns).toEqual([secondColumn, firstColumn]); }); }); - describe("moveCardSuccess", () => { - const createCardPayload = ({ - cardId, - oldIndex, - newIndex, - fromColumnId, - toColumnId, - columnDelta, - forceNextTick, - }: { - cardId: string; - oldIndex?: number; - newIndex?: number; - fromColumnId: string; - toColumnId?: string; - columnDelta?: number; - forceNextTick?: boolean; - }) => { - const cardPayload: MoveCardRequestPayload = { - cardId, - oldIndex: oldIndex ?? 0, - newIndex: newIndex ?? 0, - fromColumnId, - toColumnId, - columnDelta, - forceNextTick, - }; - return cardPayload; - }; - - it("should not move Card when board value is undefined", async () => { + describe("moveCardToNewColumn", () => { + it("should not call moveCardRequest when board value is undefined", async () => { const { boardStore } = setup({ createBoard: false }); - const cardPayload = createCardPayload({ - cardId: "cardId", - fromColumnId: "columnId", - }); - - await boardStore.moveCardSuccess(cardPayload); + await boardStore.moveCardToNewColumn("cardId"); - expect(boardStore.board).toBe(undefined); + expect(mockedBoardRestApiActions.moveCardRequest).not.toHaveBeenCalled(); }); - describe("when move is invalid", () => { - it("should not move card if card is moved to the same position", async () => { - const { boardStore, firstColumn, cards } = setup(); - - const firstColumnId = firstColumn.id; - const movingCardId = cards[1].cardId; - - const cardPayload = createCardPayload({ - cardId: movingCardId, - fromColumnId: firstColumnId, - toColumnId: firstColumnId, - }); - - await boardStore.moveCardSuccess(cardPayload); + it("should not call moveCardRequest when card ID is undefined", async () => { + const { boardStore } = setup(); - expect(boardStore.board?.columns[0].cards[1].cardId).toEqual( - movingCardId - ); - }); + await boardStore.moveCardToNewColumn("cardId"); - it("should not move card if first card is moved to the first position", async () => { - const { boardStore, firstColumn, cards } = setup(); + expect(mockedBoardRestApiActions.moveCardRequest).not.toHaveBeenCalled(); + }); - const firstColumnId = firstColumn.id; - const firstCardId = cards[0].cardId; + it("should call moveCardRequest from rest api if feature flag is disabled", async () => { + const { boardStore, firstColumn } = setup({ socketFlag: false }); - const cardPayload = createCardPayload({ - cardId: firstCardId, - newIndex: -1, - fromColumnId: firstColumnId, - toColumnId: firstColumnId, - }); + const cardId = firstColumn.cards[0].cardId; - await boardStore.moveCardSuccess(cardPayload); + await boardStore.moveCardToNewColumn(cardId); - expect(boardStore.board?.columns[0].cards[0].cardId).toEqual( - firstCardId - ); + expect(mockedBoardRestApiActions.moveCardRequest).toHaveBeenCalledWith({ + cardId, + fromColumnId: firstColumn.id, + fromColumnIndex: 0, + oldIndex: 0, + newIndex: 0, }); + }); - it("should not move card if if last card is moved to the last position", async () => { - const { boardStore, firstColumn, cards } = setup(); - - const firstColumnId = firstColumn.id; - const lastCardId = cards[2].cardId; + it("should call moveCardRequest from socket api if feature flag is enabled", async () => { + const { boardStore, firstColumn } = setup({ socketFlag: true }); - const cardPayload = createCardPayload({ - cardId: lastCardId, - oldIndex: 2, - newIndex: 3, - fromColumnId: firstColumnId, - toColumnId: firstColumnId, - }); + const cardId = firstColumn.cards[0].cardId; - await boardStore.moveCardSuccess(cardPayload); + await boardStore.moveCardToNewColumn(cardId); - expect(boardStore.board?.columns[0].cards[2].cardId).toEqual( - lastCardId - ); + expect(mockedSocketApiActions.moveCardRequest).toHaveBeenCalledWith({ + cardId, + fromColumnId: firstColumn.id, + fromColumnIndex: 0, + oldIndex: 0, + newIndex: 0, }); }); + }); - describe("when move is valid", () => { - it("should move a card in the same column", async () => { - const { boardStore, firstColumn, cards } = setup(); + describe("moveCardSuccess", () => { + it("should not move Card when board value is undefined", async () => { + const { boardStore } = setup({ createBoard: false }); - const [firstCardId, secondCardId, thirdCardId] = cards.map( - (card) => card.cardId - ); - const firstColumnId = firstColumn.id; + const cardPayload = { + cardId: "cardId", + oldIndex: 0, + newIndex: 1, + fromColumnId: "columnId", + fromColumnIndex: 0, + toColumnId: "columnId", + toColumnIndex: 0, + isOwnAction: true, + }; - const cardPayload = createCardPayload({ - cardId: firstCardId, - oldIndex: 0, - newIndex: 1, - fromColumnId: firstColumnId, - toColumnId: firstColumnId, - }); + await boardStore.moveCardSuccess(cardPayload); - await boardStore.moveCardSuccess(cardPayload); + expect(boardStore.board).toBe(undefined); + }); - const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; + it("should move a card in the same column", async () => { + const { boardStore, firstColumn, cards } = setup(); - expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ - secondCardId, - firstCardId, - thirdCardId, - ]); - }); + const [firstCardId, secondCardId, thirdCardId] = cards.map( + (card) => card.cardId + ); + const firstColumnId = firstColumn.id; + + const cardPayload = { + cardId: firstCardId, + oldIndex: 0, + newIndex: 1, + fromColumnId: firstColumnId, + fromColumnIndex: 0, + toColumnId: firstColumnId, + toColumnIndex: 0, + isOwnAction: true, + }; - it("should move a card in the same column when forceNextTick is true", async () => { - const { boardStore, firstColumn, cards } = setup(); + await boardStore.moveCardSuccess(cardPayload); - const [firstCardId, secondCardId, thirdCardId] = cards.map( - (card) => card.cardId - ); - const firstColumnId = firstColumn.id; + const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; - const cardPayload = createCardPayload({ - cardId: firstCardId, - oldIndex: 0, - newIndex: 1, - fromColumnId: firstColumnId, - toColumnId: firstColumnId, - forceNextTick: true, - }); + expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ + secondCardId, + firstCardId, + thirdCardId, + ]); + }); - await boardStore.moveCardSuccess(cardPayload); + it("should move a card in the same column when forceNextTick is true", async () => { + const { boardStore, firstColumn, cards } = setup(); - const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; + const [firstCardId, secondCardId, thirdCardId] = cards.map( + (card) => card.cardId + ); + const firstColumnId = firstColumn.id; + + const cardPayload = { + cardId: firstCardId, + oldIndex: 0, + newIndex: 1, + fromColumnId: firstColumnId, + fromColumnIndex: 0, + toColumnId: firstColumnId, + toColumnIndex: 0, + forceNextTick: true, + isOwnAction: true, + }; - expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ - secondCardId, - firstCardId, - thirdCardId, - ]); - }); + await boardStore.moveCardSuccess(cardPayload); - it("should move a card to another column", async () => { - const { boardStore, firstColumn, secondColumn, cards } = setup(); + const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; - const [firstCardId, secondCardId, thirdCardId] = cards.map( - (card) => card.cardId - ); + expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ + secondCardId, + firstCardId, + thirdCardId, + ]); + }); - const cardPayload: CardMove = { - cardId: secondCardId, - oldIndex: 1, - newIndex: 0, - fromColumnId: firstColumn.id, - toColumnId: secondColumn.id, - }; + it("should move a card to another column", async () => { + const { boardStore, firstColumn, secondColumn, cards } = setup(); - await boardStore.moveCardSuccess(cardPayload); + const [firstCardId, secondCardId, thirdCardId] = cards.map( + (card) => card.cardId + ); - const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; - const secondColumnCardsAfterMove = boardStore.board?.columns[1].cards; + const cardPayload = { + cardId: secondCardId, + oldIndex: 1, + newIndex: 0, + fromColumnId: firstColumn.id, + fromColumnIndex: 0, + toColumnId: secondColumn.id, + toColumnIndex: 1, + }; - expect(secondColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ - secondCardId, - ]); + await boardStore.moveCardSuccess({ ...cardPayload, isOwnAction: true }); - expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ - firstCardId, - thirdCardId, - ]); - }); + const firstColumnCardsAfterMove = boardStore.board?.columns[0].cards; + const secondColumnCardsAfterMove = boardStore.board?.columns[1].cards; + + expect(secondColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ + secondCardId, + ]); + + expect(firstColumnCardsAfterMove?.map((card) => card.cardId)).toEqual([ + firstCardId, + thirdCardId, + ]); }); }); @@ -772,8 +855,9 @@ describe("BoardStore", () => { oldIndex: 0, newIndex: 1, fromColumnId: boardStore.board?.columns[0].id ?? "columnId", + fromColumnIndex: 0, toColumnId: boardStore.board?.columns[0].id ?? "columnId", - columnDelta: 0, + toColumnIndex: 0, forceNextTick: false, }; @@ -791,7 +875,9 @@ describe("BoardStore", () => { oldIndex: 0, newIndex: 1, fromColumnId: boardStore.board?.columns[0].id ?? "columnId", + fromColumnIndex: 0, toColumnId: boardStore.board?.columns[0].id ?? "columnId", + toColumnIndex: 0, columnDelta: 0, forceNextTick: false, }; diff --git a/src/modules/data/board/Card.store.ts b/src/modules/data/board/Card.store.ts index 9261a59d02..00e1edc678 100644 --- a/src/modules/data/board/Card.store.ts +++ b/src/modules/data/board/Card.store.ts @@ -1,34 +1,27 @@ import { defineStore } from "pinia"; import { nextTick, ref } from "vue"; -import { useBoardApi } from "./BoardApi.composable"; import { envConfigModule } from "@/store"; -import { - ApiErrorHandlerFactory, - ErrorType, - BoardObjectType, - useErrorHandler, -} from "@/components/error-handling/ErrorHandler.composable"; import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; -import { useBoardStore, useSharedEditMode } from "@data-board"; -import { ElementMove } from "@/types/board/DragAndDrop"; -import { - CardResponse, - ContentElementType, - CreateContentElementBodyParams, -} from "@/serverApi/v3"; +import { CardResponse, ContentElementType } from "@/serverApi/v3"; import { + CreateElementSuccessPayload, DeleteCardSuccessPayload, + DeleteElementSuccessPayload, FetchCardSuccessPayload, + MoveElementSuccessPayload, UpdateCardHeightSuccessPayload, UpdateCardTitleSuccessPayload, + UpdateElementSuccessPayload, } from "./cardActions/cardActionPayload"; import { useCardRestApi } from "./cardActions/cardRestApi.composable"; import { useCardSocketApi } from "./cardActions/cardSocketApi.composable"; +import { useSharedLastCreatedElement } from "@util-board"; +import { useSharedEditMode } from "@data-board"; export const useCardStore = defineStore("cardStore", () => { - const boardStore = useBoardStore(); const cards = ref>({}); + const { lastCreatedElementId } = useSharedLastCreatedElement(); const restApi = useCardRestApi(); const isSocketEnabled = @@ -36,16 +29,8 @@ export const useCardStore = defineStore("cardStore", () => { const socketOrRest = isSocketEnabled ? useCardSocketApi() : restApi; - const { handleError, notifyWithTemplate } = useErrorHandler(); const { setFocus } = useBoardFocusHandler(); - const { setEditModeId } = useSharedEditMode(); - - const { - createElementCall, - // deleteCardCall, - deleteElementCall, - moveElementCall, - } = useBoardApi(); + const { setEditModeId, editModeId } = useSharedEditMode(); const fetchCardRequest = socketOrRest.fetchCardRequest; @@ -91,151 +76,129 @@ export const useCardStore = defineStore("cardStore", () => { const card = cards.value[payload.cardId]; if (card === undefined) return; + if (payload.cardId === editModeId.value) { + setEditModeId(undefined); + } delete cards.value[payload.cardId]; }; - const addElement = async ( - type: ContentElementType, - cardId: string, - atFirstPosition?: boolean - ) => { - const card = cards.value[cardId]; + const createElementRequest = socketOrRest.createElementRequest; + + const createElementSuccess = async (payload: CreateElementSuccessPayload) => { + const card = cards.value[payload.cardId]; if (card === undefined) return; - try { - const params: CreateContentElementBodyParams = { type }; - if (atFirstPosition) { - params.toPosition = 0; - } - const response = await createElementCall(card.id, params); - - if (atFirstPosition) { - card.elements.splice(0, 0, response.data); - } else { - card.elements.push(response.data); - } - - setFocus(response.data.id); - return response.data; - } catch (error) { - handleError(error, { - 404: notifyWithTemplateAndReload("notCreated", "boardElement"), - 400: notifyWithTemplate("notCreated", "boardElement"), - }); + const { toPosition } = payload; + if ( + toPosition !== undefined && + toPosition >= 0 && + toPosition <= card.elements.length + ) { + card.elements.splice(toPosition, 0, payload.newElement); + } else { + card.elements.push(payload.newElement); } + + lastCreatedElementId.value = payload.newElement.id; + if (payload.isOwnAction === true) { + setFocus(payload.newElement.id); + } + return payload.newElement; }; const addTextAfterTitle = async (cardId: string) => { const card = cards.value[cardId]; if (card === undefined) return; - return await addElement(ContentElementType.RichText, card.id, true); + return await createElementRequest({ + type: ContentElementType.RichText, + cardId: card.id, + toPosition: 0, + }); }; - const moveElementDown = async ( + const moveElementRequest = async ( cardId: string, - elementPayload: ElementMove + elementId: string, + elementIndex: number, + delta: 1 | -1 ) => { const card = cards.value[cardId]; if (card === undefined) return; - try { - const { elementIndex, payload: elementId } = elementPayload; - if (elementIndex === card.elements.length - 1 || elementIndex === -1) { - return; - } - await moveElement(card, elementIndex, elementId, "down"); - await moveElementCall(elementId, cardId, elementIndex + 1); - } catch (error) { - handleError(error, { - 404: notifyWithTemplateAndReload("notUpdated"), - }); - } - }; - - const moveElementUp = async (cardId: string, elementPayload: ElementMove) => { - const card = cards.value[cardId]; - if (card === undefined) return; - try { - const { elementIndex, payload: elementId } = elementPayload; - if (elementIndex <= 0) return; + const toPosition = elementIndex + delta; + if (toPosition < 0) return; + if (toPosition >= card.elements.length) return; - await moveElement(card, elementIndex, elementId, "up"); - await moveElementCall(elementId, cardId, elementIndex - 1); - } catch (error) { - handleError(error, { - 404: notifyWithTemplateAndReload("notUpdated"), - }); - } + socketOrRest.moveElementRequest({ + elementId, + toCardId: cardId, + toPosition, + }); }; - const moveElement = async ( - card: CardResponse, - elementIndex: number, - elementId: string, - direction: "up" | "down" - ) => { + const moveElementSuccess = async (payload: MoveElementSuccessPayload) => { + const card = cards.value[payload.toCardId]; if (card === undefined) return; - const element = card.elements.filter( - (element) => element.id === elementId - )[0]; - - const delta = direction === "up" ? -1 : 1; + const element = card.elements.find((e) => e.id === payload.elementId); - card.elements.splice(elementIndex, 1); - await nextTick(); - card.elements.splice(elementIndex + delta, 0, element); + if (element) { + card.elements.splice(card.elements.indexOf(element), 1); + await nextTick(); + const toPosition = Math.min(payload.toPosition, card.elements.length); + card.elements.splice(toPosition, 0, element); + } }; - const deleteElement = async ( - cardId: string, - elementId: string + const deleteElementRequest = socketOrRest.deleteElementRequest; + + const deleteElementSuccess = async ( + payload: DeleteElementSuccessPayload ): Promise => { - const card = cards.value[cardId]; + const card = cards.value[payload.cardId]; if (card === undefined) return; - try { - await deleteElementCall(elementId); - extractElement(card, elementId); - } catch (error) { - handleError(error, { - 404: notifyWithTemplateAndReload("notUpdated"), - }); - } - }; - - const extractElement = (card: CardResponse, elementId: string): void => { - const index = card.elements.findIndex((e) => e.id === elementId); + const index = card.elements.findIndex((e) => e.id === payload.elementId); if (index !== undefined && index > -1) { card.elements.splice(index, 1); } }; - const notifyWithTemplateAndReload: ApiErrorHandlerFactory = ( - errorType: ErrorType, - boardObjectType?: BoardObjectType - ) => { - return () => { - notifyWithTemplate(errorType, boardObjectType)(); - boardStore.reloadBoard(); - setEditModeId(undefined); - }; + const updateElementRequest = socketOrRest.updateElementRequest; + + const updateElementSuccess = async (payload: UpdateElementSuccessPayload) => { + const cardToUpdate = Object.values(cards.value).find((c) => + c.elements.some((e) => e.id === payload.elementId) + ); + if (cardToUpdate === undefined) return; + const cardId = cardToUpdate.id; + + if (cardId) { + const elementIndex = cardToUpdate.elements.findIndex( + (e) => e.id === payload.elementId + ); + cards.value[cardId].elements[elementIndex].content = payload.data.content; + } }; return { - addElement, + createElementRequest, + createElementSuccess, + deleteElementRequest, + deleteElementSuccess, + updateElementRequest, + updateElementSuccess, addTextAfterTitle, fetchCardRequest, fetchCardSuccess, cards, deleteCardRequest, deleteCardSuccess, - deleteElement, getCard, - moveElementDown, - moveElementUp, + moveElementRequest, + moveElementSuccess, resetState, updateCardHeightRequest, updateCardHeightSuccess, diff --git a/src/modules/data/board/Card.store.unit.ts b/src/modules/data/board/Card.store.unit.ts index 2eb7a40ef1..25b885c98a 100644 --- a/src/modules/data/board/Card.store.unit.ts +++ b/src/modules/data/board/Card.store.unit.ts @@ -1,5 +1,5 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; -import { useBoardNotifier } from "@util-board"; +import { useBoardNotifier, useSharedLastCreatedElement } from "@util-board"; import { useBoardApi } from "./BoardApi.composable"; import { useSharedEditMode } from "./EditMode.composable"; import { useI18n } from "vue-i18n"; @@ -10,12 +10,17 @@ import { DeepMocked, createMock } from "@golevelup/ts-jest"; import { createPinia, setActivePinia } from "pinia"; import setupStores from "@@/tests/test-utils/setupStores"; import EnvConfigModule from "@/store/env-config"; -import { nextTick, ref } from "vue"; +import { Ref, ref } from "vue"; import { envConfigModule } from "@/store"; -import { envsFactory } from "@@/tests/test-utils"; +import { + envsFactory, + richTextElementContentFactory, + richTextElementResponseFactory, +} from "@@/tests/test-utils"; import { cardResponseFactory } from "@@/tests/test-utils/factory/cardResponseFactory"; import { ContentElementType } from "@/serverApi/v3"; -import { AnyContentElement } from "@/types/board/ContentElement"; +import { drawingContentElementResponseFactory } from "@@/tests/test-utils/factory/drawingContentElementResponseFactory"; +import { cloneDeep } from "lodash"; jest.mock("vue-i18n"); (useI18n as jest.Mock).mockReturnValue({ t: (key: string) => key }); @@ -34,6 +39,7 @@ const mockedSharedEditMode = jest.mocked(useSharedEditMode); jest.mock("@util-board"); const mockedUseBoardNotifier = jest.mocked(useBoardNotifier); +const mockedSharedLastCreatedElement = jest.mocked(useSharedLastCreatedElement); jest.mock("@/components/error-handling/ErrorHandler.composable"); const mockedUseErrorHandler = jest.mocked(useErrorHandler); @@ -52,7 +58,11 @@ describe("CardStore", () => { ReturnType >; let mockedCardRestApiActions: DeepMocked>; + let mockedSharedLastCreatedElementActions: DeepMocked< + ReturnType + >; let setEditModeId: jest.Mock; + let editModeId: Ref; beforeEach(() => { setActivePinia(createPinia()); @@ -78,10 +88,17 @@ describe("CardStore", () => { mockedCardRestApiActions = createMock>(); mockedUseCardRestApi.mockReturnValue(mockedCardRestApiActions); + mockedSharedLastCreatedElementActions = + createMock>(); + mockedSharedLastCreatedElement.mockReturnValue( + mockedSharedLastCreatedElementActions + ); + setEditModeId = jest.fn(); + editModeId = ref(undefined); mockedSharedEditMode.mockReturnValue({ setEditModeId, - editModeId: ref(undefined), + editModeId, }); }); @@ -95,17 +112,24 @@ describe("CardStore", () => { const cardStore = useCardStore(); const cards = cardResponseFactory.buildList(3); + const elements = richTextElementResponseFactory.buildList(3); - cardStore.fetchCardSuccess({ cards }); + const cardId = cards[0].id; + const card = cards[0]; + card.elements = elements; - return { cardStore, cardId: cards[0].id }; + for (const card of cards) { + cardStore.cards[card.id] = card; + } + + return { cardStore, cardId, elements }; }; afterEach(() => { jest.resetAllMocks(); }); - describe("fetchCardTitleRequest", () => { + describe("fetchCardRequest", () => { it("should call socket Api if feature flag is enabled", async () => { const { cardStore } = setup(true); const cardIds = ["id1", "id2aewr", "id3423"]; @@ -131,7 +155,7 @@ describe("CardStore", () => { const { cardStore } = setup(); const cards = cardResponseFactory.buildList(3); - cardStore.fetchCardSuccess({ cards }); + cardStore.fetchCardSuccess({ cards, isOwnAction: true }); expect(cardStore.getCard(cards[0].id)).toEqual(cards[0]); }); @@ -169,61 +193,62 @@ describe("CardStore", () => { it("should not delete any card when card is undefined", async () => { const { cardStore } = setup(); - const cardTitles = Object.values(cardStore.cards).map( - (card) => card.title - ); + const oldCards = cloneDeep(cardStore.cards); cardStore.deleteCardSuccess({ cardId: "unkownId", + isOwnAction: true, }); - expect(Object.values(cardStore.cards).map((card) => card.title)).toEqual( - cardTitles - ); + expect(cardStore.cards).toEqual(oldCards); }); - it("should delete card", async () => { + it("set editModeId to undefined if cardId is equal to editModeId", async () => { + const { cardStore, cardId } = setup(); + + editModeId.value = cardId; + + cardStore.deleteCardSuccess({ + cardId, + isOwnAction: true, + }); + + expect(setEditModeId).toHaveBeenCalledWith(undefined); + }); + + it("should delete a card", async () => { const { cardStore, cardId } = setup(); cardStore.deleteCardSuccess({ cardId, + isOwnAction: true, }); - expect(cardStore.getCard(cardId)).toBeUndefined(); + expect(cardStore.cards[cardId]).toBeUndefined(); }); }); describe("updateCardTitleRequest", () => { it("should call socket Api if feature flag is enabled", () => { const { cardStore, cardId } = setup(true); + const payload = { cardId, newTitle: "newTitle" }; - cardStore.updateCardTitleRequest({ - cardId, - newTitle: "newTitle", - }); + cardStore.updateCardTitleRequest(payload); expect( mockedCardSocketApiActions.updateCardTitleRequest - ).toHaveBeenCalledWith({ - cardId, - newTitle: "newTitle", - }); + ).toHaveBeenCalledWith(payload); }); it("should call rest Api if feature flag is enabled", () => { const { cardStore, cardId } = setup(); + const payload = { cardId, newTitle: "newTitle" }; - cardStore.updateCardTitleRequest({ - cardId, - newTitle: "newTitle", - }); + cardStore.updateCardTitleRequest(payload); expect( mockedCardRestApiActions.updateCardTitleRequest - ).toHaveBeenCalledWith({ - cardId, - newTitle: "newTitle", - }); + ).toHaveBeenCalledWith(payload); }); }); @@ -239,6 +264,7 @@ describe("CardStore", () => { cardStore.updateCardTitleSuccess({ cardId: "unkownId", newTitle: NEW_TITLE, + isOwnAction: true, }); expect(Object.values(cardStore.cards).map((card) => card.title)).toEqual( @@ -252,6 +278,7 @@ describe("CardStore", () => { cardStore.updateCardTitleSuccess({ cardId, newTitle: NEW_TITLE, + isOwnAction: true, }); expect(cardStore.cards[cardId].title).toEqual(NEW_TITLE); @@ -261,34 +288,24 @@ describe("CardStore", () => { describe("updateCardHeightRequest", () => { it("should call socket Api if feature flag is enabled", () => { const { cardStore, cardId } = setup(true); + const payload = { cardId, newHeight: 100 }; - cardStore.updateCardHeightRequest({ - cardId, - newHeight: 100, - }); + cardStore.updateCardHeightRequest(payload); expect( mockedCardSocketApiActions.updateCardHeightRequest - ).toHaveBeenCalledWith({ - cardId, - newHeight: 100, - }); + ).toHaveBeenCalledWith(payload); }); it("should call rest Api if feature flag is enabled", () => { const { cardStore, cardId } = setup(); + const payload = { cardId, newHeight: 100 }; - cardStore.updateCardHeightRequest({ - cardId, - newHeight: 100, - }); + cardStore.updateCardHeightRequest(payload); expect( mockedCardRestApiActions.updateCardHeightRequest - ).toHaveBeenCalledWith({ - cardId, - newHeight: 100, - }); + ).toHaveBeenCalledWith(payload); }); }); @@ -304,6 +321,7 @@ describe("CardStore", () => { cardStore.updateCardHeightSuccess({ cardId: "unkownId", newHeight: NEW_HEIGHT, + isOwnAction: true, }); expect(Object.values(cardStore.cards).map((card) => card.height)).toEqual( @@ -317,6 +335,7 @@ describe("CardStore", () => { cardStore.updateCardHeightSuccess({ cardId, newHeight: NEW_HEIGHT, + isOwnAction: true, }); expect(cardStore.cards[cardId].height).toEqual(NEW_HEIGHT); @@ -341,158 +360,323 @@ describe("CardStore", () => { }); }); - describe("addElement", () => { - it("should not add element when card is undefined", async () => { - const { cardStore } = setup(); + describe("createElementRequest", () => { + it("should call socket Api if feature flag is enabled", async () => { + const { cardStore, cardId } = setup(true); - const cardHeights = Object.values(cardStore.cards).map( - (card) => card.height - ); + const payload = { + type: ContentElementType.Link, + cardId, + }; - await cardStore.addElement(ContentElementType.Link, "unknownId"); + await cardStore.createElementRequest(payload); - expect(Object.values(cardStore.cards).map((card) => card.height)).toEqual( - cardHeights - ); + expect( + mockedCardSocketApiActions.createElementRequest + ).toHaveBeenCalledWith(payload); }); - it("should add element", async () => { + it("should call rest Api if feature flag is disabled", async () => { const { cardStore, cardId } = setup(); - expect(cardStore.cards[cardId].elements.length).toEqual(0); - await cardStore.addElement(ContentElementType.Drawing, cardId); + const payload = { + type: ContentElementType.Link, + cardId, + }; + + await cardStore.createElementRequest(payload); - expect(cardStore.cards[cardId].elements.length).toEqual(1); + expect( + mockedCardRestApiActions.createElementRequest + ).toHaveBeenCalledWith(payload); }); }); - describe("moveElement", () => { - let elements: AnyContentElement[] = []; - - beforeEach(() => { - elements = []; - elements.push({ - id: "link-1", - content: { - url: "https://www.google.com/", - title: "Google", - description: "", - imageUrl: "", - }, - timestamps: { - lastUpdatedAt: "2024-05-13T14:59:46.909Z", - createdAt: "2024-05-13T14:59:46.909Z", - }, - type: ContentElementType.Link, + describe("createElementSuccess", () => { + describe("when element is provided", () => { + it("should add element to specified position", async () => { + const { cardStore, cardId } = setup(); + const newElement = drawingContentElementResponseFactory.build(); + const toPosition = 1; + + await cardStore.createElementSuccess({ + type: ContentElementType.Drawing, + cardId, + newElement, + toPosition, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements.length).toEqual(4); + expect(cardStore.cards[cardId].elements[toPosition]).toEqual( + newElement + ); }); + it("should add element to last position if toPosition is undefined", async () => { + const { cardStore, cardId } = setup(); + const newElement = drawingContentElementResponseFactory.build(); - elements.push({ - id: "link-2", - content: { - url: "https://www.google.com/", - title: "Google", - description: "", - imageUrl: "", - }, - timestamps: { - lastUpdatedAt: "2024-05-13T14:59:46.909Z", - createdAt: "2024-05-13T14:59:46.909Z", - }, - type: ContentElementType.Link, + expect(cardStore.cards[cardId].elements.length).toEqual(3); + await cardStore.createElementSuccess({ + type: ContentElementType.Drawing, + cardId, + newElement, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements.length).toEqual(4); + expect(cardStore.cards[cardId].elements[3]).toEqual(newElement); }); }); - describe("moveElementDown", () => { - it("should move element down", async () => { - const { cardStore, cardId } = setup(); - cardStore.cards[cardId].elements.push(...elements); - - cardStore.moveElementDown(cardId, { - elementIndex: 0, - payload: elements[0].id, + describe("when cardId is invalid", () => { + it("should not add element", async () => { + const { cardStore } = setup(); + const newElement = drawingContentElementResponseFactory.build(); + + expect(Object.keys(cardStore.cards).length).toEqual(3); + await cardStore.createElementSuccess({ + type: ContentElementType.Drawing, + cardId: "invalidId", + newElement, + isOwnAction: true, }); - expect(cardStore.cards[cardId].elements[0].id).toEqual(elements[1].id); + expect(Object.keys(cardStore.cards).length).toEqual(3); }); + }); - it("should not move element down when elementIndex is last", async () => { + describe("when new position is invalid", () => { + it("should not add element", async () => { const { cardStore, cardId } = setup(); - cardStore.cards[cardId].elements.push(...elements); + const newElement = drawingContentElementResponseFactory.build(); - cardStore.moveElementDown(cardId, { - elementIndex: 1, - payload: elements[1].id, + expect(Object.keys(cardStore.cards).length).toEqual(3); + await cardStore.createElementSuccess({ + type: ContentElementType.Drawing, + cardId, + newElement, + toPosition: 100, + isOwnAction: true, }); - expect(cardStore.cards[cardId].elements[1].id).toEqual(elements[1].id); + expect(Object.keys(cardStore.cards).length).toEqual(3); }); }); + }); - describe("moveElementUp", () => { - it("should move element up", async () => { - const { cardStore, cardId } = setup(); - cardStore.cards[cardId].elements.push(...elements); + describe("moveElementRequest", () => { + const MOVE_UP = -1; + const MOVE_DOWN = 1; + it("should not move element when card is undefined", async () => { + const { cardStore } = setup(); - const currentCardElements = [...cardStore.cards[cardId].elements]; + await cardStore.moveElementRequest( + "unknownId", + " elementId", + -1, + MOVE_DOWN + ); - cardStore.moveElementUp(cardId, { - elementIndex: 1, - payload: currentCardElements[1].id, - }); + expect( + mockedCardRestApiActions.moveElementRequest + ).not.toHaveBeenCalled(); + }); - await nextTick(); + it("should not move element up if first element is moved", async () => { + const { cardStore, cardId, elements } = setup(); - expect(cardStore.cards[cardId].elements[0].id).toEqual( - currentCardElements[1].id - ); + const elementId = elements[0].id; + await cardStore.moveElementRequest(cardId, elementId, 0, MOVE_UP); + + expect(cardStore.cards[cardId].elements[0].id).toEqual(elementId); + }); + + it("should not move element down if last element is moved", async () => { + const { cardStore, cardId, elements } = setup(); + const lastIndex = elements.length - 1; + const elementId = elements[lastIndex].id; + + await cardStore.moveElementRequest( + cardId, + elementId, + lastIndex, + MOVE_DOWN + ); + + expect(cardStore.cards[cardId].elements[2].id).toEqual(elementId); + }); + + it("should call socket Api if feature flag is enabled", async () => { + const { cardStore, cardId, elements } = setup(true); + const elementId = elements[0].id; + + await cardStore.moveElementRequest(cardId, elementId, 0, MOVE_DOWN); + + expect( + mockedCardSocketApiActions.moveElementRequest + ).toHaveBeenCalledWith({ + elementId, + toCardId: cardId, + toPosition: 1, }); + }); - it("should not move element up when elementIndex is 0", async () => { - const { cardStore, cardId } = setup(); - cardStore.cards[cardId].elements.push(...elements); + it("should call rest Api if feature flag is disabled", async () => { + const { cardStore, cardId, elements } = setup(); + const elementId = elements[0].id; - cardStore.moveElementUp(cardId, { - elementIndex: 0, - payload: elements[0].id, - }); + await cardStore.moveElementRequest(cardId, elementId, 0, MOVE_DOWN); - expect(cardStore.cards[cardId].elements[0].id).toEqual(elements[0].id); + expect(mockedCardRestApiActions.moveElementRequest).toHaveBeenCalledWith({ + elementId, + toCardId: cardId, + toPosition: 1, }); }); }); - describe("deleteElement", () => { - it("should delete element", async () => { - const { cardStore, cardId } = setup(); - const elementId = "elementId"; - cardStore.cards[cardId].elements.push({ - id: elementId, - content: { - url: "https://www.google.com/", - title: "Google", - description: "", - imageUrl: "", - }, - timestamps: { - lastUpdatedAt: "2024-05-13T14:59:46.909Z", - createdAt: "2024-05-13T14:59:46.909Z", - }, - type: ContentElementType.Link, + describe("moveElementSuccess", () => { + it("should not move element when card is undefined", async () => { + const { cardStore, cardId, elements } = setup(); + const elementId = elements[0].id; + + await cardStore.moveElementSuccess({ + elementId: elements[0].id, + toCardId: "unknownId", + toPosition: 1, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements[0].id).toEqual(elementId); + }); + + it("should move element down", async () => { + const { cardStore, cardId, elements } = setup(); + const elementId = elements[0].id; + const toPosition = 1; + + await cardStore.moveElementSuccess({ + elementId, + toCardId: cardId, + toPosition, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements[toPosition].id).toEqual( + elementId + ); + }); + + it("should move element up", async () => { + const { cardStore, cardId, elements } = setup(); + + const elementId = elements[2].id; + const toPosition = 1; + await cardStore.moveElementSuccess({ + elementId, + toCardId: cardId, + toPosition, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements[1].id).toEqual(elementId); + }); + }); + + describe("deleteElementSuccess", () => { + it("should not delete element if card is undefined", async () => { + const { cardStore, cardId, elements } = setup(); + const numberOfElements = cardStore.cards[cardId].elements.length; + const elementId = elements[0].id; + + await cardStore.deleteElementSuccess({ + cardId: "unkown", + elementId, + isOwnAction: true, }); - await cardStore.deleteElement(cardId, elementId); + expect(cardStore.cards[cardId].elements.length).toEqual(numberOfElements); + }); + it("should delete element", async () => { + const { cardStore, cardId, elements } = setup(); + const numberOfElements = cardStore.cards[cardId].elements.length; + const elementId = elements[0].id; + + await cardStore.deleteElementSuccess({ + cardId, + elementId, + isOwnAction: true, + }); - expect(cardStore.cards[cardId].elements.length).toEqual(0); + expect(cardStore.cards[cardId].elements.length).toEqual( + numberOfElements - 1 + ); }); }); describe("addTextAfterTitle", () => { + it("should not add text after title when card is undefined", async () => { + const { cardStore } = setup(); + + await cardStore.addTextAfterTitle("unknownId"); + + expect( + mockedCardRestApiActions.createElementRequest + ).not.toHaveBeenCalled(); + }); + it("should add text after title", async () => { - const { cardStore, cardId } = setup(); + const { cardStore, cardId, elements } = setup(); await cardStore.addTextAfterTitle(cardId); - expect(cardStore.cards[cardId].elements.length).toEqual(1); + const expectedCall = { + type: elements[0].type, + cardId, + toPosition: 0, + }; + + expect( + mockedCardRestApiActions.createElementRequest + ).toHaveBeenCalledWith(expectedCall); + }); + }); + + describe("updateElementSuccess", () => { + it("should not update element if element id does not belong to a card ", async () => { + const { cardStore, cardId } = setup(); + + const oldElements = cloneDeep(cardStore.cards[cardId].elements); + + await cardStore.updateElementSuccess({ + elementId: "non existing id", + data: { + type: ContentElementType.RichText, + content: richTextElementContentFactory.build(), + }, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements).toEqual(oldElements); + }); + + it("should update element", async () => { + const { cardStore, cardId, elements } = setup(); + + const elementToUpdate = elements[0]; + const newContent = richTextElementContentFactory.build(); + + await cardStore.updateElementSuccess({ + elementId: elementToUpdate.id, + data: { + type: elementToUpdate.type, + content: newContent, + }, + isOwnAction: true, + }); + + expect(cardStore.cards[cardId].elements[0].content).toEqual(newContent); }); }); }); diff --git a/src/modules/data/board/ContentElementState.composable.ts b/src/modules/data/board/ContentElementState.composable.ts index e31b35ccf0..1e80cbd88d 100644 --- a/src/modules/data/board/ContentElementState.composable.ts +++ b/src/modules/data/board/ContentElementState.composable.ts @@ -1,8 +1,7 @@ import { watchDebounced } from "@vueuse/core"; -import { computed, ComputedRef, Ref, ref, toRef, unref, UnwrapRef } from "vue"; -import { useBoardApi } from "./BoardApi.composable"; +import { computed, ComputedRef, Ref, ref, toRef, unref } from "vue"; import { AnyContentElement } from "@/types/board/ContentElement"; -import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; +import { useCardStore } from "./Card.store"; export const useContentElementState = ( props: { @@ -11,48 +10,28 @@ export const useContentElementState = ( }, options: { autoSaveDebounce?: number } = { autoSaveDebounce: 300 } ) => { + const cardStore = useCardStore(); const _elementRef: Ref = toRef(props, "element"); - const _responseValue = ref(unref(_elementRef)); - - const { handleError, notifyWithTemplate } = useErrorHandler(); - const { updateElementCall } = useBoardApi(); const modelValue = ref(unref(_elementRef).content); - const computedElement: ComputedRef = computed(() => ({ - ..._elementRef.value, - ..._responseValue.value, - })); + const computedElement: ComputedRef = computed(() => _elementRef.value); const isLoading = ref(false); watchDebounced( modelValue.value, async (modelValue) => { - await updateElement(modelValue); + cardStore.updateElementRequest({ + element: { + ..._elementRef.value, + content: modelValue, + }, + }); }, { debounce: options.autoSaveDebounce, maxWait: 2500 } ); - // TODO: refactor this to be properly typed - const updateElement = async (content: T["content"]) => { - isLoading.value = true; - const payload = { - ...computedElement.value, - content: { ...content }, - }; - try { - const response = await updateElementCall(payload); - _responseValue.value = response.data as UnwrapRef; - } catch (error) { - handleError(error, { - 404: notifyWithTemplate("notUpdated", "boardElement"), - }); - } finally { - isLoading.value = false; - } - }; - return { /** * Contains the content property of the element. diff --git a/src/modules/data/board/ContentElementState.unit.ts b/src/modules/data/board/ContentElementState.unit.ts index 4ec2cb6de2..6014c3fc7f 100644 --- a/src/modules/data/board/ContentElementState.unit.ts +++ b/src/modules/data/board/ContentElementState.unit.ts @@ -4,6 +4,12 @@ import { useContentElementState } from "./ContentElementState.composable"; import { NOTIFIER_MODULE_KEY } from "@/utils/inject"; import { createModuleMocks } from "@/utils/mock-store-module"; import NotifierModule from "@/store/notifier"; +import { createTestingPinia } from "@pinia/testing"; +import { setActivePinia } from "pinia"; +import { envConfigModule } from "@/store"; +import { envsFactory } from "@@/tests/test-utils"; +import setupStores from "@@/tests/test-utils/setupStores"; +import EnvConfigModule from "@/store/env-config"; jest.mock("@feature-board/shared/InlineEditInteractionHandler.composable"); @@ -29,6 +35,14 @@ jest.mock("vue-i18n", () => { }); describe("useContentElementState composable", () => { + beforeEach(() => { + setupStores({ envConfigModule: EnvConfigModule }); + const envs = envsFactory.build({ + FEATURE_COLUMN_BOARD_SOCKET_ENABLED: false, + }); + envConfigModule.setEnvs(envs); + setActivePinia(createTestingPinia()); + }); const setup = (options = { isEditMode: false, element: TEST_ELEMENT }) => { return mountComposable(() => useContentElementState(options), { global: { diff --git a/src/modules/data/board/boardActions/boardActionPayload.ts b/src/modules/data/board/boardActions/boardActionPayload.ts index 3348e4f5a8..672d583df1 100644 --- a/src/modules/data/board/boardActions/boardActionPayload.ts +++ b/src/modules/data/board/boardActions/boardActionPayload.ts @@ -11,6 +11,7 @@ export type CreateCardRequestPayload = { export type CreateCardSuccessPayload = { newCard: CardResponse; columnId: string; + isOwnAction: boolean; }; export type CreateCardFailurePayload = { errorType: ErrorType; @@ -20,8 +21,9 @@ export type CreateCardFailurePayload = { export type CreateColumnRequestPayload = { boardId: string; }; -export type CreateColumnSucccessPayload = { +export type CreateColumnSuccessPayload = { newColumn: ColumnResponse; + isOwnAction: boolean; }; export type CreateColumnFailurePayload = { errorType: ErrorType; @@ -38,6 +40,7 @@ export type DeleteColumnRequestPayload = { }; export type DeleteColumnSuccessPayload = { columnId: string; + isOwnAction: boolean; }; export type DeleteColumnFailurePayload = { errorType: ErrorType; @@ -49,8 +52,9 @@ export type MoveCardRequestPayload = { oldIndex: number; newIndex: number; fromColumnId: string; + fromColumnIndex: number; toColumnId?: string; - columnDelta?: number; + toColumnIndex?: number; forceNextTick?: boolean; }; export type MoveCardSuccessPayload = { @@ -58,9 +62,11 @@ export type MoveCardSuccessPayload = { oldIndex: number; newIndex: number; fromColumnId: string; - toColumnId?: string; - columnDelta?: number; + fromColumnIndex: number; + toColumnId: string; + toColumnIndex: number; forceNextTick?: boolean; + isOwnAction: boolean; }; export type MoveCardFailurePayload = { errorType: ErrorType; @@ -75,6 +81,7 @@ export type MoveColumnRequestPayload = { export type MoveColumnSuccessPayload = { columnMove: ColumnMove; byKeyboard: boolean; + isOwnAction: boolean; }; export type MoveColumnFailurePayload = { errorType: ErrorType; @@ -88,6 +95,7 @@ export type UpdateColumnTitleRequestPayload = { export type UpdateColumnTitleSuccessPayload = { columnId: string; newTitle: string; + isOwnAction: boolean; }; export type UpdateColumnTitleFailurePayload = { errorType: ErrorType; @@ -99,6 +107,7 @@ export type DeleteBoardRequestPayload = { }; export type DeleteBoardSuccessPayload = { id: string; + isOwnAction: boolean; }; export type DeleteBoardFailurePayload = { errorType: ErrorType; @@ -110,6 +119,7 @@ export type ReloadBoardPayload = { }; export type ReloadBoardSuccessPayload = { id: string; + isOwnAction: boolean; }; export type UpdateBoardTitleRequestPayload = { @@ -119,6 +129,7 @@ export type UpdateBoardTitleRequestPayload = { export type UpdateBoardTitleSuccessPayload = { boardId: string; newTitle: string; + isOwnAction: boolean; }; export type UpdateBoardTitleFailurePayload = { errorType: ErrorType; @@ -132,6 +143,7 @@ export type UpdateBoardVisibilityRequestPayload = { export type UpdateBoardVisibilitySuccessPayload = { boardId: string; isVisible: boolean; + isOwnAction: boolean; }; export type UpdateBoardVisibilityFailurePayload = { errorType: ErrorType; diff --git a/src/modules/data/board/boardActions/boardActions.ts b/src/modules/data/board/boardActions/boardActions.ts index d7613ee105..390f89e1d8 100644 --- a/src/modules/data/board/boardActions/boardActions.ts +++ b/src/modules/data/board/boardActions/boardActions.ts @@ -3,7 +3,7 @@ import { CreateCardRequestPayload, CreateCardSuccessPayload, CreateColumnFailurePayload, - CreateColumnSucccessPayload, + CreateColumnSuccessPayload, DeleteBoardFailurePayload, DeleteBoardRequestPayload, DeleteBoardSuccessPayload, @@ -58,7 +58,7 @@ export const createColumnRequest = createAction( ); export const createColumnSuccess = createAction( "create-column-success", - props() + props() ); export const createColumnFailure = createAction( "create-column-failure", diff --git a/src/modules/data/board/boardActions/boardRestApi.composable.ts b/src/modules/data/board/boardActions/boardRestApi.composable.ts index bc8ac7b335..fb23898018 100644 --- a/src/modules/data/board/boardActions/boardRestApi.composable.ts +++ b/src/modules/data/board/boardActions/boardRestApi.composable.ts @@ -3,8 +3,6 @@ import { useBoardApi } from "../BoardApi.composable"; import { useSharedEditMode } from "../EditMode.composable"; import * as BoardActions from "./boardActions"; import { useBoardFocusHandler } from "../BoardFocusHandler.composable"; -import { CardMove } from "@/types/board/DragAndDrop"; -import { ColumnResponse } from "@/serverApi/v3"; import { BoardObjectType, ErrorType, @@ -39,28 +37,6 @@ export const useBoardRestApi = () => { const { setEditModeId } = useSharedEditMode(); - const getColumnIndex = (columnId: string | undefined): number => { - if (columnId === undefined) return -1; - if (boardStore.board === undefined) return -1; - - const columnIndex = boardStore.board?.columns.findIndex( - (c) => c.id === columnId - ); - - if (columnIndex === undefined) return -1; - return columnIndex; - }; - - const getColumnId = (columnIndex: number): string | undefined => { - if (boardStore.board === undefined) return; - if (columnIndex === undefined) return; - if (columnIndex < 0) return; - if (columnIndex > boardStore.board.columns.length - 1) return; - if (boardStore.board.columns[columnIndex] === undefined) return; - - return boardStore.board.columns[columnIndex].id; - }; - const createCardRequest = async (payload: CreateCardRequestPayload) => { if (boardStore.board === undefined) return; @@ -70,6 +46,7 @@ export const useBoardRestApi = () => { boardStore.createCardSuccess({ newCard, columnId: payload.columnId, + isOwnAction: true, }); } catch (error) { handleError(error, { @@ -101,7 +78,7 @@ export const useBoardRestApi = () => { useBoardFocusHandler().setFocus(newColumn.id); setEditModeId(newColumn.id); - boardStore.createColumnSuccess({ newColumn }); + boardStore.createColumnSuccess({ newColumn, isOwnAction: true }); return newColumn; } catch (error) { handleError(error, { @@ -116,7 +93,7 @@ export const useBoardRestApi = () => { try { await deleteColumnCall(columnId); - boardStore.deleteColumnSuccess({ columnId }); + boardStore.deleteColumnSuccess({ columnId, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notDeleted", "boardColumn"), @@ -124,99 +101,37 @@ export const useBoardRestApi = () => { } }; - const getColumnIndices = ( - fromColumnId: string | undefined, - toColumnId: string | undefined, - columnDelta: number | undefined - ) => { - const fromColumnIndex = getColumnIndex(fromColumnId); - - const newColumnId: string | undefined = - columnDelta === undefined - ? toColumnId - : getColumnId(fromColumnIndex + columnDelta); - - return { fromColumnIndex, toColumnIndex: getColumnIndex(newColumnId) }; - }; - - const isMoveValid = ( - payload: CardMove, - columns: Array, - targetColumnIndex: number, - fromColumnIndex: number - ) => { - const { newIndex, oldIndex } = payload; - - const movedInsideColumn = fromColumnIndex === targetColumnIndex; - if (movedInsideColumn) { - if ( - (newIndex === oldIndex && fromColumnIndex === targetColumnIndex) || // same position - newIndex < 0 || // first card - can't move up - newIndex > columns[fromColumnIndex].cards.length - 1 // last card - can't move down - ) - return false; - } - - return true; - }; - const moveCardRequest = async ( payload: MoveCardRequestPayload ): Promise => { if (boardStore.board === undefined) return; try { - const { - cardId, - newIndex, - oldIndex, - toColumnId, - fromColumnId, - columnDelta, - forceNextTick, - } = payload; - - const { fromColumnIndex, toColumnIndex } = getColumnIndices( - fromColumnId, - toColumnId, - columnDelta - ); + const { cardId, newIndex, oldIndex, fromColumnId } = payload; + let { toColumnId, toColumnIndex } = payload; - let targetColumnIndex = toColumnIndex; - let targetColumnId = toColumnId; + const isInSameColumn = toColumnId === fromColumnId; - if (targetColumnIndex === -1) { + if (newIndex === oldIndex && isInSameColumn) return; + if (isInSameColumn && newIndex === -1 && oldIndex === 0) { + return; + } + if (toColumnId === undefined && toColumnIndex === undefined) { // need to create a new column const newColumn = await createColumnRequest(); if (newColumn) { - targetColumnId = newColumn.id; - targetColumnIndex = getColumnIndex(targetColumnId); + toColumnId = newColumn.id; + toColumnIndex = boardStore.getLastColumnIndex(); } } - - if (targetColumnId === undefined) return; // shouldn't happen because its either existing or newly created - - if ( - !isMoveValid( - payload, - boardStore.board.columns, - targetColumnIndex, - fromColumnIndex - ) - ) { - return; - } - - await moveCardCall(cardId, targetColumnId, newIndex); + if (toColumnId === undefined || toColumnIndex === undefined) return; // shouldn't happen because its either existing or newly created + await moveCardCall(cardId, toColumnId, newIndex); boardStore.moveCardSuccess({ - cardId, - newIndex, - oldIndex, - toColumnId: targetColumnId, - fromColumnId, - columnDelta, - forceNextTick, + ...payload, + toColumnId, + toColumnIndex, + isOwnAction: true, }); } catch (error) { handleError(error, { @@ -233,7 +148,7 @@ export const useBoardRestApi = () => { const { addedIndex, columnId } = columnMove; await moveColumnCall(columnId, boardStore.board.id, addedIndex); - boardStore.moveColumnSuccess(payload); + boardStore.moveColumnSuccess({ ...payload, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notUpdated", "boardColumn"), @@ -250,7 +165,7 @@ export const useBoardRestApi = () => { try { await updateColumnTitleCall(columnId, newTitle); - boardStore.updateColumnTitleSuccess(payload); + boardStore.updateColumnTitleSuccess({ ...payload, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notUpdated", "boardColumn"), @@ -267,7 +182,11 @@ export const useBoardRestApi = () => { try { await updateBoardTitleCall(boardId, newTitle); - boardStore.updateBoardTitleSuccess({ boardId, newTitle }); + boardStore.updateBoardTitleSuccess({ + boardId, + newTitle, + isOwnAction: true, + }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notUpdated", "board"), @@ -283,7 +202,11 @@ export const useBoardRestApi = () => { try { await updateBoardVisibilityCall(boardId, isVisible); - boardStore.updateBoardVisibilitySuccess({ boardId, isVisible }); + boardStore.updateBoardVisibilitySuccess({ + boardId, + isVisible, + isOwnAction: true, + }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notUpdated", "board"), diff --git a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts index e45df1416c..0c3ea23e0d 100644 --- a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts +++ b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts @@ -19,7 +19,6 @@ import { useBoardRestApi } from "./boardRestApi.composable"; import { useBoardApi } from "../BoardApi.composable"; import { useSharedEditMode } from "../EditMode.composable"; import { ColumnMove } from "@/types/board/DragAndDrop"; -import { MoveCardRequestPayload } from "./boardActionPayload"; jest.mock("@/components/error-handling/ErrorHandler.composable"); const mockedUseErrorHandler = jest.mocked(useErrorHandler); @@ -43,11 +42,6 @@ describe("boardRestApi", () => { beforeEach(() => { setActivePinia(createTestingPinia()); - setupStores({ envConfigModule: EnvConfigModule }); - const envs = envsFactory.build({ - FEATURE_COLUMN_BOARD_SOCKET_ENABLED: false, - }); - envConfigModule.setEnvs(envs); mockedSocketConnectionHandler = createMock>(); @@ -66,7 +60,13 @@ describe("boardRestApi", () => { }); }); - const setup = (createBoard = true) => { + const setup = (createBoard = true, isSocketEnabled = false) => { + setupStores({ envConfigModule: EnvConfigModule }); + const envs = envsFactory.build({ + FEATURE_COLUMN_BOARD_SOCKET_ENABLED: isSocketEnabled, + }); + envConfigModule.setEnvs(envs); + const boardStore = mockedPiniaStoreTyping(useBoardStore); if (createBoard) { const cards = cardSkeletonResponseFactory.buildList(3); @@ -104,6 +104,7 @@ describe("boardRestApi", () => { expect(boardStore.createCardSuccess).toHaveBeenCalledWith({ newCard: newCard, columnId: columnId, + isOwnAction: true, }); }); @@ -183,6 +184,7 @@ describe("boardRestApi", () => { expect(setEditModeId).toHaveBeenCalledWith(newColumn.id); expect(boardStore.createColumnSuccess).toHaveBeenCalledWith({ newColumn, + isOwnAction: true, }); expect(result).toEqual(newColumn); }); @@ -216,7 +218,10 @@ describe("boardRestApi", () => { await deleteColumnRequest({ columnId }); - expect(boardStore.deleteColumnSuccess).toHaveBeenCalledWith({ columnId }); + expect(boardStore.deleteColumnSuccess).toHaveBeenCalledWith({ + columnId, + isOwnAction: true, + }); }); it("should call handleError if the API call fails", async () => { @@ -233,46 +238,24 @@ describe("boardRestApi", () => { }); describe("moveCardRequest", () => { - const createCardPayload = ({ - cardId, - oldIndex, - newIndex, - fromColumnId, - toColumnId, - columnDelta, - }: { - cardId: string; - oldIndex?: number; - newIndex?: number; - fromColumnId: string; - toColumnId?: string; - columnDelta?: number; - }) => { - const cardPayload: MoveCardRequestPayload = { - cardId, - oldIndex: oldIndex ?? 0, - newIndex: newIndex ?? 0, - fromColumnId, - toColumnId, - columnDelta, - }; - return cardPayload; - }; - it("should not call moveCardSuccess when board value is undefined", async () => { - const { boardStore } = setup(false); - const { moveCardRequest } = useBoardRestApi(); + describe("when move is invalid", () => { + it("should not call moveCardSuccess when board value is undefined", async () => { + const { boardStore } = setup(false); + const { moveCardRequest } = useBoardRestApi(); - const cardPayload = createCardPayload({ - cardId: "cardId", - fromColumnId: "columnId", - }); + const cardPayload = { + oldIndex: 0, + newIndex: 1, + cardId: "cardId", + fromColumnId: "columnId", + fromColumnIndex: 0, + }; - await moveCardRequest(cardPayload); + await moveCardRequest(cardPayload); - expect(boardStore.moveCardSuccess).not.toHaveBeenCalled(); - }); + expect(boardStore.moveCardSuccess).not.toHaveBeenCalled(); + }); - describe("when move is invalid", () => { it("should not call moveCardCall if card is moved to the same position", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); @@ -280,50 +263,78 @@ describe("boardRestApi", () => { const firstColumn = boardStore.board!.columns[0]; const movingCard = firstColumn.cards[0]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: movingCard.cardId, fromColumnId: firstColumn.id, + fromColumnIndex: 0, toColumnId: firstColumn.id, - }); + toColumnIndex: 0, + oldIndex: 0, + newIndex: 0, + }; await moveCardRequest(cardPayload); expect(mockedBoardApiCalls.moveCardCall).not.toHaveBeenCalled(); }); - it("should not call moveCardCall if first card is moved to the first position", async () => { + it("should not call moveCardCall if a card in the first column is moved to the left", async () => { + const { boardStore } = setup(); + const { moveCardRequest } = useBoardRestApi(); + + const firstColumn = boardStore.board!.columns[0]; + const movingCard = firstColumn.cards[0]; + + const cardPayload = { + cardId: movingCard.cardId, + oldIndex: 0, + newIndex: 0, + fromColumnId: firstColumn.id, + fromColumnIndex: 0, + toColumnIndex: -1, + }; + + await moveCardRequest(cardPayload); + + expect(mockedBoardApiCalls.moveCardCall).not.toHaveBeenCalled(); + }); + + it("should not call moveCardCall if first card is moved to the top", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); const firstColumn = boardStore.board!.columns[0]; const firstCard = firstColumn.cards[0]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: firstCard.cardId, + oldIndex: 0, newIndex: -1, fromColumnId: firstColumn.id, + fromColumnIndex: 0, toColumnId: firstColumn.id, - }); + }; await moveCardRequest(cardPayload); expect(mockedBoardApiCalls.moveCardCall).not.toHaveBeenCalled(); }); - it("should not call moveCardCall if last card is moved to the last position", async () => { + it("should not call moveCardCall if last card is moved down", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); - const column1 = boardStore.board!.columns[0]; - const lastCard = column1.cards[2]; + const firstColumn = boardStore.board!.columns[0]; + const lastCard = firstColumn.cards[2]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: lastCard.cardId, oldIndex: 2, newIndex: 3, - fromColumnId: column1.id, - toColumnId: column1.id, - }); + fromColumnId: firstColumn.id, + fromColumnIndex: 0, + toColumnId: firstColumn.id, + }; await moveCardRequest(cardPayload); @@ -339,75 +350,80 @@ describe("boardRestApi", () => { const firstColumn = boardStore.board!.columns[0]; const movingCard = firstColumn.cards[0]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: movingCard.cardId, oldIndex: 0, newIndex: 2, fromColumnId: firstColumn.id, + fromColumnIndex: 0, toColumnId: firstColumn.id, - }); + toColumnIndex: 0, + }; await moveCardRequest(cardPayload); - expect(boardStore.moveCardSuccess).toHaveBeenCalledWith(cardPayload); + expect(boardStore.moveCardSuccess).toHaveBeenCalledWith({ + ...cardPayload, + isOwnAction: true, + }); }); - it("should call moveCardSuccess action if card is moved beyond last column and the API call is successful", async () => { + it("should call moveCardSuccess action if card is moved to another columm and the API call is successful", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); const firstColumn = boardStore.board!.columns[0]; - const movingCard = firstColumn.cards[0]; + const secondColumn = boardStore.board!.columns[1]; + const movingCard = firstColumn.cards[1]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: movingCard.cardId, + oldIndex: 1, + newIndex: 0, fromColumnId: firstColumn.id, - columnDelta: 2, - }); - - const newColumn = columnResponseFactory.build(); - - mockedBoardApiCalls.createColumnCall.mockResolvedValue(newColumn); + fromColumnIndex: 0, + toColumnId: secondColumn.id, + toColumnIndex: 1, + }; await moveCardRequest(cardPayload); - expect(mockedBoardApiCalls.moveCardCall).toHaveBeenCalledWith( - movingCard.cardId, - newColumn.id, - 0 - ); - expect(boardStore.moveCardSuccess).toHaveBeenCalledWith({ - cardId: movingCard.cardId, - oldIndex: 0, - newIndex: 0, - fromColumnId: firstColumn.id, - toColumnId: newColumn.id, - columnDelta: 2, - forceNextTick: undefined, + ...cardPayload, + isOwnAction: true, }); }); - it("should call moveCardSuccess action if card is moved to another columm and the API call is successful", async () => { + it("should call moveCardSuccess action if card is moved to a new column and the API call is successful", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); const firstColumn = boardStore.board!.columns[0]; - const secondColumn = boardStore.board!.columns[1]; - const movingCard = firstColumn.cards[1]; + const movingCard = firstColumn.cards[0]; + + const newColumn = columnResponseFactory.build(); + mockedBoardApiCalls.createColumnCall.mockResolvedValue(newColumn); + + boardStore.getLastColumnIndex.mockReturnValue(1); - const cardPayload = createCardPayload({ + const cardPayload = { cardId: movingCard.cardId, - oldIndex: 1, + oldIndex: 0, newIndex: 0, fromColumnId: firstColumn.id, - toColumnId: secondColumn.id, - }); + fromColumnIndex: 0, + }; await moveCardRequest(cardPayload); - expect(boardStore.moveCardSuccess).toHaveBeenCalledWith(cardPayload); + expect(boardStore.moveCardSuccess).toHaveBeenCalledWith({ + ...cardPayload, + toColumnId: newColumn.id, + toColumnIndex: 1, + isOwnAction: true, + }); }); + it("should call handleError if the API call fails", async () => { const { boardStore } = setup(); const { moveCardRequest } = useBoardRestApi(); @@ -415,13 +431,15 @@ describe("boardRestApi", () => { const firstColumn = boardStore.board!.columns[0]; const movingCard = firstColumn.cards[0]; - const cardPayload = createCardPayload({ + const cardPayload = { cardId: movingCard.cardId, oldIndex: 0, newIndex: 2, fromColumnId: firstColumn.id, + fromColumnIndex: 0, toColumnId: firstColumn.id, - }); + toColumnIndex: 0, + }; mockedBoardApiCalls.moveCardCall.mockRejectedValue({}); @@ -460,6 +478,7 @@ describe("boardRestApi", () => { expect(boardStore.moveColumnSuccess).toHaveBeenCalledWith({ columnMove, byKeyboard: false, + isOwnAction: true, }); }); @@ -505,7 +524,10 @@ describe("boardRestApi", () => { await updateColumnTitleRequest(payload); - expect(boardStore.updateColumnTitleSuccess).toHaveBeenCalledWith(payload); + expect(boardStore.updateColumnTitleSuccess).toHaveBeenCalledWith({ + ...payload, + isOwnAction: true, + }); }); it("should call handleError if the API call fails", async () => { @@ -547,6 +569,7 @@ describe("boardRestApi", () => { expect(boardStore.updateBoardTitleSuccess).toHaveBeenCalledWith({ boardId: "boardId", newTitle, + isOwnAction: true, }); }); @@ -588,6 +611,7 @@ describe("boardRestApi", () => { expect(boardStore.updateBoardVisibilitySuccess).toHaveBeenCalledWith({ boardId: "boardId", isVisible, + isOwnAction: true, }); }); diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.ts index 469aa1666f..6157454f0f 100644 --- a/src/modules/data/board/boardActions/boardSocketApi.composable.ts +++ b/src/modules/data/board/boardActions/boardSocketApi.composable.ts @@ -108,6 +108,7 @@ export const useBoardSocketApi = () => { boardId: boardStore.board.id, }); payload.toColumnId = response.newColumn.id; + payload.toColumnIndex = boardStore.getColumnIndex(payload.toColumnId); } emitOnSocket("move-card-request", payload); } catch (err) { diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts index 7f54f5ce70..f615916dbc 100644 --- a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts +++ b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts @@ -101,6 +101,7 @@ describe("useBoardSocketApi", () => { const payload = { newCard: cardResponseFactory.build(), columnId: "columnId", + isOwnAction: true, }; dispatch(BoardActions.createCardSuccess(payload)); @@ -113,6 +114,7 @@ describe("useBoardSocketApi", () => { const payload = { newColumn: columnResponseFactory.build(), + isOwnAction: true, }; dispatch(BoardActions.createColumnSuccess(payload)); @@ -125,6 +127,7 @@ describe("useBoardSocketApi", () => { const payload = { cardId: "cardId", + isOwnAction: true, }; dispatch(CardActions.deleteCardSuccess(payload)); @@ -138,6 +141,7 @@ describe("useBoardSocketApi", () => { const payload = { columnId: "columnId", + isOwnAction: true, }; dispatch(BoardActions.deleteColumnSuccess(payload)); @@ -153,6 +157,10 @@ describe("useBoardSocketApi", () => { oldIndex: 0, newIndex: 0, fromColumnId: "fromColumnId", + fromColumnIndex: 0, + toColumnId: "toColumnId", + toColumnIndex: 0, + isOwnAction: true, }; dispatch(BoardActions.moveCardSuccess(payload)); @@ -167,6 +175,7 @@ describe("useBoardSocketApi", () => { const payload = { columnMove: { addedIndex: 1, columnId: "testColumnId" }, byKeyboard: false, + isOwnAction: true, }; dispatch(BoardActions.moveColumnSuccess(payload)); @@ -192,6 +201,7 @@ describe("useBoardSocketApi", () => { const payload = { columnId: "cardId", newTitle: "newTitle", + isOwnAction: true, }; dispatch(BoardActions.updateColumnTitleSuccess(payload)); @@ -205,6 +215,7 @@ describe("useBoardSocketApi", () => { const payload = { boardId: "cardId", newTitle: "newTitle", + isOwnAction: true, }; dispatch(BoardActions.updateBoardTitleSuccess(payload)); @@ -218,6 +229,7 @@ describe("useBoardSocketApi", () => { const payload = { boardId: "cardId", isVisible: true, + isOwnAction: true, }; dispatch(BoardActions.updateBoardVisibilitySuccess(payload)); diff --git a/src/modules/data/board/cardActions/cardActionPayload.ts b/src/modules/data/board/cardActions/cardActionPayload.ts index cb90fb845e..3530579ea7 100644 --- a/src/modules/data/board/cardActions/cardActionPayload.ts +++ b/src/modules/data/board/cardActions/cardActionPayload.ts @@ -1,19 +1,17 @@ -import { - CardResponse, - ContentElementType, - CreateContentElementBodyParams, -} from "@/serverApi/v3"; +import { CardResponse, ContentElementType } from "@/serverApi/v3"; import { BoardObjectType, ErrorType, } from "@/components/error-handling/ErrorHandler.composable"; +import { AnyContentElement } from "@/types/board/ContentElement"; export type FetchCardRequestPayload = { cardIds: string[]; }; export type FetchCardSuccessPayload = { cards: CardResponse[]; + isOwnAction: boolean; }; export type FetchCardFailurePayload = { errorType: ErrorType; @@ -27,11 +25,13 @@ export type UpdateCardTitleRequestPayload = { export type UpdateCardTitleSuccessPayload = { cardId: string; newTitle: string; + isOwnAction: boolean; }; export type UpdateCardTitleFailurePayload = { errorType: ErrorType; boardObjectType: BoardObjectType; }; + export type UpdateCardHeightRequestPayload = { cardId: string; newHeight: number; @@ -39,31 +39,42 @@ export type UpdateCardHeightRequestPayload = { export type UpdateCardHeightSuccessPayload = { cardId: string; newHeight: number; + isOwnAction: boolean; }; export type UpdateCardHeightFailurePayload = { errorType: ErrorType; boardObjectType: BoardObjectType; }; -export type AddElementRequestPayload = { +export type CreateElementRequestPayload = { cardId: string; type: ContentElementType; - atFirstPosition?: boolean; + toPosition?: number; }; - -export type AddElementSuccessPayload = { +export type CreateElementSuccessPayload = { cardId: string; - params: CreateContentElementBodyParams; + type: ContentElementType; + toPosition?: number; + newElement: AnyContentElement; + isOwnAction: boolean; +}; +export type CreateElementFailurePayload = { + errorType: ErrorType; + boardObjectType: BoardObjectType; }; export type DeleteElementRequestPayload = { cardId: string; elementId: string; }; - export type DeleteElementSuccessPayload = { cardId: string; elementId: string; + isOwnAction: boolean; +}; +export type DeleteElementFailurePayload = { + errorType: ErrorType; + boardObjectType: BoardObjectType; }; export type DeleteCardRequestPayload = { @@ -71,10 +82,43 @@ export type DeleteCardRequestPayload = { }; export type DeleteCardSuccessPayload = { cardId: string; + isOwnAction: boolean; }; export type DeleteCardFailurePayload = { errorType: ErrorType; boardObjectType: BoardObjectType; }; +export type MoveElementRequestPayload = { + elementId: string; + toCardId: string; + toPosition: number; +}; +export type MoveElementSuccessPayload = { + elementId: string; + toCardId: string; + toPosition: number; + isOwnAction: boolean; +}; +export type MoveElementFailurePayload = { + errorType: ErrorType; + boardObjectType: BoardObjectType; +}; + +export type UpdateElementRequestPayload = { + element: AnyContentElement; +}; +export type UpdateElementSuccessPayload = { + elementId: string; + data: { + type: ContentElementType; + content: AnyContentElement["content"]; + }; + isOwnAction: boolean; +}; +export type UpdateElementFailurePayload = { + errorType: ErrorType; + boardObjectType: BoardObjectType; +}; + export type DisconnectSocketRequestPayload = Record; diff --git a/src/modules/data/board/cardActions/cardActions.ts b/src/modules/data/board/cardActions/cardActions.ts index e337e99aed..6ac0ccadd4 100644 --- a/src/modules/data/board/cardActions/cardActions.ts +++ b/src/modules/data/board/cardActions/cardActions.ts @@ -1,17 +1,29 @@ import { + CreateElementFailurePayload, + CreateElementRequestPayload, + CreateElementSuccessPayload, DeleteCardFailurePayload, DeleteCardRequestPayload, DeleteCardSuccessPayload, + DeleteElementFailurePayload, + DeleteElementRequestPayload, + DeleteElementSuccessPayload, DisconnectSocketRequestPayload, FetchCardFailurePayload, FetchCardRequestPayload, FetchCardSuccessPayload, + MoveElementRequestPayload, + MoveElementSuccessPayload, + MoveElementFailurePayload, UpdateCardHeightFailurePayload, UpdateCardHeightRequestPayload, UpdateCardHeightSuccessPayload, UpdateCardTitleFailurePayload, UpdateCardTitleRequestPayload, UpdateCardTitleSuccessPayload, + UpdateElementRequestPayload, + UpdateElementSuccessPayload, + UpdateElementFailurePayload, } from "./cardActionPayload"; import { createAction, props } from "@/types/board/ActionFactory"; @@ -20,6 +32,58 @@ export const disconnectSocket = createAction( props() ); +export const createElementRequest = createAction( + "create-element-request", + props() +); +export const createElementSuccess = createAction( + "create-element-success", + props() +); +export const createElementFailure = createAction( + "create-element-failure", + props() +); + +export const deleteElementRequest = createAction( + "delete-element-request", + props() +); +export const deleteElementSuccess = createAction( + "delete-element-success", + props() +); +export const deleteElementFailure = createAction( + "delete-element-failure", + props() +); + +export const moveElementRequest = createAction( + "move-element-request", + props() +); +export const moveElementSuccess = createAction( + "move-element-success", + props() +); +export const moveElementFailure = createAction( + "move-element-failure", + props() +); + +export const updateElementRequest = createAction( + "update-element-request", + props() +); +export const updateElementSuccess = createAction( + "update-element-success", + props() +); +export const updateElementFailure = createAction( + "update-element-failure", + props() +); + export const deleteCardRequest = createAction( "delete-card-request", props() diff --git a/src/modules/data/board/cardActions/cardRestApi.composable.ts b/src/modules/data/board/cardActions/cardRestApi.composable.ts index 9a0ba7c0d9..e330a40fce 100644 --- a/src/modules/data/board/cardActions/cardRestApi.composable.ts +++ b/src/modules/data/board/cardActions/cardRestApi.composable.ts @@ -7,10 +7,14 @@ import { useErrorHandler, } from "@/components/error-handling/ErrorHandler.composable"; import { + CreateElementRequestPayload, DeleteCardRequestPayload, + DeleteElementRequestPayload, FetchCardRequestPayload, + MoveElementRequestPayload, UpdateCardHeightRequestPayload, UpdateCardTitleRequestPayload, + UpdateElementRequestPayload, } from "./cardActionPayload"; import { useCardStore } from "../Card.store"; import { useSharedCardRequestPool } from "../CardRequestPool.composable"; @@ -24,19 +28,98 @@ export const useCardRestApi = () => { const { fetchCard: fetchCardFromApi } = useSharedCardRequestPool(); const { handleError, notifyWithTemplate } = useErrorHandler(); - const { deleteCardCall, updateCardTitle, updateCardHeightCall } = - useBoardApi(); + const { + createElementCall, + deleteElementCall, + deleteCardCall, + updateElementCall, + moveElementCall, + updateCardTitle, + updateCardHeightCall, + } = useBoardApi(); const { setEditModeId } = useSharedEditMode(); + const createElementRequest = async (payload: CreateElementRequestPayload) => { + const card = cardStore.getCard(payload.cardId); + if (card === undefined) return; + + try { + const params = { + type: payload.type, + toPosition: payload.toPosition, + }; + const newElement = await createElementCall(payload.cardId, params); + cardStore.createElementSuccess({ + ...payload, + newElement: newElement.data, + isOwnAction: true, + }); + } catch (error) { + handleError(error, { + 404: notifyWithTemplateAndReload("notDeleted", "boardCard"), + }); + } + }; + + const deleteElementRequest = async (payload: DeleteElementRequestPayload) => { + const card = cardStore.getCard(payload.cardId); + if (card === undefined) return; + + try { + await deleteElementCall(payload.elementId); + cardStore.deleteElementSuccess({ ...payload, isOwnAction: true }); + } catch (error) { + handleError(error, { + 404: notifyWithTemplateAndReload("notDeleted", "boardElement"), + }); + } + }; + + const moveElementRequest = async (payload: MoveElementRequestPayload) => { + const card = cardStore.getCard(payload.toCardId); + if (card === undefined) return; + + try { + await moveElementCall( + payload.elementId, + payload.toCardId, + payload.toPosition + ); + cardStore.moveElementSuccess({ ...payload, isOwnAction: true }); + } catch (error) { + handleError(error, { + 404: notifyWithTemplateAndReload("notMoved", "boardElement"), + }); + } + }; + + const updateElementRequest = async (payload: UpdateElementRequestPayload) => { + try { + const success = await updateElementCall(payload.element); + cardStore.updateElementSuccess({ + elementId: success.data.id, + data: { + type: success.data.type, + content: success.data.content, + }, + isOwnAction: true, + }); + } catch (error) { + handleError(error, { + 404: notifyWithTemplate("notUpdated", "boardElement"), + }); + } + }; + const deleteCardRequest = async (payload: DeleteCardRequestPayload) => { const card = cardStore.getCard(payload.cardId); if (card === undefined) return; try { await deleteCardCall(payload.cardId); - boardStore.deleteCardSuccess(payload); - cardStore.deleteCardSuccess(payload); + boardStore.deleteCardSuccess({ ...payload, isOwnAction: true }); + cardStore.deleteCardSuccess({ ...payload, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notDeleted", "boardCard"), @@ -51,7 +134,7 @@ export const useCardRestApi = () => { try { const promises = payload.cardIds.map(fetchCardFromApi); const cards = await Promise.all(promises); - cardStore.fetchCardSuccess({ cards }); + cardStore.fetchCardSuccess({ cards, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notLoaded", "boardCard"), @@ -67,7 +150,7 @@ export const useCardRestApi = () => { try { await updateCardTitle(payload.cardId, payload.newTitle); - cardStore.updateCardTitleSuccess(payload); + cardStore.updateCardTitleSuccess({ ...payload, isOwnAction: true }); } catch (error) { handleError(error, { 404: notifyWithTemplateAndReload("notUpdated"), @@ -83,7 +166,7 @@ export const useCardRestApi = () => { try { await updateCardHeightCall(payload.cardId, payload.newHeight); - cardStore.updateCardHeightSuccess(payload); + cardStore.updateCardHeightSuccess({ ...payload, isOwnAction: true }); } catch (error) { handleError(error, {}); } @@ -105,6 +188,10 @@ export const useCardRestApi = () => { const disconnectSocketRequest = (): void => {}; return { + createElementRequest, + deleteElementRequest, + moveElementRequest, + updateElementRequest, deleteCardRequest, fetchCardRequest, updateCardTitleRequest, diff --git a/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts b/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts index d8017b4ec9..ecb274efbc 100644 --- a/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts +++ b/src/modules/data/board/cardActions/cardRestApi.composable.unit.ts @@ -1,6 +1,10 @@ import { setActivePinia } from "pinia"; import { ref } from "vue"; -import { envsFactory, mockedPiniaStoreTyping } from "@@/tests/test-utils"; +import { + envsFactory, + mockedPiniaStoreTyping, + richTextElementResponseFactory, +} from "@@/tests/test-utils"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; import { useSocketConnection, useCardStore, useBoardStore } from "@data-board"; @@ -17,6 +21,8 @@ import { } from "./cardActionPayload"; import { cardResponseFactory } from "@@/tests/test-utils/factory/cardResponseFactory"; import { useSharedCardRequestPool } from "../CardRequestPool.composable"; +import { ContentElementType, RichTextElementResponse } from "@/serverApi/v3"; +import { AxiosResponse } from "axios"; jest.mock("@/components/error-handling/ErrorHandler.composable"); const mockedUseErrorHandler = jest.mocked(useErrorHandler); @@ -78,12 +84,220 @@ describe("useCardRestApi", () => { const setup = () => { const boardStore = mockedPiniaStoreTyping(useBoardStore); const cardStore = mockedPiniaStoreTyping(useCardStore); - const cards = cardResponseFactory.buildList(3); - cardStore.fetchCardSuccess({ cards: cards }); + const card = cardResponseFactory.build(); - return { boardStore, cardStore, cards }; + return { boardStore, cardStore, card }; }; + describe("createElementRequest", () => { + it("should not call createElementSuccess action when card is undefined", async () => { + const { cardStore } = setup(); + const { createElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(undefined); + + await createElementRequest({ + cardId: "cardId", + type: ContentElementType.RichText, + toPosition: 0, + }); + + expect(cardStore.createElementSuccess).not.toHaveBeenCalled(); + }); + + it("should call createElementSuccess action if the API call is successful", async () => { + const { cardStore, card } = setup(); + const { createElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + + const newElementResponse = createMock< + AxiosResponse + >({ + data: richTextElementResponseFactory.build(), + }); + mockedBoardApiCalls.createElementCall.mockResolvedValue( + newElementResponse + ); + + const payload = { + cardId: card.id, + type: ContentElementType.RichText, + toPosition: 0, + }; + + await createElementRequest(payload); + + expect(cardStore.createElementSuccess).toHaveBeenCalledWith({ + ...payload, + newElement: newElementResponse.data, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + const { cardStore, card } = setup(); + const { createElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + mockedBoardApiCalls.createElementCall.mockRejectedValue({}); + + await createElementRequest({ + cardId: card.id, + type: ContentElementType.RichText, + }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + + describe("deleteElementRequest", () => { + it("should not call deleteElementSuccess action when card is undefined", async () => { + const { cardStore } = setup(); + const { deleteElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(undefined); + + await deleteElementRequest({ + cardId: "cardId", + elementId: "elementId", + }); + + expect(cardStore.deleteElementSuccess).not.toHaveBeenCalled(); + }); + + it("should call deleteElementSuccess action if the API call is successful", async () => { + const { cardStore, card } = setup(); + const { deleteElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + + const payload = { + cardId: card.id, + elementId: "elementId", + }; + + await deleteElementRequest(payload); + + expect(cardStore.deleteElementSuccess).toHaveBeenCalledWith({ + ...payload, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + const { cardStore, card } = setup(); + const { deleteElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + mockedBoardApiCalls.deleteElementCall.mockRejectedValue({}); + + await deleteElementRequest({ + cardId: card.id, + elementId: "elementId", + }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + + describe("moveElementRequest", () => { + it("should not call moveElementSuccess action when card is undefined", async () => { + const { cardStore } = setup(); + const { moveElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(undefined); + + await moveElementRequest({ + elementId: "elementId", + toCardId: "toCardId", + toPosition: 0, + }); + + expect(cardStore.moveElementSuccess).not.toHaveBeenCalled(); + }); + + it("should call moveElementSuccess action if the API call is successful", async () => { + const { cardStore, card } = setup(); + const { moveElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + + const payload = { + elementId: "elementId", + toCardId: "toCardId", + toPosition: 0, + }; + + await moveElementRequest(payload); + + expect(cardStore.moveElementSuccess).toHaveBeenCalledWith({ + ...payload, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + const { cardStore, card } = setup(); + const { moveElementRequest } = useCardRestApi(); + + cardStore.getCard.mockReturnValue(card); + mockedBoardApiCalls.moveElementCall.mockRejectedValue({}); + + await moveElementRequest({ + elementId: "elementId", + toCardId: "toCardId", + toPosition: 0, + }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + + describe("updateElementRequest", () => { + it("should call updateElementSuccess action if the API call is successful", async () => { + const { cardStore } = setup(); + const { updateElementRequest } = useCardRestApi(); + + const element = richTextElementResponseFactory.build(); + + const updateElementResponse = createMock< + AxiosResponse + >({ + data: { id: element.id, content: element.content, type: element.type }, + }); + mockedBoardApiCalls.updateElementCall.mockResolvedValue( + updateElementResponse + ); + + await updateElementRequest({ + element, + }); + + expect(cardStore.updateElementSuccess).toHaveBeenCalledWith({ + elementId: updateElementResponse.data.id, + data: { + type: updateElementResponse.data.type, + content: updateElementResponse.data.content, + }, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + setup(); + const { updateElementRequest } = useCardRestApi(); + + mockedBoardApiCalls.updateElementCall.mockRejectedValue({}); + + await updateElementRequest({ + element: richTextElementResponseFactory.build(), + }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + describe("deleteCardRequest", () => { it("should not call deleteCardSuccess action when card is undefined", async () => { const { cardStore } = setup(); @@ -97,25 +311,26 @@ describe("useCardRestApi", () => { }); it("should call deleteCardSuccess action if the API call is successful", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { deleteCardRequest } = useCardRestApi(); - const cardId = cards[0].id; + const cardId = card.id; - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); await deleteCardRequest({ cardId }); expect(cardStore.deleteCardSuccess).toHaveBeenCalledWith({ cardId, + isOwnAction: true, }); }); it("should call handleError if the API call fails", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { deleteCardRequest } = useCardRestApi(); - const cardId = cards[0].id; + const cardId = card.id; - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); mockedBoardApiCalls.deleteCardCall.mockRejectedValue({}); await deleteCardRequest({ cardId }); @@ -126,15 +341,23 @@ describe("useCardRestApi", () => { describe("fetchCardRequest", () => { it("should call fetchCardSuccess action if the API call is successful", async () => { - const { cardStore, cards } = setup(); + const { cardStore } = setup(); const { fetchCardRequest } = useCardRestApi(); - mockedSharedCardRequestPoolCalls.fetchCard.mockResolvedValue(cards[0]); + const cards = cardResponseFactory.buildList(3); + + mockedSharedCardRequestPoolCalls.fetchCard + .mockResolvedValueOnce(cards[0]) + .mockResolvedValueOnce(cards[1]) + .mockResolvedValueOnce(cards[2]); const cardIds = cards.map((card) => card.id); await fetchCardRequest({ cardIds }); - expect(cardStore.fetchCardSuccess).toHaveBeenCalled(); + expect(cardStore.fetchCardSuccess).toHaveBeenCalledWith({ + cards, + isOwnAction: true, + }); }); it("should call handleError if the API call fails", async () => { @@ -165,32 +388,33 @@ describe("useCardRestApi", () => { }); it("should call updateCardTitleSuccess action if the API call is successful", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { updateCardTitleRequest } = useCardRestApi(); - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); const requestPayload: UpdateCardTitleRequestPayload = { - cardId: cards[0].id, + cardId: card.id, newTitle: "newTitle", }; await updateCardTitleRequest(requestPayload); - expect(cardStore.updateCardTitleSuccess).toHaveBeenCalledWith( - requestPayload - ); + expect(cardStore.updateCardTitleSuccess).toHaveBeenCalledWith({ + ...requestPayload, + isOwnAction: true, + }); }); it("should call handleError if the API call fails", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { updateCardTitleRequest } = useCardRestApi(); - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); mockedBoardApiCalls.updateCardTitle.mockRejectedValue({}); await updateCardTitleRequest({ - cardId: cards[0].id, + cardId: card.id, newTitle: "newTitle", }); @@ -214,32 +438,33 @@ describe("useCardRestApi", () => { }); it("should call updateCardHeightSuccess action if the API call is successful", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { updateCardHeightRequest } = useCardRestApi(); - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); const requestPayload: UpdateCardHeightRequestPayload = { - cardId: cards[0].id, + cardId: card.id, newHeight: 100, }; await updateCardHeightRequest(requestPayload); - expect(cardStore.updateCardHeightSuccess).toHaveBeenCalledWith( - requestPayload - ); + expect(cardStore.updateCardHeightSuccess).toHaveBeenCalledWith({ + ...requestPayload, + isOwnAction: true, + }); }); it("should call handleError if the API call fails", async () => { - const { cardStore, cards } = setup(); + const { cardStore, card } = setup(); const { updateCardHeightRequest } = useCardRestApi(); - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); mockedBoardApiCalls.updateCardHeightCall.mockRejectedValue({}); await updateCardHeightRequest({ - cardId: cards[0].id, + cardId: card.id, newHeight: 100, }); @@ -257,16 +482,16 @@ describe("useCardRestApi", () => { }; it("should notify with template", async () => { - const { boardStore, cardStore, cards } = setup(); + const { boardStore, cardStore, card } = setup(); const { updateCardTitleRequest } = useCardRestApi(); - cardStore.getCard.mockReturnValue(cards[0]); + cardStore.getCard.mockReturnValue(card); mockedBoardApiCalls.updateCardTitle.mockRejectedValue({}); mockedErrorHandler.notifyWithTemplate.mockReturnValue(jest.fn()); await updateCardTitleRequest({ - cardId: cards[0].id, + cardId: card.id, newTitle: "newTitle", }); diff --git a/src/modules/data/board/cardActions/cardSocketApi.composable.ts b/src/modules/data/board/cardActions/cardSocketApi.composable.ts index eda62ce6cf..e740bc1cc6 100644 --- a/src/modules/data/board/cardActions/cardSocketApi.composable.ts +++ b/src/modules/data/board/cardActions/cardSocketApi.composable.ts @@ -4,10 +4,14 @@ import { useCardStore } from "../Card.store"; import { PermittedStoreActions, handle, on } from "@/types/board/ActionFactory"; import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; import { + CreateElementRequestPayload, DeleteCardRequestPayload, + DeleteElementRequestPayload, FetchCardRequestPayload, + MoveElementRequestPayload, UpdateCardHeightRequestPayload, UpdateCardTitleRequestPayload, + UpdateElementRequestPayload, } from "./cardActionPayload"; import { DisconnectSocketRequestPayload } from "../boardActions/boardActionPayload"; import { useDebounceFn } from "@vueuse/core"; @@ -31,6 +35,10 @@ export const useCardSocketApi = () => { on(CardActions.disconnectSocket, disconnectSocketRequest), // success actions + on(CardActions.createElementSuccess, cardStore.createElementSuccess), + on(CardActions.deleteElementSuccess, cardStore.deleteElementSuccess), + on(CardActions.moveElementSuccess, cardStore.moveElementSuccess), + on(CardActions.updateElementSuccess, cardStore.updateElementSuccess), on(CardActions.deleteCardSuccess, cardStore.deleteCardSuccess), on(CardActions.fetchCardSuccess, cardStore.fetchCardSuccess), on(CardActions.updateCardTitleSuccess, cardStore.updateCardTitleSuccess), @@ -40,6 +48,10 @@ export const useCardSocketApi = () => { ), // failure actions + on(CardActions.createElementFailure, onFailure), + on(CardActions.deleteElementFailure, onFailure), + on(CardActions.moveElementFailure, onFailure), + on(CardActions.updateElementFailure, onFailure), on(CardActions.deleteCardFailure, onFailure), on(CardActions.fetchCardFailure, onFailure), on(CardActions.updateCardTitleFailure, onFailure), @@ -68,6 +80,30 @@ export const useCardSocketApi = () => { { maxWait: MAX_WAIT_BEFORE_FIRST_CALL_IN_MS } ); + const createElementRequest = async (payload: CreateElementRequestPayload) => { + emitOnSocket("create-element-request", payload); + }; + + const deleteElementRequest = async (payload: DeleteElementRequestPayload) => { + emitOnSocket("delete-element-request", payload); + }; + + const moveElementRequest = async (payload: MoveElementRequestPayload) => { + emitOnSocket("move-element-request", payload); + }; + + const updateElementRequest = async ({ + element, + }: UpdateElementRequestPayload) => { + emitOnSocket("update-element-request", { + elementId: element.id, + data: { + type: element.type, + content: element.content, + }, + }); + }; + const deleteCardRequest = async (payload: DeleteCardRequestPayload) => { emitOnSocket("delete-card-request", payload); }; @@ -88,6 +124,10 @@ export const useCardSocketApi = () => { return { dispatch, disconnectSocketRequest, + createElementRequest, + deleteElementRequest, + moveElementRequest, + updateElementRequest, deleteCardRequest, fetchCardRequest, updateCardTitleRequest, diff --git a/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts b/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts index 4086559411..942f6ccc46 100644 --- a/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts +++ b/src/modules/data/board/cardActions/cardSocketApi.composable.unit.ts @@ -1,15 +1,19 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; +import { ContentElementType } from "@/serverApi/v3"; import { envConfigModule } from "@/store"; import EnvConfigModule from "@/store/env-config"; import { envsFactory, mockedPiniaStoreTyping } from "@@/tests/test-utils"; +import { richTextElementResponseFactory } from "@@/tests/test-utils/factory/richTextElementResponseFactory"; import setupStores from "@@/tests/test-utils/setupStores"; import { useCardStore, useSocketConnection } from "@data-board"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { createTestingPinia } from "@pinia/testing"; -import { useBoardNotifier } from "@util-board"; +import { useBoardNotifier, useSharedLastCreatedElement } from "@util-board"; import { setActivePinia } from "pinia"; +import { computed } from "vue"; import { useI18n } from "vue-i18n"; import { + CreateElementSuccessPayload, DeleteCardRequestPayload, DisconnectSocketRequestPayload, UpdateCardHeightFailurePayload, @@ -29,6 +33,9 @@ const mockedUseErrorHandler = jest.mocked(useErrorHandler); jest.mock("@util-board"); const mockedUseBoardNotifier = jest.mocked(useBoardNotifier); +const mockUseSharedLastCreatedElement = jest.mocked( + useSharedLastCreatedElement +); describe("useCardSocketApi", () => { let mockedSocketConnectionHandler: DeepMocked< @@ -57,6 +64,11 @@ describe("useCardSocketApi", () => { createMock>(); mockedUseBoardNotifier.mockReturnValue(mockedBoardNotifierCalls); jest.useFakeTimers(); + + mockUseSharedLastCreatedElement.mockReturnValue({ + lastCreatedElementId: computed(() => "element-id"), + resetLastCreatedElementId: jest.fn(), + }); }); afterEach(() => { @@ -73,6 +85,36 @@ describe("useCardSocketApi", () => { expect(mockedSocketConnectionHandler.disconnectSocket).toHaveBeenCalled(); }); + it("should call createElementSuccess for corresponding action", () => { + const cardStore = mockedPiniaStoreTyping(useCardStore); + const { dispatch } = useCardSocketApi(); + + const payload: CreateElementSuccessPayload = { + cardId: "cardId", + type: ContentElementType.RichText, + toPosition: 0, + newElement: richTextElementResponseFactory.build(), + isOwnAction: true, + }; + dispatch(CardActions.createElementSuccess(payload)); + + expect(cardStore.createElementSuccess).toHaveBeenCalledWith(payload); + }); + + it("should call deleteElementSuccess for corresponding action", () => { + const cardStore = mockedPiniaStoreTyping(useCardStore); + const { dispatch } = useCardSocketApi(); + + const payload = { + cardId: "cardId", + elementId: "elementId", + isOwnAction: true, + }; + dispatch(CardActions.deleteElementSuccess(payload)); + + expect(cardStore.deleteElementSuccess).toHaveBeenCalledWith(payload); + }); + it("should call updateCardTitleSuccess for corresponding action", () => { const cardStore = mockedPiniaStoreTyping(useCardStore); const { dispatch } = useCardSocketApi(); @@ -80,6 +122,7 @@ describe("useCardSocketApi", () => { const payload = { cardId: "cardId", newTitle: "newTitle", + isOwnAction: true, }; dispatch(CardActions.updateCardTitleSuccess(payload)); @@ -93,6 +136,7 @@ describe("useCardSocketApi", () => { const payload = { cardId: "cardId", newHeight: 100, + isOwnAction: true, }; dispatch(CardActions.updateCardHeightSuccess(payload)); @@ -142,6 +186,82 @@ describe("useCardSocketApi", () => { }); }); + describe("createElementRequest", () => { + it("should call emitOnSocket with correct parameters", () => { + const { createElementRequest } = useCardSocketApi(); + + const payload = { + cardId: "cardId", + type: ContentElementType.RichText, + }; + + createElementRequest(payload); + + expect(mockedSocketConnectionHandler.emitOnSocket).toHaveBeenCalledWith( + "create-element-request", + payload + ); + }); + }); + + describe("deleteElementRequest", () => { + it("should call emitOnSocket with correct parameters", () => { + const { deleteElementRequest } = useCardSocketApi(); + + const payload = { + cardId: "cardId", + elementId: "elementId", + }; + + deleteElementRequest(payload); + + expect(mockedSocketConnectionHandler.emitOnSocket).toHaveBeenCalledWith( + "delete-element-request", + payload + ); + }); + }); + + describe("moveElementRequest", () => { + it("should call emitOnSocket with correct parameters", () => { + const { moveElementRequest } = useCardSocketApi(); + + const payload = { + elementId: "elementId", + toCardId: "toCardId", + toPosition: 0, + }; + + moveElementRequest(payload); + + expect(mockedSocketConnectionHandler.emitOnSocket).toHaveBeenCalledWith( + "move-element-request", + payload + ); + }); + }); + + describe("updateElementRequest", () => { + it("should call emitOnSocket with correct parameters", () => { + const { updateElementRequest } = useCardSocketApi(); + + const element = richTextElementResponseFactory.build(); + + updateElementRequest({ element }); + + expect(mockedSocketConnectionHandler.emitOnSocket).toHaveBeenCalledWith( + "update-element-request", + { + elementId: element.id, + data: { + type: element.type, + content: element.content, + }, + } + ); + }); + }); + describe("deleteCardRequest", () => { const payload: DeleteCardRequestPayload = { cardId: "cardId" }; @@ -192,7 +312,6 @@ describe("useCardSocketApi", () => { ); }); }); - describe("fetchCardRequest", () => { const payload = { cardIds: ["fake-card-id-234"], diff --git a/src/modules/data/board/index.ts b/src/modules/data/board/index.ts index eaf27d973a..c532327995 100644 --- a/src/modules/data/board/index.ts +++ b/src/modules/data/board/index.ts @@ -5,6 +5,7 @@ import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; import { useContentElementState } from "./ContentElementState.composable"; import { useEditMode, useSharedEditMode } from "./EditMode.composable"; import * as boardActions from "./boardActions/boardActions"; +import * as cardActions from "./cardActions/cardActions"; import { useSocketConnection } from "./socket/socket"; import { useCardStore } from "./Card.store"; @@ -19,4 +20,5 @@ export { useBoardPermissions, useSharedBoardPageInformation, boardActions, + cardActions, }; diff --git a/src/modules/feature/board-file-element/FileContentElement.vue b/src/modules/feature/board-file-element/FileContentElement.vue index 92b363fdf0..7d259da9fd 100644 --- a/src/modules/feature/board-file-element/FileContentElement.vue +++ b/src/modules/feature/board-file-element/FileContentElement.vue @@ -63,6 +63,7 @@ import { PropType, ref, toRef, + watch, } from "vue"; import { useFileAlerts } from "./content/alert/useFileAlerts.composable"; import FileContent from "./content/FileContent.vue"; @@ -142,12 +143,15 @@ export default defineComponent({ return isUploadingInViewMode || isEditMode; }); - onMounted(() => { - (async () => { - await fetchFile(element.value.id, FileRecordParentType.BOARDNODES); + watch(element.value, async () => { + isLoadingFileRecord.value = true; + await fetchFile(element.value.id, FileRecordParentType.BOARDNODES); + isLoadingFileRecord.value = false; + }); - isLoadingFileRecord.value = false; - })(); + onMounted(async () => { + await fetchFile(element.value.id, FileRecordParentType.BOARDNODES); + isLoadingFileRecord.value = false; }); const onKeydownArrow = (event: KeyboardEvent) => { @@ -160,6 +164,7 @@ export default defineComponent({ const onUploadFile = async (file: File): Promise => { try { await upload(file, element.value.id, FileRecordParentType.BOARDNODES); + element.value.content.caption = " "; } catch (error) { emit("delete:element", element.value.id); } diff --git a/src/modules/feature/board-file-element/content/FileContent.unit.ts b/src/modules/feature/board-file-element/content/FileContent.unit.ts index 5e238810d4..932183fa7f 100644 --- a/src/modules/feature/board-file-element/content/FileContent.unit.ts +++ b/src/modules/feature/board-file-element/content/FileContent.unit.ts @@ -10,6 +10,15 @@ import ContentElementFooter from "./footer/ContentElementFooter.vue"; import FileInputs from "./inputs/FileInputs.vue"; describe("FileContent", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + describe("When EditMode is true", () => { describe("When PreviewUrl is defined", () => { const setup = () => { @@ -92,6 +101,7 @@ describe("FileContent", () => { const fileInputs = wrapper.findComponent(FileInputs); fileInputs.vm.$emit("update:alternativeText"); + jest.runAllTimers(); expect(wrapper.emitted("update:alternativeText")).toHaveLength(1); }); @@ -104,6 +114,7 @@ describe("FileContent", () => { const fileInputs = wrapper.findComponent(FileInputs); fileInputs.vm.$emit("update:caption"); + jest.runAllTimers(); expect(wrapper.emitted("update:caption")).toHaveLength(1); }); diff --git a/src/modules/feature/board-file-element/content/FileContent.vue b/src/modules/feature/board-file-element/content/FileContent.vue index 74cdb9dffb..75d34ea44b 100644 --- a/src/modules/feature/board-file-element/content/FileContent.vue +++ b/src/modules/feature/board-file-element/content/FileContent.vue @@ -26,6 +26,7 @@ import { FileProperties } from "../shared/types/file-properties"; import FileInputs from "././inputs/FileInputs.vue"; import ContentElementFooter from "./footer/ContentElementFooter.vue"; import { FileAlert } from "../shared/types/FileAlert.enum"; +import { useDebounceFn } from "@vueuse/core"; export default defineComponent({ components: { @@ -52,11 +53,13 @@ export default defineComponent({ const onFetchFile = () => { emit("fetch:file"); }; - const onUpdateCaption = (value: string) => { + const onUpdateCaption = useDebounceFn((value: string) => { emit("update:caption", value); - }; - const onUpdateText = (value: string) => + }, 600); + + const onUpdateText = useDebounceFn((value: string) => { emit("update:alternativeText", value); + }, 600); const onAddAlert = (alert: FileAlert) => { emit("add:alert", alert); diff --git a/src/modules/feature/board-file-element/upload/FileUpload.vue b/src/modules/feature/board-file-element/upload/FileUpload.vue index 55477bed5a..f121f34e14 100644 --- a/src/modules/feature/board-file-element/upload/FileUpload.vue +++ b/src/modules/feature/board-file-element/upload/FileUpload.vue @@ -29,7 +29,7 @@