Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functional proposal details #2449

Merged
merged 8 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions app/actions/ControlActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,6 @@ export const publishUnminedTransactionsAttempt = () => (dispatch, getState) => {
}
};

export const MODAL_SHOWN = "MODAL_SHOWN";
export const MODAL_HIDDEN = "MODAL_HIDDEN";
export const modalShown = () => (dispatch) => dispatch({ type: MODAL_SHOWN });
export const modalHidden = () => (dispatch) => dispatch({ type: MODAL_HIDDEN });

export const SHOW_ABOUT_MODAL_MACOS = "SHOW_ABOUT_MODAL_MACOS";
export const showAboutModalMacOS = () => (dispatch) => dispatch({ type: SHOW_ABOUT_MODAL_MACOS });

Expand Down
10 changes: 2 additions & 8 deletions app/actions/GovernanceActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ const getProposalEligibleTickets = async (token, allEligibleTickets, shouldCache
return await getWalletEligibleTickets(allEligibleTickets, walletService);
};


// updateInventoryFromApiData receives politeia data from getTokenInventory and
// put it in decrediton's inventory format.
// @param data - data from getTokenInventory api.
Expand Down Expand Up @@ -445,7 +444,6 @@ export const getProposalDetails = (token) => async (dispatch, getState) => {
numComments: p.numcomments,
timestamp: p.timestamp,
files: files,
hasDetails: true,
hasEligibleTickets: false
};

Expand Down Expand Up @@ -489,12 +487,8 @@ export const getProposalDetails = (token) => async (dispatch, getState) => {
}
};

