From 5328ecb259b7825f9966a4191a61b5cc67436882 Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:42:15 +0100 Subject: [PATCH 01/12] BC-8426 - Improve A11y manage members page (#3455) BC-8426 - Improve A11y manage members page --------- Co-authored-by: Murat Merdoglu <64781656+muratmerdoglu-dp@users.noreply.github.com> --- src/components/templates/DefaultWireframe.vue | 2 +- src/locales/de.ts | 9 +++++---- src/locales/en.ts | 9 +++++---- src/locales/es.ts | 9 +++++---- src/locales/uk.ts | 9 +++++---- .../feature/room/RoomMembers/MembersTable.vue | 9 +++++++-- src/modules/page/room/RoomMembers.page.vue | 16 ++++++++++++++-- 7 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/components/templates/DefaultWireframe.vue b/src/components/templates/DefaultWireframe.vue index b83fc8133b..acdf83e2a8 100644 --- a/src/components/templates/DefaultWireframe.vue +++ b/src/components/templates/DefaultWireframe.vue @@ -52,7 +52,7 @@ - + weitere Informationen).', + "Füge Teilnehmende zum Raum hinzu. Lehrkräfte anderer Schulen können hinzugefügt werden, wenn sie in ihrem Profil die Sichtbarkeit im zentralen Verzeichnis aktiviert haben ({0}).", + "pages.rooms.members.infoText.moreInformation": "weitere Informationen", "pages.rooms.members.label": "Teilnehmende", "pages.rooms.members.add": "Teilnehmende hinzufügen", "pages.rooms.members.manage": "Teilnehmende verwalten", - "pages.rooms.members.remove": "Teilnehmende entfernen", + "pages.rooms.members.remove.ariaLabel": "{memberName} aus Raum entfernen", + "pages.rooms.members.remove.confirmation": + "{memberName} wirklich aus dem Raum entfernen?", "pages.rooms.members.roles.editor": "Raumeditor", "pages.rooms.members.roles.viewer": "Raumbetrachter", "pages.rooms.title": "Räume", diff --git a/src/locales/en.ts b/src/locales/en.ts index c9ff9c665d..acb5109fa7 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1644,14 +1644,15 @@ export default { "pages.rooms.members.error.load": "The participant list could not be loaded.", "pages.rooms.members.error.add": "Adding participants failed.", "pages.rooms.members.error.remove": "Deleting participants failed.", - "pages.rooms.members.remove.confirmation": - "Remove {memberName} from the room?", "pages.rooms.members.infoText": - 'Add participants to the room. Teachers from other schools can be added if they have activated visibility in the central directory in their profile (more information).', + "Add participants to the room. Teachers from other schools can be added if they have activated visibility in the central directory in their profile ({0}).", + "pages.rooms.members.infoText.moreInformation": "more information", "pages.rooms.members.label": "Participants", "pages.rooms.members.add": "Add participants", "pages.rooms.members.manage": "Manage participants", - "pages.rooms.members.remove": "Remove participants", + "pages.rooms.members.remove.ariaLabel": "Remove {memberName} from the room", + "pages.rooms.members.remove.confirmation": + "Remove {memberName} from the room?", "pages.rooms.members.roles.editor": "Room editor", "pages.rooms.members.roles.viewer": "Room viewer", "pages.rooms.title": "Rooms", diff --git a/src/locales/es.ts b/src/locales/es.ts index 500ea9467c..5b13192e3d 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1689,14 +1689,15 @@ export default { "No se pudo cargar la lista de participantes.", "pages.rooms.members.error.add": "Error al agregar participantes.", "pages.rooms.members.error.remove": "Error al eliminar participantes.", - "pages.rooms.members.remove.confirmation": - "¿Eliminar {memberName} de la sala?", "pages.rooms.members.infoText": - 'Añadir participantes a la sala. Se pueden añadir profesores de otros centros si tienen activada la visibilidad en el directorio central en su perfil (más información).', + "Añadir participantes a la sala. Se pueden añadir profesores de otros centros si tienen activada la visibilidad en el directorio central en su perfil ({0}).", + "pages.rooms.members.infoText.moreInformation": "más información", "pages.rooms.members.label": "Participantes", "pages.rooms.members.add": "Añadir participantes", "pages.rooms.members.manage": "Administrar participantes", - "pages.rooms.members.remove": "Eliminar participante", + "pages.rooms.members.remove.ariaLabel": "Eliminar {memberName} de la sala", + "pages.rooms.members.remove.confirmation": + "¿Eliminar {memberName} de la sala?", "pages.rooms.members.roles.editor": "Editor de salas", "pages.rooms.members.roles.viewer": "Visor de salas", "pages.rooms.title": "Salas", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 7dc8d81768..767de5f144 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -1666,14 +1666,15 @@ export default { "pages.rooms.members.error.load": "Не вдалося завантажити список учасників.", "pages.rooms.members.error.add": "Не вдалося додати учасників.", "pages.rooms.members.error.remove": "Не вдалося видалити учасників.", - "pages.rooms.members.remove.confirmation": - "{memberName} буде видалено з цієї кімнати. Ви впевнені, що хочете видалити?", "pages.rooms.members.infoText": - 'Додайте учасників до кімнати. Вчителі з інших шкіл можуть бути додані, якщо вони активували видимість у центральному каталозі у своєму профілі (більше інформації).', + "Додайте учасників до кімнати. Вчителі з інших шкіл можуть бути додані, якщо вони активували видимість у центральному каталозі у своєму профілі ({0}).", + "pages.rooms.members.infoText.moreInformation": "більше інформації", "pages.rooms.members.label": "Учасники", "pages.rooms.members.add": "Додайте учасників", "pages.rooms.members.manage": "Керувати учасниками", - "pages.rooms.members.remove": "Видалити учасника", + "pages.rooms.members.remove.ariaLabel": "Видалити {memberName} з кімнати", + "pages.rooms.members.remove.confirmation": + "{memberName} буде видалено з цієї кімнати. Ви впевнені, що хочете видалити?", "pages.rooms.members.roles.editor": "Редактор кімнати", "pages.rooms.members.roles.viewer": "Переглядач кімнати", "pages.rooms.title": "Кімнати", diff --git a/src/modules/feature/room/RoomMembers/MembersTable.vue b/src/modules/feature/room/RoomMembers/MembersTable.vue index c2e287e19e..b27873d63d 100644 --- a/src/modules/feature/room/RoomMembers/MembersTable.vue +++ b/src/modules/feature/room/RoomMembers/MembersTable.vue @@ -16,7 +16,7 @@ /> - + @@ -101,6 +101,11 @@ const tableHeader = [ { title: t("common.words.mainSchool"), key: "schoolName" }, { title: "", key: "actions", sortable: false, width: 50 }, ]; + +const getRemoveAriaLabel = (member: RoomMemberResponse) => + t("pages.rooms.members.remove.ariaLabel", { + memberName: `${member.firstName} ${member.lastName}`, + }); diff --git a/src/modules/page/room/RoomMembers.page.unit.ts b/src/modules/page/room/RoomMembers.page.unit.ts index 662418d1bb..e15edd5153 100644 --- a/src/modules/page/room/RoomMembers.page.unit.ts +++ b/src/modules/page/room/RoomMembers.page.unit.ts @@ -15,14 +15,14 @@ import { Router, useRoute, useRouter } from "vue-router"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import EnvConfigModule from "@/store/env-config"; import setupStores from "@@/tests/test-utils/setupStores"; -import { nextTick, ref } from "vue"; +import { nextTick, Ref, ref } from "vue"; import { Breadcrumb } from "@/components/templates/default-wireframe.types"; import { flushPromises } from "@vue/test-utils"; import { RoleName, RoomColor } from "@/serverApi/v3"; import { useConfirmationDialog } from "@ui-confirmation-dialog"; -import setupDeleteConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock"; import { useTitle } from "@vueuse/core"; import vueDompurifyHTMLPlugin from "vue-dompurify-html"; +import setupConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupConfirmationComposableMock"; jest.mock("vue-router"); const useRouterMock = useRouter; @@ -64,6 +64,9 @@ describe("RoomMembersPage", () => { let router: DeepMocked; let route: DeepMocked>; let mockUseMembersCalls: DeepMocked>; + const askConfirmationMock = jest + .fn() + .mockImplementation(async () => await Promise.resolve(true)); beforeEach(() => { route = createMock>(); @@ -85,12 +88,11 @@ describe("RoomMembersPage", () => { potentialRoomMembers: ref(mockPotentialMembers), }); - const askDeleteConfirmationMock = async () => await Promise.resolve(true); - setupDeleteConfirmationComposableMock({ - askDeleteConfirmationMock, + setupConfirmationComposableMock({ + askConfirmationMock, }); mockedUseDeleteConfirmationDialog.mockReturnValue({ - askConfirmation: askDeleteConfirmationMock, + askConfirmation: askConfirmationMock, isDialogOpen: ref(false), }); @@ -131,6 +133,7 @@ describe("RoomMembersPage", () => { testId: string; }; isMembersDialogOpen: boolean; + selectedMembers: Ref; onFabClick: ReturnType; }; @@ -209,16 +212,49 @@ describe("RoomMembersPage", () => { }); describe("@onRemoveMember", () => { - it("should call deleteMember method", async () => { - const { wrapper } = setup(); - const membersTable = wrapper.findComponent({ - name: "MembersTable", + describe("when user confirms the removal", () => { + it("should call removeMember method", async () => { + const { wrapper } = setup(); + const membersTable = wrapper.findComponent({ + name: "MembersTable", + }); + await membersTable.vm.$emit("select:members", [ + mockMembers[0].userId, + ]); + await membersTable.vm.$emit("remove:members"); + await flushPromises(); + + expect(mockUseMembersCalls.removeMembers).toHaveBeenCalledWith([ + mockMembers[0].userId, + ]); + expect(askConfirmationMock).toHaveBeenCalledWith({ + confirmActionLangKey: "common.actions.remove", + message: "pages.rooms.members.remove.confirmation", + }); + }); + }); + + describe("when user confirms multiple removal ", () => { + it("should call removeMember method", async () => { + const { wrapper } = setup(); + const membersTable = wrapper.findComponent({ + name: "MembersTable", + }); + await membersTable.vm.$emit("select:members", [ + mockMembers[0].userId, + mockMembers[1].userId, + ]); + await membersTable.vm.$emit("remove:members"); + await flushPromises(); + expect(mockUseMembersCalls.removeMembers).toHaveBeenCalledWith([ + mockMembers[0].userId, + mockMembers[1].userId, + ]); + expect(askConfirmationMock).toHaveBeenCalledWith({ + confirmActionLangKey: "common.actions.remove", + message: "pages.rooms.members.multipleRemove.confirmation", + }); }); - await membersTable.vm.$emit("remove:member", mockMembers[0]); - await flushPromises(); - expect(mockUseMembersCalls.removeMembers).toHaveBeenCalledWith([ - mockMembers[0].userId, - ]); }); }); }); diff --git a/src/modules/page/room/RoomMembers.page.vue b/src/modules/page/room/RoomMembers.page.vue index df6fc3ae20..5e149ee83a 100644 --- a/src/modules/page/room/RoomMembers.page.vue +++ b/src/modules/page/room/RoomMembers.page.vue @@ -18,24 +18,28 @@ target="_blank" rel="noopener" :ariaLabel="linkAriaLabel" - >{{ t("pages.rooms.members.infoText.moreInformation") }} + {{ t("pages.rooms.members.infoText.moreInformation") }} + +
- + diff --git a/src/components/share/SelectCourseModal.unit.ts b/src/components/share/SelectDestinationModal.unit.ts similarity index 89% rename from src/components/share/SelectCourseModal.unit.ts rename to src/components/share/SelectDestinationModal.unit.ts index a4ffe00ad4..fb36ce9a4a 100644 --- a/src/components/share/SelectCourseModal.unit.ts +++ b/src/components/share/SelectDestinationModal.unit.ts @@ -1,11 +1,11 @@ import { mount } from "@vue/test-utils"; -import SelectCourseModal from "@/components/share/SelectCourseModal.vue"; +import SelectDestinationModal from "@/components/share/SelectDestinationModal.vue"; import { createTestingI18n, createTestingVuetify, } from "@@/tests/test-utils/setup"; -describe("@components/share/SelectCourseModal", () => { +describe("@components/share/SelectDestinationModal", () => { const course = { id: "1234", title: "Mathe", @@ -20,7 +20,7 @@ describe("@components/share/SelectCourseModal", () => { }; const setup = () => { - const wrapper = mount(SelectCourseModal, { + const wrapper = mount(SelectDestinationModal, { global: { plugins: [createTestingVuetify(), createTestingI18n()], }, diff --git a/src/components/share/SelectCourseModal.vue b/src/components/share/SelectDestinationModal.vue similarity index 50% rename from src/components/share/SelectCourseModal.vue rename to src/components/share/SelectDestinationModal.vue index f447e51327..b2cb6c7f8a 100644 --- a/src/components/share/SelectCourseModal.vue +++ b/src/components/share/SelectDestinationModal.vue @@ -21,25 +21,19 @@ {{ mdiInformation }}
- {{ - t( - `components.molecules.import.${parentType}.options.selectCourse.infoText` - ) - }} + {{ infoText }}
@@ -50,32 +44,64 @@ diff --git a/src/modules/ui/layout/sidebar/SidebarItems.composable.ts b/src/modules/ui/layout/sidebar/SidebarItems.composable.ts index 6d96fafab2..1311bfb7b3 100644 --- a/src/modules/ui/layout/sidebar/SidebarItems.composable.ts +++ b/src/modules/ui/layout/sidebar/SidebarItems.composable.ts @@ -40,6 +40,7 @@ export const useSidebarItems = () => { to: "/rooms", icon: mdiAccountSupervisorCircleOutline, feature: "FEATURE_ROOMS_ENABLED", + permissions: ["ROOM_CREATE"], testId: "Räume", }, { diff --git a/src/modules/ui/layout/sidebar/SidebarSelection.composable.ts b/src/modules/ui/layout/sidebar/SidebarSelection.composable.ts index 4e235e8c9e..500d81a620 100644 --- a/src/modules/ui/layout/sidebar/SidebarSelection.composable.ts +++ b/src/modules/ui/layout/sidebar/SidebarSelection.composable.ts @@ -53,8 +53,10 @@ export const useSidebarSelection = ( // Board if (route.name === "boards-id") { return ( - item.to === "/rooms/courses-overview" && - contextType.value === BoardExternalReferenceType.Course + (item.to === "/rooms/courses-overview" && + contextType.value === BoardExternalReferenceType.Course) || + (item.to === "/rooms" && + contextType.value === BoardExternalReferenceType.Room) ); } diff --git a/src/modules/ui/layout/sidebar/SidebarSelection.composable.unit.ts b/src/modules/ui/layout/sidebar/SidebarSelection.composable.unit.ts index bce6e8b3f0..0da961beb1 100644 --- a/src/modules/ui/layout/sidebar/SidebarSelection.composable.unit.ts +++ b/src/modules/ui/layout/sidebar/SidebarSelection.composable.unit.ts @@ -33,6 +33,7 @@ describe("@ui/layout/sidebar/SidebarSelection.composable", () => { contextType: computed(() => undefined), pageTitle: computed(() => "page-title"), roomId: computed(() => "room-id"), + resetPageInformation: jest.fn(), }); }); @@ -264,6 +265,7 @@ describe("@ui/layout/sidebar/SidebarSelection.composable", () => { contextType: computed(() => BoardContextType.Course), pageTitle: computed(() => "page-title"), roomId: computed(() => "room-id"), + resetPageInformation: jest.fn(), }); return setupBoardDetailsRoute(); @@ -285,6 +287,7 @@ describe("@ui/layout/sidebar/SidebarSelection.composable", () => { contextType: computed(() => BoardContextType.User), pageTitle: computed(() => "page-title"), roomId: computed(() => "room-id"), + resetPageInformation: jest.fn(), }); return setupBoardDetailsRoute(); diff --git a/src/pages/course-rooms/CourseRoomOverview.page.unit.js b/src/pages/course-rooms/CourseRoomOverview.page.unit.js index 6572074799..b5a6fe1065 100644 --- a/src/pages/course-rooms/CourseRoomOverview.page.unit.js +++ b/src/pages/course-rooms/CourseRoomOverview.page.unit.js @@ -200,12 +200,7 @@ describe("@/pages/CourseRoomOverview.page", () => { const expected = [ { id: "1234", - isArchived: undefined, - searchText: "Mathe", - title: "Mathe", - shortTitle: "Ma", - displayColor: "#54616e", - to: "/rooms/1234", + name: "Mathe", }, ]; diff --git a/src/pages/course-rooms/CourseRoomOverview.page.vue b/src/pages/course-rooms/CourseRoomOverview.page.vue index ee6f16e6c3..41c734e2d2 100644 --- a/src/pages/course-rooms/CourseRoomOverview.page.vue +++ b/src/pages/course-rooms/CourseRoomOverview.page.vue @@ -117,7 +117,8 @@ @@ -211,7 +212,12 @@ export default defineComponent({ }); }, courses() { - return courseRoomListModule.getAllElements; + return courseRoomListModule.getAllElements.map((item) => { + return { + id: item.id, + name: item.title, + }; + }); }, hasRoomsBeingCopied() { return this.rooms.some((item) => item.copyingSince !== undefined); diff --git a/src/router/routes.ts b/src/router/routes.ts index c4d0f49643..7c8474a20c 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -245,30 +245,46 @@ export const routes: Readonly[] = [ { path: `/rooms`, component: async () => (await import("@page-room")).RoomsPage, - beforeEnter: checkRoomsFeature, + beforeEnter: Multiguard([ + checkRoomsFeature, + createPermissionGuard(["room_create"]), + ]), name: "rooms", }, { path: `/rooms/new`, component: async () => (await import("@page-room")).RoomCreatePage, - beforeEnter: checkRoomsFeature, + beforeEnter: Multiguard([ + checkRoomsFeature, + createPermissionGuard(["room_create"]), + ]), name: "rooms-new", }, { - path: `/rooms/:id`, + path: `/rooms/:id(${REGEX_ID})`, component: async () => (await import("@page-room")).RoomDetailsPage, - name: "room-details", + beforeEnter: Multiguard([ + checkRoomsFeature, + createPermissionGuard(["room_create"]), + ]), + name: "rooms-id", }, { path: `/rooms/:id/edit`, component: async () => (await import("@page-room")).RoomEditPage, - beforeEnter: checkRoomsFeature, + beforeEnter: Multiguard([ + checkRoomsFeature, + createPermissionGuard(["room_create"]), + ]), name: "room-edit", }, { path: `/rooms/:id/members`, component: async () => (await import("@page-room")).RoomMembersPage, - beforeEnter: checkRoomsFeature, + beforeEnter: Multiguard([ + checkRoomsFeature, + createPermissionGuard(["room_create"]), + ]), name: "room-members", }, // TODO BC-7877 This redirect should be removed. Currently this route is used by the legacy client (and dof_app_deploy). @@ -293,11 +309,6 @@ export const routes: Readonly[] = [ component: () => import("@/pages/course-rooms/CourseRoomOverview.page.vue"), name: "course-room-overview", }, - { - path: `/rooms/:id(${REGEX_ID})`, - component: async () => (await import("@page-room")).RoomDetailsPage, - name: "rooms-id", - }, { path: `/rooms/:id(${REGEX_ID})/board`, redirect: { name: "boards-id" }, diff --git a/src/serverApi/v3/api.ts b/src/serverApi/v3/api.ts index f05cbabad7..4366d4fe13 100644 --- a/src/serverApi/v3/api.ts +++ b/src/serverApi/v3/api.ts @@ -357,8 +357,10 @@ export enum AuthorizationContextParamsRequiredPermissionsEnum { RoleCreate = 'ROLE_CREATE', RoleEdit = 'ROLE_EDIT', RoleView = 'ROLE_VIEW', + RoomCreate = 'ROOM_CREATE', RoomEdit = 'ROOM_EDIT', RoomView = 'ROOM_VIEW', + RoomDelete = 'ROOM_DELETE', SchoolChatManage = 'SCHOOL_CHAT_MANAGE', SchoolCreate = 'SCHOOL_CREATE', SchoolEdit = 'SCHOOL_EDIT', @@ -2064,11 +2066,11 @@ export interface CopyApiResponse { */ type: CopyApiResponseTypeEnum; /** - * Id of destination course + * Id of destination parent reference * @type {string} * @memberof CopyApiResponse */ - destinationCourseId?: string; + destinationId?: string; /** * Copy progress status of copied element * @type {string} @@ -6774,6 +6776,180 @@ export interface PeriodResponse { */ until: string; } +/** + * + * @export + * @enum {string} + */ +export enum Permission { + AccountCreate = 'ACCOUNT_CREATE', + AccountDelete = 'ACCOUNT_DELETE', + AccountEdit = 'ACCOUNT_EDIT', + AccountView = 'ACCOUNT_VIEW', + AddSchoolMembers = 'ADD_SCHOOL_MEMBERS', + AdminEdit = 'ADMIN_EDIT', + AdminView = 'ADMIN_VIEW', + BaseView = 'BASE_VIEW', + CalendarCreate = 'CALENDAR_CREATE', + CalendarEdit = 'CALENDAR_EDIT', + CalendarView = 'CALENDAR_VIEW', + ChangeTeamRoles = 'CHANGE_TEAM_ROLES', + ClassCreate = 'CLASS_CREATE', + ClassEdit = 'CLASS_EDIT', + ClassFullAdmin = 'CLASS_FULL_ADMIN', + ClassList = 'CLASS_LIST', + ClassRemove = 'CLASS_REMOVE', + ClassView = 'CLASS_VIEW', + CommentsCreate = 'COMMENTS_CREATE', + CommentsEdit = 'COMMENTS_EDIT', + CommentsView = 'COMMENTS_VIEW', + ContentNonOerView = 'CONTENT_NON_OER_VIEW', + ContentView = 'CONTENT_VIEW', + ContextToolAdmin = 'CONTEXT_TOOL_ADMIN', + ContextToolUser = 'CONTEXT_TOOL_USER', + CoursegroupCreate = 'COURSEGROUP_CREATE', + CoursegroupEdit = 'COURSEGROUP_EDIT', + CourseAdministration = 'COURSE_ADMINISTRATION', + CourseCreate = 'COURSE_CREATE', + CourseDelete = 'COURSE_DELETE', + CourseEdit = 'COURSE_EDIT', + CourseRemove = 'COURSE_REMOVE', + CourseView = 'COURSE_VIEW', + CreateSupportJwt = 'CREATE_SUPPORT_JWT', + CreateTopicsAndTasks = 'CREATE_TOPICS_AND_TASKS', + DashboardView = 'DASHBOARD_VIEW', + DatasourcesCreate = 'DATASOURCES_CREATE', + DatasourcesDelete = 'DATASOURCES_DELETE', + DatasourcesEdit = 'DATASOURCES_EDIT', + DatasourcesRun = 'DATASOURCES_RUN', + DatasourcesRunView = 'DATASOURCES_RUN_VIEW', + DatasourcesView = 'DATASOURCES_VIEW', + DefaultFilePermissions = 'DEFAULT_FILE_PERMISSIONS', + DeleteTeam = 'DELETE_TEAM', + EditAllFiles = 'EDIT_ALL_FILES', + EnterthecloudStart = 'ENTERTHECLOUD_START', + FederalstateCreate = 'FEDERALSTATE_CREATE', + FederalstateEdit = 'FEDERALSTATE_EDIT', + FederalstateView = 'FEDERALSTATE_VIEW', + FilestorageCreate = 'FILESTORAGE_CREATE', + FilestorageEdit = 'FILESTORAGE_EDIT', + FilestorageRemove = 'FILESTORAGE_REMOVE', + FilestorageView = 'FILESTORAGE_VIEW', + FileCreate = 'FILE_CREATE', + FileDelete = 'FILE_DELETE', + FileMove = 'FILE_MOVE', + FolderCreate = 'FOLDER_CREATE', + FolderDelete = 'FOLDER_DELETE', + GroupList = 'GROUP_LIST', + GroupFullAdmin = 'GROUP_FULL_ADMIN', + GroupView = 'GROUP_VIEW', + HelpdeskCreate = 'HELPDESK_CREATE', + HelpdeskEdit = 'HELPDESK_EDIT', + HelpdeskView = 'HELPDESK_VIEW', + HomeworkCreate = 'HOMEWORK_CREATE', + HomeworkEdit = 'HOMEWORK_EDIT', + HomeworkView = 'HOMEWORK_VIEW', + ImportUserMigrate = 'IMPORT_USER_MIGRATE', + ImportUserUpdate = 'IMPORT_USER_UPDATE', + ImportUserView = 'IMPORT_USER_VIEW', + InstanceView = 'INSTANCE_VIEW', + InviteAdministrators = 'INVITE_ADMINISTRATORS', + InviteExperts = 'INVITE_EXPERTS', + JoinMeeting = 'JOIN_MEETING', + LeaveTeam = 'LEAVE_TEAM', + LernstoreView = 'LERNSTORE_VIEW', + LessonsCreate = 'LESSONS_CREATE', + LessonsView = 'LESSONS_VIEW', + LinkCreate = 'LINK_CREATE', + NewsCreate = 'NEWS_CREATE', + NewsEdit = 'NEWS_EDIT', + NewsView = 'NEWS_VIEW', + NextcloudUser = 'NEXTCLOUD_USER', + NotificationCreate = 'NOTIFICATION_CREATE', + NotificationEdit = 'NOTIFICATION_EDIT', + NotificationView = 'NOTIFICATION_VIEW', + OauthClientEdit = 'OAUTH_CLIENT_EDIT', + OauthClientView = 'OAUTH_CLIENT_VIEW', + PasswordEdit = 'PASSWORD_EDIT', + PwrecoveryCreate = 'PWRECOVERY_CREATE', + PwrecoveryEdit = 'PWRECOVERY_EDIT', + PwrecoveryView = 'PWRECOVERY_VIEW', + ReleasesCreate = 'RELEASES_CREATE', + ReleasesEdit = 'RELEASES_EDIT', + ReleasesView = 'RELEASES_VIEW', + RemoveMembers = 'REMOVE_MEMBERS', + RenameTeam = 'RENAME_TEAM', + RequestConsents = 'REQUEST_CONSENTS', + RoleCreate = 'ROLE_CREATE', + RoleEdit = 'ROLE_EDIT', + RoleView = 'ROLE_VIEW', + RoomCreate = 'ROOM_CREATE', + RoomEdit = 'ROOM_EDIT', + RoomView = 'ROOM_VIEW', + RoomDelete = 'ROOM_DELETE', + SchoolChatManage = 'SCHOOL_CHAT_MANAGE', + SchoolCreate = 'SCHOOL_CREATE', + SchoolEdit = 'SCHOOL_EDIT', + SchoolEditAll = 'SCHOOL_EDIT_ALL', + SchoolLogoManage = 'SCHOOL_LOGO_MANAGE', + SchoolNewsEdit = 'SCHOOL_NEWS_EDIT', + SchoolPermissionChange = 'SCHOOL_PERMISSION_CHANGE', + SchoolPermissionView = 'SCHOOL_PERMISSION_VIEW', + SchoolStudentTeamManage = 'SCHOOL_STUDENT_TEAM_MANAGE', + SchoolSystemEdit = 'SCHOOL_SYSTEM_EDIT', + SchoolSystemView = 'SCHOOL_SYSTEM_VIEW', + SchoolToolAdmin = 'SCHOOL_TOOL_ADMIN', + ScopePermissionsView = 'SCOPE_PERMISSIONS_VIEW', + StartMeeting = 'START_MEETING', + StudentCreate = 'STUDENT_CREATE', + StudentDelete = 'STUDENT_DELETE', + StudentEdit = 'STUDENT_EDIT', + StudentList = 'STUDENT_LIST', + StudentSkipRegistration = 'STUDENT_SKIP_REGISTRATION', + SubmissionsCreate = 'SUBMISSIONS_CREATE', + SubmissionsEdit = 'SUBMISSIONS_EDIT', + SubmissionsSchoolView = 'SUBMISSIONS_SCHOOL_VIEW', + SubmissionsView = 'SUBMISSIONS_VIEW', + SyncStart = 'SYNC_START', + SystemCreate = 'SYSTEM_CREATE', + SystemEdit = 'SYSTEM_EDIT', + SystemView = 'SYSTEM_VIEW', + TaskDashboardTeacherViewV3 = 'TASK_DASHBOARD_TEACHER_VIEW_V3', + TaskDashboardViewV3 = 'TASK_DASHBOARD_VIEW_V3', + TeacherCreate = 'TEACHER_CREATE', + TeacherDelete = 'TEACHER_DELETE', + TeacherEdit = 'TEACHER_EDIT', + TeacherList = 'TEACHER_LIST', + TeacherSkipRegistration = 'TEACHER_SKIP_REGISTRATION', + TeamCreate = 'TEAM_CREATE', + ToolCreateEtherpad = 'TOOL_CREATE_ETHERPAD', + TeamEdit = 'TEAM_EDIT', + TeamInviteExternal = 'TEAM_INVITE_EXTERNAL', + TeamView = 'TEAM_VIEW', + ToolAdmin = 'TOOL_ADMIN', + ToolCreate = 'TOOL_CREATE', + ToolEdit = 'TOOL_EDIT', + ToolNewView = 'TOOL_NEW_VIEW', + ToolView = 'TOOL_VIEW', + TopicCreate = 'TOPIC_CREATE', + TopicEdit = 'TOPIC_EDIT', + TopicView = 'TOPIC_VIEW', + UploadFiles = 'UPLOAD_FILES', + UseLibreoffice = 'USE_LIBREOFFICE', + UseRocketchat = 'USE_ROCKETCHAT', + UsergroupCreate = 'USERGROUP_CREATE', + UsergroupEdit = 'USERGROUP_EDIT', + UsergroupView = 'USERGROUP_VIEW', + UserChangeOwnName = 'USER_CHANGE_OWN_NAME', + UserCreate = 'USER_CREATE', + UserLoginMigrationAdmin = 'USER_LOGIN_MIGRATION_ADMIN', + UserLoginMigrationRollback = 'USER_LOGIN_MIGRATION_ROLLBACK', + UserLoginMigrationForce = 'USER_LOGIN_MIGRATION_FORCE', + UserMigrate = 'USER_MIGRATE', + UserUpdate = 'USER_UPDATE', + YearsEdit = 'YEARS_EDIT' +} + /** * * @export @@ -7128,9 +7304,11 @@ export enum RoleName { DemoStudent = 'demoStudent', DemoTeacher = 'demoTeacher', Expert = 'expert', + GuestTeacher = 'guestTeacher', + GuestStudent = 'guestStudent', Helpdesk = 'helpdesk', - RoomViewer = 'room_viewer', - RoomEditor = 'room_editor', + Roomviewer = 'roomviewer', + Roomeditor = 'roomeditor', Student = 'student', Superhero = 'superhero', Teacher = 'teacher', @@ -7260,6 +7438,12 @@ export interface RoomDetailsResponse { * @memberof RoomDetailsResponse */ color: RoomColor; + /** + * + * @type {string} + * @memberof RoomDetailsResponse + */ + schoolId: string; /** * * @type {string} @@ -7284,6 +7468,12 @@ export interface RoomDetailsResponse { * @memberof RoomDetailsResponse */ updatedAt: string; + /** + * + * @type {Array} + * @memberof RoomDetailsResponse + */ + permissions: Array; } /** * @@ -7309,6 +7499,12 @@ export interface RoomItemResponse { * @memberof RoomItemResponse */ color: RoomColor; + /** + * + * @type {string} + * @memberof RoomItemResponse + */ + schoolId: string; /** * * @type {string} @@ -8227,11 +8423,11 @@ export interface ShareTokenImportBodyParams { */ newName: string; /** - * Id of the course to which the lesson/task will be added + * Id of the parent to which the imported object will be added. * @type {string} * @memberof ShareTokenImportBodyParams */ - destinationCourseId?: string | null; + destinationId?: string | null; } /** * @@ -9284,8 +9480,8 @@ export interface UserIdAndRole { * @enum {string} */ export enum UserIdAndRoleRoleNameEnum { - Editor = 'room_editor', - Viewer = 'room_viewer' + Roomeditor = 'roomeditor', + Roomviewer = 'roomviewer' } /** @@ -20739,7 +20935,7 @@ export const RoomApiAxiosParamCreator = function (configuration?: Configuration) }, /** * - * @summary Create a new room + * @summary Update an existing room * @param {string} roomId * @param {UpdateRoomBodyParams} updateRoomBodyParams * @param {*} [options] Override http request option. @@ -20759,7 +20955,7 @@ export const RoomApiAxiosParamCreator = function (configuration?: Configuration) baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -20884,7 +21080,7 @@ export const RoomApiFp = function(configuration?: Configuration) { }, /** * - * @summary Create a new room + * @summary Update an existing room * @param {string} roomId * @param {UpdateRoomBodyParams} updateRoomBodyParams * @param {*} [options] Override http request option. @@ -20989,7 +21185,7 @@ export const RoomApiFactory = function (configuration?: Configuration, basePath? }, /** * - * @summary Create a new room + * @summary Update an existing room * @param {string} roomId * @param {UpdateRoomBodyParams} updateRoomBodyParams * @param {*} [options] Override http request option. @@ -21092,7 +21288,7 @@ export interface RoomApiInterface { /** * - * @summary Create a new room + * @summary Update an existing room * @param {string} roomId * @param {UpdateRoomBodyParams} updateRoomBodyParams * @param {*} [options] Override http request option. @@ -21211,7 +21407,7 @@ export class RoomApi extends BaseAPI implements RoomApiInterface { /** * - * @summary Create a new room + * @summary Update an existing room * @param {string} roomId * @param {UpdateRoomBodyParams} updateRoomBodyParams * @param {*} [options] Override http request option. diff --git a/src/store/copy.ts b/src/store/copy.ts index f1bf4f2943..a11675d548 100644 --- a/src/store/copy.ts +++ b/src/store/copy.ts @@ -11,8 +11,8 @@ import { CourseRoomsApiInterface, ShareTokenApiFactory, ShareTokenApiInterface, - ShareTokenBodyParamsParentTypeEnum, ShareTokenInfoResponse, + ShareTokenInfoResponseParentTypeEnum, TaskApiFactory, TaskApiInterface, } from "../serverApi/v3/api"; @@ -32,10 +32,10 @@ export enum CopyParamsTypeEnum { } interface CopyByShareTokenPayload { - type: ShareTokenBodyParamsParentTypeEnum; + type: ShareTokenInfoResponseParentTypeEnum; token: string; newName: string; - destinationCourseId?: string; + destinationId?: string; } @Module({ @@ -127,12 +127,9 @@ export default class CopyModule extends VuexModule { } @Action({ rawError: true }) - async validateShareToken( - token: string - ): Promise { + async validateShareToken(token: string): Promise { const shareTokenResponse = await this.shareApi.shareTokenControllerLookupShareToken(token); - if (!shareTokenResponse) return undefined; return shareTokenResponse.data; } @@ -141,24 +138,24 @@ export default class CopyModule extends VuexModule { token, type, newName, - destinationCourseId, + destinationId, }: CopyByShareTokenPayload): Promise { let copyResult: CopyApiResponse | undefined = undefined; - if (type === ShareTokenBodyParamsParentTypeEnum.Courses) { + if (type === ShareTokenInfoResponseParentTypeEnum.Courses) { copyResult = await this.shareApi .shareTokenControllerImportShareToken(token, { newName }) .then((response) => response.data); } if ( - type === ShareTokenBodyParamsParentTypeEnum.ColumnBoard || - type === ShareTokenBodyParamsParentTypeEnum.Lessons || - type === ShareTokenBodyParamsParentTypeEnum.Tasks + type === ShareTokenInfoResponseParentTypeEnum.ColumnBoard || + type === ShareTokenInfoResponseParentTypeEnum.Lessons || + type === ShareTokenInfoResponseParentTypeEnum.Tasks ) { copyResult = await this.shareApi .shareTokenControllerImportShareToken(token, { newName, - destinationCourseId, + destinationId, }) .then((response) => response.data); } @@ -225,9 +222,9 @@ export default class CopyModule extends VuexModule { const getUrl = (element: CopyApiResponse): string | undefined => { switch (element.type) { case CopyApiResponseTypeEnum.Task: - return `/homework/${element.id}/edit?returnUrl=rooms/${element.destinationCourseId}`; + return `/homework/${element.id}/edit?returnUrl=rooms/${element.destinationId}`; case CopyApiResponseTypeEnum.Lesson: - return `/courses/${element.destinationCourseId}/topics/${element.id}/edit?returnUrl=rooms/${element.destinationCourseId}`; + return `/courses/${element.destinationId}/topics/${element.id}/edit?returnUrl=rooms/${element.destinationId}`; case CopyApiResponseTypeEnum.Course: return `/courses/${element.id}/edit`; case CopyApiResponseTypeEnum.Columnboard: diff --git a/src/store/copy.unit.ts b/src/store/copy.unit.ts index b6db90cce0..259a975118 100644 --- a/src/store/copy.unit.ts +++ b/src/store/copy.unit.ts @@ -30,7 +30,7 @@ const serverDataPartial: CopyApiResponse = { const serverDataSuccess: CopyApiResponse = { title: "Thema X", type: CopyApiResponseTypeEnum.Lesson, - destinationCourseId: "aCourseId", + destinationId: "aCourseId", status: CopyApiResponseStatusEnum.Success, id: "123", elements: [ @@ -237,7 +237,7 @@ describe("copy module", () => { const newName = "My Course"; const payload = { token, - type: serverApi.ShareTokenBodyParamsParentTypeEnum.Courses, + type: serverApi.ShareTokenInfoResponseParentTypeEnum.Courses, newName, }; @@ -261,7 +261,7 @@ describe("copy module", () => { const copyModule = new CopyModule({}); const payload = { token: "abc123a", - type: serverApi.ShareTokenBodyParamsParentTypeEnum.Courses, + type: serverApi.ShareTokenInfoResponseParentTypeEnum.Courses, newName: "My Course", }; @@ -289,7 +289,7 @@ describe("copy module", () => { const newName = "My Lesson"; const payload = { token, - type: serverApi.ShareTokenBodyParamsParentTypeEnum.Lessons, + type: serverApi.ShareTokenInfoResponseParentTypeEnum.Lessons, newName, }; @@ -313,7 +313,7 @@ describe("copy module", () => { const copyModule = new CopyModule({}); const payload = { token: "abc123a", - type: serverApi.ShareTokenBodyParamsParentTypeEnum.Lessons, + type: serverApi.ShareTokenInfoResponseParentTypeEnum.Lessons, newName: "My Lesson", }; @@ -347,7 +347,7 @@ describe("copy module", () => { const serverData = { title: "Aufgabe", type: CopyApiResponseTypeEnum.Task, - destinationCourseId: "testCourseId", + destinationId: "testCourseId", status: CopyApiResponseStatusEnum.Failure, id: "123", elements: [ @@ -384,7 +384,7 @@ describe("copy module", () => { const serverData = { title: "ColumnBoard", type: CopyApiResponseTypeEnum.Columnboard, - destinationCourseId: "testCourseId", + destinationId: "testCourseId", status: CopyApiResponseStatusEnum.Failure, id: "123", elements: [ @@ -423,7 +423,7 @@ describe("copy module", () => { const serverData = { title: "Thema", type: CopyApiResponseTypeEnum.Lesson, - destinationCourseId: "testCourseIdX", + destinationId: "testCourseIdX", status: CopyApiResponseStatusEnum.Failure, id: "456", elements: [ diff --git a/src/store/share.ts b/src/store/share.ts index 2a1f772bc2..23f70adb20 100644 --- a/src/store/share.ts +++ b/src/store/share.ts @@ -1,6 +1,7 @@ import { $axios } from "@/utils/api"; import { Action, Module, Mutation, VuexModule } from "vuex-module-decorators"; import { + BoardExternalReferenceType, ShareTokenApiFactory, ShareTokenApiInterface, ShareTokenBodyParams, @@ -20,6 +21,7 @@ export interface SharePayload extends ShareOptions { export interface StartFlow { id: string; type: ShareTokenBodyParamsParentTypeEnum; + destinationType?: BoardExternalReferenceType; } @Module({ @@ -32,6 +34,8 @@ export default class ShareModule extends VuexModule { private parentId = ""; private shareUrl: string | undefined = undefined; private parentType = ShareTokenBodyParamsParentTypeEnum.Courses; + private destinationType: BoardExternalReferenceType = + BoardExternalReferenceType.Course; private get shareApi(): ShareTokenApiInterface { return ShareTokenApiFactory(undefined, "v3", $axios); @@ -53,7 +57,11 @@ export default class ShareModule extends VuexModule { shareTokenPayload ); if (!shareTokenResult) return undefined; - const shareUrl = `${window.location.origin}/rooms/courses-overview?import=${shareTokenResult.data.token}`; + const sharePath = + this.destinationType === BoardExternalReferenceType.Course + ? "rooms/courses-overview" + : "rooms"; + const shareUrl = `${window.location.origin}/${sharePath}?import=${shareTokenResult.data.token}`; this.setShareUrl(shareUrl); return shareTokenResult.data; } catch { @@ -62,9 +70,12 @@ export default class ShareModule extends VuexModule { } @Action - startShareFlow({ id, type }: StartFlow): void { + startShareFlow({ id, type, destinationType }: StartFlow): void { this.setParentId(id); this.setParentType(type); + if (destinationType) { + this.setDestinationType(destinationType); + } this.setShareModalOpen(true); } @@ -85,6 +96,11 @@ export default class ShareModule extends VuexModule { this.parentType = type; } + @Mutation + setDestinationType(destinationType: BoardExternalReferenceType): void { + this.destinationType = destinationType; + } + @Mutation setShareModalOpen(open: boolean): void { this.isShareModalOpen = open; diff --git a/src/store/share.unit.ts b/src/store/share.unit.ts index af5d88c2ab..4e6e4fb001 100644 --- a/src/store/share.unit.ts +++ b/src/store/share.unit.ts @@ -3,6 +3,7 @@ import * as serverApi from "../serverApi/v3/api"; import { ShareTokenApiInterface, ShareTokenBodyParamsParentTypeEnum, + BoardExternalReferenceType, } from "../serverApi/v3/api"; import setupStores from "@@/tests/test-utils/setupStores"; import courseRoomDetailsModule from "@/store/course-room-details"; @@ -114,23 +115,30 @@ describe("share module", () => { }); describe("startShareFlow", () => { - it("should call setParentId, setParentType and setShareModalOpen mutations", async () => { + it("should call setParentId, setParentType, setDestinationType and setShareModalOpen mutations", async () => { const shareModule = new ShareModule({}); const setParentIdMock = jest.spyOn(shareModule, "setParentId"); const setParentTypeMock = jest.spyOn(shareModule, "setParentType"); + const setDestinationTypeMock = jest.spyOn( + shareModule, + "setDestinationType" + ); const setShareModalOpenMock = jest.spyOn( shareModule, "setShareModalOpen" ); const testId = "test-id"; const type = ShareTokenBodyParamsParentTypeEnum.Courses; + const destinationType = BoardExternalReferenceType.Room; shareModule.startShareFlow({ id: testId, type, + destinationType, }); expect(setParentIdMock).toHaveBeenCalledWith(testId); expect(setParentTypeMock).toHaveBeenCalledWith(type); + expect(setDestinationTypeMock).toHaveBeenCalledWith(destinationType); expect(setShareModalOpenMock).toHaveBeenCalledWith(true); }); }); diff --git a/src/store/types/rooms.ts b/src/store/types/rooms.ts index 99498e22fc..c3d48a8b09 100644 --- a/src/store/types/rooms.ts +++ b/src/store/types/rooms.ts @@ -41,3 +41,8 @@ export type SharingCourseObject = { }; export type AllItems = Array; + +export type ImportDestinationItem = { + id: string; + name: string; +}; diff --git a/tests/test-utils/factory/room/roomMembersFactory.ts b/tests/test-utils/factory/room/roomMembersFactory.ts index 4d1bd7170c..358b36a77b 100644 --- a/tests/test-utils/factory/room/roomMembersFactory.ts +++ b/tests/test-utils/factory/room/roomMembersFactory.ts @@ -11,7 +11,7 @@ export const roomMemberResponseFactory = Factory.define( userId: `member${sequence}`, firstName: `firstName${sequence}`, lastName: `lastName${sequence}`, - roleName: RoleName.RoomEditor, + roleName: RoleName.Roomeditor, displayRoleName: RoleName.Teacher, schoolName: "Paul-Gerhardt-Gymnasium", }) @@ -23,7 +23,7 @@ export const roomMemberListFactory = Factory.define( firstName: `firstName${sequence}`, lastName: `lastName${sequence}`, fullName: `lastName${sequence}, firstName${sequence}`, - roleName: RoleName.RoomEditor, + roleName: RoleName.Roomeditor, displayRoleName: RoleName.Teacher, schoolName: "Paul-Gerhardt-Gymnasium", }) diff --git a/tests/test-utils/factory/roomDetailsFactory.ts b/tests/test-utils/factory/roomDetailsFactory.ts index 8e9dd0dd06..fe229d4a0e 100644 --- a/tests/test-utils/factory/roomDetailsFactory.ts +++ b/tests/test-utils/factory/roomDetailsFactory.ts @@ -7,8 +7,10 @@ export const roomDetailsFactory = Factory.define( id: `room${sequence}`, name: `room #${sequence}`, color: RoomColor.BlueGrey, + schoolId: "6749dd4e657d98af622e370c", startDate: new Date().toISOString(), endDate: new Date().toISOString(), + permissions: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }) From 58241d7a7ea6e721373069e0e06a727af5ae5293 Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:10:32 +0100 Subject: [PATCH 08/12] BC-8501 - update vuetify from 3.7.1 to 3.7.5 (#3466) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9d080c85e..475ad48429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,7 @@ "vue-router": "^4.2.4", "vue3-mq": "^3.1.3", "vuedraggable": "^4.1.0", - "vuetify": "^3.7.1", + "vuetify": "^3.7.5", "vuex": "^4.0.2" }, "devDependencies": { @@ -16737,9 +16737,9 @@ "license": "MIT" }, "node_modules/vuetify": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.2.tgz", - "integrity": "sha512-q0WTcRG977+a9Dqhb8TOaPm+Xmvj0oVhnBJhAdHWFSov3HhHTTxlH2nXP/GBTXZuuMHDbBeIWFuUR2/1Fx0PPw==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.5.tgz", + "integrity": "sha512-5aiSz8WJyGzYe3yfgDbzxsFATwHvKtdvFAaUJEDTx7xRv55s3YiOho/MFhs5iTbmh2VT4ToRgP0imBUP660UOw==", "license": "MIT", "engines": { "node": "^12.20 || >=14.13" diff --git a/package.json b/package.json index ed84f3b7dd..a2123ee00d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "vue-router": "^4.2.4", "vue3-mq": "^3.1.3", "vuedraggable": "^4.1.0", - "vuetify": "^3.7.1", + "vuetify": "^3.7.5", "vuex": "^4.0.2" }, "devDependencies": { From 47e6a3c8a6a9cc3380f7703ffe9e50b709c7b054 Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:25:57 +0100 Subject: [PATCH 09/12] BC-8501 - update vuetify from 3.7.1 to 3.7.5 (#3466) From ec18fd82ad3c7547610178337342315ebdc7e715 Mon Sep 17 00:00:00 2001 From: Igor Richter <93926487+IgorCapCoder@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:26:06 +0100 Subject: [PATCH 10/12] N21-1506 sharing ctl tools (#3461) - renewing the share modals for course, boards, topics to increase readability and new feature of sharing ctl tools across schools --- .../copy-result-modal/CopyResultModal.unit.ts | 112 +++++++++++------- .../copy-result-modal/CopyResultModal.vue | 18 ++- .../CopyResultModalListItem.unit.ts | 4 + .../CopyResultModalListItem.vue | 3 + src/components/share/ImportModal.unit.ts | 12 +- src/components/share/ImportModal.vue | 87 +++++++++++--- src/components/share/ShareModal.unit.ts | 17 ++- src/components/share/ShareModal.vue | 85 ++++++++++--- src/components/share/ShareModalResult.vue | 1 + src/locales/de.ts | 51 +++++--- src/locales/en.ts | 51 +++++--- src/locales/es.ts | 50 +++++--- src/locales/uk.ts | 51 +++++--- .../board-deleted-element/DeletedElement.vue | 28 ++--- src/store/copy.ts | 1 + 15 files changed, 398 insertions(+), 173 deletions(-) diff --git a/src/components/copy-result-modal/CopyResultModal.unit.ts b/src/components/copy-result-modal/CopyResultModal.unit.ts index b2e019eba3..eedd0d63f2 100644 --- a/src/components/copy-result-modal/CopyResultModal.unit.ts +++ b/src/components/copy-result-modal/CopyResultModal.unit.ts @@ -151,53 +151,77 @@ describe("@/components/copy-result-modal/CopyResultModal", () => { ); }); - it("should render ctl tools info if root item is a Course and has no failed file ", () => { - const copyResultItems = mockLessonResultItems([]); - - const envs = envsFactory.build({ - FEATURE_CTL_TOOLS_TAB_ENABLED: true, - }); - envConfigModule.setEnvs(envs); - const wrapper = createWrapper({ - isOpen: true, - copyResultItems, - copyResultRootItemType: CopyApiResponseTypeEnum.Course, - }); - - const dialog = wrapper.findComponent(vCustomDialog); - const content = dialog.findComponent(".v-card-text").text(); - - expect(content).toContain( - "components.molecules.copyResult.ctlTools.info" - ); - }); - - describe("when root item is a Course, has no failed file and CTL_TOOLS_COPY feature flag is enabled", () => { - const setup = () => { - const copyResultItems = mockLessonResultItems([]); - const wrapper = createWrapper({ - isOpen: true, - copyResultItems, - copyResultRootItemType: CopyApiResponseTypeEnum.Course, + describe("when there is no failed file and CTL_TOOLS_COPY & CTL_TOOLS_TAB_ENABLED feature flag is enabled", () => { + describe("when the item has element of type external tool", () => { + const setup = () => { + const envs = envsFactory.build({ + FEATURE_CTL_TOOLS_TAB_ENABLED: true, + FEATURE_CTL_TOOLS_COPY_ENABLED: true, + }); + envConfigModule.setEnvs(envs); + + const copyResultItems = mockLessonResultItems([]); + copyResultItems[0].elements.push({ + title: "Course External Tool", + type: CopyApiResponseTypeEnum.ExternalTool, + }); + copyResultItems[0].type = CopyApiResponseTypeEnum.Course; + + const wrapper = createWrapper({ + isOpen: true, + copyResultItems, + copyResultRootItemType: CopyApiResponseTypeEnum.Course, + }); + + return { wrapper }; + }; + + it("should show the warning text for non-copyable course external tools", () => { + const { wrapper } = setup(); + + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.ctlTools.withFeature.info" + ); }); + }); - return { wrapper }; - }; - - it("should render ctl tools copy info ", () => { - const envs = envsFactory.build({ - FEATURE_CTL_TOOLS_TAB_ENABLED: true, - FEATURE_CTL_TOOLS_COPY_ENABLED: true, + describe("when there is an item of type ExternalToolElement", () => { + const setup = () => { + const envs = envsFactory.build({ + FEATURE_CTL_TOOLS_TAB_ENABLED: true, + FEATURE_CTL_TOOLS_COPY_ENABLED: true, + }); + envConfigModule.setEnvs(envs); + + const copyResultItems = mockLessonResultItems([]); + copyResultItems[0].elements.push({ + title: "Board External Tool Element", + type: CopyApiResponseTypeEnum.ExternalToolElement, + }); + copyResultItems[0].type = CopyApiResponseTypeEnum.Course; + + const wrapper = createWrapper({ + isOpen: true, + copyResultItems, + copyResultRootItemType: CopyApiResponseTypeEnum.Course, + }); + + return { wrapper }; + }; + + it("should show the warning text for non-copyable course external tools", () => { + const { wrapper } = setup(); + + const dialog = wrapper.findComponent(vCustomDialog); + const content = dialog.findComponent(".v-card-text").text(); + + expect(content).toContain( + "components.molecules.copyResult.ctlTools.withFeature.info" + ); }); - envConfigModule.setEnvs(envs); - const { wrapper } = setup(); - - const dialog = wrapper.findComponent(vCustomDialog); - const content = dialog.findComponent(".v-card-text").text(); - - expect(content).toContain( - "components.molecules.copyResult.ctlTools.withFeature.info" - ); }); }); diff --git a/src/components/copy-result-modal/CopyResultModal.vue b/src/components/copy-result-modal/CopyResultModal.vue index 7082db4f53..942a81b468 100644 --- a/src/components/copy-result-modal/CopyResultModal.vue +++ b/src/components/copy-result-modal/CopyResultModal.vue @@ -128,7 +128,9 @@ export default { title: this.$t("components.molecules.copyResult.label.files"), }, { - isShow: this.hasFeatureCtlsToolsenabled, + isShow: + this.isFeatureCtlToolsEnabled && + (this.hasExternalTool || this.hasExternalToolElement), text: this.externalToolsInfoText, title: this.$t("components.molecules.copyResult.label.externalTools"), }, @@ -178,7 +180,7 @@ export default { CopyApiResponseTypeEnum.CoursegroupGroup ); }, - hasFeatureCtlsToolsenabled() { + isFeatureCtlToolsEnabled() { return envConfigModule.getCtlToolsTabEnabled; }, hasErrors() { @@ -206,6 +208,18 @@ export default { ? this.$t("components.molecules.copyResult.ctlTools.withFeature.info") : this.$t("components.molecules.copyResult.ctlTools.info"); }, + hasExternalTool() { + return this.hasElementOfType( + this.items, + CopyApiResponseTypeEnum.ExternalTool + ); + }, + hasExternalToolElement() { + return this.hasElementOfType( + this.items, + CopyApiResponseTypeEnum.ExternalToolElement + ); + }, }, methods: { hasElementOfType(items, types) { diff --git a/src/components/copy-result-modal/CopyResultModalListItem.unit.ts b/src/components/copy-result-modal/CopyResultModalListItem.unit.ts index a554d6f531..046ccd5bf1 100644 --- a/src/components/copy-result-modal/CopyResultModalListItem.unit.ts +++ b/src/components/copy-result-modal/CopyResultModalListItem.unit.ts @@ -161,6 +161,10 @@ describe("@/components/copy-result-modal/CopyResultModalListItem", () => { CopyApiResponseTypeEnum.CollaborativeTextEditorElement, "components.molecules.copyResult.label.etherpad", ], + [ + CopyApiResponseTypeEnum.ExternalToolElement, + "components.molecules.copyResult.label.toolElements", + ], ]; map.forEach(([constant, languageConstant]) => { diff --git a/src/components/copy-result-modal/CopyResultModalListItem.vue b/src/components/copy-result-modal/CopyResultModalListItem.vue index 46a05e60d2..b791e5e1f2 100644 --- a/src/components/copy-result-modal/CopyResultModalListItem.vue +++ b/src/components/copy-result-modal/CopyResultModalListItem.vue @@ -9,6 +9,7 @@ v-for="element in aggregatedElements()" :key="element.type" class="element-info" + data-testid="copy-result-list-item-element-info" > {{ element.count }} {{ element.type }} @@ -121,6 +122,8 @@ export default { return this.$t("components.molecules.copyResult.label.columnBoard"); case CopyApiResponseTypeEnum.DrawingElement: return this.$t("components.molecules.copyResult.label.tldraw"); + case CopyApiResponseTypeEnum.ExternalToolElement: + return this.$t("components.molecules.copyResult.label.toolElements"); default: return this.$t("components.molecules.copyResult.label.unknown"); } diff --git a/src/components/share/ImportModal.unit.ts b/src/components/share/ImportModal.unit.ts index 0d7ecc6ef3..a68255c9ee 100644 --- a/src/components/share/ImportModal.unit.ts +++ b/src/components/share/ImportModal.unit.ts @@ -2,11 +2,11 @@ import ImportModal from "@/components/share/ImportModal.vue"; import EnvConfigModule from "@/store/env-config"; import { ENV_CONFIG_MODULE_KEY } from "@/utils/inject"; import { createModuleMocks } from "@@/tests/test-utils/mock-store-module"; -import { mount } from "@vue/test-utils"; import { createTestingI18n, createTestingVuetify, } from "@@/tests/test-utils/setup"; +import { mount } from "@vue/test-utils"; describe("@components/share/ImportModal", () => { const setup = (envConfigModuleGetter?: Partial) => { @@ -122,11 +122,11 @@ describe("@components/share/ImportModal", () => { `[data-testid="import-modal-external-tools-info"]` ); - expect(infoText.attributes("html")).toEqual( - "components.molecules.import.courses.options.ctlTools.infoText" + expect(infoText.text()).toEqual( + "components.molecules.shareImport.options.ctlTools.infoText.unavailable" ); }); - it("should not show course file info", () => { + it("should also show course file info", () => { const { wrapper } = setup({ getCtlToolsTabEnabled: true }); const dialog = wrapper.findComponent({ name: "v-custom-dialog" }); @@ -136,7 +136,7 @@ describe("@components/share/ImportModal", () => { `[data-testid="import-modal-coursefiles-info"]` ); - expect(infoText.exists()).toBe(false); + expect(infoText.exists()).toBe(true); }); }); describe("show ctl tool info is disabled", () => { @@ -176,7 +176,7 @@ describe("@components/share/ImportModal", () => { ); expect(infoText.element.innerHTML).toEqual( - "components.molecules.import.courses.options.infoText" + "components.molecules.shareImport.options.restrictions.infoText.courseFiles" ); }); }); diff --git a/src/components/share/ImportModal.vue b/src/components/share/ImportModal.vue index 085807f37c..da9d343e9a 100644 --- a/src/components/share/ImportModal.vue +++ b/src/components/share/ImportModal.vue @@ -8,6 +8,7 @@ confirm-btn-title-key="common.actions.import" @dialog-confirmed="onConfirm" @dialog-canceled="onCancel" + data-testid="import-modal" > @@ -48,12 +99,11 @@ diff --git a/src/components/share/ShareModal.unit.ts b/src/components/share/ShareModal.unit.ts index 4af690eddd..1bc75a2555 100644 --- a/src/components/share/ShareModal.unit.ts +++ b/src/components/share/ShareModal.unit.ts @@ -1,22 +1,22 @@ -import ShareModule from "@/store/share"; -import { createModuleMocks } from "@@/tests/test-utils/mock-store-module"; -import { mount } from "@vue/test-utils"; -import ShareModal from "./ShareModal.vue"; import vCustomDialog from "@/components/organisms/vCustomDialog.vue"; import ShareModalOptionsForm from "@/components/share/ShareModalOptionsForm.vue"; import ShareModalResult from "@/components/share/ShareModalResult.vue"; import { ShareTokenBodyParamsParentTypeEnum } from "@/serverApi/v3"; +import EnvConfigModule from "@/store/env-config"; +import NotifierModule from "@/store/notifier"; +import ShareModule from "@/store/share"; import { ENV_CONFIG_MODULE_KEY, NOTIFIER_MODULE_KEY, SHARE_MODULE_KEY, } from "@/utils/inject"; -import NotifierModule from "@/store/notifier"; -import EnvConfigModule from "@/store/env-config"; +import { createModuleMocks } from "@@/tests/test-utils/mock-store-module"; import { createTestingI18n, createTestingVuetify, } from "@@/tests/test-utils/setup"; +import { mount } from "@vue/test-utils"; +import ShareModal from "./ShareModal.vue"; describe("@/components/share/ShareModal", () => { let shareModuleMock: ShareModule; @@ -137,11 +137,10 @@ describe("@/components/share/ShareModal", () => { `[data-testid="share-modal-external-tools-info"]` ); + expect(infotext.isVisible()).toBe(true); expect(infotext.text()).toEqual( - "components.molecules.share.courses.options.ctlTools.infotext" + "components.molecules.shareImport.options.ctlTools.infoText.unavailable" ); - - expect(infotext.isVisible()).toBe(true); }); }); diff --git a/src/components/share/ShareModal.vue b/src/components/share/ShareModal.vue index 06b3ef7a77..c33befa9a3 100644 --- a/src/components/share/ShareModal.vue +++ b/src/components/share/ShareModal.vue @@ -18,24 +18,72 @@
+

+ {{ t(`components.molecules.share.${type}.options.infoText`) }} +

-
- {{ t(`components.molecules.share.${type}.options.infoText`) }} -
- {{ t("components.molecules.copyResult.courseFiles.info") }} -
- {{ - t( - `components.molecules.share.courses.options.ctlTools.infotext` - ) - }} -
+
+ {{ t("components.molecules.share.options.tableHeader.InfoText") }} +
    +
  • + {{ + t( + "components.molecules.shareImport.options.restrictions.infoText.personalData" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.ctlTools.infoText.unavailable" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.ctlTools.infoText.protected" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.restrictions.infoText.courseFiles" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.restrictions.infoText.etherpad" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.restrictions.infoText.geogebra" + ) + }} +
  • +
  • + {{ + t( + "components.molecules.shareImport.options.restrictions.infoText.courseGroups" + ) + }} +
  • +
diff --git a/src/components/share/ShareModalResult.vue b/src/components/share/ShareModalResult.vue index 72e99c42dd..bde60a4dd6 100644 --- a/src/components/share/ShareModalResult.vue +++ b/src/components/share/ShareModalResult.vue @@ -5,6 +5,7 @@ :model-value="shareUrl" readonly :label="`${t(`components.molecules.share.${type}.result.linkLabel`)}`" + data-testid="share-course-result-url" />
Personenbezogene Daten werden nicht importiert.
Externe Tools werden nicht kopiert.
Der Kurs kann im Folgenden umbenannt werden.", - "components.molecules.import.courses.options.infoText": - "Es wird eine Kopie erstellt. Personenbezogene Daten werden nicht importiert. Der Kurs kann im Folgenden umbenannt werden.", - "components.molecules.import.courses.options.title": "Kurs importieren", + "components.molecules.import.courses.rename": + "Bei Bedarf kann der Name des Kurses umbenannt werden: ", + "components.molecules.import.courses.options.title": "Kurs-Kopie importieren", "components.molecules.import.lessons.label": "Thema", - "components.molecules.import.lessons.options.infoText": - "Es wird eine Kopie erstellt. Personenbezogene Daten werden nicht importiert. Das Thema kann im Folgenden umbenannt werden.", + "components.molecules.import.lessons.rename": + "Bei Bedarf kann der Name des Themas umbenannt werden: ", "components.molecules.import.lessons.options.selectCourse.infoText": "Der Kurs, in den das Thema importiert werden soll, muss im Folgenden ausgewählt werden.", "components.molecules.import.lessons.options.selectCourse": "Kurs wählen", @@ -613,9 +614,11 @@ export default { "components.molecules.import.options.loadingMessage": "Import läuft...", "components.molecules.import.options.success": "{name} wurde erfolgreich importiert", + "components.molecules.import.options.tableHeader.InfoText": + "Folgende Inhalte werden nicht importiert:", "components.molecules.import.tasks.label": "Aufgabe", - "components.molecules.import.tasks.options.infoText": - "Es wird eine Kopie erstellt. Personenbezogene Daten werden nicht importiert. Die Aufgabe kann im Folgenden umbenannt werden.", + "components.molecules.import.tasks.rename": + "Bei Bedarf kann der Name der Aufgabe umbenannt werden: ", "components.molecules.import.tasks.options.selectCourse.infoText": "Der Kurs, in den die Aufgabe importiert werden soll, muss im Folgenden ausgewählt werden.", "components.molecules.export.options.info": @@ -654,20 +657,34 @@ export default { "Organisationsleitung", "components.molecules.MintEcFooter.chapters": "Kapitelübersicht", "components.molecules.share.columnBoard.options.infoText": - "Mit dem folgenden Link kann der Bereich als Kopie von anderen Lehrkräften importiert werden. Personenbezogene Daten werden dabei nicht importiert.", + "Mit dem folgenden Link kann der Bereich als Kopie von anderen Lehrkräften importiert werden.", "components.molecules.share.columnBoard.result.linkLabel": "Link Bereich-Kopie", "components.molecules.share.courses.mail.body": "Link zum Kurs:", "components.molecules.share.courses.mail.subject": "Kurs zum Importieren", - "components.molecules.share.courses.options.ctlTools.infotext": - "Externe Tools, die dem Kurs oder Karten im Bereich zugeordnet sind, werden nicht kopiert.", + "components.molecules.shareImport.options.ctlTools.infoText.unavailable": + "In Zielschule nicht verfügbare, externe Tools", + "components.molecules.shareImport.options.ctlTools.infoText.protected": + "Geschützte Einstellungen externer Tools", "components.molecules.share.courses.options.infoText": - "Mit dem folgenden Link kann der Kurs als Kopie von anderen Lehrkräften importiert werden. Personenbezogene Daten werden dabei nicht importiert.", + "Mit dem folgenden Link kann der Kurs als Kopie von anderen Lehrkräften importiert werden.", + "components.molecules.shareImport.options.restrictions.infoText.personalData": + "Personenbezogene Daten", + "components.molecules.shareImport.options.restrictions.infoText.courseFiles": + "Dateien unter Kurs-Dateien", + "components.molecules.shareImport.options.restrictions.infoText.etherpad": + "Inhalte aus Etherpads", + "components.molecules.shareImport.options.restrictions.infoText.geogebra": + "Geogebra IDs und", + "components.molecules.shareImport.options.restrictions.infoText.courseGroups": + "Kursgruppen", + "components.molecules.share.options.tableHeader.InfoText": + "Folgende Inhalte werden nicht kopiert:", "components.molecules.share.courses.result.linkLabel": "Link Kurskopie", "components.molecules.share.lessons.mail.body": "Link zum Thema:", "components.molecules.share.lessons.mail.subject": "Thema zum Importieren", "components.molecules.share.lessons.options.infoText": - "Mit dem folgenden Link kann das Thema als Kopie von anderen Lehrkräften importiert werden. Personenbezogene Daten werden dabei nicht importiert.", + "Mit dem folgenden Link kann das Thema als Kopie von anderen Lehrkräften importiert werden.", "components.molecules.share.lessons.result.linkLabel": "Link Themakopie", "components.molecules.share.options.expiresInDays": "Link läuft nach 21 Tagen ab", @@ -681,7 +698,7 @@ export default { "components.molecules.share.tasks.mail.body": "Link zur Aufgabe:", "components.molecules.share.tasks.mail.subject": "Aufgabe zum Importieren", "components.molecules.share.tasks.options.infoText": - "Mit dem folgenden Link kann die Aufgabe als Kopie von anderen Lehrkräften importiert werden. Personenbezogene Daten werden dabei nicht importiert.", + "Mit dem folgenden Link kann die Aufgabe als Kopie von anderen Lehrkräften importiert werden.", "components.molecules.share.tasks.result.linkLabel": "Link Aufgabekopie", "components.molecules.TaskItemMenu.confirmDelete.text": 'Bist du dir sicher, dass du die Aufgabe "{taskTitle}" löschen möchtest?', diff --git a/src/locales/en.ts b/src/locales/en.ts index bf6a788f59..4e0c95d93b 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -515,7 +515,7 @@ export default { "components.molecules.copyResult.ctlTools.info": "External tools associated with the course and boarding cards are not copied.", "components.molecules.copyResult.ctlTools.withFeature.info": - "Protected parts of the tool configurations are not copied.", + "External tools and protected parts of the tool configurations that are not available in the target school are not copied.", "components.molecules.copyResult.etherpadCopy.info": "Content is not copied for data protection reasons and must be added again.", "components.molecules.copyResult.failedCopy": @@ -546,8 +546,9 @@ export default { "components.molecules.copyResult.label.tldraw": "Whiteboard", "components.molecules.copyResult.label.link": "Link", "components.molecules.copyResult.label.timeGroup": "Time Group", - "components.molecules.copyResult.label.unknown": "Unkown", + "components.molecules.copyResult.label.unknown": "Unknown", "components.molecules.copyResult.label.userGroup": "User Group", + "components.molecules.copyResult.label.toolElements": "Tool Element", "components.molecules.copyResult.metadata": "General Information", "components.molecules.copyResult.nexboardCopy.info": "Content is not copied for data protection reasons and must be added again.", @@ -576,6 +577,8 @@ export default { "components.molecules.EdusharingFooter.img_alt": "edusharing-logo", "components.molecules.EdusharingFooter.text": "powered by", "components.molecules.import.columnBoard.label": "Board title", + "components.molecules.import.columnBoard.rename": + "If necessary, the name of the board can be renamed: ", "components.molecules.import.columnBoard.options.infoText": "The board can be renamed below.", "components.molecules.import.columnBoard.options.title": "Import board", @@ -583,18 +586,16 @@ export default { "Select course", "components.molecules.import.columnBoard.options.selectCourse.infoText": "Please select the course into which you would like to import the board.", - "components.molecules.import.courses.label": "Course", + "components.molecules.import.courses.label": "Course name", "components.molecules.import.columnBoard.options.selectRoom": "Select room", "components.molecules.import.columnBoard.options.selectRoom.infoText": "Please select the room into which you would like to import the board.", - "components.molecules.import.courses.options.ctlTools.infoText": - "A copy will be created.
Personal data will not be imported.
External tools will not be copied.
The course can be renamed below.", - "components.molecules.import.courses.options.infoText": - "Participant-related data will not be copied. The course can be renamed below.", - "components.molecules.import.courses.options.title": "Import course", + "components.molecules.import.courses.rename": + "If necessary, the name of the course can be renamed: ", + "components.molecules.import.courses.options.title": "Import course copy", "components.molecules.import.lessons.label": "Topic", - "components.molecules.import.lessons.options.infoText": - "Participant-related data will not be copied. The topic can be renamed below.", + "components.molecules.import.lessons.rename": + "If necessary, the name of the topic can be renamed: ", "components.molecules.import.lessons.options.selectCourse.infoText": "Please select the course into which you would like to import the topic.", "components.molecules.import.lessons.options.selectCourse": "Select course", @@ -607,9 +608,11 @@ export default { "Unfortunately, the necessary authorization is missing.", "components.molecules.import.options.loadingMessage": "Import in progress...", "components.molecules.import.options.success": "{name} imported successfully", + "components.molecules.import.options.tableHeader.InfoText": + "The following content will not be imported:", "components.molecules.import.tasks.label": "Task", - "components.molecules.import.tasks.options.infoText": - "Participant-related data will not be copied. The task can be renamed below.", + "components.molecules.import.tasks.rename": + "If necessary, the name of the task can be renamed: ", "components.molecules.import.tasks.options.selectCourse.infoText": "Please select the course into which you would like to import the task.", "components.molecules.import.tasks.options.selectCourse": "Select course", @@ -642,18 +645,32 @@ export default { "components.molecules.MintEcFooter.chapters": "Chapter overview", "components.molecules.share.courses.mail.body": "Link to the course:", "components.molecules.share.courses.mail.subject": "Course you can import", - "components.molecules.share.courses.options.ctlTools.infotext": - "External tools associated with the course or boarding cards will not be copied.", + "components.molecules.shareImport.options.ctlTools.infoText.unavailable": + "External tools not available in the target school", + "components.molecules.shareImport.options.ctlTools.infoText.protected": + "Protected settings of external tools", "components.molecules.share.courses.options.infoText": - "With the following link, the course can be imported as a copy by other teachers. Personal data will not be imported.", + "With the following link, the course can be imported as a copy by other teachers.", + "components.molecules.shareImport.options.restrictions.infoText.personalData": + "Personal data", + "components.molecules.shareImport.options.restrictions.infoText.courseFiles": + "Files under Course Files", + "components.molecules.shareImport.options.restrictions.infoText.etherpad": + "Content from Etherpads", + "components.molecules.shareImport.options.restrictions.infoText.geogebra": + "Geogebra IDs and", + "components.molecules.shareImport.options.restrictions.infoText.courseGroups": + "Course groups", + "components.molecules.share.options.tableHeader.InfoText": + "The following content will not be copied:", "components.molecules.share.courses.result.linkLabel": "Link course copy", "components.molecules.share.lessons.mail.body": "Link to the topic:", "components.molecules.share.lessons.mail.subject": "Topic you can import", "components.molecules.share.lessons.options.infoText": - "With the following link, the topic can be imported as a copy by other teachers. Personal data will not be imported.", + "With the following link, the topic can be imported as a copy by other teachers.", "components.molecules.share.lessons.result.linkLabel": "Link topic copy", "components.molecules.share.columnBoard.options.infoText": - "With the following link, the board can be imported as a copy by other teachers. Personal data will not be imported.", + "With the following link, the board can be imported as a copy by other teachers.", "components.molecules.share.columnBoard.result.linkLabel": "Link to Board copy", "components.molecules.share.options.expiresInDays": diff --git a/src/locales/es.ts b/src/locales/es.ts index edcd7a5fc7..e7e9c76c7d 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -525,7 +525,7 @@ export default { "components.molecules.copyResult.ctlTools.info": "Las herramientas externas asociadas al curso y las tarjetas de embarque no se copian.", "components.molecules.copyResult.ctlTools.withFeature.info": - "Las partes protegidas de las configuraciones de herramientas no se copian.", + "Las herramientas externas y las partes protegidas de las configuraciones de herramientas que no están disponibles en la escuela de destino no se copian.", "components.molecules.copyResult.etherpadCopy.info": "El contenido no se copia por razones de protección de datos y debe agregarse nuevamente.", "components.molecules.copyResult.failedCopy": @@ -561,6 +561,8 @@ export default { "components.molecules.copyResult.label.timeGroup": "Grupo de tiempo", "components.molecules.copyResult.label.unknown": "Desconocido", "components.molecules.copyResult.label.userGroup": "Grupo de usuario", + "components.molecules.copyResult.label.toolElements": + "Elemento de herramienta", "components.molecules.copyResult.metadata": "Información general", "components.molecules.copyResult.nexboardCopy.info": "El contenido no se copia por razones de protección de datos y debe agregarse nuevamente.", @@ -589,6 +591,8 @@ export default { "components.molecules.EdusharingFooter.img_alt": "edusharing-logotipo", "components.molecules.EdusharingFooter.text": "desarrollado por", "components.molecules.import.columnBoard.label": "Título del tablero", + "components.molecules.import.columnBoard.rename": + "Si es necesario, se puede cambiar el nombre del tablero: ", "components.molecules.import.columnBoard.options.infoText": "Puede cambiar el nombre del tablero a continuación.", "components.molecules.import.columnBoard.options.title": "Importar tablero", @@ -596,19 +600,17 @@ export default { "Elija el curso", "components.molecules.import.columnBoard.options.selectCourse.infoText": "Seleccione el curso al que desea importar el tablero.", - "components.molecules.import.courses.label": "Curso", + "components.molecules.import.courses.label": "Nombre del curso", "components.molecules.import.columnBoard.options.selectRoom": "Seleccionar sala", "components.molecules.import.columnBoard.options.selectRoom.infoText": "Seleccione la sala en la que desea importar el tablero.", - "components.molecules.import.courses.options.ctlTools.infoText": - "Se creará una copia.
No se importarán datos personales.
No se copiarán herramientas externas.
Se puede cambiar el nombre del curso a continuación.", - "components.molecules.import.courses.options.infoText": - "Los datos relacionados con los participantes no se copiarán. El curso se puede renombrar a continuación.", - "components.molecules.import.courses.options.title": "Importar curso", + "components.molecules.import.courses.rename": + "Si es necesario, se puede cambiar el nombre del curso: ", + "components.molecules.import.courses.options.title": "Importar copia nuestra", "components.molecules.import.lessons.label": "Tema", - "components.molecules.import.lessons.options.infoText": - "Los datos relacionados con los participantes no se copiarán. El tema se puede renombrar a continuación.", + "components.molecules.import.lessons.rename": + "Si es necesario, se puede cambiar el nombre del tema: ", "components.molecules.import.lessons.options.selectCourse.infoText": "Seleccione el curso al que desea importar el tema.", "components.molecules.import.lessons.options.selectCourse": "Elija el curso", @@ -622,9 +624,11 @@ export default { "components.molecules.import.options.loadingMessage": "Importación en curso...", "components.molecules.import.options.success": "{name} importado con éxito", + "components.molecules.import.options.tableHeader.InfoText": + "No se importará el siguiente contenido:", "components.molecules.import.tasks.label": "Tarea", - "components.molecules.import.tasks.options.infoText": - "Los datos relacionados con los participantes no se copiarán. La tarea se puede renombrar a continuación.", + "components.molecules.import.tasks.rename": + "Si es necesario, se puede cambiar el nombre de la tarea: ", "components.molecules.import.tasks.options.selectCourse.infoText": "Seleccione el curso al que desea importar la tarea.", "components.molecules.import.tasks.options.selectCourse": "Elija el curso", @@ -661,16 +665,32 @@ export default { "Enlace a la copia del tablón", "components.molecules.share.courses.mail.body": "Enlace al curso:", "components.molecules.share.courses.mail.subject": "Curso de importación", - "components.molecules.share.courses.options.ctlTools.infotext": + "components.molecules.share.courses.options.ctlTools.infoText": "No se copiarán herramientas externas asociadas al curso ni tarjetas de embarque.", + "components.molecules.shareImport.options.ctlTools.infoText.unavailable": + "Herramientas externas no disponibles en la escuela de destino", + "components.molecules.shareImport.options.ctlTools.infoText.protected": + "Configuraciones protegidas de herramientas externas", "components.molecules.share.courses.options.infoText": - "Con el siguiente enlace, el curso puede ser importado como copia por otros profesores. Los datos personales no se importarán.", + "Utilizando el siguiente enlace, otros profesores pueden importar el curso como una copia.", + "components.molecules.shareImport.options.restrictions.infoText.personalData": + "Datos personales", + "components.molecules.shareImport.options.restrictions.infoText.courseFiles": + "Archivos en Archivos de curso", + "components.molecules.shareImport.options.restrictions.infoText.etherpad": + "Contenido de Etherpads", + "components.molecules.shareImport.options.restrictions.infoText.geogebra": + "ID de Geogebra y", + "components.molecules.shareImport.options.restrictions.infoText.courseGroups": + "Grupos de cursos", + "components.molecules.share.options.tableHeader.InfoText": + "No se copiará el siguiente contenido:", "components.molecules.share.courses.result.linkLabel": "Enlace a la copia del curso", "components.molecules.share.lessons.mail.body": "Enlace al tema:", "components.molecules.share.lessons.mail.subject": "Tema de importación", "components.molecules.share.lessons.options.infoText": - "Con el siguiente enlace, el tema puede ser importado como copia por otros profesores. Los datos personales no se importarán.", + "Con el siguiente enlace, el tema puede ser importado como copia por otros profesores.", "components.molecules.share.lessons.result.linkLabel": "Enlace a la copia del tema", "components.molecules.share.options.expiresInDays": @@ -685,7 +705,7 @@ export default { "components.molecules.share.tasks.mail.body": "Enlace a la tarea:", "components.molecules.share.tasks.mail.subject": "Tarea de importación", "components.molecules.share.tasks.options.infoText": - "Con el siguiente enlace, la tarea puede ser importado como copia por otros profesores. Los datos personales no se importarán.", + "Con el siguiente enlace, la tarea puede ser importado como copia por otros profesores.", "components.molecules.share.tasks.result.linkLabel": "Enlace a la copia de la tarea", "components.molecules.TaskItemMenu.confirmDelete.text": diff --git a/src/locales/uk.ts b/src/locales/uk.ts index b2b2ad29dd..7f754142b3 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -523,7 +523,7 @@ export default { "components.molecules.copyResult.ctlTools.info": "Зовнішні інструменти, пов’язані з курсом, і посадкові картки не копіюються.", "components.molecules.copyResult.ctlTools.withFeature.info": - "Захищені частини конфігурацій інструменту не копіюються.", + "Зовнішні інструменти та захищені частини конфігурацій інструментів, які недоступні в цільовій школі, не копіюються.", "components.molecules.copyResult.etherpadCopy.info": "Вміст не копіюється з міркувань захисту даних і повинен бути доданий повторно.", "components.molecules.copyResult.failedCopy": @@ -557,6 +557,8 @@ export default { "components.molecules.copyResult.label.timeGroup": "Група часу", "components.molecules.copyResult.label.unknown": "Невідомий", "components.molecules.copyResult.label.userGroup": "Група користувачів", + "components.molecules.copyResult.label.toolElements": + "Інструментальний елемент", "components.molecules.copyResult.metadata": "Загальна інформація", "components.molecules.copyResult.nexboardCopy.info": "Вміст не копіюється з міркувань захисту даних і повинен бути доданий повторно.", @@ -585,6 +587,8 @@ export default { "components.molecules.EdusharingFooter.img_alt": "логотип edusharing", "components.molecules.EdusharingFooter.text": "на платформі", "components.molecules.import.columnBoard.label": "Назва дошки", + "components.molecules.import.columnBoard.rename": + "При необхідності назву дошки можна змінити: ", "components.molecules.import.columnBoard.options.infoText": "Ви можете перейменувати дошку нижче", "components.molecules.import.columnBoard.options.title": "Дошка імпорту", @@ -592,19 +596,18 @@ export default { "Оберіть курс", "components.molecules.import.columnBoard.options.selectCourse.infoText": "Виберіть курс, до якого ви бажаєте імпортувати дошку.", - "components.molecules.import.courses.label": "Курс", + "components.molecules.import.courses.label": "Назва курсу", "components.molecules.import.columnBoard.options.selectRoom": "Оберіть кімнату", "components.molecules.import.columnBoard.options.selectRoom.infoText": "Виберіть кімнату, до якого ви бажаєте імпортувати дошку.", - "components.molecules.import.courses.options.ctlTools.infoText": - "Буде створено копію.
собисті дані не будуть імпортовані.
Зовнішні інструменти не будуть скопійовані.
Курс можна перейменувати нижче.", - "components.molecules.import.courses.options.infoText": - "Дані учасників не будуть скопійовані. Курс можна перейменувати нижче.", - "components.molecules.import.courses.options.title": "Курс імпорту", + "components.molecules.import.courses.rename": + "При необхідності назву курсу можна перейменувати: ", + "components.molecules.import.courses.options.title": + "Імпортувати копію курсу", "components.molecules.import.lessons.label": "Тема", - "components.molecules.import.lessons.options.infoText": - "Дані учасників не будуть скопійовані. Тема можна перейменувати нижче.", + "components.molecules.import.lessons.rename": + "При необхідності назву теми можна перейменувати: ", "components.molecules.import.lessons.options.selectCourse.infoText": "Будь ласка, оберіть курс з якого ви хочете імпортувати тему", "components.molecules.import.lessons.options.selectCourse": "Оберіть курс", @@ -618,9 +621,11 @@ export default { "components.molecules.import.options.loadingMessage": "Виконується імпорту...", "components.molecules.import.options.success": "{name} успішно імпортовано", + "components.molecules.import.options.tableHeader.InfoText": + "Наступний вміст не буде імпортовано:", "components.molecules.import.tasks.label": "Завдання", - "components.molecules.import.tasks.options.infoText": - "Дані, що стосуються учасників, не копіюються. Завдання можна перейменувати нижче.", + "components.molecules.import.tasks.rename": + "При необхідності назву завдання можна перейменувати: ", "components.molecules.import.tasks.options.selectCourse.infoText": "Виберіть курс, до якого ви хочете імпортувати завдання.", "components.molecules.import.tasks.options.selectCourse": "Оберіть курс", @@ -658,17 +663,33 @@ export default { "Посилання на копію дошки", "components.molecules.share.courses.mail.body": "Посилання на курс:", "components.molecules.share.courses.mail.subject": "Курс імпорту", - "components.molecules.share.courses.options.ctlTools.infotext": + "components.molecules.share.courses.options.ctlTools.infoText": "Зовнішні інструменти, пов’язані з курсом або посадочними картками, не будуть скопійовані.", + "components.molecules.shareImport.options.ctlTools.infoText.unavailable": + "Зовнішні інструменти недоступні в цільовій школі", + "components.molecules.shareImport.options.ctlTools.infoText.protected": + "Захищені налаштування зовнішніх інструментів", "components.molecules.share.courses.options.infoText": - "За наступним посиланням курс може бути імпортований як копія іншими викладачами. Персональні дані не імпортуються.", + "Використовуючи наступне посилання, курс може бути імпортований як копія іншими викладачами.", + "components.molecules.shareImport.options.restrictions.infoText.personalData": + "Персональні дані", + "components.molecules.shareImport.options.restrictions.infoText.courseFiles": + "Файли в розділі Файли курсу", + "components.molecules.shareImport.options.restrictions.infoText.etherpad": + "Вміст із Etherpads", + "components.molecules.shareImport.options.restrictions.infoText.geogebra": + "Ідентифікатори Geogebra та", + "components.molecules.shareImport.options.restrictions.infoText.courseGroups": + "Групи курсів", + "components.molecules.share.options.tableHeader.InfoText": + "Наступний вміст не буде скопійовано:", "components.molecules.share.courses.result.linkLabel": "Посилання на копію курсу", "components.molecules.share.lessons.mail.body": "Посилання на курс:", "components.molecules.share.lessons.mail.subject": "Теми, які можна імпортувати", "components.molecules.share.lessons.options.infoText": - "За наступним посиланням тему можуть імпортувати як копію інші вчителі. Особисті дані не будуть імпортовані.", + "За наступним посиланням тему можуть імпортувати як копію інші вчителі.", "components.molecules.share.lessons.result.linkLabel": "Копія теми посилання", "components.molecules.share.options.expiresInDays": "Термін дії посилання закінчується через 21 днів", @@ -683,7 +704,7 @@ export default { "components.molecules.share.tasks.mail.subject": "Завдання, які можна імпортувати", "components.molecules.share.tasks.options.infoText": - "За наступним посиланням завдання можуть імпортувати як копію інші вчителі. Особисті дані не будуть імпортовані.", + "За наступним посиланням завдання можуть імпортувати як копію інші вчителі.", "components.molecules.share.tasks.result.linkLabel": "Зв'язати копію завдання", "components.molecules.TaskItemMenu.confirmDelete.text": diff --git a/src/modules/feature/board-deleted-element/DeletedElement.vue b/src/modules/feature/board-deleted-element/DeletedElement.vue index 3460ff13d0..454a4923cb 100644 --- a/src/modules/feature/board-deleted-element/DeletedElement.vue +++ b/src/modules/feature/board-deleted-element/DeletedElement.vue @@ -8,20 +8,6 @@ ref="deletedElement" :ripple="false" > - - {{ - $t( - "components.cardElement.deletedElement.warning.externalToolElement", - { - toolName: element.content.title, - } - ) - }} - + + {{ + $t( + "components.cardElement.deletedElement.warning.externalToolElement", + { + toolName: element.content.title, + } + ) + }} + diff --git a/src/store/copy.ts b/src/store/copy.ts index a11675d548..3bbb9a45fc 100644 --- a/src/store/copy.ts +++ b/src/store/copy.ts @@ -216,6 +216,7 @@ export default class CopyModule extends VuexModule { if (type === CopyApiResponseTypeEnum.DrawingElement) return true; if (type === CopyApiResponseTypeEnum.CollaborativeTextEditorElement) return true; + if (type === CopyApiResponseTypeEnum.ExternalToolElement) return true; return false; }; From a0274e5d1fc195b53902e2c270bb9538fd276927 Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Tue, 10 Dec 2024 10:43:37 +0100 Subject: [PATCH 11/12] BC-8500 - fix removing members in members table (#3465) * fix deletion of members via the delete icon in the table * refactor: roomMemberlist handling --------- Co-authored-by: hoeppner.dataport --- .../room/RoomMembers/MembersTable.unit.ts | 444 +++++++++++++----- .../feature/room/RoomMembers/MembersTable.vue | 89 ++-- .../page/room/RoomMembers.page.unit.ts | 444 ++++++++---------- src/modules/page/room/RoomMembers.page.vue | 39 +- 4 files changed, 588 insertions(+), 428 deletions(-) diff --git a/src/modules/feature/room/RoomMembers/MembersTable.unit.ts b/src/modules/feature/room/RoomMembers/MembersTable.unit.ts index b7db2ddbeb..a0da092c36 100644 --- a/src/modules/feature/room/RoomMembers/MembersTable.unit.ts +++ b/src/modules/feature/room/RoomMembers/MembersTable.unit.ts @@ -3,175 +3,375 @@ import { createTestingVuetify, } from "@@/tests/test-utils/setup"; import MembersTable from "./MembersTable.vue"; -import { Ref } from "vue"; +import { ref } from "vue"; import { mdiMenuDown, mdiMenuUp, mdiMagnify } from "@icons/material"; import { roomMemberResponseFactory } from "@@/tests/test-utils"; -import { RoomMember } from "@data-room"; -import { flushPromises } from "@vue/test-utils"; +import { DOMWrapper, VueWrapper } from "@vue/test-utils"; +import { VDataTable, VTextField } from "vuetify/lib/components/index.mjs"; +import { useConfirmationDialog } from "@ui-confirmation-dialog"; +import setupConfirmationComposableMock from "@@/tests/test-utils/composable-mocks/setupConfirmationComposableMock"; -const mockMembers = roomMemberResponseFactory.buildList(3); +jest.mock("@ui-confirmation-dialog"); +const mockedUseRemoveConfirmationDialog = jest.mocked(useConfirmationDialog); describe("MembersTable", () => { + let askConfirmationMock: jest.Mock; + + beforeEach(() => { + askConfirmationMock = jest.fn(); + setupConfirmationComposableMock({ + askConfirmationMock, + }); + mockedUseRemoveConfirmationDialog.mockReturnValue({ + askConfirmation: askConfirmationMock, + isDialogOpen: ref(false), + }); + }); + + const tableHeaders = [ + "common.labels.firstName", + "common.labels.lastName", + "common.labels.role", + "common.words.mainSchool", + "", + ]; + const setup = () => { + const mockMembers = roomMemberResponseFactory.buildList(3); const wrapper = mount(MembersTable, { + attachTo: document.body, global: { plugins: [createTestingVuetify(), createTestingI18n()], }, - props: { members: mockMembers, selectedMembers: [] }, + props: { members: mockMembers }, }); - const wrapperVM = wrapper.vm as unknown as { - members: RoomMember[]; - search: Ref; - tableTitle: string; - tableHeader: { title: string; key: string }[]; - selectedMemberList: string[]; - }; + return { wrapper, mockMembers }; + }; + + // index 0 is the header checkbox + const selectCheckboxes = async (indices: number[], wrapper: VueWrapper) => { + const dataTable = wrapper.getComponent(VDataTable); + const checkboxes = dataTable.findAll("input[type='checkbox']"); - return { wrapper, wrapperVM }; + for (const index of indices) { + const checkbox = checkboxes[index]; + await checkbox.trigger("click"); + } + + return { checkboxes }; }; - describe("when component is mounted", () => { - it("should render member's table", () => { + const getCheckedIndices = (checkboxes: DOMWrapper[]) => + checkboxes.reduce((selectedIndices, checkbox, index) => { + if (checkbox.attributes("checked") === "") { + selectedIndices.push(index); + } + return selectedIndices; + }, [] as Array); + + it("should render members table component", () => { + const { wrapper } = setup(); + + expect(wrapper.exists()).toBe(true); + }); + + it("should render data table", () => { + const { wrapper, mockMembers } = setup(); + + const dataTable = wrapper.getComponent(VDataTable); + + expect(dataTable.props("headers")!.map((header) => header.title)).toEqual( + tableHeaders + ); + expect(dataTable.props("items")).toEqual(mockMembers); + expect(dataTable.props("sortAscIcon")).toEqual(mdiMenuDown); + expect(dataTable.props("sortDescIcon")).toEqual(mdiMenuUp); + }); + + it("should render checkboxes", async () => { + const { wrapper, mockMembers } = setup(); + + const dataTable = wrapper.findComponent(VDataTable); + const checkboxes = dataTable.findAll("input[type='checkbox']"); + + expect(checkboxes.length).toEqual(mockMembers.length + 1); // all checkboxes including header checkbox + }); + + describe("when selecting members", () => { + it("should select all members when header checkbox is clicked", async () => { const { wrapper } = setup(); - expect(wrapper.exists()).toBe(true); - expect(wrapper.findComponent(MembersTable)).toBeTruthy(); + const { checkboxes } = await selectCheckboxes([0], wrapper); + const checkedIndices = getCheckedIndices(checkboxes); + + const expectedIndices = [0, 1, 2, 3]; + + expect(checkedIndices).toEqual(expectedIndices); }); - }); - describe("DataTable component", () => { - it("should render the table component", () => { - const { wrapper, wrapperVM } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); + it("should emit select:members", async () => { + const { wrapper, mockMembers } = setup(); + + await selectCheckboxes([1], wrapper); + + const selectEvents = wrapper.emitted("select:members"); + expect(selectEvents).toHaveLength(1); + expect(selectEvents![0]).toEqual([[mockMembers[0].userId]]); + }); + + it("should render the multi action menu", async () => { + const { wrapper } = setup(); + + await selectCheckboxes([1], wrapper); + + const multiActionMenu = wrapper.find("[data-testid=multi-action-menu]"); + + expect(multiActionMenu.exists()).toBe(true); + }); + + it("should render selected members remove button", async () => { + const { wrapper } = setup(); + + await selectCheckboxes([1], wrapper); + + const removeButton = wrapper.findComponent({ + ref: "removeSelectedMembers", + }); + + expect(removeButton.exists()).toBe(true); + }); + + it("should render selected members reset button", async () => { + const { wrapper } = setup(); + + await selectCheckboxes([1, 2], wrapper); + + const resetButton = wrapper.findComponent({ + ref: "resetSelectedMembers", + }); + + expect(resetButton.exists()).toBe(true); + }); + + it("should reset member selection when clicking reset button", async () => { + const { wrapper } = setup(); + + askConfirmationMock.mockResolvedValue(false); + + await selectCheckboxes([0], wrapper); + + const resetButton = wrapper.findComponent({ + ref: "resetSelectedMembers", + }); + await resetButton.trigger("click"); + + const checkboxes = wrapper + .getComponent(VDataTable) + .findAll("input[type='checkbox']"); + + const checkedIndices = getCheckedIndices(checkboxes); + + expect(checkedIndices).toEqual([]); + }); + + it.each([ + { + description: "one member", + checkboxesToSelect: [1], + }, + { + description: "multiple members", + checkboxesToSelect: [1, 2], + }, + ])( + "should render number of selected users in multi action menu, when $description selected", + async ({ checkboxesToSelect }) => { + const { wrapper } = setup(); + + await selectCheckboxes(checkboxesToSelect, wrapper); + + const multiActionMenu = wrapper.get("[data-testid=multi-action-menu]"); + + expect(multiActionMenu.text()).toBe( + `${checkboxesToSelect.length} pages.administration.selected` + ); + } + ); + + it("should emit remove:members when selected members remove button is clicked", async () => { + const { wrapper, mockMembers } = setup(); + + askConfirmationMock.mockResolvedValue(true); + + await selectCheckboxes([1], wrapper); + + const removeButton = wrapper.findComponent({ + ref: "removeSelectedMembers", + }); + await removeButton.trigger("click"); + + const removeEvents = wrapper.emitted("remove:members"); + expect(removeEvents).toHaveLength(1); + expect(removeEvents![0]).toEqual([[mockMembers[0].userId]]); + }); + + it("should not emit remove:members event when remove was cancled", async () => { + const { wrapper } = setup(); + + askConfirmationMock.mockResolvedValue(false); - expect(dataTable).toBeTruthy(); - expect(dataTable.vm.items).toEqual(mockMembers); - expect(dataTable.vm.headers).toEqual(wrapperVM.tableHeader); - expect(dataTable.vm["sortAscIcon"]).toEqual(mdiMenuDown); - expect(dataTable.vm["sortDescIcon"]).toEqual(mdiMenuUp); + await selectCheckboxes([1], wrapper); + + const removeButton = wrapper.findComponent({ + ref: "removeSelectedMembers", + }); + await removeButton.trigger("click"); + + expect(wrapper.emitted()).not.toHaveProperty("remove:members"); }); - describe("when the remove button is clicked", () => { - it("should emit the remove event", async () => { + it.each([ + { + description: "single member", + checkboxesToSelect: [1], + expectedMessage: "pages.rooms.members.remove.confirmation", + }, + { + description: "multiple members", + checkboxesToSelect: [1, 2], + expectedMessage: "pages.rooms.members.multipleRemove.confirmation", + }, + ])( + "should render confirmation dialog with text for $description when remove button is clicked", + async ({ checkboxesToSelect, expectedMessage }) => { const { wrapper } = setup(); + + askConfirmationMock.mockResolvedValue(true); + + await selectCheckboxes(checkboxesToSelect, wrapper); + const removeButton = wrapper.findComponent({ - name: "v-btn", - ref: "removeMember", + ref: "removeSelectedMembers", }); + await removeButton.trigger("click"); - await removeButton.vm.$emit("click"); expect(wrapper.emitted()).toHaveProperty("remove:members"); + + expect(askConfirmationMock).toHaveBeenCalledWith({ + confirmActionLangKey: "common.actions.remove", + message: expectedMessage, + }); + } + ); + + it("should keep selection if confirmation dialog is canceled", async () => { + const { wrapper } = setup(); + + askConfirmationMock.mockResolvedValue(false); + + await selectCheckboxes([1], wrapper); + + const removeButton = wrapper.getComponent({ + ref: "removeSelectedMembers", }); + await removeButton.trigger("click"); + + const checkboxes = wrapper + .getComponent(VDataTable) + .findAll("input[type='checkbox']"); + + const checkedIndices = getCheckedIndices(checkboxes); + + expect(checkedIndices).toEqual([1]); + }); + }); + + describe("when no members are selected", () => { + it("should not render multi action menu when no members are selected", async () => { + const { wrapper } = setup(); + const multiActionMenu = wrapper.find("[data-testid=multi-action-menu]"); + + expect(multiActionMenu.exists()).toBe(false); }); - describe("multiple selection", () => { - it("should render checkBoxes", async () => { + describe("when the remove button in the user row is clicked", () => { + const triggerMemberRemoval = async ( + index: number, + wrapper: VueWrapper + ) => { + const dataTable = wrapper.getComponent(VDataTable); + const removeButton = dataTable.findComponent( + `[data-testid=remove-member-${index}]` + ); + + await removeButton.trigger("click"); + }; + + it("should open confirmation dialog with remove message for single member ", async () => { const { wrapper } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); - const checkBoxes = dataTable.findAll("tr input[type='checkbox']"); - expect(checkBoxes.length).toBeGreaterThan(0); - }); + askConfirmationMock.mockResolvedValue(true); - describe("when checkboxes are clicked", () => { - it("should set the selectedMembers", async () => { - const { wrapper, wrapperVM } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); - expect(wrapperVM.selectedMemberList.length).toStrictEqual(0); - dataTable.vm.$emit("update:modelValue", [ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - - expect(wrapperVM.selectedMemberList).toStrictEqual([ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - }); + await triggerMemberRemoval(0, wrapper); - describe("bulk remove button", () => { - it("should be visible", async () => { - const { wrapper } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); - const bulkRemoveButtonBefore = wrapper.findComponent({ - ref: "removeSelectedMembers", - }); - - expect(bulkRemoveButtonBefore.exists()).toBe(false); - dataTable.vm.$emit("update:modelValue", [ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - await flushPromises(); - const bulkRemoveButtonAfter = wrapper.findComponent({ - ref: "removeSelectedMembers", - }); - expect(bulkRemoveButtonAfter.exists()).toBe(true); - }); - - describe("when the bulk remove button is clicked", () => { - it("should emit the 'remove:members'", async () => { - const { wrapper } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); - dataTable.vm.$emit("update:modelValue", [ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - await flushPromises(); - const bulkRemoveButton = wrapper.findComponent({ - ref: "removeSelectedMembers", - }); - await bulkRemoveButton.vm.$emit("click"); - expect(wrapper.emitted()).toHaveProperty("remove:members"); - }); - }); + expect(askConfirmationMock).toHaveBeenCalledWith({ + confirmActionLangKey: "common.actions.remove", + message: "pages.rooms.members.remove.confirmation", }); + }); - describe("when reset button is clicked", () => { - it("should reset the selected members", async () => { - const { wrapper, wrapperVM } = setup(); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); - dataTable.vm.$emit("update:modelValue", [ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - await flushPromises(); - expect(wrapperVM.selectedMemberList).toStrictEqual([ - mockMembers[0].userId, - mockMembers[1].userId, - ]); - const resetButton = wrapper.findComponent({ - ref: "resetSelectedMembers", - }); - resetButton.vm.$emit("click"); - expect(wrapperVM.selectedMemberList).toStrictEqual([]); - }); - }); + it("should call remove:members event after confirmation", async () => { + const { wrapper, mockMembers } = setup(); + + askConfirmationMock.mockResolvedValue(true); + + await triggerMemberRemoval(0, wrapper); + + expect(wrapper.emitted()).toHaveProperty("remove:members"); + + const removeEvents = wrapper.emitted("remove:members"); + expect(removeEvents).toHaveLength(1); + expect(removeEvents![0]).toEqual([[mockMembers[0].userId]]); + }); + + it("should not call remove:members event when dialog is cancelled", async () => { + const { wrapper } = setup(); + + askConfirmationMock.mockResolvedValue(false); + + await triggerMemberRemoval(0, wrapper); + + expect(wrapper.emitted()).not.toHaveProperty("remove:members"); }); }); }); - describe("Search component", () => { + describe("when searching for members", () => { it("should render the search component", () => { - const { wrapper, wrapperVM } = setup(); - const search = wrapper.findComponent({ name: "v-text-field" }); + const { wrapper } = setup(); - expect(search).toBeTruthy(); - expect(search.vm["label"]).toEqual("common.labels.search"); - expect(search.vm["prependInnerIcon"]).toEqual(mdiMagnify); - expect(search.vm["vModel"]).toEqual(wrapperVM.search.value); + const search = wrapper.getComponent(VTextField); + + expect(search.props("label")).toEqual("common.labels.search"); + expect(search.props("prependInnerIcon")).toEqual(mdiMagnify); }); it("should filter the members based on the search value", async () => { - const { wrapper, wrapperVM } = setup(); - const search = wrapper.findComponent({ name: "v-text-field" }); + const { wrapper, mockMembers } = setup(); + + const search = wrapper.getComponent(VTextField); + const searchValue = mockMembers[0].firstName; + + await search.setValue(searchValue); - await search.vm.$emit("update:modelValue", mockMembers[0].firstName); - expect(wrapperVM.search).toBe(mockMembers[0].firstName); - const dataTable = wrapper.findComponent({ name: "v-data-table" }); + const dataTable = wrapper.getComponent(VDataTable); + const dataTableTextContent = dataTable.text(); - expect(dataTable.vm.search).toEqual(mockMembers[0].firstName); + expect(dataTable.props("search")).toEqual(searchValue); + expect(dataTableTextContent).toContain(mockMembers[0].firstName); + expect(dataTableTextContent).not.toContain(mockMembers[1].firstName); + expect(dataTableTextContent).not.toContain(mockMembers[2].firstName); }); }); }); diff --git a/src/modules/feature/room/RoomMembers/MembersTable.vue b/src/modules/feature/room/RoomMembers/MembersTable.vue index d227baafea..d8ba934427 100644 --- a/src/modules/feature/room/RoomMembers/MembersTable.vue +++ b/src/modules/feature/room/RoomMembers/MembersTable.vue @@ -2,10 +2,13 @@
-