Reordering in InputAddressMulti (#2337)

* Reorderable InputAddressMulti

* Styling

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Keith Ingram 2020-03-08 04:50:28 -03:00 committed by GitHub
parent 2c070bad4d
commit b43c7dc593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 348 additions and 93 deletions

View File

@ -43,6 +43,7 @@
"@types/chart.js": "^2.9.15",
"@types/file-saver": "^2.0.1",
"@types/i18next": "^13.0.0",
"@types/react-beautiful-dnd": "^12.1.1",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3",

View File

@ -25,6 +25,8 @@ interface SidebarState {
transition: SideBarTransition;
}
export const PORTAL_ID = 'portals';
function sanitize (value?: string): string {
return value?.toLowerCase().replace('-', ' ') || '';
}
@ -98,6 +100,7 @@ function Apps ({ className }: Props): React.ReactElement<Props> {
</Signer>
<ConnectingOverlay />
<AccountsOverlay />
<div id={PORTAL_ID} />
</div>
<WarmUp />
</>

View File

@ -51,11 +51,12 @@ export default function RecoverSetup ({ address, className, onClose }: Props): R
/>
<InputAddressMulti
available={availableHelpers}
availableLabel={t('available social recovery helpers')}
help={t('The addresses that are able to help in recovery. You can select up to {{maxHelpers}} trusted helpers.', { replace: { maxHelpers: MAX_HELPERS } })}
label={t('trusted social recovery helpers')}
onChange={setHelpers}
maxCount={MAX_HELPERS}
value={helpers}
valueLabel={t('trusted social recovery helpers')}
/>
<InputNumber
help={t('The threshold of vouches that is to be reached for the account to be recovered.')}

View File

@ -58,9 +58,9 @@ class Vote extends TxModal<Props, State> {
}
protected isDisabled = (): boolean => {
const { accountId, votes } = this.state;
const { accountId, votes, voteValue } = this.state;
return !accountId || votes.length === 0;
return !accountId || votes.length === 0 || voteValue.lten(0);
}
protected renderTrigger = (): React.ReactNode => {
@ -97,11 +97,12 @@ class Vote extends TxModal<Props, State> {
/>
<InputAddressMulti
available={available}
help={t('Filter available candidates based on name, address or short account index.')}
label={t('filter candidates')}
availableLabel={t('council candidates')}
help={t('Select and order council candidates you wish to vote for.')}
maxCount={MAX_VOTES}
onChange={this.onChangeVotes}
value={votes}
valueLabel={t('my ordered votes')}
/>
</>
);

View File

@ -39,8 +39,6 @@ function Parachain ({ className, basePath, isMine, paraInfoRef, sudoKey }: Props
);
}
console.log(parachain);
if (!id || isNull(parachain)) {
return (
<>

View File

@ -78,12 +78,13 @@ function Nominate ({ className, controllerId, nominees, onClose, next, stakingOv
/>
<InputAddressMulti
available={available}
availableLabel={t('candidate accounts')}
className='medium'
help={t('Filter available candidates based on name, address or short account index.')}
label={t('filter candidates')}
maxCount={MAX_NOMINEES}
onChange={setSelection}
value={selection || []}
valueLabel={t('nominated accounts')}
/>
</Modal.Content>
<Modal.Actions onCancel={onClose}>

View File

@ -23,6 +23,7 @@
"i18next-browser-languagedetector": "^4.0.2",
"i18next-xhr-backend": "^3.2.2",
"react": "^16.13.0",
"react-beautiful-dnd": "^13.0.0",
"react-chartjs-2": "^2.9.0",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.13.0",

View File

@ -15,61 +15,64 @@ import Toggle from './Toggle';
interface Props {
address: string;
className?: string;
isHidden?: boolean;
filter?: string;
noToggle?: boolean;
onChange?: (isChecked: boolean) => void;
value: boolean;
value?: boolean;
}
function AddressToggle ({ address, className, filter, onChange, value }: Props): React.ReactElement<Props> | null {
function getIsFiltered (address: string, filter?: string, info?: DeriveAccountInfo): boolean {
if (!filter || address.includes(filter)) {
return false;
}
const [,, extracted] = getAddressName(address);
const filterLower = filter.toLowerCase();
if (extracted.toLowerCase().includes(filterLower)) {
return false;
}
if (info) {
const { accountId, accountIndex, identity, nickname } = info;
if (identity.display?.toLowerCase().includes(filterLower) || accountId?.toString().includes(filter) || accountIndex?.toString().includes(filter) || nickname?.toLowerCase().includes(filterLower)) {
return false;
}
}
return true;
}
function AddressToggle ({ address, className, filter, isHidden, noToggle, onChange, value }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const info = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [address]);
const [isFiltered, setIsFiltered] = useState(false);
useEffect((): void => {
let isFiltered = true;
if (!filter || address.includes(filter)) {
isFiltered = false;
} else if (info) {
const [,, extracted] = getAddressName(address);
const filterLower = filter.toLowerCase();
if (extracted.toLowerCase().includes(filterLower)) {
isFiltered = false;
} else if (info) {
const { accountId, accountIndex, identity, nickname } = info;
if (identity.display?.toLowerCase().includes(filterLower) || accountId?.toString().includes(filter) || accountIndex?.toString().includes(filter) || nickname?.toLowerCase().includes(filterLower)) {
isFiltered = false;
}
}
}
setIsFiltered(isFiltered);
}, [filter, info, value]);
if (isFiltered) {
return null;
}
setIsFiltered(getIsFiltered(address, filter, info));
}, [address, filter, info]);
const _onClick = (): void => onChange && onChange(!value);
return (
<div
className={`ui--AddressToggle ${className} ${value ? 'isAye' : 'isNay'}`}
className={`ui--AddressToggle ${className} ${(value || noToggle) ? 'isAye' : 'isNay'} ${isHidden || isFiltered ? 'isHidden' : ''}`}
onClick={_onClick}
>
<AddressMini
className='ui--AddressToggle-address'
value={address}
/>
<div className='ui--AddressToggle-toggle'>
<Toggle
label=''
onChange={onChange}
value={value}
/>
</div>
{!noToggle && (
<div className='ui--AddressToggle-toggle'>
<Toggle
label=''
value={value}
/>
</div>
)}
</div>
);
}
@ -96,7 +99,17 @@ export default styled(AddressToggle)`
border-color: #ccc;
}
&.isHidden {
display: none;
}
&.isDragging {
background: white;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
}
&.isAye {
cursor: move;
.ui--AddressToggle-address {
filter: none;
opacity: 1;

View File

@ -18,6 +18,7 @@ interface Props extends BareProps {
defaultValue?: any;
help?: React.ReactNode;
icon?: React.ReactNode;
inputClassName?: string;
isAction?: boolean;
isDisabled?: boolean;
isDisabledError?: boolean;
@ -89,7 +90,7 @@ const isSelectAll = (key: string, isPreKeyDown: boolean): boolean =>
let counter = 0;
export default function Input ({ autoFocus = false, children, className, defaultValue, help, icon, isEditable = false, isAction = false, isDisabled = false, isDisabledError = false, isError = false, isFull = false, isHidden = false, isReadOnly = false, label, labelExtra, max, maxLength, min, name, onBlur, onChange, onEnter, onEscape, onKeyDown, onKeyUp, onPaste, placeholder, style, tabIndex, type = 'text', value, withEllipsis, withLabel }: Props): React.ReactElement<Props> {
export default function Input ({ autoFocus = false, children, className, defaultValue, help, icon, inputClassName, isEditable = false, isAction = false, isDisabled = false, isDisabledError = false, isError = false, isFull = false, isHidden = false, isReadOnly = false, label, labelExtra, max, maxLength, min, name, onBlur, onChange, onEnter, onEscape, onKeyDown, onKeyUp, onPaste, placeholder, style, tabIndex, type = 'text', value, withEllipsis, withLabel }: Props): React.ReactElement<Props> {
const [stateName] = useState(`in_${counter++}_at_${Date.now()}`);
const _onBlur = (): void => {
@ -133,9 +134,12 @@ export default function Input ({ autoFocus = false, children, className, default
action={isAction}
autoFocus={autoFocus}
className={
isEditable
? 'ui--Input edit icon'
: 'ui--Input'
[
isEditable
? 'ui--Input edit icon'
: 'ui--Input',
inputClassName || ''
].join(' ')
}
defaultValue={
isUndefined(value)

View File

@ -2,70 +2,194 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import ReactDOM from 'react-dom';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import styled from 'styled-components';
import { useDebounce } from '@polkadot/react-hooks';
// FIXME :()
import { PORTAL_ID } from '../../apps/src/Apps';
import { useTranslation } from './translate';
import AddressToggle from './AddressToggle';
import Input from './Input';
interface Props {
available: string[];
availableLabel: React.ReactNode;
className?: string;
help: React.ReactNode;
label: React.ReactNode;
maxCount: number;
onChange: (values: string[]) => void;
valueLabel: React.ReactNode;
value: string[];
}
function InputAddressMulti ({ available, className, help, label, maxCount, onChange, value }: Props): React.ReactElement<Props> {
function uniquesOf (list: string[]): string[] {
return [...new Set(list)];
}
function InputAddressMulti ({ available: propsAvailable = [], className, help, maxCount, onChange, availableLabel, valueLabel, value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [_filter, setFilter] = useState<string>('');
const filter = useDebounce(_filter);
const _onClick = (key: string): (isChecked: boolean) => void =>
(isChecked: boolean): void => {
let newValues = value.filter((address): boolean => address !== key);
const available = useMemo(
(): string[] => uniquesOf(propsAvailable),
[propsAvailable]
);
if (isChecked) {
newValues = [key].concat(newValues).slice(0, maxCount);
const isSelected = useMemo(
(): Record<string, boolean> => {
return available.reduce(
(result: Record<string, boolean>, address) => {
return {
...result,
[address]: value.includes(address)
};
},
{}
);
},
[value, available]
);
const onReorder = (source: any, destination: any): void => {
const result = Array.from(value);
const [removed] = result.splice(source.index, 1);
result.splice(destination.index, 0, removed);
onChange(uniquesOf(result));
};
const onSelect = (address: string): () => void => {
return (): void => {
if (isSelected[address] || (maxCount && value.length >= maxCount)) {
return;
}
onChange(newValues);
onChange(
uniquesOf(
[
...value,
address
]
)
);
};
};
const onDeselect = (index: number): () => void => {
return (): void => {
onChange(
uniquesOf([
...value.slice(0, index),
...value.slice(index + 1)
])
);
};
};
const onDragEnd = (result: any): void => {
const { source, destination } = result;
onReorder(source, destination);
};
return (
<div className={`ui--InputAddressMulti ${className}`}>
<Input
autoFocus
className='ui--InputAddressMulti-Input'
help={help}
label={label}
onChange={setFilter}
placeholder={t('partial name, address or account index')}
value={_filter}
/>
<div className='ui--InputAddressMulti-container'>
{available.map((key): React.ReactNode => (
<AddressToggle
address={key}
filter={filter}
key={key}
onChange={_onClick(key)}
value={value.includes(key)}
/>
))}
<div className='ui--InputAddressMulti-column'>
<Input
autoFocus
className='ui--InputAddressMulti-Input label-small'
label={availableLabel}
onChange={setFilter}
placeholder={t('filter by name, address, or account index')}
value={_filter}
/>
<div className='ui--InputAddressMulti-items'>
{available.map((address): React.ReactNode => (
<AddressToggle
address={address}
filter={filter}
isHidden={isSelected[address]}
key={address}
noToggle
onChange={onSelect(address)}
/>
))}
</div>
</div>
<div className='ui--InputAddressMulti-column'>
<Input
autoFocus
className='ui--InputAddressMulti-Input label-small'
help={help}
inputClassName='retain-appearance'
isDisabled
label={valueLabel}
onChange={setFilter}
placeholder={t('drag and drop to reorder')}
value={''}
/>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='available'>
{(provided: any): React.ReactNode => (
<div
className='ui--InputAddressMulti-items'
ref={provided.innerRef}
>
{value.map((address, index): React.ReactNode => (
<Draggable
key={address}
draggableId={address}
index={index}
>
{(provided: any, snapshot: any): React.ReactNode => {
const element = (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<AddressToggle
address={address}
className={snapshot.isDragging ? 'isDragging' : ''}
noToggle
onChange={onDeselect(index)}
/>
</div>
);
if (snapshot.isDragging) {
return ReactDOM.createPortal(element, document.getElementById(PORTAL_ID) as Element);
}
return element;
}}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
}
export default styled(InputAddressMulti)`
border-top-width: 0px;
margin-left: 2rem;
width: calc(100% - 2rem);
display: inline-flex;
justify-content: space-between;
.ui--InputAddressMulti-Input {
.ui.input {
margin-bottom: 0rem;
opacity: 1 !important;
input {
border-bottom-width: 0px;
@ -75,19 +199,21 @@ export default styled(InputAddressMulti)`
}
}
.ui--InputAddressMulti-container {
background: white;
border: 1px solid rgba(34,36,38,.15);
border-top-width: 0px;
border-radius: 0 0 0.25rem 0.25rem;
margin-left: 2rem;
max-height: 15rem;
overflow-y: scroll;
padding: 0.25rem 0.5rem;
.ui--InputAddressMulti-column {
display: flex;
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
min-height: 15rem;
max-height: 15rem;
width: 50%;
padding: 0.25rem 0.5rem;
.ui--AddressToggle {}
.ui--InputAddressMulti-items {
background: white;
border: 1px solid rgba(34,36,38,0.15);
border-top-width: 0;
border-radius: 0 0 0.286rem 0.286rem;
flex: 1;
overflow-y: auto;
}
}
`;

View File

@ -0,0 +1,5 @@
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
declare module 'react-beautiful-dnd';

View File

@ -55,7 +55,7 @@ export default css`
.ui.input {
width: 100%;
&.disabled {
&.disabled:not(.retain-appearance) {
opacity: 1;
input {

111
yarn.lock
View File

@ -3239,6 +3239,7 @@ __metadata:
i18next-browser-languagedetector: ^4.0.2
i18next-xhr-backend: ^3.2.2
react: ^16.13.0
react-beautiful-dnd: ^13.0.0
react-chartjs-2: ^2.9.0
react-copy-to-clipboard: ^5.0.2
react-dom: ^16.13.0
@ -3788,6 +3789,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-beautiful-dnd@npm:^12.1.1":
version: 12.1.1
resolution: "@types/react-beautiful-dnd@npm:12.1.1"
dependencies:
"@types/react": "*"
checksum: 2/976a6b6642f5ff19a9f427b014974be36f4c2a4d67d8e92d17b803245401e08c603180a91f17f736f8019a3560139029290f5757cbae9ff1509767e5cb9ce11b
languageName: node
linkType: hard
"@types/react-copy-to-clipboard@npm:^4.3.0":
version: 4.3.0
resolution: "@types/react-copy-to-clipboard@npm:4.3.0"
@ -7246,6 +7256,15 @@ __metadata:
languageName: node
linkType: hard
"css-box-model@npm:^1.2.0":
version: 1.2.0
resolution: "css-box-model@npm:1.2.0"
dependencies:
tiny-invariant: ^1.0.6
checksum: 2/5ad584c36128969604a150e5447de64979d48f7dd1c8e113c069895c859f20746103786509d282ca74126252e7ba54baa04f1b2484c8d8641591c13bf3f977e9
languageName: node
linkType: hard
"css-color-keywords@npm:^1.0.0":
version: 1.0.0
resolution: "css-color-keywords@npm:1.0.0"
@ -14075,6 +14094,13 @@ __metadata:
languageName: node
linkType: hard
"memoize-one@npm:^5.1.1":
version: 5.1.1
resolution: "memoize-one@npm:5.1.1"
checksum: 2/e7c4a9cceca71334bf09c38818c375a280dc6e8ec03a92701c16199763269ce756b7b0b370f4b464c071045349fb7b3e4a3cc7460a3f46a277f394d8634a6eb5
languageName: node
linkType: hard
"memoizee@npm:^0.4.14":
version: 0.4.14
resolution: "memoizee@npm:0.4.14"
@ -17099,6 +17125,13 @@ __metadata:
languageName: node
linkType: hard
"raf-schd@npm:^4.0.2":
version: 4.0.2
resolution: "raf-schd@npm:4.0.2"
checksum: 2/28ad12b28814dc1b145a518e5ec6011e12aaf4b490cc7548ee0f8f2e26a7ac2168b62281588320a61743f7c4f70966c2db947aae60021f17546cb721a8d45821
languageName: node
linkType: hard
"ramda@npm:^0.26":
version: 0.26.1
resolution: "ramda@npm:0.26.1"
@ -17169,6 +17202,24 @@ __metadata:
languageName: node
linkType: hard
"react-beautiful-dnd@npm:^13.0.0":
version: 13.0.0
resolution: "react-beautiful-dnd@npm:13.0.0"
dependencies:
"@babel/runtime": ^7.8.4
css-box-model: ^1.2.0
memoize-one: ^5.1.1
raf-schd: ^4.0.2
react-redux: ^7.1.1
redux: ^4.0.4
use-memo-one: ^1.1.1
peerDependencies:
react: ^16.8.5
react-dom: ^16.8.5
checksum: 2/0ce5027375308139c04f6fd1c429591fdd17b175903a63bf238d2e0b2c3db1260272870b24ee50262fc7718db914416274740e50f19eaf0e8bbd03a9cf0cd556
languageName: node
linkType: hard
"react-chartjs-2@npm:^2.9.0":
version: 2.9.0
resolution: "react-chartjs-2@npm:2.9.0"
@ -17235,7 +17286,7 @@ __metadata:
languageName: node
linkType: hard
"react-is@npm:^16.12.0, react-is@npm:^16.13.0, react-is@npm:^16.6.0, react-is@npm:^16.6.3, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6":
"react-is@npm:^16.12.0, react-is@npm:^16.13.0, react-is@npm:^16.6.0, react-is@npm:^16.6.3, react-is@npm:^16.7.0, react-is@npm:^16.8.1, react-is@npm:^16.8.6, react-is@npm:^16.9.0":
version: 16.13.0
resolution: "react-is@npm:16.13.0"
checksum: 2/122f38efe00aca3f8c391a90513cb5e2ef2710db164730c9c6125f2f089b5cd413f22669e06606c5d84df08086c9a6b50fae90efa532baa82bc3a7c5c3b8b5da
@ -17291,6 +17342,29 @@ __metadata:
languageName: node
linkType: hard
"react-redux@npm:^7.1.1":
version: 7.2.0
resolution: "react-redux@npm:7.2.0"
dependencies:
"@babel/runtime": ^7.5.5
hoist-non-react-statics: ^3.3.0
loose-envify: ^1.4.0
prop-types: ^15.7.2
react-is: ^16.9.0
peerDependencies:
react: ^16.8.3
react-dom: "*"
react-native: "*"
redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 2/e4beaa6064a5354f65bdeaf22c90fb8bf3df1ec0bcecf7c568321c6eba0cdf4b432236a828cb53801c5815a83d5090f38738f67ed14fb8cfddd21abe5fba317c
languageName: node
linkType: hard
"react-router-dom@npm:^5.1.2":
version: 5.1.2
resolution: "react-router-dom@npm:5.1.2"
@ -17591,6 +17665,16 @@ __metadata:
languageName: node
linkType: hard
"redux@npm:^4.0.4":
version: 4.0.5
resolution: "redux@npm:4.0.5"
dependencies:
loose-envify: ^1.4.0
symbol-observable: ^1.2.0
checksum: 2/112739c2fb83ae2e18335d942a883b3ee14ebada31ff3a7924511a1f38a4278a762f5823491f3eb5083492de601ed76cb2687295c7d5db9a6e098c2542cdc05b
languageName: node
linkType: hard
"regenerate-unicode-properties@npm:^8.1.0":
version: 8.1.0
resolution: "regenerate-unicode-properties@npm:8.1.0"
@ -18165,6 +18249,7 @@ __metadata:
"@types/chart.js": ^2.9.15
"@types/file-saver": ^2.0.1
"@types/i18next": ^13.0.0
"@types/react-beautiful-dnd": ^12.1.1
"@types/react-copy-to-clipboard": ^4.3.0
"@types/react-dom": ^16.9.5
"@types/react-router-dom": ^5.1.3
@ -19672,6 +19757,13 @@ __metadata:
languageName: node
linkType: hard
"symbol-observable@npm:^1.2.0":
version: 1.2.0
resolution: "symbol-observable@npm:1.2.0"
checksum: 2/268834a1d4cba19d40f367e5c2755f612969c8418e43a3be17408e392802a667f8bb542893440d58a080a8ea8da05ea98e27e472b9f4ff6fbda78a21a1a41c53
languageName: node
linkType: hard
"symbol-tree@npm:^3.2.2":
version: 3.2.4
resolution: "symbol-tree@npm:3.2.4"
@ -19963,7 +20055,7 @@ __metadata:
languageName: node
linkType: hard
"tiny-invariant@npm:^1.0.2":
"tiny-invariant@npm:^1.0.2, tiny-invariant@npm:^1.0.6":
version: 1.1.0
resolution: "tiny-invariant@npm:1.1.0"
checksum: 2/64318fbd77c451cfff23b57b9f3aef56594d9cea051a87dc538c9b371f97e8d474eaa2a7cbd60b8aa23f852393152495e8651b197607465fdf9c8ff134043b1b
@ -20815,6 +20907,15 @@ __metadata:
languageName: node
linkType: hard
"use-memo-one@npm:^1.1.1":
version: 1.1.1
resolution: "use-memo-one@npm:1.1.1"
peerDependencies:
react: ^16.8.0
checksum: 2/691a41156a966ec83246b0c684f9ce84dcf9fe1d6441d136d3a2cf681d84f5be510265e1e3a5951b1f2f45a760a3492e3c389392c1e66240d5430150148383df
languageName: node
linkType: hard
"use@npm:^3.1.0":
version: 3.1.1
resolution: "use@npm:3.1.1"
@ -21811,8 +21912,8 @@ __metadata:
linkType: hard
"ws@npm:^7.0.0, ws@npm:^7.1.0":
version: 7.2.1
resolution: "ws@npm:7.2.1"
version: 7.2.2
resolution: "ws@npm:7.2.2"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
@ -21821,7 +21922,7 @@ __metadata:
optional: true
utf-8-validate:
optional: true
checksum: 2/097beba4b2722f097b8bcf92b1da6e81838c389b5e7f0246eb622a1949083fc6868aed0f51bc4c520d2d37f983e45134f903ee98b0881b0d7d51e25a1446753d
checksum: 2/fe66f98e07cdb832d6161edd147e6bdec13cf7e811cd34b11d85fdc7b7c17eaf9498c1656dc4786c9d2207dc1cd86aef0060706af8c756ef86e9b5e9421ef566
languageName: node
linkType: hard