Skip to content

Commit

Permalink
Merge pull request #1173 from tchapgouv/1092-last-admin-altert
Browse files Browse the repository at this point in the history
feat(lastadmin): check last admin on PL change room settings
  • Loading branch information
MarcWadai authored Dec 9, 2024
2 parents 455d004 + e7f1abe commit b68ff01
Show file tree
Hide file tree
Showing 6 changed files with 432 additions and 1 deletion.
8 changes: 8 additions & 0 deletions modules/tchap-translations/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
6 changes: 6 additions & 0 deletions patches/tchap-modifications.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
28 changes: 27 additions & 1 deletion src/components/views/settings/PowerLevelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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: (
<div>
{_t("user_info|demote_self_confirm_room")}
</div>
),
button: _t("action|continue"),
});
const [confirmed] = await finished;
if (!confirmed) return;
}
// end :TCHAP:
setCurrentPowerLevel({ value, userId });

}
}
/>
);
})}
Expand Down
10 changes: 10 additions & 0 deletions src/tchap/util/TchapRoomUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -82,4 +83,13 @@ export default class TchapRoomUtils {
}
return event;
}

// check at least one admin in the list
static roomHasAtLeastOneAdmin(usersLevels: Record<string, number>) : 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);

}
}
Original file line number Diff line number Diff line change
@@ -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<ComponentProps<typeof PowerLevelSelector>>) =>
render(
<MatrixClientContext.Provider value={matrixClient}>
<PowerLevelSelector
userLevels={userLevels}
canChangeLevels={true}
currentUserLevel={userLevels[currentUser]}
title="title"
// filter nothing by default
filter={() => true}
onClick={jest.fn()}
{...props}
>
empty label
</PowerLevelSelector>
</MatrixClientContext.Provider>,
);

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);
});
});
Loading

0 comments on commit b68ff01

Please sign in to comment.