Browse Source

Merge pull request #1050 from Lezek123/nicaea-tests-feedback-adjustments

Pioneer: Tests feedback adjustments
Mokhtar Naamani 4 years ago
parent
commit
dc3b7624f3

+ 1 - 1
pioneer/packages/apps/public/locales/en/ui.json

@@ -632,7 +632,7 @@
   "My Requests": "",
   "Working groups": "",
   "Opportunities": "",
-  "My roles": "",
+  "My roles and applications": "",
   "My channels": "",
   "New channel": "",
   "My videos": "",

+ 12 - 1
pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx

@@ -5,6 +5,7 @@ import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
 import { getExtendedStatus } from './ProposalDetails';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import styled from 'styled-components';
+import ReactMarkdown from 'react-markdown';
 
 import './Proposal.css';
 
@@ -17,6 +18,12 @@ const ProposalIdBox = styled.div`
   font-size: 1.1em;
 `;
 
+const ProposalDesc = styled.div`
+  padding: 0.5rem 1rem;
+  border: 1px solid #ddd;
+  border-radius: 0.25rem;
+`;
+
 export type ProposalPreviewProps = {
   proposal: ParsedProposal;
   bestNumber?: BlockNumber;
@@ -33,7 +40,11 @@ export default function ProposalPreview ({ proposal, bestNumber }: ProposalPrevi
         <Card.Header>
           <Header as="h1">{proposal.title}</Header>
         </Card.Header>
-        <Card.Description>{proposal.description}</Card.Description>
+        <Card.Description>
+          <ProposalDesc>
+            <ReactMarkdown source={proposal.description} linkTarget='_blank' />
+          </ProposalDesc>
+        </Card.Description>
         <Details proposal={proposal} extendedStatus={extendedStatus} />
       </Card.Content>
     </Card>

+ 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>
   );

+ 3 - 0
pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx

@@ -101,6 +101,9 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
   const [afterSubmit, setAfterSubmit] = useState(null as (() => () => void) | null);
   const formContainerRef = useRef<HTMLDivElement>(null);
 
+  // Scroll to top on load
+  useEffect(() => { window.scrollTo(0, 0); }, []);
+
   // After-submit effect
   // With current version of Formik, there seems to be no other viable way to handle this (ie. for sendTx)
   useEffect(() => {

+ 1 - 1
pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx

@@ -83,7 +83,7 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
           name="workingGroup"
           placeholder="Select the working group"
           selection
-          options={Object.keys(WorkingGroupDef).map(wgKey => ({ text: wgKey + ' Wroking Group', value: wgKey }))}
+          options={Object.keys(WorkingGroupDef).map(wgKey => ({ text: wgKey + ' Working Group', value: wgKey }))}
           value={values.workingGroup}
           onChange={ handleChange }
         />

+ 91 - 41
pioneer/packages/joy-roles/src/elements.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import moment from 'moment';
-import { Card, Icon, Image, Label, Statistic } from 'semantic-ui-react';
+import { Card, Icon, Image, Label, Statistic, Button } from 'semantic-ui-react';
 import { Link } from 'react-router-dom';
 
 import { Balance } from '@polkadot/types/interfaces';
@@ -10,6 +10,10 @@ import { IProfile, MemberId } from '@joystream/types/members';
 import { GenericAccountId } from '@polkadot/types';
 import { LeadRoleState } from '@joystream/types/content-working-group';
 import { WorkerId } from '@joystream/types/working-group';
+import { WorkingGroups } from './working_groups';
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
+import { formatReward } from '@polkadot/joy-utils/functions/format';
+import styled from 'styled-components';
 
 type BalanceProps = {
   balance?: Balance;
@@ -39,11 +43,13 @@ export function HandleView (props: ProfileProps) {
 
 export type GroupMember = {
   memberId: MemberId;
+  group: WorkingGroups;
+  workerId: number;
   roleAccount: GenericAccountId;
   profile: IProfile;
   title: string;
   stake?: Balance;
-  earned?: Balance;
+  rewardRelationship?: RewardRelationship;
 }
 
 export type GroupLead = {
@@ -53,92 +59,136 @@ export type GroupLead = {
   profile: IProfile;
   title: string;
   stage?: LeadRoleState;
+  stake?: Balance;
+  rewardRelationship?: RewardRelationship;
 }
 
-type inset = {
-  inset?: boolean;
-}
-
-export function GroupLeadView (props: GroupLead & inset) {
-  let fluid = false;
-  if (typeof props.inset !== 'undefined') {
-    fluid = props.inset;
-  }
-
+export function GroupLeadView (props: GroupLead) {
   let avatar = <Identicon value={props.roleAccount.toString()} size={50} />;
   if (typeof props.profile.avatar_uri !== 'undefined' && props.profile.avatar_uri.toString() !== '') {
     avatar = <Image src={props.profile.avatar_uri.toString()} circular className='avatar' />;
   }
 
+  const { stake, rewardRelationship } = props;
+
   return (
-    <Card color='grey' className="staked-card" fluid={fluid}>
+    <Card color='grey' className="staked-card">
       <Card.Content>
         <Image floated='right'>
           {avatar}
         </Image>
         <Card.Header><HandleView profile={props.profile} /></Card.Header>
         <Card.Meta>{props.title}</Card.Meta>
+        <Card.Meta>
+          { props.workerId && (
+            <Label>{ 'Worker ID: ' + props.workerId.toString() }</Label>
+          ) }
+        </Card.Meta>
         <Card.Description>
-          <Label color='teal' ribbon={fluid}>
+          <Label color='teal'>
             <Icon name="shield" />
             { props.title }
             <Label.Detail>{/* ... */}</Label.Detail>
           </Label>
         </Card.Description>
       </Card.Content>
-      {/* <Card.Content extra>
-        <Label>Something about <Label.Detail> the lead </Label.Detail></Label>
-      </Card.Content> */}
+      <GroupMemberDetails {...{ stake, rewardRelationship }} />
     </Card>
   );
 }
 
-export function GroupMemberView (props: GroupMember & inset) {
-  let fluid = false;
-  if (typeof props.inset !== 'undefined') {
-    fluid = props.inset;
-  }
+const StakeAndReward = styled.div`
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-row-gap: 0.25em;
+  margin-bottom: 1em;
+`;
+
+type GroupMemberDetailsProps = {
+  rewardRelationship?: RewardRelationship;
+  stake?: Balance;
+}
 
-  let stake = null;
-  if (typeof props.stake !== 'undefined' && props.stake.toNumber() !== 0) {
-    stake = (
-      <Label color='green' ribbon={fluid}>
+export function GroupMemberDetails (props: GroupMemberDetailsProps) {
+  const [showDetails, setShowDetails] = useState(false);
+  const details: JSX.Element[] = [];
+
+  if (props.stake && props.stake.toNumber() > 0) {
+    details.push(
+      <Label color="green">
         <Icon name="shield" />
         Staked
         <Label.Detail>{formatBalance(props.stake)}</Label.Detail>
       </Label>
     );
+  } else {
+    details.push(
+      <Label>Stake <Label.Detail>NONE</Label.Detail></Label>
+    );
+  }
+
+  if (props.rewardRelationship) {
+    const reward = props.rewardRelationship;
+    details.push(
+      <Label>Reward <Label.Detail>{formatReward(reward)}</Label.Detail></Label>
+    );
+    details.push(
+      <Label>Earned <Label.Detail>{formatBalance(reward.total_reward_received)}</Label.Detail></Label>
+    );
+    details.push(
+      <Label>Missed <Label.Detail>{formatBalance(reward.total_reward_missed)}</Label.Detail></Label>
+    );
+    details.push(
+      <Label>
+        Next payment block:
+        <Label.Detail>{props.rewardRelationship.next_payment_at_block.unwrapOr('NONE').toString()}</Label.Detail>
+      </Label>
+    );
+  } else {
+    details.push(
+      <Label>Reward <Label.Detail>NONE</Label.Detail></Label>
+    );
   }
 
+  return (
+    <Card.Content extra>
+      { showDetails && (
+        <Card.Description>
+          <StakeAndReward>
+            {details.map((detail, index) => <div key={index}>{detail}</div>)}
+          </StakeAndReward>
+        </Card.Description>
+      ) }
+      <Button onClick={ () => setShowDetails(v => !v) } size="tiny" fluid>
+        { showDetails ? 'Hide' : 'Show'} details
+      </Button>
+    </Card.Content>
+  );
+}
+
+export function GroupMemberView (props: GroupMember) {
   let avatar = <Identicon value={props.roleAccount.toString()} size={50} />;
   if (typeof props.profile.avatar_uri !== 'undefined' && props.profile.avatar_uri.toString() !== '') {
     avatar = <Image src={props.profile.avatar_uri.toString()} circular className='avatar' />;
   }
 
-  let earned = null;
-  if (typeof props.earned !== 'undefined' &&
-    props.earned.toNumber() > 0 &&
-    !fluid) {
-    earned = (
-      <Card.Content extra>
-        <Label>Earned <Label.Detail>{formatBalance(props.earned)}</Label.Detail></Label>
-      </Card.Content>
-    );
-  }
+  const { stake, rewardRelationship } = props;
 
   return (
-    <Card color='grey' className="staked-card" fluid={fluid}>
+    <Card color='grey' className="staked-card">
       <Card.Content>
         <Image floated='right'>
           {avatar}
         </Image>
         <Card.Header><HandleView profile={props.profile} /></Card.Header>
         <Card.Meta>{props.title}</Card.Meta>
-        <Card.Description>
-          {stake}
-        </Card.Description>
+        <Card.Meta>
+          <Label>
+            { (props.group === WorkingGroups.ContentCurators ? 'Curator' : 'Worker') + ` ID: ${props.workerId.toString()}` }
+          </Label>
+        </Card.Meta>
       </Card.Content>
-      {earned}
+      <GroupMemberDetails {...{ stake, rewardRelationship }} />
     </Card>
   );
 }

+ 1 - 1
pioneer/packages/joy-roles/src/flows/apply.tsx

@@ -950,7 +950,7 @@ export function DoneStage (props: DoneStageProps) {
       </p>
       <p>
         You can track the progress of your
-        application in the <Link to="#working-group/my-roles">My roles</Link> section. Note that your application is attached
+        application in the <Link to="#working-group/my-roles">My roles and applications</Link> section. Note that your application is attached
         to your role key (see below).  If you have any issues, you can message the group lead in in the <Link to="#forum">Forum</Link> or contact them directly.
       </p>
 

+ 1 - 1
pioneer/packages/joy-roles/src/index.tsx

@@ -73,7 +73,7 @@ export const App: React.FC<Props> = (props: Props) => {
   if (props.myAddress) {
     tabs.push({
       name: 'my-roles',
-      text: t('My roles')
+      text: t('My roles and applications')
     });
   }
 

+ 1 - 1
pioneer/packages/joy-roles/src/tabs.stories.tsx

@@ -27,7 +27,7 @@ export const RolesPage = () => {
   const panes = [
     { menuItem: 'Working groups', render: renderWorkingGroups },
     { menuItem: 'Opportunities', render: renderOpportunitySandbox },
-    { menuItem: 'My roles', render: renderMyRolesSandbox }
+    { menuItem: 'My roles and applications', render: renderMyRolesSandbox }
   ];
 
   return (

+ 2 - 0
pioneer/packages/joy-roles/src/tabs/MyRoles.tsx

@@ -413,6 +413,8 @@ export function Application (props: ApplicationProps) {
         <Label.Detail className="right">
           {openingIcon(props.stage.state)}
           {openingDescription(props.stage.state)}
+          <Icon name="hashtag" style={{ marginLeft: '0.75em' }}/>
+          { props.id }
           <ApplicationLabel>
             {_.startCase(props.meta.group) + (isLeadApplication ? ' Lead' : '')}
           </ApplicationLabel>

+ 4 - 1
pioneer/packages/joy-roles/src/tabs/Opportunities.tsx

@@ -89,6 +89,9 @@ export function OpeningHeader (props: OpeningStage) {
               />
             </Link>
           </Label.Detail>
+          <Label.Detail>
+            <Icon name="hashtag" /> {props.meta.id}
+          </Label.Detail>
         </Label>
         <a>
           <CopyToClipboard text={window.location.origin + '/#/working-groups/opportunities/' + props.meta.group + '/' + props.meta.id}>
@@ -540,7 +543,7 @@ export const OpeningsView = Loadable<OpeningsViewProps>(
     };
     const groupOption = (group: WorkingGroups | null, lead = false) => ({
       value: `${basePath}${group ? `/${group}` : ''}${lead ? '/lead' : ''}`,
-      text: _.startCase(`${group || 'All opportuniries'}`) + (lead ? ' (Lead)' : '')
+      text: _.startCase(`${group || 'All opportunities'}`) + (lead ? ' (Lead)' : '')
     });
     // Can assert "props.openings!" because we're using "Loadable" which prevents them from beeing undefined
     const filteredOpenings = props.openings!.filter(o =>

+ 11 - 5
pioneer/packages/joy-roles/src/tabs/WorkingGroup.stories.tsx

@@ -12,6 +12,7 @@ import { mockProfile } from '../mocks';
 
 import 'semantic-ui-css/semantic.min.css';
 import '@polkadot/joy-roles/index.sass';
+import { WorkingGroups } from '../working_groups';
 
 export default {
   title: 'Roles / Components / Working groups tab',
@@ -29,7 +30,8 @@ export function ContentCuratorsSection () {
       ),
       title: text('Title', 'Curation lead', 'Ben'),
       stake: new u128(number('Stake', 10101, {}, 'Ben')),
-      earned: new u128(number('Earned', 347829, {}, 'Ben'))
+      workerId: 1,
+      group: WorkingGroups.ContentCurators
     },
     {
       memberId: new MemberId(2),
@@ -37,7 +39,8 @@ export function ContentCuratorsSection () {
       profile: mockProfile(text('Handle', 'bwhm0', 'Martin')),
       title: text('Title', 'Content curator', 'Martin'),
       stake: new u128(number('Stake', 10101, {}, 'Martin')),
-      earned: new u128(number('Earned', 347829, {}, 'Martin'))
+      workerId: 2,
+      group: WorkingGroups.ContentCurators
     },
     {
       memberId: new MemberId(3),
@@ -48,7 +51,8 @@ export function ContentCuratorsSection () {
       ),
       title: text('Title', 'Content curator', 'Paul'),
       stake: new u128(number('Stake', 10101, {}, 'Paul')),
-      earned: new u128(number('Earned', 347829, {}, 'Paul'))
+      workerId: 3,
+      group: WorkingGroups.ContentCurators
     },
     {
       memberId: new MemberId(4),
@@ -59,7 +63,8 @@ export function ContentCuratorsSection () {
       ),
       title: text('Title', 'Content curator', 'Alex'),
       stake: new u128(number('Stake', 10101, {}, 'Alex')),
-      earned: new u128(number('Earned', 347829, {}, 'Alex'))
+      workerId: 4,
+      group: WorkingGroups.ContentCurators
     },
     {
       memberId: new MemberId(5),
@@ -70,7 +75,8 @@ export function ContentCuratorsSection () {
       ),
       title: text('Title', 'Content curator', 'Mokhtar'),
       stake: new u128(number('Stake', 10101, {}, 'Mokhtar')),
-      earned: new u128(number('Earned', 347829, {}, 'Mokhtar'))
+      workerId: 5,
+      group: WorkingGroups.ContentCurators
     }
   ];
 

+ 1 - 1
pioneer/packages/joy-roles/src/tabs/WorkingGroup.tsx

@@ -97,7 +97,7 @@ const GroupOverview = Loadable<GroupOverviewProps>(
       <GroupOverviewSection>
         <h2>{ groupName }</h2>
         <p>{ description }</p>
-        <Card.Group>
+        <Card.Group style={{ alignItems: 'flex-start' }}>
           { workers!.map((worker, key) => (
             <GroupMemberView key={key} {...worker} />
           )) }

+ 12 - 6
pioneer/packages/joy-roles/src/transport.mock.ts

@@ -65,7 +65,8 @@ export class Transport extends TransportBase implements ITransport {
           ),
           title: 'Content curator',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 1,
+          group: WorkingGroups.ContentCurators
         },
         {
           memberId: new MemberId(2),
@@ -73,7 +74,8 @@ export class Transport extends TransportBase implements ITransport {
           profile: mockProfile('bwhm0'),
           title: 'Content curator',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 2,
+          group: WorkingGroups.ContentCurators
         },
         {
           memberId: new MemberId(3),
@@ -84,7 +86,8 @@ export class Transport extends TransportBase implements ITransport {
           ),
           title: 'Content curator',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 3,
+          group: WorkingGroups.ContentCurators
         },
         {
           memberId: new MemberId(4),
@@ -95,7 +98,8 @@ export class Transport extends TransportBase implements ITransport {
           ),
           title: 'Content curator',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 4,
+          group: WorkingGroups.ContentCurators
         },
         {
           memberId: new MemberId(3),
@@ -106,7 +110,8 @@ export class Transport extends TransportBase implements ITransport {
           ),
           title: 'Content curator',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 5,
+          group: WorkingGroups.ContentCurators
         }
       ]
     });
@@ -127,7 +132,8 @@ export class Transport extends TransportBase implements ITransport {
           ),
           title: 'Storage provider',
           stake: new u128(10101),
-          earned: new u128(347829)
+          workerId: 1,
+          group: WorkingGroups.StorageProviders
         }
       ]
     });

+ 33 - 13
pioneer/packages/joy-roles/src/transport.substrate.ts

@@ -195,14 +195,18 @@ export class Transport extends TransportBase implements ITransport {
     return this.stakeValue(stakeProfile.stake_id);
   }
 
-  protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
-    const relationship = new SingleLinkedMapEntry<RewardRelationship>(
+  protected async rewardRelationshipById (id: RewardRelationshipId): Promise<RewardRelationship | undefined> {
+    const rewardRelationship = new SingleLinkedMapEntry<RewardRelationship>(
       RewardRelationship,
-      await this.cachedApi.query.recurringRewards.rewardRelationships(
-        relationshipId
-      )
-    );
-    return relationship.value.total_reward_received;
+      await this.cachedApi.query.recurringRewards.rewardRelationships(id)
+    ).value;
+
+    return rewardRelationship.isEmpty ? undefined : rewardRelationship;
+  }
+
+  protected async workerTotalReward (relationshipId: RewardRelationshipId): Promise<Balance> {
+    const relationship = await this.rewardRelationshipById(relationshipId);
+    return relationship?.total_reward_received || new u128(0);
   }
 
   protected async memberIdFromRoleAndActorId (role: Role, id: ActorId): Promise<MemberId> {
@@ -232,6 +236,13 @@ export class Transport extends TransportBase implements ITransport {
     );
   }
 
+  protected async workerRewardRelationship (worker: GroupWorker): Promise<RewardRelationship | undefined> {
+    const rewardRelationship = worker.reward_relationship.isSome
+      ? await this.rewardRelationshipById(worker.reward_relationship.unwrap())
+      : undefined;
+    return rewardRelationship;
+  }
+
   protected async groupMember (
     group: WorkingGroups,
     id: GroupWorkerId,
@@ -252,18 +263,17 @@ export class Transport extends TransportBase implements ITransport {
       stakeValue = await this.workerStake(worker.role_stake_profile.unwrap());
     }
 
-    let earnedValue: Balance = new u128(0);
-    if (worker.reward_relationship && worker.reward_relationship.isSome) {
-      earnedValue = await this.workerTotalReward(worker.reward_relationship.unwrap());
-    }
+    const rewardRelationship = await this.workerRewardRelationship(worker);
 
     return ({
       roleAccount,
+      group,
       memberId,
+      workerId: id.toNumber(),
       profile: profile.unwrap(),
       title: workerRoleNameByGroup[group],
       stake: stakeValue,
-      earned: earnedValue
+      rewardRelationship
     });
   }
 
@@ -356,6 +366,14 @@ export class Transport extends TransportBase implements ITransport {
         throw new Error(`${group} lead profile not found!`);
       }
 
+      const rewardRelationshipId = currentLead.lead.reward_relationship;
+      const rewardRelationship = rewardRelationshipId.isSome
+        ? await this.rewardRelationshipById(rewardRelationshipId.unwrap())
+        : undefined;
+      const stake = group === WorkingGroups.StorageProviders && (currentLead.lead as Worker).role_stake_profile.isSome
+        ? await this.workerStake((currentLead.lead as Worker).role_stake_profile.unwrap())
+        : undefined;
+
       return {
         lead: {
           memberId: currentLead.memberId,
@@ -363,7 +381,9 @@ export class Transport extends TransportBase implements ITransport {
           roleAccount: currentLead.lead.role_account_id,
           profile: profile.unwrap(),
           title: _.startCase(group) + ' Lead',
-          stage: group === WorkingGroups.ContentCurators ? (currentLead.lead as Lead).stage : undefined
+          stage: group === WorkingGroups.ContentCurators ? (currentLead.lead as Lead).stage : undefined,
+          stake,
+          rewardRelationship
         },
         loaded: true
       };

+ 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 };