Bläddra i källkod

joy-proposals upgrade

Leszek Wiesner 4 år sedan
förälder
incheckning
f7d87da7fa
69 ändrade filer med 598 tillägg och 579 borttagningar
  1. 0 1
      pioneer/.eslintignore
  2. 3 0
      pioneer/packages/apps-routing/src/index.ts
  3. 16 0
      pioneer/packages/apps-routing/src/joy-proposals.ts
  4. 0 1
      pioneer/packages/joy-election/src/ApplyForm.tsx
  5. 0 1
      pioneer/packages/joy-election/src/Reveals.tsx
  6. 0 1
      pioneer/packages/joy-election/src/VoteForm.tsx
  7. 0 1
      pioneer/packages/joy-members/src/EditForm.tsx
  8. 0 0
      pioneer/packages/joy-proposals/.skip-build
  9. 142 111
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  10. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx
  11. 1 1
      pioneer/packages/joy-proposals/src/Proposal/Details.tsx
  12. 3 5
      pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx
  13. 4 3
      pioneer/packages/joy-proposals/src/Proposal/ProposalFromId.tsx
  14. 0 3
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreview.tsx
  15. 1 1
      pioneer/packages/joy-proposals/src/Proposal/ProposalPreviewList.tsx
  16. 1 1
      pioneer/packages/joy-proposals/src/Proposal/Votes.tsx
  17. 19 20
      pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx
  18. 1 1
      pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPost.tsx
  19. 2 3
      pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx
  20. 1 1
      pioneer/packages/joy-proposals/src/Proposal/discussion/ProposalDiscussion.tsx
  21. 1 2
      pioneer/packages/joy-proposals/src/forms/AddWorkingGroupOpeningForm.tsx
  22. 1 2
      pioneer/packages/joy-proposals/src/forms/BeginReviewLeaderApplicationsForm.tsx
  23. 0 1
      pioneer/packages/joy-proposals/src/forms/DecreaseWorkingGroupLeadStakeForm.tsx
  24. 16 24
      pioneer/packages/joy-proposals/src/forms/FillWorkingGroupLeaderOpeningForm.tsx
  25. 34 7
      pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx
  26. 2 3
      pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx
  27. 0 1
      pioneer/packages/joy-proposals/src/forms/MintCapacityForm.tsx
  28. 0 1
      pioneer/packages/joy-proposals/src/forms/RuntimeUpgradeForm.tsx
  29. 1 2
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx
  30. 1 1
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupMintCapForm.tsx
  31. 15 16
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  32. 0 1
      pioneer/packages/joy-proposals/src/forms/SetMaxValidatorCountForm.tsx
  33. 0 1
      pioneer/packages/joy-proposals/src/forms/SetWorkingGroupLeadRewardForm.tsx
  34. 0 1
      pioneer/packages/joy-proposals/src/forms/SetWorkingGroupMintCapacityForm.tsx
  35. 0 1
      pioneer/packages/joy-proposals/src/forms/SignalForm.tsx
  36. 0 1
      pioneer/packages/joy-proposals/src/forms/SlashWorkingGroupLeadStakeForm.tsx
  37. 0 1
      pioneer/packages/joy-proposals/src/forms/SpendingProposalForm.tsx
  38. 11 14
      pioneer/packages/joy-proposals/src/forms/TerminateWorkingGroupLeaderForm.tsx
  39. 0 23
      pioneer/packages/joy-proposals/src/forms/forms.css
  40. 0 0
      pioneer/packages/joy-proposals/src/index.css
  41. 55 56
      pioneer/packages/joy-proposals/src/index.tsx
  42. 3 3
      pioneer/packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts
  43. 4 4
      pioneer/packages/joy-proposals/src/style.ts
  44. 23 12
      pioneer/packages/joy-proposals/src/validationSchema.ts
  45. 0 0
      pioneer/packages/joy-utils/src/consts/members.ts
  46. 0 0
      pioneer/packages/joy-utils/src/consts/proposals.ts
  47. 0 0
      pioneer/packages/joy-utils/src/consts/workingGroups.ts
  48. 78 63
      pioneer/packages/joy-utils/src/react/components/TxButton.tsx
  49. 1 1
      pioneer/packages/joy-utils/src/react/components/working-groups/ApplicationDetails.tsx
  50. 1 1
      pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx
  51. 1 0
      pioneer/packages/joy-utils/src/react/hooks/index.ts
  52. 0 0
      pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx
  53. 2 2
      pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx
  54. 17 0
      pioneer/packages/joy-utils/src/transport/base.ts
  55. 0 0
      pioneer/packages/joy-utils/src/transport/chain.ts
  56. 6 7
      pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts
  57. 15 23
      pioneer/packages/joy-utils/src/transport/council.ts
  58. 18 18
      pioneer/packages/joy-utils/src/transport/index.ts
  59. 5 2
      pioneer/packages/joy-utils/src/transport/members.ts
  60. 24 45
      pioneer/packages/joy-utils/src/transport/proposals.ts
  61. 0 0
      pioneer/packages/joy-utils/src/transport/validators.ts
  62. 42 62
      pioneer/packages/joy-utils/src/transport/workingGroups.ts
  63. 0 0
      pioneer/packages/joy-utils/src/types/common.ts
  64. 14 1
      pioneer/packages/joy-utils/src/types/proposals.ts
  65. 0 0
      pioneer/packages/joy-utils/src/types/storageProviders.ts
  66. 0 0
      pioneer/packages/joy-utils/src/types/workingGroups.ts
  67. 0 17
      pioneer/packages/old-apps/apps-routing/src/joy-proposals.ts
  68. 2 3
      pioneer/tsconfig.json
  69. 10 0
      types/src/index.ts

+ 0 - 1
pioneer/.eslintignore

@@ -5,7 +5,6 @@ packages/old-apps/*
 packages/joy-forum/*
 packages/joy-help/*
 packages/joy-media/*
-packages/joy-proposals/*
 packages/joy-roles/*
 packages/joy-settings/*
 packages/joy-utils-old/*

+ 3 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -21,12 +21,14 @@ import transfer from './transfer';
 import members from './joy-members';
 import { terms, privacyPolicy } from './joy-pages';
 import election from './joy-election';
+import proposals from './joy-proposals';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
       members(t),
       election(t),
+      proposals(t),
       null,
       transfer(t),
       accounts(t),
@@ -35,6 +37,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
     : [
       members(t),
       election(t),
+      proposals(t),
       null,
       transfer(t),
       accounts(t),

+ 16 - 0
pioneer/packages/apps-routing/src/joy-proposals.ts

@@ -0,0 +1,16 @@
+import { Route } from './types';
+
+import Proposals from '@polkadot/joy-proposals/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Proposals,
+    display: {
+      needsApi: ['query.proposalsEngine.proposalCount']
+    },
+    text: t<string>('nav.proposals', 'Proposals', { ns: 'apps-routing' }),
+    icon: 'tasks',
+    name: 'proposals',
+    // TODO: useCounter with active proposals count? (could be a nice addition)
+  };
+}

+ 0 - 1
pioneer/packages/joy-election/src/ApplyForm.tsx

@@ -51,7 +51,6 @@ class ApplyForm extends React.PureComponent<Props, State> {
         <div style={{ marginTop: '.5rem' }}>
           <Labelled>
             <TxButton
-              size='large'
               isDisabled={!isStakeValid}
               label={buttonLabel}
               params={[stake]}

+ 0 - 1
pioneer/packages/joy-election/src/Reveals.tsx

@@ -86,7 +86,6 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
         <div style={{ marginTop: '.5rem' }}>
           <Labelled>
             <TxButton
-              size='large'
               isDisabled={!isVoteRevealed}
               label='Reveal this vote'
               params={[hashedVote, applicantId, salt]}

+ 0 - 1
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -150,7 +150,6 @@ class Component extends React.PureComponent<Props, State> {
           <div style={{ marginTop: '.5rem' }}>
             <Labelled>
               <TxButton
-                size='large'
                 isDisabled={!isFormValid}
                 label='Submit my vote'
                 params={[hashedVote, stake]}

+ 0 - 1
pioneer/packages/joy-members/src/EditForm.tsx

@@ -172,7 +172,6 @@ const InnerForm = (props: FormProps) => {
           <div style={{ display: 'flex' }}>
             <TxButton
               type='submit'
-              size='large'
               label={profile ? 'Update my profile' : 'Register'}
               isDisabled={!dirty || isSubmitting}
               params={buildTxParams()}

+ 0 - 0
pioneer/packages/joy-proposals/.skip-build


+ 142 - 111
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -1,35 +1,32 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
-import { Card, Header, Button, Icon, Message } from 'semantic-ui-react';
-import { ProposalType } from '@polkadot/joy-utils/types/proposals';
+import { Card, Header, Message, Icon } from 'semantic-ui-react';
+import { ProposalType, ParsedProposalDetails, SpecificProposalDetails, RuntimeUpgradeProposalDetails } from '@polkadot/joy-utils/types/proposals';
 import { bytesToString } from '@polkadot/joy-utils/functions/misc';
 import styled from 'styled-components';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import { ProposalId, TerminateRoleParameters } from '@joystream/types/proposals';
+import AddressMini from '@polkadot/react-components/AddressMini';
+import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton';
+import { ProposalId, ProposalDetails } from '@joystream/types/proposals';
 import { MemberId, Membership } from '@joystream/types/members';
-import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
+import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
-import { Option, Bytes } from '@polkadot/types/';
-import { BlockNumber } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types/';
+import { BlockNumber, Balance, AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import ReactMarkdown from 'react-markdown';
-import { WorkingGroupOpeningPolicyCommitment, RewardPolicy } from '@joystream/types/working-group';
-import {
-  ActivateOpeningAt,
-  ActivateOpeningAtKeys,
-  StakingPolicy
-} from '@joystream/types/hiring';
-import { WorkingGroup, WorkingGroupKey } from '@joystream/types/common';
+import { StakingPolicy } from '@joystream/types/hiring';
+import { WorkingGroup } from '@joystream/types/common';
 import { ApplicationsDetailsByOpening } from '@polkadot/joy-utils/react/components/working-groups/ApplicationDetails';
 import { LeadInfoFromId } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 import { formatReward } from '@polkadot/joy-utils/functions/format';
+import BN from 'bn.js';
+import { WorkerId } from '@joystream/types/src/working-group';
 
 type BodyProps = {
   title: string;
   description: string;
-  params: any[];
+  params: ParsedProposalDetails;
   type: ProposalType;
   iAmProposer: boolean;
   proposalId: number | ProposalId;
@@ -38,13 +35,18 @@ type BodyProps = {
   cancellationFee: number;
 };
 
-function ProposedAddress (props: { address?: string | null }) {
-  if (props.address === null || props.address === undefined) {
+function ProposedAddress (props: { accountId?: AccountId }) {
+  if (!props.accountId) {
     return <>NONE</>;
   }
 
   return (
-    <AddressMini value={props.address} isShort={false} isPadded={false} withAddress={true} style={{ padding: 0 }} />
+    <AddressMini
+      value={props.accountId.toString()}
+      isShort={false}
+      isPadded={false}
+      withAddress={true}
+      style={{ padding: 0 }} />
   );
 }
 
@@ -62,7 +64,7 @@ function ProposedMember (props: { memberId?: MemberId | number | null }) {
 
   return (
     <PromiseComponent error={error} loading={loading} message="Fetching profile...">
-      { (member && !member.handle.isEmpty) ? (
+      { (member && !member.isEmpty) ? (
         <ProfilePreview
           avatar_uri={ member.avatar_uri.toString() }
           root_account={ member.root_account.toString() }
@@ -99,11 +101,11 @@ class ParsedParam {
 }
 
 // The methods for parsing params by Proposal type.
-const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
-  Text: ([content]) => [
+const paramParsers: { [k in ProposalType]: (params: SpecificProposalDetails<k>) => ParsedParam[]} = {
+  Text: content => [
     new ParsedParam(
       'Content',
-      <ReactMarkdown className='TextProposalContent' source={content} linkTarget='_blank' />,
+      <ReactMarkdown className='TextProposalContent' source={content.toString()} linkTarget='_blank' />,
       true
     )
   ],
@@ -111,63 +113,65 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
     new ParsedParam('Blake2b256 hash of WASM code', hash, true),
     new ParsedParam('File size', filesize + ' bytes')
   ],
-  SetElectionParameters: ([params]) => [
-    new ParsedParam('Announcing period', params.announcing_period + ' blocks'),
-    new ParsedParam('Voting period', params.voting_period + ' blocks'),
-    new ParsedParam('Revealing period', params.revealing_period + ' blocks'),
-    new ParsedParam('Council size', params.council_size + ' members'),
-    new ParsedParam('Candidacy limit', params.candidacy_limit + ' members'),
-    new ParsedParam('New term duration', params.new_term_duration + ' blocks'),
+  SetElectionParameters: params => [
+    new ParsedParam('Announcing period', params.announcing_period.toString() + ' blocks'),
+    new ParsedParam('Voting period', params.voting_period.toString() + ' blocks'),
+    new ParsedParam('Revealing period', params.revealing_period.toString() + ' blocks'),
+    new ParsedParam('Council size', params.council_size.toString() + ' members'),
+    new ParsedParam('Candidacy limit', params.candidacy_limit.toString() + ' members'),
+    new ParsedParam('New term duration', params.new_term_duration.toString() + ' blocks'),
     new ParsedParam('Min. council stake', formatBalance(params.min_council_stake)),
     new ParsedParam('Min. voting stake', formatBalance(params.min_voting_stake))
   ],
   Spending: ([amount, account]) => [
-    new ParsedParam('Amount', formatBalance(amount)),
-    new ParsedParam('Account', <ProposedAddress address={account} />)
+    new ParsedParam('Amount', formatBalance(amount as Balance)),
+    new ParsedParam('Account', <ProposedAddress accountId={account as AccountId} />)
   ],
-  SetLead: ([memberId, accountId]) => [
-    new ParsedParam('Member', <ProposedMember memberId={ memberId } />),
-    new ParsedParam('Account id', <ProposedAddress address={accountId} />)
+  SetLead: params => [
+    new ParsedParam('Member', <ProposedMember memberId={params.unwrapOr([])[0] as MemberId | undefined} />),
+    new ParsedParam('Account id', <ProposedAddress accountId={params.unwrapOr([])[1] as AccountId | undefined} />)
   ],
-  SetContentWorkingGroupMintCapacity: ([capacity]) => [
+  SetContentWorkingGroupMintCapacity: capacity => [
     new ParsedParam('Mint capacity', formatBalance(capacity))
   ],
-  EvictStorageProvider: ([accountId]) => [
-    new ParsedParam('Storage provider account', <ProposedAddress address={accountId} />)
+  EvictStorageProvider: accountId => [
+    new ParsedParam('Storage provider account', <ProposedAddress accountId={accountId} />)
   ],
-  SetValidatorCount: ([count]) => [
-    new ParsedParam('Validator count', count)
+  SetValidatorCount: count => [
+    new ParsedParam('Validator count', count.toString())
   ],
-  SetStorageRoleParameters: ([params]) => [
+  SetStorageRoleParameters: params => [
     new ParsedParam('Min. stake', formatBalance(params.min_stake)),
     // "Min. actors": params.min_actors,
-    new ParsedParam('Max. actors', params.max_actors),
+    new ParsedParam('Max. actors', params.max_actors.toString()),
     new ParsedParam('Reward', formatBalance(params.reward)),
-    new ParsedParam('Reward period', params.reward_period + ' blocks'),
+    new ParsedParam('Reward period', `${params.reward_period.toString()} blocks`),
     // "Bonding period": params.bonding_period + " blocks",
-    new ParsedParam('Unbonding period', params.unbonding_period + ' blocks'),
+    new ParsedParam('Unbonding period', `${params.unbonding_period.toString()} blocks`),
     // "Min. service period": params.min_service_period + " blocks",
     // "Startup grace period": params.startup_grace_period + " blocks",
     new ParsedParam('Entry request fee', formatBalance(params.entry_request_fee))
   ],
-  AddWorkingGroupLeaderOpening: ([{ activate_at, commitment, human_readable_text, working_group }]) => {
-    const workingGroup = new WorkingGroup(working_group);
-    const activateAt = new ActivateOpeningAt(activate_at);
-    const activateAtBlock = activateAt.type === ActivateOpeningAtKeys.ExactBlock ? activateAt.value : null;
-    const OPCommitment = new WorkingGroupOpeningPolicyCommitment(commitment);
+  AddWorkingGroupLeaderOpening: ({
+    activate_at: activateAt,
+    commitment,
+    human_readable_text: humanReadableText,
+    working_group: workingGroup
+  }) => {
+    const activateAtBlock = activateAt.isOfType('ExactBlock') ? activateAt.asType('ExactBlock') : null;
     const {
       application_staking_policy: applicationSP,
       role_staking_policy: roleSP,
       application_rationing_policy: rationingPolicy
-    } = OPCommitment;
-    let HRT = bytesToString(new Bytes(human_readable_text));
+    } = commitment;
+    let HRT = bytesToString(humanReadableText);
     try { HRT = JSON.stringify(JSON.parse(HRT), undefined, 4); } catch (e) { /* Do nothing */ }
     const formatStake = (stake: Option<StakingPolicy>) => (
-      stake.isSome ? stake.unwrap().amount_mode.type + `(${stake.unwrap().amount})` : 'NONE'
+      stake.isSome ? stake.unwrap().amount_mode.type + `(${stake.unwrap().amount.toString()})` : 'NONE'
     );
     const formatPeriod = (unstakingPeriod: Option<BlockNumber>) => (
-      unstakingPeriod.unwrapOr(0) + ' blocks'
-    );
+      `${unstakingPeriod.unwrapOr(new BN(0)).toString()} blocks`
+    )
     return [
       new ParsedParam('Working group', workingGroup.type),
       new ParsedParam('Activate at', `${activateAt.type}${activateAtBlock ? `(${activateAtBlock.toString()})` : ''}`),
@@ -177,34 +181,35 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
         'Max. applications',
         rationingPolicy.isSome ? rationingPolicy.unwrap().max_active_applicants.toNumber() : 'UNLIMITED'
       ),
