From 1b48269db55af5cd563170a0921088aeb4e4f719 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 May 2025 09:27:33 +0100 Subject: [PATCH] Add room list sorting (#29951) * Add room list sorting * Prettier * Unit test * Playwright test * Lint * Use released compound * No tooltip wrapper needed --- package.json | 2 +- .../room-list-filter-sort.spec.ts | 21 +++ .../RoomListPanel/RoomListOptionsMenu.tsx | 24 ++- src/i18n/strings/en_EN.json | 5 + .../RoomListOptionsMenu-test.tsx | 138 ++++++++++++++++++ .../RoomListOptionsMenu-test.tsx.snap | 37 +++++ yarn.lock | 8 +- 7 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/RoomListOptionsMenu-test.tsx create mode 100644 test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap diff --git a/package.json b/package.json index 8bf8838e5c..1acd1cc885 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@types/png-chunks-extract": "^1.0.2", "@types/react-virtualized": "^9.21.30", "@vector-im/compound-design-tokens": "^4.0.0", - "@vector-im/compound-web": "^7.10.2", + "@vector-im/compound-web": "^7.11.0", "@vector-im/matrix-wysiwyg": "2.38.3", "@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4", diff --git a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts index 84a7d348b2..0d72575386 100644 --- a/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts +++ b/playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts @@ -29,6 +29,10 @@ test.describe("Room list filters and sort", () => { return page.getByRole("button", { name: "Filter" }); } + function getRoomOptionsMenu(page: Page): Locator { + return page.getByRole("button", { name: "Room Options" }); + } + /** * Get the room list * @param page @@ -252,6 +256,23 @@ test.describe("Room list filters and sort", () => { await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible(); }, ); + + test("should sort the room list alphabetically", async ({ page }) => { + const roomListView = getRoomList(page); + + await getRoomOptionsMenu(page).click(); + await page.getByRole("menuitemradio", { name: "A-Z" }).click(); + + await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/); + }); + + test("should move room to the top on message when sorting by activity", async ({ page, bot }) => { + const roomListView = getRoomList(page); + + await bot.sendMessage(unReadDmId, "Hello!"); + + await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/); + }); }); test.describe("Empty room list", () => { diff --git a/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx b/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx index b1639eee34..9917f713fc 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListOptionsMenu.tsx @@ -5,12 +5,13 @@ * Please see LICENSE files in the repository root for full details. */ -import { IconButton, Menu, MenuTitle, CheckboxMenuItem, Tooltip } from "@vector-im/compound-web"; -import React, { type Ref, type JSX, useState } from "react"; +import { IconButton, Menu, MenuTitle, CheckboxMenuItem, Tooltip, RadioMenuItem } from "@vector-im/compound-web"; +import React, { type Ref, type JSX, useState, useCallback } from "react"; import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal"; import { _t } from "../../../../languageHandler"; import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel"; +import { SortOption } from "../../../viewmodels/roomlist/useSorter"; interface MenuTriggerProps extends React.ComponentProps { ref?: Ref; @@ -39,6 +40,14 @@ interface Props { export function RoomListOptionsMenu({ vm }: Props): JSX.Element { const [open, setOpen] = useState(false); + const onActivitySelected = useCallback(() => { + vm.sort(SortOption.Activity); + }, [vm]); + + const onAtoZSelected = useCallback(() => { + vm.sort(SortOption.AToZ); + }, [vm]); + return ( } > + + + ", () => { + it("should match snapshot", () => { + const vm = { + sort: jest.fn(), + } as unknown as RoomListViewState; + + const { asFragment } = render(); + + expect(asFragment()).toMatchSnapshot(); + }); + + it("should show A to Z selected if activeSortOption is Alphabetic", async () => { + const user = userEvent.setup(); + + const vm = { + sort: jest.fn(), + activeSortOption: "Alphabetic", + } as unknown as RoomListViewState; + + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).not.toBeChecked(); + }); + + it("should show Activity selected if activeSortOption is Recency", async () => { + const user = userEvent.setup(); + + const vm = { + sort: jest.fn(), + activeSortOption: "Recency", + } as unknown as RoomListViewState; + + render(); + + // Open the menu + const button = screen.getByRole("button", { name: "Room Options" }); + await user.click(button); + + expect(screen.getByRole("menuitemradio", { name: "A-Z" })).not.toBeChecked(); + expect(screen.getByRole("menuitemradio", { name: "Activity" })).toBeChecked(); + }); + + it("should sort A to Z", async () => { + const user = userEvent.setup(); + + const vm = { + sort: jest.fn(), + } as unknown as RoomListViewState; + + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "A-Z" })); + + expect(vm.sort).toHaveBeenCalledWith("Alphabetic"); + }); + + it("should sort by activity", async () => { + const user = userEvent.setup(); + + const vm = { + sort: jest.fn(), + activeSortOption: "Alphabetic", + } as unknown as RoomListViewState; + + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemradio", { name: "Activity" })); + + expect(vm.sort).toHaveBeenCalledWith("Recency"); + }); + + it("should show message previews disabled", async () => { + const user = userEvent.setup(); + + const vm = { + shouldShowMessagePreview: false, + } as unknown as RoomListViewState; + + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).not.toBeChecked(); + }); + + it("should show message previews enabled", async () => { + const user = userEvent.setup(); + + const vm = { + shouldShowMessagePreview: true, + } as unknown as RoomListViewState; + + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + expect(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })).toBeChecked(); + }); + + it("should toggle message previews", async () => { + const user = userEvent.setup(); + + const vm = { + toggleMessagePreview: jest.fn(), + } as unknown as RoomListViewState; + + render(); + + await user.click(screen.getByRole("button", { name: "Room Options" })); + + await user.click(screen.getByRole("menuitemcheckbox", { name: "Show message previews" })); + + expect(vm.toggleMessagePreview).toHaveBeenCalled(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap new file mode 100644 index 0000000000..9bc7a88508 --- /dev/null +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListOptionsMenu-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should match snapshot 1`] = ` + + + +`; diff --git a/yarn.lock b/yarn.lock index be1896a01f..c159963cd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3750,10 +3750,10 @@ resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-4.0.2.tgz#27363d26446eaa21880ab126fa51fec112e6fd86" integrity sha512-y13bhPyJ5OzbGRl21F6+Y2adrjyK+mu67yKTx+o8MfmIpJzMSn4KkHZtcujMquWSh0e5ZAufsnk4VYvxbSpr1A== -"@vector-im/compound-web@^7.10.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.10.2.tgz#2f62c6ab83269e5b957f53bb53413a74fb65e04d" - integrity sha512-K9gA1Ah9CTJMeZTkcDFpAdVRNbu/rQEgV3PoDcEPI3e9iDds8Dhbo7EfOciPvtXCZw6Hr83lnhWDnwTFHVlahQ== +"@vector-im/compound-web@^7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-web/-/compound-web-7.11.0.tgz#b7c466e64089320b41f8eaf6f2b30950e9692ca2" + integrity sha512-lRxXUOQJHdBswhykpNs/J/cBW4fPY1qbwyDexlWxX5zCVAYiuMCWo2tI+Y7/SK4tNbDr7nwoTDRh4H9CO1L5LQ== dependencies: "@floating-ui/react" "^0.27.0" "@radix-ui/react-context-menu" "^2.2.1"