From b43c7dc593cf6f6029fd4fc504c463c091c877ea Mon Sep 17 00:00:00 2001 From: Keith Ingram Date: Sun, 8 Mar 2020 04:50:28 -0300 Subject: [PATCH] Reordering in InputAddressMulti (#2337) * Reorderable InputAddressMulti * Styling Co-authored-by: Jaco Greeff --- package.json | 1 + packages/apps/src/Apps.tsx | 3 + .../page-accounts/src/modals/RecoverSetup.tsx | 3 +- packages/page-council/src/Overview/Vote.tsx | 9 +- .../page-parachains/src/Parachain/index.tsx | 2 - .../src/Actions/Account/Nominate.tsx | 3 +- packages/react-components/package.json | 1 + .../react-components/src/AddressToggle.tsx | 83 ++++--- packages/react-components/src/Input.tsx | 12 +- .../src/InputAddressMulti.tsx | 206 ++++++++++++++---- .../src/react-beautiful-dnd.d.ts | 5 + .../react-components/src/styles/semantic.ts | 2 +- yarn.lock | 111 +++++++++- 13 files changed, 348 insertions(+), 93 deletions(-) create mode 100644 packages/react-components/src/react-beautiful-dnd.d.ts diff --git a/package.json b/package.json index 7a36cfe0ab..99fef15f6e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/apps/src/Apps.tsx b/packages/apps/src/Apps.tsx index fc05197610..8492b7b592 100644 --- a/packages/apps/src/Apps.tsx +++ b/packages/apps/src/Apps.tsx @@ -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 { +
diff --git a/packages/page-accounts/src/modals/RecoverSetup.tsx b/packages/page-accounts/src/modals/RecoverSetup.tsx index 3c6c80bafd..2b07d91ecc 100644 --- a/packages/page-accounts/src/modals/RecoverSetup.tsx +++ b/packages/page-accounts/src/modals/RecoverSetup.tsx @@ -51,11 +51,12 @@ export default function RecoverSetup ({ address, className, onClose }: Props): R /> { } 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 { /> ); diff --git a/packages/page-parachains/src/Parachain/index.tsx b/packages/page-parachains/src/Parachain/index.tsx index 055dcae043..a523043c81 100644 --- a/packages/page-parachains/src/Parachain/index.tsx +++ b/packages/page-parachains/src/Parachain/index.tsx @@ -39,8 +39,6 @@ function Parachain ({ className, basePath, isMine, paraInfoRef, sudoKey }: Props ); } - console.log(parachain); - if (!id || isNull(parachain)) { return ( <> diff --git a/packages/page-staking/src/Actions/Account/Nominate.tsx b/packages/page-staking/src/Actions/Account/Nominate.tsx index c37cdfcbf2..f490cfa98b 100644 --- a/packages/page-staking/src/Actions/Account/Nominate.tsx +++ b/packages/page-staking/src/Actions/Account/Nominate.tsx @@ -78,12 +78,13 @@ function Nominate ({ className, controllerId, nominees, onClose, next, stakingOv /> diff --git a/packages/react-components/package.json b/packages/react-components/package.json index b039609a6f..dcd8005888 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -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", diff --git a/packages/react-components/src/AddressToggle.tsx b/packages/react-components/src/AddressToggle.tsx index f8c7de072e..2794338935 100644 --- a/packages/react-components/src/AddressToggle.tsx +++ b/packages/react-components/src/AddressToggle.tsx @@ -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 | 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 | null { const { api } = useApi(); const info = useCall(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 (
-
- -
+ {!noToggle && ( +
+ +
+ )}
); } @@ -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; diff --git a/packages/react-components/src/Input.tsx b/packages/react-components/src/Input.tsx index b9ebab055c..34925766dc 100644 --- a/packages/react-components/src/Input.tsx +++ b/packages/react-components/src/Input.tsx @@ -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 { +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 { 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) diff --git a/packages/react-components/src/InputAddressMulti.tsx b/packages/react-components/src/InputAddressMulti.tsx index f4e09d26c8..9bed5bc131 100644 --- a/packages/react-components/src/InputAddressMulti.tsx +++ b/packages/react-components/src/InputAddressMulti.tsx @@ -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 { +function uniquesOf (list: string[]): string[] { + return [...new Set(list)]; +} + +function InputAddressMulti ({ available: propsAvailable = [], className, help, maxCount, onChange, availableLabel, valueLabel, value }: Props): React.ReactElement { const { t } = useTranslation(); const [_filter, setFilter] = useState(''); 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 => { + return available.reduce( + (result: Record, 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 (
- -
- {available.map((key): React.ReactNode => ( - - ))} +
+ +
+ {available.map((address): React.ReactNode => ( + + ))} +
+
+
+ + + + {(provided: any): React.ReactNode => ( +
+ {value.map((address, index): React.ReactNode => ( + + {(provided: any, snapshot: any): React.ReactNode => { + const element = ( +
+ +
+ ); + + if (snapshot.isDragging) { + return ReactDOM.createPortal(element, document.getElementById(PORTAL_ID) as Element); + } + + return element; + }} +
+ ))} + {provided.placeholder} +
+ )} +
+
); } 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; + } } `; diff --git a/packages/react-components/src/react-beautiful-dnd.d.ts b/packages/react-components/src/react-beautiful-dnd.d.ts new file mode 100644 index 0000000000..02dc292f59 --- /dev/null +++ b/packages/react-components/src/react-beautiful-dnd.d.ts @@ -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'; diff --git a/packages/react-components/src/styles/semantic.ts b/packages/react-components/src/styles/semantic.ts index 1291278fa1..059233a909 100644 --- a/packages/react-components/src/styles/semantic.ts +++ b/packages/react-components/src/styles/semantic.ts @@ -55,7 +55,7 @@ export default css` .ui.input { width: 100%; - &.disabled { + &.disabled:not(.retain-appearance) { opacity: 1; input { diff --git a/yarn.lock b/yarn.lock index f84d7bfcc3..672ecc7b4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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