ProposalDetails.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import React from 'react';
  2. import Details from './Details';
  3. import Body from './Body';
  4. import VotingSection from './VotingSection';
  5. import Votes from './Votes';
  6. import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
  7. import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
  8. import { withCalls } from '@polkadot/react-api';
  9. import { withMulti } from '@polkadot/react-api/with';
  10. import './Proposal.css';
  11. import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses, ExecutionFailedStatus } from '@joystream/types/proposals';
  12. import { BlockNumber } from '@polkadot/types/interfaces';
  13. import { MemberId } from '@joystream/types/members';
  14. import { Seat } from '@joystream/types/council';
  15. import ProposalDiscussion from './discussion/ProposalDiscussion';
  16. import styled from 'styled-components';
  17. const ProposalDetailsMain = styled.div`
  18. display: flex;
  19. @media screen and (max-width: 1199px) {
  20. flex-direction: column;
  21. }
  22. `;
  23. const ProposalDetailsVoting = styled.div`
  24. min-width: 30%;
  25. margin-left: 3%;
  26. @media screen and (max-width: 1399px) {
  27. min-width: 40%;
  28. }
  29. @media screen and (max-width: 1199px) {
  30. margin-left: 0;
  31. }
  32. `;
  33. const ProposalDetailsDiscussion = styled.div`
  34. margin-top: 1rem;
  35. max-width: 67%;
  36. @media screen and (max-width: 1399px) {
  37. max-width: none;
  38. }
  39. `;
  40. // TODO: That should probably be moved to joy-utils/functions/proposals (or transport)
  41. type BasicProposalStatus = 'Active' | 'Finalized';
  42. type ProposalPeriodStatus = 'Voting period' | 'Grace period';
  43. type ProposalDisplayStatus = BasicProposalStatus | ProposalDecisionStatuses | ApprovedProposalStatuses;
  44. export type ExtendedProposalStatus = {
  45. displayStatus: ProposalDisplayStatus;
  46. periodStatus: ProposalPeriodStatus | null;
  47. expiresIn: number | null;
  48. finalizedAtBlock: number | null;
  49. executedAtBlock: number | null;
  50. executionFailReason: string | null;
  51. }
  52. export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNumber | undefined): ExtendedProposalStatus {
  53. const basicStatus = Object.keys(proposal.status)[0] as BasicProposalStatus;
  54. let expiresIn: number | null = null;
  55. let displayStatus: ProposalDisplayStatus = basicStatus;
  56. let periodStatus: ProposalPeriodStatus | null = null;
  57. let finalizedAtBlock: number | null = null;
  58. let executedAtBlock: number | null = null;
  59. let executionFailReason: string | null = null;
  60. const best = bestNumber ? bestNumber.toNumber() : 0;
  61. const { votingPeriod, gracePeriod } = proposal.parameters;
  62. const blockAge = best - proposal.createdAtBlock;
  63. if (basicStatus === 'Active') {
  64. periodStatus = 'Voting period';
  65. expiresIn = Math.max(votingPeriod - blockAge, 0) || null;
  66. }
  67. if (basicStatus === 'Finalized') {
  68. const { finalizedAt, proposalStatus } = proposal.status.Finalized;
  69. const decisionStatus: ProposalDecisionStatuses = Object.keys(proposalStatus)[0] as ProposalDecisionStatuses;
  70. displayStatus = decisionStatus;
  71. finalizedAtBlock = finalizedAt as number;
  72. if (decisionStatus === 'Approved') {
  73. const approvedStatus: ApprovedProposalStatuses = Object.keys(proposalStatus.Approved)[0] as ApprovedProposalStatuses;
  74. if (approvedStatus === 'PendingExecution') {
  75. const finalizedAge = best - finalizedAt;
  76. periodStatus = 'Grace period';
  77. expiresIn = Math.max(gracePeriod - finalizedAge, 0) || null;
  78. } else {
  79. // Executed / ExecutionFailed
  80. displayStatus = approvedStatus;
  81. executedAtBlock = finalizedAtBlock + gracePeriod;
  82. if (approvedStatus === 'ExecutionFailed') {
  83. const executionFailedStatus = proposalStatus.Approved.ExecutionFailed as ExecutionFailedStatus;
  84. executionFailReason = Buffer.from(executionFailedStatus.error.toString().replace('0x', ''), 'hex').toString();
  85. }
  86. }
  87. }
  88. }
  89. return {
  90. displayStatus,
  91. periodStatus,
  92. expiresIn: best ? expiresIn : null,
  93. finalizedAtBlock,
  94. executedAtBlock,
  95. executionFailReason
  96. };
  97. }
  98. type ProposalDetailsProps = MyAccountProps & {
  99. proposal: ParsedProposal;
  100. proposalId: ProposalId;
  101. bestNumber?: BlockNumber;
  102. council?: Seat[];
  103. };
  104. function ProposalDetails ({
  105. proposal,
  106. proposalId,
  107. myAddress,
  108. myMemberId,
  109. iAmMember,
  110. council,
  111. bestNumber
  112. }: ProposalDetailsProps) {
  113. const iAmCouncilMember = Boolean(iAmMember && council && council.some(seat => seat.member.toString() === myAddress));
  114. const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber());
  115. const extendedStatus = getExtendedStatus(proposal, bestNumber);
  116. const isVotingPeriod = extendedStatus.periodStatus === 'Voting period';
  117. return (
  118. <div className="Proposal">
  119. <Details proposal={proposal} extendedStatus={extendedStatus} proposerLink={ true }/>
  120. <ProposalDetailsMain>
  121. <Body
  122. type={ proposal.type }
  123. title={ proposal.title }
  124. description={ proposal.description }
  125. params={ proposal.details }
  126. iAmProposer={ iAmProposer }
  127. proposalId={ proposalId }
  128. proposerId={ proposal.proposerId }
  129. isCancellable={ isVotingPeriod }
  130. cancellationFee={ proposal.cancellationFee }
  131. />
  132. <ProposalDetailsVoting>
  133. { iAmCouncilMember && (
  134. <VotingSection
  135. proposalId={proposalId}
  136. memberId={ myMemberId as MemberId }
  137. isVotingPeriod={ isVotingPeriod }/>
  138. ) }
  139. <Votes proposal={proposal}/>
  140. </ProposalDetailsVoting>
  141. </ProposalDetailsMain>
  142. <ProposalDetailsDiscussion>
  143. <ProposalDiscussion
  144. proposalId={proposalId}
  145. memberId={ iAmMember ? myMemberId : undefined }/>
  146. </ProposalDetailsDiscussion>
  147. </div>
  148. );
  149. }
  150. export default withMulti<ProposalDetailsProps>(
  151. ProposalDetails,
  152. withMyAccount,
  153. withCalls(
  154. ['derive.chain.bestNumber', { propName: 'bestNumber' }],
  155. ['query.council.activeCouncil', { propName: 'council' }] // TODO: Handle via transport?
  156. )
  157. );