diff --git a/webapp/channels/src/components/admin_console/custom_plugin_settings/index.test.tsx b/webapp/channels/src/components/admin_console/custom_plugin_settings/index.test.tsx new file mode 100644 index 0000000000..2638680af0 --- /dev/null +++ b/webapp/channels/src/components/admin_console/custom_plugin_settings/index.test.tsx @@ -0,0 +1,268 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import type {match} from 'react-router-dom'; + +import type {CloudState} from '@mattermost/types/cloud'; +import type {PluginSettings} from '@mattermost/types/config'; + +import CustomPluginSettings from 'components/admin_console/custom_plugin_settings'; + +import {screen, renderWithContext} from 'tests/react_testing_utils'; + +describe('custom plugin sections and settings', () => { + const plugin = { + id: 'testplugin', + name: 'testplugin', + description: '', + version: '', + active: true, + webapp: { + bundle_path: '/static/testplugin_bundle.js', + }, + settings_schema: { + header: 'This is the header', + footer: 'This is the footer', + settings: [], + sections: [], + }, + }; + + const baseProps = { + isDisabled: false, + environmentConfig: {}, + setNavigationBlocked: jest.fn(), + roles: {}, + cloud: {} as CloudState, + license: {}, + editRole: jest.fn(), + isCurrentUserSystemAdmin: false, + enterpriseReady: false, + match: {params: {plugin_id: 'testplugin'}} as match<{ plugin_id: string }>, + config: { + PluginSettings: { + Plugins: { + testplugin: { + }, + }, + } as unknown as PluginSettings, + }, + consoleAccess: { + read: { + about: true, + reporting: true, + environment: true, + site_configuration: true, + authentication: true, + plugins: true, + integrations: true, + compliance: true, + }, + write: { + about: true, + reporting: true, + environment: true, + site_configuration: true, + authentication: true, + plugins: true, + integrations: true, + compliance: true, + }, + }, + }; + + const baseState = { + entities: { + admin: { + plugins: { + testplugin: plugin, + }, + }, + }, + }; + + it('empty sections and settings', () => { + renderWithContext( + + , {...baseState}); + + expect(screen.getByText('testplugin')).toBeInTheDocument(); + expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument(); + expect(screen.getByText('This is the header')).toBeInTheDocument(); + expect(screen.getByText('This is the footer')).toBeInTheDocument(); + }); + + it('all custom sections with plugin disabled should show single warning', () => { + const state = { + ...baseState, + entities: { + admin: { + plugins: { + testplugin: { + ...plugin, + settings_schema: { + ...plugin.settings_schema, + sections: [ + { + key: 'section1', + title: 'Custom Section 1', + settings: [ + { + key: 'customsection1numbersetting', + label: 'Custom Section Number Setting', + type: 'number' as const, + help_text: 'Custom Section Number Setting Help Text', + }, + ], + custom: true, + }, + { + key: 'section2', + title: 'Custom Section 2', + settings: [ + { + key: 'customsection2numbersetting', + label: 'Custom Section Number Setting', + type: 'number' as const, + help_text: 'Custom Section Number Setting Help Text', + }, + ], + custom: true, + }, + ], + }, + }, + }, + }, + }, + }; + + const props = { + ...baseProps, + config: { + ...baseProps.config, + PluginStates: { + testplugin: { + Enabled: false, + }, + }, + }, + }; + + renderWithContext( + + , {...state}); + + expect(screen.getByText('testplugin')).toBeInTheDocument(); + expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument(); + expect(screen.getByText('In order to view and configure plugin settings, enable the plugin and click Save.')).toBeInTheDocument(); + expect(screen.queryByText('Custom Section 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Custom Section 2')).not.toBeInTheDocument(); + }); + + it('custom sections with plugin enabled should render as expected', () => { + const CustomSection1 = () => { + return ( +
{'Custom Component Section 1'}
+ ); + }; + + const CustomSection2 = () => { + return ( +
{'Custom Component Section 2'}
+ ); + }; + + const state = { + ...baseState, + entities: { + admin: { + plugins: { + testplugin: { + ...plugin, + settings_schema: { + ...plugin.settings_schema, + sections: [ + { + key: 'section1', + title: 'Custom Section 1', + settings: [ + { + key: 'customsection1numbersetting', + label: 'Custom Section Number Setting', + type: 'number' as const, + help_text: 'Custom Section Number Setting Help Text', + }, + ], + custom: true, + }, + { + key: 'section2', + title: 'Custom Section 2', + settings: [ + { + key: 'customsection2numbersetting', + label: 'Custom Section Number Setting', + type: 'number' as const, + help_text: 'Custom Section Number Setting Help Text', + }, + ], + custom: true, + }, + ], + }, + }, + }, + }, + }, + plugins: { + adminConsoleCustomSections: { + testplugin: { + section1: { + pluginId: 'testplugin', + key: 'section1', + component: CustomSection1 as unknown as React.Component, + }, + section2: { + pluginId: 'testplugin', + key: 'section2', + component: CustomSection2 as unknown as React.Component, + }, + }, + }, + }, + }; + + const props = { + ...baseProps, + config: { + ...baseProps.config, + PluginStates: { + testplugin: { + Enabled: true, + }, + }, + }, + }; + + renderWithContext( + + , {...state}); + + expect(screen.getByText('testplugin')).toBeInTheDocument(); + expect(screen.getByTestId('PluginSettings.PluginStates.testplugin.Enable')).toBeInTheDocument(); + expect(screen.queryByText('In order to view and configure plugin settings, enable the plugin and click Save.')).not.toBeInTheDocument(); + expect(screen.getByText('Custom Component Section 1')).toBeInTheDocument(); + expect(screen.getByText('Custom Component Section 2')).toBeInTheDocument(); + }); +}); diff --git a/webapp/channels/src/components/admin_console/custom_plugin_settings/index.ts b/webapp/channels/src/components/admin_console/custom_plugin_settings/index.ts index 23f1575fa6..8913779c80 100644 --- a/webapp/channels/src/components/admin_console/custom_plugin_settings/index.ts +++ b/webapp/channels/src/components/admin_console/custom_plugin_settings/index.ts @@ -94,9 +94,11 @@ function makeGetPluginSchema() { if (section.custom) { if (customSections[key]) { component = customSections[key]?.component; + settings = parsePluginSettings(section.settings); } else { // Show warning banner for custom sections when the plugin is disabled. settings = [{ + key: key + 'disabledWarning', type: Constants.SettingsTypes.TYPE_BANNER, label: defineMessage({ id: 'admin.plugin.customSection.pluginDisabledWarning', @@ -105,9 +107,7 @@ function makeGetPluginSchema() { banner_type: 'warning', }]; } - } - - if (settings.length === 0) { + } else { settings = parsePluginSettings(section.settings); } @@ -132,16 +132,33 @@ function makeGetPluginSchema() { } if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) { - const pluginEnableSetting = getEnablePluginSetting(plugin); - if (pluginEnableSetting.isDisabled) { - pluginEnableSetting.isDisabled = it.any(pluginEnableSetting.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins'))); - } else { - pluginEnableSetting.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins')); - } + const pluginEnableSetting = getEnablePluginSetting(plugin) as AdminDefinitionSetting; - if (sections.length > 0) { - sections[0].settings.unshift(pluginEnableSetting as AdminDefinitionSetting); + if (plugin.settings_schema && plugin.settings_schema.sections?.every((s) => s.custom && !customSections[s.key.toLowerCase()])) { + // If the plugin is composed of purely custom sections (e.g. Calls) and it's disabled (custom components are not found), we show a single warning. + const warningBanner = { + key: 'admin.plugin.customSections.pluginDisabledWarning', + type: Constants.SettingsTypes.TYPE_BANNER, + label: defineMessage({id: 'admin.plugin.customSections.pluginDisabledWarning', defaultMessage: 'In order to view and configure plugin settings, enable the plugin and click Save.'}), + banner_type: 'warning' as const, + }; + + sections = [{ + key: pluginEnabledConfigKey + '.Section', + header: plugin.settings_schema?.header, + footer: plugin.settings_schema?.footer, + settings: [pluginEnableSetting, warningBanner], + }]; + } else if (sections.length > 0) { + // Have a separate section on top with the plugin enable/disable setting. + sections.unshift({ + key: pluginEnabledConfigKey + '.Section', + header: plugin.settings_schema?.header, + footer: plugin.settings_schema?.footer, + settings: [pluginEnableSetting], + }); } else { + // Otherwise we retain existing behaviour and add the setting in front. settings.unshift(pluginEnableSetting); } } diff --git a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx index 34649c6378..fb362556c6 100644 --- a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx +++ b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx @@ -1108,6 +1108,23 @@ export class SchemaAdminSettings extends React.PureComponent { ); } + // This is a bit of special case since designs for plugin config expect the Enable/Disable setting + // to be on top and out of the sections. + if (section.key.startsWith('PluginSettings.PluginStates') && section.key.endsWith('Enable.Section')) { + sections.push( + + {header} + {settingsList} + {footer} + , + ); + + return; + } + sections.push(