diff --git a/src/locales/de.ts b/src/locales/de.ts index 872f15cebb..8c5f3bf180 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -375,6 +375,7 @@ export default { "components.board.action.moveLeft": "Nach links verschieben", "components.board.action.moveRight": "Nach rechts verschieben", "components.board.action.moveUp": "Nach oben verschieben", + "components.board.action.changeLayout": "Ansicht ändern", "components.board.action.shareLink.card": "Link zur Karte kopieren", "components.board.alert.info.teacher": "Dieser Bereich ist sichtbar für alle Kursteilnehmenden.", @@ -441,6 +442,8 @@ export default { "Titel von Karte {cardPosition} in Abschnitt {columnPosition} wurde von einem anderen Benutzer in {newTitle} geändert.", "components.board.screenReader.notification.cardUpdated.success": "Karte {cardPosition} in Abschnitt {columnPosition} wurde von einem anderen Benutzer aktualisiert.", + "components.board.screenReader.notification.boardLayoutUpdated.success": + "Die Ansicht des Bereichs wurde von einem anderen Benutzer zu {layout} geändert.", "components.board": "Bereich", "components.boardCard": "Karte", "components.boardColumn": "Abschnitt", diff --git a/src/locales/en.ts b/src/locales/en.ts index 55a4d4efcd..8d431776f5 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -373,6 +373,7 @@ export default { "components.board.action.moveLeft": "Move left", "components.board.action.moveRight": "Move right", "components.board.action.moveUp": "Move up", + "components.board.action.changeLayout": "Change layout", "components.board.action.shareLink.card": "Copy link to card", "components.board.alert.info.teacher": "This board is visible to all course participants.", @@ -437,6 +438,8 @@ export default { "Title of card {cardPosition} in column {columnPosition} was changed to {newTitle} by another user.", "components.board.screenReader.notification.cardUpdated.success": "Card {cardPosition} in column {columnPosition} was updated by another user.", + "components.board.screenReader.notification.boardLayoutUpdated.success": + "The board's view was changed to {layout} by another user.", "components.board": "board", "components.boardCard": "card", "components.boardColumn": "column", diff --git a/src/locales/es.ts b/src/locales/es.ts index 2f0bafda22..4535f9a7c5 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -378,6 +378,7 @@ export default { "components.board.action.moveLeft": "Mover a la izquierda", "components.board.action.moveRight": "Mover a la derecha", "components.board.action.moveUp": "Levantar", + "components.board.action.changeLayout": "Cambiar vista", "components.board.action.shareLink.card": "Enlace a la ficha", "components.board.alert.info.teacher": "Este tablero es visible para todos los participantes en el curso.", @@ -446,6 +447,8 @@ export default { "El título de la tarjeta {cardPosition} en la columna {columnPosition} fue cambiado a {newTitle} por otro usuario.", "components.board.screenReader.notification.cardUpdated.success": "La tarjeta {cardPosition} de la columna {columnPosition} ha sido actualizada por otro usuario.", + "components.board.screenReader.notification.boardLayoutUpdated.success": + "Otro usuario cambió la vista del panel a {layout}.", "components.board": "tablero", "components.boardCard": "tarjeta", "components.boardColumn": "columna", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 19bfe84dd9..818122541c 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -382,6 +382,7 @@ export default { "components.board.action.moveLeft": "Перемістіться вліво", "components.board.action.moveRight": "Перемістіться праворуч", "components.board.action.moveUp": "Рухатися вгору", + "components.board.action.changeLayout": "Змінити вигляд", "components.board.action.shareLink.card": "Скопіювати посилання на Карту", "components.board.alert.info.teacher": "Цю дошку бачать усі учасники курсу.", "components.board.alert.info.draft": "Ця дошка невидима для учасників курсу.", @@ -447,6 +448,8 @@ export default { "Заголовок картки {cardPosition} у колонці {columnPosition} було змінено на {newTitle} іншим користувачем.", "components.board.screenReader.notification.cardUpdated.success": "Картку {cardPosition} у стовпчику {columnPosition} було оновлено іншим користувачем.", + "components.board.screenReader.notification.boardLayoutUpdated.success": + "Інший користувач змінив вигляд панелі на {layout}.", "components.board": "Дошка", "components.boardCard": "Картка", "components.boardColumn": "Колонка", diff --git a/src/modules/data/board/Board.store.ts b/src/modules/data/board/Board.store.ts index 9c0a7d8fc3..13a17ce98e 100644 --- a/src/modules/data/board/Board.store.ts +++ b/src/modules/data/board/Board.store.ts @@ -1,8 +1,12 @@ import { applicationErrorModule, envConfigModule } from "@/store"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; import { Board } from "@/types/board/Board"; +import { createApplicationError } from "@/utils/create-application-error.factory"; import { useSharedEditMode } from "@util-board"; import { defineStore } from "pinia"; import { computed, nextTick, ref } from "vue"; +import { useI18n } from "vue-i18n"; +import { useRouter } from "vue-router"; import { CreateCardRequestPayload, CreateCardSuccessPayload, @@ -18,6 +22,8 @@ import { MoveCardSuccessPayload, MoveColumnRequestPayload, MoveColumnSuccessPayload, + UpdateBoardLayoutRequestPayload, + UpdateBoardLayoutSuccessPayload, UpdateBoardTitleRequestPayload, UpdateBoardTitleSuccessPayload, UpdateBoardVisibilityRequestPayload, @@ -30,10 +36,6 @@ import { useBoardSocketApi } from "./boardActions/boardSocketApi.composable"; import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; import { useCardStore } from "./Card.store"; import { DeleteCardSuccessPayload } from "./cardActions/cardActionPayload"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; -import { useRouter } from "vue-router"; -import { useI18n } from "vue-i18n"; import { BoardFeature } from "@/serverApi/v3"; export const useBoardStore = defineStore("boardStore", () => { @@ -225,6 +227,20 @@ export const useBoardStore = defineStore("boardStore", () => { board.value.isVisible = payload.isVisible; }; + const updateBoardLayoutRequest = async ( + payload: UpdateBoardLayoutRequestPayload + ): Promise => { + await socketOrRest.updateBoardLayoutRequest(payload); + }; + + const updateBoardLayoutSuccess = ( + payload: UpdateBoardLayoutSuccessPayload + ): void => { + if (!board.value) return; + + board.value.layout = payload.layout; + }; + const moveColumnRequest = async (payload: MoveColumnRequestPayload) => { await socketOrRest.moveColumnRequest({ targetBoardId: board.value?.id, @@ -401,6 +417,8 @@ export const useBoardStore = defineStore("boardStore", () => { updateBoardTitleSuccess, updateBoardVisibilityRequest, updateBoardVisibilitySuccess, + updateBoardLayoutRequest, + updateBoardLayoutSuccess, fetchBoardRequest, fetchBoardSuccess, reloadBoard, diff --git a/src/modules/data/board/Board.store.unit.ts b/src/modules/data/board/Board.store.unit.ts index 2bdc64d6e2..6bbee77f12 100644 --- a/src/modules/data/board/Board.store.unit.ts +++ b/src/modules/data/board/Board.store.unit.ts @@ -1,8 +1,10 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; -import { envConfigModule, applicationErrorModule } from "@/store"; -import EnvConfigModule from "@/store/env-config"; +import { applicationErrorModule, envConfigModule } from "@/store"; import ApplicationErrorModule from "@/store/application-error"; +import EnvConfigModule from "@/store/env-config"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; import { ColumnMove } from "@/types/board/DragAndDrop"; +import { createApplicationError } from "@/utils/create-application-error.factory"; import { mockedPiniaStoreTyping } from "@@/tests/test-utils"; import { boardResponseFactory, @@ -21,14 +23,14 @@ import { } from "@util-board"; import { createPinia, setActivePinia } from "pinia"; import { computed, ref } from "vue"; +import { Router, useRoute, useRouter } from "vue-router"; +import { BoardLayout } from "../../../serverApi/v3"; import { useBoardStore } from "./Board.store"; +import { UpdateBoardLayoutRequestPayload } from "./boardActions/boardActionPayload"; import { useBoardRestApi } from "./boardActions/boardRestApi.composable"; import { useBoardSocketApi } from "./boardActions/boardSocketApi.composable"; -import { useCardSocketApi } from "./cardActions/cardSocketApi.composable"; import { useBoardFocusHandler } from "./BoardFocusHandler.composable"; -import { Router, useRoute, useRouter } from "vue-router"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; +import { useCardSocketApi } from "./cardActions/cardSocketApi.composable"; jest.mock("./boardActions/boardSocketApi.composable"); const mockedUseBoardSocketApi = jest.mocked(useBoardSocketApi); @@ -622,6 +624,36 @@ describe("BoardStore", () => { }); }); + describe("updateBoardLayoutSuccess", () => { + describe("when board value is undefined", () => { + it("should not update the board layout", () => { + const { boardStore } = setup({ createBoard: false }); + + boardStore.updateBoardLayoutSuccess({ + boardId: "boardId", + layout: BoardLayout.Columns, + isOwnAction: true, + }); + + expect(boardStore.board).toBe(undefined); + }); + }); + + describe("when board is defined", () => { + it("should update the board layout", () => { + const { boardStore } = setup(); + + boardStore.updateBoardLayoutSuccess({ + boardId: "boardId", + layout: BoardLayout.List, + isOwnAction: true, + }); + + expect(boardStore.board?.layout).toStrictEqual(BoardLayout.List); + }); + }); + }); + describe("moveColumnSuccess", () => { it("should not move a column when board value is undefined", async () => { const { boardStore, firstColumn } = setup({ createBoard: false }); @@ -997,6 +1029,33 @@ describe("BoardStore", () => { }); }); + describe("@updateBoardLayoutRequest", () => { + const payload: UpdateBoardLayoutRequestPayload = { + boardId: "boardId", + layout: BoardLayout.Columns, + }; + + it("should call socketApi.updateBoardLayoutRequest when feature flag is set true", async () => { + const { boardStore } = setup({ socketFlag: true }); + + await boardStore.updateBoardLayoutRequest(payload); + + expect( + mockedSocketApiActions.updateBoardLayoutRequest + ).toHaveBeenCalledWith(payload); + }); + + it("should call restApi.updateBoardLayoutRequest when feature flag is set false", async () => { + const { boardStore } = setup(); + + await boardStore.updateBoardLayoutRequest(payload); + + expect( + mockedBoardRestApiActions.updateBoardLayoutRequest + ).toHaveBeenCalledWith(payload); + }); + }); + describe("@moveColumnRequest", () => { const payload = { columnMove: { diff --git a/src/modules/data/board/BoardApi.composable.ts b/src/modules/data/board/BoardApi.composable.ts index a3559f6b46..2a50a95f06 100644 --- a/src/modules/data/board/BoardApi.composable.ts +++ b/src/modules/data/board/BoardApi.composable.ts @@ -3,6 +3,7 @@ import { BoardCardApiFactory, BoardColumnApiFactory, BoardElementApiFactory, + BoardLayout, BoardResponse, CardResponse, ColumnResponse, @@ -32,7 +33,7 @@ import { BoardContextType } from "@/types/board/BoardContext"; import { AnyContentElement } from "@/types/board/ContentElement"; import { $axios, mapAxiosErrorToResponseError } from "@/utils/api"; import { createApplicationError } from "@/utils/create-application-error.factory"; -import { AxiosPromise } from "axios"; +import { AxiosPromise, AxiosResponse } from "axios"; export const useBoardApi = () => { const boardApi = BoardApiFactory(undefined, "/v3", $axios); @@ -302,6 +303,13 @@ export const useBoardApi = () => { return boardApi.boardControllerUpdateVisibility(boardId, { isVisible }); }; + const updateBoardLayoutCall = async ( + boardId: string, + layout: BoardLayout + ): Promise> => { + return boardApi.boardControllerUpdateLayout(boardId, { layout }); + }; + return { fetchBoardCall, createColumnCall, @@ -320,5 +328,6 @@ export const useBoardApi = () => { updateElementCall, createCardCall, getContextInfo, + updateBoardLayoutCall, }; }; diff --git a/src/modules/data/board/BoardApi.composable.unit.ts b/src/modules/data/board/BoardApi.composable.unit.ts index 56a896b404..367848bfef 100644 --- a/src/modules/data/board/BoardApi.composable.unit.ts +++ b/src/modules/data/board/BoardApi.composable.unit.ts @@ -1,6 +1,8 @@ import { + BoardLayout, ContentElementType, ExternalToolElementResponse, + LayoutBodyParams, } from "@/serverApi/v3"; import * as serverApi from "@/serverApi/v3/api"; import { CardResponse, DrawingElementResponse } from "@/serverApi/v3/api"; @@ -557,4 +559,16 @@ describe("BoardApi.composable", () => { ); }); }); + + describe("updateBoardLayoutCall", () => { + it("should call boardControllerUpdateLayout api", async () => { + const { updateBoardLayoutCall } = useBoardApi(); + + await updateBoardLayoutCall("board-id", BoardLayout.List); + + expect(boardApi.boardControllerUpdateLayout).toHaveBeenCalledWith< + [string, LayoutBodyParams] + >("board-id", { layout: BoardLayout.List }); + }); + }); }); diff --git a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts index 516aab324d..a930aa8004 100644 --- a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts +++ b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.ts @@ -1,17 +1,19 @@ import { useAriaLiveNotifier } from "@/composables/ariaLiveNotifier"; +import { BoardLayout } from "@/serverApi/v3"; import { useI18n } from "vue-i18n"; import { useBoardStore } from "../Board.store"; -import { useCardStore } from "../Card.store"; import { CreateCardSuccessPayload, CreateColumnSuccessPayload, DeleteColumnSuccessPayload, MoveCardSuccessPayload, MoveColumnSuccessPayload, + UpdateBoardLayoutSuccessPayload, UpdateBoardTitleSuccessPayload, UpdateBoardVisibilitySuccessPayload, UpdateColumnTitleSuccessPayload, } from "../boardActions/boardActionPayload"; +import { useCardStore } from "../Card.store"; import { CreateElementSuccessPayload, @@ -51,6 +53,8 @@ export const SR_I18N_KEYS_MAP = { "components.board.screenReader.notification.cardTitleUpdated.success", CARD_UPDATED_SUCCESS: "components.board.screenReader.notification.cardUpdated.success", + BOARD_LAYOUT_UPDATED_SUCCESS: + "components.board.screenReader.notification.boardLayoutUpdated.success", }; export const useBoardAriaNotification = () => { @@ -276,6 +280,32 @@ export const useBoardAriaNotification = () => { ); }; + const notifyUpdateBoardLayoutSuccess = ( + action: UpdateBoardLayoutSuccessPayload + ): void => { + const { layout, isOwnAction } = action; + if (isOwnAction) return; + + let layoutName: string; + switch (layout) { + case BoardLayout.Columns: + layoutName = t("pages.room.dialog.boardLayout.multiColumn"); + break; + case BoardLayout.List: + layoutName = t("pages.room.dialog.boardLayout.singleColumn"); + break; + default: + layoutName = t("common.labels.unknown"); + break; + } + + notifyOnScreenReader( + t(SR_I18N_KEYS_MAP.BOARD_LAYOUT_UPDATED_SUCCESS, { + layout: layoutName, + }) + ); + }; + return { notifyCreateCardSuccess, notifyCreateColumnSuccess, @@ -291,5 +321,6 @@ export const useBoardAriaNotification = () => { notifyUpdateElementSuccess, notifyDeleteElementSuccess, notifyMoveElementSuccess, + notifyUpdateBoardLayoutSuccess, }; }; diff --git a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts index 46c5d912a6..011f97e270 100644 --- a/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts +++ b/src/modules/data/board/ariaNotification/ariaLiveNotificationHandler.unit.ts @@ -1,7 +1,5 @@ -import { - useBoardAriaNotification, - SR_I18N_KEYS_MAP, -} from "./ariaLiveNotificationHandler"; +import { BoardLayout, ContentElementType } from "@/serverApi/v3"; +import { AnyContentElement } from "@/types/board/ContentElement"; import { cardResponseFactory, columnResponseFactory, @@ -10,8 +8,10 @@ import { CreateCardSuccessPayload, CreateColumnSuccessPayload, } from "../boardActions/boardActionPayload"; -import { ContentElementType } from "@/serverApi/v3"; -import { AnyContentElement } from "@/types/board/ContentElement"; +import { + SR_I18N_KEYS_MAP, + useBoardAriaNotification, +} from "./ariaLiveNotificationHandler"; const card = { elements: [ @@ -222,6 +222,53 @@ describe("useBoardAriaNotification", () => { }); }); + describe("when notifying on boardLayoutUpdate", () => { + it("should notify on boardLayoutUpdate to columns", () => { + const { notifyUpdateBoardLayoutSuccess } = useBoardAriaNotification(); + const element = document.getElementById("notify-screen-reader-polite"); + notifyUpdateBoardLayoutSuccess({ + boardId: "boardId", + layout: BoardLayout.Columns, + isOwnAction: false, + }); + + jest.advanceTimersByTime(3000); + expect(element?.innerHTML).toContain( + SR_I18N_KEYS_MAP.BOARD_LAYOUT_UPDATED_SUCCESS + ); + }); + + it("should notify on boardLayoutUpdate to list", () => { + const { notifyUpdateBoardLayoutSuccess } = useBoardAriaNotification(); + const element = document.getElementById("notify-screen-reader-polite"); + notifyUpdateBoardLayoutSuccess({ + boardId: "boardId", + layout: BoardLayout.List, + isOwnAction: false, + }); + + jest.advanceTimersByTime(3000); + expect(element?.innerHTML).toContain( + SR_I18N_KEYS_MAP.BOARD_LAYOUT_UPDATED_SUCCESS + ); + }); + + it("should notify on boardLayoutUpdate to unknown", () => { + const { notifyUpdateBoardLayoutSuccess } = useBoardAriaNotification(); + const element = document.getElementById("notify-screen-reader-polite"); + notifyUpdateBoardLayoutSuccess({ + boardId: "boardId", + layout: BoardLayout.Grid, + isOwnAction: false, + }); + + jest.advanceTimersByTime(3000); + expect(element?.innerHTML).toContain( + SR_I18N_KEYS_MAP.BOARD_LAYOUT_UPDATED_SUCCESS + ); + }); + }); + it("should notify on columnTitleUpdate", () => { const { notifyUpdateColumnTitleSuccess } = useBoardAriaNotification(); const element = document.getElementById("notify-screen-reader-polite"); diff --git a/src/modules/data/board/boardActions/boardActionPayload.ts b/src/modules/data/board/boardActions/boardActionPayload.ts index 97eb5bae9b..1b29d5f514 100644 --- a/src/modules/data/board/boardActions/boardActionPayload.ts +++ b/src/modules/data/board/boardActions/boardActionPayload.ts @@ -1,4 +1,5 @@ import { + BoardLayout, BoardResponse, CardResponse, ColumnResponse, @@ -128,3 +129,14 @@ export type UpdateBoardVisibilityFailurePayload = UpdateBoardVisibilityRequestPayload; export type DisconnectSocketRequestPayload = Record; + +export type UpdateBoardLayoutRequestPayload = { + boardId: string; + layout: BoardLayout; +}; +export type UpdateBoardLayoutSuccessPayload = { + boardId: string; + layout: BoardLayout; + isOwnAction: boolean; +}; +export type UpdateBoardLayoutFailurePayload = UpdateBoardLayoutRequestPayload; diff --git a/src/modules/data/board/boardActions/boardActions.ts b/src/modules/data/board/boardActions/boardActions.ts index 778304e7ea..cd178e6a1d 100644 --- a/src/modules/data/board/boardActions/boardActions.ts +++ b/src/modules/data/board/boardActions/boardActions.ts @@ -1,3 +1,4 @@ +import { createAction, props } from "@/types/board/ActionFactory"; import { CreateCardFailurePayload, CreateCardRequestPayload, @@ -22,6 +23,9 @@ import { MoveColumnSuccessPayload, ReloadBoardPayload, ReloadBoardSuccessPayload, + UpdateBoardLayoutFailurePayload, + UpdateBoardLayoutRequestPayload, + UpdateBoardLayoutSuccessPayload, UpdateBoardTitleFailurePayload, UpdateBoardTitleRequestPayload, UpdateBoardTitleSuccessPayload, @@ -32,7 +36,6 @@ import { UpdateColumnTitleRequestPayload, UpdateColumnTitleSuccessPayload, } from "./boardActionPayload"; -import { createAction, props } from "@/types/board/ActionFactory"; export const disconnectSocket = createAction( "disconnect-socket", @@ -182,3 +185,18 @@ export const updateColumnTitleFailure = createAction( "update-column-title-failure", props() ); + +export const updateBoardLayoutRequest = createAction( + "update-board-layout-request", + props() +); + +export const updateBoardLayoutSuccess = createAction( + "update-board-layout-success", + props() +); + +export const updateBoardLayoutFailure = createAction( + "update-board-layout-failure", + props() +); diff --git a/src/modules/data/board/boardActions/boardRestApi.composable.ts b/src/modules/data/board/boardActions/boardRestApi.composable.ts index c1ff684d56..d49def94a4 100644 --- a/src/modules/data/board/boardActions/boardRestApi.composable.ts +++ b/src/modules/data/board/boardActions/boardRestApi.composable.ts @@ -3,7 +3,11 @@ import { ErrorType, useErrorHandler, } from "@/components/error-handling/ErrorHandler.composable"; +import { applicationErrorModule, courseRoomDetailsModule } from "@/store"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; +import { createApplicationError } from "@/utils/create-application-error.factory"; import { useSharedEditMode } from "@util-board"; +import { useI18n } from "vue-i18n"; import { useBoardStore } from "../Board.store"; import { useBoardApi } from "../BoardApi.composable"; import { @@ -13,15 +17,12 @@ import { FetchBoardRequestPayload, MoveCardRequestPayload, MoveColumnRequestPayload, + UpdateBoardLayoutRequestPayload, UpdateBoardTitleRequestPayload, UpdateBoardVisibilityRequestPayload, UpdateColumnTitleRequestPayload, } from "./boardActionPayload"; import * as BoardActions from "./boardActions"; -import { applicationErrorModule, courseRoomDetailsModule } from "@/store"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; -import { useI18n } from "vue-i18n"; export const useBoardRestApi = () => { const boardStore = useBoardStore(); @@ -37,6 +38,7 @@ export const useBoardRestApi = () => { updateColumnTitleCall, updateBoardTitleCall, updateBoardVisibilityCall, + updateBoardLayoutCall, } = useBoardApi(); const { t } = useI18n(); @@ -231,6 +233,26 @@ export const useBoardRestApi = () => { } }; + const updateBoardLayoutRequest = async ( + payload: UpdateBoardLayoutRequestPayload + ) => { + if (boardStore.board === undefined) return; + const { boardId, layout } = payload; + + try { + await updateBoardLayoutCall(boardId, layout); + boardStore.updateBoardLayoutSuccess({ + boardId, + layout, + isOwnAction: true, + }); + } catch (error) { + handleError(error, { + 404: notifyWithTemplateAndReload("notUpdated", "board"), + }); + } + }; + const notifyWithTemplateAndReload = ( errorType: ErrorType, boardObjectType?: BoardObjectType @@ -272,6 +294,7 @@ export const useBoardRestApi = () => { updateColumnTitleRequest, updateBoardTitleRequest, updateBoardVisibilityRequest, + updateBoardLayoutRequest, reloadBoard, reloadBoardSuccess, disconnectSocketRequest, diff --git a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts index 7b0be97f7a..bea4fe58d2 100644 --- a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts +++ b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts @@ -1,12 +1,15 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; import { applicationErrorModule, - envConfigModule, courseRoomDetailsModule, + envConfigModule, } from "@/store"; -import EnvConfigModule from "@/store/env-config"; +import ApplicationErrorModule from "@/store/application-error"; import CourseRoomDetailsModule from "@/store/course-room-details"; +import EnvConfigModule from "@/store/env-config"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; import { ColumnMove } from "@/types/board/DragAndDrop"; +import { createApplicationError } from "@/utils/create-application-error.factory"; import { boardResponseFactory, cardSkeletonResponseFactory, @@ -22,12 +25,10 @@ import { createTestingPinia } from "@pinia/testing"; import { useSharedEditMode } from "@util-board"; import { setActivePinia } from "pinia"; import { computed, ref } from "vue"; +import { Router, useRouter } from "vue-router"; +import { BoardLayout } from "../../../../serverApi/v3"; import { useBoardApi } from "../BoardApi.composable"; import { useBoardRestApi } from "./boardRestApi.composable"; -import ApplicationErrorModule from "@/store/application-error"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; -import { Router, useRouter } from "vue-router"; jest.mock("@/components/error-handling/ErrorHandler.composable"); const mockedUseErrorHandler = jest.mocked(useErrorHandler); @@ -699,6 +700,50 @@ describe("boardRestApi", () => { }); }); + describe("updateBoardLayoutRequest", () => { + it("should not call updateBoardLayoutSuccess action when board value is undefined", async () => { + const { boardStore } = setup(false); + const { updateBoardLayoutRequest } = useBoardRestApi(); + + await updateBoardLayoutRequest({ + boardId: "boardId", + layout: BoardLayout.Columns, + }); + + expect(boardStore.updateBoardLayoutSuccess).not.toHaveBeenCalled(); + }); + + it("should call updateBoardLayoutSuccess action if the API call is successful", async () => { + const { boardStore } = setup(); + const { updateBoardLayoutRequest } = useBoardRestApi(); + + await updateBoardLayoutRequest({ + boardId: "boardId", + layout: BoardLayout.Columns, + }); + + expect(boardStore.updateBoardLayoutSuccess).toHaveBeenCalledWith({ + boardId: "boardId", + layout: BoardLayout.Columns, + isOwnAction: true, + }); + }); + + it("should call handleError if the API call fails", async () => { + setup(); + const { updateBoardLayoutRequest } = useBoardRestApi(); + + mockedBoardApiCalls.updateBoardLayoutCall.mockRejectedValue({}); + + await updateBoardLayoutRequest({ + boardId: "boardId", + layout: BoardLayout.Columns, + }); + + expect(mockedErrorHandler.handleError).toHaveBeenCalled(); + }); + }); + describe("notifyWithTemplateAndReload", () => { /** * Simulates actually calling the error handling function for a 404 error. diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.ts index 16638de630..a3a799211f 100644 --- a/src/modules/data/board/boardActions/boardSocketApi.composable.ts +++ b/src/modules/data/board/boardActions/boardSocketApi.composable.ts @@ -1,29 +1,29 @@ -import * as BoardActions from "./boardActions"; -import * as CardActions from "../cardActions/cardActions"; -import { useSocketConnection, useForceRender } from "@data-board"; +import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; +import { CreateCardBodyParamsRequiredEmptyElementsEnum } from "@/serverApi/v3"; +import { applicationErrorModule } from "@/store"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; +import { handle, on, PermittedStoreActions } from "@/types/board/ActionFactory"; +import { createApplicationError } from "@/utils/create-application-error.factory"; +import { useForceRender, useSocketConnection } from "@data-board"; +import { useI18n } from "vue-i18n"; +import { useBoardAriaNotification } from "../ariaNotification/ariaLiveNotificationHandler"; import { useBoardStore } from "../Board.store"; +import * as CardActions from "../cardActions/cardActions"; import { CreateCardRequestPayload, CreateColumnRequestPayload, DeleteBoardRequestPayload, DeleteColumnRequestPayload, - DisconnectSocketRequestPayload, FetchBoardRequestPayload, MoveCardRequestPayload, MoveCardSuccessPayload, MoveColumnRequestPayload, + UpdateBoardLayoutRequestPayload, UpdateBoardTitleRequestPayload, UpdateBoardVisibilityRequestPayload, UpdateColumnTitleRequestPayload, } from "./boardActionPayload"; -import { PermittedStoreActions, handle, on } from "@/types/board/ActionFactory"; -import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; -import { useBoardAriaNotification } from "../ariaNotification/ariaLiveNotificationHandler"; -import { CreateCardBodyParamsRequiredEmptyElementsEnum } from "@/serverApi/v3"; -import { applicationErrorModule } from "@/store"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; -import { useI18n } from "vue-i18n"; +import * as BoardActions from "./boardActions"; export const useBoardSocketApi = () => { const boardStore = useBoardStore(); @@ -38,6 +38,7 @@ export const useBoardSocketApi = () => { notifyUpdateBoardTitleSuccess, notifyUpdateBoardVisibilitySuccess, notifyUpdateColumnTitleSuccess, + notifyUpdateBoardLayoutSuccess, } = useBoardAriaNotification(); const { t } = useI18n(); @@ -65,6 +66,10 @@ export const useBoardSocketApi = () => { BoardActions.updateBoardVisibilitySuccess, boardStore.updateBoardVisibilitySuccess ), + on( + BoardActions.updateBoardLayoutSuccess, + boardStore.updateBoardLayoutSuccess + ), ]; const failureActions = [ @@ -81,6 +86,7 @@ export const useBoardSocketApi = () => { BoardActions.updateBoardVisibilityFailure, updateBoardVisibilityFailure ), + on(BoardActions.updateBoardLayoutFailure, updateBoardLayoutFailure), ]; const ariaLiveNotifications = [ @@ -96,6 +102,7 @@ export const useBoardSocketApi = () => { notifyUpdateBoardVisibilitySuccess ), on(BoardActions.updateColumnTitleSuccess, notifyUpdateColumnTitleSuccess), + on(BoardActions.updateBoardLayoutSuccess, notifyUpdateBoardLayoutSuccess), ]; handle( @@ -183,6 +190,12 @@ export const useBoardSocketApi = () => { emitOnSocket("update-board-visibility-request", payload); }; + const updateBoardLayoutRequest = ( + payload: UpdateBoardLayoutRequestPayload + ) => { + emitOnSocket("update-board-layout-request", payload); + }; + const setRenderKeyAfterMoveCard = (payload: MoveCardSuccessPayload) => { const { generateRenderKey } = useForceRender(payload.fromColumnId); generateRenderKey(); @@ -211,6 +224,8 @@ export const useBoardSocketApi = () => { notifySocketError("notUpdated", "board"); const updateBoardVisibilityFailure = () => notifySocketError("notUpdated", "board"); + const updateBoardLayoutFailure = () => + notifySocketError("notUpdated", "board"); return { dispatch, @@ -225,5 +240,6 @@ export const useBoardSocketApi = () => { updateColumnTitleRequest, updateBoardTitleRequest, updateBoardVisibilityRequest, + updateBoardLayoutRequest, }; }; diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts index 40e4be6f07..4083bb9992 100644 --- a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts +++ b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts @@ -1,7 +1,9 @@ import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable"; -import { envConfigModule, applicationErrorModule } from "@/store"; -import EnvConfigModule from "@/store/env-config"; +import { applicationErrorModule, envConfigModule } from "@/store"; import ApplicationErrorModule from "@/store/application-error"; +import EnvConfigModule from "@/store/env-config"; +import { HttpStatusCode } from "@/store/types/http-status-code.enum"; +import { createApplicationError } from "@/utils/create-application-error.factory"; import { boardResponseFactory, cardResponseFactory, @@ -20,16 +22,19 @@ import { createTestingPinia } from "@pinia/testing"; import { useBoardNotifier, useSharedLastCreatedElement } from "@util-board"; import { setActivePinia } from "pinia"; import { useI18n } from "vue-i18n"; +import { Router, useRouter } from "vue-router"; +import { BoardLayout } from "../../../../serverApi/v3"; import { DeleteCardFailurePayload } from "../cardActions/cardActionPayload"; import * as CardActions from "../cardActions/cardActions"; import { CreateCardFailurePayload, CreateColumnFailurePayload, DeleteColumnFailurePayload, - DisconnectSocketRequestPayload, MoveCardFailurePayload, MoveCardRequestPayload, MoveColumnFailurePayload, + UpdateBoardLayoutFailurePayload, + UpdateBoardLayoutSuccessPayload, UpdateBoardTitleFailurePayload, UpdateBoardVisibilityFailurePayload, UpdateColumnTitleFailurePayload, @@ -37,9 +42,6 @@ import { import * as BoardActions from "./boardActions"; import { useBoardRestApi } from "./boardRestApi.composable"; import { useBoardSocketApi } from "./boardSocketApi.composable"; -import { HttpStatusCode } from "@/store/types/http-status-code.enum"; -import { createApplicationError } from "@/utils/create-application-error.factory"; -import { Router, useRouter } from "vue-router"; jest.mock("../socket/socket"); const mockedUseSocketConnection = jest.mocked(useSocketConnection); @@ -274,6 +276,20 @@ describe("useBoardSocketApi", () => { ); }); + it("should call updateBoardLayoutSuccess for corresponding action", () => { + const boardStore = mockedPiniaStoreTyping(useBoardStore); + const { dispatch } = useBoardSocketApi(); + + const payload: UpdateBoardLayoutSuccessPayload = { + boardId: "cardId", + layout: BoardLayout.Columns, + isOwnAction: true, + }; + dispatch(BoardActions.updateBoardLayoutSuccess(payload)); + + expect(boardStore.updateBoardLayoutSuccess).toHaveBeenCalledWith(payload); + }); + describe("failure actions", () => { it("should call applicationErrorModule.setError for fetchBoardFailure action", () => { const setErrorSpy = jest.spyOn(applicationErrorModule, "setError"); @@ -424,6 +440,22 @@ describe("useBoardSocketApi", () => { "board" ); }); + + it("should call notifySocketError for updateBoardLayoutFailure action", () => { + const { dispatch } = useBoardSocketApi(); + + const payload: UpdateBoardLayoutFailurePayload = { + boardId: "test", + layout: BoardLayout.Columns, + }; + + dispatch(BoardActions.updateBoardLayoutFailure(payload)); + + expect(mockedErrorHandler.notifySocketError).toHaveBeenCalledWith( + "notUpdated", + "board" + ); + }); }); }); @@ -674,4 +706,22 @@ describe("useBoardSocketApi", () => { ); }); }); + + describe("updateBoardLayoutRequest", () => { + it("should call action with correct parameters", () => { + const { updateBoardLayoutRequest } = useBoardSocketApi(); + + updateBoardLayoutRequest({ + boardId: "boardId", + layout: BoardLayout.Columns, + }); + + expect(mockedSocketConnectionHandler.emitOnSocket).toHaveBeenCalledWith< + [string, UpdateBoardLayoutFailurePayload] + >("update-board-layout-request", { + boardId: "boardId", + layout: BoardLayout.Columns, + }); + }); + }); }); diff --git a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts index d7fdd12340..beaeee8147 100644 --- a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts +++ b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts @@ -1,27 +1,27 @@ +import * as serverApi from "@/serverApi/v3/api"; import { - roomMemberListFactory, + RoleName, + RoomMemberResponse, + SchoolUserListResponse, +} from "@/serverApi/v3/api"; +import { authModule, schoolsModule } from "@/store"; +import AuthModule from "@/store/auth"; +import SchoolsModule from "@/store/schools"; +import { initializeAxios } from "@/utils/api"; +import { + meResponseFactory, mockApiResponse, roomMemberFactory, + roomMemberListFactory, roomMemberSchoolResponseFactory, schoolFactory, - meResponseFactory, } from "@@/tests/test-utils"; +import setupStores from "@@/tests/test-utils/setupStores"; +import { useRoomMembers } from "@data-room"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; -import * as serverApi from "@/serverApi/v3/api"; -import { initializeAxios } from "@/utils/api"; +import { useBoardNotifier } from "@util-board"; import { AxiosInstance } from "axios"; -import { useRoomMembers } from "@data-room"; import { useI18n } from "vue-i18n"; -import { - RoleName, - RoomMemberResponse, - SchoolUserListResponse, -} from "@/serverApi/v3/api"; -import { useBoardNotifier } from "@util-board"; -import { schoolsModule, authModule } from "@/store"; -import SchoolsModule from "@/store/schools"; -import AuthModule from "@/store/auth"; -import setupStores from "@@/tests/test-utils/setupStores"; jest.mock("vue-i18n"); (useI18n as jest.Mock).mockReturnValue({ t: (key: string) => key }); diff --git a/src/modules/feature/board/board/Board.unit.ts b/src/modules/feature/board/board/Board.unit.ts index c245c240a7..3e56edd82a 100644 --- a/src/modules/feature/board/board/Board.unit.ts +++ b/src/modules/feature/board/board/Board.unit.ts @@ -2,6 +2,7 @@ import CopyResultModal from "@/components/copy-result-modal/CopyResultModal.vue" import { useApplicationError } from "@/composables/application-error.composable"; import { useCopy } from "@/composables/copy"; import { + BoardLayout, ConfigResponse, CopyApiResponse, CopyApiResponseTypeEnum, @@ -53,6 +54,7 @@ import { } from "@data-board"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { createTestingPinia } from "@pinia/testing"; +import { SelectBoardLayoutDialog } from "@ui-room-details"; import { extractDataAttribute, useBoardNotifier, @@ -65,6 +67,7 @@ import { computed, nextTick, ref } from "vue"; import { Router, useRoute, useRouter } from "vue-router"; import BoardVue from "./Board.vue"; import BoardColumnVue from "./BoardColumn.vue"; +import BoardHeader from "./BoardHeader.vue"; import BoardHeaderVue from "./BoardHeader.vue"; jest.mock("@util-board/BoardNotifier.composable"); @@ -1086,4 +1089,85 @@ describe("Board", () => { }); }); }); + + describe("Change board layout", () => { + describe("when the 'change layout' menu button is clicked", () => { + it("should open the change dialog", async () => { + const { wrapper } = setup(); + + const boardHeader = wrapper.findComponent(BoardHeader); + const boardLayoutDialog = wrapper.findComponent( + SelectBoardLayoutDialog + ); + + boardHeader.vm.$emit("change-layout"); + await nextTick(); + + expect(boardLayoutDialog.props("modelValue")).toEqual(true); + }); + }); + + describe("when the change layout dialog is confirmed", () => { + describe("when layout has changed", () => { + it("should close the dialog", async () => { + const { wrapper } = setup(); + + const boardLayoutDialog = wrapper.findComponent( + SelectBoardLayoutDialog + ); + await boardLayoutDialog.setValue(true, "modelValue"); + + boardLayoutDialog.vm.$emit("select", BoardLayout.List); + await nextTick(); + + expect(boardLayoutDialog.props("modelValue")).toEqual(false); + }); + + it("should send the update request", async () => { + const { wrapper, boardStore, board } = setup(); + + const boardLayoutDialog = wrapper.findComponent( + SelectBoardLayoutDialog + ); + + boardLayoutDialog.vm.$emit("select", BoardLayout.List); + await nextTick(); + + expect(boardStore.updateBoardLayoutRequest).toHaveBeenCalledWith({ + boardId: board.id, + layout: BoardLayout.List, + }); + }); + }); + + describe("when the layout has not changed", () => { + it("should close the dialog", async () => { + const { wrapper } = setup(); + + const boardLayoutDialog = wrapper.findComponent( + SelectBoardLayoutDialog + ); + await boardLayoutDialog.setValue(true, "modelValue"); + + boardLayoutDialog.vm.$emit("select", BoardLayout.List); + await nextTick(); + + expect(boardLayoutDialog.props("modelValue")).toEqual(false); + }); + + it("should not send an update request", async () => { + const { wrapper, boardStore, board } = setup(); + + const boardLayoutDialog = wrapper.findComponent( + SelectBoardLayoutDialog + ); + + boardLayoutDialog.vm.$emit("select", board.layout); + await nextTick(); + + expect(boardStore.updateBoardLayoutRequest).not.toHaveBeenCalled(); + }); + }); + }); + }); }); diff --git a/src/modules/feature/board/board/Board.vue b/src/modules/feature/board/board/Board.vue index 36270e3341..ad40b70e1a 100644 --- a/src/modules/feature/board/board/Board.vue +++ b/src/modules/feature/board/board/Board.vue @@ -18,6 +18,7 @@ @copy:board="onCopyBoard" @share:board="onShareBoard" @delete:board="openDeleteBoardDialog(boardId)" + @change-layout="onUpdateBoardLayout" />
@@ -86,6 +87,11 @@ @copy-dialog-closed="onCopyResultModalClosed" /> +
@@ -123,6 +129,7 @@ import { } from "@data-board"; import { ConfirmationDialog } from "@ui-confirmation-dialog"; import { LightBox } from "@ui-light-box"; +import { SelectBoardLayoutDialog } from "@ui-room-details"; import { BOARD_IS_LIST_LAYOUT, extractDataAttribute, @@ -139,6 +146,7 @@ import { onUnmounted, PropType, provide, + ref, watch, } from "vue"; import { useI18n } from "vue-i18n"; @@ -431,4 +439,23 @@ const onShareBoard = () => { const openDeleteBoardDialog = async (id: string) => { boardStore.deleteBoardRequest({ boardId: id }, roomId.value); }; + +const isSelectBoardLayoutDialogOpen = ref(false); + +const onUpdateBoardLayout = async () => { + if (!hasEditPermission) return; + + isSelectBoardLayoutDialogOpen.value = true; +}; + +const onSelectBoardLayout = async (layout: BoardLayout) => { + isSelectBoardLayoutDialogOpen.value = false; + + if (!hasEditPermission || board.value?.layout === layout) return; + + boardStore.updateBoardLayoutRequest({ + boardId: props.boardId, + layout, + }); +}; diff --git a/src/modules/feature/board/board/BoardHeader.unit.ts b/src/modules/feature/board/board/BoardHeader.unit.ts index f73419f15c..f14db21fe2 100644 --- a/src/modules/feature/board/board/BoardHeader.unit.ts +++ b/src/modules/feature/board/board/BoardHeader.unit.ts @@ -12,12 +12,13 @@ import { } from "@@/tests/test-utils/setup"; import { useBoardFocusHandler, useBoardPermissions } from "@data-board"; import { + KebabMenuActionChangeLayout, KebabMenuActionCopy, KebabMenuActionDelete, KebabMenuActionPublish, + KebabMenuActionRename, KebabMenuActionRevert, KebabMenuActionShare, - KebabMenuActionRename, } from "@ui-kebab-menu"; import { useCourseBoardEditMode } from "@util-board"; import { shallowMount } from "@vue/test-utils"; @@ -76,6 +77,10 @@ describe("BoardHeader", () => { return { startEditMode: mockedStartEditMode, wrapper }; }; + afterEach(() => { + jest.clearAllMocks(); + }); + describe("when component is mounted", () => { it("should be found in the dom", () => { const { wrapper } = setup(); @@ -281,6 +286,19 @@ describe("BoardHeader", () => { }); }); + describe("when the 'change layout' menu button is clicked", () => { + it("should emit 'change-layout'", async () => { + const { wrapper } = setup(); + + const changeLayoutButton = wrapper.findComponent( + KebabMenuActionChangeLayout + ); + await changeLayoutButton.trigger("click"); + + expect(wrapper.emitted("change-layout")).toHaveLength(1); + }); + }); + describe("when board is a draft", () => { it("should display draft label", () => { const { wrapper } = setup( diff --git a/src/modules/feature/board/board/BoardHeader.vue b/src/modules/feature/board/board/BoardHeader.vue index 36083c32e9..b131c683e7 100644 --- a/src/modules/feature/board/board/BoardHeader.vue +++ b/src/modules/feature/board/board/BoardHeader.vue @@ -44,6 +44,7 @@ + ) => { } }; +const onChangeBoardLayout = async () => { + emit("change-layout"); +}; + const emitTitle = useDebounceFn((newTitle: string) => { if (newTitle.length < 1) return; diff --git a/src/modules/feature/media-shelf/MediaBoardAvailableLine.unit.ts b/src/modules/feature/media-shelf/MediaBoardAvailableLine.unit.ts index ca67f703a6..9f2db36c15 100644 --- a/src/modules/feature/media-shelf/MediaBoardAvailableLine.unit.ts +++ b/src/modules/feature/media-shelf/MediaBoardAvailableLine.unit.ts @@ -1,4 +1,4 @@ -import { MediaBoardLayoutType } from "@/serverApi/v3"; +import { BoardLayout } from "@/serverApi/v3"; import { mediaAvailableLineElementResponseFactory, mediaAvailableLineResponseFactory, @@ -32,7 +32,7 @@ describe("MediaBoardAvailableLine", () => { const getWrapper = ( props: ComponentProps = { line: mediaAvailableLineResponseFactory.build(), - layout: MediaBoardLayoutType.List, + layout: BoardLayout.List, } ) => { const wrapper = mount(MediaBoardAvailableLine, { @@ -89,7 +89,7 @@ describe("MediaBoardAvailableLine", () => { const { wrapper } = getWrapper({ line: availableMedia, - layout: MediaBoardLayoutType.List, + layout: BoardLayout.List, }); const toLineId = "toLineId"; @@ -235,7 +235,7 @@ describe("MediaBoardAvailableLine", () => { const setup = () => { const { wrapper } = getWrapper({ line: mediaAvailableLineResponseFactory.build(), - layout: MediaBoardLayoutType.Grid, + layout: BoardLayout.Grid, }); return { @@ -256,7 +256,7 @@ describe("MediaBoardAvailableLine", () => { const setup = () => { const { wrapper } = getWrapper({ line: mediaAvailableLineResponseFactory.build(), - layout: MediaBoardLayoutType.List, + layout: BoardLayout.List, }); return { diff --git a/src/modules/feature/media-shelf/MediaBoardAvailableLine.vue b/src/modules/feature/media-shelf/MediaBoardAvailableLine.vue index a08ea4a3b3..82722344eb 100644 --- a/src/modules/feature/media-shelf/MediaBoardAvailableLine.vue +++ b/src/modules/feature/media-shelf/MediaBoardAvailableLine.vue @@ -65,10 +65,10 @@ diff --git a/src/modules/ui/kebab-menu/index.ts b/src/modules/ui/kebab-menu/index.ts index ac8db6fd2f..44a52c923a 100644 --- a/src/modules/ui/kebab-menu/index.ts +++ b/src/modules/ui/kebab-menu/index.ts @@ -1,5 +1,6 @@ import KebabMenu from "./KebabMenu.vue"; import KebabMenuAction from "./KebabMenuAction.vue"; +import KebabMenuActionChangeLayout from "./KebabMenuActionChangeLayout.vue"; import KebabMenuActionCopy from "./KebabMenuActionCopy.vue"; import KebabMenuActionDelete from "./KebabMenuActionDelete.vue"; import KebabMenuActionEdit from "./KebabMenuActionEdit.vue"; @@ -27,5 +28,6 @@ export { KebabMenuActionRevert, KebabMenuActionShare, KebabMenuActionRename, + KebabMenuActionChangeLayout, KebabMenuActionShareLink, }; diff --git a/src/modules/ui/room-details/SelectBoardLayoutDialog.unit.ts b/src/modules/ui/room-details/SelectBoardLayoutDialog.unit.ts index f1fc20a020..f92ff9a868 100644 --- a/src/modules/ui/room-details/SelectBoardLayoutDialog.unit.ts +++ b/src/modules/ui/room-details/SelectBoardLayoutDialog.unit.ts @@ -1,3 +1,4 @@ +import { BoardLayout } from "@/serverApi/v3"; import { createTestingI18n, createTestingVuetify, @@ -6,13 +7,14 @@ import { mount } from "@vue/test-utils"; import SelectBoardLayoutDialog from "./SelectBoardLayoutDialog.vue"; describe("@ui-room-details/SelectBoardLayoutDialog", () => { - const setup = () => { + const setup = (currentLayout?: BoardLayout) => { const wrapper = mount(SelectBoardLayoutDialog, { global: { plugins: [createTestingVuetify(), createTestingI18n()], }, props: { modelValue: true, + currentLayout, }, }); @@ -25,23 +27,52 @@ describe("@ui-room-details/SelectBoardLayoutDialog", () => { expect(wrapper.exists()).toBe(true); }); - it("should emit correct event on select", async () => { - const { wrapper } = setup(); + describe("when selecting multi column board", () => { + it("should emit correct event", async () => { + const { wrapper } = setup(); - const multiColumnButton = wrapper.findComponent( - "[data-testid=dialog-add-multi-column-board]" - ); - await multiColumnButton.trigger("click"); + const multiColumnButton = wrapper.findComponent( + "[data-testid=dialog-add-multi-column-board]" + ); + await multiColumnButton.trigger("click"); - expect(wrapper.emitted("select:multi-column")).toHaveLength(1); + expect(wrapper.emitted("select")).toEqual([[BoardLayout.Columns]]); + }); }); - it("should close dialog", async () => { - const { wrapper } = setup(); + describe("when selecting single column board", () => { + it("should emit correct event", async () => { + const { wrapper } = setup(); + + const multiColumnButton = wrapper.findComponent( + "[data-testid=dialog-add-single-column-board]" + ); + await multiColumnButton.trigger("click"); + + expect(wrapper.emitted("select")).toEqual([[BoardLayout.List]]); + }); + }); - const closeBtn = wrapper.findComponent("[data-testid=dialog-close]"); - await closeBtn.trigger("click"); + describe("when clicking the close button", () => { + it("should close the dialog", async () => { + const { wrapper } = setup(); - expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + const closeBtn = wrapper.findComponent("[data-testid=dialog-close]"); + await closeBtn.trigger("click"); + + expect(wrapper.emitted("update:modelValue")).toEqual([[false]]); + }); + }); + + describe("when a board layout is changed", () => { + it("should highlight the currently selected option", async () => { + const { wrapper } = setup(BoardLayout.Columns); + + const multiColumnButton = wrapper.findComponent( + "[data-testid=dialog-add-multi-column-board]" + ); + + expect(multiColumnButton.classes()).toContain("selected"); + }); }); }); diff --git a/src/modules/ui/room-details/SelectBoardLayoutDialog.vue b/src/modules/ui/room-details/SelectBoardLayoutDialog.vue index 0357e63671..258c3132b1 100644 --- a/src/modules/ui/room-details/SelectBoardLayoutDialog.vue +++ b/src/modules/ui/room-details/SelectBoardLayoutDialog.vue @@ -1,5 +1,5 @@