[MM-55277] Team's menu doesn't follow standard accessible keyboard behavior (#29917)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b59f10cbbd
Коммит
502506a517
@@ -12,7 +12,6 @@
|
||||
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
||||
"@mattermost/client": "*",
|
||||
"@mattermost/compass-components": "^0.2.12",
|
||||
"@mattermost/compass-icons": "0.1.39",
|
||||
"@mattermost/desktop-api": "5.10.0-2",
|
||||
"@mattermost/types": "*",
|
||||
"@mui/base": "5.0.0-alpha.127",
|
||||
|
||||
@@ -17,6 +17,7 @@ import MultiSelect from 'components/multiselect/multiselect';
|
||||
import type {Value} from 'components/multiselect/multiselect';
|
||||
|
||||
import groupsAvatar from 'images/groups-avatar.png';
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
const GROUPS_PER_PAGE = 50;
|
||||
@@ -30,6 +31,7 @@ type Props = {
|
||||
intl: IntlShape;
|
||||
searchTerm: string;
|
||||
groups: Group[];
|
||||
focusOriginElement?: string;
|
||||
|
||||
// used in tandem with 'skipCommit' to allow using this component without performing actual linking
|
||||
excludeGroups?: Group[];
|
||||
@@ -114,6 +116,9 @@ export class AddGroupsToTeamModal extends React.PureComponent<Props, State> {
|
||||
|
||||
// public for tests
|
||||
public handleExit = (): void => {
|
||||
if (this.props.focusOriginElement) {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DateTime} from 'luxon';
|
||||
import React from 'react';
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
@@ -22,12 +23,6 @@ function getBaseState(): DeepPartial<GlobalState> {
|
||||
type: 'D',
|
||||
name: 'current_user_id__teammate_user_id',
|
||||
},
|
||||
dm_near_timezone: {
|
||||
id: 'dm_same_timezone',
|
||||
teammate_id: 'teammate_near_timezone_id',
|
||||
type: 'D',
|
||||
name: 'current_user_id__teammate_near_timezone_id',
|
||||
},
|
||||
bot_dm_channel_id: {
|
||||
id: 'bot_dm_channel_id',
|
||||
teammate_id: 'bot_user_id',
|
||||
@@ -57,18 +52,6 @@ function getBaseState(): DeepPartial<GlobalState> {
|
||||
manualTimezone: '',
|
||||
},
|
||||
},
|
||||
teammate_near_timezone_id: {
|
||||
id: 'teammate_near_timezone_id',
|
||||
username: 'teammate_near_timezone_username',
|
||||
nickname: 'teammate_near_timezone_nickname',
|
||||
first_name: 'teammate_near_timezone_first_name',
|
||||
last_name: 'teammate_near_timezone_last_name',
|
||||
timezone: {
|
||||
useAutomaticTimezone: 'false',
|
||||
automaticTimezone: '',
|
||||
manualTimezone: 'CET',
|
||||
},
|
||||
},
|
||||
bot_user_id: {
|
||||
id: 'bot_user_id',
|
||||
username: 'bot_username',
|
||||
@@ -109,12 +92,13 @@ function getBaseState(): DeepPartial<GlobalState> {
|
||||
}
|
||||
|
||||
describe('useTimePostBoxIndicator', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2021-01-01T18:00:00Z').getTime());
|
||||
});
|
||||
|
||||
it('should pass base case', () => {
|
||||
const fakeLocal = DateTime.local(2025, 1, 1, 3, {
|
||||
zone: 'Asia/Kolkata',
|
||||
});
|
||||
|
||||
DateTime.local = jest.fn(() => fakeLocal);
|
||||
|
||||
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('dm_channel_id'), getBaseState());
|
||||
|
||||
expect(current.isDM).toBe(true);
|
||||
@@ -127,20 +111,12 @@ describe('useTimePostBoxIndicator', () => {
|
||||
expect(current.teammateTimezone.automaticTimezone).toBe('IST');
|
||||
});
|
||||
|
||||
it('should not show if within working hours', () => {
|
||||
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('dm_near_timezone'), getBaseState());
|
||||
|
||||
expect(current.isDM).toBe(true);
|
||||
expect(current.showDndWarning).toBe(false);
|
||||
expect(current.isSelfDM).toBe(false);
|
||||
expect(current.isBot).toBe(false);
|
||||
expect(current.showRemoteUserHour).toBe(false);
|
||||
expect(current.isScheduledPostEnabled).toBe(true);
|
||||
expect(current.teammateTimezone.useAutomaticTimezone).toBe(false);
|
||||
expect(current.teammateTimezone.manualTimezone).toBe('CET');
|
||||
});
|
||||
|
||||
it('should work for DM with bots', () => {
|
||||
const fakeLocal = DateTime.local(2025, 1, 1, 3, {
|
||||
zone: 'Asia/Kolkata',
|
||||
});
|
||||
|
||||
DateTime.local = jest.fn(() => fakeLocal);
|
||||
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('bot_dm_channel_id'), getBaseState());
|
||||
|
||||
expect(current.isDM).toBe(true);
|
||||
@@ -155,6 +131,12 @@ describe('useTimePostBoxIndicator', () => {
|
||||
});
|
||||
|
||||
it('should handle teammate not loaded', () => {
|
||||
const fakeLocal = DateTime.local(2025, 1, 1, 1, {
|
||||
zone: 'Asia/Kolkata',
|
||||
});
|
||||
|
||||
DateTime.local = jest.fn(() => fakeLocal);
|
||||
|
||||
const {result: {current}} = renderHookWithContext(() => useTimePostBoxIndicator('unknown_dm_channel_id'), getBaseState());
|
||||
|
||||
expect(current.isDM).toBe(true);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {IDENTIFIER_PATH_PATTERN, ID_PATH_PATTERN, TEAM_NAME_PATH_PATTERN} from '
|
||||
|
||||
import type {OwnProps, PropsFromRedux} from './index';
|
||||
|
||||
const ChannelHeaderMobile = makeAsyncComponent('ChannelHeaderMobile', lazy(() => import('components/channel_header_mobile')));
|
||||
const MobileChannelHeader = makeAsyncComponent('MobileChannelHeader', lazy(() => import('components/mobile_channel_header')));
|
||||
const GlobalThreads = makeAsyncComponent('GlobalThreads', lazy(() => import('components/threading/global_threads')),
|
||||
(
|
||||
<div className='app__content'>
|
||||
@@ -76,15 +76,7 @@ export default class CenterChannel extends React.PureComponent<Props, State> {
|
||||
'move--left-small': this.props.rhsMenuOpen,
|
||||
})}
|
||||
>
|
||||
{isMobileView && (
|
||||
<>
|
||||
<div className='row header'>
|
||||
<div id='navbar_wrapper'>
|
||||
<ChannelHeaderMobile/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isMobileView && <MobileChannelHeader/>}
|
||||
<div className='row main'>
|
||||
<Switch>
|
||||
<Route
|
||||
|
||||
@@ -34,6 +34,10 @@ jest.mock('mattermost-redux/selectors/entities/general', () => ({
|
||||
...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'),
|
||||
}));
|
||||
|
||||
jest.mock('selectors/views/browser', () => ({
|
||||
getIsMobileView: () => false,
|
||||
}));
|
||||
|
||||
describe('ChannelController', () => {
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {cleanUpStatusAndProfileFetchingPoll} from 'mattermost-redux/actions/stat
|
||||
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
|
||||
|
||||
import {addVisibleUsersInCurrentChannelAndSelfToStatusPoll} from 'actions/status_actions';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
import CenterChannel from 'components/channel_layout/center_channel';
|
||||
@@ -24,6 +25,7 @@ import {isInternetExplorer, isEdge} from 'utils/user_agent';
|
||||
|
||||
const ProductNoticesModal = makeAsyncComponent('ProductNoticesModal', lazy(() => import('components/product_notices_modal')));
|
||||
const ResetStatusModal = makeAsyncComponent('ResetStatusModal', lazy(() => import('components/reset_status_modal')));
|
||||
const MobileSidebarRight = makeAsyncComponent('MobileSidebarRight', lazy(() => import('components/mobile_sidebar_right')));
|
||||
|
||||
const BODY_CLASS_FOR_CHANNEL = ['app__body', 'channel-view'];
|
||||
|
||||
@@ -32,6 +34,7 @@ type Props = {
|
||||
}
|
||||
|
||||
export default function ChannelController(props: Props) {
|
||||
const isMobileView = useSelector(getIsMobileView);
|
||||
const enabledUserStatuses = useSelector(getIsUserStatusesConfigEnabled);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -85,6 +88,7 @@ export default function ChannelController(props: Props) {
|
||||
<ResetStatusModal/>
|
||||
</div>
|
||||
</div>
|
||||
{isMobileView && <MobileSidebarRight/>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export type Props = {
|
||||
initialValue?: string;
|
||||
inviteAsGuest?: boolean;
|
||||
roleForTrackFlow: {started_by_role: string};
|
||||
focusOriginElement: string;
|
||||
focusOriginElement?: string;
|
||||
}
|
||||
|
||||
export const View = {
|
||||
@@ -132,7 +132,9 @@ export default class InvitationModal extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
handleExit = () => {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
if (this.props.focusOriginElement) {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}
|
||||
this.props.onExited?.();
|
||||
};
|
||||
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,40 +0,0 @@
|
||||
.LearnAboutTeamsLink {
|
||||
>span {
|
||||
width: 100%;
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 18px;
|
||||
column-gap: 10px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
>i {
|
||||
display: inline-flex !important;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: hsla(221, 77%, 48%, 0.08);
|
||||
font-size: 14px !important;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// 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 ExternalLink from 'components/external_link';
|
||||
import './learn_about_teams_link.scss';
|
||||
|
||||
const LearnAboutTeamsLink = () => {
|
||||
return (
|
||||
<div className='LearnAboutTeamsLink'>
|
||||
<FormattedMessage
|
||||
id='learn_about_teams'
|
||||
defaultMessage='<a>Learn about teams</a>'
|
||||
values={{
|
||||
a: (chunks) => (
|
||||
<ExternalLink
|
||||
location='learn_about_teams'
|
||||
href='https://mattermost.com/pl/mattermost-academy-team-training'
|
||||
>
|
||||
<i className='icon icon-lightbulb-outline'/>
|
||||
<span>{chunks}</span>
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default LearnAboutTeamsLink;
|
||||
@@ -1,13 +0,0 @@
|
||||
.MainMenu_dropdown-link {
|
||||
a {
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: rgba(63, 67, 80, 0.1);
|
||||
}
|
||||
|
||||
i + span:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import type {ComponentProps} from 'react';
|
||||
import React from 'react';
|
||||
import {createIntl} from 'react-intl';
|
||||
import {Provider} from 'react-redux';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
import mockStore from 'tests/test_store';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import {MainMenu} from './main_menu';
|
||||
import type {Props} from './main_menu';
|
||||
|
||||
describe('components/Menu', () => {
|
||||
// Neccessary for components enhanced by HOCs due to issue with enzyme.
|
||||
// See https://github.com/enzymejs/enzyme/issues/539
|
||||
const getMainMenuWrapper = (props: Props) => {
|
||||
return shallow(<MainMenu {...props}/>);
|
||||
|
||||
// const wrapper = shallowWithIntl(<MainMenu {...props}/>);
|
||||
// return wrapper.find('MainMenu').shallow();
|
||||
};
|
||||
|
||||
const defaultProps: ComponentProps<typeof MainMenu> = {
|
||||
mobile: false,
|
||||
teamId: 'team-id',
|
||||
teamName: 'team_name',
|
||||
currentUser: TestHelper.getUserMock(),
|
||||
appDownloadLink: undefined,
|
||||
enableCommands: false,
|
||||
enableIncomingWebhooks: false,
|
||||
enableOAuthServiceProvider: false,
|
||||
enableOutgoingWebhooks: false,
|
||||
canManageSystemBots: false,
|
||||
canManageIntegrations: true,
|
||||
experimentalPrimaryTeam: undefined,
|
||||
helpLink: undefined,
|
||||
reportAProblemLink: undefined,
|
||||
moreTeamsToJoin: false,
|
||||
pluginMenuItems: [],
|
||||
isMentionSearch: false,
|
||||
intl: createIntl({locale: 'en', defaultLocale: 'en', timeZone: 'Etc/UTC', textComponent: 'span'}),
|
||||
guestAccessEnabled: true,
|
||||
canInviteTeamMember: true,
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
showMentions: jest.fn(),
|
||||
showFlaggedPosts: jest.fn(),
|
||||
closeRightHandSide: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
},
|
||||
teamIsGroupConstrained: false,
|
||||
isCloud: false,
|
||||
isStarterFree: false,
|
||||
isFreeTrial: false,
|
||||
usageDeltaTeams: 1,
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
entities: {
|
||||
channels: {
|
||||
myMembers: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
license: {
|
||||
Cloud: 'false',
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
myMembers: {
|
||||
'team-id': {
|
||||
team_id: 'team-id',
|
||||
user_id: 'test-user-id',
|
||||
roles: 'team_user',
|
||||
scheme_user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'test-user-id',
|
||||
profiles: {
|
||||
'test-user-id': {
|
||||
id: 'test-user-id',
|
||||
roles: 'system_user system_manager',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_manager: {
|
||||
permissions: [
|
||||
Permissions.SYSCONSOLE_WRITE_PLUGINS,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot with id', () => {
|
||||
const props = {...defaultProps, id: 'test-id'};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with most of the thing disabled', () => {
|
||||
const wrapper = getMainMenuWrapper(defaultProps);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with most of the thing disabled in mobile', () => {
|
||||
const props = {...defaultProps, mobile: true};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with most of the thing enabled', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
appDownloadLink: 'test',
|
||||
enableCommands: true,
|
||||
enableCustomEmoji: true,
|
||||
canCreateOrDeleteCustomEmoji: true,
|
||||
enableIncomingWebhooks: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
enableOutgoingWebhooks: true,
|
||||
enableUserCreation: true,
|
||||
enableEmailInvitations: true,
|
||||
enablePluginMarketplace: true,
|
||||
experimentalPrimaryTeam: 'test',
|
||||
helpLink: 'test-link-help',
|
||||
reportAProblemLink: 'test-report-link',
|
||||
moreTeamsToJoin: true,
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with most of the thing enabled in mobile', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
mobile: true,
|
||||
appDownloadLink: 'test',
|
||||
enableCommands: true,
|
||||
enableCustomEmoji: true,
|
||||
canCreateOrDeleteCustomEmoji: true,
|
||||
enableIncomingWebhooks: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
enableOutgoingWebhooks: true,
|
||||
enableUserCreation: true,
|
||||
enableEmailInvitations: true,
|
||||
enablePluginMarketplace: true,
|
||||
experimentalPrimaryTeam: 'test',
|
||||
helpLink: 'test-link-help',
|
||||
reportAProblemLink: 'test-report-link',
|
||||
moreTeamsToJoin: true,
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with plugins', () => {
|
||||
const props: ComponentProps<typeof MainMenu> = {
|
||||
...defaultProps,
|
||||
pluginMenuItems: [{
|
||||
id: 'plugin-id-1',
|
||||
pluginId: 'plugin-1',
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
text: 'some text',
|
||||
},
|
||||
{
|
||||
id: 'plugind-id-2',
|
||||
pluginId: 'plugin-2',
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
text: 'some text',
|
||||
},
|
||||
],
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with plugins in mobile', () => {
|
||||
const props: ComponentProps<typeof MainMenu> = {
|
||||
...defaultProps,
|
||||
mobile: true,
|
||||
pluginMenuItems: [{
|
||||
id: 'plugin-id-1',
|
||||
pluginId: 'plugin-1',
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
text: 'some text',
|
||||
},
|
||||
{
|
||||
id: 'plugind-id-2',
|
||||
pluginId: 'plugin-2',
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn,
|
||||
text: 'some text',
|
||||
},
|
||||
],
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show leave team option when primary team is not set', () => {
|
||||
const props = {...defaultProps, teamIsGroupConstrained: false, experimentalPrimaryTeam: undefined};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
|
||||
// show leave team option when experimentalPrimaryTeam is not set
|
||||
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
|
||||
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
|
||||
});
|
||||
|
||||
test('should hide leave team option when experimentalPrimaryTeam is same as current team', () => {
|
||||
const props = {...defaultProps, teamIsGroupConstrained: false};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
|
||||
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
|
||||
});
|
||||
|
||||
test('should hide leave team option when experimentalPrimaryTeam is same as current team', () => {
|
||||
const props = {...defaultProps, teamIsGroupConstrained: false, experimentalPrimaryTeam: 'other-team'};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper.find('#leaveTeam')).toHaveLength(1);
|
||||
expect(wrapper.find('#leaveTeam').find(Menu.ItemToggleModalRedux).props().show).toEqual(true);
|
||||
});
|
||||
|
||||
test('mobile view should hide the subscribe now button when does not have permissions', () => {
|
||||
const noPermissionsState = {...defaultState};
|
||||
noPermissionsState.entities.roles.roles.system_manager.permissions = [];
|
||||
const store = mockStore(noPermissionsState);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<MainMenu {...defaultProps}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('UpgradeLink')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('mobile view should hide start trial menu item because user state does not have permission to write license', () => {
|
||||
const store = mockStore(defaultState);
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<Provider store={store}>
|
||||
<MainMenu {...defaultProps}/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#startTrial')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should match snapshot with guest access disabled and no team invite permission', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
guestAccessEnabled: false,
|
||||
canInviteTeamMember: false,
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with cloud free trial', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isCloud: true,
|
||||
isStarterFree: false,
|
||||
isFreeTrial: true,
|
||||
usageDeltaTeams: -1,
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper.find('#createTeam')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with cloud free and team limit reached', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
isCloud: true,
|
||||
isStarterFree: true,
|
||||
isFreeTrial: false,
|
||||
usageDeltaTeams: 0,
|
||||
};
|
||||
const wrapper = getMainMenuWrapper(props);
|
||||
expect(wrapper.find('#createTeam')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,538 +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 {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import AboutBuildModal from 'components/about_build_modal';
|
||||
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
|
||||
import InvitationModal from 'components/invitation_modal';
|
||||
import LeaveTeamModal from 'components/leave_team_modal';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
|
||||
import TeamGroupsManageModal from 'components/team_groups_manage_modal';
|
||||
import TeamMembersModal from 'components/team_members_modal';
|
||||
import TeamSettingsModal from 'components/team_settings_modal';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
import LeaveTeamIcon from 'components/widgets/icons/leave_team_icon';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
||||
|
||||
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
|
||||
import {Constants, LicenseSkus, ModalIdentifiers, MattermostFeatures} from 'utils/constants';
|
||||
import {cmdOrCtrlPressed, isKeyPressed} from 'utils/keyboard';
|
||||
import {makeUrlSafe} from 'utils/url';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
import type {MainMenuAction} from 'types/store/plugins';
|
||||
|
||||
import LearnAboutTeamsLink from './learn_about_teams_link';
|
||||
import './main_menu.scss';
|
||||
|
||||
export type Props = {
|
||||
mobile: boolean;
|
||||
id?: string;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
siteName?: string;
|
||||
currentUser?: UserProfile;
|
||||
appDownloadLink?: string;
|
||||
enableCommands: boolean;
|
||||
enableIncomingWebhooks: boolean;
|
||||
enableOAuthServiceProvider: boolean;
|
||||
enableOutgoingWebhooks: boolean;
|
||||
canManageSystemBots: boolean;
|
||||
canManageIntegrations: boolean;
|
||||
experimentalPrimaryTeam?: string;
|
||||
helpLink?: string;
|
||||
reportAProblemLink?: string;
|
||||
moreTeamsToJoin: boolean;
|
||||
pluginMenuItems?: MainMenuAction[];
|
||||
isMentionSearch?: boolean;
|
||||
teamIsGroupConstrained: boolean;
|
||||
isLicensedForLDAPGroups?: boolean;
|
||||
intl: IntlShape;
|
||||
isCloud: boolean;
|
||||
isStarterFree: boolean;
|
||||
isFreeTrial: boolean;
|
||||
usageDeltaTeams: number;
|
||||
guestAccessEnabled: boolean;
|
||||
canInviteTeamMember: boolean;
|
||||
actions: {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
showMentions: () => void;
|
||||
showFlaggedPosts: () => void;
|
||||
closeRightHandSide: () => void;
|
||||
closeRhsMenu: () => void;
|
||||
};
|
||||
|
||||
};
|
||||
export class MainMenu extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
teamType: '',
|
||||
mobile: false,
|
||||
pluginMenuItems: [],
|
||||
};
|
||||
|
||||
async componentDidMount(): Promise<void> {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
handleKeyDown = (e: KeyboardEvent): void => {
|
||||
if (cmdOrCtrlPressed(e) && e.shiftKey && isKeyPressed(e, Constants.KeyCodes.A)) {
|
||||
e.preventDefault();
|
||||
this.props.actions.openModal({modalId: ModalIdentifiers.USER_SETTINGS, dialogType: UserSettingsModal, dialogProps: {isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton'}});
|
||||
}
|
||||
};
|
||||
|
||||
handleEmitUserLoggedOutEvent = (): void => {
|
||||
GlobalActions.emitUserLoggedOutEvent();
|
||||
};
|
||||
|
||||
getFlagged = (e: Event): void => {
|
||||
e.preventDefault();
|
||||
this.props.actions.showFlaggedPosts();
|
||||
this.props.actions.closeRhsMenu();
|
||||
};
|
||||
|
||||
searchMentions = (e: Event): void => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.isMentionSearch) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
} else {
|
||||
this.props.actions.closeRhsMenu();
|
||||
this.props.actions.showMentions();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
appDownloadLink,
|
||||
currentUser,
|
||||
teamIsGroupConstrained,
|
||||
isLicensedForLDAPGroups,
|
||||
teamId = '',
|
||||
guestAccessEnabled,
|
||||
canInviteTeamMember,
|
||||
} = this.props;
|
||||
|
||||
const safeAppDownloadLink = makeUrlSafe(appDownloadLink || '');
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pluginItems = this.props.pluginMenuItems?.map((item) => (
|
||||
<Menu.ItemAction
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={() => {
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
}}
|
||||
text={item.text}
|
||||
icon={this.props.mobile && item.mobileIcon}
|
||||
/>
|
||||
));
|
||||
|
||||
const someIntegrationEnabled = this.props.enableIncomingWebhooks || this.props.enableOutgoingWebhooks || this.props.enableCommands || this.props.enableOAuthServiceProvider || this.props.canManageSystemBots;
|
||||
const showIntegrations = !this.props.mobile && someIntegrationEnabled && this.props.canManageIntegrations;
|
||||
const teamsLimitReached = this.props.isStarterFree && !this.props.isFreeTrial && this.props.usageDeltaTeams >= 0;
|
||||
const createTeamRestricted = this.props.isCloud && (this.props.isFreeTrial || teamsLimitReached);
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let invitePeopleModal = null;
|
||||
if (guestAccessEnabled || canInviteTeamMember) {
|
||||
invitePeopleModal = (
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='invitePeople'
|
||||
modalId={ModalIdentifiers.INVITATION}
|
||||
dialogType={InvitationModal}
|
||||
text={formatMessage({
|
||||
id: 'navbar_dropdown.invitePeople',
|
||||
defaultMessage: 'Invite People',
|
||||
})}
|
||||
extraText={formatMessage({
|
||||
id: 'navbar_dropdown.invitePeopleExtraText',
|
||||
defaultMessage: 'Add people to the team',
|
||||
})}
|
||||
icon={this.props.mobile && <i className='fa fa-user-plus'/>}
|
||||
onClick={() => trackEvent('ui', 'click_sidebar_team_dropdown_invite_people')}
|
||||
dialogProps={{focusOriginElement: 'sidebarDropdownMenuButton'}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.mobile ? (
|
||||
<Menu
|
||||
id={this.props.id}
|
||||
ariaLabel={formatMessage({id: 'navbar_dropdown.menuAriaLabel', defaultMessage: 'main menu'})}
|
||||
>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate
|
||||
permissions={[Permissions.SYSCONSOLE_WRITE_BILLING]}
|
||||
>
|
||||
<Menu.CloudTrial
|
||||
id='menuCloudTrial'
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate
|
||||
permissions={[Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE]}
|
||||
>
|
||||
<Menu.StartTrial
|
||||
id='startTrial'
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
id='recentMentions'
|
||||
onClick={this.searchMentions}
|
||||
icon={<i className='mentions'>{'@'}</i>}
|
||||
text={formatMessage({id: 'sidebar_right_menu.recentMentions', defaultMessage: 'Recent Mentions'})}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
id='flaggedPosts'
|
||||
onClick={this.getFlagged}
|
||||
icon={<i className='fa fa-bookmark'/>}
|
||||
text={formatMessage({id: 'sidebar_right_menu.flagged', defaultMessage: 'Saved messages'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='profileSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: false, focusOriginElement: 'userAccountMenuButton2'}}
|
||||
text={formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
|
||||
icon={<i className='fa fa-user'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='accountSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: true, focusOriginElement: 'userAccountMenuButton3'}}
|
||||
text={formatMessage({id: 'navbar_dropdown.accountSettings', defaultMessage: 'Settings'})}
|
||||
icon={<i className='fa fa-cog'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='addGroupsToTeam'
|
||||
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
|
||||
dialogType={AddGroupsToTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
|
||||
icon={<i className='fa fa-user-plus'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
|
||||
>
|
||||
{invitePeopleModal}
|
||||
</TeamPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='teamSettings'
|
||||
modalId={ModalIdentifiers.TEAM_SETTINGS}
|
||||
dialogType={TeamSettingsModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
|
||||
icon={<i className='fa fa-globe'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageGroups'
|
||||
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
|
||||
dialogProps={{
|
||||
teamID: teamId,
|
||||
}}
|
||||
dialogType={TeamGroupsManageModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
|
||||
icon={<i className='fa fa-user-plus'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
|
||||
icon={<i className='fa fa-users'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
invert={true}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='viewMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
|
||||
icon={<i className='fa fa-users'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
|
||||
<Menu.ItemLink
|
||||
id='createTeam'
|
||||
show={!teamsLimitReached}
|
||||
to='/create_team'
|
||||
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
|
||||
icon={<i className='fa fa-plus-square'/>}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
<Menu.ItemLink
|
||||
id='joinTeam'
|
||||
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
|
||||
to='/select_team'
|
||||
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
|
||||
icon={<i className='fa fa-plus-square'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='leaveTeam'
|
||||
show={!teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
|
||||
modalId={ModalIdentifiers.LEAVE_TEAM}
|
||||
dialogType={LeaveTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
|
||||
icon={<LeaveTeamIcon/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
{pluginItems}
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemLink
|
||||
id='integrations'
|
||||
show={showIntegrations}
|
||||
to={'/' + this.props.teamName + '/integrations'}
|
||||
text={formatMessage({id: 'navbar_dropdown.integrations', defaultMessage: 'Integrations'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemExternalLink
|
||||
id='helpLink'
|
||||
show={Boolean(this.props.helpLink)}
|
||||
url={this.props.helpLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.help', defaultMessage: 'Help'})}
|
||||
icon={<i className='fa fa-question'/>}
|
||||
/>
|
||||
<Menu.ItemExternalLink
|
||||
id='reportLink'
|
||||
show={Boolean(this.props.reportAProblemLink)}
|
||||
url={this.props.reportAProblemLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.report', defaultMessage: 'Report a Problem'})}
|
||||
icon={<i className='fa fa-phone'/>}
|
||||
/>
|
||||
<Menu.ItemExternalLink
|
||||
id='nativeAppLink'
|
||||
show={this.props.appDownloadLink && !UserAgent.isMobileApp()}
|
||||
url={safeAppDownloadLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.nativeApps', defaultMessage: 'Download Apps'})}
|
||||
icon={<i className='fa fa-mobile'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='about'
|
||||
modalId={ModalIdentifiers.ABOUT}
|
||||
dialogType={AboutBuildModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.about', defaultMessage: 'About {appTitle}'}, {appTitle: this.props.siteName || 'Mattermost'})}
|
||||
icon={<i className='fa fa-info'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
id='logout'
|
||||
onClick={this.handleEmitUserLoggedOutEvent}
|
||||
text={formatMessage({id: 'navbar_dropdown.logout', defaultMessage: 'Log Out'})}
|
||||
icon={<i className='fa fa-sign-out'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
) : (
|
||||
<Menu
|
||||
id={this.props.id}
|
||||
ariaLabel={formatMessage({id: 'sidebar.team_menu.menuAriaLabel', defaultMessage: 'team menu'})}
|
||||
>
|
||||
<Menu.Group>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='addGroupsToTeam'
|
||||
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
|
||||
dialogType={AddGroupsToTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
|
||||
>
|
||||
{invitePeopleModal}
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='teamSettings'
|
||||
modalId={ModalIdentifiers.TEAM_SETTINGS}
|
||||
dialogType={TeamSettingsModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageGroups'
|
||||
show={teamIsGroupConstrained && isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
|
||||
dialogProps={{teamID: teamId}}
|
||||
dialogType={TeamGroupsManageModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
invert={true}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='viewMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<Menu.ItemLink
|
||||
id='joinTeam'
|
||||
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
|
||||
to='/select_team'
|
||||
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='leaveTeam'
|
||||
className='destructive'
|
||||
show={!teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
|
||||
modalId={ModalIdentifiers.LEAVE_TEAM}
|
||||
dialogType={LeaveTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
|
||||
<Menu.ItemLink
|
||||
id='createTeam'
|
||||
to='/create_team'
|
||||
className={createTeamRestricted ? 'MenuItem__with-icon-tooltip' : ''}
|
||||
disabled={teamsLimitReached}
|
||||
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
|
||||
sibling={createTeamRestricted && (
|
||||
<RestrictedIndicator
|
||||
feature={MattermostFeatures.CREATE_MULTIPLE_TEAMS}
|
||||
minimumPlanRequiredForFeature={LicenseSkus.Professional}
|
||||
blocked={!this.props.isFreeTrial}
|
||||
tooltipMessage={formatMessage({
|
||||
id: 'navbar_dropdown.create.tooltip.cloudFreeTrial',
|
||||
defaultMessage: 'During your trial you are able to create multiple teams. These teams will be archived after your trial.',
|
||||
})}
|
||||
titleAdminPreTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleAdminPreTrial',
|
||||
defaultMessage: 'Try unlimited teams with a free trial',
|
||||
})}
|
||||
messageAdminPreTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageAdminPreTrial',
|
||||
defaultMessage: 'Create unlimited teams with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.',
|
||||
},
|
||||
{
|
||||
trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS,
|
||||
},
|
||||
)}
|
||||
titleAdminPostTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleAdminPostTrial',
|
||||
defaultMessage: 'Upgrade to create unlimited teams',
|
||||
})}
|
||||
messageAdminPostTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageAdminPostTrial',
|
||||
defaultMessage: 'Multiple teams allow for context-specific spaces that are more attuned to your and your teams’ needs. Upgrade to the Professional plan to create unlimited teams.',
|
||||
})}
|
||||
titleEndUser={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleEndUser',
|
||||
defaultMessage: 'Multiple teams available in paid plans',
|
||||
})}
|
||||
messageEndUser={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageEndUser',
|
||||
defaultMessage: 'Multiple teams allow for context-specific spaces that are more attuned to your teams’ needs.',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
<Menu.Group>
|
||||
<div className='MainMenu_dropdown-link'>
|
||||
<LearnAboutTeamsLink/>
|
||||
</div>
|
||||
</Menu.Group>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
{pluginItems}
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(MainMenu);
|
||||
@@ -10,6 +10,7 @@
|
||||
padding: 4px 0;
|
||||
background-color: var(--center-channel-bg);
|
||||
box-shadow: var(--elevation-4), 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.12) inset;
|
||||
margin-block-start: 4px;
|
||||
}
|
||||
&.AsSubMenu {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -24,7 +24,7 @@ import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ChannelHeaderMobile from './channel_header_mobile';
|
||||
import MobileChannelHeader from './mobile_channel_header';
|
||||
|
||||
const isCurrentChannelMuted = createSelector(
|
||||
'isCurrentChannelMuted',
|
||||
@@ -53,4 +53,4 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(ChannelHeaderMobile));
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileChannelHeader));
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import ChannelHeaderMobile from './channel_header_mobile';
|
||||
import ChannelHeaderMobile from './mobile_channel_header';
|
||||
|
||||
describe('components/ChannelHeaderMobile/ChannelHeaderMobile', () => {
|
||||
global.document.querySelector = jest.fn().mockReturnValue({
|
||||
@@ -33,7 +33,7 @@ type Props = {
|
||||
};
|
||||
}
|
||||
|
||||
export default class ChannelHeaderMobile extends React.PureComponent<Props> {
|
||||
export default class MobileChannelHeader extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
document.querySelector('.inner-wrap')?.addEventListener('click', this.hideSidebars);
|
||||
}
|
||||
26
webapp/channels/src/components/mobile_sidebar_right/index.ts
Обычный файл
26
webapp/channels/src/components/mobile_sidebar_right/index.ts
Обычный файл
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {getIsRhsMenuOpen} from 'selectors/rhs';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MobileSidebarRight from './mobile_sidebar_right';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
currentUser: getCurrentUser(state),
|
||||
isOpen: getIsRhsMenuOpen(state),
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connect(mapStateToProps)(MobileSidebarRight);
|
||||
@@ -4,15 +4,29 @@
|
||||
.sidebar--menu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
display: none;
|
||||
overflow: auto;
|
||||
width: 220px;
|
||||
width: 290px;
|
||||
height: 100%;
|
||||
padding: 0 0 2em;
|
||||
border-right: variables.$border-gray;
|
||||
background: var(--sidebar-bg);
|
||||
color: rgba(var(--sidebar-text-rgb), 0.8);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transform: translate3d(290px, 0, 0);
|
||||
transition: transform 0.3s linear;
|
||||
|
||||
&.move--left {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.mentions {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Menu .MenuItem.MenuItem--with-icon .MenuItem__help-text {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
@@ -31,53 +45,6 @@
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.team__header {
|
||||
@include mixins.pie-clearfix;
|
||||
|
||||
display: none;
|
||||
padding: 0 15px;
|
||||
background: var(--sidebar-header-bg);
|
||||
|
||||
a {
|
||||
color: variables.$white;
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
margin: 16px -5px 0;
|
||||
font-size: 0.85em;
|
||||
|
||||
.dropdown-toggle {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
li a {
|
||||
padding: 3 20px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown__icon {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.team__name {
|
||||
overflow: hidden;
|
||||
max-width: 80%;
|
||||
float: left;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
> li {
|
||||
> a,
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {memo} from 'react';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
|
||||
import useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
|
||||
|
||||
import MobileRightDrawerItems from './mobile_sidebar_right_items';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
import './mobile_sidebar_right.scss';
|
||||
|
||||
const TRANSITION_TIMEOUT = 300; // in ms
|
||||
|
||||
type Props = PropsFromRedux;
|
||||
|
||||
const MobileRightDrawer = ({
|
||||
isOpen,
|
||||
currentUser,
|
||||
}: Props) => {
|
||||
const usageDeltas = useGetUsageDeltas();
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('sidebar--menu', {'move--left': isOpen})}
|
||||
id='sidebar-menu'
|
||||
>
|
||||
<div className='nav-pills__container mobile-main-menu'>
|
||||
<CSSTransition
|
||||
in={isOpen}
|
||||
enter={true}
|
||||
exit={true}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
timeout={TRANSITION_TIMEOUT}
|
||||
>
|
||||
<MobileRightDrawerItems
|
||||
usageDeltaTeams={usageDeltas.teams.active}
|
||||
/>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MobileRightDrawer);
|
||||
@@ -1,25 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {getCloudSubscription as selectCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {
|
||||
getConfig,
|
||||
getLicense,
|
||||
} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveICurrentTeamPermission, haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {
|
||||
getJoinableTeamIds,
|
||||
getCurrentTeam,
|
||||
} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {showMentions, showFlaggedPosts, closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs';
|
||||
import {getRhsState} from 'selectors/rhs';
|
||||
|
||||
@@ -28,28 +24,18 @@ import {isCloudLicense} from 'utils/license_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MainMenu from './main_menu';
|
||||
import MobileSidebarRightItems from './mobile_sidebar_right_items';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
const appDownloadLink = config.AppDownloadLink;
|
||||
const enableCommands = config.EnableCommands === 'true';
|
||||
const siteName = config.SiteName;
|
||||
const enableIncomingWebhooks = config.EnableIncomingWebhooks === 'true';
|
||||
const enableOAuthServiceProvider = config.EnableOAuthServiceProvider === 'true';
|
||||
const enableOutgoingWebhooks = config.EnableOutgoingWebhooks === 'true';
|
||||
const experimentalPrimaryTeam = config.ExperimentalPrimaryTeam;
|
||||
const helpLink = config.HelpLink;
|
||||
const reportAProblemLink = config.ReportAProblemLink;
|
||||
|
||||
const canManageTeamIntegrations = (haveICurrentTeamPermission(state, Permissions.MANAGE_SLASH_COMMANDS) || haveICurrentTeamPermission(state, Permissions.MANAGE_OAUTH) || haveICurrentTeamPermission(state, Permissions.MANAGE_INCOMING_WEBHOOKS) || haveICurrentTeamPermission(state, Permissions.MANAGE_OUTGOING_WEBHOOKS));
|
||||
const canManageSystemBots = (haveISystemPermission(state, {permission: Permissions.MANAGE_BOTS}) || haveISystemPermission(state, {permission: Permissions.MANAGE_OTHERS_BOTS}));
|
||||
const canManageIntegrations = canManageTeamIntegrations || canManageSystemBots;
|
||||
const canInviteTeamMember = haveICurrentTeamPermission(state, Permissions.ADD_USER_TO_TEAM);
|
||||
|
||||
const joinableTeams = getJoinableTeamIds(state);
|
||||
const moreTeamsToJoin = joinableTeams && joinableTeams.length > 0;
|
||||
const rhsState = getRhsState(state);
|
||||
@@ -64,12 +50,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
|
||||
return {
|
||||
appDownloadLink,
|
||||
enableCommands,
|
||||
canManageIntegrations,
|
||||
enableIncomingWebhooks,
|
||||
enableOAuthServiceProvider,
|
||||
enableOutgoingWebhooks,
|
||||
canManageSystemBots,
|
||||
experimentalPrimaryTeam,
|
||||
helpLink,
|
||||
reportAProblemLink,
|
||||
@@ -78,13 +58,10 @@ function mapStateToProps(state: GlobalState) {
|
||||
siteName,
|
||||
teamId: currentTeam?.id,
|
||||
teamName: currentTeam?.name,
|
||||
currentUser,
|
||||
isMentionSearch: rhsState === RHSStates.MENTION,
|
||||
teamIsGroupConstrained: Boolean(currentTeam?.group_constrained),
|
||||
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
|
||||
guestAccessEnabled: config.EnableGuestAccounts === 'true',
|
||||
canInviteTeamMember,
|
||||
isCloud,
|
||||
isStarterFree,
|
||||
isFreeTrial,
|
||||
};
|
||||
@@ -93,7 +70,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
openModal,
|
||||
showMentions,
|
||||
showFlaggedPosts,
|
||||
closeRightHandSide,
|
||||
@@ -102,4 +78,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default withRouter<any, any>(connect(mapStateToProps, mapDispatchToProps)(MainMenu));
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(MobileSidebarRightItems);
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import type {MockIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
|
||||
import {MobileSidebarRightItems} from './mobile_sidebar_right_items';
|
||||
import type {Props} from './mobile_sidebar_right_items';
|
||||
|
||||
describe('MobileSidebarRightItems', () => {
|
||||
const defaultProps: Props = {
|
||||
teamId: 'team-id',
|
||||
teamName: 'team_name',
|
||||
appDownloadLink: undefined,
|
||||
experimentalPrimaryTeam: undefined,
|
||||
helpLink: undefined,
|
||||
reportAProblemLink: undefined,
|
||||
moreTeamsToJoin: false,
|
||||
pluginMenuItems: [],
|
||||
isMentionSearch: false,
|
||||
usageDeltaTeams: 0,
|
||||
siteName: 'site-name',
|
||||
isLicensedForLDAPGroups: false,
|
||||
guestAccessEnabled: true,
|
||||
actions: {
|
||||
showMentions: jest.fn(),
|
||||
showFlaggedPosts: jest.fn(),
|
||||
closeRightHandSide: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
},
|
||||
teamIsGroupConstrained: false,
|
||||
isStarterFree: false,
|
||||
isFreeTrial: false,
|
||||
intl: {
|
||||
formatMessage: ({defaultMessage}) => defaultMessage,
|
||||
} as MockIntl,
|
||||
};
|
||||
|
||||
const defaultState = {
|
||||
entities: {
|
||||
channels: {
|
||||
myMembers: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
license: {
|
||||
Cloud: 'false',
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
myMembers: {
|
||||
'team-id': {
|
||||
team_id: 'team-id',
|
||||
user_id: 'test-user-id',
|
||||
roles: 'team_user',
|
||||
scheme_user: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'test-user-id',
|
||||
profiles: {
|
||||
'test-user-id': {
|
||||
id: 'test-user-id',
|
||||
roles: 'system_user system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_admin: {
|
||||
permissions: [
|
||||
Permissions.CREATE_TEAM,
|
||||
Permissions.SYSCONSOLE_WRITE_PLUGINS,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should render basic menu items', () => {
|
||||
renderWithContext(<MobileSidebarRightItems {...defaultProps}/>, defaultState);
|
||||
expect(screen.getByText('Recent Mentions')).toBeInTheDocument();
|
||||
expect(screen.getByText('Saved messages')).toBeInTheDocument();
|
||||
expect(screen.getByText('Profile')).toBeInTheDocument();
|
||||
expect(screen.getByText('Settings')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show leave team option when primary team is not set', () => {
|
||||
renderWithContext(
|
||||
<MobileSidebarRightItems
|
||||
{...defaultProps}
|
||||
teamIsGroupConstrained={false}
|
||||
experimentalPrimaryTeam={undefined}
|
||||
/>,
|
||||
defaultState,
|
||||
);
|
||||
expect(screen.getByText('Leave Team')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should hide leave team option when team is group constrained', () => {
|
||||
renderWithContext(
|
||||
<MobileSidebarRightItems
|
||||
{...defaultProps}
|
||||
teamIsGroupConstrained={true}
|
||||
/>,
|
||||
defaultState,
|
||||
);
|
||||
expect(screen.queryByText('Leave Team')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show create team option with proper permissions', () => {
|
||||
renderWithContext(<MobileSidebarRightItems {...defaultProps}/>, defaultState);
|
||||
expect(screen.getByText('Create a Team')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show plugins when provided', () => {
|
||||
const pluginMenuItems = [
|
||||
{
|
||||
id: 'plugin-1',
|
||||
pluginId: 'plugin-1',
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
action: jest.fn(),
|
||||
text: 'Plugin Item 1',
|
||||
},
|
||||
];
|
||||
renderWithContext(
|
||||
<MobileSidebarRightItems
|
||||
{...defaultProps}
|
||||
pluginMenuItems={pluginMenuItems}
|
||||
/>,
|
||||
defaultState,
|
||||
);
|
||||
expect(screen.getByText('Plugin Item 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show help link when provided', () => {
|
||||
renderWithContext(
|
||||
<MobileSidebarRightItems
|
||||
{...defaultProps}
|
||||
helpLink='https://help.example.com'
|
||||
/>,
|
||||
defaultState,
|
||||
);
|
||||
expect(screen.getByText('Help')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show report link when provided', () => {
|
||||
renderWithContext(
|
||||
<MobileSidebarRightItems
|
||||
{...defaultProps}
|
||||
reportAProblemLink='https://report.example.com'
|
||||
/>,
|
||||
defaultState,
|
||||
);
|
||||
expect(screen.getByText('Report a Problem')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
// 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 {WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {emitUserLoggedOutEvent} from 'actions/global_actions';
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import AboutBuildModal from 'components/about_build_modal';
|
||||
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
|
||||
import InvitationModal from 'components/invitation_modal';
|
||||
import LeaveTeamModal from 'components/leave_team_modal';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
|
||||
import TeamGroupsManageModal from 'components/team_groups_manage_modal';
|
||||
import TeamMembersModal from 'components/team_members_modal';
|
||||
import TeamSettingsModal from 'components/team_settings_modal';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
import LeaveTeamIcon from 'components/widgets/icons/leave_team_icon';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {makeUrlSafe} from 'utils/url';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
|
||||
export interface Props extends PropsFromRedux, WrappedComponentProps {
|
||||
usageDeltaTeams: number;
|
||||
}
|
||||
|
||||
export class MobileSidebarRightItems extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
pluginMenuItems: [],
|
||||
};
|
||||
|
||||
onRecentMentionItemClick = (e: Event): void => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.isMentionSearch) {
|
||||
this.props.actions.closeRightHandSide();
|
||||
} else {
|
||||
this.props.actions.closeRhsMenu();
|
||||
this.props.actions.showMentions();
|
||||
}
|
||||
};
|
||||
|
||||
onShowFlaggedPostItemClick = (e: Event): void => {
|
||||
e.preventDefault();
|
||||
this.props.actions.showFlaggedPosts();
|
||||
this.props.actions.closeRhsMenu();
|
||||
};
|
||||
|
||||
onLogoutItemClick = (): void => {
|
||||
emitUserLoggedOutEvent();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const safeAppDownloadLink = makeUrlSafe(this.props.appDownloadLink || '');
|
||||
const teamsLimitReached = this.props.isStarterFree && !this.props.isFreeTrial && this.props.usageDeltaTeams >= 0;
|
||||
|
||||
const pluginItems = this.props.pluginMenuItems.map((item) => (
|
||||
<Menu.ItemAction
|
||||
id={item.id + '_pluginmenuitem'}
|
||||
key={item.id + '_pluginmenuitem'}
|
||||
onClick={() => {
|
||||
if (item.action) {
|
||||
item.action();
|
||||
}
|
||||
}}
|
||||
text={item.text}
|
||||
icon={item.mobileIcon}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Menu
|
||||
ariaLabel={formatMessage({id: 'navbar_dropdown.menuAriaLabel', defaultMessage: 'main menu'})}
|
||||
>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate
|
||||
permissions={[Permissions.SYSCONSOLE_WRITE_BILLING]}
|
||||
>
|
||||
<Menu.CloudTrial
|
||||
id='menuCloudTrial'
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate
|
||||
permissions={[Permissions.SYSCONSOLE_WRITE_ABOUT_EDITION_AND_LICENSE]}
|
||||
>
|
||||
<Menu.StartTrial
|
||||
id='startTrial'
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
id='recentMentions'
|
||||
onClick={this.onRecentMentionItemClick}
|
||||
icon={<i className='mentions'>{'@'}</i>}
|
||||
text={formatMessage({id: 'sidebar_right_menu.recentMentions', defaultMessage: 'Recent Mentions'})}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
id='flaggedPosts'
|
||||
onClick={this.onShowFlaggedPostItemClick}
|
||||
icon={<i className='fa fa-bookmark'/>}
|
||||
text={formatMessage({id: 'sidebar_right_menu.flagged', defaultMessage: 'Saved messages'})}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='profileSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: false}}
|
||||
text={formatMessage({id: 'navbar_dropdown.profileSettings', defaultMessage: 'Profile'})}
|
||||
icon={<i className='fa fa-user'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='accountSettings'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: true}}
|
||||
text={formatMessage({id: 'navbar_dropdown.accountSettings', defaultMessage: 'Settings'})}
|
||||
icon={<i className='fa fa-cog'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='addGroupsToTeam'
|
||||
show={this.props.teamIsGroupConstrained && this.props.isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
|
||||
dialogType={AddGroupsToTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.addGroupsToTeam', defaultMessage: 'Add Groups to Team'})}
|
||||
icon={<i className='fa fa-user-plus'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
{this.props.guestAccessEnabled && (
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.ADD_USER_TO_TEAM, Permissions.INVITE_GUEST]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='invitePeople'
|
||||
modalId={ModalIdentifiers.INVITATION}
|
||||
dialogType={InvitationModal}
|
||||
text={formatMessage({
|
||||
id: 'navbar_dropdown.invitePeople',
|
||||
defaultMessage: 'Invite People',
|
||||
})}
|
||||
extraText={formatMessage({
|
||||
id: 'navbar_dropdown.invitePeopleExtraText',
|
||||
defaultMessage: 'Add people to the team',
|
||||
})}
|
||||
icon={<i className='fa fa-user-plus'/>}
|
||||
onClick={() => trackEvent('ui', 'click_sidebar_team_dropdown_invite_people')}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
)}
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='teamSettings'
|
||||
modalId={ModalIdentifiers.TEAM_SETTINGS}
|
||||
dialogType={TeamSettingsModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
|
||||
icon={<i className='fa fa-globe'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.MANAGE_TEAM]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageGroups'
|
||||
show={this.props.teamIsGroupConstrained && this.props.isLicensedForLDAPGroups}
|
||||
modalId={ModalIdentifiers.MANAGE_TEAM_GROUPS}
|
||||
dialogProps={{
|
||||
teamID: this.props.teamId,
|
||||
}}
|
||||
dialogType={TeamGroupsManageModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageGroups', defaultMessage: 'Manage Groups'})}
|
||||
icon={<i className='fa fa-user-plus'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='manageMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.manageMembers', defaultMessage: 'Manage Members'})}
|
||||
icon={<i className='fa fa-users'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
<TeamPermissionGate
|
||||
teamId={this.props.teamId}
|
||||
permissions={[Permissions.REMOVE_USER_FROM_TEAM, Permissions.MANAGE_TEAM_ROLES]}
|
||||
invert={true}
|
||||
>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='viewMembers'
|
||||
modalId={ModalIdentifiers.TEAM_MEMBERS}
|
||||
dialogType={TeamMembersModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.viewMembers', defaultMessage: 'View Members'})}
|
||||
icon={<i className='fa fa-users'/>}
|
||||
/>
|
||||
</TeamPermissionGate>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<SystemPermissionGate permissions={[Permissions.CREATE_TEAM]}>
|
||||
<Menu.ItemLink
|
||||
id='createTeam'
|
||||
show={!teamsLimitReached}
|
||||
to='/create_team'
|
||||
text={formatMessage({id: 'navbar_dropdown.create', defaultMessage: 'Create a Team'})}
|
||||
icon={<i className='fa fa-plus-square'/>}
|
||||
/>
|
||||
</SystemPermissionGate>
|
||||
<Menu.ItemLink
|
||||
id='joinTeam'
|
||||
show={!this.props.experimentalPrimaryTeam && this.props.moreTeamsToJoin}
|
||||
to='/select_team'
|
||||
text={formatMessage({id: 'navbar_dropdown.join', defaultMessage: 'Join Another Team'})}
|
||||
icon={<i className='fa fa-plus-square'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='leaveTeam'
|
||||
show={!this.props.teamIsGroupConstrained && this.props.experimentalPrimaryTeam !== this.props.teamName}
|
||||
modalId={ModalIdentifiers.LEAVE_TEAM}
|
||||
dialogType={LeaveTeamModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.leave', defaultMessage: 'Leave Team'})}
|
||||
icon={<LeaveTeamIcon/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
{pluginItems}
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemExternalLink
|
||||
id='helpLink'
|
||||
show={Boolean(this.props.helpLink)}
|
||||
url={this.props.helpLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.help', defaultMessage: 'Help'})}
|
||||
icon={<i className='fa fa-question'/>}
|
||||
/>
|
||||
<Menu.ItemExternalLink
|
||||
id='reportLink'
|
||||
show={Boolean(this.props.reportAProblemLink)}
|
||||
url={this.props.reportAProblemLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.report', defaultMessage: 'Report a Problem'})}
|
||||
icon={<i className='fa fa-phone'/>}
|
||||
/>
|
||||
<Menu.ItemExternalLink
|
||||
id='nativeAppLink'
|
||||
show={this.props.appDownloadLink}
|
||||
url={safeAppDownloadLink}
|
||||
text={formatMessage({id: 'navbar_dropdown.nativeApps', defaultMessage: 'Download Apps'})}
|
||||
icon={<i className='fa fa-mobile'/>}
|
||||
/>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='about'
|
||||
modalId={ModalIdentifiers.ABOUT}
|
||||
dialogType={AboutBuildModal}
|
||||
text={formatMessage({id: 'navbar_dropdown.about', defaultMessage: 'About {appTitle}'}, {appTitle: this.props.siteName || 'Mattermost'})}
|
||||
icon={<i className='fa fa-info'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
id='logout'
|
||||
onClick={this.onLogoutItemClick}
|
||||
text={formatMessage({id: 'navbar_dropdown.logout', defaultMessage: 'Log Out'})}
|
||||
icon={<i className='fa fa-sign-out'/>}
|
||||
/>
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(MobileSidebarRightItems);
|
||||
@@ -164,7 +164,7 @@ export default class MoreDirectChannels extends React.PureComponent<Props, State
|
||||
|
||||
if (this.exitToChannel) {
|
||||
getHistory().push(this.exitToChannel);
|
||||
} else {
|
||||
} else if (this.props.focusOriginElement) {
|
||||
setTimeout(() => {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}, 0);
|
||||
|
||||
@@ -10,10 +10,12 @@ import {TourTip, useMeasurePunchouts} from '@mattermost/components';
|
||||
import {setShowOnboardingCompleteProfileTour} from 'actions/views/onboarding_tasks';
|
||||
import {isShowOnboardingCompleteProfileTour} from 'selectors/views/onboarding_tasks';
|
||||
|
||||
import {ELEMENT_ID_FOR_USER_ACCOUNT_MENU} from 'components/user_account_menu/user_account_menu';
|
||||
|
||||
import {OnboardingTasksName, TaskNameMapToSteps} from './constants';
|
||||
import {useHandleOnBoardingTaskData} from './onboarding_tasks_manager';
|
||||
|
||||
const translate = {x: 1, y: -9};
|
||||
const translate = {x: 1.5, y: -9};
|
||||
|
||||
export const CompleteYourProfileTour = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -43,7 +45,7 @@ export const CompleteYourProfileTour = () => {
|
||||
</p>
|
||||
);
|
||||
|
||||
const overlayPunchOut = useMeasurePunchouts(['userAccountMenu'], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
const overlayPunchOut = useMeasurePunchouts([ELEMENT_ID_FOR_USER_ACCOUNT_MENU], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
const onDismiss = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
@@ -26,7 +26,6 @@ import LoggedIn from 'components/logged_in';
|
||||
import LoggedInRoute from 'components/logged_in_route';
|
||||
import {LAUNCHING_WORKSPACE_FULLSCREEN_Z_INDEX} from 'components/preparing_workspace/launching_workspace';
|
||||
import {Animations} from 'components/preparing_workspace/steps';
|
||||
import SidebarMobileRightMenu from 'components/sidebar_mobile_right_menu';
|
||||
|
||||
import webSocketClient from 'client/web_websocket_client';
|
||||
import {initializePlugins} from 'plugins';
|
||||
@@ -584,7 +583,6 @@ export default class Root extends React.PureComponent<Props, State> {
|
||||
</div>
|
||||
<Pluggable pluggableName='Global'/>
|
||||
<AppBar/>
|
||||
<SidebarMobileRightMenu/>
|
||||
</CompassThemeProvider>
|
||||
</Switch>
|
||||
</RootProvider>
|
||||
|
||||
@@ -33,7 +33,20 @@ exports[`components/select_team/SelectTeam should match snapshot 1`] = `
|
||||
id="signup_team.join_open"
|
||||
/>
|
||||
</h4>
|
||||
<LearnAboutTeamsLink />
|
||||
<ForwardRef
|
||||
className="LearnAboutTeamsLink"
|
||||
href="https://mattermost.com/pl/mattermost-academy-team-training"
|
||||
location="learn_about_teams"
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="icon icon-lightbulb-outline"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn about teams"
|
||||
id="learn_about_teams"
|
||||
/>
|
||||
</ForwardRef>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
bufferValue={280}
|
||||
@@ -150,7 +163,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on create team
|
||||
id="signup_team.join_open"
|
||||
/>
|
||||
</h4>
|
||||
<LearnAboutTeamsLink />
|
||||
<ForwardRef
|
||||
className="LearnAboutTeamsLink"
|
||||
href="https://mattermost.com/pl/mattermost-academy-team-training"
|
||||
location="learn_about_teams"
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="icon icon-lightbulb-outline"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn about teams"
|
||||
id="learn_about_teams"
|
||||
/>
|
||||
</ForwardRef>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
bufferValue={280}
|
||||
@@ -404,7 +430,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on no joinable
|
||||
id="signup_team.join_open"
|
||||
/>
|
||||
</h4>
|
||||
<LearnAboutTeamsLink />
|
||||
<ForwardRef
|
||||
className="LearnAboutTeamsLink"
|
||||
href="https://mattermost.com/pl/mattermost-academy-team-training"
|
||||
location="learn_about_teams"
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="icon icon-lightbulb-outline"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn about teams"
|
||||
id="learn_about_teams"
|
||||
/>
|
||||
</ForwardRef>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
bufferValue={280}
|
||||
@@ -633,7 +672,20 @@ exports[`components/select_team/SelectTeam should match snapshot, on no joinable
|
||||
id="signup_team.join_open"
|
||||
/>
|
||||
</h4>
|
||||
<LearnAboutTeamsLink />
|
||||
<ForwardRef
|
||||
className="LearnAboutTeamsLink"
|
||||
href="https://mattermost.com/pl/mattermost-academy-team-training"
|
||||
location="learn_about_teams"
|
||||
>
|
||||
<i
|
||||
aria-hidden={true}
|
||||
className="icon icon-lightbulb-outline"
|
||||
/>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Learn about teams"
|
||||
id="learn_about_teams"
|
||||
/>
|
||||
</ForwardRef>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
bufferValue={280}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
margin-top: 10px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
>h4 {
|
||||
> h4 {
|
||||
margin: 0;
|
||||
|
||||
span {
|
||||
@@ -15,4 +15,39 @@
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
a.LearnAboutTeamsLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 18px;
|
||||
column-gap: 10px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(var(--denim-button-bg-rgb), 0.08);
|
||||
font-size: 14px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import AnnouncementBar from 'components/announcement_bar';
|
||||
import BackButton from 'components/common/back_button';
|
||||
import InfiniteScroll from 'components/common/infinite_scroll';
|
||||
import SiteNameAndDescription from 'components/common/site_name_and_description';
|
||||
import ExternalLink from 'components/external_link';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import LearnAboutTeamsLink from 'components/main_menu/learn_about_teams_link';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import LogoutIcon from 'components/widgets/icons/fa_logout_icon';
|
||||
|
||||
@@ -28,10 +28,12 @@ import Constants from 'utils/constants';
|
||||
import * as UserAgent from 'utils/user_agent';
|
||||
|
||||
import SelectTeamItem from './components/select_team_item';
|
||||
|
||||
import './select_team.scss';
|
||||
|
||||
export const TEAMS_PER_PAGE = 30;
|
||||
const TEAM_MEMBERSHIP_DENIAL_ERROR_ID = 'api.team.add_members.user_denied';
|
||||
const MATTERMOST_ACADEMY_TEAM_TRAINING_LINK = 'https://mattermost.com/pl/mattermost-academy-team-training';
|
||||
|
||||
type Actions = {
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => any;
|
||||
@@ -289,7 +291,20 @@ export default class SelectTeam extends React.PureComponent<Props, State> {
|
||||
defaultMessage='Teams you can join: '
|
||||
/>
|
||||
</h4>
|
||||
<LearnAboutTeamsLink/>
|
||||
<ExternalLink
|
||||
location='learn_about_teams'
|
||||
href={MATTERMOST_ACADEMY_TEAM_TRAINING_LINK}
|
||||
className='LearnAboutTeamsLink'
|
||||
>
|
||||
<i
|
||||
className='icon icon-lightbulb-outline'
|
||||
aria-hidden={true}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='learn_about_teams'
|
||||
defaultMessage='Learn about teams'
|
||||
/>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<InfiniteScroll
|
||||
callBack={this.fetchMoreTeams}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/sidebar should match empty div snapshot when teamId is missing 1`] = `<div />`;
|
||||
|
||||
exports[`components/sidebar should match snapshot 1`] = `
|
||||
<ResizableLhs
|
||||
className=""
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
canCreateChannel={true}
|
||||
canCreateCustomGroups={true}
|
||||
canJoinPublicChannel={true}
|
||||
handleOpenDirectMessagesModal={[Function]}
|
||||
invitePeopleModal={[Function]}
|
||||
showCreateCategoryModal={[Function]}
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
unreadFilterEnabled={false}
|
||||
/>
|
||||
<div
|
||||
aria-label="channel navigator region"
|
||||
className="a11y__region"
|
||||
data-a11y-sort-order="6"
|
||||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
>
|
||||
<Pluggable
|
||||
pluggableName="LeftSidebarHeader"
|
||||
/>
|
||||
</div>
|
||||
<Connect(injectIntl(SidebarList))
|
||||
handleOpenMoreDirectChannelsModal={[Function]}
|
||||
onDragEnd={[Function]}
|
||||
onDragStart={[Function]}
|
||||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
</ResizableLhs>
|
||||
`;
|
||||
|
||||
exports[`components/sidebar should match snapshot when direct channels modal is open 1`] = `
|
||||
<ResizableLhs
|
||||
className=""
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
canCreateChannel={true}
|
||||
canCreateCustomGroups={true}
|
||||
canJoinPublicChannel={true}
|
||||
handleOpenDirectMessagesModal={[Function]}
|
||||
invitePeopleModal={[Function]}
|
||||
showCreateCategoryModal={[Function]}
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
unreadFilterEnabled={false}
|
||||
/>
|
||||
<div
|
||||
aria-label="channel navigator region"
|
||||
className="a11y__region"
|
||||
data-a11y-sort-order="6"
|
||||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
>
|
||||
<Pluggable
|
||||
pluggableName="LeftSidebarHeader"
|
||||
/>
|
||||
</div>
|
||||
<Connect(injectIntl(SidebarList))
|
||||
handleOpenMoreDirectChannelsModal={[Function]}
|
||||
onDragEnd={[Function]}
|
||||
onDragStart={[Function]}
|
||||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
<MoreDirectChannels
|
||||
focusOriginElement="newDirectMessageButton"
|
||||
isExistingChannel={false}
|
||||
onModalDismissed={[Function]}
|
||||
/>
|
||||
</ResizableLhs>
|
||||
`;
|
||||
|
||||
exports[`components/sidebar should match snapshot when more channels modal is open 1`] = `
|
||||
<ResizableLhs
|
||||
className=""
|
||||
id="SidebarContainer"
|
||||
>
|
||||
<SidebarHeader
|
||||
canCreateChannel={true}
|
||||
canCreateCustomGroups={true}
|
||||
canJoinPublicChannel={true}
|
||||
handleOpenDirectMessagesModal={[Function]}
|
||||
invitePeopleModal={[Function]}
|
||||
showCreateCategoryModal={[Function]}
|
||||
showCreateUserGroupModal={[Function]}
|
||||
showMoreChannelsModal={[Function]}
|
||||
showNewChannelModal={[Function]}
|
||||
unreadFilterEnabled={false}
|
||||
/>
|
||||
<div
|
||||
aria-label="channel navigator region"
|
||||
className="a11y__region"
|
||||
data-a11y-sort-order="6"
|
||||
id="lhsNavigator"
|
||||
role="application"
|
||||
>
|
||||
<Connect(injectIntl(ChannelNavigator)) />
|
||||
</div>
|
||||
<div
|
||||
className="sidebar--left__icons"
|
||||
>
|
||||
<Pluggable
|
||||
pluggableName="LeftSidebarHeader"
|
||||
/>
|
||||
</div>
|
||||
<Connect(injectIntl(SidebarList))
|
||||
handleOpenMoreDirectChannelsModal={[Function]}
|
||||
onDragEnd={[Function]}
|
||||
onDragStart={[Function]}
|
||||
/>
|
||||
<Connect(DataPrefetch) />
|
||||
</ResizableLhs>
|
||||
`;
|
||||
@@ -1,113 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
|
||||
const HeaderLine = styled.div`
|
||||
display: flex;
|
||||
padding: 2px 16px 0 0;
|
||||
flex-grow: 1;
|
||||
user-select: none;
|
||||
color: var(--sidebar-header-text-color);
|
||||
`;
|
||||
|
||||
const VerticalStack = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
teamDescription?: string;
|
||||
teamId?: string;
|
||||
currentUser: UserProfile;
|
||||
teamDisplayName?: string;
|
||||
actions: Actions;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
};
|
||||
|
||||
export default class Contents extends React.PureComponent<Props> {
|
||||
handleCustomStatusEmojiClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const customStatusInputModalData = {
|
||||
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||
dialogType: CustomStatusModal,
|
||||
};
|
||||
this.props.actions.openModal(customStatusInputModalData);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.currentUser || !this.props.teamId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let teamNameWithToolTip = (
|
||||
<h1
|
||||
id='headerTeamName'
|
||||
className='team__name'
|
||||
data-teamid={this.props.teamId}
|
||||
>
|
||||
{this.props.teamDisplayName}
|
||||
</h1>
|
||||
);
|
||||
|
||||
if (this.props.teamDescription) {
|
||||
teamNameWithToolTip = (
|
||||
<WithTooltip
|
||||
title={this.props.teamDescription}
|
||||
>
|
||||
{teamNameWithToolTip}
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='SidebarHeaderDropdownButton'
|
||||
id='sidebarHeaderDropdownButton'
|
||||
>
|
||||
<HeaderLine
|
||||
id='headerInfo'
|
||||
className='header__info'
|
||||
>
|
||||
<VerticalStack>
|
||||
{teamNameWithToolTip}
|
||||
<div
|
||||
id='headerInfoContent'
|
||||
className='header__info__content'
|
||||
>
|
||||
<div
|
||||
id='headerUsername'
|
||||
className='user__name'
|
||||
>
|
||||
{'@' + this.props.currentUser.username}
|
||||
</div>
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
emojiStyle={{
|
||||
verticalAlign: 'top',
|
||||
marginLeft: 2,
|
||||
}}
|
||||
onClick={this.handleCustomStatusEmojiClick as unknown as () => void}
|
||||
/>
|
||||
</div>
|
||||
</VerticalStack>
|
||||
</HeaderLine>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +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 {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import Contents from './contents';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
teamDescription: currentTeam?.description,
|
||||
teamDisplayName: currentTeam?.display_name,
|
||||
teamId: currentTeam?.id,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
openModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Contents);
|
||||
@@ -1,5 +1,34 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import MobileSidebarHeader from './mobile_sidebar_header';
|
||||
export default MobileSidebarHeader;
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
return {
|
||||
username: currentUser?.username,
|
||||
teamDisplayName: currentTeam?.display_name ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openModal,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(MobileSidebarHeader);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
.mobileSidebarHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
gap: 4px;
|
||||
|
||||
h1 {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
color: var(--sidebar-header-text-color);
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__username {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
color: rgba(var(--sidebar-header-text-color-rgb), 0.75);
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 14px;
|
||||
gap: 2px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
span:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,45 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import type {MouseEvent, KeyboardEvent} from 'react';
|
||||
|
||||
import Contents from './contents';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||
|
||||
export default function MobileSidebarHeader() {
|
||||
const intl = useIntl();
|
||||
const ariaLabel = intl.formatMessage({id: 'accessibility.sections.lhsHeader', defaultMessage: 'team menu region'});
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
import './mobile_sidebar_header.scss';
|
||||
|
||||
type Props = PropsFromRedux;
|
||||
|
||||
export default function MobileSidebarHeader(props: Props) {
|
||||
if (!props.username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleCustomStatusEmojiClick(event: MouseEvent<HTMLSpanElement> | KeyboardEvent<HTMLSpanElement>) {
|
||||
event.stopPropagation();
|
||||
|
||||
const customStatusInputModalData = {
|
||||
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||
dialogType: CustomStatusModal,
|
||||
};
|
||||
props.openModal(customStatusInputModalData);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id='lhsHeader'
|
||||
aria-label={ariaLabel}
|
||||
tabIndex={-1}
|
||||
role='application'
|
||||
className='SidebarHeader team__header theme a11y__region'
|
||||
data-a11y-sort-order='5'
|
||||
>
|
||||
<div
|
||||
className='d-flex'
|
||||
>
|
||||
<Contents/>
|
||||
<div className='mobileSidebarHeader'>
|
||||
<h1>{props.teamDisplayName}</h1>
|
||||
<div className='mobileSidebarHeader__username'>
|
||||
<span>{'@' + props.username}</span>
|
||||
<CustomStatusEmoji
|
||||
showTooltip={false}
|
||||
emojiStyle={{
|
||||
verticalAlign: 'top',
|
||||
}}
|
||||
onClick={handleCustomStatusEmojiClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// 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 {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {Permissions, Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import mergeObjects from 'packages/mattermost-redux/test/merge_objects';
|
||||
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
|
||||
import {fireEvent, renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
|
||||
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
@@ -18,14 +17,61 @@ import type {GlobalState} from 'types/store';
|
||||
import Sidebar from './sidebar';
|
||||
|
||||
describe('components/sidebar', () => {
|
||||
const currentTeamId = 'fake_team_id';
|
||||
const currentTeam = TestHelper.getTeamMock({
|
||||
id: 'current_team_id',
|
||||
display_name: 'Current Test Team',
|
||||
});
|
||||
|
||||
const initialState: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: currentTeam.id,
|
||||
teams: {
|
||||
[currentTeam.id]: currentTeam,
|
||||
},
|
||||
myMembers: {
|
||||
[currentTeam.id]: {
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_admin: {
|
||||
permissions: [Permissions.MANAGE_TEAM],
|
||||
},
|
||||
system_user: {
|
||||
permissions: [],
|
||||
},
|
||||
team_user: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
canCreatePublicChannel: true,
|
||||
canCreatePrivateChannel: true,
|
||||
canJoinPublicChannel: true,
|
||||
isOpen: false,
|
||||
teamId: currentTeamId,
|
||||
teamId: currentTeam.id,
|
||||
hasSeenModal: true,
|
||||
isCloud: false,
|
||||
unreadFilterEnabled: false,
|
||||
@@ -43,94 +89,36 @@ describe('components/sidebar', () => {
|
||||
},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('should render the sidebar components correctly', () => {
|
||||
renderWithContext(
|
||||
<Sidebar {...baseProps}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
// Check for SidebarContainer that is the parent of the sidebar
|
||||
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('application', {name: /channel sidebar region/i})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should match snapshot when direct channels modal is open', () => {
|
||||
const wrapper = shallow(
|
||||
<Sidebar {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.instance().setState({showDirectChannelsModal: true});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when more channels modal is open', () => {
|
||||
const wrapper = shallow(
|
||||
<Sidebar {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.instance().setState({showMoreChannelsModal: true});
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('Should call Shortcut modal on FORWARD_SLASH+ctrl/meta', () => {
|
||||
const wrapper = shallow<Sidebar>(
|
||||
<Sidebar {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
let key = Constants.KeyCodes.BACK_SLASH[0] as string;
|
||||
let keyCode = Constants.KeyCodes.BACK_SLASH[1] as number;
|
||||
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
|
||||
expect(wrapper.instance().props.actions.openModal).not.toHaveBeenCalled();
|
||||
|
||||
key = 'ù';
|
||||
keyCode = Constants.KeyCodes.FORWARD_SLASH[1] as number;
|
||||
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
|
||||
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
|
||||
|
||||
key = '/';
|
||||
keyCode = Constants.KeyCodes.SEVEN[1] as number;
|
||||
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
|
||||
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
|
||||
|
||||
key = Constants.KeyCodes.FORWARD_SLASH[0] as string;
|
||||
keyCode = Constants.KeyCodes.FORWARD_SLASH[1] as number;
|
||||
instance.handleKeyDownEvent({ctrlKey: true, preventDefault: jest.fn(), key, keyCode} as any);
|
||||
expect(wrapper.instance().props.actions.openModal).toHaveBeenCalledWith(expect.objectContaining({modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL}));
|
||||
});
|
||||
|
||||
test('should toggle direct messages modal correctly', () => {
|
||||
const wrapper = shallow<Sidebar>(
|
||||
<Sidebar {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
const mockEvent: Partial<Event> = {preventDefault: jest.fn()};
|
||||
|
||||
instance.hideMoreDirectChannelsModal = jest.fn();
|
||||
instance.showMoreDirectChannelsModal = jest.fn();
|
||||
|
||||
instance.handleOpenMoreDirectChannelsModal(mockEvent as any);
|
||||
expect(instance.showMoreDirectChannelsModal).toHaveBeenCalled();
|
||||
|
||||
instance.setState({showDirectChannelsModal: true});
|
||||
instance.handleOpenMoreDirectChannelsModal(mockEvent as any);
|
||||
expect(instance.hideMoreDirectChannelsModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should match empty div snapshot when teamId is missing', () => {
|
||||
test('should not rendering anything when teamId is missing', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
teamId: '',
|
||||
};
|
||||
const wrapper = shallow(
|
||||
renderWithContext(
|
||||
<Sidebar {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(screen.queryByRole('application', {name: /channel sidebar region/i})).toBeNull();
|
||||
});
|
||||
|
||||
describe('unreads category', () => {
|
||||
const currentUserId = 'current_user_id';
|
||||
|
||||
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: currentTeamId});
|
||||
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: currentTeamId});
|
||||
const channel1 = TestHelper.getChannelMock({id: 'channel1', team_id: currentTeam.id});
|
||||
const channel2 = TestHelper.getChannelMock({id: 'channel2', team_id: currentTeam.id});
|
||||
|
||||
const baseState: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
@@ -141,7 +129,7 @@ describe('components/sidebar', () => {
|
||||
channel2,
|
||||
},
|
||||
channelsInTeam: {
|
||||
[currentTeamId]: new Set([channel1.id, channel2.id]),
|
||||
[currentTeam.id]: new Set([channel1.id, channel2.id]),
|
||||
},
|
||||
messageCounts: {
|
||||
channel1: {total: 10},
|
||||
@@ -153,13 +141,40 @@ describe('components/sidebar', () => {
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId,
|
||||
currentTeamId: currentTeam.id,
|
||||
teams: {
|
||||
[currentTeamId]: TestHelper.getTeamMock({id: currentTeamId}),
|
||||
[currentTeam.id]: currentTeam,
|
||||
},
|
||||
myMembers: {
|
||||
[currentTeam.id]: {
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId,
|
||||
profiles: {
|
||||
[currentUserId]: {
|
||||
id: currentUserId,
|
||||
roles: 'system_user system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_admin: {
|
||||
permissions: [Permissions.MANAGE_TEAM],
|
||||
},
|
||||
system_user: {
|
||||
permissions: [],
|
||||
},
|
||||
team_user: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -263,4 +278,111 @@ describe('components/sidebar', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modals', () => {
|
||||
test('should call Shortcut modal on FORWARD_SLASH+ctrl/meta', () => {
|
||||
const openModalSpy = jest.fn();
|
||||
const closeModalSpy = jest.fn();
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
isKeyBoardShortcutModalOpen: false,
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
openModal: openModalSpy,
|
||||
closeModal: closeModalSpy,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<Sidebar {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
|
||||
|
||||
// Test with backslash key (should not trigger the modal)
|
||||
fireEvent.keyDown(document, {
|
||||
key: '\\',
|
||||
code: 'Backslash',
|
||||
keyCode: Constants.KeyCodes.BACK_SLASH[1],
|
||||
ctrlKey: true,
|
||||
});
|
||||
expect(openModalSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Test with 'ù' key but with forward slash keyCode (should trigger the modal)
|
||||
fireEvent.keyDown(document, {
|
||||
key: 'ù',
|
||||
code: 'Slash',
|
||||
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
|
||||
ctrlKey: true,
|
||||
});
|
||||
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
|
||||
}));
|
||||
|
||||
// Reset the spy
|
||||
openModalSpy.mockClear();
|
||||
|
||||
// Test with '/' key but with seven keyCode (should trigger the modal)
|
||||
fireEvent.keyDown(document, {
|
||||
key: '/',
|
||||
code: 'Digit7',
|
||||
keyCode: Constants.KeyCodes.SEVEN[1],
|
||||
ctrlKey: true,
|
||||
});
|
||||
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
|
||||
}));
|
||||
|
||||
// Reset the spy
|
||||
openModalSpy.mockClear();
|
||||
|
||||
// Test with forward slash key (should trigger the modal)
|
||||
fireEvent.keyDown(document, {
|
||||
key: '/',
|
||||
code: 'Slash',
|
||||
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
|
||||
ctrlKey: true,
|
||||
});
|
||||
expect(openModalSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
modalId: ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL,
|
||||
}));
|
||||
});
|
||||
|
||||
test('should close Shortcut modal on FORWARD_SLASH+ctrl/meta when already open', () => {
|
||||
const openModalSpy = jest.fn();
|
||||
const closeModalSpy = jest.fn();
|
||||
|
||||
const props = {
|
||||
...baseProps,
|
||||
isKeyBoardShortcutModalOpen: true, // Modal is already open
|
||||
actions: {
|
||||
...baseProps.actions,
|
||||
openModal: openModalSpy,
|
||||
closeModal: closeModalSpy,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<Sidebar {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(document.getElementById('SidebarContainer')).toBeInTheDocument();
|
||||
|
||||
// Test with forward slash key (should close the modal since it's already open)
|
||||
fireEvent.keyDown(document, {
|
||||
key: '/',
|
||||
code: 'Slash',
|
||||
keyCode: Constants.KeyCodes.FORWARD_SLASH[1],
|
||||
ctrlKey: true,
|
||||
});
|
||||
|
||||
// Should call closeModal with the keyboard shortcuts modal ID
|
||||
expect(closeModalSpy).toHaveBeenCalledWith(ModalIdentifiers.KEYBOARD_SHORTCUTS_MODAL);
|
||||
|
||||
// Should not call openModal
|
||||
expect(openModalSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,7 +17,8 @@ import * as Menu from 'components/menu';
|
||||
import {OnboardingTourSteps} from 'components/tours';
|
||||
import {useShowOnboardingTutorialStep, CreateAndJoinChannelsTour, InvitePeopleTour} from 'components/tours/onboarding_tour';
|
||||
|
||||
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU = 'browseOrAddChannelMenuButton';
|
||||
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU = 'browserOrAddChannelMenu';
|
||||
export const ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON = 'browseOrAddChannelMenuButton';
|
||||
|
||||
type Props = {
|
||||
canCreateChannel: boolean;
|
||||
@@ -52,6 +53,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
/>
|
||||
)}
|
||||
trailingElements={showCreateAndJoinChannelsTutorialTip && <CreateAndJoinChannelsTour/>}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -69,6 +71,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
defaultMessage='Browse channels'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,6 +87,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
defaultMessage='Open a direct message'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -100,6 +104,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
defaultMessage='Create new user group'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -117,6 +122,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
defaultMessage='Create new category'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -139,13 +145,14 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
</>
|
||||
)}
|
||||
trailingElements={showInvitePeopleTutorialTip && <InvitePeopleTour/>}
|
||||
aria-haspopup='true'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU,
|
||||
id: ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON,
|
||||
'aria-label': formatMessage({
|
||||
id: 'sidebarLeft.browserOrCreateChannelMenuButton.label',
|
||||
defaultMessage: 'Browse or create channels',
|
||||
@@ -158,7 +165,7 @@ export default function SidebarBrowserOrAddChannelMenu(props: Props) {
|
||||
}}
|
||||
menu={{
|
||||
id: 'browserOrAddChannelMenu',
|
||||
'aria-labelledby': ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU,
|
||||
'aria-labelledby': ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON,
|
||||
}}
|
||||
>
|
||||
{createNewChannelMenuItem}
|
||||
|
||||
@@ -6,31 +6,31 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
color: var(--sidebar-text);
|
||||
cursor: pointer;
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
|
||||
.title {
|
||||
display: inline-block;
|
||||
button#sidebarTeamMenuButton {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-chevron-down {
|
||||
margin-right: -1px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
#SidebarContainer & {
|
||||
padding: 2px 6px;
|
||||
font-family: Metropolis, sans-serif;
|
||||
font-size: 16px;
|
||||
gap: 1px;
|
||||
|
||||
& > span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& > .icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebarTeamMenu {
|
||||
.learnAboutTeamsMenuItem {
|
||||
.leading-element,.label-elements>span {
|
||||
color: var(--link-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,141 +1,156 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {renderWithContext, screen} from 'tests/react_testing_utils';
|
||||
import {CloudProducts} from 'utils/constants';
|
||||
import {FileSizes} from 'utils/file_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import BrowserOrAddChannelMenu from './sidebar_browse_or_add_channel_menu';
|
||||
import SidebarHeader from './sidebar_header';
|
||||
import type {Props} from './sidebar_header';
|
||||
|
||||
let props: Props;
|
||||
describe('SidebarHeader', () => {
|
||||
const defaultProps: Props = {
|
||||
showNewChannelModal: jest.fn(),
|
||||
showMoreChannelsModal: jest.fn(),
|
||||
invitePeopleModal: jest.fn(),
|
||||
showCreateCategoryModal: jest.fn(),
|
||||
canCreateChannel: true,
|
||||
canJoinPublicChannel: true,
|
||||
handleOpenDirectMessagesModal: jest.fn(),
|
||||
unreadFilterEnabled: true,
|
||||
showCreateUserGroupModal: jest.fn(),
|
||||
canCreateCustomGroups: true,
|
||||
};
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
let mockState: any;
|
||||
const team = TestHelper.getTeamMock({
|
||||
display_name: 'Steadfast',
|
||||
});
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
describe('Components/SidebarHeader', () => {
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
showNewChannelModal: jest.fn(),
|
||||
showMoreChannelsModal: jest.fn(),
|
||||
invitePeopleModal: jest.fn(),
|
||||
showCreateCategoryModal: jest.fn(),
|
||||
canCreateChannel: true,
|
||||
canJoinPublicChannel: true,
|
||||
handleOpenDirectMessagesModal: jest.fn(),
|
||||
unreadFilterEnabled: true,
|
||||
showCreateUserGroupModal: jest.fn(),
|
||||
canCreateCustomGroups: true,
|
||||
};
|
||||
|
||||
mockState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
const initialState = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: team.id,
|
||||
teams: {
|
||||
[team.id]: team,
|
||||
},
|
||||
preferences: {
|
||||
myPreferences: {},
|
||||
myMembers: {
|
||||
[team.id]: {
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
profiles: {
|
||||
uid: {
|
||||
id: 'uid',
|
||||
roles: 'system_user system_admin',
|
||||
},
|
||||
},
|
||||
currentUserId: 'uid',
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_admin: {
|
||||
permissions: [Permissions.MANAGE_TEAM],
|
||||
},
|
||||
system_user: {
|
||||
permissions: [],
|
||||
},
|
||||
team_user: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
usage: {
|
||||
integrations: {
|
||||
enabled: 11,
|
||||
enabledLoaded: true,
|
||||
},
|
||||
messages: {
|
||||
history: 10000,
|
||||
historyLoaded: true,
|
||||
},
|
||||
files: {
|
||||
totalStorage: FileSizes.Gigabyte,
|
||||
totalStorageLoaded: true,
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'currentteam',
|
||||
teams: {
|
||||
currentteam: {
|
||||
id: 'currentteam',
|
||||
description: 'et iste illum reprehenderit aliquid in rem itaque in maxime eius.',
|
||||
},
|
||||
active: 1,
|
||||
teamsLoaded: true,
|
||||
},
|
||||
boards: {
|
||||
cards: 500,
|
||||
cardsLoaded: true,
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
product_id: 'test_prod_1',
|
||||
trial_end_at: 1652807380,
|
||||
is_free_trial: 'false',
|
||||
},
|
||||
products: {
|
||||
test_prod_1: {
|
||||
id: 'test_prod_1',
|
||||
sku: CloudProducts.STARTER,
|
||||
price_per_seat: 0,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
profiles: {
|
||||
uid: {},
|
||||
},
|
||||
currentUserId: 'uid',
|
||||
},
|
||||
usage: {
|
||||
integrations: {
|
||||
enabled: 11,
|
||||
enabledLoaded: true,
|
||||
},
|
||||
messages: {
|
||||
history: 10000,
|
||||
historyLoaded: true,
|
||||
},
|
||||
files: {
|
||||
totalStorage: FileSizes.Gigabyte,
|
||||
totalStorageLoaded: true,
|
||||
},
|
||||
teams: {
|
||||
active: 1,
|
||||
teamsLoaded: true,
|
||||
},
|
||||
boards: {
|
||||
cards: 500,
|
||||
cardsLoaded: true,
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
product_id: 'test_prod_1',
|
||||
trial_end_at: 1652807380,
|
||||
is_free_trial: 'false',
|
||||
},
|
||||
products: {
|
||||
test_prod_1: {
|
||||
id: 'test_prod_1',
|
||||
sku: CloudProducts.STARTER,
|
||||
price_per_seat: 0,
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
limitsLoaded: true,
|
||||
limits: {
|
||||
limitsLoaded: true,
|
||||
limits: {
|
||||
integrations: {
|
||||
enabled: 10,
|
||||
},
|
||||
messages: {
|
||||
history: 10000,
|
||||
},
|
||||
files: {
|
||||
total_storage: FileSizes.Gigabyte,
|
||||
},
|
||||
teams: {
|
||||
active: 1,
|
||||
},
|
||||
boards: {
|
||||
cards: 500,
|
||||
views: 5,
|
||||
},
|
||||
integrations: {
|
||||
enabled: 10,
|
||||
},
|
||||
messages: {
|
||||
history: 10000,
|
||||
},
|
||||
files: {
|
||||
total_storage: FileSizes.Gigabyte,
|
||||
},
|
||||
teams: {
|
||||
active: 1,
|
||||
},
|
||||
boards: {
|
||||
cards: 500,
|
||||
views: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
addChannelDropdown: {
|
||||
isOpen: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
test('should render the team menu button', () => {
|
||||
renderWithContext(<SidebarHeader {...defaultProps}/>, initialState);
|
||||
|
||||
expect(screen.getByText('Steadfast')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', {name: team.display_name})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show BrowserOrAddChannelMenu', () => {
|
||||
const wrapper = shallow(<SidebarHeader {...props}/>);
|
||||
expect(wrapper.find(BrowserOrAddChannelMenu).length).toBe(1);
|
||||
test('should render the \'Browse or create channels\' menu button', () => {
|
||||
renderWithContext(<SidebarHeader {...defaultProps}/>, initialState);
|
||||
|
||||
expect(screen.getByRole('button', {name: /Browse or create channels/i})).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should embed teams menu dropdown into heading', () => {
|
||||
const wrapper = shallow(<SidebarHeader {...props}/>);
|
||||
expect(wrapper.find(IconButton).length).toBe(0);
|
||||
expect(wrapper.find('i').prop('className')).toBe('icon icon-chevron-down');
|
||||
test('should not render anything when team is empty', () => {
|
||||
const state = {...initialState};
|
||||
state.entities.teams.currentTeamId = '';
|
||||
renderWithContext(<SidebarHeader {...defaultProps}/>, state);
|
||||
|
||||
expect(screen.queryByRole('button', {name: team.display_name})).toBeNull();
|
||||
expect(screen.queryByRole('button', {name: /Add Channel Dropdown/i})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import React from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
|
||||
import MainMenu from 'components/main_menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import WithTooltip from 'components/with_tooltip';
|
||||
|
||||
import SidebarBrowseOrAddChannelMenu from './sidebar_browse_or_add_channel_menu';
|
||||
import SidebarTeamMenu from './sidebar_team_menu';
|
||||
|
||||
import './sidebar_header.scss';
|
||||
|
||||
@@ -30,52 +26,14 @@ export type Props = {
|
||||
|
||||
const SidebarHeader = (props: Props) => {
|
||||
const currentTeam = useSelector(getCurrentTeam);
|
||||
const usageDeltas = useGetUsageDeltas();
|
||||
|
||||
const [menuToggled, setMenuToggled] = useState(false);
|
||||
|
||||
const handleMenuToggle = () => {
|
||||
setMenuToggled(!menuToggled);
|
||||
};
|
||||
|
||||
if (!currentTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
id='sidebar-header-container'
|
||||
className='sidebarHeaderContainer'
|
||||
>
|
||||
<MenuWrapper
|
||||
onToggle={handleMenuToggle}
|
||||
className='SidebarHeaderMenuWrapper test-team-header'
|
||||
>
|
||||
<WithTooltip
|
||||
title={currentTeam.description ? currentTeam.description : currentTeam.display_name}
|
||||
>
|
||||
<h1 className='sidebarHeader'>
|
||||
<button
|
||||
className='style--none sidebar-header'
|
||||
type='button'
|
||||
aria-haspopup='menu'
|
||||
aria-expanded={menuToggled}
|
||||
aria-controls='sidebarDropdownMenu'
|
||||
id='sidebarDropdownMenuButton'
|
||||
>
|
||||
<span className='title'>{currentTeam.display_name}</span>
|
||||
<i
|
||||
className='icon icon-chevron-down'
|
||||
aria-hidden={true}
|
||||
/>
|
||||
</button>
|
||||
</h1>
|
||||
</WithTooltip>
|
||||
<MainMenu
|
||||
id='sidebarDropdownMenu'
|
||||
usageDeltaTeams={usageDeltas.teams.active}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
<header className='sidebarHeaderContainer'>
|
||||
<SidebarTeamMenu currentTeam={currentTeam}/>
|
||||
{(props.canCreateChannel || props.canJoinPublicChannel) && (
|
||||
<SidebarBrowseOrAddChannelMenu
|
||||
canCreateChannel={props.canCreateChannel}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type {DeepPartial} from '@mattermost/types/utilities';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import SidebarTeamMenu from './sidebar_team_menu';
|
||||
|
||||
describe('components/sidebar/sidebar_header/sidebar_team_menu', () => {
|
||||
const currentTeam = TestHelper.getTeamMock({
|
||||
id: 'team-id',
|
||||
name: 'team-name',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
});
|
||||
|
||||
const initialState: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
EnableGuestAccounts: 'true',
|
||||
},
|
||||
license: {
|
||||
IsLicensed: 'true',
|
||||
LDAPGroups: 'true',
|
||||
},
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: currentTeam.id,
|
||||
teams: {
|
||||
[currentTeam.id]: currentTeam,
|
||||
},
|
||||
myMembers: {
|
||||
[currentTeam.id]: {
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'current-user-id',
|
||||
profiles: {
|
||||
'current-user-id': {
|
||||
id: 'current-user-id',
|
||||
roles: 'system_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
system_user: {
|
||||
permissions: [Permissions.CREATE_TEAM],
|
||||
},
|
||||
team_user: {
|
||||
permissions: [
|
||||
Permissions.ADD_USER_TO_TEAM,
|
||||
Permissions.INVITE_GUEST,
|
||||
Permissions.MANAGE_TEAM,
|
||||
Permissions.REMOVE_USER_FROM_TEAM,
|
||||
Permissions.MANAGE_TEAM_ROLES,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
subscription: {
|
||||
is_free_trial: 'false',
|
||||
},
|
||||
products: {},
|
||||
},
|
||||
usage: {
|
||||
files: {
|
||||
totalStorage: 0,
|
||||
totalStorageLoaded: true,
|
||||
},
|
||||
messages: {
|
||||
history: 0,
|
||||
historyLoaded: true,
|
||||
},
|
||||
teams: {
|
||||
active: 1,
|
||||
cloudArchived: 0,
|
||||
teamsLoaded: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
components: {
|
||||
MainMenu: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
currentTeam,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should open team menu when clicked', async () => {
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
const menuButton = screen.getByText(currentTeam.display_name);
|
||||
expect(menuButton).toBeInTheDocument();
|
||||
menuButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Team settings')).toBeInTheDocument();
|
||||
expect(screen.getByText('Manage members')).toBeInTheDocument();
|
||||
expect(screen.getByText('Leave team')).toBeInTheDocument();
|
||||
expect(screen.getByText('Create a team')).toBeInTheDocument();
|
||||
expect(screen.getByText('Learn about teams')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show leave team option when primary team is not set', async () => {
|
||||
// State with no primary team set
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
general: {
|
||||
...initialState.entities?.general,
|
||||
config: {
|
||||
...initialState.entities?.general?.config,
|
||||
ExperimentalPrimaryTeam: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
state,
|
||||
);
|
||||
|
||||
screen.getByText(currentTeam.display_name).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Leave team')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should hide leave team option when experimentalPrimaryTeam is same as current team', async () => {
|
||||
// State with current team set as primary team
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
general: {
|
||||
...initialState.entities?.general,
|
||||
config: {
|
||||
...initialState.entities?.general?.config,
|
||||
ExperimentalPrimaryTeam: currentTeam.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
state,
|
||||
);
|
||||
|
||||
screen.getByText(currentTeam.display_name).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Leave team')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display plugin menu items when available', async () => {
|
||||
// State with plugin components
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
...initialState,
|
||||
plugins: {
|
||||
...initialState.plugins,
|
||||
components: {
|
||||
...initialState.plugins?.components,
|
||||
MainMenu: [
|
||||
{
|
||||
id: 'plugin-1',
|
||||
pluginId: 'plugin-1',
|
||||
text: 'Plugin Menu Item 1',
|
||||
action: jest.fn(),
|
||||
mobileIcon: <i className='icon-plugin-1'/>,
|
||||
},
|
||||
{
|
||||
id: 'plugin-2',
|
||||
pluginId: 'plugin-2',
|
||||
text: 'Plugin Menu Item 2',
|
||||
action: jest.fn(),
|
||||
mobileIcon: <i className='icon-plugin-2'/>,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
state,
|
||||
);
|
||||
|
||||
screen.getByText(currentTeam.display_name).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Plugin Menu Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Plugin Menu Item 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should hide "Invite people" option when user lacks permission to invite', async () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
roles: {
|
||||
...initialState.entities?.roles,
|
||||
roles: {
|
||||
...initialState.entities?.roles?.roles,
|
||||
team_user: {
|
||||
permissions: [
|
||||
Permissions.ADD_USER_TO_TEAM,
|
||||
Permissions.MANAGE_TEAM,
|
||||
Permissions.REMOVE_USER_FROM_TEAM,
|
||||
Permissions.MANAGE_TEAM_ROLES,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
state,
|
||||
);
|
||||
|
||||
screen.getByText(currentTeam.display_name).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Invite people')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show restricted indicator for "Create a team" on cloud free plan', async () => {
|
||||
// State with cloud free plan
|
||||
const stateWithCloudFree: DeepPartial<GlobalState> = {
|
||||
...initialState,
|
||||
entities: {
|
||||
...initialState.entities,
|
||||
general: {
|
||||
...initialState.entities?.general,
|
||||
license: {
|
||||
...initialState.entities?.general?.license,
|
||||
Cloud: 'true',
|
||||
},
|
||||
},
|
||||
cloud: {
|
||||
...initialState.entities?.cloud,
|
||||
subscription: {
|
||||
is_free_trial: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<SidebarTeamMenu {...baseProps}/>,
|
||||
stateWithCloudFree,
|
||||
);
|
||||
|
||||
screen.getByText(currentTeam.display_name).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Create a team')).toBeInTheDocument();
|
||||
|
||||
// Verify the RestrictedIndicator is rendered
|
||||
expect(document.querySelector('.RestrictedIndicator__icon-tooltip')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
|
||||
import {
|
||||
LightbulbOutlineIcon,
|
||||
AccountPlusOutlineIcon,
|
||||
AccountMultiplePlusOutlineIcon,
|
||||
SettingsOutlineIcon,
|
||||
AccountMultipleOutlineIcon,
|
||||
ExitToAppIcon,
|
||||
MessagePlusOutlineIcon,
|
||||
PlusIcon,
|
||||
MonitorAccountIcon,
|
||||
} from '@mattermost/compass-icons/components';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles_helpers';
|
||||
import {getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getMainMenuPluginComponents} from 'selectors/plugins';
|
||||
|
||||
import AddGroupsToTeamModal from 'components/add_groups_to_team_modal';
|
||||
import useGetUsageDeltas from 'components/common/hooks/useGetUsageDeltas';
|
||||
import InvitationModal from 'components/invitation_modal';
|
||||
import LeaveTeamModal from 'components/leave_team_modal';
|
||||
import * as Menu from 'components/menu';
|
||||
import TeamGroupsManageModal from 'components/team_groups_manage_modal';
|
||||
import TeamMembersModal from 'components/team_members_modal';
|
||||
import TeamSettingsModal from 'components/team_settings_modal';
|
||||
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
||||
|
||||
import {FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS} from 'utils/cloud_utils';
|
||||
import {LicenseSkus, ModalIdentifiers, MattermostFeatures, CloudProducts} from 'utils/constants';
|
||||
import {isCloudLicense} from 'utils/license_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
interface Props {
|
||||
currentTeam: Team;
|
||||
}
|
||||
|
||||
export default function SidebarTeamMenu(props: Props) {
|
||||
const license = useSelector(getLicense);
|
||||
const config = useSelector(getConfig);
|
||||
|
||||
const havePermissionToCreateTeam = useSelector((state: GlobalState) => haveISystemPermission(state, {permission: Permissions.CREATE_TEAM}));
|
||||
const havePermissionToManageTeam = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.MANAGE_TEAM));
|
||||
const havePermissionToAddUserToTeam = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.ADD_USER_TO_TEAM));
|
||||
const havePermissionToInviteGuest = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.INVITE_GUEST));
|
||||
const isCloud = isCloudLicense(license);
|
||||
const isGuestAccessEnabled = config?.EnableGuestAccounts === 'true';
|
||||
const isTeamGroupConstrained = Boolean(props.currentTeam?.group_constrained);
|
||||
const isLicensedForLDAPGroups = license?.LDAPGroups === 'true';
|
||||
const experimentalPrimaryTeam = config.ExperimentalPrimaryTeam;
|
||||
const joinableTeams = useSelector(getJoinableTeamIds);
|
||||
const haveMoreJoinableTeams = joinableTeams?.length > 0;
|
||||
const canJoinAnotherTeam = !experimentalPrimaryTeam && haveMoreJoinableTeams;
|
||||
|
||||
const tooltipText = props.currentTeam.description ? props.currentTeam.description : props.currentTeam.display_name;
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
id: 'sidebarTeamMenuButton',
|
||||
class: 'btn btn-sm btn-quaternary btn-inverted',
|
||||
children: (
|
||||
<>
|
||||
<span>{props.currentTeam.display_name}</span>
|
||||
<i className='icon icon-chevron-down'/>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
menuButtonTooltip={{
|
||||
text: tooltipText,
|
||||
}}
|
||||
menu={{
|
||||
id: 'sidebarTeamMenu',
|
||||
}}
|
||||
>
|
||||
{((isGuestAccessEnabled && havePermissionToInviteGuest) || havePermissionToAddUserToTeam) && (
|
||||
<InvitePeopleMenuItem/>
|
||||
)}
|
||||
{isTeamGroupConstrained && isLicensedForLDAPGroups && havePermissionToManageTeam && (
|
||||
<AddGroupsToTeamMenuItem/>
|
||||
)}
|
||||
{havePermissionToManageTeam && (
|
||||
<TeamSettingsMenuItem/>
|
||||
)}
|
||||
<ManageViewMembersMenuItem/>
|
||||
{(isTeamGroupConstrained && isLicensedForLDAPGroups && havePermissionToManageTeam) && (
|
||||
<ManageGroupsMenuItem
|
||||
teamID={props.currentTeam.id}
|
||||
/>
|
||||
)}
|
||||
{(!isTeamGroupConstrained && experimentalPrimaryTeam !== props.currentTeam.name) && (
|
||||
<LeaveTeamMenuItem/>
|
||||
)}
|
||||
{(canJoinAnotherTeam || havePermissionToCreateTeam) && <Menu.Separator/>}
|
||||
{canJoinAnotherTeam &&
|
||||
<JoinAnotherTeamMenuItem/>
|
||||
}
|
||||
{havePermissionToCreateTeam && (
|
||||
<CreateTeamMenuItem
|
||||
isCloud={isCloud}
|
||||
/>
|
||||
)}
|
||||
<Menu.Separator/>
|
||||
<LearnAboutTeamsMenuItem/>
|
||||
<PluginMenuItems/>
|
||||
</Menu.Container>
|
||||
);
|
||||
}
|
||||
|
||||
function InvitePeopleMenuItem(props: Menu.FirstMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.INVITATION,
|
||||
dialogType: InvitationModal,
|
||||
dialogProps: {
|
||||
focusOriginElement: 'sidebarTeamMenuButton',
|
||||
},
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={handleClick}
|
||||
leadingElement={(
|
||||
<AccountMultiplePlusOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
labels={(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.invitePeopleMenuItem.primaryLabel'
|
||||
defaultMessage='Invite people'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.invitePeopleMenuItem.secondaryLabel'
|
||||
defaultMessage='Add or invite people to the team'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
aria-haspopup='dialog'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AddGroupsToTeamMenuItem(props: Menu.FirstMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.ADD_GROUPS_TO_TEAM,
|
||||
dialogType: AddGroupsToTeamModal,
|
||||
dialogProps: {
|
||||
focusOriginElement: 'sidebarTeamMenuButton',
|
||||
},
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={handleClick}
|
||||
leadingElement={(
|
||||
<AccountPlusOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.addGroupsToTeamMenuItem.primaryLabel'
|
||||
defaultMessage='Add groups'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='dialog'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamSettingsMenuItem(props: Menu.FirstMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.TEAM_SETTINGS,
|
||||
dialogType: TeamSettingsModal,
|
||||
dialogProps: {
|
||||
focusOriginElement: 'sidebarTeamMenuButton',
|
||||
},
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<SettingsOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.teamSettingsMenuItem.primaryLabel'
|
||||
defaultMessage='Team settings'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='dialog'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ManageViewMembersMenuItem(props: Menu.FirstMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const havePermissionToRemoveUserFromTeam = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.REMOVE_USER_FROM_TEAM));
|
||||
const havePermissionToManageTeamRoles = useSelector((state: GlobalState) => haveICurrentTeamPermission(state, Permissions.MANAGE_TEAM_ROLES));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.TEAM_MEMBERS,
|
||||
dialogType: TeamMembersModal,
|
||||
dialogProps: {
|
||||
focusOriginElement: 'sidebarTeamMenuButton',
|
||||
},
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
let label = (
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.viewMembersMenuItem.primaryLabel'
|
||||
defaultMessage='View members'
|
||||
/>
|
||||
);
|
||||
if (havePermissionToRemoveUserFromTeam && havePermissionToManageTeamRoles) {
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.manageMembersMenuItem.primaryLabel'
|
||||
defaultMessage='Manage members'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<AccountMultipleOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
labels={label}
|
||||
aria-haspopup='dialog'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ManageGroupsMenuItemProps {
|
||||
teamID: Team['id'];
|
||||
}
|
||||
|
||||
function ManageGroupsMenuItem({teamID}: ManageGroupsMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.MANAGE_TEAM_GROUPS,
|
||||
dialogType: TeamGroupsManageModal,
|
||||
dialogProps: {
|
||||
teamID,
|
||||
},
|
||||
}));
|
||||
}, [dispatch, teamID]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<MonitorAccountIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.manageGroupsMenuItem.primaryLabel'
|
||||
defaultMessage='Manage groups'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='dialog'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LeaveTeamMenuItem() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.LEAVE_TEAM,
|
||||
dialogType: LeaveTeamModal,
|
||||
}));
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<ExitToAppIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
isDestructive={true}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.leaveTeamMenuItem.primaryLabel'
|
||||
defaultMessage='Leave team'
|
||||
/>
|
||||
)}
|
||||
aria-haspopup='dialog'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function JoinAnotherTeamMenuItem() {
|
||||
const history = useHistory();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
history.push('/select_team');
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<MessagePlusOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.joinAnotherTeamMenuItem.primaryLabel'
|
||||
defaultMessage='Join another team'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateTeamMenuItemProps {
|
||||
isCloud: boolean;
|
||||
}
|
||||
|
||||
function CreateTeamMenuItem({isCloud}: CreateTeamMenuItemProps) {
|
||||
const history = useHistory();
|
||||
|
||||
const cloudSubscription = useSelector(getCloudSubscription);
|
||||
const subscriptionProduct = useSelector(getSubscriptionProduct);
|
||||
const isFreeTrial = isCloud && cloudSubscription?.is_free_trial === 'true';
|
||||
const isStarterFree = isCloud && subscriptionProduct?.sku === CloudProducts.STARTER;
|
||||
const usageDeltas = useGetUsageDeltas();
|
||||
const isTeamsLimitReached = isStarterFree && !isFreeTrial && usageDeltas.teams.active >= 0;
|
||||
const isTeamCreateRestricted = isCloud && (isFreeTrial || isTeamsLimitReached);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isTeamsLimitReached || isTeamCreateRestricted) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push('/create_team');
|
||||
}, [history, isTeamsLimitReached]);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={(
|
||||
<PlusIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
onClick={handleClick}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.createTeamMenuItem.primaryLabel'
|
||||
defaultMessage='Create a team'
|
||||
/>
|
||||
)}
|
||||
trailingElements={isTeamCreateRestricted && <RestrictedIndicatorForCreateTeam isFreeTrial={isFreeTrial}/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function RestrictedIndicatorForCreateTeam({isFreeTrial}: {isFreeTrial: boolean}) {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<RestrictedIndicator
|
||||
feature={MattermostFeatures.CREATE_MULTIPLE_TEAMS}
|
||||
minimumPlanRequiredForFeature={LicenseSkus.Professional}
|
||||
blocked={!isFreeTrial}
|
||||
tooltipMessage={formatMessage({
|
||||
id: 'navbar_dropdown.create.tooltip.cloudFreeTrial',
|
||||
defaultMessage: 'During your trial you are able to create multiple teams. These teams will be archived after your trial.',
|
||||
})}
|
||||
titleAdminPreTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleAdminPreTrial',
|
||||
defaultMessage: 'Try unlimited teams with a free trial',
|
||||
})}
|
||||
messageAdminPreTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageAdminPreTrial',
|
||||
defaultMessage: 'Create unlimited teams with one of our paid plans. Get the full experience of Enterprise when you start a free, {trialLength} day trial.',
|
||||
},
|
||||
{
|
||||
trialLength: FREEMIUM_TO_ENTERPRISE_TRIAL_LENGTH_DAYS,
|
||||
},
|
||||
)}
|
||||
titleAdminPostTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleAdminPostTrial',
|
||||
defaultMessage: 'Upgrade to create unlimited teams',
|
||||
})}
|
||||
messageAdminPostTrial={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageAdminPostTrial',
|
||||
defaultMessage: "Multiple teams allow for context-specific spaces that are more attuned to your and your teams' needs. Upgrade to the Professional plan to create unlimited teams.",
|
||||
})}
|
||||
titleEndUser={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.titleEndUser',
|
||||
defaultMessage: 'Multiple teams available in paid plans',
|
||||
})}
|
||||
messageEndUser={formatMessage({
|
||||
id: 'navbar_dropdown.create.modal.messageEndUser',
|
||||
defaultMessage: "Multiple teams allow for context-specific spaces that are more attuned to your teams' needs.",
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const MATTERMOST_ACADEMY_TEAM_TRAINING_LINK = 'https://mattermost.com/pl/mattermost-academy-team-training';
|
||||
|
||||
function LearnAboutTeamsMenuItem() {
|
||||
const handleClick = useCallback(() => {
|
||||
window.open(MATTERMOST_ACADEMY_TEAM_TRAINING_LINK, '_blank', 'noopener noreferrer');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
className='learnAboutTeamsMenuItem'
|
||||
onClick={handleClick}
|
||||
leadingElement={(
|
||||
<LightbulbOutlineIcon
|
||||
size={18}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
)}
|
||||
labels={(
|
||||
<FormattedMessage
|
||||
id='sidebarLeft.teamMenu.learnAboutTeamsMenuItem.primaryLabel'
|
||||
defaultMessage='Learn about teams'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginMenuItems() {
|
||||
const pluginInMainMenu = useSelector(getMainMenuPluginComponents);
|
||||
|
||||
if (pluginInMainMenu.length > 0) {
|
||||
const pluginMenuItems = pluginInMainMenu.map((plugin) => {
|
||||
function handleClick() {
|
||||
if (plugin.action) {
|
||||
plugin.action();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
id={`${plugin.id}_pluginmenuitem`}
|
||||
key={plugin.id}
|
||||
onClick={handleClick}
|
||||
labels={<span>{plugin.text}</span>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Separator/>
|
||||
{pluginMenuItems}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const ANIMATION_DURATION = 500;
|
||||
|
||||
export const TRANSITION_TIMEOUT = {
|
||||
enter: ANIMATION_DURATION,
|
||||
exit: ANIMATION_DURATION,
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {getIsRhsMenuOpen} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import SidebarMobileRightMenu from './sidebar_mobile_right_menu';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const currentTeam = getCurrentTeam(state);
|
||||
|
||||
const siteName = config.SiteName;
|
||||
|
||||
return {
|
||||
teamDisplayName: currentTeam && currentTeam.display_name,
|
||||
isMobileView: getIsMobileView(state),
|
||||
isOpen: getIsRhsMenuOpen(state),
|
||||
siteName,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(SidebarMobileRightMenu);
|
||||
@@ -1,68 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {memo} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {CSSTransition} from 'react-transition-group';
|
||||
|
||||
import MainMenu from 'components/main_menu';
|
||||
|
||||
import {Constants} from 'utils/constants';
|
||||
|
||||
import {TRANSITION_TIMEOUT} from './constant';
|
||||
|
||||
type Props = {
|
||||
isMobileView: boolean;
|
||||
isOpen: boolean;
|
||||
teamDisplayName?: string;
|
||||
siteName?: string;
|
||||
};
|
||||
|
||||
const SidebarRightMenu = ({
|
||||
siteName: defaultSiteName,
|
||||
teamDisplayName: defaultTeamDisplayName,
|
||||
isOpen,
|
||||
isMobileView,
|
||||
}: Props) => {
|
||||
let siteName = '';
|
||||
if (defaultSiteName != null) {
|
||||
siteName = defaultSiteName;
|
||||
}
|
||||
let teamDisplayName = siteName;
|
||||
if (defaultTeamDisplayName) {
|
||||
teamDisplayName = defaultTeamDisplayName;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('sidebar--menu', {'move--left': isOpen && isMobileView})}
|
||||
id='sidebar-menu'
|
||||
>
|
||||
<div className='team__header theme'>
|
||||
<Link
|
||||
className='team__name'
|
||||
to={`/channels/${Constants.DEFAULT_CHANNEL}`}
|
||||
>
|
||||
{teamDisplayName}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='nav-pills__container mobile-main-menu'>
|
||||
<CSSTransition
|
||||
in={isOpen && isMobileView}
|
||||
classNames='MobileRightSidebarMenu'
|
||||
enter={true}
|
||||
exit={true}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
timeout={TRANSITION_TIMEOUT}
|
||||
>
|
||||
<MainMenu mobile={true}/>
|
||||
</CSSTransition>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SidebarRightMenu);
|
||||
@@ -24,7 +24,7 @@ exports[`components/TeamMembersModal should match snapshot 1`] = `
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
|
||||
@@ -13,6 +13,7 @@ import InvitationModal from 'components/invitation_modal';
|
||||
import MemberListTeam from 'components/member_list_team';
|
||||
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {ModalData} from 'types/actions';
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
currentTeam?: Team;
|
||||
onExited: () => void;
|
||||
onLoad?: () => void;
|
||||
focusOriginElement?: string;
|
||||
actions: {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
};
|
||||
@@ -60,6 +62,13 @@ export default class TeamMembersModal extends React.PureComponent<Props, State>
|
||||
this.handleHide();
|
||||
};
|
||||
|
||||
handleExit = () => {
|
||||
if (this.props.focusOriginElement) {
|
||||
focusElement(this.props.focusOriginElement, true);
|
||||
}
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
render() {
|
||||
let teamDisplayName = '';
|
||||
if (this.props.currentTeam) {
|
||||
@@ -71,7 +80,7 @@ export default class TeamMembersModal extends React.PureComponent<Props, State>
|
||||
dialogClassName='a11y__modal more-modal'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.props.onExited}
|
||||
onExited={this.handleExit}
|
||||
role='none'
|
||||
aria-labelledby='teamMemberModalLabel'
|
||||
id='teamMembersModal'
|
||||
|
||||
@@ -8,14 +8,17 @@ import {useIntl} from 'react-intl';
|
||||
|
||||
import TeamSettings from 'components/team_settings';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
|
||||
const SettingsSidebar = React.lazy(() => import('components/settings_sidebar'));
|
||||
|
||||
type Props = {
|
||||
onExited: () => void;
|
||||
canInviteUsers: boolean;
|
||||
focusOriginElement?: string;
|
||||
}
|
||||
|
||||
const TeamSettingsModal = ({onExited, canInviteUsers}: Props) => {
|
||||
const TeamSettingsModal = ({onExited, canInviteUsers, focusOriginElement}: Props) => {
|
||||
const [activeTab, setActiveTab] = useState('info');
|
||||
const [show, setShow] = useState<boolean>(true);
|
||||
const [hasChanges, setHasChanges] = useState<boolean>(false);
|
||||
@@ -36,11 +39,14 @@ const TeamSettingsModal = ({onExited, canInviteUsers}: Props) => {
|
||||
const handleHide = useCallback(() => setShow(false), []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (focusOriginElement) {
|
||||
focusElement(focusOriginElement, true);
|
||||
}
|
||||
setActiveTab('info');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
onExited();
|
||||
}, [onExited]);
|
||||
}, [onExited, focusOriginElement]);
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
const el = ReactDOM.findDOMNode(modalBodyRef.current) as HTMLDivElement;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {switchToChannels} from 'actions/views/onboarding_tasks';
|
||||
|
||||
import {openMenu, dismissMenu} from 'components/menu';
|
||||
import {OnboardingTaskCategory, OnboardingTaskList, OnboardingTasksName} from 'components/onboarding_tasks';
|
||||
import {ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU} from 'components/sidebar/sidebar_header/sidebar_browse_or_add_channel_menu';
|
||||
import {ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON} from 'components/sidebar/sidebar_header/sidebar_browse_or_add_channel_menu';
|
||||
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
|
||||
@@ -52,11 +52,11 @@ export const useHandleNavigationAndExtraActions = (tourCategory: string) => {
|
||||
break;
|
||||
}
|
||||
case OnboardingTourSteps.CREATE_AND_JOIN_CHANNELS : {
|
||||
openMenu(ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU);
|
||||
openMenu(ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON);
|
||||
break;
|
||||
}
|
||||
case OnboardingTourSteps.INVITE_PEOPLE : {
|
||||
openMenu(ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU);
|
||||
openMenu(ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU_BUTTON);
|
||||
break;
|
||||
}
|
||||
case OnboardingTourSteps.SEND_MESSAGE : {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {useMeasurePunchouts} from '@mattermost/components';
|
||||
|
||||
import {ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU} from 'components/sidebar/sidebar_header/sidebar_browse_or_add_channel_menu';
|
||||
|
||||
import OnboardingTourTip from './onboarding_tour_tip';
|
||||
|
||||
const translate = {x: -3, y: 13};
|
||||
@@ -26,7 +28,7 @@ export const CreateAndJoinChannelsTour = () => {
|
||||
</p>
|
||||
);
|
||||
|
||||
const overlayPunchOut = useMeasurePunchouts(['browserOrAddChannelMenu'], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
const overlayPunchOut = useMeasurePunchouts([ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
|
||||
return (
|
||||
<OnboardingTourTip
|
||||
|
||||
@@ -6,6 +6,8 @@ import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {useMeasurePunchouts} from '@mattermost/components';
|
||||
|
||||
import {ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU} from 'components/sidebar/sidebar_header/sidebar_browse_or_add_channel_menu';
|
||||
|
||||
import OnboardingTourTip from './onboarding_tour_tip';
|
||||
|
||||
const translate = {x: -3, y: -25};
|
||||
@@ -26,7 +28,7 @@ export const InvitePeopleTour = () => {
|
||||
</p>
|
||||
);
|
||||
|
||||
const overlayPunchOut = useMeasurePunchouts(['browserOrAddChannelMenu'], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
const overlayPunchOut = useMeasurePunchouts([ELEMENT_ID_FOR_BROWSE_OR_ADD_CHANNEL_MENU], [], {x: -2.5, y: -2.5, width: 5, height: 5});
|
||||
|
||||
return (
|
||||
<OnboardingTourTip
|
||||
|
||||
@@ -30,6 +30,7 @@ import './user_account_menu.scss';
|
||||
type Props = PropsFromRedux;
|
||||
|
||||
export const ELEMENT_ID_FOR_USER_ACCOUNT_MENU_BUTTON = 'userAccountMenuButton';
|
||||
export const ELEMENT_ID_FOR_USER_ACCOUNT_MENU = 'userAccountMenu';
|
||||
|
||||
export default function UserAccountMenu(props: Props) {
|
||||
const {formatMessage} = useIntl();
|
||||
@@ -63,7 +64,7 @@ export default function UserAccountMenu(props: Props) {
|
||||
),
|
||||
}}
|
||||
menu={{
|
||||
id: 'userAccountMenu',
|
||||
id: ELEMENT_ID_FOR_USER_ACCOUNT_MENU,
|
||||
width: '264px',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
|
||||
@@ -31,14 +31,6 @@ export default function UserAccountProfileMenuItem(props: Props) {
|
||||
const onboardingTaskStep = useSelector((state: GlobalState) => getInt(state, OnboardingTaskCategory, OnboardingTasksName.COMPLETE_YOUR_PROFILE, 0));
|
||||
const isCompleteYourProfileTaskPending = onboardingTaskStep === TaskNameMapToSteps[OnboardingTasksName.COMPLETE_YOUR_PROFILE].STARTED;
|
||||
|
||||
function handleClick() {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {isContentProductSettings: false, focusOriginElement: 'userAccountMenuButton'},
|
||||
}));
|
||||
}
|
||||
|
||||
function handleTourClick() {
|
||||
const taskName = OnboardingTasksName.COMPLETE_YOUR_PROFILE;
|
||||
const steps = TaskNameMapToSteps[taskName];
|
||||
@@ -51,6 +43,21 @@ export default function UserAccountProfileMenuItem(props: Props) {
|
||||
}]));
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
dispatch(openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {
|
||||
isContentProductSettings: false,
|
||||
focusOriginElement: 'userAccountMenuButton',
|
||||
},
|
||||
}));
|
||||
|
||||
if (isCompleteYourProfileTaskPending) {
|
||||
handleTourClick();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.Item
|
||||
leadingElement={
|
||||
@@ -66,12 +73,7 @@ export default function UserAccountProfileMenuItem(props: Props) {
|
||||
/>
|
||||
}
|
||||
trailingElements={isCompleteYourProfileTaskPending && (
|
||||
<div
|
||||
onClick={handleTourClick}
|
||||
className='userAccountMenu_profileMenuItem_tourWrapper'
|
||||
>
|
||||
<CompleteYourProfileTour/>
|
||||
</div>
|
||||
<CompleteYourProfileTour/>
|
||||
)}
|
||||
aria-haspopup={true}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
"accessibility.sections.centerContent": "message list main region",
|
||||
"accessibility.sections.centerFooter": "message input complimentary region",
|
||||
"accessibility.sections.channelHeader": "channel header region",
|
||||
"accessibility.sections.lhsHeader": "team menu region",
|
||||
"accessibility.sections.lhsList": "channel sidebar region",
|
||||
"accessibility.sections.lhsNavigator": "channel navigator region",
|
||||
"accessibility.sections.rhs": "{regionTitle} complimentary region",
|
||||
@@ -4268,7 +4267,7 @@
|
||||
"last_users_message.others": "{numOthers} others ",
|
||||
"last_users_message.removed_from_channel.type": "were **removed from the channel**.",
|
||||
"last_users_message.removed_from_team.type": "were **removed from the team**.",
|
||||
"learn_about_teams": "<a>Learn about teams</a>",
|
||||
"learn_about_teams": "Learn about teams",
|
||||
"learn_more_about_trial.modal.ldapDescription": "Use AD/LDAP groups to organize and apply actions to multiple users at once. Manage team and channel memberships, permissions and more.",
|
||||
"learn_more_about_trial.modal.ldapTitle": "Synchronize your Active Directory/LDAP groups",
|
||||
"learn_more_about_trial.modal.systemConsoleDescription": "Assign customizable admin roles to give designated users read and/or write access to select sections of System Console.",
|
||||
@@ -5273,7 +5272,6 @@
|
||||
"sidebar.team_menu.button.plusIcon": "Plus Icon",
|
||||
"sidebar.team_menu.button.teamImage": "{teamName} Team Image",
|
||||
"sidebar.team_menu.button.teamInitials": "{teamName} Team Initials",
|
||||
"sidebar.team_menu.menuAriaLabel": "team menu",
|
||||
"sidebar.team_select": "{siteName} - Join a team",
|
||||
"sidebar.types.channels": "CHANNELS",
|
||||
"sidebar.types.direct_messages": "DIRECT MESSAGES",
|
||||
@@ -5289,6 +5287,17 @@
|
||||
"sidebarLeft.browserOrCreateChannelMenu.invitePeopleMenuItem.secondaryLabel": "Add people to the team",
|
||||
"sidebarLeft.browserOrCreateChannelMenu.openDirectMessageMenuItem.primaryLabel": "Open a direct message",
|
||||
"sidebarLeft.browserOrCreateChannelMenuButton.label": "Browse or create channels",
|
||||
"sidebarLeft.teamMenu.addGroupsToTeamMenuItem.primaryLabel": "Add groups",
|
||||
"sidebarLeft.teamMenu.createTeamMenuItem.primaryLabel": "Create a team",
|
||||
"sidebarLeft.teamMenu.invitePeopleMenuItem.primaryLabel": "Invite people",
|
||||
"sidebarLeft.teamMenu.invitePeopleMenuItem.secondaryLabel": "Add or invite people to the team",
|
||||
"sidebarLeft.teamMenu.joinAnotherTeamMenuItem.primaryLabel": "Join another team",
|
||||
"sidebarLeft.teamMenu.learnAboutTeamsMenuItem.primaryLabel": "Learn about teams",
|
||||
"sidebarLeft.teamMenu.leaveTeamMenuItem.primaryLabel": "Leave team",
|
||||
"sidebarLeft.teamMenu.manageGroupsMenuItem.primaryLabel": "Manage groups",
|
||||
"sidebarLeft.teamMenu.manageMembersMenuItem.primaryLabel": "Manage members",
|
||||
"sidebarLeft.teamMenu.teamSettingsMenuItem.primaryLabel": "Team settings",
|
||||
"sidebarLeft.teamMenu.viewMembersMenuItem.primaryLabel": "View members",
|
||||
"signup_team_system_console": "Go to System Console",
|
||||
"signup_team.guest_without_channels": "Your guest account has no channels assigned. Please contact an administrator.",
|
||||
"signup_team.join_open": "Teams you can join: ",
|
||||
|
||||
@@ -1,671 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`plugins/MainMenuActions should match snapshot in mobile view with some plugin and ability to click plugin 1`] = `
|
||||
<div
|
||||
aria-label="main menu"
|
||||
className="a11y__popup Menu"
|
||||
>
|
||||
<ul
|
||||
className="Menu__content dropdown-menu"
|
||||
onClick={[Function]}
|
||||
role="menu"
|
||||
style={Object {}}
|
||||
>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_billing",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuCloudTrial
|
||||
id="menuCloudTrial"
|
||||
/>
|
||||
</Memo(SystemPermissionGate)>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"sysconsole_write_about_edition_and_license",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuStartTrial
|
||||
id="startTrial"
|
||||
/>
|
||||
</Memo(SystemPermissionGate)>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="mentions"
|
||||
>
|
||||
@
|
||||
</i>
|
||||
}
|
||||
id="recentMentions"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Recent Mentions"
|
||||
/>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-bookmark"
|
||||
/>
|
||||
}
|
||||
id="flaggedPosts"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Saved messages"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton2",
|
||||
"isContentProductSettings": false,
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user"
|
||||
/>
|
||||
}
|
||||
id="profileSettings"
|
||||
modalId="user_settings"
|
||||
show={true}
|
||||
text="Profile"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "userAccountMenuButton3",
|
||||
"isContentProductSettings": true,
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-cog"
|
||||
/>
|
||||
}
|
||||
id="accountSettings"
|
||||
modalId="user_settings"
|
||||
show={true}
|
||||
text="Settings"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="addGroupsToTeam"
|
||||
modalId="add_groups_to_team"
|
||||
show={true}
|
||||
text="Add Groups to Team"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"add_user_to_team",
|
||||
"invite_guest",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
extraText="Add people to the team"
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="invitePeople"
|
||||
modalId="invitation"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Invite People"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-globe"
|
||||
/>
|
||||
}
|
||||
id="teamSettings"
|
||||
modalId="team_settings"
|
||||
show={true}
|
||||
text="Team Settings"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"teamID": "someteamid",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-user-plus"
|
||||
/>
|
||||
}
|
||||
id="manageGroups"
|
||||
modalId="manage_team_groups"
|
||||
show={true}
|
||||
text="Manage Groups"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-users"
|
||||
/>
|
||||
}
|
||||
id="manageMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="Manage Members"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
invert={true}
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-users"
|
||||
/>
|
||||
}
|
||||
id="viewMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="View Members"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"create_team",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-plus-square"
|
||||
/>
|
||||
}
|
||||
id="createTeam"
|
||||
show={true}
|
||||
text="Create a Team"
|
||||
to="/create_team"
|
||||
/>
|
||||
</Memo(SystemPermissionGate)>
|
||||
<MenuItemLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-plus-square"
|
||||
/>
|
||||
}
|
||||
id="joinTeam"
|
||||
show={true}
|
||||
text="Join Another Team"
|
||||
to="/select_team"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={<LeaveTeamIcon />}
|
||||
id="leaveTeam"
|
||||
modalId="leave_team"
|
||||
show={false}
|
||||
text="Leave Team"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-anchor"
|
||||
/>
|
||||
}
|
||||
id="someplugin_pluginmenuitem"
|
||||
key="someplugin_pluginmenuitem"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="some plugin text"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemLink
|
||||
id="integrations"
|
||||
show={false}
|
||||
text="Integrations"
|
||||
to="/somename/integrations"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-question"
|
||||
/>
|
||||
}
|
||||
id="helpLink"
|
||||
show={false}
|
||||
text="Help"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-phone"
|
||||
/>
|
||||
}
|
||||
id="reportLink"
|
||||
show={false}
|
||||
text="Report a Problem"
|
||||
/>
|
||||
<MenuItemExternalLink
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-mobile"
|
||||
/>
|
||||
}
|
||||
id="nativeAppLink"
|
||||
show={true}
|
||||
text="Download Apps"
|
||||
url=""
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-info"
|
||||
/>
|
||||
}
|
||||
id="about"
|
||||
modalId="about"
|
||||
show={true}
|
||||
text="About Mattermost"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemAction
|
||||
icon={
|
||||
<i
|
||||
className="fa fa-sign-out"
|
||||
/>
|
||||
}
|
||||
id="logout"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Log Out"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`plugins/MainMenuActions should match snapshot in web view 1`] = `
|
||||
<div
|
||||
aria-label="team menu"
|
||||
className="a11y__popup Menu"
|
||||
>
|
||||
<ul
|
||||
className="Menu__content dropdown-menu"
|
||||
onClick={[Function]}
|
||||
role="menu"
|
||||
style={Object {}}
|
||||
>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="addGroupsToTeam"
|
||||
modalId="add_groups_to_team"
|
||||
show={true}
|
||||
text="Add Groups to Team"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"add_user_to_team",
|
||||
"invite_guest",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"focusOriginElement": "sidebarDropdownMenuButton",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
extraText="Add people to the team"
|
||||
icon={false}
|
||||
id="invitePeople"
|
||||
modalId="invitation"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Invite People"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="teamSettings"
|
||||
modalId="team_settings"
|
||||
show={true}
|
||||
text="Team Settings"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"manage_team",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogProps={
|
||||
Object {
|
||||
"teamID": "someteamid",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="manageGroups"
|
||||
modalId="manage_team_groups"
|
||||
show={true}
|
||||
text="Manage Groups"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="manageMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="Manage Members"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<Memo(TeamPermissionGate)
|
||||
invert={true}
|
||||
permissions={
|
||||
Array [
|
||||
"remove_user_from_team",
|
||||
"manage_team_roles",
|
||||
]
|
||||
}
|
||||
teamId="someteamid"
|
||||
>
|
||||
<MenuItemToggleModalRedux
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="viewMembers"
|
||||
modalId="team_members"
|
||||
show={true}
|
||||
text="View Members"
|
||||
/>
|
||||
</Memo(TeamPermissionGate)>
|
||||
<MenuItemLink
|
||||
id="joinTeam"
|
||||
show={true}
|
||||
text="Join Another Team"
|
||||
to="/select_team"
|
||||
/>
|
||||
<MenuItemToggleModalRedux
|
||||
className="destructive"
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
id="leaveTeam"
|
||||
modalId="leave_team"
|
||||
show={false}
|
||||
text="Leave Team"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<Memo(SystemPermissionGate)
|
||||
permissions={
|
||||
Array [
|
||||
"create_team",
|
||||
]
|
||||
}
|
||||
>
|
||||
<MenuItemLink
|
||||
className=""
|
||||
disabled={false}
|
||||
id="createTeam"
|
||||
show={true}
|
||||
sibling={false}
|
||||
text="Create a Team"
|
||||
to="/create_team"
|
||||
/>
|
||||
</Memo(SystemPermissionGate)>
|
||||
<Memo(MenuGroup)>
|
||||
<div
|
||||
className="MainMenu_dropdown-link"
|
||||
>
|
||||
<LearnAboutTeamsLink />
|
||||
</div>
|
||||
</Memo(MenuGroup)>
|
||||
</Memo(MenuGroup)>
|
||||
<Memo(MenuGroup)>
|
||||
<MenuItemAction
|
||||
icon={false}
|
||||
id="someplugin_pluginmenuitem"
|
||||
key="someplugin_pluginmenuitem"
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="some plugin text"
|
||||
/>
|
||||
</Memo(MenuGroup)>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ComponentProps} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import MainMenu from 'components/main_menu/main_menu';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
describe('plugins/MainMenuActions', () => {
|
||||
const pluginAction = jest.fn();
|
||||
|
||||
const requiredProps: ComponentProps<typeof MainMenu> = {
|
||||
teamId: 'someteamid',
|
||||
teamName: 'somename',
|
||||
currentUser: {id: 'someuserid', roles: 'system_user'} as UserProfile,
|
||||
enableCommands: true,
|
||||
enableIncomingWebhooks: true,
|
||||
enableOutgoingWebhooks: true,
|
||||
enableOAuthServiceProvider: true,
|
||||
canManageSystemBots: true,
|
||||
pluginMenuItems: [{
|
||||
id: 'someplugin',
|
||||
pluginId: 'test',
|
||||
text: 'some plugin text',
|
||||
action: pluginAction,
|
||||
mobileIcon: <i className='fa fa-anchor'/>,
|
||||
}],
|
||||
canManageIntegrations: true,
|
||||
moreTeamsToJoin: true,
|
||||
guestAccessEnabled: true,
|
||||
teamIsGroupConstrained: true,
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
showMentions: jest.fn(),
|
||||
showFlaggedPosts: jest.fn(),
|
||||
closeRightHandSide: jest.fn(),
|
||||
closeRhsMenu: jest.fn(),
|
||||
},
|
||||
isCloud: false,
|
||||
isStarterFree: false,
|
||||
canInviteTeamMember: false,
|
||||
isFreeTrial: false,
|
||||
usageDeltaTeams: -1,
|
||||
mobile: false,
|
||||
};
|
||||
|
||||
test('should match snapshot in web view', () => {
|
||||
let wrapper = shallowWithIntl(
|
||||
<MainMenu
|
||||
{...requiredProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper = wrapper.shallow();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should match snapshot in mobile view with some plugin and ability to click plugin', () => {
|
||||
const props = {
|
||||
...requiredProps,
|
||||
mobile: true,
|
||||
};
|
||||
|
||||
let wrapper = shallowWithIntl(
|
||||
<MainMenu
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper = wrapper.shallow();
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem')).toHaveLength(1);
|
||||
|
||||
wrapper.findWhere((node) => node.key() === 'someplugin_pluginmenuitem').simulate('click');
|
||||
expect(pluginAction).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,6 +328,19 @@ button {
|
||||
&:active {
|
||||
background-color: rgb(var(--button-bg-rgb), 0.12);
|
||||
}
|
||||
|
||||
&.btn-inverted {
|
||||
color: rgb(var(--button-color-rgb));
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--button-color-rgb), 0.12);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&[aria-expanded="true"][aria-haspopup="true"] {
|
||||
background-color: rgb(var(--button-color-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
@use 'sidebar-card';
|
||||
@use 'sidebar-header-dropdown-button';
|
||||
@use 'single_image_view';
|
||||
@use 'sidebar-header';
|
||||
@use 'toggle';
|
||||
@use 'team-channel-settings';
|
||||
@use 'infinite-scroll';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
.SidebarHeader {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.MenuWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.MenuWrapper:first-child {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.status-selector {
|
||||
margin: 3px 8px 0 6px;
|
||||
}
|
||||
|
||||
.sidebar-header-dropdown__icon {
|
||||
svg {
|
||||
fill: var(--sidebar-header-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -695,7 +695,6 @@
|
||||
// Team Header in Sidebar
|
||||
.sidebar--left,
|
||||
#SidebarContainer,
|
||||
.sidebar--menu,
|
||||
.admin-sidebar {
|
||||
.divider {
|
||||
border-top: 1px solid transparent;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@use 'markdown';
|
||||
@use 'navigation';
|
||||
@use 'sidebar-left';
|
||||
@use 'sidebar-menu';
|
||||
@use 'sidebar-right';
|
||||
@use 'team-button';
|
||||
@use 'team-sidebar';
|
||||
|
||||
@@ -340,30 +340,6 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.SidebarHeaderMenuWrapper {
|
||||
overflow: hidden;
|
||||
padding-left: 5px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgba(var(--sidebar-text-rgb), 0.08);
|
||||
}
|
||||
|
||||
&.MenuWrapper--open {
|
||||
background-color: rgba(var(--sidebar-text-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.SidebarMenu {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1358,19 +1334,6 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual override of the global menu (and items) styling for the sidebar dropdown menu
|
||||
#sidebarDropdownMenu {
|
||||
position: fixed;
|
||||
|
||||
.MenuItem {
|
||||
>button,
|
||||
>div,
|
||||
>a {
|
||||
padding: 0 32px 0 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// global header style adjustments
|
||||
@media screen and (min-width: 769px) {
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
}
|
||||
|
||||
.navbar-header {
|
||||
margin: 0 -15px;
|
||||
float: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.dropdown__icon {
|
||||
display: inline-block;
|
||||
@@ -19,6 +20,10 @@
|
||||
height: 16px;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
button.navbar-toggle.navbar-right__icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggle {
|
||||
@@ -723,7 +728,7 @@
|
||||
|
||||
.sidebar--left .team__header .Avatar,
|
||||
#SidebarContainer .team__header .Avatar,
|
||||
.sidebar--menu .team__header .Avatar {
|
||||
.team__header .Avatar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1186,34 +1191,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--menu {
|
||||
width: 290px;
|
||||
border: none;
|
||||
transform: translate3d(290px, 0, 0);
|
||||
transition: transform 0.35s ease;
|
||||
visibility: hidden;
|
||||
|
||||
&.visible {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&.move--left {
|
||||
display: block;
|
||||
transform: translate3d(0, 0, 0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.mentions {
|
||||
font-size: 17px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.Menu .MenuItem.MenuItem--with-icon .MenuItem__help-text {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--left {
|
||||
width: 290px;
|
||||
border: none;
|
||||
@@ -1320,22 +1297,6 @@
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.team__header {
|
||||
@include mixins.clearfix;
|
||||
|
||||
padding: 15px;
|
||||
pointer-events: none;
|
||||
|
||||
.team__name,
|
||||
.user__name {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-header-dropdown {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search__form {
|
||||
display: block;
|
||||
}
|
||||
@@ -1522,7 +1483,7 @@
|
||||
}
|
||||
|
||||
.inner-wrap {
|
||||
transition: all 0.35s ease;
|
||||
transition: all 0.3s linear;
|
||||
|
||||
.app__body & {
|
||||
&::before {
|
||||
|
||||
@@ -98,6 +98,14 @@ export const getAppBarPluginComponents = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const getMainMenuPluginComponents = createSelector(
|
||||
'getMainMenuPluginComponents',
|
||||
(state: GlobalState) => state.plugins.components.MainMenu,
|
||||
(components = []) => {
|
||||
return components;
|
||||
},
|
||||
);
|
||||
|
||||
export const shouldShowAppBar = createSelector(
|
||||
'shouldShowAppBar',
|
||||
appBarEnabled,
|
||||
|
||||
9
webapp/package-lock.json
сгенерированный
9
webapp/package-lock.json
сгенерированный
@@ -15,6 +15,7 @@
|
||||
"platform/types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mattermost/compass-icons": "0.1.48",
|
||||
"react-intl": "6.6.2",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
@@ -65,7 +66,6 @@
|
||||
"@guyplusplus/turndown-plugin-gfm": "1.0.7",
|
||||
"@mattermost/client": "*",
|
||||
"@mattermost/compass-components": "^0.2.12",
|
||||
"@mattermost/compass-icons": "0.1.39",
|
||||
"@mattermost/desktop-api": "5.10.0-2",
|
||||
"@mattermost/types": "*",
|
||||
"@mui/base": "5.0.0-alpha.127",
|
||||
@@ -5227,9 +5227,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@mattermost/compass-icons": {
|
||||
"version": "0.1.39",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.39.tgz",
|
||||
"integrity": "sha512-rr+grRMg9xs020O4PC2H+/q4+B8hjuGay49i2xEVxGATaHJFkGItDVXqlfPVDcQPVOmZI7m66H+DOnnYrr5pBQ=="
|
||||
"version": "0.1.48",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.48.tgz",
|
||||
"integrity": "sha512-z36LlyZryL+Ssc1pbCGE1bidRmdcYlHGkVtcFP9qpd7+Jpk/sRiv0OjCMzcBHDnCiVT8jIfSmtNrTd+TJdVnmQ=="
|
||||
},
|
||||
"node_modules/@mattermost/components": {
|
||||
"resolved": "platform/components",
|
||||
@@ -29639,6 +29639,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime-corejs3": "^7.17.8",
|
||||
"@mattermost/compass-icons": "0.1.48",
|
||||
"@mui/base": "5.0.0-alpha.127",
|
||||
"@mui/material": "5.11.16",
|
||||
"@mui/styled-engine-sc": "5.11.11",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"gen-lang-imports": "node scripts/gen_lang_imports.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mattermost/compass-icons": "0.1.48",
|
||||
"react-intl": "6.6.2",
|
||||
"typescript": "5.6.3"
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ const config = {
|
||||
},
|
||||
modules: false,
|
||||
corejs: {
|
||||
version: "3.26",
|
||||
version: '3.26',
|
||||
proposals: true,
|
||||
},
|
||||
debug: false,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime-corejs3": "^7.17.8",
|
||||
"@mattermost/compass-icons": "0.1.48",
|
||||
"@mui/base": "5.0.0-alpha.127",
|
||||
"@mui/material": "5.11.16",
|
||||
"@mui/styled-engine-sc": "5.11.11",
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import scss from 'rollup-plugin-scss';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import scss from 'rollup-plugin-scss';
|
||||
|
||||
import packagejson from './package.json';
|
||||
|
||||
const externals = [
|
||||
...Object.keys(packagejson.dependencies || {}),
|
||||
...Object.keys(packagejson.peerDependencies || {}),
|
||||
'@mattermost/compass-icons/components',
|
||||
'lodash/throttle',
|
||||
'mattermost-redux',
|
||||
'reselect',
|
||||
|
||||
Ссылка в новой задаче
Block a user