Skip to content

Commit

Permalink
Use state machine to update vote choice
Browse files Browse the repository at this point in the history
  • Loading branch information
vctt94 committed Apr 3, 2020
1 parent 7069e6d commit 008c579
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 104 deletions.
125 changes: 125 additions & 0 deletions app/components/views/ProposalDetails/ChooseVoteOption.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { FormattedMessage as T } from "react-intl";
import { PassphraseModalButton } from "buttons";
import { fetchMachine } from "stateMachines/FetchStateMachine";
import { useMachine } from "@xstate/react";
import { StakeyBounceXs } from "indicators";
import { useDispatch } from "react-redux";
import { useState } from "react";
import { ProposalError } from "./helpers"
import * as gov from "actions/GovernanceActions";

const VoteOption = ({ value, description, onClick, checked }) => (
<div className="proposal-vote-option">
<input className={value} type="radio" id={value} name="proposalVoteChoice"
readOnly={!onClick} onChange={onClick}
value={value}
checked ={checked}
/>
<label className={"radio-label " + value} htmlFor={value}/>{description}
</div>
);

function UpdateVoteChoiceModalButton({ onSubmit, newVoteChoice, eligibleTicketCount }) {
return (
<PassphraseModalButton
modalTitle={
<>
<T id="proposals.updateVoteChoiceModal.title" m="Confirm Your Vote" />
<div className="proposal-vote-confirmation">
<div className={newVoteChoice+"-proposal"}/>
{newVoteChoice}
</div>
</>
}
modalDescription={
<T
id="proposalDetails.votingInfo.eligibleCount"
m="You have {count, plural, one {one ticket} other {# tickets}} eligible for voting"
values={{ count: eligibleTicketCount }}
/>
}
disabled={!newVoteChoice}
onSubmit={onSubmit}
buttonLabel={<T id="proposals.updateVoteChoiceModal.btnLabel" m="Cast Vote" />}
/>
)};

function getError(error) {
if (!error) return;
if (typeof error === "string") return error;
if (typeof error === "object") {
if (error.message) return error.message;
return JSON.stringify(error);
}
}

function ChooseVoteOption({
viewedProposalDetails, voteOptions, currentVoteChoice, votingComplete, eligibleTicketCount
}) {
const [ newVoteChoice, setVoteOption ] = useState(null);

const dispatch = useDispatch();
const onUpdateVoteChoice = (privatePassphrase) => dispatch(
gov.updateVoteChoice(viewedProposalDetails, newVoteChoice, privatePassphrase)
);
const [ state, send ] = useMachine(fetchMachine, {
actions: {
initial: () => ({}),
load: (context, event) => {
const { privatePassphrase } = event;
if(!newVoteChoice) return;
onUpdateVoteChoice(privatePassphrase)
.then(() => send("RESOLVE"))
.catch(error => send({ type: "REJECT", error }));
}
}
});

const error = state && state.context && getError(state.context.error);
const ChooseOptions = () => (
<>
<div className="proposal-details-voting-preference">
<div className="proposal-details-voting-preference-title"><T id="proposalDetails.votingInfo.votingPreferenceTitle" m="My Voting Preference" /></div>
<div className="proposal-details-current-choice-box">
{ voteOptions.map(o => {
return <VoteOption
value={o.id} key={o.id}
description={o.id.charAt(0).toUpperCase()+o.id.slice(1)}
onClick={ () => currentVoteChoice === "abstain" && setVoteOption(o.id) }
checked={ newVoteChoice ? newVoteChoice === o.id : currentVoteChoice !== "abstain" ? currentVoteChoice.id === o.id : null }
/>
})}
</div>
</div>
{ !votingComplete &&
<UpdateVoteChoiceModalButton {...{
newVoteChoice, onSubmit: (privatePassphrase) => send({type: "FETCH", privatePassphrase }), eligibleTicketCount
}} />
}
</>
);

switch (state.value) {
case "idle":
return <ChooseOptions {...{
setVoteOption, newVoteChoice, eligibleTicketCount, currentVoteChoice,
voteOptions, votingComplete
}} />;
case "loading":
return (
<div className="proposal-details-updating-vote-choice">
<StakeyBounceXs />
<T id="proposalDetails.votingInfo.updatingVoteChoice" m="Updating vote choice" />...
</div>
);
case "success":
return <ChooseOptions {...{
setVoteOption, newVoteChoice, eligibleTicketCount, currentVoteChoice,
voteOptions, votingComplete
}} />;
case "failure":
return <ProposalError {...{ error }} />;
}
}

export default ChooseVoteOption;
49 changes: 24 additions & 25 deletions app/components/views/ProposalDetails/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,58 @@ import { PoliteiaLink, VerticalAccordion } from "shared";
import {
ProposalNotVoting, NoTicketsVotingInfo, OverviewField, OverviewVotingProgressInfo,
NoElligibleTicketsVotingInfo, UpdatingVoteChoice, TimeValue,
ChooseVoteOption, ProposalText, ProposalAbandoned
ProposalText, ProposalAbandoned
} from "./helpers";
import ChooseVoteOption from "./ChooseVoteOption";
import {
VOTESTATUS_ACTIVEVOTE, VOTESTATUS_FINISHEDVOTE, PROPOSALSTATUS_ABANDONED
} from "actions/GovernanceActions";
import { useSelector } from "react-redux";
import { useState } from "react"
import * as sel from "selectors";

