Browse Source

Merge pull request #1381 from Lezek123/pioneer-old-proposals

Pioneer: Exploring old proposals
Mokhtar Naamani 4 years ago
parent
commit
459039c32b

+ 42 - 20
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -33,6 +33,7 @@ type BodyProps = {
   proposerId: number | MemberId;
   proposerId: number | MemberId;
   isCancellable: boolean;
   isCancellable: boolean;
   cancellationFee: number;
   cancellationFee: number;
+  historical?: boolean;
 };
 };
 
 
 function ProposedAddress (props: { accountId?: AccountId }) {
 function ProposedAddress (props: { accountId?: AccountId }) {
@@ -97,7 +98,7 @@ class ParsedParam {
 }
 }
 
 
 // The methods for parsing params by Proposal type.
 // The methods for parsing params by Proposal type.
-const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>) => ParsedParam[]} = {
+const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>, historical?: boolean) => ParsedParam[]} = {
   Text: (content) => [
   Text: (content) => [
     new ParsedParam(
     new ParsedParam(
       'Content',
       'Content',
@@ -237,12 +238,14 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
     new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
   ],
   ],
-  BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [
+  BeginReviewWorkingGroupLeaderApplication: ([id, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
     new ParsedParam(
       'Opening id',
       'Opening id',
-      <Link to={`/working-groups/opportunities/storageProviders/${id.toString()}`}>#{id.toString()}</Link>
+      historical
+        ? `#${id.toString()}`
+        : <Link to={`/working-groups/opportunities/storageProviders/${id.toString()}`}>#{id.toString()}</Link>
     )
     )
   ],
   ],
   FillWorkingGroupLeaderOpening: ({
   FillWorkingGroupLeaderOpening: ({
@@ -250,46 +253,56 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     successful_application_id: succesfulApplicationId,
     successful_application_id: succesfulApplicationId,
     reward_policy: rewardPolicy,
     reward_policy: rewardPolicy,
     working_group: workingGroup
     working_group: workingGroup
-  }) => [
+  }, historical) => [
     new ParsedParam('Working group', workingGroup.type),
     new ParsedParam('Working group', workingGroup.type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
     new ParsedParam(
       'Opening id',
       'Opening id',
-      <Link to={`/working-groups/opportunities/storageProviders/${openingId.toString()}`}>#{openingId.toString()}</Link>),
+      historical
+        ? `#${openingId.toString()}`
+        : <Link to={`/working-groups/opportunities/storageProviders/${openingId.toString()}`}>#{openingId.toString()}</Link>),
     new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'),
     new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'),
     new ParsedParam(
     new ParsedParam(
       'Result',
       'Result',
-      <ApplicationsDetailsByOpening
-        openingId={openingId.toNumber()}
-        acceptedIds={[succesfulApplicationId.toNumber()]}
-        group={workingGroup.type}/>,
+      historical
+        ? `Accepted application ID: ${succesfulApplicationId.toNumber()}`
+        : <ApplicationsDetailsByOpening
+          openingId={openingId.toNumber()}
+          acceptedIds={[succesfulApplicationId.toNumber()]}
+          group={workingGroup.type}/>,
       true
       true
     )
     )
   ],
   ],
-  SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  SlashWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Slash amount', formatBalance(amount as Balance)),
     new ParsedParam('Slash amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
-  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Decrease amount', formatBalance(amount as Balance)),
     new ParsedParam('Decrease amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
-  SetWorkingGroupLeaderReward: ([leadId, amount, group]) => [
+  SetWorkingGroupLeaderReward: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('New reward amount', formatBalance(amount as Balance)),
     new ParsedParam('New reward amount', formatBalance(amount as Balance)),
     new ParsedParam(
     new ParsedParam(
       'Lead',
       'Lead',
-      <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
+      historical
+        ? `#${(leadId as WorkerId).toNumber()}`
+        : <LeadInfoFromId group={(group as WorkingGroup).type} leadId={(leadId as WorkerId).toNumber()}/>,
       true
       true
     )
     )
   ],
   ],
@@ -298,12 +311,19 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     rationale,
     rationale,
     worker_id: leadId,
     worker_id: leadId,
     slash
     slash
-  }) => {
+  },
+  historical) => {
     return [
     return [
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Rationale', bytesToString(rationale), true),
       new ParsedParam('Rationale', bytesToString(rationale), true),
       new ParsedParam('Slash stake', slash.isTrue ? 'YES' : 'NO'),
       new ParsedParam('Slash stake', slash.isTrue ? 'YES' : 'NO'),
-      new ParsedParam('Lead', <LeadInfoFromId group={workingGroup.type} leadId={leadId.toNumber()}/>, true)
+      new ParsedParam(
+        'Lead',
+        historical
+          ? `#${leadId.toNumber()}`
+          : <LeadInfoFromId group={workingGroup.type} leadId={leadId.toNumber()}/>,
+        true
+      )
     ];
     ];
   }
   }
 };
 };
