From e7f1abed3974bee541e7d88a3b41cb03aa2108c8 Mon Sep 17 00:00:00 2001 From: "marc.sirisak" Date: Mon, 2 Dec 2024 10:39:47 +0100 Subject: [PATCH] feat(lastadmin): check last admin on PL change room settings --- .../tchap_translations.json | 8 + patches/tchap-modifications.json | 6 + .../views/settings/PowerLevelSelector.tsx | 28 ++- src/tchap/util/TchapRoomUtils.ts | 10 + .../settings/PowerLevelSelector-test.tsx | 146 +++++++++++ .../PowerLevelSelector-test.tsx.snap | 235 ++++++++++++++++++ 6 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 test/unit-tests/tchap/components/views/settings/PowerLevelSelector-test.tsx create mode 100644 test/unit-tests/tchap/components/views/settings/__snapshots__/PowerLevelSelector-test.tsx.snap diff --git a/modules/tchap-translations/tchap_translations.json b/modules/tchap-translations/tchap_translations.json index 67484f54cc..eb020ef175 100644 --- a/modules/tchap-translations/tchap_translations.json +++ b/modules/tchap-translations/tchap_translations.json @@ -917,5 +917,13 @@ "error_dialog|activate_room_share_link|description": { "en": "An error occured, we could'nt activate the access by link to the room", "fr": "Une erreur est survenue, nous n'avons pas pu activer le partage de salon par lien" + }, + "user_info|demote_self_confirm_room": { + "en": "Before removing the admin role, verify that there is at least one admin in the room. Otherwise, this room won't be manageable anymore. Note that only an admin can reaffect this role to a user", + "fr": "Avant de retirer votre rôle d'administrateur, vérifiez qu'un autre administrateur est présent dans le salon. Sans cela, le salon ne pourra plus être géré. Notez que seul un administrateur pourra vous réattribuer ce rôle." + }, + "user_info|demote_self_confirm_description_space": { + "en": "Before removing the admin role, verify that there is at least one admin in the space. Otherwise, this space won't be manageable anymore. Note that only an admin can reaffect this role to a user", + "fr": "Avant de retirer votre rôle d'administrateur, vérifiez qu'un autre administrateur est présent dans l'espace. Sans cela, l'espace ne pourra plus être géré. Notez que seul un administrateur pourra vous réattribuer ce rôle." } } diff --git a/patches/tchap-modifications.json b/patches/tchap-modifications.json index e5c2605c92..61b80c3fbd 100644 --- a/patches/tchap-modifications.json +++ b/patches/tchap-modifications.json @@ -119,5 +119,11 @@ "src/components/structures/UserMenu.tsx", "src/components/views/settings/tabs/user/SessionManagerTab.tsx" ] + }, + "last-admin-warning-room-settings": { + "issue": "https://github.com/tchapgouv/tchap-web-v4/issues/1092", + "files": [ + "src/components/views/settings/PowerLevelSelector.tsx" + ] } } \ No newline at end of file diff --git a/src/components/views/settings/PowerLevelSelector.tsx b/src/components/views/settings/PowerLevelSelector.tsx index a2de6791d7..b00504f518 100644 --- a/src/components/views/settings/PowerLevelSelector.tsx +++ b/src/components/views/settings/PowerLevelSelector.tsx @@ -13,6 +13,10 @@ import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import PowerSelector from "../elements/PowerSelector"; import { _t } from "../../../languageHandler"; import SettingsFieldset from "./SettingsFieldset"; +import QuestionDialog from "../dialogs/QuestionDialog"; // :TCHAP: last-admin-warning-room-settings + +import TchapRoomUtils from "~tchap-web/src/tchap/util/TchapRoomUtils"; // :TCHAP: last-admin-warning-room-settings +import Modal from "~tchap-web/src/Modal"; // :TCHAP: last-admin-warning-room-settings /** * Display in a fieldset, the power level of the users and allow to change them. @@ -96,7 +100,29 @@ export function PowerLevelSelector({ disabled={!canChange} label={userId} key={userId} - onChange={(value) => setCurrentPowerLevel({ value, userId })} + onChange={async (value) => { + // :TCHAP: last-admin-warning-room-settings + const userLevelsTmp = Object.assign({}, userLevels); + userLevelsTmp[userId] = value; + + if (!TchapRoomUtils.roomHasAtLeastOneAdmin(userLevelsTmp)) { + const { finished } = Modal.createDialog(QuestionDialog, { + title: _t("common|warning"), + description: ( +
+ {_t("user_info|demote_self_confirm_room")} +
+ ), + button: _t("action|continue"), + }); + const [confirmed] = await finished; + if (!confirmed) return; + } + // end :TCHAP: + setCurrentPowerLevel({ value, userId }); + + } + } /> ); })} diff --git a/src/tchap/util/TchapRoomUtils.ts b/src/tchap/util/TchapRoomUtils.ts index 7329f4b37a..5566056ea7 100644 --- a/src/tchap/util/TchapRoomUtils.ts +++ b/src/tchap/util/TchapRoomUtils.ts @@ -7,6 +7,7 @@ import { MatrixClientPeg } from "~tchap-web/src/MatrixClientPeg"; import { TchapRoomAccessRule, TchapRoomAccessRulesEventId, TchapRoomType } from "../@types/tchap"; import { GuestAccess, JoinRule } from "matrix-js-sdk/src/matrix"; +import { RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types"; export default class TchapRoomUtils { //inspired by https://github.com/tchapgouv/tchap-android/blob/develop/vector/src/main/java/fr/gouv/tchap/core/utils/RoomUtils.kt#L31 @@ -82,4 +83,13 @@ export default class TchapRoomUtils { } return event; } + + // check at least one admin in the list + static roomHasAtLeastOneAdmin(usersLevels: Record) : boolean{ + const userLevelValues = Object.values(usersLevels); + + // At least one user as the pL 100 which means he is admin + return userLevelValues.some((uL) => uL === 100); + + } } diff --git a/test/unit-tests/tchap/components/views/settings/PowerLevelSelector-test.tsx b/test/unit-tests/tchap/components/views/settings/PowerLevelSelector-test.tsx new file mode 100644 index 0000000000..8625b3fcb3 --- /dev/null +++ b/test/unit-tests/tchap/components/views/settings/PowerLevelSelector-test.tsx @@ -0,0 +1,146 @@ +/* + * Copyright 2024 New Vector Ltd. + * Copyright 2024 The Matrix.org Foundation C.I.C. + * + * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only + * Please see LICENSE files in the repository root for full details. + */ + +import { render, screen } from "jest-matrix-react"; +import React, { ComponentProps } from "react"; +import userEvent from "@testing-library/user-event"; + +import { PowerLevelSelector } from "~tchap-web/src/components/views/settings/PowerLevelSelector"; +import { stubClient } from "~tchap-web/test/test-utils"; +import MatrixClientContext from "~tchap-web/src/contexts/MatrixClientContext"; + +describe("PowerLevelSelector", () => { + const matrixClient = stubClient(); + + const currentUser = matrixClient.getUserId()!; + const userLevels = { + [currentUser]: 100, + "@alice:server.org": 50, + "@bob:server.org": 0, + }; + + const renderPLS = (props: Partial>) => + render( + + true} + onClick={jest.fn()} + {...props} + > + empty label + + , + ); + + it("should render", () => { + renderPLS({}); + expect(screen.getByRole("group")).toMatchSnapshot(); + }); + + it("should display only the current user", async () => { + // Display only the current user + renderPLS({ filter: (user) => user === currentUser }); + + // Only alice should be displayed + const userSelects = screen.getAllByRole("combobox"); + expect(userSelects).toHaveLength(1); + expect(userSelects[0]).toHaveAccessibleName(currentUser); + + expect(screen.getByRole("group")).toMatchSnapshot(); + }); + + it("should not be able to change the power level if `canChangeLevels` is false", async () => { + renderPLS({ canChangeLevels: false }); + + // The selects should be disabled + const userSelects = screen.getAllByRole("combobox"); + userSelects.forEach((select) => expect(select).toBeDisabled()); + }); + + it("should be able to change only the level of someone with a lower level", async () => { + const userLevels = { + [currentUser]: 50, + "@alice:server.org": 100, + }; + renderPLS({ userLevels }); + + expect(screen.getByRole("combobox", { name: currentUser })).toBeEnabled(); + expect(screen.getByRole("combobox", { name: "@alice:server.org" })).toBeDisabled(); + }); + + it("should display the children if there is no user to display", async () => { + // No user to display + renderPLS({ filter: () => false }); + + expect(screen.getByText("empty label")).toBeInTheDocument(); + }); + + // TCHAP // + it("should be able to change the power level of the current user", async () => { + const onClick = jest.fn(); + const userLevels = { + [currentUser]: 100, + "@alice:server.org": 100, + "@bob:server.org": 0, + }; + renderPLS({ userLevels, onClick }); + + // Until the power level is changed, the apply button should be disabled + // compound button is using aria-disabled instead of the disabled attribute, we can't toBeDisabled on it + expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "true"); + + const select = screen.getByRole("combobox", { name: currentUser }); + // Sanity check + expect(select).toHaveValue("100"); + + // Change current user power level to 50 + await userEvent.selectOptions(select, "50"); + expect(select).toHaveValue("50"); + // After the user level changes, the apply button should be enabled + expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "false"); + + // Click on Apply should call onClick with the new power level + await userEvent.click(screen.getByRole("button", { name: "Apply" })); + expect(onClick).toHaveBeenCalledWith(50, currentUser); + }); + + it("should display modal warning if user is last admin", async () => { + const onClick = jest.fn(); + + renderPLS({ onClick }); + + // Until the power level is changed, the apply button should be disabled + // compound button is using aria-disabled instead of the disabled attribute, we can't toBeDisabled on it + expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "true"); + + const select = screen.getByRole("combobox", { name: currentUser }); + // Sanity check + expect(select).toHaveValue("100"); + + // Change current user power level to 50 + await userEvent.selectOptions(select, "50"); + + // modal should appear because only admin in the room + expect(screen.findByText("WARNING")).toBeTruthy(); + + await userEvent.click(screen.getByRole("button", { name: "Continue" })); + + expect(select).toHaveValue("50"); + // After the user level changes, the apply button should be enabled + expect(screen.getByRole("button", { name: "Apply" })).toHaveAttribute("aria-disabled", "false"); + + // Click on Apply should call onClick with the new power level + await userEvent.click(screen.getByRole("button", { name: "Apply" })); + expect(onClick).toHaveBeenCalledWith(50, currentUser); + }); +}); diff --git a/test/unit-tests/tchap/components/views/settings/__snapshots__/PowerLevelSelector-test.tsx.snap b/test/unit-tests/tchap/components/views/settings/__snapshots__/PowerLevelSelector-test.tsx.snap new file mode 100644 index 0000000000..7ba419a4e2 --- /dev/null +++ b/test/unit-tests/tchap/components/views/settings/__snapshots__/PowerLevelSelector-test.tsx.snap @@ -0,0 +1,235 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PowerLevelSelector should display only the current user 1`] = ` +
+ + title + +
+
+
+ + +
+
+ +
+
+`; + +exports[`PowerLevelSelector should render 1`] = ` +
+ + title + +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ +
+
+`;