Merge pull request #82 from gnosis/feature/tokens-view
#73 Feature - Safe overview
This commit is contained in:
commit
348eb96045
|
@ -72,6 +72,7 @@
|
|||
"immutable": "^4.0.0-rc.9",
|
||||
"jest": "^22.4.2",
|
||||
"json-loader": "^0.5.7",
|
||||
"material-ui-search-bar": "^1.0.0-beta.13",
|
||||
"postcss-loader": "^2.1.1",
|
||||
"postcss-mixins": "^6.2.0",
|
||||
"postcss-simple-vars": "^4.1.0",
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import Modal from '@material-ui/core/Modal'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
description: string,
|
||||
open: boolean,
|
||||
handleClose: Function,
|
||||
children: React$Node,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
const styles = () => ({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
display: 'flex',
|
||||
},
|
||||
paper: {
|
||||
position: 'absolute',
|
||||
top: '120px',
|
||||
width: '500px',
|
||||
height: '530px',
|
||||
borderRadius: '3px',
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const GnoModal = ({
|
||||
title, description, open, children, handleClose, classes,
|
||||
}: Props) => (
|
||||
<Modal
|
||||
aria-labelledby={title}
|
||||
aria-describedby={description}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
className={classes.root}
|
||||
>
|
||||
<div className={classes.paper}>
|
||||
{ children }
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
export default withStyles(styles)(GnoModal)
|
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { List } from 'immutable'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import TableHead from '@material-ui/core/TableHead'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel'
|
||||
import { type Order } from '~/components/Table/sorting'
|
||||
|
||||
export type Column = {
|
||||
id: string,
|
||||
numeric: boolean,
|
||||
order: boolean, // If data for sorting will be provided in a different attr
|
||||
disablePadding: boolean,
|
||||
label: string,
|
||||
custom: boolean, // If content will be rendered by user manually
|
||||
width?: number,
|
||||
}
|
||||
|
||||
export const cellWidth = (width: number | typeof undefined) => {
|
||||
if (!width) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
width: `${width}px`,
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
columns: List<Column>,
|
||||
orderBy: string, // id of one of the described columns
|
||||
order: Order,
|
||||
onSort: (property: string, orderAttr: boolean) => void,
|
||||
}
|
||||
|
||||
class GnoTableHead extends React.PureComponent<Props> {
|
||||
changeSort = (property: string, orderAttr: boolean) => () => {
|
||||
this.props.onSort(property, orderAttr)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { columns, order, orderBy } = this.props
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column: Column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
numeric={column.numeric}
|
||||
padding={column.disablePadding ? 'none' : 'default'}
|
||||
sortDirection={orderBy === column.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === column.id}
|
||||
direction={order}
|
||||
onClick={this.changeSort(column.id, column.order)}
|
||||
>
|
||||
{column.label}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GnoTableHead
|
|
@ -0,0 +1,155 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { List } from 'immutable'
|
||||
import Table from '@material-ui/core/Table'
|
||||
import TableBody from '@material-ui/core/TableBody'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import TablePagination from '@material-ui/core/TablePagination'
|
||||
import { type Order, stableSort, getSorting } from '~/components/Table/sorting'
|
||||
import TableHead, { type Column } from '~/components/Table/TableHead'
|
||||
import { xl } from '~/theme/variables'
|
||||
|
||||
type Props<K> = {
|
||||
label: string,
|
||||
defaultOrderBy: string,
|
||||
columns: List<Column>,
|
||||
data: Array<K>,
|
||||
classes: Object,
|
||||
children: Function,
|
||||
size: number,
|
||||
defaultFixed?: boolean,
|
||||
}
|
||||
|
||||
type State = {
|
||||
page: number,
|
||||
order: Order,
|
||||
orderBy: string,
|
||||
orderProp: boolean,
|
||||
rowsPerPage: number,
|
||||
fixed: boolean,
|
||||
}
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 -1px 4px 0 rgba(74, 85, 121, 0.5)',
|
||||
},
|
||||
selectRoot: {
|
||||
lineHeight: '40px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
white: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
paginationRoot: {
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 4px 0 rgba(74, 85, 121, 0.5)',
|
||||
marginBottom: xl,
|
||||
},
|
||||
empty: {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
const FIXED_HEIGHT = 49
|
||||
|
||||
class GnoTable<K> extends React.Component<Props<K>, State> {
|
||||
state = {
|
||||
page: 0,
|
||||
order: 'asc',
|
||||
orderBy: this.props.defaultOrderBy,
|
||||
fixed: !!this.props.defaultFixed,
|
||||
orderProp: false,
|
||||
rowsPerPage: 5,
|
||||
}
|
||||
|
||||
onSort = (property: string, orderProp: boolean) => {
|
||||
const orderBy = property
|
||||
let order = 'desc'
|
||||
|
||||
if (this.state.orderBy === property && this.state.order === 'desc') {
|
||||
order = 'asc'
|
||||
}
|
||||
|
||||
this.setState(() => ({
|
||||
order, orderBy, orderProp, fixed: false,
|
||||
}))
|
||||
}
|
||||
|
||||
handleChangePage = (e: SyntheticInputEvent<HTMLInputElement>, page: number) => {
|
||||
this.setState({ page })
|
||||
}
|
||||
|
||||
handleChangeRowsPerPage = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
const rowsPerPage = Number(event.target.value)
|
||||
this.setState({ rowsPerPage })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data, label, columns, classes, children, size,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
order, orderBy, page, orderProp, rowsPerPage, fixed,
|
||||
} = this.state
|
||||
|
||||
const backProps = {
|
||||
'aria-label': 'Previous Page',
|
||||
}
|
||||
|
||||
const nextProps = {
|
||||
'aria-label': 'Next Page',
|
||||
}
|
||||
|
||||
const paginationClasses = {
|
||||
selectRoot: classes.selectRoot,
|
||||
root: classes.paginationRoot,
|
||||
input: classes.white,
|
||||
}
|
||||
|
||||
const sortedData = stableSort(data, getSorting(order, orderBy, orderProp), fixed)
|
||||
.slice(page * rowsPerPage, ((page * rowsPerPage) + rowsPerPage))
|
||||
|
||||
const emptyRows = rowsPerPage - Math.min(rowsPerPage, data.length - (page * rowsPerPage))
|
||||
const emptyStyle = {
|
||||
height: FIXED_HEIGHT * emptyRows,
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Table aria-labelledby={label} className={classes.root}>
|
||||
<TableHead
|
||||
columns={columns}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onSort={this.onSort}
|
||||
/>
|
||||
<TableBody>
|
||||
{ children(sortedData) }
|
||||
{ emptyRows > 0 &&
|
||||
<TableRow style={emptyStyle}>
|
||||
<TableCell colSpan={4} />
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={size}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
backIconButtonProps={backProps}
|
||||
nextIconButtonProps={nextProps}
|
||||
onChangePage={this.handleChangePage}
|
||||
onChangeRowsPerPage={this.handleChangeRowsPerPage}
|
||||
classes={paginationClasses}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(GnoTable)
|
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
|
||||
export const FIXED = 'fixed'
|
||||
type Fixed = {
|
||||
fixed?: boolean,
|
||||
}
|
||||
|
||||
export type SortRow<T> = T & Fixed
|
||||
|
||||
export const buildOrderFieldFrom = (attr: string) => `${attr}Order`
|
||||
|
||||
|
||||
const desc = (a: Object, b: Object, orderBy: string, orderProp: boolean) => {
|
||||
const order = orderProp ? buildOrderFieldFrom(orderBy) : orderBy
|
||||
|
||||
if (b[order] < a[order]) {
|
||||
return -1
|
||||
}
|
||||
if (b[order] > a[order]) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export const stableSort = <SortRow>(array: Array<SortRow>, cmp: any, fixed: boolean): Array<SortRow> => {
|
||||
const fixedElems: Array<SortRow> = fixed ? array.filter((elem: any) => elem.fixed) : []
|
||||
const data: Array<SortRow> = fixed ? array.filter((elem: any) => !elem[FIXED]) : array
|
||||
const stabilizedThis = data.map((el, index) => [el, index])
|
||||
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = cmp(a[0], b[0])
|
||||
if (order !== 0) {
|
||||
return order
|
||||
}
|
||||
|
||||
return a[1] - b[1]
|
||||
})
|
||||
|
||||
const sortedElems: Array<SortRow> = stabilizedThis.map(el => el[0])
|
||||
|
||||
return fixedElems.concat(sortedElems)
|
||||
}
|
||||
|
||||
export type Order = 'asc' | 'desc'
|
||||
|
||||
export const getSorting = (order: Order, orderBy: string, orderProp: boolean) =>
|
||||
(order === 'desc' ? (a: any, b: any) => desc(a, b, orderBy, orderProp) : (a: any, b: any) => -desc(a, b, orderBy, orderProp))
|
|
@ -3,19 +3,20 @@ import * as React from 'react'
|
|||
import { type Size, getSize } from '~/theme/size'
|
||||
import { border } from '~/theme/variables'
|
||||
|
||||
const calculateStyleFrom = (margin?: Size) => ({
|
||||
const calculateStyleFrom = (color?: string, margin?: Size) => ({
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
backgroundColor: border,
|
||||
backgroundColor: color || border,
|
||||
margin: `${getSize(margin)} 0px`,
|
||||
})
|
||||
|
||||
type Props = {
|
||||
margin?: Size,
|
||||
color?: string,
|
||||
}
|
||||
|
||||
const Hairline = ({ margin }: Props) => {
|
||||
const style = calculateStyleFrom(margin)
|
||||
const Hairline = ({ margin, color }: Props) => {
|
||||
const style = calculateStyleFrom(color, margin)
|
||||
|
||||
return <div style={style} />
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4';
|
|||
type Props = {
|
||||
align?: 'left' | 'center' | 'right',
|
||||
margin?: 'sm' | 'md' | 'lg' | 'xl',
|
||||
color?: 'soft' | 'medium' | 'dark' | 'white' | 'fancy' | 'primary' | 'secondary' | 'warning' | 'disabled',
|
||||
tag: HeadingTag,
|
||||
truncate?: boolean,
|
||||
children: React$Node,
|
||||
|
@ -19,7 +20,7 @@ type Props = {
|
|||
class Heading extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const {
|
||||
align, tag, truncate, margin, children, ...props
|
||||
align, tag, truncate, margin, color, children, ...props
|
||||
} = this.props
|
||||
|
||||
const className = cx(
|
||||
|
@ -27,6 +28,7 @@ class Heading extends React.PureComponent<Props> {
|
|||
align,
|
||||
tag,
|
||||
margin ? capitalize(margin, 'margin') : undefined,
|
||||
color,
|
||||
{ truncate },
|
||||
)
|
||||
|
||||
|
|
|
@ -63,3 +63,39 @@
|
|||
.marginXl {
|
||||
margin: 0 0 $xl 0;
|
||||
}
|
||||
|
||||
.soft {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.medium {
|
||||
color: #686868;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.fancy {
|
||||
color: $fancy;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: $fontColor;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: $disabled;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: white;
|
||||
}
|
||||
|
|
|
@ -10,12 +10,9 @@ import PageFrame from '~/components/layout/PageFrame'
|
|||
import { history, store } from '~/store'
|
||||
import theme from '~/theme/mui'
|
||||
import AppRoutes from '~/routes'
|
||||
import fetchSafes from '~/routes/safe/store/actions/fetchSafes'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
store.dispatch(fetchSafes())
|
||||
|
||||
const Root = () => (
|
||||
<Provider store={store}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { lg, md } from '~/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
manage: {
|
||||
fontSize: '24px',
|
||||
},
|
||||
close: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
const Send = ({ classes, onClose }: Props) => (
|
||||
<React.Fragment>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph className={classes.manage} noMargin>Receive Funds</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default withStyles(styles)(Send)
|
|
@ -0,0 +1,40 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { lg, md } from '~/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
manage: {
|
||||
fontSize: '24px',
|
||||
},
|
||||
close: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
const Send = ({ classes, onClose }: Props) => (
|
||||
<React.Fragment>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph className={classes.manage} noMargin>Send Funds</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default withStyles(styles)(Send)
|
|
@ -0,0 +1,127 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import SearchBar from 'material-ui-search-bar'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import Search from '@material-ui/icons/Search'
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Divider from '~/components/layout/Divider'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Spacer from '~/components/Spacer'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { lg, md, sm, xs, mediumFontSize } from '~/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
manage: {
|
||||
fontSize: '24px',
|
||||
},
|
||||
actions: {
|
||||
height: '50px',
|
||||
},
|
||||
close: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
search: {
|
||||
color: '#a2a8ba',
|
||||
paddingLeft: sm,
|
||||
},
|
||||
padding: {
|
||||
padding: `0 ${md}`,
|
||||
},
|
||||
add: {
|
||||
fontWeight: 'normal',
|
||||
paddingRight: md,
|
||||
paddingLeft: md,
|
||||
},
|
||||
searchInput: {
|
||||
backgroundColor: 'transparent',
|
||||
lineHeight: 'initial',
|
||||
fontSize: mediumFontSize,
|
||||
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',
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
fontSize: mediumFontSize,
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
},
|
||||
searchIcon: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent !important',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
}
|
||||
|
||||
class Tokens extends React.Component<Props> {
|
||||
requestSearch = () => {
|
||||
// eslint-disable-next-line
|
||||
console.log("Filtering by name or symbol...")
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClose, classes } = this.props
|
||||
const searchClasses = {
|
||||
input: classes.searchInput,
|
||||
root: classes.searchRoot,
|
||||
iconButton: classes.searchIcon,
|
||||
searchContainer: classes.searchContainer,
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row align="center" grow className={classes.heading}>
|
||||
<Paragraph className={classes.manage} noMargin>Manage Tokens</Paragraph>
|
||||
<IconButton onClick={onClose} disableRipple>
|
||||
<Close className={classes.close} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row align="center" className={classNames(classes.padding, classes.actions)}>
|
||||
<Search className={classes.search} />
|
||||
<SearchBar
|
||||
placeholder="Search by name or symbol"
|
||||
classes={searchClasses}
|
||||
onRequestSearch={this.requestSearch}
|
||||
searchIcon={<div />}
|
||||
/>
|
||||
<Spacer />
|
||||
<Divider />
|
||||
<Spacer />
|
||||
<Button variant="contained" size="small" color="secondary" className={classes.add}>
|
||||
+ ADD CUSTOM TOKEN
|
||||
</Button>
|
||||
</Row>
|
||||
<Hairline />
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Tokens)
|
|
@ -0,0 +1,84 @@
|
|||
// @flow
|
||||
import { List } from 'immutable'
|
||||
import { buildOrderFieldFrom, FIXED, type SortRow } from '~/components/Table/sorting'
|
||||
import { type Column } from '~/components/Table/TableHead'
|
||||
|
||||
export const BALANCE_TABLE_ASSET_ID = 'asset'
|
||||
export const BALANCE_TABLE_BALANCE_ID = 'balance'
|
||||
export const BALANCE_TABLE_VALUE_ID = 'value'
|
||||
|
||||
type BalanceData = {
|
||||
asset: string,
|
||||
balance: string,
|
||||
}
|
||||
|
||||
export type BalanceRow = SortRow<BalanceData>
|
||||
|
||||
export const getBalanceData = (): Array<BalanceRow> => [
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'CVL Journalism',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '234 CVL',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 234,
|
||||
},
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'ABC Periodico',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '1.394 ABC',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 1.394,
|
||||
},
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'Ethereum',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '9.394 ETH',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 9.394,
|
||||
[FIXED]: true,
|
||||
},
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'Gnosis',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '0.599 GNO',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 0.559,
|
||||
},
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'OmiseGO',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '39.922 OMG',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 39.922,
|
||||
},
|
||||
{
|
||||
[BALANCE_TABLE_ASSET_ID]: 'Moe Feo',
|
||||
[BALANCE_TABLE_BALANCE_ID]: '0 MOE',
|
||||
[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)]: 0,
|
||||
},
|
||||
]
|
||||
|
||||
export const generateColumns = () => {
|
||||
const assetRow: Column = {
|
||||
id: BALANCE_TABLE_ASSET_ID,
|
||||
order: false,
|
||||
numeric: false,
|
||||
disablePadding: false,
|
||||
label: 'Asset',
|
||||
custom: false,
|
||||
width: 250,
|
||||
}
|
||||
|
||||
const balanceRow: Column = {
|
||||
id: BALANCE_TABLE_BALANCE_ID,
|
||||
order: true,
|
||||
numeric: true,
|
||||
disablePadding: false,
|
||||
label: 'Balance',
|
||||
custom: false,
|
||||
}
|
||||
|
||||
const actions: Column = {
|
||||
id: 'actions',
|
||||
order: false,
|
||||
numeric: false,
|
||||
disablePadding: false,
|
||||
label: '',
|
||||
custom: true,
|
||||
}
|
||||
|
||||
return List([assetRow, balanceRow, actions])
|
||||
}
|
||||
|
||||
export const filterByZero = (data: Array<BalanceRow>, hideZero: boolean): Array<BalanceRow> =>
|
||||
data.filter((row: BalanceRow) => (hideZero ? row[buildOrderFieldFrom(BALANCE_TABLE_BALANCE_ID)] !== 0 : true))
|
|
@ -0,0 +1,192 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import CallReceived from '@material-ui/icons/CallReceived'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import Checkbox from '@material-ui/core/Checkbox'
|
||||
import TableRow from '@material-ui/core/TableRow'
|
||||
import TableCell from '@material-ui/core/TableCell'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Modal from '~/components/Modal'
|
||||
import { type Column, cellWidth } from '~/components/Table/TableHead'
|
||||
import Table from '~/components/Table'
|
||||
import { sm, xs } from '~/theme/variables'
|
||||
import { getBalanceData, generateColumns, BALANCE_TABLE_ASSET_ID, type BalanceRow, filterByZero } from './dataFetcher'
|
||||
import Tokens from './Tokens'
|
||||
import Send from './Send'
|
||||
import Receive from './Receive'
|
||||
|
||||
type State = {
|
||||
hideZero: boolean,
|
||||
showToken: boolean,
|
||||
showReceive: boolean,
|
||||
showSend: boolean,
|
||||
}
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
width: '20px',
|
||||
marginRight: sm,
|
||||
},
|
||||
zero: {
|
||||
letterSpacing: '-0.5px',
|
||||
},
|
||||
message: {
|
||||
margin: `${sm} 0`,
|
||||
},
|
||||
actionIcon: {
|
||||
marginRight: theme.spacing.unit,
|
||||
},
|
||||
iconSmall: {
|
||||
fontSize: 16,
|
||||
},
|
||||
hide: {
|
||||
'&:hover': {
|
||||
backgroundColor: '#fff3e2',
|
||||
},
|
||||
'&:hover $actions': {
|
||||
visibility: 'initial',
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'flex-end',
|
||||
visibility: 'hidden',
|
||||
},
|
||||
send: {
|
||||
minWidth: '0px',
|
||||
marginRight: sm,
|
||||
width: '70px',
|
||||
},
|
||||
receive: {
|
||||
minWidth: '0px',
|
||||
width: '95px',
|
||||
},
|
||||
leftIcon: {
|
||||
marginRight: xs,
|
||||
},
|
||||
links: {
|
||||
textDecoration: 'underline',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
granted: boolean,
|
||||
}
|
||||
|
||||
type Action = 'Token' | 'Send' | 'Receive'
|
||||
|
||||
class Balances extends React.Component<Props, State> {
|
||||
state = {
|
||||
hideZero: false,
|
||||
showToken: false,
|
||||
showSend: false,
|
||||
showReceive: false,
|
||||
}
|
||||
|
||||
onShow = (action: Action) => () => {
|
||||
this.setState(() => ({ [`show${action}`]: true }))
|
||||
}
|
||||
|
||||
onHide = (action: Action) => () => {
|
||||
this.setState(() => ({ [`show${action}`]: false }))
|
||||
}
|
||||
|
||||
handleChange = (e: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
const { checked } = e.target
|
||||
|
||||
this.setState(() => ({ hideZero: checked }))
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
hideZero, showToken, showReceive, showSend,
|
||||
} = this.state
|
||||
const { classes, granted } = this.props
|
||||
|
||||
const columns = generateColumns()
|
||||
const autoColumns = columns.filter(c => !c.custom)
|
||||
const checkboxClasses = {
|
||||
root: classes.root,
|
||||
}
|
||||
|
||||
const filteredData = filterByZero(getBalanceData(), hideZero)
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Row align="center" className={classes.message}>
|
||||
<Col xs={6}>
|
||||
<Checkbox
|
||||
classes={checkboxClasses}
|
||||
checked={hideZero}
|
||||
onChange={this.handleChange}
|
||||
color="secondary"
|
||||
disableRipple
|
||||
/>
|
||||
<Paragraph className={classes.zero}>Hide zero balances</Paragraph>
|
||||
</Col>
|
||||
<Col xs={6} end="sm">
|
||||
<Paragraph noMargin size="md" color="secondary" className={classes.links} onClick={this.onShow('Token')}>
|
||||
Manage Tokens
|
||||
</Paragraph>
|
||||
<Modal
|
||||
title="Manage Tokens"
|
||||
description="Enable and disable tokens to be listed"
|
||||
handleClose={this.onHide('Token')}
|
||||
open={showToken}
|
||||
>
|
||||
<Tokens onClose={this.onHide('Token')} />
|
||||
</Modal>
|
||||
</Col>
|
||||
</Row>
|
||||
<Table
|
||||
label="Balances"
|
||||
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
size={filteredData.length}
|
||||
defaultFixed
|
||||
>
|
||||
{(sortedData: Array<BalanceRow>) => sortedData.map((row: any, index: number) => (
|
||||
<TableRow tabIndex={-1} key={index} className={classes.hide}>
|
||||
{ autoColumns.map((column: Column) => (
|
||||
<TableCell key={column.id} style={cellWidth(column.width)} numeric={column.numeric} component="td">
|
||||
{row[column.id]}
|
||||
</TableCell>
|
||||
)) }
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{ granted &&
|
||||
<Button variant="contained" size="small" color="secondary" className={classes.send} onClick={this.onShow('Send')}>
|
||||
<CallMade className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Send
|
||||
</Button>
|
||||
}
|
||||
<Button variant="contained" size="small" color="secondary" className={classes.receive} onClick={this.onShow('Receive')}>
|
||||
<CallReceived className={classNames(classes.leftIcon, classes.iconSmall)} />
|
||||
Receive
|
||||
</Button>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
<Modal title="Send Tokens" description="Send Tokens Form" handleClose={this.onHide('Send')} open={showSend}>
|
||||
<Send onClose={this.onHide('Send')} />
|
||||
</Modal>
|
||||
<Modal title="Receive Tokens" description="Receive Tokens Form" handleClose={this.onHide('Receive')} open={showReceive}>
|
||||
<Receive onClose={this.onHide('Receive')} />
|
||||
</Modal>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Balances)
|
|
@ -1,20 +1,131 @@
|
|||
// @flow
|
||||
import * as React from 'react'
|
||||
import OpenInNew from '@material-ui/icons/OpenInNew'
|
||||
import Tabs from '@material-ui/core/Tabs'
|
||||
import Tab from '@material-ui/core/Tab'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import Heading from '~/components/layout/Heading'
|
||||
import Row from '~/components/layout/Row'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import NoSafe from '~/components/NoSafe'
|
||||
import { type SelectorProps } from '~/routes/safe/container/selector'
|
||||
import GnoSafe from './Safe'
|
||||
import { openAddressInEtherScan } from '~/logic/wallets/getWeb3'
|
||||
import { sm, xs, secondary, smallFontSize } from '~/theme/variables'
|
||||
import Balances from './Balances'
|
||||
|
||||
type Props = SelectorProps
|
||||
type Props = SelectorProps & {
|
||||
classes: Object,
|
||||
granted: boolean,
|
||||
}
|
||||
|
||||
const Layout = ({
|
||||
safe, activeTokens, provider, userAddress,
|
||||
}: Props) => (
|
||||
<React.Fragment>
|
||||
{ safe
|
||||
? <GnoSafe safe={safe} tokens={activeTokens} userAddress={userAddress} />
|
||||
: <NoSafe provider={provider} text="Not found safe" />
|
||||
type State = {
|
||||
value: number,
|
||||
}
|
||||
|
||||
const openIconStyle = {
|
||||
height: '16px',
|
||||
color: secondary,
|
||||
}
|
||||
|
||||
const styles = () => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
name: {
|
||||
marginLeft: sm,
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
user: {
|
||||
justifyContent: 'left',
|
||||
},
|
||||
open: {
|
||||
paddingLeft: sm,
|
||||
width: 'auto',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
readonly: {
|
||||
fontSize: smallFontSize,
|
||||
letterSpacing: '0.5px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#a2a8ba',
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
textTransform: 'uppercase',
|
||||
padding: `0 ${sm}`,
|
||||
marginLeft: sm,
|
||||
borderRadius: xs,
|
||||
lineHeight: '28px',
|
||||
},
|
||||
})
|
||||
|
||||
class Layout extends React.Component<Props, State> {
|
||||
state = {
|
||||
value: 0,
|
||||
}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
handleChange = (event, value) => {
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
safe, provider, network, classes, granted,
|
||||
} = this.props
|
||||
const { value } = this.state
|
||||
|
||||
if (!safe) {
|
||||
return <NoSafe provider={provider} text="Not found safe" />
|
||||
}
|
||||
// <GnoSafe safe={safe} tokens={activeTokens} userAddress={userAddress} />
|
||||
const address = safe.get('address')
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Block className={classes.container} margin="xl">
|
||||
<Identicon address={address} diameter={50} />
|
||||
<Block className={classes.name}>
|
||||
<Row>
|
||||
<Heading tag="h2" color="secondary">{safe.get('name')}</Heading>
|
||||
{ !granted &&
|
||||
<Block className={classes.readonly} >
|
||||
Read Only
|
||||
</Block>
|
||||
}
|
||||
</Row>
|
||||
<Block align="center" className={classes.user}>
|
||||
<Paragraph size="md" color="disabled" noMargin>{address}</Paragraph>
|
||||
<OpenInNew
|
||||
className={classes.open}
|
||||
style={openIconStyle}
|
||||
onClick={openAddressInEtherScan(address, network)}
|
||||
/>
|
||||
</Block>
|
||||
</Block>
|
||||
</Block>
|
||||
<Row>
|
||||
<Tabs
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
indicatorColor="secondary"
|
||||
textColor="secondary"
|
||||
>
|
||||
<Tab label="Balances" />
|
||||
<Tab label="Transactions" />
|
||||
<Tab label="Settings" />
|
||||
</Tabs>
|
||||
</Row>
|
||||
<Hairline color="#c8ced4" />
|
||||
{value === 0 && <Balances granted={granted} />}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Layout)
|
||||
|
|
|
@ -3,7 +3,6 @@ import * as React from 'react'
|
|||
import { connect } from 'react-redux'
|
||||
import Page from '~/components/layout/Page'
|
||||
import Layout from '~/routes/safe/component/Layout'
|
||||
import NoRights from '~/routes/safe/component/NoRights'
|
||||
import selector, { type SelectorProps } from './selector'
|
||||
import actions, { type Actions } from './actions'
|
||||
|
||||
|
@ -15,16 +14,11 @@ const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 15000
|
|||
|
||||
class SafeView extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchSafe(this.props.safeUrl)
|
||||
this.intervalId = setInterval(() => {
|
||||
const {
|
||||
safe, fetchTokens, fetchSafe,
|
||||
} = this.props
|
||||
if (!safe) {
|
||||
return
|
||||
}
|
||||
const safeAddress = safe.get('address')
|
||||
fetchTokens(safeAddress)
|
||||
fetchSafe(safe)
|
||||
const { safeUrl, fetchTokens, fetchSafe } = this.props
|
||||
fetchTokens(safeUrl)
|
||||
fetchSafe(safeUrl)
|
||||
}, TIMEOUT)
|
||||
}
|
||||
|
||||
|
@ -47,15 +41,19 @@ class SafeView extends React.PureComponent<Props> {
|
|||
|
||||
render() {
|
||||
const {
|
||||
safe, provider, activeTokens, granted, userAddress,
|
||||
safe, provider, activeTokens, granted, userAddress, network,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{ granted
|
||||
? <Layout activeTokens={activeTokens} provider={provider} safe={safe} userAddress={userAddress} />
|
||||
: <NoRights />
|
||||
}
|
||||
<Layout
|
||||
activeTokens={activeTokens}
|
||||
provider={provider}
|
||||
safe={safe}
|
||||
userAddress={userAddress}
|
||||
network={network}
|
||||
granted={granted}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,22 @@
|
|||
import { List } from 'immutable'
|
||||
import { createSelector, createStructuredSelector, type Selector } from 'reselect'
|
||||
import { safeSelector, type RouterProps, type SafeSelectorProps } from '~/routes/safe/store/selectors'
|
||||
import { providerNameSelector, userAccountSelector } from '~/logic/wallets/store/selectors'
|
||||
import { providerNameSelector, userAccountSelector, networkSelector } from '~/logic/wallets/store/selectors'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { type Owner } from '~/routes/safe/store/model/owner'
|
||||
import { type GlobalState } from '~/store'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { activeTokensSelector } from '~/routes/tokens/store/selectors'
|
||||
import { type Token } from '~/routes/tokens/store/model/token'
|
||||
import { safeParamAddressSelector } from '../store/selectors'
|
||||
|
||||
export type SelectorProps = {
|
||||
safe: SafeSelectorProps,
|
||||
provider: string,
|
||||
activeTokens: List<Token>,
|
||||
userAddress: string,
|
||||
network: string,
|
||||
safeUrl: string,
|
||||
}
|
||||
|
||||
export const grantedSelector: Selector<GlobalState, RouterProps, boolean> = createSelector(
|
||||
|
@ -44,4 +47,6 @@ export default createStructuredSelector({
|
|||
activeTokens: activeTokensSelector,
|
||||
granted: grantedSelector,
|
||||
userAddress: userAccountSelector,
|
||||
network: networkSelector,
|
||||
safeUrl: safeParamAddressSelector,
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@ import { makeOwner, type Owner } from '~/routes/safe/store/model/owner'
|
|||
|
||||
export const ADD_SAFE = 'ADD_SAFE'
|
||||
|
||||
export const buildOwnersFrom = (names: string[], addresses: string[]) => {
|
||||
export const buildOwnersFrom = (names: Array<string>, addresses: Array<string>) => {
|
||||
const owners = names.map((name: string, index: number) => makeOwner({ name, address: addresses[index] }))
|
||||
|
||||
return List(owners)
|
||||
|
|
|
@ -3,10 +3,11 @@ import type { Dispatch as ReduxDispatch } from 'redux'
|
|||
import { List, Map } from 'immutable'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { makeOwner } from '~/routes/safe/store/model/owner'
|
||||
import { type SafeProps, type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import { type SafeProps, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import updateSafe from '~/routes/safe/store/actions/updateSafe'
|
||||
import { getOwners } from '~/utils/localStorage'
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { getOwners, getSafeName } from '~/utils/localStorage'
|
||||
import { getGnosisSafeContract } from '~/logic/contracts/safeContracts'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
|
||||
const buildOwnersFrom = (safeOwners: string[], storedOwners: Map<string, string>) => (
|
||||
safeOwners.map((ownerAddress: string) => {
|
||||
|
@ -15,16 +16,17 @@ const buildOwnersFrom = (safeOwners: string[], storedOwners: Map<string, string>
|
|||
})
|
||||
)
|
||||
|
||||
export const buildSafe = async (storedSafe: Object) => {
|
||||
const safeAddress = storedSafe.address
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
export const buildSafe = async (safeAddress: string, safeName: string) => {
|
||||
const web3 = getWeb3()
|
||||
const GnosisSafe = await getGnosisSafeContract(web3)
|
||||
const gnosisSafe = GnosisSafe.at(safeAddress)
|
||||
|
||||
const threshold = Number(await gnosisSafe.getThreshold())
|
||||
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), getOwners(safeAddress)))
|
||||
|
||||
const safe: SafeProps = {
|
||||
address: safeAddress,
|
||||
name: storedSafe.name,
|
||||
name: safeName,
|
||||
threshold,
|
||||
owners,
|
||||
}
|
||||
|
@ -32,9 +34,10 @@ export const buildSafe = async (storedSafe: Object) => {
|
|||
return makeSafe(safe)
|
||||
}
|
||||
|
||||
export default (safe: Safe) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
export default (safeAddress: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
try {
|
||||
const safeRecord = await buildSafe(safe.toJSON())
|
||||
const safeName = getSafeName(safeAddress) || 'LOADED SAFE'
|
||||
const safeRecord = await buildSafe(safeAddress, safeName)
|
||||
|
||||
return dispatch(updateSafe(safeRecord))
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import { Map } from 'immutable'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { load, SAFES_KEY } from '~/utils/localStorage'
|
||||
import updateSafes from '~/routes/safe/store/actions/updateSafes'
|
||||
import { buildSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||
import { type Safe } from '~/routes/safe/store/model/safe'
|
||||
|
||||
const buildSafesFrom = async (loadedSafes: Object): Promise<Map<string, Safe>> => {
|
||||
const safes = Map()
|
||||
|
||||
const keys = Object.keys(loadedSafes)
|
||||
try {
|
||||
const safeRecords = await Promise.all(keys.map((address: string) => buildSafe(loadedSafes[address])))
|
||||
|
||||
return safes.withMutations(async (map) => {
|
||||
safeRecords.forEach((safe: Safe) => map.set(safe.get('address'), safe))
|
||||
})
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Error while fetching safes information")
|
||||
|
||||
return Map()
|
||||
}
|
||||
}
|
||||
|
||||
export default () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const storedSafes = load(SAFES_KEY)
|
||||
|
||||
const safes = storedSafes ? await buildSafesFrom(storedSafes) : Map()
|
||||
|
||||
return dispatch(updateSafes(safes))
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
// @flow
|
||||
export * from './addSafe'
|
||||
export { default as addSafe } from './addSafe'
|
|
@ -1,33 +1,70 @@
|
|||
// @flow
|
||||
import { Map } from 'immutable'
|
||||
import { handleActions, type ActionType } from 'redux-actions'
|
||||
import addSafe, { ADD_SAFE } from '~/routes/safe/store/actions/addSafe'
|
||||
import { type Safe, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import { saveSafes, setOwners } from '~/utils/localStorage'
|
||||
import updateSafes, { UPDATE_SAFES } from '~/routes/safe/store/actions/updateSafes'
|
||||
import addSafe, { ADD_SAFE, buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
||||
import { type Safe, type SafeProps, makeSafe } from '~/routes/safe/store/model/safe'
|
||||
import { type OwnerProps } from '~/routes/safe/store/model/owner'
|
||||
import { saveSafes, setOwners, load, SAFES_KEY } from '~/utils/localStorage'
|
||||
import updateSafe, { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
|
||||
export type State = Map<string, Safe>
|
||||
|
||||
/*
|
||||
type Action<T> = {
|
||||
key: string,
|
||||
payload: T,
|
||||
};
|
||||
export const buildSafe = (storedSafe: SafeProps) => {
|
||||
const names = storedSafe.owners.map((owner: OwnerProps) => owner.name)
|
||||
const addresses = storedSafe.owners.map((owner: OwnerProps) => owner.address)
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
|
||||
type AddSafeType = Action<SafeProps>
|
||||
const safe: SafeProps = {
|
||||
address: storedSafe.address,
|
||||
name: storedSafe.name,
|
||||
threshold: storedSafe.threshold,
|
||||
owners,
|
||||
}
|
||||
|
||||
return makeSafe(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: Safe) => map.set(safe.get('address'), safe))
|
||||
})
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Error while fetching safes information")
|
||||
|
||||
return Map()
|
||||
}
|
||||
}
|
||||
|
||||
export const safesInitialState = (): State => {
|
||||
const storedSafes = load(SAFES_KEY)
|
||||
const safes = storedSafes ? buildSafesFrom(storedSafes) : Map()
|
||||
|
||||
return safes
|
||||
}
|
||||
|
||||
action: AddSafeType
|
||||
*/
|
||||
|
||||
export default handleActions({
|
||||
[UPDATE_SAFE]: (state: State, action: ActionType<typeof updateSafe>): State =>
|
||||
state.update(action.payload.get('address'), prevSafe =>
|
||||
(prevSafe.equals(action.payload) ? prevSafe : action.payload)),
|
||||
[UPDATE_SAFES]: (state: State, action: ActionType<typeof updateSafes>): State =>
|
||||
action.payload,
|
||||
[UPDATE_SAFE]: (state: State, action: ActionType<typeof updateSafe>): State => {
|
||||
const safe = action.payload
|
||||
const safeAddress = safe.get('address')
|
||||
|
||||
const hasSafe = !!state.get(safeAddress)
|
||||
if (hasSafe) {
|
||||
return state.update(safeAddress, prevSafe =>
|
||||
(prevSafe.equals(safe) ? prevSafe : safe))
|
||||
}
|
||||
|
||||
return state.set(safeAddress, safe)
|
||||
},
|
||||
[ADD_SAFE]: (state: State, action: ActionType<typeof addSafe>): State => {
|
||||
const safe: Safe = makeSafe(action.payload)
|
||||
setOwners(safe.get('address'), safe.get('owners'))
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { buildOwnersFrom } from '~/routes/safe/store/actions'
|
||||
import { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
||||
|
||||
class SafeBuilder {
|
||||
safe: Safe
|
||||
|
|
|
@ -8,6 +8,7 @@ import { type Owner } from '~/routes/safe/store/model/owner'
|
|||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
|
||||
export const safesMapSelector = (state: GlobalState): Map<string, Safe> => state.safes
|
||||
|
||||
const safesListSelector: Selector<GlobalState, {}, List<Safe>> = createSelector(
|
||||
safesMapSelector,
|
||||
(safes: Map<string, Safe>): List<Safe> => safes.toList(),
|
||||
|
|
|
@ -4,7 +4,7 @@ import { routerMiddleware, routerReducer } from 'react-router-redux'
|
|||
import { combineReducers, createStore, applyMiddleware, compose, type Reducer, type Store } 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 State as SafeState, safesInitialState } from '~/routes/safe/store/reducer/safe'
|
||||
import tokens, { TOKEN_REDUCER_ID, type State as TokensState } from '~/routes/tokens/store/reducer/tokens'
|
||||
import transactions, { type State as TransactionsState, TRANSACTIONS_REDUCER_ID } from '~/routes/safe/store/reducer/transactions'
|
||||
|
||||
|
@ -24,6 +24,8 @@ export type GlobalState = {
|
|||
transactions: TransactionsState,
|
||||
}
|
||||
|
||||
export type GetState = () => GlobalState
|
||||
|
||||
const reducers: Reducer<GlobalState> = combineReducers({
|
||||
routing: routerReducer,
|
||||
[PROVIDER_REDUCER_ID]: provider,
|
||||
|
@ -32,13 +34,11 @@ const reducers: Reducer<GlobalState> = combineReducers({
|
|||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||
})
|
||||
|
||||
/*
|
||||
const initialState = {
|
||||
[TRANSACTIONS_REDUCER_ID]: transactionsInitialState(),
|
||||
[SAFE_REDUCER_ID]: safesInitialState(),
|
||||
}
|
||||
*/
|
||||
|
||||
export const store: Store<GlobalState> = createStore(reducers, {}, finalCreateStore)
|
||||
export const store: Store<GlobalState> = createStore(reducers, initialState, finalCreateStore)
|
||||
|
||||
export const aNewStore = (localState?: Object): Store<GlobalState> =>
|
||||
createStore(reducers, localState, finalCreateStore)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
// @flow
|
||||
import { makeSafe, type Safe } from '~/routes/safe/store/model/safe'
|
||||
import { buildOwnersFrom } from '~/routes/safe/store/actions'
|
||||
import addSafe, { buildOwnersFrom } from '~/routes/safe/store/actions/addSafe'
|
||||
import { FIELD_NAME, FIELD_CONFIRMATIONS, FIELD_OWNERS, getOwnerNameBy, getOwnerAddressBy } from '~/routes/open/components/fields'
|
||||
import { getWeb3, getProviderInfo } from '~/logic/wallets/getWeb3'
|
||||
import { promisify } from '~/utils/promisify'
|
||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||
import { createSafe, type OpenState } from '~/routes/open/container/Open'
|
||||
import { type GlobalState } from '~/store/index'
|
||||
import { makeProvider } from '~/logic/wallets/store/model/provider'
|
||||
|
|
|
@ -120,7 +120,7 @@ describe('React DOM TESTS > Add and remove owners', () => {
|
|||
await assureThresholdIs(gnosisSafe, 1)
|
||||
await assureOwnersAre(gnosisSafe, accounts[2], accounts[0], accounts[1])
|
||||
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), address)
|
||||
expect(safe.get('owners').count()).toBe(3)
|
||||
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
|
||||
|
@ -149,7 +149,7 @@ describe('React DOM TESTS > Add and remove owners', () => {
|
|||
await assureThresholdIs(gnosisSafe, 2)
|
||||
await assureOwnersAre(gnosisSafe, accounts[2], accounts[0], accounts[1])
|
||||
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), address)
|
||||
expect(safe.get('owners').count()).toBe(3)
|
||||
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
|
||||
|
@ -179,7 +179,7 @@ describe('React DOM TESTS > Add and remove owners', () => {
|
|||
await assureThresholdIs(gnosisSafe, 1)
|
||||
await assureOwnersAre(gnosisSafe, accounts[0])
|
||||
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), address)
|
||||
expect(safe.get('owners').count()).toBe(1)
|
||||
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
|
||||
|
@ -204,7 +204,7 @@ describe('React DOM TESTS > Add and remove owners', () => {
|
|||
await assureThresholdIs(gnosisSafe, 1)
|
||||
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), address)
|
||||
expect(safe.get('owners').count()).toBe(2)
|
||||
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
|
||||
|
@ -230,7 +230,7 @@ describe('React DOM TESTS > Add and remove owners', () => {
|
|||
await assureThresholdIs(gnosisSafe, 2)
|
||||
await assureOwnersAre(gnosisSafe, accounts[0], accounts[1])
|
||||
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), address)
|
||||
expect(safe.get('owners').count()).toBe(2)
|
||||
await assureOwnersAre(gnosisSafe, ...getAddressesFrom(safe))
|
||||
|
|
|
@ -36,12 +36,12 @@ describe('Transactions Suite', () => {
|
|||
const executor = accounts[0]
|
||||
const nonce = await gnosisSafe.nonce()
|
||||
const firstTxHash = await createTransaction(safe, 'Add Owner Second account', safeAddress, 0, nonce, executor, firstTxData)
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), safeAddress)
|
||||
|
||||
const secondTxData = gnosisSafe.contract.addOwnerWithThreshold.getData(accounts[2], 2)
|
||||
const secondTxHash = await createTransaction(safe, 'Add Owner Third account', safeAddress, 0, nonce + 100, executor, secondTxData)
|
||||
await store.dispatch(fetchSafe(safe))
|
||||
await store.dispatch(fetchSafe(safe.get('address')))
|
||||
safe = getSafeFrom(store.getState(), safeAddress)
|
||||
|
||||
// WHEN
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import red from '@material-ui/core/colors/red'
|
||||
import { createMuiTheme } from '@material-ui/core/styles'
|
||||
import { largeFontSize, mediumFontSize, smallFontSize, disabled, primary, secondary, md, lg, background } from './variables'
|
||||
import { largeFontSize, mediumFontSize, smallFontSize, disabled, primary, secondary, md, lg, background, bolderFont, boldFont } from './variables'
|
||||
|
||||
export type WithStyles = {
|
||||
classes: Object,
|
||||
|
@ -125,6 +125,62 @@ export default createMuiTheme({
|
|||
color: primary,
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
root: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
selected: {
|
||||
fontWeight: bolderFont,
|
||||
},
|
||||
},
|
||||
MuiTablePagination: {
|
||||
toolbar: {
|
||||
'& > span:nth-child(2)': {
|
||||
order: 1,
|
||||
},
|
||||
},
|
||||
selectIcon: {
|
||||
height: '100%',
|
||||
top: '0px',
|
||||
},
|
||||
caption: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
letterSpacing: '-0.5px',
|
||||
order: 3,
|
||||
color: disabled,
|
||||
},
|
||||
input: {
|
||||
order: 2,
|
||||
width: '60px',
|
||||
padding: `0 ${md} 0 0`,
|
||||
},
|
||||
actions: {
|
||||
order: 4,
|
||||
color: disabled,
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
root: {
|
||||
fontFamily: 'Roboto Mono, monospace',
|
||||
},
|
||||
head: {
|
||||
letterSpacing: '1px',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: boldFont,
|
||||
},
|
||||
body: {
|
||||
color: primary,
|
||||
letterSpacing: '-0.5px',
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
},
|
||||
MuiBackdrop: {
|
||||
root: {
|
||||
backdropFilter: 'blur(1px)',
|
||||
backgroundColor: 'rgba(228, 232, 241, 0.75)',
|
||||
},
|
||||
},
|
||||
},
|
||||
palette,
|
||||
})
|
||||
|
|
|
@ -23,6 +23,16 @@ export const load = (key: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getSafeName = (safeAddress: string) => {
|
||||
const safes = load(SAFES_KEY)
|
||||
if (!safes) {
|
||||
return undefined
|
||||
}
|
||||
const safe = safes[safeAddress]
|
||||
|
||||
return safe ? safe.name : undefined
|
||||
}
|
||||
|
||||
export const saveSafes = (safes: Object) => {
|
||||
try {
|
||||
const serializedState = JSON.stringify(safes)
|
||||
|
|
|
@ -7975,6 +7975,13 @@ material-colors@^1.2.1:
|
|||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
|
||||
|
||||
material-ui-search-bar@^1.0.0-beta.13:
|
||||
version "1.0.0-beta.13"
|
||||
resolved "https://registry.yarnpkg.com/material-ui-search-bar/-/material-ui-search-bar-1.0.0-beta.13.tgz#08c246431666f91c3ca52df78987b86352783ee1"
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
math-expression-evaluator@^1.2.14:
|
||||
version "1.2.17"
|
||||
resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac"
|
||||
|
|
Loading…
Reference in New Issue