@@ -364,14 +384,16 @@ export default function Body ({
   proposalId,
   proposalId,
   proposerId,
   proposerId,
   isCancellable,
   isCancellable,
-  cancellationFee
+  cancellationFee,
+  historical
 }: BodyProps) {
 }: BodyProps) {
   // Assert more generic type (since TypeScript cannot possibly know the value of "type" here yet)
   // Assert more generic type (since TypeScript cannot possibly know the value of "type" here yet)
-  const parseParams = paramParsers[type] as (params: SpecificProposalDetails<ProposalType>) => ParsedParam[];
+  const parseParams = paramParsers[type] as (params: SpecificProposalDetails<ProposalType>, historical?: boolean) => ParsedParam[];
   const parsedParams = parseParams(
   const parsedParams = parseParams(
     type === 'RuntimeUpgrade'
     type === 'RuntimeUpgrade'
       ? params as RuntimeUpgradeProposalDetails
       ? params as RuntimeUpgradeProposalDetails
-      : (params as ProposalDetails).asType(type)
+      : (params as ProposalDetails).asType(type),
+    historical
   );
   );
 
 
   return (
   return (

+ 10 - 6
pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -56,7 +56,7 @@ export type ExtendedProposalStatus = {
   executionFailReason: string | null;
   executionFailReason: string | null;
 }
 }
 
 
-export function getExtendedStatus (proposal: ParsedProposal, bestNumber: BlockNumber | undefined): ExtendedProposalStatus {
+export function getExtendedStatus (proposal: ParsedProposal, bestNumber?: BlockNumber): ExtendedProposalStatus {
   const basicStatus: BasicProposalStatus = proposal.status.type;
   const basicStatus: BasicProposalStatus = proposal.status.type;
   let expiresIn: number | null = null;
   let expiresIn: number | null = null;
 
 
@@ -120,6 +120,7 @@ type ProposalDetailsProps = MyAccountProps & {
   proposalId: ProposalId;
   proposalId: ProposalId;
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
   council?: Seat[];
   council?: Seat[];
+  historical?: boolean;
 };
 };
 
 
 function ProposalDetails ({
 function ProposalDetails ({
@@ -129,11 +130,12 @@ function ProposalDetails ({
   myMemberId,
   myMemberId,
   iAmMember,
   iAmMember,
   council,
   council,
-  bestNumber
+  bestNumber,
+  historical
 }: ProposalDetailsProps) {
 }: ProposalDetailsProps) {
   const iAmCouncilMember = Boolean(iAmMember && council && council.some((seat) => seat.member.toString() === myAddress));
   const iAmCouncilMember = Boolean(iAmMember && council && council.some((seat) => seat.member.toString() === myAddress));
   const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber());
   const iAmProposer = Boolean(iAmMember && myMemberId !== undefined && proposal.proposerId === myMemberId.toNumber());
-  const extendedStatus = getExtendedStatus(proposal, bestNumber);
+  const extendedStatus = getExtendedStatus(proposal, historical ? undefined : bestNumber);
   const isVotingPeriod = extendedStatus.periodStatus === 'Voting period';
   const isVotingPeriod = extendedStatus.periodStatus === 'Voting period';
 
 
   return (
   return (
@@ -150,21 +152,23 @@ function ProposalDetails ({
           proposerId={ proposal.proposerId }
           proposerId={ proposal.proposerId }
           isCancellable={ isVotingPeriod }
           isCancellable={ isVotingPeriod }
           cancellationFee={ proposal.cancellationFee }
           cancellationFee={ proposal.cancellationFee }
+          historical={historical}
         />
         />
         <ProposalDetailsVoting>
         <ProposalDetailsVoting>
-          { iAmCouncilMember && (
+          { (iAmCouncilMember && !historical) && (
             <VotingSection
             <VotingSection
               proposalId={proposalId}
               proposalId={proposalId}
               memberId={ myMemberId as MemberId }
               memberId={ myMemberId as MemberId }
               isVotingPeriod={ isVotingPeriod }/>
               isVotingPeriod={ isVotingPeriod }/>
           ) }
           ) }
-          <Votes proposal={proposal}/>
+          <Votes proposal={proposal} historical={historical}/>
         </ProposalDetailsVoting>
         </ProposalDetailsVoting>
       </ProposalDetailsMain>
       </ProposalDetailsMain>
       <ProposalDetailsDiscussion>
       <ProposalDetailsDiscussion>
         <ProposalDiscussion
         <ProposalDiscussion
           proposalId={proposalId}
           proposalId={proposalId}
-          memberId={ iAmMember ? myMemberId : undefined }/>
+          memberId={ iAmMember ? myMemberId : undefined }
+          historical={historical}/>
       </ProposalDetailsDiscussion>
       </ProposalDetailsDiscussion>
     </div>
     </div>
   );
   );

+ 26 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx

@@ -1,12 +1,37 @@
 import React from 'react';
 import React from 'react';
 import { RouteComponentProps } from 'react-router-dom';
 import { RouteComponentProps } from 'react-router-dom';
 import ProposalDetails from './ProposalDetails';
 import ProposalDetails from './ProposalDetails';
-import { useProposalSubscription } from '@polkadot/joy-utils/react/hooks';
+import { usePromise, useProposalSubscription, useTransport } from '@polkadot/joy-utils/react/hooks';
 import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { useApi } from '@polkadot/react-hooks';
 import { useApi } from '@polkadot/react-hooks';
+import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
 
 
 type RouteParams = { id?: string | undefined };
 type RouteParams = { id?: string | undefined };
 
 
+export function HistoricalProposalFromId (props: RouteComponentProps<RouteParams>) {
+  const {
+    match: {
+      params: { id }
+    }
+  } = props;
+  const { api } = useApi();
+
+  const transport = useTransport();
+  const [proposal, error, loading] = usePromise(
+    () => transport.proposals.historicalProposalById(api.createType('ProposalId', id)),
+    null as ParsedProposal | null
+  );
+
+  return (
+    <PromiseComponent
+      error={error}
+      loading={loading}
+      message={'Fetching proposal...'}>
+      <ProposalDetails proposal={ proposal } proposalId={ id } historical/>
+    </PromiseComponent>
+  );
+}
+
 export default function ProposalFromId (props: RouteComponentProps<RouteParams>) {
 export default function ProposalFromId (props: RouteComponentProps<RouteParams>) {
   const {
   const {
     match: {
     match: {

+ 4 - 3
pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx

@@ -25,15 +25,16 @@ const ProposalDesc = styled.div`
 export type ProposalPreviewProps = {
 export type ProposalPreviewProps = {
   proposal: ParsedProposal;
   proposal: ParsedProposal;
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
+  historical?: boolean;
 };
 };
 
 
-export default function ProposalPreview ({ proposal, bestNumber }: ProposalPreviewProps) {
-  const extendedStatus = getExtendedStatus(proposal, bestNumber);
+export default function ProposalPreview ({ proposal, bestNumber, historical }: ProposalPreviewProps) {
+  const extendedStatus = getExtendedStatus(proposal, historical ? undefined : bestNumber);
 
 
   return (
   return (
     <Card
     <Card
       fluid
       fluid
-      href={`#/proposals/${proposal.id.toString()}`}>
+      href={`#/proposals/${historical ? 'historical/' : ''}${proposal.id.toString()}`}>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <Card.Content>
       <Card.Content>
         <Card.Header>
         <Card.Header>

+ 14 - 8
pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -13,6 +13,7 @@ import { Dropdown } from '@polkadot/react-components';
 
 
 type ProposalPreviewListProps = {
 type ProposalPreviewListProps = {
   bestNumber?: BlockNumber;
   bestNumber?: BlockNumber;
+  historical?: boolean;
 };
 };
 
 
 const FilterContainer = styled.div`
 const FilterContainer = styled.div`
@@ -22,6 +23,7 @@ const FilterContainer = styled.div`
   margin-bottom: 1.75rem;
   margin-bottom: 1.75rem;
 `;
 `;
 const StyledDropdown = styled(Dropdown)`
 const StyledDropdown = styled(Dropdown)`
+  margin-left: auto;
   .dropdown {
   .dropdown {
     width: 200px;
     width: 200px;
   }
   }
@@ -30,15 +32,17 @@ const PaginationBox = styled.div`
   margin-bottom: 1em;
   margin-bottom: 1em;
 `;
 `;
 
 
-function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
+function ProposalPreviewList ({ bestNumber, historical }: ProposalPreviewListProps) {
   const { pathname } = useLocation();
   const { pathname } = useLocation();
   const transport = useTransport();
   const transport = useTransport();
   const [activeFilter, setActiveFilter] = useState<ProposalStatusFilter>('All');
   const [activeFilter, setActiveFilter] = useState<ProposalStatusFilter>('All');
   const [currentPage, setCurrentPage] = useState<number>(1);
   const [currentPage, setCurrentPage] = useState<number>(1);
   const [proposalsBatch, error, loading] = usePromise<ProposalsBatch | undefined>(
   const [proposalsBatch, error, loading] = usePromise<ProposalsBatch | undefined>(
-    () => transport.proposals.proposalsBatch(activeFilter, currentPage),
+    () => historical
+      ? transport.proposals.historicalProposalsBatch(activeFilter, currentPage)
+      : transport.proposals.proposalsBatch(activeFilter, currentPage),
     undefined,
     undefined,
-    [activeFilter, currentPage]
+    [activeFilter, currentPage, historical]
   );
   );
 
 
   const filterOptions = proposalStatusFilters.map((filter) => ({
   const filterOptions = proposalStatusFilters.map((filter) => ({
@@ -54,10 +58,12 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
   return (
   return (
     <Container className='Proposal' fluid>
     <Container className='Proposal' fluid>
       <FilterContainer>
       <FilterContainer>
-        <Button primary as={Link} to={`${pathname}/new`}>
-          <Icon name='add' />
-          New proposal
-        </Button>
+        { !historical && (
+          <Button primary as={Link} to={`${pathname}/new`}>
+            <Icon name='add' />
+            New proposal
+          </Button>
+        ) }
         <StyledDropdown
         <StyledDropdown
           label='Proposal state'
           label='Proposal state'
           options={filterOptions}
           options={filterOptions}
@@ -85,7 +91,7 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
             ? (
             ? (
               <Card.Group>
               <Card.Group>
                 {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => (
                 {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => (
-                  <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} />
+                  <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} historical={historical}/>
                 ))}
                 ))}
               </Card.Group>
               </Card.Group>
             )
             )

+ 6 - 3
pioneer/packages/joy-proposals/src/Proposal/Votes.tsx

@@ -10,14 +10,17 @@ import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseCompon
 
 
 type VotesProps = {
 type VotesProps = {
   proposal: ParsedProposal;
   proposal: ParsedProposal;
+  historical?: boolean;
 };
 };
 
 
-export default function Votes ({ proposal: { id, votingResults } }: VotesProps) {
+export default function Votes ({ proposal: { id, votingResults }, historical }: VotesProps) {
   const transport = useTransport();
   const transport = useTransport();
   const [votes, error, loading] = usePromise<ProposalVotes | null>(
   const [votes, error, loading] = usePromise<ProposalVotes | null>(
-    () => transport.proposals.votes(id),
+    () => historical
+      ? transport.proposals.historicalVotes(id)
+      : transport.proposals.votes(id),
     null,
     null,
-    [votingResults]
+    [votingResults, historical]
   );
   );
 
 
   return (
   return (

+ 4 - 1
pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx

@@ -45,17 +45,20 @@ type ProposalDiscussionPostProps = {
   post: ParsedPost;
   post: ParsedPost;
   memberId?: MemberId;
   memberId?: MemberId;
   refreshDiscussion: () => void;
   refreshDiscussion: () => void;
+  historical?: boolean;
 }
 }
 
 
 export default function DiscussionPost ({
 export default function DiscussionPost ({
   post,
   post,
   memberId,
   memberId,
-  refreshDiscussion
+  refreshDiscussion,
+  historical
 }: ProposalDiscussionPostProps) {
 }: ProposalDiscussionPostProps) {
   const { author, authorId, text, createdAt, editsCount } = post;
   const { author, authorId, text, createdAt, editsCount } = post;
   const [editing, setEditing] = useState(false);
   const [editing, setEditing] = useState(false);
   const constraints = useTransport().proposals.discussionContraints();
   const constraints = useTransport().proposals.discussionContraints();
   const canEdit = (
   const canEdit = (
+    !historical &&
     memberId &&
     memberId &&
     post.postId &&
     post.postId &&
     authorId.toNumber() === memberId.toNumber() &&
     authorId.toNumber() === memberId.toNumber() &&

+ 11 - 5
pioneer/packages/joy-proposals/src/Proposal/discussion/ProposalDiscussion.tsx

@@ -11,16 +11,21 @@ import { MemberId } from '@joystream/types/members';
 type ProposalDiscussionProps = {
 type ProposalDiscussionProps = {
   proposalId: ProposalId;
   proposalId: ProposalId;
   memberId?: MemberId;
   memberId?: MemberId;
+  historical?: boolean;
 };
 };
 
 
 export default function ProposalDiscussion ({
 export default function ProposalDiscussion ({
   proposalId,
   proposalId,
-  memberId
+  memberId,
+  historical
 }: ProposalDiscussionProps) {
 }: ProposalDiscussionProps) {
   const transport = useTransport();
   const transport = useTransport();
   const [discussion, error, loading, refreshDiscussion] = usePromise<ParsedDiscussion | null | undefined>(
   const [discussion, error, loading, refreshDiscussion] = usePromise<ParsedDiscussion | null | undefined>(
-    () => transport.proposals.discussion(proposalId),
-    undefined
+    () => historical
+      ? transport.proposals.historicalDiscussion(proposalId)
+      : transport.proposals.discussion(proposalId),
+    undefined,
+    [historical]
   );
   );
   const constraints = transport.proposals.discussionContraints();
   const constraints = transport.proposals.discussionContraints();
 
 
@@ -36,14 +41,15 @@ export default function ProposalDiscussion ({
                 key={post.postId ? post.postId.toNumber() : `k-${key}`}
                 key={post.postId ? post.postId.toNumber() : `k-${key}`}
                 post={post}
                 post={post}
                 memberId={memberId}
                 memberId={memberId}
-                refreshDiscussion={refreshDiscussion}/>
+                refreshDiscussion={refreshDiscussion}
+                historical={historical}/>
             ))
             ))
           )
           )
             : (
             : (
               <Header as='h4' style={{ margin: '1rem 0' }}>Nothing has been posted here yet!</Header>
               <Header as='h4' style={{ margin: '1rem 0' }}>Nothing has been posted here yet!</Header>
             )
             )
           }
           }
-          { memberId && (
+          { (memberId && !historical) && (
             <DiscussionPostForm
             <DiscussionPostForm
               threadId={discussion.threadId}
               threadId={discussion.threadId}
               memberId={memberId}
               memberId={memberId}

+ 37 - 3
pioneer/packages/joy-proposals/src/index.tsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import React from 'react';
 import { Route, Switch, RouteComponentProps } from 'react-router';
 import { Route, Switch, RouteComponentProps } from 'react-router';
+import Tabs from '@polkadot/react-components/Tabs';
 import { Link } from 'react-router-dom';
 import { Link } from 'react-router-dom';
 import styled from 'styled-components';
 import styled from 'styled-components';
-import { Breadcrumb } from 'semantic-ui-react';
+import { Breadcrumb, Message } from 'semantic-ui-react';
 
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
@@ -27,6 +28,7 @@ import { SignalForm,
   TerminateWorkingGroupLeaderForm } from './forms';
   TerminateWorkingGroupLeaderForm } from './forms';
 import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import style from './style';
 import style from './style';
+import { HistoricalProposalFromId } from './Proposal/ProposalFromId';
 
 
 const ProposalsMain = styled.main`${style}`;
 const ProposalsMain = styled.main`${style}`;
 
 
@@ -42,11 +44,27 @@ const StyledHeader = styled.header`
 `;
 `;
 
 
 function App (props: Props): React.ReactElement<Props> {
 function App (props: Props): React.ReactElement<Props> {
-  const { basePath } = props;
+  const { basePath, t } = props;
+  const tabs = [
+    {
+      isRoot: true,
+      name: 'current',
+      text: t('Current')
+    },
+    {
+      name: 'historical',
+      text: t('Historical'),
+      hasParams: true
+    }
+  ];
 
 
   return (
   return (
     <ProposalsMain className='proposal--App'>
     <ProposalsMain className='proposal--App'>
       <StyledHeader>
       <StyledHeader>
+        <Tabs
+          basePath={basePath}
+          items={tabs}
+        />
         <Breadcrumb>
         <Breadcrumb>
           <Switch>
           <Switch>
             <Route path={`${basePath}/new/:type`} render={(props: RouteComponentProps<{ type?: string }>) => (
             <Route path={`${basePath}/new/:type`} render={(props: RouteComponentProps<{ type?: string }>) => (
@@ -63,11 +81,25 @@ function App (props: Props): React.ReactElement<Props> {
               <Breadcrumb.Divider icon='right angle' />
               <Breadcrumb.Divider icon='right angle' />
               <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
               <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
             </Route>
             </Route>
+            <Route path={`${basePath}/historical`}>
+              <Breadcrumb.Section active>Historical Proposals</Breadcrumb.Section>
+            </Route>
             <Route>
             <Route>
               <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
               <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
             </Route>
             </Route>
           </Switch>
           </Switch>
         </Breadcrumb>
         </Breadcrumb>
+        <Switch>
+          <Route path={`${basePath}/historical`}>
+            <Message warning active>
+              <Message.Header>{"You're in a historical proposals view."}</Message.Header>
+              <Message.Content>
+                The data presented here comes from previous Joystream testnet chain, which
+                means all proposals are read-only and can no longer be interacted with!
+              </Message.Content>
+            </Message>
+          </Route>
+        </Switch>
       </StyledHeader>
       </StyledHeader>
       <Switch>
       <Switch>
         <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
         <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
@@ -92,7 +124,9 @@ function App (props: Props): React.ReactElement<Props> {
         <Route exact path={`${basePath}/new/terminate-working-group-leader-role`} component={TerminateWorkingGroupLeaderForm} />
         <Route exact path={`${basePath}/new/terminate-working-group-leader-role`} component={TerminateWorkingGroupLeaderForm} />
         <Route exact path={`${basePath}/active`} component={NotDone} />
         <Route exact path={`${basePath}/active`} component={NotDone} />
         <Route exact path={`${basePath}/finalized`} component={NotDone} />
         <Route exact path={`${basePath}/finalized`} component={NotDone} />
-        <Route exact path={`${basePath}/:id`} component={ProposalFromId} />
+        <Route exact path={`${basePath}/historical`} render={() => <ProposalPreviewList historical={true}/>} />
+        <Route exact path={`${basePath}/historical/:id`} component={HistoricalProposalFromId}/>
+        <Route exact path={`${basePath}/:id`} component={ProposalFromId}/>
         <Route component={ProposalPreviewList} />
         <Route component={ProposalPreviewList} />
       </Switch>
       </Switch>
     </ProposalsMain>
     </ProposalsMain>

+ 157 - 12
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -8,13 +8,15 @@ import { ParsedProposal,
   DiscussionContraints,
   DiscussionContraints,
   ProposalStatusFilter,
   ProposalStatusFilter,
   ProposalsBatch,
   ProposalsBatch,
-  ParsedProposalDetails } from '../types/proposals';
+  ParsedProposalDetails,
+  RuntimeUpgradeProposalDetails,
+  HistoricalProposalData } from '../types/proposals';
 import { ParsedMember } from '../types/members';
 import { ParsedMember } from '../types/members';
 
 
 import BaseTransport from './base';
 import BaseTransport from './base';
 
 
 import { ThreadId, PostId } from '@joystream/types/common';
 import { ThreadId, PostId } from '@joystream/types/common';
-import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails } from '@joystream/types/proposals';
+import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails, ProposalStatus } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
 import { MemberId } from '@joystream/types/members';
 import { u32, Bytes, Null } from '@polkadot/types/';
 import { u32, Bytes, Null } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
 import { BalanceOf } from '@polkadot/types/interfaces';
@@ -31,6 +33,8 @@ import CouncilTransport from './council';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { APIQueryCache } from './APIQueryCache';
 import { APIQueryCache } from './APIQueryCache';
 
 
+import HISTORICAL_PROPOSALS from './static/historical-proposals.json';
+
 type ProposalDetailsCacheEntry = {
 type ProposalDetailsCacheEntry = {
   type: ProposalType;
   type: ProposalType;
   details: ParsedProposalDetails;
   details: ParsedProposalDetails;
@@ -143,21 +147,31 @@ export default class ProposalsTransport extends BaseTransport {
     return result.map(([proposalId]) => proposalId);
     return result.map(([proposalId]) => proposalId);
   }
   }
 
 
-  async proposalsBatch (status: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
+  private checkProposalStatusFilter (status: ProposalStatus, filter: ProposalStatusFilter) {
+    if (filter === 'All') {
+      return true;
+    }
+
+    if (filter === 'Active' && status.isOfType('Active')) {
+      return true;
+    }
+
+    if (!status.isOfType('Finalized')) {
+      return false;
+    }
+
+    return status.asType('Finalized').proposalStatus.type === filter;
+  }
+
+  async proposalsBatch (statusFilter: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
     const ids = (status === 'Active' ? await this.activeProposalsIds() : await this.proposalsIds())
     const ids = (status === 'Active' ? await this.activeProposalsIds() : await this.proposalsIds())
       .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
       .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
     let rawProposalsWithIds = (await Promise.all(ids.map((id) => this.rawProposalById(id))))
     let rawProposalsWithIds = (await Promise.all(ids.map((id) => this.rawProposalById(id))))
       .map((proposal, index) => ({ id: ids[index], proposal }));
       .map((proposal, index) => ({ id: ids[index], proposal }));
 
 
-    if (status !== 'All' && status !== 'Active') {
-      rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => {
-        if (!proposal.status.isOfType('Finalized')) {
-          return false;
-        }
-
-        return proposal.status.asType('Finalized').proposalStatus.type === status;
-      });
-    }
+    rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => (
+      this.checkProposalStatusFilter(proposal.status, statusFilter)
+    ));
 
 
     const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
     const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
 
 
@@ -283,4 +297,135 @@ export default class ProposalsTransport extends BaseTransport {
       maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
       maxPostLength: (this.api.consts.proposalsDiscussion.postLengthLimit as u32).toNumber()
     };
     };
   }
   }
+
+  private replaceHistoricalProposalLinks (text: string) {
+    return text.replace(
+      /testnet\.joystream\.org\/#\/proposals\/([0-9]+)/g,
+      'testnet.joystream.org/#/proposals/historical/$1'
+    );
+  }
+
+  private parseHistoricalProposalDetails (proposal: HistoricalProposalData['proposal']): ParsedProposalDetails {
+    const { type, details } = proposal;
+
+    if (type === 'RuntimeUpgrade') {
+      return details as RuntimeUpgradeProposalDetails;
+    }
+
+    if (type === 'Text') {
+      details[0] = this.replaceHistoricalProposalLinks(details[0] as string);
+    }
+
+    return this.api.createType('ProposalDetails', {
+      [type]: details.length > 1 ? details : details[0]
+    });
+  }
+
+  // Historical proposals methods
+  private parseHistoricalProposal ({ proposal }: HistoricalProposalData): ParsedProposal {
+    return {
+      ...proposal,
+      id: this.api.createType('ProposalId', proposal.id),
+      type: proposal.type as ProposalType,
+      status: this.api.createType('ProposalStatus', proposal.status),
+      createdAt: new Date(proposal.createdAt),
+      parameters: this.api.createType('ProposalParameters', proposal.parameters),
+      votingResults: this.api.createType('VotingResults', proposal.votingResults),
+      details: this.parseHistoricalProposalDetails(proposal),
+      description: this.replaceHistoricalProposalLinks(proposal.description)
+    };
+  }
+
+  historicalProposalById (id: ProposalId): Promise<ParsedProposal> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id === id.toNumber());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalProposal(proposalData));
+      }
+    });
+  }
+
+  private parseHistoricalProposalDiscussion (proposalData: HistoricalProposalData): ParsedDiscussion {
+    const { discussion } = proposalData;
+
+    return {
+      ...discussion,
+      threadId: this.api.createType('ThreadId', discussion.threadId),
+      posts: discussion.posts.map((post) => ({
+        ...post,
+        postId: this.api.createType('PostId', post.postId),
+        threadId: this.api.createType('ThreadId', post.threadId),
+        createdAt: new Date(post.createdAt),
+        updatedAt: new Date(post.updatedAt),
+        author: this.api.createType('Membership', post.author),
+        authorId: this.api.createType('MemberId', post.authorId),
+        text: this.replaceHistoricalProposalLinks(post.text)
+      }))
+    };
+  }
+
+  historicalProposalsBatch (statusFilter: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
+    return new Promise((resolve, reject) => {
+      const filteredProposalsData = HISTORICAL_PROPOSALS
+        .sort((a, b) => b.proposal.id - a.proposal.id)
+        .filter(({ proposal }) => (
+          this.checkProposalStatusFilter(this.api.createType('ProposalStatus', proposal.status), statusFilter)
+        ));
+
+      const totalBatches = Math.ceil(filteredProposalsData.length / batchSize);
+      const proposalsInBatchData = filteredProposalsData.slice((batchNumber - 1) * batchSize, batchNumber * batchSize);
+      const parsedProposals: ParsedProposal[] = proposalsInBatchData
+        .map((proposalData) => this.parseHistoricalProposal(proposalData));
+
+      resolve({
+        batchNumber,
+        batchSize: parsedProposals.length,
+        totalBatches,
+        proposals: parsedProposals
+      });
+    });
+  }
+
+  historicalDiscussion (id: number|ProposalId): Promise<ParsedDiscussion | null> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id.toString() === id.toString());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalProposalDiscussion(proposalData));
+      }
+    });
+  }
+
+  private parseHistoricalVotes (proposalData: HistoricalProposalData): ProposalVotes {
+    const { votes } = proposalData;
+
+    return {
+      ...votes,
+      votes: votes.votes.map((vote) => ({
+        ...vote,
+        vote: this.api.createType('VoteKind', vote.vote),
+        member: {
+          ...vote.member,
+          memberId: this.api.createType('MemberId', vote.member.memberId)
+        }
+      }))
+    };
+  }
+
+  historicalVotes (proposalId: ProposalId): Promise<ProposalVotes> {
+    return new Promise((resolve, reject) => {
+      const proposalData = HISTORICAL_PROPOSALS.find(({ proposal }) => proposal.id.toString() === proposalId.toString());
+
+      if (!proposalData) {
+        reject(new Error('Historical proposal not found!'));
+      } else {
+        resolve(this.parseHistoricalVotes(proposalData));
+      }
+    });
+  }
 }
 }

File diff suppressed because it is too large
+ 19232 - 0
pioneer/packages/joy-utils/src/transport/static/historical-proposals.json


+ 43 - 0
pioneer/packages/joy-utils/src/types/proposals.ts

@@ -118,3 +118,46 @@ export type DiscussionContraints = {
   maxPostLength: number;
   maxPostLength: number;
   maxPostEdits: number;
   maxPostEdits: number;
 }
 }
+
+export type HistoricalParsedPost = {
+  postId: number;
+  threadId: number;
+  text: string;
+  createdAt: string;
+  createdAtBlock: number;
+  updatedAt: string;
+  updatedAtBlock: number;
+  author: ParsedMember;
+  authorId: number;
+  editsCount: number;
+}
+
+export type HistoricalProposalData = {
+  proposal: {
+    id: number,
+    parameters: unknown, // JSON of ProposalParameters
+    proposerId: number,
+    title: string,
+    description: string,
+    createdAt: string,
+    status: unknown, // JSON of ProposalStatus
+    votingResults: unknown, // JSON of VotingResults
+    details: unknown[], // JSON of ParsedProposalDetails
+    type: string,
+    proposer: ParsedMember,
+    createdAtBlock: number,
+    cancellationFee: number
+  },
+  votes: {
+    councilMembersLength: number,
+    votes: {
+      vote: number;
+      member: ParsedMember & { memberId: number },
+    }[]
+  },
+  discussion: {
+    title: string,
+    threadId: number,
+    posts: HistoricalParsedPost[]
+  }
+}

Some files were not shown because too many files changed in this diff