Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/merge-modules
# Conflicts: # pnpm-lock.yaml # pnpm-workspace.yaml
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -110,7 +110,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||
"@typescript-eslint/parser": "^8.53.1",
|
||||
"@vector-im/compound-web": "catalog:",
|
||||
"@vitest/browser-playwright": "^4.0.17",
|
||||
"@vitest/browser-playwright": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"eslint": "8",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
|
||||
@@ -150,7 +150,8 @@
|
||||
"low_priority": "Low priority",
|
||||
"mark_read": "Mark as read",
|
||||
"mark_unread": "Mark as unread",
|
||||
"move_to_section": "Move to"
|
||||
"move_to_section": "Move to",
|
||||
"remove_from_section": "Remove from section"
|
||||
},
|
||||
"notification_options": "Notification options",
|
||||
"open_space_menu": "Open space menu",
|
||||
|
||||
@@ -32,6 +32,12 @@ const meta = {
|
||||
</div>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/qurBlLqjf3mRNpyZ1ffamm/ER-213---Sections?node-id=1233-22137&t=ftTEpAma7PgRaaqB-4",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof RoomListToast>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ComponentType, type JSX, type MouseEventHandler } from "react";
|
||||
import React, { type JSX, type MouseEventHandler } from "react";
|
||||
import { Toast } from "@vector-im/compound-web";
|
||||
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
|
||||
|
||||
import styles from "./RoomListToast.module.css";
|
||||
import { useI18n } from "../../../core/i18n/i18nContext";
|
||||
@@ -32,19 +31,19 @@ interface RoomListToastProps {
|
||||
export function RoomListToast({ type, onClose }: Readonly<RoomListToastProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
|
||||
let content: { text: string; icon: ComponentType<React.SVGAttributes<SVGElement>> };
|
||||
let text: string;
|
||||
switch (type) {
|
||||
case "section_created":
|
||||
content = { text: _t("room_list|section_created"), icon: CheckIcon };
|
||||
text = _t("room_list|section_created");
|
||||
break;
|
||||
case "chat_moved":
|
||||
content = { text: _t("room_list|chat_moved"), icon: CheckIcon };
|
||||
text = _t("room_list|chat_moved");
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Toast className={styles.toast} Icon={content.icon} onClose={onClose} tooltip={_t("action|close")}>
|
||||
{content.text}
|
||||
<Toast className={styles.toast} onClose={onClose} tooltip={_t("action|close")}>
|
||||
{text}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,19 +11,6 @@ exports[`<RoomListToast /> > renders ChatMoved story 1`] = `
|
||||
<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
|
||||
@@ -67,19 +54,6 @@ exports[`<RoomListToast /> > renders SectionCreated story 1`] = `
|
||||
<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>
|
||||
Section created
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -16868,19 +16868,6 @@ exports[`<RoomListView /> > renders Toast story 1`] = `
|
||||
<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>
|
||||
Section created
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -37,6 +37,7 @@ const RoomListItemDragOverlayWrapperImpl = ({
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
onRemoveFromSection,
|
||||
renderAvatar: renderAvatarProp,
|
||||
...rest
|
||||
}: RoomListItemDragOverlayProps): JSX.Element => {
|
||||
@@ -52,6 +53,7 @@ const RoomListItemDragOverlayWrapperImpl = ({
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
onRemoveFromSection,
|
||||
});
|
||||
return <RoomListItemDragOverlayView vm={vm} renderAvatar={renderAvatarProp} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.sectionLabel {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
@@ -8,28 +8,15 @@
|
||||
import React, { type JSX } from "react";
|
||||
import { render, screen } from "@test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { RoomListItemMoreOptionsMenu, MoreOptionContent } from "./RoomListItemMoreOptionsMenu";
|
||||
import { useMockedViewModel } from "../../../../core/viewmodel";
|
||||
import type { RoomListItemViewSnapshot } from "./RoomListItemView";
|
||||
import { defaultSnapshot } from "./default-snapshot";
|
||||
import { mockedActions as mockCallbacks } from "./mocked-actions";
|
||||
|
||||
describe("<RoomListItemMoreOptionsMenu />", () => {
|
||||
const mockCallbacks = {
|
||||
onOpenRoom: vi.fn(),
|
||||
onMarkAsRead: vi.fn(),
|
||||
onMarkAsUnread: vi.fn(),
|
||||
onToggleFavorite: vi.fn(),
|
||||
onToggleLowPriority: vi.fn(),
|
||||
onInvite: vi.fn(),
|
||||
onCopyRoomLink: vi.fn(),
|
||||
onLeaveRoom: vi.fn(),
|
||||
onSetRoomNotifState: vi.fn(),
|
||||
onCreateSection: vi.fn(),
|
||||
onToggleSection: vi.fn(),
|
||||
};
|
||||
|
||||
const renderMenu = (overrides: Partial<RoomListItemViewSnapshot> = {}): ReturnType<typeof render> => {
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel(
|
||||
@@ -242,6 +229,20 @@ describe("<RoomListItemMoreOptionsMenu />", () => {
|
||||
expect(mockCallbacks.onCreateSection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onRemoveFromSection when Remove from section is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel({ ...defaultSnapshot, isInSection: true }, mockCallbacks);
|
||||
return <MoreOptionContent vm={vm} />;
|
||||
};
|
||||
render(<TestComponent />);
|
||||
|
||||
const removeFromSection = screen.getByRole("menuitem", { name: "Remove from section" });
|
||||
await user.click(removeFromSection);
|
||||
|
||||
expect(mockCallbacks.onRemoveFromSection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render section items in move to section submenu", () => {
|
||||
const sections = [
|
||||
{ tag: "m.favourite", name: "Favourites", isSelected: false },
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { useState, type JSX } from "react";
|
||||
import React, { useMemo, useState, type JSX } from "react";
|
||||
import { IconButton, Menu, MenuItem, Separator, SubMenu, ToggleMenuItem } from "@vector-im/compound-web";
|
||||
import {
|
||||
MarkAsReadIcon,
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
OverflowHorizontalIcon,
|
||||
ArrowRightIcon,
|
||||
CheckIcon,
|
||||
MinusIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { _t } from "../../../../core/i18n/i18n";
|
||||
import { useViewModel, type ViewModel } from "../../../../core/viewmodel";
|
||||
import type { RoomListItemViewSnapshot, RoomListItemViewActions } from "./RoomListItemView";
|
||||
import styles from "./RoomListItemMoreOptionsMenu.module.css";
|
||||
|
||||
/**
|
||||
* View model type for room list item
|
||||
@@ -73,6 +75,8 @@ interface MoreOptionContentProps {
|
||||
|
||||
export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
const snapshot = useViewModel(vm);
|
||||
const hasSections = snapshot.sections.length > 0;
|
||||
const isInSection = useMemo(() => snapshot.sections.some((section) => section.isSelected), [snapshot.sections]);
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div onKeyDown={(e) => e.stopPropagation()}>
|
||||
@@ -141,6 +145,7 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
<MenuItem
|
||||
key={section.tag}
|
||||
label={section.name}
|
||||
labelProps={{ className: styles.sectionLabel }}
|
||||
onSelect={() => vm.onToggleSection(section.tag)}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
@@ -151,10 +156,19 @@ export function MoreOptionContent({ vm }: MoreOptionContentProps): JSX.Element {
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
<Separator />
|
||||
{hasSections && <Separator />}
|
||||
<MenuItem label={_t("action|new_section")} onSelect={vm.onCreateSection} hideChevron={true} />
|
||||
</SubMenu>
|
||||
)}
|
||||
{isInSection && (
|
||||
<MenuItem
|
||||
Icon={MinusIcon}
|
||||
label={_t("room_list|more_options|remove_from_section")}
|
||||
onSelect={vm.onRemoveFromSection}
|
||||
onClick={(evt) => evt.stopPropagation()}
|
||||
hideChevron={true}
|
||||
/>
|
||||
)}
|
||||
<Separator />
|
||||
<MenuItem
|
||||
kind="critical"
|
||||
|
||||
@@ -8,29 +8,16 @@
|
||||
import React, { type JSX } from "react";
|
||||
import { render, screen } from "@test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { RoomListItemNotificationMenu } from "./RoomListItemNotificationMenu";
|
||||
import { RoomNotifState } from "./RoomNotifs";
|
||||
import { useMockedViewModel } from "../../../../core/viewmodel";
|
||||
import type { RoomListItemViewSnapshot } from "./RoomListItemView";
|
||||
import { defaultSnapshot } from "./default-snapshot";
|
||||
import { mockedActions as mockCallbacks } from "./mocked-actions";
|
||||
|
||||
describe("<RoomListItemNotificationMenu />", () => {
|
||||
const mockCallbacks = {
|
||||
onOpenRoom: vi.fn(),
|
||||
onMarkAsRead: vi.fn(),
|
||||
onMarkAsUnread: vi.fn(),
|
||||
onToggleFavorite: vi.fn(),
|
||||
onToggleLowPriority: vi.fn(),
|
||||
onInvite: vi.fn(),
|
||||
onCopyRoomLink: vi.fn(),
|
||||
onLeaveRoom: vi.fn(),
|
||||
onSetRoomNotifState: vi.fn(),
|
||||
onCreateSection: vi.fn(),
|
||||
onToggleSection: vi.fn(),
|
||||
};
|
||||
|
||||
const renderMenu = (roomNotifState: RoomNotifState = RoomNotifState.AllMessages): ReturnType<typeof render> => {
|
||||
const TestComponent = (): JSX.Element => {
|
||||
const vm = useMockedViewModel(
|
||||
|
||||
@@ -72,7 +72,12 @@
|
||||
|
||||
.dragging {
|
||||
outline: 1px solid var(--cpd-color-border-interactive-hovered);
|
||||
background-color: color-mix(in srgb, var(--cpd-color-bg-action-tertiary-hovered) 90%, transparent);
|
||||
background-color: color-mix(in srgb, var(--cpd-color-bg-action-tertiary-hovered) 95%, transparent);
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
|
||||
.dragSource .container {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -40,6 +40,7 @@ const RoomListItemWrapperImpl = ({
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
onRemoveFromSection,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onFocus,
|
||||
@@ -60,6 +61,7 @@ const RoomListItemWrapperImpl = ({
|
||||
onSetRoomNotifState,
|
||||
onCreateSection,
|
||||
onToggleSection,
|
||||
onRemoveFromSection,
|
||||
});
|
||||
return (
|
||||
<RoomListItemView
|
||||
|
||||
@@ -125,6 +125,8 @@ export interface RoomListItemViewActions {
|
||||
onCreateSection: () => void;
|
||||
/** Called when toggling a room's membership in a section */
|
||||
onToggleSection: (tag: string) => void;
|
||||
/** Called when removing the room from a section */
|
||||
onRemoveFromSection: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +152,8 @@ export interface RoomListItemViewProps extends Omit<React.HTMLAttributes<HTMLBut
|
||||
isLastItem: boolean;
|
||||
/** Function to render the room avatar */
|
||||
renderAvatar: (room: Room) => ReactNode;
|
||||
/** Whether this item is the source of an active drag operation */
|
||||
isDragSource?: boolean;
|
||||
ref?: Ref<Element>;
|
||||
}
|
||||
|
||||
@@ -165,6 +169,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
isFirstItem,
|
||||
isLastItem,
|
||||
renderAvatar,
|
||||
isDragSource = false,
|
||||
ref,
|
||||
...props
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
@@ -191,6 +196,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
[styles.bold]: item.isBold,
|
||||
[styles.firstItem]: isFirstItem,
|
||||
[styles.lastItem]: isLastItem,
|
||||
[styles.dragSource]: isDragSource,
|
||||
mx_RoomListItemView_selected: isSelected,
|
||||
})}
|
||||
gap="var(--cpd-space-3x)"
|
||||
|
||||
@@ -45,7 +45,7 @@ export const defaultSnapshot: RoomListItemViewSnapshot = {
|
||||
},
|
||||
{
|
||||
tag: "element.io.section.work",
|
||||
name: "Work",
|
||||
name: "Work with a very long name that should be truncated",
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,4 +21,5 @@ export const mockedActions: RoomListItemViewActions = {
|
||||
onSetRoomNotifState: fn(),
|
||||
onCreateSection: fn(),
|
||||
onToggleSection: fn(),
|
||||
onRemoveFromSection: fn(),
|
||||
};
|
||||
|
||||
@@ -55,12 +55,16 @@ export const RoomListItemWrapper = memo(function RoomListItemWrapper({
|
||||
*/
|
||||
function DraggableWrapper(props: RoomListItemViewProps): JSX.Element {
|
||||
const item = useViewModel(props.vm);
|
||||
const { ref: draggableRef, handleRef } = useDraggable({
|
||||
const {
|
||||
ref: draggableRef,
|
||||
handleRef,
|
||||
isDragSource,
|
||||
} = useDraggable({
|
||||
id: item.id,
|
||||
// We clone the item in the dnd overlay to avoid putting a hole in the list
|
||||
plugins: [Feedback.configure({ feedback: "clone" })],
|
||||
modifiers: [RestrictToVerticalAxis],
|
||||
});
|
||||
const dndRef = useMergeRefs([draggableRef, handleRef]);
|
||||
return <RoomListItemView {...props} ref={dndRef} />;
|
||||
return <RoomListItemView {...props} ref={dndRef} isDragSource={isDragSource} />;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ export function createMockRoomItemViewModel(roomId: string, name: string, index:
|
||||
onSetRoomNotifState: fn(),
|
||||
onCreateSection: fn(),
|
||||
onToggleSection: fn(),
|
||||
onRemoveFromSection: fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||