{
entities: {
general: {
firstAdminCompleteSetup: false,
+ config: {
+ FeatureFlagStreamlinedMarketplace: 'false',
+ },
+ license: {
+ Cloud: 'false',
+ },
},
admin: {
pluginStatuses: {},
@@ -156,4 +163,49 @@ describe('components/marketplace/', () => {
expect(wrapper.shallow()).toMatchSnapshot();
});
+
+ test('hides search, shows web marketplace banner in FeatureFlags.StreamlinedMarketplace', () => {
+ const setState = jest.fn();
+ const useStateSpy = jest.spyOn(React, 'useState');
+ useStateSpy.mockImplementation(() => [true, setState]);
+
+ mockState.views.marketplace.plugins = [
+ samplePlugin,
+ sampleInstalledPlugin,
+ ];
+
+ (mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true';
+
+ const wrapper = shallow(
+
,
+ );
+
+ wrapper.update();
+ const content = wrapper.shallow();
+
+ expect(content.exists('#searchMarketplaceTextbox')).toBe(false);
+ expect(content.exists(WebMarketplaceBanner)).toBe(true);
+
+ expect(content).toMatchSnapshot();
+ });
+
+ test("doesn't show web marketplace banner in FeatureFlags.StreamlinedMarketplace for Cloud", () => {
+ const setState = jest.fn();
+ const useStateSpy = jest.spyOn(React, 'useState');
+ useStateSpy.mockImplementation(() => [true, setState]);
+
+ (mockState.entities.general.config as any).FeatureFlagStreamlinedMarketplace = 'true';
+ mockState.entities.general.license.Cloud = 'true';
+
+ const wrapper = shallow(
+
,
+ );
+
+ wrapper.update();
+ const content = wrapper.shallow();
+
+ expect(content.exists(WebMarketplaceBanner)).toBe(false);
+
+ expect(content).toMatchSnapshot();
+ });
});
diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx
index 23fd7a69f0..03445ea59d 100644
--- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx
+++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx
@@ -13,7 +13,8 @@ import {MagnifyIcon} from '@mattermost/compass-icons/components';
import {FooterPagination, GenericModal} from '@mattermost/components';
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
-import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
+import {getFirstAdminVisitMarketplaceStatus, getLicense} from 'mattermost-redux/selectors/entities/general';
+import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {ActionResult} from 'mattermost-redux/types/actions';
import {fetchListing, filterListing} from 'actions/marketplace';
@@ -27,10 +28,13 @@ import {getListing, getInstalledListing} from 'selectors/views/marketplace';
import {isModalOpen} from 'selectors/views/modals';
import {GlobalState} from 'types/store';
import {ModalIdentifiers} from 'utils/constants';
+import WebMarketplaceBanner from './web_marketplace_banner';
+import {isCloudLicense} from 'utils/license_utils';
import './marketplace_modal.scss';
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
+import classNames from 'classnames';
const MarketplaceTabs = {
ALL_LISTING: 'all',
@@ -63,6 +67,9 @@ const MarketplaceModal = ({
const installedListing = useSelector(getInstalledListing);
const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses);
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
+ const isStreamlinedMarketplaceEnabled = useSelector(streamlinedMarketplaceEnabled);
+ const license = useSelector(getLicense);
+ const isCloud = isCloudLicense(license);
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
const [filter, setFilter] = useState('');
@@ -162,39 +169,62 @@ const MarketplaceModal = ({
handleChangeTab(MarketplaceTabs.ALL_LISTING);
}, [handleChangeTab]);
- const getHeaderInput = useCallback(() => (
-
}
- placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})}
- useLegend={false}
- autoFocus={true}
- clearable={true}
- value={filter}
- onChange={handleOnChange}
- onClear={handleOnClear}
- />
- ), [filter, handleOnChange, handleOnClear]);
+ const getHeaderInput = useCallback(() => {
+ if (isStreamlinedMarketplaceEnabled) {
+ return null;
+ }
- const getFooterContent = useCallback(() => (
-
- ), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]);
+ return (
+
}
+ placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})}
+ useLegend={false}
+ autoFocus={true}
+ clearable={true}
+ value={filter}
+ onChange={handleOnChange}
+ onClear={handleOnClear}
+ />
+ );
+ }, [filter, handleOnChange, handleOnClear]);
+
+ const getFooterContent = useCallback(() => {
+ if (isStreamlinedMarketplaceEnabled && listing.length <= ITEMS_PER_PAGE) {
+ return null;
+ }
+
+ return (
+
+ );
+ }, [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey, isStreamlinedMarketplaceEnabled]);
+
+ const getAppendedContent = useCallback(() => {
+ if (!isStreamlinedMarketplaceEnabled || isCloud) {
+ return null;
+ }
+
+ return
;
+ }, [isStreamlinedMarketplaceEnabled, isCloud]);
return (
-
-
- {loading ? (
-
- ) : (
-
- )}
-
-
+ {isStreamlinedMarketplaceEnabled ? (
+ <>
-
-
+ >
+ ) : (
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ )}
);
};
diff --git a/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx b/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx
new file mode 100644
index 0000000000..a3868fee15
--- /dev/null
+++ b/webapp/channels/src/components/plugin_marketplace/web_marketplace_banner.tsx
@@ -0,0 +1,109 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {useIntl} from 'react-intl';
+import styled from 'styled-components';
+import ExternalLink from '../external_link';
+
+import webMarketplaceBannerBackground from 'images/marketplace-notice-background.jpg';
+import pluginIconConfluence from 'images/icons/confluence.svg';
+import pluginIconGiphy from 'images/icons/giphy.svg';
+import pluginIconPagerDuty from 'images/icons/pager-duty.svg';
+
+import {ArrowRightIcon} from '@mattermost/compass-icons/components';
+
+const WEB_MARKETPLACE_LINK = 'https://mattermost.com/marketplace';
+
+const WebMarketplaceBanner = () => {
+ const {formatMessage} = useIntl();
+
+ return (
+
+
+
+ {formatMessage({id: 'marketplace_modal.web_marketplace_link.title', defaultMessage: 'Explore Community Integrations'})}
+
+
+
+ {formatMessage({id: 'marketplace_modal.web_marketplace_link.desc', defaultMessage: 'We have dozens of community integrations available. So definitely do check them out!'})}
+
+
+
+
+
+
+
+
+ );
+};
+
+const ExternalBannerLink = styled(ExternalLink)`
+ &&,
+ &&:hover,
+ &&:focus {
+ color: var(--denim-center-channel-bg, #FFF);
+ text-decoration: none;
+ }
+ && {
+ display: grid;
+ grid-template-columns: auto auto;
+ justify-content: space-between;
+ text-align: left;
+ padding: 24px 32px;
+ }
+`;
+
+const WebMarketplaceBannerRoot = styled.section`
+ background-image: url(${webMarketplaceBannerBackground});
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ border-radius: 0 0 12px 12px !important;
+ margin: -1px;
+`;
+
+const Title = styled.div`
+ font-family: Metropolis;
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 600;
+ line-height: 24px;
+ margin: 4px 0;
+ grid-column: 1;
+
+ svg {
+ vertical-align: middle;
+ display: inline-block;
+ margin-left: 4px;
+ }
+`;
+
+const Description = styled.p`
+ font-family: Open Sans;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ grid-column: 1;
+ margin-bottom: 4px;
+`;
+
+const PluginIcon = styled.img`
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+`;
+
+const IconsContainer = styled.div`
+ grid-column: 2;
+ grid-row: span 2/2;
+ ${PluginIcon}:nth-child(n+2) {
+ margin-left: calc(-54px / 1/4);
+ }
+`;
+
+export default WebMarketplaceBanner;
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 787d8db7cf..14f46c88a3 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -3949,6 +3949,8 @@
"marketplace_modal.tabs.all_listing": "All",
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
"marketplace_modal.title": "App Marketplace",
+ "marketplace_modal.web_marketplace_link.desc": "We have dozens of community integrations available. So definitely do check them out!",
+ "marketplace_modal.web_marketplace_link.title": "Explore Community Integrations",
"menu.cloudFree.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features.
Learn more",
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
"menu.cloudFree.postTrial.tryEnterprise": "Interested in a limitless plan with high-security features?
See plans",
diff --git a/webapp/channels/src/images/icons/confluence.svg b/webapp/channels/src/images/icons/confluence.svg
new file mode 100644
index 0000000000..934c65b374
--- /dev/null
+++ b/webapp/channels/src/images/icons/confluence.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/channels/src/images/icons/giphy.svg b/webapp/channels/src/images/icons/giphy.svg
new file mode 100644
index 0000000000..78669eeaa5
--- /dev/null
+++ b/webapp/channels/src/images/icons/giphy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/channels/src/images/icons/pager-duty.svg b/webapp/channels/src/images/icons/pager-duty.svg
new file mode 100644
index 0000000000..d0d0f42aab
--- /dev/null
+++ b/webapp/channels/src/images/icons/pager-duty.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/webapp/channels/src/images/marketplace-notice-background.jpg b/webapp/channels/src/images/marketplace-notice-background.jpg
new file mode 100644
index 0000000000..8f6169fcef
Binary files /dev/null and b/webapp/channels/src/images/marketplace-notice-background.jpg differ
diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
index 1dc6b8f9f2..7ac235685e 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/preferences.ts
@@ -281,3 +281,7 @@ export function deprecateCloudFree(state: GlobalState): boolean {
export function cloudReverseTrial(state: GlobalState): boolean {
return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true';
}
+
+export function streamlinedMarketplaceEnabled(state: GlobalState): boolean {
+ return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true';
+}
diff --git a/webapp/platform/components/src/generic_modal/generic_modal.scss b/webapp/platform/components/src/generic_modal/generic_modal.scss
index 9e70d63799..7a9ae57afa 100644
--- a/webapp/platform/components/src/generic_modal/generic_modal.scss
+++ b/webapp/platform/components/src/generic_modal/generic_modal.scss
@@ -22,6 +22,10 @@
max-height: 100%;
padding: 0;
+ &.divider {
+ border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
+ }
+
.form-control {
height: 40px;
box-sizing: border-box;
@@ -106,7 +110,7 @@
border-radius: 4px;
&.divider {
- border-top: 1px solid rgba(63, 67, 80, 0.08);
+ border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
}
}
diff --git a/webapp/platform/components/src/generic_modal/generic_modal.tsx b/webapp/platform/components/src/generic_modal/generic_modal.tsx
index 3bca20efd0..18421b9ebf 100644
--- a/webapp/platform/components/src/generic_modal/generic_modal.tsx
+++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx
@@ -39,8 +39,10 @@ export type Props = {
keyboardEscape?: boolean;
headerInput?: React.ReactNode;
bodyPadding?: boolean;
+ bodyDivider?: boolean;
footerContent?: React.ReactNode;
footerDivider?: boolean;
+ appendedContent?: React.ReactNode;
headerButton?: React.ReactNode;
};
@@ -199,7 +201,7 @@ export class GenericModal extends React.PureComponent
{
>
)}
-
+
{this.props.compassDesign ? (
this.props.errorText && (
@@ -226,6 +228,7 @@ export class GenericModal extends React.PureComponent
{
)}
)}
+ {Boolean(this.props.appendedContent) && this.props.appendedContent}
);