Add SectionNotice to plugin user settings (#25740)

* Add SectionNotice to plugin user settings

* Make section notice more general

* i18n-extract

* Address feedback

* Add missing import

* Fix lint

* Use OpenSans typography
Этот коммит содержится в:
Daniel Espino García
2023-12-20 15:04:04 +01:00
коммит произвёл GitHub
родитель 0fbe3b8e9b
Коммит 721f05e01e
21 изменённых файлов: 608 добавлений и 88 удалений

Просмотреть файл

@@ -22,12 +22,14 @@ import {getGroupMessageMembersCommonTeams} from 'actions/team_actions';
import {trackEvent} from 'actions/telemetry_actions';
import ChannelNameFormField from 'components/channel_name_form_field/channel_name_form_field';
import type {Actions} from 'components/convert_gm_to_channel_modal/index';
import NoCommonTeamsError from 'components/convert_gm_to_channel_modal/no_common_teams/no_common_teams';
import TeamSelector from 'components/convert_gm_to_channel_modal/team_selector/team_selector';
import WarningTextSection from 'components/convert_gm_to_channel_modal/warning_text_section/warning_text_section';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import NoCommonTeamsError from './no_common_teams/no_common_teams';
import TeamSelector from './team_selector/team_selector';
import WarningTextSection from './warning_text_section/warning_text_section';
import type {Actions} from './index';
const enum ServerErrorId {
CHANNEL_NAME_EXISTS = 'store.sql_channel.save_channel.exists.app_error',
}

Просмотреть файл

@@ -2,29 +2,24 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useIntl} from 'react-intl';
import 'components/convert_gm_to_channel_modal/warning_text_section/warning_text_section.scss';
import SectionNotice from 'components/section_notice';
const NoCommonTeamsError = (): JSX.Element => {
const intl = useIntl();
return (
<div className='warning-section error'>
<i className='fa fa-exclamation-circle'/>
<div className='warning-text'>
<div className='warning-header'>
<FormattedMessage
id='sidebar_left.sidebar_channel_modal.no_common_teams_error.heading'
defaultMessage='Unable to convert to a channel because group members are part of different teams'
/>
</div>
<div className='warning-body'>
<FormattedMessage
id='sidebar_left.sidebar_channel_modal.no_common_teams_error.body'
defaultMessage='Group Message cannot be converted to a channel because members are not a part of the same team. Add all members to a single team to convert this group message to a channel in that team.'
/>
</div>
</div>
</div>
<SectionNotice
title={intl.formatMessage({
id: 'sidebar_left.sidebar_channel_modal.no_common_teams_error.heading',
defaultMessage: 'Unable to convert to a channel because group members are part of different teams',
})}
text={intl.formatMessage({
id: 'sidebar_left.sidebar_channel_modal.no_common_teams_error.body',
defaultMessage: 'Group Message cannot be converted to a channel because members are not a part of the same team. Add all members to a single team to convert this group message to a channel in that team.',
})}
type={'danger'}
/>
);
};

Просмотреть файл

@@ -1,35 +0,0 @@
.warning-section {
display: flex;
padding: 16px;
border: 1px solid rgba(var(--sidebar-text-active-border-rgb), 0.16);
background: rgba(var(--sidebar-text-active-border-rgb), 0.08);
border-radius: 4px;
gap: 12px;
&.error {
border: 1px solid rgba(var(--dnd-indicator-rgb), 0.16);
background: rgba(var(--dnd-indicator-rgb), 0.08);
.fa.fa-exclamation-circle {
color: rgba(var(--dnd-indicator-rgb), 1);
}
}
.fa.fa-exclamation-circle {
width: 24px;
height: 24px;
color: rgba(var(--sidebar-text-active-border-rgb), 1);
font-size: 24px;
}
.warning-text {
display: flex;
flex-direction: column;
color: var(--center-channel-color-88);
gap: 8px;
}
.warning-header {
font-weight: bold;
}
}

Просмотреть файл

