Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add PowerLevelSelector.tsx.
Browse files Browse the repository at this point in the history
It's extracting the current behavior of the privileged users and muted of `RolesRoomSettingsTab.tsx` into a dedicated component.
It's also adding a new apply button.
  • Loading branch information
florianduros committed Mar 15, 2024
1 parent 94bd798 commit 61c2973
Show file tree
Hide file tree
Showing 5 changed files with 507 additions and 0 deletions.
1 change: 1 addition & 0 deletions res/css/_components.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@
@import "./views/rooms/wysiwyg_composer/components/_FormattingButtons.pcss";
@import "./views/rooms/wysiwyg_composer/components/_LinkModal.pcss";
@import "./views/settings/_AvatarSetting.pcss";
@import "./views/settings/_PowerLevelSelector.pcss";
@import "./views/settings/_CrossSigningPanel.pcss";
@import "./views/settings/_CryptographyPanel.pcss";
@import "./views/settings/_FontScalingPanel.pcss";
Expand Down
21 changes: 21 additions & 0 deletions res/css/views/settings/_PowerLevelSelector.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/

.mx_PowerLevelSelector_Button {
align-self: flex-start;
}
140 changes: 140 additions & 0 deletions src/components/views/settings/PowerLevelSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/

import React, { useState, JSX, PropsWithChildren } from "react";
import { Button } from "@vector-im/compound-web";
import { compare } from "matrix-js-sdk/src/utils";

import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import PowerSelector from "../elements/PowerSelector";
import { _t } from "../../../languageHandler";
import SettingsFieldset from "./SettingsFieldset";

/**
* Display in a fieldset, the power level of the users and allow to change them.
* The apply button is disabled until the power level of an user is changed
*/
interface PowerLevelSelectorProps {
/**
* The power levels of the users
* The key is the user id and the value is the power level
*/
userLevels: Record<string, number>;
/**
* Whether the user can change the power levels of other users
*/
canChangeLevels: boolean;
/**
* The current user power level
*/
currentUserLevel: number;
/**
* The callback when the apply button is clicked
* @param value - new power level for the user
* @param userId - the user id
*/
onClick: (value: number, userId: string) => void;
/**
* Filter the users to display
* @param user
*/
filter: (user: string) => boolean;
/**
* The title of the fieldset
*/
title: string;
}

export function PowerLevelSelector({
userLevels,
canChangeLevels,
currentUserLevel,
onClick,
filter,
title,
children,
}: PropsWithChildren<PowerLevelSelectorProps>): JSX.Element | null {
const matrixClient = useMatrixClientContext();
const [currentPowerLevel, setCurrentPowerLevel] = useState<{ value: number; userId: string } | null>(null);

// If the power level has changed, we need to enable the apply button
const powerLevelChanged = Boolean(
currentPowerLevel && currentPowerLevel.value !== userLevels[currentPowerLevel?.userId],
);

// We sort the users by power level, then we filter them
const users = Object.keys(userLevels)
.sort((userA, userB) => sortUser(userA, userB, userLevels))
.filter(filter);

// No user to display, we return the children into fragment to convert it to JSX.Element type
if (!users.length) return <>{children}</>;

return (
<SettingsFieldset legend={title}>
{users.map((userId) => {
// We only want to display users with a valid power level aka an integer
if (!Number.isInteger(userLevels[userId])) return;

const isMe = userId === matrixClient.getUserId();
// If I can change levels, I can change the level of anyone with a lower level than mine
const canChange = canChangeLevels && (userLevels[userId] < currentUserLevel || isMe);

// When the new power level is selected, the fields are rerendered and we need to keep the current value
const userLevel = currentPowerLevel?.userId === userId ? currentPowerLevel?.value : userLevels[userId];

return (
<PowerSelector
value={userLevel}
disabled={!canChange}
label={userId}
key={userId}
onChange={(value) => setCurrentPowerLevel({ value, userId })}
/>
);
})}

<Button
size="sm"
kind="primary"
// mx_Dialog_nonDialogButton is necessary to avoid the Dialog CSS to override the button style
className="mx_Dialog_nonDialogButton mx_PowerLevelSelector_Button"
onClick={() => {
if (currentPowerLevel !== null) {
onClick(currentPowerLevel.value, currentPowerLevel.userId);
setCurrentPowerLevel(null);
}
}}
disabled={!powerLevelChanged}
>
{_t("action|apply")}
</Button>
</SettingsFieldset>
);
}

/**
* Sort the users by power level, then by name
* @param userA
* @param userB
* @param userLevels
*/
function sortUser(userA: string, userB: string, userLevels: PowerLevelSelectorProps["userLevels"]): number {
const powerLevelDiff = userLevels[userA] - userLevels[userB];
return powerLevelDiff !== 0 ? powerLevelDiff : compare(userA.toLocaleLowerCase(), userB.toLocaleLowerCase());
}
112 changes: 112 additions & 0 deletions test/components/views/settings/PowerLevelSelector-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
*
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/

import { render, screen } from "@testing-library/react";
import React, { ComponentProps } from "react";
import userEvent from "@testing-library/user-event";

import { PowerLevelSelector } from "../../../../src/components/views/settings/PowerLevelSelector";
import { stubClient } from "../../../test-utils";
import MatrixClientContext from "../../../../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 be able to change the power level of the current user", 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");

// Change current user power level to 50
const select = screen.getByRole("combobox", { name: currentUser });
expect(select).toHaveValue("100");

// After the user level changes, the apply button should be enabled
await userEvent.selectOptions(select, "50");
expect(select).toHaveValue("50");
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 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();
});
});
Loading

0 comments on commit 61c2973

Please sign in to comment.