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(