Преглед на файлове

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

Pioneer: Exploring old proposals
Mokhtar Naamani преди 4 години
родител
ревизия
459039c32b

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

@@ -33,6 +33,7 @@ type BodyProps = {
   proposerId: number | MemberId;
   isCancellable: boolean;
   cancellationFee: number;
+  historical?: boolean;
 };
 
 function ProposedAddress (props: { accountId?: AccountId }) {
@@ -97,7 +98,7 @@ class ParsedParam {
 }
 
 // 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) => [
     new ParsedParam(
       'Content',
@@ -237,12 +238,14 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
   ],
-  BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [
+  BeginReviewWorkingGroupLeaderApplication: ([id, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
       '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: ({
@@ -250,46 +253,56 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     successful_application_id: succesfulApplicationId,
     reward_policy: rewardPolicy,
     working_group: workingGroup
-  }) => [
+  }, historical) => [
     new ParsedParam('Working group', workingGroup.type),
     // TODO: Adjust the link to work with multiple groups after working-groups are normalized!
     new ParsedParam(
       '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(
       '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
     )
   ],
-  SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  SlashWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Slash amount', formatBalance(amount as Balance)),
     new ParsedParam(
       '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
     )
   ],
-  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => [
+  DecreaseWorkingGroupLeaderStake: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('Decrease amount', formatBalance(amount as Balance)),
     new ParsedParam(
       '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
     )
   ],
-  SetWorkingGroupLeaderReward: ([leadId, amount, group]) => [
+  SetWorkingGroupLeaderReward: ([leadId, amount, group], historical) => [
     new ParsedParam('Working group', (group as WorkingGroup).type),
     new ParsedParam('New reward amount', formatBalance(amount as Balance)),
     new ParsedParam(
       '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
     )
   ],
@@ -298,12 +311,19 @@ const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>)
     rationale,
     worker_id: leadId,
     slash
-  }) => {
+  },
+  historical) => {
     return [
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Rationale', bytesToString(rationale), true),
       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,
   proposerId,
   isCancellable,
-  cancellationFee
+  cancellationFee,
+  historical
 }: BodyProps) {
   // 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(
     type === 'RuntimeUpgrade'
       ? params as RuntimeUpgradeProposalDetails
-      : (params as ProposalDetails).asType(type)
+      : (params as ProposalDetails).asType(type),
+    historical
   );
 
   return (

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

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

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

@@ -1,12 +1,37 @@
 import React from 'react';
 import { RouteComponentProps } from 'react-router-dom';
 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 { useApi } from '@polkadot/react-hooks';
+import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
 
 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>) {
   const {
     match: {

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

@@ -25,15 +25,16 @@ const ProposalDesc = styled.div`
 export type ProposalPreviewProps = {
   proposal: ParsedProposal;
   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 (
     <Card
       fluid
-      href={`#/proposals/${proposal.id.toString()}`}>
+      href={`#/proposals/${historical ? 'historical/' : ''}${proposal.id.toString()}`}>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <Card.Content>
         <Card.Header>

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

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

+ 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 = {
   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 [votes, error, loading] = usePromise<ProposalVotes | null>(
-    () => transport.proposals.votes(id),
+    () => historical
+      ? transport.proposals.historicalVotes(id)
+      : transport.proposals.votes(id),
     null,
-    [votingResults]
+    [votingResults, historical]
   );
 
   return (

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

@@ -45,17 +45,20 @@ type ProposalDiscussionPostProps = {
   post: ParsedPost;
   memberId?: MemberId;
   refreshDiscussion: () => void;
+  historical?: boolean;
 }
 
 export default function DiscussionPost ({
   post,
   memberId,
-  refreshDiscussion
+  refreshDiscussion,
+  historical
 }: ProposalDiscussionPostProps) {
   const { author, authorId, text, createdAt, editsCount } = post;
   const [editing, setEditing] = useState(false);
   const constraints = useTransport().proposals.discussionContraints();
   const canEdit = (
+    !historical &&
     memberId &&
     post.postId &&
     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 = {
   proposalId: ProposalId;
   memberId?: MemberId;
+  historical?: boolean;
 };
 
 export default function ProposalDiscussion ({
   proposalId,
-  memberId
+  memberId,
+  historical
 }: ProposalDiscussionProps) {
   const transport = useTransport();
   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();
 
@@ -36,14 +41,15 @@ export default function ProposalDiscussion ({
                 key={post.postId ? post.postId.toNumber() : `k-${key}`}
                 post={post}
                 memberId={memberId}
-                refreshDiscussion={refreshDiscussion}/>
+                refreshDiscussion={refreshDiscussion}
+                historical={historical}/>
             ))
           )
             : (
               <Header as='h4' style={{ margin: '1rem 0' }}>Nothing has been posted here yet!</Header>
             )
           }
-          { memberId && (
+          { (memberId && !historical) && (
             <DiscussionPostForm
               threadId={discussion.threadId}
               memberId={memberId}

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

@@ -1,8 +1,9 @@
 import React from 'react';
 import { Route, Switch, RouteComponentProps } from 'react-router';
+import Tabs from '@polkadot/react-components/Tabs';
 import { Link } from 'react-router-dom';
 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 { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
@@ -27,6 +28,7 @@ import { SignalForm,
   TerminateWorkingGroupLeaderForm } from './forms';
 import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
 import style from './style';
+import { HistoricalProposalFromId } from './Proposal/ProposalFromId';
 
 const ProposalsMain = styled.main`${style}`;
 
@@ -42,11 +44,27 @@ const StyledHeader = styled.header`
 `;
 
 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 (
     <ProposalsMain className='proposal--App'>
       <StyledHeader>
+        <Tabs
+          basePath={basePath}
+          items={tabs}
+        />
         <Breadcrumb>
           <Switch>
             <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.Section active>New proposal</Breadcrumb.Section>
             </Route>
+            <Route path={`${basePath}/historical`}>
+              <Breadcrumb.Section active>Historical Proposals</Breadcrumb.Section>
+            </Route>
             <Route>
               <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
             </Route>
           </Switch>
         </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>
       <Switch>
         <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}/active`} 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} />
       </Switch>
     </ProposalsMain>

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

@@ -8,13 +8,15 @@ import { ParsedProposal,
   DiscussionContraints,
   ProposalStatusFilter,
   ProposalsBatch,
-  ParsedProposalDetails } from '../types/proposals';
+  ParsedProposalDetails,
+  RuntimeUpgradeProposalDetails,
+  HistoricalProposalData } from '../types/proposals';
 import { ParsedMember } from '../types/members';
 
 import BaseTransport from './base';
 
 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 { u32, Bytes, Null } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
@@ -31,6 +33,8 @@ import CouncilTransport from './council';
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { APIQueryCache } from './APIQueryCache';
 
+import HISTORICAL_PROPOSALS from './static/historical-proposals.json';
+
 type ProposalDetailsCacheEntry = {
   type: ProposalType;
   details: ParsedProposalDetails;
@@ -143,21 +147,31 @@ export default class ProposalsTransport extends BaseTransport {
     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())
       .sort((id1, id2) => id2.cmp(id1)); // Sort by newest
     let rawProposalsWithIds = (await Promise.all(ids.map((id) => this.rawProposalById(id))))
       .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);
 
@@ -283,4 +297,135 @@ export default class ProposalsTransport extends BaseTransport {
       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));
+      }
+    });
+  }
 }

Файловите разлики са ограничени, защото са твърде много
+ 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;
   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[]
+  }
+}

Някои файлове не бяха показани, защото твърде много файлове са промени