diff --git a/server/public/model/manifest.go b/server/public/model/manifest.go
index 98c4e9abf5..4d337f1d48 100644
--- a/server/public/model/manifest.go
+++ b/server/public/model/manifest.go
@@ -91,6 +91,29 @@ type PluginSetting struct {
Hosting string `json:"hosting"`
}
+type PluginSettingsSection struct {
+ // A unique identifier for this section.
+ Key string `json:"key" yaml:"key"`
+
+ // Optional text to display as section title.
+ Title string `json:"title" yaml:"title"`
+
+ // Optional text to display as section subtitle.
+ Subtitle string `json:"subtitle" yaml:"subtitle"`
+
+ // A list of setting definitions to display inside the section.
+ Settings []*PluginSetting `json:"settings" yaml:"settings"`
+
+ // Optional text to display above the settings. Supports Markdown formatting.
+ Header string `json:"header" yaml:"header"`
+
+ // Optional text to display below the settings. Supports Markdown formatting.
+ Footer string `json:"footer" yaml:"footer"`
+
+ // If true, the section will load the custom component registered using `registry.registerAdminConsoleCustomSection`
+ Custom bool `json:"custom" yaml:"custom"`
+}
+
type PluginSettingsSchema struct {
// Optional text to display above the settings. Supports Markdown formatting.
Header string `json:"header" yaml:"header"`
@@ -100,6 +123,9 @@ type PluginSettingsSchema struct {
// A list of setting definitions.
Settings []*PluginSetting `json:"settings" yaml:"settings"`
+
+ // A list of settings section definitions.
+ Sections []*PluginSettingsSection `json:"sections" yaml:"sections"`
}
// The plugin manifest defines the metadata required to load and present your plugin. The manifest
@@ -330,6 +356,27 @@ func (s *PluginSettingsSchema) isValid() error {
}
}
+ for _, section := range s.Sections {
+ if err := section.IsValid(); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (s *PluginSettingsSection) IsValid() error {
+ if s.Key == "" {
+ return errors.New("invalid empty Key")
+ }
+
+ for _, setting := range s.Settings {
+ err := setting.isValid()
+ if err != nil {
+ return err
+ }
+ }
+
return nil
}
diff --git a/server/public/model/manifest_test.go b/server/public/model/manifest_test.go
index 8f42ab5f4b..4702dea6d3 100644
--- a/server/public/model/manifest_test.go
+++ b/server/public/model/manifest_test.go
@@ -65,6 +65,37 @@ func TestIsValid(t *testing.T) {
Default: "thedefault",
},
},
+ Sections: []*PluginSettingsSection{
+ {
+ Key: "section1",
+ Title: "section title",
+ Subtitle: "section subtitle",
+ Settings: []*PluginSetting{
+ {
+ Key: "section1setting1",
+ DisplayName: "thedisplayname",
+ Type: "custom",
+ },
+ {
+ Key: "section1setting2",
+ DisplayName: "thedisplayname",
+ Type: "custom",
+ },
+ },
+ Header: "section header",
+ Footer: "section footer",
+ },
+ {
+ Key: "section2",
+ Settings: []*PluginSetting{
+ {
+ Key: "section2setting1",
+ DisplayName: "thedisplayname",
+ Type: "custom",
+ },
+ },
+ },
+ },
},
}, false},
}
@@ -103,6 +134,62 @@ func TestIsValidSettingsSchema(t *testing.T) {
}
}
+func TestPluginSettingsSectionIsValid(t *testing.T) {
+ for name, test := range map[string]struct {
+ Section PluginSettingsSection
+ ExpectedError string
+ }{
+ "missing key": {
+ Section: PluginSettingsSection{
+ Settings: []*PluginSetting{
+ {
+ Type: "custom",
+ Placeholder: "some Text",
+ },
+ },
+ },
+ ExpectedError: "invalid empty Key",
+ },
+ "invalid setting": {
+ Section: PluginSettingsSection{
+ Key: "sectionKey",
+ Settings: []*PluginSetting{
+ {
+ Type: "invalid",
+ },
+ },
+ },
+ ExpectedError: "invalid setting type: invalid",
+ },
+ "valid empty": {
+ Section: PluginSettingsSection{
+ Key: "sectionKey",
+ Settings: []*PluginSetting{},
+ },
+ },
+ "valid": {
+ Section: PluginSettingsSection{
+ Key: "sectionKey",
+ Settings: []*PluginSetting{
+ {
+ Type: "custom",
+ Placeholder: "some Text",
+ },
+ },
+ },
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ err := test.Section.IsValid()
+ if test.ExpectedError != "" {
+ assert.EqualError(t, err, test.ExpectedError)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
func TestSettingIsValid(t *testing.T) {
for name, test := range map[string]struct {
Setting PluginSetting
diff --git a/webapp/channels/src/actions/admin_actions.jsx b/webapp/channels/src/actions/admin_actions.jsx
index 50b08d0812..92f8f67f23 100644
--- a/webapp/channels/src/actions/admin_actions.jsx
+++ b/webapp/channels/src/actions/admin_actions.jsx
@@ -464,6 +464,19 @@ export function registerAdminConsoleCustomSetting(pluginId, key, component, {sho
};
}
+export function registerAdminConsoleCustomSection(pluginId, key, component) {
+ return (storeDispatch) => {
+ storeDispatch({
+ type: ActionTypes.RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION,
+ data: {
+ pluginId,
+ key,
+ component,
+ },
+ });
+ };
+}
+
export async function getSamlMetadataFromIdp(success, error, samlMetadataURL) {
const {data, error: err} = await dispatch(AdminActions.getSamlMetadataFromIdp(samlMetadataURL));
if (data && success) {
diff --git a/webapp/channels/src/actions/admin_actions.test.js b/webapp/channels/src/actions/admin_actions.test.js
index 22571c895e..8a60078179 100644
--- a/webapp/channels/src/actions/admin_actions.test.js
+++ b/webapp/channels/src/actions/admin_actions.test.js
@@ -54,4 +54,17 @@ describe('Actions.Admin', () => {
},
}}});
});
+
+ test('Register a custom plugin section adds the component to the state', async () => {
+ expect(store.getState().plugins.adminConsoleCustomSections).toEqual({});
+
+ store.dispatch(Actions.registerAdminConsoleCustomSection('plugin-id', 'sectionA', React.Component));
+ expect(store.getState().plugins.adminConsoleCustomSections).toEqual(
+ {'plugin-id': {
+ sectiona: {
+ key: 'sectionA',
+ pluginId: 'plugin-id',
+ component: React.Component,
+ }}});
+ });
});
diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx
index 51b7c0444f..bd7773b9f2 100644
--- a/webapp/channels/src/components/admin_console/admin_definition.tsx
+++ b/webapp/channels/src/components/admin_console/admin_definition.tsx
@@ -3220,6 +3220,7 @@ const AdminDefinition: AdminDefinitionType = {
name: defineMessage({id: 'admin.authentication.ldap', defaultMessage: 'AD/LDAP'}),
sections: [
{
+ key: 'admin.authentication.ldap.connection',
title: 'Connection',
subtitle: 'Connection and security level to your AD/LDAP server.',
settings: [
@@ -3385,6 +3386,7 @@ const AdminDefinition: AdminDefinitionType = {
],
},
{
+ key: 'admin.authentication.ldap.dn_and_filters',
title: 'Base DN & Filters',
settings: [
{
@@ -3476,6 +3478,7 @@ const AdminDefinition: AdminDefinitionType = {
],
},
{
+ key: 'admin.authentication.ldap.account_synchronization',
title: 'Account Synchronization',
settings: [
{
@@ -3631,6 +3634,7 @@ const AdminDefinition: AdminDefinitionType = {
],
},
{
+ key: 'admin.authentication.ldap.group_synchronization',
title: 'Group Synchronization',
settings: [
{
@@ -3661,6 +3665,7 @@ const AdminDefinition: AdminDefinitionType = {
],
},
{
+ key: 'admin.authentication.ldap.synchronization_performance',
title: 'Synchronization Performance',
settings: [
{
@@ -3734,6 +3739,7 @@ const AdminDefinition: AdminDefinitionType = {
],
},
{
+ key: 'admin.authentication.ldap.synchronization_history',
title: 'Synchronization History',
subtitle: 'See the table below for the status of each synchronization',
settings: [
diff --git a/webapp/channels/src/components/admin_console/custom_plugin_settings/custom_plugin_settings.test.tsx b/webapp/channels/src/components/admin_console/custom_plugin_settings/custom_plugin_settings.test.tsx
index 384c862f44..84f41b60b0 100644
--- a/webapp/channels/src/components/admin_console/custom_plugin_settings/custom_plugin_settings.test.tsx
+++ b/webapp/channels/src/components/admin_console/custom_plugin_settings/custom_plugin_settings.test.tsx
@@ -11,6 +11,7 @@ import CustomPluginSettings from 'components/admin_console/custom_plugin_setting
import {escapePathPart} from 'components/admin_console/schema_admin_settings';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
+import {screen, renderWithContext} from 'tests/react_testing_utils';
import type {AdminDefinitionSetting} from '../types';
@@ -136,7 +137,7 @@ describe('components/admin_console/CustomPluginSettings', () => {
,
);
@@ -170,10 +171,192 @@ describe('components/admin_console/CustomPluginSettings', () => {
Plugins: {},
} as PluginSettings,
}}
- schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings}}
+ schema={{...plugin.settings_schema, id: plugin.id, name: plugin.name, settings, sections: undefined}}
patchConfig={jest.fn()}
/>,
);
expect(wrapper).toMatchSnapshot();
});
});
+
+function CustomSection(props: {settingsList: React.ReactNode[]}) {
+ return (
{'Custom Section'} {props.settingsList}
);
+}
+
+function CustomSetting() {
+ return ({'Custom Setting'}
);
+}
+
+describe('custom plugin sections', () => {
+ let config: {PluginSettings: PluginSettings};
+
+ const baseProps = {
+ isDisabled: false,
+ environmentConfig: {},
+ setNavigationBlocked: jest.fn(),
+ roles: {},
+ cloud: {} as CloudState,
+ license: {},
+ editRole: jest.fn(),
+ consoleAccess: {read: {}, write: {}},
+ isCurrentUserSystemAdmin: false,
+ enterpriseReady: false,
+ };
+
+ it('empty sections', () => {
+ const schema = {
+ id: 'testplugin',
+ name: 'testplugin',
+ description: '',
+ version: '',
+ active: true,
+ webapp: {
+ bundle_path: '/static/testplugin_bundle.js',
+ },
+ sections: [],
+ };
+
+ renderWithContext(
+ ,
+ );
+ expect(screen.getByText('testplugin')).toBeInTheDocument();
+ });
+
+ it('render sections', () => {
+ const schema = {
+ id: 'testplugin',
+ name: 'testplugin',
+ description: '',
+ version: '',
+ active: true,
+ webapp: {
+ bundle_path: '/static/testplugin_bundle.js',
+ },
+ sections: [
+ {
+ key: 'section1',
+ title: 'Section 1',
+ settings: [],
+ header: 'Section1 Header',
+ footer: 'Section1 Footer',
+ },
+ {
+ key: 'section2',
+ title: 'Section 2',
+ settings: [
+ {
+ key: 'section2setting1',
+ label: 'Section 2 Setting 1',
+ type: 'text' as const,
+ help_text: 'Section 2 Setting 1 Help Text',
+ },
+ ],
+ header: 'Section2 Header',
+ footer: 'Section2 Footer',
+ },
+ {
+ key: 'section3',
+ settings: [],
+ },
+ ],
+ };
+
+ renderWithContext(
+ ,
+ );
+
+ expect(screen.getByText('testplugin')).toBeInTheDocument();
+
+ expect(screen.getByText('Section 1')).toBeInTheDocument();
+ expect(screen.getByText('Section1 Header')).toBeInTheDocument();
+ expect(screen.getByText('Section1 Footer')).toBeInTheDocument();
+
+ expect(screen.getByText('Section 2')).toBeInTheDocument();
+ expect(screen.getByText('Section2 Header')).toBeInTheDocument();
+ expect(screen.getByText('Section2 Footer')).toBeInTheDocument();
+ expect(screen.getByText('Section 2 Setting 1')).toBeInTheDocument();
+ expect(screen.getByText('Section 2 Setting 1 Help Text')).toBeInTheDocument();
+
+ expect(screen.queryByText('Section 3')).not.toBeInTheDocument();
+ });
+
+ it('custom sections and settings', () => {
+ const schema = {
+ id: 'testplugin',
+ name: 'testplugin',
+ description: '',
+ version: '',
+ active: true,
+ webapp: {
+ bundle_path: '/static/testplugin_bundle.js',
+ },
+ sections: [
+ {
+ key: 'section1',
+ title: 'Custom Section 1',
+ settings: [
+ {
+ key: 'customsectionnumbersetting',
+ label: 'Custom Section Number Setting',
+ type: 'number' as const,
+ help_text: 'Custom Section Number Setting Help Text',
+ },
+ {
+ key: 'customsectioncustomsetting',
+ type: 'custom' as const,
+ component: CustomSetting,
+ },
+ ],
+ custom: true,
+ component: CustomSection,
+ },
+ {
+ key: 'section2',
+ title: 'Section 2',
+ settings: [
+ {
+ key: 'section2setting1',
+ label: 'Section 2 Setting 1',
+ type: 'text' as const,
+ help_text: 'Section 2 Setting 1 Help Text',
+ },
+ ],
+ header: 'Section2 Header',
+ footer: 'Section2 Footer',
+ },
+ ],
+ };
+
+ renderWithContext(
+ ,
+ );
+
+ expect(screen.getByText('testplugin')).toBeInTheDocument();
+
+ expect(screen.getByText('Custom Section')).toBeInTheDocument();
+ expect(screen.getByText('Custom Section Number Setting')).toBeInTheDocument();
+ expect(screen.getByText('Custom Section Number Setting Help Text')).toBeInTheDocument();
+ expect(screen.getByText('Custom Setting')).toBeInTheDocument();
+
+ expect(screen.getByText('Section 2')).toBeInTheDocument();
+ expect(screen.getByText('Section2 Header')).toBeInTheDocument();
+ expect(screen.getByText('Section2 Footer')).toBeInTheDocument();
+ expect(screen.getByText('Section 2 Setting 1')).toBeInTheDocument();
+ expect(screen.getByText('Section 2 Setting 1 Help Text')).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 ea3becfc82..23f1575fa6 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
@@ -5,7 +5,7 @@ import type {MessageDescriptor} from 'react-intl';
import {defineMessage} from 'react-intl';
import {connect} from 'react-redux';
-import type {PluginRedux} from '@mattermost/types/plugins';
+import type {PluginRedux, PluginSetting, PluginSettingSection} from '@mattermost/types/plugins';
import type {GlobalState} from '@mattermost/types/store';
import {createSelector} from 'mattermost-redux/selectors/create_selector';
@@ -13,19 +13,19 @@ import {appsFeatureFlagEnabled} from 'mattermost-redux/selectors/entities/apps';
import {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
import {getRoles} from 'mattermost-redux/selectors/entities/roles';
-import {getAdminConsoleCustomComponents} from 'selectors/admin_console';
+import {getAdminConsoleCustomComponents, getAdminConsoleCustomSections} from 'selectors/admin_console';
import {appsPluginID} from 'utils/apps';
import {Constants} from 'utils/constants';
-import type {AdminConsolePluginComponent} from 'types/store/plugins';
+import type {AdminConsolePluginComponent, AdminConsolePluginCustomSection} from 'types/store/plugins';
import CustomPluginSettings from './custom_plugin_settings';
import getEnablePluginSetting from './enable_plugin_setting';
import {it} from '../admin_definition';
import {escapePathPart} from '../schema_admin_settings';
-import type {AdminDefinitionSetting, AdminDefinitionSubSectionSchema} from '../types';
+import type {AdminDefinitionSetting, AdminDefinitionSubSectionSchema, AdminDefinitionConfigSchemaSection} from '../types';
type OwnProps = { match: { params: { plugin_id: string } } }
@@ -34,9 +34,10 @@ function makeGetPluginSchema() {
'makeGetPluginSchema',
(state: GlobalState, pluginId: string) => state.entities.admin.plugins?.[pluginId],
(state: GlobalState, pluginId: string) => getAdminConsoleCustomComponents(state, pluginId),
+ (state: GlobalState, pluginId: string) => getAdminConsoleCustomSections(state, pluginId),
(state) => appsFeatureFlagEnabled(state),
isCurrentLicenseCloud,
- (plugin: PluginRedux | undefined, customComponents: Record, appsFeatureFlagIsEnabled, isCloudLicense) => {
+ (plugin: PluginRedux | undefined, customComponents: Record, customSections: Record, appsFeatureFlagIsEnabled, isCloudLicense) => {
if (!plugin) {
return null;
}
@@ -44,9 +45,8 @@ function makeGetPluginSchema() {
const escapedPluginId = escapePathPart(plugin.id);
const pluginEnabledConfigKey = 'PluginSettings.PluginStates.' + escapedPluginId + '.Enable';
- let settings: Array> = [];
- if (plugin.settings_schema && plugin.settings_schema.settings) {
- settings = plugin.settings_schema.settings.map((setting) => {
+ const parsePluginSettings = (settings: PluginSetting[]) => {
+ return settings.map((setting) => {
const key = setting.key.toLowerCase();
let component = null;
let bannerType = '';
@@ -82,8 +82,53 @@ function makeGetPluginSchema() {
banner_type: bannerType,
component,
showTitle: customComponents[key] ? customComponents[key].options.showTitle : false,
- };
- }) as Array>;
+ } as Partial;
+ });
+ };
+
+ const parsePluginSettingSections = (sections: PluginSettingSection[]) => {
+ return sections.map((section) => {
+ const key = section.key.toLowerCase();
+ let component;
+ let settings: Array> = [];
+ if (section.custom) {
+ if (customSections[key]) {
+ component = customSections[key]?.component;
+ } else {
+ // Show warning banner for custom sections when the plugin is disabled.
+ settings = [{
+ type: Constants.SettingsTypes.TYPE_BANNER,
+ label: defineMessage({
+ id: 'admin.plugin.customSection.pluginDisabledWarning',
+ defaultMessage: 'In order to view this section, enable the plugin and click Save.',
+ }),
+ banner_type: 'warning',
+ }];
+ }
+ }
+
+ if (settings.length === 0) {
+ settings = parsePluginSettings(section.settings);
+ }
+
+ return {
+ key,
+ title: section.title,
+ subtitle: section.subtitle,
+ settings,
+ header: section.header,
+ footer: section.footer,
+ component,
+ } as AdminDefinitionConfigSchemaSection;
+ });
+ };
+
+ let sections: AdminDefinitionConfigSchemaSection[] = [];
+ let settings: Array> = [];
+ if (plugin.settings_schema && plugin.settings_schema.sections) {
+ sections = parsePluginSettingSections(plugin.settings_schema.sections);
+ } else if (plugin.settings_schema && plugin.settings_schema.settings) {
+ settings = parsePluginSettings(plugin.settings_schema.settings);
}
if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) {
@@ -93,22 +138,34 @@ function makeGetPluginSchema() {
} else {
pluginEnableSetting.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
}
- settings.unshift(pluginEnableSetting);
+
+ if (sections.length > 0) {
+ sections[0].settings.unshift(pluginEnableSetting as AdminDefinitionSetting);
+ } else {
+ settings.unshift(pluginEnableSetting);
+ }
}
- settings.forEach((s) => {
+ const checkDisableSetting = (s: Partial) => {
if (s.isDisabled) {
s.isDisabled = it.any(s.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins')));
} else {
s.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
}
- });
+ };
+
+ if (sections.length > 0) {
+ sections.forEach((section) => section.settings.forEach(checkDisableSetting));
+ } else {
+ settings.forEach(checkDisableSetting);
+ }
return {
...plugin.settings_schema,
id: plugin.id,
name: plugin.name,
- settings,
+ settings: sections.length > 0 ? undefined : settings,
+ sections: sections.length > 0 ? sections : undefined,
translate: Boolean(plugin.translate),
} as AdminDefinitionSubSectionSchema;
},
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 9c24b20b3e..34649c6378 100644
--- a/webapp/channels/src/components/admin_console/schema_admin_settings.tsx
+++ b/webapp/channels/src/components/admin_console/schema_admin_settings.tsx
@@ -1073,6 +1073,17 @@ export class SchemaAdminSettings extends React.PureComponent {
});
}
+ if (section.component) {
+ const CustomComponent = section.component;
+ sections.push((
+
+ ));
+ return;
+ }
+
let header;
if (section.header) {
header = (
@@ -1098,7 +1109,10 @@ export class SchemaAdminSettings extends React.PureComponent {
}
sections.push(
-
+
{
+ store.dispatch(registerAdminConsoleCustomSection(this.id, key, component));
+ });
// Register a Right-Hand Sidebar component by providing a title for the right hand component.
// Accepts the following:
diff --git a/webapp/channels/src/reducers/plugins/index.test.ts b/webapp/channels/src/reducers/plugins/index.test.ts
index bc991d0246..b4d1dc4793 100644
--- a/webapp/channels/src/reducers/plugins/index.test.ts
+++ b/webapp/channels/src/reducers/plugins/index.test.ts
@@ -13,6 +13,7 @@ import pluginReducers from '.';
function getBaseState(): PluginsState {
return {
adminConsoleCustomComponents: {},
+ adminConsoleCustomSections: {},
adminConsoleReducers: {},
components: {} as any,
plugins: {},
diff --git a/webapp/channels/src/reducers/plugins/index.ts b/webapp/channels/src/reducers/plugins/index.ts
index 3b4ed77955..e212813433 100644
--- a/webapp/channels/src/reducers/plugins/index.ts
+++ b/webapp/channels/src/reducers/plugins/index.ts
@@ -13,7 +13,13 @@ import {UserTypes} from 'mattermost-redux/action_types';
import {ActionTypes} from 'utils/constants';
import {extractPluginConfiguration} from 'utils/plugins/plugin_setting_extraction';
-import type {PluginsState, PluginComponent, AdminConsolePluginComponent, Menu} from 'types/store/plugins';
+import type {
+ PluginsState,
+ PluginComponent,
+ AdminConsolePluginComponent,
+ AdminConsolePluginCustomSection,
+ Menu,
+} from 'types/store/plugins';
function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
if (!menu.subMenu) {
@@ -361,6 +367,44 @@ function adminConsoleCustomComponents(state: {[pluginId: string]: Record} = {}, action: AnyAction) {
+ switch (action.type) {
+ case ActionTypes.RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION: {
+ if (!action.data) {
+ return state;
+ }
+
+ const pluginId = action.data.pluginId;
+ const key = action.data.key.toLowerCase();
+
+ const nextState = {...state};
+ let nextObject: Record = {};
+ if (nextState[pluginId]) {
+ nextObject = {...nextState[pluginId]};
+ }
+ nextObject[key] = action.data;
+ nextState[pluginId] = nextObject;
+
+ return nextState;
+ }
+ case ActionTypes.REMOVED_WEBAPP_PLUGIN: {
+ if (!action.data || !state[action.data.id]) {
+ return state;
+ }
+
+ const pluginId = action.data.id;
+ const nextState = {...state};
+ delete nextState[pluginId];
+ return nextState;
+ }
+
+ case UserTypes.LOGOUT_SUCCESS:
+ return {};
+ default:
+ return state;
+ }
+}
+
function siteStatsHandlers(state: PluginsState['siteStatsHandlers'] = {}, action: AnyAction) {
switch (action.type) {
case ActionTypes.RECEIVED_PLUGIN_STATS_HANDLER:
@@ -441,6 +485,10 @@ export default combineReducers({
// React component to render on the plugin's system console.
adminConsoleCustomComponents,
+ // objects where every key is a plugin id and the value is an object mapping keys to a custom
+ // React component to render on the plugin's system console as custom section.
+ adminConsoleCustomSections,
+
// objects where every key is a plugin id and the value is a promise to fetch stats from
// a plugin to render on system console
siteStatsHandlers,
diff --git a/webapp/channels/src/selectors/admin_console.jsx b/webapp/channels/src/selectors/admin_console.jsx
index 5b33383419..2e997f4084 100644
--- a/webapp/channels/src/selectors/admin_console.jsx
+++ b/webapp/channels/src/selectors/admin_console.jsx
@@ -29,6 +29,9 @@ export const getAdminDefinition = createSelector(
export const getAdminConsoleCustomComponents = (state, pluginId) =>
state.plugins.adminConsoleCustomComponents[pluginId] || {};
+export const getAdminConsoleCustomSections = (state, pluginId) =>
+ state.plugins.adminConsoleCustomSections[pluginId] || {};
+
export const getConsoleAccess = createSelector(
'getConsoleAccess',
getAdminDefinition,
diff --git a/webapp/channels/src/types/store/plugins.ts b/webapp/channels/src/types/store/plugins.ts
index d0a87fbbd3..030b6a6f55 100644
--- a/webapp/channels/src/types/store/plugins.ts
+++ b/webapp/channels/src/types/store/plugins.ts
@@ -49,6 +49,7 @@ export type PluginsState = {
postTypes: {
[postType: string]: PostPluginComponent;
};
+
postCardTypes: {
[postType: string]: PostPluginComponent;
};
@@ -56,11 +57,19 @@ export type PluginsState = {
adminConsoleReducers: {
[pluginId: string]: any;
};
+
adminConsoleCustomComponents: {
[pluginId: string]: {
[settingName: string]: AdminConsolePluginComponent;
};
};
+
+ adminConsoleCustomSections: {
+ [pluginId: string]: {
+ [sectionKey: string]: AdminConsolePluginCustomSection;
+ };
+ };
+
siteStatsHandlers: {
[pluginId: string]: PluginSiteStatsHandler;
};
@@ -149,6 +158,12 @@ export type AdminConsolePluginComponent = {
};
};
+export type AdminConsolePluginCustomSection = {
+ pluginId: string;
+ key: string;
+ component: React.Component;
+};
+
export type PostWillRenderEmbedPluginComponent = {
id: string;
pluginId: string;
diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx
index 82da550ead..f8c1ea29bb 100644
--- a/webapp/channels/src/utils/constants.tsx
+++ b/webapp/channels/src/utils/constants.tsx
@@ -235,6 +235,7 @@ export const ActionTypes = keyMirror({
RECEIVED_ADMIN_CONSOLE_REDUCER: null,
REMOVED_ADMIN_CONSOLE_REDUCER: null,
RECEIVED_ADMIN_CONSOLE_CUSTOM_COMPONENT: null,
+ RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION: null,
RECEIVED_PLUGIN_STATS_HANDLER: null,
RECEIVED_PLUGIN_USER_SETTINGS: null,
diff --git a/webapp/platform/types/src/plugins.ts b/webapp/platform/types/src/plugins.ts
index 8bc524cbca..7fb70b1df5 100644
--- a/webapp/platform/types/src/plugins.ts
+++ b/webapp/platform/types/src/plugins.ts
@@ -44,6 +44,17 @@ export type PluginSettingsSchema = {
header: string;
footer: string;
settings: PluginSetting[];
+ sections?: PluginSettingSection[];
+};
+
+export type PluginSettingSection = {
+ key: string;
+ title?: string;
+ subtitle?: string;
+ settings: PluginSetting[];
+ header?: string;
+ footer?: string;
+ custom?: boolean;
};
export type PluginSetting = {