* 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>
Этот коммит содержится в:
Daniel Espino García
2023-12-14 11:30:31 +01:00
коммит произвёл GitHub
родитель 563f51f3db
Коммит acd413ef69
36 изменённых файлов: 2163 добавлений и 56 удалений

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

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