Add plugin user settings (#25517)
* Add plugin user settings * Feedback and UI improvements * Extract the plugin configuration instead of just validating it * Fix lint * Fix lint * Divide between settings and sections * i18n-extract * Adjust icon location * Add tests * Improve documentation * Force plugin id * Fix test --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
563f51f3db
Коммит
acd413ef69
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import type {ComponentProps} from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import SettingsSidebar from './settings_sidebar';
|
||||
|
||||
type Props = ComponentProps<typeof SettingsSidebar>;
|
||||
|
||||
const baseProps: Props = {
|
||||
isMobileView: false,
|
||||
tabs: [],
|
||||
updateTab: jest.fn(),
|
||||
pluginTabs: [],
|
||||
};
|
||||
|
||||
describe('properly use the correct icon', () => {
|
||||
it('icon as a string', () => {
|
||||
const iconTitle = 'Icon title';
|
||||
const icon = 'icon';
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
tabs: [{
|
||||
icon,
|
||||
iconTitle,
|
||||
name: 'tab',
|
||||
uiName: 'Tab UI Name',
|
||||
}],
|
||||
};
|
||||
renderWithContext(<SettingsSidebar {...props}/>);
|
||||
|
||||
const element = screen.queryByTitle(iconTitle);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element!.nodeName).toBe('I');
|
||||
expect(element!.className).toBe(icon);
|
||||
});
|
||||
|
||||
it('icon as an image', () => {
|
||||
const iconTitle = 'Icon title';
|
||||
const url = 'icon_url';
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
pluginTabs: [{
|
||||
icon: {url},
|
||||
iconTitle,
|
||||
name: 'tab',
|
||||
uiName: 'Tab UI Name',
|
||||
}],
|
||||
};
|
||||
renderWithContext(<SettingsSidebar {...props}/>);
|
||||
|
||||
const element = screen.queryByAltText(iconTitle);
|
||||
expect(element).toBeInTheDocument();
|
||||
expect(element!.nodeName).toBe('IMG');
|
||||
expect(element!.getAttribute('src')).toBe(url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('show PLUGIN PREFERENCES only when plugin tabs are added', () => {
|
||||
it('not show when there are no plugin tabs', () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
tabs: [{
|
||||
icon: 'icon',
|
||||
iconTitle: 'title',
|
||||
name: 'tab',
|
||||
uiName: 'Tab UI Name',
|
||||
}],
|
||||
};
|
||||
renderWithContext(<SettingsSidebar {...props}/>);
|
||||
|
||||
expect(screen.queryByText('PLUGIN PREFERENCES')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('show when there are plugin tabs', () => {
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
pluginTabs: [{
|
||||
icon: 'icon',
|
||||
iconTitle: 'title',
|
||||
name: 'tab',
|
||||
uiName: 'Tab UI Name',
|
||||
}],
|
||||
};
|
||||
renderWithContext(<SettingsSidebar {...props}/>);
|
||||
|
||||
expect(screen.queryByText('PLUGIN PREFERENCES')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tabs are properly rendered', () => {
|
||||
it('plugin tabs are properly rendered', () => {
|
||||
const uiName1 = 'Tab UI Name 1';
|
||||
const uiName2 = 'Tab UI Name 2';
|
||||
const props: Props = {
|
||||
...baseProps,
|
||||
pluginTabs: [
|
||||
{
|
||||
icon: 'icon1',
|
||||
iconTitle: 'title1',
|
||||
name: 'tab1',
|
||||
uiName: uiName1,
|
||||
},
|
||||
{
|
||||
icon: 'icon2',
|
||||
iconTitle: 'title2',
|
||||
name: 'tab2',
|
||||
uiName: uiName2,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
renderWithContext(<SettingsSidebar {...props}/>);
|
||||
|
||||
expect(screen.queryByText(uiName1)).toBeInTheDocument();
|
||||
expect(screen.queryByText(uiName2)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import type {RefObject} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {isKeyPressed} from 'utils/keyboard';
|
||||
@@ -10,7 +11,7 @@ import * as UserAgent from 'utils/user_agent';
|
||||
import {a11yFocus} from 'utils/utils';
|
||||
|
||||
export type Tab = {
|
||||
icon: string;
|
||||
icon: string | {url: string};
|
||||
iconTitle: string;
|
||||
name: string;
|
||||
uiName: string;
|
||||
@@ -19,9 +20,10 @@ export type Tab = {
|
||||
export type Props = {
|
||||
activeTab?: string;
|
||||
tabs: Tab[];
|
||||
pluginTabs?: Tab[];
|
||||
updateTab: (name: string) => void;
|
||||
isMobileView: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export default class SettingsSidebar extends React.PureComponent<Props> {
|
||||
buttonRefs: Array<RefObject<HTMLButtonElement>>;
|
||||
@@ -57,42 +59,78 @@ export default class SettingsSidebar extends React.PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const tabList = this.props.tabs.map((tab, index) => {
|
||||
const key = `${tab.name}_li`;
|
||||
const isActive = this.props.activeTab === tab.name;
|
||||
let className = '';
|
||||
if (isActive) {
|
||||
className = 'active';
|
||||
}
|
||||
private renderTab(tab: Tab, index: number) {
|
||||
const key = `${tab.name}_li`;
|
||||
const isActive = this.props.activeTab === tab.name;
|
||||
let className = '';
|
||||
if (isActive) {
|
||||
className = 'active';
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
id={`${tab.name}Li`}
|
||||
key={key}
|
||||
className={className}
|
||||
role='presentation'
|
||||
>
|
||||
<button
|
||||
ref={this.buttonRefs[index]}
|
||||
id={`${tab.name}Button`}
|
||||
className='cursor--pointer style--none'
|
||||
onClick={this.handleClick.bind(null, tab)}
|
||||
onKeyUp={this.handleKeyUp.bind(null, index)}
|
||||
aria-label={tab.uiName.toLowerCase()}
|
||||
role='tab'
|
||||
aria-selected={isActive}
|
||||
tabIndex={!isActive && !this.props.isMobileView ? -1 : 0}
|
||||
>
|
||||
<i
|
||||
className={tab.icon}
|
||||
title={tab.iconTitle}
|
||||
/>
|
||||
{tab.uiName}
|
||||
</button>
|
||||
</li>
|
||||
let icon;
|
||||
if (typeof tab.icon === 'string') {
|
||||
icon = (
|
||||
<i
|
||||
className={tab.icon}
|
||||
title={tab.iconTitle}
|
||||
/>
|
||||
);
|
||||
});
|
||||
} else {
|
||||
icon = (
|
||||
<img
|
||||
src={tab.icon.url}
|
||||
alt={tab.iconTitle}
|
||||
className='icon'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
id={`${tab.name}Li`}
|
||||
key={key}
|
||||
className={className}
|
||||
role='presentation'
|
||||
>
|
||||
<button
|
||||
ref={this.buttonRefs[index]}
|
||||
id={`${tab.name}Button`}
|
||||
className='cursor--pointer style--none'
|
||||
onClick={this.handleClick.bind(null, tab)}
|
||||
onKeyUp={this.handleKeyUp.bind(null, index)}
|
||||
aria-label={tab.uiName.toLowerCase()}
|
||||
role='tab'
|
||||
aria-selected={isActive}
|
||||
tabIndex={!isActive && !this.props.isMobileView ? -1 : 0}
|
||||
>
|
||||
{icon}
|
||||
{tab.uiName}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const tabList = this.props.tabs.map((tab, index) => this.renderTab(tab, index));
|
||||
let pluginTabList: React.ReactNode;
|
||||
if (this.props.pluginTabs?.length) {
|
||||
pluginTabList = (
|
||||
<>
|
||||
<hr/>
|
||||
<li
|
||||
key={'plugin preferences heading'}
|
||||
role='heading'
|
||||
className={'header'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={'userSettingsModal.pluginPreferences.header'}
|
||||
defaultMessage={'PLUGIN PREFERENCES'}
|
||||
/>
|
||||
</li>
|
||||
{this.props.pluginTabs.map((tab, index) => this.renderTab(tab, index))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -103,6 +141,7 @@ export default class SettingsSidebar extends React.PureComponent<Props> {
|
||||
aria-orientation='vertical'
|
||||
>
|
||||
{tabList}
|
||||
{pluginTabList}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
Ссылка в новой задаче
Block a user