This repository has been archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 828
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
94bd798
commit 61c2973
Showing
5 changed files
with
507 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
112
test/components/views/settings/PowerLevelSelector-test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.