MM-50087_Add marketplace button to apps bar (#22653)

Этот коммит содержится в:
Julian Mondragón
2023-03-28 10:54:30 -05:00
коммит произвёл GitHub
родитель 6c9ec24fb9
Коммит da7a6825ce
21 изменённых файлов: 485 добавлений и 240 удалений

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

@@ -5,10 +5,10 @@ export function verifyPluginMarketplaceVisibility(shouldBeVisible) {
cy.uiOpenProductMenu().within(() => {
if (shouldBeVisible) {
// * Verify Marketplace button should exist
cy.findByText('Marketplace').should('exist');
cy.findByText('App Marketplace').should('exist');
} else {
// * Verify Marketplace button should not exist
cy.findByText('Marketplace').should('not.exist');
cy.findByText('App Marketplace').should('not.exist');
}
});
}

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

@@ -6,6 +6,7 @@ import {Client4} from 'mattermost-redux/client';
import * as Channels from 'mattermost-redux/selectors/entities/channels';
import * as Teams from 'mattermost-redux/selectors/entities/teams';
import {Permissions} from 'mattermost-redux/constants';
import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
import * as GlobalActions from 'actions/global_actions';
@@ -55,7 +56,7 @@ const initialState = {
roles: {
custom_role: {
permissions: [
'sysconsole_read_plugins',
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
},
},

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

@@ -130,7 +130,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
return {data: true};
case '/marketplace':
// check if user has permissions to access the read plugins
if (!haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_READ_PLUGINS)) {
if (!haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_WRITE_PLUGINS)) {
return {error: {message: localizeMessage('marketplace_command.no_permission', 'You do not have the appropriate permissions to access the marketplace.')}};
}
@@ -139,7 +139,7 @@ export function executeCommand(message: string, args: CommandArgs): ActionFunc {
return {error: {message: localizeMessage('marketplace_command.disabled', 'The marketplace is disabled. Please contact your System Administrator for details.')}};
}
dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal}));
dispatch(openModal({modalId: ModalIdentifiers.PLUGIN_MARKETPLACE, dialogType: MarketplaceModal, dialogProps: {openedFrom: 'command'}}));
return {data: true};
case '/templates': {
const workTemplateEnabled = areWorkTemplatesEnabled(state);

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

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/actions_menu/ActionsMenu has actions - end user - should not show actions and app marketplace 1`] = `
exports[`components/actions_menu/ActionsMenu has actions - marketplace disabled or user not having SYSCONSOLE_WRITE_PLUGINS - should not show actions and app marketplace 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""
@@ -68,7 +68,7 @@ exports[`components/actions_menu/ActionsMenu has actions - end user - should not
</MenuWrapper>
`;
exports[`components/actions_menu/ActionsMenu has actions - sysadmin - should show actions and app marketplace 1`] = `
exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled and user has SYSCONSOLE_WRITE_PLUGINS - should show actions and app marketplace 1`] = `
<MenuWrapper
animationComponent={[Function]}
className=""

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

@@ -45,6 +45,7 @@ describe('components/actions_menu/ActionsMenu', () => {
handleDismissTip: jest.fn(),
showPulsatingDot: false,
location: 'center',
canOpenMarketplace: false,
actions: {
openModal: jest.fn(),
openAppsModal: jest.fn(),
@@ -62,27 +63,29 @@ describe('components/actions_menu/ActionsMenu', () => {
wrapper.setProps({
pluginMenuItems: dropdownComponents,
canOpenMarketplace: true,
});
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(true);
});
test('has actions - sysadmin - should show actions and app marketplace', () => {
test('has actions - marketplace enabled and user has SYSCONSOLE_WRITE_PLUGINS - should show actions and app marketplace', () => {
const wrapper = shallowWithIntl(
<ActionsMenu {...baseProps}/>,
);
wrapper.setProps({
pluginMenuItems: dropdownComponents,
canOpenMarketplace: true,
});
expect(wrapper).toMatchSnapshot();
});
test('has actions - end user - should not show actions and app marketplace', () => {
test('has actions - marketplace disabled or user not having SYSCONSOLE_WRITE_PLUGINS - should not show actions and app marketplace', () => {
const wrapper = shallowWithIntl(
<ActionsMenu {...baseProps}/>,
);
wrapper.setProps({
pluginMenuItems: dropdownComponents,
isSysAdmin: false,
canOpenMarketplace: false,
});
expect(wrapper).toMatchSnapshot();
});
@@ -91,6 +94,11 @@ describe('components/actions_menu/ActionsMenu', () => {
const wrapper = shallowWithIntl(
<ActionsMenu {...baseProps}/>,
);
wrapper.setProps({
canOpenMarketplace: true,
});
expect(wrapper).toMatchSnapshot();
});
@@ -116,6 +124,7 @@ describe('components/actions_menu/ActionsMenu', () => {
components: {
[PLUGGABLE_COMPONENT]: dropdownComponents,
},
canOpenMarketplace: true,
});
expect(wrapper.find('#divider_post_post_id_1_marketplace').exists()).toBe(true);
});

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

@@ -20,6 +20,7 @@ import Permissions from 'mattermost-redux/constants/permissions';
import {ActionsTutorialTip} from 'components/actions_menu/actions_menu_tutorial_tip';
import {ModalData} from 'types/actions';
import MarketplaceModal from 'components/plugin_marketplace';
import {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
import OverlayTrigger from 'components/overlay_trigger';
import * as PostUtils from 'utils/post_utils';
import * as Utils from 'utils/utils';
@@ -49,6 +50,7 @@ export type Props = {
handleDismissTip: () => void;
showPulsatingDot?: boolean;
showTutorialTip: boolean;
canOpenMarketplace: boolean;
/**
* Components for overriding provided by plugins
@@ -145,9 +147,11 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
}
handleOpenMarketplace = (): void => {
const openedFrom: OpenedFromType = 'actions_menu';
const openMarketplaceData = {
modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
dialogType: MarketplaceModal,
dialogProps: {openedFrom},
};
this.props.actions.openModal(openMarketplaceData);
};
@@ -341,7 +345,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
const {formatMessage} = this.props.intl;
let marketPlace = null;
if (this.props.isSysAdmin) {
if (this.props.canOpenMarketplace) {
marketPlace = (
<React.Fragment key={'marketplace'}>
{this.renderDivider('marketplace')}
@@ -363,11 +367,11 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
const hasPluginItems = Boolean(pluginItems?.length);
const hasPluginMenuItems = hasPluginItems || hasApps || hasPluggables;
if (!this.props.isSysAdmin && !hasPluginMenuItems) {
if (!this.props.canOpenMarketplace && !hasPluginMenuItems) {
return null;
}
if (hasPluginItems || hasApps || hasPluggables) {
if (hasPluginMenuItems) {
const pluggable = (
<Pluggable
postId={this.props.post.id}

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

@@ -44,6 +44,7 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
showTutorialTip: false,
appsEnabled: false,
isSysAdmin: true,
canOpenMarketplace: false,
};
const wrapper = shallow(

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

@@ -44,6 +44,7 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
showTutorialTip: false,
appsEnabled: false,
isSysAdmin: true,
canOpenMarketplace: false,
};
const wrapper = shallow(

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

@@ -26,6 +26,10 @@ import {GlobalState} from 'types/store';
import {openModal} from 'actions/views/modals';
import {makeFetchBindings, postEphemeralCallResponseForPost, handleBindingClick, openAppsModal} from 'actions/apps';
import {Permissions} from 'mattermost-redux/constants';
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
import ActionsMenu from './actions_menu';
import {makeGetPostOptionBinding} from './selectors';
@@ -65,6 +69,10 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
pluginMenuItems: state.plugins.components.PostDropdownMenu,
teamId: getCurrentTeamId(state),
isMobileView: getIsMobileView(state),
canOpenMarketplace: (
isMarketplaceEnabled(state) &&
haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_WRITE_PLUGINS)
),
};
}

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

@@ -1,63 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/app_bar/app_bar should match snapshot on mount 1`] = `
.c0:last-child,
.c0:first-child {
display: none;
}
<AppBar>
<div
className="app-bar"
>
<AppBarPluginComponent
component={
Object {
"action": [MockFunction],
"icon": "fallback_component",
"id": "the_component_id",
"pluginId": "playbooks",
"tooltipText": "Playbooks Tooltip",
}
}
key="the_component_id"
<div
className="app-bar__top"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
placement="right"
>
<span>
Playbooks Tooltip
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
<AppBarPluginComponent
component={
Object {
"action": [MockFunction],
"icon": "fallback_component",
"id": "the_component_id",
"pluginId": "playbooks",
"tooltipText": "Playbooks Tooltip",
}
}
key="the_component_id"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
<Tooltip
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
intl={null}
placement="right"
>
<span>
Playbooks Tooltip
</span>
</OverlayWrapper>
</Tooltip>
}
placement="left"
trigger={
@@ -67,77 +42,73 @@ exports[`components/app_bar/app_bar should match snapshot on mount 1`] = `
]
}
>
<div
className="app-bar__icon"
id="app-bar-icon-playbooks"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="pluginTooltip-app-bar-icon-playbooks"
intl={null}
placement="right"
>
<span>
Playbooks Tooltip
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
className="app-bar__icon"
id="app-bar-icon-playbooks"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
fallback_component
<div
className="app-bar__old-icon app-bar__icon-inner app-bar__icon-inner--centered"
>
fallback_component
</div>
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</OverlayTrigger>
</AppBarPluginComponent>
<_StyledHr
className="app-bar__divider"
key="divider"
>
</AppBarPluginComponent>
<hr
className="c0 app-bar__divider"
className="app-bar__divider"
key="divider"
/>
</_StyledHr>
<AppBarBinding
binding={
Object {
"app_id": "com.mattermost.zendesk",
"label": "Create Subscription",
}
}
key="com.mattermost.zendesk_Create Subscription"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
placement="right"
>
<span>
Create Subscription
</span>
</Tooltip>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
<AppBarBinding
binding={
Object {
"app_id": "com.mattermost.zendesk",
"label": "Create Subscription",
}
}
key="com.mattermost.zendesk_Create Subscription"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
<Tooltip
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
intl={null}
placement="right"
>
<span>
Create Subscription
</span>
</OverlayWrapper>
</Tooltip>
}
placement="left"
trigger={
@@ -147,25 +118,49 @@ exports[`components/app_bar/app_bar should match snapshot on mount 1`] = `
]
}
>
<div
aria-label="Create Subscription"
className="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<OverlayWrapper
bsClass="tooltip"
id="tooltip-app-bar-icon-com.mattermost.zendesk"
intl={null}
placement="right"
>
<span>
Create Subscription
</span>
</OverlayWrapper>
}
placement="left"
trigger={
Array [
"hover",
"focus",
]
}
>
<div
className="app-bar__icon-inner"
aria-label="Create Subscription"
className="app-bar__icon"
id="app-bar-icon-com.mattermost.zendesk"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
>
<img />
<div
className="app-bar__icon-inner"
>
<img />
</div>
</div>
</div>
</OverlayTrigger>
</OverlayTrigger>
</OverlayTrigger>
</AppBarBinding>
</AppBarBinding>
</div>
</div>
</AppBar>
`;

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

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
@import 'utils/mixins';
$app-bar-icon-size: 24px;
$app-bar-width: 48px;
@@ -10,129 +12,143 @@ $app-bar-width: 48px;
display: none;
}
position: relative;
width: $app-bar-width;
padding-top: 16px;
display: flex;
min-height: 0;
flex-flow: column;
border-left: solid 1px rgba(var(--center-channel-color-rgb), 0.12);
background-color: var(--center-channel-bg);
-ms-overflow-style: none;
overflow-x: hidden;
overflow-y: scroll;
scrollbar-width: none;
text-align: center;
&::before {
position: absolute;
top: 0;
display: block;
width: 100%;
height: 100%;
border-left: solid 1px rgba(var(--center-channel-color-rgb), 0.12);
background-color: rgba(var(--center-channel-color-rgb), 0.04);
content: '';
}
.app-bar__icon {
&__top {
position: relative;
// Render App Bar icons on top of the RHS background div
//(see `@media screen and (min-width: 769px) > #sidebar-right` in sass/layout/_sidebar-right.scss)
z-index: 21;
width: 100%;
border-left: none;
margin-bottom: 16px;
cursor: pointer;
display: flex;
width: $app-bar-width;
flex: 1;
flex-flow: column;
padding-top: 16px;
background-color: rgba(var(--center-channel-color-rgb), 0.04);
-ms-overflow-style: none;
overflow-x: hidden;
overflow-y: scroll;
scrollbar-width: none;
text-align: center;
&--active {
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: $app-bar-icon-size;
background-color: var(--sidebar-text-active-border);
border-radius: 0 2px 2px 0;
content: '';
.app-bar__icon {
position: relative;
// Render App Bar icons on top of the RHS background div
//(see `@media screen and (min-width: 769px) > #sidebar-right` in sass/layout/_sidebar-right.scss)
z-index: 21;
width: 100%;
border-left: none;
margin-bottom: 16px;
cursor: pointer;
&--active {
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: $app-bar-icon-size;
background-color: var(--sidebar-text-active-border);
border-radius: 0 2px 2px 0;
content: '';
}
.app-bar__icon-inner,
span:not(.pulsating_dot) {
// if we want to show a tourtip/pulsating dot in any of the app bar icons, these styles must be ommitted when span.pulsating_dot
box-shadow: 0 0 0 2px var(--sidebar-text-active-border);
&:hover {
box-shadow: 0 0 0 2px rgba(var(--sidebar-text-active-border-rgb), 0.92) !important;
}
}
}
.app-bar__icon-inner,
span:not(.pulsating_dot) {
// if we want to show a tourtip/pulsating dot in any of the app bar icons, these styles must be ommitted when span.pulsating_dot
box-shadow: 0 0 0 2px var(--sidebar-text-active-border);
&:hover {
box-shadow: 0 0 0 2px rgba(var(--sidebar-text-active-border-rgb), 0.92) !important;
}
}
}
.app-bar__icon-inner,
span:not(.pulsating_dot) {
display: block;
overflow: hidden;
width: $app-bar-icon-size;
height: $app-bar-icon-size;
margin: 0 auto;
border-radius: 50%;
line-height: 1;
&:hover {
box-shadow: 0 0 0 2px rgba(var(--center-channel-color-rgb), 0.16);
}
img {
display: block;
overflow: hidden;
width: $app-bar-icon-size;
height: $app-bar-icon-size;
margin: 0 auto;
border-radius: 50%;
}
}
line-height: 1;
span:not(.pulsating_dot) {
padding: 2px;
background-color: white;
fill: var(--button-bg);
font-size: 14px;
line-height: 20px;
vertical-align: middle;
&:hover {
box-shadow: 0 0 0 2px rgba(var(--center-channel-color-rgb), 0.16);
}
&.CompassIcon,
&.icon-brand-zoom {
font-size: 20px;
&::before {
margin: 0 0 0 0.5px;
img {
width: $app-bar-icon-size;
height: $app-bar-icon-size;
border-radius: 50%;
}
}
}
.app-bar__old-icon {
color: rgba(var(--center-channel-color-rgb), 0.56);
span:not(.pulsating_dot) {
padding: 2px;
background-color: white;
fill: var(--button-bg);
font-size: 14px;
line-height: 20px;
vertical-align: middle;
&:hover,
&--active {
color: rgba(var(--center-channel-color-rgb), 0.72);
&.CompassIcon,
&.icon-brand-zoom {
font-size: 20px;
&::before {
margin: 0 0 0 0.5px;
}
}
}
.app-bar__old-icon {
color: rgba(var(--center-channel-color-rgb), 0.56);
&:hover,
&--active {
color: rgba(var(--center-channel-color-rgb), 0.72);
}
}
.app-bar__icon-inner--centered {
display: grid;
place-items: center;
}
}
.app-bar__icon-inner--centered {
display: grid;
place-items: center;
.app-bar__divider {
width: 28px;
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
margin-top: 14px;
margin-bottom: 14px;
}
.app-bar__icon.channel-header__icon--active {
background: rgba(var(--button-bg-rgb), 0.08);
color: var(--button-bg);
fill: var(--button-bg);
}
}
.app-bar__divider {
width: 28px;
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
margin-top: 14px;
margin-bottom: 14px;
}
&__bottom {
display: flex;
flex-flow: column;
align-items: center;
padding-top: 24px;
padding-bottom: 36px;
background-color: rgba(var(--center-channel-color-rgb), 0.04);
.app-bar__icon.channel-header__icon--active {
background: rgba(var(--button-bg-rgb), 0.08);
color: var(--button-bg);
fill: var(--button-bg);
.app_bar__marketplace_button {
@include icon-button;
@include icon-button-small-compact;
}
}
}
// This style is defined outside the .app-bar block above because it doesn't seem to work when defined there
.app-bar::-webkit-scrollbar {
.app-bar__top::-webkit-scrollbar {
display: none;
}

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {mount} from 'enzyme';
import {mount, shallow} from 'enzyme';
import 'jest-styled-components';
import {AppBinding} from '@mattermost/types/apps';
@@ -10,6 +10,7 @@ import {AppBinding} from '@mattermost/types/apps';
import {PluginComponent} from 'types/store/plugins';
import {GlobalState} from 'types/store';
import {Permissions} from 'mattermost-redux/constants';
import {AppBindingLocations} from 'mattermost-redux/constants/apps';
import AppBar from './app_bar';
@@ -82,6 +83,21 @@ describe('components/app_bar/app_bar', () => {
myPreferences: {
},
} as any,
users: {
currentUserId: 'user1',
profiles: {
user1: {
roles: 'system_user',
},
},
} as any,
roles: {
roles: {
system_user: {
permissions: [],
},
},
} as any,
},
} as GlobalState;
});
@@ -134,4 +150,48 @@ describe('components/app_bar/app_bar', () => {
expect(wrapper).toMatchSnapshot();
});
test('should not show marketplace if disabled or user does not have SYSCONSOLE_WRITE_PLUGINS permission', async () => {
mockState.entities.general = {
config: {
EnableAppBar: 'true',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
},
} as any;
const wrapper = shallow(
<AppBar/>,
);
expect(wrapper.find('AppBarMarketplace').exists()).toEqual(false);
});
test('should show marketplace if enabled and user has SYSCONSOLE_WRITE_PLUGINS permission', async () => {
mockState.entities.general = {
config: {
EnableAppBar: 'true',
FeatureFlagAppsEnabled: 'true',
EnableMarketplace: 'true',
PluginsEnabled: 'true',
},
} as any;
mockState.entities.roles = {
roles: {
system_user: {
permissions: [
Permissions.SYSCONSOLE_WRITE_PLUGINS,
],
},
},
} as any;
const wrapper = shallow(
<AppBar/>,
);
expect(wrapper.find('AppBarMarketplace').exists()).toEqual(true);
});
});

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

@@ -12,8 +12,15 @@ import {getAppBarAppBindings} from 'mattermost-redux/selectors/entities/apps';
import {getAppBarPluginComponents, getChannelHeaderPluginComponents, shouldShowAppBar} from 'selectors/plugins';
import {suitePluginIds} from 'utils/constants';
import {Permissions} from 'mattermost-redux/constants';
import {isMarketplaceEnabled} from 'mattermost-redux/selectors/entities/general';
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
import {GlobalState} from '@mattermost/types/store';
import AppBarPluginComponent, {isAppBarPluginComponent} from './app_bar_plugin_component';
import AppBarBinding, {isAppBinding} from './app_bar_binding';
import AppBarMarketplace from './app_bar_marketplace';
import './app_bar.scss';
@@ -24,6 +31,10 @@ export default function AppBar() {
const currentProduct = useCurrentProduct();
const currentProductId = useCurrentProductId();
const enabled = useSelector(shouldShowAppBar);
const canOpenMarketplace = useSelector((state: GlobalState) => (
isMarketplaceEnabled(state) &&
haveICurrentTeamPermission(state, Permissions.SYSCONSOLE_WRITE_PLUGINS)
));
if (
!enabled ||
@@ -40,11 +51,15 @@ export default function AppBar() {
const items: ReactNode[] = [
...coreProductComponents,
divider,
getDivider(coreProductComponents.length, (pluginComponents.length + channelHeaderComponents.length + appBarBindings.length)),
...pluginComponents,
...channelHeaderComponents,
...appBarBindings,
].map((x) => {
if (!x) {
return x;
}
if (isAppBarPluginComponent(x)) {
if (!inScope(x.supportedProductIds ?? null, currentProductId, currentProduct?.pluginId)) {
return null;
@@ -69,26 +84,23 @@ export default function AppBar() {
return x;
});
if (!items.some((x) => Boolean(x) && x !== divider)) {
return null;
}
return (
<div className={'app-bar'}>
{items}
<div className={'app-bar__top'}>
{items}
</div>
{canOpenMarketplace && (
<div className='app-bar__bottom'>
<AppBarMarketplace/>
</div>
)}
</div>
);
}
const divider = (
const getDivider = (beforeCount: number, afterCount: number) => (beforeCount && afterCount ? (
<hr
key='divider'
className={'app-bar__divider'}
// eslint-disable-next-line react/no-unknown-property
css={`
:last-child, :first-child {
display: none;
}
`}
className='app-bar__divider'
/>
);
) : null);

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

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback} from 'react';
import {useDispatch} from 'react-redux';
import {useIntl} from 'react-intl';
import {Tooltip} from 'react-bootstrap';
import Icon from '@mattermost/compass-components/foundations/icon';
import {openModal} from 'actions/views/modals';
import MarketplaceModal from 'components/plugin_marketplace';
import OverlayTrigger from 'components/overlay_trigger';
import {Constants, ModalIdentifiers} from 'utils/constants';
const AppBarMarketplace = () => {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const handleOpenMarketplace = useCallback(() => {
dispatch(
openModal({
modalId: ModalIdentifiers.PLUGIN_MARKETPLACE,
dialogType: MarketplaceModal,
dialogProps: {openedFrom: 'app_bar'},
}),
);
}, [dispatch]);
const label = formatMessage({id: 'app_bar.marketplace', defaultMessage: 'App Marketplace'});
return (
<OverlayTrigger
trigger={['hover', 'focus']}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement='left'
overlay={(
<Tooltip id='tooltip-app-bar-marketplace'>
<span>{label}</span>
</Tooltip>
)}
>
<button
key='app_bar_marketplace'
className='app_bar__marketplace_button'
aria-label={label}
onClick={handleOpenMarketplace}
>
<Icon
size={16}
glyph={'view-grid-plus-outline'}
/>
</button>
</OverlayTrigger>
);
};
export default AppBarMarketplace;

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

@@ -301,6 +301,7 @@ export default function OpenPluginInstallPost(props: {post: Post}) {
className='color--link'
modalId={ModalIdentifiers.PLUGIN_MARKETPLACE}
dialogType={MarketplaceModal}
dialogProps={{openedFrom: 'open_plugin_install_post'}}
>
{text}
</ToggleModalButton>

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

@@ -147,6 +147,11 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
teamId=""
>
<MenuItemToggleModalRedux
dialogProps={
Object {
"openedFrom": "product_menu",
}
}
dialogType={
Object {
"$$typeof": Symbol(react.memo),
@@ -157,14 +162,14 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
}
icon={
<Icon
glyph="apps"
glyph="view-grid-plus-outline"
size={16}
/>
}
id="marketplaceModal"
modalId="plugin_marketplace"
show={false}
text="Marketplace"
text="App Marketplace"
/>
</Connect(TeamPermissionGate)>
<MenuItemExternalLink
@@ -350,6 +355,11 @@ exports[`components/global/product_switcher_menu should match snapshot with most
teamId=""
>
<MenuItemToggleModalRedux
dialogProps={
Object {
"openedFrom": "product_menu",
}
}
dialogType={
Object {
"$$typeof": Symbol(react.memo),
@@ -360,14 +370,14 @@ exports[`components/global/product_switcher_menu should match snapshot with most
}
icon={
<Icon
glyph="apps"
glyph="view-grid-plus-outline"
size={16}
/>
}
id="marketplaceModal"
modalId="plugin_marketplace"
show={true}
text="Marketplace"
text="App Marketplace"
/>
</Connect(TeamPermissionGate)>
<MenuItemExternalLink
@@ -673,6 +683,11 @@ exports[`components/global/product_switcher_menu should show integrations should
teamId=""
>
<MenuItemToggleModalRedux
dialogProps={
Object {
"openedFrom": "product_menu",
}
}
dialogType={
Object {
"$$typeof": Symbol(react.memo),
@@ -683,14 +698,14 @@ exports[`components/global/product_switcher_menu should show integrations should
}
icon={
<Icon
glyph="apps"
glyph="view-grid-plus-outline"
size={16}
/>
}
id="marketplaceModal"
modalId="plugin_marketplace"
show={false}
text="Marketplace"
text="App Marketplace"
/>
</Connect(TeamPermissionGate)>
<MenuItemExternalLink

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

@@ -213,11 +213,12 @@ const ProductMenuList = (props: Props): JSX.Element | null => {
modalId={ModalIdentifiers.PLUGIN_MARKETPLACE}
show={isMessaging && !isMobile && enablePluginMarketplace}
dialogType={MarketplaceModal}
text={formatMessage({id: 'navbar_dropdown.marketplace', defaultMessage: 'Marketplace'})}
dialogProps={{openedFrom: 'product_menu'}}
text={formatMessage({id: 'navbar_dropdown.marketplace', defaultMessage: 'App Marketplace'})}
icon={
<Icon
size={16}
glyph={'apps'}
glyph='view-grid-plus-outline'
/>
}
/>

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

@@ -119,6 +119,7 @@ describe('components/marketplace/', () => {
pluginStatuses: {},
siteURL: 'http://example.com',
firstAdminVisitMarketplaceStatus: false,
openedFrom: 'actions_menu',
actions: {
closeModal: jest.fn(),
fetchListing: jest.fn(() => {
@@ -191,8 +192,21 @@ describe('components/marketplace/', () => {
wrapper.setState({filter: 'nps'});
wrapper.instance().doSearch();
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened');
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: 'actions_menu'});
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_search', {filter: 'nps'});
});
test('Should call for opened track event on mount', () => {
const openedFrom = 'actions_menu';
shallow<MarketplaceModal>(
<MarketplaceModal
{...baseProps}
openedFrom={openedFrom}
/>,
);
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: openedFrom});
});
});
});

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

@@ -31,6 +31,8 @@ const MarketplaceTabs = {
const SEARCH_TIMEOUT_MILLISECONDS = 200;
export type OpenedFromType = 'actions_menu' | 'app_bar' | 'channel_header' | 'command' | 'open_plugin_install_post' | 'product_menu';
type AllListingProps = {
listing: Array<MarketplacePlugin | MarketplaceApp>;
};
@@ -97,6 +99,7 @@ export type MarketplaceModalProps = {
siteURL: string;
pluginStatuses?: Record<string, PluginStatusRedux>;
firstAdminVisitMarketplaceStatus: boolean;
openedFrom: OpenedFromType;
actions: {
closeModal: () => void;
fetchListing(localOnly?: boolean): Promise<{error?: Error}>;
@@ -131,7 +134,7 @@ export default class MarketplaceModal extends React.PureComponent<MarketplaceMod
}
componentDidMount(): void {
trackEvent('plugins', 'ui_marketplace_opened');
trackEvent('plugins', 'ui_marketplace_opened', {from: this.props.openedFrom});
this.fetchListing();
this.props.actions.getPluginStatuses();

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

@@ -2645,6 +2645,7 @@
"api.team.join_team.post_and_forget": "{username} joined the team.",
"api.team.leave.left": "{username} left the team.",
"api.team.remove_user_from_team.removed": "{removedUsername} was removed from the team.",
"app_bar.marketplace": "App Marketplace",
"app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})",
"app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}",
"app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}",
@@ -4222,7 +4223,7 @@
"navbar_dropdown.logout": "Log Out",
"navbar_dropdown.manageGroups": "Manage Groups",
"navbar_dropdown.manageMembers": "Manage Members",
"navbar_dropdown.marketplace": "Marketplace",
"navbar_dropdown.marketplace": "App Marketplace",
"navbar_dropdown.menuAriaLabel": "main menu",
"navbar_dropdown.nativeApps": "Download Apps",
"navbar_dropdown.profileSettings": "Profile",

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

@@ -262,6 +262,49 @@
}
}
@mixin icon-button {
display: flex;
align-items: center;
justify-content: center;
border: 0;
background: none;
border-radius: 4px;
color: rgba(var(--center-channel-color-rgb), 0.56);
&:focus,
&:focus-within {
box-sizing: border-box;
border: 2px solid rgba(var(--denim-button-bg-rgb), 0.32);
box-shadow: none;
outline: none;
}
&:hover {
border: 0;
background: rgba(var(--center-channel-color-rgb), 0.08);
color: rgba(var(--center-channel-color-rgb), 0.72);
}
&:active {
border: 0;
background: rgba(var(--button-bg-rgb), 0.08);
color: var(--button-bg);
}
&:disabled {
background: none;
color: rgba(var(--center-channel-color-rgb), 0.32);
cursor: not-allowed;
}
}
@mixin icon-button-small-compact {
width: 28px;
height: 28px;
padding: 6px;
font-size: 16px;
}
@mixin simple-in-and-out-after($classPrefix, $transition_time: 300ms) {
.#{$classPrefix}--enter-from-after {
&-enter {