mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-56758] Enhance search implementation for Admin console (#26099)
This commit is contained in:
parent
9ff26f7b4f
commit
e7f537e502
@ -12,6 +12,7 @@ import type {Role} from '@mattermost/types/roles';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import SchemaAdminSettings from 'components/admin_console/schema_admin_settings';
|
||||
import SearchKeywordMarking from 'components/admin_console/search_keyword_marking';
|
||||
import AnnouncementBarController from 'components/announcement_bar';
|
||||
import BackstageNavbar from 'components/backstage/components/backstage_navbar';
|
||||
import DiscardChangesModal from 'components/discard_changes_modal';
|
||||
@ -23,7 +24,6 @@ import {applyTheme, resetTheme} from 'utils/utils';
|
||||
import {LhsItemType} from 'types/store/lhs';
|
||||
|
||||
import AdminSidebar from './admin_sidebar';
|
||||
import Highlight from './highlight';
|
||||
import type {AdminDefinitionSubSection, AdminDefinitionSection} from './types';
|
||||
|
||||
import type {PropsFromRedux} from './index';
|
||||
@ -31,7 +31,7 @@ import type {PropsFromRedux} from './index';
|
||||
export type Props = PropsFromRedux & RouteComponentProps;
|
||||
|
||||
type State = {
|
||||
filter: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
// not every page in the system console will need the license and config, but the vast majority will
|
||||
@ -48,11 +48,11 @@ type ExtraProps = {
|
||||
isCurrentUserSystemAdmin: boolean;
|
||||
}
|
||||
|
||||
export default class AdminConsole extends React.PureComponent<Props, State> {
|
||||
class AdminConsole extends React.PureComponent<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filter: '',
|
||||
search: '',
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,8 +73,8 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
|
||||
applyTheme(this.props.currentTheme);
|
||||
}
|
||||
|
||||
private onFilterChange = (filter: string) => {
|
||||
this.setState({filter});
|
||||
private handleSearchChange = (search: string) => {
|
||||
this.setState({search});
|
||||
};
|
||||
|
||||
private mainRolesLoaded(roles: Record<string, Role>) {
|
||||
@ -192,14 +192,6 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
const discardChangesModal: JSX.Element = (
|
||||
<DiscardChangesModal
|
||||
show={showNavigationPrompt}
|
||||
onConfirm={confirmNavigation}
|
||||
onCancel={cancelNavigation}
|
||||
/>
|
||||
);
|
||||
|
||||
const extraProps: ExtraProps = {
|
||||
enterpriseReady: this.props.buildEnterpriseReady,
|
||||
license,
|
||||
@ -212,23 +204,33 @@ export default class AdminConsole extends React.PureComponent<Props, State> {
|
||||
cloud: this.props.cloud,
|
||||
isCurrentUserSystemAdmin: this.props.isCurrentUserSystemAdmin,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnnouncementBarController/>
|
||||
<SystemNotice/>
|
||||
<BackstageNavbar team={this.props.team}/>
|
||||
<AdminSidebar onFilterChange={this.onFilterChange}/>
|
||||
<AdminSidebar onSearchChange={this.handleSearchChange}/>
|
||||
<div
|
||||
className='admin-console__wrapper admin-console'
|
||||
id='adminConsoleWrapper'
|
||||
>
|
||||
<Highlight filter={this.state.filter}>
|
||||
<SearchKeywordMarking
|
||||
keyword={this.state.search}
|
||||
pathname={this.props.location.pathname}
|
||||
>
|
||||
{this.renderRoutes(extraProps)}
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
{discardChangesModal}
|
||||
<DiscardChangesModal
|
||||
show={showNavigationPrompt}
|
||||
onConfirm={confirmNavigation}
|
||||
onCancel={cancelNavigation}
|
||||
/>
|
||||
<ModalController/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminConsole;
|
||||
|
@ -43,8 +43,8 @@ exports[`components/AdminSidebar Plugins should filter plugins 1`] = `
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter="autolink"
|
||||
<SearchKeywordMarking
|
||||
keyword="autolink"
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -74,7 +74,7 @@ exports[`components/AdminSidebar Plugins should filter plugins 1`] = `
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -123,8 +123,8 @@ exports[`components/AdminSidebar Plugins should match snapshot 1`] = `
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -481,7 +481,7 @@ exports[`components/AdminSidebar Plugins should match snapshot 1`] = `
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -530,8 +530,8 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -1175,7 +1175,7 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -1224,8 +1224,8 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -1869,7 +1869,7 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -1918,13 +1918,13 @@ exports[`components/AdminSidebar should match snapshot, no access 1`] = `
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
/>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -1973,8 +1973,8 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -2618,7 +2618,7 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -2667,8 +2667,8 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -3312,7 +3312,7 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -3361,8 +3361,8 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -4168,7 +4168,7 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
@ -4217,8 +4217,8 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
|
||||
<div
|
||||
className="nav-pills__container"
|
||||
>
|
||||
<Highlight
|
||||
filter=""
|
||||
<SearchKeywordMarking
|
||||
keyword=""
|
||||
>
|
||||
<ul
|
||||
className="nav nav-pills nav-stacked"
|
||||
@ -5121,7 +5121,7 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
|
||||
/>
|
||||
</AdminSidebarCategory>
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
|
@ -61,7 +61,7 @@ describe('components/AdminSidebar', () => {
|
||||
webapp: {bundle_path: 'webapp/dist/main.js'},
|
||||
},
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -175,7 +175,7 @@ describe('components/AdminSidebar', () => {
|
||||
webapp: {bundle_path: 'webapp/dist/main.js'},
|
||||
},
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -221,7 +221,7 @@ describe('components/AdminSidebar', () => {
|
||||
webapp: {bundle_path: 'webapp/dist/main.js'},
|
||||
},
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -269,7 +269,7 @@ describe('components/AdminSidebar', () => {
|
||||
webapp: {bundle_path: 'webapp/dist/main.js'},
|
||||
},
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -345,7 +345,7 @@ describe('components/AdminSidebar', () => {
|
||||
webapp: {bundle_path: 'webapp/dist/main.js'},
|
||||
},
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -379,7 +379,7 @@ describe('components/AdminSidebar', () => {
|
||||
plugins: {
|
||||
'mattermost-autolink': samplePlugin1,
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
@ -467,7 +467,7 @@ describe('components/AdminSidebar', () => {
|
||||
plugins: {
|
||||
'mattermost-autolink': samplePlugin1,
|
||||
},
|
||||
onFilterChange: jest.fn(),
|
||||
onSearchChange: jest.fn(),
|
||||
actions: {
|
||||
getPlugins: jest.fn(),
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ import type {PluginRedux} from '@mattermost/types/plugins';
|
||||
import AdminSidebarCategory from 'components/admin_console/admin_sidebar/admin_sidebar_category';
|
||||
import AdminSidebarSection from 'components/admin_console/admin_sidebar/admin_sidebar_section';
|
||||
import AdminSidebarHeader from 'components/admin_console/admin_sidebar_header';
|
||||
import Highlight from 'components/admin_console/highlight';
|
||||
import SearchKeywordMarking from 'components/admin_console/search_keyword_marking';
|
||||
import QuickInput from 'components/quick_input';
|
||||
import SearchIcon from 'components/widgets/icons/search_icon';
|
||||
|
||||
@ -27,7 +27,7 @@ import type {PropsFromRedux} from './index';
|
||||
|
||||
export interface Props extends PropsFromRedux {
|
||||
intl: IntlShape;
|
||||
onFilterChange: (term: string) => void;
|
||||
onSearchChange: (term: string) => void;
|
||||
}
|
||||
|
||||
type State = {
|
||||
@ -94,11 +94,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
onFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const filter = e.target.value;
|
||||
if (filter === '') {
|
||||
this.setState({sections: null, filter});
|
||||
this.props.onFilterChange(filter);
|
||||
this.props.onSearchChange(filter);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
||||
}
|
||||
const sections = this.idx.search(query);
|
||||
this.setState({sections, filter});
|
||||
this.props.onFilterChange(filter);
|
||||
this.props.onSearchChange(filter);
|
||||
|
||||
if (this.props.navigationBlocked) {
|
||||
return;
|
||||
@ -286,7 +286,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
||||
|
||||
handleClearFilter = () => {
|
||||
this.setState({sections: null, filter: ''});
|
||||
this.props.onFilterChange('');
|
||||
this.props.onSearchChange('');
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -302,7 +302,7 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
||||
<QuickInput
|
||||
className={'filter ' + (this.state.filter ? 'active' : '')}
|
||||
type='text'
|
||||
onChange={this.onFilterChange}
|
||||
onChange={this.handleSearchChange}
|
||||
value={this.state.filter}
|
||||
placeholder={this.props.intl.formatMessage({id: 'admin.sidebar.filter', defaultMessage: 'Find settings'})}
|
||||
ref={this.searchRef}
|
||||
@ -320,11 +320,11 @@ class AdminSidebar extends React.PureComponent<Props, State> {
|
||||
renderView={renderScrollView}
|
||||
>
|
||||
<div className='nav-pills__container'>
|
||||
<Highlight filter={this.state.filter}>
|
||||
<SearchKeywordMarking keyword={this.state.filter}>
|
||||
<ul className={classNames('nav nav-pills nav-stacked', {'task-list-shown': showTaskList})}>
|
||||
{this.renderRootMenu(this.props.adminDefinition)}
|
||||
</ul>
|
||||
</Highlight>
|
||||
</SearchKeywordMarking>
|
||||
</div>
|
||||
</Scrollbars>
|
||||
</div>
|
||||
|
@ -1,49 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import Mark from 'mark.js';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
filter: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class Highlight extends React.PureComponent<Props> {
|
||||
private markInstance?: Mark;
|
||||
private ref: React.RefObject<HTMLDivElement>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.ref = React.createRef<HTMLDivElement>();
|
||||
}
|
||||
|
||||
private redrawHighlight = debounce(() => {
|
||||
if (this.markInstance) {
|
||||
this.markInstance.unmark();
|
||||
}
|
||||
|
||||
if (!this.props.filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is necesary to recreate the instances to get again the DOM elements after the re-render
|
||||
this.markInstance = new Mark(this.ref.current);
|
||||
this.markInstance.mark(this.props.filter, {accuracy: 'complementary'});
|
||||
}, 100, {leading: true, trailing: true});
|
||||
|
||||
public render() {
|
||||
// Run on next frame
|
||||
setTimeout(this.redrawHighlight, 0);
|
||||
return (
|
||||
<div ref={this.ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import Mark from 'mark.js';
|
||||
import React, {useEffect, useMemo, useRef} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
type Props = {
|
||||
keyword?: string;
|
||||
pathname?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const DEBOUNCE_WAIT_TIME = 200;
|
||||
|
||||
export default function SearchKeywordMarking({
|
||||
keyword = '',
|
||||
pathname,
|
||||
children,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const markJsRef = useRef<Mark>();
|
||||
|
||||
function doMark(keyword: string, container: HTMLDivElement) {
|
||||
markJsRef.current = new Mark(container);
|
||||
markJsRef.current.mark(keyword, {
|
||||
accuracy: 'complementary',
|
||||
exclude: ['.ignore-marking *'],
|
||||
});
|
||||
}
|
||||
|
||||
const debouncedDoMark = useMemo(() => debounce((keywordToMark?: string, markJs?: Mark, container?: HTMLDivElement | null) => {
|
||||
if (!keywordToMark || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (markJs) {
|
||||
// We need to mark again only after its 'done' callback is called
|
||||
// if we dont then there is a possiblity of creating multiple marks in the same container
|
||||
markJs.unmark({done: () => doMark(keywordToMark, container)});
|
||||
} else {
|
||||
// If there's no previous instance, just create a new one
|
||||
doMark(keywordToMark, container);
|
||||
}
|
||||
}, DEBOUNCE_WAIT_TIME), []);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedDoMark(keyword, markJsRef.current, containerRef.current);
|
||||
|
||||
return (() => {
|
||||
debouncedDoMark.cancel();
|
||||
|
||||
if (markJsRef.current) {
|
||||
markJsRef.current.unmark();
|
||||
}
|
||||
});
|
||||
}, [keyword, pathname]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -17,7 +17,7 @@ exports[`components/admin_console/system_users should match default snapshot 1`]
|
||||
<RevokeSessionsButton />
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="admin-console__wrapper"
|
||||
className="admin-console__wrapper ignore-marking"
|
||||
>
|
||||
<div
|
||||
className="admin-console__content"
|
||||
|
@ -315,7 +315,7 @@ export class SystemUsers extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
<RevokeSessionsButton/>
|
||||
</AdminHeader>
|
||||
<div className='admin-console__wrapper'>
|
||||
<div className='admin-console__wrapper ignore-marking'>
|
||||
<div className='admin-console__content'>
|
||||
<div className='more-modal__list member-list-holder'>
|
||||
<div className='system-users__filter-row'>
|
||||
|
Loading…
Reference in New Issue
Block a user