feat: open dispute screen (#191)

* feat: open dispute screen
* fix: code review
* fix: tests
This commit is contained in:
Richard Ramos 2019-05-01 12:00:28 -04:00 committed by GitHub
parent 691f2931c3
commit 7a0aec96ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 189 additions and 25 deletions

BIN
src/images/success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,4 +1,4 @@
import {ARBITRATION_UNSOLVED, GET_DISPUTED_ESCROWS, RESOLVE_DISPUTE, RESOLVE_DISPUTE_FAILED, LOAD_ARBITRATION, GET_ARBITRATORS} from './constants';
import {ARBITRATION_UNSOLVED, GET_DISPUTED_ESCROWS, RESOLVE_DISPUTE, RESOLVE_DISPUTE_FAILED, LOAD_ARBITRATION, GET_ARBITRATORS, OPEN_DISPUTE} from './constants';
import Escrow from '../../../embarkArtifacts/contracts/Escrow';
export const getDisputedEscrows = () => ({type: GET_DISPUTED_ESCROWS});
@ -17,6 +17,8 @@ export const resolveDispute = (escrowId, result) => {
};
};
export const openDispute = (escrowId) => ({type: OPEN_DISPUTE, escrowId, toSend: Escrow.methods.openCase(escrowId)});
export const loadArbitration = (escrowId) => {
return {type: LOAD_ARBITRATION, escrowId};
};

View File

@ -7,6 +7,11 @@ export const RESOLVE_DISPUTE_PRE_SUCCESS = 'RESOLVE_DISPUTE_PRE_SUCCESS';
export const RESOLVE_DISPUTE_SUCCEEDED = 'RESOLVE_DISPUTE_SUCCEEDED';
export const RESOLVE_DISPUTE_FAILED = 'RESOLVE_DISPUTE_FAILED';
export const OPEN_DISPUTE = 'OPEN_DISPUTE';
export const OPEN_DISPUTE_PRE_SUCCESS = 'OPEN_DISPUTE_PRE_SUCCESS';
export const OPEN_DISPUTE_SUCCEEDED = 'OPEN_DISPUTE_SUCCEEDED';
export const OPEN_DISPUTE_FAILED = 'OPEN_DISPUTE_FAILED';
export const ARBITRATION_UNSOLVED = "0";
export const ARBITRATION_SOLVED_BUYER = "1";
export const ARBITRATION_SOLVED_SELLER = "2";

View File

