MM-59065 - New channel menu using new menu system (#30093)
* New channel menu using new menu system * fix e2e-tests * remove extraneous separator * lint fix * fix test after merge * update to pass properties to first menu item * fix e2etest * refactor: Update channel header menu items to use const event handlers * refactor: Extract plugin item click handler in channel header menu * refactor: Improve error handling and button click handlers in mobile channel header plugins * lint fixes * updates for code reveiw * run i18n-extract * fix unit test * fix: Close channel dropdown menu by clicking channel header title * fix: Use keyboard escape to close channel dropdown menu in e2e tests * fix cypress test * fix: Resolve MUI Menu component fragment rendering issue * cleanup * remove unneccessary css * fixing testing issues --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8eadf849bb
Коммит
fd717cfa64
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Purpose of this file to exists is only required until channel header dropdown is migrated to new menus
|
||||
import type {ComponentProps} from 'react';
|
||||
import React, {memo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
LinkVariantIcon,
|
||||
PaperclipIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {getChannelBookmarks} from 'mattermost-redux/selectors/entities/channel_bookmarks';
|
||||
|
||||
import {useBookmarkAddActions} from 'components/channel_bookmarks/channel_bookmarks_menu';
|
||||
import {MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from 'components/channel_bookmarks/utils';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
inHeaderDropdown?: boolean;
|
||||
};
|
||||
|
||||
const ChannelBookmarksSubmenu = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(props.channel.id);
|
||||
const canAdd = useChannelBookmarkPermission(props.channel.id, 'add');
|
||||
const canUploadFiles = useCanUploadFiles();
|
||||
const limitReached = useSelector((state: GlobalState) => {
|
||||
const bookmarks = getChannelBookmarks(state, props.channel.id);
|
||||
return bookmarks && Object.keys(bookmarks).length >= MAX_BOOKMARKS_PER_CHANNEL;
|
||||
});
|
||||
|
||||
if (!canAdd || limitReached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items: ComponentProps<typeof Menu.ItemSubMenu>['subMenu'] = [
|
||||
{
|
||||
id: 'channelBookmarksAddLink',
|
||||
icon: <LinkVariantIcon size={16}/>,
|
||||
direction: 'right',
|
||||
text: formatMessage({id: 'channel_bookmarks.addLink', defaultMessage: 'Add a link'}),
|
||||
action: handleCreateLink,
|
||||
},
|
||||
];
|
||||
|
||||
if (canUploadFiles) {
|
||||
items.push({
|
||||
id: 'channelBookmarksAttachFile',
|
||||
icon: <PaperclipIcon size={16}/>,
|
||||
direction: 'right',
|
||||
text: formatMessage({id: 'channel_bookmarks.attachFile', defaultMessage: 'Attach a file'}),
|
||||
action: handleCreateFile,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.ItemSubMenu
|
||||
id={`channel-menu-${props.channel.id}-bookmarks`}
|
||||
subMenu={items}
|
||||
text={formatMessage({id: 'sidebar_left.sidebar_channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})}
|
||||
direction={'right'}
|
||||
styleSelectableItem={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChannelBookmarksSubmenu);
|
||||
@@ -1,27 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type {ReactNode} from 'react';
|
||||
import React, {memo, useState, useRef, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import React, {memo} from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getIsRhsOpen} from 'selectors/rhs';
|
||||
|
||||
import {ChannelHeaderDropdown} from 'components/channel_header_dropdown';
|
||||
import ProfilePicture from 'components/profile_picture';
|
||||
import SharedChannelIndicator from 'components/shared_channel_indicator';
|
||||
import ArchiveIcon from 'components/widgets/icons/archive_icon';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import BotTag from 'components/widgets/tag/bot_tag';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
@@ -29,6 +21,8 @@ import ChannelHeaderTitleDirect from './channel_header_title_direct';
|
||||
import ChannelHeaderTitleFavorite from './channel_header_title_favorite';
|
||||
import ChannelHeaderTitleGroup from './channel_header_title_group';
|
||||
|
||||
import ChannelHeaderMenu from '../channel_header_menu/channel_header_menu';
|
||||
|
||||
type Props = {
|
||||
dmUser?: UserProfile;
|
||||
gmMembers?: UserProfile[];
|
||||
@@ -38,45 +32,22 @@ const ChannelHeaderTitle = ({
|
||||
dmUser,
|
||||
gmMembers,
|
||||
}: Props) => {
|
||||
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
const intl = useIntl();
|
||||
const channel = useSelector(getCurrentChannel);
|
||||
const currentUser = useSelector(getCurrentUser);
|
||||
|
||||
const headerItemRef = useRef<HTMLElement | null>(null);
|
||||
const isRHSOpen = useSelector(getIsRhsOpen);
|
||||
|
||||
useEffect(() => {
|
||||
enableToolTipIfNeeded();
|
||||
|
||||
// Re-check on window resize
|
||||
const handleResize = () => enableToolTipIfNeeded();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [channel, gmMembers, dmUser, isRHSOpen]);
|
||||
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enableToolTipIfNeeded = () => {
|
||||
const element = headerItemRef.current;
|
||||
const isTooltip = element && element.offsetWidth < element.scrollWidth;
|
||||
setShowTooltip(isTooltip as boolean);
|
||||
};
|
||||
|
||||
const isDirect = (channel.type === Constants.DM_CHANNEL);
|
||||
const isGroup = (channel.type === Constants.GM_CHANNEL);
|
||||
const channelIsArchived = channel.delete_at !== 0;
|
||||
|
||||
let archivedIcon: React.ReactNode = null;
|
||||
let archivedIcon;
|
||||
if (channelIsArchived) {
|
||||
archivedIcon = <ArchiveIcon className='icon icon__archive icon channel-header-archived-icon svg-text-color'/>;
|
||||
}
|
||||
|
||||
let sharedIcon = null;
|
||||
let sharedIcon;
|
||||
if (channel.shared) {
|
||||
sharedIcon = (
|
||||
<SharedChannelIndicator
|
||||
@@ -118,20 +89,6 @@ const ChannelHeaderTitle = ({
|
||||
);
|
||||
}
|
||||
|
||||
const personalChannelHeaderAriaLabel = intl.formatMessage({
|
||||
id: 'channel_header.directchannel',
|
||||
defaultMessage: '{displayName} (you) Channel Menu',
|
||||
}, {
|
||||
displayName: channel.display_name,
|
||||
});
|
||||
|
||||
const othersChannelHeaderAriaLabel = intl.formatMessage({
|
||||
id: 'channel_header.otherchannel',
|
||||
defaultMessage: '{displayName} Channel Menu',
|
||||
}, {
|
||||
displayName: channel.display_name,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='channel-header__top'>
|
||||
<ChannelHeaderTitleFavorite/>
|
||||
@@ -142,60 +99,12 @@ const ChannelHeaderTitle = ({
|
||||
status={channel.status}
|
||||
/>
|
||||
)}
|
||||
<MenuWrapper onToggle={setTitleMenuOpen}>
|
||||
<div
|
||||
id='channelHeaderDropdownButton'
|
||||
>
|
||||
<button
|
||||
id='channelHeaderMenuButton'
|
||||
className={classNames('channel-header__trigger style--none', {active: titleMenuOpen})}
|
||||
aria-label={
|
||||
(isDirect && currentUser.id === dmUser?.id) ? personalChannelHeaderAriaLabel.toLowerCase() : othersChannelHeaderAriaLabel.toLowerCase()
|
||||
}
|
||||
aria-expanded={titleMenuOpen}
|
||||
aria-controls='channelHeaderDropdownMenu'
|
||||
>
|
||||
{showTooltip ? (
|
||||
<WithTooltip
|
||||
title={channelTitle as string}
|
||||
>
|
||||
<strong
|
||||
id='channelHeaderTitle'
|
||||
className='heading'
|
||||
ref={headerItemRef}
|
||||
>
|
||||
<span>
|
||||
{archivedIcon}
|
||||
{channelTitle}
|
||||
{sharedIcon}
|
||||
</span>
|
||||
</strong>
|
||||
</WithTooltip>
|
||||
) : (
|
||||
<strong
|
||||
id='channelHeaderTitle'
|
||||
className='heading'
|
||||
ref={headerItemRef}
|
||||
>
|
||||
<span>
|
||||
{archivedIcon}
|
||||
{channelTitle}
|
||||
{sharedIcon}
|
||||
</span>
|
||||
</strong>
|
||||
)}
|
||||
<span
|
||||
id='channelHeaderDropdownIcon'
|
||||
className='icon icon-chevron-down header-dropdown-chevron-icon'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<ChannelHeaderDropdown
|
||||
ariaLabel={
|
||||
(isDirect && currentUser.id === dmUser?.id) ? personalChannelHeaderAriaLabel.toLowerCase() : othersChannelHeaderAriaLabel.toLowerCase()}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
<ChannelHeaderMenu
|
||||
dmUser={dmUser}
|
||||
gmMembers={gmMembers}
|
||||
sharedIcon={sharedIcon}
|
||||
archivedIcon={archivedIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ChannelHeaderDropdown from './channel_header_dropdown_items';
|
||||
import type {Props} from './channel_header_dropdown_items';
|
||||
|
||||
describe('components/ChannelHeaderDropdown', () => {
|
||||
const defaultProps = {
|
||||
user: TestHelper.getUserMock({id: 'test-user-id'}),
|
||||
channel: TestHelper.getChannelMock({id: 'test-channel-id'}),
|
||||
isDefault: false,
|
||||
isFavorite: false,
|
||||
isReadonly: false,
|
||||
isMuted: false,
|
||||
isArchived: false,
|
||||
isMobile: false,
|
||||
penultimateViewedChannelName: 'test-channel',
|
||||
pluginMenuItems: [],
|
||||
isLicensedForLDAPGroups: false,
|
||||
isChannelBookmarksEnabled: false,
|
||||
};
|
||||
test('should match snapshot with no plugin items', () => {
|
||||
const wrapper = shallow(<ChannelHeaderDropdown {...defaultProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with plugins', () => {
|
||||
const props: Props = {
|
||||
...defaultProps,
|
||||
pluginMenuItems: [
|
||||
{id: 'plugin-1', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-1-text', shouldRender: () => true},
|
||||
{id: 'plugin-2', pluginId: 'playbooks', action: jest.fn(), text: 'plugin-2-text', shouldRender: () => true},
|
||||
],
|
||||
};
|
||||
const wrapper = shallow(<ChannelHeaderDropdown {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
|
||||
import {ChannelHeaderDropdownItems} from 'components/channel_header_dropdown';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
const ChannelHeaderDropdown = ({ariaLabel}: {
|
||||
ariaLabel: string;
|
||||
}) =>
|
||||
(
|
||||
<Menu
|
||||
id='channelHeaderDropdownMenu'
|
||||
ariaLabel={ariaLabel}
|
||||
>
|
||||
<ChannelHeaderDropdownItems isMobile={false}/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
export default memo(ChannelHeaderDropdown);
|
||||
@@ -1,355 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
|
||||
import ChannelBookmarksSubmenu from 'components/channel_bookmarks_sub_menu';
|
||||
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import ChannelMoveToSubMenuOld from 'components/channel_move_to_sub_menu_old';
|
||||
import ChannelNotificationsModal from 'components/channel_notifications_modal';
|
||||
import ConvertChannelModal from 'components/convert_channel_modal';
|
||||
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
|
||||
import DeleteChannelModal from 'components/delete_channel_modal';
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
|
||||
import MoreDirectChannels from 'components/more_direct_channels';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import RenameChannelModal from 'components/rename_channel_modal';
|
||||
import UnarchiveChannelModal from 'components/unarchive_channel_modal';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug';
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
import type {ChannelHeaderAction} from 'types/store/plugins';
|
||||
|
||||
import MenuItemCloseChannel from './menu_items/close_channel';
|
||||
import MenuItemCloseMessage from './menu_items/close_message';
|
||||
import MenuItemLeaveChannel from './menu_items/leave_channel';
|
||||
import MenuItemOpenMembersRHS from './menu_items/open_members_rhs';
|
||||
import MenuItemToggleFavoriteChannel from './menu_items/toggle_favorite_channel';
|
||||
import MenuItemToggleInfo from './menu_items/toggle_info';
|
||||
import MenuItemToggleMuteChannel from './menu_items/toggle_mute_channel';
|
||||
import MenuItemViewPinnedPosts from './menu_items/view_pinned_posts';
|
||||
|
||||
export type Props = {
|
||||
user: UserProfile;
|
||||
channel?: Channel;
|
||||
isDefault: boolean;
|
||||
isFavorite: boolean;
|
||||
isReadonly: boolean;
|
||||
isMuted: boolean;
|
||||
isArchived: boolean;
|
||||
isMobile: boolean;
|
||||
penultimateViewedChannelName: string;
|
||||
pluginMenuItems: ChannelHeaderAction[];
|
||||
isLicensedForLDAPGroups: boolean;
|
||||
isChannelBookmarksEnabled: boolean;
|
||||
}
|
||||
|
||||
export default class ChannelHeaderDropdown extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
channel,
|
||||
isDefault,
|
||||
isFavorite,
|
||||
isMuted,
|
||||
isReadonly,
|
||||
isArchived,
|
||||
isMobile,
|
||||
penultimateViewedChannelName,
|
||||
isLicensedForLDAPGroups,
|
||||
isChannelBookmarksEnabled,
|
||||
} = this.props;
|
||||
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPrivate = channel.type === Constants.PRIVATE_CHANNEL;
|
||||
const isGroupConstrained = channel.group_constrained === true;
|
||||
const channelMembersPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS;
|
||||
const channelPropertiesPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
|
||||
const channelDeletePermission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL;
|
||||
const channelUnarchivePermission = Permissions.MANAGE_TEAM;
|
||||
|
||||
let divider;
|
||||
if (isMobile) {
|
||||
divider = (
|
||||
<li className='MenuGroup mobile-menu-divider'>
|
||||
<hr/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const pluginItems = this.props.pluginMenuItems.map((item) => {
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={() => {
|
||||
if (item.action) {
|
||||
item.action(channel.id);
|
||||
}
|
||||
}}
|
||||
text={item.text}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItemToggleInfo
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
channel={channel}
|
||||
/>
|
||||
{/* Remove when this components is migrated to new menus */}
|
||||
<ChannelMoveToSubMenuOld
|
||||
channel={channel}
|
||||
openUp={false}
|
||||
inHeaderDropdown={true}
|
||||
/>
|
||||
<Menu.Group divider={divider}>
|
||||
<MenuItemToggleFavoriteChannel
|
||||
show={isMobile}
|
||||
channel={channel}
|
||||
isFavorite={isFavorite}
|
||||
/>
|
||||
<MenuItemViewPinnedPosts
|
||||
show={isMobile}
|
||||
channel={channel}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelNotificationPreferences'
|
||||
show={channel.type !== Constants.DM_CHANNEL && !isArchived}
|
||||
modalId={ModalIdentifiers.CHANNEL_NOTIFICATIONS}
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{
|
||||
channel,
|
||||
currentUser: user,
|
||||
focusOriginElement: 'channelHeaderMenuButton',
|
||||
}}
|
||||
text={localizeMessage({id: 'navbar.preferences', defaultMessage: 'Notification Preferences'})}
|
||||
/>
|
||||
<MenuItemToggleMuteChannel
|
||||
id='channelToggleMuteChannel'
|
||||
user={user}
|
||||
channel={channel}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
</Menu.Group>
|
||||
|
||||
<Menu.Group divider={divider}>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelMembersPermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelInviteMembers'
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && !isGroupConstrained}
|
||||
modalId={ModalIdentifiers.CHANNEL_INVITE}
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelAddMembers'
|
||||
show={channel.type === Constants.GM_CHANNEL && !isArchived && !isGroupConstrained}
|
||||
modalId={ModalIdentifiers.CREATE_DM_CHANNEL}
|
||||
dialogType={MoreDirectChannels}
|
||||
dialogProps={{isExistingChannel: true, focusOriginElement: 'channelHeaderMenuButton'}}
|
||||
text={localizeMessage({id: 'navbar.addMembers', defaultMessage: 'Add Members'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelViewMembers'
|
||||
channel={channel}
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && (isArchived || isDefault)}
|
||||
text={localizeMessage({id: 'channel_header.viewMembers', defaultMessage: 'View Members'})}
|
||||
/>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelMembersPermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelAddGroups'
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && isGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.ADD_GROUPS_TO_CHANNEL}
|
||||
dialogType={AddGroupsToChannelModal}
|
||||
text={localizeMessage({id: 'navbar.addGroups', defaultMessage: 'Add Groups'})}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelManageGroups'
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault && isGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.MANAGE_CHANNEL_GROUPS}
|
||||
dialogType={ChannelGroupsManageModal}
|
||||
dialogProps={{channelID: channel.id}}
|
||||
text={localizeMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
|
||||
/>
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelManageMembers'
|
||||
channel={channel}
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault}
|
||||
text={localizeMessage({id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'})}
|
||||
editMembers={!isArchived}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelMembersPermission]}
|
||||
invert={true}
|
||||
>
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelViewMembers'
|
||||
channel={channel}
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isDefault}
|
||||
text={localizeMessage({id: 'channel_header.viewMembers', defaultMessage: 'View Members'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
</Menu.Group>
|
||||
|
||||
<Menu.Group divider={divider}>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelEditHeader'
|
||||
show={(channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) && !isArchived && !isReadonly}
|
||||
modalId={ModalIdentifiers.EDIT_CHANNEL_HEADER}
|
||||
dialogType={EditChannelHeaderModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'channel_header.setConversationHeader', defaultMessage: 'Edit Conversation Header'})}
|
||||
/>
|
||||
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='convertGMPrivateChannel'
|
||||
show={channel.type === Constants.GM_CHANNEL && !isArchived && !isReadonly && !isGuest(user.roles)}
|
||||
modalId={ModalIdentifiers.CONVERT_GM_TO_CHANNEL}
|
||||
dialogType={ConvertGmToChannelModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'sidebar_left.sidebar_channel_menu_convert_to_channel', defaultMessage: 'Convert to Private Channel'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
|
||||
<Menu.Group divider={divider}>
|
||||
{isChannelBookmarksEnabled && <ChannelBookmarksSubmenu channel={channel}/>}
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelPropertiesPermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelEditHeader'
|
||||
show={channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL && !isArchived && !isReadonly}
|
||||
modalId={ModalIdentifiers.EDIT_CHANNEL_HEADER}
|
||||
dialogType={EditChannelHeaderModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'channel_header.setHeader', defaultMessage: 'Edit Channel Header'})}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelEditPurpose'
|
||||
show={!isArchived && !isReadonly && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
modalId={ModalIdentifiers.EDIT_CHANNEL_PURPOSE}
|
||||
dialogType={EditChannelPurposeModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'channel_header.setPurpose', defaultMessage: 'Edit Channel Purpose'})}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelRename'
|
||||
show={!isArchived && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
modalId={ModalIdentifiers.RENAME_CHANNEL}
|
||||
dialogType={RenameChannelModal}
|
||||
dialogProps={{channel}}
|
||||
text={localizeMessage({id: 'channel_header.rename', defaultMessage: 'Rename Channel'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelConvertToPrivate'
|
||||
show={!isArchived && !isDefault && channel.type === Constants.OPEN_CHANNEL}
|
||||
modalId={ModalIdentifiers.CONVERT_CHANNEL}
|
||||
dialogType={ConvertChannelModal}
|
||||
dialogProps={{
|
||||
channelId: channel.id,
|
||||
channelDisplayName: channel.display_name,
|
||||
}}
|
||||
text={localizeMessage({id: 'channel_header.convert', defaultMessage: 'Convert to Private Channel'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
<MenuItemLeaveChannel
|
||||
id='channelLeaveChannel'
|
||||
channel={channel}
|
||||
isDefault={isDefault}
|
||||
isGuestUser={isGuest(user.roles)}
|
||||
/>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelDeletePermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelArchiveChannel'
|
||||
show={!isArchived && !isDefault && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
modalId={ModalIdentifiers.DELETE_CHANNEL}
|
||||
className='MenuItem__dangerous'
|
||||
dialogType={DeleteChannelModal}
|
||||
dialogProps={{
|
||||
channel,
|
||||
penultimateViewedChannelName,
|
||||
}}
|
||||
text={localizeMessage({id: 'channel_header.delete', defaultMessage: 'Archive Channel'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
{isMobile &&
|
||||
<MobileChannelHeaderPlug
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>}
|
||||
<MenuItemCloseMessage
|
||||
id='channelCloseMessage'
|
||||
channel={channel}
|
||||
currentUser={user}
|
||||
/>
|
||||
<MenuItemCloseChannel
|
||||
isArchived={isArchived}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
{pluginItems}
|
||||
</Menu.Group>
|
||||
<Menu.Group divider={divider}>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelUnarchivePermission]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='channelUnarchiveChannel'
|
||||
show={isArchived && !isDefault && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
modalId={ModalIdentifiers.UNARCHIVE_CHANNEL}
|
||||
dialogType={UnarchiveChannelModal}
|
||||
dialogProps={{
|
||||
channel,
|
||||
}}
|
||||
text={localizeMessage({id: 'channel_header.unarchive', defaultMessage: 'Unarchive Channel'})}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
</Menu.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
||||
import {
|
||||
getCurrentChannel,
|
||||
isCurrentChannelDefault,
|
||||
isCurrentChannelFavorite,
|
||||
isCurrentChannelMuted,
|
||||
isCurrentChannelArchived,
|
||||
getRedirectChannelNameForTeam,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {
|
||||
getUser,
|
||||
getCurrentUser,
|
||||
getUserStatuses,
|
||||
getCurrentUserId,
|
||||
} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getPenultimateViewedChannelName} from 'selectors/local_storage';
|
||||
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
|
||||
|
||||
import {getIsChannelBookmarksEnabled} from 'components/channel_bookmarks/utils';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import Desktop from './channel_header_dropdown';
|
||||
import Items from './channel_header_dropdown_items';
|
||||
import Mobile from './mobile_channel_header_dropdown';
|
||||
|
||||
const getTeammateId = createSelector(
|
||||
'getTeammateId',
|
||||
getCurrentChannel,
|
||||
getCurrentUserId,
|
||||
(channel, currentUserId) => {
|
||||
if (channel?.type !== Constants.DM_CHANNEL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Utils.getUserIdFromChannelId(channel.name, currentUserId);
|
||||
},
|
||||
);
|
||||
|
||||
const getTeammateStatus = createSelector(
|
||||
'getTeammateStatus',
|
||||
getUserStatuses,
|
||||
getTeammateId,
|
||||
(userStatuses, teammateId) => {
|
||||
if (!teammateId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return userStatuses[teammateId];
|
||||
},
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => ({
|
||||
user: getCurrentUser(state),
|
||||
channel: getCurrentChannel(state),
|
||||
isDefault: isCurrentChannelDefault(state),
|
||||
isFavorite: isCurrentChannelFavorite(state),
|
||||
isMuted: isCurrentChannelMuted(state),
|
||||
isReadonly: false,
|
||||
isArchived: isCurrentChannelArchived(state),
|
||||
penultimateViewedChannelName: getPenultimateViewedChannelName(state) || getRedirectChannelNameForTeam(state, getCurrentTeamId(state)),
|
||||
pluginMenuItems: getChannelHeaderMenuPluginComponents(state),
|
||||
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
|
||||
isChannelBookmarksEnabled: getIsChannelBookmarksEnabled(state),
|
||||
});
|
||||
|
||||
const mobileMapStateToProps = (state: GlobalState) => {
|
||||
const user = getCurrentUser(state);
|
||||
const channel = getCurrentChannel(state);
|
||||
const teammateId = getTeammateId(state);
|
||||
|
||||
let teammateIsBot = false;
|
||||
let displayName = '';
|
||||
if (teammateId) {
|
||||
const teammate = getUser(state, teammateId);
|
||||
teammateIsBot = teammate && teammate.is_bot;
|
||||
displayName = Utils.getDisplayNameByUser(state, teammate);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
channel,
|
||||
teammateId,
|
||||
teammateIsBot,
|
||||
teammateStatus: getTeammateStatus(state),
|
||||
displayName,
|
||||
};
|
||||
};
|
||||
|
||||
export const ChannelHeaderDropdown = Desktop;
|
||||
export const ChannelHeaderDropdownItems = connect(mapStateToProps)(Items);
|
||||
export const MobileChannelHeaderDropdown = connect(mobileMapStateToProps)(Mobile);
|
||||
@@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel shoud be hidden if the channel is not archived 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[MockFunction]}
|
||||
show={false}
|
||||
text="Close Channel"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.CloseChannel should match snapshot 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[MockFunction]}
|
||||
show={true}
|
||||
text="Close Channel"
|
||||
/>
|
||||
`;
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import CloseChannel from './close_channel';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItem.CloseChannel', () => {
|
||||
const baseProps = {
|
||||
isArchived: true,
|
||||
actions: {
|
||||
goToLastViewedChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(<CloseChannel {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('shoud be hidden if the channel is not archived', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isArchived: false,
|
||||
};
|
||||
const wrapper = shallow(<CloseChannel {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should runs goToLastViewedChannel function on click', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
goToLastViewedChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<CloseChannel {...props}/>);
|
||||
wrapper.find(Menu.ItemAction).simulate('click');
|
||||
expect(props.actions.goToLastViewedChannel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
type Props = {
|
||||
isArchived: boolean;
|
||||
actions: {
|
||||
goToLastViewedChannel: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
const CloseChannel = ({
|
||||
isArchived,
|
||||
actions,
|
||||
}: Props): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
show={isArchived}
|
||||
onClick={actions.goToLastViewedChannel}
|
||||
text={intl.formatMessage({
|
||||
id: 'center_panel.archived.closeChannel',
|
||||
defaultMessage: 'Close Channel',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CloseChannel);
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {goToLastViewedChannel} from 'actions/views/channel';
|
||||
|
||||
import CloseChannel from './close_channel';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
goToLastViewedChannel,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CloseChannel);
|
||||
@@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for DM Channel 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Close Direct Message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.CloseMessage should match snapshot for GM Channel 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Close Group Message"
|
||||
/>
|
||||
`;
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {ChannelType} from '@mattermost/types/channels';
|
||||
import type {TeamType} from '@mattermost/types/teams';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import CloseMessage from './close_message';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItem.CloseMessage', () => {
|
||||
const baseProps = {
|
||||
currentUser: TestHelper.getUserMock(),
|
||||
redirectChannel: 'test-default-channel',
|
||||
currentTeam: TestHelper.getTeamMock({
|
||||
id: 'team_id',
|
||||
name: 'test-team',
|
||||
display_name: 'Test team display name',
|
||||
description: 'Test team description',
|
||||
type: 'team-type' as TeamType,
|
||||
}),
|
||||
actions: {
|
||||
savePreferences: jest.fn(() => Promise.resolve()),
|
||||
leaveDirectChannel: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
};
|
||||
|
||||
const groupChannel = TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
type: Constants.GM_CHANNEL as ChannelType,
|
||||
});
|
||||
|
||||
const directChannel = TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
teammate_id: 'teammate-id',
|
||||
});
|
||||
|
||||
it('should match snapshot for DM Channel', () => {
|
||||
const props = {...baseProps, channel: directChannel};
|
||||
const wrapper = shallow(<CloseMessage {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot for GM Channel', () => {
|
||||
const props = {...baseProps, channel: groupChannel};
|
||||
const wrapper = shallow(<CloseMessage {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should run savePreferences function on click for DM', () => {
|
||||
const props = {...baseProps, channel: directChannel};
|
||||
const wrapper = shallow(<CloseMessage {...props}/>);
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(props.actions.savePreferences).toBeCalledWith(props.currentUser.id, [{user_id: props.currentUser.id, category: Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: props.channel.teammate_id, value: 'false'}]);
|
||||
});
|
||||
|
||||
it('should run savePreferences function on click for GM', () => {
|
||||
const props = {...baseProps, channel: groupChannel};
|
||||
const wrapper = shallow(<CloseMessage {...props}/>);
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(props.actions.savePreferences).toBeCalledWith(props.currentUser.id, [{user_id: props.currentUser.id, category: Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: props.channel.id, value: 'false'}]);
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {PreferenceType} from '@mattermost/types/preferences';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {Constants} from 'utils/constants';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Object with info about currentUser
|
||||
*/
|
||||
currentUser: UserProfile;
|
||||
|
||||
/**
|
||||
* Object with info about currentTeam
|
||||
*/
|
||||
currentTeam?: Team;
|
||||
|
||||
/**
|
||||
* String with info about redirect
|
||||
*/
|
||||
redirectChannel: string;
|
||||
|
||||
/**
|
||||
* Object with info about channel
|
||||
*/
|
||||
channel: Channel;
|
||||
|
||||
/**
|
||||
* Use for test selector
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Object with action creators
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Action creator to update user preferences
|
||||
*/
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
|
||||
|
||||
/**
|
||||
* Action creator to leave DM/GM
|
||||
*/
|
||||
leaveDirectChannel: (channelName: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export default class CloseMessage extends React.PureComponent<Props> {
|
||||
handleClose = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
const {
|
||||
channel,
|
||||
currentUser,
|
||||
currentTeam,
|
||||
redirectChannel,
|
||||
actions: {
|
||||
savePreferences,
|
||||
leaveDirectChannel,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let name: string;
|
||||
let category;
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
|
||||
name = channel.teammate_id!;
|
||||
} else {
|
||||
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
|
||||
name = channel.id;
|
||||
}
|
||||
|
||||
leaveDirectChannel(channel.name);
|
||||
savePreferences(currentUser.id, [{user_id: currentUser.id, category, name, value: 'false'}]);
|
||||
|
||||
if (currentTeam) {
|
||||
getHistory().push(`/${currentTeam.name}/channels/${redirectChannel}`);
|
||||
}
|
||||
};
|
||||
|
||||
render(): React.ReactNode {
|
||||
const {id, channel} = this.props;
|
||||
|
||||
let text;
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
text = localizeMessage({id: 'center_panel.direct.closeDirectMessage', defaultMessage: 'Close Direct Message'});
|
||||
} else if (channel.type === Constants.GM_CHANNEL) {
|
||||
text = localizeMessage({id: 'center_panel.direct.closeGroupMessage', defaultMessage: 'Close Group Message'});
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={id}
|
||||
show={channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL}
|
||||
onClick={this.handleClose}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getRedirectChannelNameForTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {leaveDirectChannel} from 'actions/views/channel';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import CloseMessage from './close_message';
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
currentTeam: getCurrentTeam(state),
|
||||
redirectChannel: getRedirectChannelNameForTeam(state, getCurrentTeamId(state)),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({savePreferences, leaveDirectChannel}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CloseMessage);
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel is default channel 1`] = `
|
||||
<MenuItemAction
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Leave Channel"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 1`] = `
|
||||
<MenuItemAction
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Leave Channel"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should be hidden if the channel type is DM or GM 2`] = `
|
||||
<MenuItemAction
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={false}
|
||||
text="Leave Channel"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.LeaveChannel should match snapshot 1`] = `
|
||||
<MenuItemAction
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Leave Channel"
|
||||
/>
|
||||
`;
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {leaveChannel} from 'actions/views/channel';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import LeaveChannel from './leave_channel';
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
leaveChannel,
|
||||
openModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(LeaveChannel);
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import LeaveChannel from './leave_channel';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItem.LeaveChannel', () => {
|
||||
const baseProps = {
|
||||
channel: TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
type: 'O',
|
||||
}),
|
||||
isGuestUser: false,
|
||||
isDefault: false,
|
||||
actions: {
|
||||
leaveChannel: jest.fn(),
|
||||
openModal: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(<LeaveChannel {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be hidden if the channel is default channel', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isDefault: true,
|
||||
};
|
||||
const wrapper = shallow(<LeaveChannel {...props}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should be hidden if the channel type is DM or GM', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: {...baseProps.channel},
|
||||
};
|
||||
const makeWrapper = () => shallow(<LeaveChannel {...props}/>);
|
||||
|
||||
props.channel.type = 'D';
|
||||
expect(makeWrapper()).toMatchSnapshot();
|
||||
|
||||
props.channel.type = 'G';
|
||||
expect(makeWrapper()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should runs leaveChannel function on click only if the channel is not private', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: {...baseProps.channel},
|
||||
actions: {...baseProps.actions},
|
||||
};
|
||||
const wrapper = shallow(<LeaveChannel {...props}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
expect(props.actions.leaveChannel).toHaveBeenCalledWith(props.channel.id);
|
||||
expect(props.actions.openModal).not.toHaveBeenCalled();
|
||||
|
||||
props.channel.type = 'P';
|
||||
props.actions.leaveChannel = jest.fn();
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
|
||||
expect(props.actions.leaveChannel).not.toHaveBeenCalled();
|
||||
|
||||
expect(props.actions.openModal).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
|
||||
dialogProps: {
|
||||
channel: props.channel,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import LeaveChannelModal from 'components/leave_channel_modal';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
type Props = PropsFromRedux & {
|
||||
|
||||
/**
|
||||
* Object with info about user
|
||||
*/
|
||||
channel: Channel;
|
||||
|
||||
/**
|
||||
* Boolean whether the channel is default
|
||||
*/
|
||||
isDefault: boolean;
|
||||
|
||||
/**
|
||||
* Boolean whether the user is a guest or no
|
||||
*/
|
||||
isGuestUser: boolean;
|
||||
|
||||
/**
|
||||
* Use for test selector
|
||||
*/
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const LeaveChannel = ({
|
||||
isDefault = true,
|
||||
isGuestUser = false,
|
||||
channel,
|
||||
actions: {
|
||||
leaveChannel,
|
||||
openModal,
|
||||
},
|
||||
id,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleLeave = useCallback((e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
|
||||
dialogType: LeaveChannelModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
leaveChannel(channel.id);
|
||||
}
|
||||
}, [channel, leaveChannel, openModal]);
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={id}
|
||||
show={(!isDefault || isGuestUser) && channel.type !== Constants.DM_CHANNEL && channel.type !== Constants.GM_CHANNEL}
|
||||
onClick={handleLeave}
|
||||
text={intl.formatMessage({id: 'channel_header.leave', defaultMessage: 'Leave Channel'})}
|
||||
isDangerous={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LeaveChannel);
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {showChannelMembers} from 'actions/views/rhs';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import OpenChannelMembersRHS from './open_members_rhs';
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => ({
|
||||
rhsOpen: getIsRhsOpen(state) && getRhsState(state) === RHSStates.CHANNEL_MEMBERS,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
showChannelMembers,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OpenChannelMembersRHS);
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
type Action = {
|
||||
showChannelMembers: (channelId: string, editMembers: boolean) => void;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
channel: Channel;
|
||||
show: boolean;
|
||||
id: string;
|
||||
editMembers?: boolean;
|
||||
text: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
rhsOpen: boolean;
|
||||
actions: Action;
|
||||
} & OwnProps;
|
||||
|
||||
const ToggleChannelMembersRHS = ({
|
||||
show,
|
||||
id,
|
||||
channel,
|
||||
rhsOpen,
|
||||
text,
|
||||
editMembers = false,
|
||||
actions,
|
||||
}: Props) => {
|
||||
const openRHSIfNotOpen = () => {
|
||||
if (rhsOpen) {
|
||||
return;
|
||||
}
|
||||
actions.showChannelMembers(channel.id, editMembers);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
show={show}
|
||||
id={id}
|
||||
onClick={openRHSIfNotOpen}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleChannelMembersRHS;
|
||||
@@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for favorite channel 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Remove from Favorites"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel should match snapshot for not favorite channel 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Add to Favorites"
|
||||
/>
|
||||
`;
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import ToggleFavoriteChannel from './toggle_favorite_channel';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
favoriteChannel,
|
||||
unfavoriteChannel,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ToggleFavoriteChannel);
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {ChannelType} from '@mattermost/types/channels';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import ToggleFavoriteChannel from './toggle_favorite_channel';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItem.ToggleFavoriteChannel', () => {
|
||||
const baseProps = {
|
||||
channel: {
|
||||
id: 'channel_id',
|
||||
display_name: 'channel_display_name',
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
delete_at: 0,
|
||||
team_id: '',
|
||||
type: 'O' as ChannelType,
|
||||
name: '',
|
||||
header: '',
|
||||
purpose: '',
|
||||
last_post_at: 0,
|
||||
last_root_post_at: 0,
|
||||
creator_id: '',
|
||||
scheme_id: '',
|
||||
group_constrained: false,
|
||||
},
|
||||
actions: {
|
||||
favoriteChannel: jest.fn(),
|
||||
unfavoriteChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const propsForFavorite = {
|
||||
...baseProps,
|
||||
isFavorite: true,
|
||||
actions: {
|
||||
favoriteChannel: jest.fn(),
|
||||
unfavoriteChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const propsForNotFavorite = {
|
||||
...baseProps,
|
||||
isFavorite: false,
|
||||
actions: {
|
||||
favoriteChannel: jest.fn(),
|
||||
unfavoriteChannel: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
it('should match snapshot for favorite channel', () => {
|
||||
const wrapper = shallow(<ToggleFavoriteChannel {...propsForFavorite}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should runs unfavoriteChannel function for favorite channel', () => {
|
||||
const wrapper = shallow(<ToggleFavoriteChannel {...propsForFavorite}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
|
||||
expect(propsForFavorite.actions.unfavoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id);
|
||||
expect(propsForFavorite.actions.favoriteChannel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should match snapshot for not favorite channel', () => {
|
||||
const wrapper = shallow(<ToggleFavoriteChannel {...propsForNotFavorite}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should runs favoriteChannel function for not favorite channel', () => {
|
||||
const wrapper = shallow(<ToggleFavoriteChannel {...propsForNotFavorite}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
|
||||
expect(propsForNotFavorite.actions.favoriteChannel).toHaveBeenCalledWith(propsForFavorite.channel.id);
|
||||
expect(propsForNotFavorite.actions.unfavoriteChannel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import type {MouseEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
type Action = {
|
||||
favoriteChannel: (channelId: string) => void;
|
||||
unfavoriteChannel: (channelId: string) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
show?: boolean;
|
||||
channel: Channel;
|
||||
isFavorite: boolean;
|
||||
actions: Action;
|
||||
};
|
||||
|
||||
const ToggleFavoriteChannel = ({
|
||||
show = true,
|
||||
isFavorite,
|
||||
actions: {
|
||||
favoriteChannel,
|
||||
unfavoriteChannel,
|
||||
},
|
||||
channel,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const toggleFavoriteChannel = useCallback((channelId: string) => {
|
||||
return isFavorite ? unfavoriteChannel(channelId) : favoriteChannel(channelId);
|
||||
}, [isFavorite, favoriteChannel, unfavoriteChannel]);
|
||||
|
||||
const handleClick = useCallback((e: MouseEvent<HTMLButtonElement>): void => {
|
||||
e.preventDefault();
|
||||
toggleFavoriteChannel(channel.id);
|
||||
}, [channel.id, toggleFavoriteChannel]);
|
||||
|
||||
let text;
|
||||
if (isFavorite) {
|
||||
text = intl.formatMessage({id: 'channelHeader.removeFromFavorites', defaultMessage: 'Remove from Favorites'});
|
||||
} else {
|
||||
text = intl.formatMessage({id: 'channelHeader.addToFavorites', defaultMessage: 'Add to Favorites'});
|
||||
}
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
show={show}
|
||||
onClick={handleClick}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ToggleFavoriteChannel);
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {closeRightHandSide, showChannelInfo} from 'actions/views/rhs';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ToggleInfo from './toggle_info';
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => ({
|
||||
rhsOpen: getIsRhsOpen(state) && getRhsState(state) === RHSStates.CHANNEL_INFO,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
closeRightHandSide,
|
||||
showChannelInfo,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ToggleInfo);
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
type Action = {
|
||||
closeRightHandSide: () => void;
|
||||
showChannelInfo: (channelId: string) => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
show: boolean;
|
||||
channel: Channel;
|
||||
rhsOpen: boolean;
|
||||
actions: Action;
|
||||
};
|
||||
|
||||
const ToggleInfo = ({show, channel, rhsOpen, actions}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const toggleRHS = () => {
|
||||
if (rhsOpen) {
|
||||
actions.closeRightHandSide();
|
||||
return;
|
||||
}
|
||||
actions.showChannelInfo(channel.id);
|
||||
};
|
||||
|
||||
let text;
|
||||
if (rhsOpen) {
|
||||
text = intl.formatMessage({id: 'channelHeader.hideInfo', defaultMessage: 'Close Info'});
|
||||
} else {
|
||||
text = intl.formatMessage({id: 'channelHeader.viewInfo', defaultMessage: 'View Info'});
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
show={show}
|
||||
onClick={toggleRHS}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleInfo;
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItemToggleMuteChannel should match snapshot 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Mute Channel"
|
||||
/>
|
||||
`;
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import MenuItemToggleMuteChannel from './toggle_mute_channel';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
updateChannelNotifyProps,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(MenuItemToggleMuteChannel);
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuItemAction from 'components/widgets/menu/menu_items/menu_item_action';
|
||||
|
||||
import {Constants, NotificationLevels} from 'utils/constants';
|
||||
|
||||
import MenuItemToggleMuteChannel from './toggle_mute_channel';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItemToggleMuteChannel', () => {
|
||||
const baseProps = {
|
||||
user: {
|
||||
id: 'user_id',
|
||||
} as UserProfile,
|
||||
channel: {
|
||||
id: 'channel_id',
|
||||
type: 'O',
|
||||
} as Channel,
|
||||
isMuted: false,
|
||||
actions: {
|
||||
updateChannelNotifyProps: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(<MenuItemToggleMuteChannel {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should unmute channel on click the channel was muted', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isMuted: true,
|
||||
actions: {
|
||||
updateChannelNotifyProps: jest.fn(),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<MenuItemToggleMuteChannel {...props}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click');
|
||||
|
||||
expect(props.actions.updateChannelNotifyProps).toBeCalledWith(
|
||||
props.user.id,
|
||||
props.channel.id,
|
||||
{mark_unread: NotificationLevels.ALL},
|
||||
);
|
||||
});
|
||||
|
||||
it('should mute channel on click the channel was unmuted', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isMuted: false,
|
||||
actions: {
|
||||
updateChannelNotifyProps: jest.fn(),
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(<MenuItemToggleMuteChannel {...props}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click');
|
||||
|
||||
expect(props.actions.updateChannelNotifyProps).toBeCalledWith(
|
||||
props.user.id,
|
||||
props.channel.id,
|
||||
{mark_unread: NotificationLevels.MENTION},
|
||||
);
|
||||
});
|
||||
|
||||
it('should show Mute Channel to all channel types except DM_CHANNEL and GM_CHANNEL', () => {
|
||||
[
|
||||
Constants.OPEN_CHANNEL,
|
||||
Constants.PRIVATE_CHANNEL,
|
||||
Constants.ARCHIVED_CHANNEL,
|
||||
].forEach((channelType) => {
|
||||
const channel = {
|
||||
id: 'channel_id',
|
||||
type: channelType,
|
||||
} as Channel;
|
||||
|
||||
const wrapper = shallow(
|
||||
<MenuItemToggleMuteChannel
|
||||
{...baseProps}
|
||||
channel={channel}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.find(MenuItemAction).props().show).toEqual(true);
|
||||
expect(wrapper.find(MenuItemAction).props().text).toEqual('Mute Channel');
|
||||
});
|
||||
});
|
||||
it('should show Mute Conversation to channel types DM_CHANNEL and GM_CHANNEL', () => {
|
||||
[
|
||||
Constants.DM_CHANNEL,
|
||||
Constants.GM_CHANNEL,
|
||||
].forEach((channelType) => {
|
||||
const channel = {
|
||||
id: 'channel_id',
|
||||
type: channelType,
|
||||
} as Channel;
|
||||
|
||||
const wrapper = shallow(
|
||||
<MenuItemToggleMuteChannel
|
||||
{...baseProps}
|
||||
channel={channel}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.find(MenuItemAction).props().show).toEqual(true);
|
||||
expect(wrapper.find(MenuItemAction).props().text).toEqual('Mute Conversation');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {Channel, ChannelNotifyProps} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {Constants, NotificationLevels} from 'utils/constants';
|
||||
|
||||
export type Actions = {
|
||||
updateChannelNotifyProps(userId: string, channelId: string, props: Partial<ChannelNotifyProps>): void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Object with info about the current user
|
||||
*/
|
||||
user: UserProfile;
|
||||
|
||||
/**
|
||||
* Object with info about the current channel
|
||||
*/
|
||||
channel: Channel;
|
||||
|
||||
/**
|
||||
* Boolean whether the current channel is muted
|
||||
*/
|
||||
isMuted: boolean;
|
||||
|
||||
/**
|
||||
* Use for test selector
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Object with action creators
|
||||
*/
|
||||
actions: Actions;
|
||||
};
|
||||
|
||||
export default function MenuItemToggleMuteChannel({
|
||||
id,
|
||||
isMuted,
|
||||
channel,
|
||||
user,
|
||||
actions,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
actions.updateChannelNotifyProps(user.id, channel.id, {
|
||||
mark_unread: (isMuted ? NotificationLevels.ALL : NotificationLevels.MENTION) as 'all' | 'mention',
|
||||
});
|
||||
}, [actions, isMuted, user.id, channel.id]);
|
||||
|
||||
let text;
|
||||
if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
|
||||
text = isMuted ?
|
||||
intl.formatMessage({id: 'channel_header.unmuteConversation', defaultMessage: 'Unmute Conversation'}) :
|
||||
intl.formatMessage({id: 'channel_header.muteConversation', defaultMessage: 'Mute Conversation'});
|
||||
} else {
|
||||
text = isMuted ?
|
||||
intl.formatMessage({id: 'channel_header.unmute', defaultMessage: 'Unmute Channel'}) :
|
||||
intl.formatMessage({id: 'channel_header.mute', defaultMessage: 'Mute Channel'});
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
id={id}
|
||||
onClick={handleClick}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts should match snapshot 1`] = `
|
||||
<MenuItemAction
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="View Pinned Posts"
|
||||
/>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {closeRightHandSide, showPinnedPosts} from 'actions/views/rhs';
|
||||
import {getRhsState} from 'selectors/rhs';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ViewPinnedPosts from './view_pinned_posts';
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => ({
|
||||
hasPinnedPosts: getRhsState(state) === RHSStates.PIN,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
actions: bindActionCreators({
|
||||
closeRightHandSide,
|
||||
showPinnedPosts,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ViewPinnedPosts);
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import ViewPinnedPosts from './view_pinned_posts';
|
||||
|
||||
describe('components/ChannelHeaderDropdown/MenuItem.ViewPinnedPosts', () => {
|
||||
const baseProps = {
|
||||
channel: {
|
||||
id: 'channel_id',
|
||||
},
|
||||
hasPinnedPosts: true,
|
||||
actions: {
|
||||
closeRightHandSide: jest.fn(),
|
||||
showPinnedPosts: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(<ViewPinnedPosts {...baseProps}/>);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should runs closeRightHandSide function if has any pinned posts', () => {
|
||||
const wrapper = shallow(<ViewPinnedPosts {...baseProps}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
|
||||
expect(baseProps.actions.closeRightHandSide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should runs showPinnedPosts function if has not pinned posts', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
hasPinnedPosts: false,
|
||||
};
|
||||
const wrapper = shallow(<ViewPinnedPosts {...props}/>);
|
||||
|
||||
wrapper.find(Menu.ItemAction).simulate('click', {
|
||||
preventDefault: jest.fn(),
|
||||
});
|
||||
|
||||
expect(baseProps.actions.showPinnedPosts).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, memo} from 'react';
|
||||
import type {MouseEvent} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
type Props = {
|
||||
show?: boolean;
|
||||
channel: any;
|
||||
hasPinnedPosts: boolean;
|
||||
actions: {
|
||||
closeRightHandSide: () => void;
|
||||
showPinnedPosts: (id: any) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const ViewPinnedPosts = ({
|
||||
channel,
|
||||
hasPinnedPosts,
|
||||
actions: {
|
||||
closeRightHandSide,
|
||||
showPinnedPosts,
|
||||
},
|
||||
show,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const handleClick = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (hasPinnedPosts) {
|
||||
closeRightHandSide();
|
||||
} else {
|
||||
showPinnedPosts(channel.id);
|
||||
}
|
||||
}, [channel.id, closeRightHandSide, showPinnedPosts, hasPinnedPosts]);
|
||||
|
||||
return (
|
||||
<Menu.ItemAction
|
||||
show={show}
|
||||
onClick={handleClick}
|
||||
text={intl.formatMessage({id: 'navbar.viewPinnedPosts', defaultMessage: 'View Pinned Posts'})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ViewPinnedPosts);
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {ChannelHeaderDropdownItems} from 'components/channel_header_dropdown';
|
||||
import StatusIcon from 'components/status_icon';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import MobileChannelHeaderDropdownAnimation from './mobile_channel_header_dropdown_animation';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
channel?: Channel;
|
||||
teammateId: string | null;
|
||||
teammateIsBot?: boolean;
|
||||
teammateStatus?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const MobileChannelHeaderDropdown = ({
|
||||
user,
|
||||
channel,
|
||||
teammateId,
|
||||
displayName,
|
||||
teammateIsBot,
|
||||
teammateStatus,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const getChannelTitle = () => {
|
||||
if (!channel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
if (user.id === teammateId) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='channel_header.directchannel.you'
|
||||
defaultMessage='{displayname} (you)'
|
||||
values={{displayname: displayName}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return displayName;
|
||||
}
|
||||
return channel.display_name;
|
||||
};
|
||||
|
||||
let dmHeaderIconStatus;
|
||||
|
||||
if (!teammateIsBot) {
|
||||
dmHeaderIconStatus = (
|
||||
<StatusIcon status={teammateStatus}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuWrapper animationComponent={MobileChannelHeaderDropdownAnimation}>
|
||||
<a>
|
||||
<span className='heading'>
|
||||
{dmHeaderIconStatus}
|
||||
{getChannelTitle()}
|
||||
</span>
|
||||
<span
|
||||
className='fa fa-angle-down header-dropdown__icon'
|
||||
title={intl.formatMessage({id: 'generic_icons.dropdown', defaultMessage: 'Dropdown Icon'})}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<Menu ariaLabel={intl.formatMessage({id: 'channel_header.menuAriaLabel', defaultMessage: 'Channel Menu'})}>
|
||||
<ChannelHeaderDropdownItems isMobile={true}/>
|
||||
<div className='Menu__close visible-xs-block'>
|
||||
{'×'}
|
||||
</div>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MobileChannelHeaderDropdown);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
|
||||
type Props = {
|
||||
children?: ReactNode;
|
||||
show: boolean;
|
||||
};
|
||||
|
||||
const timeout = {
|
||||
enter: ANIMATION_DURATION,
|
||||
exit: ANIMATION_DURATION,
|
||||
};
|
||||
|
||||
const MobileChannelHeaderDropdownAnimation = ({show, children}: Props) => {
|
||||
return (
|
||||
<CSSTransition
|
||||
in={show}
|
||||
classNames='mobile-channel-header-dropdown'
|
||||
enter={true}
|
||||
exit={true}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
timeout={timeout}
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(MobileChannelHeaderDropdownAnimation);
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import ChevronDownIcon from '@mattermost/compass-icons/components/chevron-down';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {
|
||||
getCurrentChannel,
|
||||
isCurrentChannelDefault,
|
||||
isCurrentChannelFavorite,
|
||||
isCurrentChannelMuted,
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {
|
||||
getCurrentUser,
|
||||
} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getChannelHeaderMenuPluginComponents} from 'selectors/plugins';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import ChannelDirectMenu from './channel_header_menu_items/channel_header_direct_menu';
|
||||
import ChannelGroupMenu from './channel_header_menu_items/channel_header_group_menu';
|
||||
import ChannelHeaderMobileMenu from './channel_header_menu_items/channel_header_mobile_menu';
|
||||
import ChannelPublicPrivateMenu from './channel_header_menu_items/channel_header_public_private_menu';
|
||||
|
||||
import ChannelHeaderTitleDirect from '../channel_header/channel_header_title_direct';
|
||||
import ChannelHeaderTitleGroup from '../channel_header/channel_header_title_group';
|
||||
|
||||
type Props = {
|
||||
dmUser?: UserProfile;
|
||||
gmMembers?: UserProfile[];
|
||||
archivedIcon?: JSX.Element;
|
||||
sharedIcon?: JSX.Element;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function ChannelHeaderMenu({dmUser, gmMembers, isMobile, archivedIcon, sharedIcon}: Props): JSX.Element | null {
|
||||
const intl = useIntl();
|
||||
|
||||
const user = useSelector(getCurrentUser);
|
||||
const channel = useSelector(getCurrentChannel);
|
||||
const isDefault = useSelector(isCurrentChannelDefault);
|
||||
const isFavorite = useSelector(isCurrentChannelFavorite);
|
||||
const isMuted = useSelector(isCurrentChannelMuted);
|
||||
const isLicensedForLDAPGroups = useSelector(getLicense).LDAPGroups === 'true';
|
||||
const pluginMenuItems = useSelector(getChannelHeaderMenuPluginComponents);
|
||||
|
||||
const isReadonly = false;
|
||||
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDirect = (channel.type === Constants.DM_CHANNEL);
|
||||
const isGroup = (channel.type === Constants.GM_CHANNEL);
|
||||
|
||||
let channelTitle: ReactNode = channel.display_name;
|
||||
let ariaLabel = intl.formatMessage({
|
||||
id: 'channel_header.otherchannel',
|
||||
defaultMessage: '{displayName} Channel Menu',
|
||||
}, {
|
||||
displayName: channel.display_name,
|
||||
});
|
||||
if (isDirect && dmUser) {
|
||||
channelTitle = <ChannelHeaderTitleDirect dmUser={dmUser}/>;
|
||||
if (user.id === dmUser.id) {
|
||||
ariaLabel = intl.formatMessage({
|
||||
id: 'channel_header.directchannel',
|
||||
defaultMessage: '{displayName} (you) Channel Menu',
|
||||
}, {
|
||||
displayName: channel.display_name,
|
||||
});
|
||||
}
|
||||
} else if (isGroup) {
|
||||
channelTitle = <ChannelHeaderTitleGroup gmMembers={gmMembers}/>;
|
||||
}
|
||||
|
||||
const pluginItems = pluginMenuItems.map((item) => {
|
||||
const handlePluginItemClick = () => {
|
||||
if (item.action) {
|
||||
item.action(channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={handlePluginItemClick}
|
||||
labels={<span>{item.text}</span>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButtonTooltip={{
|
||||
text: channelTitle as string,
|
||||
}}
|
||||
menuButton={{
|
||||
id: 'channelHeaderDropdownButton',
|
||||
class: classNames('channel-header__trigger style--none'),
|
||||
children: (
|
||||
<>
|
||||
{archivedIcon}
|
||||
<strong
|
||||
id='channelHeaderTitle'
|
||||
className='heading'
|
||||
>
|
||||
{channelTitle as string}
|
||||
</strong>
|
||||
{sharedIcon}
|
||||
<ChevronDownIcon size={16}/>
|
||||
</>
|
||||
),
|
||||
'aria-label': ariaLabel.toLowerCase(),
|
||||
}}
|
||||
menu={{
|
||||
id: 'channelHeaderDropdownMenu',
|
||||
}}
|
||||
transformOrigin={{
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
{isDirect && (
|
||||
<ChannelDirectMenu
|
||||
channel={channel}
|
||||
user={user}
|
||||
isMuted={isMuted}
|
||||
pluginItems={pluginItems}
|
||||
isFavorite={isFavorite}
|
||||
isMobile={isMobile || false}
|
||||
/>
|
||||
)}
|
||||
{isGroup && (
|
||||
<ChannelGroupMenu
|
||||
channel={channel}
|
||||
user={user}
|
||||
isMuted={isMuted}
|
||||
pluginItems={pluginItems}
|
||||
isFavorite={isFavorite}
|
||||
isMobile={isMobile || false}
|
||||
/>
|
||||
)}
|
||||
{(!isDirect && !isGroup) && (
|
||||
<ChannelPublicPrivateMenu
|
||||
channel={channel}
|
||||
user={user}
|
||||
isMuted={isMuted}
|
||||
pluginItems={pluginItems}
|
||||
isFavorite={isFavorite}
|
||||
isMobile={isMobile || false}
|
||||
isDefault={isDefault}
|
||||
isReadonly={isReadonly}
|
||||
isLicensedForLDAPGroups={isLicensedForLDAPGroups}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ChannelHeaderMobileMenu
|
||||
isMobile={isMobile || false}
|
||||
pluginItems={pluginItems}
|
||||
channel={channel}
|
||||
/>
|
||||
</Menu.Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {CogOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import CloseMessage from '../menu_items/close_message';
|
||||
import EditConversationHeader from '../menu_items/edit_conversation_header';
|
||||
import MenuItemPluginItems from '../menu_items/plugins_submenu';
|
||||
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
|
||||
import MenuItemToggleInfo from '../menu_items/toggle_info';
|
||||
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
|
||||
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
user: UserProfile;
|
||||
isMuted: boolean;
|
||||
isMobile: boolean;
|
||||
isFavorite: boolean;
|
||||
pluginItems: ReactNode[];
|
||||
}
|
||||
|
||||
const ChannelHeaderDirectMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, ...rest}: Props) => {
|
||||
return (
|
||||
<>
|
||||
<MenuItemToggleInfo
|
||||
channel={channel}
|
||||
{...rest}
|
||||
/>
|
||||
<MenuItemToggleMuteChannel
|
||||
userID={user.id}
|
||||
channel={channel}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
{isMobile && (
|
||||
<>
|
||||
<MenuItemToggleFavoriteChannel
|
||||
channelID={channel.id}
|
||||
isFavorite={isFavorite}
|
||||
/>
|
||||
<MenuItemViewPinnedPosts
|
||||
channelID={channel.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
<ChannelMoveToSubMenu
|
||||
channel={channel}
|
||||
/>
|
||||
{!isMobile && (
|
||||
<MenuItemPluginItems pluginItems={pluginItems}/>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
<CloseMessage
|
||||
currentUserID={user.id}
|
||||
channel={channel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelHeaderDirectMenu;
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CogOutlineIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
|
||||
import CloseMessage from '../menu_items/close_message';
|
||||
import MenuItemConvertToPrivate from '../menu_items/convert_gm_to_private';
|
||||
import EditConversationHeader from '../menu_items/edit_conversation_header';
|
||||
import MenuItemNotification from '../menu_items/notification';
|
||||
import MenuItemOpenMembersRHS from '../menu_items/open_members_rhs';
|
||||
import MenuItemPluginItems from '../menu_items/plugins_submenu';
|
||||
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
|
||||
import MenuItemToggleInfo from '../menu_items/toggle_info';
|
||||
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
|
||||
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
user: UserProfile;
|
||||
isMuted: boolean;
|
||||
isMobile: boolean;
|
||||
isFavorite: boolean;
|
||||
pluginItems: ReactNode[];
|
||||
}
|
||||
|
||||
const ChannelHeaderGroupMenu = ({channel, user, isMuted, isMobile, isFavorite, pluginItems, ...rest}: Props) => {
|
||||
const isGroupConstrained = channel?.group_constrained === true;
|
||||
const isArchived = channel.delete_at !== 0;
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItemToggleInfo
|
||||
channel={channel}
|
||||
{...rest}
|
||||
/>
|
||||
<MenuItemToggleMuteChannel
|
||||
userID={user.id}
|
||||
channel={channel}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
{isMobile && (
|
||||
<>
|
||||
<MenuItemToggleFavoriteChannel
|
||||
channelID={channel.id}
|
||||
isFavorite={isFavorite}
|
||||
/>
|
||||
<MenuItemViewPinnedPosts
|
||||
channelID={channel.id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isArchived && (
|
||||
<MenuItemNotification
|
||||
user={user}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{(isArchived && isGroupConstrained && isGuest(user.roles)) && (
|
||||
<EditConversationHeader
|
||||
leadingElement={<CogOutlineIcon size='18px'/>}
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
{(!isArchived && !isGroupConstrained && !isGuest(user.roles)) && (
|
||||
<Menu.SubMenu
|
||||
id={'channelSettings'}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.settings'
|
||||
defaultMessage='Settings'
|
||||
/>
|
||||
}
|
||||
leadingElement={<CogOutlineIcon size={18}/>}
|
||||
trailingElements={<ChevronRightIcon size={16}/>}
|
||||
menuId={'channelSettings-menu'}
|
||||
menuAriaLabel={formatMessage({id: 'channel_header.settings', defaultMessage: 'Settings'})}
|
||||
>
|
||||
<EditConversationHeader
|
||||
channel={channel}
|
||||
/>
|
||||
<MenuItemConvertToPrivate
|
||||
channel={channel}
|
||||
/>
|
||||
</Menu.SubMenu>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
{(!isArchived && !isGroupConstrained) && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS]}
|
||||
>
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelMembers'
|
||||
channel={channel}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.members'
|
||||
defaultMessage='Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
<ChannelMoveToSubMenu channel={channel}/>
|
||||
{!isMobile && (
|
||||
<MenuItemPluginItems pluginItems={pluginItems}/>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
<CloseMessage
|
||||
currentUserID={user.id}
|
||||
channel={channel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelHeaderGroupMenu;
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React, {memo} from 'react';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import MobileChannelHeaderPlugins from '../menu_items/mobile_channel_header_plugins';
|
||||
|
||||
type Props = {
|
||||
isMobile: boolean;
|
||||
channel: Channel;
|
||||
pluginItems: ReactNode[];
|
||||
}
|
||||
|
||||
const ChannelHeaderMobileMenu = (props: Props): JSX.Element => {
|
||||
if (!props.isMobile) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={props.channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
{props.pluginItems}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChannelHeaderMobileMenu);
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {isGuest} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import ChannelMoveToSubMenu from 'components/channel_move_to_sub_menu';
|
||||
import * as Menu from 'components/menu';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import MenuItemArchiveChannel from '../menu_items/archive_channel';
|
||||
import MenuItemChannelBookmarks from '../menu_items/channel_bookmarks_submenu';
|
||||
import MenuItemChannelSettings from '../menu_items/channel_settings_submenu';
|
||||
import MenuItemCloseChannel from '../menu_items/close_channel';
|
||||
import MenuItemGroupsMenuItems from '../menu_items/groups';
|
||||
import MenuItemLeaveChannel from '../menu_items/leave_channel';
|
||||
import MenuItemNotification from '../menu_items/notification';
|
||||
import MenuItemOpenMembersRHS from '../menu_items/open_members_rhs';
|
||||
import MenuItemPluginItems from '../menu_items/plugins_submenu';
|
||||
import MenuItemToggleFavoriteChannel from '../menu_items/toggle_favorite_channel';
|
||||
import MenuItemToggleInfo from '../menu_items/toggle_info';
|
||||
import MenuItemToggleMuteChannel from '../menu_items/toggle_mute_channel';
|
||||
import MenuItemUnarchiveChannel from '../menu_items/unarchive_channel';
|
||||
import MenuItemViewPinnedPosts from '../menu_items/view_pinned_posts';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
user: UserProfile;
|
||||
isMuted: boolean;
|
||||
isReadonly: boolean;
|
||||
isDefault: boolean;
|
||||
isMobile: boolean;
|
||||
isFavorite: boolean;
|
||||
isLicensedForLDAPGroups: boolean;
|
||||
pluginItems: ReactNode[];
|
||||
}
|
||||
|
||||
const ChannelHeaderPublicMenu = ({channel, user, isMuted, isReadonly, isDefault, isMobile, isFavorite, isLicensedForLDAPGroups, pluginItems, ...rest}: Props) => {
|
||||
const isGroupConstrained = channel?.group_constrained === true;
|
||||
const isArchived = channel.delete_at !== 0;
|
||||
const isPrivate = channel?.type === Constants.PRIVATE_CHANNEL;
|
||||
|
||||
const channelMembersPermission = isPrivate ? Permissions.MANAGE_PRIVATE_CHANNEL_MEMBERS : Permissions.MANAGE_PUBLIC_CHANNEL_MEMBERS;
|
||||
const channelDeletePermission = isPrivate ? Permissions.DELETE_PRIVATE_CHANNEL : Permissions.DELETE_PUBLIC_CHANNEL;
|
||||
const channelUnarchivePermission = Permissions.MANAGE_TEAM;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItemToggleInfo
|
||||
channel={channel}
|
||||
{...rest}
|
||||
/>
|
||||
<MenuItemToggleMuteChannel
|
||||
userID={user.id}
|
||||
channel={channel}
|
||||
isMuted={isMuted}
|
||||
/>
|
||||
{!isArchived && (
|
||||
<>
|
||||
<MenuItemNotification
|
||||
user={user}
|
||||
channel={channel}
|
||||
/>
|
||||
<MenuItemChannelSettings
|
||||
isReadonly={isReadonly}
|
||||
isDefault={isDefault}
|
||||
channel={channel}
|
||||
/>
|
||||
<MenuItemChannelBookmarks
|
||||
channel={channel}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
{isMobile && (
|
||||
<>
|
||||
<MenuItemToggleFavoriteChannel
|
||||
channelID={channel.id}
|
||||
isFavorite={isFavorite}
|
||||
/>
|
||||
<MenuItemViewPinnedPosts
|
||||
channelID={channel.id}
|
||||
/>
|
||||
<Menu.Separator/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isArchived || isDefault) && (
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelMembers'
|
||||
channel={channel}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.members'
|
||||
defaultMessage='Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isArchived && !isDefault && (
|
||||
<>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelMembersPermission]}
|
||||
>
|
||||
{isGroupConstrained && isLicensedForLDAPGroups && (
|
||||
<MenuItemGroupsMenuItems
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelMembers'
|
||||
channel={channel}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.members'
|
||||
defaultMessage='Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelMembersPermission]}
|
||||
invert={true}
|
||||
>
|
||||
<MenuItemOpenMembersRHS
|
||||
id='channelMembers'
|
||||
channel={channel}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.members'
|
||||
defaultMessage='Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Menu.Separator/>
|
||||
<ChannelMoveToSubMenu channel={channel}/>
|
||||
{!isMobile && (
|
||||
<MenuItemPluginItems pluginItems={pluginItems}/>
|
||||
)}
|
||||
{!isDefault && (
|
||||
<Menu.Separator/>
|
||||
)}
|
||||
{!isDefault && !isGuest(user.roles) && (
|
||||
<MenuItemLeaveChannel
|
||||
id='channelLeaveChannel'
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isArchived && (
|
||||
<MenuItemCloseChannel/>
|
||||
)}
|
||||
|
||||
{!isArchived && !isDefault && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelDeletePermission]}
|
||||
>
|
||||
<MenuItemArchiveChannel
|
||||
channel={channel}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
|
||||
{isArchived && !isDefault && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelUnarchivePermission]}
|
||||
>
|
||||
<MenuItemUnarchiveChannel
|
||||
channel={channel}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelHeaderPublicMenu;
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import AddChannelMembers from './add_channel_members';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/AddChannelMembers', () => {
|
||||
const channel = TestHelper.getChannelMock({header: 'Test Header'});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly', () => {
|
||||
renderWithContext(
|
||||
<AddChannelMembers
|
||||
channel={channel}
|
||||
/>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add Members');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
});
|
||||
|
||||
test('dispatches openModal action on click', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<AddChannelMembers
|
||||
channel={channel}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add Members');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.CHANNEL_INVITE,
|
||||
dialogType: ChannelInviteModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {AccountPlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ChannelInviteModal from 'components/channel_invite_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const AddChannelMembers = ({channel}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAddMembers = () => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.CHANNEL_INVITE,
|
||||
dialogType: ChannelInviteModal,
|
||||
dialogProps: {channel},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='channelAddMembers'
|
||||
leadingElement={<AccountPlusOutlineIcon size='18px'/>}
|
||||
onClick={handleAddMembers}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar.addMembers'
|
||||
defaultMessage='Add Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddChannelMembers);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
import MoreDirectChannels from 'components/more_direct_channels';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import AddGroupMembers from './add_group_members';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/AddGroupMembers', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly', () => {
|
||||
renderWithContext(
|
||||
<AddGroupMembers/>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add Members');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
});
|
||||
|
||||
test('dispatches openModal action on click', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<AddGroupMembers/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add Members');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
|
||||
dialogType: MoreDirectChannels,
|
||||
dialogProps: {
|
||||
focusOriginElement: 'channelHeaderDropdownButton',
|
||||
isExistingChannel: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {AccountMultipleOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
import MoreDirectChannels from 'components/more_direct_channels';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
const AddGroupMembers = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleAddGroupMembers = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CREATE_DM_CHANNEL,
|
||||
dialogType: MoreDirectChannels,
|
||||
dialogProps: {isExistingChannel: true, focusOriginElement: 'channelHeaderDropdownButton'},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='channelAddMembers'
|
||||
leadingElement={<AccountMultipleOutlineIcon size='18px'/>}
|
||||
onClick={handleAddGroupMembers}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar.addMembers'
|
||||
defaultMessage='Add Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddGroupMembers);
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
import LocalStorageStore from 'stores/local_storage_store';
|
||||
|
||||
import DeleteChannelModal from 'components/delete_channel_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ArchiveChannel from './archive_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ArchiveChannel', () => {
|
||||
const initialState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'current_channel_id',
|
||||
channels: {
|
||||
current_channel_id: TestHelper.getChannelMock({
|
||||
id: 'current_channel_id',
|
||||
name: 'default-name',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team_id',
|
||||
name: 'team-1',
|
||||
display_name: 'Team 1',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
'team-id': {roles: 'team_role'},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
locale: 'en',
|
||||
roles: 'system_role'},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
LocalStorageStore.setPenultimateChannelName('current_user_id', 'team-id', 'current_channel_id');
|
||||
|
||||
const channel = TestHelper.getChannelMock({header: 'Test Header'});
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly', () => {
|
||||
renderWithContext(
|
||||
<ArchiveChannel channel={channel}/>, initialState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Archive Channel');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
});
|
||||
|
||||
test('dispatches openModal action on click with default channel', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ArchiveChannel channel={channel}/>
|
||||
</WithTestMenuContext>, initialState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Archive Channel');
|
||||
expect(menuItem).toBeInTheDocument(); // Check if text "Add Members" renders
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.DELETE_CHANNEL,
|
||||
dialogType: DeleteChannelModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
penultimateViewedChannelName: 'current_channel_id',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {getRedirectChannelNameForCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getPenultimateViewedChannelName} from 'selectors/local_storage';
|
||||
|
||||
import DeleteChannelModal from 'components/delete_channel_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const ArchiveChannel = ({
|
||||
channel,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const redirectChannelName = useSelector(getRedirectChannelNameForCurrentTeam);
|
||||
const penultimateViewedChannelName = useSelector(getPenultimateViewedChannelName) || redirectChannelName;
|
||||
|
||||
const handleArchiveChannel = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.DELETE_CHANNEL,
|
||||
dialogType: DeleteChannelModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
penultimateViewedChannelName,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='channelArchiveChannel'
|
||||
leadingElement={<ArchiveOutlineIcon size={18}/>}
|
||||
onClick={handleArchiveChannel}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.delete'
|
||||
defaultMessage='Archive Channel'
|
||||
/>
|
||||
}
|
||||
isDestructive={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ArchiveChannel);
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Purpose of this file to exists is only required until channel header dropdown is migrated to new menus
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
LinkVariantIcon,
|
||||
PaperclipIcon,
|
||||
BookmarkOutlineIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {getChannelBookmarks} from 'mattermost-redux/selectors/entities/channel_bookmarks';
|
||||
|
||||
import {useBookmarkAddActions} from 'components/channel_bookmarks/channel_bookmarks_menu';
|
||||
import {MAX_BOOKMARKS_PER_CHANNEL, useCanUploadFiles, useChannelBookmarkPermission} from 'components/channel_bookmarks/utils';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
};
|
||||
|
||||
const ChannelBookmarksSubmenu = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const {handleCreateLink, handleCreateFile} = useBookmarkAddActions(props.channel.id);
|
||||
const canAdd = useChannelBookmarkPermission(props.channel.id, 'add');
|
||||
const canUploadFiles = useCanUploadFiles();
|
||||
|
||||
useSelector((state: GlobalState) => {
|
||||
const bookmarks = getChannelBookmarks(state, props.channel.id);
|
||||
return bookmarks && Object.keys(bookmarks).length >= MAX_BOOKMARKS_PER_CHANNEL;
|
||||
});
|
||||
|
||||
if (!canAdd) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
id={`channel-menu-${props.channel.id}-bookmarks`}
|
||||
leadingElement={<BookmarkOutlineIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='channel_menu.bookmarks'
|
||||
defaultMessage='Bookmarks Bar'
|
||||
/>
|
||||
)}
|
||||
trailingElements={(
|
||||
<ChevronRightIcon size={16}/>
|
||||
)}
|
||||
menuId={`channel-menu-${props.channel.id}-menu`}
|
||||
menuAriaLabel={formatMessage({id: 'channel_menu.bookmarks', defaultMessage: 'Bookmarks Bar'})}
|
||||
>
|
||||
<Menu.Item
|
||||
id={`channel-menu-${props.channel.id}-bookmarks-link`}
|
||||
leadingElement={<LinkVariantIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='channel_menu.bookmarks.addLink'
|
||||
defaultMessage='Add a link'
|
||||
/>
|
||||
)}
|
||||
onClick={() => handleCreateLink()}
|
||||
/>
|
||||
{canUploadFiles && (
|
||||
<Menu.Item
|
||||
id={`channel-menu-${props.channel.id}-bookmarks-file`}
|
||||
leadingElement={<PaperclipIcon size={18}/>}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='channel_menu.bookmarks.addFile'
|
||||
defaultMessage='Attach a file'
|
||||
/>
|
||||
)}
|
||||
onClick={() => handleCreateFile()}
|
||||
/>
|
||||
)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChannelBookmarksSubmenu);
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
CogOutlineIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ConvertChannelModal from 'components/convert_channel_modal';
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import RenameChannelModal from 'components/rename_channel_modal';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
isReadonly: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
const ChannelSettingsSubmenu = ({channel, isReadonly, isDefault}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
|
||||
const handleRenameChannel = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.RENAME_CHANNEL,
|
||||
dialogType: RenameChannelModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditHeader = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditPurpose = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
||||
dialogType: EditChannelPurposeModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleConvertToPrivate = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CONVERT_CHANNEL,
|
||||
dialogType: ConvertChannelModal,
|
||||
dialogProps: {
|
||||
channelId: channel.id,
|
||||
channelDisplayName: channel.display_name,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
id={'channelSettings'}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channelSettings'
|
||||
defaultMessage='Channel Settings'
|
||||
/>
|
||||
}
|
||||
leadingElement={<CogOutlineIcon size={18}/>}
|
||||
trailingElements={<ChevronRightIcon size={16}/>}
|
||||
menuId={'channelSettings-menu'}
|
||||
menuAriaLabel={formatMessage({id: 'channelSettings', defaultMessage: 'Channel Settings'})}
|
||||
>
|
||||
{!isReadonly && (
|
||||
<Menu.Item
|
||||
id='channelRename'
|
||||
onClick={handleRenameChannel}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.rename'
|
||||
defaultMessage='Rename Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isReadonly && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelPropertiesPermission]}
|
||||
>
|
||||
<Menu.Item
|
||||
id='channelEditHeader'
|
||||
onClick={handleEditHeader}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.setHeader'
|
||||
defaultMessage='Edit Channel Header'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
|
||||
{!isReadonly && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelPropertiesPermission]}
|
||||
>
|
||||
<Menu.Item
|
||||
id='channelEditPurpose'
|
||||
onClick={handleEditPurpose}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.setPurpose'
|
||||
defaultMessage='Edit Channel Purpose'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
|
||||
{!isDefault && channel.type === Constants.OPEN_CHANNEL && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
|
||||
>
|
||||
<Menu.Item
|
||||
id='channelConvertToPrivate'
|
||||
onClick={handleConvertToPrivate}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.convert'
|
||||
defaultMessage='Convert to Private Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChannelSettingsSubmenu);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as channelActions from 'actions/views/channel';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
|
||||
import CloseChannel from './close_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/CloseChannel', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(channelActions, 'goToLastViewedChannel');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<CloseChannel/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Close Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(channelActions.goToLastViewedChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {goToLastViewedChannel} from 'actions/views/channel';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {}
|
||||
|
||||
const CloseChannel = ({...rest}: Props): JSX.Element => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={goToLastViewedChannel}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='center_panel.archived.closeChannel'
|
||||
defaultMessage='Close Channel'
|
||||
/>}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default React.memo(CloseChannel);
|
||||
@@ -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 * as preferences from 'mattermost-redux/actions/preferences';
|
||||
|
||||
import * as channelActions from 'actions/views/channel';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {Constants} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import CloseMessage from './close_message';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/CloseMessage', () => {
|
||||
const initialState = {
|
||||
entities: {
|
||||
channels: {
|
||||
currentChannelId: 'current_channel_id',
|
||||
channels: {
|
||||
current_channel_id: TestHelper.getChannelMock({
|
||||
id: 'current_channel_id',
|
||||
name: 'default-name',
|
||||
display_name: 'Default',
|
||||
delete_at: 0,
|
||||
type: 'O',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team_id',
|
||||
name: 'team-1',
|
||||
display_name: 'Team 1',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
'team-id': {roles: 'team_role'},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
locale: 'en',
|
||||
roles: 'system_role',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const groupChannel = TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
name: 'groupChannel',
|
||||
type: Constants.GM_CHANNEL as ChannelType,
|
||||
});
|
||||
|
||||
const directChannel = TestHelper.getChannelMock({
|
||||
id: 'channel_id',
|
||||
type: Constants.DM_CHANNEL as ChannelType,
|
||||
teammate_id: 'teammate-id',
|
||||
name: 'directChannel',
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(channelActions, 'leaveDirectChannel');
|
||||
jest.spyOn(preferences, 'savePreferences');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly for group channel', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<CloseMessage
|
||||
currentUserID='current_user_id'
|
||||
channel={groupChannel}
|
||||
/>
|
||||
</WithTestMenuContext>, initialState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Close Group Message');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(channelActions.leaveDirectChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.leaveDirectChannel).toHaveBeenCalledWith(groupChannel.name);
|
||||
|
||||
expect(preferences.savePreferences).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(preferences.savePreferences).toHaveBeenCalledWith(
|
||||
'current_user_id',
|
||||
[{user_id: 'current_user_id', category: Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW, name: groupChannel.id, value: 'false'}],
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the component correctly for direct channel', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<CloseMessage
|
||||
currentUserID='current_user_id'
|
||||
channel={directChannel}
|
||||
/>
|
||||
</WithTestMenuContext>, initialState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Close Direct Message');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(channelActions.leaveDirectChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.leaveDirectChannel).toHaveBeenCalledWith(directChannel.name);
|
||||
|
||||
expect(preferences.savePreferences).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(preferences.savePreferences).toHaveBeenCalledWith(
|
||||
'current_user_id',
|
||||
[{user_id: 'current_user_id', category: Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, name: directChannel.teammate_id, value: 'false'}],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {CloseIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
import {getRedirectChannelNameForCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {leaveDirectChannel} from 'actions/views/channel';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
currentUserID: string;
|
||||
channel: Channel;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export default function CloseMessage(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const redirectChannel = useSelector(getRedirectChannelNameForCurrentTeam);
|
||||
|
||||
const handleClose = () => {
|
||||
const {
|
||||
channel,
|
||||
currentUserID,
|
||||
} = props;
|
||||
|
||||
let name: string;
|
||||
let category;
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
category = Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW;
|
||||
name = channel.teammate_id!;
|
||||
} else {
|
||||
category = Constants.Preferences.CATEGORY_GROUP_CHANNEL_SHOW;
|
||||
name = channel.id;
|
||||
}
|
||||
|
||||
dispatch(leaveDirectChannel(channel.name));
|
||||
dispatch(savePreferences(currentUserID, [{user_id: currentUserID, category, name, value: 'false'}]));
|
||||
|
||||
if (currentTeam) {
|
||||
getHistory().push(`/${currentTeam.name}/channels/${redirectChannel}`);
|
||||
}
|
||||
};
|
||||
|
||||
const {id, channel} = props;
|
||||
|
||||
// DM
|
||||
let text = (
|
||||
<FormattedMessage
|
||||
id='center_panel.direct.closeDirectMessage'
|
||||
defaultMessage='Close Direct Message'
|
||||
/>);
|
||||
if (channel.type === Constants.GM_CHANNEL) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='center_panel.direct.closeGroupMessage'
|
||||
defaultMessage='Close Group Message'
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={id}
|
||||
leadingElement={<CloseIcon size='18px'/>}
|
||||
onClick={handleClose}
|
||||
labels={text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ConvertGMtoPrivate from './convert_gm_to_private';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ConvertGMtoPrivate', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ConvertGMtoPrivate channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Convert to Private Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.CONVERT_GM_TO_CHANNEL,
|
||||
dialogType: ConvertGmToChannelModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const ConvertGMtoPrivate = ({channel}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const handleConvertToPrivate = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CONVERT_GM_TO_CHANNEL,
|
||||
dialogType: ConvertGmToChannelModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='convertGMPrivateChannel'
|
||||
onClick={handleConvertToPrivate}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='sidebar_left.sidebar_channel_menu_convert_to_channel'
|
||||
defaultMessage='Convert to Private Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ConvertGMtoPrivate);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import ConvertChannelModal from 'components/convert_channel_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ConvertPublictoPrivate from './convert_public_to_private';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ConvertPublicToPrivate', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ConvertPublictoPrivate channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Convert to Private Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.CONVERT_CHANNEL,
|
||||
dialogType: ConvertChannelModal,
|
||||
dialogProps: {
|
||||
channelId: channel.id,
|
||||
channelDisplayName: channel.display_name,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ConvertChannelModal from 'components/convert_channel_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const ConvertPublictoPrivate = ({channel}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const handleConvertToPrivate = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CONVERT_CHANNEL,
|
||||
dialogType: ConvertChannelModal,
|
||||
dialogProps: {
|
||||
channelId: channel.id,
|
||||
channelDisplayName: channel.display_name,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='channelConvertToPrivate'
|
||||
onClick={handleConvertToPrivate}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.convert'
|
||||
defaultMessage='Convert to Private Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ConvertPublictoPrivate);
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
import RenameChannelModal from 'components/rename_channel_modal';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import EditChannelSettings from './edit_channel_settings';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/EditChannelSettings', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<EditChannelSettings
|
||||
channel={channel}
|
||||
isReadonly={false}
|
||||
isDefault={false}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Rename Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.RENAME_CHANNEL,
|
||||
dialogType: RenameChannelModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import EditChannelPurposeModal from 'components/edit_channel_purpose_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import RenameChannelModal from 'components/rename_channel_modal';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import MenuItemConvertToPrivate from './convert_public_to_private';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
isReadonly: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
const EditChannelSettings = ({channel, isReadonly, isDefault}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const channelPropertiesPermission = channel.type === Constants.PRIVATE_CHANNEL ? Permissions.MANAGE_PRIVATE_CHANNEL_PROPERTIES : Permissions.MANAGE_PUBLIC_CHANNEL_PROPERTIES;
|
||||
|
||||
const handleRenameChannel = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.RENAME_CHANNEL,
|
||||
dialogType: RenameChannelModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditHeader = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditPurpose = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_PURPOSE,
|
||||
dialogType: EditChannelPurposeModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!isReadonly && (
|
||||
<>
|
||||
<Menu.Item
|
||||
id='channelRename'
|
||||
onClick={handleRenameChannel}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.rename'
|
||||
defaultMessage='Rename Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[channelPropertiesPermission]}
|
||||
>
|
||||
<Menu.Item
|
||||
id='channelEditHeader'
|
||||
onClick={handleEditHeader}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.setHeader'
|
||||
defaultMessage='Edit Channel Header'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Menu.Item
|
||||
id='channelEditPurpose'
|
||||
onClick={handleEditPurpose}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.setPurpose'
|
||||
defaultMessage='Edit Channel Purpose'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isDefault && channel.type === Constants.OPEN_CHANNEL && (
|
||||
<ChannelPermissionGate
|
||||
channelId={channel.id}
|
||||
teamId={channel.team_id}
|
||||
permissions={[Permissions.CONVERT_PUBLIC_CHANNEL_TO_PRIVATE]}
|
||||
>
|
||||
<MenuItemConvertToPrivate
|
||||
channel={channel}
|
||||
/>
|
||||
</ChannelPermissionGate>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditChannelSettings);
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import EditConversationHeader from './edit_conversation_header';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/EditConversationHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<EditConversationHeader channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Edit Header');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
leadingElement?: React.ReactNode;
|
||||
}
|
||||
|
||||
const EditConversationHeader = ({channel, leadingElement}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const handleEditHeader = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.EDIT_CHANNEL_HEADER,
|
||||
dialogType: EditChannelHeaderModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id='channelEditHeader'
|
||||
leadingElement={leadingElement}
|
||||
onClick={handleEditHeader}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.setConversationHeader'
|
||||
defaultMessage='Edit Header'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(EditConversationHeader);
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
|
||||
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import Groups from './groups';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/Groups', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
test('renders the component correctly, handle click event for add groups', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<Groups channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add Groups');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.ADD_GROUPS_TO_CHANNEL,
|
||||
dialogType: AddGroupsToChannelModal,
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event for manage groups', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<Groups channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItemMG = screen.getByText('Manage Groups');
|
||||
expect(menuItemMG).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItemMG); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.MANAGE_CHANNEL_GROUPS,
|
||||
dialogType: ChannelGroupsManageModal,
|
||||
dialogProps: {channelID: channel.id},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {AccountMultipleOutlineIcon, AccountMultiplePlusOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import AddGroupsToChannelModal from 'components/add_groups_to_channel_modal';
|
||||
import ChannelGroupsManageModal from 'components/channel_groups_manage_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const Groups = ({channel, ...rest}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const handleAddGroups = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.ADD_GROUPS_TO_CHANNEL,
|
||||
dialogType: AddGroupsToChannelModal,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleManageGroups = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.MANAGE_CHANNEL_GROUPS,
|
||||
dialogType: ChannelGroupsManageModal,
|
||||
dialogProps: {channelID: channel.id},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Item
|
||||
id='channelAddGroups'
|
||||
leadingElement={<AccountMultiplePlusOutlineIcon size='18px'/>}
|
||||
onClick={handleAddGroups}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar.addGroups'
|
||||
defaultMessage='Add Groups'
|
||||
/>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
<Menu.Item
|
||||
id='channelManageGroups'
|
||||
leadingElement={<AccountMultipleOutlineIcon size='18px'/>}
|
||||
onClick={handleManageGroups}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.manageGroups'
|
||||
defaultMessage='Manage Groups'
|
||||
/>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Groups);
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as channelActions from 'actions/views/channel';
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import LeaveChannelModal from 'components/leave_channel_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import LeaveChannel from './leave_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/LeaveChannelTest', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
jest.spyOn(channelActions, 'leaveChannel');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event correctly for Public Channel', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<LeaveChannel channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Leave Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.leaveChannel).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.leaveChannel).toHaveBeenCalledWith(channel.id);
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event for manage groups', () => {
|
||||
const channel = TestHelper.getChannelMock({type: 'P'});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<LeaveChannel channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItemMG = screen.getByText('Leave Channel');
|
||||
expect(menuItemMG).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItemMG); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
|
||||
dialogType: LeaveChannelModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {LogoutVariantIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {leaveChannel} from 'actions/views/channel';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import LeaveChannelModal from 'components/leave_channel_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
// import type {PropsFromRedux} from './index';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const LeaveChannel = ({
|
||||
channel,
|
||||
id,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const handleLeave = () => {
|
||||
if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.LEAVE_PRIVATE_CHANNEL_MODAL,
|
||||
dialogType: LeaveChannelModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(leaveChannel(channel.id));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={id}
|
||||
leadingElement={<LogoutVariantIcon size='18px'/>}
|
||||
onClick={handleLeave}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.leave'
|
||||
defaultMessage='Leave Channel'
|
||||
/>
|
||||
}
|
||||
isDestructive={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LeaveChannel);
|
||||
@@ -0,0 +1,369 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {AppBindingLocations, AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
|
||||
import * as appsActions from 'actions/apps';
|
||||
import * as channelActions from 'actions/views/channel';
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent, waitFor} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import MobileChannelHeaderPlugins from './mobile_channel_header_plugins';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/MobileChannelHeaderPlugins, with no extended components', () => {
|
||||
jest.mock('actions/apps', () => ({
|
||||
...jest.requireActual('actions/apps'),
|
||||
handleBindingClick: jest.fn(),
|
||||
}));
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
jest.spyOn(channelActions, 'leaveChannel');
|
||||
|
||||
// jest.spyOn(appsActions, 'handleBindingClick');
|
||||
jest.spyOn(appsActions, 'openAppsModal');
|
||||
jest.spyOn(appsActions, 'postEphemeralCallResponseForChannel');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const channel = TestHelper.getChannelMock();
|
||||
const action = jest.fn();
|
||||
const pluginState = {
|
||||
plugins: {
|
||||
components: {
|
||||
MobileChannelHeaderButton: [
|
||||
{
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action,
|
||||
dropdownText: 'some dropdown text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bindingState = {
|
||||
entities: {
|
||||
apps: {
|
||||
main: {
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'appid',
|
||||
location: AppBindingLocations.CHANNEL_HEADER_ICON,
|
||||
icon: 'http://test.com/icon.png',
|
||||
label: 'Label',
|
||||
hint: 'Hint',
|
||||
bindings: [
|
||||
{
|
||||
app_id: 'app1',
|
||||
location: 'channel-header-1',
|
||||
label: 'App 1 Channel Header',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/call/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
FeatureFlagAppsEnabled: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('renders the component correctly', () => {
|
||||
const {container} = renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test('renders the component correctly, with one extended component, and handle click event', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, pluginState,
|
||||
);
|
||||
const menuItem = screen.getByText('some dropdown text');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
fireEvent.click(menuItem);
|
||||
expect(action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders the component correctly, with two extended component', () => {
|
||||
const testState = {
|
||||
...pluginState,
|
||||
plugins: {
|
||||
...pluginState.plugins,
|
||||
components: {
|
||||
...pluginState.plugins.components,
|
||||
MobileChannelHeaderButton: [
|
||||
...pluginState.plugins.components.MobileChannelHeaderButton,
|
||||
{
|
||||
id: 'someid2',
|
||||
pluginId: 'pluginid2',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some other dropdown text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, testState,
|
||||
);
|
||||
const menuItem = screen.getByText('some dropdown text');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
const menuItem2 = screen.getByText('some other dropdown text');
|
||||
expect(menuItem2).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the component correctly, with two extended bindings', () => {
|
||||
const testState = cloneDeep(bindingState);
|
||||
testState.entities.apps.main.bindings[0].bindings.push({
|
||||
app_id: 'app2',
|
||||
location: 'channel-header-2',
|
||||
label: 'App 2 Channel Header',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/call/path',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, testState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('App 1 Channel Header');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
const menuItem2 = screen.getByText('App 2 Channel Header');
|
||||
expect(menuItem2).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Processes handleBinding, returns AppCallResponseTypes.OK', async () => {
|
||||
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
type: AppCallResponseTypes.OK,
|
||||
text: 'hello',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, bindingState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('App 1 Channel Header');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem);
|
||||
await waitFor(() => {
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
expect(appsActions.postEphemeralCallResponseForChannel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Processes handleBinding, returns AppCallResponseTypes.Form', async () => {
|
||||
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
type: AppCallResponseTypes.FORM,
|
||||
form: {
|
||||
submit: {
|
||||
path: '/call/path',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, bindingState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('App 1 Channel Header');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem);
|
||||
await waitFor(() => {
|
||||
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
expect(appsActions.openAppsModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Processes handleBinding, returns Error', async () => {
|
||||
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
|
||||
return Promise.resolve({
|
||||
error: {
|
||||
type: AppCallResponseTypes.ERROR,
|
||||
text: 'Error returned from method',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={true}
|
||||
/>
|
||||
</WithTestMenuContext>, bindingState,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('App 1 Channel Header');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem);
|
||||
await waitFor(() => {
|
||||
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
expect(appsActions.postEphemeralCallResponseForChannel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the component correctly, with one extended component, isDropDown false', () => {
|
||||
const action = jest.fn();
|
||||
const pluginState = {
|
||||
plugins: {
|
||||
components: {
|
||||
MobileChannelHeaderButton: [
|
||||
{
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action,
|
||||
dropdownText: 'some dropdown text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={false}
|
||||
/>
|
||||
</WithTestMenuContext>, pluginState,
|
||||
);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
fireEvent.click(button);
|
||||
expect(action).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders the component correctly, with one extended appbinding, isDropDown false', async () => {
|
||||
jest.spyOn(appsActions, 'handleBindingClick').mockReturnValueOnce(() => {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
type: AppCallResponseTypes.OK,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={false}
|
||||
/>
|
||||
</WithTestMenuContext>, bindingState,
|
||||
);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(appsActions.handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('renders noting if multiple appbindings or components isDropDown false', () => {
|
||||
const pluginState = {
|
||||
plugins: {
|
||||
components: {
|
||||
MobileChannelHeaderButton: [
|
||||
{
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some dropdown text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const bothState = {
|
||||
...bindingState,
|
||||
...pluginState,
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={false}
|
||||
/>
|
||||
</WithTestMenuContext>, bothState,
|
||||
);
|
||||
const button = screen.queryByRole('button');
|
||||
expect(button).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
|
||||
import type {AppBinding} from '@mattermost/types/apps';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {AppCallResponseTypes, AppBindingLocations} from 'mattermost-redux/constants/apps';
|
||||
import {makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
|
||||
import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps';
|
||||
import {getChannelMobileHeaderPluginButtons} from 'selectors/plugins';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {createCallContext} from 'utils/apps';
|
||||
|
||||
import type {MobileChannelHeaderButtonAction} from 'types/store/plugins';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
isDropdown: boolean;
|
||||
}
|
||||
|
||||
const MobileChannelHeaderPlugins = (props: Props): JSX.Element => {
|
||||
const mobileComponents = useSelector(getChannelMobileHeaderPluginButtons);
|
||||
const channelMember = useSelector(getMyCurrentChannelMembership);
|
||||
const getChannelHeaderBindings = useSelector(makeAppBindingsSelector(AppBindingLocations.CHANNEL_HEADER_ICON));
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const createAppButton = (binding: AppBinding) => {
|
||||
const handleAppButtonClick = () => fireAppAction(binding);
|
||||
|
||||
if (props.isDropdown) {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={'mobileChannelHeaderItem' + binding.app_id + binding.location}
|
||||
onClick={handleAppButtonClick}
|
||||
labels={<span>{binding.label}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
id={`${binding.app_id}_${binding.location}`}
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={handleAppButtonClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
<img
|
||||
alt=''
|
||||
src={binding.icon}
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const createButton = (plug: MobileChannelHeaderButtonAction) => {
|
||||
const handlePluginButtonClick = () => fireAction(plug);
|
||||
|
||||
if (props.isDropdown) {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={'mobileChannelHeaderItem' + plug.id}
|
||||
id={'mobileChannelHeaderItem' + plug.id}
|
||||
onClick={handlePluginButtonClick}
|
||||
labels={<span>{plug.dropdownText}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={handlePluginButtonClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
{plug.icon}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const createList = (plugs: MobileChannelHeaderButtonAction[]) => {
|
||||
return plugs.map(createButton);
|
||||
};
|
||||
|
||||
const createAppList = (bindings: AppBinding[]) => {
|
||||
return bindings.map(createAppButton);
|
||||
};
|
||||
|
||||
const fireAction = (plug: MobileChannelHeaderButtonAction) => {
|
||||
return plug.action?.(props.channel, channelMember);
|
||||
};
|
||||
|
||||
const fireAppAction = async (binding: AppBinding) => {
|
||||
const {channel} = props;
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.location,
|
||||
channel.id,
|
||||
channel.team_id,
|
||||
);
|
||||
|
||||
const handleAppResponse = (callResp: any, errorResponse?: any) => {
|
||||
if (errorResponse) {
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
dispatch(postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
dispatch(postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id));
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
dispatch(openAppsModal(callResp.form, context));
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const errorMessage = intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
});
|
||||
dispatch(postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const res = await dispatch(handleBindingClick(binding, context, intl));
|
||||
if (res.error) {
|
||||
handleAppResponse(null, res.error);
|
||||
return;
|
||||
}
|
||||
handleAppResponse(res.data!);
|
||||
};
|
||||
|
||||
const components = mobileComponents || [];
|
||||
const bindings = getChannelHeaderBindings || [];
|
||||
|
||||
if (components.length === 0 && bindings.length === 0) {
|
||||
return <></>;
|
||||
} else if (components.length === 1 && bindings.length === 0) {
|
||||
return createButton(components[0]);
|
||||
} else if (components.length === 0 && bindings.length === 1) {
|
||||
return createAppButton(bindings[0]);
|
||||
}
|
||||
|
||||
if (!props.isDropdown) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const plugItems = createList(components);
|
||||
const appItems = createAppList(bindings);
|
||||
return (
|
||||
<>
|
||||
<Menu.Separator/>
|
||||
{appItems}
|
||||
{plugItems}
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
// Exported for tests
|
||||
export {MobileChannelHeaderPlugins as RawMobileChannelHeaderPlug};
|
||||
|
||||
export default memo(MobileChannelHeaderPlugins);
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import ChannelNotificationsModal from 'components/channel_notifications_modal';
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import Notification from './notification';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/Notification', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
|
||||
// Mock useDispatch to return our custom dispatch function
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
const user = TestHelper.getUserMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<Notification
|
||||
channel={channel}
|
||||
user={user}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItemMG = screen.getByText('Notification Preferences');
|
||||
expect(menuItemMG).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItemMG); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.CHANNEL_NOTIFICATIONS,
|
||||
dialogType: ChannelNotificationsModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
currentUser: user,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {BellOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import ChannelNotificationsModal from 'components/channel_notifications_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
user: UserProfile;
|
||||
}
|
||||
|
||||
const Notification = ({channel, user, ...rest}: Props): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleNotificationPreferences = () => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.CHANNEL_NOTIFICATIONS,
|
||||
dialogType: ChannelNotificationsModal,
|
||||
dialogProps: {
|
||||
channel,
|
||||
currentUser: user,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={<BellOutlineIcon size='18px'/>}
|
||||
id='channelNotificationPreferences'
|
||||
onClick={handleNotificationPreferences}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar.preferences'
|
||||
defaultMessage='Notification Preferences'
|
||||
/>
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Notification);
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as rhsActions from 'actions/views/rhs';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import OpenMembersRHS from './open_members_rhs';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/OpenMembersRHS', () => {
|
||||
beforeEach(() => {
|
||||
// jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true}));
|
||||
jest.spyOn(rhsActions, 'showChannelMembers').mockReturnValue(() => Promise.resolve({data: true}));
|
||||
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles click event, rhs closed', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<OpenMembersRHS
|
||||
channel={channel}
|
||||
id={'testID'}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.viewMembers'
|
||||
defaultMessage='View Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('View Members');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(rhsActions.showChannelMembers).toHaveBeenCalledTimes(1);
|
||||
expect(rhsActions.showChannelMembers).toHaveBeenCalledWith(channel.id, false);
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event, rhs open', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: RHSStates.CHANNEL_MEMBERS,
|
||||
isSidebarOpen: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<OpenMembersRHS
|
||||
channel={channel}
|
||||
id={'testID'}
|
||||
text={
|
||||
<FormattedMessage
|
||||
id='channel_header.viewMembers'
|
||||
defaultMessage='View Members'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('View Members');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu
|
||||
expect(rhsActions.showChannelMembers).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {AccountOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {showChannelMembers} from 'actions/views/rhs';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
id: string;
|
||||
editMembers?: boolean;
|
||||
text: React.ReactElement;
|
||||
};
|
||||
|
||||
const OpenMembersRHS = ({
|
||||
id,
|
||||
channel,
|
||||
text,
|
||||
editMembers = false,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
let rhsOpen = useSelector(getIsRhsOpen);
|
||||
const rhsState = useSelector(getRhsState);
|
||||
if (rhsState !== RHSStates.CHANNEL_MEMBERS) {
|
||||
rhsOpen = false;
|
||||
}
|
||||
const openRHSIfNotOpen = () => {
|
||||
if (rhsOpen) {
|
||||
return;
|
||||
}
|
||||
dispatch(showChannelMembers(channel.id, editMembers));
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={<AccountOutlineIcon size={16}/>}
|
||||
id={id}
|
||||
onClick={openRHSIfNotOpen}
|
||||
labels={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenMembersRHS;
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ReactNode} from 'react';
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {
|
||||
AppsIcon,
|
||||
ChevronRightIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
type Props = {
|
||||
pluginItems: ReactNode[];
|
||||
};
|
||||
|
||||
const PluginsSubmenu = (props: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
if (!props.pluginItems || !props.pluginItems.length) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
id={'moreActions'}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='pluginsMenu.more_actions'
|
||||
defaultMessage='More actions'
|
||||
/>
|
||||
}
|
||||
leadingElement={<AppsIcon size='18px'/>}
|
||||
trailingElements={<ChevronRightIcon size={16}/>}
|
||||
menuId={'moreActions-menu'}
|
||||
menuAriaLabel={formatMessage({id: 'pluginsMenu.more_actions', defaultMessage: 'More actions'})}
|
||||
>
|
||||
{props.pluginItems}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PluginsSubmenu);
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as channelActions from 'mattermost-redux/actions/channels';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ToggleFavoriteChannel from './toggle_favorite_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ToggleFavoriteChannel', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(channelActions, 'favoriteChannel').mockReturnValue(() => Promise.resolve({data: true}));
|
||||
jest.spyOn(channelActions, 'unfavoriteChannel');
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event, is favorite false', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleFavoriteChannel
|
||||
channelID={channel.id}
|
||||
isFavorite={false}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Add to Favorites');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.favoriteChannel).toHaveBeenCalledTimes(1);
|
||||
expect(channelActions.favoriteChannel).toHaveBeenCalledWith(channel.id);
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event, is favorite true', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleFavoriteChannel
|
||||
channelID={channel.id}
|
||||
isFavorite={true}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Remove from Favorites');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.unfavoriteChannel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {favoriteChannel, unfavoriteChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
type Props = {
|
||||
channelID: string;
|
||||
isFavorite: boolean;
|
||||
};
|
||||
|
||||
const ToggleFavoriteChannel = ({
|
||||
isFavorite,
|
||||
channelID,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite) {
|
||||
dispatch(unfavoriteChannel(channelID));
|
||||
} else {
|
||||
dispatch(favoriteChannel(channelID));
|
||||
}
|
||||
};
|
||||
|
||||
let text = (
|
||||
<FormattedMessage
|
||||
id='channelHeader.addToFavorites'
|
||||
defaultMessage='Add to Favorites'
|
||||
/>);
|
||||
if (isFavorite) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channelHeader.removeFromFavorites'
|
||||
defaultMessage='Remove from Favorites'
|
||||
/>);
|
||||
}
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={toggleFavorite}
|
||||
labels={text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ToggleFavoriteChannel);
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as rhsActions from 'actions/views/rhs';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ToggleInfo from './toggle_info';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ToggleInfo', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true}));
|
||||
jest.spyOn(rhsActions, 'showChannelInfo');
|
||||
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles click event, rhs closed', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleInfo channel={channel}/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('View Info');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
// expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(rhsActions.showChannelInfo).toHaveBeenCalledTimes(1);
|
||||
expect(rhsActions.showChannelInfo).toHaveBeenCalledWith(channel.id);
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event, rhs open', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: RHSStates.CHANNEL_INFO,
|
||||
isSidebarOpen: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleInfo channel={channel}/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Close Info');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(rhsActions.closeRightHandSide).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useSelector, useDispatch} from 'react-redux';
|
||||
|
||||
import {InformationOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {closeRightHandSide, showChannelInfo} from 'actions/views/rhs';
|
||||
import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
interface Props extends Menu.FirstMenuItemProps {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const ToggleInfo = ({channel, ...rest}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
let rhsOpen = useSelector(getIsRhsOpen);
|
||||
const rhsState = useSelector(getRhsState);
|
||||
if (rhsState !== RHSStates.CHANNEL_INFO) {
|
||||
rhsOpen = false;
|
||||
}
|
||||
|
||||
const toggleRHS = () => {
|
||||
if (rhsOpen) {
|
||||
dispatch(closeRightHandSide());
|
||||
return;
|
||||
}
|
||||
dispatch(showChannelInfo(channel.id));
|
||||
};
|
||||
|
||||
let text;
|
||||
if (rhsOpen) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channelHeader.hideInfo'
|
||||
defaultMessage='Close Info'
|
||||
/>);
|
||||
} else {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channelHeader.viewInfo'
|
||||
defaultMessage='View Info'
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Item
|
||||
leadingElement={<InformationOutlineIcon size='18px'/>}
|
||||
onClick={toggleRHS}
|
||||
labels={text}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleInfo;
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as channelActions from 'mattermost-redux/actions/channels';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {NotificationLevels} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ToggleMuteChannel from './toggle_mute_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ToggleMuteChannel', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
const user = TestHelper.getUserMock();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(channelActions, 'updateChannelNotifyProps').mockReturnValue(() => Promise.resolve({data: true}));
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, public channel, not muted', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleMuteChannel
|
||||
channel={channel}
|
||||
userID={user.id}
|
||||
isMuted={false}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Mute Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
channel.id,
|
||||
{mark_unread: NotificationLevels.MENTION},
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the component correctly, public channel, muted', () => {
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleMuteChannel
|
||||
channel={channel}
|
||||
userID={user.id}
|
||||
isMuted={true}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Unmute Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
channel.id,
|
||||
{mark_unread: NotificationLevels.ALL},
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the component correctly, dm channel, not muted', () => {
|
||||
const channel = TestHelper.getChannelMock({type: 'D'});
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleMuteChannel
|
||||
channel={channel}
|
||||
userID={user.id}
|
||||
isMuted={false}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Mute');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
channel.id,
|
||||
{mark_unread: NotificationLevels.MENTION},
|
||||
);
|
||||
});
|
||||
test('renders the component correctly, dm channel, muted', () => {
|
||||
const channel = TestHelper.getChannelMock({type: 'D'});
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ToggleMuteChannel
|
||||
channel={channel}
|
||||
userID={user.id}
|
||||
isMuted={true}
|
||||
/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Unmute');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledTimes(1);
|
||||
expect(channelActions.updateChannelNotifyProps).toHaveBeenCalledWith(
|
||||
user.id,
|
||||
channel.id,
|
||||
{mark_unread: NotificationLevels.ALL},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import {BellOffOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {updateChannelNotifyProps} from 'mattermost-redux/actions/channels';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {Constants, NotificationLevels} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
userID: string;
|
||||
channel: Channel;
|
||||
isMuted: boolean;
|
||||
};
|
||||
|
||||
export default function ToggleMuteChannel({
|
||||
isMuted,
|
||||
channel,
|
||||
userID,
|
||||
}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = () => {
|
||||
dispatch(updateChannelNotifyProps(
|
||||
userID,
|
||||
channel.id,
|
||||
{
|
||||
mark_unread: (isMuted ? NotificationLevels.ALL : NotificationLevels.MENTION) as 'all' | 'mention',
|
||||
},
|
||||
));
|
||||
};
|
||||
|
||||
let text;
|
||||
if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
|
||||
if (isMuted) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channel_header.unmuteConversation'
|
||||
defaultMessage='Unmute'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channel_header.muteConversation'
|
||||
defaultMessage='Mute'
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if (isMuted) {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channel_header.unmute'
|
||||
defaultMessage='Unmute Channel'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='channel_header.mute'
|
||||
defaultMessage='Mute Channel'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={<BellOffOutlineIcon size='18px'/>}
|
||||
id='channelToggleMuteChannel'
|
||||
onClick={handleClick}
|
||||
labels={text}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as modalActions from 'actions/views/modals';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
import UnarchiveChannelModal from 'components/unarchive_channel_modal';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import UnarchiveChannel from './unarchive_channel';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/UnarchiveChannel', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(modalActions, 'openModal');
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handle click event', () => {
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<UnarchiveChannel channel={channel}/>
|
||||
</WithTestMenuContext>, {},
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Unarchive Channel');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(modalActions.openModal).toHaveBeenCalledTimes(1);
|
||||
expect(modalActions.openModal).toHaveBeenCalledWith({
|
||||
modalId: ModalIdentifiers.UNARCHIVE_CHANNEL,
|
||||
dialogType: UnarchiveChannelModal,
|
||||
dialogProps: {channel},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
import UnarchiveChannelModal from 'components/unarchive_channel_modal';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
const UnarchiveChannel = ({
|
||||
channel,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleUnarchiveChannel = () => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.UNARCHIVE_CHANNEL,
|
||||
dialogType: UnarchiveChannelModal,
|
||||
dialogProps: {channel},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Separator/>
|
||||
<Menu.Item
|
||||
id='channelUnarchiveChannel'
|
||||
onClick={handleUnarchiveChannel}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='channel_header.unarchive'
|
||||
defaultMessage='Unarchive Channel'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(UnarchiveChannel);
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import * as rhsActions from 'actions/views/rhs';
|
||||
|
||||
import {WithTestMenuContext} from 'components/menu/menu_context_test';
|
||||
|
||||
import {renderWithContext, screen, fireEvent} from 'tests/react_testing_utils';
|
||||
import {RHSStates} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ViewPinnedPosts from './view_pinned_posts';
|
||||
|
||||
describe('components/ChannelHeaderMenu/MenuItems/ViewPinnedPosts', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(rhsActions, 'closeRightHandSide').mockImplementation(() => () => ({data: true}));
|
||||
jest.spyOn(rhsActions, 'showPinnedPosts').mockReturnValue(() => Promise.resolve({data: true}));
|
||||
|
||||
jest.spyOn(require('react-redux'), 'useDispatch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ViewPinnedPosts channelID={channel.id}/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('View Pinned Posts');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(rhsActions.showPinnedPosts).toHaveBeenCalledTimes(1);
|
||||
expect(rhsActions.showPinnedPosts).toHaveBeenCalledWith(channel.id);
|
||||
});
|
||||
|
||||
test('renders the component correctly, handles correct click event', () => {
|
||||
const state = {
|
||||
views: {
|
||||
rhs: {
|
||||
rhsState: RHSStates.PIN,
|
||||
},
|
||||
},
|
||||
};
|
||||
const channel = TestHelper.getChannelMock();
|
||||
|
||||
renderWithContext(
|
||||
<WithTestMenuContext>
|
||||
<ViewPinnedPosts channelID={channel.id}/>
|
||||
</WithTestMenuContext>, state,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('View Pinned Posts');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(menuItem); // Simulate click on the menu item
|
||||
expect(useDispatch).toHaveBeenCalledTimes(1); // Ensure dispatch was called
|
||||
expect(rhsActions.closeRightHandSide).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import {closeRightHandSide, showPinnedPosts} from 'actions/views/rhs';
|
||||
import {getRhsState} from 'selectors/rhs';
|
||||
|
||||
import * as Menu from 'components/menu';
|
||||
|
||||
import {RHSStates} from 'utils/constants';
|
||||
|
||||
type Props = {
|
||||
channelID: string;
|
||||
}
|
||||
|
||||
const ViewPinnedPosts = ({
|
||||
channelID,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const rhsState = useSelector(getRhsState);
|
||||
const hasPinnedPosts = rhsState === RHSStates.PIN;
|
||||
|
||||
const handleClick = () => {
|
||||
if (hasPinnedPosts) {
|
||||
dispatch(closeRightHandSide());
|
||||
} else {
|
||||
dispatch(showPinnedPosts(channelID));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={handleClick}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='navbar.viewPinnedPosts'
|
||||
defaultMessage='View Pinned Posts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ViewPinnedPosts);
|
||||
@@ -1,285 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot 1`] = `
|
||||
<nav
|
||||
className="navbar navbar-default navbar-fixed-top"
|
||||
id="navbar"
|
||||
role="navigation"
|
||||
>
|
||||
<div
|
||||
className="container-fluid theme"
|
||||
>
|
||||
<div
|
||||
className="navbar-header"
|
||||
>
|
||||
<Connect(CollapseLhsButton) />
|
||||
<div
|
||||
className="navbar-brand"
|
||||
>
|
||||
<Connect(Component) />
|
||||
</div>
|
||||
<div
|
||||
className="spacer"
|
||||
/>
|
||||
<Connect(NavbarInfoButton)
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(Component) />
|
||||
<Connect(injectIntl(MobileChannelHeaderPlug))
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
/>
|
||||
<Connect(CollapseRhsButton) />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for default channel 1`] = `
|
||||
<nav
|
||||
className="navbar navbar-default navbar-fixed-top"
|
||||
id="navbar"
|
||||
role="navigation"
|
||||
>
|
||||
<div
|
||||
className="container-fluid theme"
|
||||
>
|
||||
<div
|
||||
className="navbar-header"
|
||||
>
|
||||
<Connect(CollapseLhsButton) />
|
||||
<div
|
||||
className="navbar-brand"
|
||||
>
|
||||
<Connect(Component) />
|
||||
</div>
|
||||
<div
|
||||
className="spacer"
|
||||
/>
|
||||
<Connect(NavbarInfoButton)
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "Town Square",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "123",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "town-square",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(Component) />
|
||||
<Connect(injectIntl(MobileChannelHeaderPlug))
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "Town Square",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "123",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "town-square",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "O",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
/>
|
||||
<Connect(CollapseRhsButton) />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, for private channel 1`] = `
|
||||
<nav
|
||||
className="navbar navbar-default navbar-fixed-top"
|
||||
id="navbar"
|
||||
role="navigation"
|
||||
>
|
||||
<div
|
||||
className="container-fluid theme"
|
||||
>
|
||||
<div
|
||||
className="navbar-header"
|
||||
>
|
||||
<Connect(CollapseLhsButton) />
|
||||
<div
|
||||
className="navbar-brand"
|
||||
>
|
||||
<Connect(Component) />
|
||||
</div>
|
||||
<div
|
||||
className="spacer"
|
||||
/>
|
||||
<Connect(NavbarInfoButton)
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "P",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(Component) />
|
||||
<Connect(injectIntl(MobileChannelHeaderPlug))
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "DN",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "P",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
/>
|
||||
<Connect(CollapseRhsButton) />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
|
||||
exports[`components/ChannelHeaderMobile/ChannelHeaderMobile should match snapshot, if DM channel 1`] = `
|
||||
<nav
|
||||
className="navbar navbar-default navbar-fixed-top"
|
||||
id="navbar"
|
||||
role="navigation"
|
||||
>
|
||||
<div
|
||||
className="container-fluid theme"
|
||||
>
|
||||
<div
|
||||
className="navbar-header"
|
||||
>
|
||||
<Connect(CollapseLhsButton) />
|
||||
<div
|
||||
className="navbar-brand"
|
||||
>
|
||||
<Connect(Component) />
|
||||
</div>
|
||||
<div
|
||||
className="spacer"
|
||||
/>
|
||||
<Connect(NavbarInfoButton)
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "user_id_1__user_id_2",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "D",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(Component) />
|
||||
<Connect(injectIntl(MobileChannelHeaderPlug))
|
||||
channel={
|
||||
Object {
|
||||
"create_at": 0,
|
||||
"creator_id": "id",
|
||||
"delete_at": 0,
|
||||
"display_name": "display_name",
|
||||
"group_constrained": false,
|
||||
"header": "header",
|
||||
"id": "channel_id",
|
||||
"last_post_at": 0,
|
||||
"last_root_post_at": 0,
|
||||
"name": "user_id_1__user_id_2",
|
||||
"purpose": "purpose",
|
||||
"scheme_id": "id",
|
||||
"team_id": "team_id",
|
||||
"type": "D",
|
||||
"update_at": 0,
|
||||
}
|
||||
}
|
||||
isDropdown={false}
|
||||
/>
|
||||
<Connect(CollapseRhsButton) />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
`;
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ChannelHeaderMobile from './channel_header_mobile';
|
||||
@@ -14,86 +14,91 @@ describe('components/ChannelHeaderMobile/ChannelHeaderMobile', () => {
|
||||
removeEventListener: jest.fn(),
|
||||
});
|
||||
|
||||
const baseProps = {
|
||||
user: TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
}),
|
||||
channel: TestHelper.getChannelMock({
|
||||
type: 'O',
|
||||
id: 'channel_id',
|
||||
display_name: 'display_name',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
member: TestHelper.getChannelMembershipMock({
|
||||
channel_id: 'channel_id',
|
||||
user_id: 'user_id',
|
||||
}),
|
||||
teamDisplayName: 'team_display_name',
|
||||
isPinnedPosts: true,
|
||||
actions: {
|
||||
closeLhs: jest.fn(),
|
||||
closeRhs: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
},
|
||||
isLicensed: true,
|
||||
isMobileView: false,
|
||||
isFavoriteChannel: false,
|
||||
const user = TestHelper.getUserMock({
|
||||
id: 'user_id',
|
||||
});
|
||||
const channel = TestHelper.getChannelMock({
|
||||
type: 'O',
|
||||
id: 'channel_id',
|
||||
display_name: 'display_name',
|
||||
team_id: 'team_id',
|
||||
});
|
||||
const actions = {
|
||||
closeLhs: jest.fn(),
|
||||
closeRhs: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ChannelHeaderMobile {...baseProps}/>,
|
||||
);
|
||||
describe('components/ChannelHeaderMenu/MenuItem/ChannelHeaderMobile', () => {
|
||||
test('renders the component correctly', () => {
|
||||
renderWithContext(
|
||||
<div
|
||||
className='inner-wrap'
|
||||
data-testid='wrapper'
|
||||
>
|
||||
<ChannelHeaderMobile
|
||||
channel={channel}
|
||||
isMobileView={false}
|
||||
user={user}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
let menuItem = screen.getByText('Toggle sidebar');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
test('should match snapshot, for default channel', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
type: 'O',
|
||||
id: '123',
|
||||
name: 'town-square',
|
||||
display_name: 'Town Square',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<ChannelHeaderMobile {...props}/>,
|
||||
);
|
||||
menuItem = screen.getByLabelText('Info');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
menuItem = screen.getByLabelText('Search');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
test('should match snapshot, if DM channel', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
type: 'D',
|
||||
id: 'channel_id',
|
||||
name: 'user_id_1__user_id_2',
|
||||
display_name: 'display_name',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
};
|
||||
const wrapper = shallow(<ChannelHeaderMobile {...props}/>);
|
||||
menuItem = screen.getByText('Toggle right sidebar');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = screen.getByTestId('wrapper');
|
||||
expect(wrapper).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match snapshot, for private channel', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
channel: TestHelper.getChannelMock({
|
||||
type: 'P',
|
||||
id: 'channel_id',
|
||||
display_name: 'display_name',
|
||||
team_id: 'team_id',
|
||||
}),
|
||||
};
|
||||
const wrapper = shallow(<ChannelHeaderMobile {...props}/>);
|
||||
test('renders the component correctly, global threads', () => {
|
||||
renderWithContext(
|
||||
<div
|
||||
className='inner-wrap'
|
||||
data-testid='wrapper'
|
||||
>
|
||||
<ChannelHeaderMobile
|
||||
channel={channel}
|
||||
isMobileView={false}
|
||||
inGlobalThreads={true}
|
||||
user={user}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const menuItem = screen.getByText('Followed threads');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the component correctly, in drafts', () => {
|
||||
renderWithContext(
|
||||
<div
|
||||
className='inner-wrap'
|
||||
data-testid='wrapper'
|
||||
>
|
||||
<ChannelHeaderMobile
|
||||
channel={channel}
|
||||
isMobileView={false}
|
||||
inDrafts={true}
|
||||
user={user}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const menuItem = screen.getByText('Drafts');
|
||||
expect(menuItem).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,29 +8,22 @@ import {FormattedMessage} from 'react-intl';
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {MobileChannelHeaderDropdown} from 'components/channel_header_dropdown';
|
||||
|
||||
import MobileChannelHeaderPlug from 'plugins/mobile_channel_header_plug';
|
||||
|
||||
import ChannelInfoButton from './channel_info_button';
|
||||
import CollapseLhsButton from './collapse_lhs_button';
|
||||
import CollapseRhsButton from './collapse_rhs_button';
|
||||
import ShowSearchButton from './show_search_button';
|
||||
import UnmuteChannelButton from './unmute_channel_button';
|
||||
|
||||
import ChannelHeaderMenu from '../channel_header_menu/channel_header_menu';
|
||||
import MobileChannelHeaderPlugins from '../channel_header_menu/menu_items/mobile_channel_header_plugins';
|
||||
|
||||
type Props = {
|
||||
channel?: Channel;
|
||||
|
||||
/**
|
||||
* Relative url for the team, used to redirect if a link in the channel header is clicked
|
||||
*/
|
||||
currentRelativeTeamUrl?: string;
|
||||
|
||||
inGlobalThreads?: boolean;
|
||||
inDrafts?: boolean;
|
||||
isMobileView: boolean;
|
||||
isMuted?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
isRHSOpen?: boolean;
|
||||
user: UserProfile;
|
||||
actions: {
|
||||
@@ -85,7 +78,10 @@ export default class ChannelHeaderMobile extends React.PureComponent<Props> {
|
||||
} else if (channel) {
|
||||
heading = (
|
||||
<>
|
||||
<MobileChannelHeaderDropdown/>
|
||||
<ChannelHeaderMenu
|
||||
isMobile={true}
|
||||
/>
|
||||
|
||||
{isMuted && (
|
||||
<UnmuteChannelButton
|
||||
user={user}
|
||||
@@ -114,13 +110,13 @@ export default class ChannelHeaderMobile extends React.PureComponent<Props> {
|
||||
channel={channel}
|
||||
/>
|
||||
)}
|
||||
<ShowSearchButton/>
|
||||
{channel && (
|
||||
<MobileChannelHeaderPlug
|
||||
<MobileChannelHeaderPlugins
|
||||
channel={channel}
|
||||
isDropdown={false}
|
||||
/>
|
||||
)}
|
||||
<ShowSearchButton/>
|
||||
<CollapseRhsButton/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import MenuIcon from 'components/widgets/icons/menu_icon';
|
||||
|
||||
@@ -22,6 +23,12 @@ const CollapseRhsButton: React.FunctionComponent<Props> = (props: Props) => (
|
||||
data-target='#sidebar-nav'
|
||||
onClick={props.actions.toggleRhsMenu}
|
||||
>
|
||||
<span className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='navbar.toggle3'
|
||||
defaultMessage='Toggle right sidebar'
|
||||
/>
|
||||
</span>
|
||||
<MenuIcon/>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -62,7 +62,7 @@ const ConvertGmToChannelModal = (props: Props) => {
|
||||
map((user) => displayUsername(user, props.teammateNameDisplaySetting));
|
||||
|
||||
setChannelMemberNames(validProfilesInChannel);
|
||||
}, [props.profilesInChannel]);
|
||||
}, [props.profilesInChannel, props.currentUserId, props.teammateNameDisplaySetting]);
|
||||
|
||||
const [commonTeamsById, setCommonTeamsById] = useState<{[id: string]: Team}>({});
|
||||
const [commonTeamsFetched, setCommonTeamsFetched] = useState<boolean>(false);
|
||||
@@ -109,7 +109,7 @@ const ConvertGmToChannelModal = (props: Props) => {
|
||||
|
||||
work();
|
||||
setTimeout(() => setLoadingAnimationTimeout(true), 1200);
|
||||
}, []);
|
||||
}, [dispatch, props.channel.id]);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (!selectedTeamId) {
|
||||
@@ -136,7 +136,7 @@ const ConvertGmToChannelModal = (props: Props) => {
|
||||
setConversionError(undefined);
|
||||
trackEvent('actions', 'convert_group_message_to_private_channel', {channel_id: props.channel.id});
|
||||
props.onExited();
|
||||
}, [selectedTeamId, props.channel.id, channelName, channelURL.current, props.actions.moveChannelsInSidebar]);
|
||||
}, [selectedTeamId, props, channelName, formatMessage]);
|
||||
|
||||
const showLoader = !commonTeamsFetched || !loadingAnimationTimeout;
|
||||
const canCreate = selectedTeamId !== undefined && channelName !== '' && !nameError && !urlError;
|
||||
|
||||
@@ -5,6 +5,8 @@ import {connect} from 'react-redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import type {Channel} from '@mattermost/types/channels';
|
||||
|
||||
import {convertGroupMessageToPrivateChannel} from 'mattermost-redux/actions/channels';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {
|
||||
@@ -16,15 +18,17 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {moveChannelsInSidebar} from 'actions/views/channel_sidebar';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
|
||||
import type {Props} from 'components/convert_gm_to_channel_modal/convert_gm_to_channel_modal';
|
||||
import ConvertGmToChannelModal from 'components/convert_gm_to_channel_modal/convert_gm_to_channel_modal';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ConvertGmToChannelModal from './convert_gm_to_channel_modal';
|
||||
|
||||
type OwnProps = {
|
||||
channel: Channel;
|
||||
}
|
||||
function makeMapStateToProps() {
|
||||
const getProfilesInChannel = makeGetProfilesInChannel();
|
||||
|
||||
return (state: GlobalState, ownProps: Props) => {
|
||||
return (state: GlobalState, ownProps: OwnProps) => {
|
||||
const allProfilesInChannel = getProfilesInChannel(state, ownProps.channel.id);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const teammateNameDisplaySetting = getTeammateNameDisplaySetting(state);
|
||||
|
||||
@@ -495,8 +495,8 @@ exports[`components/sidebar/sidebar_channel/sidebar_channel_menu should show cor
|
||||
id="mute-channel_id"
|
||||
labels={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Mute Conversation"
|
||||
id="sidebar_left.sidebar_channel_menu.muteConversation"
|
||||
defaultMessage="Mute"
|
||||
id="sidebar_left.sidebar_channel_menu.mute"
|
||||
/>
|
||||
}
|
||||
leadingElement={
|
||||
|
||||
@@ -148,8 +148,8 @@ const SidebarChannelMenu = ({
|
||||
if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
|
||||
muteChannelText = (
|
||||
<FormattedMessage
|
||||
id='sidebar_left.sidebar_channel_menu.unmuteConversation'
|
||||
defaultMessage='Unmute Conversation'
|
||||
id='sidebar_left.sidebar_channel_menu.unmute'
|
||||
defaultMessage='Unmute'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -176,8 +176,8 @@ const SidebarChannelMenu = ({
|
||||
if (channel.type === Constants.DM_CHANNEL || channel.type === Constants.GM_CHANNEL) {
|
||||
muteChannelText = (
|
||||
<FormattedMessage
|
||||
id='sidebar_left.sidebar_channel_menu.muteConversation'
|
||||
defaultMessage='Mute Conversation'
|
||||
id='sidebar_left.sidebar_channel_menu.mute'
|
||||
defaultMessage='Mute'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3230,22 +3230,22 @@
|
||||
"channel_header.lastActive": "Last online {timestamp}",
|
||||
"channel_header.lastOnline": "Last online {timestamp}",
|
||||
"channel_header.leave": "Leave Channel",
|
||||
"channel_header.manageMembers": "Manage Members",
|
||||
"channel_header.menuAriaLabel": "Channel Menu",
|
||||
"channel_header.members": "Members",
|
||||
"channel_header.mute": "Mute Channel",
|
||||
"channel_header.muteConversation": "Mute Conversation",
|
||||
"channel_header.muteConversation": "Mute",
|
||||
"channel_header.openChannelInfo": "View Info",
|
||||
"channel_header.otherchannel": "{displayName} Channel Menu",
|
||||
"channel_header.pinnedPosts": "Pinned messages",
|
||||
"channel_header.recentMentions": "Recent mentions",
|
||||
"channel_header.rename": "Rename Channel",
|
||||
"channel_header.search": "Search",
|
||||
"channel_header.setConversationHeader": "Edit Conversation Header",
|
||||
"channel_header.setConversationHeader": "Edit Header",
|
||||
"channel_header.setHeader": "Edit Channel Header",
|
||||
"channel_header.setPurpose": "Edit Channel Purpose",
|
||||
"channel_header.settings": "Settings",
|
||||
"channel_header.unarchive": "Unarchive Channel",
|
||||
"channel_header.unmute": "Unmute Channel",
|
||||
"channel_header.unmuteConversation": "Unmute Conversation",
|
||||
"channel_header.unmuteConversation": "Unmute",
|
||||
"channel_header.userHelpGuide": "Help",
|
||||
"channel_header.viewMembers": "View Members",
|
||||
"channel_info_rhs.about_area_id": "ID:",
|
||||
@@ -3330,6 +3330,9 @@
|
||||
"channel_members_rhs.member.send_message": "Send message",
|
||||
"channel_members_rhs.search_bar.aria.cancel_search_button": "cancel members search",
|
||||
"channel_members_rhs.search_bar.placeholder": "Search members",
|
||||
"channel_menu.bookmarks": "Bookmarks Bar",
|
||||
"channel_menu.bookmarks.addFile": "Attach a file",
|
||||
"channel_menu.bookmarks.addLink": "Add a link",
|
||||
"channel_modal.alreadyExist": "A channel with that URL already exists",
|
||||
"channel_modal.cancel": "Cancel",
|
||||
"channel_modal.create_board.tooltip_description": "Use any of our templates to manage your tasks or start from scratch with your own!",
|
||||
@@ -3390,6 +3393,7 @@
|
||||
"channelNotifications.mobileNotification.newMessages": "All new messages {optionalDefault}",
|
||||
"channelNotifications.mobileNotification.nothing": "Nothing {optionalDefault}",
|
||||
"channelSelectorModal.title": "Add Channels to <b>Channel Selection</b> List",
|
||||
"channelSettings": "Channel Settings",
|
||||
"channelView.archivedChannel": "You are viewing an <b>archived channel</b>. New messages cannot be posted.",
|
||||
"channelView.archivedChannelWithDeactivatedUser": "You are viewing an archived channel with a <b>deactivated user</b>. New messages cannot be posted.",
|
||||
"channelView.login.successfull": "Login Successful",
|
||||
@@ -4499,6 +4503,7 @@
|
||||
"navbar.addMembers": "Add Members",
|
||||
"navbar.preferences": "Notification Preferences",
|
||||
"navbar.toggle2": "Toggle sidebar",
|
||||
"navbar.toggle3": "Toggle right sidebar",
|
||||
"navbar.viewPinnedPosts": "View Pinned Posts",
|
||||
"newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.",
|
||||
"newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar",
|
||||
@@ -4662,6 +4667,7 @@
|
||||
"plan.self_serve": "Self-serve",
|
||||
"pluggable.errorOccurred": "An error occurred in the {pluginId} plugin.",
|
||||
"pluggable.errorRefresh": "Refresh?",
|
||||
"pluginsMenu.more_actions": "More actions",
|
||||
"post_body.check_for_out_of_channel_groups_mentions.message": "did not get notified by this mention because they are not in the channel. They cannot be added to the channel because they are not a member of the linked groups. To add them to this channel, they must be added to the linked groups.",
|
||||
"post_body.check_for_out_of_channel_mentions.link.and": " and ",
|
||||
"post_body.check_for_out_of_channel_mentions.link.private": "add them to this private channel",
|
||||
@@ -5183,7 +5189,6 @@
|
||||
"sidebar_left.sidebar_category.newLabel": "new",
|
||||
"sidebar_left.sidebar_channel_menu_convert_to_channel": "Convert to Private Channel",
|
||||
"sidebar_left.sidebar_channel_menu.addMembers": "Add Members",
|
||||
"sidebar_left.sidebar_channel_menu.bookmarks": "Bookmarks Bar",
|
||||
"sidebar_left.sidebar_channel_menu.channels": "Channels",
|
||||
"sidebar_left.sidebar_channel_menu.copyLink": "Copy Link",
|
||||
"sidebar_left.sidebar_channel_menu.dropdownAriaLabel": "Edit channel menu",
|
||||
@@ -5198,11 +5203,11 @@
|
||||
"sidebar_left.sidebar_channel_menu.moveTo": "Move to...",
|
||||
"sidebar_left.sidebar_channel_menu.moveTo.dropdownAriaLabel": "Move to submenu",
|
||||
"sidebar_left.sidebar_channel_menu.moveToNewCategory": "New Category",
|
||||
"sidebar_left.sidebar_channel_menu.mute": "Mute",
|
||||
"sidebar_left.sidebar_channel_menu.muteChannel": "Mute Channel",
|
||||
"sidebar_left.sidebar_channel_menu.muteConversation": "Mute Conversation",
|
||||
"sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Unfavorite",
|
||||
"sidebar_left.sidebar_channel_menu.unmute": "Unmute",
|
||||
"sidebar_left.sidebar_channel_menu.unmuteChannel": "Unmute Channel",
|
||||
"sidebar_left.sidebar_channel_menu.unmuteConversation": "Unmute Conversation",
|
||||
"sidebar_left.sidebar_channel_modal.channel_name_placeholder": "Enter a name for the channel",
|
||||
"sidebar_left.sidebar_channel_modal.confirmation_text": "Convert to private channel",
|
||||
"sidebar_left.sidebar_channel_modal.header": "Convert to Private Channel",
|
||||
|
||||
@@ -1234,6 +1234,11 @@ export const getMyFirstChannelForTeams: (state: GlobalState) => RelationOneToOne
|
||||
},
|
||||
);
|
||||
|
||||
export const getRedirectChannelNameForCurrentTeam = (state: GlobalState): string => {
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
return getRedirectChannelNameForTeam(state, currentTeamId);
|
||||
};
|
||||
|
||||
export const getRedirectChannelNameForTeam = (state: GlobalState, teamId: string): string => {
|
||||
const defaultChannelForTeam = getDefaultChannelForTeams(state)[teamId];
|
||||
const canIJoinPublicChannelsInTeam = haveITeamPermission(state,
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,42 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
|
||||
import {appsEnabled, makeAppBindingsSelector} from 'mattermost-redux/selectors/entities/apps';
|
||||
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {handleBindingClick, openAppsModal, postEphemeralCallResponseForChannel} from 'actions/apps';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MobileChannelHeaderPlug from './mobile_channel_header_plug';
|
||||
|
||||
const getChannelHeaderBindings = makeAppBindingsSelector(AppBindingLocations.CHANNEL_HEADER_ICON);
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const apps = appsEnabled(state);
|
||||
return {
|
||||
appBindings: getChannelHeaderBindings(state),
|
||||
appsEnabled: apps,
|
||||
channelMember: getMyCurrentChannelMembership(state),
|
||||
components: state.plugins.components.MobileChannelHeaderButton,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel,
|
||||
openAppsModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeaderPlug);
|
||||
@@ -1,473 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {mount} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import MobileChannelHeaderPlug, {RawMobileChannelHeaderPlug} from 'plugins/mobile_channel_header_plug/mobile_channel_header_plug';
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {createCallContext} from 'utils/apps';
|
||||
|
||||
describe('plugins/MobileChannelHeaderPlug', () => {
|
||||
const testPlug = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some dropdown text',
|
||||
};
|
||||
|
||||
const testBinding = {
|
||||
app_id: 'appid',
|
||||
location: 'test',
|
||||
icon: 'http://test.com/icon.png',
|
||||
label: 'Label',
|
||||
hint: 'Hint',
|
||||
form: {
|
||||
submit: {
|
||||
path: '/call/path',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const testChannel = {} as Channel;
|
||||
const testChannelMember = {} as ChannelMembership;
|
||||
const testTheme = {} as Theme;
|
||||
const intl = {
|
||||
formatMessage: (message: {id: string; defaultMessage: string}) => {
|
||||
return message.defaultMessage;
|
||||
},
|
||||
} as any;
|
||||
|
||||
test('should match snapshot with no extended component', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing a button
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('button')).toHaveLength(1);
|
||||
|
||||
wrapper.instance().fireAction = jest.fn();
|
||||
wrapper.find('button').first().simulate('click');
|
||||
expect(wrapper.instance().fireAction).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().fireAction).toBeCalledWith(testPlug);
|
||||
});
|
||||
|
||||
test('should match snapshot with two extended components', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug, {...testPlug, id: 'someid2'}]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with no bindings', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one binding', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing a button
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('button')).toHaveLength(1);
|
||||
|
||||
wrapper.instance().fireAppAction = jest.fn();
|
||||
wrapper.find('button').first().simulate('click');
|
||||
expect(wrapper.instance().fireAppAction).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().fireAppAction).toBeCalledWith(testBinding);
|
||||
});
|
||||
|
||||
test('should match snapshot with two bindings', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={false}
|
||||
appBindings={[testBinding, {...testBinding, app_id: 'app2'}]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended components and one binding', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={false}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with no extended component, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing an anchor
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('a')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot with two extended components, in dropdown', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[testPlug, {...testPlug, id: 'someid2'}]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.fireAction = jest.fn();
|
||||
|
||||
wrapper.find('a').first().simulate('click');
|
||||
expect(instance.fireAction).toHaveBeenCalledTimes(1);
|
||||
expect(instance.fireAction).toBeCalledWith(testPlug);
|
||||
});
|
||||
|
||||
test('should match snapshot with no binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render nothing
|
||||
expect(wrapper.find('li').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('should match snapshot with one binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a single list item containing an anchor
|
||||
expect(wrapper.find('li')).toHaveLength(1);
|
||||
expect(wrapper.find('a')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot with two bindings, in dropdown', () => {
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding, {...testBinding, app_id: 'app2'}]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
instance.fireAppAction = jest.fn();
|
||||
|
||||
wrapper.find('a').first().simulate('click');
|
||||
expect(instance.fireAppAction).toHaveBeenCalledTimes(1);
|
||||
expect(instance.fireAppAction).toBeCalledWith(testBinding);
|
||||
});
|
||||
|
||||
test('should match snapshot with one extended component and one binding, in dropdown', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MobileChannelHeaderPlug
|
||||
components={[testPlug]}
|
||||
channel={testChannel}
|
||||
channelMember={testChannelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
// Render a two list items containing anchors
|
||||
expect(wrapper.find('li')).toHaveLength(2);
|
||||
expect(wrapper.find('a')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should call plugin.action on fireAction', () => {
|
||||
const channel = {id: 'channel_id'} as Channel;
|
||||
const channelMember = {} as ChannelMembership;
|
||||
const newTestPlug = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
icon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
dropdownText: 'some dropdown text',
|
||||
};
|
||||
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[newTestPlug]}
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={false}
|
||||
appBindings={[]}
|
||||
actions={{
|
||||
handleBindingClick: jest.fn(),
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.instance().fireAction(newTestPlug);
|
||||
expect(newTestPlug.action).toHaveBeenCalledTimes(1);
|
||||
expect(newTestPlug.action).toBeCalledWith(channel, channelMember);
|
||||
});
|
||||
|
||||
test('should call handleBindingClick on fireAppAction', () => {
|
||||
const channel = {id: 'channel_id'} as Channel;
|
||||
const channelMember = {} as ChannelMembership;
|
||||
|
||||
const handleBindingClick = jest.fn().mockResolvedValue({data: {type: AppCallResponseTypes.OK}});
|
||||
|
||||
const wrapper = mount<RawMobileChannelHeaderPlug>(
|
||||
<RawMobileChannelHeaderPlug
|
||||
components={[]}
|
||||
channel={channel}
|
||||
channelMember={channelMember}
|
||||
theme={testTheme}
|
||||
isDropdown={true}
|
||||
appsEnabled={true}
|
||||
appBindings={[testBinding]}
|
||||
actions={{
|
||||
handleBindingClick,
|
||||
postEphemeralCallResponseForChannel: jest.fn(),
|
||||
openAppsModal: jest.fn(),
|
||||
}}
|
||||
intl={intl}
|
||||
/>,
|
||||
);
|
||||
|
||||
const context = createCallContext(
|
||||
testBinding.app_id,
|
||||
testBinding.location,
|
||||
channel.id,
|
||||
channel.team_id,
|
||||
);
|
||||
|
||||
wrapper.instance().fireAppAction(testBinding);
|
||||
expect(handleBindingClick).toHaveBeenCalledTimes(1);
|
||||
expect(handleBindingClick).toBeCalledWith(testBinding, context, expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {injectIntl} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import type {AppBinding} from '@mattermost/types/apps';
|
||||
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||
|
||||
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import type {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {createCallContext} from 'utils/apps';
|
||||
|
||||
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForChannel} from 'types/apps';
|
||||
import type {MobileChannelHeaderButtonAction} from 'types/store/plugins';
|
||||
|
||||
type Props = {
|
||||
|
||||
/*
|
||||
* Components or actions to add as channel header buttons
|
||||
*/
|
||||
components?: MobileChannelHeaderButtonAction[];
|
||||
|
||||
/*
|
||||
* Set to true if the plug is in the dropdown
|
||||
*/
|
||||
isDropdown: boolean;
|
||||
channel: Channel;
|
||||
channelMember?: ChannelMembership;
|
||||
|
||||
/*
|
||||
* Logged in user's theme
|
||||
*/
|
||||
theme: Theme;
|
||||
appBindings: AppBinding[];
|
||||
appsEnabled: boolean;
|
||||
intl: IntlShape;
|
||||
actions: {
|
||||
handleBindingClick: HandleBindingClick;
|
||||
postEphemeralCallResponseForChannel: PostEphemeralCallResponseForChannel;
|
||||
openAppsModal: OpenAppsModal;
|
||||
};
|
||||
}
|
||||
|
||||
class MobileChannelHeaderPlug extends React.PureComponent<Props> {
|
||||
createAppButton = (binding: AppBinding) => {
|
||||
const onClick = () => this.fireAppAction(binding);
|
||||
|
||||
if (this.props.isDropdown) {
|
||||
return (
|
||||
<li
|
||||
key={'mobileChannelHeaderItem' + binding.app_id + binding.location}
|
||||
role='presentation'
|
||||
className='MenuItem'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={onClick}
|
||||
>
|
||||
{binding.label}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
id={`${binding.app_id}_${binding.location}`}
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
<img
|
||||
src={binding.icon}
|
||||
width='16'
|
||||
height='16'
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
createButton = (plug: MobileChannelHeaderButtonAction) => {
|
||||
const onClick = () => this.fireAction(plug);
|
||||
|
||||
if (this.props.isDropdown) {
|
||||
return (
|
||||
<li
|
||||
key={'mobileChannelHeaderItem' + plug.id}
|
||||
role='presentation'
|
||||
className='MenuItem'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={onClick}
|
||||
>
|
||||
{plug.dropdownText}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className='flex-parent--center'>
|
||||
<button
|
||||
className='navbar-toggle navbar-right__icon'
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className='icon navbar-plugin-button'>
|
||||
{plug.icon}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
createList(plugs: MobileChannelHeaderButtonAction[]) {
|
||||
return plugs.map(this.createButton);
|
||||
}
|
||||
|
||||
createAppList(bindings: AppBinding[]) {
|
||||
return bindings.map(this.createAppButton);
|
||||
}
|
||||
|
||||
fireAction(plug: MobileChannelHeaderButtonAction) {
|
||||
return plug.action?.(this.props.channel, this.props.channelMember);
|
||||
}
|
||||
|
||||
fireAppAction = async (binding: AppBinding) => {
|
||||
const {channel, intl} = this.props;
|
||||
const context = createCallContext(
|
||||
binding.app_id,
|
||||
binding.location,
|
||||
channel.id,
|
||||
channel.team_id,
|
||||
);
|
||||
|
||||
const res = await this.props.actions.handleBindingClick(binding, context, intl);
|
||||
|
||||
if (res.error) {
|
||||
const errorResponse = res.error;
|
||||
const errorMessage = errorResponse.text || intl.formatMessage({
|
||||
id: 'apps.error.unknown',
|
||||
defaultMessage: 'Unknown error occurred.',
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(errorResponse, errorMessage, channel.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const callResp = res.data!;
|
||||
switch (callResp.type) {
|
||||
case AppCallResponseTypes.OK:
|
||||
if (callResp.text) {
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, callResp.text, channel.id);
|
||||
}
|
||||
break;
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
break;
|
||||
case AppCallResponseTypes.FORM:
|
||||
if (callResp.form) {
|
||||
this.props.actions.openAppsModal(callResp.form, context);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
const errorMessage = this.props.intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
defaultMessage: 'App response type not supported. Response type: {type}.',
|
||||
}, {
|
||||
type: callResp.type,
|
||||
});
|
||||
this.props.actions.postEphemeralCallResponseForChannel(callResp, errorMessage, channel.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const components = this.props.components || [];
|
||||
const bindings = this.props.appBindings || [];
|
||||
|
||||
if (components.length === 0 && bindings.length === 0) {
|
||||
return null;
|
||||
} else if (components.length === 1 && bindings.length === 0) {
|
||||
return this.createButton(components[0]);
|
||||
} else if (components.length === 0 && bindings.length === 1) {
|
||||
return this.createAppButton(bindings[0]);
|
||||
}
|
||||
|
||||
if (!this.props.isDropdown) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const plugItems = this.createList(components);
|
||||
const appItems = this.createAppList(bindings);
|
||||
return (<>
|
||||
{plugItems}
|
||||
{appItems}
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
export {MobileChannelHeaderPlug as RawMobileChannelHeaderPlug};
|
||||
|
||||
export default injectIntl(MobileChannelHeaderPlug);
|
||||
@@ -35,100 +35,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.Menu {
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2.5em 0 0;
|
||||
padding-bottom: 50px;
|
||||
background: functions.alpha-color(variables.$black, 0.9);
|
||||
transition: all 0.35s ease;
|
||||
|
||||
body.announcement-bar--fixed & {
|
||||
top: calc(48px + #{variables.$announcement-bar-height});
|
||||
height: calc(100% - 48px - #{variables.$announcement-bar-height});
|
||||
}
|
||||
}
|
||||
|
||||
.Menu__content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
padding: 2rem 2rem 5rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.Menu__close {
|
||||
position: fixed;
|
||||
top: 70px;
|
||||
right: 20px;
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding-top: 13px;
|
||||
border: 1px solid variables.$white;
|
||||
border-radius: 50%;
|
||||
margin-left: -15px;
|
||||
color: variables.$white;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 23px;
|
||||
font-weight: 200;
|
||||
line-height: 0;
|
||||
opacity: 1;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown__divider {
|
||||
& + .dropdown__divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
>li {
|
||||
>a,
|
||||
>button,
|
||||
>.styleSelectableItemDiv {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
padding: 0.4rem 2rem;
|
||||
margin: 0 auto;
|
||||
color: variables.$white;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.SubMenu__icon-right {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-right__icon {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-bottom: 1px solid functions.alpha-color(variables.$white, 0.3);
|
||||
margin: 8px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
top: 2px;
|
||||
width: 16px;
|
||||
@@ -559,24 +465,6 @@
|
||||
&.dropdown-menu {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&.mobile-channel-header-dropdown-enter {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
&.mobile-channel-header-dropdown-enter-active {
|
||||
transform: translateY(0%);
|
||||
transition: transform 0.35s ease-in;
|
||||
}
|
||||
|
||||
&.mobile-channel-header-dropdown-enter-done {
|
||||
transform: translateY(0%);
|
||||
}
|
||||
|
||||
&.mobile-channel-header-dropdown-exit {
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.35s ease-in;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2175,24 +2063,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) and (max-height: 580px) {
|
||||
#navbar_wrapper {
|
||||
.navbar-default {
|
||||
.dropdown-menu {
|
||||
padding-top: 1em;
|
||||
|
||||
>li {
|
||||
>a {
|
||||
border: none;
|
||||
font-size: 13px;
|
||||
line-height: 27px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// on iOS, allow clicks within an input's label to actually propagate through to the input itself,
|
||||
// but still allow clicks to a elements to go trough
|
||||
// http://stackoverflow.com/a/34810294/6325807
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getChannelHeaderMenuPluginComponents, getPluginUserSettings} from 'selectors/plugins';
|
||||
import {getChannelHeaderMenuPluginComponents, getPluginUserSettings, getChannelMobileHeaderPluginButtons} from 'selectors/plugins';
|
||||
|
||||
describe('selectors/plugins', () => {
|
||||
describe('getPluginUserSettings', () => {
|
||||
@@ -207,4 +207,34 @@ describe('selectors/plugins', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getChannelMobileHeaderPluginButtons', () => {
|
||||
it('has no settings', () => {
|
||||
const state = {
|
||||
plugins: {
|
||||
components: {
|
||||
MobileChannelHeaderButton: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const settings = getChannelMobileHeaderPluginButtons(state);
|
||||
expect(settings).toEqual([]);
|
||||
});
|
||||
it('has settings', () => {
|
||||
const headerButton = {
|
||||
id: 'someid',
|
||||
pluginId: 'pluginid',
|
||||
dropdownText: 'some dropdown text',
|
||||
};
|
||||
const state = {
|
||||
plugins: {
|
||||
components: {
|
||||
MobileChannelHeaderButton: [headerButton],
|
||||
},
|
||||
},
|
||||
};
|
||||
const settings = getChannelMobileHeaderPluginButtons(state);
|
||||
expect(settings).toEqual([headerButton]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +74,14 @@ export const getChannelHeaderMenuPluginComponents = createShallowSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const getChannelMobileHeaderPluginButtons = createSelector(
|
||||
'getChannelMobileHeaderPluginButtons',
|
||||
(state: GlobalState) => state.plugins.components.MobileChannelHeaderButton,
|
||||
(components = []) => {
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const getChannelIntroPluginButtons = createSelector(
|
||||
'getChannelIntroPluginButtons',
|
||||
(state: GlobalState) => state.plugins.components.ChannelIntroButton,
|
||||
|
||||
Ссылка в новой задаче
Block a user