diff --git a/.eslintrc b/.eslintrc index f04dfa1b..da0eb9a1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,7 +45,8 @@ ], "react/require-default-props": 0, "react/no-array-index-key": 0, - "react/jsx-props-no-spreading": 0 + "react/jsx-props-no-spreading": 0, + "react/state-in-constructor": 0 }, "env": { "jest/globals": true, diff --git a/scripts/test.js b/scripts/test.js index ff7207c1..76223eff 100644 --- a/scripts/test.js +++ b/scripts/test.js @@ -11,6 +11,7 @@ require('dotenv').config({ silent: true }) const jest = require('jest') const argv = process.argv.slice(2) +argv.push('--runInBand') // Watch unless on CI or in coverage mode if (!process.env.CI && argv.indexOf('--coverage') < 0) { diff --git a/src/components/Footer/index.jsx b/src/components/Footer/index.jsx deleted file mode 100644 index 8ab1c99d..00000000 --- a/src/components/Footer/index.jsx +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import React from 'react' -import Block from '~/components/layout/Block' -import Link from '~/components/layout/Link' -import Paragraph from '~/components/layout/Paragraph' -import { WELCOME_ADDRESS, SAFELIST_ADDRESS } from '~/routes/routes' -import styles from './index.scss' - -const Footer = () => ( - - - Add Safe - - - Safe List - - -) - -export default Footer diff --git a/src/components/Footer/index.scss b/src/components/Footer/index.scss deleted file mode 100644 index 0f3b3eba..00000000 --- a/src/components/Footer/index.scss +++ /dev/null @@ -1,24 +0,0 @@ -.footer { - font-size: $smallFontSize; - display: grid; - grid-template-columns: 100px 100px 1fr; - grid-template-rows: 36px; - justify-items: center; - align-items: center; - border: solid 0.5px $border; - background-color: white; - margin-top: 50px; -} - -@media only screen and (max-width: $(screenXs)px) { - .footer { - grid-template-columns: none; - grid-template-rows: auto auto; - grid-row-gap: $sm; - justify-items: center; - } - - .footer > a { - padding: 0; - } -} \ No newline at end of file diff --git a/src/components/Footer/index.stories.js b/src/components/Footer/index.stories.js deleted file mode 100644 index e0b29e91..00000000 --- a/src/components/Footer/index.stories.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import { storiesOf } from '@storybook/react' -import * as React from 'react' -import styles from '~/components/layout/PageFrame/index.scss' -import Component from './index' - -const FrameDecorator = (story) => ( -
-
- {story()} -
-) - -storiesOf('Components /Footer', module) - .addDecorator(FrameDecorator) - .add('Loaded', () => ) diff --git a/src/components/Header/component/Layout.jsx b/src/components/Header/component/Layout.jsx index fdbc92d1..877647ea 100644 --- a/src/components/Header/component/Layout.jsx +++ b/src/components/Header/component/Layout.jsx @@ -12,8 +12,11 @@ import Col from '~/components/layout/Col' import Img from '~/components/layout/Img' import Row from '~/components/layout/Row' import Spacer from '~/components/Spacer' -import { border, sm, md } from '~/theme/variables' +import { + border, sm, md, headerHeight, +} from '~/theme/variables' import Provider from './Provider' +import SafeListHeader from './SafeListHeader' const logo = require('../assets/gnosis-safe-logo.svg') @@ -32,10 +35,12 @@ const styles = () => ({ left: '4px', }, summary: { - borderBottom: `solid 1px ${border}`, + borderBottom: `solid 2px ${border}`, alignItems: 'center', - height: '53px', + height: headerHeight, + boxShadow: '0 2px 4px 0 rgba(212, 212, 211, 0.59)', backgroundColor: 'white', + zIndex: 1301, }, logo: { padding: `${sm} ${md}`, @@ -55,6 +60,8 @@ const Layout = openHoc(({ + + diff --git a/src/components/Header/component/SafeListHeader/index.jsx b/src/components/Header/component/SafeListHeader/index.jsx new file mode 100644 index 00000000..dafbf3da --- /dev/null +++ b/src/components/Header/component/SafeListHeader/index.jsx @@ -0,0 +1,68 @@ +// @flow +import * as React from 'react' +import { connect } from 'react-redux' +import { makeStyles } from '@material-ui/core/styles' +import IconButton from '@material-ui/core/IconButton' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import ExpandLessIcon from '@material-ui/icons/ExpandLess' +import Paragraph from '~/components/layout/Paragraph' +import Col from '~/components/layout/Col' +import { + xs, sm, md, border, +} from '~/theme/variables' +import { safesCountSelector } from '~/routes/safe/store/selectors' +import { SidebarContext } from '~/components/Sidebar' + +export const TOGGLE_SIDEBAR_BTN_TESTID = 'TOGGLE_SIDEBAR_BTN' + +const useStyles = makeStyles({ + container: { + flexGrow: 0, + padding: `0 ${md}`, + }, + counter: { + background: border, + padding: xs, + borderRadius: '3px', + marginLeft: sm, + lineHeight: 'normal', + }, + icon: { + marginLeft: sm, + }, +}) + +type Props = { + safesCount: number, +} + +const { useContext } = React + +const SafeListHeader = ({ safesCount }: Props) => { + const classes = useStyles() + const { toggleSidebar, isOpen } = useContext(SidebarContext) + + return ( + + Safes + {' '} + + {safesCount} + + + {isOpen ? : } + + + ) +} + +export default connect( + // $FlowFixMe + (state) => ({ safesCount: safesCountSelector(state) }), + null, +)(SafeListHeader) diff --git a/src/components/Sidebar/SafeList/DefaultBadge.jsx b/src/components/Sidebar/SafeList/DefaultBadge.jsx new file mode 100644 index 00000000..467b605d --- /dev/null +++ b/src/components/Sidebar/SafeList/DefaultBadge.jsx @@ -0,0 +1,36 @@ +// @flow +import * as React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Img from '~/components/layout/Img' +import Block from '~/components/layout/Block' +import Paragraph from '~/components/layout/Paragraph' +import { primary, sm } from '~/theme/variables' +import StarIcon from './assets/star.svg' + +const useStyles = makeStyles({ + container: { + background: primary, + padding: '5px', + boxSizing: 'border-box', + width: '73px', + justifyContent: 'space-around', + marginLeft: sm, + color: '#fff', + borderRadius: '3px', + }, +}) + +const DefaultBadge = () => { + const classes = useStyles() + + return ( + + Star Icon + + default + + + ) +} + +export default DefaultBadge diff --git a/src/components/Sidebar/SafeList/assets/star.svg b/src/components/Sidebar/SafeList/assets/star.svg new file mode 100644 index 00000000..3cf6bbd0 --- /dev/null +++ b/src/components/Sidebar/SafeList/assets/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/Sidebar/SafeList/index.jsx b/src/components/Sidebar/SafeList/index.jsx new file mode 100644 index 00000000..8e2f1232 --- /dev/null +++ b/src/components/Sidebar/SafeList/index.jsx @@ -0,0 +1,114 @@ +// @flow +import * as React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import { List } from 'immutable' +import MuiList from '@material-ui/core/List' +import ListItem from '@material-ui/core/ListItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' +import Link from '~/components/layout/Link' +import Hairline from '~/components/layout/Hairline' +import Paragraph from '~/components/layout/Paragraph' +import ButtonLink from '~/components/layout/ButtonLink' +import Identicon from '~/components/Identicon' +import { + mediumFontSize, sm, secondary, primary, +} from '~/theme/variables' +import { shortVersionOf, sameAddress } from '~/logic/wallets/ethAddresses' +import { type Safe } from '~/routes/safe/store/models/safe' +import { SAFELIST_ADDRESS } from '~/routes/routes' +import DefaultBadge from './DefaultBadge' + +export const SIDEBAR_SAFELIST_ROW_TESTID = 'SIDEBAR_SAFELIST_ROW_TESTID' + +type SafeListProps = { + safes: List, + onSafeClick: Function, + setDefaultSafe: Function, + defaultSafe: string, +} + +const useStyles = makeStyles({ + icon: { + marginRight: sm, + }, + list: { + overflow: 'hidden', + overflowY: 'scroll', + padding: 0, + height: '100%', + }, + listItemRoot: { + paddingTop: 0, + paddingBottom: 0, + '&:hover $makeDefaultBtn': { + visibility: 'initial', + }, + '&:focus $makeDefaultBtn': { + visibility: 'initial', + }, + }, + safeName: { + color: secondary, + }, + safeAddress: { + color: primary, + fontSize: mediumFontSize, + }, + makeDefaultBtn: { + padding: 0, + marginLeft: sm, + visibility: 'hidden', + }, +}) + +const SafeList = ({ + safes, onSafeClick, setDefaultSafe, defaultSafe, +}: SafeListProps) => { + const classes = useStyles() + + return ( + + {safes.map((safe) => ( + + + + + + + + + {safe.ethBalance} + {' '} +ETH + + {sameAddress(defaultSafe, safe.address) ? ( + + ) : ( + { + e.preventDefault() + e.stopPropagation() + + setDefaultSafe(safe.address) + }} + > + Make default + + )} + + + + + ))} + + ) +} + +export default SafeList diff --git a/src/components/Sidebar/index.jsx b/src/components/Sidebar/index.jsx new file mode 100644 index 00000000..07f3f9d1 --- /dev/null +++ b/src/components/Sidebar/index.jsx @@ -0,0 +1,142 @@ +// @flow +import * as React from 'react' +import { List } from 'immutable' +import SearchBar from 'material-ui-search-bar' +import { connect } from 'react-redux' +import ClickAwayListener from '@material-ui/core/ClickAwayListener' +import Drawer from '@material-ui/core/Drawer' +import SearchIcon from '@material-ui/icons/Search' +import Divider from '~/components/layout/Divider' +import Button from '~/components/layout/Button' +import Link from '~/components/layout/Link' +import Spacer from '~/components/Spacer' +import Hairline from '~/components/layout/Hairline' +import Row from '~/components/layout/Row' +import { type Safe } from '~/routes/safe/store/models/safe' +import { defaultSafeSelector } from '~/routes/safe/store/selectors' +import setDefaultSafe from '~/routes/safe/store/actions/setDefaultSafe' +import { sortedSafeListSelector } from './selectors' +import useSidebarStyles from './style' +import SafeList from './SafeList' +import { WELCOME_ADDRESS } from '~/routes/routes' + +const { useState, useEffect } = React + +type TSidebarContext = { + isOpen: boolean, + toggleSidebar: Function, +} + +export const SidebarContext = React.createContext({ + isOpen: false, + toggleSidebar: () => {}, +}) + +type SidebarProps = { + children: React.Node, + safes: List, + setDefaultSafeAction: Function, + defaultSafe: string, +} + +const filterBy = (filter: string, safes: List): List => safes.filter( + (safe: Safe) => !filter + || safe.address.toLowerCase().includes(filter.toLowerCase()) + || safe.name.toLowerCase().includes(filter.toLowerCase()), +) + +const Sidebar = ({ + children, safes, setDefaultSafeAction, defaultSafe, +}: SidebarProps) => { + const [isOpen, setIsOpen] = useState(false) + const [filter, setFilter] = useState('') + const classes = useSidebarStyles() + + useEffect(() => { + setTimeout(() => { + setFilter('') + }, 300) + }, [isOpen]) + + const searchClasses = { + input: classes.searchInput, + root: classes.searchRoot, + iconButton: classes.searchIconInput, + searchContainer: classes.searchContainer, + } + + const toggleSidebar = () => { + setIsOpen((prevIsOpen) => !prevIsOpen) + } + + const handleFilterChange = (value: string) => { + setFilter(value) + } + + const handleFilterCancel = () => { + setFilter('') + } + + const handleEsc = (e: SyntheticKeyboardEvent<*>) => { + if (e.keyCode === 27) { + toggleSidebar() + } + } + + const filteredSafes = filterBy(filter, safes) + + return ( + + + +
+ + + } + onChange={handleFilterChange} + onCancelSearch={handleFilterCancel} + /> + + + + + + + + + + + {children} + + ) +} + +export default connect( + // $FlowFixMe + (state) => ({ safes: sortedSafeListSelector(state), defaultSafe: defaultSafeSelector(state) }), + { setDefaultSafeAction: setDefaultSafe }, +)(Sidebar) diff --git a/src/components/Sidebar/selectors.js b/src/components/Sidebar/selectors.js new file mode 100644 index 00000000..1c6e56c3 --- /dev/null +++ b/src/components/Sidebar/selectors.js @@ -0,0 +1,11 @@ +// @flow +import { List } from 'immutable' +import { createSelector, type Selector } from 'reselect' +import { type Safe } from '~/routes/safe/store/models/safe' +import { safesListSelector } from '~/routes/safe/store/selectors' +import { type GlobalState } from '~/store/index' + +export const sortedSafeListSelector: Selector> = createSelector( + safesListSelector, + (safes: List): List => safes.sort((a: Safe, b: Safe) => (a.name > b.name ? 1 : -1)), +) diff --git a/src/components/Sidebar/style.js b/src/components/Sidebar/style.js new file mode 100644 index 00000000..1083a530 --- /dev/null +++ b/src/components/Sidebar/style.js @@ -0,0 +1,59 @@ +// @flow +import { makeStyles } from '@material-ui/core/styles' +import { + xs, mediumFontSize, secondaryText, md, headerHeight, +} from '~/theme/variables' + +const sidebarWidth = '400px' + +const useSidebarStyles = makeStyles({ + sidebar: { + width: sidebarWidth, + }, + sidebarPaper: { + width: sidebarWidth, + }, + headerPlaceholder: { + minHeight: headerHeight, + }, + addSafeBtn: { + fontSize: mediumFontSize, + }, + searchIcon: { + color: secondaryText, + paddingLeft: md, + }, + searchInput: { + backgroundColor: 'transparent', + lineHeight: 'initial', + padding: 0, + '& > input::placeholder': { + letterSpacing: '-0.5px', + fontSize: mediumFontSize, + color: 'black', + }, + '& > input': { + letterSpacing: '-0.5px', + }, + }, + searchContainer: { + width: '180px', + marginLeft: xs, + marginRight: xs, + }, + searchRoot: { + letterSpacing: '-0.5px', + border: 'none', + boxShadow: 'none', + '& > button': { + display: 'none', + }, + }, + searchIconInput: { + '&:hover': { + backgroundColor: 'transparent !important', + }, + }, +}) + +export default useSidebarStyles diff --git a/src/components/layout/Divider/index.js b/src/components/layout/Divider/index.js index 0d81c3f7..3dca7fde 100644 --- a/src/components/layout/Divider/index.js +++ b/src/components/layout/Divider/index.js @@ -4,7 +4,7 @@ import { border } from '~/theme/variables' const style = { height: '100%', - borderRight: `solid 1px ${border}`, + borderRight: `solid 2px ${border}`, } const Divider = () =>
diff --git a/src/components/layout/PageFrame/index.jsx b/src/components/layout/PageFrame/index.jsx index 4b260890..6159ca41 100644 --- a/src/components/layout/PageFrame/index.jsx +++ b/src/components/layout/PageFrame/index.jsx @@ -1,7 +1,7 @@ // @flow -import React from 'react' -import Footer from '~/components/Footer' +import * as React from 'react' import Header from '~/components/Header' +import SidebarProvider from '~/components/Sidebar' import { SharedSnackbarProvider } from '~/components/SharedSnackBar' import styles from './index.scss' @@ -12,9 +12,10 @@ type Props = { const PageFrame = ({ children }: Props) => (
-
- {children} -
+ +
+ {children} +
) diff --git a/src/index.js b/src/index.js index 1d831f27..a7992734 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ import Root from '~/components/Root' import { store } from '~/store' import loadSafesFromStorage from '~/routes/safe/store/actions/loadSafesFromStorage' import loadActiveTokens from '~/logic/tokens/store/actions/loadActiveTokens' +import loadDefaultSafe from '~/routes/safe/store/actions/loadDefaultSafe' if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line @@ -16,5 +17,6 @@ if (process.env.NODE_ENV !== 'production') { store.dispatch(loadActiveTokens()) store.dispatch(loadSafesFromStorage()) +store.dispatch(loadDefaultSafe()) ReactDOM.render(, document.getElementById('root')) diff --git a/src/logic/safe/utils/safeStorage.js b/src/logic/safe/utils/safeStorage.js index 0dc3ce82..da498908 100644 --- a/src/logic/safe/utils/safeStorage.js +++ b/src/logic/safe/utils/safeStorage.js @@ -6,6 +6,7 @@ import { loadFromStorage, saveToStorage, removeFromStorage } from '~/utils/stora export const SAFES_KEY = 'SAFES' export const TX_KEY = 'TX' export const OWNERS_KEY = 'OWNERS' +export const DEFAULT_SAFE_KEY = 'DEFAULT_SAFE' export const getSafeName = async (safeAddress: string) => { const safes = await loadFromStorage(SAFES_KEY) @@ -42,11 +43,26 @@ export const getOwners = async (safeAddress: string): Promise => { + const defaultSafe = await loadFromStorage(DEFAULT_SAFE_KEY) + + return defaultSafe || '' +} + +export const saveDefaultSafe = async (safeAddress: string): Promise => { + try { + await saveToStorage(DEFAULT_SAFE_KEY, safeAddress) + } catch (err) { + // eslint-disable-next-line + console.error('Error saving default safe to storage: ', err) + } +} + export const removeOwners = async (safeAddress: string): Promise => { try { await removeFromStorage(`${OWNERS_KEY}-${safeAddress}`) } catch (err) { // eslint-disable-next-line - console.log('Error removing owners from localstorage') + console.error('Error removing owners from localstorage: ', err) } } diff --git a/src/logic/tokens/utils/tokenHelpers.js b/src/logic/tokens/utils/tokenHelpers.js index 1fac59f9..7723664a 100644 --- a/src/logic/tokens/utils/tokenHelpers.js +++ b/src/logic/tokens/utils/tokenHelpers.js @@ -49,4 +49,4 @@ export const isAddressAToken = async (tokenAddress: string) => { return call !== '0x' } -export const isTokenTransfer = async (data: string, value: number) => data.substring(0, 10) === '0xa9059cbb' && value === 0 +export const isTokenTransfer = async (data: string, value: number) => data && data.substring(0, 10) === '0xa9059cbb' && value === 0 diff --git a/src/logic/wallets/store/reducer/provider.js b/src/logic/wallets/store/reducer/provider.js index 23f719e5..9b494ba3 100644 --- a/src/logic/wallets/store/reducer/provider.js +++ b/src/logic/wallets/store/reducer/provider.js @@ -7,7 +7,7 @@ export const PROVIDER_REDUCER_ID = 'providers' export type State = Provider -export default handleActions( +export default handleActions( { [ADD_PROVIDER]: (state: State, { payload }: ActionType) => makeProvider(payload), }, diff --git a/src/routes/index.js b/src/routes/index.js index 64186ea5..79bd4845 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,6 +1,11 @@ // @flow -import React from 'react' -import { Switch, Redirect, Route } from 'react-router-dom' +import React, { useState, useEffect } from 'react' +import { connect } from 'react-redux' +import { + Switch, Redirect, Route, withRouter, +} from 'react-router-dom' +import { defaultSafeSelector } from '~/routes/safe/store/selectors' +import Loader from '~/components/Loader' import Welcome from './welcome/container' import { SAFELIST_ADDRESS, @@ -13,8 +18,6 @@ import { const Safe = React.lazy(() => import('./safe/container')) -const SafeList = React.lazy(() => import('./safeList/container')) - const Open = React.lazy(() => import('./open/container/Open')) const Opening = React.lazy(() => import('./opening/container')) @@ -23,16 +26,54 @@ const Load = React.lazy(() => import('./load/container/Load')) const SAFE_ADDRESS = `${SAFELIST_ADDRESS}/:${SAFE_PARAM_ADDRESS}` -const Routes = () => ( - - - - - - - - - -) +type RoutesProps = { + defaultSafe?: string, + location: Object, +} -export default Routes +const Routes = ({ defaultSafe, location }: RoutesProps) => { + const [isInitialLoad, setInitialLoad] = useState(true) + + useEffect(() => { + if (location.pathname !== '/') { + setInitialLoad(false) + } + }, []) + + return ( + + { + if (!isInitialLoad) { + return + } + + if (typeof defaultSafe === 'undefined') { + return + } + + setInitialLoad(false) + if (defaultSafe) { + return + } + + return + }} + /> + + + + + + + + ) +} + +export default connect( + // $FlowFixMe + (state) => ({ defaultSafe: defaultSafeSelector(state) }), + null, +)(withRouter(Routes)) diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx index 3c8a6cd6..86f0268e 100644 --- a/src/routes/load/components/DetailsForm/index.jsx +++ b/src/routes/load/components/DetailsForm/index.jsx @@ -50,6 +50,7 @@ export const SAFE_MASTERCOPY_ERROR = 'Mastercopy used by this safe is not the sa export const safeFieldsValidation = async (values: Object) => { const errors = {} const web3 = getWeb3() + const safeAddress = values[FIELD_LOAD_ADDRESS] if (!safeAddress || mustBeEthereumAddress(safeAddress) !== undefined) { return errors diff --git a/src/routes/load/container/Load.jsx b/src/routes/load/container/Load.jsx index d3065f04..86d3b6b3 100644 --- a/src/routes/load/container/Load.jsx +++ b/src/routes/load/container/Load.jsx @@ -24,6 +24,7 @@ export const loadSafe = async ( ) => { const safeProps = await buildSafe(safeAddress, safeName) safeProps.owners = owners + await addSafe(safeProps) const storedSafes = (await loadFromStorage(SAFES_KEY)) || {} diff --git a/src/routes/load/container/actions.js b/src/routes/load/container/actions.js index ef7f6914..fd88b01d 100644 --- a/src/routes/load/container/actions.js +++ b/src/routes/load/container/actions.js @@ -1,5 +1,5 @@ // @flow -import { addSafe } from '~/routes/safe/store/actions/addSafe' +import addSafe from '~/routes/safe/store/actions/addSafe' export type Actions = { addSafe: Function, diff --git a/src/routes/open/components/Layout.jsx b/src/routes/open/components/Layout.jsx index 97cd710e..75fe9fba 100644 --- a/src/routes/open/components/Layout.jsx +++ b/src/routes/open/components/Layout.jsx @@ -7,7 +7,7 @@ import Block from '~/components/layout/Block' import Heading from '~/components/layout/Heading' import Row from '~/components/layout/Row' import Review from '~/routes/open/components/ReviewInformation' -import SafeNameField, { safeNameValidation } from '~/routes/open/components/SafeNameForm' +import SafeNameField from '~/routes/open/components/SafeNameForm' import SafeOwnersFields from '~/routes/open/components/SafeOwnersConfirmationsForm' import { getOwnerNameBy, getOwnerAddressBy, FIELD_CONFIRMATIONS } from '~/routes/open/components/fields' import { history } from '~/store' diff --git a/src/routes/open/container/Open.jsx b/src/routes/open/container/Open.jsx index 5f9b6c13..3d8f9734 100644 --- a/src/routes/open/container/Open.jsx +++ b/src/routes/open/container/Open.jsx @@ -3,8 +3,9 @@ import * as React from 'react' import { connect } from 'react-redux' import Page from '~/components/layout/Page' import { - getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom, + getAccountsFrom, getThresholdFrom, getNamesFrom, getSafeNameFrom, getOwnersFrom, } from '~/routes/open/utils/safeDataExtractor' +import { buildSafe } from '~/routes/safe/store/actions/fetchSafe' import { getGnosisSafeInstanceAt, deploySafeContract, initContracts } from '~/logic/contracts/safeContracts' import { checkReceiptStatus } from '~/logic/wallets/ethTransactions' import { history } from '~/store' @@ -24,18 +25,23 @@ export type OpenState = { } export const createSafe = async (values: Object, userAccount: string, addSafe: AddSafe): Promise => { - const accounts = getAccountsFrom(values) + const ownerAddresses = getAccountsFrom(values) const numConfirmations = getThresholdFrom(values) const name = getSafeNameFrom(values) - const owners = getNamesFrom(values) + const ownersNames = getNamesFrom(values) - const safe = await deploySafeContract(accounts, numConfirmations, userAccount) + await initContracts() + + const safe = await deploySafeContract(ownerAddresses, numConfirmations, userAccount) await checkReceiptStatus(safe.tx) const safeAddress = safe.logs[0].args.proxy const safeContract = await getGnosisSafeInstanceAt(safeAddress) + const safeProps = await buildSafe(safeAddress, name) + const owners = getOwnersFrom(ownersNames, ownerAddresses.sort()) + safeProps.owners = owners - addSafe(name, safeContract.address, numConfirmations, owners, accounts) + addSafe(safeProps) if (stillInOpeningView()) { const url = { diff --git a/src/routes/open/utils/safeDataExtractor.js b/src/routes/open/utils/safeDataExtractor.js index b20f070f..b555b5fb 100644 --- a/src/routes/open/utils/safeDataExtractor.js +++ b/src/routes/open/utils/safeDataExtractor.js @@ -1,5 +1,6 @@ // @flow -import { makeOwner } from '~/routes/safe/store/models/owner' +import { List } from 'immutable' +import { makeOwner, type Owner } from '~/routes/safe/store/models/owner' export const getAccountsFrom = (values: Object): string[] => { const accounts = Object.keys(values) @@ -17,10 +18,10 @@ export const getNamesFrom = (values: Object): string[] => { return accounts.map((account) => values[account]).slice(0, values.owners) } -export const getOwnersFrom = (names: string[], addresses: string[]): Array => { +export const getOwnersFrom = (names: string[], addresses: string[]): List => { const owners = names.map((name: string, index: number) => makeOwner({ name, address: addresses[index] })) - return owners + return List(owners) } export const getThresholdFrom = (values: Object): number => Number(values.confirmations) diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx index 357a98b2..9e5d80b4 100644 --- a/src/routes/safe/components/Layout.jsx +++ b/src/routes/safe/components/Layout.jsx @@ -137,7 +137,6 @@ class Layout extends React.Component { className={classes.send} onClick={() => showSendFunds('Ether')} disabled={!granted} - testId="balance-send-btn" > Send diff --git a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx index f44cf32a..95630467 100644 --- a/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/ExpandedTx/ApproveTxModal/index.jsx @@ -60,7 +60,7 @@ const ApproveTxModal = ({ const { title, description } = getModalTitleAndDescription(thresholdReached) const oneConfirmationLeft = tx.confirmations.size + 1 === threshold - const handleExecuteCheckbox = () => setApproveAndExecute(prevApproveAndExecute => !prevApproveAndExecute) + const handleExecuteCheckbox = () => setApproveAndExecute((prevApproveAndExecute) => !prevApproveAndExecute) return ( diff --git a/src/routes/safe/container/index.jsx b/src/routes/safe/container/index.jsx index ff8b4317..fa6b170e 100644 --- a/src/routes/safe/container/index.jsx +++ b/src/routes/safe/container/index.jsx @@ -3,6 +3,7 @@ import * as React from 'react' import { connect } from 'react-redux' import Page from '~/components/layout/Page' import Layout from '~/routes/safe/components/Layout' +import { type Token } from '~/logic/tokens/store/model/token' import selector, { type SelectorProps } from './selector' import actions, { type Actions } from './actions' diff --git a/src/routes/safe/container/selector.js b/src/routes/safe/container/selector.js index e0fa4ff4..efaee36b 100644 --- a/src/routes/safe/container/selector.js +++ b/src/routes/safe/container/selector.js @@ -59,7 +59,7 @@ export const grantedSelector: Selector = crea return false } - const owners: List = safe.get('owners') + const { owners }: List = safe if (!owners) { return false } diff --git a/src/routes/safe/store/actions/addSafe.js b/src/routes/safe/store/actions/addSafe.js index a3395ac8..5a6bbf1b 100644 --- a/src/routes/safe/store/actions/addSafe.js +++ b/src/routes/safe/store/actions/addSafe.js @@ -1,10 +1,12 @@ // @flow import { List } from 'immutable' import { createAction } from 'redux-actions' -import type { Dispatch as ReduxDispatch } from 'redux' +import type { Dispatch as ReduxDispatch, GetState } from 'redux' import { type GlobalState } from '~/store' -import SafeRecord, { type Safe } from '~/routes/safe/store/models/safe' -import { makeOwner, type Owner } from '~/routes/safe/store/models/owner' +import { safesListSelector } from '~/routes/safe/store/selectors' +import { type Safe } from '~/routes/safe/store/models/safe' +import { makeOwner } from '~/routes/safe/store/models/owner' +import setDefaultSafe from '~/routes/safe/store/actions/setDefaultSafe' export const ADD_SAFE = 'ADD_SAFE' @@ -22,19 +24,18 @@ export const addSafe = createAction(ADD_SAFE, (s safe, })) -const saveSafe = (name: string, address: string, threshold: number, ownersName: string[], ownersAddress: string[]) => ( +const saveSafe = (safe: Safe) => ( dispatch: ReduxDispatch, + getState: GetState, ) => { - const owners: List = buildOwnersFrom(ownersName, ownersAddress) - - const safe: Safe = SafeRecord({ - name, - address, - threshold, - owners, - }) + const state = getState() + const safeList = safesListSelector(state) dispatch(addSafe(safe)) + + if (safeList.size === 0) { + dispatch(setDefaultSafe(safe.address)) + } } export default saveSafe diff --git a/src/routes/safe/store/actions/fetchSafe.js b/src/routes/safe/store/actions/fetchSafe.js index 83fd2774..f49577a6 100644 --- a/src/routes/safe/store/actions/fetchSafe.js +++ b/src/routes/safe/store/actions/fetchSafe.js @@ -4,11 +4,10 @@ import { List, Map } from 'immutable' import { type GlobalState } from '~/store/index' import { makeOwner } from '~/routes/safe/store/models/owner' import type { SafeProps } from '~/routes/safe/store/models/safe' -import { addSafe } from '~/routes/safe/store/actions/addSafe' +import addSafe from '~/routes/safe/store/actions/addSafe' import { getOwners, getSafeName } from '~/logic/safe/utils' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { getBalanceInEtherOf } from '~/logic/wallets/getWeb3' -import updateSafe from '~/routes/safe/store/actions/updateSafe' const buildOwnersFrom = ( safeOwners: string[], @@ -36,16 +35,12 @@ export const buildSafe = async (safeAddress: string, safeName: string) => { return safe } -export default (safeAddress: string, update: boolean = false) => async (dispatch: ReduxDispatch) => { +export default (safeAddress: string) => async (dispatch: ReduxDispatch) => { try { const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE' const safeProps: SafeProps = await buildSafe(safeAddress, safeName) - if (update) { - dispatch(updateSafe(safeProps)) - } else { - dispatch(addSafe(safeProps)) - } + dispatch(addSafe(safeProps)) } catch (err) { // eslint-disable-next-line console.error('Error while updating safe information: ', err) diff --git a/src/routes/safe/store/actions/loadDefaultSafe.js b/src/routes/safe/store/actions/loadDefaultSafe.js new file mode 100644 index 00000000..71d4cc32 --- /dev/null +++ b/src/routes/safe/store/actions/loadDefaultSafe.js @@ -0,0 +1,18 @@ +// @flow +import type { Dispatch as ReduxDispatch } from 'redux' +import { type GlobalState } from '~/store/index' +import { getDefaultSafe } from '~/logic/safe/utils' +import setDefaultSafe from './setDefaultSafe' + +const loadDefaultSafe = () => async (dispatch: ReduxDispatch) => { + try { + const defaultSafe: string = await getDefaultSafe() + + dispatch(setDefaultSafe(defaultSafe)) + } catch (err) { + // eslint-disable-next-line + console.error('Error while getting defautl safe from storage:', err) + } +} + +export default loadDefaultSafe diff --git a/src/routes/safe/store/actions/setDefaultSafe.js b/src/routes/safe/store/actions/setDefaultSafe.js new file mode 100644 index 00000000..6987a0eb --- /dev/null +++ b/src/routes/safe/store/actions/setDefaultSafe.js @@ -0,0 +1,8 @@ +// @flow +import { createAction } from 'redux-actions' + +export const SET_DEFAULT_SAFE = 'SET_DEFAULT_SAFE' + +const setDefaultSafe = createAction(SET_DEFAULT_SAFE) + +export default setDefaultSafe diff --git a/src/routes/safe/store/middleware/safeStorage.js b/src/routes/safe/store/middleware/safeStorage.js index 3a3e8766..d52bd317 100644 --- a/src/routes/safe/store/middleware/safeStorage.js +++ b/src/routes/safe/store/middleware/safeStorage.js @@ -9,14 +9,17 @@ import { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner' import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner' import { EDIT_SAFE_OWNER } from '~/routes/safe/store/actions/editSafeOwner' import { type GlobalState } from '~/store/' -import { saveSafes, setOwners, removeOwners } from '~/logic/safe/utils' -import { safesMapSelector } from '~/routes/safeList/store/selectors' -import { getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors' +import { + saveSafes, setOwners, removeOwners, saveDefaultSafe, +} from '~/logic/safe/utils' +import { safesMapSelector, getActiveTokensAddressesForAllSafes } from '~/routes/safe/store/selectors' + import { tokensSelector } from '~/logic/tokens/store/selectors' import type { Token } from '~/logic/tokens/store/model/token' import { makeOwner } from '~/routes/safe/store/models/owner' import { saveActiveTokens } from '~/logic/tokens/utils/tokensStorage' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes' +import { SET_DEFAULT_SAFE } from '~/routes/safe/store/actions/setDefaultSafe' const watchedActions = [ ADD_SAFE, @@ -27,6 +30,7 @@ const watchedActions = [ REPLACE_SAFE_OWNER, EDIT_SAFE_OWNER, ACTIVATE_TOKEN_FOR_ALL_SAFES, + SET_DEFAULT_SAFE, ] const recalculateActiveTokens = (state: GlobalState): void => { @@ -109,6 +113,12 @@ const safeStorageMware = (store: Store) => (next: Function) => asyn setOwners(safeAddress, owners.update(ownerToUpdateIndex, (owner) => owner.set('name', ownerName))) break } + case SET_DEFAULT_SAFE: { + if (action.payload) { + saveDefaultSafe(action.payload) + break + } + } default: break } diff --git a/src/routes/safe/store/reducer/safe.js b/src/routes/safe/store/reducer/safe.js index bc942637..09dfd5d8 100644 --- a/src/routes/safe/store/reducer/safe.js +++ b/src/routes/safe/store/reducer/safe.js @@ -2,11 +2,9 @@ import { Map, List } from 'immutable' import { handleActions, type ActionType } from 'redux-actions' import { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe' -import SafeRecord, { type Safe, type SafeProps } from '~/routes/safe/store/models/safe' +import SafeRecord, { type SafeProps } from '~/routes/safe/store/models/safe' import TokenBalance from '~/routes/safe/store/models/tokenBalance' import { makeOwner, type OwnerProps } from '~/routes/safe/store/models/owner' -import { loadFromStorage } from '~/utils/storage' -import { SAFES_KEY } from '~/logic/safe/utils' import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe' import { ACTIVATE_TOKEN_FOR_ALL_SAFES } from '~/routes/safe/store/actions/activateTokenForAllSafes' import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe' @@ -14,10 +12,11 @@ import { ADD_SAFE_OWNER } from '~/routes/safe/store/actions/addSafeOwner' import { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner' import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner' import { EDIT_SAFE_OWNER } from '~/routes/safe/store/actions/editSafeOwner' +import { SET_DEFAULT_SAFE } from '~/routes/safe/store/actions/setDefaultSafe' export const SAFE_REDUCER_ID = 'safes' -export type State = Map +export type SafeReducerState = Map export const buildSafe = (storedSafe: SafeProps) => { const names = storedSafe.owners.map((owner: OwnerProps) => owner.name) @@ -36,40 +35,15 @@ export const buildSafe = (storedSafe: SafeProps) => { return safe } -const buildSafesFrom = (loadedSafes: Object): Map => { - const safes: Map = Map() - - const keys = Object.keys(loadedSafes) - try { - const safeRecords = keys.map((address: string) => buildSafe(loadedSafes[address])) - - return safes.withMutations(async (map) => { - safeRecords.forEach((safe: SafeProps) => map.set(safe.address, safe)) - }) - } catch (err) { - // eslint-disable-next-line - console.log('Error while fetching safes information') - - return Map() - } -} - -export const safesInitialState = async (): Promise => { - const storedSafes = await loadFromStorage(SAFES_KEY) - const safes = storedSafes ? buildSafesFrom(storedSafes) : Map() - - return safes -} - -export default handleActions( +export default handleActions( { - [UPDATE_SAFE]: (state: State, action: ActionType): State => { + [UPDATE_SAFE]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const safe = action.payload const safeAddress = safe.address - return state.update(safeAddress, (prevSafe) => prevSafe.merge(safe)) + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge(safe)) }, - [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: State, action: ActionType): State => { + [ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const tokenAddress = action.payload const newState = state.withMutations((map) => { @@ -77,59 +51,59 @@ export default handleActions( const safeActiveTokens = map.getIn([safeAddress, 'activeTokens']) const activeTokens = safeActiveTokens.push(tokenAddress) - map.update(safeAddress, (prevSafe) => prevSafe.merge({ activeTokens })) + map.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ activeTokens })) }) }) return newState }, - [ADD_SAFE]: (state: State, action: ActionType): State => { + [ADD_SAFE]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const { safe }: { safe: SafeProps } = action.payload // if you add a new safe it needs to be set as a record // in case of update it shouldn't, because a record would be initialized // with initial props and it would overwrite existing ones - if (state.has(safe.address)) { - return state.update(safe.address, (prevSafe) => prevSafe.merge(safe)) + if (state.hasIn(['safes', safe.address])) { + return state.updateIn(['safes', safe.address], (prevSafe) => prevSafe.merge(safe)) } - return state.set(safe.address, SafeRecord(safe)) + return state.setIn(['safes', safe.address], SafeRecord(safe)) }, - [REMOVE_SAFE]: (state: State, action: ActionType): State => { + [REMOVE_SAFE]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const safeAddress = action.payload - return state.delete(safeAddress) + return state.deleteIn(['safes', safeAddress]) }, - [ADD_SAFE_OWNER]: (state: State, action: ActionType): State => { + [ADD_SAFE_OWNER]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const { safeAddress, ownerName, ownerAddress } = action.payload - return state.update(safeAddress, (prevSafe) => prevSafe.merge({ + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ owners: prevSafe.owners.push(makeOwner({ address: ownerAddress, name: ownerName })), })) }, - [REMOVE_SAFE_OWNER]: (state: State, action: ActionType): State => { + [REMOVE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const { safeAddress, ownerAddress } = action.payload - return state.update(safeAddress, (prevSafe) => prevSafe.merge({ + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ owners: prevSafe.owners.filter((o) => o.address.toLowerCase() !== ownerAddress.toLowerCase()), })) }, - [REPLACE_SAFE_OWNER]: (state: State, action: ActionType): State => { + [REPLACE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const { safeAddress, oldOwnerAddress, ownerName, ownerAddress, } = action.payload - return state.update(safeAddress, (prevSafe) => prevSafe.merge({ + return state.updateIn(['safes', safeAddress], (prevSafe) => prevSafe.merge({ owners: prevSafe.owners .filter((o) => o.address.toLowerCase() !== oldOwnerAddress.toLowerCase()) .push(makeOwner({ address: ownerAddress, name: ownerName })), })) }, - [EDIT_SAFE_OWNER]: (state: State, action: ActionType): State => { + [EDIT_SAFE_OWNER]: (state: SafeReducerState, action: ActionType): SafeReducerState => { const { safeAddress, ownerAddress, ownerName } = action.payload - return state.update(safeAddress, (prevSafe) => { + return state.updateIn(['safes', safeAddress], (prevSafe) => { const ownerToUpdateIndex = prevSafe.owners.findIndex( (o) => o.address.toLowerCase() === ownerAddress.toLowerCase(), ) @@ -137,6 +111,11 @@ export default handleActions( return prevSafe.merge({ owners: updatedOwners }) }) }, + [SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType): SafeReducerState => state.set('defaultSafe', action.payload), }, - Map(), + Map({ + // $FlowFixMe + defaultSafe: undefined, + safes: Map(), + }), ) diff --git a/src/routes/safe/store/selectors/index.js b/src/routes/safe/store/selectors/index.js index f194061c..46574d4b 100644 --- a/src/routes/safe/store/selectors/index.js +++ b/src/routes/safe/store/selectors/index.js @@ -5,11 +5,10 @@ import { createSelector, createStructuredSelector, type Selector } from 'reselec import { type GlobalState } from '~/store/index' import { SAFE_PARAM_ADDRESS } from '~/routes/routes' import { type Safe } from '~/routes/safe/store/models/safe' -import { safesMapSelector } from '~/routes/safeList/store/selectors' import { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions' import { type Transaction } from '~/routes/safe/store/models/transaction' import { type Confirmation } from '~/routes/safe/store/models/confirmation' -import { safesListSelector } from '~/routes/safeList/store/selectors/' +import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' export type RouterProps = { match: Match, @@ -23,6 +22,25 @@ type TransactionProps = { transaction: Transaction, } +const safesStateSelector = (state: GlobalState): Map => state[SAFE_REDUCER_ID] + +export const safesMapSelector = (state: GlobalState): Map => state[SAFE_REDUCER_ID].get('safes') + +export const safesListSelector: Selector> = createSelector( + safesMapSelector, + (safes: Map): List => safes.toList(), +) + +export const safesCountSelector: Selector = createSelector( + safesMapSelector, + (safes: Map): number => safes.size, +) + +export const defaultSafeSelector: Selector = createSelector( + safesStateSelector, + (safeState: Map): string => safeState.get('defaultSafe'), +) + const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID] const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction diff --git a/src/routes/safe/test/Safe.threshold.test.js b/src/routes/safe/test/Safe.threshold.test.js deleted file mode 100644 index 33632e2c..00000000 --- a/src/routes/safe/test/Safe.threshold.test.js +++ /dev/null @@ -1,130 +0,0 @@ -// @flow -/* -import { aNewStore } from '~/store' -import { aDeployedSafe } from '~/routes/safe/store/test/builder/deployedSafe.builder' -import { getWeb3 } from '~/logic/wallets/getWeb3' -import { sleep } from '~/utils/timer' -import { type Match } from 'react-router-dom' -import { promisify } from '~/utils/promisify' -import { processTransaction } from '~/routes/safe/component/Transactions/processTransactions' -import { - confirmationsTransactionSelector, - safeSelector, - safeTransactionsSelector -} from '~/routes/safe/store/selectors' -import { getTransactionFromReduxStore } from '~/routes/safe/test/testMultisig' -import { buildMathPropsFrom } from '~/test/utils/buildReactRouterProps' -import { createTransaction } from '~/wallets/createTransactions' -import { getGnosisSafeInstanceAt } from '~/wallets/safeContracts' -import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions' -*/ -describe('React DOM TESTS > Change threshold', () => { - it('should update the threshold directly if safe has 1 threshold', async () => {}) -}) - -/* - // GIVEN - const numOwners = 2 - const threshold = 1 - const store = aNewStore() - const address = await aDeployedSafe(store, 10, threshold, numOwners) - const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb)) - const match: Match = buildMathPropsFrom(address) - const safe = safeSelector(store.getState(), { match }) - if (!safe) throw new Error() - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - - // WHEN - const nonce = Date.now() - const data = gnosisSafe.contract.changeThreshold.getData(2) - await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data) - await sleep(1500) - await store.dispatch(fetchTransactions()) - - // THEN - const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address }) - expect(transactions.count()).toBe(1) - - const thresholdTx = transactions.get(0) - if (!thresholdTx) throw new Error() - expect(thresholdTx.get('tx')).not.toBe(null) - expect(thresholdTx.get('tx')).not.toBe(undefined) - expect(thresholdTx.get('tx')).not.toBe('') - - const safeThreshold = await gnosisSafe.getThreshold() - expect(Number(safeThreshold)).toEqual(2) - }) - - const changeThreshold = async (store, safeAddress, executor) => { - const tx = getTransactionFromReduxStore(store, safeAddress) - if (!tx) throw new Error() - const confirmed = confirmationsTransactionSelector(store.getState(), { transaction: tx }) - const data = tx.get('data') - expect(data).not.toBe(null) - expect(data).not.toBe(undefined) - expect(data).not.toBe('') - await processTransaction(safeAddress, tx, confirmed, executor) - await sleep(1800) - } - - it('should wait for confirmation to update threshold when safe has 1+ threshold', async () => { - // GIVEN - const numOwners = 3 - const threshold = 2 - const store = aNewStore() - const address = await aDeployedSafe(store, 10, threshold, numOwners) - const accounts = await promisify(cb => getWeb3().eth.getAccounts(cb)) - const match: Match = buildMathPropsFrom(address) - const safe = safeSelector(store.getState(), { match }) - if (!safe) throw new Error() - const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress) - - // WHEN - const nonce = Date.now() - const data = gnosisSafe.contract.changeThreshold.getData(3) - await createTransaction(safe, "Change Safe's threshold", address, 0, nonce, accounts[0], data) - await sleep(1500) - await store.dispatch(fetchTransactions()) - - let transactions = safeTransactionsSelector(store.getState(), { safeAddress: address }) - if (!transactions) throw new Error() - expect(transactions.count()).toBe(1) - - let thresholdTx = transactions.get(0) - if (!thresholdTx) throw new Error() - expect(thresholdTx.get('tx')).toBe('') - let firstOwnerConfirmation = thresholdTx.get('confirmations').get(0) - if (!firstOwnerConfirmation) throw new Error() - expect(firstOwnerConfirmation.get('status')).toBe(true) - let secondOwnerConfirmation = thresholdTx.get('confirmations').get(1) - if (!secondOwnerConfirmation) throw new Error() - expect(secondOwnerConfirmation.get('status')).toBe(false) - - let safeThreshold = await gnosisSafe.getThreshold() - expect(Number(safeThreshold)).toEqual(2) - - // THEN - await changeThreshold(store, address, accounts[1]) - safeThreshold = await gnosisSafe.getThreshold() - expect(Number(safeThreshold)).toEqual(3) - - await store.dispatch(fetchTransactions()) - sleep(1200) - transactions = safeTransactionsSelector(store.getState(), { safeAddress: address }) - expect(transactions.count()).toBe(1) - - thresholdTx = transactions.get(0) - if (!thresholdTx) throw new Error() - expect(thresholdTx.get('tx')).not.toBe(undefined) - expect(thresholdTx.get('tx')).not.toBe(null) - expect(thresholdTx.get('tx')).not.toBe('') - - firstOwnerConfirmation = thresholdTx.get('confirmations').get(0) - if (!firstOwnerConfirmation) throw new Error() - expect(firstOwnerConfirmation.get('status')).toBe(true) - secondOwnerConfirmation = thresholdTx.get('confirmations').get(1) - if (!secondOwnerConfirmation) throw new Error() - expect(secondOwnerConfirmation.get('status')).toBe(true) - }) -}) -*/ diff --git a/src/routes/safe/test/testMultisig.js b/src/routes/safe/test/testMultisig.js deleted file mode 100644 index 2f9f1655..00000000 --- a/src/routes/safe/test/testMultisig.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index' -import { type GlobalState } from '~/store/index' - -export const getTransactionFromReduxStore = (store: Store, address: string, index: number = 0) => { - const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address }) - - return transactions.get(index) -} diff --git a/src/routes/safeList/components/Layout.jsx b/src/routes/safeList/components/Layout.jsx deleted file mode 100644 index a7dde521..00000000 --- a/src/routes/safeList/components/Layout.jsx +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import { List } from 'immutable' -import * as React from 'react' -import NoSafe from '~/components/NoSafe' -import { type Safe } from '~/routes/safe/store/models/safe' -import SafeTable from '~/routes/safeList/components/SafeTable' - -type Props = { - safes: List, - provider: string, -} - -const SafeList = ({ safes, provider }: Props) => { - const safesAvailable = safes && safes.count() > 0 - - return ( - <> - { safesAvailable - ? - : } - - ) -} - -export default SafeList diff --git a/src/routes/safeList/components/Layout.stories.js b/src/routes/safeList/components/Layout.stories.js deleted file mode 100644 index 391b8de6..00000000 --- a/src/routes/safeList/components/Layout.stories.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { storiesOf } from '@storybook/react' -import { List } from 'immutable' -import * as React from 'react' -import styles from '~/components/layout/PageFrame/index.scss' -import Component from './Layout' - -const FrameDecorator = (story) =>
{story()}
- -storiesOf('Routes /safes', module) - .addDecorator(FrameDecorator) - .add('Safe List whithout safes and connected', () => ) - .add('Safe List whithout safes and NOT connected', () => ) diff --git a/src/routes/safeList/components/SafeTable.jsx b/src/routes/safeList/components/SafeTable.jsx deleted file mode 100644 index 78dc14d2..00000000 --- a/src/routes/safeList/components/SafeTable.jsx +++ /dev/null @@ -1,44 +0,0 @@ -// @flow -import { List } from 'immutable' -import * as React from 'react' -import Button from '~/components/layout/Button' -import Link from '~/components/layout/Link' -import Table, { - TableBody, TableCell, TableHead, TableRow, -} from '~/components/layout/Table' -import { type Safe } from '~/routes/safe/store/models/safe' -import { SAFELIST_ADDRESS } from '~/routes/routes' - -type Props = { - safes: List -} -const SafeTable = ({ safes }: Props) => ( - - - - Open - Name - Deployed Address - Confirmations - Number of owners - - - - {safes.map(safe => ( - - - - - - - {safe.get('name')} - {safe.get('address')} - {safe.get('threshold')} - {safe.get('owners').count()} - - ))} - -
-) - -export default SafeTable diff --git a/src/routes/safeList/container/index.jsx b/src/routes/safeList/container/index.jsx deleted file mode 100644 index 6eaf1bb7..00000000 --- a/src/routes/safeList/container/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { List } from 'immutable' -import * as React from 'react' -import { connect } from 'react-redux' -import Page from '~/components/layout/Page' -import { type Safe } from '~/routes/safe/store/models/safe' -import Layout from '../components/Layout' -import selector from './selector' - -type Props = { - safes: List, - provider: string, -} - -const SafeList = ({ safes, provider }: Props) => ( - - - -) - -export default connect<*, *, *, *>(selector)(SafeList) diff --git a/src/routes/safeList/container/selector.js b/src/routes/safeList/container/selector.js deleted file mode 100644 index 9a301e0e..00000000 --- a/src/routes/safeList/container/selector.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import { createStructuredSelector } from 'reselect' -import { safesListSelector } from '~/routes/safeList/store/selectors' -import { providerNameSelector } from '~/logic/wallets/store/selectors' - -export default createStructuredSelector({ - safes: safesListSelector, - provider: providerNameSelector, -}) diff --git a/src/routes/safeList/store/selectors/index.js b/src/routes/safeList/store/selectors/index.js deleted file mode 100644 index b9247ec0..00000000 --- a/src/routes/safeList/store/selectors/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow -import { List, Map } from 'immutable' -import { createSelector, type Selector } from 'reselect' -import { type GlobalState } from '~/store/index' -import { type Safe } from '~/routes/safe/store/models/safe' -import { SAFE_REDUCER_ID } from '~/routes/safe/store/reducer/safe' - -export const safesMapSelector = (state: GlobalState): Map => state[SAFE_REDUCER_ID] - -export const safesListSelector: Selector> = createSelector( - safesMapSelector, - (safes: Map): List => safes.toList(), -) diff --git a/src/store/index.js b/src/store/index.js index 9542511c..0d514224 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -6,7 +6,7 @@ import { } from 'redux' import thunk from 'redux-thunk' import provider, { PROVIDER_REDUCER_ID, type State as ProviderState } from '~/logic/wallets/store/reducer/provider' -import safe, { SAFE_REDUCER_ID, type State as SafeState } from '~/routes/safe/store/reducer/safe' +import safe, { SAFE_REDUCER_ID, type SafeReducerState as SafeState } from '~/routes/safe/store/reducer/safe' import safeStorage from '~/routes/safe/store/middleware/safeStorage' import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/logic/tokens/store/reducer/tokens' import transactions, { @@ -37,6 +37,9 @@ const reducers: Reducer = combineReducers({ [TRANSACTIONS_REDUCER_ID]: transactions, }) -export const store: Store = createStore(reducers, finalCreateStore) +export const store: Store = createStore( + reducers, + finalCreateStore, +) export const aNewStore = (localState?: Object): Store => createStore(reducers, localState, finalCreateStore) diff --git a/src/test/builder/safe.redux.builder.js b/src/test/builder/safe.redux.builder.js index 9839c6b2..23e7bd34 100644 --- a/src/test/builder/safe.redux.builder.js +++ b/src/test/builder/safe.redux.builder.js @@ -72,6 +72,7 @@ export const aMinedSafe = async ( store: Store, owners: number = 1, threshold: number = 1, + name: string = 'Safe Name', ): Promise => { const provider = await getProviderInfo() const walletRecord = makeProvider(provider) @@ -79,7 +80,7 @@ export const aMinedSafe = async ( const accounts = await getWeb3().eth.getAccounts() const form = { - [FIELD_NAME]: 'Safe Name', + [FIELD_NAME]: name, [FIELD_CONFIRMATIONS]: `${threshold}`, [FIELD_OWNERS]: `${owners}`, } diff --git a/src/test/safe.dom.create.test.js b/src/test/safe.dom.create.test.js index 4f6809c6..7b8759ff 100644 --- a/src/test/safe.dom.create.test.js +++ b/src/test/safe.dom.create.test.js @@ -1,20 +1,21 @@ // @flow import * as React from 'react' import { type Store } from 'redux' -import { render, fireEvent } from '@testing-library/react' +import { render, fireEvent, act } from '@testing-library/react' import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router' +import { sleep } from '~/utils/timer' import { ADD_OWNER_BUTTON } from '~/routes/open/components/SafeOwnersConfirmationsForm' import Open from '~/routes/open/container/Open' import { aNewStore, history, type GlobalState } from '~/store' -import { sleep } from '~/utils/timer' import { getProviderInfo, getWeb3 } from '~/logic/wallets/getWeb3' import addProvider from '~/logic/wallets/store/actions/addProvider' import { makeProvider } from '~/logic/wallets/store/model/provider' import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts' import { whenSafeDeployed } from './builder/safe.dom.utils' -// https://github.com/testing-library/@testing-library/react/issues/281 +// For some reason it warns about events not wrapped in act +// But they're wrapped :( const originalError = console.error beforeAll(() => { console.error = (...args) => { @@ -53,9 +54,10 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num // Fill Safe's name const nameInput: HTMLInputElement = createSafeForm.getByPlaceholderText('Name of the new Safe') - fireEvent.change(nameInput, { target: { value: 'Adolfo Safe' } }) - fireEvent.submit(form) - await sleep(400) + await act(async () => { + fireEvent.change(nameInput, { target: { value: 'Adolfo Safe' } }) + fireEvent.submit(form) + }) // Fill owners const addedUpfront = 1 @@ -63,7 +65,11 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num expect(addOwnerButton.getElementsByTagName('span')[0].textContent).toEqual(ADD_OWNER_BUTTON) for (let i = addedUpfront; i < numOwners; i += 1) { - fireEvent.click(addOwnerButton) + /* eslint-disable */ + await act(async () => { + fireEvent.click(addOwnerButton) + }) + /* eslint-enable */ } const ownerNameInputs = createSafeForm.getAllByPlaceholderText('Owner Name*') @@ -75,23 +81,31 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num const ownerNameInput = ownerNameInputs[i] const ownerAddressInput = ownerAddressInputs[i] - fireEvent.change(ownerNameInput, { target: { value: `Owner ${i + 1}` } }) - fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } }) + /* eslint-disable */ + await act(async () => { + fireEvent.change(ownerNameInput, { target: { value: `Owner ${i + 1}` } }) + fireEvent.change(ownerAddressInput, { target: { value: accounts[i] } }) + }) + /* eslint-enable */ } // Fill Threshold // The test is fragile here, MUI select btn is hard to find const thresholdSelect = createSafeForm.getAllByRole('button')[2] - fireEvent.click(thresholdSelect) + await act(async () => { + fireEvent.click(thresholdSelect) + }) const thresholdOptions = createSafeForm.getAllByRole('option') - fireEvent.click(thresholdOptions[numOwners - 1]) - fireEvent.submit(form) - await sleep(400) + await act(async () => { + fireEvent.click(thresholdOptions[numOwners - 1]) + fireEvent.submit(form) + }) // Submit - fireEvent.submit(form) - await sleep(400) + await act(async () => { + fireEvent.submit(form) + }) // giving some time to the component for updating its state with safe // before destroying its context @@ -100,6 +114,7 @@ const deploySafe = async (createSafeForm: any, threshold: number, numOwners: num const aDeployedSafe = async (specificStore: Store, threshold?: number = 1, numOwners?: number = 1) => { const safe: React.Component<{}> = await renderOpenSafeForm(specificStore) + await sleep(1500) const safeAddress = await deploySafe(safe, threshold, numOwners) return safeAddress diff --git a/src/test/safe.dom.funds.threshold>1.test.js b/src/test/safe.dom.funds.threshold>1.test.js index a1ed2088..25ab2ded 100644 --- a/src/test/safe.dom.funds.threshold>1.test.js +++ b/src/test/safe.dom.funds.threshold>1.test.js @@ -1,5 +1,5 @@ // @flow -import { fireEvent } from '@testing-library/react' +import { fireEvent, waitForElement } from '@testing-library/react' import { aNewStore } from '~/store' import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { sendEtherTo } from '~/test/utils/tokenMovements' @@ -59,18 +59,20 @@ describe('DOM > Feature > Sending Funds', () => { expect(txRows.length).toBe(1) fireEvent.click(txRows[0]) - await sleep(100) - fireEvent.click(SafeDom.getByTestId(CONFIRM_TX_BTN_TEST_ID)) - await sleep(100) + + const confirmBtn = await waitForElement(() => SafeDom.getByTestId(CONFIRM_TX_BTN_TEST_ID)) + fireEvent.click(confirmBtn) // Travel confirm modal - fireEvent.click(SafeDom.getByTestId(APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID)) - await sleep(2000) + const approveTxBtn = await waitForElement(() => SafeDom.getByTestId(APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID)) + fireEvent.click(approveTxBtn) // EXECUTE TX - fireEvent.click(SafeDom.getByTestId(EXECUTE_TX_BTN_TEST_ID)) - await sleep(100) - fireEvent.click(SafeDom.getByTestId(APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID)) + const executeTxBtn = await waitForElement(() => SafeDom.getByTestId(EXECUTE_TX_BTN_TEST_ID)) + fireEvent.click(executeTxBtn) + + const confirmReviewTxBtn = await waitForElement(() => SafeDom.getByTestId(APPROVE_TX_MODAL_SUBMIT_BTN_TEST_ID)) + fireEvent.click(confirmReviewTxBtn) await sleep(500) // THEN diff --git a/src/test/safe.dom.settings.owners.test.js b/src/test/safe.dom.settings.owners.test.js index 7b838499..3a789276 100644 --- a/src/test/safe.dom.settings.owners.test.js +++ b/src/test/safe.dom.settings.owners.test.js @@ -1,5 +1,5 @@ // @flow -import { fireEvent } from '@testing-library/react' +import { fireEvent, waitForElement } from '@testing-library/react' import { aNewStore } from '~/store' import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { renderSafeView } from '~/test/builder/safe.dom.utils' @@ -55,17 +55,15 @@ describe('DOM > Feature > Settings - Manage owners', () => { await sleep(1300) // Travel to settings - const settingsBtn = SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID) + const settingsBtn = await waitForElement(() => SafeDom.getByTestId(SETTINGS_TAB_BTN_TEST_ID)) fireEvent.click(settingsBtn) - await sleep(200) // click on owners settings - const ownersSettingsBtn = SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID) + const ownersSettingsBtn = await waitForElement(() => SafeDom.getByTestId(OWNERS_SETTINGS_TAB_TEST_ID)) fireEvent.click(ownersSettingsBtn) - await sleep(200) // open rename owner modal - const renameOwnerBtn = SafeDom.getByTestId(RENAME_OWNER_BTN_TEST_ID) + const renameOwnerBtn = await waitForElement(() => SafeDom.getByTestId(RENAME_OWNER_BTN_TEST_ID)) fireEvent.click(renameOwnerBtn) // rename owner diff --git a/src/test/safe.dom.sidebar.test.js b/src/test/safe.dom.sidebar.test.js new file mode 100644 index 00000000..a6545a50 --- /dev/null +++ b/src/test/safe.dom.sidebar.test.js @@ -0,0 +1,55 @@ +// @flow +import { act, fireEvent } from '@testing-library/react' +import { aNewStore } from '~/store' +import { aMinedSafe } from '~/test/builder/safe.redux.builder' +import { renderSafeView } from '~/test/builder/safe.dom.utils' +import '@testing-library/jest-dom/extend-expect' +import { TOGGLE_SIDEBAR_BTN_TESTID } from '~/components/Header/component/SafeListHeader' +import { SIDEBAR_SAFELIST_ROW_TESTID } from '~/components/Sidebar/SafeList' +import { sleep } from '~/utils/timer' + +describe('DOM > Feature > Sidebar', () => { + let store + let safeAddress: string + beforeEach(async () => { + store = aNewStore() + safeAddress = await aMinedSafe(store) + }) + + it('Shows "default" label for a single safe', async () => { + const SafeDom = await renderSafeView(store, safeAddress) + + act(() => { + fireEvent.click(SafeDom.getByTestId(TOGGLE_SIDEBAR_BTN_TESTID)) + }) + + const safes = SafeDom.getAllByTestId(SIDEBAR_SAFELIST_ROW_TESTID) + expect(safes.length).toBe(1) + + expect(safes[0]).toContainElement(SafeDom.getByText('default')) + }) + + it('Changes default safe', async () => { + const SafeDom = await renderSafeView(store, safeAddress) + await aMinedSafe(store) + + await sleep(2000) + + act(() => { + fireEvent.click(SafeDom.getByTestId(TOGGLE_SIDEBAR_BTN_TESTID)) + }) + + const safes = SafeDom.getAllByTestId(SIDEBAR_SAFELIST_ROW_TESTID) + expect(safes.length).toBe(2) + + expect(safes[1]).toContainElement(SafeDom.getByText('default')) + expect(safes[0]).toContainElement(SafeDom.getByText('Make default')) + + act(() => { + fireEvent.click(SafeDom.getByText('Make default')) + }) + + expect(safes[0]).toContainElement(SafeDom.getByText('default')) + expect(safes[1]).toContainElement(SafeDom.getByText('Make default')) + }) +}) diff --git a/src/test/utils/transactions/moveFunds.helper.js b/src/test/utils/transactions/moveFunds.helper.js index 64b89074..d33d6402 100644 --- a/src/test/utils/transactions/moveFunds.helper.js +++ b/src/test/utils/transactions/moveFunds.helper.js @@ -1,6 +1,6 @@ // @flow import * as React from 'react' -import { fireEvent } from '@testing-library/react' +import { fireEvent, waitForElement } from '@testing-library/react' import { sleep } from '~/utils/timer' export const fillAndSubmitSendFundsForm = async ( @@ -24,7 +24,7 @@ export const fillAndSubmitSendFundsForm = async ( fireEvent.click(reviewBtn) // Submit the tx (Review Tx screen) - const submitBtn = SafeDom.getByTestId('submit-tx-btn') + const submitBtn = await waitForElement(() => SafeDom.getByTestId('submit-tx-btn')) fireEvent.click(submitBtn) await sleep(1000) } diff --git a/src/theme/mui.js b/src/theme/mui.js index 1cf13754..01d96c8a 100644 --- a/src/theme/mui.js +++ b/src/theme/mui.js @@ -206,6 +206,11 @@ export default createMuiTheme({ color: primary, }, }, + MuiSvgIcon: { + colorSecondary: { + color: secondaryText, + }, + }, MuiTab: { root: { fontFamily: 'Averta, monospace', diff --git a/src/theme/variables.js b/src/theme/variables.js index e7143318..84d9f9ea 100644 --- a/src/theme/variables.js +++ b/src/theme/variables.js @@ -17,6 +17,7 @@ const lg = '24px' const xl = '32px' const xxl = '40px' const marginButtonImg = '12px' +const headerHeight = '53px' module.exports = { primary, @@ -29,6 +30,7 @@ module.exports = { warning: warningColor, error: errorColor, connected: connectedColor, + headerHeight, xs, sm, md, diff --git a/yarn.lock b/yarn.lock index cf12fc7f..a645d497 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1326,9 +1326,9 @@ regenerator-runtime "^0.13.2" "@babel/runtime@^7.5.0": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.2.tgz#c3d6e41b304ef10dcf13777a33e7694ec4a9a6dd" - integrity sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg== + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205" + integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ== dependencies: regenerator-runtime "^0.13.2" @@ -1417,14 +1417,14 @@ "@emotion/weak-memoize" "0.2.3" "@emotion/cache@^10.0.17", "@emotion/cache@^10.0.9": - version "10.0.19" - resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.19.tgz#d258d94d9c707dcadaf1558def968b86bb87ad71" - integrity sha512-BoiLlk4vEsGBg2dAqGSJu0vJl/PgVtCYLBFJaEO8RmQzPugXewQCXZJNXTDFaRlfCs0W+quesayav4fvaif5WQ== + version "10.0.17" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.17.tgz#3491a035f62f276620d586677bfc3d4fad0b8472" + integrity sha512-442/miwbuwIDfSzfMqZNxuzxSEbskcz/bZ86QBYzEjFrr/oq9w+y5kJY1BHbGhDtr91GO232PZ5NN9XYMwr/Qg== dependencies: "@emotion/sheet" "0.9.3" "@emotion/stylis" "0.8.4" "@emotion/utils" "0.11.2" - "@emotion/weak-memoize" "0.2.4" + "@emotion/weak-memoize" "0.2.3" "@emotion/core@^10.0.14": version "10.0.17" @@ -1464,11 +1464,6 @@ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.2.tgz#53211e564604beb9befa7a4400ebf8147473eeef" integrity sha512-RMtr1i6E8MXaBWwhXL3yeOU8JXRnz8GNxHvaUfVvwxokvayUY0zoBeWbKw1S9XkufmGEEdQd228pSZXFkAln8Q== -"@emotion/hash@0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.3.tgz#a166882c81c0c6040975dd30df24fae8549bd96f" - integrity sha512-14ZVlsB9akwvydAdaEnVnvqu6J2P6ySv39hYyl/aoB6w/V+bXX0tay8cF6paqbgZsN2n5Xh15uF4pE+GvE+itw== - "@emotion/is-prop-valid@0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.2.tgz#b9692080da79041683021fcc32f96b40c54c59dc" @@ -1476,30 +1471,18 @@ dependencies: "@emotion/memoize" "0.7.2" -"@emotion/is-prop-valid@0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.3.tgz#cbe62ddbea08aa022cdf72da3971570a33190d29" - integrity sha512-We7VBiltAJ70KQA0dWkdPMXnYoizlxOXpvtjmu5/MBnExd+u0PGgV27WCYanmLAbCwAU30Le/xA0CQs/F/Otig== - dependencies: - "@emotion/memoize" "0.7.3" - "@emotion/memoize@0.7.2": version "0.7.2" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.2.tgz#7f4c71b7654068dfcccad29553520f984cc66b30" integrity sha512-hnHhwQzvPCW1QjBWFyBtsETdllOM92BfrKWbUTmh9aeOlcVOiXvlPsK4104xH8NsaKfg86PTFsWkueQeUfMA/w== -"@emotion/memoize@0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.3.tgz#5b6b1c11d6a6dddf1f2fc996f74cf3b219644d78" - integrity sha512-2Md9mH6mvo+ygq1trTeVp2uzAKwE2P7In0cRpD/M9Q70aH8L+rxMLbb3JCN2JoSWsV2O+DdFjfbbXoMoLBczow== - -"@emotion/serialize@^0.11.10", "@emotion/serialize@^0.11.11": - version "0.11.11" - resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.11.tgz#c92a5e5b358070a7242d10508143306524e842a4" - integrity sha512-YG8wdCqoWtuoMxhHZCTA+egL0RSGdHEc+YCsmiSBPBEDNuVeMWtjEWtGrhUterSChxzwnWBXvzSxIFQI/3sHLw== +"@emotion/serialize@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.10.tgz#53207dba7e28bd96928fc2a37e20b31b712bf9a2" + integrity sha512-04AB+wU00vv9jLgkWn13c/GJg2yXp3w7ZR3Q1O6mBSE6mbUmYeNX3OpBhfp//6r47lFyY0hBJJue+bA30iokHQ== dependencies: - "@emotion/hash" "0.7.3" - "@emotion/memoize" "0.7.3" + "@emotion/hash" "0.7.2" + "@emotion/memoize" "0.7.2" "@emotion/unitless" "0.7.4" "@emotion/utils" "0.11.2" csstype "^2.5.7" @@ -1531,13 +1514,13 @@ "@emotion/utils" "0.11.2" "@emotion/styled-base@^10.0.17": - version "10.0.19" - resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.19.tgz#53655274797194d86453354fdb2c947b46032db6" - integrity sha512-Sz6GBHTbOZoeZQKvkE9gQPzaJ6/qtoQ/OPvyG2Z/6NILlYk60Es1cEcTgTkm26H8y7A0GSgp4UmXl+srvsnFPg== + version "10.0.17" + resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.0.17.tgz#701af0cd256be2977db8d67c33630f542e460b85" + integrity sha512-vqQvxluZZKPByAB4zYZys0Qo/kVDP/03hAeg1K+TYpnZRwTi7WteOodc+/5669RPVNcfb93fphQpM5BYJnI1/g== dependencies: "@babel/runtime" "^7.5.5" - "@emotion/is-prop-valid" "0.8.3" - "@emotion/serialize" "^0.11.11" + "@emotion/is-prop-valid" "0.8.2" + "@emotion/serialize" "^0.11.10" "@emotion/utils" "0.11.2" "@emotion/styled@^10.0.14": @@ -1576,11 +1559,6 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz#dfa0c92efe44a1d1a7974fb49ffeb40ef2da5a27" integrity sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ== -"@emotion/weak-memoize@0.2.4": - version "0.2.4" - resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.4.tgz#622a72bebd1e3f48d921563b4b60a762295a81fc" - integrity sha512-6PYY5DVdAY1ifaQW6XYTnOMihmBVT27elqSjEoodchsGjzYlEsTQMcEhSud99kVawatyTZRTiVkJ/c6lwbQ7nA== - "@gnosis.pm/safe-contracts@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-contracts/-/safe-contracts-1.0.0.tgz#2b562b1e23a0da1047a9f38ef71a70f811e75dd9" @@ -2699,9 +2677,9 @@ integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== "@types/reach__router@^1.2.3": - version "1.2.5" - resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.5.tgz#add874f43b9733175be2b19de59602b91cc90860" - integrity sha512-Lna9cD38dN3deqJ6ThZgMKoAzW1LE3u+uUbPGdHUqquoM/fnZitSV1xfJxHjovu4SsNkpN9udkte3wEyrBPawQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.2.4.tgz#44a701fdf15934880f6dfdef38ca49bc30e2d372" + integrity sha512-a+MFhebeSGi0LwHZ0UhH/ke77rWtNQnt8YmaHnquSaY3HmyEi+BPQi3GhPcUPnC9X5BLw/qORw3BPxGb1mCtEw== dependencies: "@types/history" "*" "@types/react" "*" @@ -3961,14 +3939,14 @@ babel-plugin-emotion@^10.0.14: source-map "^0.5.7" babel-plugin-emotion@^10.0.17: - version "10.0.19" - resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.19.tgz#67b9b213f7505c015f163a387a005c12c502b1de" - integrity sha512-1pJb5uKN/gx6bi3gGr588Krj49sxARI9KmxhtMUa+NRJb6lR3OfC51mh3NlWRsOqdjWlT4cSjnZpnFq5K3T5ZA== + version "10.0.17" + resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.0.17.tgz#5673fbed7b1ed61b4b98d5530f33c8a4d1b08484" + integrity sha512-KNuBadotqYWpQexHhHOu7M9EV1j2c+Oh/JJqBfEQDusD6mnORsCZKHkl+xYwK82CPQ/23wRrsBIEYnKjtbMQJw== dependencies: "@babel/helper-module-imports" "^7.0.0" - "@emotion/hash" "0.7.3" - "@emotion/memoize" "0.7.3" - "@emotion/serialize" "^0.11.11" + "@emotion/hash" "0.7.2" + "@emotion/memoize" "0.7.2" + "@emotion/serialize" "^0.11.10" babel-plugin-macros "^2.0.0" babel-plugin-syntax-jsx "^6.18.0" convert-source-map "^1.5.0" @@ -7076,12 +7054,12 @@ emojis-list@^2.0.0: integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= emotion-theming@^10.0.14: - version "10.0.19" - resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.19.tgz#66d13db74fccaefad71ba57c915b306cf2250295" - integrity sha512-dQRBPLAAQ6eA8JKhkLCIWC8fdjPbiNC1zNTdFF292h9amhZXofcNGUP7axHoHX4XesqQESYwZrXp53OPInMrKw== + version "10.0.18" + resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.18.tgz#7d636eb465cb190590e17d815b8d318be512ef7d" + integrity sha512-zFAax4setUIKDj+cmbl3nxXDBRIMsPmiRNpg+qDmX9wTHW2TPWpETMGaDWB67LwK63rfSIkeTH7stFFnyKd2pQ== dependencies: "@babel/runtime" "^7.5.5" - "@emotion/weak-memoize" "0.2.4" + "@emotion/weak-memoize" "0.2.3" hoist-non-react-statics "^3.3.0" emotion-theming@^10.0.9: @@ -14841,9 +14819,9 @@ react-router@5.0.1: warning "^4.0.1" react-select@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.0.5.tgz#f2810e63fa8a6be375b3fa6f390284e9e33c9573" - integrity sha512-2tBXZ1XSqbk2boMUzSmKXwGl/6W46VkSMSLMy+ShccOVyD1kDTLPwLX7lugISkRMmL0v5BcLtriXOLfYwO0otw== + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.0.4.tgz#16bde37c24fd4f6444914d4681e78f15ffbc86d3" + integrity sha512-fbVISKa/lSUlLsltuatfUiKcWCNvdLXxFFyrzVQCBUsjxJZH/m7UMPdw/ywmRixAmwXAP++MdbNNZypOsiDEfA== dependencies: "@babel/runtime" "^7.4.4" "@emotion/cache" "^10.0.9"