Browse Source

Merge pull request #270 from meriadec/master

Group operations by day in Dashboard and account page
master
Meriadec Pillet 7 years ago
committed by GitHub
parent
commit
6d1e80ea03
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/__mocks__/render.js
  2. 6
      src/components/AccountPage/index.js
  3. 32
      src/components/DashboardPage/index.js
  4. 109
      src/components/OperationsList/index.js
  5. 68
      src/components/OperationsList/stories.js
  6. 2
      src/components/SettingsPage/index.js
  7. 2
      src/helpers/staticPath.js
  8. 2
      src/renderer/i18n/instanciate.js
  9. 3
      src/renderer/init.js

6
src/__mocks__/render.js

@ -2,8 +2,10 @@ import React from 'react'
import { Provider } from 'react-redux'
import renderer from 'react-test-renderer'
import { ThemeProvider } from 'styled-components'
import { I18nextProvider } from 'react-i18next'
import createStore from 'renderer/createStore'
import i18n from 'renderer/i18n/electron'
import theme from 'styles/theme'
@ -11,9 +13,11 @@ export default function render(component, state) {
const store = createStore({ state })
return renderer
.create(
<I18nextProvider i18n={i18n} initialLanguage="en">
<Provider store={store}>
<ThemeProvider theme={theme}>{component}</ThemeProvider>
</Provider>,
</Provider>
</I18nextProvider>,
)
.toJSON()
}

6
src/components/AccountPage/index.js

@ -168,11 +168,7 @@ class AccountPage extends PureComponent<Props, State> {
)}
/>
</Box>
<OperationsList
account={account}
operations={account.operations}
title={t('account:lastOperations')}
/>
<OperationsList account={account} title={t('account:lastOperations')} />
</Box>
)
}

32
src/components/DashboardPage/index.js

