mirror of https://github.com/trezor/trezor-wallet
commit
ad553e436a
@ -1,100 +1,210 @@
|
||||
/* @flow */
|
||||
|
||||
|
||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||
import * as WALLET from 'actions/constants/wallet';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as TOKEN from 'actions/constants/token';
|
||||
import * as PENDING from 'actions/constants/pendingTx';
|
||||
|
||||
import * as stateUtils from 'reducers/utils';
|
||||
import * as reducerUtils from 'reducers/utils';
|
||||
|
||||
import type {
|
||||
AsyncAction,
|
||||
PayloadAction,
|
||||
Action,
|
||||
GetState,
|
||||
Dispatch,
|
||||
State,
|
||||
} from 'flowtype';
|
||||
|
||||
type SelectedAccountState = $ElementType<State, 'selectedAccount'>;
|
||||
|
||||
export type SelectedAccountAction = {
|
||||
type: typeof ACCOUNT.DISPOSE,
|
||||
} | {
|
||||
type: typeof ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||
payload: $ElementType<State, 'selectedAccount'>
|
||||
payload: SelectedAccountState,
|
||||
};
|
||||
|
||||
type AccountStatus = {
|
||||
type: string; // notification type
|
||||
title: string; // notification title
|
||||
message?: string; // notification message
|
||||
shouldRender: boolean; // should render account page
|
||||
}
|
||||
|
||||
export const dispose = (): Action => ({
|
||||
type: ACCOUNT.DISPOSE,
|
||||
});
|
||||
|
||||
export const updateSelectedValues = (prevState: State, action: Action): AsyncAction => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const locationChange: boolean = action.type === LOCATION_CHANGE;
|
||||
const state: State = getState();
|
||||
const { location } = state.router;
|
||||
const getAccountStatus = (state: State, selectedAccount: SelectedAccountState): ?AccountStatus => {
|
||||
const device = state.wallet.selectedDevice;
|
||||
if (!device || !device.state) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: 'Loading device...',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
account,
|
||||
discovery,
|
||||
network,
|
||||
} = selectedAccount;
|
||||
|
||||
// corner case: accountState didn't finish loading state after LOCATION_CHANGE action
|
||||
if (!network) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: 'Loading account state...',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
// handle devices state change (from trezor-connect events or location change)
|
||||
if (locationChange
|
||||
|| prevState.accounts !== state.accounts
|
||||
|| prevState.discovery !== state.discovery
|
||||
|| prevState.tokens !== state.tokens
|
||||
|| prevState.pending !== state.pending) {
|
||||
const account = stateUtils.getSelectedAccount(state);
|
||||
const network = stateUtils.getSelectedNetwork(state);
|
||||
const discovery = stateUtils.getDiscoveryProcess(state);
|
||||
const tokens = stateUtils.getAccountTokens(state, account);
|
||||
const pending = stateUtils.getAccountPendingTx(state.pending, account);
|
||||
|
||||
const payload: $ElementType<State, 'selectedAccount'> = {
|
||||
location: location.pathname,
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
pending,
|
||||
const blockchain = state.blockchain.find(b => b.name === network.network);
|
||||
if (blockchain && !blockchain.connected) {
|
||||
return {
|
||||
type: 'backend',
|
||||
title: `${network.name} backend is not connected`,
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
let needUpdate: boolean = false;
|
||||
Object.keys(payload).forEach((key) => {
|
||||
if (Array.isArray(payload[key])) {
|
||||
if (Array.isArray(state.selectedAccount[key]) && payload[key].length !== state.selectedAccount[key].length) {
|
||||
needUpdate = true;
|
||||
// account not found (yet). checking why...
|
||||
if (!account) {
|
||||
if (!discovery || discovery.waitingForDevice) {
|
||||
if (device.connected) {
|
||||
// case 1: device is connected but discovery not started yet (probably waiting for auth)
|
||||
if (device.available) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: 'Loading accounts...',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
} else if (payload[key] !== state.selectedAccount[key]) {
|
||||
needUpdate = true;
|
||||
// case 2: device is unavailable (created with different passphrase settings) account cannot be accessed
|
||||
return {
|
||||
type: 'info',
|
||||
title: `Device ${device.instanceLabel} is unavailable`,
|
||||
message: 'Change passphrase settings to use this device',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (needUpdate) {
|
||||
dispatch({
|
||||
type: ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||
payload,
|
||||
});
|
||||
|
||||
if (location.state.send) {
|
||||
const rejectedTxs = pending.filter(tx => tx.rejected);
|
||||
rejectedTxs.forEach((tx) => {
|
||||
dispatch({
|
||||
type: NOTIFICATION.ADD,
|
||||
payload: {
|
||||
type: 'warning',
|
||||
title: 'Pending transaction rejected',
|
||||
message: `Transaction with id: ${tx.id} not found.`,
|
||||
cancelable: true,
|
||||
actions: [
|
||||
{
|
||||
label: 'OK',
|
||||
callback: () => {
|
||||
dispatch({
|
||||
type: PENDING.TX_RESOLVED,
|
||||
tx,
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
// case 3: device is disconnected
|
||||
return {
|
||||
type: 'info',
|
||||
title: `Device ${device.instanceLabel} is disconnected`,
|
||||
message: 'Connect device to load accounts',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (discovery.completed) {
|
||||
// case 4: account not found and discovery is completed
|
||||
return {
|
||||
type: 'warning',
|
||||
title: 'Account does not exist',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
// case 6: discovery is not completed yet
|
||||
return {
|
||||
type: 'info',
|
||||
title: 'Loading accounts...',
|
||||
shouldRender: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Additional status: account does exists and it's visible but shouldn't be active
|
||||
if (!device.connected) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: `Device ${device.instanceLabel} is disconnected`,
|
||||
shouldRender: true,
|
||||
};
|
||||
}
|
||||
if (!device.available) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: `Device ${device.instanceLabel} is unavailable`,
|
||||
message: 'Change passphrase settings to use this device',
|
||||
shouldRender: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Additional status: account does exists, but waiting for discovery to complete
|
||||
if (discovery && !discovery.completed) {
|
||||
return {
|
||||
type: 'info',
|
||||
title: 'Loading accounts...',
|
||||
shouldRender: true,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// list of all actions which has influence on "selectedAccount" reducer
|
||||
// other actions will be ignored
|
||||
const actions = [
|
||||
LOCATION_CHANGE,
|
||||
WALLET.SET_SELECTED_DEVICE,
|
||||
WALLET.UPDATE_SELECTED_DEVICE,
|
||||
...Object.values(ACCOUNT).filter(v => typeof v === 'string' && v !== ACCOUNT.UPDATE_SELECTED_ACCOUNT), // exported values got unwanted "__esModule: true" as first element
|
||||
...Object.values(DISCOVERY).filter(v => typeof v === 'string'),
|
||||
...Object.values(TOKEN).filter(v => typeof v === 'string'),
|
||||
...Object.values(PENDING).filter(v => typeof v === 'string'),
|
||||
];
|
||||
|
||||
/*
|
||||
* Called from WalletService
|
||||
*/
|
||||
export const observe = (prevState: State, action: Action): PayloadAction<boolean> => (dispatch: Dispatch, getState: GetState): boolean => {
|
||||
// ignore not listed actions
|
||||
if (actions.indexOf(action.type) < 0) return false;
|
||||
|
||||
const state: State = getState();
|
||||
const { location } = state.router;
|
||||
// displayed route is not an account route
|
||||
if (!location.state.account) return false;
|
||||
|
||||
// get new values for selected account
|
||||
const account = reducerUtils.getSelectedAccount(state);
|
||||
const network = reducerUtils.getSelectedNetwork(state);
|
||||
const discovery = reducerUtils.getDiscoveryProcess(state);
|
||||
const tokens = reducerUtils.getAccountTokens(state, account);
|
||||
const pending = reducerUtils.getAccountPendingTx(state.pending, account);
|
||||
|
||||
// prepare new state for "selectedAccount" reducer
|
||||
const newState: SelectedAccountState = {
|
||||
location: state.router.location.pathname,
|
||||
account,
|
||||
network,
|
||||
discovery,
|
||||
tokens,
|
||||
pending,
|
||||
notification: null,
|
||||
shouldRender: false,
|
||||
};
|
||||
|
||||
// get "selectedAccount" status from newState
|
||||
const status = getAccountStatus(state, newState);
|
||||
newState.notification = status || null;
|
||||
newState.shouldRender = status ? status.shouldRender : true;
|
||||
// check if newState is different than previous state
|
||||
const stateChanged = reducerUtils.observeChanges(prevState.selectedAccount, newState, {
|
||||
account: ['balance', 'nonce'],
|
||||
discovery: ['accountIndex', 'interrupted', 'completed', 'waitingForBlockchain', 'waitingForDevice'],
|
||||
});
|
||||
|
||||
if (stateChanged) {
|
||||
// update values in reducer
|
||||
dispatch({
|
||||
type: ACCOUNT.UPDATE_SELECTED_ACCOUNT,
|
||||
payload: newState,
|
||||
});
|
||||
}
|
||||
return stateChanged;
|
||||
};
|
||||
|
@ -1,56 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { PRIORITY } from 'constants/notifications';
|
||||
import Group from './components/Group';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
class NotificationsGroup extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.notifications = [
|
||||
{ type: 'warning', title: 'adddaa', message: 'aaaa' },
|
||||
{ type: 'error', title: 'aaddda', message: 'aaaa' },
|
||||
{ type: 'info', title: 'aafffa', message: 'aaaa' },
|
||||
{ type: 'error', title: 'aggaa', message: 'aaaa' },
|
||||
{ type: 'warning', title: 'aasssa', message: 'aaaa' },
|
||||
{ type: 'success', title: 'afaa', message: 'aaaa' },
|
||||
{ type: 'error', title: 'aada', message: 'aaaa' },
|
||||
{ type: 'error', title: 'aafffa', message: 'aaaa' },
|
||||
];
|
||||
}
|
||||
|
||||
groupNotifications = notifications => notifications
|
||||
.reduce((acc, obj) => {
|
||||
const key = obj.type;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(obj);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
sortByPriority(notifications) {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { notifications } = this;
|
||||
const notificationGroups = this.groupNotifications(notifications);
|
||||
const sortedNotifications = this.sortByPriority(notificationGroups);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{Object.keys(sortedNotifications).map(group => (
|
||||
<Group
|
||||
groupNotifications={notificationGroups[group]}
|
||||
type={group}
|
||||
/>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationsGroup;
|
@ -0,0 +1,36 @@
|
||||
/* @flow */
|
||||
import * as React from 'react';
|
||||
import { Notification } from 'components/Notification';
|
||||
|
||||
import type { Props } from '../../index';
|
||||
|
||||
// There could be only one account notification
|
||||
export default (props: Props) => {
|
||||
const { notification } = props.selectedAccount;
|
||||
if (notification) {
|
||||
if (notification.type === 'backend') {
|
||||
// special case: backend is down
|
||||
// TODO: this is a different component with "auto resolve" button
|
||||
|
||||
return (
|
||||
<Notification
|
||||
type="error"
|
||||
title={notification.title}
|
||||
message={notification.message}
|
||||
actions={
|
||||
[{
|
||||
label: 'Connect',
|
||||
callback: async () => {
|
||||
await props.blockchainReconnect('trop');
|
||||
},
|
||||
}]
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
// return (<Notification type="error" title={notification.title} message={notification.message} />);
|
||||
}
|
||||
return (<Notification type={notification.type} title={notification.title} message={notification.message} />);
|
||||
}
|
||||
return null;
|
||||
};
|
@ -0,0 +1,88 @@
|
||||
import React, { Component } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { PRIORITY } from 'constants/notifications';
|
||||
import Group from './components/Group';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
class NotificationsGroup extends Component {
|
||||
groupNotifications = notifications => notifications
|
||||
.reduce((acc, obj) => {
|
||||
const key = obj.type;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(obj);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
sortByPriority(notifications) {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { close, notifications } = this.props;
|
||||
// const notifications = [
|
||||
// {
|
||||
// key: 1,
|
||||
// title: 'this is a title of error notification',
|
||||
// type: 'error',
|
||||
// message: 'this is a message of error notification',
|
||||
// },
|
||||
// {
|
||||
// key: 2,
|
||||
// title: 'this is a title of warning notification',
|
||||
// type: 'warning',
|
||||
// message: 'this is a message of warning notification',
|
||||
// },
|
||||
// {
|
||||
// key: 3,
|
||||
// title: 'this is a title of warning notification',
|
||||
// type: 'warning',
|
||||
// message: 'this is a message of warning notification',
|
||||
// },
|
||||
// {
|
||||
// key: 4,
|
||||
// title: 'this is a title of warning notification sds d',
|
||||
// type: 'warning',
|
||||
// message: 'this is a message of warning notification',
|
||||
// },
|
||||
// {
|
||||
// key: 5,
|
||||
// title: 'this is a title of warning notification as',
|
||||
// type: 'info',
|
||||
// message: 'this is a message of warning notification',
|
||||
// },
|
||||
// {
|
||||
// key: 6,
|
||||
// title: 'this is a title of info notification s ',
|
||||
// type: 'info',
|
||||
// message: 'this is a message of info notification',
|
||||
// },
|
||||
// ];
|
||||
const notificationGroups = this.groupNotifications(notifications);
|
||||
const sortedNotifications = this.sortByPriority(notificationGroups);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{Object.keys(sortedNotifications).map(group => (
|
||||
<Group
|
||||
key={group}
|
||||
groupNotifications={notificationGroups[group]}
|
||||
type={group}
|
||||
close={close}
|
||||
/>
|
||||
))}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationsGroup.propTypes = {
|
||||
notifications: PropTypes.array.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NotificationsGroup;
|
@ -0,0 +1,15 @@
|
||||
/* @flow */
|
||||
import * as React from 'react';
|
||||
import NotificationsGroups from './components/NotificationsGroups';
|
||||
|
||||
import type { Props } from '../../index';
|
||||
|
||||
export default (props: Props) => {
|
||||
const { notifications, close } = props;
|
||||
return (
|
||||
<NotificationsGroups
|
||||
notifications={notifications}
|
||||
close={close}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
/* @flow */
|
||||
import * as React from 'react';
|
||||
import { Notification } from 'components/Notification';
|
||||
|
||||
import type { Props } from '../../index';
|
||||
|
||||
export default (props: Props) => {
|
||||
const { location } = props.router;
|
||||
if (!location) return null;
|
||||
|
||||
const notifications: Array<Notification> = [];
|
||||
// if (location.state.device) {
|
||||
// notifications.push(<Notification key="example" type="info" title="Static example" />);
|
||||
// }
|
||||
|
||||
return notifications;
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
/* @flow */
|
||||
import * as React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import type { MapStateToProps, MapDispatchToProps } from 'react-redux';
|
||||
import type { State, Dispatch } from 'flowtype';
|
||||
|
||||
import { reconnect } from 'actions/DiscoveryActions';
|
||||
import * as NotificationActions from 'actions/NotificationActions';
|
||||
|
||||
import StaticNotifications from './components/Static';
|
||||
import AccountNotifications from './components/Account';
|
||||
import ActionNotifications from './components/Action';
|
||||
|
||||
export type StateProps = {
|
||||
router: $ElementType<State, 'router'>;
|
||||
notifications: $ElementType<State, 'notifications'>;
|
||||
selectedAccount: $ElementType<State, 'selectedAccount'>;
|
||||
wallet: $ElementType<State, 'wallet'>;
|
||||
blockchain: $ElementType<State, 'blockchain'>;
|
||||
children?: React.Node;
|
||||
}
|
||||
|
||||
export type DispatchProps = {
|
||||
close: typeof NotificationActions.close;
|
||||
blockchainReconnect: typeof reconnect;
|
||||
}
|
||||
|
||||
export type Props = StateProps & DispatchProps;
|
||||
|
||||
type OwnProps = {};
|
||||
|
||||
const Notifications = (props: Props) => (
|
||||
<React.Fragment>
|
||||
<StaticNotifications {...props} />
|
||||
<AccountNotifications {...props} />
|
||||
<ActionNotifications {...props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
const mapStateToProps: MapStateToProps<State, OwnProps, StateProps> = (state: State): StateProps => ({
|
||||
router: state.router,
|
||||
notifications: state.notifications,
|
||||
selectedAccount: state.selectedAccount,
|
||||
wallet: state.wallet,
|
||||
blockchain: state.blockchain,
|
||||
});
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<Dispatch, OwnProps, DispatchProps> = (dispatch: Dispatch): DispatchProps => ({
|
||||
close: bindActionCreators(NotificationActions.close, dispatch),
|
||||
blockchainReconnect: bindActionCreators(reconnect, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Notifications);
|
@ -0,0 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 1`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 2`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 3`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 4`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 5`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 6`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 7`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 8`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 9`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoud be the same - returns false 10`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 1`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 2`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 3`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 4`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 5`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 6`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 7`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 8`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 9`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 10`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 11`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 12`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 13`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 14`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges shoul NOT be the same - returns true 15`] = `true`;
|
||||
|
||||
exports[`reducers utils observeChanges test filter 1`] = `false`;
|
||||
|
||||
exports[`reducers utils observeChanges test filter 2`] = `true`;
|
@ -0,0 +1,148 @@
|
||||
import * as reducerUtils from '../index';
|
||||
|
||||
describe('reducers utils', () => {
|
||||
it('observeChanges shoud be the same - returns false', () => {
|
||||
const data = [
|
||||
// example of same data (false)
|
||||
{
|
||||
previous: {},
|
||||
current: {},
|
||||
},
|
||||
{
|
||||
previous: 1,
|
||||
current: 1,
|
||||
},
|
||||
{
|
||||
previous: [],
|
||||
current: [],
|
||||
},
|
||||
{
|
||||
previous: [1, 1, 1],
|
||||
current: [1, 1, 1],
|
||||
},
|
||||
{
|
||||
previous: 'a',
|
||||
current: 'a',
|
||||
},
|
||||
{
|
||||
previous: { one: 1 },
|
||||
current: { one: 1 },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: 1 } },
|
||||
current: { one: { two: 1 } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, 2, 3] } },
|
||||
current: { one: { two: [1, 2, 3] } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, { three: 1 }, 3] } },
|
||||
current: { one: { two: [1, { three: 1 }, 3] } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, { three: 1 }, { four: 3, five: { six: 3 } }] } },
|
||||
current: { one: { two: [1, { three: 1 }, { four: 3, five: { six: 3 } }] } },
|
||||
},
|
||||
];
|
||||
|
||||
data.forEach((item) => {
|
||||
expect(reducerUtils.observeChanges(
|
||||
item.previous, item.current,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('observeChanges shoul NOT be the same - returns true', () => {
|
||||
const data = [
|
||||
// example of different data (true)
|
||||
{
|
||||
previous: null,
|
||||
current: {},
|
||||
},
|
||||
{
|
||||
previous: { one: 1 },
|
||||
current: {},
|
||||
},
|
||||
{
|
||||
previous: { one: 1, three: 3 },
|
||||
current: { one: 1, two: 2 },
|
||||
},
|
||||
{
|
||||
previous: [{}, {}],
|
||||
current: [],
|
||||
},
|
||||
{
|
||||
previous: [1, 1, 1],
|
||||
current: [1, 1],
|
||||
},
|
||||
{
|
||||
previous: 'a',
|
||||
current: 'b',
|
||||
},
|
||||
{
|
||||
previous: ['a'],
|
||||
current: ['b'],
|
||||
},
|
||||
{
|
||||
previous: 1,
|
||||
current: '1',
|
||||
},
|
||||
{
|
||||
previous: { one: 1 },
|
||||
current: { one: 2 },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: 1 } },
|
||||
current: { one: { two: 2 } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: 1 } },
|
||||
current: { one: { two: 2 } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, 2, 3] } },
|
||||
current: { one: { two: [1, 1, 3] } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, { three: 1 }, 3] } },
|
||||
current: { one: { two: [1, { three: 2 }, 3] } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, { three: 1 }, { four: 3, five: { six: 3 } }] } },
|
||||
current: { one: { two: [1, { three: 1 }, { four: 3, five: { six: 1 } }] } },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: [1, { three: 1 }, { four: 3, five: { sixxx: 3 } }] } },
|
||||
current: { one: { two: [1, { three: 1 }, { four: 3, five: { six: 1 } }] } },
|
||||
},
|
||||
];
|
||||
|
||||
data.forEach((item) => {
|
||||
expect(reducerUtils.observeChanges(
|
||||
item.previous, item.current,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('observeChanges test filter', () => {
|
||||
const data = [
|
||||
{
|
||||
previous: { one: { two: 2, three: 3 } },
|
||||
current: { one: { two: 2, three: 4 } },
|
||||
filter: { one: ['two'] },
|
||||
},
|
||||
{
|
||||
previous: { one: { two: 2, three: 3 } },
|
||||
current: { one: { two: 1, three: 3 } },
|
||||
filter: { one: ['two'] },
|
||||
},
|
||||
];
|
||||
|
||||
data.forEach((item) => {
|
||||
expect(reducerUtils.observeChanges(
|
||||
item.previous, item.current, item.filter,
|
||||
)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Notification } from 'components/Notification';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
min-width: 720px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const InitializationError = (props: { error: ?string }) => (
|
||||
<Wrapper>
|
||||
<Notification
|
||||
title="Initialization error"
|
||||
message={props.error || ''}
|
||||
type="error"
|
||||
cancelable={false}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export default InitializationError;
|
Loading…
Reference in new issue