@@ -2,9 +2,9 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useIntl} from 'react-intl';
import 'components/convert_gm_to_channel_modal/warning_text_section/warning_text_section.scss';
import SectionNotice from 'components/section_notice';
export type Props = {
channelMemberNames: string[];
@@ -21,26 +21,19 @@ const WarningTextSection = (props: Props): JSX.Element => {
}
return (
<div className='warning-section'>
<i className='fa fa-exclamation-circle'/>
<div className='warning-text'>
<div className='warning-header'>
<FormattedMessage
id='sidebar_left.sidebar_channel_modal.warning_header'
defaultMessage='Conversation history will be visible to any channel members'
/>
</div>
<div className='warning-body'>
<FormattedMessage
id='sidebar_left.sidebar_channel_modal.warning_body'
defaultMessage='You are about to convert the Group Message with {memberNames} to a Channel. This cannot be undone.'
values={{
memberNames,
}}
/>
</div>
</div>
</div>
<SectionNotice
title={intl.formatMessage({
id: 'sidebar_left.sidebar_channel_modal.warning_header',
defaultMessage: 'Conversation history will be visible to any channel members',
})}
text={intl.formatMessage({
id: 'sidebar_left.sidebar_channel_modal.warning_body',
defaultMessage: 'You are about to convert the Group Message with {memberNames} to a Channel. This cannot be undone.',
},
{
memberNames,
})}
/>
);
};
export default WarningTextSection;

Просмотреть файл

@@ -0,0 +1,120 @@
.sectionNoticeContainer {
position: relative;
border-width: 1px;
border-style: solid;
border-radius: 4px;
p {
margin: 0;
}
&.info {
border-color: rgba(var(--sidebar-text-active-border-rgb), 0.16);
background: rgba(var(--sidebar-text-active-border-rgb), 0.08);
p {
color: var(--center-channel-color);
}
}
&.success {
border-color: rgba(var(--online-indicator-rgb), 0.16);
background: rgba(var(--online-indicator-rgb), 0.08);
p {
color: var(--center-channel-color);
}
}
&.danger {
border-color: rgba(var(--dnd-indicator-rgb), 0.16);
background: rgba(var(--dnd-indicator-rgb), 0.08);
p {
color: var(--center-channel-color);
}
}
&.welcome {
border-color: rgba(var(--center-channel-color-rgb), 0.08);
background: rgba(var(--center-channel-color-rgb), 0.04);
p {
color: rgba(var(--center-channel-color-rgb), 0.72);
}
}
&.warning {
border-color: rgba(var(--away-indicator-rgb), 0.16);
background: rgba(var(--away-indicator-rgb), 0.08);
p {
color: var(--center-channel-color);
}
}
}
.sectionNoticeActions {
display: flex;
flex-flow: wrap;
}
.sectionNoticeClose {
position: absolute;
top: 10px;
right: 10px;
}
.sectionNoticeContent {
display: flex;
flex-direction: row;
align-items: start;
padding: 16px;
gap: 12px;
}
.sectionNoticeIcon {
font-size: 20px;
&.info {
color: var(--sidebar-text-active-border);
}
&.success {
color: var(--online-indicator);
}
&.danger {
color: var(--dnd-indicator);
}
&.warning {
color: var(--away-indicator);
}
}
.sectionNoticeTitle {
margin: 0;
color: var(--center-channel-color);
font-family: 'Open Sans';
font-size: 14px;
font-weight: 600;
line-height: 20px;
&.welcome {
font-family: 'Metropolis';
font-size: 20px;
line-height: 28px;
}
}
.sectionNoticeBody {
display: flex;
flex-direction: column;
gap: 8px;
}
.sectionNoticeButton {
margin-top: 4px;
margin-bottom: 4px;
}