@ -5,11 +5,9 @@ import { compose } from 'redux'
import { translate } from 'react-i18next'
import { connect } from 'react-redux'
import { push } from 'react-router-redux'
import type { Account, Operation } from '@ledgerhq/wallet-common/lib/types'
import type { Account } from '@ledgerhq/wallet-common/lib/types'
import chunk from 'lodash/chunk'
import get from 'lodash/get'
import sortBy from 'lodash/sortBy'
import type { T } from 'types/common'
@ -49,33 +47,11 @@ type Props = {
type State = {
accountsChunk: Array<Array<Account | null>>,
allOperations: Operation[],
selectedTime: string,
daysCount: number,
}
const ACCOUNTS_BY_LINE = 3
const ALL_OPERATIONS_LIMIT = 10
const getAllOperations = accounts => {
const allOperations = accounts.reduce((result, account) => {
const operations = get(account, 'operations', [])
result = [
...result,
...operations.map(t => ({
...t,
account,
})),
]
return result
}, [])
return sortBy(allOperations, t => t.date)
.reverse()
.slice(0, ALL_OPERATIONS_LIMIT)
}
const getAccountsChunk = accounts => {
// create shallow copy of accounts, to be mutated
@ -89,7 +65,6 @@ const getAccountsChunk = accounts => {
class DashboardPage extends PureComponent<Props, State> {
state = {
accountsChunk: getAccountsChunk(this.props.accounts),
allOperations: getAllOperations(this.props.accounts),
selectedTime: 'week',
daysCount: 7,
}
@ -98,7 +73,6 @@ class DashboardPage extends PureComponent<Props, State> {
if (nextProps.accounts !== this.props.accounts) {
this.setState({
accountsChunk: getAccountsChunk(nextProps.accounts),
allOperations: getAllOperations(nextProps.accounts),
})
}
}
@ -111,7 +85,7 @@ class DashboardPage extends PureComponent<Props, State> {
render() {
const { push, accounts, t, counterValue } = this.props
const { accountsChunk, allOperations, selectedTime, daysCount } = this.state
const { accountsChunk, selectedTime, daysCount } = this.state
const totalAccounts = accounts.length
@ -193,7 +167,7 @@ class DashboardPage extends PureComponent<Props, State> {
<OperationsList
canShowMore
onAccountClick={account => push(`/account/${account.id}`)}
operations={allOperations}
accounts={accounts}
title={t('dashboard:recentActivity')}
withAccount
/>

109
src/components/OperationsList/index.js

@ -1,16 +1,20 @@
// @flow
import React, { Component } from 'react'
import React, { PureComponent } from 'react'
import styled from 'styled-components'
import moment from 'moment'
import { connect } from 'react-redux'
import { compose } from 'redux'
import { translate } from 'react-i18next'
import { getIconByCoinType } from '@ledgerhq/currencies/react'
import {
groupAccountOperationsByDay,
groupAccountsOperationsByDay,
} from '@ledgerhq/wallet-common/lib/helpers/account'
import type { Account, Operation as OperationType } from '@ledgerhq/wallet-common/lib/types'
import noop from 'lodash/noop'
import isEqual from 'lodash/isEqual'
import keyBy from 'lodash/keyBy'
import type { T } from 'types/common'
@ -22,9 +26,9 @@ import IconAngleDown from 'icons/AngleDown'
import Box, { Card } from 'components/base/Box'
import CounterValue from 'components/CounterValue'
import Defer from 'components/base/Defer'
import FormattedVal from 'components/base/FormattedVal'
import Text from 'components/base/Text'
import Defer from 'components/base/Defer'
import ConfirmationCheck from './ConfirmationCheck'
@ -33,6 +37,14 @@ const ACCOUNT_COL_SIZE = 150
const AMOUNT_COL_SIZE = 150
const CONFIRMATION_COL_SIZE = 44
const calendarOpts = {
sameDay: 'LL – [Today]',
nextDay: 'LL – [Tomorrow]',
lastDay: 'LL – [Yesterday]',
lastWeek: 'LL',
sameElse: 'LL',
}
const Day = styled(Text).attrs({
color: 'dark',
fontSize: 3,
@ -69,6 +81,7 @@ const Cell = styled(Box).attrs({
alignItems: 'center',
})`
width: ${p => (p.size ? `${p.size}px` : '')};
overflow: ${p => (p.noOverflow ? 'hidden' : '')};
`
const ShowMore = styled(Box).attrs({
@ -155,6 +168,7 @@ const Operation = ({
{withAccount &&
account && (
<Cell
noOverflow
size={ACCOUNT_COL_SIZE}
horizontal
flow={2}
@ -214,67 +228,74 @@ const mapDispatchToProps = {
type Props = {
account: Account,
accounts: Account[],
canShowMore: boolean,
onAccountClick?: Function,
openModal: Function,
operations: OperationType[],
t: T,
title?: string,
withAccount?: boolean,
nbToShow: number,
title?: string,
}
export class OperationsList extends Component<Props> {
export class OperationsList extends PureComponent<Props> {
static defaultProps = {
account: null,
onAccountClick: noop,
withAccount: false,
canShowMore: false,
nbToShow: 20,
}
shouldComponentUpdate(nextProps: Props) {
if (this.props.account !== nextProps.account) {
return true
}
if (this.props.withAccount !== nextProps.withAccount) {
return true
}
if (this.props.canShowMore !== nextProps.canShowMore) {
return true
}
if (this._hashCache === null) {
return true
}
return !isEqual(this._hashCache, this.getHashCache(nextProps.operations))
}
getHashCache = (operations: OperationType[]) => operations.map(t => t.id)
handleClickOperation = (data: Object) => this.props.openModal(MODAL_OPERATION_DETAILS, data)
_hashCache = null
render() {
const { account, canShowMore, onAccountClick, operations, t, title, withAccount } = this.props
const {
account,
title,
accounts,
canShowMore,
onAccountClick,
t,
withAccount,
nbToShow,
} = this.props
this._hashCache = this.getHashCache(operations)
if (!account && !accounts) {
console.warn('Preventing render OperationsList because not received account or accounts') // eslint-disable-line no-console
return null
}
const groupedOperations = accounts
? groupAccountsOperationsByDay(accounts, nbToShow)
: groupAccountOperationsByDay(account, nbToShow)
const accountsMap = accounts ? keyBy(accounts, 'id') : { [account.id]: account }
return (
<Defer>
<Box>
<Card flow={1} title={title} p={0}>
<Box>
{operations.map(op => {
// $FlowFixMe
const acc = account || op.account
<Box flow={4}>
{title && (
<Text color="dark" ff="Museo Sans" fontSize={6}>
{title}
</Text>
)}
{groupedOperations.map(group => {
const d = moment(group.day)
return (
<Box flow={2} key={group.day.toISOString()}>
<Box ff="Open Sans|SemiBold" fontSize={4} color="grey">
{d.calendar(null, calendarOpts)}
</Box>
<Card p={0}>
{group.data.map(op => {
const account = accountsMap[op.accountId]
if (!account) {
return null
}
return (
<Operation
account={acc}
key={`${op.id}${acc ? `-${acc.id}` : ''}`}
minConfirmations={acc.minConfirmations}
key={op.hash}
account={account}
minConfirmations={account.minConfirmations}
onAccountClick={onAccountClick}
onOperationClick={this.handleClickOperation}
t={t}
@ -283,8 +304,10 @@ export class OperationsList extends Component<Props> {
/>
)
})}
</Box>
</Card>
</Box>
)
})}
{canShowMore && (
<ShowMore>
<span>{t('operationsList:showMore')}</span>

68
src/components/OperationsList/stories.js

@ -1,78 +1,24 @@
// @flow
import React from 'react'
import { getCurrencyByCoinType, getDefaultUnitByCoinType } from '@ledgerhq/currencies'
import { genAccount } from '@ledgerhq/wallet-common/lib/mock/account'
import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs'
import { accounts } from 'components/SelectAccount/stories'
import OperationsList from 'components/OperationsList'
import Box from 'components/base/Box'
const stories = storiesOf('Components', module)
const unit = getDefaultUnitByCoinType(0)
const account = ({ name }) => ({
...accounts[0],
minConfirmations: 10,
currency: getCurrencyByCoinType(0),
name,
coinType: 0,
unit,
})
const operations = [
{
address: '5c6ea1716520c7d6e038d36a3223faced3c',
hash: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62',
id: '5c6ea1716520c7d6e038d36a3223faced3c4b8f7ffb69d9fb5bd527d562fdb62',
amount: 1.3e8,
date: new Date('2018-01-09T16:03:52Z'),
confirmations: 1,
account: account({
name: 'Account 1',
}),
},
{
address: '5c6ea1716520c7d6e038d36a3223faced3c',
hash: '26bdf265d725db5bf9d96bff7f8b4c3decaf3223a63d830e6d7c0256171ae6c5',
id: '26bdf265d725db5bf9d96bff7f8b4c3decaf3223a63d830e6d7c0256171ae6c5',
amount: 1.6e8,
date: new Date('2018-01-09T16:03:52Z'),
confirmations: 11,
account: account({
name: 'Account 1',
}),
},
{
address: '27416a48caab90fab053b507b8b6b9d4',
hash: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4',
id: '27416a48caab90fab053b507b8b6b9d48fba75421d3bfdbae4b85f64024bc9c4',
amount: -6.5e8,
date: new Date('2018-01-09T16:02:40Z'),
confirmations: 11,
account: account({
name: 'Account 2',
}),
},
{
address: '27416a48caab90fab053b507b8b6b9d4',
hash: '4c9cb42046f58b4eabdfb3d12457abf84d9b6b8b705b350baf09baac84a61472',
id: '4c9cb42046f58b4eabdfb3d12457abf84d9b6b8b705b350baf09baac84a61472',
amount: -4.2e8,
date: new Date('2018-01-09T16:02:40Z'),
confirmations: 1,
account: account({
name: 'Account 2',
}),
},
]
const account1 = genAccount('account1')
const account2 = genAccount('account2')
stories.add('OperationsList', () => (
<Box bg="lightGrey" p={6} m={-4}>
<OperationsList
operations={operations}
accounts={[account1, account2]}
canShowMore={boolean('canShowMore')}
withAccount={boolean('withAccount')}
/>
</Box>
))

2
src/components/SettingsPage/index.js

@ -4,6 +4,7 @@ import React, { PureComponent } from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { translate } from 'react-i18next'
import moment from 'moment'
import type { Settings, T } from 'types/common'
import type { SaveSettings } from 'actions/settings'
@ -56,6 +57,7 @@ class SettingsPage extends PureComponent<Props, State> {
if (newSettings.language !== settings.language) {
i18n.changeLanguage(newSettings.language)
moment.locale(newSettings.language)
}
if (newSettings.counterValue !== settings.counterValue) {

2
src/helpers/staticPath.js

@ -7,4 +7,4 @@ export default (__DEV__ && !STORYBOOK_ENV && NODE_ENV !== 'test'
? __static
: isRunningInAsar
? __dirname.replace(/app\.asar$/, 'static')
: !STORYBOOK_ENV ? `${__dirname}/../static` : 'static')
: !STORYBOOK_ENV ? `${__dirname}/../../static` : 'static')

2
src/renderer/i18n/instanciate.js

@ -4,7 +4,7 @@ const commonConfig = {
fallbackLng: 'en',
debug: false,
react: {
wait: true,
wait: process.env.NODE_ENV !== 'test',
},
}

3
src/renderer/init.js

@ -5,6 +5,7 @@ import { remote } from 'electron'
import { render } from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import createHistory from 'history/createHashHistory'
import moment from 'moment'
import createStore from 'renderer/createStore'
import events from 'renderer/events'
@ -37,6 +38,8 @@ const state = store.getState() || {}
const language = getLanguage(state)
const locked = isLocked(state)
moment.locale(language)
function r(Comp) {
if (rootNode) {
render(<AppContainer>{Comp}</AppContainer>, rootNode)

Loading…
Cancel
Save