diff --git a/e2e/cypress/tests/integration/channels/plugins/marketplace/helpers.js b/e2e/cypress/tests/integration/channels/plugins/marketplace/helpers.js index 1820015814..b1ce96d0a5 100644 --- a/e2e/cypress/tests/integration/channels/plugins/marketplace/helpers.js +++ b/e2e/cypress/tests/integration/channels/plugins/marketplace/helpers.js @@ -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'); } }); } diff --git a/webapp/channels/src/actions/command.test.js b/webapp/channels/src/actions/command.test.js index c83037f136..de9a9b01be 100644 --- a/webapp/channels/src/actions/command.test.js +++ b/webapp/channels/src/actions/command.test.js @@ -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, ], }, }, diff --git a/webapp/channels/src/actions/command.ts b/webapp/channels/src/actions/command.ts index 255dff4c51..2b89fae193 100644 --- a/webapp/channels/src/actions/command.ts +++ b/webapp/channels/src/actions/command.ts @@ -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); diff --git a/webapp/channels/src/components/actions_menu/__snapshots__/actions_menu.test.tsx.snap b/webapp/channels/src/components/actions_menu/__snapshots__/actions_menu.test.tsx.snap index 260071f49f..60367b08bf 100644 --- a/webapp/channels/src/components/actions_menu/__snapshots__/actions_menu.test.tsx.snap +++ b/webapp/channels/src/components/actions_menu/__snapshots__/actions_menu.test.tsx.snap @@ -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`] = ` `; -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`] = ` { 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( , ); 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( , ); wrapper.setProps({ pluginMenuItems: dropdownComponents, - isSysAdmin: false, + canOpenMarketplace: false, }); expect(wrapper).toMatchSnapshot(); }); @@ -91,6 +94,11 @@ describe('components/actions_menu/ActionsMenu', () => { const wrapper = shallowWithIntl( , ); + + 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); }); diff --git a/webapp/channels/src/components/actions_menu/actions_menu.tsx b/webapp/channels/src/components/actions_menu/actions_menu.tsx index 8a31075676..547aac2da1 100644 --- a/webapp/channels/src/components/actions_menu/actions_menu.tsx +++ b/webapp/channels/src/components/actions_menu/actions_menu.tsx @@ -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 { } 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 { const {formatMessage} = this.props.intl; let marketPlace = null; - if (this.props.isSysAdmin) { + if (this.props.canOpenMarketplace) { marketPlace = ( {this.renderDivider('marketplace')} @@ -363,11 +367,11 @@ export class ActionMenuClass extends React.PureComponent { 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 = ( { showTutorialTip: false, appsEnabled: false, isSysAdmin: true, + canOpenMarketplace: false, }; const wrapper = shallow( diff --git a/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx b/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx index e712e9bb4b..3f4f3c3534 100644 --- a/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx +++ b/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx @@ -44,6 +44,7 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => { showTutorialTip: false, appsEnabled: false, isSysAdmin: true, + canOpenMarketplace: false, }; const wrapper = shallow( diff --git a/webapp/channels/src/components/actions_menu/index.ts b/webapp/channels/src/components/actions_menu/index.ts index 517aac9fad..21c9c2588b 100644 --- a/webapp/channels/src/components/actions_menu/index.ts +++ b/webapp/channels/src/components/actions_menu/index.ts @@ -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) + ), }; } diff --git a/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap b/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap index 0f2403dc6b..528d89c426 100644 --- a/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap +++ b/webapp/channels/src/components/app_bar/__snapshots__/app_bar.test.tsx.snap @@ -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; -} -
- - - - Playbooks Tooltip - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] + Playbooks Tooltip - + } placement="left" trigger={ @@ -67,77 +42,73 @@ exports[`components/app_bar/app_bar should match snapshot on mount 1`] = ` ] } > -
+ + Playbooks Tooltip + + + } + placement="left" + trigger={ + Array [ + "hover", + "focus", + ] + } >
- fallback_component +
+ fallback_component +
-
+
- -
- <_StyledHr - className="app-bar__divider" - key="divider" - > +
- - - - - Create Subscription - - - } - placement="left" - trigger={ - Array [ - "hover", - "focus", - ] + Create Subscription - + } placement="left" trigger={ @@ -147,25 +118,49 @@ exports[`components/app_bar/app_bar should match snapshot on mount 1`] = ` ] } > -
+ + Create Subscription + + + } + placement="left" + trigger={ + Array [ + "hover", + "focus", + ] + } >
- +
+ +
-
+
- -
+ +
`; diff --git a/webapp/channels/src/components/app_bar/app_bar.scss b/webapp/channels/src/components/app_bar/app_bar.scss index c45afabf44..ce7813b3c9 100644 --- a/webapp/channels/src/components/app_bar/app_bar.scss +++ b/webapp/channels/src/components/app_bar/app_bar.scss @@ -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; } diff --git a/webapp/channels/src/components/app_bar/app_bar.test.tsx b/webapp/channels/src/components/app_bar/app_bar.test.tsx index 03598dbf3d..3f8d429853 100644 --- a/webapp/channels/src/components/app_bar/app_bar.test.tsx +++ b/webapp/channels/src/components/app_bar/app_bar.test.tsx @@ -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( + , + ); + + 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( + , + ); + + expect(wrapper.find('AppBarMarketplace').exists()).toEqual(true); + }); }); diff --git a/webapp/channels/src/components/app_bar/app_bar.tsx b/webapp/channels/src/components/app_bar/app_bar.tsx index ec05a5bd19..3ae5815c2a 100644 --- a/webapp/channels/src/components/app_bar/app_bar.tsx +++ b/webapp/channels/src/components/app_bar/app_bar.tsx @@ -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 (
- {items} +
+ {items} +
+ {canOpenMarketplace && ( +
+ +
+ )}
); } -const divider = ( +const getDivider = (beforeCount: number, afterCount: number) => (beforeCount && afterCount ? (
-); +) : null); diff --git a/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx b/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx new file mode 100644 index 0000000000..11b379bd75 --- /dev/null +++ b/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 ( + + {label} + + )} + > + + + ); +}; + +export default AppBarMarketplace; diff --git a/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx b/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx index 06020b66c4..d16f073cf1 100644 --- a/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx +++ b/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx @@ -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} diff --git a/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_list/__snapshots__/product_menu_list.test.tsx.snap b/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_list/__snapshots__/product_menu_list.test.tsx.snap index 94179aca7a..f8ea6848e7 100644 --- a/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_list/__snapshots__/product_menu_list.test.tsx.snap +++ b/webapp/channels/src/components/global_header/left_controls/product_menu/product_menu_list/__snapshots__/product_menu_list.test.tsx.snap @@ -147,6 +147,11 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1 teamId="" > } id="marketplaceModal" modalId="plugin_marketplace" show={false} - text="Marketplace" + text="App Marketplace" /> } id="marketplaceModal" modalId="plugin_marketplace" show={true} - text="Marketplace" + text="App Marketplace" /> } id="marketplaceModal" modalId="plugin_marketplace" show={false} - text="Marketplace" + text="App Marketplace" /> { 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={ } /> diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx index 4bf24800a9..3e2938603c 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx @@ -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( + , + ); + + expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: openedFrom}); + }); }); }); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx index bc80cde1de..59ac7510ba 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx @@ -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; }; @@ -97,6 +99,7 @@ export type MarketplaceModalProps = { siteURL: string; pluginStatuses?: Record; firstAdminVisitMarketplaceStatus: boolean; + openedFrom: OpenedFromType; actions: { closeModal: () => void; fetchListing(localOnly?: boolean): Promise<{error?: Error}>; @@ -131,7 +134,7 @@ export default class MarketplaceModal extends React.PureComponent