mirror of
https://github.com/mattermost/mattermost.git
synced 2026-09-24 16:05:00 +08:00
MM-63056 Migrate thread menu to new component and update label
This commit is contained in:
+3
-60
@@ -51,26 +51,7 @@ exports[`components/threading/global_threads/thread_item should report total num
|
||||
isFollowing={true}
|
||||
threadId="1y8hpek81byspd4enyk9mp1ncw"
|
||||
unreadTimestamp={1611786714912}
|
||||
>
|
||||
<WithTooltip
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Actions"
|
||||
id="threading.threadItem.menu"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Memo(Button)
|
||||
aria-label="Actions"
|
||||
className="Button___icon"
|
||||
marginTop={true}
|
||||
>
|
||||
<DotsVerticalIcon
|
||||
size={18}
|
||||
/>
|
||||
</Memo(Button)>
|
||||
</WithTooltip>
|
||||
</MockThreadMenu>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="preview"
|
||||
@@ -205,26 +186,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
|
||||
isFollowing={true}
|
||||
threadId="1y8hpek81byspd4enyk9mp1ncw"
|
||||
unreadTimestamp={1611786714912}
|
||||
>
|
||||
<WithTooltip
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Actions"
|
||||
id="threading.threadItem.menu"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Memo(Button)
|
||||
aria-label="Actions"
|
||||
className="Button___icon"
|
||||
marginTop={true}
|
||||
>
|
||||
<DotsVerticalIcon
|
||||
size={18}
|
||||
/>
|
||||
</Memo(Button)>
|
||||
</WithTooltip>
|
||||
</MockThreadMenu>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="preview"
|
||||
@@ -357,26 +319,7 @@ exports[`components/threading/global_threads/thread_item should report unread me
|
||||
isFollowing={true}
|
||||
threadId="1y8hpek81byspd4enyk9mp1ncw"
|
||||
unreadTimestamp={1611786714912}
|
||||
>
|
||||
<WithTooltip
|
||||
title={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Actions"
|
||||
id="threading.threadItem.menu"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Memo(Button)
|
||||
aria-label="Actions"
|
||||
className="Button___icon"
|
||||
marginTop={true}
|
||||
>
|
||||
<DotsVerticalIcon
|
||||
size={18}
|
||||
/>
|
||||
</Memo(Button)>
|
||||
</WithTooltip>
|
||||
</MockThreadMenu>
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="preview"
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
&:focus-within,
|
||||
&:has([aria-expanded="true"]) {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
|
||||
.alt-visible {
|
||||
|
||||
+1
-24
@@ -7,7 +7,6 @@ import type {MouseEvent, KeyboardEvent} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import {PostPriority} from '@mattermost/types/posts';
|
||||
@@ -27,12 +26,10 @@ import {getIsMobileView} from 'selectors/views/browser';
|
||||
import Markdown from 'components/markdown';
|
||||
import {makeGetMentionKeysForPost} from 'components/post_markdown';
|
||||
import PriorityBadge from 'components/post_priority/post_priority_badge';
|
||||
import Button from 'components/threading/common/button';
|
||||
import Timestamp from 'components/timestamp';
|
||||
import CRTListTutorialTip from 'components/tours/crt_tour/crt_list_tutorial_tip';
|
||||
import Tag from 'components/widgets/tag/tag';
|
||||
import Avatars from 'components/widgets/users/avatars';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {CrtTutorialSteps, Preferences} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -256,27 +253,7 @@ function ThreadItem({
|
||||
isFollowing={isFollowing ?? false}
|
||||
hasUnreads={Boolean(newReplies)}
|
||||
unreadTimestamp={unreadTimestamp}
|
||||
>
|
||||
<WithTooltip
|
||||
title={(
|
||||
<FormattedMessage
|
||||
id='threading.threadItem.menu'
|
||||
defaultMessage='Actions'
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
marginTop={true}
|
||||
className='Button___icon'
|
||||
aria-label={formatMessage({
|
||||
id: 'threading.threadItem.menu',
|
||||
defaultMessage: 'Actions',
|
||||
})}
|
||||
>
|
||||
<DotsVerticalIcon size={18}/>
|
||||
</Button>
|
||||
</WithTooltip>
|
||||
</ThreadMenu>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* The strange interaction here where we need a click/keydown handler messes with the ESLint rules, so we just disable it */}
|
||||
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/threading/common/thread_menu should match snapshot 1`] = `
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button>
|
||||
test
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel="Actions"
|
||||
openLeft={true}
|
||||
>
|
||||
<MenuItemAction
|
||||
extraText="You will be notified about replies"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Follow thread"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Open in channel"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Mark as unread"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Save"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Copy link"
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
`;
|
||||
|
||||
exports[`components/threading/common/thread_menu should match snapshot after opening 1`] = `
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button>
|
||||
test
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel="Actions"
|
||||
openLeft={true}
|
||||
>
|
||||
<MenuItemAction
|
||||
extraText="You will be notified about replies"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Follow thread"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Open in channel"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Mark as unread"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Save"
|
||||
/>
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Copy link"
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
`;
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
.ThreadMenu {
|
||||
.MenuItem__help-text {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
+169
-90
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import set from 'lodash/set';
|
||||
import React from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
|
||||
@@ -14,13 +12,11 @@ import {
|
||||
} from 'actions/post_actions';
|
||||
import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||
import {fakeDate} from 'tests/helpers/date';
|
||||
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
|
||||
import {copyToClipboard} from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ThreadMenu from '../thread_menu';
|
||||
|
||||
jest.mock('mattermost-redux/actions/threads');
|
||||
@@ -46,162 +42,245 @@ jest.mock('../../hooks', () => {
|
||||
});
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
let mockState: GlobalState;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
describe('components/threading/common/thread_menu', () => {
|
||||
let props: ComponentProps<typeof ThreadMenu>;
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
preferences: {myPreferences: {}},
|
||||
teams: {currentTeamId: 'tid'},
|
||||
general: {config: {}},
|
||||
users: {currentUserId: 'uid'},
|
||||
},
|
||||
views: {
|
||||
browser: {
|
||||
windowSize: 'desktopView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
threadId: '1y8hpek81byspd4enyk9mp1ncw',
|
||||
unreadTimestamp: 1610486901110,
|
||||
hasUnreads: false,
|
||||
isFollowing: false,
|
||||
children: (
|
||||
<button>{'test'}</button>
|
||||
),
|
||||
};
|
||||
|
||||
mockState = {entities: {preferences: {myPreferences: {}}}} as GlobalState;
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('should render thread menu button', () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(screen.getByRole('button', {name: 'More Actions'})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match snapshot after opening', () => {
|
||||
const wrapper = shallow(
|
||||
test('should open menu when button is clicked', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
expect(screen.getByRole('menuitem', {name: /Follow thread/})).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', {name: /Open in channel/})).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', {name: /Mark as unread/})).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', {name: /Save/})).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitem', {name: /Copy link/})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow following', () => {
|
||||
const wrapper = shallow(
|
||||
test('should allow following', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
isFollowing={false}
|
||||
/>,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Follow thread'}).simulate('click');
|
||||
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', true);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const followButton = await screen.findByRole('menuitem', {name: /Follow thread/});
|
||||
await userEvent.click(followButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', true);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow unfollowing', () => {
|
||||
const wrapper = shallow(
|
||||
test('should allow unfollowing', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
isFollowing={true}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Unfollow thread'}).simulate('click');
|
||||
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', false);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const unfollowButton = screen.getByRole('menuitem', {name: /Unfollow thread/});
|
||||
await userEvent.click(unfollowButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setThreadFollow).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', false);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow opening in channel', () => {
|
||||
const wrapper = shallow(
|
||||
test('should allow opening in channel', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Open in channel'}).simulate('click');
|
||||
expect(mockRouting.goToInChannel).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const openInChannelButton = screen.getByRole('menuitem', {name: /Open in channel/});
|
||||
await userEvent.click(openInChannelButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouting.goToInChannel).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow marking as read', () => {
|
||||
test('should allow marking as read', async () => {
|
||||
const resetFakeDate = fakeDate(new Date(1612582579566));
|
||||
const wrapper = shallow(
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
hasUnreads={true}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Mark as read'}).simulate('click');
|
||||
expect(markLastPostInThreadAsUnread).not.toHaveBeenCalled();
|
||||
expect(updateThreadRead).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
|
||||
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const markAsReadButton = screen.getByRole('menuitem', {name: /Mark as read/});
|
||||
await userEvent.click(markAsReadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(markLastPostInThreadAsUnread).not.toHaveBeenCalled();
|
||||
expect(updateThreadRead).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
|
||||
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1612582579566);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
resetFakeDate();
|
||||
});
|
||||
|
||||
test('should allow marking as unread', () => {
|
||||
const wrapper = shallow(
|
||||
test('should allow marking as unread', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
hasUnreads={false}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Mark as unread'}).simulate('click');
|
||||
expect(updateThreadRead).not.toHaveBeenCalled();
|
||||
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1610486901110);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('should allow saving', () => {
|
||||
const wrapper = shallow(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Save'}).simulate('click');
|
||||
expect(savePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test('should allow unsaving', () => {
|
||||
set(mockState, 'entities.preferences.myPreferences', {
|
||||
'flagged_post--1y8hpek81byspd4enyk9mp1ncw': {
|
||||
user_id: 'uid',
|
||||
category: 'flagged_post',
|
||||
name: '1y8hpek81byspd4enyk9mp1ncw',
|
||||
value: 'true',
|
||||
},
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const markAsUnreadButton = screen.getByRole('menuitem', {name: /Mark as unread/});
|
||||
await userEvent.click(markAsUnreadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateThreadRead).not.toHaveBeenCalled();
|
||||
expect(markLastPostInThreadAsUnread).toHaveBeenCalledWith('uid', 'tid', '1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(manuallyMarkThreadAsUnread).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw', 1610486901110);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
const wrapper = shallow(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Unsave'}).simulate('click');
|
||||
expect(unsavePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should allow link copying', () => {
|
||||
const wrapper = shallow(
|
||||
test('should allow saving', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
wrapper.find(Menu.ItemAction).find({text: 'Copy link'}).simulate('click');
|
||||
expect(copyToClipboard).toHaveBeenCalledWith('http://localhost:8065/team-name-1/pl/1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const saveButton = screen.getByRole('menuitem', {name: /Save/});
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(savePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
test('should allow unsaving', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
mergeObjects(baseState, {
|
||||
entities: {
|
||||
preferences: {
|
||||
myPreferences: {
|
||||
'flagged_post--1y8hpek81byspd4enyk9mp1ncw': {
|
||||
user_id: 'uid',
|
||||
category: 'flagged_post',
|
||||
name: '1y8hpek81byspd4enyk9mp1ncw',
|
||||
value: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const unsaveButton = screen.getByRole('menuitem', {name: /Unsave/});
|
||||
await userEvent.click(unsaveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unsavePost).toHaveBeenCalledWith('1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow link copying', async () => {
|
||||
renderWithContext(
|
||||
<ThreadMenu
|
||||
{...props}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
|
||||
const menuButton = screen.getByRole('button', {name: 'More Actions'});
|
||||
await userEvent.click(menuButton);
|
||||
|
||||
const copyLinkButton = screen.getByRole('menuitem', {name: /Copy link/});
|
||||
await userEvent.click(copyLinkButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(copyToClipboard).toHaveBeenCalledWith('http://localhost:8065/team-name-1/pl/1y8hpek81byspd4enyk9mp1ncw');
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+125
-105
@@ -2,10 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
|
||||
import type {UserThread} from '@mattermost/types/threads';
|
||||
|
||||
import {setThreadFollow, updateThreadRead, markLastPostInThreadAsUnread} from 'mattermost-redux/actions/threads';
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
} from 'actions/post_actions';
|
||||
import {manuallyMarkThreadAsUnread} from 'actions/views/threads';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {useReadout} from 'hooks/useReadout';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
@@ -28,13 +27,10 @@ import type {GlobalState} from 'types/store';
|
||||
|
||||
import {useThreadRouting} from '../../hooks';
|
||||
|
||||
import './thread_menu.scss';
|
||||
|
||||
type Props = {
|
||||
threadId: UserThread['id'];
|
||||
isFollowing?: boolean;
|
||||
hasUnreads: boolean;
|
||||
children: ReactNode;
|
||||
unreadTimestamp: number;
|
||||
};
|
||||
|
||||
@@ -43,7 +39,6 @@ function ThreadMenu({
|
||||
isFollowing = false,
|
||||
unreadTimestamp,
|
||||
hasUnreads,
|
||||
children,
|
||||
}: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
@@ -85,106 +80,131 @@ function ThreadMenu({
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenuWrapper
|
||||
stopPropagationOnToggle={true}
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: `thread-menu-${threadId}`,
|
||||
class: 'btn btn-icon btn-sm',
|
||||
'aria-label': formatMessage({
|
||||
id: 'threading.threadHeader.menu',
|
||||
defaultMessage: 'More Actions',
|
||||
}),
|
||||
children: (
|
||||
<DotsVerticalIcon size={18}/>
|
||||
),
|
||||
}}
|
||||
menuButtonTooltip={{
|
||||
text: formatMessage({
|
||||
id: 'threading.threadHeader.menu',
|
||||
defaultMessage: 'More Actions',
|
||||
}),
|
||||
}}
|
||||
menu={{
|
||||
id: `thread-menu-dropdown-${threadId}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Menu
|
||||
ariaLabel={formatMessage({
|
||||
id: 'threading.threadItem.menu',
|
||||
defaultMessage: 'Actions',
|
||||
})}
|
||||
openLeft={true}
|
||||
>
|
||||
<Menu.ItemAction
|
||||
{...isFollowing ? {
|
||||
text: formatMessage({
|
||||
id: 'threading.threadMenu.unfollow',
|
||||
defaultMessage: 'Unfollow thread',
|
||||
}),
|
||||
extraText: formatMessage({
|
||||
id: 'threading.threadMenu.unfollowExtra',
|
||||
defaultMessage: 'You won’t be notified about replies',
|
||||
}),
|
||||
} : {
|
||||
text: formatMessage({
|
||||
id: 'threading.threadMenu.follow',
|
||||
defaultMessage: 'Follow thread',
|
||||
}),
|
||||
extraText: formatMessage({
|
||||
id: 'threading.threadMenu.followExtra',
|
||||
defaultMessage: 'You will be notified about replies',
|
||||
}),
|
||||
}}
|
||||
onClick={useCallback(() => {
|
||||
dispatch(setThreadFollow(currentUserId, currentTeamId, threadId, !isFollowing));
|
||||
readAloud(isFollowing ? formatMessage({
|
||||
id: 'threading.threadMenu.unfollowed',
|
||||
defaultMessage: 'Unfollowed thread',
|
||||
}) : formatMessage({
|
||||
id: 'threading.threadMenu.followed',
|
||||
defaultMessage: 'Followed thread',
|
||||
}));
|
||||
}, [currentUserId, currentTeamId, threadId, isFollowing, setThreadFollow, readAloud, formatMessage])}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
text={formatMessage({
|
||||
id: 'threading.threadMenu.openInChannel',
|
||||
defaultMessage: 'Open in channel',
|
||||
})}
|
||||
onClick={useCallback(() => {
|
||||
goToInChannel(threadId);
|
||||
readAloud(formatMessage({
|
||||
id: 'threading.threadMenu.openingChannel',
|
||||
defaultMessage: 'Opening channel',
|
||||
}));
|
||||
}, [threadId, readAloud, formatMessage])}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
text={hasUnreads ? formatMessage({
|
||||
id: 'threading.threadMenu.markRead',
|
||||
defaultMessage: 'Mark as read',
|
||||
<Menu.Item
|
||||
labels={isFollowing ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.unfollow'
|
||||
defaultMessage='Unfollow thread'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.unfollowExtra'
|
||||
defaultMessage='You won’t be notified about replies'
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.follow'
|
||||
defaultMessage='Follow thread'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.followExtra'
|
||||
defaultMessage='You will be notified about replies'
|
||||
/>
|
||||
</>)
|
||||
}
|
||||
onClick={useCallback(() => {
|
||||
dispatch(setThreadFollow(currentUserId, currentTeamId, threadId, !isFollowing));
|
||||
readAloud(isFollowing ? formatMessage({
|
||||
id: 'threading.threadMenu.unfollowed',
|
||||
defaultMessage: 'Unfollowed thread',
|
||||
}) : formatMessage({
|
||||
id: 'threading.threadMenu.markUnread',
|
||||
defaultMessage: 'Mark as unread',
|
||||
})}
|
||||
onClick={handleReadUnread}
|
||||
/>
|
||||
|
||||
<Menu.ItemAction
|
||||
text={isSaved ? formatMessage({
|
||||
id: 'threading.threadMenu.unsave',
|
||||
defaultMessage: 'Unsave',
|
||||
id: 'threading.threadMenu.followed',
|
||||
defaultMessage: 'Followed thread',
|
||||
}));
|
||||
}, [currentUserId, currentTeamId, threadId, isFollowing, setThreadFollow, readAloud, formatMessage])}
|
||||
/>
|
||||
<Menu.Item
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.openInChannel'
|
||||
defaultMessage='Open in channel'
|
||||
/>
|
||||
}
|
||||
onClick={useCallback(() => {
|
||||
goToInChannel(threadId);
|
||||
readAloud(formatMessage({
|
||||
id: 'threading.threadMenu.openingChannel',
|
||||
defaultMessage: 'Opening channel',
|
||||
}));
|
||||
}, [threadId, readAloud, formatMessage])}
|
||||
/>
|
||||
<Menu.Item
|
||||
labels={hasUnreads ? (
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.markRead'
|
||||
defaultMessage='Mark as read'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.markUnread'
|
||||
defaultMessage='Mark as unread'
|
||||
/>
|
||||
)}
|
||||
onClick={handleReadUnread}
|
||||
/>
|
||||
<Menu.Item
|
||||
labels={isSaved ? (
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.unsave'
|
||||
defaultMessage='Unsave'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
)}
|
||||
onClick={useCallback(() => {
|
||||
dispatch(isSaved ? unsavePost(threadId) : savePost(threadId));
|
||||
readAloud(isSaved ? formatMessage({
|
||||
id: 'threading.threadMenu.unsaved',
|
||||
defaultMessage: 'Unsaved',
|
||||
}) : formatMessage({
|
||||
id: 'threading.threadMenu.save',
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
onClick={useCallback(() => {
|
||||
dispatch(isSaved ? unsavePost(threadId) : savePost(threadId));
|
||||
readAloud(isSaved ? formatMessage({
|
||||
id: 'threading.threadMenu.unsaved',
|
||||
defaultMessage: 'Unsaved',
|
||||
}) : formatMessage({
|
||||
id: 'threading.threadMenu.saved',
|
||||
defaultMessage: 'Saved',
|
||||
}));
|
||||
}, [threadId, isSaved])}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
text={formatMessage({
|
||||
id: 'threading.threadMenu.copy',
|
||||
defaultMessage: 'Copy link',
|
||||
})}
|
||||
onClick={useCallback(() => {
|
||||
copyToClipboard(`${getSiteURL()}/${team}/pl/${threadId}`);
|
||||
readAloud(formatMessage({
|
||||
id: 'threading.threadMenu.linkCopied',
|
||||
defaultMessage: 'Link copied',
|
||||
}));
|
||||
}, [team, threadId])}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
id: 'threading.threadMenu.saved',
|
||||
defaultMessage: 'Saved',
|
||||
}));
|
||||
}, [threadId, isSaved])}
|
||||
/>
|
||||
<Menu.Item
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='threading.threadMenu.copy'
|
||||
defaultMessage='Copy link'
|
||||
/>
|
||||
}
|
||||
onClick={useCallback(() => {
|
||||
copyToClipboard(`${getSiteURL()}/${team}/pl/${threadId}`);
|
||||
readAloud(formatMessage({
|
||||
id: 'threading.threadMenu.linkCopied',
|
||||
defaultMessage: 'Link copied',
|
||||
}));
|
||||
}, [team, threadId])}
|
||||
/>
|
||||
</Menu.Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-13
@@ -45,19 +45,7 @@ exports[`components/threading/global_threads/thread_pane should match snapshot 1
|
||||
isFollowing={true}
|
||||
threadId="1y8hpek81byspd4enyk9mp1ncw"
|
||||
unreadTimestamp={1611786714912}
|
||||
>
|
||||
<WithTooltip
|
||||
title="More Actions"
|
||||
>
|
||||
<Memo(Button)
|
||||
className="Button___icon Button___large"
|
||||
>
|
||||
<DotsVerticalIcon
|
||||
size={18}
|
||||
/>
|
||||
</Memo(Button)>
|
||||
</WithTooltip>
|
||||
</Memo(ThreadMenu)>
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
/>
|
||||
|
||||
+1
-8
@@ -13,6 +13,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: var(--border-default);
|
||||
gap: 4px;
|
||||
grid-area: header;
|
||||
|
||||
--button-separator-height: 24px;
|
||||
@@ -55,14 +56,6 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.MenuWrapper {
|
||||
margin-left: 4px;
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ThreadViewer {
|
||||
|
||||
+1
-14
@@ -6,7 +6,6 @@ import type {ReactNode} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
|
||||
import {DotsVerticalIcon} from '@mattermost/compass-icons/components';
|
||||
import type {UserThread} from '@mattermost/types/threads';
|
||||
|
||||
import {setThreadFollow} from 'mattermost-redux/actions/threads';
|
||||
@@ -14,7 +13,6 @@ import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import Header from 'components/widgets/header';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
@@ -118,18 +116,7 @@ const ThreadPane = ({
|
||||
isFollowing={isFollowing}
|
||||
hasUnreads={Boolean(thread.unread_replies || thread.unread_mentions)}
|
||||
unreadTimestamp={unreadTimestamp}
|
||||
>
|
||||
<WithTooltip
|
||||
title={formatMessage({
|
||||
id: 'threading.threadHeader.menu',
|
||||
defaultMessage: 'More Actions',
|
||||
})}
|
||||
>
|
||||
<Button className='Button___icon Button___large'>
|
||||
<DotsVerticalIcon size={18}/>
|
||||
</Button>
|
||||
</WithTooltip>
|
||||
</ThreadMenu>
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -5762,7 +5762,6 @@
|
||||
"threading.numReplies": "{totalReplies, plural, =0 {Reply} =1 {# reply} other {# replies}}",
|
||||
"threading.threadHeader.menu": "More Actions",
|
||||
"threading.threadItem.ariaLabel": "Thread by {author}",
|
||||
"threading.threadItem.menu": "Actions",
|
||||
"threading.threadItem.timestamp": "Last reply ",
|
||||
"threading.threadList.markRead": "Mark all threads as read",
|
||||
"threading.threadList.tabsLabel": "Filter visible threads",
|
||||
|
||||
Reference in New Issue
Block a user