[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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c0a7a19294
Коммит
be94c47607
@@ -91,6 +91,29 @@ type PluginSetting struct {
|
|||||||
Hosting string `json:"hosting"`
|
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 {
|
type PluginSettingsSchema struct {
|
||||||
// Optional text to display above the settings. Supports Markdown formatting.
|
// Optional text to display above the settings. Supports Markdown formatting.
|
||||||
Header string `json:"header" yaml:"header"`
|
Header string `json:"header" yaml:"header"`
|
||||||
@@ -100,6 +123,9 @@ type PluginSettingsSchema struct {
|
|||||||
|
|
||||||
// A list of setting definitions.
|
// A list of setting definitions.
|
||||||
Settings []*PluginSetting `json:"settings" yaml:"settings"`
|
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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,37 @@ func TestIsValid(t *testing.T) {
|
|||||||
Default: "thedefault",
|
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},
|
}, 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) {
|
func TestSettingIsValid(t *testing.T) {
|
||||||
for name, test := range map[string]struct {
|
for name, test := range map[string]struct {
|
||||||
Setting PluginSetting
|
Setting PluginSetting
|
||||||
|
|||||||
@@ -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) {
|
export async function getSamlMetadataFromIdp(success, error, samlMetadataURL) {
|
||||||
const {data, error: err} = await dispatch(AdminActions.getSamlMetadataFromIdp(samlMetadataURL));
|
const {data, error: err} = await dispatch(AdminActions.getSamlMetadataFromIdp(samlMetadataURL));
|
||||||
if (data && success) {
|
if (data && success) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}}});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3220,6 +3220,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
name: defineMessage({id: 'admin.authentication.ldap', defaultMessage: 'AD/LDAP'}),
|
name: defineMessage({id: 'admin.authentication.ldap', defaultMessage: 'AD/LDAP'}),
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.connection',
|
||||||
title: 'Connection',
|
title: 'Connection',
|
||||||
subtitle: 'Connection and security level to your AD/LDAP server.',
|
subtitle: 'Connection and security level to your AD/LDAP server.',
|
||||||
settings: [
|
settings: [
|
||||||
@@ -3385,6 +3386,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.dn_and_filters',
|
||||||
title: 'Base DN & Filters',
|
title: 'Base DN & Filters',
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
@@ -3476,6 +3478,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.account_synchronization',
|
||||||
title: 'Account Synchronization',
|
title: 'Account Synchronization',
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
@@ -3631,6 +3634,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.group_synchronization',
|
||||||
title: 'Group Synchronization',
|
title: 'Group Synchronization',
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
@@ -3661,6 +3665,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.synchronization_performance',
|
||||||
title: 'Synchronization Performance',
|
title: 'Synchronization Performance',
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
@@ -3734,6 +3739,7 @@ const AdminDefinition: AdminDefinitionType = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'admin.authentication.ldap.synchronization_history',
|
||||||
title: 'Synchronization History',
|
title: 'Synchronization History',
|
||||||
subtitle: 'See the table below for the status of each synchronization',
|
subtitle: 'See the table below for the status of each synchronization',
|
||||||
settings: [
|
settings: [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import CustomPluginSettings from 'components/admin_console/custom_plugin_setting
|
|||||||
import {escapePathPart} from 'components/admin_console/schema_admin_settings';
|
import {escapePathPart} from 'components/admin_console/schema_admin_settings';
|
||||||
|
|
||||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||||
|
import {screen, renderWithContext} from 'tests/react_testing_utils';
|
||||||
|
|
||||||
import type {AdminDefinitionSetting} from '../types';
|
import type {AdminDefinitionSetting} from '../types';
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ describe('components/admin_console/CustomPluginSettings', () => {
|
|||||||
<CustomPluginSettings
|
<CustomPluginSettings
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
config={config}
|
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()}
|
patchConfig={jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
@@ -170,10 +171,192 @@ describe('components/admin_console/CustomPluginSettings', () => {
|
|||||||
Plugins: {},
|
Plugins: {},
|
||||||
} as PluginSettings,
|
} 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()}
|
patchConfig={jest.fn()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type {MessageDescriptor} from 'react-intl';
|
|||||||
import {defineMessage} from 'react-intl';
|
import {defineMessage} from 'react-intl';
|
||||||
import {connect} from 'react-redux';
|
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 type {GlobalState} from '@mattermost/types/store';
|
||||||
|
|
||||||
import {createSelector} from 'mattermost-redux/selectors/create_selector';
|
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 {isCurrentLicenseCloud} from 'mattermost-redux/selectors/entities/cloud';
|
||||||
import {getRoles} from 'mattermost-redux/selectors/entities/roles';
|
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 {appsPluginID} from 'utils/apps';
|
||||||
import {Constants} from 'utils/constants';
|
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 CustomPluginSettings from './custom_plugin_settings';
|
||||||
import getEnablePluginSetting from './enable_plugin_setting';
|
import getEnablePluginSetting from './enable_plugin_setting';
|
||||||
|
|
||||||
import {it} from '../admin_definition';
|
import {it} from '../admin_definition';
|
||||||
import {escapePathPart} from '../schema_admin_settings';
|
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 } } }
|
type OwnProps = { match: { params: { plugin_id: string } } }
|
||||||
|
|
||||||
@@ -34,9 +34,10 @@ function makeGetPluginSchema() {
|
|||||||
'makeGetPluginSchema',
|
'makeGetPluginSchema',
|
||||||
(state: GlobalState, pluginId: string) => state.entities.admin.plugins?.[pluginId],
|
(state: GlobalState, pluginId: string) => state.entities.admin.plugins?.[pluginId],
|
||||||
(state: GlobalState, pluginId: string) => getAdminConsoleCustomComponents(state, pluginId),
|
(state: GlobalState, pluginId: string) => getAdminConsoleCustomComponents(state, pluginId),
|
||||||
|
(state: GlobalState, pluginId: string) => getAdminConsoleCustomSections(state, pluginId),
|
||||||
(state) => appsFeatureFlagEnabled(state),
|
(state) => appsFeatureFlagEnabled(state),
|
||||||
isCurrentLicenseCloud,
|
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) {
|
if (!plugin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -44,9 +45,8 @@ function makeGetPluginSchema() {
|
|||||||
const escapedPluginId = escapePathPart(plugin.id);
|
const escapedPluginId = escapePathPart(plugin.id);
|
||||||
const pluginEnabledConfigKey = 'PluginSettings.PluginStates.' + escapedPluginId + '.Enable';
|
const pluginEnabledConfigKey = 'PluginSettings.PluginStates.' + escapedPluginId + '.Enable';
|
||||||
|
|
||||||
let settings: Array<Partial<AdminDefinitionSetting>> = [];
|
const parsePluginSettings = (settings: PluginSetting[]) => {
|
||||||
if (plugin.settings_schema && plugin.settings_schema.settings) {
|
return settings.map((setting) => {
|
||||||
settings = plugin.settings_schema.settings.map((setting) => {
|
|
||||||
const key = setting.key.toLowerCase();
|
const key = setting.key.toLowerCase();
|
||||||
let component = null;
|
let component = null;
|
||||||
let bannerType = '';
|
let bannerType = '';
|
||||||
@@ -82,8 +82,53 @@ function makeGetPluginSchema() {
|
|||||||
banner_type: bannerType,
|
banner_type: bannerType,
|
||||||
component,
|
component,
|
||||||
showTitle: customComponents[key] ? customComponents[key].options.showTitle : false,
|
showTitle: customComponents[key] ? customComponents[key].options.showTitle : false,
|
||||||
};
|
} as Partial<AdminDefinitionSetting>;
|
||||||
}) as Array<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) {
|
if (plugin.id !== appsPluginID || appsFeatureFlagIsEnabled) {
|
||||||
@@ -93,22 +138,34 @@ function makeGetPluginSchema() {
|
|||||||
} else {
|
} else {
|
||||||
pluginEnableSetting.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
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) {
|
if (s.isDisabled) {
|
||||||
s.isDisabled = it.any(s.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins')));
|
s.isDisabled = it.any(s.isDisabled, it.not(it.userHasWritePermissionOnResource('plugins')));
|
||||||
} else {
|
} else {
|
||||||
s.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
s.isDisabled = it.not(it.userHasWritePermissionOnResource('plugins'));
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (sections.length > 0) {
|
||||||
|
sections.forEach((section) => section.settings.forEach(checkDisableSetting));
|
||||||
|
} else {
|
||||||
|
settings.forEach(checkDisableSetting);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...plugin.settings_schema,
|
...plugin.settings_schema,
|
||||||
id: plugin.id,
|
id: plugin.id,
|
||||||
name: plugin.name,
|
name: plugin.name,
|
||||||
settings,
|
settings: sections.length > 0 ? undefined : settings,
|
||||||
|
sections: sections.length > 0 ? sections : undefined,
|
||||||
translate: Boolean(plugin.translate),
|
translate: Boolean(plugin.translate),
|
||||||
} as AdminDefinitionSubSectionSchema;
|
} as AdminDefinitionSubSectionSchema;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
let header;
|
||||||
if (section.header) {
|
if (section.header) {
|
||||||
header = (
|
header = (
|
||||||
@@ -1098,7 +1109,10 @@ export class SchemaAdminSettings extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sections.push(
|
sections.push(
|
||||||
<div className={'config-section'}>
|
<div
|
||||||
|
className={'config-section'}
|
||||||
|
key={section.key}
|
||||||
|
>
|
||||||
<SettingsGroup
|
<SettingsGroup
|
||||||
show={true}
|
show={true}
|
||||||
title={section.title}
|
title={section.title}
|
||||||
|
|||||||
@@ -167,12 +167,14 @@ type AdminDefinitionConfigSchemaSettings = {
|
|||||||
header?: string | MessageDescriptor;
|
header?: string | MessageDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminDefinitionConfigSchemaSection = {
|
export type AdminDefinitionConfigSchemaSection = {
|
||||||
title: string;
|
key: string;
|
||||||
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
settings: AdminDefinitionSetting[];
|
settings: AdminDefinitionSetting[];
|
||||||
header?: string | MessageDescriptor;
|
header?: string | MessageDescriptor;
|
||||||
footer?: string | MessageDescriptor;
|
footer?: string | MessageDescriptor;
|
||||||
|
component?: Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RestrictedIndicatorType = {
|
type RestrictedIndicatorType = {
|
||||||
|
|||||||
@@ -1922,6 +1922,7 @@
|
|||||||
"admin.plugin.backToPlugins": "Go back to the Plugins",
|
"admin.plugin.backToPlugins": "Go back to the Plugins",
|
||||||
"admin.plugin.choose": "Choose File",
|
"admin.plugin.choose": "Choose File",
|
||||||
"admin.plugin.cluster_instance": "Cluster Instance",
|
"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.customSetting.pluginDisabledWarning": "In order to view this setting, enable the plugin and click Save.",
|
||||||
"admin.plugin.disable": "Disable",
|
"admin.plugin.disable": "Disable",
|
||||||
"admin.plugin.disabling": "Disabling...",
|
"admin.plugin.disabling": "Disabling...",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
registerAdminConsolePlugin,
|
registerAdminConsolePlugin,
|
||||||
unregisterAdminConsolePlugin,
|
unregisterAdminConsolePlugin,
|
||||||
registerAdminConsoleCustomSetting,
|
registerAdminConsoleCustomSetting,
|
||||||
|
registerAdminConsoleCustomSection,
|
||||||
} from 'actions/admin_actions';
|
} from 'actions/admin_actions';
|
||||||
import {showRHSPlugin, hideRHSPlugin, toggleRHSPlugin} from 'actions/views/rhs';
|
import {showRHSPlugin, hideRHSPlugin, toggleRHSPlugin} from 'actions/views/rhs';
|
||||||
import {
|
import {
|
||||||
@@ -864,6 +865,12 @@ export default class PluginRegistry {
|
|||||||
store.dispatch(registerAdminConsolePlugin(this.id, func));
|
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.
|
// Register a custom React component to manage the plugin configuration for the given setting key.
|
||||||
// Accepts the following:
|
// Accepts the following:
|
||||||
// - key - A key specified in the settings_schema.settings block of the plugin's manifest.
|
// - 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}));
|
store.dispatch(registerAdminConsoleCustomSetting(this.id, key, component, {showTitle}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Unregister a previously registered admin console definition override function.
|
// Register a custom React component to render as a section in the plugin configuration page.
|
||||||
// Returns undefined.
|
// Accepts the following:
|
||||||
unregisterAdminConsolePlugin() {
|
// - key - A key specified in the settings_schema.sections block of the plugin's manifest.
|
||||||
store.dispatch(unregisterAdminConsolePlugin(this.id));
|
// - 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.
|
// Register a Right-Hand Sidebar component by providing a title for the right hand component.
|
||||||
// Accepts the following:
|
// Accepts the following:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import pluginReducers from '.';
|
|||||||
function getBaseState(): PluginsState {
|
function getBaseState(): PluginsState {
|
||||||
return {
|
return {
|
||||||
adminConsoleCustomComponents: {},
|
adminConsoleCustomComponents: {},
|
||||||
|
adminConsoleCustomSections: {},
|
||||||
adminConsoleReducers: {},
|
adminConsoleReducers: {},
|
||||||
components: {} as any,
|
components: {} as any,
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ import {UserTypes} from 'mattermost-redux/action_types';
|
|||||||
import {ActionTypes} from 'utils/constants';
|
import {ActionTypes} from 'utils/constants';
|
||||||
import {extractPluginConfiguration} from 'utils/plugins/plugin_setting_extraction';
|
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) {
|
function hasMenuId(menu: Menu|PluginComponent, menuId: string) {
|
||||||
if (!menu.subMenu) {
|
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) {
|
function siteStatsHandlers(state: PluginsState['siteStatsHandlers'] = {}, action: AnyAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.RECEIVED_PLUGIN_STATS_HANDLER:
|
case ActionTypes.RECEIVED_PLUGIN_STATS_HANDLER:
|
||||||
@@ -441,6 +485,10 @@ export default combineReducers({
|
|||||||
// React component to render on the plugin's system console.
|
// React component to render on the plugin's system console.
|
||||||
adminConsoleCustomComponents,
|
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
|
// 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
|
// a plugin to render on system console
|
||||||
siteStatsHandlers,
|
siteStatsHandlers,
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export const getAdminDefinition = createSelector(
|
|||||||
export const getAdminConsoleCustomComponents = (state, pluginId) =>
|
export const getAdminConsoleCustomComponents = (state, pluginId) =>
|
||||||
state.plugins.adminConsoleCustomComponents[pluginId] || {};
|
state.plugins.adminConsoleCustomComponents[pluginId] || {};
|
||||||
|
|
||||||
|
export const getAdminConsoleCustomSections = (state, pluginId) =>
|
||||||
|
state.plugins.adminConsoleCustomSections[pluginId] || {};
|
||||||
|
|
||||||
export const getConsoleAccess = createSelector(
|
export const getConsoleAccess = createSelector(
|
||||||
'getConsoleAccess',
|
'getConsoleAccess',
|
||||||
getAdminDefinition,
|
getAdminDefinition,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type PluginsState = {
|
|||||||
postTypes: {
|
postTypes: {
|
||||||
[postType: string]: PostPluginComponent;
|
[postType: string]: PostPluginComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
postCardTypes: {
|
postCardTypes: {
|
||||||
[postType: string]: PostPluginComponent;
|
[postType: string]: PostPluginComponent;
|
||||||
};
|
};
|
||||||
@@ -56,11 +57,19 @@ export type PluginsState = {
|
|||||||
adminConsoleReducers: {
|
adminConsoleReducers: {
|
||||||
[pluginId: string]: any;
|
[pluginId: string]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
adminConsoleCustomComponents: {
|
adminConsoleCustomComponents: {
|
||||||
[pluginId: string]: {
|
[pluginId: string]: {
|
||||||
[settingName: string]: AdminConsolePluginComponent;
|
[settingName: string]: AdminConsolePluginComponent;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
adminConsoleCustomSections: {
|
||||||
|
[pluginId: string]: {
|
||||||
|
[sectionKey: string]: AdminConsolePluginCustomSection;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
siteStatsHandlers: {
|
siteStatsHandlers: {
|
||||||
[pluginId: string]: PluginSiteStatsHandler;
|
[pluginId: string]: PluginSiteStatsHandler;
|
||||||
};
|
};
|
||||||
@@ -149,6 +158,12 @@ export type AdminConsolePluginComponent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminConsolePluginCustomSection = {
|
||||||
|
pluginId: string;
|
||||||
|
key: string;
|
||||||
|
component: React.Component;
|
||||||
|
};
|
||||||
|
|
||||||
export type PostWillRenderEmbedPluginComponent = {
|
export type PostWillRenderEmbedPluginComponent = {
|
||||||
id: string;
|
id: string;
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export const ActionTypes = keyMirror({
|
|||||||
RECEIVED_ADMIN_CONSOLE_REDUCER: null,
|
RECEIVED_ADMIN_CONSOLE_REDUCER: null,
|
||||||
REMOVED_ADMIN_CONSOLE_REDUCER: null,
|
REMOVED_ADMIN_CONSOLE_REDUCER: null,
|
||||||
RECEIVED_ADMIN_CONSOLE_CUSTOM_COMPONENT: null,
|
RECEIVED_ADMIN_CONSOLE_CUSTOM_COMPONENT: null,
|
||||||
|
RECEIVED_ADMIN_CONSOLE_CUSTOM_SECTION: null,
|
||||||
RECEIVED_PLUGIN_STATS_HANDLER: null,
|
RECEIVED_PLUGIN_STATS_HANDLER: null,
|
||||||
RECEIVED_PLUGIN_USER_SETTINGS: null,
|
RECEIVED_PLUGIN_USER_SETTINGS: null,
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ export type PluginSettingsSchema = {
|
|||||||
header: string;
|
header: string;
|
||||||
footer: string;
|
footer: string;
|
||||||
settings: PluginSetting[];
|
settings: PluginSetting[];
|
||||||
|
sections?: PluginSettingSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PluginSettingSection = {
|
||||||
|
key: string;
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
settings: PluginSetting[];
|
||||||
|
header?: string;
|
||||||
|
footer?: string;
|
||||||
|
custom?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PluginSetting = {
|
export type PluginSetting = {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user