From de542865801bd233743bb7dd30218dff4e6b07fd Mon Sep 17 00:00:00 2001 From: Uwe Ilgenstein Date: Wed, 18 Sep 2024 09:55:33 +0200 Subject: [PATCH] BC-7985 - Adapt sidebar active selection to rooms changes (#3390) * implement selective sidebar selection for /rooms/:id * fix sidebar selection for board * move sidebar selection to composable * add/fix tests --- src/layouts/LoggedIn.unit.ts | 12 +- src/modules/data/board/BoardApi.composable.ts | 4 +- .../board/BoardPageInformation.composable.ts | 4 + .../BoardPageInformation.composable.unit.ts | 31 +- src/modules/data/room/RoomDetails.state.ts | 25 -- src/modules/data/room/RoomDetails.store.ts | 44 +++ src/modules/data/room/index.ts | 2 +- src/modules/feature/board/board/Board.unit.ts | 1 + src/modules/page/room/RoomDetails.page.vue | 18 +- src/modules/ui/layout/sidebar/Sidebar.unit.ts | 8 +- .../sidebar/SidebarCategoryItem.unit.ts | 7 + .../ui/layout/sidebar/SidebarItem.unit.ts | 20 +- src/modules/ui/layout/sidebar/SidebarItem.vue | 27 +- .../sidebar/SidebarSelection.composable.ts | 74 +++++ .../SidebarSelection.composable.unit.ts | 302 ++++++++++++++++++ src/pages/Home.page.vue | 1 + src/router/routes.ts | 5 + src/types/board/BoardContext.ts | 3 + 18 files changed, 513 insertions(+), 75 deletions(-) delete mode 100644 src/modules/data/room/RoomDetails.state.ts create mode 100644 src/modules/data/room/RoomDetails.store.ts create mode 100644 src/modules/ui/layout/sidebar/SidebarSelection.composable.ts create mode 100644 src/modules/ui/layout/sidebar/SidebarSelection.composable.unit.ts create mode 100644 src/pages/Home.page.vue create mode 100644 src/types/board/BoardContext.ts diff --git a/src/layouts/LoggedIn.unit.ts b/src/layouts/LoggedIn.unit.ts index 3b7e538584..d1168afeb6 100644 --- a/src/layouts/LoggedIn.unit.ts +++ b/src/layouts/LoggedIn.unit.ts @@ -19,6 +19,8 @@ import { h, nextTick } from "vue"; import { VApp } from "vuetify/lib/components/index.mjs"; import LoggedInLayout from "./LoggedIn.layout.vue"; import { Topbar } from "@ui-layout"; +import { createTestingPinia } from "@pinia/testing"; +import setupStores from "@@/tests/test-utils/setupStores"; jest.mock("vue-router", () => ({ useRoute: () => ({ path: "rooms/courses-list" }), @@ -43,12 +45,20 @@ const setup = () => { }, }); + setupStores({ + envConfigModule: EnvConfigModule, + }); + const wrapper = mount(VApp, { slots: { default: h(LoggedInLayout), }, global: { - plugins: [createTestingVuetify(), createTestingI18n()], + plugins: [ + createTestingVuetify(), + createTestingI18n(), + createTestingPinia(), + ], provide: { [AUTH_MODULE_KEY.valueOf()]: authModule, [ENV_CONFIG_MODULE_KEY.valueOf()]: envConfigModule, diff --git a/src/modules/data/board/BoardApi.composable.ts b/src/modules/data/board/BoardApi.composable.ts index e05522f8db..1f8493c72a 100644 --- a/src/modules/data/board/BoardApi.composable.ts +++ b/src/modules/data/board/BoardApi.composable.ts @@ -17,6 +17,7 @@ import { CourseRoomsApiFactory, SubmissionContainerElementContentBody, } from "@/serverApi/v3"; +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"; @@ -185,7 +186,7 @@ export const useBoardApi = () => { }); }; - type ContextInfo = { id: string; name: string }; + type ContextInfo = { id: string; type: BoardContextType; name: string }; const getContextInfo = async ( boardId: string @@ -205,6 +206,7 @@ export const useBoardApi = () => { } return { id: roomResponse.data.roomId, + type: context.type, name: roomResponse.data.title, }; }; diff --git a/src/modules/data/board/BoardPageInformation.composable.ts b/src/modules/data/board/BoardPageInformation.composable.ts index 5520dd25b0..4612de5f09 100644 --- a/src/modules/data/board/BoardPageInformation.composable.ts +++ b/src/modules/data/board/BoardPageInformation.composable.ts @@ -4,6 +4,7 @@ import { createSharedComposable } from "@vueuse/core"; import { ref, Ref } from "vue"; import { useI18n } from "vue-i18n"; import { useBoardApi } from "./BoardApi.composable"; +import { BoardContextType } from "@/types/board/BoardContext"; const useBoardPageInformation = () => { const { t } = useI18n(); @@ -20,6 +21,7 @@ const useBoardPageInformation = () => { const pageTitle: Ref = ref(getPageTitle()); const roomId: Ref = ref(undefined); const breadcrumbs: Ref = ref([]); + const contextType: Ref = ref(); function getBreadcrumbs( contextInfo: { id: string; name: string } | undefined @@ -44,12 +46,14 @@ const useBoardPageInformation = () => { const contextInfo = await getContextInfo(id); pageTitle.value = getPageTitle(contextInfo?.name); breadcrumbs.value = getBreadcrumbs(contextInfo); + contextType.value = contextInfo?.type; roomId.value = contextInfo?.id; }; return { createPageInformation, breadcrumbs, + contextType, pageTitle, roomId, }; diff --git a/src/modules/data/board/BoardPageInformation.composable.unit.ts b/src/modules/data/board/BoardPageInformation.composable.unit.ts index fd85ba2585..fdfe1001d7 100644 --- a/src/modules/data/board/BoardPageInformation.composable.unit.ts +++ b/src/modules/data/board/BoardPageInformation.composable.unit.ts @@ -2,6 +2,7 @@ import { mountComposable } from "@@/tests/test-utils/mountComposable"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { useBoardApi } from "./BoardApi.composable"; import { useSharedBoardPageInformation } from "./BoardPageInformation.composable"; +import { BoardContextType } from "@/types/board/BoardContext"; jest.mock("./BoardApi.composable"); const mockedUseBoardApi = jest.mocked(useBoardApi); @@ -36,13 +37,25 @@ describe("BoardPageInformation.composable", () => { const setup = () => { mockedBoardApiCalls.getContextInfo.mockResolvedValue({ id: "courseId", + type: BoardContextType.Course, name: "Course #1", }); - const { createPageInformation, breadcrumbs, pageTitle, roomId } = - mountComposable(() => useSharedBoardPageInformation()); - - return { createPageInformation, breadcrumbs, pageTitle, roomId }; + const { + createPageInformation, + breadcrumbs, + contextType, + pageTitle, + roomId, + } = mountComposable(() => useSharedBoardPageInformation()); + + return { + createPageInformation, + breadcrumbs, + contextType, + pageTitle, + roomId, + }; }; it("should return two breadcrumbs: 1. course page and and 2. course-overview page", async () => { @@ -74,6 +87,16 @@ describe("BoardPageInformation.composable", () => { expect(roomId.value).toEqual("courseId"); }); + + it("should set context type", async () => { + const { createPageInformation, contextType } = setup(); + + const fakeId = "abc123-2"; + + await createPageInformation(fakeId); + + expect(contextType.value).toEqual(BoardContextType.Course); + }); }); describe("when board context does not exist", () => { diff --git a/src/modules/data/room/RoomDetails.state.ts b/src/modules/data/room/RoomDetails.state.ts deleted file mode 100644 index 58ef05ca6c..0000000000 --- a/src/modules/data/room/RoomDetails.state.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Room } from "@/types/room/Room"; -import { delay } from "@/utils/helpers"; -import { ref } from "vue"; -import { roomsData } from "./rooms-mock-data"; - -export const useRoomDetailsState = () => { - const room = ref(); - const isLoading = ref(true); - const isRoom = ref(false); - - const fetchRoom = async (id: string) => { - await delay(100); - // TODO call API - room.value = roomsData.find((r) => r.id === id); - isRoom.value = room.value != null; - isLoading.value = false; - }; - - return { - fetchRoom, - isLoading, - isRoom, - room, - }; -}; diff --git a/src/modules/data/room/RoomDetails.store.ts b/src/modules/data/room/RoomDetails.store.ts new file mode 100644 index 0000000000..a0dd51c2ac --- /dev/null +++ b/src/modules/data/room/RoomDetails.store.ts @@ -0,0 +1,44 @@ +import { Room } from "@/types/room/Room"; +import { delay } from "@/utils/helpers"; +import { ref } from "vue"; +import { roomsData } from "./rooms-mock-data"; +import { defineStore } from "pinia"; + +export enum RoomVariant { + ROOM = "room", + COURSE_ROOM = "courseRoom", +} + +export const useRoomDetailsStore = defineStore("roomDetailsStore", () => { + const isLoading = ref(true); + const room = ref(); + const roomVariant = ref(); + + const fetchRoom = async (id: string) => { + await delay(100); + // TODO call API + room.value = roomsData.find((r) => r.id === id); + roomVariant.value = + room.value != null ? RoomVariant.ROOM : RoomVariant.COURSE_ROOM; + isLoading.value = false; + }; + + const resetState = () => { + isLoading.value = true; + room.value = undefined; + }; + + const deactivateRoom = () => { + resetState(); + isLoading.value = false; + }; + + return { + deactivateRoom, + fetchRoom, + isLoading, + resetState, + room, + roomVariant, + }; +}); diff --git a/src/modules/data/room/index.ts b/src/modules/data/room/index.ts index e86232bd40..c730814c25 100644 --- a/src/modules/data/room/index.ts +++ b/src/modules/data/room/index.ts @@ -1,5 +1,5 @@ export { useCourseApi } from "./courseApi.composable"; export { useRoomsState } from "./Rooms.state"; -export { useRoomDetailsState } from "./RoomDetails.state"; +export { useRoomDetailsStore, RoomVariant } from "./RoomDetails.store"; export { useCourseInfoApi } from "./courseInfoApi.composable"; export { useCourseList } from "./courseList.composable"; diff --git a/src/modules/feature/board/board/Board.unit.ts b/src/modules/feature/board/board/Board.unit.ts index 05ee7cd409..8a93c14a6c 100644 --- a/src/modules/feature/board/board/Board.unit.ts +++ b/src/modules/feature/board/board/Board.unit.ts @@ -129,6 +129,7 @@ describe("Board", () => { mockedUseSharedBoardPageInformation.mockReturnValue({ createPageInformation: jest.fn(), breadcrumbs: ref([]), + contextType: ref(), pageTitle: ref("page-title"), roomId: ref("room-id"), }); diff --git a/src/modules/page/room/RoomDetails.page.vue b/src/modules/page/room/RoomDetails.page.vue index b3e0ef13e8..da5e5abc25 100644 --- a/src/modules/page/room/RoomDetails.page.vue +++ b/src/modules/page/room/RoomDetails.page.vue @@ -13,15 +13,18 @@ diff --git a/src/modules/ui/layout/sidebar/Sidebar.unit.ts b/src/modules/ui/layout/sidebar/Sidebar.unit.ts index a2491bfde1..20d5533fd7 100644 --- a/src/modules/ui/layout/sidebar/Sidebar.unit.ts +++ b/src/modules/ui/layout/sidebar/Sidebar.unit.ts @@ -1,5 +1,5 @@ import { mount } from "@vue/test-utils"; -import { h, nextTick } from "vue"; +import { h, nextTick, ref } from "vue"; import { VApp } from "vuetify/lib/components/index.mjs"; import { createTestingI18n, @@ -18,11 +18,15 @@ import FilePathsModule from "@/store/filePaths"; import { createModuleMocks } from "@/utils/mock-store-module"; import { SchulcloudTheme } from "@/serverApi/v3"; import { envsFactory } from "@@/tests/test-utils"; +import { useSidebarSelection } from "./SidebarSelection.composable"; jest.mock("vue-router", () => ({ useRoute: () => ({ path: "rooms/courses-list" }), })); +jest.mock("./SidebarSelection.composable"); +const mockedUseSidebarSelection = jest.mocked(useSidebarSelection); + const setup = (permissions?: string[]) => { const authModule = createModuleMocks(AuthModule, { getUserPermissions: permissions, @@ -43,6 +47,8 @@ const setup = (permissions?: string[]) => { }, }); + mockedUseSidebarSelection.mockReturnValue({ isActive: ref(false) }); + const wrapper = mount(VApp, { global: { plugins: [createTestingVuetify(), createTestingI18n()], diff --git a/src/modules/ui/layout/sidebar/SidebarCategoryItem.unit.ts b/src/modules/ui/layout/sidebar/SidebarCategoryItem.unit.ts index 2527086313..4b1a036fc7 100644 --- a/src/modules/ui/layout/sidebar/SidebarCategoryItem.unit.ts +++ b/src/modules/ui/layout/sidebar/SidebarCategoryItem.unit.ts @@ -5,6 +5,8 @@ import { } from "@@/tests/test-utils/setup"; import SidebarCategoryItem from "./SidebarCategoryItem.vue"; import { SidebarGroupItem } from "../types"; +import { useSidebarSelection } from "./SidebarSelection.composable"; +import { ref } from "vue"; const groupItem: SidebarGroupItem = { icon: "mdiOpen", @@ -33,7 +35,12 @@ jest.mock("vue-router", () => ({ useRoute: () => ({ path: "rooms/courses-list" }), })); +jest.mock("./SidebarSelection.composable"); +const mockedUseSidebarSelection = jest.mocked(useSidebarSelection); + describe("@ui-layout/SidebarCategoryItem", () => { + mockedUseSidebarSelection.mockReturnValue({ isActive: ref(false) }); + const setup = (sidebarItem: SidebarGroupItem) => { const wrapper = mount(SidebarCategoryItem, { global: { diff --git a/src/modules/ui/layout/sidebar/SidebarItem.unit.ts b/src/modules/ui/layout/sidebar/SidebarItem.unit.ts index 214735df5d..1424ecb846 100644 --- a/src/modules/ui/layout/sidebar/SidebarItem.unit.ts +++ b/src/modules/ui/layout/sidebar/SidebarItem.unit.ts @@ -5,6 +5,8 @@ import { } from "@@/tests/test-utils/setup"; import SidebarItem from "./SidebarItem.vue"; import { SidebarSingleItem } from "../types"; +import { ref } from "vue"; +import { useSidebarSelection } from "./SidebarSelection.composable"; const iconItem: SidebarSingleItem = { icon: "mdiOpen", @@ -23,8 +25,13 @@ jest.mock("vue-router", () => ({ useRoute: () => ({ path: "rooms/courses-list" }), })); +jest.mock("./SidebarSelection.composable"); +const mockedUseSidebarSelection = jest.mocked(useSidebarSelection); + describe("@ui-layout/SidebarItem", () => { const setup = (sidebarItem: SidebarSingleItem) => { + mockedUseSidebarSelection.mockReturnValue({ isActive: ref(true) }); + const wrapper = mount(SidebarItem, { global: { plugins: [createTestingVuetify(), createTestingI18n()], @@ -51,7 +58,7 @@ describe("@ui-layout/SidebarItem", () => { expect(wrapper.findComponent(".v-icon").exists()).toBe(false); }); - it("should highlight correct sidebar item", () => { + it("should highlight item when selection is active", () => { const { wrapper } = setup({ icon: "mdiOpen", title: "title", @@ -61,15 +68,4 @@ describe("@ui-layout/SidebarItem", () => { expect(wrapper.classes()).toContain("v-list-item--active"); }); - - it("should not highlight wrong sidebar item", () => { - const { wrapper } = setup({ - icon: "mdiOpen", - title: "title", - testId: "testId", - to: "/administration/rooms/new", - }); - - expect(wrapper.classes()).not.toContain("v-list-item--active"); - }); }); diff --git a/src/modules/ui/layout/sidebar/SidebarItem.vue b/src/modules/ui/layout/sidebar/SidebarItem.vue index 6ac94950bb..b18df650c8 100644 --- a/src/modules/ui/layout/sidebar/SidebarItem.vue +++ b/src/modules/ui/layout/sidebar/SidebarItem.vue @@ -22,7 +22,7 @@