@ -8,10 +8,14 @@ import {
RESOLVE_DISPUTE_FAILED,
LOAD_ARBITRATION_SUCCEEDED,
GET_ARBITRATORS_FAILED,
GET_ARBITRATORS_SUCCEEDED
GET_ARBITRATORS_SUCCEEDED,
OPEN_DISPUTE_FAILED,
OPEN_DISPUTE,
OPEN_DISPUTE_SUCCEEDED,
OPEN_DISPUTE_PRE_SUCCESS
} from './constants';
const DEFAULT_STATE = {escrows: [], arbitration: null, arbitrators: []};
const DEFAULT_STATE = {escrows: [], arbitration: null, arbitrators: [], receipt: null};
function reducer(state = DEFAULT_STATE, action) {
let escrows = state.escrows;
@ -19,9 +23,11 @@ function reducer(state = DEFAULT_STATE, action) {
case GET_DISPUTED_ESCROWS:
return {
...state, ...{
loading: true
loading: true,
receipt: null
}
};
case OPEN_DISPUTE_PRE_SUCCESS:
case RESOLVE_DISPUTE_PRE_SUCCESS:
return {
...state, ...{
@ -35,6 +41,13 @@ function reducer(state = DEFAULT_STATE, action) {
loading: false
}
};
case OPEN_DISPUTE_SUCCEEDED:
return {
...state,
loading: false,
receipt: action.receipt
};
case OPEN_DISPUTE_FAILED:
case GET_DISPUTED_ESCROWS_FAILED:
case RESOLVE_DISPUTE_FAILED:
case GET_ARBITRATORS_FAILED:
@ -44,6 +57,7 @@ function reducer(state = DEFAULT_STATE, action) {
loading: false
}
};
case OPEN_DISPUTE:
case RESOLVE_DISPUTE:
return {
...state, ...{

View File

@ -8,17 +8,18 @@ import {
GET_DISPUTED_ESCROWS, GET_DISPUTED_ESCROWS_FAILED, GET_DISPUTED_ESCROWS_SUCCEEDED,
RESOLVE_DISPUTE, RESOLVE_DISPUTE_FAILED, RESOLVE_DISPUTE_SUCCEEDED,
RESOLVE_DISPUTE_PRE_SUCCESS, LOAD_ARBITRATION, LOAD_ARBITRATION_FAILED, LOAD_ARBITRATION_SUCCEEDED, GET_ARBITRATORS,
GET_ARBITRATORS_SUCCEEDED, GET_ARBITRATORS_FAILED
GET_ARBITRATORS_SUCCEEDED, GET_ARBITRATORS_FAILED, OPEN_DISPUTE, OPEN_DISPUTE_SUCCEEDED, OPEN_DISPUTE_FAILED, OPEN_DISPUTE_PRE_SUCCESS
} from './constants';
import {doTransaction} from "../../utils/saga";
window.Arbitration = Arbitration;
export function *onResolveDispute() {
yield takeEvery(RESOLVE_DISPUTE, doTransaction.bind(null, RESOLVE_DISPUTE_PRE_SUCCESS, RESOLVE_DISPUTE_SUCCEEDED, RESOLVE_DISPUTE_FAILED));
}
export function *onOpenDispute() {
yield takeEvery(OPEN_DISPUTE, doTransaction.bind(null, OPEN_DISPUTE_PRE_SUCCESS, OPEN_DISPUTE_SUCCEEDED, OPEN_DISPUTE_FAILED));
}
export function *doGetArbitrators() {
try {
const cnt = yield call(Arbitration.methods.getNumLicenseOwners().call);
@ -103,4 +104,4 @@ export function *onLoadArbitration() {
yield takeEvery(LOAD_ARBITRATION, doLoadArbitration);
}
export default [fork(onGetEscrows), fork(onResolveDispute), fork(onLoadArbitration), fork(onGetArbitrators)];
export default [fork(onGetEscrows), fork(onResolveDispute), fork(onLoadArbitration), fork(onGetArbitrators), fork(onOpenDispute)];

View File

@ -14,6 +14,7 @@ import fourOFour from '../components/ErrorInformation/404';
import Home from '../pages/Home';
import Profile from '../pages/Profile';
import Escrow from '../pages/Escrow';
import OpenDispute from '../pages/OpenDispute';
import Arbitration from '../pages/Arbitration';
import MyProfile from '../pages/MyProfile';
import EditMyContact from '../pages/EditMyContact';
@ -98,7 +99,7 @@ class App extends Component {
<Route exact path="/license" component={License}/>
<Route exact path="/escrow/:id" component={Escrow}/>
<Route exact path="/arbitration/:id" component={Arbitration}/>
<Route exact path="/openCase/:id" component={OpenDispute} />
<Route exact path="/offers/list" component={OffersList}/>
<Route exact path="/offers/map" component={OffersMap}/>

View File

@ -4,17 +4,20 @@ import PropTypes from 'prop-types';
import exclamationCircle from "../../../../images/exclamation-circle.png";
import RoundedIcon from "../../../ui/RoundedIcon";
import escrow from '../../../features/escrow';
import {Link} from "react-router-dom";
const OpenDispute = ({trade}) => {
const shouldDisplay = trade.status !== escrow.helpers.tradeStates.waiting && trade.status !== escrow.helpers.tradeStates.funded;
return shouldDisplay && <Row className="mt-4 text-danger">
<Col xs="2">
<RoundedIcon image={exclamationCircle} bgColor="red"/>
</Col>
<Col xs="10" className="my-auto">
<h6 className="m-0 font-weight-normal">Open dispute</h6>
</Col>
</Row>;
return shouldDisplay && (
<Row className="mt-4 text-danger" tag={Link} to={"/openCase/" + trade.escrowId}>
<Col xs="2">
<RoundedIcon image={exclamationCircle} bgColor="red"/>
</Col>
<Col xs="10" className="my-auto">
<h6 className="m-0 font-weight-normal">Open dispute</h6>
</Col>
</Row>
);
};
OpenDispute.propTypes = {

View File

@ -9,8 +9,18 @@ import Identicon from "../../../components/UserInformation/Identicon";
import {formatBalance} from "../../../utils/numbers";
import {tradeStates} from "../../../features/escrow/helpers";
const getTradeStyle = (tradeState) => {
switch(tradeState){
const getTradeStyle = (trade) => {
if(trade.arbitration){
if(trade.arbitration.open){
trade.status = tradeStates.arbitration_open;
} else {
if(trade.arbitration.result !== '0'){
trade.status = tradeStates.arbitration_closed;
}
}
}
switch(trade.status){
case tradeStates.waiting:
case tradeStates.funded:
case tradeStates.paid:
@ -26,7 +36,7 @@ const getTradeStyle = (tradeState) => {
case tradeStates.arbitration_closed:
return {text: 'Resolved', className: 'bg-primary'};
default:
return {text: tradeState, className: 'bg-secondary'};
return {text: trade.status, className: 'bg-secondary'};
}
};
@ -37,7 +47,7 @@ class Trades extends Component {
<Card body className="py-2 px-3 shadow-sm">
{this.props.trades.map((trade, index) => {
const isBuyer = trade.buyer === address;
const tradeStyle = getTradeStyle(trade.status);
const tradeStyle = getTradeStyle(trade);
return <Link key={index} to={"/escrow/" + trade.escrowId}>
<Row className="my-1 border-bottom">
<Col className="align-self-center pr-0" xs="2">
@ -50,7 +60,7 @@ class Trades extends Component {
{isBuyer ? 'Buy' : 'Sell' } {formatBalance(trade.tokenAmount)} {trade.token.symbol}
</Col>
<Col className="align-self-center text-center text-success" xs="4">
<span className={"p-1 text-uppercase d-inline text-white rounded-sm " + tradeStyle.className}>{tradeStyle.text}</span>
<span className={"p-1 text-uppercase d-inline text-white rounded-sm text-small " + tradeStyle.className}>{tradeStyle.text}</span>
</Col>
</Row>
</Link>;

View File

@ -137,7 +137,7 @@ exports[`Trades should render correctly 1`] = `
xs="4"
>
<span
className="p-1 text-uppercase d-inline text-white rounded-sm bg-secondary"
className="p-1 text-uppercase d-inline text-white rounded-sm text-small bg-secondary"
>
open
</span>

View File

@ -32,6 +32,15 @@ class MyProfile extends Component {
render() {
const profile = this.props.profile;
const trades = this.props.trades.map(x => {
const dispute = this.props.disputes.find(y => y.escrowId === x.escrowId);
if(dispute){
x.arbitration = dispute.arbitration;
}
return x;
});
return (
<Fragment>
<UserInformation isArbitrator={profile.isArbitrator} reputation={profile.reputation} address={profile.address} username={profile.username}/>
@ -42,7 +51,7 @@ class MyProfile extends Component {
</Fragment>}
{ !profile.isArbitrator && <Fragment>
<Trades trades={this.props.trades} address={this.props.address}/>
<Trades trades={trades} address={this.props.address}/>
<Offers offers={profile.offers} location={profile.location} />
{profile.username && <StatusContactCode value={profile.statusContactCode} />}
</Fragment> }

View File

@ -0,0 +1,119 @@
import React, {Component} from 'react';
import Textarea from 'react-validation/build/textarea';
import Form from 'react-validation/build/form';
import {Button} from 'reactstrap';
import Loading from '../../components/Loading';
import ConfirmDialog from '../../components/ConfirmDialog';
import PropTypes from 'prop-types';
import {withRouter} from "react-router-dom";
import {connect} from "react-redux";
import arbitration from '../../features/arbitration';
import successImage from '../../../images/success.png';
class OpenDispute extends Component {
state = {
value: '',
displayDialog: false
}
constructor(props){
super(props);
props.loadArbitration(props.escrowId);
}
componentDidUpdate() {
if (this.props.escrow.arbitration) {
if(this.props.escrow.arbitration.open || this.props.escrow.arbitration.result !== "0"){
this.props.history.push('/');
}
}
}
handleChange = (e) => {
this.setState({value: e.target.value});
}
displayDialog = show => () => {
this.setState({displayDialog: show});
};
goToProfile = () => {
this.props.history.push('/profile');
}
handleClickDialog = escrowId => () => {
this.props.openDispute(escrowId);
}
render(){
const {escrow, loading, receipt} = this.props;
if(!escrow) return <Loading page />;
if(loading) return <Loading mining />;
if(receipt) return (
<div className="text-center p-5">
<img src={successImage} alt="Success" width="160" height="160" className="mt-5" />
<h2 className="mt-5">Your dispute was successfully open</h2>
<p className="text-muted">Follow the progress of your dispute in the profile.</p>
<p>
<Button color="primary" onClick={this.goToProfile}>Okay</Button>
</p>
</div>
);
return (
<div className="openDispute">
<h2>Open dispute</h2>
<p>Describe details of your trade</p>
<Form>
<Textarea
type="text"
name="disputeDetails"
id="disputeDetails"
rows="7"
placeholder="What has happened?"
className="form-control mb-2"
value={this.state.value}
onChange={this.handleChange}
validations={[]}
/>
<p className="text-muted">The process of resolving your dispute could take up on 5 days. You will be sharing your chat logs with an arbiter.</p>
<p className="text-center">
<Button color="primary" disabled={!this.state.value} onClick={this.displayDialog(true)}>Send</Button>
</p>
</Form>
<ConfirmDialog display={this.state.displayDialog} onConfirm={this.handleClickDialog(escrow.escrowId)} onCancel={this.displayDialog(false)} title="Open dispute" content="Are you sure?" />
</div>
);
}
}
OpenDispute.propTypes = {
history: PropTypes.object,
escrow: PropTypes.object,
escrowId: PropTypes.string,
loadArbitration: PropTypes.func,
openDispute: PropTypes.func,
loading: PropTypes.bool,
receipt: PropTypes.object
};
const mapStateToProps = (state, props) => {
return {
escrowId: props.match.params.id.toString(),
escrow: arbitration.selectors.getArbitration(state),
loading: arbitration.selectors.loading(state),
receipt: arbitration.selectors.receipt(state)
};
};
export default connect(
mapStateToProps,
{
loadArbitration: arbitration.actions.loadArbitration,
openDispute: arbitration.actions.openDispute
}
)(withRouter(OpenDispute));