function ProposalDetails ({ viewedProposalDetails, goBackHistory,
showPurchaseTicketsPage, setVoteOption, onUpdateVoteChoice,
newVoteChoice, updateVoteChoiceAttempt, text, showWalletEligibleTickets,
onToggleWalletEligibleTickets }) {

function ProposalDetails ({
viewedProposalDetails, showPurchaseTicketsPage, setVoteOption,
newVoteChoice, updateVoteChoiceAttempt, text, goBackHistory,
onUpdateVoteChoice
}) {
const {
creator, timestamp, endTimestamp, currentVoteChoice, hasEligibleTickets,
name, token, voteStatus, proposalStatus, voteOptions, voteCounts,
version, quorumMinimumVotes, walletEligibleTickets
} = viewedProposalDetails;
const eligibleTicketCount = viewedProposalDetails.walletEligibleTickets && viewedProposalDetails.walletEligibleTickets.length;
const tsDate = useSelector(sel.tsDate);
const hasTickets = useSelector(sel.hasTickets);
const { name, token, voteStatus, proposalStatus, voteOptions, voteCounts,
creator, timestamp, endTimestamp, currentVoteChoice, hasEligibleTickets,
version, quorumMinimumVotes, walletEligibleTickets } = viewedProposalDetails;
const [ showWalletEligibleTickets, toggleWalletEligibleTickets ] = useState(false);

const getVoteInfo = ({
voteStatus, voteOptions, onUpdateVoteChoice, setVoteOption, newVoteChoice,
eligibleTicketCount,currentVoteChoice, showPurchaseTicketsPage
}) => {
// getVoteInfo is an auxiliar function to get the properly vote info component.
function getVoteInfo () {
if (voteStatus === VOTESTATUS_FINISHEDVOTE) {
return <ChooseVoteOption {...{ voteOptions, currentVoteChoice, votingComplete: true }} />;
}
if (voteStatus === VOTESTATUS_ACTIVEVOTE) {
if (updateVoteChoiceAttempt) {
return <UpdatingVoteChoice />;
}
if (!hasTickets) {
return <NoTicketsVotingInfo {...{ showPurchaseTicketsPage }} />;
}
if (!hasEligibleTickets) {
return <NoElligibleTicketsVotingInfo {...{ showPurchaseTicketsPage }} />;
}

return <ChooseVoteOption {...{ voteOptions, onUpdateVoteChoice,
setVoteOption, newVoteChoice, eligibleTicketCount,
currentVoteChoice, votingComplete: false }} />;
return <ChooseVoteOption {...{
viewedProposalDetails, voteOptions, setVoteOption, newVoteChoice,
eligibleTicketCount, currentVoteChoice, votingComplete: false
}} />;
}
return <ProposalNotVoting />;
};

const eligibleTicketCount = viewedProposalDetails.walletEligibleTickets && viewedProposalDetails.walletEligibleTickets.length;
let voteInfo = null;
// Check if proposal is abandoned. If it is not we check its vote status
if (proposalStatus === PROPOSALSTATUS_ABANDONED) {
voteInfo = <ProposalAbandoned />;
} else {
voteInfo = getVoteInfo({
voteStatus, voteOptions, onUpdateVoteChoice, setVoteOption, newVoteChoice,
eligibleTicketCount,currentVoteChoice, showPurchaseTicketsPage
voteStatus, voteOptions, setVoteOption, newVoteChoice, eligibleTicketCount,
currentVoteChoice, showPurchaseTicketsPage, hasTickets, hasEligibleTickets
});
}

Expand Down Expand Up @@ -99,7 +98,7 @@ function ProposalDetails ({ viewedProposalDetails, goBackHistory,
</div>
}
show={showWalletEligibleTickets}
onToggleAccordion={onToggleWalletEligibleTickets}
onToggleAccordion={ () => toggleWalletEligibleTickets(!showWalletEligibleTickets)}
className="proposal-details-wallet-eligible-tickets"
>
{walletEligibleTickets.map((t, i) => (
Expand Down

This file was deleted.

33 changes: 1 addition & 32 deletions app/components/views/ProposalDetails/helpers.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { shell } from "electron";
import { KeyBlueButton, InvisibleConfirmPoliteiaModalButton } from "buttons";
import { FormattedMessage as T } from "react-intl";
import { StakeyBounceXs, VotingProgress, PoliteiaLoading } from "indicators";
import { VotingProgress, PoliteiaLoading } from "indicators";
import { showCheck } from "helpers";
import UpdateVoteChoiceModalButton from "./UpdateVoteChoiceModalButton";
import { default as ReactMarkdown } from "react-markdown";
import { FormattedRelative } from "shared";

Expand Down Expand Up @@ -32,36 +31,6 @@ export const NoElligibleTicketsVotingInfo = ({ showPurchaseTicketsPage }) => (
</>
);

const VoteOption = ({ value, description, onClick, checked }) => (
<div className="proposal-vote-option">
<input className={value} type="radio" id={value} name="proposalVoteChoice" readOnly={!onClick} onChange={onClick ? () => onClick(value) : null}
value={value} checked={checked} />
<label className={"radio-label " + value} htmlFor={value}/>{description}
</div>
);

export const ChooseVoteOption = ({ voteOptions, onUpdateVoteChoice, setVoteOption, newVoteChoice, currentVoteChoice, votingComplete, eligibleTicketCount }) => (
<>
<div className="proposal-details-voting-preference">
<div className="proposal-details-voting-preference-title"><T id="proposalDetails.votingInfo.votingPreferenceTitle" m="My Voting Preference" /></div>
<div className="proposal-details-current-choice-box">
{voteOptions.map(o => (
<VoteOption value={o.id} description={o.id.charAt(0).toUpperCase()+o.id.slice(1)} key={o.id} checked={currentVoteChoice !== "abstain" ? o.id === currentVoteChoice.id : null }
onClick={ (currentVoteChoice === "abstain" && !votingComplete) ? setVoteOption : null }/>
))}
</div>
</div>
{!votingComplete && <UpdateVoteChoiceModalButton {...{ newVoteChoice, onUpdateVoteChoice, eligibleTicketCount }} />}
</>
);

export const UpdatingVoteChoice = () => (
<div className="proposal-details-updating-vote-choice">
<StakeyBounceXs />
<T id="proposalDetails.votingInfo.updatingVoteChoice" m="Updating vote choice" />...
</div>
);

export const OverviewField = showCheck(( { label, value } ) => (
<div className="proposal-details-overview-field">
<div className="label">{label}:</div>
Expand Down
14 changes: 1 addition & 13 deletions app/components/views/ProposalDetails/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,13 @@ function ProposalDetails() {
let viewedProposalDetails;
let text = "";

const [ showWalletEligibleTickets, toggleShowWalletEligibleTickets ] = useState(false);
const [ newVoteChoice, setVoteOption ] = useState(null);
const { token } = useParams();
const proposalsDetails = useSelector(sel.proposalsDetails);
const getProposalError = useSelector(sel.getProposalError);

const getProposalDetails = (token) => dispatch(gov.getProposalDetails(token));
const goBackHistory = () => dispatch(cli.goBackHistory());

async function onUpdateVoteChoice(privatePassphrase) {
if (!viewedProposalDetails || !newVoteChoice) return;
await dispatch(gov.updateVoteChoice(viewedProposalDetails, newVoteChoice, privatePassphrase));
return true;
}

const [ state, send ] = useMachine(fetchMachine, {
actions: {
initial: () => {
Expand All @@ -55,11 +47,7 @@ function ProposalDetails() {
text += politeiaMarkdownIndexMd(f.payload);
}
});
return <Page {...{
newVoteChoice, showWalletEligibleTickets, text, viewedProposalDetails,
onToggleWalletEligibleTickets: toggleShowWalletEligibleTickets,
goBackHistory, onUpdateVoteChoice, setVoteOption
}} />;
return <Page {...{ text, viewedProposalDetails, goBackHistory }} />;
case "failure":
return <ProposalError error={getProposalError} />;
default:
Expand Down
6 changes: 0 additions & 6 deletions app/reducers/governance.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,14 @@ export default function governance(state = {}, action) {
},
proposals: { ...action.proposals }
};
case UPDATEVOTECHOICE_ATTEMPT:
return { ...state, updateVoteChoiceAttempt: true };
case UPDATEVOTECHOICE_SUCCESS:
return {
...state,
proposals: { ...action.proposals },
updateVoteChoiceAttempt: false,
proposalsDetails: { ...state.proposalsDetails,
[action.token]: { ...action.proposal }
}
};

case UPDATEVOTECHOICE_FAILED:
return { ...state, updateVoteChoiceAttempt: false };
case WALLETREADY:
return { ...state,
lastPoliteiaAccessTime: action.lastPoliteiaAccessTime,
Expand Down
1 change: 0 additions & 1 deletion app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,6 @@ export const allAgendas = createSelector(

export const treasuryBalance = get([ "grpc", "treasuryBalance" ]);

export const updateVoteChoiceAttempt = get([ "governance", "updateVoteChoiceAttempt" ]);
export const proposals = get([ "governance", "proposals" ]);
export const proposallistpagesize = get([ "governance", "proposallistpagesize" ]);
export const getProposalsAttempt = get([ "governance", "getProposalsAttempt" ]);
Expand Down
10 changes: 8 additions & 2 deletions app/stateMachines/FetchStateMachine.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ export const fetchMachine = Machine({
id: "fetch",
initial: "idle",
context: {
retries: 0
retries: 0,
error: null
},
states: {
idle: {
Expand All @@ -18,7 +19,12 @@ export const fetchMachine = Machine({
entry: [ "load" ],
on: {
RESOLVE: "success",
REJECT: "failure"
REJECT: {
target: "failure",
actions: assign({
error: (context, event) => event.error
})
}
}
},
success: {
Expand Down

0 comments on commit 008c579

Please sign in to comment.