diff --git a/src/components/Table/index.jsx b/src/components/Table/index.jsx index d05376bb94..3f3b42bbed 100644 --- a/src/components/Table/index.jsx +++ b/src/components/Table/index.jsx @@ -24,6 +24,7 @@ type Props = { defaultOrder: 'desc' | 'asc', noBorder: boolean, disablePagination: boolean, + disableLoadingOnEmptyTable?: boolean, } type State = { @@ -154,6 +155,7 @@ class GnoTable extends React.Component, State> { defaultFixed, defaultRowsPerPage, noBorder, + disableLoadingOnEmptyTable, } = this.props const { order, orderBy, page, orderProp, rowsPerPage, fixed, @@ -176,7 +178,7 @@ class GnoTable extends React.Component, State> { } const emptyRows = displayRows - Math.min(displayRows, data.size - page * displayRows) - const isEmpty = size === 0 + const isEmpty = size === 0 && !disableLoadingOnEmptyTable return ( <> diff --git a/src/logic/addressBook/model/addressBook.js b/src/logic/addressBook/model/addressBook.js new file mode 100644 index 0000000000..2ddc497b2e --- /dev/null +++ b/src/logic/addressBook/model/addressBook.js @@ -0,0 +1,13 @@ +// @flow +import type { RecordOf } from 'immutable' + +export type AddressBookEntry = { + address: string; + name: string; +} + +export type AddressBookProps = { + addressBookList: AddressBookEntry[] +} + +export type AddressBook = RecordOf diff --git a/src/logic/addressBook/store/actions/addAddressBook.js b/src/logic/addressBook/store/actions/addAddressBook.js new file mode 100644 index 0000000000..90f388fcd4 --- /dev/null +++ b/src/logic/addressBook/store/actions/addAddressBook.js @@ -0,0 +1,7 @@ +// @flow +import { createAction } from 'redux-actions' +import type { AddressBook } from '~/logic/addressBook/model/addressBook' + +export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK' + +export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook: AddressBook) => ({ addressBook })) diff --git a/src/logic/addressBook/store/actions/loadAddressBook.js b/src/logic/addressBook/store/actions/loadAddressBook.js new file mode 100644 index 0000000000..2fd79627fc --- /dev/null +++ b/src/logic/addressBook/store/actions/loadAddressBook.js @@ -0,0 +1,18 @@ +// @flow +import type { Dispatch as ReduxDispatch } from 'redux' +import { type GlobalState } from '~/store/index' +import { getAddressBookFromStorage } from '~/logic/addressBook/utils' +import { addAddressBook } from '~/logic/addressBook/store/actions/addAddressBook' + +const loadAddressBook = () => async (dispatch: ReduxDispatch) => { + try { + const addressBook = await getAddressBookFromStorage() + + dispatch(addAddressBook(addressBook)) + } catch (err) { + // eslint-disable-next-line + console.error('Error while loading active tokens from storage:', err) + } +} + +export default loadAddressBook diff --git a/src/logic/addressBook/store/reducer/addressBook.js b/src/logic/addressBook/store/reducer/addressBook.js new file mode 100644 index 0000000000..cf0f46c9b7 --- /dev/null +++ b/src/logic/addressBook/store/reducer/addressBook.js @@ -0,0 +1,20 @@ +// @flow +import { Map } from 'immutable' +import { handleActions, type ActionType } from 'redux-actions' +import type { Cookie } from '~/logic/cookies/model/cookie' +import { ADD_ADDRESS_BOOK } from '~/logic/addressBook/store/actions/addAddressBook' + +export const ADDRESS_BOOK_REDUCER_ID = 'addressBook' + +export type State = Map> + +export default handleActions( + { + [ADD_ADDRESS_BOOK]: (state: State, action: ActionType): State => { + const { addressBook } = action.payload + + return state.set('addressBook', addressBook) + }, + }, + Map(), +) diff --git a/src/logic/addressBook/store/selectors/index.js b/src/logic/addressBook/store/selectors/index.js new file mode 100644 index 0000000000..fc82b3072c --- /dev/null +++ b/src/logic/addressBook/store/selectors/index.js @@ -0,0 +1,14 @@ +// @flow +import { List } from 'immutable' +import { createSelector, Selector } from 'reselect' +import type { Provider } from '~/logic/wallets/store/model/provider' +import { ADDRESS_BOOK_REDUCER_ID } from '~/logic/addressBook/store/reducer/addressBook' +import type { GlobalState } from '~/store' +import type { AddressBook } from '~/logic/addressBook/model/addressBook' + +export const getAddressBook = (state: any): Provider => state[ADDRESS_BOOK_REDUCER_ID].get('addressBook') || [] + +export const getAddressBookListSelector: Selector = createSelector( + getAddressBook, + (addressBook: AddressBook) => (addressBook ? List(addressBook) : List([])), +) diff --git a/src/logic/addressBook/utils/index.js b/src/logic/addressBook/utils/index.js new file mode 100644 index 0000000000..05ad89cb3f --- /dev/null +++ b/src/logic/addressBook/utils/index.js @@ -0,0 +1,21 @@ +// @flow +import { Map } from 'immutable' +import type { AddressBookProps } from '~/logic/addressBook/model/addressBook' +import { loadFromStorage, saveToStorage } from '~/utils/storage' +import type { Token } from '~/logic/tokens/store/model/token' + +const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY' + +export const getAddressBookFromStorage = async (): Promise => { + const data = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY) + + return data || [] +} + +export const saveAddressBook = async (tokens: Map) => { + try { + await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, tokens.toJS()) + } catch (err) { + console.error('Error storing tokens in localstorage', err) + } +} diff --git a/src/routes/safe/components/AddressBook/columns.js b/src/routes/safe/components/AddressBook/columns.js new file mode 100644 index 0000000000..d066421664 --- /dev/null +++ b/src/routes/safe/components/AddressBook/columns.js @@ -0,0 +1,44 @@ +// @flow +import { List } from 'immutable' +import { type Column } from '~/components/Table/TableHead' + +export const ADDRESS_BOOK_ROW_ID = 'address-book-row' +export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook' +export const AB_NAME_ID = 'name' +export const AB_ADDRESS_ID = 'address' +export const AB_ADDRESS_ACTIONS_ID = 'actions' +export const EDIT_ENTRY_BUTTON = 'edit-entry-btn' +export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn' +export const SEND_ENTRY_BUTTON = 'send-entry-btn' + + +export const generateColumns = () => { + const nameColumn: Column = { + id: AB_NAME_ID, + order: false, + disablePadding: false, + label: 'Name', + width: 150, + custom: false, + align: 'left', + } + + const addressColumn: Column = { + id: AB_ADDRESS_ID, + order: false, + disablePadding: false, + label: 'Address', + custom: false, + align: 'left', + } + + const actionsColumn: Column = { + id: AB_ADDRESS_ACTIONS_ID, + order: false, + disablePadding: false, + label: '', + custom: true, + } + + return List([nameColumn, addressColumn, actionsColumn]) +} diff --git a/src/routes/safe/components/AddressBook/index.jsx b/src/routes/safe/components/AddressBook/index.jsx new file mode 100644 index 0000000000..e1c688bf1a --- /dev/null +++ b/src/routes/safe/components/AddressBook/index.jsx @@ -0,0 +1,125 @@ +// @flow +import React, { useEffect } from 'react' + +import cn from 'classnames' +import { List } from 'immutable' +import TableRow from '@material-ui/core/TableRow' +import TableCell from '@material-ui/core/TableCell' +import { withStyles } from '@material-ui/core/styles' +import classNames from 'classnames/bind' +import CallMade from '@material-ui/icons/CallMade' +import { useDispatch, useSelector } from 'react-redux' +import Block from '~/components/layout/Block' +import Row from '~/components/layout/Row' +import { type Column, cellWidth } from '~/components/Table/TableHead' +import Table from '~/components/Table' +import Button from '~/components/layout/Button' + +import { styles } from './style' +import type { OwnerRow } from '~/routes/safe/components/Settings/ManageOwners/dataFetcher' +import OwnerAddressTableCell from '~/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell' +import Img from '~/components/layout/Img' +import RenameOwnerIcon from '~/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg' +import RemoveOwnerIcon from '~/routes/safe/components/Settings/assets/icons/bin.svg' +import { + AB_ADDRESS_ID, + ADDRESS_BOOK_ROW_ID, + EDIT_ENTRY_BUTTON, + generateColumns, REMOVE_ENTRY_BUTTON, SEND_ENTRY_BUTTON, +} from '~/routes/safe/components/AddressBook/columns' +import loadAddressBook from '~/logic/addressBook/store/actions/loadAddressBook' +import { getAddressBookListSelector } from '~/logic/addressBook/store/selectors' +import Col from '~/components/layout/Col' +import ButtonLink from '~/components/layout/ButtonLink' + + +type Props = { + classes: Object +} + + +const AddressBookTable = ({ + classes, +}: Props) => { + const columns = generateColumns() + const autoColumns = columns.filter((c) => !c.custom) + const dispatch = useDispatch() + useEffect(() => { + dispatch(loadAddressBook()) + }, []) + + const addressBook = useSelector(getAddressBookListSelector) + + return ( + <> + + + {}} testId="manage-tokens-btn"> + + Create entry + + + + + + {(sortedData: List) => sortedData.map((row: any, index: number) => ( + = 3 && index === sortedData.size - 1 && classes.noBorderBottom)} + data-testid={ADDRESS_BOOK_ROW_ID} + > + {autoColumns.map((column: Column) => ( + + {column.id === AB_ADDRESS_ID ? ( + + ) : ( + row[column.id] + )} + + ))} + + + Edit entry {}} + testId={EDIT_ENTRY_BUTTON} + /> + Remove entry {}} + testId={REMOVE_ENTRY_BUTTON} + /> + + + + + ))} +
+
+ + ) +} + +export default withStyles(styles)(AddressBookTable) diff --git a/src/routes/safe/components/AddressBook/style.js b/src/routes/safe/components/AddressBook/style.js new file mode 100644 index 0000000000..d032285b26 --- /dev/null +++ b/src/routes/safe/components/AddressBook/style.js @@ -0,0 +1,71 @@ +// @flow +import { + lg, md, sm, marginButtonImg, +} from '~/theme/variables' + +export const styles = () => ({ + formContainer: { + minHeight: '420px', + }, + title: { + padding: lg, + paddingBottom: 0, + }, + annotation: { + paddingLeft: lg, + }, + hide: { + '&:hover': { + backgroundColor: '#fff3e2', + }, + '&:hover $actions': { + visibility: 'initial', + }, + }, + actions: { + justifyContent: 'flex-end', + visibility: 'hidden', + minWidth: '100px', + }, + noBorderBottom: { + '& > td': { + borderBottom: 'none', + }, + }, + controlsRow: { + backgroundColor: 'white', + padding: lg, + borderRadius: sm, + }, + editEntryButton: { + cursor: 'pointer', + marginBottom: marginButtonImg, + }, + removeEntryButton: { + marginLeft: lg, + marginRight: lg, + marginBottom: marginButtonImg, + cursor: 'pointer', + }, + message: { + margin: `${sm} 0`, + padding: `${md} 0`, + maxHeight: '54px', + boxSizing: 'border-box', + justifyContent: 'flex-end', + }, + send: { + width: '75px', + minWidth: '75px', + borderRadius: '4px', + '& > span': { + fontSize: '14px', + }, + }, + leftIcon: { + marginRight: sm, + }, + iconSmall: { + fontSize: 16, + }, +}) diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx index a97f047fc6..5387f0e24d 100644 --- a/src/routes/safe/components/Layout.jsx +++ b/src/routes/safe/components/Layout.jsx @@ -30,10 +30,12 @@ import Balances from './Balances' import Transactions from './Transactions' import Settings from './Settings' import { styles } from './style' +import AddressBookTable from '~/routes/safe/components/AddressBook' export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' export const TRANSACTIONS_TAB_BTN_TEST_ID = 'transactions-tab-btn' +export const ADDRESS_BOOK_TAB_BTN_TEST_ID = 'address-book-tab-btn' export const SAFE_VIEW_NAME_HEADING_TEST_ID = 'safe-name-heading' type Props = SelectorProps & @@ -149,6 +151,7 @@ const Layout = (props: Props) => { > + @@ -212,6 +215,25 @@ const Layout = (props: Props) => { /> )} /> + ( + + )} + /> { - const { address } = props + const { address, showLinks } = props return ( - {address} + { showLinks ? ( +
+ +
+ ) : {address} }
) } diff --git a/src/store/index.js b/src/store/index.js index 042eff4404..4cd92932be 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -25,6 +25,7 @@ import notifications, { import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues' import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies' import notificationsMiddleware from '~/routes/safe/store/middleware/notificationsMiddleware' +import addressBook, { ADDRESS_BOOK_REDUCER_ID } from '~/logic/addressBook/store/reducer/addressBook' export const history = createBrowserHistory() @@ -56,6 +57,7 @@ const reducers: Reducer = combineReducers({ [NOTIFICATIONS_REDUCER_ID]: notifications, [CURRENCY_VALUES_KEY]: currencyValues, [COOKIES_REDUCER_ID]: cookies, + [ADDRESS_BOOK_REDUCER_ID]: addressBook, }) export const store: Store = createStore(reducers, finalCreateStore)