Change user permission by using a new apply button (#12346)
* Add PowerLevelSelector.tsx. 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. * Use `PowerLevelSelector` to render privileged and muted users in `RolesRoomSettingsTab` * Update existing tests * Add playwright test * Fix typo * Fix typo
This commit is contained in:
parent
a5ed97b903
commit
ddd0ec48ac
8 changed files with 607 additions and 63 deletions
142
src/components/views/settings/PowerLevelSelector.tsx
Normal file
142
src/components/views/settings/PowerLevelSelector.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
*
|
||||
* 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.
|
||||
* If there is no user to display, the children is displayed instead.
|
||||
*/
|
||||
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}
|
||||
aria-label={_t("action|apply")}
|
||||
>
|
||||
{_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());
|
||||
}
|
|
@ -18,7 +18,6 @@ import React from "react";
|
|||
import { EventType, RoomMember, RoomState, RoomStateEvent, Room, IContent } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { throttle, get } from "lodash";
|
||||
import { compare } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { _t, _td, TranslationKey } from "../../../../../languageHandler";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
@ -34,6 +33,7 @@ import { AddPrivilegedUsers } from "../../AddPrivilegedUsers";
|
|||
import SettingsTab from "../SettingsTab";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import { PowerLevelSelector } from "../../PowerLevelSelector";
|
||||
|
||||
interface IEventShowOpts {
|
||||
isState?: boolean;
|
||||
|
@ -240,9 +240,6 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
title: _t("room_settings|permissions|error_changing_pl_title"),
|
||||
description: _t("room_settings|permissions|error_changing_pl_description"),
|
||||
});
|
||||
|
||||
// Rethrow so that the PowerSelector can roll back
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -352,65 +349,29 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
let privilegedUsersSection = <div>{_t("room_settings|permissions|no_privileged_users")}</div>;
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
const privilegedUsers: JSX.Element[] = [];
|
||||
const mutedUsers: JSX.Element[] = [];
|
||||
privilegedUsersSection = (
|
||||
<PowerLevelSelector
|
||||
title={_t("room_settings|permissions|privileged_users_section")}
|
||||
userLevels={userLevels}
|
||||
canChangeLevels={canChangeLevels}
|
||||
currentUserLevel={currentUserLevel}
|
||||
onClick={this.onUserPowerLevelChanged}
|
||||
filter={(user) => userLevels[user] > defaultUserLevel}
|
||||
>
|
||||
<div>{_t("room_settings|permissions|no_privileged_users")}</div>
|
||||
</PowerLevelSelector>
|
||||
);
|
||||
|
||||
Object.keys(userLevels).forEach((user) => {
|
||||
if (!Number.isInteger(userLevels[user])) return;
|
||||
const isMe = user === client.getUserId();
|
||||
const canChange = canChangeLevels && (userLevels[user] < currentUserLevel || isMe);
|
||||
if (userLevels[user] > defaultUserLevel) {
|
||||
// privileged
|
||||
privilegedUsers.push(
|
||||
<PowerSelector
|
||||
value={userLevels[user]}
|
||||
disabled={!canChange}
|
||||
label={user}
|
||||
key={user}
|
||||
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
|
||||
onChange={this.onUserPowerLevelChanged}
|
||||
/>,
|
||||
);
|
||||
} else if (userLevels[user] < defaultUserLevel) {
|
||||
// muted
|
||||
mutedUsers.push(
|
||||
<PowerSelector
|
||||
value={userLevels[user]}
|
||||
disabled={!canChange}
|
||||
label={user}
|
||||
key={user}
|
||||
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
|
||||
onChange={this.onUserPowerLevelChanged}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a: JSX.Element, b: JSX.Element): number => {
|
||||
const aKey = a.key as string;
|
||||
const bKey = b.key as string;
|
||||
const plDiff = userLevels[bKey] - userLevels[aKey];
|
||||
return plDiff !== 0 ? plDiff : compare(aKey.toLocaleLowerCase(), bKey.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
mutedUsers.sort(comparator);
|
||||
|
||||
if (privilegedUsers.length) {
|
||||
privilegedUsersSection = (
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|privileged_users_section")}>
|
||||
{privilegedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection = (
|
||||
<SettingsFieldset legend={_t("room_settings|permissions|muted_users_section")}>
|
||||
{mutedUsers}
|
||||
</SettingsFieldset>
|
||||
);
|
||||
}
|
||||
mutedUsersSection = (
|
||||
<PowerLevelSelector
|
||||
title={_t("room_settings|permissions|muted_users_section")}
|
||||
userLevels={userLevels}
|
||||
canChangeLevels={canChangeLevels}
|
||||
currentUserLevel={currentUserLevel}
|
||||
onClick={this.onUserPowerLevelChanged}
|
||||
filter={(user) => userLevels[user] < defaultUserLevel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const banned = room.getMembersWithMembership("ban");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue