From 15d074acf3a67ba9b28d2d3dc635dba5f042ea7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:41:11 +0100 Subject: [PATCH 1/7] N21-2332 Add school license info to school external tools (#3555) --- .../ExternalToolSection.unit.ts | 128 +++++++++---- .../administration/ExternalToolSection.vue | 76 +++++++- .../external-tool-section-utils.composable.ts | 34 +++- ...rnal-tool-section-utils.composable.unit.ts | 155 +++++++++++++++- .../school-external-tool-item.ts | 5 + src/locales/de.ts | 6 + src/locales/en.ts | 6 + src/locales/es.ts | 6 + src/locales/uk.ts | 6 + .../data/license/SchoolLicense.store.ts | 52 ++++++ .../data/license/SchoolLicense.store.unit.ts | 140 +++++++++++++++ src/modules/data/license/index.ts | 1 + .../license/schoolLicenseApi.composable.ts | 14 +- .../schoolLicenseApi.composable.unit.ts | 39 ++++ src/serverApi/v3/api.ts | 170 ++++++++++++++++++ .../mapper/school-external-tool.mapper.ts | 1 + .../external-tool/school-external-tool.ts | 9 +- src/store/types/data-table-header.ts | 10 -- tests/test-utils/factory/index.ts | 1 + .../mediaSchoolLicenseResponseFactory.ts | 11 ++ tests/test-utils/mockedPiniaStoreTyping.ts | 41 +++-- 21 files changed, 825 insertions(+), 86 deletions(-) create mode 100644 src/modules/data/license/SchoolLicense.store.ts create mode 100644 src/modules/data/license/SchoolLicense.store.unit.ts delete mode 100644 src/store/types/data-table-header.ts create mode 100644 tests/test-utils/factory/mediaSchoolLicenseResponseFactory.ts diff --git a/src/components/administration/ExternalToolSection.unit.ts b/src/components/administration/ExternalToolSection.unit.ts index 6add8716e0..46abc452f9 100644 --- a/src/components/administration/ExternalToolSection.unit.ts +++ b/src/components/administration/ExternalToolSection.unit.ts @@ -1,4 +1,4 @@ -import { ConfigResponse } from "@/serverApi/v3"; +import { ConfigResponse, MediaSourceLicenseType } from "@/serverApi/v3"; import AuthModule from "@/store/auth"; import EnvConfigModule from "@/store/env-config"; import { SchoolExternalToolMetadata } from "@/store/external-tool"; @@ -10,6 +10,7 @@ import { NOTIFIER_MODULE_KEY, SCHOOL_EXTERNAL_TOOLS_MODULE_KEY, } from "@/utils/inject"; +import { mockedPiniaStoreTyping, MockedStore } from "@@/tests/test-utils"; import { envsFactory, meResponseFactory, @@ -23,9 +24,12 @@ import { createTestingVuetify, } from "@@/tests/test-utils/setup"; import { useSchoolExternalToolUsage } from "@data-external-tool"; +import { useSchoolLicenseStore } from "@data-license"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; import { mdiAlert, mdiCheckCircle } from "@icons/material"; +import { createTestingPinia } from "@pinia/testing"; import { mount } from "@vue/test-utils"; +import { setActivePinia } from "pinia"; import { nextTick, ref } from "vue"; import { Router, useRouter } from "vue-router"; import { VCardText } from "vuetify/lib/components/index.mjs"; @@ -43,6 +47,13 @@ describe("ExternalToolSection", () => { let useSchoolExternalToolUsageMock: DeepMocked< ReturnType >; + let schoolLicenseStore: MockedStore; + + beforeAll(() => { + setActivePinia(createTestingPinia()); + + schoolLicenseStore = mockedPiniaStoreTyping(useSchoolLicenseStore); + }); const createDatasheetButtonIndex = 1; @@ -79,7 +90,6 @@ describe("ExternalToolSection", () => { const wrapper = mount(ExternalToolSection, { global: { plugins: [createTestingVuetify(), createTestingI18n()], - provide: { [SCHOOL_EXTERNAL_TOOLS_MODULE_KEY.valueOf()]: schoolExternalToolsModule, @@ -88,7 +98,6 @@ describe("ExternalToolSection", () => { [ENV_CONFIG_MODULE_KEY.valueOf()]: envConfigModule, }, }, - stubs: { VIcon: true, }, @@ -130,6 +139,12 @@ describe("ExternalToolSection", () => { schoolExternalToolsModule.loadSchoolExternalTools ).toHaveBeenCalledWith("schoolId"); }); + + it("should load the school licenses", () => { + getWrapper(); + + expect(schoolLicenseStore.fetchMediaSchoolLicenses).toHaveBeenCalled(); + }); }); }); @@ -145,35 +160,56 @@ describe("ExternalToolSection", () => { name: firstToolName, status: schoolToolConfigurationStatusFactory.build(), isDeactivated: false, + medium: { + mediumId: "tool1", + mediaSourceId: "licensedSource", + mediaSourceName: "Medium Source Name", + mediaSourceLicenseType: MediaSourceLicenseType.SchoolLicense, + }, }); - const { wrapper, schoolExternalToolsModule } = getWrapper({ - getSchoolExternalTools: [ - schoolExternalTool, - { - id: "testId2", - toolId: "toolId", - schoolId: "schoolId", - parameters: [], - name: secondToolName, - status: schoolToolConfigurationStatusFactory.build({ - isOutdatedOnScopeSchool: true, - }), - isDeactivated: false, - }, - { - id: "testId3", - toolId: "toolId", - schoolId: "schoolId", - parameters: [], - name: "Test3", - status: schoolToolConfigurationStatusFactory.build({ - isGloballyDeactivated: true, - }), - isDeactivated: true, - }, - ], - }); + schoolLicenseStore.isLicensed.mockReturnValueOnce(true); + schoolLicenseStore.isLicensed.mockReturnValue(false); + + const { wrapper, schoolExternalToolsModule } = getWrapper( + { + getSchoolExternalTools: [ + schoolExternalTool, + { + id: "testId2", + toolId: "toolId", + schoolId: "schoolId", + parameters: [], + name: secondToolName, + status: schoolToolConfigurationStatusFactory.build({ + isOutdatedOnScopeSchool: true, + }), + isDeactivated: false, + medium: { + mediumId: "tool2", + mediaSourceId: "notLicensedSource", + mediaSourceName: undefined, + mediaSourceLicenseType: MediaSourceLicenseType.SchoolLicense, + }, + }, + { + id: "testId3", + toolId: "toolId", + schoolId: "schoolId", + parameters: [], + name: "Test3", + status: schoolToolConfigurationStatusFactory.build({ + isGloballyDeactivated: true, + }), + isDeactivated: true, + medium: undefined, + }, + ], + }, + { + FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: true, + } + ); const windowMock = createMock(); jest.spyOn(window, "open").mockImplementation(() => windowMock); @@ -204,9 +240,12 @@ describe("ExternalToolSection", () => { "components.administration.externalToolsSection.table.header.status" ); expect(vueWrapperArray[2].find("span").text()).toEqual( + "components.administration.externalToolsSection.table.header.medium" + ); + expect(vueWrapperArray[3].find("span").text()).toEqual( "components.administration.externalToolsSection.table.header.restrictedTo" ); - expect(vueWrapperArray[3].find("span").text()).toEqual(""); + expect(vueWrapperArray[4].find("span").text()).toEqual(""); }); }); @@ -235,24 +274,41 @@ describe("ExternalToolSection", () => { const secondRow = tableRows[1].findAll("td"); const thirdRow = tableRows[2].findAll("td"); - expect(true).toBeTruthy(); + expect(firstRow[1].html()).toContain(mdiCheckCircle); expect(firstRow[1].find("span").text()).toEqual( "components.externalTools.status.latest" ); - expect(firstRow[1].html()).toContain(mdiCheckCircle); - expect(secondRow[1].find("span").text()).toEqual( - "components.externalTools.status.outdated" - ); + expect(secondRow[1].html()).toContain(mdiAlert); expect(secondRow[1].find("span").text()).toEqual( "components.externalTools.status.outdated" ); + expect(thirdRow[1].html()).toContain(mdiAlert); expect(thirdRow[1].find("span").text()).toEqual( "components.externalTools.status.deactivated" ); }); + it("medium status should be rendered in the datatable", () => { + const { wrapper } = setupItems(); + + const tableRows = wrapper.find("tbody").findAll("tr"); + + const firstRow = tableRows[0].findAll("td"); + const secondRow = tableRows[1].findAll("td"); + const thirdRow = tableRows[2].findAll("td"); + + expect(firstRow[2].html()).toContain(mdiCheckCircle); + expect(firstRow[2].find("span").text()).toEqual("Medium Source Name"); + + expect(secondRow[2].html()).toContain(mdiAlert); + expect(secondRow[2].find("span").text()).toEqual(""); + + expect(thirdRow[2].html()).not.toContain("v-icon"); + expect(thirdRow[2].find("span").text()).toEqual("-"); + }); + describe("when actions buttons are rendered", () => { it("the buttons should be displayed", () => { const { wrapper } = setupItems(); diff --git a/src/components/administration/ExternalToolSection.vue b/src/components/administration/ExternalToolSection.vue index de176a3bd2..a914f61795 100644 --- a/src/components/administration/ExternalToolSection.vue +++ b/src/components/administration/ExternalToolSection.vue @@ -33,6 +33,50 @@ + 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 5/7] 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 6/7] 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 7/7] 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 @@ +