[MM-55277] Team's menu doesn't follow standard accessible keyboard behavior (#29917)

Этот коммит содержится в:
M-ZubairAhmed
2025-04-18 19:18:54 +05:30
коммит произвёл GitHub
родитель b59f10cbbd
Коммит 502506a517
130 изменённых файлов: 2208 добавлений и 5137 удалений

Просмотреть файл

@@ -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);
}

Просмотреть файл

@@ -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 сгенерированный
Просмотреть файл

@@ -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',