[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
This commit is contained in:
Claudio Costa
2024-07-17 18:24:33 +02:00
committed by GitHub
parent c0a7a19294
commit be94c47607
17 changed files with 545 additions and 25 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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,
}}});
});
});

View File

@@ -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: [

View File

@@ -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', () => {
<CustomPluginSettings
{...baseProps}
config={config}
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()}
/>,
);
@@ -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 (<div>{'Custom Section'} {props.settingsList}</div>);
}
function CustomSetting() {
return (<div>{'Custom Setting'}</div>);
}
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(
<CustomPluginSettings
{...baseProps}
config={config}
schema={schema}
patchConfig={jest.fn()}
/>,
);
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(
<CustomPluginSettings
{...baseProps}
config={config}
schema={schema}
patchConfig={jest.fn()}
/>,
);
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(
<CustomPluginSettings
{...baseProps}
config={config}
schema={schema}
patchConfig={jest.fn()}
/>,
);
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();
});
});

View File

@@ -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<string, AdminConsolePluginComponent>, appsFeatureFlagIsEnabled, isCloudLicense) => {
(plugin: PluginRedux | undefined, customComponents: Record<string, AdminConsolePluginComponent>, customSections: Record<string, AdminConsolePluginCustomSection>, 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<Partial<AdminDefinitionSetting>> = [];
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<Partial<AdminDefinitionSetting>>;
} as Partial<AdminDefinitionSetting>;
});
};
const parsePluginSettingSections = (sections: PluginSettingSection[]) => {
return sections.map((section) => {
const key = section.key.toLowerCase();
let component;
let settings: Array<Partial<AdminDefinitionSetting>> = [];
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<Partial<AdminDefinitionSetting>> = [];
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<AdminDefinitionSetting>) => {
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;
},

View File

@@ -1073,6 +1073,17 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
});
}
if (section.component) {
const CustomComponent = section.component;
sections.push((
<CustomComponent
settingsList={settingsList}
key={section.key}
/>
));
return;
}
let header;
if (section.header) {
header = (
@@ -1098,7 +1109,10 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
}
sections.push(
<div className={'config-section'}>
<div
className={'config-section'}
key={section.key}
>
<SettingsGroup
show={true}
title={section.title}

View File

@@ -167,12 +167,14 @@ type AdminDefinitionConfigSchemaSettings = {
header?: string | MessageDescriptor;
}
type AdminDefinitionConfigSchemaSection = {
title: string;
export type AdminDefinitionConfigSchemaSection = {
key: string;
title?: string;
subtitle?: string;
settings: AdminDefinitionSetting[];
header?: string | MessageDescriptor;
footer?: string | MessageDescriptor;
component?: Component;
}
type RestrictedIndicatorType = {

View File

@@ -1922,6 +1922,7 @@
"admin.plugin.backToPlugins": "Go back to the Plugins",
"admin.plugin.choose": "Choose File",
"admin.plugin.cluster_instance": "Cluster Instance",
"admin.plugin.customSection.pluginDisabledWarning": "In order to view this section, enable the plugin and click Save.",
"admin.plugin.customSetting.pluginDisabledWarning": "In order to view this setting, enable the plugin and click Save.",
"admin.plugin.disable": "Disable",
"admin.plugin.disabling": "Disabling...",

View File

@@ -15,6 +15,7 @@ import {
registerAdminConsolePlugin,
unregisterAdminConsolePlugin,
registerAdminConsoleCustomSetting,
registerAdminConsoleCustomSection,
} from 'actions/admin_actions';
import {showRHSPlugin, hideRHSPlugin, toggleRHSPlugin} from 'actions/views/rhs';
import {
@@ -864,6 +865,12 @@ export default class PluginRegistry {
store.dispatch(registerAdminConsolePlugin(this.id, func));
});
// Unregister a previously registered admin console definition override function.
// Returns undefined.
unregisterAdminConsolePlugin() {
store.dispatch(unregisterAdminConsolePlugin(this.id));
}
// Register a custom React component to manage the plugin configuration for the given setting key.
// Accepts the following:
// - key - A key specified in the settings_schema.settings block of the plugin's manifest.
@@ -888,11 +895,22 @@ export default class PluginRegistry {
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
});
// Unregister a previously registered admin console definition override function.
// Returns undefined.
unregisterAdminConsolePlugin() {
store.dispatch(unregisterAdminConsolePlugin(this.id));
}
// Register a custom React component to render as a section in the plugin configuration page.
// Accepts the following:
// - key - A key specified in the settings_schema.sections block of the plugin's manifest.
// - component - A react component to render in place of the default handling.
registerAdminConsoleCustomSection = reArg([
'key',
'component',
], ({
key,
component,
}: {
key: string;
component: PluginComponent['component'];
}) => {
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:

View File

@@ -13,6 +13,7 @@ import pluginReducers from '.';
function getBaseState(): PluginsState {
return {
adminConsoleCustomComponents: {},
adminConsoleCustomSections: {},
adminConsoleReducers: {},
components: {} as any,
plugins: {},

View File

@@ -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<string,
}
}
function adminConsoleCustomSections(state: {[pluginId: string]: Record<string, AdminConsolePluginCustomSection>} = {}, 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<string, AdminConsolePluginCustomSection> = {};
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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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 = {