From 8653fc87e199806237ca85d178d7bf51a82847ad Mon Sep 17 00:00:00 2001 From: virgilchiriac <17074330+virgilchiriac@users.noreply.github.com> Date: Mon, 24 Feb 2025 11:44:05 +0100 Subject: [PATCH 01/16] BC-9059 - fix axios call from learn-store (#3570) --- src/components/lern-store/AddContentButton.vue | 11 ++++++----- src/components/lern-store/LernstoreDetailView.vue | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/lern-store/AddContentButton.vue b/src/components/lern-store/AddContentButton.vue index 159992130f..0d20635455 100644 --- a/src/components/lern-store/AddContentButton.vue +++ b/src/components/lern-store/AddContentButton.vue @@ -62,6 +62,7 @@ import { getMediatype, getID, } from "@/utils/helpers"; +import { $axios } from "@/utils/api"; let slowAPICall; @@ -121,10 +122,10 @@ export default { url() { if (getMediatype(this.resource) === "file-h5p") { let baseUrlH5p = ""; - if (this.$axios.defaults.baseURL.includes("/api")) { - baseUrlH5p = this.$axios.defaults.baseURL.slice(0, -4); + if ($axios.defaults.baseURL.includes("/api")) { + baseUrlH5p = $axios.defaults.baseURL.slice(0, -4); } else { - baseUrlH5p = this.$axios.defaults.baseURL; + baseUrlH5p = $axios.defaults.baseURL; } return ( baseUrlH5p + @@ -153,7 +154,7 @@ export default { if (getMediatype(element) === "file-h5p") { const elementID = getID(element); if (elementID !== null) { - const baseUrlH5p = this.$axios.defaults.baseURL.slice(0, -4); + const baseUrlH5p = $axios.defaults.baseURL.slice(0, -4); elementUrl = `${baseUrlH5p}/content/${elementID}`; } else { elementUrl = null; @@ -185,7 +186,7 @@ export default { let url = element.url; if (element.merlinReference) { const requestUrl = `/v1/edu-sharing-merlinToken/?merlinReference=${element.merlinReference}`; - url = (await this.$axios.get(requestUrl)).data || element.url; + url = (await $axios.get(requestUrl)).data || element.url; } return { title: element.title, diff --git a/src/components/lern-store/LernstoreDetailView.vue b/src/components/lern-store/LernstoreDetailView.vue index 5c4a9cc709..44cc93a208 100644 --- a/src/components/lern-store/LernstoreDetailView.vue +++ b/src/components/lern-store/LernstoreDetailView.vue @@ -195,6 +195,7 @@ import { buildPageTitle } from "@/utils/pageTitle"; import { RenderHTML } from "@feature-render-html"; import { mdiCalendar, mdiClose, mdiOpenInNew, mdiPound } from "@icons/material"; import BaseLink from "../base/BaseLink"; +import { $axios } from "@/utils/api"; const DEFAULT_AUTHOR = "admin"; @@ -319,7 +320,7 @@ export default { methods: { async goToMerlinContent(merlinReference) { const requestUrl = `/v1/edu-sharing-merlinToken/?merlinReference=${merlinReference}`; - const url = (await this.$axios.get(requestUrl)).data; + const url = (await $axios.get(requestUrl)).data; window.open(url, "_blank"); }, isNotStudent(roles) { From 8f4b56a4d4b18587f21f99fe0d7a17a32391869c Mon Sep 17 00:00:00 2001 From: Murat Merdoglu <64781656+muratmerdoglu-dp@users.noreply.github.com> Date: Mon, 24 Feb 2025 12:39:33 +0100 Subject: [PATCH 02/16] BC-8569 - Handover of Owner Role (#3552) - BC-8569 - implementing handing over the ownership to another participant in the same room --- src/locales/de.ts | 34 ++- src/locales/en.ts | 38 +++- src/locales/es.ts | 37 +++- src/locales/uk.ts | 32 ++- .../roomMembers/roomMembers.composable.ts | 32 +++ .../roomMembers.composable.unit.ts | 90 ++++++++ .../room/RoomMembers/ChangeRole.unit.ts | 105 +++++++++- .../feature/room/RoomMembers/ChangeRole.vue | 195 ++++++++++++++---- .../feature/room/RoomMembers/MembersTable.vue | 21 +- .../page/room/RoomMembers.page.unit.ts | 18 ++ src/modules/page/room/RoomMembers.page.vue | 9 + src/serverApi/v3/api.ts | 104 ++++++++++ 12 files changed, 632 insertions(+), 83 deletions(-) diff --git a/src/locales/de.ts b/src/locales/de.ts index 52664575f4..01bfeead5f 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1816,23 +1816,26 @@ export default { "Hinzufügen von Teilnehmenden fehlgeschlagen.", "pages.rooms.members.error.remove": "Löschen von Teilnehmenden fehlgeschlagen.", + "pages.rooms.members.error.updateRole": + "Die Änderung der Raumberechtigung ist fehlgeschlagen.", "pages.rooms.members.infoText": "Füge Mitglieder 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": "Mitglieder hinzufügen", - "pages.rooms.members.actionMenu.ariaLabel": "Aktionsmenü für {memberName}", + "pages.rooms.members.actionMenu.ariaLabel": + "Aktionsmenü für {memberFullName}", "pages.rooms.members.changePermission": "Raumberechtigungen ändern", "pages.rooms.members.changePermission.ariaLabel": - "Berechtigung für {memberName} ändern", + "Berechtigung für {memberFullName} ändern", "pages.rooms.members.manage": "Raum-Mitglieder", - "pages.rooms.members.remove.ariaLabel": "{memberName} aus Raum entfernen", + "pages.rooms.members.remove.ariaLabel": "{memberFullName} aus Raum entfernen", "pages.rooms.members.resetSelection.ariaLabel": "Ausgewählte Mitglieder aus der Liste zurücksetzen", "pages.rooms.members.multipleRemove.ariaLabel": "Mehrere Mitglieder aus dem Raum entfernen", "pages.rooms.members.remove.confirmation": - "{memberName} wirklich aus dem Raum entfernen?", + "{memberFullName} wirklich aus dem Raum entfernen?", "pages.rooms.members.multipleRemove.confirmation": "Ausgewählte Mitglieder wirklich aus dem Raum entfernen?", "pages.rooms.members.roomPermissions.admin": "Verwalten", @@ -1843,15 +1846,30 @@ export default { "pages.rooms.members.tableHeader.schoolRole": "Schulrolle", "pages.rooms.members.tableHeader.actions": "Aktionen", "pages.rooms.members.roleChange.subTitle": - "{memberName} erhält die folgenden Berechtigungen im Raum „{roomName}”:", + "{memberFullName} erhält die folgenden Berechtigungen im Raum „{roomName}”:", "pages.rooms.members.roleChange.multipleUser.subTitle": "Die ausgewählten Mitglieder erhalten die folgenden Berechtigungen im Raum „{roomName}”:", - "pages.rooms.members.roleChange.Roomviewer.subText": + "pages.rooms.members.roleChange.Roomviewer.label": "Auf die Bereiche im Raum zugreifen und Inhalte ansehen", - "pages.rooms.members.roleChange.Roomeditor.subText": + "pages.rooms.members.roleChange.Roomeditor.label": "Inhalte erstellen und bearbeiten", - "pages.rooms.members.roleChange.Roomadmin.subText": + "pages.rooms.members.roleChange.Roomadmin.label": "Gleiche Berechtigungen wie „Bearbeiten”, zusätzlich andere Mitglieder hinzufügen, entfernen, deren Raumberechtigungen ändern sowie Raum bearbeiten", + "pages.rooms.members.roleChange.Roomowner.label": + "Gleiche Berechtigungen wie „Verwalten”, zusätzlich Raum löschen", + "pages.rooms.members.roleChange.Roomowner.label.subText": + "Achtung: Kann nur eine Person im Raum erhalten!", + "pages.rooms.members.roleChange.dialogTitle.handOver": + "Raumberechtigung „Besitzen” wirklich übertragen?", + "pages.rooms.members.roleChange.handOverBtn.text": "Übertragen", + "pages.rooms.members.handOverAlert.label": + "Diese Raumberechtigung wird an {memberFullName} übertragen.", + "pages.rooms.members.handOverAlert.label.subText": + "{currentUserFullName} verliert die Berechtigung „Besitzen” und erhält die Berechtigung „Verwalten”.", + "pages.rooms.members.handOverAlert.confirm.label": + "Bei Übertragung dieser Berechtigung an {memberFullName} verliert {currentUserFullName} das Recht, den Raum zu löschen.", + "pages.rooms.members.handOverAlert.confirm.label.subText": + "Diese Aktion kann nur von {memberFullName} rückgängig gemacht werden.", "pages.rooms.title": "Räume", "pages.taskCard.addElement": "Element hinzufügen", "pages.taskCard.deleteElement.text": diff --git a/src/locales/en.ts b/src/locales/en.ts index dac937121b..869e0bf087 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1784,23 +1784,27 @@ 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.error.updateRole": + "The change of the room permission has failed.", "pages.rooms.members.infoText": "Add members 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 members", - "pages.rooms.members.actionMenu.ariaLabel": "Action menu for {memberName}", + "pages.rooms.members.actionMenu.ariaLabel": + "Action menu for {memberFullName}", "pages.rooms.members.changePermission": "Change permissions", "pages.rooms.members.changePermission.ariaLabel": - "Change permissions for {memberName}", + "Change permissions for {memberFullName}", "pages.rooms.members.manage": "Room members", - "pages.rooms.members.remove.ariaLabel": "Remove {memberName} from the room", + "pages.rooms.members.remove.ariaLabel": + "Remove {memberFullName} from the room", "pages.rooms.members.resetSelection.ariaLabel": "Reset selected members from the list", "pages.rooms.members.multipleRemove.ariaLabel": "Remove multiple members from the room", "pages.rooms.members.remove.confirmation": - "Remove {memberName} from the room?", + "Remove {memberFullName} from the room?", "pages.rooms.members.multipleRemove.confirmation": "Remove selected members from the room?", "pages.rooms.members.roomPermissions.admin": "Administer", @@ -1811,15 +1815,29 @@ export default { "pages.rooms.members.tableHeader.schoolRole": "School Role", "pages.rooms.members.tableHeader.actions": "Actions", "pages.rooms.members.roleChange.subTitle": - "{memberName} receives the following permissions in the room “{roomName}”:", + "{memberFullName} receives the following permissions in the room “{roomName}”:", "pages.rooms.members.roleChange.multipleUser.subTitle": "The selected members will receive the following room permissions in “{roomName}”:", - "pages.rooms.members.roleChange.Roomviewer.subText": - "Access the areas in the room and view content", - "pages.rooms.members.roleChange.Roomeditor.subText": - "Create and edit content", - "pages.rooms.members.roleChange.Roomadmin.subText": + "pages.rooms.members.roleChange.Roomviewer.label": + "Access the boards in the room and view content", + "pages.rooms.members.roleChange.Roomeditor.label": "Create and edit content", + "pages.rooms.members.roleChange.Roomadmin.label": 'Same permissions as "Edit", plus add and remove other members, change their room permissions and edit the room', + "pages.rooms.members.roleChange.Roomowner.label": + "Same permissions as “ Administer”, additionally delete room", + "pages.rooms.members.roleChange.Roomowner.label.subText": + "Attention: Only one person in the room can receive this permission!", + "pages.rooms.members.roleChange.dialogTitle.handOver": + "Room permission “Ownership” really transferred?", + "pages.rooms.members.roleChange.handOverBtn.text": "Transfer ownership", + "pages.rooms.members.handOverAlert.label": + "This room's permissions are being transferred to {memberFullName}.", + "pages.rooms.members.handOverAlert.label.subText": + "{currentUserFullName} loses the “Own” permissions and receives the “Administer” permission.", + "pages.rooms.members.handOverAlert.confirm.label": + "If this permission is transferred to {memberFullName}, {currentUserFullName} loses the right to delete the room.", + "pages.rooms.members.handOverAlert.confirm.label.subText": + "This action can only be undone by {memberFullName}.", "pages.rooms.title": "Rooms", "pages.taskCard.addElement": "Add element", "pages.taskCard.deleteElement.text": diff --git a/src/locales/es.ts b/src/locales/es.ts index 4152cf2c85..fc775d832f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1833,24 +1833,27 @@ 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.error.updateRole": + "El cambio de autorizaciones en la sala ha fallado.", "pages.rooms.members.infoText": "Añadir miembros 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 miembros", "pages.rooms.members.actionMenu.ariaLabel": - "Menú de acciones para {memberName}", + "Menú de acciones para {memberFullName}", "pages.rooms.members.changePermission": "Cambiar permisos", "pages.rooms.members.changePermission.ariaLabel": - "Cambiar el permiso para {memberName}", + "Cambiar el permiso para {memberFullName}", "pages.rooms.members.manage": "Miembros de la sala", - "pages.rooms.members.remove.ariaLabel": "Eliminar {memberName} de la sala", + "pages.rooms.members.remove.ariaLabel": + "Eliminar {memberFullName} de la sala", "pages.rooms.members.resetSelection.ariaLabel": "Restablecer las miembros seleccionados de la lista", "pages.rooms.members.multipleRemove.ariaLabel": "Eliminar varios miembros de la sala", "pages.rooms.members.remove.confirmation": - "¿Eliminar {memberName} de la sala?", + "¿Eliminar {memberFullName} de la sala?", "pages.rooms.members.multipleRemove.confirmation": "¿Eliminar miembros seleccionados de la sala?", "pages.rooms.members.roles.editor": "Editor de salas", @@ -1863,15 +1866,29 @@ export default { "pages.rooms.members.tableHeader.schoolRole": "Rol en la escuela", "pages.rooms.members.tableHeader.actions": "Acciones", "pages.rooms.members.roleChange.subTitle": - "{memberName} recibe los siguientes permisos de sala en “{roomName}”:", + "{memberFullName} recibe los siguientes permisos de sala en “{roomName}”:", "pages.rooms.members.roleChange.multipleUser.subTitle": "Los miembros seleccionados recibirán los siguientes permisos de sala en “{roomName}”:", - "pages.rooms.members.roleChange.Roomviewer.subText": - "Accede a las áreas de la sala y visualiza el contenido", - "pages.rooms.members.roleChange.Roomeditor.subText": - "Crear y editar contenido", - "pages.rooms.members.roleChange.Roomadmin.subText": + "pages.rooms.members.roleChange.Roomviewer.label": + "Accede a los tableros de la sala y visualiza el contenido", + "pages.rooms.members.roleChange.Roomeditor.label": "Crear y editar contenido", + "pages.rooms.members.roleChange.Roomadmin.label": 'Los mismos permisos que "Editar", además de agregar y eliminar otros miembros, cambiar sus permisos de sala y editar la sala', + "pages.rooms.members.roleChange.Roomowner.label": + "Las mismas autorizaciones que «Administrar», además de eliminar la sala", + "pages.rooms.members.roleChange.dialogTitle.handOver": + "¿El permiso de habitación realmente se transfiere como “propiedad”?", + "pages.rooms.members.roleChange.Roomowner.label.subText": + "Atención: ¡Solo una persona en la sala puede recibir esto!", + "pages.rooms.members.roleChange.handOverBtn.text": "Transferir propiedad", + "pages.rooms.members.handOverAlert.label": + "Los permisos de esta sala se están transfiriendo a {memberFullName}.", + "pages.rooms.members.handOverAlert.label.subText": + "{currentUserFullName} pierde los permisos «Propietario» y recibe el permiso «Administrar».", + "pages.rooms.members.handOverAlert.confirm.label": + "Si este permiso se transfiere a {memberFullName}, {currentUserFullName} pierde el derecho a eliminar la sala.", + "pages.rooms.members.handOverAlert.confirm.label.subText": + "Esta acción sólo puede ser deshecha por {memberFullName}.", "pages.rooms.title": "Salas", "pages.taskCard.addElement": "Añadir artículo", "pages.taskCard.deleteElement.text": diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 990caa1194..3a14c1af58 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -1813,23 +1813,24 @@ export default { "pages.rooms.members.error.load": "Не вдалося завантажити список учасників.", "pages.rooms.members.error.add": "Не вдалося додати учасників.", "pages.rooms.members.error.remove": "Не вдалося видалити учасників.", + "pages.rooms.members.error.updateRole": "Не вдалося змінити дозвіл кімнати.", "pages.rooms.members.infoText": "Додайте члени до кімнати. Вчителі з інших шкіл можуть бути додані, якщо вони активували видимість у центральному каталозі у своєму профілі ({0}).", "pages.rooms.members.infoText.moreInformation": "більше інформації", "pages.rooms.members.label": "Учасники", "pages.rooms.members.add": "Додайте члени", - "pages.rooms.members.actionMenu.ariaLabel": "Меню дій для {memberName}", + "pages.rooms.members.actionMenu.ariaLabel": "Меню дій для {memberFullName}", "pages.rooms.members.changePermission": "Змінити дозволи", "pages.rooms.members.changePermission.ariaLabel": - "Змінити дозвіл для {memberName}", + "Змінити дозвіл для {memberFullName}", "pages.rooms.members.manage": "Учасник кімнати", - "pages.rooms.members.remove.ariaLabel": "Видалити {memberName} з кімнати", + "pages.rooms.members.remove.ariaLabel": "Видалити {memberFullName} з кімнати", "pages.rooms.members.resetSelection.ariaLabel": "Скинути вибраних членів зі списку", "pages.rooms.members.multipleRemove.ariaLabel": "Видалити кількох членів із кімнати", "pages.rooms.members.remove.confirmation": - "{memberName} буде видалено з цієї кімнати. Ви впевнені, що хочете видалити?", + "{memberFullName} буде видалено з цієї кімнати. Ви впевнені, що хочете видалити?", "pages.rooms.members.multipleRemove.confirmation": "Видалити вибраних членів із кімнати?", "pages.rooms.members.roomPermissions.admin": "Керувати", @@ -1840,15 +1841,30 @@ export default { "pages.rooms.members.tableHeader.schoolRole": "Роль у школі", "pages.rooms.members.tableHeader.actions": "Дії", "pages.rooms.members.roleChange.subTitle": - "{memberName} надаються наступні повноваження в приміщенні “{roomName}”:", + "{memberFullName} надаються наступні повноваження в приміщенні “{roomName}”:", "pages.rooms.members.roleChange.multipleUser.subTitle": "Вибрані учасники отримають такі дозволи на кімнату в “{roomName}”:", - "pages.rooms.members.roleChange.Roomviewer.subText": + "pages.rooms.members.roleChange.Roomviewer.label": "Доступ до зон кімнати та перегляд вмісту", - "pages.rooms.members.roleChange.Roomeditor.subText": + "pages.rooms.members.roleChange.Roomeditor.label": "Створюйте та редагуйте контент", - "pages.rooms.members.roleChange.Roomadmin.subText": + "pages.rooms.members.roleChange.Roomadmin.label": "Такі самі дозволи, як і «Редагувати», а також додавати та видаляти інших учасників, змінювати їхні дозволи для кімнати та редагувати кімнату", + "pages.rooms.members.roleChange.Roomowner.label": + "Ті самі повноваження, що й «Керування», плюс видалення кімнати", + "pages.rooms.members.roleChange.dialogTitle.handOver": + "Дозвіл на кімнату «Власність» дійсно передано?", + "pages.rooms.members.roleChange.Roomowner.label.subText": + "Увага: Тільки одна людина в кімнаті може отримати ці повноваження!", + "pages.rooms.members.roleChange.handOverBtn.text": "Передача власності", + "pages.rooms.members.handOverAlert.label": + "Дозволи цієї кімнати передаються {memberFullName}.", + "pages.rooms.members.handOverAlert.label.subText": + "{currentUserFullName} втрачає права «Власник» і отримує права «Адміністратор».", + "pages.rooms.members.handOverAlert.confirm.label": + "Якщо цей дозвіл буде передано {memberFullName}, {currentUserFullName} втратить право видаляти кімнату.", + "pages.rooms.members.handOverAlert.confirm.label.subText": + "Ця дія може бути скасована лише {memberFullName}.", "pages.rooms.title": "Кімнати", "pages.taskCard.addElement": "Додати елемент", "pages.taskCard.deleteElement.text": diff --git a/src/modules/data/room/roomMembers/roomMembers.composable.ts b/src/modules/data/room/roomMembers/roomMembers.composable.ts index a87cfc82bb..bf642c5284 100644 --- a/src/modules/data/room/roomMembers/roomMembers.composable.ts +++ b/src/modules/data/room/roomMembers/roomMembers.composable.ts @@ -193,8 +193,40 @@ export const useRoomMembers = (roomId: string) => { } }; + const changeRoomOwner = async (userId: string) => { + try { + await roomApi.roomControllerChangeRoomOwner(roomId, { userId }); + setRoomOwner(userId); + } catch { + showFailure(t("pages.rooms.members.error.updateRole")); + } + }; + + const setRoomOwner = async (userId: string) => { + const currentOwner = roomMembers.value.find( + (member) => member.roomRoleName === RoleName.Roomowner + ); + const memberToBeOwner = roomMembers.value.find( + (member) => member.userId === userId + ); + if (!currentOwner || !memberToBeOwner) { + showFailure(t("pages.rooms.members.error.updateRole")); + return; + } + + updateMemberRole(memberToBeOwner, RoleName.Roomowner); + updateMemberRole(currentOwner, RoleName.Roomadmin); + }; + + const updateMemberRole = (member: RoomMember, roleName: RoleName) => { + member.roomRoleName = roleName; + member.displayRoomRole = roomRole[roleName]; + member.isSelectable = false; + }; + return { addMembers, + changeRoomOwner, fetchMembers, getPotentialMembers, getSchools, diff --git a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts index 926fc21440..1fc5d18933 100644 --- a/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts +++ b/src/modules/data/room/roomMembers/roomMembers.composable.unit.ts @@ -226,6 +226,28 @@ describe("useRoomMembers", () => { }); }); + describe("currentUser", () => { + it("should set the currentUser", async () => { + const mockMe = meResponseFactory.build(); + const membersMock = roomMemberFactory(RoleName.Roomviewer).buildList(3); + authModule.setMe({ + ...mockMe, + user: { ...mockMe.user, id: membersMock[1].userId }, + }); + membersMock[1].roomRoleName = RoleName.Roomowner; + roomApiMock.roomControllerGetMembers.mockResolvedValue( + mockApiResponse({ + data: { data: membersMock }, + }) + ); + + const { roomMembers, currentUser } = useRoomMembers(roomId); + roomMembers.value = membersMock; + + expect(currentUser.value).toEqual(membersMock[1]); + }); + }); + describe("getSchools", () => { it("should get schools", async () => { const { getSchools, schools } = useRoomMembers(roomId); @@ -435,4 +457,72 @@ describe("useRoomMembers", () => { ); }); }); + + describe("changeRoomOwner", () => { + it("should call the API", async () => { + const { changeRoomOwner, roomMembers } = useRoomMembers(roomId); + + const membersMock = roomMemberFactory(RoleName.Roomviewer).buildList(3); + roomMembers.value = membersMock; + + await changeRoomOwner(membersMock[1].userId); + + expect(roomApiMock.roomControllerChangeRoomOwner).toHaveBeenCalledWith( + roomId, + { + userId: membersMock[1].userId, + } + ); + }); + + it("should swap the ownership in the state", async () => { + const { changeRoomOwner, roomMembers } = useRoomMembers(roomId); + + const roomViewers = roomMemberFactory(RoleName.Roomviewer).buildList(3); + const roomOwner = roomMemberFactory(RoleName.Roomowner).build(); + const futureRoomOwner = roomViewers.pop(); + if (futureRoomOwner) { + roomMembers.value = [roomOwner, futureRoomOwner, ...roomViewers]; + } + + expect(roomOwner.roomRoleName).toBe(RoleName.Roomowner); + expect(futureRoomOwner?.roomRoleName).toBe(RoleName.Roomviewer); + + await changeRoomOwner(futureRoomOwner?.userId ?? ""); + + expect(roomOwner.roomRoleName).toBe(RoleName.Roomadmin); + expect(futureRoomOwner?.roomRoleName).toBe(RoleName.Roomowner); + }); + + it('should show an error if the "currentOwner" or "memberToBeOwner" is not found', async () => { + const { changeRoomOwner } = useRoomMembers(roomId); + + const membersMock = roomMemberFactory(RoleName.Roomviewer).buildList(3); + const futureRoomOwner = membersMock.pop(); + if (futureRoomOwner) { + roomApiMock.roomControllerChangeRoomOwner.mockResolvedValue( + mockApiResponse({}) + ); + } + + await changeRoomOwner(futureRoomOwner?.userId ?? ""); + + expect(mockedBoardNotifierCalls.showFailure).toHaveBeenCalledWith( + "pages.rooms.members.error.updateRole" + ); + }); + + it("should throw an error if the API call fails", async () => { + const { changeRoomOwner } = useRoomMembers(roomId); + + const error = new Error("Test error"); + roomApiMock.roomControllerChangeRoomOwner.mockRejectedValue(error); + + await changeRoomOwner("test-id"); + + expect(mockedBoardNotifierCalls.showFailure).toHaveBeenCalledWith( + "pages.rooms.members.error.updateRole" + ); + }); + }); }); diff --git a/src/modules/feature/room/RoomMembers/ChangeRole.unit.ts b/src/modules/feature/room/RoomMembers/ChangeRole.unit.ts index 72e41f66a6..ce525f550f 100644 --- a/src/modules/feature/room/RoomMembers/ChangeRole.unit.ts +++ b/src/modules/feature/room/RoomMembers/ChangeRole.unit.ts @@ -7,13 +7,16 @@ import { createTestingVuetify, } from "@@/tests/test-utils/setup"; import { RoomMember } from "@data-room"; +import { nextTick } from "vue"; describe("ChangeRole.vue", () => { const members = [ - roomMemberFactory(RoleName.Roomviewer).build(), roomMemberFactory(RoleName.Roomadmin).build(), + roomMemberFactory(RoleName.Roomviewer).build(), ]; + const currentUser = roomMemberFactory(RoleName.Roomowner).build(); + const roomName = "Test Room"; const setup = (options?: { members: RoomMember[] }) => { @@ -24,6 +27,7 @@ describe("ChangeRole.vue", () => { props: { members: options?.members || members, roomName, + currentUser, }, }); }; @@ -60,6 +64,77 @@ describe("ChangeRole.vue", () => { ); expect(wrapper.text()).toContain("pages.rooms.members.changePermission"); }); + + describe("when the current user is the owner and only one member selected", () => { + it("should render the 'Own' option", () => { + const wrapper = setup({ members: [members[0]] }); + + const radioButtons = wrapper.findAllComponents({ name: "v-radio" }); + expect(radioButtons.length).toBe(4); + }); + + it("should render 'Alert' component", async () => { + const wrapper = setup({ members: [members[0]] }); + + const alertElementBefore = wrapper.findComponent({ name: "v-alert" }); + expect(alertElementBefore.exists()).toBe(false); + + const radioGroup = wrapper.findComponent({ + name: "v-radio-group", + }); + radioGroup.setValue(RoleName.Roomowner); + await nextTick(); + + const alertElementAfter = wrapper.findComponent({ name: "v-alert" }); + expect(alertElementAfter.exists()).toBe(true); + expect(alertElementAfter.text()).toContain( + "pages.rooms.members.handOverAlert.label" + ); + }); + + describe("when the confirm button clicked", () => { + it("should not render the radio buttons", async () => { + const wrapper = setup({ members: [members[0]] }); + + const radioGroup = wrapper.findComponent({ + name: "v-radio-group", + }); + radioGroup.setValue(RoleName.Roomowner); + await nextTick(); + + const confirmButton = wrapper.find( + "[data-testid='change-role-confirm-btn']" + ); + await confirmButton.trigger("click"); + + const radioButtons = wrapper.findAllComponents({ name: "v-radio" }); + expect(radioButtons.length).toBe(0); + }); + + it("should show the confirmation alert and 'Handover' button", async () => { + const wrapper = setup({ members: [members[0]] }); + + const radioGroup = wrapper.findComponent({ + name: "v-radio-group", + }); + radioGroup.setValue(RoleName.Roomowner); + await nextTick(); + + const confirmButton = wrapper.find( + "[data-testid='change-role-confirm-btn']" + ); + await confirmButton.trigger("click"); + + const alertElementAfter = wrapper.findComponent({ + name: "v-alert", + }); + expect(alertElementAfter.exists()).toBe(true); + expect(alertElementAfter.text()).toContain( + "pages.rooms.members.handOverAlert.confirm.label" + ); + }); + }); + }); }); describe("@selectedRole", () => { @@ -109,7 +184,7 @@ describe("ChangeRole.vue", () => { }); it("should emit 'confirm' event when confirm button is clicked", async () => { - const wrapper = setup({ members: [members[0]] }); + const wrapper = setup({ members: [members[1]] }); await wrapper .find("[data-testid='change-role-confirm-btn']") .trigger("click"); @@ -120,5 +195,31 @@ describe("ChangeRole.vue", () => { members[0].userId, ]); }); + + it("should emit 'change-room-owner' when the Handover button is clicked", async () => { + const wrapper = setup({ members: [members[0]] }); + + const radioGroup = wrapper.findComponent({ + name: "v-radio-group", + }); + radioGroup.setValue(RoleName.Roomowner); + await nextTick(); + + const confirmButton = wrapper.find( + "[data-testid='change-role-confirm-btn']" + ); + await confirmButton.trigger("click"); + + const handOverButton = wrapper.find( + "[data-testid='change-owner-confirm-btn']" + ); + + expect(handOverButton.exists()).toBe(true); + await handOverButton.trigger("click"); + + const emitted = wrapper.emitted(); + expect(emitted).toHaveProperty("change-room-owner"); + expect(emitted["change-room-owner"][0]).toEqual([members[0].userId]); + }); }); }); diff --git a/src/modules/feature/room/RoomMembers/ChangeRole.vue b/src/modules/feature/room/RoomMembers/ChangeRole.vue index 21eb79186a..4beae7a876 100644 --- a/src/modules/feature/room/RoomMembers/ChangeRole.vue +++ b/src/modules/feature/room/RoomMembers/ChangeRole.vue @@ -1,48 +1,86 @@ @@ -82,6 +128,7 @@ import { import { useFocusTrap } from "@vueuse/integrations/useFocusTrap"; import { VCard, VRadio } from "vuetify/lib/components/index.mjs"; import { RoomMember } from "@data-room"; +import { WarningAlert } from "@ui-alert"; const props = defineProps({ members: { @@ -93,10 +140,26 @@ const props = defineProps({ type: String, required: true, }, + currentUser: { + type: Object as PropType, + required: true, + }, }); const { t } = useI18n(); const selectedRole = ref(null); const memberToChangeRole = toRef(props, "members")?.value; +const isChangeOwnershipOptionVisible = computed(() => { + return ( + props.currentUser?.roomRoleName === RoleName.Roomowner && + memberToChangeRole.length === 1 + ); +}); +const isOwnershipHandoverMode = ref(false); +const dialogTitle = computed(() => + isOwnershipHandoverMode.value + ? t("pages.rooms.members.roleChange.dialogTitle.handOver") + : t("pages.rooms.members.changePermission") +); if (memberToChangeRole.length > 1) { const roleNamesInProp = memberToChangeRole.map( @@ -110,11 +173,18 @@ if (memberToChangeRole.length > 1) { selectedRole.value = memberToChangeRole[0]?.roomRoleName; } +const currentUserFullName = computed(() => { + return `${props.currentUser?.firstName} ${props.currentUser?.lastName}`; +}); + +const memberFullName = computed(() => { + return `${memberToChangeRole[0]?.firstName} ${memberToChangeRole[0]?.lastName}`; +}); + const infoText = computed(() => { if (memberToChangeRole.length === 1) { - const memberName = `${memberToChangeRole[0]?.firstName} ${memberToChangeRole[0]?.lastName}`; return t("pages.rooms.members.roleChange.subTitle", { - memberName, + memberFullName: memberFullName.value, roomName: props.roomName, }); } @@ -125,11 +195,16 @@ const infoText = computed(() => { const emit = defineEmits<{ (e: "confirm", selectedRole: RoleEnum, id?: string): void; + (e: "change-room-owner", id: string): void; (e: "cancel"): void; }>(); const onConfirm = () => { if (!selectedRole.value) return; + if (selectedRole.value === RoleName.Roomowner) { + isOwnershipHandoverMode.value = true; + return; + } emit( "confirm", selectedRole.value as RoleEnum, @@ -137,10 +212,47 @@ const onConfirm = () => { ); }; +const onChangeOwner = () => { + emit("change-room-owner", memberToChangeRole[0].userId); +}; + const onCancel = () => { emit("cancel"); }; +const radioOptions = computed(() => { + const baseRoles = [ + { + role: RoleName.Roomviewer, + labelHeader: t("pages.rooms.members.roomPermissions.viewer"), + labelDescriptions: ["pages.rooms.members.roleChange.Roomviewer.label"], + }, + { + role: RoleName.Roomeditor, + labelHeader: t("pages.rooms.members.roomPermissions.editor"), + labelDescriptions: ["pages.rooms.members.roleChange.Roomeditor.label"], + }, + { + role: RoleName.Roomadmin, + labelHeader: t("pages.rooms.members.roomPermissions.admin"), + labelDescriptions: ["pages.rooms.members.roleChange.Roomadmin.label"], + }, + ]; + + if (isChangeOwnershipOptionVisible.value) { + baseRoles.push({ + role: RoleName.Roomowner, + labelHeader: t("pages.rooms.members.roomPermissions.owner"), + labelDescriptions: [ + "pages.rooms.members.roleChange.Roomowner.label", + "pages.rooms.members.roleChange.Roomowner.label.subText", + ], + }); + } + + return baseRoles; +}); + const changeRoleContent = ref(); useFocusTrap(changeRoleContent, { immediate: true, @@ -148,9 +260,16 @@ useFocusTrap(changeRoleContent, { diff --git a/src/modules/feature/room/RoomMembers/MembersTable.vue b/src/modules/feature/room/RoomMembers/MembersTable.vue index ee27d6c466..0d7d5791c8 100644 --- a/src/modules/feature/room/RoomMembers/MembersTable.vue +++ b/src/modules/feature/room/RoomMembers/MembersTable.vue @@ -79,7 +79,7 @@ import { KebabMenuActionChangePermission, KebabMenuActionRemoveMember, } from "@ui-kebab-menu"; -import { computed, PropType, ref, toRef } from "vue"; +import { computed, PropType, ref, toRef, watch } from "vue"; import { useI18n } from "vue-i18n"; import { mdiMenuDown, mdiMenuUp, mdiMagnify } from "@icons/material"; import { @@ -107,7 +107,14 @@ const props = defineProps({ }, }); const { askConfirmation } = useConfirmationDialog(); -const tableSelectedUserIds = computed(() => props.selectedUserIds); +const tableSelectedUserIds = ref([]); + +watch( + () => props.selectedUserIds, + (newVal: string[]) => { + tableSelectedUserIds.value = newVal; + } +); const emit = defineEmits<{ (e: "remove:members", userIds: string[]): void; @@ -155,7 +162,7 @@ const confirmRemoval = async (userIds: string[]) => { (member) => member.userId === userIds[0] ); message = t("pages.rooms.members.remove.confirmation", { - memberName: `${member?.firstName} ${member?.lastName}`, + memberFullName: `${member?.firstName} ${member?.lastName}`, }); } const shouldRemove = await askConfirmation({ @@ -169,19 +176,19 @@ const getAriaLabel = ( member: RoomMember, actionFor?: "remove" | "changeRole" ) => { - const memberName = `${member.firstName} ${member.lastName}`; + const memberFullName = `${member.firstName} ${member.lastName}`; if (actionFor === "changeRole") { return t("pages.rooms.members.changePermission.ariaLabel", { - memberName, + memberFullName, }); } if (actionFor === "remove") { return t("pages.rooms.members.remove.ariaLabel", { - memberName, + memberFullName, }); } return t("pages.rooms.members.actionMenu.ariaLabel", { - memberName, + memberFullName, }); }; diff --git a/src/modules/page/room/RoomMembers.page.unit.ts b/src/modules/page/room/RoomMembers.page.unit.ts index 32fb6adf2b..903351f466 100644 --- a/src/modules/page/room/RoomMembers.page.unit.ts +++ b/src/modules/page/room/RoomMembers.page.unit.ts @@ -313,6 +313,24 @@ describe("RoomMembersPage", () => { }); }); + describe("onChangeOwner", () => { + it("should call 'updateOwner' method", async () => { + mockRoomMemberCalls.isLoading = ref(false); + const { wrapper, members } = setup(); + await nextTick(); + + const membersTable = wrapper.findComponent(MembersTable); + membersTable.vm.$emit("change:permission", [members[1].userId]); + await nextTick(); + + const changeRoleDialog = wrapper.findComponent(ChangeRole); + changeRoleDialog.vm.$emit("change-room-owner", members[1].userId); + expect(mockRoomMemberCalls.changeRoomOwner).toHaveBeenCalledWith( + members[1].userId + ); + }); + }); + describe("visibility options", () => { describe("title menu visibility", () => { it.each([ diff --git a/src/modules/page/room/RoomMembers.page.vue b/src/modules/page/room/RoomMembers.page.vue index 8bf2f38d30..8e08b54393 100644 --- a/src/modules/page/room/RoomMembers.page.vue +++ b/src/modules/page/room/RoomMembers.page.vue @@ -79,8 +79,10 @@ @@ -134,6 +136,7 @@ const { currentUser, selectedIds, addMembers, + changeRoomOwner, fetchMembers, getPotentialMembers, getSchools, @@ -222,6 +225,12 @@ const onSelectMembers = (userIds: string[]) => { selectedIds.value = userIds; }; +const onChangeOwner = async (id: string) => { + await changeRoomOwner(id); + isChangeRoleDialogOpen.value = false; + selectedIds.value = []; +}; + onMounted(async () => { if (room.value === undefined) { await fetchRoom(roomId); diff --git a/src/serverApi/v3/api.ts b/src/serverApi/v3/api.ts index c8d501781f..64adb382c4 100644 --- a/src/serverApi/v3/api.ts +++ b/src/serverApi/v3/api.ts @@ -6777,6 +6777,19 @@ export interface ParentConsentResponse { */ _id: string; } +/** + * + * @export + * @interface PassOwnershipBodyParams + */ +export interface PassOwnershipBodyParams { + /** + * The IDs of the users + * @type {string} + * @memberof PassOwnershipBodyParams + */ + userId: string; +} /** * * @export @@ -20924,6 +20937,50 @@ export const RoomApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @summary Passes the ownership of the room to another user. Can only be used if you are the owner, and you will loose the ownership and become a roomadmin instead. + * @param {string} roomId + * @param {PassOwnershipBodyParams} passOwnershipBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + roomControllerChangeRoomOwner: async (roomId: string, passOwnershipBodyParams: PassOwnershipBodyParams, options: any = {}): Promise => { + // verify required parameter 'roomId' is not null or undefined + assertParamExists('roomControllerChangeRoomOwner', 'roomId', roomId) + // verify required parameter 'passOwnershipBodyParams' is not null or undefined + assertParamExists('roomControllerChangeRoomOwner', 'passOwnershipBodyParams', passOwnershipBodyParams) + const localVarPath = `/rooms/{roomId}/members/pass-ownership` + .replace(`{${"roomId"}}`, encodeURIComponent(String(roomId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(passOwnershipBodyParams, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Create a new room @@ -21320,6 +21377,18 @@ export const RoomApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.roomControllerChangeRolesOfMembers(roomId, changeRoomRoleBodyParams, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Passes the ownership of the room to another user. Can only be used if you are the owner, and you will loose the ownership and become a roomadmin instead. + * @param {string} roomId + * @param {PassOwnershipBodyParams} passOwnershipBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async roomControllerChangeRoomOwner(roomId: string, passOwnershipBodyParams: PassOwnershipBodyParams, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.roomControllerChangeRoomOwner(roomId, passOwnershipBodyParams, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Create a new room @@ -21454,6 +21523,17 @@ export const RoomApiFactory = function (configuration?: Configuration, basePath? roomControllerChangeRolesOfMembers(roomId: string, changeRoomRoleBodyParams: ChangeRoomRoleBodyParams, options?: any): AxiosPromise { return localVarFp.roomControllerChangeRolesOfMembers(roomId, changeRoomRoleBodyParams, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Passes the ownership of the room to another user. Can only be used if you are the owner, and you will loose the ownership and become a roomadmin instead. + * @param {string} roomId + * @param {PassOwnershipBodyParams} passOwnershipBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + roomControllerChangeRoomOwner(roomId: string, passOwnershipBodyParams: PassOwnershipBodyParams, options?: any): AxiosPromise { + return localVarFp.roomControllerChangeRoomOwner(roomId, passOwnershipBodyParams, options).then((request) => request(axios, basePath)); + }, /** * * @summary Create a new room @@ -21578,6 +21658,17 @@ export interface RoomApiInterface { */ roomControllerChangeRolesOfMembers(roomId: string, changeRoomRoleBodyParams: ChangeRoomRoleBodyParams, options?: any): AxiosPromise; + /** + * + * @summary Passes the ownership of the room to another user. Can only be used if you are the owner, and you will loose the ownership and become a roomadmin instead. + * @param {string} roomId + * @param {PassOwnershipBodyParams} passOwnershipBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof RoomApiInterface + */ + roomControllerChangeRoomOwner(roomId: string, passOwnershipBodyParams: PassOwnershipBodyParams, options?: any): AxiosPromise; + /** * * @summary Create a new room @@ -21706,6 +21797,19 @@ export class RoomApi extends BaseAPI implements RoomApiInterface { return RoomApiFp(this.configuration).roomControllerChangeRolesOfMembers(roomId, changeRoomRoleBodyParams, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Passes the ownership of the room to another user. Can only be used if you are the owner, and you will loose the ownership and become a roomadmin instead. + * @param {string} roomId + * @param {PassOwnershipBodyParams} passOwnershipBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof RoomApi + */ + public roomControllerChangeRoomOwner(roomId: string, passOwnershipBodyParams: PassOwnershipBodyParams, options?: any) { + return RoomApiFp(this.configuration).roomControllerChangeRoomOwner(roomId, passOwnershipBodyParams, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Create a new room From 4fec300c6ddf0752079fbdac67bf82a74190fc0a Mon Sep 17 00:00:00 2001 From: NFriedo <69233063+NFriedo@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:05:00 +0100 Subject: [PATCH 03/16] BC-8549 - Improve change-role-option single/batch action (#3550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the change-role option on the room members page. It also improves accessibility of all menus that use KebabMenu/KebabMenuList, by focussing the first menu item when opening the menu via keyboard/screenreader. *small translation adjustments for all languages, using "room permissions" instead of just "permission" *change spanish language keys to use "autorización" instead of "permisos" * focus first element in kebab menu * focus first item board menu * focus first item in multi action menu * fix action menu on display sizes<600px for extra small displays --- src/locales/de.ts | 2 +- src/locales/en.ts | 4 +- src/locales/es.ts | 21 +- src/locales/uk.ts | 4 +- .../feature/room/RoomMembers/ActionMenu.vue | 19 +- .../room/RoomMembers/MembersTable.unit.ts | 421 ++++++++++-------- .../feature/room/RoomMembers/MembersTable.vue | 43 +- .../page/room/RoomMembers.page.unit.ts | 76 ++++ src/modules/page/room/RoomMembers.page.vue | 9 +- src/modules/ui/board/BoardMenu.vue | 9 +- src/modules/ui/kebab-menu/KebabMenu.vue | 11 +- src/modules/ui/kebab-menu/KebabMenuAction.vue | 4 +- .../ui/kebab-menu/KebabMenuList.unit.ts | 57 +++ src/modules/ui/kebab-menu/KebabMenuList.vue | 19 + src/modules/ui/kebab-menu/index.ts | 2 + 15 files changed, 441 insertions(+), 260 deletions(-) create mode 100644 src/modules/ui/kebab-menu/KebabMenuList.unit.ts create mode 100644 src/modules/ui/kebab-menu/KebabMenuList.vue diff --git a/src/locales/de.ts b/src/locales/de.ts index 01bfeead5f..9b39aa92a4 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1827,7 +1827,7 @@ export default { "Aktionsmenü für {memberFullName}", "pages.rooms.members.changePermission": "Raumberechtigungen ändern", "pages.rooms.members.changePermission.ariaLabel": - "Berechtigung für {memberFullName} ändern", + "Raumberechtigungen für {memberFullName} ändern", "pages.rooms.members.manage": "Raum-Mitglieder", "pages.rooms.members.remove.ariaLabel": "{memberFullName} aus Raum entfernen", "pages.rooms.members.resetSelection.ariaLabel": diff --git a/src/locales/en.ts b/src/locales/en.ts index 869e0bf087..ab038a8f48 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1793,9 +1793,9 @@ export default { "pages.rooms.members.add": "Add members", "pages.rooms.members.actionMenu.ariaLabel": "Action menu for {memberFullName}", - "pages.rooms.members.changePermission": "Change permissions", + "pages.rooms.members.changePermission": "Change room permissions", "pages.rooms.members.changePermission.ariaLabel": - "Change permissions for {memberFullName}", + "Change room permissions for {memberFullName}", "pages.rooms.members.manage": "Room members", "pages.rooms.members.remove.ariaLabel": "Remove {memberFullName} from the room", diff --git a/src/locales/es.ts b/src/locales/es.ts index fc775d832f..aba2ce1403 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1842,9 +1842,10 @@ export default { "pages.rooms.members.add": "Añadir miembros", "pages.rooms.members.actionMenu.ariaLabel": "Menú de acciones para {memberFullName}", - "pages.rooms.members.changePermission": "Cambiar permisos", + "pages.rooms.members.changePermission": + "Cambiar las autorizaciones en la sala", "pages.rooms.members.changePermission.ariaLabel": - "Cambiar el permiso para {memberFullName}", + "Cambiar autorizaciones de sala para {memberFullName}", "pages.rooms.members.manage": "Miembros de la sala", "pages.rooms.members.remove.ariaLabel": "Eliminar {memberFullName} de la sala", @@ -1862,31 +1863,31 @@ export default { "pages.rooms.members.roomPermissions.owner": "Propietario", "pages.rooms.members.roomPermissions.editor": "Editar", "pages.rooms.members.roomPermissions.viewer": "Leer", - "pages.rooms.members.tableHeader.roomRole": "Permisos de la sala", + "pages.rooms.members.tableHeader.roomRole": "Autorizaciones de sala", "pages.rooms.members.tableHeader.schoolRole": "Rol en la escuela", "pages.rooms.members.tableHeader.actions": "Acciones", "pages.rooms.members.roleChange.subTitle": - "{memberFullName} recibe los siguientes permisos de sala en “{roomName}”:", + "{memberFullName} recibe las siguientes autorizaciones en la sala “{roomName}”:", "pages.rooms.members.roleChange.multipleUser.subTitle": - "Los miembros seleccionados recibirán los siguientes permisos de sala en “{roomName}”:", + "Los miembros seleccionados reciben las siguientes autorizaciones en la sala “{roomName}”:", "pages.rooms.members.roleChange.Roomviewer.label": "Accede a los tableros de la sala y visualiza el contenido", "pages.rooms.members.roleChange.Roomeditor.label": "Crear y editar contenido", "pages.rooms.members.roleChange.Roomadmin.label": - 'Los mismos permisos que "Editar", además de agregar y eliminar otros miembros, cambiar sus permisos de sala y editar la sala', + 'Las mismas autorizaciones que en "Editar", además de añadir y eliminar otros miembros, cambiar sus autorizaciones de sala y editar salas.', "pages.rooms.members.roleChange.Roomowner.label": "Las mismas autorizaciones que «Administrar», además de eliminar la sala", "pages.rooms.members.roleChange.dialogTitle.handOver": - "¿El permiso de habitación realmente se transfiere como “propiedad”?", + "¿Realmente se transfieren las autorizaciones de “Propietario” de salas?", "pages.rooms.members.roleChange.Roomowner.label.subText": "Atención: ¡Solo una persona en la sala puede recibir esto!", "pages.rooms.members.roleChange.handOverBtn.text": "Transferir propiedad", "pages.rooms.members.handOverAlert.label": - "Los permisos de esta sala se están transfiriendo a {memberFullName}.", + "Esta autorización de sala se transfiere a {memberFullName}.", "pages.rooms.members.handOverAlert.label.subText": - "{currentUserFullName} pierde los permisos «Propietario» y recibe el permiso «Administrar».", + "{currentUserFullName} pierde la autorización «Propietario» y gana la autorización «Administrar».", "pages.rooms.members.handOverAlert.confirm.label": - "Si este permiso se transfiere a {memberFullName}, {currentUserFullName} pierde el derecho a eliminar la sala.", + "Si esta autorización se transfiere a {memberFullName}, {currentUserFullName} pierde el derecho a eliminar la sala.", "pages.rooms.members.handOverAlert.confirm.label.subText": "Esta acción sólo puede ser deshecha por {memberFullName}.", "pages.rooms.title": "Salas", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 3a14c1af58..088955df75 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -1820,9 +1820,9 @@ export default { "pages.rooms.members.label": "Учасники", "pages.rooms.members.add": "Додайте члени", "pages.rooms.members.actionMenu.ariaLabel": "Меню дій для {memberFullName}", - "pages.rooms.members.changePermission": "Змінити дозволи", + "pages.rooms.members.changePermission": "Змінити дозволи кімнат", "pages.rooms.members.changePermission.ariaLabel": - "Змінити дозвіл для {memberFullName}", + "Змінити дозвіл кімнат для {memberFullName}", "pages.rooms.members.manage": "Учасник кімнати", "pages.rooms.members.remove.ariaLabel": "Видалити {memberFullName} з кімнати", "pages.rooms.members.resetSelection.ariaLabel": diff --git a/src/modules/feature/room/RoomMembers/ActionMenu.vue b/src/modules/feature/room/RoomMembers/ActionMenu.vue index 1a68797949..4f6f09eef7 100644 --- a/src/modules/feature/room/RoomMembers/ActionMenu.vue +++ b/src/modules/feature/room/RoomMembers/ActionMenu.vue @@ -1,13 +1,13 @@ - - - + @@ -31,6 +27,7 @@ import type { Slot, VNode } from "vue"; import { Comment, Fragment } from "vue"; import { mdiDotsVertical } from "@icons/material"; +import { KebabMenuList } from "@ui-kebab-menu"; const isVnodeEmpty = (vnodes: Array) => { return vnodes.every((node: VNode) => { diff --git a/src/modules/ui/kebab-menu/KebabMenuAction.vue b/src/modules/ui/kebab-menu/KebabMenuAction.vue index 83b47a71e5..ed5901b278 100644 --- a/src/modules/ui/kebab-menu/KebabMenuAction.vue +++ b/src/modules/ui/kebab-menu/KebabMenuAction.vue @@ -5,9 +5,7 @@ :aria-label="ariaLabel" > diff --git a/src/modules/ui/kebab-menu/KebabMenuList.unit.ts b/src/modules/ui/kebab-menu/KebabMenuList.unit.ts new file mode 100644 index 0000000000..7b316165f3 --- /dev/null +++ b/src/modules/ui/kebab-menu/KebabMenuList.unit.ts @@ -0,0 +1,57 @@ +import { + createTestingI18n, + createTestingVuetify, +} from "@@/tests/test-utils/setup"; +import { KebabMenuList } from "@ui-kebab-menu"; +import { VList, VListItem } from "vuetify/lib/components/index.mjs"; + +describe("KebabMenuList", () => { + const setup = () => { + const wrapper = mount(KebabMenuList, { + attachTo: document.body, + global: { + plugins: [createTestingVuetify(), createTestingI18n()], + }, + slots: { + default: + 'Item 1Item 2', + }, + }); + + return wrapper; + }; + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe("when component is mounted", () => { + it("should render", () => { + const wrapper = setup(); + + expect(wrapper.exists()).toBe(true); + expect(wrapper.findComponent(VList).exists()).toBe(true); + }); + + it("should have aria role menu", () => { + const wrapper = setup(); + const menuList = wrapper.findComponent(VList); + + expect(menuList.attributes("role")).toBe("menu"); + }); + + it("should focus the first menu item", () => { + const wrapper = setup(); + const menuList = wrapper.findComponent(VList); + + jest.runAllTimers(); + + const firstMenuItem = menuList.findAllComponents(VListItem)[0].element; + + expect(document.activeElement).toBe(firstMenuItem); + }); + }); +}); diff --git a/src/modules/ui/kebab-menu/KebabMenuList.vue b/src/modules/ui/kebab-menu/KebabMenuList.vue new file mode 100644 index 0000000000..dab6c98f81 --- /dev/null +++ b/src/modules/ui/kebab-menu/KebabMenuList.vue @@ -0,0 +1,19 @@ + + diff --git a/src/modules/ui/kebab-menu/index.ts b/src/modules/ui/kebab-menu/index.ts index 884565edcd..53e9de721e 100644 --- a/src/modules/ui/kebab-menu/index.ts +++ b/src/modules/ui/kebab-menu/index.ts @@ -17,6 +17,7 @@ import KebabMenuActionShareLink from "./KebabMenuActionShareLink.vue"; import KebabMenuActionLeaveRoom from "./KebabMenuActionLeaveRoom.vue"; import KebabMenuActionChangePermission from "./KebabMenuActionChangePermission.vue"; import KebabMenuActionRemoveMember from "./KebabMenuActionRemoveMember.vue"; +import KebabMenuList from "./KebabMenuList.vue"; export { KebabMenu, @@ -38,4 +39,5 @@ export { KebabMenuActionRevert, KebabMenuActionShare, KebabMenuActionShareLink, + KebabMenuList, }; From 49d80c24f02e8c3cb5b3ac96fce231d34ae517a6 Mon Sep 17 00:00:00 2001 From: Sergej Hoffmann <97111299+SevenWaysDP@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:30:52 +0100 Subject: [PATCH 04/16] BC-9088 - Remove FileContentElement not possible --- src/modules/feature/board-file-element/FileContentElement.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/feature/board-file-element/FileContentElement.vue b/src/modules/feature/board-file-element/FileContentElement.vue index f5ed777ce2..907fddc0ab 100644 --- a/src/modules/feature/board-file-element/FileContentElement.vue +++ b/src/modules/feature/board-file-element/FileContentElement.vue @@ -30,7 +30,7 @@ From f11895c7a4bc4dea75a5923f416e4b5dcfb72dba Mon Sep 17 00:00:00 2001 From: odalys-dataport <82401838+odalys-dataport@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:11:03 +0100 Subject: [PATCH 05/16] BC-8008 - Fix any type linter warning (#3549) * adjusting types * restructure imports * fixing general any type warnings --------- Co-authored-by: wolfganggreschus Co-authored-by: Martin Schuhmacher <55735359+MartinSchuhmacher@users.noreply.github.com> --- eslint.config.js | 2 +- .../external-tool-section-utils.composable.ts | 2 +- ...rnal-tool-section-utils.composable.unit.ts | 30 +- .../error-handling/ErrorHandler.composable.ts | 11 +- .../molecules/CommonCartridgeExportModal.vue | 22 +- .../CommonCartridgeImportModal.unit.ts | 14 +- src/components/molecules/InfoBox.unit.ts | 7 +- .../molecules/TaskItemStudent.unit.ts | 13 +- src/components/molecules/TaskItemStudent.vue | 2 +- .../molecules/vImportUsersMatchSearch.unit.ts | 55 +-- .../molecules/vImportUsersMatchSearch.vue | 2 + src/components/organisms/TasksList.vue | 4 +- .../administration/ImportUsers.unit.ts | 45 +- .../organisms/administration/ImportUsers.vue | 2 +- .../templates/RoomDashboard.unit.ts | 9 +- .../templates/default-wireframe.types.ts | 2 +- src/modules/data/board/Board.store.unit.ts | 4 +- .../board/boardInactivity.composable.unit.ts | 38 +- ...laborativeTextEditorApi.composable.unit.ts | 2 +- .../FileStorageApi.composable.unit.ts | 34 +- .../board/shared/BoardAnyTitleInput.unit.ts | 68 ++- src/modules/feature/news-form/FormNews.vue | 13 +- .../ui/date-time-picker/TimePicker.vue | 12 +- .../ui/preview-image/PreviewImage.unit.ts | 19 +- src/modules/ui/preview-image/PreviewImage.vue | 1 - .../room-details/SelectBoardLayoutDialog.vue | 4 +- .../ui/speed-dial-menu/SpeedDialMenu.unit.ts | 29 +- .../SchoolSettings.page.unit.ts | 5 +- .../administration/SchoolSettings.page.vue | 4 +- .../CourseRoomDetails.page.unit.ts | 18 +- .../course-rooms/CourseRoomDetails.page.vue | 6 +- .../course-rooms/CourseRoomList.page.vue | 4 +- src/plugins/store.ts | 2 +- src/store/accounts.ts | 17 +- src/store/accounts.unit.ts | 12 +- src/store/auth.unit.ts | 2 +- src/store/autoLogout.ts | 49 +- src/store/content.ts | 2 +- src/store/content.unit.ts | 22 +- src/store/course-room-details.ts | 78 +-- src/store/course-room-details.unit.ts | 169 ++++--- src/store/course-room-list.ts | 64 ++- src/store/course-room-list.unit.ts | 178 ++++--- src/store/env-config.unit.ts | 12 +- src/store/filePaths.ts | 30 +- src/store/filePaths.unit.ts | 21 +- src/store/finished-tasks.unit.ts | 26 +- src/store/import-users.ts | 91 ++-- src/store/import-users.unit.ts | 467 +++++++----------- src/store/privacy-policy.unit.ts | 20 +- src/store/schools.ts | 10 +- src/store/schools.unit.ts | 136 ++--- src/store/store-accessor.ts | 2 +- src/store/tasks.unit.ts | 1 + src/store/terms-of-use.unit.ts | 117 +++-- src/store/types/content.ts | 12 +- src/store/types/rooms.ts | 4 +- src/types/board/Board.ts | 2 +- src/types/course-room/CourseRoom.ts | 15 + src/types/vuetify/index.ts | 8 +- 60 files changed, 1100 insertions(+), 952 deletions(-) create mode 100644 src/types/course-room/CourseRoom.ts diff --git a/eslint.config.js b/eslint.config.js index 82fa229ba2..901e9afb46 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -51,7 +51,7 @@ module.exports = [ "error", { allowInterfaces: "with-single-extends" }, ], - "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-require-imports": "warn", "@typescript-eslint/no-restricted-imports": [ diff --git a/src/components/administration/external-tool-section-utils.composable.ts b/src/components/administration/external-tool-section-utils.composable.ts index 693b9b99d9..1208603eea 100644 --- a/src/components/administration/external-tool-section-utils.composable.ts +++ b/src/components/administration/external-tool-section-utils.composable.ts @@ -11,7 +11,7 @@ export function useExternalToolsSectionUtils( t: (key: string) => string = () => "", mediaLicenseEnabled = false ) { - const getHeaders: DataTableHeader[] = [ + const getHeaders: DataTableHeader[] = [ { title: t("common.labels.name"), value: "name", diff --git a/src/components/administration/external-tool-section-utils.composable.unit.ts b/src/components/administration/external-tool-section-utils.composable.unit.ts index e79e2fbff7..e02f920aea 100644 --- a/src/components/administration/external-tool-section-utils.composable.unit.ts +++ b/src/components/administration/external-tool-section-utils.composable.unit.ts @@ -61,9 +61,7 @@ describe("useSchoolExternalToolUtils", () => { const { tool } = setupTool(); const { getHeaders } = setup(tool); - const headers: DataTableHeader[] = getHeaders; - - expect(Array.isArray(headers)).toBeTruthy(); + expect(Array.isArray(getHeaders)).toBeTruthy(); }); describe("when translate the headers", () => { @@ -97,24 +95,22 @@ describe("useSchoolExternalToolUtils", () => { const { tool } = setupTool(); const { getHeaders, expectedTranslation } = setup(tool); - const headers: DataTableHeader[] = getHeaders; - - expect(headers[0].title).toEqual(expectedTranslation); - expect(headers[0].value).toEqual("name"); + expect(getHeaders[0].title).toEqual(expectedTranslation); + expect(getHeaders[0].value).toEqual("name"); - expect(headers[1].title).toEqual(expectedTranslation); - expect(headers[1].value).toEqual("statusText"); + expect(getHeaders[1].title).toEqual(expectedTranslation); + expect(getHeaders[1].value).toEqual("statusText"); - expect(headers[2].title).toEqual(expectedTranslation); - expect(headers[2].value).toEqual("medium"); + expect(getHeaders[2].title).toEqual(expectedTranslation); + expect(getHeaders[2].value).toEqual("medium"); - expect(headers[3].title).toEqual(expectedTranslation); - expect(headers[3].value).toEqual("restrictToContexts"); + expect(getHeaders[3].title).toEqual(expectedTranslation); + expect(getHeaders[3].value).toEqual("restrictToContexts"); - expect(headers[4].title).toEqual(""); - expect(headers[4].value).toEqual("actions"); - expect(headers[4].sortable).toBe(false); - expect(headers[4].align).toEqual("end"); + expect(getHeaders[4].title).toEqual(""); + expect(getHeaders[4].value).toEqual("actions"); + expect(getHeaders[4].sortable).toBe(false); + expect(getHeaders[4].align).toEqual("end"); }); }); diff --git a/src/components/error-handling/ErrorHandler.composable.ts b/src/components/error-handling/ErrorHandler.composable.ts index aabe9af756..b089dd2338 100644 --- a/src/components/error-handling/ErrorHandler.composable.ts +++ b/src/components/error-handling/ErrorHandler.composable.ts @@ -7,7 +7,8 @@ export type ErrorType = | "notCreated" | "notLoaded" | "notUpdated" - | "notDeleted"; + | "notDeleted" + | "notMoved"; export type BoardObjectType = | "board" @@ -22,8 +23,12 @@ export type ApiErrorHandler = ( error?: ApiResponseError | ApiValidationError ) => Promise | void; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ApiErrorHandlerFactory = (...args: any[]) => ApiErrorHandler; +export type ApiErrorHandlerFactory = ( + errorType: ErrorType, + boardObjectType?: BoardObjectType, + status?: ErrorStatus, + timeout?: number +) => ApiErrorHandler; export type ErrorMap = Record; diff --git a/src/components/molecules/CommonCartridgeExportModal.vue b/src/components/molecules/CommonCartridgeExportModal.vue index bab53482e1..c53b56968a 100644 --- a/src/components/molecules/CommonCartridgeExportModal.vue +++ b/src/components/molecules/CommonCartridgeExportModal.vue @@ -156,7 +156,13 @@ +