+      new ParsedParam('Max. review period length', `${commitment.max_review_period_length.toString()} blocks`),
       new ParsedParam(
         'Terminate unstaking period (role stake)',
-        formatPeriod(OPCommitment.terminate_role_stake_unstaking_period)
+        formatPeriod(commitment.terminate_role_stake_unstaking_period)
       ),
       new ParsedParam(
         'Exit unstaking period (role stake)',
-        formatPeriod(OPCommitment.exit_role_stake_unstaking_period)
+        formatPeriod(commitment.exit_role_stake_unstaking_period)
       ),
       // <required_to_prevent_sneaking>
       new ParsedParam(
         'Terminate unstaking period (appl. stake)',
-        formatPeriod(OPCommitment.terminate_application_stake_unstaking_period)
+        formatPeriod(commitment.terminate_application_stake_unstaking_period)
       ),
       new ParsedParam(
         'Exit unstaking period (appl. stake)',
-        formatPeriod(OPCommitment.exit_role_application_stake_unstaking_period)
+        formatPeriod(commitment.exit_role_application_stake_unstaking_period)
       ),
       new ParsedParam(
         'Appl. accepted unstaking period (appl. stake)',
-        formatPeriod(OPCommitment.fill_opening_successful_applicant_application_stake_unstaking_period)
+        formatPeriod(commitment.fill_opening_successful_applicant_application_stake_unstaking_period)
       ),
       new ParsedParam(
         'Appl. failed unstaking period (role stake)',
-        formatPeriod(OPCommitment.fill_opening_failed_applicant_role_stake_unstaking_period)
+        formatPeriod(commitment.fill_opening_failed_applicant_role_stake_unstaking_period)
       ),
       new ParsedParam(
         'Appl. failed unstaking period (appl. stake)',
-        formatPeriod(OPCommitment.fill_opening_failed_applicant_application_stake_unstaking_period)
+        formatPeriod(commitment.fill_opening_failed_applicant_application_stake_unstaking_period)
       ),
       new ParsedParam(
         'Crowded out unstaking period (role stake)',
@@ -227,55 +232,76 @@ const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
     ];
   },
   SetWorkingGroupMintCapacity: ([capacity, group]) => [
-    new ParsedParam('Working group', (new WorkingGroup(group)).type),
-    new ParsedParam('Mint capacity', formatBalance(capacity))
+    new ParsedParam('Working group', (group as WorkingGroup).type),
+    new ParsedParam('Mint capacity', formatBalance((capacity as Balance)))
   ],
   BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [
-    new ParsedParam('Working group', (new WorkingGroup(group)).type),
+    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}`}>#{id}</Link>)
+    new ParsedParam(
+      'Opening id',
+      <Link to={`/working-groups/opportunities/storageProviders/${id.toString()}`}>#{id.toString()}</Link>
+    )
+  ],
+  FillWorkingGroupLeaderOpening: ({
+    opening_id: openingId,
+    successful_application_id: succesfulApplicationId,
+    reward_policy: rewardPolicy,
+    working_group: workingGroup
+  }) => [
+    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>),
+    new ParsedParam('Reward policy', rewardPolicy.isSome ? formatReward(rewardPolicy.unwrap(), true) : 'NONE'),
+    new ParsedParam(
+      'Result',
+      <ApplicationsDetailsByOpening
+        openingId={openingId.toNumber()}
+        acceptedIds={[succesfulApplicationId.toNumber()]}
+        group={workingGroup.type}/>,
+      true
+    )
   ],
-  FillWorkingGroupLeaderOpening: ([params]) => {
-    const { opening_id, successful_application_id, reward_policy, working_group } = params;
-    const rewardPolicy = reward_policy && new RewardPolicy(reward_policy);
-    return [
-      new ParsedParam('Working group', (new WorkingGroup(working_group)).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/${opening_id}`}>#{opening_id}</Link>),
-      new ParsedParam('Reward policy', rewardPolicy ? formatReward(rewardPolicy, true) : 'NONE'),
-      new ParsedParam(
-        'Result',
-        <ApplicationsDetailsByOpening
-          openingId={opening_id}
-          acceptedIds={[successful_application_id]}
-          group={(new WorkingGroup(working_group)).type as WorkingGroupKey}/>,
-        true
-      )
-    ];
-  },
   SlashWorkingGroupLeaderStake: ([leadId, amount, group]) => [
-    new ParsedParam('Working group', (new WorkingGroup(group)).type),
-    new ParsedParam('Slash amount', formatBalance(amount)),
-    new ParsedParam('Lead', <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKey)} leadId={leadId}/>, true)
+    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()}/>,
+      true
+    )
   ],
   DecreaseWorkingGroupLeaderStake: ([leadId, amount, group]) => [
-    new ParsedParam('Working group', (new WorkingGroup(group)).type),
-    new ParsedParam('Decrease amount', formatBalance(amount)),
-    new ParsedParam('Lead', <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKey)} leadId={leadId}/>, true)
+    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()}/>,
+      true
+    )
   ],
   SetWorkingGroupLeaderReward: ([leadId, amount, group]) => [
-    new ParsedParam('Working group', (new WorkingGroup(group)).type),
-    new ParsedParam('New reward amount', formatBalance(amount)),
-    new ParsedParam('Lead', <LeadInfoFromId group={(new WorkingGroup(group).type as WorkingGroupKey)} leadId={leadId}/>, true)
+    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()}/>,
+      true
+    )
   ],
-  TerminateWorkingGroupLeaderRole: ([params]) => {
-    const paramsObj = new TerminateRoleParameters(params);
-    const { working_group: workingGroup, rationale, worker_id: leadId, slash } = paramsObj;
+  TerminateWorkingGroupLeaderRole: ({
+    working_group: workingGroup,
+    rationale,
+    worker_id: leadId,
+    slash
+  }) => {
     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 as WorkingGroupKey} leadId={leadId.toNumber()}/>, true)
+      new ParsedParam('Lead', <LeadInfoFromId group={workingGroup.type} leadId={leadId.toNumber()}/>, true)
     ];
   }
 };
