Bladeren bron

Proposals pagination

Leszek Wiesner 4 jaren geleden
bovenliggende
commit
a506351a7a

+ 50 - 94
pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx

@@ -1,54 +1,16 @@
 import React, { useState } from 'react';
-import { Button, Card, Container, Icon } from 'semantic-ui-react';
+import { Button, Card, Container, Icon, Pagination } from 'semantic-ui-react';
 import styled from 'styled-components';
 import { Link, useLocation } from 'react-router-dom';
 
 import ProposalPreview from './ProposalPreview';
-import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
+import { ParsedProposal, proposalStatusFilters, ProposalStatusFilter, ProposalsBatch } from '@polkadot/joy-utils/types/proposals';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 import { withCalls } from '@polkadot/react-api';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { Dropdown } from '@polkadot/react-components';
 
-const filters = ['All', 'Active', 'Canceled', 'Approved', 'Rejected', 'Slashed', 'Expired'] as const;
-
-type ProposalFilter = typeof filters[number];
-
-function filterProposals (filter: ProposalFilter, proposals: ParsedProposal[]) {
-  if (filter === 'All') {
-    return proposals;
-  } else if (filter === 'Active') {
-    return proposals.filter((prop: ParsedProposal) => {
-      const [activeOrFinalized] = Object.keys(prop.status);
-      return activeOrFinalized === 'Active';
-    });
-  }
-
-  return proposals.filter((prop: ParsedProposal) => {
-    if (prop.status.Finalized == null || prop.status.Finalized.proposalStatus == null) {
-      return false;
-    }
-
-    const [finalStatus] = Object.keys(prop.status.Finalized.proposalStatus);
-    return finalStatus === filter;
-  });
-}
-
-function mapFromProposals (proposals: ParsedProposal[]) {
-  const proposalsMap = new Map<ProposalFilter, ParsedProposal[]>();
-
-  proposalsMap.set('All', proposals);
-  proposalsMap.set('Canceled', filterProposals('Canceled', proposals));
-  proposalsMap.set('Active', filterProposals('Active', proposals));
-  proposalsMap.set('Approved', filterProposals('Approved', proposals));
-  proposalsMap.set('Rejected', filterProposals('Rejected', proposals));
-  proposalsMap.set('Slashed', filterProposals('Slashed', proposals));
-  proposalsMap.set('Expired', filterProposals('Expired', proposals));
-
-  return proposalsMap;
-}
-
 type ProposalPreviewListProps = {
   bestNumber?: BlockNumber;
 };
@@ -59,56 +21,35 @@ const FilterContainer = styled.div`
   justify-content: space-between;
   margin-bottom: 1.75rem;
 `;
-const FilterOption = styled.span`
-  display: inline-flex;
-  align-items: center;
-`;
-const ProposalFilterCountBadge = styled.span`
-  background-color: rgba(0, 0, 0, .3);
-  color: #fff;
-
-  border-radius: 10px;
-  height: 19px;
-  min-width: 19px;
-  padding: 0 4px;
-
-  font-size: .8rem;
-  font-weight: 500;
-  line-height: 1;
-
-  display: flex;
-  align-items: center;
-  justify-content: center;
-
-  margin-left: 6px;
-`;
 const StyledDropdown = styled(Dropdown)`
   .dropdown {
     width: 200px;
   }
 `;
+const PaginationBox = styled.div`
+  margin-bottom: 1em;
+`;
 
 function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
   const { pathname } = useLocation();
   const transport = useTransport();
