Skip to content

Commit

Permalink
Merge pull request #1871 from daostack/cw-1864-proposal-resolution-type
Browse files Browse the repository at this point in the history
New approval (proposal) card #1864
  • Loading branch information
andreymikhadyuk authored Jul 26, 2023
2 parents f304646 + cce0878 commit de6f753
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 40 deletions.
41 changes: 32 additions & 9 deletions src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
CommonFeed,
Governance,
PredefinedTypes,
ResolutionType,
} from "@/shared/models";
import { TextEditorValue } from "@/shared/ui-kit";
import {
Expand All @@ -38,7 +39,9 @@ import {
ProposalFeedVotingInfo,
ProposalFeedButtonContainer,
UserVoteInfo,
ImmediateProposalInfo,
} from "./components";
import { ImmediateProposalVoteInfo } from "./components/ImmediateProposalVoteInfo";
import { useProposalSpecificData } from "./hooks";
import {
checkIsVotingAllowed,
Expand Down Expand Up @@ -314,21 +317,41 @@ const ProposalFeedCard: React.FC<ProposalFeedCardProps> = (props) => {
onHover(false);
}}
>
<ProposalFeedVotingInfo
proposal={proposal}
governanceCircles={governanceCircles}
/>
{proposal.resolutionType === ResolutionType.WAIT_FOR_EXPIRATION && (
<>
<ProposalFeedVotingInfo
proposal={proposal}
governanceCircles={governanceCircles}
/>
<UserVoteInfo
userVote={userVote}
userHasPermissionsToVote={userHasPermissionsToVote}
isCountdownState={isCountdownState}
/>
</>
)}

{proposal.resolutionType === ResolutionType.IMMEDIATE && (
<>
<ImmediateProposalInfo
proposal={proposal}
governanceCircles={governanceCircles}
proposerUserName={getUserName(feedItemUser)}
/>
<ImmediateProposalVoteInfo
proposal={proposal}
userVote={userVote}
/>
</>
)}

{isVotingAllowed && (
<ProposalFeedButtonContainer
proposalId={proposal.id}
onVoteCreate={setVote}
resolutionType={proposal.resolutionType}
/>
)}
<UserVoteInfo
userVote={userVote}
userHasPermissionsToVote={userHasPermissionsToVote}
isCountdownState={isCountdownState}
/>
</FeedCardContent>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import "../../../../../../constants";

.container {
display: flex;
flex-direction: column;
margin-bottom: 2rem;
}

.title {
font-size: $moderate-small-2;
}

.subtitle {
font-size: $mobile-title;
color: var(--gray-60);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from "react";
import { getVotersString } from "@/pages/OldCommon/containers/ProposalContainer/helpers";
import { Governance, Proposal } from "@/shared/models";
import styles from "./ImmediateProposalInfo.module.scss";

interface ImmediateProposalInfoProps {
proposal: Proposal;
governanceCircles: Governance["circles"];
proposerUserName: string;
}

export const ImmediateProposalInfo = ({
proposal,
governanceCircles,
proposerUserName,
}: ImmediateProposalInfoProps) => {
const votersString = getVotersString(
proposal.global.weights,
governanceCircles,
);

return (
<div className={styles.container}>
<div className={styles.title}>
{`${proposerUserName} requests to join ${votersString} circle`}
</div>
{/* Show this only when the required number of voters is greater than 1. Logic for this will be added in the future, see detalis here https://github.com/daostack/common-backend/issues/1844 */}
{/* <div className={styles.subtitle}>
{`Approval by ${votersString} (1 from ${proposal.votes.totalMembersWithVotingRight} required)`}
</div> */}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ImmediateProposalInfo";
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import "../../../../../../constants";

.container {
display: flex;
}

.voteInfo {
flex-direction: column;
margin-left: 0.8rem;
}

.title {
font-size: $moderate-small-2;
}

.subtitle {
font-size: $mobile-title;
color: var(--gray-60);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { ReactNode, useEffect } from "react";
import { useSelector } from "react-redux";
import { selectUser } from "@/pages/Auth/store/selectors";
import { useEligibleVoters } from "@/shared/hooks/useCases";
import { VoteAbstain, VoteAgainst, VoteFor } from "@/shared/icons";
import {
DateFormat,
Proposal,
ProposalState,
Vote,
VoteOutcome,
} from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
import { formatDate } from "@/shared/utils";
import styles from "./ImmediateProposalVoteInfo.module.scss";

interface ImmediateProposalVoteInfoProps {
proposal: Proposal;
userVote: Vote | null;
}

const VOTE_OUTCOME_TO_TEXT_MAP: Record<VoteOutcome, string> = {
[VoteOutcome.Approved]: "Approved",
[VoteOutcome.Abstained]: "Abstained",
[VoteOutcome.Rejected]: "Rejected",
};

const VOTE_OUTCOME_TO_ICON_MAP: Record<VoteOutcome, ReactNode> = {
[VoteOutcome.Approved]: <VoteFor className={styles.icon} />,
[VoteOutcome.Abstained]: <VoteAbstain className={styles.icon} />,
[VoteOutcome.Rejected]: <VoteAgainst className={styles.icon} />,
};

export const ImmediateProposalVoteInfo = ({
proposal,
userVote,
}: ImmediateProposalVoteInfoProps) => {
const user = useSelector(selectUser());
const { loading, voters, fetchEligibleVoters, error } = useEligibleVoters();

const isExpired =
proposal.state === ProposalState.FAILED && proposal.votes.total === 0;

/**
* For now we assume that IMMEDIATE proposal is always a single vote proposal.
* In future we would want to support more than a single vote so the logic and the UI might change.
* See more details here https://github.com/daostack/common-backend/issues/1844.
*/
const vote = voters && voters.length > 0 ? voters[0] : undefined;

useEffect(() => {
if (!isExpired) {
fetchEligibleVoters(proposal.id);
}
}, [proposal.id, isExpired]);

if (error) {
return (
<div>
Oops! Something went wrong while loading the vote info. Please try again
later.
</div>
);
}

if (!isExpired && loading) {
return <Loader />;
}

const outcome = userVote?.outcome || vote?.vote?.outcome;
const userId = userVote?.voterId || vote?.userId;
const createdAt =
userVote?.createdAt.seconds || vote?.vote?.createdAt.seconds;

return (
<div className={styles.container}>
{outcome && VOTE_OUTCOME_TO_ICON_MAP[outcome]}
<div className={styles.voteInfo}>
<div className={styles.title}>
{isExpired && "Expired"}
{outcome &&
`${VOTE_OUTCOME_TO_TEXT_MAP[outcome]} by ${
user?.uid === userId ? "You" : vote?.user.displayName
}`}
</div>
<div className={styles.subtitle}>
{createdAt &&
formatDate(new Date(createdAt * 1000), DateFormat.LongHuman)}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./ImmediateProposalVoteInfo";
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
}
}

.containerImmediate {
grid-template-areas: "approve reject";
}

.buttonApprove {
grid-area: approve;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React, { FC, useState } from "react";
import classNames from "classnames";
import { useModal } from "@/shared/hooks";
import { Vote, VoteOutcome } from "@/shared/models";
import { ResolutionType, Vote, VoteOutcome } from "@/shared/models";
import { VoteButton } from "../VoteButton";
import { VoteModal } from "./components";
import styles from "./ProposalFeedButtonContainer.module.scss";

interface ProposalFeedButtonContainerProps {
proposalId: string;
onVoteCreate: (vote: Vote) => void;
resolutionType: ResolutionType;
}

export const ProposalFeedButtonContainer: FC<
ProposalFeedButtonContainerProps
> = (props) => {
const { proposalId, onVoteCreate } = props;
const { proposalId, onVoteCreate, resolutionType } = props;
const [voteOutcome, setVoteOutcome] = useState<VoteOutcome | null>(null);
const {
isShowing: isVoteModalOpen,
Expand All @@ -31,23 +33,33 @@ export const ProposalFeedButtonContainer: FC<
onVoteCreate(vote);
};

const isImmediate = resolutionType === ResolutionType.IMMEDIATE;

return (
<>
<div className={styles.container}>
<div
className={classNames(styles.container, {
[styles.containerImmediate]: isImmediate,
})}
>
<VoteButton
className={styles.buttonApprove}
voteOutcome={VoteOutcome.Approved}
onClick={handleVoteButtonClick}
resolutionType={resolutionType}
/>
<VoteButton
className={styles.buttonAbstain}
voteOutcome={VoteOutcome.Abstained}
onClick={handleVoteButtonClick}
/>
{!isImmediate && (
<VoteButton
className={styles.buttonAbstain}
voteOutcome={VoteOutcome.Abstained}
onClick={handleVoteButtonClick}
/>
)}
<VoteButton
className={styles.buttonReject}
voteOutcome={VoteOutcome.Rejected}
onClick={handleVoteButtonClick}
resolutionType={resolutionType}
/>
</div>
{voteOutcome && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
.button {
display: flex;
justify-content: space-between;
border-radius: 0.25rem;
border-radius: 0.3rem;
padding: 0.875rem 1.25rem;
background-color: transparent;
border: 0.06rem solid;
}

.buttonText {
Expand All @@ -15,41 +17,35 @@
}

.approveText {
color: $logo-1;
}
.approveButton {
background-color: $success-100;
box-shadow: $success-100-shadow;
color: var(--primary-fill);
border-color: var(--primary-fill);
}

.approveButton:hover {
background-color: $success-100-hover;
}

.abstainText {
color: $c-neutrals-300;
}

.abstainButton {
background-color: $c-neutrals-100;
box-shadow: $c-neutrals-100-shadow;
color: var(--strong-gray);
border-color: var(--strong-gray);
}

.abstainButton:hover {
background-color: $c-neutrals-100-hover;
}

.rejectText {
color: $c-error-300;
}

.rejectButton {
background-color: $c-error-100;
box-shadow: $c-error-100-shadow;
color: var(--red);
border-color: var(--red);
}

.rejectButton:hover {
background-color: $c-error-100-hover;
}

.icon {
Expand Down
Loading

0 comments on commit de6f753

Please sign in to comment.