Merge pull request #179 from gnosis/74-safe-list-sidebar
Feature #74: Safe list sidebar
This commit is contained in:
commit
baae35061b
|
@ -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,
|
||||
|
|
34
package.json
34
package.json
|
@ -30,18 +30,18 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@gnosis.pm/safe-contracts": "^1.0.0",
|
||||
"@gnosis.pm/util-contracts": "2.0.1",
|
||||
"@material-ui/core": "4.4.2",
|
||||
"@material-ui/icons": "4.4.1",
|
||||
"@gnosis.pm/util-contracts": "2.0.4",
|
||||
"@material-ui/core": "4.4.3",
|
||||
"@material-ui/icons": "4.4.3",
|
||||
"@testing-library/jest-dom": "4.1.0",
|
||||
"@welldone-software/why-did-you-render": "3.3.3",
|
||||
"@welldone-software/why-did-you-render": "3.3.5",
|
||||
"axios": "0.19.0",
|
||||
"bignumber.js": "9.0.0",
|
||||
"connected-react-router": "6.5.2",
|
||||
"date-fns": "2.1.0",
|
||||
"date-fns": "2.2.1",
|
||||
"ethereum-ens": "0.7.8",
|
||||
"final-form": "4.18.5",
|
||||
"history": "4.10.0",
|
||||
"history": "4.10.1",
|
||||
"immortal-db": "^1.0.2",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
|
@ -51,7 +51,7 @@
|
|||
"react-dom": "16.9.0",
|
||||
"react-final-form": "6.3.0",
|
||||
"react-final-form-listeners": "^1.0.2",
|
||||
"react-hot-loader": "4.12.13",
|
||||
"react-hot-loader": "4.12.14",
|
||||
"react-infinite-scroll-component": "4.5.3",
|
||||
"react-qr-reader": "^2.2.1",
|
||||
"react-redux": "7.1.1",
|
||||
|
@ -89,10 +89,10 @@
|
|||
"@babel/preset-flow": "^7.0.0-beta.40",
|
||||
"@babel/preset-react": "^7.0.0-beta.40",
|
||||
"@sambego/storybook-state": "^1.0.7",
|
||||
"@storybook/addon-actions": "5.1.11",
|
||||
"@storybook/addon-knobs": "5.1.11",
|
||||
"@storybook/addon-links": "5.1.11",
|
||||
"@storybook/react": "5.1.11",
|
||||
"@storybook/addon-actions": "5.2.1",
|
||||
"@storybook/addon-knobs": "5.2.1",
|
||||
"@storybook/addon-links": "5.2.1",
|
||||
"@storybook/react": "5.2.1",
|
||||
"@testing-library/react": "9.1.4",
|
||||
"autoprefixer": "9.6.1",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"ethereumjs-abi": "0.6.8",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "4.2.0",
|
||||
"flow-bin": "0.107.0",
|
||||
"flow-bin": "0.108.0",
|
||||
"fs-extra": "8.1.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.0.4",
|
||||
|
@ -132,15 +132,15 @@
|
|||
"storybook-host": "5.1.0",
|
||||
"storybook-router": "^0.3.3",
|
||||
"style-loader": "1.0.0",
|
||||
"truffle": "5.0.35",
|
||||
"truffle": "5.0.37",
|
||||
"truffle-contract": "4.0.31",
|
||||
"truffle-solidity-loader": "0.1.32",
|
||||
"uglifyjs-webpack-plugin": "2.2.0",
|
||||
"url-loader": "^2.1.0",
|
||||
"webpack": "4.39.3",
|
||||
"webpack-bundle-analyzer": "3.4.1",
|
||||
"webpack-cli": "3.3.8",
|
||||
"webpack-dev-server": "3.8.0",
|
||||
"webpack": "4.40.2",
|
||||
"webpack-bundle-analyzer": "3.5.1",
|
||||
"webpack-cli": "3.3.9",
|
||||
"webpack-dev-server": "3.8.1",
|
||||
"webpack-manifest-plugin": "^2.0.0-rc.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = () => (
|
||||
<Block className={styles.footer}>
|
||||
<Link to={WELCOME_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Add Safe</Paragraph>
|
||||
</Link>
|
||||
<Link to={SAFELIST_ADDRESS}>
|
||||
<Paragraph size="sm" color="primary" noMargin>Safe List</Paragraph>
|
||||
</Link>
|
||||
</Block>
|
||||
)
|
||||
|
||||
export default Footer
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) => (
|
||||
<div className={styles.frame}>
|
||||
<div style={{ flex: '1' }} />
|
||||
{story()}
|
||||
</div>
|
||||
)
|
||||
|
||||
storiesOf('Components /Footer', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Loaded', () => <Component />)
|
|
@ -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(({
|
|||
</Link>
|
||||
</Col>
|
||||
<Divider />
|
||||
<SafeListHeader />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Provider open={open} toggle={toggle} info={providerInfo}>
|
||||
|
|
|
@ -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 (
|
||||
<Col start="xs" middle="xs" className={classes.container}>
|
||||
Safes
|
||||
{' '}
|
||||
<Paragraph size="xs" className={classes.counter}>
|
||||
{safesCount}
|
||||
</Paragraph>
|
||||
<IconButton
|
||||
onClick={toggleSidebar}
|
||||
className={classes.icon}
|
||||
aria-label="Expand Safe List"
|
||||
data-testid={TOGGLE_SIDEBAR_BTN_TESTID}
|
||||
>
|
||||
{isOpen ? <ExpandLessIcon /> : <ExpandMoreIcon color="secondary" />}
|
||||
</IconButton>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect<Object, Object, ?Function, ?Object>(
|
||||
// $FlowFixMe
|
||||
(state) => ({ safesCount: safesCountSelector(state) }),
|
||||
null,
|
||||
)(SafeListHeader)
|
|
@ -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 (
|
||||
<Block align="left" className={classes.container}>
|
||||
<Img src={StarIcon} alt="Star Icon" />
|
||||
<Paragraph noMargin size="xs">
|
||||
default
|
||||
</Paragraph>
|
||||
</Block>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultBadge
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||
<path fill="#FFF" fill-rule="nonzero" d="M6 0L3.852 3.336 0 4.344l2.52 3.084-.228 3.972L6 9.954 9.708 11.4 9.48 7.428 12 4.344 8.148 3.336 6 0zM4.428 4.8c.372 0 .672.3.672.678 0 .371-.3.672-.672.672a.672.672 0 0 1-.678-.672c0-.378.3-.678.678-.678zm3.15 0c.372 0 .672.3.672.678 0 .371-.3.672-.672.672a.672.672 0 0 1-.678-.672c0-.378.3-.678.678-.678zM4.2 7.5h3.6a1.95 1.95 0 0 1-3.6 0z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 482 B |
|
@ -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<Safe>,
|
||||
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 (
|
||||
<MuiList className={classes.list}>
|
||||
{safes.map((safe) => (
|
||||
<React.Fragment key={safe.address}>
|
||||
<Link to={`${SAFELIST_ADDRESS}/${safe.address}`} onClick={onSafeClick} data-testid={SIDEBAR_SAFELIST_ROW_TESTID}>
|
||||
<ListItem classes={{ root: classes.listItemRoot }}>
|
||||
<ListItemIcon>
|
||||
<Identicon address={safe.address} diameter={32} className={classes.icon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={safe.name}
|
||||
secondary={shortVersionOf(safe.address, 4)}
|
||||
classes={{ primary: classes.safeName, secondary: classes.safeAddress }}
|
||||
/>
|
||||
<Paragraph size="lg" color="primary">
|
||||
{safe.ethBalance}
|
||||
{' '}
|
||||
ETH
|
||||
</Paragraph>
|
||||
{sameAddress(defaultSafe, safe.address) ? (
|
||||
<DefaultBadge />
|
||||
) : (
|
||||
<ButtonLink
|
||||
className={classes.makeDefaultBtn}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
setDefaultSafe(safe.address)
|
||||
}}
|
||||
>
|
||||
Make default
|
||||
</ButtonLink>
|
||||
)}
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</MuiList>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeList
|
|
@ -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<TSidebarContext>({
|
||||
isOpen: false,
|
||||
toggleSidebar: () => {},
|
||||
})
|
||||
|
||||
type SidebarProps = {
|
||||
children: React.Node,
|
||||
safes: List<Safe>,
|
||||
setDefaultSafeAction: Function,
|
||||
defaultSafe: string,
|
||||
}
|
||||
|
||||
const filterBy = (filter: string, safes: List<Safe>): List<Safe> => 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<boolean>(false)
|
||||
const [filter, setFilter] = useState<string>('')
|
||||
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 (
|
||||
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
||||
<ClickAwayListener onClickAway={toggleSidebar}>
|
||||
<Drawer
|
||||
className={classes.sidebar}
|
||||
open={isOpen}
|
||||
onKeyDown={handleEsc}
|
||||
classes={{ paper: classes.sidebarPaper }}
|
||||
ModalProps={{ onBackdropClick: toggleSidebar }}
|
||||
>
|
||||
<div className={classes.headerPlaceholder} />
|
||||
<Row align="center">
|
||||
<SearchIcon className={classes.searchIcon} />
|
||||
<SearchBar
|
||||
classes={searchClasses}
|
||||
placeholder="Search by name or address"
|
||||
searchIcon={<div />}
|
||||
onChange={handleFilterChange}
|
||||
onCancelSearch={handleFilterCancel}
|
||||
/>
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Button
|
||||
component={Link}
|
||||
to={WELCOME_ADDRESS}
|
||||
className={classes.addSafeBtn}
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
+ Add Safe
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Row>
|
||||
<Hairline />
|
||||
<SafeList
|
||||
safes={filteredSafes}
|
||||
onSafeClick={toggleSidebar}
|
||||
setDefaultSafe={setDefaultSafeAction}
|
||||
defaultSafe={defaultSafe}
|
||||
/>
|
||||
</Drawer>
|
||||
</ClickAwayListener>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect<Object, Object, ?Function, ?Object>(
|
||||
// $FlowFixMe
|
||||
(state) => ({ safes: sortedSafeListSelector(state), defaultSafe: defaultSafeSelector(state) }),
|
||||
{ setDefaultSafeAction: setDefaultSafe },
|
||||
)(Sidebar)
|
|
@ -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<GlobalState, {}, List<Safe>> = createSelector(
|
||||
safesListSelector,
|
||||
(safes: List<Safe>): List<Safe> => safes.sort((a: Safe, b: Safe) => (a.name > b.name ? 1 : -1)),
|
||||
)
|
|
@ -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
|
|
@ -4,7 +4,7 @@ import { border } from '~/theme/variables'
|
|||
|
||||
const style = {
|
||||
height: '100%',
|
||||
borderRight: `solid 1px ${border}`,
|
||||
borderRight: `solid 2px ${border}`,
|
||||
}
|
||||
|
||||
const Divider = () => <div style={style} />
|
||||
|
|
|
@ -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) => (
|
||||
<SharedSnackbarProvider>
|
||||
<div className={styles.frame}>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
<SidebarProvider>
|
||||
<Header />
|
||||
{children}
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</SharedSnackbarProvider>
|
||||
)
|
||||
|
|
|
@ -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(<Root />, document.getElementById('root'))
|
||||
|
|
|
@ -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<Map<string, string
|
|||
return data ? Map(data) : Map()
|
||||
}
|
||||
|
||||
export const getDefaultSafe = async (): Promise<string> => {
|
||||
const defaultSafe = await loadFromStorage(DEFAULT_SAFE_KEY)
|
||||
|
||||
return defaultSafe || ''
|
||||
}
|
||||
|
||||
export const saveDefaultSafe = async (safeAddress: string): Promise<void> => {
|
||||
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<void> => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -7,7 +7,7 @@ export const PROVIDER_REDUCER_ID = 'providers'
|
|||
|
||||
export type State = Provider
|
||||
|
||||
export default handleActions(
|
||||
export default handleActions<State, Function>(
|
||||
{
|
||||
[ADD_PROVIDER]: (state: State, { payload }: ActionType<typeof addProvider>) => makeProvider(payload),
|
||||
},
|
||||
|
|
|
@ -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 = () => (
|
||||
<Switch>
|
||||
<Redirect exact from="/" to={WELCOME_ADDRESS} />
|
||||
<Route exact path={WELCOME_ADDRESS} component={Welcome} />
|
||||
<Route exact path={OPEN_ADDRESS} component={Open} />
|
||||
<Route exact path={SAFELIST_ADDRESS} component={SafeList} />
|
||||
<Route exact path={SAFE_ADDRESS} component={Safe} />
|
||||
<Route exact path={OPENING_ADDRESS} component={Opening} />
|
||||
<Route exact path={LOAD_ADDRESS} component={Load} />
|
||||
</Switch>
|
||||
)
|
||||
type RoutesProps = {
|
||||
defaultSafe?: string,
|
||||
location: Object,
|
||||
}
|
||||
|
||||
export default Routes
|
||||
const Routes = ({ defaultSafe, location }: RoutesProps) => {
|
||||
const [isInitialLoad, setInitialLoad] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/') {
|
||||
setInitialLoad(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
if (!isInitialLoad) {
|
||||
return <Redirect to={WELCOME_ADDRESS} />
|
||||
}
|
||||
|
||||
if (typeof defaultSafe === 'undefined') {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
setInitialLoad(false)
|
||||
if (defaultSafe) {
|
||||
return <Redirect to={`${SAFELIST_ADDRESS}/${defaultSafe}`} />
|
||||
}
|
||||
|
||||
return <Redirect to={WELCOME_ADDRESS} />
|
||||
}}
|
||||
/>
|
||||
<Route exact path={WELCOME_ADDRESS} component={Welcome} />
|
||||
<Route exact path={OPEN_ADDRESS} component={Open} />
|
||||
<Route exact path={SAFE_ADDRESS} component={Safe} />
|
||||
<Route exact path={OPENING_ADDRESS} component={Opening} />
|
||||
<Route exact path={LOAD_ADDRESS} component={Load} />
|
||||
<Redirect to="/" />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect<Object, Object, ?Function, ?Object>(
|
||||
// $FlowFixMe
|
||||
(state) => ({ defaultSafe: defaultSafeSelector(state) }),
|
||||
null,
|
||||
)(withRouter(Routes))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) || {}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<OpenState> => {
|
||||
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 = {
|
||||
|
|
|
@ -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<string, string> => {
|
||||
export const getOwnersFrom = (names: string[], addresses: string[]): List<Owner> => {
|
||||
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)
|
||||
|
|
|
@ -137,7 +137,6 @@ class Layout extends React.Component<Props, State> {
|
|||
className={classes.send}
|
||||
onClick={() => showSendFunds('Ether')}
|
||||
disabled={!granted}
|
||||
testId="balance-send-btn"
|
||||
>
|
||||
<CallMade alt="Send Transaction" className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Send
|
||||
|
|
|
@ -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 (
|
||||
<SharedSnackbarConsumer>
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = crea
|
|||
return false
|
||||
}
|
||||
|
||||
const owners: List<Owner> = safe.get('owners')
|
||||
const { owners }: List<Owner> = safe
|
||||
if (!owners) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -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<string, Function, ActionReturn>(ADD_SAFE, (s
|
|||
safe,
|
||||
}))
|
||||
|
||||
const saveSafe = (name: string, address: string, threshold: number, ownersName: string[], ownersAddress: string[]) => (
|
||||
const saveSafe = (safe: Safe) => (
|
||||
dispatch: ReduxDispatch<GlobalState>,
|
||||
getState: GetState<GlobalState>,
|
||||
) => {
|
||||
const owners: List<Owner> = 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
|
||||
|
|
|
@ -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<GlobalState>) => {
|
||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
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)
|
||||
|
|
|
@ -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<GlobalState>) => {
|
||||
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
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const SET_DEFAULT_SAFE = 'SET_DEFAULT_SAFE'
|
||||
|
||||
const setDefaultSafe = createAction<string, *>(SET_DEFAULT_SAFE)
|
||||
|
||||
export default setDefaultSafe
|
|
@ -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<GlobalState>) => (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
|
||||
}
|
||||
|
|
|
@ -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<string, Safe>
|
||||
export type SafeReducerState = Map<string, *>
|
||||
|
||||
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<string, Safe> => {
|
||||
const safes: Map<string, Safe> = 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<State> => {
|
||||
const storedSafes = await loadFromStorage(SAFES_KEY)
|
||||
const safes = storedSafes ? buildSafesFrom(storedSafes) : Map()
|
||||
|
||||
return safes
|
||||
}
|
||||
|
||||
export default handleActions<State, *>(
|
||||
export default handleActions<SafeReducerState, *>(
|
||||
{
|
||||
[UPDATE_SAFE]: (state: State, action: ActionType<Function>): State => {
|
||||
[UPDATE_SAFE]: (state: SafeReducerState, action: ActionType<Function>): 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<Function>): State => {
|
||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const tokenAddress = action.payload
|
||||
|
||||
const newState = state.withMutations((map) => {
|
||||
|
@ -77,59 +51,59 @@ export default handleActions<State, *>(
|
|||
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<Function>): State => {
|
||||
[ADD_SAFE]: (state: SafeReducerState, action: ActionType<Function>): 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<Function>): State => {
|
||||
[REMOVE_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const safeAddress = action.payload
|
||||
|
||||
return state.delete(safeAddress)
|
||||
return state.deleteIn(['safes', safeAddress])
|
||||
},
|
||||
[ADD_SAFE_OWNER]: (state: State, action: ActionType<Function>): State => {
|
||||
[ADD_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): 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<Function>): State => {
|
||||
[REMOVE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): 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<Function>): State => {
|
||||
[REPLACE_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): 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<Function>): State => {
|
||||
[EDIT_SAFE_OWNER]: (state: SafeReducerState, action: ActionType<Function>): 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<State, *>(
|
|||
return prevSafe.merge({ owners: updatedOwners })
|
||||
})
|
||||
},
|
||||
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => state.set('defaultSafe', action.payload),
|
||||
},
|
||||
Map(),
|
||||
Map({
|
||||
// $FlowFixMe
|
||||
defaultSafe: undefined,
|
||||
safes: Map(),
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -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<string, *> => state[SAFE_REDUCER_ID]
|
||||
|
||||
export const safesMapSelector = (state: GlobalState): Map<string, Safe> => state[SAFE_REDUCER_ID].get('safes')
|
||||
|
||||
export const safesListSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
|
||||
safesMapSelector,
|
||||
(safes: Map<string, Safe>): List<Safe> => safes.toList(),
|
||||
)
|
||||
|
||||
export const safesCountSelector: Selector<GlobalState, {}, number> = createSelector(
|
||||
safesMapSelector,
|
||||
(safes: Map<string, Safe>): number => safes.size,
|
||||
)
|
||||
|
||||
export const defaultSafeSelector: Selector<GlobalState, {}, string> = createSelector(
|
||||
safesStateSelector,
|
||||
(safeState: Map<string, *>): string => safeState.get('defaultSafe'),
|
||||
)
|
||||
|
||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const oneTransactionSelector = (state: GlobalState, props: TransactionProps) => props.transaction
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
*/
|
|
@ -1,9 +0,0 @@
|
|||
// @flow
|
||||
import { safeTransactionsSelector } from '~/routes/safe/store/selectors/index'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
|
||||
export const getTransactionFromReduxStore = (store: Store<GlobalState>, address: string, index: number = 0) => {
|
||||
const transactions = safeTransactionsSelector(store.getState(), { safeAddress: address })
|
||||
|
||||
return transactions.get(index)
|
||||
}
|
|
@ -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<Safe>,
|
||||
provider: string,
|
||||
}
|
||||
|
||||
const SafeList = ({ safes, provider }: Props) => {
|
||||
const safesAvailable = safes && safes.count() > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
{ safesAvailable
|
||||
? <SafeTable safes={safes} />
|
||||
: <NoSafe provider={provider} text="No safes created, please create a new one" />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SafeList
|
|
@ -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) => <div className={styles.frame}>{story()}</div>
|
||||
|
||||
storiesOf('Routes /safes', module)
|
||||
.addDecorator(FrameDecorator)
|
||||
.add('Safe List whithout safes and connected', () => <Component provider="METAMASK" safes={List([])} />)
|
||||
.add('Safe List whithout safes and NOT connected', () => <Component provider="" safes={List([])} />)
|
|
@ -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<Safe>
|
||||
}
|
||||
const SafeTable = ({ safes }: Props) => (
|
||||
<Table size={900}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Open</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Deployed Address</TableCell>
|
||||
<TableCell align="right">Confirmations</TableCell>
|
||||
<TableCell align="right">Number of owners</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{safes.map(safe => (
|
||||
<TableRow key={safe.address}>
|
||||
<TableCell>
|
||||
<Link to={`${SAFELIST_ADDRESS}/${safe.address}`}>
|
||||
<Button variant="contained" size="small" color="primary">Open</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell padding="none">{safe.get('name')}</TableCell>
|
||||
<TableCell padding="none">{safe.get('address')}</TableCell>
|
||||
<TableCell padding="none" align="right">{safe.get('threshold')}</TableCell>
|
||||
<TableCell padding="none" align="right">{safe.get('owners').count()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)
|
||||
|
||||
export default SafeTable
|
|
@ -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<Safe>,
|
||||
provider: string,
|
||||
}
|
||||
|
||||
const SafeList = ({ safes, provider }: Props) => (
|
||||
<Page>
|
||||
<Layout safes={safes} provider={provider} />
|
||||
</Page>
|
||||
)
|
||||
|
||||
export default connect<*, *, *, *>(selector)(SafeList)
|
|
@ -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<Object, *>({
|
||||
safes: safesListSelector,
|
||||
provider: providerNameSelector,
|
||||
})
|
|
@ -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<string, Safe> => state[SAFE_REDUCER_ID]
|
||||
|
||||
export const safesListSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
|
||||
safesMapSelector,
|
||||
(safes: Map<string, Safe>): List<Safe> => safes.toList(),
|
||||
)
|
|
@ -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<GlobalState> = combineReducers({
|
|||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||
})
|
||||
|
||||
export const store: Store<GlobalState> = createStore(reducers, finalCreateStore)
|
||||
export const store: Store<GlobalState> = createStore(
|
||||
reducers,
|
||||
finalCreateStore,
|
||||
)
|
||||
|
||||
export const aNewStore = (localState?: Object): Store<GlobalState> => createStore(reducers, localState, finalCreateStore)
|
||||
|
|
|
@ -72,6 +72,7 @@ export const aMinedSafe = async (
|
|||
store: Store<GlobalState>,
|
||||
owners: number = 1,
|
||||
threshold: number = 1,
|
||||
name: string = 'Safe Name',
|
||||
): Promise<string> => {
|
||||
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}`,
|
||||
}
|
||||
|
|
|
@ -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<GlobalState>, 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -206,6 +206,11 @@ export default createMuiTheme({
|
|||
color: primary,
|
||||
},
|
||||
},
|
||||
MuiSvgIcon: {
|
||||
colorSecondary: {
|
||||
color: secondaryText,
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
root: {
|
||||
fontFamily: 'Averta, monospace',
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue