From be94c47607b156c31cb5906670479a95ab806f0f Mon Sep 17 00:00:00 2001 From: Claudio Costa Date: Wed, 17 Jul 2024 18:24:33 +0200 Subject: [PATCH] [MM-57418] Implement support for defining plugin settings sections (#27654) * Implement support for defining plugin settings sections * Implement custom plugin configuration sections * Tests * Update test * Improvements --- server/public/model/manifest.go | 47 +++++ server/public/model/manifest_test.go | 87 ++++++++ webapp/channels/src/actions/admin_actions.jsx | 13 ++ .../src/actions/admin_actions.test.js | 13 ++ .../admin_console/admin_definition.tsx | 6 + .../custom_plugin_settings.test.tsx | 187 +++++++++++++++++- .../custom_plugin_settings/index.ts | 85 ++++++-- .../admin_console/schema_admin_settings.tsx | 16 +- .../src/components/admin_console/types.ts | 6 +- webapp/channels/src/i18n/en.json | 1 + webapp/channels/src/plugins/registry.ts | 28 ++- .../src/reducers/plugins/index.test.ts | 1 + webapp/channels/src/reducers/plugins/index.ts | 50 ++++- .../channels/src/selectors/admin_console.jsx | 3 + webapp/channels/src/types/store/plugins.ts | 15 ++ webapp/channels/src/utils/constants.tsx | 1 + webapp/platform/types/src/plugins.ts | 11 ++ 17 files changed, 545 insertions(+), 25 deletions(-) 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 = {