MM-49869 - add channels button lhs sidebar (#22701)

Этот коммит содержится в:
Pablo Andrés Vélez Vidal
2023-03-28 18:57:09 +02:00
коммит произвёл GitHub
родитель 29cff3269c
Коммит bb939e28c9
15 изменённых файлов: 464 добавлений и 39 удалений

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

@@ -9,3 +9,10 @@ export function setAddChannelDropdown(open: boolean) {
open,
};
}
export function setAddChannelCtaDropdown(open: boolean) {
return {
type: ActionTypes.ADD_CHANNEL_CTA_DROPDOWN_TOGGLE,
open,
};
}

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

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/new_channel_modal should match snapshot 1`] = `
<MenuWrapper
animationComponent={[Function]}
className="AddChannelsCtaDropdown"
onToggle={[Function]}
open={false}
>
<button
aria-label="Add Channels Dropdown"
className="SidebarChannelNavigator__addChannelsCtaLhsButton SidebarChannelNavigator__addChannelsCtaLhsButton--untouched"
id="addChannelsCta"
>
<li
aria-label="Add channels"
>
<i
className="icon-plus-box"
/>
<span>
Add Channels
</span>
</li>
</button>
<Menu
ariaLabel="Add Channels Dropdown"
id="AddChannelCtaDropdown"
>
<MenuGroup>
<MenuItemAction
id="showNewChannel"
onClick={[Function]}
show={true}
text="Create New Channel"
/>
<MenuItemAction
id="showMoreChannels"
onClick={[Function]}
show={true}
text="Browse Channels"
/>
</MenuGroup>
</Menu>
</MenuWrapper>
`;

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

@@ -82,7 +82,7 @@ exports[`components/sidebar/invite_members_button should match snapshot 1`] = `
>
<li
aria-label="Invite Members"
className="SidebarChannelNavigator_inviteMembersLhsButton SidebarChannelNavigator_inviteMembersLhsButton--untouched"
className="SidebarChannelNavigator__inviteMembersLhsButton SidebarChannelNavigator__inviteMembersLhsButton--untouched"
>
<i
className="icon-plus-box"

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

