MM-50087_Add marketplace button to apps bar (#22653)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6c9ec24fb9
Коммит
da7a6825ce
@@ -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);
|
||||
|
||||
60
webapp/channels/src/components/app_bar/app_bar_marketplace.tsx
Обычный файл
60
webapp/channels/src/components/app_bar/app_bar_marketplace.tsx
Обычный файл
@@ -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 {
|
||||
|
||||
Ссылка в новой задаче
Block a user