export const viewProposalDetails = (token) => (dispatch, getState) => {
const details = sel.proposalsDetails(getState());
if (!details[token] || !details[token].hasDetails) {
dispatch(getProposalDetails(token));
}
dispatch(pushHistory("/proposal/details/" + token));
export const viewProposalDetails = (token) => (dispatch) => {
dispatch(pushHistory(`/proposal/details/${token}`));
};

export const UPDATEVOTECHOICE_ATTEMPT = "UPDATEVOTECHOICE_ATTEMPT";
Expand Down
70 changes: 30 additions & 40 deletions app/components/modals/Modal.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,47 @@
import { showCheck, eventOutsideElement } from "helpers";
import ReactDOM from "react-dom";
import { modal } from "connectors";
import EventListener from "react-event-listener";
import "style/Modals.less";
import Draggable from "react-draggable";
import cx from "classnames";

@autobind
class Modal extends React.Component {

constructor(props) {
super(props);
this.modalRef = React.createRef();
}

componentDidMount() {
this.props.modalShown();
}

componentWillUnmount() {
this.props.modalHidden();
}

mouseUp(event) {
const el = this.modalRef.current;
import { useRef } from "react";
import { useSelector } from "react-redux";
import * as sel from "selectors";

function Modal({
children, className, draggable, onCancelModal
}) {
const expandSideBar = useSelector(sel.expandSideBar);
const showingSidebarMenu = useSelector(sel.showingSidebarMenu);
const domNode = document.getElementById("modal-portal");
const modalRef = useRef(null);

const mouseUp = (event) => {
const el = modalRef.current;
if (eventOutsideElement(el, event.target)) {
this.props.onCancelModal && this.props.onCancelModal();
onCancelModal && onCancelModal();
}
}
};

onKeyDown(event) {
const onKeyDown = (event) => {
// 27: ESC key
if (event.keyCode === 27) {
this.props.onCancelModal && this.props.onCancelModal();
onCancelModal && onCancelModal();
}
}
};

render() {
const { children, className, expandSideBar, showingSidebarMenu, draggable } = this.props;
const domNode = document.getElementById("modal-portal");

const innerView = <div ref={this.modalRef} className={cx((showingSidebarMenu ? expandSideBar ? "app-modal " : "app-modal-reduced-bar " : "app-modal-standalone "), className && className, draggable && " draggable-modal ")}>
{children}
</div>;
const innerView = <div ref={modalRef} className={cx((showingSidebarMenu ? expandSideBar ? "app-modal " : "app-modal-reduced-bar " : "app-modal-standalone "), className && className, draggable && " draggable-modal ")}>
{children}
</div>;

return ReactDOM.createPortal(
<EventListener target="document" onMouseUp={this.mouseUp} onKeyDown={this.onKeyDown}>
<div className={showingSidebarMenu ? expandSideBar ? "app-modal-overlay" : "app-modal-overlay-reduced-bar" : "app-modal-overlay-standalone"}>
{draggable ? <Draggable bounds="parent" cancel=".cancel-dragging">{innerView}</Draggable> : innerView }
</div>
</EventListener>
, domNode);
}
return ReactDOM.createPortal(
<EventListener target="document" onMouseUp={mouseUp} onKeyDown={onKeyDown}>
<div className={showingSidebarMenu ? expandSideBar ? "app-modal-overlay" : "app-modal-overlay-reduced-bar" : "app-modal-overlay-standalone"}>
{draggable ? <Draggable bounds="parent" cancel=".cancel-dragging">{innerView}</Draggable> : innerView }
</div>
</EventListener>
, domNode);
}

export default showCheck(modal(Modal));
export default showCheck(Modal);
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { FormattedMessage as T } from "react-intl";
import { EnableExternalRequestButton } from "buttons";
import { EXTERNALREQUEST_POLITEIA } from "main_dev/externalRequests";
import { proposals } from "connectors";

export default proposals(({ getTokenAndInitialBatch }) => (
export default ({ getTokenAndInitialBatch }) => (
<div className="politeia-disabled-wrapper">
<p><T id="proposals.enablePoliteia.description" m="Politeia integration is currently disabled in your privacy settings. Please enable it if you want to be able to access the proposal system." /></p>
<EnableExternalRequestButton requestType={EXTERNALREQUEST_POLITEIA} onClick={getTokenAndInitialBatch}>
<T id="proposals.enablePoliteia.button" m="Enable Politeia Integration" />
</EnableExternalRequestButton>
</div>
));
);
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function ProposalList ({ finishedVote, tab }) {
send("RESOLVE");
},
load: () => {
if (!inventory || !inventory[tab]) return;
if (!proposals || !proposals[tab] || !inventory || !inventory[tab]) return;
if (proposals[tab].length >= inventory[tab].length) {
setNoMore(true);
return send("RESOLVE");
Expand Down
4 changes: 3 additions & 1 deletion app/components/views/GovernancePage/Proposals/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FormattedMessage as T } from "react-intl";
import { PoliteiaLink as PiLink } from "shared";
import { TabbedPage, TabbedPageTab as Tab } from "layout";
import { createElement as h, useEffect, useReducer } from "react";
import * as gov from "actions/GovernanceActions";
import * as sel from "selectors";

const PageHeader = () => (
Expand Down Expand Up @@ -62,13 +63,14 @@ function Proposals() {
useEffect(() => {
dispatch(setLastPoliteiaAccessTime());
}, []);
const getTokenAndInitialBatch = () => dispatch(gov.getTokenAndInitialBatch());
useEffect(() => {
const tab = getProposalsTab(location);
setTab(tab);
}, [ location ]);

if (!politeiaEnabled) {
return <PoliteiaDisabled />;
return <PoliteiaDisabled {...{ getTokenAndInitialBatch }} />;
}
return (
<TabbedPage caret={<div/>} header={<PageHeader />} >
Expand Down
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" />}
/>
);}

const 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({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function ChooseVoteOption({
const ChooseVoteOption = ({...}) =>

Copy link
Member Author

@vctt94 vctt94 Apr 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, as it is a component, I'd rather let it as function. Otherwise, as an arrow function it does not show at the stack trace, when erroring, which makes debug harder.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, makes sense

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;
Loading