@@ -189,14 +189,12 @@ const AddChannelDropdown = ({
placement='top'
overlay={tooltip}
>
<>
<button
className={'AddChannelDropdown_dropdownButton'}
aria-label={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channel Dropdown'})}
>
<i className='icon-plus'/>
</button>
</>
<button
className={'AddChannelDropdown_dropdownButton'}
aria-label={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channel Dropdown'})}
>
<i className='icon-plus'/>
</button>
</OverlayTrigger>
<Menu
id='AddChannelDropdown'

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

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {GlobalState} from 'types/store';
import Permissions from 'mattermost-redux/constants/permissions';
import {UsersState} from '@mattermost/types/users';
import {trackEvent} from 'actions/telemetry_actions.jsx';
import AddChannelsCtaButton from './add_channels_cta_button';
jest.mock('actions/telemetry_actions.jsx', () => {
const original = jest.requireActual('actions/telemetry_actions.jsx');
return {
...original,
trackEvent: jest.fn(),
};
});
const mockDispatch = jest.fn();
let mockState: GlobalState;
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux') as typeof import('react-redux'),
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
useDispatch: () => mockDispatch,
}));
describe('components/new_channel_modal', () => {
beforeEach(() => {
mockState = {
entities: {
general: {
config: {},
},
channels: {
currentChannelId: 'current_channel_id',
channels: {},
roles: {
current_channel_id: [
'channel_user',
'channel_admin',
],
},
},
teams: {
currentTeamId: 'current_team_id',
myMembers: {
current_team_id: {
roles: 'team_user team_admin',
},
},
teams: {
current_team_id: {
id: 'current_team_id',
description: 'Curent team description',
name: 'current-team',
},
},
},
preferences: {
myPreferences: {},
},
users: {
currentUserId: 'current_user_id',
profiles: {
current_user_id: {roles: 'system_user'},
},
},
roles: {
roles: {
guest_user: {
permissions: [],
},
system_user: {
permissions: [Permissions.JOIN_PUBLIC_CHANNELS, Permissions.CREATE_PRIVATE_CHANNEL, Permissions.CREATE_PUBLIC_CHANNEL],
},
},
},
},
views: {
addChannelCtaDropdown: {
isOpen: false,
},
},
} as unknown as GlobalState;
});
test('should match snapshot', () => {
expect(
shallow(
<AddChannelsCtaButton/>,
),
).toMatchSnapshot();
});
test('should find the add channels button when user has permissions', () => {
const wrapper = mountWithIntl(
<AddChannelsCtaButton/>,
);
expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeTruthy();
});
test('should return nothing when user does not have permissions', () => {
const guestUser = {
currentUserId: 'guest_user_id',
profiles: {
user_id: {
id: 'guest_user_id',
roles: 'team_role',
},
},
} as unknown as UsersState;
mockState = {...mockState, entities: {...mockState.entities, users: guestUser}};
const wrapper = mountWithIntl(
<AddChannelsCtaButton/>,
);
expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeFalsy();
});
test('should fire dispatch to save preferences when button is clicked', () => {
const wrapper = mountWithIntl(
<AddChannelsCtaButton/>,
);
const button = wrapper.find('.AddChannelsCtaDropdown button');
expect(mockDispatch).not.toHaveBeenCalled();
button.simulate('click');
expect(mockDispatch).toHaveBeenCalled();
});
test('should fire trackEvent to send telemetry when button is clicked', () => {
const wrapper = mountWithIntl(
<AddChannelsCtaButton/>,
);
const button = wrapper.find('.AddChannelsCtaDropdown button');
expect(mockDispatch).not.toHaveBeenCalled();
button.simulate('click');
expect(trackEvent).toHaveBeenCalledWith('ui', 'add_channels_cta_button_clicked');
});
});

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

@@ -0,0 +1,155 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useIntl} from 'react-intl';
import {useSelector, useDispatch} from 'react-redux';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import Menu from 'components/widgets/menu/menu';
import MoreChannels from 'components/more_channels';
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
import {isAddChannelCtaDropdownOpen} from 'selectors/views/add_channel_dropdown';
import {GlobalState} from 'types/store';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {haveICurrentChannelPermission} from 'mattermost-redux/selectors/entities/roles';
import {DispatchFunc} from 'mattermost-redux/types/actions';
import Permissions from 'mattermost-redux/constants/permissions';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {setAddChannelCtaDropdown} from 'actions/views/add_channel_dropdown';
import {openModal} from 'actions/views/modals';
import {trackEvent} from 'actions/telemetry_actions';
import {ModalIdentifiers, Preferences, Touched} from 'utils/constants';
const AddChannelsCtaButton = (): JSX.Element | null => {
const dispatch = useDispatch<DispatchFunc>();
const currentTeamId = useSelector(getCurrentTeamId);
const intl = useIntl();
const touchedAddChannelsCtaButton = useSelector((state: GlobalState) => getBool(state, Preferences.TOUCHED, Touched.ADD_CHANNELS_CTA));
const canCreatePublicChannel = useSelector((state: GlobalState) => haveICurrentChannelPermission(state, Permissions.CREATE_PUBLIC_CHANNEL));
const canCreatePrivateChannel = useSelector((state: GlobalState) => haveICurrentChannelPermission(state, Permissions.CREATE_PRIVATE_CHANNEL));
const canCreateChannel = canCreatePrivateChannel || canCreatePublicChannel;
const canJoinPublicChannel = useSelector((state: GlobalState) => haveICurrentChannelPermission(state, Permissions.JOIN_PUBLIC_CHANNELS));
const isAddChannelCtaOpen = useSelector(isAddChannelCtaDropdownOpen);
const currentUserId = useSelector(getCurrentUserId);
const openAddChannelsCtaOpen = useCallback((open: boolean) => {
dispatch(setAddChannelCtaDropdown(open));
}, []);
let buttonClass = 'SidebarChannelNavigator__addChannelsCtaLhsButton';
if (!touchedAddChannelsCtaButton) {
buttonClass += ' SidebarChannelNavigator__addChannelsCtaLhsButton--untouched';
}
if ((!canCreateChannel && !canJoinPublicChannel) || !currentTeamId) {
return null;
}
const showMoreChannelsModal = () => {
dispatch(openModal({
modalId: ModalIdentifiers.MORE_CHANNELS,
dialogType: MoreChannels,
dialogProps: {morePublicChannelsModalType: 'public'},
}));
trackEvent('ui', 'browse_channels_button_is_clicked');
};
const showNewChannelModal = () => {
dispatch(openModal({
modalId: ModalIdentifiers.NEW_CHANNEL_MODAL,
dialogType: NewChannelModal,
}));
trackEvent('ui', 'create_new_channel_button_is_clicked');
};
const renderDropdownItems = () => {
let joinPublicChannel;
if (canJoinPublicChannel) {
joinPublicChannel = (
<Menu.ItemAction
id='showMoreChannels'
onClick={showMoreChannelsModal}
text={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.browseChannels', defaultMessage: 'Browse Channels'})}
/>
);
}
let createChannel;
if (canCreateChannel) {
createChannel = (
<Menu.ItemAction
id='showNewChannel'
onClick={showNewChannelModal}
text={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.createNewChannel', defaultMessage: 'Create New Channel'})}
/>
);
}
return (
<>
<Menu.Group>
{createChannel}
{joinPublicChannel}
</Menu.Group>
</>
);
};
const trackOpen = (opened: boolean) => {
openAddChannelsCtaOpen(opened);
trackEvent('ui', 'add_channels_cta_button_clicked');
if (!touchedAddChannelsCtaButton) {
dispatch(savePreferences(
currentUserId,
[{
category: Preferences.TOUCHED,
user_id: currentUserId,
name: Touched.ADD_CHANNELS_CTA,
value: 'true',
}],
));
}
};
return (
<MenuWrapper
className='AddChannelsCtaDropdown'
onToggle={trackOpen}
open={isAddChannelCtaOpen}
>
<button
className={buttonClass}
id={'addChannelsCta'}
aria-label={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channels Dropdown'})}
>
<li
aria-label={intl.formatMessage({id: 'sidebar_left.sidebar_channel_navigator.addChannelsCta', defaultMessage: 'Add channels'})}
>
<i className='icon-plus-box'/>
<span>
{intl.formatMessage({id: 'sidebar_left.addChannelsCta', defaultMessage: 'Add Channels'})}
</span>
</li>
</button>
<Menu
id='AddChannelCtaDropdown'
ariaLabel={intl.formatMessage({id: 'sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channels Dropdown'})}
>
{renderDropdownItems()}
</Menu>
</MenuWrapper>
);
};
export default AddChannelsCtaButton;

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

@@ -32,7 +32,7 @@ type Props = {
isAdmin: boolean;
}
const InviteMembersButton: React.FC<Props> = (props: Props): JSX.Element | null => {
const InviteMembersButton = (props: Props): JSX.Element | null => {
const dispatch = useDispatch<DispatchFunc>();
const intl = useIntl();
@@ -50,10 +50,10 @@ const InviteMembersButton: React.FC<Props> = (props: Props): JSX.Element | null
props.onClick();
};
let buttonClass = 'SidebarChannelNavigator_inviteMembersLhsButton';
let buttonClass = 'SidebarChannelNavigator__inviteMembersLhsButton';
if (!props.touchedInviteMembersButton && Number(totalUserCount) <= Constants.USER_LIMIT) {
buttonClass += ' SidebarChannelNavigator_inviteMembersLhsButton--untouched';
buttonClass += ' SidebarChannelNavigator__inviteMembersLhsButton--untouched';
}
if (!currentTeamId || !totalUserCount) {

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

@@ -24,6 +24,8 @@ import KeyboardShortcutSequence, {
KEYBOARD_SHORTCUTS,
} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import AddChannelsCtaButton from '../add_channels_cta_button';
import SidebarCategorySortingMenu from './sidebar_category_sorting_menu';
import SidebarCategoryMenu from './sidebar_category_menu';
@@ -342,6 +344,13 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
);
}
let addChannelsCtaButton = null;
if (category.type === 'channels' && !category.collapsed) {
addChannelsCtaButton = (
<AddChannelsCtaButton/>
);
}
return (
<div
className={classNames('SidebarChannelGroup a11y__section', {
@@ -399,6 +408,7 @@ export default class SidebarCategory extends React.PureComponent<Props, State> {
}}
</Droppable>
{inviteMembersButton}
{addChannelsCtaButton}
</div>
);
}}

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

@@ -4854,6 +4854,7 @@
"shortcuts.team_nav.prev.mac": "Previous team:\t⌘|⌥|Up",
"shortcuts.team_nav.switcher": "Navigate to a specific team:\tCtrl|Alt|[1-9]",
"shortcuts.team_nav.switcher.mac": "Navigate to a specific team:\t⌘|⌥|[1-9]",
"sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Add Channel Dropdown",
"sidebar_left.add_channel_dropdown.browseChannels": "Browse Channels",
"sidebar_left.add_channel_dropdown.browseOrCreateChannels": "Browse or create channels",
"sidebar_left.add_channel_dropdown.createCategory": "Create New Category",
@@ -4863,6 +4864,7 @@
"sidebar_left.add_channel_dropdown.invitePeopleExtraText": "Add people to the team",
"sidebar_left.add_channel_dropdown.work_template": "Create from a template",
"sidebar_left.add_channel_dropdown.work_template_extra": "Set up a channel with linked boards, and playbooks",
"sidebar_left.addChannelsCta": "Add channels",
"sidebar_left.channel_filter.filterByUnread": "Filter by unread",
"sidebar_left.channel_filter.filterUnreadAria": "unreads filter",
"sidebar_left.channel_filter.showAllChannels": "Show all channels",
@@ -4901,6 +4903,7 @@
"sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Unfavorite",
"sidebar_left.sidebar_channel_menu.unmuteChannel": "Unmute Channel",
"sidebar_left.sidebar_channel_menu.unmuteConversation": "Unmute Conversation",
"sidebar_left.sidebar_channel_navigator.addChannelsCta": "Add channels",
"sidebar_left.sidebar_channel_navigator.inviteUsers": "Invite Users",
"sidebar_left.sidebar_channel.selectedCount": "{count} selected",
"sidebar_right_menu.console": "System Console",

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

@@ -0,0 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {GenericAction} from 'mattermost-redux/types/actions';
import {ActionTypes} from 'utils/constants';
export function isOpen(state = false, action: GenericAction) {
switch (action.type) {
case ActionTypes.ADD_CHANNEL_CTA_DROPDOWN_TOGGLE:
return action.open;
default:
return state;
}
}
export default combineReducers({
isOpen,
});

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

@@ -25,6 +25,7 @@ import productMenu from './product_menu';
import textbox from './textbox';
import statusDropdown from './status_dropdown';
import addChannelDropdown from './add_channel_dropdown';
import addChannelCtaDropdown from './add_channel_cta_dropdown';
import threads from './threads';
import onboardingTasks from './onboarding_tasks';
@@ -50,6 +51,7 @@ export default combineReducers({
channelSidebar,
statusDropdown,
addChannelDropdown,
addChannelCtaDropdown,
onboardingTasks,
threads,
productMenu,

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

@@ -178,6 +178,46 @@ $sidebarOpacityAnimationDuration: 0.15s;
}
}
&__inviteMembersLhsButton,
&__addChannelsCtaLhsButton {
display: flex;
padding: 6px 0;
margin-left: 15px;
color: rgba(var(--sidebar-text-rgb), 0.72);
line-height: 20px;
list-style: none;
i {
font-size: 20px;
}
span {
align-self: flex-end;
margin-top: -2px;
margin-left: 5px;
}
&--untouched {
color: var(--sidebar-unread-text);
font-weight: $font-weight--semibold;
i::before {
font-weight: $font-weight--semibold;
}
}
}
&__addChannelsCtaLhsButton {
border: none;
margin-left: 0;
background: none;
li {
display: flex;
margin-left: 15px;
}
}
.AddChannelDropdown_dropdownButton,
.SidebarChannelNavigator_inviteUsers,
.SidebarChannelNavigator_jumpToButton,
@@ -700,35 +740,12 @@ $sidebarOpacityAnimationDuration: 0.15s;
}
}
.SidebarChannelNavigator_inviteMembersLhsButton {
display: flex;
padding: 6px 0;
margin-left: 15px;
color: rgba(var(--sidebar-text-rgb), 0.72);
line-height: 20px;
list-style: none;
i {
font-size: 20px;
}
span {
align-self: flex-end;
margin-top: -2px;
margin-left: 5px;
}
&--untouched {
color: var(--sidebar-unread-text);
font-weight: $font-weight--semibold;
i::before {
font-weight: $font-weight--semibold;
}
}
.AddChannelsCtaDropdown .dropdown-menu {
margin-left: 20px;
}
#introTextInvite {
#introTextInvite,
#addChannelsCta {
display: flex;
width: 100%;
@@ -737,6 +754,14 @@ $sidebarOpacityAnimationDuration: 0.15s;
}
}
#AddChannelCtaDropdown {
position: fixed;
ul {
min-width: 232px !important;
}
}
.SidebarChannelNavigator_inviteUsersSticky {
position: absolute;
z-index: 2;

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

@@ -6,3 +6,7 @@ import {GlobalState} from 'types/store';
export function isAddChannelDropdownOpen(state: GlobalState) {
return state.views.addChannelDropdown.isOpen;
}
export function isAddChannelCtaDropdownOpen(state: GlobalState) {
return state.views.addChannelCtaDropdown.isOpen;
}

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

@@ -180,6 +180,10 @@ export type ViewsState = {
isOpen: boolean;
};
addChannelCtaDropdown: {
isOpen: boolean;
};
onboardingTasks: {
isShowOnboardingTaskCompletion: boolean;
isShowOnboardingCompleteProfileTour: boolean;

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

@@ -163,6 +163,7 @@ export const Preferences = {
// For one off things that have a special, attention-grabbing UI until you interact with them
export const Touched = {
INVITE_MEMBERS: 'invite_members',
ADD_CHANNELS_CTA: 'add_channels_cta',
};
// Category for actions/interactions that will happen just once
@@ -264,6 +265,7 @@ export const ActionTypes = keyMirror({
STATUS_DROPDOWN_TOGGLE: null,
ADD_CHANNEL_DROPDOWN_TOGGLE: null,
ADD_CHANNEL_CTA_DROPDOWN_TOGGLE: null,
SHOW_ONBOARDING_TASK_COMPLETION: null,
SHOW_ONBOARDING_COMPLETE_PROFILE_TOUR: null,