Просмотреть файл

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent, screen} from '@testing-library/react';
import type {ComponentProps} from 'react';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import SectionNotice from './section_notice';
type Props = ComponentProps<typeof SectionNotice>;
function getBaseProps(): Props {
return {
text: 'some text',
title: 'some title',
primaryButton: {
onClick: jest.fn(),
text: 'primary button title',
},
secondaryButton: {
onClick: jest.fn(),
text: 'secondary button title',
},
linkButton: {
onClick: jest.fn(),
text: 'link button title',
},
isDismissable: true,
onDismissClick: jest.fn(),
type: 'info',
};
}
describe('PluginAction', () => {
it('does show the correct information', () => {
const props = getBaseProps();
renderWithContext(<SectionNotice {...props}/>);
const primaryButton = screen.getByText(props.primaryButton!.text);
const secondaryButton = screen.getByText(props.secondaryButton!.text);
const linkButton = screen.getByText(props.linkButton!.text);
const closeButton = screen.getByLabelText('Dismiss notice');
expect(primaryButton).toBeInTheDocument();
expect(secondaryButton).toBeInTheDocument();
expect(linkButton).toBeInTheDocument();
expect(closeButton).toBeInTheDocument();
expect(screen.queryByText(props.text)).toBeInTheDocument();
expect(screen.queryByText(props.title)).toBeInTheDocument();
fireEvent.click(primaryButton);
expect(props.primaryButton?.onClick).toHaveBeenCalledTimes(1);
fireEvent.click(secondaryButton);
expect(props.secondaryButton?.onClick).toHaveBeenCalledTimes(1);
fireEvent.click(linkButton);
expect(props.linkButton?.onClick).toHaveBeenCalledTimes(1);
fireEvent.click(closeButton);
expect(props.onDismissClick).toHaveBeenCalledTimes(1);
});
it('does not show the button if no button is passed', () => {
const props = getBaseProps();
props.primaryButton = undefined;
props.secondaryButton = undefined;
props.linkButton = undefined;
props.isDismissable = false;
renderWithContext(<SectionNotice {...props}/>);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});

Просмотреть файл

@@ -0,0 +1,101 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import {useIntl} from 'react-intl';
import Markdown from 'components/markdown';
import './section_notice.scss';
type Button = {
onClick: () => void;
text: string;
}
type Props = {
title: string;
text: string;
primaryButton?: Button;
secondaryButton?: Button;
linkButton?: Button;
type?: 'info' | 'success' | 'danger' | 'welcome' | 'warning';
isDismissable?: boolean;
onDismissClick?: () => void;
};
const iconByType = {
info: 'icon-information-outline',
success: 'icon-check',
danger: 'icon-alert-outline',
warning: 'icon-alert-outline',
welcome: undefined,
};
const SectionNotice = ({
title,
text,
primaryButton,
secondaryButton,
linkButton,
type = 'info',
isDismissable,
onDismissClick,
}: Props) => {
const intl = useIntl();
const icon = iconByType[type];
const showDismiss = Boolean(isDismissable && onDismissClick);
const buttonClass = 'btn btn-sm sectionNoticeButton';
return (
<div className={classNames('sectionNoticeContainer', type)}>
<div className={'sectionNoticeContent'}>
{icon && <i className={classNames('icon sectionNoticeIcon', icon, type)}/>}
<div className='sectionNoticeBody'>
<h4 className={classNames('sectionNoticeTitle', {welcome: type === 'welcome'})}>{title}</h4>
<Markdown message={text}/>
<div className='sectionNoticeActions'>
{primaryButton &&
<button
onClick={primaryButton.onClick}
className={classNames(buttonClass, 'btn-primary')}
>
{primaryButton.text}
</button>
}
{secondaryButton &&
<button
onClick={secondaryButton.onClick}
className={classNames(buttonClass, 'btn-secondary')}
>
{secondaryButton.text}
</button>
}
{linkButton &&
<button
onClick={linkButton.onClick}
className={classNames(buttonClass, 'btn-link')}
>
{linkButton.text}
</button>
}
</div>
</div>
</div>
{showDismiss &&
<button
className='btn btn-icon btn-sm sectionNoticeClose'
onClick={onDismissClick}
aria-label={intl.formatMessage({
id: 'sectionNotice.dismiss',
defaultMessage: 'Dismiss notice',
})}
>
<i className='icon icon-close'/>
</button>
}
</div>
);
};
export default SectionNotice;

Просмотреть файл

@@ -18,7 +18,7 @@ import UserSettingsModal from './index';
type Props = ComponentProps<typeof UserSettingsModal>;
const baseProps: Props = {
isContentProductSettings: false,
isContentProductSettings: true,
onExited: jest.fn(),
};
@@ -39,6 +39,40 @@ describe('do first render to avoid other testing issues', () => {
renderWithContext(<UserSettingsModal {...baseProps}/>, baseState);
});
describe('plugin tabs are only rendered on content product settings', () => {
it('plugin tabs are properly rendered', async () => {
const uiName1 = 'plugin_a';
const uiName2 = 'plugin_b';
const state: DeepPartial<GlobalState> = {
plugins: {
userSettings: {
plugin_a: {
id: 'plugin_a',
sections: [],
uiName: uiName1,
},
plugin_b: {
id: 'plugin_b',
sections: [],
uiName: uiName2,
},
},
},
};
renderWithContext(
<UserSettingsModal
{...baseProps}
isContentProductSettings={false}
/>,
mergeObjects(baseState, state),
);
expect(screen.queryByText(uiName1)).not.toBeInTheDocument();
expect(screen.queryByText(uiName2)).not.toBeInTheDocument();
});
});
describe('tabs are properly rendered', () => {
it('plugin tabs are properly rendered', async () => {
const uiName1 = 'plugin_a';

Просмотреть файл

@@ -280,11 +280,18 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
return (<div/>);
}
const tabs = [];
let pluginTabs;
if (this.props.isContentProductSettings) {
tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'icon icon-bell-outline', iconTitle: Utils.localizeMessage('user.settings.notifications.icon', 'Notification Settings Icon')});
tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'icon icon-eye-outline', iconTitle: Utils.localizeMessage('user.settings.display.icon', 'Display Settings Icon')});
tabs.push({name: 'sidebar', uiName: formatMessage(holders.sidebar), icon: 'icon icon-dock-left', iconTitle: Utils.localizeMessage('user.settings.sidebar.icon', 'Sidebar Settings Icon')});
tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'icon icon-tune', iconTitle: Utils.localizeMessage('user.settings.advance.icon', 'Advanced Settings Icon')});
pluginTabs = Object.values(this.props.pluginSettings).map((v) => ({
icon: v.icon ? {url: v.icon} : 'icon-power-plug-outline',
iconTitle: v.uiName,
name: v.id,
uiName: v.uiName,
}));
} else {
tabs.push({name: 'profile', uiName: formatMessage(holders.profile), icon: 'icon icon-settings-outline', iconTitle: Utils.localizeMessage('user.settings.profile.icon', 'Profile Settings Icon')});
tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'icon icon-lock-outline', iconTitle: Utils.localizeMessage('user.settings.security.icon', 'Security Settings Icon')});
@@ -327,12 +334,7 @@ class UserSettingsModal extends React.PureComponent<Props, State> {
<Provider store={store}>
<SettingsSidebar
tabs={tabs}
pluginTabs={Object.values(this.props.pluginSettings).map((v) => ({
icon: v.icon ? {url: v.icon} : 'icon-power-plug-outline',
iconTitle: v.uiName,
name: v.id,
uiName: v.uiName,
}))}
pluginTabs={pluginTabs}
activeTab={this.state.active_tab}
updateTab={this.updateTab}
/>

Просмотреть файл

@@ -13,6 +13,16 @@ exports[`plugin tab all props are properly passed to the children 1`] = `
<SettingDesktopHeader
text="plugin A Settings"
/>
<PluginAction
action={
Object {
"buttonText": "buttonText",
"onClick": [MockFunction],
"text": "actionText",
"title": "actionTitle",
}
}
/>
<div
className="divider-dark first"
/>

Просмотреть файл

@@ -18,6 +18,12 @@ const baseProps: Props = {
collapseModal: jest.fn(),
settings: {
id: 'pluginA',
action: {
text: 'actionText',
buttonText: 'buttonText',
onClick: jest.fn(),
title: 'actionTitle',
},
sections: [
{
settings: [

Просмотреть файл

@@ -6,6 +6,7 @@ import {useIntl} from 'react-intl';
import type {PluginConfiguration} from 'types/plugins/user_settings';
import PluginAction from './plugin_action';
import PluginSetting from './plugin_setting';
import SettingDesktopHeader from '../setting_desktop_header';
@@ -42,6 +43,7 @@ const PluginTab = ({
/>
<div className='user-settings'>
<SettingDesktopHeader text={headerText}/>
<PluginAction action={settings.action}/>
<div className='divider-dark first'/>
{settings.sections.map(
(v) =>

Просмотреть файл

@@ -0,0 +1,3 @@
.pluginActionContainer {
margin-bottom: 20px;
}

Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fireEvent, render, screen} from '@testing-library/react';
import type {ComponentProps} from 'react';
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import PluginAction from './plugin_action';
type Props = ComponentProps<typeof PluginAction>;
function getBaseProps(): Props {
return {
action: {
title: 'some title',
text: 'some text',
buttonText: 'button text',
onClick: jest.fn(),
},
};
}
describe('PluginAction', () => {
it('does not show when no action is provided', () => {
const {container} = render(<PluginAction/>);
expect(container.firstChild).toBeNull();
});
it('does show the correct information', () => {
const props = getBaseProps();
renderWithContext(<PluginAction {...props}/>);
const button = screen.getByText(props.action!.buttonText);
expect(button).toBeInTheDocument();
expect(screen.queryByText(props.action!.text)).toBeInTheDocument();
expect(screen.queryByText(props.action!.title)).toBeInTheDocument();
fireEvent.click(button);
expect(props.action?.onClick).toHaveBeenCalled();
});
});

Просмотреть файл

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ComponentProps} from 'react';
import React, {useMemo} from 'react';
import SectionNotice from 'components/section_notice';
import type {PluginConfigurationAction} from 'types/plugins/user_settings';
import './plugin_action.scss';
type Props = {
action?: PluginConfigurationAction;
};
const PluginAction = ({
action,
}: Props) => {
const props = useMemo<ComponentProps<typeof SectionNotice>>(() => {
return action ? {
text: action.text,
title: action.title,
primaryButton: {
onClick: action?.onClick,
text: action?.buttonText,
},
} : {
text: '',
title: '',
};
}, [action]);
if (!action) {
return null;
}
return (
<div className={'pluginActionContainer'}>
<SectionNotice {...props}/>
</div>
);
};
export default PluginAction;

Просмотреть файл

@@ -66,6 +66,16 @@ describe('plugin setting', () => {
expect(screen.queryByText(OPTION_1_TEXT)).toBeInTheDocument();
});
it('isDisabled is respected', () => {
const props = getBaseProps();
props.section.disabled = true;
renderWithContext(<PluginSetting {...props}/>);
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
expect(screen.queryByText(SECTION_TITLE)).toBeInTheDocument();
fireEvent.click(screen.getByText(SECTION_TITLE));
expect(screen.queryByText(OPTION_1_TEXT)).not.toBeInTheDocument();
});
it('properly take the current value from the preferences', () => {
const category = getPluginPreferenceKey(PLUGIN_ID);
const prefKey = getPreferenceKey(category, SETTING_1_NAME);

Просмотреть файл

@@ -112,6 +112,7 @@ const PluginSetting = ({
title={section.title}
updateSection={updateSection}
describe={minDescribe}
isDisabled={section.disabled}
/>
);
};

Просмотреть файл

@@ -4751,6 +4751,7 @@
"search_list_option.on": "Messages on a date",
"search_list_option.phrases": "Messages with phrases",
"search_results.channel-files-header": "Recent files",
"sectionNotice.dismiss": "Dismiss notice",
"select_team.icon": "Select Team Icon",
"select_team.join.icon": "Join Team Icon",
"select_team.private.icon": "Private Team",

Просмотреть файл

@@ -12,6 +12,14 @@ export type PluginConfiguration = {
/** URL to the icon to show in the UI. No icon will show the plug outline icon. */
icon?: string;
sections: PluginConfigurationSection[];
action?: PluginConfigurationAction;
}
export type PluginConfigurationAction = {
title: string;
text: string;
buttonText: string;
onClick: () => void;
}
export type PluginConfigurationSection = {
@@ -20,6 +28,9 @@ export type PluginConfigurationSection = {
/** The title of the section. All titles must be different. */
title: string;
/** Whether the section is disabled. */
disabled?: boolean;
/**
* This function will be called whenever a section is saved.
*

Просмотреть файл

@@ -8,6 +8,12 @@ import {extractPluginConfiguration} from './plugin_setting_extraction';
function getFullExample(): PluginConfiguration {
return {
id: '',
action: {
buttonText: 'some button text',
onClick: () => 1,
text: 'some text',
title: 'some title',
},
sections: [
{
settings: [
@@ -68,6 +74,7 @@ function getFullExample(): PluginConfiguration {
],
title: 'title 2',
onSubmit: () => 2,
disabled: true,
},
],
uiName: 'some name',
@@ -99,6 +106,32 @@ describe('plugin setting extraction', () => {
expect(res!.id).toBe(pluginId);
});
it('action gets properly added', () => {
const config = getFullExample();
const res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.action).toBeTruthy();
expect(res?.action?.buttonText).toBe(config.action?.buttonText);
expect(res?.action?.text).toBe(config.action?.text);
expect(res?.action?.title).toBe(config.action?.title);
expect(res?.action?.onClick).toBe(config.action?.onClick);
});
it('sections get properly added', () => {
const config = getFullExample();
const res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.sections).toHaveLength(2);
expect(res?.sections[0].disabled).toBe(config.sections[0].disabled);
expect(res?.sections[0].title).toBe(config.sections[0].title);
expect(res?.sections[0].onSubmit).toBe(config.sections[0].onSubmit);
expect(res?.sections[0].settings).toHaveLength(config.sections[0].settings.length);
expect(res?.sections[1].disabled).toBe(config.sections[1].disabled);
expect(res?.sections[1].title).toBe(config.sections[1].title);
expect(res?.sections[1].onSubmit).toBe(config.sections[1].onSubmit);
expect(res?.sections[1].settings).toHaveLength(config.sections[1].settings.length);
});
it('reject configs without name', () => {
const config: any = getFullExample();
config.uiName = '';
@@ -233,6 +266,32 @@ describe('plugin setting extraction', () => {
expect(res?.sections[0].settings).toHaveLength(1);
});
it('filter out ill defined action', () => {
let config: any = getFullExample();
delete config.action?.buttonText;
let res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.action).toBeFalsy();
config = getFullExample();
delete config.action?.title;
res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.action).toBeFalsy();
config = getFullExample();
delete config.action?.text;
res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.action).toBeFalsy();
config = getFullExample();
delete config.action?.onClick;
res = extractPluginConfiguration(config, 'PluginId');
expect(res).toBeTruthy();
expect(res?.action).toBeFalsy();
});
it('(future proof) filter out extra config arguments', () => {
const config: any = getFullExample();
config.futureProperty = 'hello';

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {BasePluginConfigurationSetting, PluginConfiguration, PluginConfigurationRadioSetting, PluginConfigurationRadioSettingOption, PluginConfigurationSection} from 'types/plugins/user_settings';
import type {BasePluginConfigurationSetting, PluginConfiguration, PluginConfigurationAction, PluginConfigurationRadioSetting, PluginConfigurationRadioSettingOption, PluginConfigurationSection} from 'types/plugins/user_settings';
export function extractPluginConfiguration(pluginConfiguration: unknown, pluginId: string) {
if (!pluginConfiguration) {
@@ -33,11 +33,17 @@ export function extractPluginConfiguration(pluginConfiguration: unknown, pluginI
return undefined;
}
let action;
if ('action' in pluginConfiguration && pluginConfiguration.action) {
action = extractPluginConfigurationAction(pluginConfiguration.action);
}
const result: PluginConfiguration = {
id: pluginId,
icon,
sections: [],
uiName: pluginConfiguration.uiName,
action,
};
for (const section of pluginConfiguration.sections) {
@@ -54,6 +60,39 @@ export function extractPluginConfiguration(pluginConfiguration: unknown, pluginI
return result;
}
function extractPluginConfigurationAction(action: unknown): PluginConfigurationAction | undefined {
if (!action) {
return undefined;
}
if (typeof action !== 'object') {
return undefined;
}
if (!('title' in action) || !action.title || typeof action.title !== 'string') {
return undefined;
}
if (!('text' in action) || !action.text || typeof action.text !== 'string') {
return undefined;
}
if (!('buttonText' in action) || !action.buttonText || typeof action.buttonText !== 'string') {
return undefined;
}
if (!('onClick' in action) || !action.onClick || typeof action.onClick !== 'function') {
return undefined;
}
return {
title: action.title,
text: action.text,
buttonText: action.buttonText,
onClick: action.onClick as PluginConfigurationAction['onClick'],
};
}
function extractPluginConfigurationSection(section: unknown) {
if (!section) {
return undefined;
@@ -84,9 +123,19 @@ function extractPluginConfigurationSection(section: unknown) {
}
}
let disabled;
if ('disabled' in section && section.disabled) {
if (typeof section.disabled === 'boolean') {
disabled = section.disabled;
} else {
return undefined;
}
}
const result: PluginConfigurationSection = {
settings: [],
title: section.title,
disabled,
onSubmit,
};