mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-52981, MM-53559: Streamlined in-product marketplace (#24311)
This commit is contained in:
4
.github/workflows/mmctl-test-template.yml
vendored
4
.github/workflows/mmctl-test-template.yml
vendored
@@ -34,6 +34,10 @@ jobs:
|
||||
run: |
|
||||
cd server
|
||||
make setup-go-work
|
||||
- name: Setup needed prepackaged plugins
|
||||
run: |
|
||||
cd server
|
||||
make prepackaged-plugins PLUGIN_PACKAGES=mattermost-plugin-jira-v3.2.5
|
||||
- name: Run docker compose
|
||||
run: |
|
||||
cd server/build
|
||||
|
||||
@@ -17,6 +17,7 @@ describe('Plugin Marketplace', () => {
|
||||
|
||||
before(() => {
|
||||
cy.shouldNotRunOnCloudEdition();
|
||||
cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230
|
||||
cy.shouldHavePluginUploadEnabled();
|
||||
|
||||
cy.apiInitSetup().then(({team}) => {
|
||||
@@ -72,6 +73,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.get('#marketplaceTabs-pane-allListing').should('be.visible');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('MM-T2001 autofocus on search plugin input box', () => {
|
||||
cy.uiClose();
|
||||
|
||||
@@ -82,6 +84,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.findByPlaceholderText('Search Marketplace').should('be.focused');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('render the list of all plugins by default', () => {
|
||||
// * Verify all plugins tab should be active
|
||||
cy.get('#marketplaceTabs-tab-allListing').should('be.visible').parent().should('have.class', 'active');
|
||||
@@ -92,6 +95,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.get('#marketplaceTabs-pane-installed').should('not.exist');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
// this test uses exist, not visible, due to issues with Cypress
|
||||
it('render the list of installed plugins on demand', () => {
|
||||
// # Click on installed plugins tab
|
||||
@@ -117,6 +121,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.get('#modal_marketplace').should('not.exist');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('should filter all on search', () => {
|
||||
// # Load all plugins before searching
|
||||
cy.get('.more-modal__row').should('have.length', 15);
|
||||
@@ -136,6 +141,7 @@ describe('Plugin Marketplace', () => {
|
||||
should('have.length', 1);
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('should show an error bar on failing to filter', () => {
|
||||
// # Enable Plugin Marketplace
|
||||
cy.apiUpdateConfig({
|
||||
@@ -168,6 +174,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.get('#marketplace-plugin-com\\.mattermost\\.webex').find('.btn.btn-outline', {timeout: TIMEOUTS.ONE_MIN}).scrollIntoView().should('be.visible').and('have.text', 'Configure');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('should install a plugin from search results on demand', () => {
|
||||
// # Uninstall any existing webex plugin
|
||||
cy.apiRemovePluginById('com.mattermost.webex');
|
||||
@@ -226,6 +233,7 @@ describe('Plugin Marketplace', () => {
|
||||
cy.get('#marketplace-plugin-github').should('be.visible');
|
||||
});
|
||||
|
||||
// https://mattermost.atlassian.net/browse/MM-54230
|
||||
it('MM-T1986 change tab to "All Plugins" when "Install Plugins" link is clicked', () => {
|
||||
cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => {
|
||||
// # Switch tab to installed plugin
|
||||
|
||||
@@ -679,6 +679,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
OnboardingTourTips: true,
|
||||
DeprecateCloudFree: false,
|
||||
CloudReverseTrial: false,
|
||||
StreamlinedMarketplace: true
|
||||
},
|
||||
ImportSettings: {
|
||||
Directory: './import',
|
||||
|
||||
@@ -157,7 +157,8 @@ MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl')
|
||||
TEMPLATES_DIR=templates
|
||||
|
||||
# Plugins Packages
|
||||
PLUGIN_PACKAGES ?= mattermost-plugin-antivirus-v1.0.0
|
||||
PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
|
||||
PLUGIN_PACKAGES += mattermost-plugin-antivirus-v1.0.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-autolink-v1.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.18.0
|
||||
@@ -300,7 +301,7 @@ plugin-checker:
|
||||
$(GO) run $(GOFLAGS) ./public/plugin/checker
|
||||
|
||||
prepackaged-plugins: ## Populate the prepackaged-plugins directory
|
||||
@echo Downloading prepackaged plugins
|
||||
@echo Downloading prepackaged plugins: $(PLUGIN_PACKAGES)
|
||||
mkdir -p prepackaged_plugins
|
||||
@cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
||||
curl -f -O -L https://plugins-store.test.mattermost.com/release/$$plugin_package.tar.gz; \
|
||||
|
||||
@@ -462,6 +462,9 @@ func TestDisableOnRemove(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetMarketplacePlugins(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -682,6 +685,9 @@ func TestGetMarketplacePlugins(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
samplePlugins := []*model.MarketplacePlugin{
|
||||
{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
@@ -825,6 +831,9 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
samplePlugins := []*model.MarketplacePlugin{
|
||||
{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
@@ -950,6 +959,9 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetLocalPluginInMarketplace(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -1111,6 +1123,9 @@ func TestGetLocalPluginInMarketplace(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetRemotePluginInMarketplace(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -1166,7 +1181,70 @@ func TestGetRemotePluginInMarketplace(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoteMarketplaceDisabledByStreamlinedMarketplaceFlag(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "true")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
marketplacePlugins := []*model.MarketplacePlugin{
|
||||
{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
HomepageURL: "https://example.com/mattermost/mattermost-plugin-nps",
|
||||
IconData: "https://example.com/icon.svg",
|
||||
DownloadURL: "www.github.com/example",
|
||||
Manifest: &model.Manifest{
|
||||
Id: "marketplace.test",
|
||||
Name: "marketplacetest",
|
||||
Description: "a marketplace plugin",
|
||||
Version: "0.1.2",
|
||||
MinServerVersion: "",
|
||||
},
|
||||
},
|
||||
InstalledVersion: "",
|
||||
},
|
||||
}
|
||||
|
||||
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
|
||||
res.WriteHeader(http.StatusOK)
|
||||
json, err := json.Marshal([]*model.MarketplacePlugin{marketplacePlugins[0]})
|
||||
require.NoError(t, err)
|
||||
res.Write(json)
|
||||
}))
|
||||
defer testServer.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.Enable = true
|
||||
*cfg.PluginSettings.EnableMarketplace = true
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
*cfg.PluginSettings.MarketplaceURL = testServer.URL
|
||||
})
|
||||
|
||||
prepackagePlugin := &plugin.PrepackagedPlugin{
|
||||
Manifest: &model.Manifest{
|
||||
Version: "0.0.1",
|
||||
Id: "prepackaged.test",
|
||||
},
|
||||
}
|
||||
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil)
|
||||
|
||||
// No marketplace plugins returned
|
||||
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Only returns the prepackaged plugins
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, prepackagePlugin.Manifest, plugins[0].Manifest)
|
||||
}
|
||||
|
||||
func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -1212,6 +1290,30 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
env := th.App.GetPluginsEnvironment()
|
||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil)
|
||||
|
||||
t.Run("prepackaged plugins are shown in Cloud", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
lic := th.App.Srv().License()
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
defer th.App.Srv().SetLicense(lic)
|
||||
|
||||
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPlugins := marketplacePlugins
|
||||
expectedPlugins = append(expectedPlugins, &model.MarketplacePlugin{
|
||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||
Manifest: prepackagePlugin.Manifest,
|
||||
},
|
||||
})
|
||||
|
||||
require.ElementsMatch(t, expectedPlugins, plugins)
|
||||
require.Len(t, plugins, 2)
|
||||
})
|
||||
|
||||
t.Run("get remote and prepackaged plugins", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
@@ -1271,24 +1373,12 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||
require.Len(t, plugins, 1)
|
||||
require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest)
|
||||
})
|
||||
|
||||
t.Run("prepackaged plugins are not shown in Cloud", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||
*cfg.PluginSettings.EnableUploads = true
|
||||
})
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||
|
||||
plugins, _, err := th.SystemAdminClient.GetMarketplacePlugins(context.Background(), &model.MarketplacePluginFilter{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.ElementsMatch(t, marketplacePlugins, plugins)
|
||||
require.Len(t, plugins, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -1639,6 +1729,9 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
|
||||
path, _ := fileutils.FindDir("tests")
|
||||
|
||||
signatureFilename := "testplugin2.tar.gz.sig"
|
||||
|
||||
@@ -828,6 +828,8 @@ func TestPushNotificationAck(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCompleteOnboarding(t *testing.T) {
|
||||
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||
th := Setup(t)
|
||||
defer th.TearDown()
|
||||
|
||||
|
||||
@@ -552,7 +552,7 @@ func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) {
|
||||
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
|
||||
plugins := map[string]*model.MarketplacePlugin{}
|
||||
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && !filter.LocalOnly {
|
||||
if *a.Config().PluginSettings.EnableRemoteMarketplace && !a.Config().FeatureFlags.StreamlinedMarketplace && !filter.LocalOnly {
|
||||
p, appErr := a.getRemotePlugins()
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
@@ -561,20 +561,12 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m
|
||||
}
|
||||
|
||||
if !filter.RemoteOnly {
|
||||
// Some plugin don't work on cloud. The remote Marketplace is aware of this fact,
|
||||
// but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins
|
||||
// shouldn't be shown in the Marketplace modal.
|
||||
// This is a short term fix. The long term solution is to have a separate set of
|
||||
// prepacked plugins for cloud: https://mattermost.atlassian.net/browse/MM-31331.
|
||||
license := a.Srv().License()
|
||||
if license == nil || !license.IsCloud() {
|
||||
appErr := a.mergePrepackagedPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
appErr := a.mergePrepackagedPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
appErr := a.mergeLocalPlugins(plugins)
|
||||
appErr = a.mergeLocalPlugins(plugins)
|
||||
if appErr != nil {
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl
|
||||
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
|
||||
}
|
||||
|
||||
if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace {
|
||||
if *ch.cfgSvc.Config().PluginSettings.EnableRemoteMarketplace && !ch.cfgSvc.Config().FeatureFlags.StreamlinedMarketplace {
|
||||
var plugin *model.BaseMarketplacePlugin
|
||||
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
|
||||
// The plugin might only be prepackaged and not on the Marketplace.
|
||||
|
||||
@@ -48,6 +48,8 @@ type FeatureFlags struct {
|
||||
EnableExportDirectDownload bool
|
||||
|
||||
DataRetentionConcurrencyEnabled bool
|
||||
|
||||
StreamlinedMarketplace bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -65,6 +67,7 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.CloudReverseTrial = false
|
||||
f.EnableExportDirectDownload = false
|
||||
f.DataRetentionConcurrencyEnabled = true
|
||||
f.StreamlinedMarketplace = true
|
||||
}
|
||||
|
||||
// ToMap returns the feature flags as a map[string]string
|
||||
|
||||
@@ -18,12 +18,14 @@ import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||
|
||||
import PluginManagement from './plugin_management';
|
||||
import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
function mapStateToProps(state: any) {
|
||||
return {
|
||||
plugins: state.entities.admin.plugins,
|
||||
pluginStatuses: state.entities.admin.pluginStatuses,
|
||||
appsFeatureFlagEnabled: appsFeatureFlagEnabled(state),
|
||||
streamlinedMarketplaceFlagEnabled: streamlinedMarketplaceEnabled(state),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ describe('components/PluginManagement', () => {
|
||||
},
|
||||
},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
@@ -234,6 +235,7 @@ describe('components/PluginManagement', () => {
|
||||
pluginStatuses: {},
|
||||
plugins: {},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
@@ -324,6 +326,7 @@ describe('components/PluginManagement', () => {
|
||||
},
|
||||
},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
@@ -381,6 +384,7 @@ describe('components/PluginManagement', () => {
|
||||
},
|
||||
},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
@@ -438,6 +442,7 @@ describe('components/PluginManagement', () => {
|
||||
},
|
||||
},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
@@ -497,6 +502,7 @@ describe('components/PluginManagement', () => {
|
||||
},
|
||||
},
|
||||
appsFeatureFlagEnabled: false,
|
||||
streamlinedMarketplaceFlagEnabled: false,
|
||||
actions: {
|
||||
uploadPlugin: jest.fn(),
|
||||
installPluginFromUrl: jest.fn(),
|
||||
|
||||
@@ -205,16 +205,17 @@ const PluginItem = ({
|
||||
className={deactivating || isDisabled ? 'disabled' : ''}
|
||||
onClick={handleDisable}
|
||||
>
|
||||
{deactivating ?
|
||||
{deactivating ? (
|
||||
<FormattedMessage
|
||||
id='admin.plugin.disabling'
|
||||
defaultMessage='Disabling...'
|
||||
/> :
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='admin.plugin.disable'
|
||||
defaultMessage='Disable'
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
@@ -224,16 +225,17 @@ const PluginItem = ({
|
||||
className={activating || isDisabled ? 'disabled' : ''}
|
||||
onClick={handleEnable}
|
||||
>
|
||||
{activating ?
|
||||
{activating ? (
|
||||
<FormattedMessage
|
||||
id='admin.plugin.enabling'
|
||||
defaultMessage='Enabling...'
|
||||
/> :
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='admin.plugin.enable'
|
||||
defaultMessage='Enable'
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -415,6 +417,7 @@ type Props = BaseProps & {
|
||||
pluginStatuses: Record<string, PluginStatus>;
|
||||
plugins: any;
|
||||
appsFeatureFlagEnabled: boolean;
|
||||
streamlinedMarketplaceFlagEnabled: boolean;
|
||||
actions: {
|
||||
uploadPlugin: (fileData: File, force: boolean) => any;
|
||||
removePlugin: (pluginId: string) => any;
|
||||
@@ -1214,39 +1217,43 @@ export default class PluginManagement extends AdminSettings<Props, State> {
|
||||
onChange={this.handleChange}
|
||||
setByEnv={this.isSetByEnv('PluginSettings.EnableMarketplace')}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id='enableRemoteMarketplace'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.plugins.settings.enableRemoteMarketplace'
|
||||
defaultMessage='Enable Remote Marketplace:'
|
||||
{!this.props.streamlinedMarketplaceFlagEnabled && (
|
||||
<>
|
||||
<BooleanSetting
|
||||
id='enableRemoteMarketplace'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.plugins.settings.enableRemoteMarketplace'
|
||||
defaultMessage='Enable Remote Marketplace:'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMarkdownMessage
|
||||
id='admin.plugins.settings.enableRemoteMarketplaceDesc'
|
||||
defaultMessage='When true, marketplace fetches latest plugins from the configured Marketplace URL.'
|
||||
/>
|
||||
}
|
||||
value={this.state.enableRemoteMarketplace}
|
||||
disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace}
|
||||
onChange={this.handleChange}
|
||||
setByEnv={this.isSetByEnv('PluginSettings.EnableRemoteMarketplace')}
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMarkdownMessage
|
||||
id='admin.plugins.settings.enableRemoteMarketplaceDesc'
|
||||
defaultMessage='When true, marketplace fetches latest plugins from the configured Marketplace URL.'
|
||||
<TextSetting
|
||||
id={'marketplaceUrl'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.plugins.settings.marketplaceUrl'
|
||||
defaultMessage='Marketplace URL:'
|
||||
/>
|
||||
}
|
||||
helpText={this.getMarketplaceURLHelpText(this.state.marketplaceUrl, this.state.enableUploads)}
|
||||
value={this.state.marketplaceUrl}
|
||||
disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace || !this.state.enableRemoteMarketplace}
|
||||
onChange={this.handleChange}
|
||||
setByEnv={this.isSetByEnv('PluginSettings.MarketplaceURL')}
|
||||
/>
|
||||
}
|
||||
value={this.state.enableRemoteMarketplace}
|
||||
disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace}
|
||||
onChange={this.handleChange}
|
||||
setByEnv={this.isSetByEnv('PluginSettings.EnableRemoteMarketplace')}
|
||||
/>
|
||||
<TextSetting
|
||||
id={'marketplaceUrl'}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.plugins.settings.marketplaceUrl'
|
||||
defaultMessage='Marketplace URL:'
|
||||
/>
|
||||
}
|
||||
helpText={this.getMarketplaceURLHelpText(this.state.marketplaceUrl, this.state.enableUploads)}
|
||||
value={this.state.marketplaceUrl}
|
||||
disabled={this.props.isDisabled || !this.state.enable || !this.state.enableUploads || !this.state.enableMarketplace || !this.state.enableRemoteMarketplace}
|
||||
onChange={this.handleChange}
|
||||
setByEnv={this.isSetByEnv('PluginSettings.MarketplaceURL')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{pluginsContainer}
|
||||
|
||||
@@ -1,5 +1,225 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/marketplace/ doesn't show web marketplace banner in FeatureFlags.StreamlinedMarketplace for Cloud 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<div
|
||||
className="GenericModal__header"
|
||||
>
|
||||
<h1
|
||||
id="genericModalLabel"
|
||||
>
|
||||
App Marketplace
|
||||
</h1>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className="divider"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="genericModalError"
|
||||
>
|
||||
<i
|
||||
className="icon icon-alert-outline"
|
||||
/>
|
||||
<span>
|
||||
Error connecting to the marketplace server. Please check your settings in the
|
||||
<Link
|
||||
key=".1"
|
||||
to="/admin_console/plugins/plugin_management"
|
||||
>
|
||||
System Console
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
>
|
||||
<MarketplaceList
|
||||
filter=""
|
||||
listRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
listing={Array []}
|
||||
noResultsMessage="No plugins found"
|
||||
page={0}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</div>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/ hides search, shows web marketplace banner in FeatureFlags.StreamlinedMarketplace 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-label="App Marketplace"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal GenericModal GenericModal__compassDesign marketplace-modal streamlined-marketplace with-web-marketplace-link"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="marketplace-modal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<div
|
||||
className="GenericModal__wrapper-enter-key-press-catcher"
|
||||
onKeyDown={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
>
|
||||
<div
|
||||
className="GenericModal__header"
|
||||
>
|
||||
<h1
|
||||
id="genericModalLabel"
|
||||
>
|
||||
App Marketplace
|
||||
</h1>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className="divider"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="genericModalError"
|
||||
>
|
||||
<i
|
||||
className="icon icon-alert-outline"
|
||||
/>
|
||||
<span>
|
||||
Error connecting to the marketplace server. Please check your settings in the
|
||||
<Link
|
||||
key=".1"
|
||||
to="/admin_console/plugins/plugin_management"
|
||||
>
|
||||
System Console
|
||||
</Link>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
>
|
||||
<MarketplaceList
|
||||
filter=""
|
||||
listRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
listing={
|
||||
Array [
|
||||
Object {
|
||||
"author_type": "mattermost",
|
||||
"download_url": "https://github.com/mattermost/mattermost-plugin-nps/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz",
|
||||
"enterprise": false,
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-nps",
|
||||
"installed_version": "",
|
||||
"manifest": Object {
|
||||
"description": "This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost",
|
||||
"id": "com.mattermost.nps",
|
||||
"min_server_version": "5.14.0",
|
||||
"name": "User Satisfaction Surveys",
|
||||
"version": "1.0.3",
|
||||
},
|
||||
"release_stage": "production",
|
||||
},
|
||||
Object {
|
||||
"author_type": "mattermost",
|
||||
"download_url": "https://github.com/mattermost/mattermost-test/releases/download/v1.0.3/com.mattermost.nps-1.0.3.tar.gz",
|
||||
"enterprise": false,
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-test",
|
||||
"installed_version": "1.0.3",
|
||||
"manifest": Object {
|
||||
"description": "This plugin is to test",
|
||||
"id": "com.mattermost.test",
|
||||
"min_server_version": "5.14.0",
|
||||
"name": "Test",
|
||||
"version": "1.0.3",
|
||||
},
|
||||
"release_stage": "production",
|
||||
},
|
||||
]
|
||||
}
|
||||
noResultsMessage="No plugins found"
|
||||
page={0}
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<WebMarketplaceBanner />
|
||||
</div>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/ should render default 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
@@ -73,6 +293,7 @@ exports[`components/marketplace/ should render default 1`] = `
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
@@ -209,6 +430,7 @@ exports[`components/marketplace/ should render with error banner 1`] = `
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
@@ -362,6 +584,7 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
@@ -506,6 +729,7 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
@@ -668,6 +892,7 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,20 @@
|
||||
@import 'utils/mixins';
|
||||
|
||||
.marketplace-modal {
|
||||
width: 800px;
|
||||
width: 832px;
|
||||
|
||||
&.streamlined-marketplace {
|
||||
.modal-header {
|
||||
padding: 26px 32px 26px !important;
|
||||
}
|
||||
|
||||
&.with-web-marketplace-link {
|
||||
.modal-content {
|
||||
border-bottom-left-radius: 16px !important;
|
||||
border-bottom-right-radius: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.navigation-row {
|
||||
overflow: unset;
|
||||
@@ -211,6 +224,10 @@
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {GlobalState} from 'types/store';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
||||
import WebMarketplaceBanner from './web_marketplace_banner';
|
||||
|
||||
let mockState: GlobalState;
|
||||
|
||||
@@ -76,6 +77,12 @@ describe('components/marketplace/', () => {
|
||||
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(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
const content = wrapper.shallow();
|
||||
|
||||
expect(content.exists(WebMarketplaceBanner)).toBe(false);
|
||||
|
||||
expect(content).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(() => (
|
||||
<Input
|
||||
id='searchMarketplaceTextbox'
|
||||
name='searchMarketplaceTextbox'
|
||||
containerClassName='marketplace-modal-search'
|
||||
inputClassName='search_input'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
inputPrefix={<MagnifyIcon size={24}/>}
|
||||
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(() => (
|
||||
<FooterPagination
|
||||
page={page}
|
||||
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onNextPage={handleOnNextPage}
|
||||
onPreviousPage={handleOnPreviousPage}
|
||||
/>
|
||||
), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]);
|
||||
return (
|
||||
<Input
|
||||
id='searchMarketplaceTextbox'
|
||||
name='searchMarketplaceTextbox'
|
||||
containerClassName='marketplace-modal-search'
|
||||
inputClassName='search_input'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
inputPrefix={<MagnifyIcon size={24}/>}
|
||||
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 (
|
||||
<FooterPagination
|
||||
page={page}
|
||||
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onNextPage={handleOnNextPage}
|
||||
onPreviousPage={handleOnPreviousPage}
|
||||
/>
|
||||
);
|
||||
}, [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey, isStreamlinedMarketplaceEnabled]);
|
||||
|
||||
const getAppendedContent = useCallback(() => {
|
||||
if (!isStreamlinedMarketplaceEnabled || isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WebMarketplaceBanner/>;
|
||||
}, [isStreamlinedMarketplaceEnabled, isCloud]);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='marketplace-modal'
|
||||
className='marketplace-modal'
|
||||
className={classNames('marketplace-modal', {
|
||||
'streamlined-marketplace': isStreamlinedMarketplaceEnabled,
|
||||
'with-web-marketplace-link': isStreamlinedMarketplaceEnabled && !isCloud,
|
||||
})}
|
||||
modalHeaderText={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
errorText={serverError ? (
|
||||
@@ -209,58 +239,72 @@ const MarketplaceModal = ({
|
||||
show={show}
|
||||
compassDesign={true}
|
||||
bodyPadding={false}
|
||||
bodyDivider={isStreamlinedMarketplaceEnabled}
|
||||
footerDivider={true}
|
||||
onExited={handleOnClose}
|
||||
footerContent={getFooterContent()}
|
||||
appendedContent={getAppendedContent()}
|
||||
headerInput={getHeaderInput()}
|
||||
>
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={tabKey}
|
||||
onSelect={handleChangeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={formatMessage({id: 'marketplace_modal.tabs.all_listing', defaultMessage: 'All'})}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingScreen className='loading'/>
|
||||
) : (
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={listing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={formatMessage(
|
||||
{id: 'marketplace_modal.tabs.installed_listing', defaultMessage: 'Installed ({count})'},
|
||||
{count: installedListing.length},
|
||||
)}
|
||||
>
|
||||
{isStreamlinedMarketplaceEnabled ? (
|
||||
<>
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={installedListing}
|
||||
listing={listing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({
|
||||
id: 'marketplace_modal.no_plugins_installed',
|
||||
defaultMessage: 'No plugins installed found',
|
||||
})}
|
||||
noResultsAction={{
|
||||
label: formatMessage({id: 'marketplace_modal.install_plugins', defaultMessage: 'Install plugins'}),
|
||||
onClick: handleNoResultsButtonClick,
|
||||
}}
|
||||
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</>
|
||||
) : (
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={tabKey}
|
||||
onSelect={handleChangeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={formatMessage({id: 'marketplace_modal.tabs.all_listing', defaultMessage: 'All'})}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingScreen className='loading'/>
|
||||
) : (
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={listing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={formatMessage(
|
||||
{id: 'marketplace_modal.tabs.installed_listing', defaultMessage: 'Installed ({count})'},
|
||||
{count: installedListing.length},
|
||||
)}
|
||||
>
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={installedListing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({
|
||||
id: 'marketplace_modal.no_plugins_installed',
|
||||
defaultMessage: 'No plugins installed found',
|
||||
})}
|
||||
noResultsAction={{
|
||||
label: formatMessage({id: 'marketplace_modal.install_plugins', defaultMessage: 'Install plugins'}),
|
||||
onClick: handleNoResultsButtonClick,
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)}
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<WebMarketplaceBannerRoot className='WebMarketplaceBanner'>
|
||||
<ExternalBannerLink
|
||||
href={WEB_MARKETPLACE_LINK}
|
||||
location='marketplace_modal'
|
||||
>
|
||||
<Title>
|
||||
{formatMessage({id: 'marketplace_modal.web_marketplace_link.title', defaultMessage: 'Explore Community Integrations'})}
|
||||
<ArrowRightIcon size={24}/>
|
||||
</Title>
|
||||
<Description>
|
||||
{formatMessage({id: 'marketplace_modal.web_marketplace_link.desc', defaultMessage: 'We have dozens of community integrations available. So definitely do check them out!'})}
|
||||
</Description>
|
||||
<IconsContainer>
|
||||
<PluginIcon src={pluginIconConfluence}/>
|
||||
<PluginIcon src={pluginIconGiphy}/>
|
||||
<PluginIcon src={pluginIconPagerDuty}/>
|
||||
</IconsContainer>
|
||||
</ExternalBannerLink>
|
||||
</WebMarketplaceBannerRoot>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -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. <openModalLink>Learn more</openModalLink>",
|
||||
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
|
||||
"menu.cloudFree.postTrial.tryEnterprise": "Interested in a limitless plan with high-security features? <openModalLink>See plans</openModalLink>",
|
||||
|
||||
1
webapp/channels/src/images/icons/confluence.svg
Normal file
1
webapp/channels/src/images/icons/confluence.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="48" viewBox="0 0 48 48" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset="0" stop-color="#0050d3"/><stop offset=".94" stop-color="#007ffc"/><stop offset="1" stop-color="#0082ff"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="37.5113" x2="18.7215" xlink:href="#a" y1="40.2376" y2="29.4358"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="10.466" x2="29.2558" xlink:href="#a" y1="7.74603" y2="18.5431"/><rect fill="#fff" height="48" rx="24" width="48"/><path d="m10.0648 31.5952c-.31054.5064-.65929 1.094-.93161 1.5622-.12744.2154-.16531.4722-.10546.7152.05986.2429.21266.4528.42555.5843l6.21072 3.822c.1079.0666.228.1111.3533.1308s.2533.0143.3764-.0159c.1232-.0303.2392-.0847.3411-.1602s.1877-.1706.2525-.2796c.2437-.4156.5638-.9555.9125-1.5336 2.4604-4.0608 4.9399-3.564 9.3973-1.4332l6.1582 2.9286c.1154.0549.2407.0861.3684.0918.1277.0056.2553-.0144.3751-.059.1199-.0445.2296-.1127.3226-.2004s.1675-.1932.219-.3102l2.9572-6.6884c.1005-.2296.1067-.4895.0173-.7236-.0894-.2342-.2671-.4238-.495-.5281-1.2995-.6115-3.8841-1.8346-6.2107-2.9525-8.3892-4.08-15.5029-3.8124-20.9444 5.0498z" fill="url(#b)"/><path d="m37.9125 16.4028c.3105-.5064.6593-1.0988.9555-1.5622.127-.216.1642-.4732.1034-.7162-.0608-.2431-.2146-.4525-.4283-.5833l-6.2107-3.82198c-.1077-.06588-.2274-.10974-.3522-.12907-.1248-.01932-.2522-.01372-.3748.01649s-.238.08442-.3396.15951c-.1015.07509-.1871.16955-.2519.27795-.2485.4156-.5686.9555-.9173 1.5336-2.4604 4.0608-4.9351 3.5639-9.3973 1.4332l-6.1772-2.9143c-.1152-.0541-.2401-.0848-.3673-.09-.1272-.0053-.2542.015-.3735.0595-.1192.0445-.2284.1124-.3211.1997-.0927.0872-.167.1922-.2185.3086l-2.9573 6.6884c-.1004.2296-.1066.4895-.0172.7236.0894.2342.2671.4238.495.5281 1.2995.6116 3.8888 1.8298 6.2107 2.9525 8.3844 4.0465 15.4981 3.779 20.9396-5.0641z" fill="url(#c)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
webapp/channels/src/images/icons/giphy.svg
Normal file
1
webapp/channels/src/images/icons/giphy.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 32 KiB |
1
webapp/channels/src/images/icons/pager-duty.svg
Normal file
1
webapp/channels/src/images/icons/pager-duty.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
BIN
webapp/channels/src/images/marketplace-notice-background.jpg
Normal file
BIN
webapp/channels/src/images/marketplace-notice-background.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Props, State> {
|
||||
</>
|
||||
)}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Modal.Body className={classNames({divider: this.props.bodyDivider})}>
|
||||
{this.props.compassDesign ? (
|
||||
this.props.errorText && (
|
||||
<div className='genericModalError'>
|
||||
@@ -226,6 +228,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
)}
|
||||
</Modal.Footer>
|
||||
)}
|
||||
{Boolean(this.props.appendedContent) && this.props.appendedContent}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user