MM-52981, MM-53559: Streamlined in-product marketplace (#24311)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d57581d01e
Коммит
818225dffe
4
.github/workflows/mmctl-test-template.yml
поставляемый
4
.github/workflows/mmctl-test-template.yml
поставляемый
@@ -34,6 +34,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd server
|
cd server
|
||||||
make setup-go-work
|
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
|
- name: Run docker compose
|
||||||
run: |
|
run: |
|
||||||
cd server/build
|
cd server/build
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.shouldNotRunOnCloudEdition();
|
cy.shouldNotRunOnCloudEdition();
|
||||||
|
cy.shouldHaveFeatureFlag('StreamlinedMarketplace', 'false'); // https://mattermost.atlassian.net/browse/MM-54230
|
||||||
cy.shouldHavePluginUploadEnabled();
|
cy.shouldHavePluginUploadEnabled();
|
||||||
|
|
||||||
cy.apiInitSetup().then(({team}) => {
|
cy.apiInitSetup().then(({team}) => {
|
||||||
@@ -72,6 +73,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
cy.get('#marketplaceTabs-pane-allListing').should('be.visible');
|
cy.get('#marketplaceTabs-pane-allListing').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://mattermost.atlassian.net/browse/MM-54230
|
||||||
it('MM-T2001 autofocus on search plugin input box', () => {
|
it('MM-T2001 autofocus on search plugin input box', () => {
|
||||||
cy.uiClose();
|
cy.uiClose();
|
||||||
|
|
||||||
@@ -82,6 +84,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
cy.findByPlaceholderText('Search Marketplace').should('be.focused');
|
cy.findByPlaceholderText('Search Marketplace').should('be.focused');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://mattermost.atlassian.net/browse/MM-54230
|
||||||
it('render the list of all plugins by default', () => {
|
it('render the list of all plugins by default', () => {
|
||||||
// * Verify all plugins tab should be active
|
// * Verify all plugins tab should be active
|
||||||
cy.get('#marketplaceTabs-tab-allListing').should('be.visible').parent().should('have.class', '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');
|
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
|
// this test uses exist, not visible, due to issues with Cypress
|
||||||
it('render the list of installed plugins on demand', () => {
|
it('render the list of installed plugins on demand', () => {
|
||||||
// # Click on installed plugins tab
|
// # Click on installed plugins tab
|
||||||
@@ -117,6 +121,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
cy.get('#modal_marketplace').should('not.exist');
|
cy.get('#modal_marketplace').should('not.exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://mattermost.atlassian.net/browse/MM-54230
|
||||||
it('should filter all on search', () => {
|
it('should filter all on search', () => {
|
||||||
// # Load all plugins before searching
|
// # Load all plugins before searching
|
||||||
cy.get('.more-modal__row').should('have.length', 15);
|
cy.get('.more-modal__row').should('have.length', 15);
|
||||||
@@ -136,6 +141,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
should('have.length', 1);
|
should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// https://mattermost.atlassian.net/browse/MM-54230
|
||||||
it('should show an error bar on failing to filter', () => {
|
it('should show an error bar on failing to filter', () => {
|
||||||
// # Enable Plugin Marketplace
|
// # Enable Plugin Marketplace
|
||||||
cy.apiUpdateConfig({
|
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');
|
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', () => {
|
it('should install a plugin from search results on demand', () => {
|
||||||
// # Uninstall any existing webex plugin
|
// # Uninstall any existing webex plugin
|
||||||
cy.apiRemovePluginById('com.mattermost.webex');
|
cy.apiRemovePluginById('com.mattermost.webex');
|
||||||
@@ -226,6 +233,7 @@ describe('Plugin Marketplace', () => {
|
|||||||
cy.get('#marketplace-plugin-github').should('be.visible');
|
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', () => {
|
it('MM-T1986 change tab to "All Plugins" when "Install Plugins" link is clicked', () => {
|
||||||
cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => {
|
cy.get('#marketplaceTabs').scrollIntoView().should('be.visible').within(() => {
|
||||||
// # Switch tab to installed plugin
|
// # Switch tab to installed plugin
|
||||||
|
|||||||
@@ -679,6 +679,7 @@ const defaultServerConfig: AdminConfig = {
|
|||||||
OnboardingTourTips: true,
|
OnboardingTourTips: true,
|
||||||
DeprecateCloudFree: false,
|
DeprecateCloudFree: false,
|
||||||
CloudReverseTrial: false,
|
CloudReverseTrial: false,
|
||||||
|
StreamlinedMarketplace: true
|
||||||
},
|
},
|
||||||
ImportSettings: {
|
ImportSettings: {
|
||||||
Directory: './import',
|
Directory: './import',
|
||||||
|
|||||||
@@ -157,7 +157,8 @@ MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl')
|
|||||||
TEMPLATES_DIR=templates
|
TEMPLATES_DIR=templates
|
||||||
|
|
||||||
# Plugins Packages
|
# 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-autolink-v1.4.0
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
PLUGIN_PACKAGES += mattermost-plugin-aws-SNS-v1.2.0
|
||||||
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.18.0
|
PLUGIN_PACKAGES += mattermost-plugin-calls-v0.18.0
|
||||||
@@ -300,7 +301,7 @@ plugin-checker:
|
|||||||
$(GO) run $(GOFLAGS) ./public/plugin/checker
|
$(GO) run $(GOFLAGS) ./public/plugin/checker
|
||||||
|
|
||||||
prepackaged-plugins: ## Populate the prepackaged-plugins directory
|
prepackaged-plugins: ## Populate the prepackaged-plugins directory
|
||||||
@echo Downloading prepackaged plugins
|
@echo Downloading prepackaged plugins: $(PLUGIN_PACKAGES)
|
||||||
mkdir -p prepackaged_plugins
|
mkdir -p prepackaged_plugins
|
||||||
@cd prepackaged_plugins && for plugin_package in $(PLUGIN_PACKAGES) ; do \
|
@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; \
|
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) {
|
func TestGetMarketplacePlugins(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -682,6 +685,9 @@ func TestGetMarketplacePlugins(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
samplePlugins := []*model.MarketplacePlugin{
|
samplePlugins := []*model.MarketplacePlugin{
|
||||||
{
|
{
|
||||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
@@ -825,6 +831,9 @@ func TestGetInstalledMarketplacePlugins(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchGetMarketplacePlugins(t *testing.T) {
|
func TestSearchGetMarketplacePlugins(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
samplePlugins := []*model.MarketplacePlugin{
|
samplePlugins := []*model.MarketplacePlugin{
|
||||||
{
|
{
|
||||||
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
BaseMarketplacePlugin: &model.BaseMarketplacePlugin{
|
||||||
@@ -950,6 +959,9 @@ func TestSearchGetMarketplacePlugins(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLocalPluginInMarketplace(t *testing.T) {
|
func TestGetLocalPluginInMarketplace(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -1111,6 +1123,9 @@ func TestGetLocalPluginInMarketplace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRemotePluginInMarketplace(t *testing.T) {
|
func TestGetRemotePluginInMarketplace(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -1166,7 +1181,70 @@ func TestGetRemotePluginInMarketplace(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
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) {
|
func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -1212,6 +1290,30 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
|||||||
env := th.App.GetPluginsEnvironment()
|
env := th.App.GetPluginsEnvironment()
|
||||||
env.SetPrepackagedPlugins([]*plugin.PrepackagedPlugin{prepackagePlugin}, nil)
|
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) {
|
t.Run("get remote and prepackaged plugins", func(t *testing.T) {
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
*cfg.PluginSettings.EnableRemoteMarketplace = true
|
||||||
@@ -1271,24 +1373,12 @@ func TestGetPrepackagedPluginInMarketplace(t *testing.T) {
|
|||||||
require.Len(t, plugins, 1)
|
require.Len(t, plugins, 1)
|
||||||
require.Equal(t, newerPrepackagePlugin.Manifest, plugins[0].Manifest)
|
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) {
|
func TestInstallMarketplacePlugin(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
@@ -1639,6 +1729,9 @@ func TestInstallMarketplacePlugin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) {
|
func TestInstallMarketplacePluginPrepackagedDisabled(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
|
|
||||||
path, _ := fileutils.FindDir("tests")
|
path, _ := fileutils.FindDir("tests")
|
||||||
|
|
||||||
signatureFilename := "testplugin2.tar.gz.sig"
|
signatureFilename := "testplugin2.tar.gz.sig"
|
||||||
|
|||||||
@@ -828,6 +828,8 @@ func TestPushNotificationAck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteOnboarding(t *testing.T) {
|
func TestCompleteOnboarding(t *testing.T) {
|
||||||
|
os.Setenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE", "false")
|
||||||
|
defer os.Unsetenv("MM_FEATUREFLAGS_STREAMLINEDMARKETPLACE")
|
||||||
th := Setup(t)
|
th := Setup(t)
|
||||||
defer th.TearDown()
|
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) {
|
func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*model.MarketplacePlugin, *model.AppError) {
|
||||||
plugins := map[string]*model.MarketplacePlugin{}
|
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()
|
p, appErr := a.getRemotePlugins()
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return nil, appErr
|
return nil, appErr
|
||||||
@@ -561,20 +561,12 @@ func (a *App) GetMarketplacePlugins(filter *model.MarketplacePluginFilter) ([]*m
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !filter.RemoteOnly {
|
if !filter.RemoteOnly {
|
||||||
// Some plugin don't work on cloud. The remote Marketplace is aware of this fact,
|
appErr := a.mergePrepackagedPlugins(plugins)
|
||||||
// but prepackaged plugins are not. Hence, on a cloud installation prepackaged plugins
|
if appErr != nil {
|
||||||
// shouldn't be shown in the Marketplace modal.
|
return nil, appErr
|
||||||
// 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.mergeLocalPlugins(plugins)
|
appErr = a.mergeLocalPlugins(plugins)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return nil, appErr
|
return nil, appErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ func (ch *Channels) InstallMarketplacePlugin(request *model.InstallMarketplacePl
|
|||||||
signatureFile = bytes.NewReader(prepackagedPlugin.Signature)
|
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
|
var plugin *model.BaseMarketplacePlugin
|
||||||
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
|
plugin, appErr = ch.getRemoteMarketplacePlugin(request.Id, request.Version)
|
||||||
// The plugin might only be prepackaged and not on the Marketplace.
|
// The plugin might only be prepackaged and not on the Marketplace.
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ type FeatureFlags struct {
|
|||||||
EnableExportDirectDownload bool
|
EnableExportDirectDownload bool
|
||||||
|
|
||||||
DataRetentionConcurrencyEnabled bool
|
DataRetentionConcurrencyEnabled bool
|
||||||
|
|
||||||
|
StreamlinedMarketplace bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FeatureFlags) SetDefaults() {
|
func (f *FeatureFlags) SetDefaults() {
|
||||||
@@ -65,6 +67,7 @@ func (f *FeatureFlags) SetDefaults() {
|
|||||||
f.CloudReverseTrial = false
|
f.CloudReverseTrial = false
|
||||||
f.EnableExportDirectDownload = false
|
f.EnableExportDirectDownload = false
|
||||||
f.DataRetentionConcurrencyEnabled = true
|
f.DataRetentionConcurrencyEnabled = true
|
||||||
|
f.StreamlinedMarketplace = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToMap returns the feature flags as a map[string]string
|
// 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 {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps';
|
||||||
|
|
||||||
import PluginManagement from './plugin_management';
|
import PluginManagement from './plugin_management';
|
||||||
|
import {streamlinedMarketplaceEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
function mapStateToProps(state: any) {
|
function mapStateToProps(state: any) {
|
||||||
return {
|
return {
|
||||||
plugins: state.entities.admin.plugins,
|
plugins: state.entities.admin.plugins,
|
||||||
pluginStatuses: state.entities.admin.pluginStatuses,
|
pluginStatuses: state.entities.admin.pluginStatuses,
|
||||||
appsFeatureFlagEnabled: appsFeatureFlagEnabled(state),
|
appsFeatureFlagEnabled: appsFeatureFlagEnabled(state),
|
||||||
|
streamlinedMarketplaceFlagEnabled: streamlinedMarketplaceEnabled(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ describe('components/PluginManagement', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
@@ -234,6 +235,7 @@ describe('components/PluginManagement', () => {
|
|||||||
pluginStatuses: {},
|
pluginStatuses: {},
|
||||||
plugins: {},
|
plugins: {},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
@@ -324,6 +326,7 @@ describe('components/PluginManagement', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
@@ -381,6 +384,7 @@ describe('components/PluginManagement', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
@@ -438,6 +442,7 @@ describe('components/PluginManagement', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
@@ -497,6 +502,7 @@ describe('components/PluginManagement', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
appsFeatureFlagEnabled: false,
|
appsFeatureFlagEnabled: false,
|
||||||
|
streamlinedMarketplaceFlagEnabled: false,
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: jest.fn(),
|
uploadPlugin: jest.fn(),
|
||||||
installPluginFromUrl: jest.fn(),
|
installPluginFromUrl: jest.fn(),
|
||||||
|
|||||||
@@ -205,16 +205,17 @@ const PluginItem = ({
|
|||||||
className={deactivating || isDisabled ? 'disabled' : ''}
|
className={deactivating || isDisabled ? 'disabled' : ''}
|
||||||
onClick={handleDisable}
|
onClick={handleDisable}
|
||||||
>
|
>
|
||||||
{deactivating ?
|
{deactivating ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.plugin.disabling'
|
id='admin.plugin.disabling'
|
||||||
defaultMessage='Disabling...'
|
defaultMessage='Disabling...'
|
||||||
/> :
|
/>
|
||||||
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.plugin.disable'
|
id='admin.plugin.disable'
|
||||||
defaultMessage='Disable'
|
defaultMessage='Disable'
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -224,16 +225,17 @@ const PluginItem = ({
|
|||||||
className={activating || isDisabled ? 'disabled' : ''}
|
className={activating || isDisabled ? 'disabled' : ''}
|
||||||
onClick={handleEnable}
|
onClick={handleEnable}
|
||||||
>
|
>
|
||||||
{activating ?
|
{activating ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.plugin.enabling'
|
id='admin.plugin.enabling'
|
||||||
defaultMessage='Enabling...'
|
defaultMessage='Enabling...'
|
||||||
/> :
|
/>
|
||||||
|
) : (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='admin.plugin.enable'
|
id='admin.plugin.enable'
|
||||||
defaultMessage='Enable'
|
defaultMessage='Enable'
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -415,6 +417,7 @@ type Props = BaseProps & {
|
|||||||
pluginStatuses: Record<string, PluginStatus>;
|
pluginStatuses: Record<string, PluginStatus>;
|
||||||
plugins: any;
|
plugins: any;
|
||||||
appsFeatureFlagEnabled: boolean;
|
appsFeatureFlagEnabled: boolean;
|
||||||
|
streamlinedMarketplaceFlagEnabled: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
uploadPlugin: (fileData: File, force: boolean) => any;
|
uploadPlugin: (fileData: File, force: boolean) => any;
|
||||||
removePlugin: (pluginId: string) => any;
|
removePlugin: (pluginId: string) => any;
|
||||||
@@ -1214,39 +1217,43 @@ export default class PluginManagement extends AdminSettings<Props, State> {
|
|||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
setByEnv={this.isSetByEnv('PluginSettings.EnableMarketplace')}
|
setByEnv={this.isSetByEnv('PluginSettings.EnableMarketplace')}
|
||||||
/>
|
/>
|
||||||
<BooleanSetting
|
{!this.props.streamlinedMarketplaceFlagEnabled && (
|
||||||
id='enableRemoteMarketplace'
|
<>
|
||||||
label={
|
<BooleanSetting
|
||||||
<FormattedMessage
|
id='enableRemoteMarketplace'
|
||||||
id='admin.plugins.settings.enableRemoteMarketplace'
|
label={
|
||||||
defaultMessage='Enable Remote Marketplace:'
|
<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')}
|
||||||
/>
|
/>
|
||||||
}
|
<TextSetting
|
||||||
helpText={
|
id={'marketplaceUrl'}
|
||||||
<FormattedMarkdownMessage
|
label={
|
||||||
id='admin.plugins.settings.enableRemoteMarketplaceDesc'
|
<FormattedMessage
|
||||||
defaultMessage='When true, marketplace fetches latest plugins from the configured Marketplace URL.'
|
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}
|
{pluginsContainer}
|
||||||
|
|||||||
@@ -1,5 +1,225 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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`] = `
|
exports[`components/marketplace/ should render default 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
animation={true}
|
animation={true}
|
||||||
@@ -73,6 +293,7 @@ exports[`components/marketplace/ should render default 1`] = `
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
bsClass="modal-body"
|
bsClass="modal-body"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -209,6 +430,7 @@ exports[`components/marketplace/ should render with error banner 1`] = `
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
bsClass="modal-body"
|
bsClass="modal-body"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -362,6 +584,7 @@ exports[`components/marketplace/ should render with no plugins available 1`] = `
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
bsClass="modal-body"
|
bsClass="modal-body"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -506,6 +729,7 @@ exports[`components/marketplace/ should render with plugins available 1`] = `
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
bsClass="modal-body"
|
bsClass="modal-body"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -668,6 +892,7 @@ exports[`components/marketplace/ should render with plugins installed 1`] = `
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody
|
<ModalBody
|
||||||
bsClass="modal-body"
|
bsClass="modal-body"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,7 +2,20 @@
|
|||||||
@import 'utils/mixins';
|
@import 'utils/mixins';
|
||||||
|
|
||||||
.marketplace-modal {
|
.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 {
|
div.navigation-row {
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
@@ -211,6 +224,10 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {GlobalState} from 'types/store';
|
|||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
||||||
|
import WebMarketplaceBanner from './web_marketplace_banner';
|
||||||
|
|
||||||
let mockState: GlobalState;
|
let mockState: GlobalState;
|
||||||
|
|
||||||
@@ -76,6 +77,12 @@ describe('components/marketplace/', () => {
|
|||||||
entities: {
|
entities: {
|
||||||
general: {
|
general: {
|
||||||
firstAdminCompleteSetup: false,
|
firstAdminCompleteSetup: false,
|
||||||
|
config: {
|
||||||
|
FeatureFlagStreamlinedMarketplace: 'false',
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
Cloud: 'false',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
pluginStatuses: {},
|
pluginStatuses: {},
|
||||||
@@ -156,4 +163,49 @@ describe('components/marketplace/', () => {
|
|||||||
|
|
||||||
expect(wrapper.shallow()).toMatchSnapshot();
|
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 {FooterPagination, GenericModal} from '@mattermost/components';
|
||||||
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||||
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
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 {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {fetchListing, filterListing} from 'actions/marketplace';
|
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||||
@@ -27,10 +28,13 @@ import {getListing, getInstalledListing} from 'selectors/views/marketplace';
|
|||||||
import {isModalOpen} from 'selectors/views/modals';
|
import {isModalOpen} from 'selectors/views/modals';
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
import WebMarketplaceBanner from './web_marketplace_banner';
|
||||||
|
import {isCloudLicense} from 'utils/license_utils';
|
||||||
|
|
||||||
import './marketplace_modal.scss';
|
import './marketplace_modal.scss';
|
||||||
|
|
||||||
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
|
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const MarketplaceTabs = {
|
const MarketplaceTabs = {
|
||||||
ALL_LISTING: 'all',
|
ALL_LISTING: 'all',
|
||||||
@@ -63,6 +67,9 @@ const MarketplaceModal = ({
|
|||||||
const installedListing = useSelector(getInstalledListing);
|
const installedListing = useSelector(getInstalledListing);
|
||||||
const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses);
|
const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses);
|
||||||
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
|
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
|
||||||
|
const isStreamlinedMarketplaceEnabled = useSelector(streamlinedMarketplaceEnabled);
|
||||||
|
const license = useSelector(getLicense);
|
||||||
|
const isCloud = isCloudLicense(license);
|
||||||
|
|
||||||
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
|
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
@@ -162,39 +169,62 @@ const MarketplaceModal = ({
|
|||||||
handleChangeTab(MarketplaceTabs.ALL_LISTING);
|
handleChangeTab(MarketplaceTabs.ALL_LISTING);
|
||||||
}, [handleChangeTab]);
|
}, [handleChangeTab]);
|
||||||
|
|
||||||
const getHeaderInput = useCallback(() => (
|
const getHeaderInput = useCallback(() => {
|
||||||
<Input
|
if (isStreamlinedMarketplaceEnabled) {
|
||||||
id='searchMarketplaceTextbox'
|
return null;
|
||||||
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(() => (
|
return (
|
||||||
<FooterPagination
|
<Input
|
||||||
page={page}
|
id='searchMarketplaceTextbox'
|
||||||
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
|
name='searchMarketplaceTextbox'
|
||||||
itemsPerPage={ITEMS_PER_PAGE}
|
containerClassName='marketplace-modal-search'
|
||||||
onNextPage={handleOnNextPage}
|
inputClassName='search_input'
|
||||||
onPreviousPage={handleOnPreviousPage}
|
type='text'
|
||||||
/>
|
inputSize={SIZE.LARGE}
|
||||||
), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]);
|
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 (
|
return (
|
||||||
<GenericModal
|
<GenericModal
|
||||||
id='marketplace-modal'
|
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'})}
|
modalHeaderText={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||||
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||||
errorText={serverError ? (
|
errorText={serverError ? (
|
||||||
@@ -209,58 +239,72 @@ const MarketplaceModal = ({
|
|||||||
show={show}
|
show={show}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
bodyPadding={false}
|
bodyPadding={false}
|
||||||
|
bodyDivider={isStreamlinedMarketplaceEnabled}
|
||||||
footerDivider={true}
|
footerDivider={true}
|
||||||
onExited={handleOnClose}
|
onExited={handleOnClose}
|
||||||
footerContent={getFooterContent()}
|
footerContent={getFooterContent()}
|
||||||
|
appendedContent={getAppendedContent()}
|
||||||
headerInput={getHeaderInput()}
|
headerInput={getHeaderInput()}
|
||||||
>
|
>
|
||||||
<Tabs
|
{isStreamlinedMarketplaceEnabled ? (
|
||||||
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
|
<MarketplaceList
|
||||||
listRef={listRef}
|
listRef={listRef}
|
||||||
listing={installedListing}
|
listing={listing}
|
||||||
page={page}
|
page={page}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
noResultsMessage={formatMessage({
|
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||||
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>
|
) : (
|
||||||
|
<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>
|
</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.all_listing": "All",
|
||||||
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
|
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
|
||||||
"marketplace_modal.title": "App Marketplace",
|
"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.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features. <openModalLink>Learn more</openModalLink>",
|
||||||
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
|
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
|
||||||
"menu.cloudFree.postTrial.tryEnterprise": "Interested in a limitless plan with high-security features? <openModalLink>See plans</openModalLink>",
|
"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
Обычный файл
1
webapp/channels/src/images/icons/confluence.svg
Обычный файл
@@ -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>
|
||||||
|
После Ширина: | Высота: | Размер: 1.9 KiB |
1
webapp/channels/src/images/icons/giphy.svg
Обычный файл
1
webapp/channels/src/images/icons/giphy.svg
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
После Ширина: | Высота: | Размер: 32 KiB |
1
webapp/channels/src/images/icons/pager-duty.svg
Обычный файл
1
webapp/channels/src/images/icons/pager-duty.svg
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
После Ширина: | Высота: | Размер: 42 KiB |
Двоичные данные
webapp/channels/src/images/marketplace-notice-background.jpg
Обычный файл
Двоичные данные
webapp/channels/src/images/marketplace-notice-background.jpg
Обычный файл
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 84 KiB |
@@ -281,3 +281,7 @@ export function deprecateCloudFree(state: GlobalState): boolean {
|
|||||||
export function cloudReverseTrial(state: GlobalState): boolean {
|
export function cloudReverseTrial(state: GlobalState): boolean {
|
||||||
return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true';
|
return getFeatureFlagValue(state, 'CloudReverseTrial') === 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function streamlinedMarketplaceEnabled(state: GlobalState): boolean {
|
||||||
|
return getFeatureFlagValue(state, 'StreamlinedMarketplace') === 'true';
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.divider {
|
||||||
|
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -106,7 +110,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&.divider {
|
&.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;
|
keyboardEscape?: boolean;
|
||||||
headerInput?: React.ReactNode;
|
headerInput?: React.ReactNode;
|
||||||
bodyPadding?: boolean;
|
bodyPadding?: boolean;
|
||||||
|
bodyDivider?: boolean;
|
||||||
footerContent?: React.ReactNode;
|
footerContent?: React.ReactNode;
|
||||||
footerDivider?: boolean;
|
footerDivider?: boolean;
|
||||||
|
appendedContent?: React.ReactNode;
|
||||||
headerButton?: React.ReactNode;
|
headerButton?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,7 +201,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body className={classNames({divider: this.props.bodyDivider})}>
|
||||||
{this.props.compassDesign ? (
|
{this.props.compassDesign ? (
|
||||||
this.props.errorText && (
|
this.props.errorText && (
|
||||||
<div className='genericModalError'>
|
<div className='genericModalError'>
|
||||||
@@ -226,6 +228,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
)}
|
)}
|
||||||
|
{Boolean(this.props.appendedContent) && this.props.appendedContent}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user