-  const [proposals, error, loading] = usePromise<ParsedProposal[]>(() => transport.proposals.proposals(), []);
-  const [activeFilter, setActiveFilter] = useState<ProposalFilter>('All');
-
-  const proposalsMap = mapFromProposals(proposals);
-  const filteredProposals = proposalsMap.get(activeFilter) as ParsedProposal[];
-  const sortedProposals = filteredProposals.sort((p1, p2) => p2.id.cmp(p1.id));
+  const [activeFilter, setActiveFilter] = useState<ProposalStatusFilter>('All');
+  const [currentPage, setCurrentPage] = useState<number>(1);
+  const [proposalsBatch, error, loading] = usePromise<ProposalsBatch | undefined>(
+    () => transport.proposals.proposalsBatch(activeFilter, currentPage),
+    undefined,
+    [activeFilter, currentPage]
+  );
 
-  const filterOptions = filters.map(filter => ({
-    text: (
-      <FilterOption>
-        {filter}
-        <ProposalFilterCountBadge>{(proposalsMap.get(filter) as ParsedProposal[]).length}</ProposalFilterCountBadge>
-      </FilterOption>
-    ),
+  const filterOptions = proposalStatusFilters.map(filter => ({
+    text: filter,
     value: filter
   }));
 
-  const _onChangePrefix = (f: ProposalFilter) => setActiveFilter(f);
+  const _onChangePrefix = (f: ProposalStatusFilter) => {
+    setCurrentPage(1);
+    setActiveFilter(f);
+  };
 
   return (
     <Container className="Proposal" fluid>
@@ -117,25 +58,40 @@ function ProposalPreviewList ({ bestNumber }: ProposalPreviewListProps) {
           <Icon name="add" />
           New proposal
         </Button>
-        {!loading && (
-          <StyledDropdown
-            label="Proposal state"
-            options={filterOptions}
-            value={activeFilter}
-            onChange={_onChangePrefix}
-          />
-        )}
+        <StyledDropdown
+          label="Proposal state"
+          options={filterOptions}
+          value={activeFilter}
+          onChange={_onChangePrefix}
+        />
       </FilterContainer>
       <PromiseComponent error={ error } loading={ loading } message="Fetching proposals...">
-        {
-          sortedProposals.length ? (
-            <Card.Group>
-              {sortedProposals.map((prop: ParsedProposal, idx: number) => (
-                <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} />
-              ))}
-            </Card.Group>
-          ) : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.`
-        }
+        { proposalsBatch && (<>
+          <PaginationBox>
+            { proposalsBatch.totalBatches > 1 && (
+              <Pagination
+                activePage={ currentPage }
+                ellipsisItem={{ content: <Icon name='ellipsis horizontal' />, icon: true }}
+                firstItem={{ content: <Icon name='angle double left' />, icon: true }}
+                lastItem={{ content: <Icon name='angle double right' />, icon: true }}
+                prevItem={{ content: <Icon name='angle left' />, icon: true }}
+                nextItem={{ content: <Icon name='angle right' />, icon: true }}
+                totalPages={ proposalsBatch.totalBatches }
+                onPageChange={ (e, data) => setCurrentPage((data.activePage && parseInt(data.activePage.toString())) || 1) }
+              />
+            ) }
+          </PaginationBox>
+           { proposalsBatch.proposals.length
+             ? (
+               <Card.Group>
+                 {proposalsBatch.proposals.map((prop: ParsedProposal, idx: number) => (
+                   <ProposalPreview key={`${prop.title}-${idx}`} proposal={prop} bestNumber={bestNumber} />
+                 ))}
+               </Card.Group>
+             )
+             : `There are currently no ${activeFilter !== 'All' ? activeFilter.toLocaleLowerCase() : 'submitted'} proposals.`
+           }
+        </>) }
       </PromiseComponent>
     </Container>
   );

+ 1 - 0
pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx

@@ -16,6 +16,7 @@ export default function usePromise<T> (
 
   let isSubscribed = true;
   const execute = useCallback(() => {
+    setState({ value: state.value, error: null, isPending: true });
     return promise()
       .then(value => {
         if (isSubscribed) {

+ 41 - 8
pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -6,16 +6,18 @@ import {
   ProposalVotes,
   ParsedPost,
   ParsedDiscussion,
-  DiscussionContraints
+  DiscussionContraints,
+  ProposalStatusFilter,
+  ProposalsBatch
 } 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, Finalized, ProposalDecisionStatus } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
-import { u32, u64, Bytes, Vec } from '@polkadot/types/';
+import { u32, u64, Bytes, Null } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
 
 import { bytesToString } from '../functions/misc';
@@ -30,6 +32,7 @@ import CouncilTransport from './council';
 
 import { blake2AsHex } from '@polkadot/util-crypto';
 import { APIQueryCache } from '../APIQueryCache';
+import { MultipleLinkedMapEntry } from '../LinkedMapEntry';
 
 type ProposalDetailsCacheEntry = {
   type: ProposalType;
@@ -100,9 +103,11 @@ export default class ProposalsTransport extends BaseTransport {
     }
   }
 
-  async proposalById (id: ProposalId): Promise<ParsedProposal> {
+  async proposalById (id: ProposalId, rawProposal?: Proposal): Promise<ParsedProposal> {
     const { type, details } = await this.typeAndDetails(id);
-    const rawProposal = await this.rawProposalById(id);
+    if (!rawProposal) {
+      rawProposal = await this.rawProposalById(id);
+    }
     const proposer = (await this.membersT.memberProfile(rawProposal.proposerId)).toJSON() as ParsedMember;
     const proposal = rawProposal.toJSON() as {
       title: string;
@@ -133,15 +138,43 @@ export default class ProposalsTransport extends BaseTransport {
     return Array.from({ length: total }, (_, i) => new ProposalId(i + 1));
   }
 
+  async activeProposalsIds () {
+    const result = new MultipleLinkedMapEntry(ProposalId, Null, await this.proposalsEngine.activeProposalIds());
+    // linked_keys will be [0] if there are no active proposals!
+    return result.linked_keys.join('') !== '0' ? result.linked_keys : [];
+  }
+
   async proposals () {
     const ids = await this.proposalsIds();
     return Promise.all(ids.map(id => this.proposalById(id)));
   }
 
-  async activeProposals () {
-    const activeProposalIds = (await this.proposalsEngine.activeProposalIds()) as Vec<ProposalId>;
+  async proposalsBatch (status: 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.type !== 'Finalized') {
+          return false;
+        }
+        const finalStatus = ((proposal.status.value as Finalized).get('proposalStatus') as ProposalDecisionStatus);
+        return finalStatus.type === status;
+      });
+    }
+
+    const totalBatches = Math.ceil(rawProposalsWithIds.length / batchSize);
+    rawProposalsWithIds = rawProposalsWithIds.slice((batchNumber - 1) * batchSize, batchNumber * batchSize);
+    const proposals = await Promise.all(rawProposalsWithIds.map(({ proposal, id }) => this.proposalById(id, proposal)));
 
-    return Promise.all(activeProposalIds.map(id => this.proposalById(id)));
+    return {
+      batchNumber,
+      batchSize: rawProposalsWithIds.length,
+      totalBatches,
+      proposals
+    };
   }
 
   async proposedBy (member: MemberId) {

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

@@ -25,6 +25,9 @@ export const ProposalTypes = [
 
 export type ProposalType = typeof ProposalTypes[number];
 
+export const proposalStatusFilters = ['All', 'Active', 'Canceled', 'Approved', 'Rejected', 'Slashed', 'Expired'] as const;
+export type ProposalStatusFilter = typeof proposalStatusFilters[number];
+
 export type ParsedProposal = {
   id: ProposalId;
   type: ProposalType;
@@ -49,6 +52,13 @@ export type ParsedProposal = {
   cancellationFee: number;
 };
 
+export type ProposalsBatch = {
+  batchNumber: number;
+  batchSize: number;
+  totalBatches: number;
+  proposals: ParsedProposal[];
+};
+
 export type ProposalVote = {
   vote: VoteKind | null;
   member: ParsedMember & { memberId: MemberId };