Room list: assign room to custom section (#33238)
* feat(sc): add new toast type for room list * feat(sc): add section entries in room list item menu * feat(rls): expose util functions * feat: allows to tag room with custom sections * feat(vm): add new Chat moved toast to room list vm * feat(vm): add section selection to room list item vm * feat(e2e): add tests for adding room in a custom section * test(e2e): update existing screenshots * chore: fix lint after merge * chore: remove outline in test
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@@ -99,6 +99,7 @@
|
||||
"voice_call": "Open room %(roomName)s with a voice call."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"chat_moved": "Chat moved",
|
||||
"collapse_filters": "Collapse filter list",
|
||||
"empty": {
|
||||
"no_chats": "No chats yet",
|
||||
|
||||
+6
@@ -38,3 +38,9 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const SectionCreated: Story = {};
|
||||
|
||||
export const ChatMoved: Story = {
|
||||
args: {
|
||||
type: "chat_moved",
|
||||
},
|
||||
};
|
||||
|
||||
+6
-1
@@ -13,7 +13,7 @@ import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./RoomListToast.stories";
|
||||
|
||||
const { SectionCreated } = composeStories(stories);
|
||||
const { SectionCreated, ChatMoved } = composeStories(stories);
|
||||
|
||||
describe("<RoomListToast />", () => {
|
||||
it("renders SectionCreated story", () => {
|
||||
@@ -21,6 +21,11 @@ describe("<RoomListToast />", () => {
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders ChatMoved story", () => {
|
||||
const { container } = render(<ChatMoved />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("calls onClose when the close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SectionCreated />);
|
||||
|
||||
+4
-1
@@ -12,7 +12,7 @@ import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"
|
||||
import styles from "./RoomListToast.module.css";
|
||||
import { useI18n } from "../../../core/i18n/i18nContext";
|
||||
|
||||
export type ToastType = "section_created";
|
||||
export type ToastType = "section_created" | "chat_moved";
|
||||
|
||||
interface RoomListToastProps {
|
||||
/** The type of toast to display */
|
||||
@@ -37,6 +37,9 @@ export function RoomListToast({ type, onClose }: Readonly<RoomListToastProps>):
|
||||
case "section_created":
|
||||
content = { text: _t("room_list|section_created"), icon: CheckIcon };
|
||||
break;
|
||||
case "chat_moved":
|
||||
content = { text: _t("room_list|chat_moved"), icon: CheckIcon };
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+56
@@ -1,5 +1,61 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<RoomListToast /> > renders ChatMoved story 1`] = `
|
||||
<div>
|
||||
<div
|
||||
style="position: relative; width: 320px; height: 100px; background-color: grey;"
|
||||
>
|
||||
<div
|
||||
class="_typography_6v6n8_153 _font-body-sm-medium_6v6n8_41 _toast-container_1ysb3_8 RoomListToast-module_toast _has-close_1ysb3_30"
|
||||
>
|
||||
<div
|
||||
class="_content_1ysb3_34"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="_icon_1ysb3_26"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.55 17.575q-.2 0-.375-.062a.9.9 0 0 1-.325-.213L4.55 13q-.274-.274-.262-.713.012-.437.287-.712a.95.95 0 0 1 .7-.275q.425 0 .7.275L9.55 15.15l8.475-8.475q.274-.275.713-.275.437 0 .712.275.275.274.275.713 0 .437-.275.712l-9.2 9.2q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
|
||||
/>
|
||||
</svg>
|
||||
Chat moved
|
||||
</div>
|
||||
<button
|
||||
aria-labelledby="_r_6_"
|
||||
class="_icon-button_1215g_8 _close_1ysb3_41 _no-background_1215g_42"
|
||||
data-kind="secondary"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 24px;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_147l5_17"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<RoomListToast /> > renders SectionCreated story 1`] = `
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -17,3 +17,4 @@ export { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton";
|
||||
export { RoomListEmptyStateView } from "./RoomListEmptyStateView";
|
||||
export type { RoomListEmptyStateViewProps } from "./RoomListEmptyStateView";
|
||||
export * from "../VirtualizedRoomListView/RoomListItemAccessibilityWrapper/RoomListItemView";
|
||||
export * from "./RoomListToast";
|
||||
|
||||
+56
@@ -27,6 +27,7 @@ describe("<RoomListItemMoreOptionsMenu />", () => {
|
||||
onLeaveRoom: vi.fn(),
|
||||
onSetRoomNotifState: vi.fn(),
|
||||
onCreateSection: vi.fn(),
|
||||
onToggleSection: vi.fn(),
|
||||
};
|
||||
|
||||
const renderMenu = (overrides: Partial<RoomListItemViewSnapshot> = {}): ReturnType<typeof render> => {
|
||||
@@ -240,4 +241,59 @@ describe("<RoomListItemMoreOptionsMenu />", () => {
|
||||
|
||||
expect(mockCallbacks.onCreateSection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render section items in move to section submenu", () => {
|
||||
const sections = [
|
||||
{ tag: "m.favourite", name: "Favourites", isSelected: false },
|
||||
{ tag: "element.io.section.custom1", name: "Work", isSelected: true },
|
||||
{ tag: "element.io.section.custom2", name: "Personal", isSelected: false },
|
||||
];
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel({ ...defaultSnapshot, sections }, mockCallbacks);
|
||||
return <MoreOptionContent vm={vm} />;
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
const favouriteItem = screen.getByRole("menuitem", { name: "Favourites" });
|
||||
expect(favouriteItem).toBeInTheDocument();
|
||||
expect(favouriteItem).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
const workItem = screen.getByRole("menuitem", { name: "Work" });
|
||||
expect(workItem).toBeInTheDocument();
|
||||
expect(workItem).toHaveAttribute("aria-checked", "true");
|
||||
|
||||
const personalItem = screen.getByRole("menuitem", { name: "Personal" });
|
||||
expect(personalItem).toBeInTheDocument();
|
||||
expect(personalItem).toHaveAttribute("aria-checked", "false");
|
||||
});
|
||||
|
||||
it("should call onToggleSection when a section item is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const sections = [
|
||||
{ tag: "m.favourite", name: "Favourites", isSelected: false },
|
||||
{ tag: "element.io.section.custom1", name: "Work", isSelected: false },
|
||||
];
|
||||
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel({ ...defaultSnapshot, sections }, mockCallbacks);
|
||||
return <MoreOptionContent vm={vm} />;
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
const workItem = screen.getByRole("menuitem", { name: "Work" });
|
||||
await user.click(workItem);
|
||||
|
||||
expect(mockCallbacks.onToggleSection).toHaveBeenCalledWith("element.io.section.custom1");
|
||||
});
|
||||
|
||||
it("should not render section items when sections array is empty", () => {
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel({ ...defaultSnapshot, sections: [] }, mockCallbacks);
|
||||
return <MoreOptionContent vm={vm} />;
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(screen.getByRole("menuitem", { name: "New section" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+16
@@ -17,6 +17,7 @@ import {
|
||||
LeaveIcon,
|
||||
OverflowHorizontalIcon,
|
||||
ArrowRightIcon,
|
||||
CheckIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../../core/i18n/i18n";
|
||||
@@ -136,6 +137,21 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{snapshot.sections.map((section) => (
|
||||
<MenuItem
|
||||
key={section.tag}
|
||||
label={section.name}
|
||||
onSelect={() => vm.onToggleSection(section.tag)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
aria-checked={section.isSelected}
|
||||
>
|
||||
{section.isSelected && (
|
||||
<CheckIcon color="var(--cpd-color-icon-tertiary)" width="24px" height="24px" />
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Separator />
|
||||
<MenuItem label={_t("action|new_section")} onSelect={vm.onCreateSection} hideChevron={true} />
|
||||
</SubMenu>
|
||||
)}
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ describe("<RoomListItemNotificationMenu />", () => {
|
||||
onLeaveRoom: vi.fn(),
|
||||
onSetRoomNotifState: vi.fn(),
|
||||
onCreateSection: vi.fn(),
|
||||
onToggleSection: vi.fn(),
|
||||
};
|
||||
|
||||
const renderMenu = (roomNotifState: RoomNotifState = RoomNotifState.AllMessages): ReturnType<typeof render> => {
|
||||
|
||||
+2
@@ -39,6 +39,7 @@ const RoomListItemWrapperImpl = ({
|
||||
onLeaveRoom,
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onFocus,
|
||||
@@ -58,6 +59,7 @@ const RoomListItemWrapperImpl = ({
|
||||
onLeaveRoom,
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
});
|
||||
return (
|
||||
<RoomListItemView
|
||||
|
||||
+17
@@ -44,6 +44,19 @@ function getA11yLabel(roomName: string, notification: NotificationDecorationData
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes a section that a room can be assigned to.
|
||||
* Used to render toggle items in the "Move to section" submenu.
|
||||
*/
|
||||
export interface Section {
|
||||
/** The tag that identifies this section (e.g. `m.favourite`, custom tag) */
|
||||
tag: string;
|
||||
/** The human-readable display name of the section */
|
||||
name: string;
|
||||
/** Whether the room currently belongs to this section */
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot for a room list item.
|
||||
* Contains all the data needed to render a room in the list.
|
||||
@@ -81,6 +94,8 @@ export interface RoomListItemViewSnapshot {
|
||||
roomNotifState: RoomNotifState;
|
||||
/** Whether the room can be moved to a section */
|
||||
canMoveToSection: boolean;
|
||||
/** Available sections the room can be assigned to */
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +123,8 @@ export interface RoomListItemViewActions {
|
||||
onSetRoomNotifState: (state: RoomNotifState) => void;
|
||||
/** Called when creating a new section */
|
||||
onCreateSection: () => void;
|
||||
/** Called when toggling a room's membership in a section */
|
||||
onToggleSection: (tag: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
@@ -37,4 +37,21 @@ export const defaultSnapshot: RoomListItemViewSnapshot = {
|
||||
canMarkAsUnread: true,
|
||||
roomNotifState: RoomNotifState.AllMessages,
|
||||
canMoveToSection: true,
|
||||
sections: [
|
||||
{
|
||||
tag: "m.favourite",
|
||||
name: "Favourites",
|
||||
isSelected: false,
|
||||
},
|
||||
{
|
||||
tag: "element.io.section.work",
|
||||
name: "Work",
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
tag: "m.lowpriority",
|
||||
name: "Low Priority",
|
||||
isSelected: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ export type {
|
||||
RoomListItemViewModel,
|
||||
RoomListItemViewActions,
|
||||
RoomListItemViewProps,
|
||||
Section,
|
||||
} from "./RoomListItemView";
|
||||
export { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu";
|
||||
export type { RoomListItemNotificationMenuProps } from "./RoomListItemNotificationMenu";
|
||||
|
||||
+1
@@ -20,4 +20,5 @@ export const mockedActions: RoomListItemViewActions = {
|
||||
onLeaveRoom: fn(),
|
||||
onSetRoomNotifState: fn(),
|
||||
onCreateSection: fn(),
|
||||
onToggleSection: fn(),
|
||||
};
|
||||
|
||||
@@ -106,6 +106,7 @@ export const createMockRoomSnapshot = (id: string, name: string, index: number):
|
||||
canMarkAsUnread: true,
|
||||
roomNotifState: RoomNotifState.AllMessages,
|
||||
canMoveToSection: true,
|
||||
sections: [],
|
||||
});
|
||||
|
||||
export function createMockRoomItemViewModel(roomId: string, name: string, index: number): RoomListItemViewModel {
|
||||
@@ -123,6 +124,7 @@ export function createMockRoomItemViewModel(roomId: string, name: string, index:
|
||||
onLeaveRoom: fn(),
|
||||
onSetRoomNotifState: fn(),
|
||||
onCreateSection: fn(),
|
||||
onToggleSection: fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user