mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-21 05:54:10 +08:00
Fixed a bug where user profile popover closed automatically when opened for the first time for a user from channel member list in RHS (#35918)
* Fixed a bug where user profile popover closed automatically when opened for the first time for a user from channel member list in RHS * Added tests * fixed a test
This commit is contained in:
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test('should open and close profile popover from channel members RHS', async ({pw}) => {
|
||||
// # Initialize setup with two users in the same team and channel
|
||||
const {user, team, adminClient} = await pw.initSetup();
|
||||
|
||||
const channel = await adminClient.createChannel(
|
||||
pw.random.channel({teamId: team.id, displayName: 'Test Channel', name: 'test-channel'}),
|
||||
);
|
||||
await adminClient.addToChannel(user.id, channel.id);
|
||||
|
||||
const testUser = await adminClient.createUser(await pw.random.user(), '', '');
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
await adminClient.addToChannel(testUser.id, channel.id);
|
||||
|
||||
// # Log in and navigate to the channel
|
||||
const {channelsPage, page} = await pw.testBrowser.login(user);
|
||||
await channelsPage.goto(team.name, channel.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open channel members RHS by clicking the Members button in the header
|
||||
await channelsPage.centerView.header.openChannelMenu();
|
||||
const membersMenuItem = page.locator('#channelMembers');
|
||||
await membersMenuItem.click();
|
||||
|
||||
// * Verify the channel members RHS is visible
|
||||
await channelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// # Find the test user in the member list and click their display name to open profile popover
|
||||
const memberEntry = page.getByTestId(`memberline-${testUser.id}`);
|
||||
await expect(memberEntry).toBeVisible();
|
||||
|
||||
const displayName = memberEntry.locator('.channel-members-rhs__display-name');
|
||||
await displayName.click();
|
||||
|
||||
// * Verify the profile popover is visible
|
||||
const popover = channelsPage.userProfilePopover;
|
||||
await popover.toBeVisible();
|
||||
await expect(popover.container.getByText(`@${testUser.username}`)).toBeVisible();
|
||||
|
||||
// # Click outside the popover to close it
|
||||
await page.mouse.click(1, 1);
|
||||
|
||||
// * Verify the profile popover is no longer visible
|
||||
await expect(popover.container).not.toBeVisible();
|
||||
});
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useEffect, useRef, useState} from 'react';
|
||||
import React, {memo, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {VariableSizeList} from 'react-window';
|
||||
import type {ListChildComponentProps} from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Member from './member';
|
||||
import MemberListItem from './member_list_item';
|
||||
import type {ItemData} from './member_list_item';
|
||||
|
||||
export interface ChannelMember {
|
||||
user: UserProfile;
|
||||
@@ -92,44 +92,20 @@ const MemberList = ({
|
||||
return 48;
|
||||
};
|
||||
|
||||
const Item = ({index, style}: ListChildComponentProps) => {
|
||||
if (isItemLoaded(index)) {
|
||||
switch (members[index].type) {
|
||||
case ListItemType.Member:
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const member = members[index].data as ChannelMember;
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
key={member.user.id}
|
||||
>
|
||||
<Member
|
||||
channel={channel}
|
||||
index={index}
|
||||
totalUsers={members.filter((l) => l.type === ListItemType.Member).length}
|
||||
member={member}
|
||||
editing={editing}
|
||||
actions={{openDirectMessage, fetchRemoteClusterInfo}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case ListItemType.Separator:
|
||||
case ListItemType.FirstSeparator:
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={style}
|
||||
>
|
||||
{members[index].data as JSX.Element}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const totalMemberCount = useMemo(
|
||||
() => members.filter((l) => l.type === ListItemType.Member).length,
|
||||
[members],
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
const itemData: ItemData = useMemo(() => ({
|
||||
members,
|
||||
hasNextPage,
|
||||
channel,
|
||||
editing,
|
||||
totalMemberCount,
|
||||
openDirectMessage,
|
||||
fetchRemoteClusterInfo,
|
||||
}), [members, hasNextPage, channel, editing, totalMemberCount, openDirectMessage, fetchRemoteClusterInfo]);
|
||||
|
||||
if (members.length === 0) {
|
||||
return null;
|
||||
@@ -153,12 +129,12 @@ const MemberList = ({
|
||||
ref(list);
|
||||
variableSizeListRef.current = list;
|
||||
}}
|
||||
|
||||
itemData={itemData}
|
||||
itemSize={getItemSize}
|
||||
height={height}
|
||||
width={width}
|
||||
>
|
||||
{Item}
|
||||
{MemberListItem}
|
||||
</VariableSizeList>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {ChannelMember} from './member_list';
|
||||
import {ListItemType} from './member_list';
|
||||
import type {ItemData} from './member_list_item';
|
||||
import MemberListItem from './member_list_item';
|
||||
|
||||
jest.mock('./member', () => {
|
||||
return (props: any) => (
|
||||
<div data-testid={`mock-member-${props.member.user.id}`}>
|
||||
{props.member.displayName}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
describe('components/channel_members_rhs/MemberListItem', () => {
|
||||
const mockChannel = TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
display_name: 'Test Channel',
|
||||
name: 'test-channel',
|
||||
type: 'O' as ChannelType,
|
||||
team_id: 'team_id',
|
||||
});
|
||||
|
||||
const mockUser = TestHelper.getUserMock({
|
||||
id: 'user_id_1',
|
||||
username: 'testuser',
|
||||
nickname: 'Test User',
|
||||
roles: 'system_user',
|
||||
});
|
||||
|
||||
const mockMembership = TestHelper.getChannelMembershipMock({
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id_1',
|
||||
});
|
||||
|
||||
const mockMember: ChannelMember = {
|
||||
user: mockUser,
|
||||
membership: mockMembership,
|
||||
status: 'online',
|
||||
displayName: 'Test User',
|
||||
};
|
||||
|
||||
const baseItemData: ItemData = {
|
||||
members: [
|
||||
{type: ListItemType.Member, data: mockMember},
|
||||
],
|
||||
hasNextPage: false,
|
||||
channel: mockChannel,
|
||||
editing: false,
|
||||
totalMemberCount: 1,
|
||||
openDirectMessage: jest.fn(),
|
||||
fetchRemoteClusterInfo: jest.fn(),
|
||||
};
|
||||
|
||||
const baseStyle = {top: 0, left: 0, width: '100%', height: 48, position: 'absolute' as const};
|
||||
|
||||
test('should render a Member component for a member item', () => {
|
||||
const separatorData: ItemData = {
|
||||
...baseItemData,
|
||||
members: [
|
||||
{type: ListItemType.Separator, data: <span>{'Separator Label'}</span>},
|
||||
],
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<MemberListItem
|
||||
index={0}
|
||||
style={baseStyle}
|
||||
data={separatorData}
|
||||
isScrolling={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Separator Label')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should render multiple members at different indices', () => {
|
||||
const secondUser = TestHelper.getUserMock({
|
||||
id: 'user_id_2',
|
||||
username: 'seconduser',
|
||||
nickname: 'Second User',
|
||||
});
|
||||
|
||||
const secondMember: ChannelMember = {
|
||||
...mockMember,
|
||||
user: secondUser,
|
||||
displayName: 'Second User',
|
||||
};
|
||||
|
||||
const multiMemberData: ItemData = {
|
||||
...baseItemData,
|
||||
members: [
|
||||
{type: ListItemType.Member, data: mockMember},
|
||||
{type: ListItemType.Member, data: secondMember},
|
||||
],
|
||||
totalMemberCount: 2,
|
||||
};
|
||||
|
||||
const {rerender} = renderWithContext(
|
||||
<MemberListItem
|
||||
index={0}
|
||||
style={baseStyle}
|
||||
data={multiMemberData}
|
||||
isScrolling={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test User')).toBeVisible();
|
||||
|
||||
rerender(
|
||||
<MemberListItem
|
||||
index={1}
|
||||
style={baseStyle}
|
||||
data={multiMemberData}
|
||||
isScrolling={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Second User')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import type {ListChildComponentProps} from 'react-window';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Member from './member';
|
||||
import type {ChannelMember, ListItem} from './member_list';
|
||||
import {ListItemType} from './member_list';
|
||||
|
||||
export interface ItemData {
|
||||
members: ListItem[];
|
||||
hasNextPage: boolean;
|
||||
channel: Channel;
|
||||
editing: boolean;
|
||||
totalMemberCount: number;
|
||||
openDirectMessage: (user: UserProfile) => void;
|
||||
fetchRemoteClusterInfo: (remoteId: string, includeDeleted?: boolean, forceRefresh?: boolean) => void;
|
||||
}
|
||||
|
||||
const MemberListItem = memo(({index, style, data}: ListChildComponentProps<ItemData>) => {
|
||||
const {members, hasNextPage, channel, editing, totalMemberCount, openDirectMessage, fetchRemoteClusterInfo} = data;
|
||||
const isItemLoaded = !hasNextPage || index < members.length;
|
||||
|
||||
if (isItemLoaded) {
|
||||
switch (members[index].type) {
|
||||
case ListItemType.Member: {
|
||||
const member = members[index].data as ChannelMember;
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
key={member.user.id}
|
||||
>
|
||||
<Member
|
||||
channel={channel}
|
||||
index={index}
|
||||
totalUsers={totalMemberCount}
|
||||
member={member}
|
||||
editing={editing}
|
||||
actions={{openDirectMessage, fetchRemoteClusterInfo}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ListItemType.Separator:
|
||||
case ListItemType.FirstSeparator:
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={style}
|
||||
>
|
||||
{members[index].data as JSX.Element}
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default MemberListItem;
|
||||
+1
-1
@@ -33,7 +33,7 @@ const ProfilePopoverCustomAttributes = ({
|
||||
if (!userProfile.custom_profile_attributes) {
|
||||
dispatch(getCustomProfileAttributeValues(userID));
|
||||
}
|
||||
});
|
||||
}, [dispatch, userID, userProfile.custom_profile_attributes]);
|
||||
|
||||
const attributeSections = customProfileAttributeFields.map((attribute) => {
|
||||
if (!hideStatus && userProfile.custom_profile_attributes) {
|
||||
|
||||
Reference in New Issue
Block a user