Add account filtering feature (#832)

* add account filter

* Update packages/extension-ui/src/partials/Header.tsx
This commit is contained in:
Thibaut Sardan 2021-11-07 11:38:55 +00:00 committed by GitHub
parent 926848f056
commit 3d5d63b00a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 22 deletions

View File

@ -3,9 +3,12 @@
import type { ThemeProps } from '../../types';
import React, { useContext } from 'react';
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import { AccountWithChildren } from '@polkadot/extension-base/background/types';
import getNetworkMap from '@polkadot/extension-ui/util/getNetworkMap';
import { AccountContext } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { Header } from '../../partials';
@ -18,7 +21,25 @@ interface Props extends ThemeProps {
function Accounts ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const [filter, setFilter] = useState('');
const [filteredAccount, setFilteredAccount] = useState<AccountWithChildren[]>([]);
const { hierarchy } = useContext(AccountContext);
const networkMap = useMemo(() => getNetworkMap(), []);
useEffect(() => {
setFilteredAccount(
filter
? hierarchy.filter((account) =>
account.name?.toLowerCase().includes(filter) ||
(account.genesisHash && networkMap.get(account.genesisHash)?.toLowerCase().includes(filter))
)
: hierarchy
);
}, [filter, hierarchy, networkMap]);
const _onFilter = useCallback((filter: string) => {
setFilter(filter.toLowerCase());
}, []);
return (
<>
@ -27,12 +48,14 @@ function Accounts ({ className }: Props): React.ReactElement {
: (
<>
<Header
onFilter={_onFilter}
showAdd
showSearch
showSettings
text={t<string>('Accounts')}
/>
<div className={className}>
{hierarchy.map((json, index): React.ReactNode => (
{filteredAccount.map((json, index): React.ReactNode => (
<AccountsTree
{...json}
key={`${index}:${json.address}`}

View File

@ -51,6 +51,7 @@ function AuthManagement ({ className }: Props): React.ReactElement<Props> {
onChange={_onChangeFilter}
placeholder={t<string>('example.com')}
value={filter}
withReset
/>
<div className={className}>
{

View File

@ -1,23 +1,34 @@
// Copyright 2017-2021 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback } from 'react';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useRef } from 'react';
import styled from 'styled-components';
import { ThemeProps } from '../types';
import { Input } from './TextInputs';
interface Props {
interface Props extends ThemeProps {
className?: string;
onChange: (filter: string) => void;
placeholder: string;
value: string;
withReset?: boolean;
}
function InputFilter ({ className, onChange, placeholder, value }: Props) {
function InputFilter ({ className, onChange, placeholder, value, withReset = false }: Props) {
const inputRef: React.RefObject<HTMLInputElement> | null = useRef(null);
const onChangeFilter = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
}, [onChange]);
const onResetFilter = useCallback(() => {
onChange('');
inputRef.current && inputRef.current.select();
}, [onChange]);
return (
<div className={className}>
<Input
@ -26,15 +37,32 @@ function InputFilter ({ className, onChange, placeholder, value }: Props) {
autoFocus
onChange={onChangeFilter}
placeholder={placeholder}
ref={inputRef}
spellCheck={false}
type='text'
value={value}
/>
{withReset && !!value && (
<FontAwesomeIcon
className='resetIcon'
icon={faTimes}
onClick={onResetFilter}
/>
)}
</div>
);
}
export default styled(InputFilter)`
export default styled(InputFilter)(({ theme }: Props) => `
padding-left: 1rem !important;
padding-right: 1rem !important;
`;
position: relative;
.resetIcon {
position: absolute;
right: 28px;
top: 12px;
color: ${theme.iconNeutralColor};
cursor: pointer;
}
`);

View File

@ -3,30 +3,37 @@
import type { ThemeProps } from '../types';
import { faArrowLeft, faCog, faPlusCircle } from '@fortawesome/free-solid-svg-icons';
import { faArrowLeft, faCog, faPlusCircle, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useCallback, useRef, useState } from 'react';
import styled from 'styled-components';
import logo from '../assets/pjs.svg';
import InputFilter from '../components/InputFilter';
import Link from '../components/Link';
import useOutsideClick from '../hooks/useOutsideClick';
import useTranslation from '../hooks/useTranslation';
import MenuAdd from './MenuAdd';
import MenuSettings from './MenuSettings';
interface Props extends ThemeProps {
children?: React.ReactNode;
className?: string;
onFilter?: (filter: string) => void;
showAdd?: boolean;
showBackArrow?: boolean;
showSearch?: boolean;
showSettings?: boolean;
smallMargin?: boolean;
text?: React.ReactNode;
}
function Header ({ children, className = '', showAdd, showBackArrow, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
function Header ({ children, className = '', onFilter, showAdd, showBackArrow, showSearch, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
const [isAddOpen, setShowAdd] = useState(false);
const [isSettingsOpen, setShowSettings] = useState(false);
const [isSearchOpen, setShowSearch] = useState(false);
const [filter, setFilter] = useState('');
const { t } = useTranslation();
const addRef = useRef(null);
const setRef = useRef(null);
@ -48,6 +55,22 @@ function Header ({ children, className = '', showAdd, showBackArrow, showSetting
[]
);
const _onChangeFilter = useCallback((filter: string) => {
setFilter(filter);
onFilter && onFilter(filter);
}, [onFilter]);
const _toggleSearch = useCallback(
(): void => {
if (isSearchOpen) {
_onChangeFilter('');
}
setShowSearch((isSearchOpen) => !isSearchOpen);
},
[_onChangeFilter, isSearchOpen]
);
return (
<div className={`${className} ${smallMargin ? 'smallMargin' : ''}`}>
<div className='container'>
@ -73,6 +96,25 @@ function Header ({ children, className = '', showAdd, showBackArrow, showSetting
}
<span className='logoText'>{text || 'polkadot{.js}'}</span>
</div>
{showSearch && (
<div className={`searchBarWrapper ${isSearchOpen ? 'selected' : ''}`}>
{isSearchOpen && (
<InputFilter
className='inputFilter'
onChange={_onChangeFilter}
placeholder={t<string>('Search by name or network...')}
value={filter}
withReset
/>
)}
<FontAwesomeIcon
className={`searchIcon ${isSearchOpen ? 'selected' : ''}`}
icon={faSearch}
onClick={_toggleSearch}
size='lg'
/>
</div>
)}
<div className='popupMenus'>
{showAdd && (
<div
@ -154,29 +196,48 @@ export default React.memo(styled(Header)(({ theme }: Props) => `
}
}
.popupMenus {
align-self: center;
.popupMenus, .searchBarWrapper {
align-self: center;
}
.popupToggle {
display: inline-block;
vertical-align: middle;
&:last-child {
margin-right: 24px;
}
.searchBarWrapper {
flex: 1;
display: flex;
justify-content: end;
align-items: center;;
.searchIcon {
margin-right: 8px;
&:hover {
cursor: pointer;
}
}
.popupToggle+.popupToggle {
margin-left: 8px;
}
.popupToggle {
display: inline-block;
vertical-align: middle;
&:last-child {
margin-right: 24px;
}
&:hover {
cursor: pointer;
}
}
.inputFilter {
width: 100%
}
.popupToggle+.popupToggle {
margin-left: 8px;
}
}
.plusIcon, .cogIcon {
.plusIcon, .cogIcon, .searchIcon {
color: ${theme.iconNeutralColor};
&.selected {