@@ -330,15 +356,20 @@ export default function Body ({
   type,
   title,
   description,
-  params = [],
+  params,
   iAmProposer,
   proposalId,
   proposerId,
   isCancellable,
   cancellationFee
 }: BodyProps) {
-  const parseParams = paramParsers[type];
-  const parsedParams = parseParams(params);
+  // 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 parsedParams = parseParams(
+    type === 'RuntimeUpgrade'
+      ? params as RuntimeUpgradeProposalDetails
+      : (params as ProposalDetails).asType(type)
+  );
   return (
     <Card fluid>
       <Card.Content>
@@ -368,17 +399,17 @@ export default function Body ({
                 The cancellation fee for this type of proposal is:&nbsp;
                 <b>{ cancellationFee ? formatBalance(cancellationFee) : 'NONE' }</b>
               </p>
-              <Button.Group color="red">
-                <TxButton
-                  params={ [proposerId, proposalId] }
-                  tx={ 'proposalsEngine.cancelProposal' }
-                  onClick={ sendTx => { sendTx(); } }
-                  className={'icon left labeled'}
-                >
-                  <Icon name="cancel" inverted />
-                  Withdraw proposal
-                </TxButton>
-              </Button.Group>
+              <SemanticTxButton
+                params={ [proposerId, proposalId] }
+                tx={ 'proposalsEngine.cancelProposal' }
+                onClick={ sendTx => { sendTx(); } }
+                icon
+                color={ 'red' }
+                labelPosition={ 'left' }
+              >
+                <Icon name="cancel" inverted />
+                Withdraw proposal
+              </SemanticTxButton>
             </Message.Content>
           </Message>
           </>) }

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

@@ -4,7 +4,7 @@ import { Item, Dropdown } from 'semantic-ui-react';
 
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { Categories } from '@polkadot/joy-utils/types/proposals';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import './ChooseProposalType.css';
 import { RouteComponentProps } from 'react-router-dom';
 

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

@@ -5,7 +5,7 @@ import { metadata as proposalConsts } from '@polkadot/joy-utils/consts/proposals
 import { ExtendedProposalStatus } from './ProposalDetails';
 import styled from 'styled-components';
 
-import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
+import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview';
 
 const DetailsContainer = styled(Item.Group)`
   display: grid;

+ 3 - 5
pioneer/packages/joy-proposals/src/Proposal/ProposalDetails.tsx

@@ -4,17 +4,15 @@ import Details from './Details';
 import Body from './Body';
 import VotingSection from './VotingSection';
 import Votes from './Votes';
-import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
+import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/react/hocs/accounts';
 import { ParsedProposal, ProposalVotes } from '@polkadot/joy-utils/types/proposals';
 import { withCalls } from '@polkadot/react-api';
-import { withMulti } from '@polkadot/react-api/with';
-
-import './Proposal.css';
+import { withMulti } from '@polkadot/react-api/hoc';
 import { ProposalId, ProposalDecisionStatuses, ApprovedProposalStatuses, ExecutionFailedStatus } from '@joystream/types/proposals';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { MemberId } from '@joystream/types/members';
 import { Seat } from '@joystream/types/council';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import ProposalDiscussion from './discussion/ProposalDiscussion';
 
 import styled from 'styled-components';

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

@@ -2,8 +2,8 @@ import React from 'react';
 import { RouteComponentProps } from 'react-router-dom';
 import ProposalDetails from './ProposalDetails';
 import { useProposalSubscription } from '@polkadot/joy-utils/react/hooks';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
-import { ProposalId } from '@joystream/types/proposals';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
+import { useApi } from '@polkadot/react-hooks';
 
 export default function ProposalFromId (props: RouteComponentProps<any>) {
   const {
@@ -11,8 +11,9 @@ export default function ProposalFromId (props: RouteComponentProps<any>) {
       params: { id }
     }
   } = props;
+  const { api } = useApi();
 
-  const { proposal: proposalState, votes: votesState } = useProposalSubscription(new ProposalId(id));
+  const { proposal: proposalState, votes: votesState } = useProposalSubscription(api.createType('ProposalId', id));
 
   return (
     <PromiseComponent

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

@@ -7,8 +7,6 @@ import { BlockNumber } from '@polkadot/types/interfaces';
 import styled from 'styled-components';
 import ReactMarkdown from 'react-markdown';
 
-import './Proposal.css';
-
 const ProposalIdBox = styled.div`
   position: absolute;
   top: 0;
@@ -33,7 +31,6 @@ export default function ProposalPreview ({ proposal, bestNumber }: ProposalPrevi
   return (
     <Card
       fluid
-      className="Proposal"
       href={`#/proposals/${proposal.id}`}>
       <ProposalIdBox>{ `#${proposal.id.toString()}` }</ProposalIdBox>
       <Card.Content>

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

@@ -6,7 +6,7 @@ import { Link, useLocation } from 'react-router-dom';
 import ProposalPreview from './ProposalPreview';
 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 PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { withCalls } from '@polkadot/react-api';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { Dropdown } from '@polkadot/react-components';

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

@@ -4,7 +4,7 @@ import useVoteStyles from './useVoteStyles';
 import { ProposalVotes } from '@polkadot/joy-utils/types/proposals';
 import { VoteKind } from '@joystream/types/proposals';
 import { VoteKindStr } from './VotingSection';
-import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
+import ProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview';
 
 type VotesProps = {
   votes: ProposalVotes;

+ 19 - 20
pioneer/packages/joy-proposals/src/Proposal/VotingSection.tsx

@@ -1,8 +1,8 @@
 import React, { useState } from 'react';
 
-import { Icon, Button, Message, Divider, Header } from 'semantic-ui-react';
+import { Icon, Message, Divider, Header } from 'semantic-ui-react';
 import useVoteStyles from './useVoteStyles';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton';
 import { MemberId } from '@joystream/types/members';
 import { ProposalId, VoteKind, VoteKinds } from '@joystream/types/proposals';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
@@ -35,24 +35,23 @@ type VoteButtonProps = {
 function VoteButton ({ voteKind, proposalId, memberId, onSuccess }: VoteButtonProps) {
   const { icon, color } = useVoteStyles(voteKind);
   return (
-    // Button.Group "cheat" to force TxButton color
-    <Button.Group color={color} style={{ marginRight: '5px' }}>
-      <TxButton
-        // isDisabled={ isSubmitting }
-        params={[
-          memberId,
-          proposalId,
-          voteKind
-        ]}
-        tx={ 'proposalsEngine.vote' }
-        onClick={ sendTx => sendTx() }
-        txFailedCb={ () => null }
-        txSuccessCb={ onSuccess }
-        className={'icon left labeled'}>
-        <Icon name={icon} inverted />
-        { voteKind }
-      </TxButton>
-    </Button.Group>
+    <SemanticTxButton
+      params={[
+        memberId,
+        proposalId,
+        voteKind
+      ]}
+      tx={ 'proposalsEngine.vote' }
+      onClick={ sendTx => sendTx() }
+      txFailedCb={ () => null }
+      txSuccessCb={ onSuccess }
+      color={color}
+      style={{ marginRight: '5px' }}
+      icon
+      labelPosition={ 'left' }>
+      <Icon name={icon} inverted />
+      { voteKind }
+    </SemanticTxButton>
   );
 }
 

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

@@ -1,7 +1,7 @@
 import React, { useState } from 'react';
 import { Button, Icon } from 'semantic-ui-react';
 import { ParsedPost } from '@polkadot/joy-utils/types/proposals';
-import MemberProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
+import MemberProfilePreview from '@polkadot/joy-utils/react/components/MemberProfilePreview';
 import DiscussionPostForm from './DiscussionPostForm';
 import { MemberId } from '@joystream/types/members';
 import { useTransport } from '@polkadot/joy-utils/react/hooks';

+ 2 - 3
pioneer/packages/joy-proposals/src/Proposal/discussion/DiscussionPostForm.tsx

@@ -2,8 +2,8 @@ import React from 'react';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
-import * as JoyForms from '@polkadot/joy-utils/forms';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { SubmittableResult } from '@polkadot/api';
 import { Button } from 'semantic-ui-react';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -89,7 +89,6 @@ const DiscussionPostFormInner = (props: InnerProps) => {
       <LabelledField invisibleLabel {...props}>
         <TxButton
           type="submit"
-          size="large"
           label={isEditForm ? 'Update' : 'Add Post'}
           isDisabled={isSubmitting || !isValid}
           params={buildTxParams()}

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

@@ -3,7 +3,7 @@ import { Divider, Header } from 'semantic-ui-react';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { ProposalId } from '@joystream/types/proposals';
 import { ParsedDiscussion } from '@polkadot/joy-utils/types/proposals';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import DiscussionPost from './DiscussionPost';
 import DiscussionPostForm from './DiscussionPostForm';
 import { MemberId } from '@joystream/types/members';

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

@@ -15,7 +15,6 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { FormField, InputFormField, TextareaFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { ActivateOpeningAtKey, ActivateOpeningAtDef, StakingAmountLimitModeKeys, IApplicationRationingPolicy, IStakingPolicy } from '@joystream/types/hiring';
 import { GenericJoyStreamRoleSchema } from '@joystream/types/hiring/schemas/role.schema.typings';
 import { Dropdown, Grid, Message, Checkbox } from 'semantic-ui-react';
@@ -347,7 +346,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
     ...genericFormDefaultOptions.validationSchema,
     ...Validation.AddWorkingGroupLeaderOpening(
       props.currentBlock?.toNumber() || 0,
-      props.HRTConstraint || InputValidationLengthConstraint.createWithMaxAllowed()
+      props.HRTConstraint
     )
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,

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

@@ -14,13 +14,12 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import FormField from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Dropdown, Message } from 'semantic-ui-react';
 import _ from 'lodash';
 import Validation from '../validationSchema';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { OpeningData } from '@polkadot/joy-utils/types/workingGroups';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { getFormErrorLabelsProps } from './errorHandling';
 
 export type FormValues = WGFormValues & {

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

@@ -15,7 +15,6 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Grid } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import _ from 'lodash';

+ 16 - 24
pioneer/packages/joy-proposals/src/forms/FillWorkingGroupLeaderOpeningForm.tsx

@@ -14,24 +14,19 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { FormField, RewardPolicyFields } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Dropdown, DropdownItemProps, Header, Checkbox, Message } from 'semantic-ui-react';
 import _ from 'lodash';
 import Validation from '../validationSchema';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { OpeningData, ParsedApplication } from '@polkadot/joy-utils/types/workingGroups';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { formatBalance } from '@polkadot/util';
 import { withCalls } from '@polkadot/react-api';
-import { Option } from '@polkadot/types';
 import { BlockNumber } from '@polkadot/types/interfaces';
-import { u32 as U32, u128 as U128 } from '@polkadot/types/primitive';
 import { getFormErrorLabelsProps } from './errorHandling';
-import { RewardPolicy } from '@joystream/types/working-group';
-import { FillOpeningParameters } from '@joystream/types/proposals';
-import { WorkingGroup } from '@joystream/types/common';
-import { OpeningId, ApplicationId } from '@joystream/types/hiring';
 import { ApplicationsDetails } from '@polkadot/joy-utils/react/components/working-groups/ApplicationDetails';
+import { SimplifiedTypeInterface } from '@polkadot/joy-utils/types/common';
+import { IFillOpeningParameters } from '@joystream/types/src/proposals';
 
 export type FormValues = WGFormValues & {
   openingId: string;
@@ -61,23 +56,20 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
 };
 type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
-const valuesToFillOpeningParams = (values: FormValues): FillOpeningParameters => (
-  new FillOpeningParameters({
-    working_group: new WorkingGroup(values.workingGroup),
-    successful_application_id: new ApplicationId(values.successfulApplicant),
-    opening_id: new OpeningId(values.openingId),
-    reward_policy: new (Option.with(RewardPolicy))(
+const valuesToFillOpeningParams = (values: FormValues): SimplifiedTypeInterface<IFillOpeningParameters> => (
+  {
+    working_group: values.workingGroup,
+    successful_application_id: values.successfulApplicant,
+    opening_id: values.openingId,
+    reward_policy:
       values.includeReward
-        ? new RewardPolicy({
-          amount_per_payout: new U128(values.rewardAmount),
-          next_payment_at_block: new U32(values.rewardNextBlock),
-          payout_interval: new (Option.with('BlockNumber'))(
-            values.rewardRecurring ? values.rewardInterval : null
-          ) as Option<BlockNumber>
-        })
-        : null
-    ) as Option<RewardPolicy>
-  })
+        ? {
+          amount_per_payout: values.rewardAmount,
+          next_payment_at_block: values.rewardNextBlock,
+          payout_interval: values.rewardRecurring ? values.rewardInterval : undefined
+        }
+        : undefined
+  }
 );
 
 const FillWorkingGroupLeaderOpeningForm: React.FunctionComponent<FormInnerProps> = props => {

+ 34 - 7
pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx

@@ -4,11 +4,12 @@ import { Form, Icon, Button, Message } from 'semantic-ui-react';
 import { getFormErrorLabelsProps } from './errorHandling';
 import Validation from '../validationSchema';
 import { InputFormField, TextareaFormField } from './FormFields';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
 import { SubmittableResult } from '@polkadot/api';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
-import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import { withMulti } from '@polkadot/react-api/with';
+import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { withMulti } from '@polkadot/react-api/hoc';
 import { withCalls } from '@polkadot/react-api';
 import { CallProps } from '@polkadot/react-api/types';
 import { Balance, Event } from '@polkadot/types/interfaces';
@@ -16,8 +17,8 @@ import { RouteComponentProps } from 'react-router';
 import { ProposalType } from '@polkadot/joy-utils/types/proposals';
 import proposalsConsts from '@polkadot/joy-utils/consts/proposals';
 import { formatBalance } from '@polkadot/util';
-import './forms.css';
 import { ProposalId } from '@joystream/types/proposals';
+import styled from 'styled-components';
 
 // Generic form values
 export type GenericFormValues = {
@@ -74,6 +75,33 @@ export const genericFormDefaultOptions: GenericFormDefaultOptions = {
   }
 };
 
+const StyledGenericProposalForm = styled.div`
+  .proposal-form {
+    margin: 0 auto;
+  }
+
+  .ui.form.proposal-form {
+    & label {
+      font-size: 1rem;
+    }
+
+    & input[name="tokens"] {
+      max-width: 16rem;
+    }
+  }
+
+  .form-buttons {
+    display: flex;
+    button {
+      margin-right: 0.5em;
+    }
+  }
+
+  .ui.dropdown .ui.avatar.image {
+    width: 2em !important;
+  }
+`;
+
 // Generic proposal form with basic structure, "Title" and "Rationale" fields
 // Other fields can be passed as children
 export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps> = props => {
@@ -162,7 +190,7 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
     proposalsConsts[proposalType].stake;
 
   return (
-    <div className="Forms" ref={formContainerRef}>
+    <StyledGenericProposalForm ref={formContainerRef}>
       <Form
         className="proposal-form"
         onSubmit={txMethod
@@ -199,7 +227,6 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
             <TxButton
               type="button" // Tx button uses custom submit handler - "onTxButtonClick"
               label="Submit proposal"
-              icon="paper plane"
               isDisabled={disabled || isSubmitting}
               params={(submitParams || []).map(p => (p === '{STAKE}' ? requiredStake : p))}
               tx={`proposalsCodex.${txMethod}`}
@@ -220,7 +247,7 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
           </Button>
         </div>
       </Form>
-    </div>
+    </StyledGenericProposalForm>
   );
 };
 

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

@@ -11,10 +11,9 @@ import {
 import { FormField } from './FormFields';
 import { ProposalType } from '@polkadot/joy-utils/types/proposals';
 import { WorkingGroupKey, WorkingGroupDef } from '@joystream/types/common';
-import './forms.css';
 import { Dropdown, Message } from 'semantic-ui-react';
 import { usePromise, useTransport } from '@polkadot/joy-utils/react/hooks';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
 import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 
@@ -70,7 +69,7 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
   const leadMissing = leadRequired && (!leadRes.loading && !leadRes.error) && !leadRes.lead;
   const stakeMissing = leadStakeRequired && (!leadRes.loading && !leadRes.error) && (leadRes.lead && !leadRes.lead.stake);
   const rewardMissing = leadRewardRequired && (!leadRes.loading && !leadRes.error) && (leadRes.lead && !leadRes.lead.reward);
-  const isDisabled = disabled || leadMissing || stakeMissing || rewardMissing || leadRes.error;
+  const isDisabled = disabled || leadMissing || stakeMissing || rewardMissing || Boolean(leadRes.error);
 
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
   return (

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

@@ -16,7 +16,6 @@ import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import { ProposalType } from '@polkadot/joy-utils/types/proposals';
 import { formatBalance } from '@polkadot/util';
-import './forms.css';
 
 export type FormValues = GenericFormValues & {
   capacity: string;

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

@@ -13,7 +13,6 @@ import {
 } from './GenericProposalForm';
 import Validation from '../validationSchema';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import FileDropdown from './FileDropdown';
 
 export type FormValues = GenericFormValues & {

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

@@ -17,9 +17,8 @@ import { FormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { Membership } from '@joystream/types/members';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 import _ from 'lodash';
-import './forms.css';
 
 export type FormValues = GenericFormValues & {
   workingGroupLead: any;

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

@@ -2,7 +2,7 @@ import React from 'react';
 import MintCapacityForm from './MintCapacityForm';
 import { RouteComponentProps } from 'react-router';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
 
 const ContentWorkingGroupMintCapForm = (props: RouteComponentProps) => {
   const transport = useTransport();

+ 15 - 16
pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx

@@ -15,12 +15,11 @@ import {
 import Validation from '../validationSchema';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import { createType } from '@polkadot/types';
-import './forms.css';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import _ from 'lodash';
-import { ElectionParameters } from '@joystream/types/council';
-import { PromiseComponent } from '@polkadot/joy-utils/react/components';
+import PromiseComponent from '@polkadot/joy-utils/react/components/PromiseComponent';
+import { IElectionParameters } from '@joystream/types/src/council';
+import { SimplifiedTypeInterface } from '@polkadot/joy-utils/types/common';
 
 export type FormValues = GenericFormValues & {
   announcingPeriod: string;
@@ -50,17 +49,17 @@ type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormVal
 type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
 type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
-function createElectionParameters (values: FormValues): ElectionParameters {
-  return new ElectionParameters({
-    announcing_period: createType('BlockNumber', parseInt(values.announcingPeriod)),
-    voting_period: createType('BlockNumber', parseInt(values.votingPeriod)),
-    revealing_period: createType('BlockNumber', parseInt(values.revealingPeriod)),
-    council_size: createType('u32', values.councilSize),
-    candidacy_limit: createType('u32', values.candidacyLimit),
-    new_term_duration: createType('BlockNumber', parseInt(values.newTermDuration)),
-    min_council_stake: createType('Balance', values.minCouncilStake),
-    min_voting_stake: createType('Balance', values.minVotingStake)
-  });
+function createElectionParameters (values: FormValues): SimplifiedTypeInterface<IElectionParameters> {
+  return {
+    announcing_period: parseInt(values.announcingPeriod),
+    voting_period: parseInt(values.votingPeriod),
+    revealing_period: parseInt(values.revealingPeriod),
+    council_size: values.councilSize,
+    candidacy_limit: values.candidacyLimit,
+    new_term_duration: parseInt(values.newTermDuration),
+    min_council_stake: values.minCouncilStake,
+    min_voting_stake: values.minVotingStake
+  };
 }
 
 const SetCouncilParamsForm: React.FunctionComponent<FormInnerProps> = props => {
@@ -69,7 +68,7 @@ const SetCouncilParamsForm: React.FunctionComponent<FormInnerProps> = props => {
   const [placeholders, setPlaceholders] = useState<{ [k in keyof FormValues]: string }>(defaultValues);
 
   const transport = useTransport();
-  const [councilParams, error, loading] = usePromise<ElectionParameters | null>(() => transport.council.electionParameters(), null);
+  const [councilParams, error, loading] = usePromise<IElectionParameters | null>(() => transport.council.electionParameters(), null);
   useEffect(() => {
     if (councilParams) {
       const fetchedPlaceholders = { ...placeholders };

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

@@ -15,7 +15,6 @@ import Validation from '../validationSchema';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
-import './forms.css';
 
 export type FormValues = GenericFormValues & {
   maxValidatorCount: string;

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

@@ -15,7 +15,6 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Grid } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import _ from 'lodash';

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

@@ -15,7 +15,6 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Grid } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import _ from 'lodash';

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

@@ -14,7 +14,6 @@ import {
 import Validation from '../validationSchema';
 import { TextareaFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 
 export type FormValues = GenericFormValues & {
   description: string;

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

@@ -15,7 +15,6 @@ import {
 } from './GenericWorkingGroupProposalForm';
 import { InputFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import { Grid } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import _ from 'lodash';

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

@@ -17,7 +17,6 @@ import { InputFormField, FormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import { InputAddress } from '@polkadot/react-components/index';
 import { formatBalance } from '@polkadot/util';
-import './forms.css';
 
 export type FormValues = GenericFormValues & {
   destinationAccount: any;

+ 11 - 14
pioneer/packages/joy-proposals/src/forms/TerminateWorkingGroupLeaderForm.tsx

@@ -13,20 +13,17 @@ import {
   defaultValues as wgFromDefaultValues
 } from './GenericWorkingGroupProposalForm';
 import { withFormContainer } from './FormContainer';
-import './forms.css';
 import _ from 'lodash';
 import Validation from '../validationSchema';
 import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
 import { getFormErrorLabelsProps } from './errorHandling';
 import FormField, { TextareaFormField } from './FormFields';
 import { Checkbox } from 'semantic-ui-react';
-import { TerminateRoleParameters } from '@joystream/types/proposals';
-import { WorkerId } from '@joystream/types/working-group';
-import { Bytes } from '@polkadot/types';
-import { WorkingGroup, InputValidationLengthConstraint } from '@joystream/types/common';
-import { bool as Bool } from '@polkadot/types/primitive';
+import { InputValidationLengthConstraint } from '@joystream/types/common';
 import { withCalls } from '@polkadot/react-api';
 import { formatBalance } from '@polkadot/util';
+import { SimplifiedTypeInterface } from '@polkadot/joy-utils/types/common';
+import { ITerminateRoleParameters } from '@joystream/types/src/proposals';
 
 export type FormValues = WGFormValues & {
   terminationRationale: string;
@@ -46,13 +43,13 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
 };
 type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
-const valuesToTerminateRoleParams = (values: FormValues, lead: WorkerData): TerminateRoleParameters => {
-  return new TerminateRoleParameters({
-    worker_id: new WorkerId(lead.workerId),
-    rationale: new Bytes(values.terminationRationale),
-    slash: lead.stake ? new Bool(values.slashStake) : new Bool(false),
-    working_group: new WorkingGroup(values.workingGroup)
-  });
+const valuesToTerminateRoleParams = (values: FormValues, lead: WorkerData): SimplifiedTypeInterface<ITerminateRoleParameters> => {
+  return {
+    worker_id: lead.workerId,
+    rationale: values.terminationRationale,
+    slash: lead.stake ? values.slashStake : false,
+    working_group: values.workingGroup
+  };
 };
 
 const TerminateWorkingGroupLeaderForm: React.FunctionComponent<FormInnerProps> = props => {
@@ -111,7 +108,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   validationSchema: (props: FormContainerProps) => Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
     ...Validation.TerminateWorkingGroupLeaderRole(
-      props.terminationRationaleConstraint || InputValidationLengthConstraint.createWithMaxAllowed()
+      props.terminationRationaleConstraint
     )
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,

+ 0 - 23
pioneer/packages/joy-proposals/src/forms/forms.css

@@ -1,23 +0,0 @@
-.Forms {
-  .proposal-form {
-    margin: 0 auto;
-  }
-
-  .ui.form.proposal-form {
-    & label {
-      font-size: 1rem;
-    }
-
-    & input[name="tokens"] {
-      max-width: 16rem;
-    }
-  }
-
-  .form-buttons {
-    display: flex;
-  }
-
-  .ui.dropdown .ui.avatar.image {
-    width: 2em !important;
-  }
-}

+ 0 - 0
pioneer/packages/joy-proposals/src/index.css


+ 55 - 56
pioneer/packages/joy-proposals/src/index.tsx

@@ -4,13 +4,10 @@ import { Link } from 'react-router-dom';
 import styled from 'styled-components';
 import { Breadcrumb } from 'semantic-ui-react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
-import { TransportProvider } from '@polkadot/joy-utils/react/context';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
 import _ from 'lodash';
 
-import './index.css';
-
 import translate from './translate';
 import NotDone from './NotDone';
 import {
@@ -30,8 +27,12 @@ import {
   SetWorkingGroupLeadRewardForm,
   TerminateWorkingGroupLeaderForm
 } from './forms';
+import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
+import style from './style';
+
+const ProposalsMain = styled.main`${style}`;
 
-interface Props extends AppProps, I18nProps {}
+interface Props extends AppMainRouteProps, I18nProps {}
 
 const StyledHeader = styled.header`
   text-align: left;
@@ -46,59 +47,57 @@ function App (props: Props): React.ReactElement<Props> {
   const { basePath } = props;
 
   return (
-    <TransportProvider>
-      <main className="proposal--App">
-        <StyledHeader>
-          <Breadcrumb>
-            <Switch>
-              <Route path={`${basePath}/new/:type`} render={props => (
-                <>
-                  <Breadcrumb.Section link as={Link} to={basePath}>Proposals</Breadcrumb.Section>
-                  <Breadcrumb.Divider icon="right angle" />
-                  <Breadcrumb.Section link as={Link} to={`${basePath}/new`}>New proposal</Breadcrumb.Section>
-                  <Breadcrumb.Divider icon="right angle" />
-                  <Breadcrumb.Section active>{_.startCase(props.match.params.type)}</Breadcrumb.Section>
-                </>
-              )} />
-              <Route path={`${basePath}/new`}>
+    <ProposalsMain className="proposal--App">
+      <StyledHeader>
+        <Breadcrumb>
+          <Switch>
+            <Route path={`${basePath}/new/:type`} render={props => (
+              <>
                 <Breadcrumb.Section link as={Link} to={basePath}>Proposals</Breadcrumb.Section>
                 <Breadcrumb.Divider icon="right angle" />
-                <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
-              </Route>
-              <Route>
-                <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
-              </Route>
-            </Switch>
-          </Breadcrumb>
-        </StyledHeader>
-        <Switch>
-          <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
-          <Route exact path={`${basePath}/new/text`} component={SignalForm} />
-          <Route exact path={`${basePath}/new/runtime-upgrade`} component={RuntimeUpgradeForm} />
-          <Route exact path={`${basePath}/new/set-election-parameters`} component={SetCouncilParamsForm} />
-          <Route exact path={`${basePath}/new/spending`} component={SpendingProposalForm} />
-          <Route exact path={`${basePath}/new/set-lead`} component={SetContentWorkingGroupLeadForm} />
-          <Route
-            exact
-            path={`${basePath}/new/set-content-working-group-mint-capacity`}
-            component={SetContentWorkingGroupMintCapForm}
-          />
-          <Route exact path={`${basePath}/new/set-validator-count`} component={SetMaxValidatorCountForm} />
-          <Route exact path={`${basePath}/new/add-working-group-leader-opening`} component={AddWorkingGroupOpeningForm} />
-          <Route exact path={`${basePath}/new/set-working-group-mint-capacity`} component={SetWorkingGroupMintCapacityForm} />
-          <Route exact path={`${basePath}/new/begin-review-working-group-leader-application`} component={BeginReviewLeaderApplicationsForm} />
-          <Route exact path={`${basePath}/new/fill-working-group-leader-opening`} component={FillWorkingGroupLeaderOpeningForm} />
-          <Route exact path={`${basePath}/new/decrease-working-group-leader-stake`} component={DecreaseWorkingGroupLeadStakeFrom} />
-          <Route exact path={`${basePath}/new/slash-working-group-leader-stake`} component={SlashWorkingGroupLeadStakeForm} />
-          <Route exact path={`${basePath}/new/set-working-group-leader-reward`} component={SetWorkingGroupLeadRewardForm} />
-          <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 component={ProposalPreviewList} />
-        </Switch>
-      </main>
-    </TransportProvider>
+                <Breadcrumb.Section link as={Link} to={`${basePath}/new`}>New proposal</Breadcrumb.Section>
+                <Breadcrumb.Divider icon="right angle" />
+                <Breadcrumb.Section active>{_.startCase(props.match.params.type)}</Breadcrumb.Section>
+              </>
+            )} />
+            <Route path={`${basePath}/new`}>
+              <Breadcrumb.Section link as={Link} to={basePath}>Proposals</Breadcrumb.Section>
+              <Breadcrumb.Divider icon="right angle" />
+              <Breadcrumb.Section active>New proposal</Breadcrumb.Section>
+            </Route>
+            <Route>
+              <Breadcrumb.Section active>Proposals</Breadcrumb.Section>
+            </Route>
+          </Switch>
+        </Breadcrumb>
+      </StyledHeader>
+      <Switch>
+        <Route exact path={`${basePath}/new`} component={ChooseProposalType} />
+        <Route exact path={`${basePath}/new/text`} component={SignalForm} />
+        <Route exact path={`${basePath}/new/runtime-upgrade`} component={RuntimeUpgradeForm} />
+        <Route exact path={`${basePath}/new/set-election-parameters`} component={SetCouncilParamsForm} />
+        <Route exact path={`${basePath}/new/spending`} component={SpendingProposalForm} />
+        <Route exact path={`${basePath}/new/set-lead`} component={SetContentWorkingGroupLeadForm} />
+        <Route
+          exact
+          path={`${basePath}/new/set-content-working-group-mint-capacity`}
+          component={SetContentWorkingGroupMintCapForm}
+        />
+        <Route exact path={`${basePath}/new/set-validator-count`} component={SetMaxValidatorCountForm} />
+        <Route exact path={`${basePath}/new/add-working-group-leader-opening`} component={AddWorkingGroupOpeningForm} />
+        <Route exact path={`${basePath}/new/set-working-group-mint-capacity`} component={SetWorkingGroupMintCapacityForm} />
+        <Route exact path={`${basePath}/new/begin-review-working-group-leader-application`} component={BeginReviewLeaderApplicationsForm} />
+        <Route exact path={`${basePath}/new/fill-working-group-leader-opening`} component={FillWorkingGroupLeaderOpeningForm} />
+        <Route exact path={`${basePath}/new/decrease-working-group-leader-stake`} component={DecreaseWorkingGroupLeadStakeFrom} />
+        <Route exact path={`${basePath}/new/slash-working-group-leader-stake`} component={SlashWorkingGroupLeadStakeForm} />
+        <Route exact path={`${basePath}/new/set-working-group-leader-reward`} component={SetWorkingGroupLeadRewardForm} />
+        <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 component={ProposalPreviewList} />
+      </Switch>
+    </ProposalsMain>
   );
 }
 

+ 3 - 3
pioneer/packages/joy-proposals/src/stories/data/ProposalDetails.mock.ts

@@ -1,13 +1,13 @@
 import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
-import { ProposalId } from '@joystream/types/proposals';
+import { createMock } from '@joystream/types';
 
 const mockedProposal: ParsedProposal = {
-  id: new ProposalId(100),
+  id: createMock('ProposalId', 100),
   title: 'Awesome Proposal',
   description: 'Please send me some tokens for coffee',
   createdAtBlock: 36,
   type: 'Text',
-  details: ['Ciao'],
+  details: createMock('ProposalDetails', { Text: 'Ciao' }),
   parameters: {
     approvalQuorumPercentage: 66,
     approvalThresholdPercentage: 80,

+ 4 - 4
pioneer/packages/joy-proposals/src/Proposal/Proposal.css → pioneer/packages/joy-proposals/src/style.ts

@@ -1,7 +1,7 @@
-.Proposal {
-  position: relative;
+import { css } from 'styled-components';
 
-  .description {
+export default css`
+  .ui.card .description {
     word-wrap: break-word;
     word-break: break-word;
   }
@@ -26,4 +26,4 @@
   .ui.tabular.list-menu {
     margin-bottom: 2rem;
   }
-}
+`;

+ 23 - 12
pioneer/packages/joy-proposals/src/validationSchema.ts

@@ -152,9 +152,9 @@ type FormValuesByType<T extends ValidationTypeKeys> =
   never;
 
 type ValidationSchemaFuncParamsByType<T extends ValidationTypeKeys> =
-  T extends 'AddWorkingGroupLeaderOpening' ? [number, InputValidationLengthConstraint] :
+  T extends 'AddWorkingGroupLeaderOpening' ? [number, InputValidationLengthConstraint | undefined] :
   T extends 'FillWorkingGroupLeaderOpening' ? [number] :
-  T extends 'TerminateWorkingGroupLeaderRole' ? [InputValidationLengthConstraint] :
+  T extends 'TerminateWorkingGroupLeaderRole' ? [InputValidationLengthConstraint | undefined] :
   [];
 
 /* eslint-enable @typescript-eslint/indent */
@@ -178,6 +178,23 @@ function minMaxInt (min: number, max: number, fieldName: string) {
     .max(max, errorMessage(fieldName, min, max));
 }
 
+function minMaxStrFromConstraint(constraint: InputValidationLengthConstraint | undefined, fieldName: string) {
+  const schema = Yup.string().required(`${fieldName} is required!`);
+  return constraint
+    ? (
+      schema
+        .min(
+          constraint.min.toNumber(),
+          `${fieldName} must be at least ${constraint.min.toNumber()} character(s) long`
+        )
+        .max(
+          constraint.max.toNumber(),
+          `${fieldName} cannot be more than ${constraint.max.toNumber()} character(s) long`
+        )
+    )
+    : schema;
+}
+
 const Validation: ValidationType = {
   All: () => ({
     title: Yup.string()
@@ -303,7 +320,7 @@ const Validation: ValidationType = {
         errorMessage('The max validator count', MAX_VALIDATOR_COUNT_MIN, MAX_VALIDATOR_COUNT_MAX)
       )
   }),
-  AddWorkingGroupLeaderOpening: (currentBlock: number, { min: HRTMin, max: HRTMax }: InputValidationLengthConstraint) => ({
+  AddWorkingGroupLeaderOpening: (currentBlock: number, HRTConstraint?: InputValidationLengthConstraint) => ({
     workingGroup: Yup.string(),
     activateAt: Yup.string().required(),
     activateAtBlock: Yup.number()
@@ -346,8 +363,7 @@ const Validation: ValidationType = {
       LEAVE_ROLE_UNSTAKING_MAX,
       'Leave role unstaking period'
     ),
-    humanReadableText: Yup.string()
-      .required()
+    humanReadableText: minMaxStrFromConstraint(HRTConstraint, 'human_readable_text')
       .test(
         'schemaIsValid',
         'Schema validation failed!',
@@ -368,8 +384,6 @@ const Validation: ValidationType = {
           return true;
         }
       )
-      .min(HRTMin.toNumber(), `human_readable_text must be at least ${HRTMin.toNumber()} character(s) long`)
-      .max(HRTMax.toNumber(), `human_readable_text cannot be more than ${HRTMax.toNumber()} character(s) long`)
   }),
   SetWorkingGroupMintCapacity: () => ({
     workingGroup: Yup.string(),
@@ -421,12 +435,9 @@ const Validation: ValidationType = {
     workingGroup: Yup.string(),
     amount: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
   }),
-  TerminateWorkingGroupLeaderRole: ({ min, max }: InputValidationLengthConstraint) => ({
+  TerminateWorkingGroupLeaderRole: (rationaleConstraint?: InputValidationLengthConstraint) => ({
     workingGroup: Yup.string(),
-    terminationRationale: Yup.string()
-      .required('Termination rationale is required')
-      .min(min.toNumber(), `Termination rationale must be at least ${min.toNumber()} character(s) long`)
-      .max(max.toNumber(), `Termination rationale cannot be more than ${max.toNumber()} character(s) long`),
+    terminationRationale: minMaxStrFromConstraint(rationaleConstraint, 'Termination rationale'),
     slashStake: Yup.boolean()
   })
 };

+ 0 - 0
pioneer/packages/joy-utils-old/src/consts/members.ts → pioneer/packages/joy-utils/src/consts/members.ts


+ 0 - 0
pioneer/packages/joy-utils-old/src/consts/proposals.ts → pioneer/packages/joy-utils/src/consts/proposals.ts


+ 0 - 0
pioneer/packages/joy-utils-old/src/consts/workingGroups.ts → pioneer/packages/joy-utils/src/consts/workingGroups.ts


+ 78 - 63
pioneer/packages/joy-utils/src/react/components/TxButton.tsx

@@ -1,67 +1,55 @@
-import React from 'react';
-import { BareProps, ApiProps } from '@polkadot/react-api/types';
-import { QueueTxExtrinsicAdd, PartialQueueTxExtrinsic, TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import React, { useContext } from 'react';
+import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
 import { Button } from '@polkadot/react-components/index';
-import { QueueConsumer } from '@polkadot/react-components/Status/Context';
-import { withApi } from '@polkadot/react-api/index';
+import QueueContext from '@polkadot/react-components/Status/Context';
 import { assert } from '@polkadot/util';
-import { withMyAccount, MyAccountProps } from '../hocs/accounts';
-import { IconName } from '@fortawesome/fontawesome-svg-core';
-
-type InjectedProps = {
-  queueExtrinsic: QueueTxExtrinsicAdd;
-};
+import { ButtonProps as DefaultButtonProps } from '@polkadot/react-components/Button/types';
+import { StrictButtonProps as SemanticButtonStrictProps, Button as SemanticButton } from 'semantic-ui-react';
+import { useApi } from '@polkadot/react-hooks';
+import { useMyAccount } from '../hooks';
+import _ from 'lodash';
 
 export type OnTxButtonClick = (sendTx: () => void) => void;
 
-type BasicButtonProps = {
+type TxButtonBaseProps = {
   accountId?: string;
   type?: 'submit' | 'button';
-  isBasic?: boolean;
-  isDisabled?: boolean;
-  label?: React.ReactNode;
   params: Array<any>;
   tx: string;
-
-  className?: string;
-  style?: Record<string, string | number>;
-  children?: React.ReactNode;
-  compact?: boolean;
-  icon?: IconName;
-
   onClick?: OnTxButtonClick;
   txFailedCb?: TxFailedCallback;
   txSuccessCb?: TxCallback;
   txStartCb?: () => void;
   txUpdateCb?: TxCallback;
-};
-
-type PropsWithApi = BareProps & ApiProps & MyAccountProps & PartialQueueTxExtrinsic & BasicButtonProps
-
-class TxButtonInner extends React.PureComponent<PropsWithApi & InjectedProps> {
-  render () {
-    const { myAddress, accountId, isDisabled, icon = 'check', onClick } = this.props;
-    const origin = accountId || myAddress;
-
-    return (
-      <Button
-        {...this.props}
-        isDisabled={isDisabled || !origin}
-        icon={icon}
-        onClick={() => {
-          if (onClick) onClick(this.send);
-          else this.send();
-        }}
-      />
-    );
-  }
+}
 
-  private send = (): void => {
-    const {
-      myAddress, accountId, api, params, queueExtrinsic, tx,
-      txFailedCb, txSuccessCb, txStartCb, txUpdateCb
-    } = this.props;
-    const origin = accountId || myAddress;
+// Allows us to exclude those from button props
+const txButtonNotPassedProps: readonly (keyof TxButtonBaseProps)[] = [
+  'accountId',
+  'params',
+  'tx',
+  'onClick',
+  'txFailedCb',
+  'txSuccessCb',
+  'txStartCb',
+  'txUpdateCb'
+ ] as const;
+
+type SemanticButtonProps = SemanticButtonStrictProps & { style?: React.CSSProperties };
+
+type TxButtonProps<ButtonComponentProps extends Record<string, any>> = Omit<ButtonComponentProps, 'onClick'> & TxButtonBaseProps;
+
+function useTxButton(props: TxButtonBaseProps) {
+  const { queueExtrinsic } = useContext(QueueContext);
+  const { api } = useApi();
+  const { state: { address: myAddress } } = useMyAccount();
+  const {
+    accountId, params, tx,
+    txFailedCb, txSuccessCb, txStartCb, txUpdateCb
+  } = props;
+  const origin = accountId || myAddress;
+
+  const sendTx = () => {
     const [section, method] = tx.split('.');
 
     assert(api.tx[section] && api.tx[section][method], `Unable to find api.tx.${section}.${method}`);
@@ -75,24 +63,51 @@ class TxButtonInner extends React.PureComponent<PropsWithApi & InjectedProps> {
       txUpdateCb
     });
   }
+
+  return { origin, sendTx };
 }
 
-class TxButton extends React.PureComponent<PropsWithApi> {
-  render () {
-    return (
-      <QueueConsumer>
-        {({ queueExtrinsic }) => (
-          <TxButtonInner
-            {...this.props}
-            queueExtrinsic={queueExtrinsic}
-          />
-        )}
-      </QueueConsumer>
-    );
-  }
+// Make icon optional since we provide default value
+type DefaultTxButtonProps = TxButtonProps<Omit<DefaultButtonProps, 'icon'> & { icon?: DefaultButtonProps['icon'] }>;
+
+export const DefaultTxButton = (props: DefaultTxButtonProps) => {
+  const { origin, sendTx } = useTxButton(props);
+  const { isDisabled, icon = 'check', onClick } = props;
+  const buttonProps = _.omit(props, txButtonNotPassedProps);
+
+  return (
+    <Button
+      {...buttonProps}
+      isDisabled={isDisabled || !origin}
+      icon={icon}
+      onClick={() => {
+        if (onClick) onClick(sendTx);
+        else sendTx();
+      }}
+    />
+  );
+}
+
+type SemanticTxButtonProps = TxButtonProps<SemanticButtonProps>;
+
+export const SemanticTxButton = (props: SemanticTxButtonProps) => {
+  const { origin, sendTx } = useTxButton(props);
+  const { disabled, onClick } = props;
+  const buttonProps = _.omit(props, txButtonNotPassedProps);
+
+  return (
+    <SemanticButton
+      {...buttonProps}
+      disabled={disabled || !origin}
+      onClick={() => {
+        if (onClick) onClick(sendTx);
+        else sendTx();
+      }}
+    />
+  );
 }
 
-export default withApi(withMyAccount(TxButton));
+export default DefaultTxButton;
 
 // const SubstrateTxButton = withApi(withMyAccount(TxButton));
 

+ 1 - 1
pioneer/packages/joy-utils-old/src/react/components/working-groups/ApplicationDetails.tsx → pioneer/packages/joy-utils/src/react/components/working-groups/ApplicationDetails.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from 'react';
 import { ParsedApplication } from '../../../types/workingGroups';
-import { ProfilePreviewFromStruct as MemberPreview } from '../../../MemberProfilePreview';
+import { ProfilePreviewFromStruct as MemberPreview } from '../MemberProfilePreview';
 import { useTransport, usePromise } from '../../hooks';
 import { Item, Label, Button } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';

+ 1 - 1
pioneer/packages/joy-utils-old/src/react/components/working-groups/LeadInfo.tsx → pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { WorkerData } from '../../../types/workingGroups';
-import { ProfilePreviewFromStruct as MemberPreview } from '../../../MemberProfilePreview';
+import { ProfilePreviewFromStruct as MemberPreview } from '../MemberProfilePreview';
 import { Label, Message } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import { WorkingGroupKey } from '@joystream/types/common';

+ 1 - 0
pioneer/packages/joy-utils/src/react/hooks/index.ts

@@ -2,3 +2,4 @@ export { default as useMyAccount } from './useMyAccount';
 export { default as useMyMembership } from './useMyMembership';
 export { default as usePromise } from './usePromise';
 export { default as useTransport } from './useTransport';
+export { default as useProposalSubscription } from './proposals/useProposalSubscription';

+ 0 - 0
pioneer/packages/joy-utils-old/src/react/hooks/proposals/useProposalSubscription.tsx → pioneer/packages/joy-utils/src/react/hooks/proposals/useProposalSubscription.tsx


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

@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
 import { normalizeError } from '../../functions/misc';
 import { randomBytes } from 'crypto';
 
-export type UsePromiseReturnValues<T> = [T, string | null, boolean];
+export type UsePromiseReturnValues<T> = [T, string | null, boolean, () => void];
 
 export default function usePromise<T> (
   promise: () => Promise<T>,
@@ -65,5 +65,5 @@ export default function usePromise<T> (
 
   const { value, error, isPending } = state;
 
-  return [value, error, isPending];
+  return [value, error, isPending, executeAndSubscribePromise];
 }

+ 17 - 0
pioneer/packages/joy-utils/src/transport/base.ts

@@ -1,4 +1,7 @@
 import { ApiPromise } from '@polkadot/api';
+import { UInt } from '@polkadot/types/codec';
+import { Codec, CodecArg } from '@polkadot/types/types';
+import { QueryableStorageEntry } from '@polkadot/api/types/storage';
 import { APIQueryCache } from './APIQueryCache';
 
 export default abstract class BaseTransport {
@@ -63,4 +66,18 @@ export default abstract class BaseTransport {
 
     return this.api.query[module][method];
   }
+
+  protected async entriesByIds<IDType extends UInt, ValueType extends Codec> (
+    apiMethod: QueryableStorageEntry<'promise'>,
+    firstKey?: CodecArg // First key in case of double maps
+  ): Promise<[IDType, ValueType][]> {
+    const entries: [IDType, ValueType][] = (await apiMethod.entries<ValueType>(firstKey))
+      .map(([storageKey, value]) => ([
+        // If double-map (first key is provided), we map entries by second key
+        storageKey.args[firstKey !== undefined ? 1 : 0] as IDType,
+        value
+      ]));
+
+    return entries.sort((a, b) => a[0].toNumber() - b[0].toNumber());
+  }
 }

+ 0 - 0
pioneer/packages/joy-utils-old/src/transport/chain.ts → pioneer/packages/joy-utils/src/transport/chain.ts


+ 6 - 7
pioneer/packages/joy-utils-old/src/transport/contentWorkingGroup.ts → pioneer/packages/joy-utils/src/transport/contentWorkingGroup.ts

@@ -1,12 +1,11 @@
 import { Membership } from '@joystream/types/members';
-import { u128, Vec, Option } from '@polkadot/types/';
+import { u128, Option } from '@polkadot/types/';
 import BaseTransport from './base';
 import { MintId, Mint } from '@joystream/types/mint';
 import { LeadId, Lead } from '@joystream/types/content-working-group';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
-import { APIQueryCache } from '../APIQueryCache';
-import { SingleLinkedMapEntry } from '..';
+import { APIQueryCache } from './APIQueryCache';
 
 export default class ContentWorkingGroupTransport extends BaseTransport {
   private membersT: MembersTransport;
@@ -18,8 +17,8 @@ export default class ContentWorkingGroupTransport extends BaseTransport {
 
   async currentMintCap (): Promise<number> {
     const WGMintId = (await this.contentWorkingGroup.mint()) as MintId;
-    const WGMint = (await this.minting.mints(WGMintId)) as Vec<Mint>;
-    return (WGMint[0].get('capacity') as u128).toNumber();
+    const WGMint = (await this.minting.mints(WGMintId)) as Mint;
+    return (WGMint.get('capacity') as u128).toNumber();
   }
 
   async currentLead (): Promise<{ id: number; profile: Membership } | null> {
@@ -28,9 +27,9 @@ export default class ContentWorkingGroupTransport extends BaseTransport {
 
     if (!leadId) return null;
 
-    const lead = new SingleLinkedMapEntry(Lead, await this.contentWorkingGroup.leadById(leadId)).value;
+    const lead = await this.contentWorkingGroup.leadById(leadId) as Lead;
 
-    if (!lead.stage.isOfType('Active')) {
+    if (lead.isEmpty || !lead.stage.isOfType('Active')) {
       return null;
     }
 

+ 15 - 23
pioneer/packages/joy-utils-old/src/transport/council.ts → pioneer/packages/joy-utils/src/transport/council.ts

@@ -1,14 +1,13 @@
 import { ParsedMember } from '../types/members';
 import BaseTransport from './base';
-import { Seats, ElectionParameters } from '@joystream/types/council';
+import { Seats, IElectionParameters } from '@joystream/types/council';
 import { MemberId, Membership } from '@joystream/types/members';
 import { u32, Vec } from '@polkadot/types/';
 import { Balance, BlockNumber } from '@polkadot/types/interfaces';
-import { FIRST_MEMBER_ID } from '../consts/members';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
 import ChainTransport from './chain';
-import { APIQueryCache } from '../APIQueryCache';
+import { APIQueryCache } from './APIQueryCache';
 
 export default class CouncilTransport extends BaseTransport {
   private membersT: MembersTransport;
@@ -46,28 +45,21 @@ export default class CouncilTransport extends BaseTransport {
   async membersExceptCouncil (): Promise<{ id: number; profile: Membership }[]> {
     // Council members to filter out
     const activeCouncil = (await this.council.activeCouncil()) as Seats;
-    const membersCount = ((await this.members.nextMemberId()) as MemberId).toNumber();
-    const profiles: { id: number; profile: Membership }[] = [];
-    for (let id = FIRST_MEMBER_ID.toNumber(); id < membersCount; ++id) {
-      const profile = (await this.membersT.membershipById(new MemberId(id)));
-      if (
-        !profile ||
+
+    return (await this.membersT.allMembers())
+      .filter(([memberId, member]) => (
         // Filter out council members
-        activeCouncil.some(
-          seat =>
-            seat.member.toString() === profile.controller_account.toString() ||
-            seat.member.toString() === profile.root_account.toString()
+        !activeCouncil.some((seat) =>
+            seat.member.eq(member.controller_account) ||
+            seat.member.eq(member.root_account)
         )
-      ) {
-        continue;
-      }
-      profiles.push({ id, profile });
-    }
-
-    return profiles;
+      ))
+      .map(([memberId, member]) => (
+        { id: memberId.toNumber(), profile: member }
+      ));
   }
 
-  async electionParameters (): Promise<ElectionParameters> {
+  async electionParameters (): Promise<IElectionParameters> {
     const announcing_period = (await this.councilElection.announcingPeriod()) as BlockNumber;
     const voting_period = (await this.councilElection.votingPeriod()) as BlockNumber;
     const revealing_period = (await this.councilElection.revealingPeriod()) as BlockNumber;
@@ -77,7 +69,7 @@ export default class CouncilTransport extends BaseTransport {
     const candidacy_limit = (await this.councilElection.candidacyLimit()) as u32;
     const council_size = (await this.councilElection.councilSize()) as u32;
 
-    return new ElectionParameters({
+    return {
       announcing_period,
       voting_period,
       revealing_period,
@@ -86,6 +78,6 @@ export default class CouncilTransport extends BaseTransport {
       min_voting_stake,
       candidacy_limit,
       council_size
-    });
+    }
   }
 }

+ 18 - 18
pioneer/packages/joy-utils/src/transport/index.ts

@@ -1,34 +1,34 @@
 import { ApiPromise } from '@polkadot/api';
-// import ChainTransport from './chain';
-// import ContentWorkingGroupTransport from './contentWorkingGroup';
-// import ProposalsTransport from './proposals';
+import ChainTransport from './chain';
+import ContentWorkingGroupTransport from './contentWorkingGroup';
+import ProposalsTransport from './proposals';
 import MembersTransport from './members';
-// import CouncilTransport from './council';
-// import ValidatorsTransport from './validators';
-// import WorkingGroupsTransport from './workingGroups';
+import CouncilTransport from './council';
+import ValidatorsTransport from './validators';
+import WorkingGroupsTransport from './workingGroups';
 import { APIQueryCache } from './APIQueryCache';
 
 export default class Transport {
   protected api: ApiPromise;
   protected cacheApi: APIQueryCache;
   // Specific transports
-  // public chain: ChainTransport;
+  public chain: ChainTransport;
   public members: MembersTransport;
-  // public council: CouncilTransport;
-  // public proposals: ProposalsTransport;
-  // public contentWorkingGroup: ContentWorkingGroupTransport;
-  // public validators: ValidatorsTransport;
-  // public workingGroups: WorkingGroupsTransport;
+  public council: CouncilTransport;
+  public proposals: ProposalsTransport;
+  public contentWorkingGroup: ContentWorkingGroupTransport;
+  public validators: ValidatorsTransport;
+  public workingGroups: WorkingGroupsTransport;
 
   constructor (api: ApiPromise) {
     this.api = api;
     this.cacheApi = new APIQueryCache(api);
-    // this.chain = new ChainTransport(api, this.cacheApi);
+    this.chain = new ChainTransport(api, this.cacheApi);
     this.members = new MembersTransport(api, this.cacheApi);
-    // this.validators = new ValidatorsTransport(api, this.cacheApi);
-    // this.council = new CouncilTransport(api, this.cacheApi, this.members, this.chain);
-    // this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
-    // this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
-    // this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
+    this.validators = new ValidatorsTransport(api, this.cacheApi);
+    this.council = new CouncilTransport(api, this.cacheApi, this.members, this.chain);
+    this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
+    this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
+    this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
   }
 }

+ 5 - 2
pioneer/packages/joy-utils/src/transport/members.ts

@@ -8,8 +8,7 @@ export default class MembersTransport extends BaseTransport {
   async membershipById (id: MemberId | number): Promise<Membership | null> {
     const member = (await this.members.membershipById(id)) as Membership;
 
-    // Can't just use member.isEmpty because member.suspended is Bool (which isEmpty method always returns false)
-    return member.handle.isEmpty ? null : member;
+    return member.isEmpty ? null : member;
   }
 
   // Throws if profile not found
@@ -39,4 +38,8 @@ export default class MembersTransport extends BaseTransport {
       profile
     };
   }
+
+  async allMembers(): Promise<[MemberId, Membership][]> {
+    return this.entriesByIds<MemberId, Membership>(this.api.query.members.membershipById);
+  }
 }

+ 24 - 45
pioneer/packages/joy-utils-old/src/transport/proposals.ts → pioneer/packages/joy-utils/src/transport/proposals.ts

@@ -8,7 +8,8 @@ import {
   ParsedDiscussion,
   DiscussionContraints,
   ProposalStatusFilter,
-  ProposalsBatch
+  ProposalsBatch,
+  ParsedProposalDetails
 } from '../types/proposals';
 import { ParsedMember } from '../types/members';
 
@@ -17,13 +18,12 @@ import BaseTransport from './base';
 import { ThreadId, PostId } from '@joystream/types/common';
 import { Proposal, ProposalId, VoteKind, DiscussionThread, DiscussionPost, ProposalDetails, Finalized, ProposalDecisionStatus } from '@joystream/types/proposals';
 import { MemberId } from '@joystream/types/members';
-import { u32, u64, Bytes, Null } from '@polkadot/types/';
+import { u32, Bytes, Null } from '@polkadot/types/';
 import { BalanceOf } from '@polkadot/types/interfaces';
 
 import { bytesToString } from '../functions/misc';
 import _ from 'lodash';
 import { metadata as proposalsConsts, apiMethods as proposalsApiMethods } from '../consts/proposals';
-import { FIRST_MEMBER_ID } from '../consts/members';
 
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
@@ -31,12 +31,11 @@ import ChainTransport from './chain';
 import CouncilTransport from './council';
 
 import { blake2AsHex } from '@polkadot/util-crypto';
-import { APIQueryCache } from '../APIQueryCache';
-import { MultipleLinkedMapEntry } from '../LinkedMapEntry';
+import { APIQueryCache } from './APIQueryCache';
 
 type ProposalDetailsCacheEntry = {
   type: ProposalType;
-  details: any[];
+  details: ParsedProposalDetails;
 }
 type ProposalDetailsCache = {
   [id: number]: ProposalDetailsCacheEntry | undefined;
@@ -83,18 +82,14 @@ export default class ProposalsTransport extends BaseTransport {
     if (cachedProposalDetails) {
       return cachedProposalDetails;
     } else {
-      // TODO: The right typesafe handling with JoyEnum would be very useful here
       const rawDetails = await this.rawProposalDetails(id);
-      const type = rawDetails.type as ProposalType;
-      let details: any[];
+      const type = rawDetails.type;
+      let details: ParsedProposalDetails = rawDetails;
       if (type === 'RuntimeUpgrade') {
         // In case of RuntimeUpgrade proposal we override details to just contain the hash and filesize
         // (instead of full WASM bytecode)
         const wasm = rawDetails.value as Bytes;
         details = [blake2AsHex(wasm, 256), wasm.length];
-      } else {
-        const detailsJSON = rawDetails.value.toJSON();
-        details = Array.isArray(detailsJSON) ? detailsJSON : [detailsJSON];
       }
       // Save entry in cache
       this.proposalDetailsCache[id.toNumber()] = { type, details };
@@ -135,18 +130,15 @@ export default class ProposalsTransport extends BaseTransport {
 
   async proposalsIds () {
     const total: number = (await this.proposalCount()).toNumber();
-    return Array.from({ length: total }, (_, i) => new ProposalId(i + 1));
+    return Array.from({ length: total }, (_, i) => this.api.createType('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 : [];
-  }
+    const result = await this.entriesByIds<ProposalId, Null>(
+      this.api.query.proposalsEngine.activeProposalIds
+    )
 
-  async proposals () {
-    const ids = await this.proposalsIds();
-    return Promise.all(ids.map(id => this.proposalById(id)));
+    return result.map(([proposalId]) => proposalId);
   }
 
   async proposalsBatch (status: ProposalStatusFilter, batchNumber = 1, batchSize = 5): Promise<ProposalsBatch> {
@@ -157,11 +149,10 @@ export default class ProposalsTransport extends BaseTransport {
 
     if (status !== 'All' && status !== 'Active') {
       rawProposalsWithIds = rawProposalsWithIds.filter(({ proposal }) => {
-        if (proposal.status.type !== 'Finalized') {
+        if (!proposal.status.isOfType('Finalized')) {
           return false;
         }
-        const finalStatus = ((proposal.status.value as Finalized).get('proposalStatus') as ProposalDecisionStatus);
-        return finalStatus.type === status;
+        return proposal.status.asType('Finalized').proposalStatus.type === status;
       });
     }
 
@@ -177,11 +168,6 @@ export default class ProposalsTransport extends BaseTransport {
     };
   }
 
-  async proposedBy (member: MemberId) {
-    const proposals = await this.proposals();
-    return proposals.filter(({ proposerId }) => member.eq(proposerId));
-  }
-
   async voteByProposalAndMember (proposalId: ProposalId, voterId: MemberId): Promise<VoteKind | null> {
     const vote = (await this.proposalsEngine.voteExistsByProposalByVoter(proposalId, voterId)) as VoteKind;
     const hasVoted = (await this.api.query.proposalsEngine.voteExistsByProposalByVoter.size(proposalId, voterId)).toNumber();
@@ -189,23 +175,18 @@ export default class ProposalsTransport extends BaseTransport {
   }
 
   async votes (proposalId: ProposalId): Promise<ProposalVotes> {
-    const voteEntries = await this.doubleMapEntries(
-      'proposalsEngine.voteExistsByProposalByVoter', // Double map of intrest
-      proposalId, // First double-map key value
-      (v) => new VoteKind(v), // Converter from hex
-      async () => (await this.membersT.nextMemberId()), // A function that returns the number of iterations to go through when chekcing possible values for the second double-map key (memberId)
-      FIRST_MEMBER_ID.toNumber() // Min. possible value for second double-map key (memberId)
+    const voteEntries = await this.entriesByIds<MemberId, VoteKind>(
+      this.api.query.proposalsEngine.voteExistsByProposalByVoter,
+      proposalId
     );
 
     const votesWithMembers: ProposalVote[] = [];
-    for (const voteEntry of voteEntries) {
-      const memberId = voteEntry.secondKey;
-      const vote = voteEntry.value;
+    for (const [memberId, vote] of voteEntries) {
       const parsedMember = (await this.membersT.expectedMembership(memberId)).toJSON() as ParsedMember;
       votesWithMembers.push({
         vote,
         member: {
-          memberId: new MemberId(memberId),
+          memberId,
           ...parsedMember
         }
       });
@@ -252,17 +233,15 @@ export default class ProposalsTransport extends BaseTransport {
       return null;
     }
     const thread = (await this.proposalsDiscussion.threadById(threadId)) as DiscussionThread;
-    const postEntries = await this.doubleMapEntries(
-      'proposalsDiscussion.postThreadIdByPostId',
-      threadId,
-      (v) => new DiscussionPost(v),
-      async () => ((await this.proposalsDiscussion.postCount()) as u64).toNumber()
+    const postEntries = await this.entriesByIds<PostId, DiscussionPost>(
+      this.api.query.proposalsDiscussion.postThreadIdByPostId,
+      threadId
     );
 
     const parsedPosts: ParsedPost[] = [];
-    for (const { secondKey: postId, value: post } of postEntries) {
+    for (const [postId, post] of postEntries) {
       parsedPosts.push({
-        postId: new PostId(postId),
+        postId: postId,
         threadId: post.thread_id,
         text: bytesToString(post.text),
         createdAt: await this.chainT.blockTimestamp(post.created_at.toNumber()),

+ 0 - 0
pioneer/packages/joy-utils-old/src/transport/validators.ts → pioneer/packages/joy-utils/src/transport/validators.ts


+ 42 - 62
pioneer/packages/joy-utils-old/src/transport/workingGroups.ts → pioneer/packages/joy-utils/src/transport/workingGroups.ts

@@ -3,16 +3,14 @@ import { Balance } from '@polkadot/types/interfaces';
 import BaseTransport from './base';
 import { ApiPromise } from '@polkadot/api';
 import MembersTransport from './members';
-import { SingleLinkedMapEntry } from '../index';
 import { Worker, WorkerId, Opening as WGOpening, Application as WGApplication, OpeningTypeKey } from '@joystream/types/working-group';
 import { apiModuleByGroup } from '../consts/workingGroups';
 import { WorkingGroupKey } from '@joystream/types/common';
 import { WorkerData, OpeningData, ParsedApplication } from '../types/workingGroups';
 import { OpeningId, ApplicationId, Opening, Application, ActiveOpeningStageKey } from '@joystream/types/hiring';
-import { MultipleLinkedMapEntry } from '../LinkedMapEntry';
 import { Stake, StakeId } from '@joystream/types/stake';
-import { RewardRelationshipId, RewardRelationship } from '@joystream/types/recurring-rewards';
-import { APIQueryCache } from '../APIQueryCache';
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
+import { APIQueryCache } from './APIQueryCache';
 
 export default class WorkingGroupsTransport extends BaseTransport {
   private membersT: MembersTransport;
@@ -22,19 +20,20 @@ export default class WorkingGroupsTransport extends BaseTransport {
     this.membersT = membersTransport;
   }
 
+  protected apiQueryByGroup(group: WorkingGroupKey) {
+    const module = apiModuleByGroup[group];
+    return this.api.query[module];
+  }
+
   protected queryByGroup (group: WorkingGroupKey) {
     const module = apiModuleByGroup[group];
     return this.cacheApi.query[module];
   }
 
   public async groupMemberById (group: WorkingGroupKey, workerId: number): Promise<WorkerData | null> {
-    const workerLink = new SingleLinkedMapEntry(
-      Worker,
-      await this.queryByGroup(group).workerById(workerId)
-    );
-    const worker = workerLink.value;
+    const worker = await this.queryByGroup(group).workerById(workerId) as Worker;
 
-    if (!worker.is_active) {
+    if (worker.isEmpty) {
       return null;
     }
 
@@ -43,7 +42,7 @@ export default class WorkingGroupsTransport extends BaseTransport {
       : undefined;
 
     const reward = worker.reward_relationship.isSome
-      ? (await this.rewardRelationship(worker.reward_relationship.unwrap()))
+      ? (await this.recurringRewards.rewardRelationships(worker.reward_relationship.unwrap()) as RewardRelationship)
       : undefined;
 
     const profile = await this.membersT.expectedMembership(worker.member_id);
@@ -64,21 +63,18 @@ export default class WorkingGroupsTransport extends BaseTransport {
   }
 
   public async allOpenings (group: WorkingGroupKey, type?: OpeningTypeKey): Promise<OpeningData[]> {
-    const nextId = (await this.queryByGroup(group).nextOpeningId()) as OpeningId;
-
-    if (nextId.eq(0)) {
-      return [];
-    }
-
-    const query = this.queryByGroup(group).openingById();
-    const result = new MultipleLinkedMapEntry(OpeningId, WGOpening, await query);
+    const wgOpeningEntries = await this.entriesByIds<OpeningId, WGOpening>(this.apiQueryByGroup(group).openingById);
+    const hiringOpenings = await Promise.all(
+      wgOpeningEntries.map(
+        ([wgOpeningId, wgOpening]) => this.hiring.openingById(wgOpening.hiring_opening_id)
+      )
+    ) as Opening[];
+
+    return hiringOpenings
+      .map((hiringOpening, index) => {
+        const id = wgOpeningEntries[index][0];
+        const opening = wgOpeningEntries[index][1];
 
-    const { linked_keys: openingIds, linked_values: openings } = result;
-    return (await Promise.all(openings.map(opening => this.hiring.openingById(opening.hiring_opening_id))))
-      .map((hiringOpeningRes, index) => {
-        const id = openingIds[index];
-        const opening = openings[index];
-        const hiringOpening = (new SingleLinkedMapEntry(Opening, hiringOpeningRes)).value;
         return { id, opening, hiringOpening };
       })
       .filter(openingData => !type || openingData.opening.opening_type.isOfType(type));
@@ -93,42 +89,24 @@ export default class WorkingGroupsTransport extends BaseTransport {
   }
 
   async wgApplicationById (group: WorkingGroupKey, wgApplicationId: number | ApplicationId): Promise<WGApplication> {
-    const nextAppId = await this.queryByGroup(group).nextApplicationId() as ApplicationId;
+    const wgApplication = (await this.queryByGroup(group).applicationById(wgApplicationId)) as WGApplication;
 
-    if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
-      throw new Error(`Invalid working group application ID (${wgApplicationId})!`);
+    if (wgApplication.isEmpty) {
+      throw new Error(`Working group application not found (ID: ${wgApplicationId})!`);
     }
 
-    return new SingleLinkedMapEntry(
-      WGApplication,
-      await this.queryByGroup(group).applicationById(wgApplicationId)
-    ).value;
-  }
-
-  protected async hiringApplicationById (id: number | ApplicationId): Promise<Application> {
-    return new SingleLinkedMapEntry(
-      Application,
-      await this.hiring.applicationById(id)
-    ).value;
+    return wgApplication;
   }
 
   protected async stakeValue (stakeId: StakeId): Promise<Balance> {
-    return new SingleLinkedMapEntry(
-      Stake,
-      await this.stake.stakes(stakeId)
-    ).value.value;
-  }
+    const stake = await this.stake.stakes(stakeId) as Stake;
 
-  protected async rewardRelationship (relationshipId: RewardRelationshipId): Promise<RewardRelationship> {
-    return new SingleLinkedMapEntry(
-      RewardRelationship,
-      await this.recurringRewards.rewardRelationships(relationshipId)
-    ).value;
+    return stake.value;
   }
 
   protected async parseApplication (wgApplicationId: number, wgApplication: WGApplication): Promise<ParsedApplication> {
     const appId = wgApplication.application_id;
-    const application = await this.hiringApplicationById(appId);
+    const application = await this.hiring.applicationById(appId) as Application;
 
     const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application;
 
@@ -152,18 +130,20 @@ export default class WorkingGroupsTransport extends BaseTransport {
   }
 
   async openingApplications (group: WorkingGroupKey, wgOpeningId: number): Promise<ParsedApplication[]> {
-    const applications: ParsedApplication[] = [];
-
-    const nextAppId = await this.queryByGroup(group).nextApplicationId() as ApplicationId;
-    for (let i = 0; i < nextAppId.toNumber(); i++) {
-      const wgApplication = await this.wgApplicationById(group, i);
-      if (wgApplication.opening_id.toNumber() !== wgOpeningId) {
-        continue;
-      }
-      applications.push(await this.parseApplication(i, wgApplication));
-    }
-
-    return applications;
+    const wgApplicationsEntries =
+      await this.entriesByIds<ApplicationId, WGApplication>(this.apiQueryByGroup(group).applicationById);
+
+    return Promise.all(
+      wgApplicationsEntries
+        .filter(
+          ([wgApplicationById, wgApplication]) => wgApplication.opening_id.eq(wgOpeningId)
+        )
+        .map(
+          ([wgApplicationId, wgApplication]) => (
+            this.parseApplication(wgApplicationId.toNumber(), wgApplication)
+          )
+        )
+    );
   }
 
   async openingActiveApplications (group: WorkingGroupKey, wgOpeningId: number): Promise<ParsedApplication[]> {

+ 0 - 0
pioneer/packages/joy-utils-old/src/types/common.ts → pioneer/packages/joy-utils/src/types/common.ts


+ 14 - 1
pioneer/packages/joy-utils-old/src/types/proposals.ts → pioneer/packages/joy-utils/src/types/proposals.ts

@@ -2,6 +2,7 @@ import { ProposalId, VoteKind } from '@joystream/types/proposals';
 import { MemberId, Membership } from '@joystream/types/members';
 import { ThreadId, PostId } from '@joystream/types/common';
 import { ParsedMember } from './members';
+import { ProposalDetails } from '@joystream/types/src/proposals';
 
 export const ProposalTypes = [
   'Text',
@@ -28,6 +29,18 @@ export type ProposalType = typeof ProposalTypes[number];
 export const proposalStatusFilters = ['All', 'Active', 'Canceled', 'Approved', 'Rejected', 'Slashed', 'Expired'] as const;
 export type ProposalStatusFilter = typeof proposalStatusFilters[number];
 
+// Overriden for better optimalization
+export type RuntimeUpgradeProposalDetails = [
+  string, // hash as hex
+  number // file size in bytes
+]
+
+export type ParsedProposalDetails = ProposalDetails | RuntimeUpgradeProposalDetails;
+
+export type SpecificProposalDetails<T extends keyof ProposalDetails['typeDefinitions']> =
+  T extends 'RuntimeUpgrade' ? RuntimeUpgradeProposalDetails :
+  InstanceType<ProposalDetails['typeDefinitions'][Exclude<T, 'RuntimeUpgrade'>]>;
+
 export type ParsedProposal = {
   id: ProposalId;
   type: ProposalType;
@@ -38,7 +51,7 @@ export type ParsedProposal = {
   proposerId: number;
   createdAtBlock: number;
   createdAt: Date;
-  details: any[];
+  details: ParsedProposalDetails;
   votingResults: any;
   parameters: {
     approvalQuorumPercentage: number;

+ 0 - 0
pioneer/packages/joy-utils-old/src/types/storageProviders.ts → pioneer/packages/joy-utils/src/types/storageProviders.ts


+ 0 - 0
pioneer/packages/joy-utils-old/src/types/workingGroups.ts → pioneer/packages/joy-utils/src/types/workingGroups.ts


+ 0 - 17
pioneer/packages/old-apps/apps-routing/src/joy-proposals.ts

@@ -1,17 +0,0 @@
-import { Routes } from './types';
-
-import Proposals from '@polkadot/joy-proposals/';
-
-export default [
-  {
-    Component: Proposals,
-    display: {
-      needsApi: ['query.proposalsEngine.proposalCount']
-    },
-    i18n: {
-      defaultValue: 'Proposals'
-    },
-    icon: 'tasks',
-    name: 'proposals'
-  }
-] as Routes;

+ 2 - 3
pioneer/tsconfig.json

@@ -7,7 +7,6 @@
     "packages/joy-forum/**/*",
     "packages/joy-help/**/*",
     "packages/joy-media/**/*",
-    "packages/joy-proposals/**/*",
     "packages/joy-roles/**/*",
     "packages/joy-settings/**/*",
     "packages/joy-utils-old/**/*"
@@ -34,8 +33,8 @@
       "@polkadot/joy-members/*": [ "packages/joy-members/src/*" ],
       "@polkadot/joy-pages/": [ "packages/joy-pages/src/" ],
       "@polkadot/joy-pages/*": [ "packages/joy-pages/src/*" ],
-      // "@polkadot/joy-proposals/": [ "packages/joy-proposals/src/" ],
-      // "@polkadot/joy-proposals/*": [ "packages/joy-proposals/src/*" ],
+      "@polkadot/joy-proposals/": [ "packages/joy-proposals/src/" ],
+      "@polkadot/joy-proposals/*": [ "packages/joy-proposals/src/*" ],
       // "@polkadot/joy-roles/": [ "packages/joy-roles/src/" ],
       // "@polkadot/joy-roles/*": [ "packages/joy-roles/src/*" ],
       // "@polkadot/joy-settings/": [ "packages/joy-settings/src/" ],

+ 10 - 0
types/src/index.ts

@@ -15,6 +15,8 @@ import workingGroup from './working-group'
 import discovery from './discovery'
 import media from './media'
 import proposals from './proposals'
+import { InterfaceTypes } from '@polkadot/types/types/registry'
+import { createType, TypeRegistry } from '@polkadot/types'
 
 export {
   common,
@@ -58,3 +60,11 @@ export const types: RegistryTypes = {
   Address: 'AccountId',
   LookupSource: 'AccountId',
 }
+
+// Allows creating types without api instance (it's not a recommended way though, so should be used just for mocks)
+const mockRegistry = new TypeRegistry();
+mockRegistry.register(types);
+
+export function createMock<TypeName extends keyof InterfaceTypes>(type: TypeName, value: any): InterfaceTypes[TypeName] {
+  return createType(mockRegistry, type, value);
+}