浏览代码

Merge pull request #945 from Lezek123/nicaea-proposals-final

Pioneer: Remaining Nicaea proposals
Mokhtar Naamani 4 年之前
父节点
当前提交
a14ad84adb
共有 46 个文件被更改,包括 2008 次插入814 次删除
  1. 206 85
      pioneer/packages/joy-proposals/src/Proposal/Body.tsx
  2. 2 12
      pioneer/packages/joy-proposals/src/Proposal/ChooseProposalType.tsx
  3. 5 3
      pioneer/packages/joy-proposals/src/Proposal/Details.tsx
  4. 1 2
      pioneer/packages/joy-proposals/src/Proposal/ProposalTypePreview.tsx
  5. 10 5
      pioneer/packages/joy-proposals/src/forms/AddWorkingGroupOpeningForm.tsx
  6. 118 0
      pioneer/packages/joy-proposals/src/forms/BeginReviewLeaderApplicationsForm.tsx
  7. 102 0
      pioneer/packages/joy-proposals/src/forms/DecreaseWorkingGroupLeadStakeForm.tsx
  8. 0 88
      pioneer/packages/joy-proposals/src/forms/EvictStorageProviderForm.tsx
  9. 232 0
      pioneer/packages/joy-proposals/src/forms/FillWorkingGroupLeaderOpeningForm.tsx
  10. 82 11
      pioneer/packages/joy-proposals/src/forms/FormFields.tsx
  11. 5 6
      pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx
  12. 47 14
      pioneer/packages/joy-proposals/src/forms/GenericWorkingGroupProposalForm.tsx
  13. 2 2
      pioneer/packages/joy-proposals/src/forms/MintCapacityForm.tsx
  14. 2 2
      pioneer/packages/joy-proposals/src/forms/RuntimeUpgradeForm.tsx
  15. 4 3
      pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx
  16. 2 9
      pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx
  17. 3 3
      pioneer/packages/joy-proposals/src/forms/SetMaxValidatorCountForm.tsx
  18. 0 256
      pioneer/packages/joy-proposals/src/forms/SetStorageRoleParamsForm.tsx
  19. 93 0
      pioneer/packages/joy-proposals/src/forms/SetWorkingGroupLeadRewardForm.tsx
  20. 86 0
      pioneer/packages/joy-proposals/src/forms/SetWorkingGroupMintCapacityForm.tsx
  21. 2 2
      pioneer/packages/joy-proposals/src/forms/SignalForm.tsx
  22. 101 0
      pioneer/packages/joy-proposals/src/forms/SlashWorkingGroupLeadStakeForm.tsx
  23. 2 3
      pioneer/packages/joy-proposals/src/forms/SpendingProposalForm.tsx
  24. 125 0
      pioneer/packages/joy-proposals/src/forms/TerminateWorkingGroupLeaderForm.tsx
  25. 4 4
      pioneer/packages/joy-proposals/src/forms/errorHandling.ts
  26. 7 2
      pioneer/packages/joy-proposals/src/forms/index.ts
  27. 25 5
      pioneer/packages/joy-proposals/src/index.tsx
  28. 0 6
      pioneer/packages/joy-proposals/src/stories/ProposalForms.stories.tsx
  29. 3 3
      pioneer/packages/joy-proposals/src/stories/data/ProposalTypesInfo.mock.ts
  30. 163 185
      pioneer/packages/joy-proposals/src/validationSchema.ts
  31. 19 20
      pioneer/packages/joy-roles/src/classifiers.ts
  32. 18 8
      pioneer/packages/joy-utils/src/MemberProfilePreview.tsx
  33. 99 14
      pioneer/packages/joy-utils/src/consts/proposals.ts
  34. 2 2
      pioneer/packages/joy-utils/src/consts/workingGroups.ts
  35. 22 0
      pioneer/packages/joy-utils/src/functions/format.ts
  36. 102 0
      pioneer/packages/joy-utils/src/react/components/working-groups/ApplicationDetails.tsx
  37. 56 0
      pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx
  38. 22 3
      pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx
  39. 12 0
      pioneer/packages/joy-utils/src/transport/base.ts
  40. 8 9
      pioneer/packages/joy-utils/src/transport/proposals.ts
  41. 140 15
      pioneer/packages/joy-utils/src/transport/workingGroups.ts
  42. 10 2
      pioneer/packages/joy-utils/src/types/proposals.ts
  43. 29 2
      pioneer/packages/joy-utils/src/types/workingGroups.ts
  44. 8 1
      types/src/common.ts
  45. 14 25
      types/src/hiring/index.ts
  46. 13 2
      types/src/working-group/index.ts

+ 206 - 85
pioneer/packages/joy-proposals/src/Proposal/Body.tsx

@@ -1,4 +1,5 @@
 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 { bytesToString } from '@polkadot/joy-utils/functions/misc';
@@ -6,20 +7,25 @@ import { blake2AsHex } from '@polkadot/util-crypto';
 import styled from 'styled-components';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 import TxButton from '@polkadot/joy-utils/TxButton';
-import { ProposalId } from '@joystream/types/proposals';
+import { ProposalId, TerminateRoleParameters } from '@joystream/types/proposals';
 import { MemberId, Profile } from '@joystream/types/members';
 import ProfilePreview from '@polkadot/joy-utils/MemberProfilePreview';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import { Option, Bytes } from '@polkadot/types/';
+import { BlockNumber } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 import ReactMarkdown from 'react-markdown';
-import { WorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
+import { WorkingGroupOpeningPolicyCommitment, RewardPolicy } from '@joystream/types/working-group';
 import {
   ActivateOpeningAt,
-  ActivateOpeningAtKeys
+  ActivateOpeningAtKeys,
+  StakingPolicy
 } from '@joystream/types/hiring';
-import { WorkingGroup } from '@joystream/types/common';
+import { WorkingGroup, WorkingGroupKey } 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';
 
 type BodyProps = {
   title: string;
@@ -81,91 +87,202 @@ const ParsedHRT = styled.pre`
   white-space: pre-wrap;
 `;
 
+type ParsedParamValue = string | number | JSX.Element;
+
+class ParsedParam {
+  name: string;
+  value: ParsedParamValue;
+  fullWidth: boolean;
+
+  constructor (name: string, value: ParsedParamValue, fullWidth = false) {
+    this.name = name;
+    this.value = value;
+    this.fullWidth = fullWidth;
+  }
+}
+
 // The methods for parsing params by Proposal type.
-// They take the params as array and return { LABEL: VALUE } object.
-const paramParsers: { [x in ProposalType]: (params: any[]) => { [key: string]: string | number | JSX.Element } } = {
-  Text: ([content]) => ({
-    Content: <ReactMarkdown className='TextProposalContent' source={content} linkTarget='_blank' />
-  }),
+const paramParsers: { [x in ProposalType]: (params: any[]) => ParsedParam[]} = {
+  Text: ([content]) => [
+    new ParsedParam(
+      'Content',
+      <ReactMarkdown className='TextProposalContent' source={content} linkTarget='_blank' />,
+      true
+    )
+  ],
   RuntimeUpgrade: ([wasm]) => {
     const buffer: Buffer = Buffer.from(wasm.replace('0x', ''), 'hex');
-    return {
-      'Blake2b256 hash of WASM code': blake2AsHex(buffer, 256),
-      'File size': buffer.length + ' bytes'
-    };
+    return [
+      new ParsedParam('Blake2b256 hash of WASM code', blake2AsHex(buffer, 256), true),
+      new ParsedParam('File size', buffer.length + ' bytes')
+    ];
   },
-  SetElectionParameters: ([params]) => ({
-    'Announcing period': params.announcing_period + ' blocks',
-    'Voting period': params.voting_period + ' blocks',
-    'Revealing period': params.revealing_period + ' blocks',
-    'Council size': params.council_size + ' members',
-    'Candidacy limit': params.candidacy_limit + ' members',
-    'New term duration': params.new_term_duration + ' blocks',
-    'Min. council stake': formatBalance(params.min_council_stake),
-    'Min. voting stake': formatBalance(params.min_voting_stake)
-  }),
-  Spending: ([amount, account]) => ({
-    Amount: formatBalance(amount),
-    Account: <ProposedAddress address={account} />
-  }),
-  SetLead: ([memberId, accountId]) => ({
-    Member: <ProposedMember memberId={ memberId } />,
-    'Account id': <ProposedAddress address={accountId} />
-  }),
-  SetContentWorkingGroupMintCapacity: ([capacity]) => ({
-    'Mint capacity': formatBalance(capacity)
-  }),
-  EvictStorageProvider: ([accountId]) => ({
-    'Storage provider account': <ProposedAddress address={accountId} />
-  }),
-  SetValidatorCount: ([count]) => ({
-    'Validator count': count
-  }),
-  SetStorageRoleParameters: ([params]) => ({
-    'Min. stake': formatBalance(params.min_stake),
+  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'),
+    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} />)
+  ],
+  SetLead: ([memberId, accountId]) => [
+    new ParsedParam('Member', <ProposedMember memberId={ memberId } />),
+    new ParsedParam('Account id', <ProposedAddress address={accountId} />)
+  ],
+  SetContentWorkingGroupMintCapacity: ([capacity]) => [
+    new ParsedParam('Mint capacity', formatBalance(capacity))
+  ],
+  EvictStorageProvider: ([accountId]) => [
+    new ParsedParam('Storage provider account', <ProposedAddress address={accountId} />)
+  ],
+  SetValidatorCount: ([count]) => [
+    new ParsedParam('Validator count', count)
+  ],
+  SetStorageRoleParameters: ([params]) => [
+    new ParsedParam('Min. stake', formatBalance(params.min_stake)),
     // "Min. actors": params.min_actors,
-    'Max. actors': params.max_actors,
-    Reward: formatBalance(params.reward),
-    'Reward period': params.reward_period + ' blocks',
+    new ParsedParam('Max. actors', params.max_actors),
+    new ParsedParam('Reward', formatBalance(params.reward)),
+    new ParsedParam('Reward period', params.reward_period + ' blocks'),
     // "Bonding period": params.bonding_period + " blocks",
-    'Unbonding period': params.unbonding_period + ' blocks',
+    new ParsedParam('Unbonding period', params.unbonding_period + ' blocks'),
     // "Min. service period": params.min_service_period + " blocks",
     // "Startup grace period": params.startup_grace_period + " blocks",
-    'Entry request fee': formatBalance(params.entry_request_fee)
-  }),
+    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);
     const {
-      application_staking_policy: aSP,
-      role_staking_policy: rSP,
+      application_staking_policy: applicationSP,
+      role_staking_policy: roleSP,
       application_rationing_policy: rationingPolicy
     } = OPCommitment;
     let HRT = bytesToString(new Bytes(human_readable_text));
     try { HRT = JSON.stringify(JSON.parse(HRT), undefined, 4); } catch (e) { /* Do nothing */ }
-    return {
-      'Working group': workingGroup.type,
-      'Activate at': `${activateAt.type}${activateAtBlock ? `(${activateAtBlock.toString()})` : ''}`,
-      'Application stake': aSP.isSome ? aSP.unwrap().amount_mode.type + `(${aSP.unwrap().amount})` : 'NONE',
-      'Role stake': rSP.isSome ? rSP.unwrap().amount_mode.type + `(${rSP.unwrap().amount})` : 'NONE',
-      'Max. applications': rationingPolicy.isSome ? rationingPolicy.unwrap().max_active_applicants.toNumber() : 'UNLIMITED',
-      'Terminate unstaking period (role stake)': OPCommitment.terminate_role_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Exit unstaking period (role stake)': OPCommitment.exit_role_stake_unstaking_period.unwrapOr(0) + ' blocks',
+    const formatStake = (stake: Option<StakingPolicy>) => (
+      stake.isSome ? stake.unwrap().amount_mode.type + `(${stake.unwrap().amount})` : 'NONE'
+    );
+    const formatPeriod = (unstakingPeriod: Option<BlockNumber>) => (
+      unstakingPeriod.unwrapOr(0) + ' blocks'
+    );
+    return [
+      new ParsedParam('Working group', workingGroup.type),
+      new ParsedParam('Activate at', `${activateAt.type}${activateAtBlock ? `(${activateAtBlock.toString()})` : ''}`),
+      new ParsedParam('Application stake', formatStake(applicationSP)),
+      new ParsedParam('Role stake', formatStake(roleSP)),
+      new ParsedParam(
+        'Max. applications',
+        rationingPolicy.isSome ? rationingPolicy.unwrap().max_active_applicants.toNumber() : 'UNLIMITED'
+      ),
+      new ParsedParam(
+        'Terminate unstaking period (role stake)',
+        formatPeriod(OPCommitment.terminate_role_stake_unstaking_period)
+      ),
+      new ParsedParam(
+        'Exit unstaking period (role stake)',
+        formatPeriod(OPCommitment.exit_role_stake_unstaking_period)
+      ),
       // <required_to_prevent_sneaking>
-      'Terminate unstaking period (appl. stake)': OPCommitment.terminate_application_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Exit unstaking period (appl. stake)': OPCommitment.exit_role_application_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Appl. accepted unstaking period (appl. stake)': OPCommitment.fill_opening_successful_applicant_application_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Appl. failed unstaking period (role stake)': OPCommitment.fill_opening_failed_applicant_role_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Appl. failed unstaking period (appl. stake)': OPCommitment.fill_opening_failed_applicant_application_stake_unstaking_period.unwrapOr(0) + ' blocks',
-      'Crowded out unstaking period (role stake)': ((rSP.isSome && rSP.unwrap().crowded_out_unstaking_period_length.unwrapOr(0)) || 0) + ' blocks',
-      'Review period expierd unstaking period (role stake)': ((rSP.isSome && rSP.unwrap().review_period_expired_unstaking_period_length.unwrapOr(0)) || 0) + ' blocks',
-      'Crowded out unstaking period (appl. stake)': ((aSP.isSome && aSP.unwrap().crowded_out_unstaking_period_length.unwrapOr(0)) || 0) + ' blocks',
-      'Review period expierd unstaking period (appl. stake)': ((aSP.isSome && aSP.unwrap().review_period_expired_unstaking_period_length.unwrapOr(0)) || 0) + ' blocks',
+      new ParsedParam(
+        'Terminate unstaking period (appl. stake)',
+        formatPeriod(OPCommitment.terminate_application_stake_unstaking_period)
+      ),
+      new ParsedParam(
+        'Exit unstaking period (appl. stake)',
+        formatPeriod(OPCommitment.exit_role_application_stake_unstaking_period)
+      ),
+      new ParsedParam(
+        'Appl. accepted unstaking period (appl. stake)',
+        formatPeriod(OPCommitment.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)
+      ),
+      new ParsedParam(
+        'Appl. failed unstaking period (appl. stake)',
+        formatPeriod(OPCommitment.fill_opening_failed_applicant_application_stake_unstaking_period)
+      ),
+      new ParsedParam(
+        'Crowded out unstaking period (role stake)',
+        roleSP.isSome ? formatPeriod(roleSP.unwrap().crowded_out_unstaking_period_length) : '0 blocks'
+      ),
+      new ParsedParam(
+        'Review period expierd unstaking period (role stake)',
+        roleSP.isSome ? formatPeriod(roleSP.unwrap().review_period_expired_unstaking_period_length) : '0 blocks'
+      ),
+      new ParsedParam(
+        'Crowded out unstaking period (appl. stake)',
+        applicationSP.isSome ? formatPeriod(applicationSP.unwrap().crowded_out_unstaking_period_length) : '0 blocks'
+      ),
+      new ParsedParam(
+        'Review period expierd unstaking period (appl. stake)',
+        applicationSP.isSome ? formatPeriod(applicationSP.unwrap().review_period_expired_unstaking_period_length) : '0 blocks'
+      ),
       // </required_to_prevent_sneaking>
-      'Human readable text': <ParsedHRT>{ HRT }</ParsedHRT>
-    };
+      new ParsedParam('Human readable text', <ParsedHRT>{ HRT }</ParsedHRT>, true)
+    ];
+  },
+  SetWorkingGroupMintCapacity: ([capacity, group]) => [
+    new ParsedParam('Working group', (new WorkingGroup(group)).type),
+    new ParsedParam('Mint capacity', formatBalance(capacity))
+  ],
+  BeginReviewWorkingGroupLeaderApplication: ([id, group]) => [
+    new ParsedParam('Working group', (new WorkingGroup(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/${id}`}>#{id}</Link>)
+  ],
+  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)
+  ],
+  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)
+  ],
+  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)
+  ],
+  TerminateWorkingGroupLeaderRole: ([params]) => {
+    const paramsObj = new TerminateRoleParameters(params);
+    const { working_group: workingGroup, rationale, worker_id: leadId, slash } = paramsObj;
+    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)
+    ];
   }
 };
 
@@ -173,14 +290,14 @@ const StyledProposalDescription = styled(Card.Description)`
   font-size: 1.15rem;
 `;
 const ProposalParams = styled.div`
-  display: grid;
-  font-weight: bold;
-  grid-template-columns: min-content 1fr;
-  grid-row-gap: 0.5rem;
   border: 1px solid rgba(0,0,0,.2);
-  padding: 1.5rem 1.5rem 1rem 1.25rem;
+  padding: 1.5rem 2rem 1rem 2rem;
   position: relative;
   margin-top: 1.7rem;
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-column-gap: 1rem;
+  grid-row-gap: 0.5rem;
   @media screen and (max-width: 767px) {
     grid-template-columns: 1fr;
   }
@@ -194,21 +311,25 @@ const ParamsHeader = styled.h4`
   padding: 0.3rem;
   left: 0.5rem;
 `;
+type ProposalParamProps = { fullWidth?: boolean };
+const ProposalParam = ({ fullWidth, children }: React.PropsWithChildren<ProposalParamProps>) => (
+  <div style={{ gridColumn: (fullWidth || undefined) && '1/3' }}>
+    { children }
+  </div>
+);
 const ProposalParamName = styled.div`
-  margin-right: 1rem;
-  white-space: nowrap;
+  font-size: 0.9rem;
+  font-weight: normal;
 `;
 const ProposalParamValue = styled.div`
   color: black;
   word-wrap: break-word;
   word-break: break-word;
   font-size: 1.15rem;
+  font-weight: bold;
   & .TextProposalContent {
     font-weight: normal;
   }
-  @media screen and (max-width: 767px) {
-    margin-top: -0.25rem;
-  }
 `;
 
 export default function Body ({
@@ -235,15 +356,15 @@ export default function Body ({
         </StyledProposalDescription>
         <ProposalParams>
           <ParamsHeader>Parameters:</ParamsHeader>
-          { Object.entries(parsedParams).map(([paramName, paramValue]) => (
-            <React.Fragment key={paramName}>
-              <ProposalParamName>{paramName}:</ProposalParamName>
-              <ProposalParamValue>{paramValue}</ProposalParamValue>
-            </React.Fragment>
+          { parsedParams.map(({ name, value, fullWidth }) => (
+            <ProposalParam key={name} fullWidth={fullWidth}>
+              <ProposalParamName>{name}:</ProposalParamName>
+              <ProposalParamValue>{value}</ProposalParamValue>
+            </ProposalParam>
           ))}
         </ProposalParams>
         { iAmProposer && isCancellable && (<>
-          <Message warning active>
+          <Message warning visible>
             <Message.Content>
               <Message.Header>Proposal cancellation</Message.Header>
               <p style={{ margin: '0.5em 0', padding: '0' }}>

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

@@ -3,27 +3,17 @@ import ProposalTypePreview from './ProposalTypePreview';
 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 './ChooseProposalType.css';
 import { RouteComponentProps } from 'react-router-dom';
 
-export const Categories = {
-  storage: 'Storage',
-  council: 'Council',
-  validators: 'Validators',
-  cwg: 'Content Working Group',
-  other: 'Other'
-} as const;
-
-export type Category = typeof Categories[keyof typeof Categories];
-
 export default function ChooseProposalType (props: RouteComponentProps) {
   const transport = useTransport();
 
   const [proposalTypes, error, loading] = usePromise(() => transport.proposals.proposalsTypesParameters(), []);
   const [category, setCategory] = useState('');
 
-  console.log({ proposalTypes, loading, error });
   return (
     <div className="ChooseProposalType">
       <PromiseComponent error={error} loading={loading} message={'Fetching proposals\' parameters...'}>
@@ -39,7 +29,7 @@ export default function ChooseProposalType (props: RouteComponentProps) {
         </div>
         <Item.Group>
           {proposalTypes
-            .filter(typeInfo => !category || typeInfo.category === category)
+            .filter(typeInfo => (!category || typeInfo.category === category) && !typeInfo.outdated)
             .map((typeInfo, idx) => (
               <ProposalTypePreview key={`${typeInfo} - ${idx}`} typeInfo={typeInfo} history={props.history} />
             ))}

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

@@ -1,6 +1,7 @@
 import React from 'react';
-import { Item, Header } from 'semantic-ui-react';
+import { Item, Header, Label } from 'semantic-ui-react';
 import { ParsedProposal } from '@polkadot/joy-utils/types/proposals';
+import { metadata as proposalConsts } from '@polkadot/joy-utils/consts/proposals';
 import { ExtendedProposalStatus } from './ProposalDetails';
 import styled from 'styled-components';
 
@@ -71,7 +72,6 @@ type DetailsProps = {
 export default function Details ({ proposal, extendedStatus, proposerLink = false }: DetailsProps) {
   const { type, createdAt, createdAtBlock, proposer } = proposal;
   const { displayStatus, periodStatus, expiresIn, finalizedAtBlock, executedAtBlock, executionFailReason } = extendedStatus;
-  console.log(proposal);
   return (
     <DetailsContainer>
       <Detail name="Proposed By">
@@ -83,7 +83,9 @@ export default function Details ({ proposal, extendedStatus, proposerLink = fals
         />
         <Item.Extra>{ `${createdAt.toLocaleString()}` }</Item.Extra>
       </Detail>
-      <Detail name="Proposal type" value={type} />
+      <Detail name="Proposal type" value={type}>
+        <Item.Extra>{ proposalConsts[type].outdated && <Label size="small">Outdated proposal type</Label> }</Item.Extra>
+      </Detail>
       <Detail name="Stage" value={displayStatus}>
         <Item.Extra>
           { createdAtBlock && <BlockInfo>Created at block <b>#{ createdAtBlock }</b></BlockInfo> }

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

@@ -3,8 +3,7 @@ import React from 'react';
 import { History } from 'history';
 import { Item, Icon, Button, Label } from 'semantic-ui-react';
 
-import { Category } from './ChooseProposalType';
-import { ProposalType } from '@polkadot/joy-utils/types/proposals';
+import { ProposalType, Category } from '@polkadot/joy-utils/types/proposals';
 import _ from 'lodash';
 import styled from 'styled-components';
 import useVoteStyles from './useVoteStyles';

+ 10 - 5
pioneer/packages/joy-proposals/src/forms/AddWorkingGroupOpeningForm.tsx

@@ -23,13 +23,13 @@ import { formatBalance } from '@polkadot/util';
 import _ from 'lodash';
 import { IWorkingGroupOpeningPolicyCommitment } from '@joystream/types/working-group';
 import { IAddOpeningParameters } from '@joystream/types/proposals';
-import { WorkingGroupKeys } from '@joystream/types/common';
+import { WorkingGroupKey, InputValidationLengthConstraint } from '@joystream/types/common';
 import { BlockNumber } from '@polkadot/types/interfaces';
 import { withCalls } from '@polkadot/react-api';
 import { SimplifiedTypeInterface } from '@polkadot/joy-utils/types/common';
 import Validation from '../validationSchema';
 
-type FormValues = WGFormValues & {
+export type FormValues = WGFormValues & {
   activateAt: ActivateOpeningAtKey;
   activateAtBlock: string;
   maxReviewPeriodLength: string;
@@ -64,7 +64,7 @@ const defaultValues: FormValues = {
   humanReadableText: ''
 };
 
-const HRTDefault: (memberHandle: string, group: WorkingGroupKeys) => GenericJoyStreamRoleSchema =
+const HRTDefault: (memberHandle: string, group: WorkingGroupKey) => GenericJoyStreamRoleSchema =
   (memberHandle, group) => ({
     version: 1,
     headline: `Looking for ${group} Working Group Leader!`,
@@ -101,6 +101,7 @@ type FormAdditionalProps = {}; // Aditional props coming all the way from export
 type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
 type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
   currentBlock?: BlockNumber;
+  HRTConstraint?: InputValidationLengthConstraint;
 };
 type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
@@ -344,14 +345,18 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: (props: FormContainerProps) => Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    ...Validation.AddWorkingGroupLeaderOpening(props.currentBlock?.toNumber() || 0)
+    ...Validation.AddWorkingGroupLeaderOpening(
+      props.currentBlock?.toNumber() || 0,
+      props.HRTConstraint || InputValidationLengthConstraint.createWithMaxAllowed()
+    )
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'AddWorkingGroupOpeningForm'
 })(AddWorkingGroupOpeningForm);
 
 export default withCalls<ExportComponentProps>(
-  ['derive.chain.bestNumber', { propName: 'currentBlock' }]
+  ['derive.chain.bestNumber', { propName: 'currentBlock' }],
+  ['query.storageWorkingGroup.openingHumanReadableText', { propName: 'HRTConstraint' }]
 )(
   withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer)
 );

+ 118 - 0
pioneer/packages/joy-proposals/src/forms/BeginReviewLeaderApplicationsForm.tsx

@@ -0,0 +1,118 @@
+import React from 'react';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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 { getFormErrorLabelsProps } from './errorHandling';
+
+export type FormValues = WGFormValues & {
+  openingId: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  openingId: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const BeginReviewLeadeApplicationsForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, values, myMemberId, errors, touched } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const transport = useTransport();
+  const [openings, openingsError, openingsLoading] = usePromise(
+    () => transport.workingGroups.activeOpenings(values.workingGroup, 'AcceptingApplications', 'Leader'),
+    [] as OpeningData[],
+    [values.workingGroup]
+  );
+  const openingsOptions = openings
+    // Map to options
+    .map(od => {
+      const hrt = od.hiringOpening.parse_human_readable_text_with_fallback();
+      return {
+        text: `${od.id.toString()}: ${hrt.headline} (${hrt.job.title})`,
+        value: od.id.toString()
+      };
+    });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createBeginReviewWorkingGroupLeaderApplicationsProposal"
+      proposalType="BeginReviewWorkingGroupLeaderApplication"
+      disabled={!openingsOptions.length}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        values.openingId,
+        values.workingGroup
+      ]}
+    >
+      <PromiseComponent error={openingsError} loading={openingsLoading} message="Fetching openings...">
+        { !openingsOptions.length
+          ? (
+            <Message error visible>
+              <Message.Header>No openings available!</Message.Header>
+              <Message.Content>
+                This proposal cannot be created, because no leader openings in <i>Accepting Applications</i> stage are currently available
+                in {values.workingGroup} Working Group.
+              </Message.Content>
+            </Message>
+          )
+          : (
+            <FormField
+              label="Working Group Opening"
+              error={errorLabelsProps.openingId}
+              showErrorMsg>
+              <Dropdown
+                onChange={handleChange}
+                name={'openingId'}
+                selection
+                options={openingsOptions}
+                value={values.openingId}
+              />
+            </FormField>
+          )
+        }
+      </PromiseComponent>
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.BeginReviewWorkingGroupLeaderApplication()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'BeginReviewLeadeApplicationsForm'
+})(BeginReviewLeadeApplicationsForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -0,0 +1,102 @@
+import React, { useState, useEffect } from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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';
+import Validation from '../validationSchema';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
+
+export type FormValues = WGFormValues & {
+  amount: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  amount: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const DecreaseWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId, setFieldError } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<WorkerData | null>(null);
+
+  // Here we validate if stake <= current lead stake.
+  // Because it depends on selected working group,
+  // there's no easy way to do it using validationSchema
+  useEffect(() => {
+    if (lead && parseInt(values.amount) > (lead.stake || 0) && !errors.amount) {
+      setFieldError('amount', `The stake cannot exceed current leader's stake (${formatBalance(lead.stake)})`);
+    }
+  });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createDecreaseWorkingGroupLeaderStakeProposal"
+      proposalType="DecreaseWorkingGroupLeaderStake"
+      leadRequired={true}
+      leadStakeRequired={true}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.stake) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="Amount to decrease"
+              onChange={handleChange}
+              name="amount"
+              error={errorLabelsProps.amount}
+              value={values.amount}
+              unit={formatBalance.getDefaults().unit}
+            />
+          </Grid.Column>
+        </Grid>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.DecreaseWorkingGroupLeaderStake()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'DecreaseWorkingGroupLeadStakeForm'
+})(DecreaseWorkingGroupLeadStakeForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

+ 0 - 88
pioneer/packages/joy-proposals/src/forms/EvictStorageProviderForm.tsx

@@ -1,88 +0,0 @@
-import React from 'react';
-import { getFormErrorLabelsProps } from './errorHandling';
-import * as Yup from 'yup';
-import { Label, Loader } from 'semantic-ui-react';
-import {
-  GenericProposalForm,
-  GenericFormValues,
-  genericFormDefaultOptions,
-  genericFormDefaultValues,
-  withProposalFormData,
-  ProposalFormExportProps,
-  ProposalFormContainerProps,
-  ProposalFormInnerProps
-} from './GenericProposalForm';
-import Validation from '../validationSchema';
-import { FormField } from './FormFields';
-import { withFormContainer } from './FormContainer';
-import { InputAddress } from '@polkadot/react-components/index';
-import { accountIdsToOptions } from '@polkadot/joy-election/utils';
-import { AccountId } from '@polkadot/types/interfaces';
-import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
-import './forms.css';
-
-type FormValues = GenericFormValues & {
-  storageProvider: any;
-};
-
-const defaultValues: FormValues = {
-  ...genericFormDefaultValues,
-  storageProvider: ''
-};
-
-type FormAdditionalProps = {}; // Aditional props coming all the way from export comonent into the inner form.
-type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
-type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
-type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
-
-const EvictStorageProviderForm: React.FunctionComponent<FormInnerProps> = props => {
-  const { errors, touched, values, setFieldValue } = props;
-  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
-  const transport = useTransport();
-  const [storageProviders /* error */, , loading] = usePromise<AccountId[]>(() => transport.storageProviders.providers(), []);
-  const storageProvidersOptions = accountIdsToOptions(storageProviders);
-  return (
-    <GenericProposalForm
-      {...props}
-      txMethod="createEvictStorageProviderProposal"
-      proposalType="EvictStorageProvider"
-      submitParams={[props.myMemberId, values.title, values.rationale, '{STAKE}', values.storageProvider]}
-    >
-      {loading ? (
-        <>
-          <Loader active inline style={{ marginRight: '5px' }} /> Fetching storage providers...
-        </>
-      ) : (
-        <FormField
-          error={errorLabelsProps.storageProvider}
-          label="Storage provider"
-          help="The storage provider you propose to evict"
-        >
-          <InputAddress
-            onChange={address => setFieldValue('storageProvider', address)}
-            type="address"
-            placeholder="Select storage provider"
-            value={values.storageProvider}
-            options={storageProvidersOptions}
-          />
-          {errorLabelsProps.storageProvider && <Label {...errorLabelsProps.storageProvider} prompt />}
-        </FormField>
-      )}
-    </GenericProposalForm>
-  );
-};
-
-const FormContainer = withFormContainer<FormContainerProps, FormValues>({
-  mapPropsToValues: (props: FormContainerProps) => ({
-    ...defaultValues,
-    ...(props.initialData || {})
-  }),
-  validationSchema: Yup.object().shape({
-    ...genericFormDefaultOptions.validationSchema,
-    storageProvider: Validation.EvictStorageProvider.storageProvider
-  }),
-  handleSubmit: genericFormDefaultOptions.handleSubmit,
-  displayName: 'EvictStorageProvidersForm'
-})(EvictStorageProviderForm);
-
-export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

+ 232 - 0
pioneer/packages/joy-proposals/src/forms/FillWorkingGroupLeaderOpeningForm.tsx

@@ -0,0 +1,232 @@
+import React from 'react';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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 { 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';
+
+export type FormValues = WGFormValues & {
+  openingId: string;
+  successfulApplicant: string;
+  includeReward: boolean;
+  rewardAmount: string;
+  rewardNextBlock: string;
+  rewardRecurring: boolean;
+  rewardInterval: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  openingId: '',
+  successfulApplicant: '',
+  includeReward: true,
+  rewardAmount: '',
+  rewardNextBlock: '',
+  rewardRecurring: true,
+  rewardInterval: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
+  currentBlock?: BlockNumber;
+};
+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))(
+      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>
+  })
+);
+
+const FillWorkingGroupLeaderOpeningForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, setFieldValue, values, myMemberId, errors, touched } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const transport = useTransport();
+  const [openings, openingsError, openingsLoading] = usePromise<OpeningData[]>(
+    () => transport.workingGroups.activeOpenings(values.workingGroup, 'ReviewPeriod', 'Leader'),
+    [],
+    [values.workingGroup]
+  );
+  const openingsOptions: DropdownItemProps[] = openings
+    // Map to options
+    .map(od => {
+      const hrt = od.hiringOpening.parse_human_readable_text_with_fallback();
+      return {
+        text: `${od.id.toString()}: ${hrt.headline} (${hrt.job.title})`,
+        value: od.id.toString()
+      };
+    });
+  const [activeApplications, applError, applLoading] = usePromise<ParsedApplication[]>(
+    () => values.openingId !== ''
+      ? transport.workingGroups.openingActiveApplications(values.workingGroup, parseInt(values.openingId))
+      : new Promise((resolve, reject) => resolve([] as ParsedApplication[])),
+    [],
+    [values.workingGroup, values.openingId]
+  );
+  const applicationsOptions = activeApplications
+    .map(a => {
+      return {
+        text: `${a.wgApplicationId}: ${a.member.handle}`,
+        image: a.member.avatar_uri.toString() ? { avatar: true, src: a.member.avatar_uri.toString() } : undefined,
+        description:
+          (a.stakes.application ? `Appl. stake: ${formatBalance(a.stakes.application)}` : '') +
+          (a.stakes.role ? (a.stakes.application && ', ') + `Role stake: ${formatBalance(a.stakes.role)}` : ''),
+        value: a.wgApplicationId.toString()
+      };
+    });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createFillWorkingGroupLeaderOpeningProposal"
+      proposalType="FillWorkingGroupLeaderOpening"
+      disabled={!openingsOptions.length || !applicationsOptions.length}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        valuesToFillOpeningParams(values)
+      ]}
+    >
+      <PromiseComponent error={openingsError} loading={openingsLoading} message="Fetching openings...">
+        { !openingsOptions.length
+          ? (
+            <Message error visible>
+              <Message.Header>No openings available!</Message.Header>
+              <Message.Content>
+                This proposal cannot be created, because no leader openings in <i>Review Period</i> are currently available
+                in {values.workingGroup} Working Group.
+              </Message.Content>
+            </Message>
+          )
+          : (
+            <FormField
+              label="Working Group Opening"
+              error={errorLabelsProps.openingId}>
+              <Dropdown
+                onChange={(...args) => {
+                  setFieldValue('successfulApplicants', []);
+                  // "as any" assert is required due to some invalid typing of Formik's "handleChange" function (it takes 2 args, not 1)
+                  return (handleChange as any)(...args);
+                }}
+                placeholder={'Select an opening'}
+                name={'openingId'}
+                selection
+                options={openingsOptions}
+                value={values.openingId}
+              />
+            </FormField>
+          )
+        }
+      </PromiseComponent>
+      { values.openingId && (
+        <PromiseComponent error={applError} loading={applLoading} message="Fetching applications...">
+          { !applicationsOptions.length
+            ? (
+              <Message error visible>
+                <Message.Header>No applications available!</Message.Header>
+                <Message.Content>
+                  FillWorkingGroupLeaderOpening proposal cannot be created for this opening,
+                  because there are no active applications to select from.
+                </Message.Content>
+              </Message>
+            )
+            : (
+              <>
+                <FormField
+                  label="Successful applicant"
+                  error={errorLabelsProps.successfulApplicant}>
+                  <Dropdown
+                    placeholder="Select successful applicant"
+                    fluid
+                    selection
+                    options={applicationsOptions}
+                    value={values.successfulApplicant}
+                    onChange={handleChange}
+                    name="successfulApplicant"/>
+                </FormField>
+                {values.successfulApplicant && (<>
+                  <Header as="h3">Selected applicant:</Header>
+                  <ApplicationsDetails applications={
+                    [activeApplications.find(a => a.wgApplicationId.toString() === values.successfulApplicant)!]
+                  }/>
+                  <Header as="h3">Reward policy:</Header>
+                  <FormField>
+                    <Checkbox
+                      toggle
+                      onChange={(e, data) => { setFieldValue('includeReward', data.checked); }}
+                      label={'Include reward'}
+                      checked={values.includeReward}/>
+                  </FormField>
+                  { values.includeReward && <RewardPolicyFields {...{ values, errorLabelsProps, handleChange, setFieldValue }}/> }
+                </>)}
+              </>
+            )
+          }
+        </PromiseComponent>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: (props: FormContainerProps) => Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.FillWorkingGroupLeaderOpening(props.currentBlock?.toNumber() || 0)
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'FillWorkingGroupLeaderOpeningForm'
+})(FillWorkingGroupLeaderOpeningForm);
+
+export default withCalls<ExportComponentProps>(
+  ['derive.chain.bestNumber', { propName: 'currentBlock' }]
+)(
+  withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer)
+);

+ 82 - 11
pioneer/packages/joy-proposals/src/forms/FormFields.tsx

@@ -1,6 +1,10 @@
 import React from 'react';
-import { Form, FormInputProps, FormTextAreaProps } from 'semantic-ui-react';
+import { Form, FormInputProps, FormTextAreaProps, Label, LabelProps, Checkbox } from 'semantic-ui-react';
+import { FormikProps } from 'formik';
 import LabelWithHelp from './LabelWithHelp';
+import { FormErrorLabelsProps } from './errorHandling';
+import { formatBalance } from '@polkadot/util';
+import styled from 'styled-components';
 
 /*
  * Generic form field components
@@ -10,16 +14,17 @@ import LabelWithHelp from './LabelWithHelp';
  * and to easily switch the structure/display of a typical form field.
 */
 
-type InputFormFieldProps = FormInputProps & {
+type InputFormFieldProps = Omit<FormInputProps, 'error'> & {
   help?: string;
   unit?: string;
+  error?: LabelProps;
 };
 
 export function InputFormField (props: InputFormFieldProps) {
   const { unit } = props;
-  const fieldProps = { ...props, label: undefined };
+  const fieldProps = { ...props, label: undefined, error: undefined };
   return (
-    <FormField {...props}>
+    <FormField {...props} showErrorMsg={true}>
       <Form.Input
         {...fieldProps}
         style={ unit ? { display: 'flex', alignItems: 'center' } : undefined }>
@@ -30,31 +35,97 @@ export function InputFormField (props: InputFormFieldProps) {
   );
 }
 
-type TextareaFormFieldProps = FormTextAreaProps & {
+type TextareaFormFieldProps = Omit<FormTextAreaProps, 'error'> & {
   help?: string;
+  error?: LabelProps;
 };
 
 export function TextareaFormField (props: TextareaFormFieldProps) {
-  const fieldProps = { ...props, label: undefined };
+  const fieldProps = { ...props, label: undefined, error: undefined };
   return (
-    <FormField {...props}>
+    <FormField {...props} showErrorMsg={true}>
       <Form.TextArea {...fieldProps}/>
     </FormField>
   );
 }
 
-type FormFieldProps = InputFormFieldProps | TextareaFormFieldProps;
+type FormFieldProps = Omit<(InputFormFieldProps | TextareaFormFieldProps), 'error'> & {
+  error?: LabelProps;
+  showErrorMsg?: boolean;
+};
+
+const StyledFormField = styled(Form.Field)`
+  & .field {
+    margin-bottom: 0 !important;
+  }
+`;
 
 export function FormField (props: React.PropsWithChildren<FormFieldProps>) {
-  const { error, label, help, children } = props;
+  const { error, showErrorMsg = false, label, help, children } = props;
   return (
-    <Form.Field error={Boolean(error)}>
+    <StyledFormField error={!!error}>
       { (label && help)
         ? <LabelWithHelp text={ label.toString() } help={ help }/>
         : (label ? <label>{ label.toString() }</label> : null)
       }
       { children }
-    </Form.Field>
+      { Boolean(showErrorMsg && error) && <Label {...error} prompt/> }
+    </StyledFormField>
+  );
+}
+
+type ReawrdPolicyFieldsType = {
+  rewardAmount: string;
+  rewardNextBlock: string;
+  rewardRecurring: boolean;
+  rewardInterval: string;
+}
+type RewardPolicyFieldsProps<ValuesT extends ReawrdPolicyFieldsType> =
+  Pick<FormikProps<ValuesT>, 'values' | 'handleChange' | 'setFieldValue'> & {
+    errorLabelsProps: FormErrorLabelsProps<ValuesT>;
+  };
+export function RewardPolicyFields<ValuesT extends ReawrdPolicyFieldsType> ({
+  values,
+  errorLabelsProps,
+  handleChange,
+  setFieldValue
+}: RewardPolicyFieldsProps<ValuesT>) {
+  return (
+    <>
+      <InputFormField
+        label="Amount per payout"
+        unit={formatBalance.getDefaults().unit}
+        onChange={handleChange}
+        name={'rewardAmount'}
+        error={errorLabelsProps.rewardAmount}
+        value={values.rewardAmount}
+        placeholder={'ie. 100'}
+      />
+      <InputFormField
+        label="Next payment at block"
+        onChange={handleChange}
+        name={'rewardNextBlock'}
+        error={errorLabelsProps.rewardNextBlock}
+        value={values.rewardNextBlock}
+      />
+      <FormField>
+        <Checkbox
+          toggle
+          onChange={(e, data) => { setFieldValue('rewardRecurring', data.checked); }}
+          label={'Recurring'}
+          checked={values.rewardRecurring}/>
+      </FormField>
+      { values.rewardRecurring && (
+        <InputFormField
+          label="Reward interval"
+          onChange={handleChange}
+          name={'rewardInterval'}
+          error={errorLabelsProps.rewardInterval}
+          value={values.rewardInterval}
+          unit={'Blocks'}
+        />
+      ) }
+    </>
   );
 }
 

+ 5 - 6
pioneer/packages/joy-proposals/src/forms/GenericProposalForm.tsx

@@ -49,6 +49,7 @@ type GenericProposalFormAdditionalProps = {
   txMethod?: string;
   submitParams?: any[];
   proposalType?: ProposalType;
+  disabled?: boolean;
 };
 
 type GenericFormContainerProps = ProposalFormContainerProps<
@@ -66,10 +67,7 @@ export const genericFormDefaultOptions: GenericFormDefaultOptions = {
     ...(props.initialData || {})
   }),
   validationSchema: {
-
-    title: Validation.All.title,
-    rationale: Validation.All.rationale
-
+    ...Validation.All()
   },
   handleSubmit: (values, { setSubmitting, resetForm }) => {
     // This is handled via TxButton
@@ -96,7 +94,8 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
     setSubmitting,
     history,
     balances_totalIssuance,
-    proposalType
+    proposalType,
+    disabled = false
   } = props;
   const errorLabelsProps = getFormErrorLabelsProps<GenericFormValues>(errors, touched);
   const [afterSubmit, setAfterSubmit] = useState(null as (() => () => void) | null);
@@ -198,7 +197,7 @@ export const GenericProposalForm: React.FunctionComponent<GenericFormInnerProps>
               type="button" // Tx button uses custom submit handler - "onTxButtonClick"
               label="Submit proposal"
               icon="paper plane"
-              isDisabled={isSubmitting}
+              isDisabled={disabled || isSubmitting}
               params={(submitParams || []).map(p => (p === '{STAKE}' ? requiredStake : p))}
               tx={`proposalsCodex.${txMethod}`}
               txFailedCb={onTxFailed}

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

@@ -10,15 +10,16 @@ import {
 } from './GenericProposalForm';
 import { FormField } from './FormFields';
 import { ProposalType } from '@polkadot/joy-utils/types/proposals';
-import { WorkingGroupKeys, WorkingGroupDef } from '@joystream/types/common';
+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 { ProfilePreviewFromStruct as MemberPreview } from '@polkadot/joy-utils/MemberProfilePreview';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
+import { LeadInfo } from '@polkadot/joy-utils/react/components/working-groups/LeadInfo';
 
 export type FormValues = GenericFormValues & {
-  workingGroup: WorkingGroupKeys;
+  workingGroup: WorkingGroupKey;
 };
 
 export const defaultValues: FormValues = {
@@ -32,6 +33,11 @@ type FormAdditionalProps = {
   submitParams: any[];
   proposalType: ProposalType;
   showLead?: boolean;
+  leadRequired?: boolean;
+  leadStakeRequired?: boolean;
+  leadRewardRequired?: boolean;
+  onLeadChange?: (lead: WorkerData | null) => void;
+  disabled?: boolean;
 };
 
 // We don't exactly use "container" and "export" components here, but those types are useful for
@@ -41,18 +47,34 @@ type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
 export type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
 export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerProps> = props => {
-  const { handleChange, errors, touched, values, showLead = true } = props;
+  const {
+    handleChange,
+    errors,
+    touched,
+    values,
+    showLead = true,
+    leadRequired = false,
+    leadStakeRequired = false,
+    leadRewardRequired = false,
+    onLeadChange,
+    disabled = false
+  } = props;
   const transport = useTransport();
   const [lead, error, loading] = usePromise(
     () => transport.workingGroups.currentLead(values.workingGroup),
     null,
-    [values.workingGroup]
+    [values.workingGroup],
+    onLeadChange
   );
   const leadRes = { lead, error, loading };
+  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 errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
   return (
-    <GenericProposalForm {...props}>
+    <GenericProposalForm {...props} disabled={isDisabled}>
       <FormField
         error={errorLabelsProps.workingGroup}
         label="Working group"
@@ -68,16 +90,27 @@ export const GenericWorkingGroupProposalForm: React.FunctionComponent<FormInnerP
       </FormField>
       { showLead && (
         <PromiseComponent message={'Fetching current lead...'} {...leadRes}>
-          <Message info>
-            <Message.Content>
-              <Message.Header>Current {values.workingGroup} Working Group lead:</Message.Header>
-              <div style={{ padding: '0.5rem 0' }}>
-                { leadRes.lead ? <MemberPreview profile={leadRes.lead.profile} /> : 'NONE' }
-              </div>
-            </Message.Content>
-          </Message>
+          <LeadInfo lead={leadRes.lead} group={values.workingGroup} header={true}/>
         </PromiseComponent>
       ) }
+      { leadMissing && (
+        <Message error visible>
+          <Message.Header>Leader required</Message.Header>
+          Selected working group has no active leader. An active leader is required in order to create this proposal.
+        </Message>
+      ) }
+      { stakeMissing && (
+        <Message error visible>
+          <Message.Header>No role stake</Message.Header>
+          Selected working group leader has no associated role stake, which is required in order to create this proposal.
+        </Message>
+      ) }
+      { rewardMissing && (
+        <Message error visible>
+          <Message.Header>No reward relationship</Message.Header>
+          Selected working group leader has no reward relationship, which is required in order to create this proposal.
+        </Message>
+      ) }
       { props.children }
     </GenericProposalForm>
   );

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

@@ -18,7 +18,7 @@ import { ProposalType } from '@polkadot/joy-utils/types/proposals';
 import { formatBalance } from '@polkadot/util';
 import './forms.css';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   capacity: string;
 };
 
@@ -70,7 +70,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    capacity: Validation.SetContentWorkingGroupMintCapacity.mintCapacity
+    ...Validation.SetContentWorkingGroupMintCapacity()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'MintCapacityForm'

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

@@ -16,7 +16,7 @@ import { withFormContainer } from './FormContainer';
 import './forms.css';
 import FileDropdown from './FileDropdown';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   // wasm blob as ArrayBuffer, or an Error string
   WASM: ArrayBuffer | string;
 };
@@ -62,7 +62,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    WASM: Validation.RuntimeUpgrade.WASM
+    ...Validation.RuntimeUpgrade()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'RuntimeUpgradeForm'

+ 4 - 3
pioneer/packages/joy-proposals/src/forms/SetContentWorkingGroupLeadForm.tsx

@@ -21,7 +21,7 @@ import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 import _ from 'lodash';
 import './forms.css';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   workingGroupLead: any;
 };
 
@@ -120,7 +120,8 @@ const SetContentWorkingGroupsLeadForm: React.FunctionComponent<FormInnerProps> =
             label="New Content Working Group Lead"
             help={
               'The member you propose to set as a new Content Working Group Lead. ' +
-              'Start typing handle or use "id:[ID]" query.'
+              'Start typing handle or use "id:[ID]" query. ' +
+              'Current council members are not allowed to be selected and are excluded from the list.'
             }
           >
             {
@@ -178,7 +179,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    workingGroupLead: Validation.SetLead.workingGroupLead
+    ...Validation.SetLead()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'SetContentWorkingGroupLeadForm'

+ 2 - 9
pioneer/packages/joy-proposals/src/forms/SetCouncilParamsForm.tsx

@@ -22,7 +22,7 @@ import _ from 'lodash';
 import { ElectionParameters } from '@joystream/types/council';
 import { PromiseComponent } from '@polkadot/joy-utils/react/components';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   announcingPeriod: string;
   votingPeriod: string;
   minVotingStake: string;
@@ -203,14 +203,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    announcingPeriod: Validation.SetElectionParameters.announcingPeriod,
-    votingPeriod: Validation.SetElectionParameters.votingPeriod,
-    minVotingStake: Validation.SetElectionParameters.minVotingStake,
-    revealingPeriod: Validation.SetElectionParameters.revealingPeriod,
-    minCouncilStake: Validation.SetElectionParameters.minCouncilStake,
-    newTermDuration: Validation.SetElectionParameters.newTermDuration,
-    candidacyLimit: Validation.SetElectionParameters.candidacyLimit,
-    councilSize: Validation.SetElectionParameters.councilSize
+    ...Validation.SetElectionParameters()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'SetCouncilParamsForm'

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

@@ -17,7 +17,7 @@ import { withFormContainer } from './FormContainer';
 import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
 import './forms.css';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   maxValidatorCount: string;
 };
 
@@ -33,7 +33,7 @@ type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
 
 const SetMaxValidatorCountForm: React.FunctionComponent<FormInnerProps> = props => {
   const transport = useTransport();
-  const [validatorCount] = usePromise<number>(() => transport.validators.maxCount(), NaN);
+  const [validatorCount] = usePromise<number>(() => transport.validators.maxCount(), 20);
   const { handleChange, errors, touched, values, setFieldValue } = props;
   const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
 
@@ -69,7 +69,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    maxValidatorCount: Validation.SetValidatorCount.maxValidatorCount
+    ...Validation.SetValidatorCount()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'SetMaxValidatorCountForm'

+ 0 - 256
pioneer/packages/joy-proposals/src/forms/SetStorageRoleParamsForm.tsx

@@ -1,256 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Form, Divider } from 'semantic-ui-react';
-import { getFormErrorLabelsProps } from './errorHandling';
-import * as Yup from 'yup';
-import {
-  GenericProposalForm,
-  GenericFormValues,
-  genericFormDefaultOptions,
-  genericFormDefaultValues,
-  withProposalFormData,
-  ProposalFormExportProps,
-  ProposalFormContainerProps,
-  ProposalFormInnerProps
-} from './GenericProposalForm';
-import Validation from '../validationSchema';
-import { InputFormField } from './FormFields';
-import { withFormContainer } from './FormContainer';
-import { BlockNumber, Balance } from '@polkadot/types/interfaces';
-import { u32 } from '@polkadot/types/primitive';
-import { createType } from '@polkadot/types';
-import { useTransport, usePromise } from '@polkadot/joy-utils/react/hooks';
-import { StorageRoleParameters, IStorageRoleParameters } from '@polkadot/joy-utils/types/storageProviders';
-import { formatBalance } from '@polkadot/util';
-import './forms.css';
-
-// Move to joy-types?
-type RoleParameters = {
-  min_stake: Balance;
-  min_actors: u32;
-  max_actors: u32;
-  reward: Balance;
-  reward_period: BlockNumber;
-  bonding_period: BlockNumber;
-  unbonding_period: BlockNumber;
-  min_service_period: BlockNumber;
-  startup_grace_period: BlockNumber;
-  entry_request_fee: Balance;
-};
-
-// All of those are strings, because that's how those values are beeing passed from inputs
-type FormValues = GenericFormValues &
-{
-  [K in keyof RoleParameters]: string;
-};
-
-const defaultValues: FormValues = {
-  ...genericFormDefaultValues,
-  min_stake: '',
-  min_actors: '',
-  max_actors: '',
-  reward: '',
-  reward_period: '',
-  bonding_period: '',
-  unbonding_period: '',
-  min_service_period: '',
-  startup_grace_period: '',
-  entry_request_fee: ''
-};
-
-type FormAdditionalProps = {}; // Aditional props coming all the way from export comonent into the inner form.
-type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
-type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
-type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
-
-function createRoleParameters (values: FormValues): RoleParameters {
-  return {
-    min_stake: createType('Balance', values.min_stake),
-    min_actors: createType('u32', values.min_actors),
-    max_actors: createType('u32', values.max_actors),
-    reward: createType('Balance', values.reward),
-    reward_period: createType('BlockNumber', values.reward_period),
-    bonding_period: createType('BlockNumber', values.bonding_period),
-    unbonding_period: createType('BlockNumber', values.unbonding_period),
-    min_service_period: createType('BlockNumber', values.min_service_period),
-    startup_grace_period: createType('BlockNumber', values.startup_grace_period),
-    entry_request_fee: createType('Balance', values.entry_request_fee)
-  };
-}
-
-const SetStorageRoleParamsForm: React.FunctionComponent<FormInnerProps> = props => {
-  const transport = useTransport();
-  const [params] = usePromise<IStorageRoleParameters | null>(() => transport.storageProviders.roleParameters(), null);
-  const { handleChange, errors, touched, values, setFieldValue } = props;
-  const [placeholders, setPlaceholders] = useState<{ [k in keyof FormValues]: string }>(defaultValues);
-  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
-
-  useEffect(() => {
-    if (params) {
-      const stringParams = Object.keys(params).reduce((obj, key) => {
-        return { ...obj, [`${key}`]: String(params[key as keyof IStorageRoleParameters]) };
-      }, {});
-      const fetchedPlaceholders = { ...placeholders, ...stringParams };
-
-      StorageRoleParameters.forEach(field => {
-        setFieldValue(field, params[field].toString());
-      });
-      setPlaceholders(fetchedPlaceholders);
-    }
-  }, [params]);
-
-  return (
-    <GenericProposalForm
-      {...props}
-      txMethod="createSetStorageRoleParametersProposal"
-      proposalType="SetStorageRoleParameters"
-      submitParams={[props.myMemberId, values.title, values.rationale, '{STAKE}', createRoleParameters(values)]}
-    >
-      <Divider horizontal>Parameters</Divider>
-      <Form.Group widths="equal" style={{ marginBottom: '2em' }}>
-        <InputFormField
-          label="Min. actors"
-          help="Minimum number of actors in this role"
-          onChange={handleChange}
-          name="min_actors"
-          placeholder={placeholders.min_actors}
-          error={errorLabelsProps.min_actors}
-          value={values.min_actors}
-          disabled
-        />
-        <InputFormField
-          label="Max. actors"
-          help="Maximum number of actors in this role"
-          fluid
-          onChange={handleChange}
-          name="max_actors"
-          placeholder={placeholders.max_actors}
-          error={errorLabelsProps.max_actors}
-          value={values.max_actors}
-        />
-      </Form.Group>
-      <Form.Group widths="equal" style={{ marginBottom: '2em' }}>
-        <InputFormField
-          label="Reward"
-          help="Reward for performing this role (for each period)"
-          fluid
-          onChange={handleChange}
-          name="reward"
-          placeholder={placeholders.reward}
-          error={errorLabelsProps.reward}
-          value={values.reward}
-          unit={ formatBalance.getDefaults().unit }
-        />
-        <InputFormField
-          label="Reward period"
-          help="Reward period in blocks"
-          fluid
-          onChange={handleChange}
-          name="reward_period"
-          placeholder={placeholders.reward_period}
-          error={errorLabelsProps.reward_period}
-          value={values.reward_period}
-          unit="blocks"
-          disabled
-        />
-      </Form.Group>
-      <Form.Group widths="equal" style={{ marginBottom: '2em' }}>
-        <InputFormField
-          label="Min. stake"
-          help="Minimum stake for this role"
-          onChange={handleChange}
-          name="min_stake"
-          placeholder={placeholders.min_stake}
-          error={errorLabelsProps.min_stake}
-          value={values.min_stake}
-          unit={ formatBalance.getDefaults().unit }
-        />
-        <InputFormField
-          label="Min. service period"
-          help="Minimum period of service in blocks"
-          fluid
-          onChange={handleChange}
-          name="min_service_period"
-          placeholder={placeholders.min_service_period}
-          error={errorLabelsProps.min_service_period}
-          value={values.min_service_period}
-          unit="blocks"
-          disabled
-        />
-      </Form.Group>
-      <Form.Group widths="equal" style={{ marginBottom: '2em' }}>
-        <InputFormField
-          label="Bonding period"
-          help="Bonding period in blocks"
-          fluid
-          onChange={handleChange}
-          name="bonding_period"
-          placeholder={placeholders.bonding_period}
-          error={errorLabelsProps.bonding_period}
-          value={values.bonding_period}
-          unit="blocks"
-          disabled
-        />
-        <InputFormField
-          label="Unbounding period"
-          help="Unbounding period in blocks"
-          fluid
-          onChange={handleChange}
-          name="unbonding_period"
-          placeholder={placeholders.unbonding_period}
-          error={errorLabelsProps.unbonding_period}
-          value={values.unbonding_period}
-          unit="blocks"
-        />
-      </Form.Group>
-      <Form.Group widths="equal" style={{ marginBottom: '2em' }}>
-        <InputFormField
-          label="Startup grace period"
-          help="Startup grace period in blocks"
-          fluid
-          onChange={handleChange}
-          name="startup_grace_period"
-          placeholder={placeholders.startup_grace_period}
-          error={errorLabelsProps.startup_grace_period}
-          value={values.startup_grace_period}
-          unit="blocks"
-          disabled
-        />
-        <InputFormField
-          label="Entry request fee"
-          help="Entry request fee"
-          fluid
-          onChange={handleChange}
-          name="entry_request_fee"
-          placeholder={placeholders.entry_request_fee}
-          error={errorLabelsProps.entry_request_fee}
-          value={values.entry_request_fee}
-          unit={ formatBalance.getDefaults().unit }
-        />
-      </Form.Group>
-    </GenericProposalForm>
-  );
-};
-
-const FormContainer = withFormContainer<FormContainerProps, FormValues>({
-  mapPropsToValues: (props: FormContainerProps) => ({
-    ...defaultValues,
-    ...(props.initialData || {})
-  }),
-  validationSchema: Yup.object().shape({
-    ...genericFormDefaultOptions.validationSchema,
-    min_stake: Validation.SetStorageRoleParameters.min_stake,
-    min_actors: Validation.SetStorageRoleParameters.min_actors,
-    max_actors: Validation.SetStorageRoleParameters.max_actors,
-    reward: Validation.SetStorageRoleParameters.reward,
-    reward_period: Validation.SetStorageRoleParameters.reward_period,
-    bonding_period: Validation.SetStorageRoleParameters.bonding_period,
-    unbonding_period: Validation.SetStorageRoleParameters.unbonding_period,
-    min_service_period: Validation.SetStorageRoleParameters.min_service_period,
-    startup_grace_period: Validation.SetStorageRoleParameters.startup_grace_period,
-    entry_request_fee: Validation.SetStorageRoleParameters.entry_request_fee
-  }),
-  handleSubmit: genericFormDefaultOptions.handleSubmit,
-  displayName: 'SetStorageRoleParamsForm'
-})(SetStorageRoleParamsForm);
-
-export default withProposalFormData(FormContainer);

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

@@ -0,0 +1,93 @@
+import React, { useState } from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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';
+import Validation from '../validationSchema';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
+
+export type FormValues = WGFormValues & {
+  amount: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  amount: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const SetWorkingGroupLeadRewardForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<WorkerData | null>(null);
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createSetWorkingGroupLeaderRewardProposal"
+      proposalType="SetWorkingGroupLeaderReward"
+      leadRequired={true}
+      leadRewardRequired={true}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.reward) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="New reward amount"
+              onChange={handleChange}
+              name="amount"
+              error={errorLabelsProps.amount}
+              value={values.amount}
+              unit={formatBalance.getDefaults().unit}
+            />
+          </Grid.Column>
+        </Grid>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.SetWorkingGroupLeaderReward()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'SetWorkingGroupLeadRewardForm'
+})(SetWorkingGroupLeadRewardForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -0,0 +1,86 @@
+import React from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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';
+import Validation from '../validationSchema';
+
+export type FormValues = WGFormValues & {
+  capacity: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  capacity: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const SetWorkingGroupMintCapacityForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createSetWorkingGroupMintCapacityProposal"
+      proposalType="SetWorkingGroupMintCapacity"
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        values.capacity,
+        values.workingGroup
+      ]}
+    >
+      <Grid columns="4" doubling stackable verticalAlign="bottom">
+        <Grid.Column>
+          <InputFormField
+            label="Mint capacity"
+            onChange={handleChange}
+            name="capacity"
+            error={errorLabelsProps.capacity}
+            value={values.capacity}
+            placeholder={'ie. 100000'}
+            unit={formatBalance.getDefaults().unit}
+          />
+        </Grid.Column>
+      </Grid>
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.SetWorkingGroupMintCapacity()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'SetWorkingGroupMintCapacityForm'
+})(SetWorkingGroupMintCapacityForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -16,7 +16,7 @@ import { TextareaFormField } from './FormFields';
 import { withFormContainer } from './FormContainer';
 import './forms.css';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   description: string;
 };
 
@@ -61,7 +61,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    description: Validation.Text.description
+    ...Validation.Text()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'SignalForm'

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

@@ -0,0 +1,101 @@
+import React, { useState, useEffect } from 'react';
+import { getFormErrorLabelsProps } from './errorHandling';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  defaultValues as wgFromDefaultValues
+} 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';
+import Validation from '../validationSchema';
+import { WorkerData } from '@polkadot/joy-utils/types/workingGroups';
+
+export type FormValues = WGFormValues & {
+  amount: string;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  amount: ''
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps>;
+type FormInnerProps = ProposalFormInnerProps<FormContainerProps, FormValues>;
+
+const SlashWorkingGroupLeadStakeForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId, setFieldError } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<WorkerData | null>(null);
+  // Here we validate if stake <= current lead stake.
+  // Because it depends on selected working group,
+  // there's no easy way to do it using validationSchema
+  useEffect(() => {
+    if (lead && parseInt(values.amount) > (lead.stake || 0) && !errors.amount) {
+      setFieldError('amount', `The stake cannot exceed current leader's stake (${formatBalance(lead.stake)})`);
+    }
+  });
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createSlashWorkingGroupLeaderStakeProposal"
+      proposalType="SlashWorkingGroupLeaderStake"
+      leadRequired={true}
+      leadStakeRequired={true}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead?.workerId,
+        values.amount,
+        values.workingGroup
+      ]}
+    >
+      { (lead && lead.stake) && (
+        <Grid columns="4" doubling stackable verticalAlign="bottom">
+          <Grid.Column>
+            <InputFormField
+              label="Amount to slash"
+              onChange={handleChange}
+              name="amount"
+              error={errorLabelsProps.amount}
+              value={values.amount}
+              unit={formatBalance.getDefaults().unit}
+            />
+          </Grid.Column>
+        </Grid>
+      ) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.SlashWorkingGroupLeaderStake()
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'SlashWorkingGroupLeadStakeForm'
+})(SlashWorkingGroupLeadStakeForm);
+
+export default withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer);

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

@@ -19,7 +19,7 @@ import { InputAddress } from '@polkadot/react-components/index';
 import { formatBalance } from '@polkadot/util';
 import './forms.css';
 
-type FormValues = GenericFormValues & {
+export type FormValues = GenericFormValues & {
   destinationAccount: any;
   tokens: string;
 };
@@ -86,8 +86,7 @@ const FormContainer = withFormContainer<FormContainerProps, FormValues>({
   }),
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    tokens: Validation.Spending.tokens,
-    destinationAccount: Validation.Spending.destinationAccount
+    ...Validation.Spending()
   }),
   handleSubmit: genericFormDefaultOptions.handleSubmit,
   displayName: 'SpendingProposalsForm'

+ 125 - 0
pioneer/packages/joy-proposals/src/forms/TerminateWorkingGroupLeaderForm.tsx

@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import * as Yup from 'yup';
+import {
+  withProposalFormData,
+  ProposalFormExportProps,
+  ProposalFormContainerProps,
+  ProposalFormInnerProps,
+  genericFormDefaultOptions
+} from './GenericProposalForm';
+import {
+  GenericWorkingGroupProposalForm,
+  FormValues as WGFormValues,
+  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 { withCalls } from '@polkadot/react-api';
+import { formatBalance } from '@polkadot/util';
+
+export type FormValues = WGFormValues & {
+  terminationRationale: string;
+  slashStake: boolean;
+};
+
+const defaultValues: FormValues = {
+  ...wgFromDefaultValues,
+  terminationRationale: '',
+  slashStake: false
+};
+
+type FormAdditionalProps = {}; // Aditional props coming all the way from export component into the inner form.
+type ExportComponentProps = ProposalFormExportProps<FormAdditionalProps, FormValues>;
+type FormContainerProps = ProposalFormContainerProps<ExportComponentProps> & {
+  terminationRationaleConstraint?: InputValidationLengthConstraint;
+};
+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 TerminateWorkingGroupLeaderForm: React.FunctionComponent<FormInnerProps> = props => {
+  const { handleChange, errors, touched, values, myMemberId, setFieldValue } = props;
+  const errorLabelsProps = getFormErrorLabelsProps<FormValues>(errors, touched);
+  const [lead, setLead] = useState<WorkerData | null>(null);
+
+  return (
+    <GenericWorkingGroupProposalForm
+      {...props}
+      txMethod="createTerminateWorkingGroupLeaderRoleProposal"
+      proposalType="TerminateWorkingGroupLeaderRole"
+      leadRequired={true}
+      onLeadChange={(lead: WorkerData | null) => setLead(lead)}
+      submitParams={[
+        myMemberId,
+        values.title,
+        values.rationale,
+        '{STAKE}',
+        lead && valuesToTerminateRoleParams(values, lead)
+      ]}
+    >
+      { lead && (<>
+        <TextareaFormField
+          label="Termination rationale"
+          help={
+            'This rationale is an required argument of "terminateWorkerRole" extrinsic, ' +
+            'it may differ from proposal rationale and has different length constraints. ' +
+            'If the propsal gets executed, this rationale will become part of "TerminatedLeader" event.'
+          }
+          onChange={handleChange}
+          name="terminationRationale"
+          placeholder="Provide a clear rationale for terminating the leader role..."
+          error={errorLabelsProps.terminationRationale}
+          value={values.terminationRationale}
+        />
+        { lead.stake && (
+          <FormField>
+            <Checkbox
+              toggle
+              onChange={(e, data) => { setFieldValue('slashStake', data.checked); }}
+              label={ `Slash leader stake (${formatBalance(lead.stake)})` }
+              checked={values.slashStake}/>
+          </FormField>
+        ) }
+      </>) }
+    </GenericWorkingGroupProposalForm>
+  );
+};
+
+const FormContainer = withFormContainer<FormContainerProps, FormValues>({
+  mapPropsToValues: (props: FormContainerProps) => ({
+    ...defaultValues,
+    ...(props.initialData || {})
+  }),
+  validationSchema: (props: FormContainerProps) => Yup.object().shape({
+    ...genericFormDefaultOptions.validationSchema,
+    ...Validation.TerminateWorkingGroupLeaderRole(
+      props.terminationRationaleConstraint || InputValidationLengthConstraint.createWithMaxAllowed()
+    )
+  }),
+  handleSubmit: genericFormDefaultOptions.handleSubmit,
+  displayName: 'TerminateWorkingGroupLeaderForm'
+})(TerminateWorkingGroupLeaderForm);
+
+export default withCalls<ExportComponentProps>(
+  ['query.storageWorkingGroup.workerExitRationaleText', { propName: 'terminationRationaleConstraint' }]
+)(
+  withProposalFormData<FormContainerProps, ExportComponentProps>(FormContainer)
+);

+ 4 - 4
pioneer/packages/joy-proposals/src/forms/errorHandling.ts

@@ -1,7 +1,7 @@
 import { FormikErrors, FormikTouched } from 'formik';
 import { LabelProps } from 'semantic-ui-react';
 
-type FieldErrorLabelProps = LabelProps | null; // This is used for displaying semantic-ui errors
+type FieldErrorLabelProps = LabelProps | undefined; // This is used for displaying semantic-ui errors
 export type FormErrorLabelsProps<ValuesT> = { [T in keyof ValuesT]: FieldErrorLabelProps };
 
 // Single form field error state.
@@ -13,10 +13,10 @@ export function getErrorLabelProps<ValuesT> (
   fieldName: keyof ValuesT,
   pointing: LabelProps['pointing'] = undefined
 
-): FieldErrorLabelProps {
+): FieldErrorLabelProps | undefined {
   return (errors[fieldName] && touched[fieldName])
-    ? { content: errors[fieldName], pointing }
-    : null;
+    ? { content: errors[fieldName], pointing, size: 'large' }
+    : undefined;
 }
 
 // All form fields error states (uses default value for "pointing").

+ 7 - 2
pioneer/packages/joy-proposals/src/forms/index.ts

@@ -1,12 +1,17 @@
 export { default as SignalForm } from './SignalForm';
 export { default as SpendingProposalForm } from './SpendingProposalForm';
-export { default as EvictStorageProviderForm } from './EvictStorageProviderForm';
 export { default as MintCapacityForm } from './MintCapacityForm';
 export { default as SetCouncilParamsForm } from './SetCouncilParamsForm';
 export { default as SetContentWorkingGroupLeadForm } from './SetContentWorkingGroupLeadForm';
-export { default as SetStorageRoleParamsForm } from './SetStorageRoleParamsForm';
 export { default as RuntimeUpgradeForm } from './RuntimeUpgradeForm';
 export { default as SetContentWorkingGroupMintCapForm } from './SetContentWorkingGroupMintCapForm';
 export { default as SetCouncilMintCapForm } from './SetCouncilMintCapForm';
 export { default as SetMaxValidatorCountForm } from './SetMaxValidatorCountForm';
 export { default as AddWorkingGroupOpeningForm } from './AddWorkingGroupOpeningForm';
+export { default as SetWorkingGroupMintCapacityForm } from './SetWorkingGroupMintCapacityForm';
+export { default as BeginReviewLeaderApplicationsForm } from './BeginReviewLeaderApplicationsForm';
+export { default as FillWorkingGroupLeaderOpeningForm } from './FillWorkingGroupLeaderOpeningForm';
+export { default as DecreaseWorkingGroupLeadStakeFrom } from './DecreaseWorkingGroupLeadStakeForm';
+export { default as SlashWorkingGroupLeadStakeForm } from './SlashWorkingGroupLeadStakeForm';
+export { default as SetWorkingGroupLeadRewardForm } from './SetWorkingGroupLeadRewardForm';
+export { default as TerminateWorkingGroupLeaderForm } from './TerminateWorkingGroupLeaderForm';

+ 25 - 5
pioneer/packages/joy-proposals/src/index.tsx

@@ -7,6 +7,7 @@ import { Breadcrumb } from 'semantic-ui-react';
 import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { TransportProvider } from '@polkadot/joy-utils/react/context';
 import { ProposalPreviewList, ProposalFromId, ChooseProposalType } from './Proposal';
+import _ from 'lodash';
 
 import './index.css';
 
@@ -14,15 +15,20 @@ import translate from './translate';
 import NotDone from './NotDone';
 import {
   SignalForm,
-  EvictStorageProviderForm,
   SpendingProposalForm,
   SetContentWorkingGroupLeadForm,
   SetContentWorkingGroupMintCapForm,
   SetCouncilParamsForm,
-  SetStorageRoleParamsForm,
   SetMaxValidatorCountForm,
   RuntimeUpgradeForm,
-  AddWorkingGroupOpeningForm
+  AddWorkingGroupOpeningForm,
+  SetWorkingGroupMintCapacityForm,
+  BeginReviewLeaderApplicationsForm,
+  FillWorkingGroupLeaderOpeningForm,
+  DecreaseWorkingGroupLeadStakeFrom,
+  SlashWorkingGroupLeadStakeForm,
+  SetWorkingGroupLeadRewardForm,
+  TerminateWorkingGroupLeaderForm
 } from './forms';
 
 interface Props extends AppProps, I18nProps {}
@@ -45,6 +51,15 @@ function App (props: Props): React.ReactElement<Props> {
         <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`}>
                 <Breadcrumb.Section link as={Link} to={basePath}>Proposals</Breadcrumb.Section>
                 <Breadcrumb.Divider icon="right angle" />
@@ -68,10 +83,15 @@ function App (props: Props): React.ReactElement<Props> {
             path={`${basePath}/new/set-content-working-group-mint-capacity`}
             component={SetContentWorkingGroupMintCapForm}
           />
-          <Route exact path={`${basePath}/new/evict-storage-provider`} component={EvictStorageProviderForm} />
           <Route exact path={`${basePath}/new/set-validator-count`} component={SetMaxValidatorCountForm} />
-          <Route exact path={`${basePath}/new/set-storage-role-parameters`} component={SetStorageRoleParamsForm} />
           <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} />

+ 0 - 6
pioneer/packages/joy-proposals/src/stories/ProposalForms.stories.tsx

@@ -1,11 +1,9 @@
 import '../index.css';
 import {
   SignalForm,
-  EvictStorageProviderForm,
   SpendingProposalForm,
   SetCouncilParamsForm,
   SetContentWorkingGroupLeadForm,
-  SetStorageRoleParamsForm,
   RuntimeUpgradeForm,
   SetContentWorkingGroupMintCapForm,
   SetCouncilMintCapForm,
@@ -19,16 +17,12 @@ export default {
 
 export const Signal = () => withMock(SignalForm);
 
-export const StorageProviders = () => withMock(EvictStorageProviderForm);
-
 export const SpendingProposal = () => withMock(SpendingProposalForm);
 
 export const SetCouncilParams = () => withMock(SetCouncilParamsForm);
 
 export const SetContentWorkingGroupLead = () => withMock(SetContentWorkingGroupLeadForm);
 
-export const SetStorageRoleParams = () => withMock(SetStorageRoleParamsForm);
-
 export const RuntimeUpgrade = () => withMock(RuntimeUpgradeForm);
 
 export const ContentWorkingGroupMintCap = () => withMock(SetContentWorkingGroupMintCapForm);

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

@@ -1,5 +1,5 @@
 import { ProposalTypeInfo } from '../../Proposal/ProposalTypePreview';
-import { Categories } from '../../Proposal/ChooseProposalType';
+import { Categories } from '@polkadot/joy-utils/types/proposals';
 
 const MockProposalTypesInfo: ProposalTypeInfo[] = [
   {
@@ -52,7 +52,7 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   },
   {
     type: 'EvictStorageProvider',
-    category: Categories.storage,
+    category: Categories.other,
     description:
         'Change the total reward across all validators in a given block.' +
         'This is not the direct reward, but base reward for Pallet staking module.' +
@@ -68,7 +68,7 @@ const MockProposalTypesInfo: ProposalTypeInfo[] = [
   },
   {
     type: 'SetStorageRoleParameters',
-    category: Categories.storage,
+    category: Categories.other,
     description:
         'Change the total reward across all validators in a given block.' +
         'This is not the direct reward, but base reward for Pallet staking module.' +

+ 163 - 185
pioneer/packages/joy-proposals/src/validationSchema.ts

@@ -1,5 +1,23 @@
 import * as Yup from 'yup';
 import { schemaValidator, ActivateOpeningAtKeys } from '@joystream/types/hiring';
+import { ProposalTypes } from '@polkadot/joy-utils/types/proposals';
+import { GenericFormValues } from './forms/GenericProposalForm';
+import { InputValidationLengthConstraint } from '@joystream/types/common';
+import { FormValues as SignalFormValues } from './forms/SignalForm';
+import { FormValues as RuntimeUpgradeFormValues } from './forms/RuntimeUpgradeForm';
+import { FormValues as SetCouncilParamsFormValues } from './forms/SetCouncilParamsForm';
+import { FormValues as SpendingProposalFormValues } from './forms/SpendingProposalForm';
+import { FormValues as SetContentWorkingGroupLeadFormValues } from './forms/SetContentWorkingGroupLeadForm';
+import { FormValues as SetContentWorkingGroupMintCapacityFormValues } from './forms/MintCapacityForm';
+import { FormValues as SetMaxValidatorCountFormValues } from './forms/SetMaxValidatorCountForm';
+import { FormValues as AddWorkingGroupLeaderOpeningFormValues } from './forms/AddWorkingGroupOpeningForm';
+import { FormValues as SetWorkingGroupMintCapacityFormValues } from './forms/SetWorkingGroupMintCapacityForm';
+import { FormValues as BeginReviewLeaderApplicationsFormValues } from './forms/BeginReviewLeaderApplicationsForm';
+import { FormValues as FillWorkingGroupLeaderOpeningFormValues } from './forms/FillWorkingGroupLeaderOpeningForm';
+import { FormValues as DecreaseWorkingGroupLeadStakeFormValues } from './forms/DecreaseWorkingGroupLeadStakeForm';
+import { FormValues as SlashWorkingGroupLeadStakeFormValues } from './forms/SlashWorkingGroupLeadStakeForm';
+import { FormValues as SetWorkingGroupLeadRewardFormValues } from './forms/SetWorkingGroupLeadRewardForm';
+import { FormValues as TerminateWorkingGroupLeaderFormValues } from './forms/TerminateWorkingGroupLeaderForm';
 
 // TODO: If we really need this (currency unit) we can we make "Validation" a functiction that returns an object.
 // We could then "instantialize" it in "withFormContainer" where instead of passing
@@ -49,28 +67,6 @@ const MAX_VALIDATOR_COUNT_MAX = 100;
 const MINT_CAPACITY_MIN = 0;
 const MINT_CAPACITY_MAX = 1000000;
 
-// Set Storage Role Parameters
-const MIN_STAKE_MIN = 1;
-const MIN_STAKE_MAX = 10000000;
-const MIN_ACTORS_MIN = 0;
-const MIN_ACTORS_MAX = 1;
-const MAX_ACTORS_MIN = 2;
-const MAX_ACTORS_MAX = 99;
-const REWARD_MIN = 1;
-const REWARD_MAX = 99999;
-const REWARD_PERIOD_MIN = 600;
-const REWARD_PERIOD_MAX = 3600;
-const BONDING_PERIOD_MIN = 600;
-const BONDING_PERIOD_MAX = 28800;
-const UNBONDING_PERIOD_MIN = 600;
-const UNBONDING_PERIOD_MAX = 28800;
-const MIN_SERVICE_PERIOD_MIN = 600;
-const MIN_SERVICE_PERIOD_MAX = 28800;
-const STARTUP_GRACE_PERIOD_MIN = 600;
-const STARTUP_GRACE_PERIOD_MAX = 28800;
-const ENTRY_REQUEST_FEE_MIN = 1;
-const ENTRY_REQUEST_FEE_MAX = 100000;
-
 // Add Working Group Leader Opening Parameters
 // TODO: Discuss the actual values
 const MIN_EXACT_BLOCK_MINUS_CURRENT = 14400 * 5; // ~5 days
@@ -88,6 +84,27 @@ const TERMINATE_ROLE_UNSTAKING_MAX = 14 * 14400; // 14 days
 const LEAVE_ROLE_UNSTAKING_MIN = 0;
 const LEAVE_ROLE_UNSTAKING_MAX = 14 * 14400; // 14 days
 
+// Set Working Group Mint Capacity
+// TODO: Discuss the actual values
+const WG_MINT_CAP_MIN = 0;
+const WG_MINT_CAP_MAX = 1000000;
+
+// Fill Working Group Leader Opening / Set Working Group Lead Reward
+// TODO: Discuss the actual values
+const MIN_REWARD_AMOUNT = 1;
+const MAX_REWARD_AMOUNT = 100000;
+const MIN_REWARD_INTERVAL = 1;
+const MAX_REWARD_INTERVAL = 30 * 14400; // 30 days
+// 3 days margin (voting_period) to prevent FillOpeningInvalidNextPaymentBlock
+// Should we worry that much about it though?
+const MIN_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 3 * 14400;
+const MAX_NEXT_PAYMENT_BLOCK_MINUS_CURRENT = 30 * 14400; // 30 days
+
+// Decrease/Slash Working Group Leader Stake
+const DECREASE_LEAD_STAKE_MIN = 1;
+const SLASH_LEAD_STAKE_MIN = 1;
+// Max is validated in form component, because it depends on selected working group's leader stake
+
 function errorMessage (name: string, min?: number | string, max?: number | string, unit?: string): string {
   return `${name} should be at least ${min} and no more than ${max}${unit ? ` ${unit}.` : '.'}`;
 }
@@ -97,80 +114,59 @@ Validation is used to validate a proposal form.
 Each proposal type should validate the fields of his form, anything is valid as long as it fits in a Yup Schema.
 In a form, validation should be injected in the Yup Schema just by accessing it in this object.
 Ex:
-// EvictStorageProvider Form
+// Text Form
 
 import Validation from 'path/to/validationSchema'
 ...
   validationSchema: Yup.object().shape({
     ...genericFormDefaultOptions.validationSchema,
-    storageProvider: Validation.EvictStorageProvider.storageProvider
+    ...Validation.Text()
   }),
 
 */
 
+type ProposalTypeKeys = typeof ProposalTypes[number];
+type OutdatedProposals = 'EvictStorageProvider' | 'SetStorageRoleParameters';
+type ValidationTypeKeys = Exclude<ProposalTypeKeys, OutdatedProposals> | 'All';
+
+/* eslint-disable @typescript-eslint/indent */
+// /\ This prevents eslint from trying to make "stairs" out of those multiple conditions.
+// They are more readable when one is directly under the other (switch-case style)
+type FormValuesByType<T extends ValidationTypeKeys> =
+  T extends 'All' ? GenericFormValues :
+  T extends 'Text' ? Omit<SignalFormValues, keyof GenericFormValues> :
+  T extends 'RuntimeUpgrade' ? Omit<RuntimeUpgradeFormValues, keyof GenericFormValues> :
+  T extends 'SetElectionParameters' ? Omit<SetCouncilParamsFormValues, keyof GenericFormValues> :
+  T extends 'Spending' ? Omit<SpendingProposalFormValues, keyof GenericFormValues> :
+  T extends 'SetLead' ? Omit<SetContentWorkingGroupLeadFormValues, keyof GenericFormValues> :
+  T extends 'SetContentWorkingGroupMintCapacity' ? Omit<SetContentWorkingGroupMintCapacityFormValues, keyof GenericFormValues> :
+  T extends 'SetValidatorCount' ? Omit<SetMaxValidatorCountFormValues, keyof GenericFormValues> :
+  T extends 'AddWorkingGroupLeaderOpening' ? Omit<AddWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
+  T extends 'SetWorkingGroupMintCapacity' ? Omit<SetWorkingGroupMintCapacityFormValues, keyof GenericFormValues> :
+  T extends 'BeginReviewWorkingGroupLeaderApplication' ? Omit<BeginReviewLeaderApplicationsFormValues, keyof GenericFormValues> :
+  T extends 'FillWorkingGroupLeaderOpening' ? Omit<FillWorkingGroupLeaderOpeningFormValues, keyof GenericFormValues> :
+  T extends 'DecreaseWorkingGroupLeaderStake' ? Omit<DecreaseWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
+  T extends 'SlashWorkingGroupLeaderStake' ? Omit<SlashWorkingGroupLeadStakeFormValues, keyof GenericFormValues> :
+  T extends 'SetWorkingGroupLeaderReward' ? Omit<SetWorkingGroupLeadRewardFormValues, keyof GenericFormValues> :
+  T extends 'TerminateWorkingGroupLeaderRole' ? Omit<TerminateWorkingGroupLeaderFormValues, keyof GenericFormValues> :
+  never;
+
+type ValidationSchemaFuncParamsByType<T extends ValidationTypeKeys> =
+  T extends 'AddWorkingGroupLeaderOpening' ? [number, InputValidationLengthConstraint] :
+  T extends 'FillWorkingGroupLeaderOpening' ? [number] :
+  T extends 'TerminateWorkingGroupLeaderRole' ? [InputValidationLengthConstraint] :
+  [];
+
+/* eslint-enable @typescript-eslint/indent */
+
+type ValidationSchemaFunc<FieldValuesT extends {}, ParamsT extends any[] = []> = (...params: ParamsT) =>
+({ [fieldK in keyof FieldValuesT]: Yup.Schema<any> });
+
 type ValidationType = {
-  All: {
-    title: Yup.StringSchema<string>;
-    rationale: Yup.StringSchema<string>;
-  };
-  Text: {
-    description: Yup.StringSchema<string>;
-  };
-  RuntimeUpgrade: {
-    WASM: Yup.MixedSchema<any>;
-  };
-  SetElectionParameters: {
-    announcingPeriod: Yup.NumberSchema<number>;
-    votingPeriod: Yup.NumberSchema<number>;
-    minVotingStake: Yup.NumberSchema<number>;
-    revealingPeriod: Yup.NumberSchema<number>;
-    minCouncilStake: Yup.NumberSchema<number>;
-    newTermDuration: Yup.NumberSchema<number>;
-    candidacyLimit: Yup.NumberSchema<number>;
-    councilSize: Yup.NumberSchema<number>;
-  };
-  Spending: {
-    tokens: Yup.NumberSchema<number>;
-    destinationAccount: Yup.StringSchema<string>;
-  };
-  SetLead: {
-    workingGroupLead: Yup.StringSchema<string>;
-  };
-  SetContentWorkingGroupMintCapacity: {
-    mintCapacity: Yup.NumberSchema<number>;
-  };
-  EvictStorageProvider: {
-    storageProvider: Yup.StringSchema<string | null>;
-  };
-  SetValidatorCount: {
-    maxValidatorCount: Yup.NumberSchema<number>;
-  };
-  SetStorageRoleParameters: {
-    min_stake: Yup.NumberSchema<number>;
-    min_actors: Yup.NumberSchema<number>;
-    max_actors: Yup.NumberSchema<number>;
-    reward: Yup.NumberSchema<number>;
-    reward_period: Yup.NumberSchema<number>;
-    bonding_period: Yup.NumberSchema<number>;
-    unbonding_period: Yup.NumberSchema<number>;
-    min_service_period: Yup.NumberSchema<number>;
-    startup_grace_period: Yup.NumberSchema<number>;
-    entry_request_fee: Yup.NumberSchema<number>;
-  };
-  AddWorkingGroupLeaderOpening: (currentBlock: number) => {
-    applicationsLimited: Yup.BooleanSchema<boolean>;
-    activateAt: Yup.StringSchema<string>;
-    activateAtBlock: Yup.NumberSchema<number>;
-    maxReviewPeriodLength: Yup.NumberSchema<number>;
-    maxApplications: Yup.NumberSchema<number>;
-    applicationStakeRequired: Yup.BooleanSchema<boolean>;
-    applicationStakeValue: Yup.NumberSchema<number>;
-    roleStakeRequired: Yup.BooleanSchema<boolean>;
-    roleStakeValue: Yup.NumberSchema<number>;
-    terminateRoleUnstakingPeriod: Yup.NumberSchema<number>;
-    leaveRoleUnstakingPeriod: Yup.NumberSchema<number>;
-    humanReadableText: Yup.StringSchema<string>;
-  };
+  [validationTypeK in ValidationTypeKeys]: ValidationSchemaFunc<
+  FormValuesByType<validationTypeK>,
+  ValidationSchemaFuncParamsByType<validationTypeK>
+  >
 };
 
 // Helpers for common validation
@@ -183,26 +179,26 @@ function minMaxInt (min: number, max: number, fieldName: string) {
 }
 
 const Validation: ValidationType = {
-  All: {
+  All: () => ({
     title: Yup.string()
       .required('Title is required!')
       .max(TITLE_MAX_LENGTH, `Title should be under ${TITLE_MAX_LENGTH} characters.`),
     rationale: Yup.string()
       .required('Rationale is required!')
       .max(RATIONALE_MAX_LENGTH, `Rationale should be under ${RATIONALE_MAX_LENGTH} characters.`)
-  },
-  Text: {
+  }),
+  Text: () => ({
     description: Yup.string()
       .required('Description is required!')
       .max(DESCRIPTION_MAX_LENGTH, `Description should be under ${DESCRIPTION_MAX_LENGTH}`)
-  },
-  RuntimeUpgrade: {
+  }),
+  RuntimeUpgrade: () => ({
     WASM: Yup.mixed()
       .test('fileArrayBuffer', 'Unexpected data format, file cannot be processed.', value => typeof value.byteLength !== 'undefined')
       .test('fileSizeMin', `Minimum file size is ${FILE_SIZE_BYTES_MIN} bytes.`, value => value.byteLength >= FILE_SIZE_BYTES_MIN)
       .test('fileSizeMax', `Maximum file size is ${FILE_SIZE_BYTES_MAX} bytes.`, value => value.byteLength <= FILE_SIZE_BYTES_MAX)
-  },
-  SetElectionParameters: {
+  }),
+  SetElectionParameters: () => ({
     announcingPeriod: Yup.number()
       .required('All fields must be filled!')
       .integer('This field must be an integer.')
@@ -273,8 +269,8 @@ const Validation: ValidationType = {
       .integer('This field must be an integer.')
       .min(COUNCIL_SIZE_MIN, errorMessage('The council size', COUNCIL_SIZE_MIN, COUNCIL_SIZE_MAX))
       .max(COUNCIL_SIZE_MAX, errorMessage('The council size', COUNCIL_SIZE_MIN, COUNCIL_SIZE_MAX))
-  },
-  Spending: {
+  }),
+  Spending: () => ({
     tokens: Yup.number()
       .positive('The token amount should be positive.')
       .integer('This field must be an integer.')
@@ -282,24 +278,19 @@ const Validation: ValidationType = {
       .required('You need to specify an amount of tokens.'),
     destinationAccount: Yup.string()
       .required('Select a destination account!')
-  },
-  SetLead: {
+  }),
+  SetLead: () => ({
     workingGroupLead: Yup.string().required('Select a proposed lead!')
-  },
-  SetContentWorkingGroupMintCapacity: {
-    mintCapacity: Yup.number()
+  }),
+  SetContentWorkingGroupMintCapacity: () => ({
+    capacity: Yup.number()
       .positive('Mint capacity should be positive.')
       .integer('This field must be an integer.')
       .min(MINT_CAPACITY_MIN, errorMessage('Mint capacity', MINT_CAPACITY_MIN, MINT_CAPACITY_MAX, CURRENCY_UNIT))
       .max(MINT_CAPACITY_MAX, errorMessage('Mint capacity', MINT_CAPACITY_MIN, MINT_CAPACITY_MAX, CURRENCY_UNIT))
       .required('You need to specify a mint capacity.')
-  },
-  EvictStorageProvider: {
-    storageProvider: Yup.string()
-      .nullable()
-      .required('Select a storage provider!')
-  },
-  SetValidatorCount: {
+  }),
+  SetValidatorCount: () => ({
     maxValidatorCount: Yup.number()
       .required('Enter the max validator count')
       .integer('This field must be an integer.')
@@ -311,84 +302,9 @@ const Validation: ValidationType = {
         MAX_VALIDATOR_COUNT_MAX,
         errorMessage('The max validator count', MAX_VALIDATOR_COUNT_MIN, MAX_VALIDATOR_COUNT_MAX)
       )
-  },
-  SetStorageRoleParameters: {
-    min_stake: Yup.number()
-      .required('All parameters are required')
-      .positive('The minimum stake should be positive.')
-      .integer('This field must be an integer.')
-      .max(MIN_STAKE_MAX, errorMessage('Minimum stake', MIN_STAKE_MIN, MIN_STAKE_MAX, CURRENCY_UNIT)),
-    min_actors: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(MIN_ACTORS_MIN, errorMessage('Minimum actors', MIN_ACTORS_MIN, MIN_ACTORS_MAX))
-      .max(MIN_ACTORS_MAX, errorMessage('Minimum actors', MIN_ACTORS_MIN, MIN_ACTORS_MAX)),
-    max_actors: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(MAX_ACTORS_MIN, errorMessage('Max actors', MAX_ACTORS_MIN, MAX_ACTORS_MAX))
-      .max(MAX_ACTORS_MAX, errorMessage('Max actors', MAX_ACTORS_MIN, MAX_ACTORS_MAX)),
-    reward: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(REWARD_MIN, errorMessage('Reward', REWARD_MIN, REWARD_MAX, CURRENCY_UNIT))
-      .max(REWARD_MAX, errorMessage('Reward', REWARD_MIN, REWARD_MAX, CURRENCY_UNIT)),
-    reward_period: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(REWARD_PERIOD_MIN, errorMessage('The reward period', REWARD_PERIOD_MIN, REWARD_PERIOD_MAX, 'blocks'))
-      .max(REWARD_PERIOD_MAX, errorMessage('The reward period', REWARD_PERIOD_MIN, REWARD_PERIOD_MAX, 'blocks')),
-    bonding_period: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(BONDING_PERIOD_MIN, errorMessage('The bonding period', BONDING_PERIOD_MIN, BONDING_PERIOD_MAX, 'blocks'))
-      .max(BONDING_PERIOD_MAX, errorMessage('The bonding period', BONDING_PERIOD_MIN, BONDING_PERIOD_MAX, 'blocks')),
-    unbonding_period: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(
-        UNBONDING_PERIOD_MIN,
-        errorMessage('The unbonding period', UNBONDING_PERIOD_MIN, UNBONDING_PERIOD_MAX, 'blocks')
-      )
-      .max(
-        UNBONDING_PERIOD_MAX,
-        errorMessage('The unbonding period', UNBONDING_PERIOD_MIN, UNBONDING_PERIOD_MAX, 'blocks')
-      ),
-    min_service_period: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(
-        MIN_SERVICE_PERIOD_MIN,
-        errorMessage('The minimum service period', MIN_SERVICE_PERIOD_MIN, MIN_SERVICE_PERIOD_MAX, 'blocks')
-      )
-      .max(
-        MIN_SERVICE_PERIOD_MAX,
-        errorMessage('The minimum service period', MIN_SERVICE_PERIOD_MIN, MIN_SERVICE_PERIOD_MAX, 'blocks')
-      ),
-    startup_grace_period: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(
-        STARTUP_GRACE_PERIOD_MIN,
-        errorMessage('The startup grace period', STARTUP_GRACE_PERIOD_MIN, STARTUP_GRACE_PERIOD_MAX, 'blocks')
-      )
-      .max(
-        STARTUP_GRACE_PERIOD_MAX,
-        errorMessage('The startup grace period', STARTUP_GRACE_PERIOD_MIN, STARTUP_GRACE_PERIOD_MAX, 'blocks')
-      ),
-    entry_request_fee: Yup.number()
-      .required('All parameters are required')
-      .integer('This field must be an integer.')
-      .min(
-        ENTRY_REQUEST_FEE_MIN,
-        errorMessage('The entry request fee', ENTRY_REQUEST_FEE_MIN, ENTRY_REQUEST_FEE_MAX, CURRENCY_UNIT)
-      )
-      .max(
-        STARTUP_GRACE_PERIOD_MAX,
-        errorMessage('The entry request fee', ENTRY_REQUEST_FEE_MIN, ENTRY_REQUEST_FEE_MAX, CURRENCY_UNIT)
-      )
-  },
-  AddWorkingGroupLeaderOpening: (currentBlock: number) => ({
+  }),
+  AddWorkingGroupLeaderOpening: (currentBlock: number, { min: HRTMin, max: HRTMax }: InputValidationLengthConstraint) => ({
+    workingGroup: Yup.string(),
     activateAt: Yup.string().required(),
     activateAtBlock: Yup.number()
       .when('activateAt', {
@@ -407,12 +323,14 @@ const Validation: ValidationType = {
         then: minMaxInt(MAX_APPLICATIONS_MIN, MAX_APPLICATIONS_MAX, 'Max. number of applications')
       }),
     applicationStakeRequired: Yup.boolean(),
+    applicationStakeMode: Yup.string(),
     applicationStakeValue: Yup.number()
       .when('applicationStakeRequired', {
         is: true,
         then: minMaxInt(APPLICATION_STAKE_VALUE_MIN, APPLICATION_STAKE_VALUE_MAX, 'Application stake value')
       }),
     roleStakeRequired: Yup.boolean(),
+    roleStakeMode: Yup.string(),
     roleStakeValue: Yup.number()
       .when('roleStakeRequired', {
         is: true,
@@ -450,6 +368,66 @@ 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(),
+    capacity: minMaxInt(WG_MINT_CAP_MIN, WG_MINT_CAP_MAX, 'Mint capacity')
+  }),
+  BeginReviewWorkingGroupLeaderApplication: () => ({
+    workingGroup: Yup.string(),
+    openingId: Yup.number().required('Select an opening!')
+  }),
+  FillWorkingGroupLeaderOpening: (currentBlock: number) => ({
+    workingGroup: Yup.string(),
+    openingId: Yup.number().required('Select an opening!'),
+    successfulApplicant: Yup.number().required('Select a succesful applicant!'),
+    includeReward: Yup.boolean(),
+    rewardAmount: Yup.number()
+      .when('includeReward', {
+        is: true,
+        then: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
+      }),
+    rewardNextBlock: Yup.number()
+      .when('includeReward', {
+        is: true,
+        then: minMaxInt(
+          MIN_NEXT_PAYMENT_BLOCK_MINUS_CURRENT + currentBlock,
+          MAX_NEXT_PAYMENT_BLOCK_MINUS_CURRENT + currentBlock,
+          'Next payment block'
+        )
+      }),
+    rewardRecurring: Yup.boolean(),
+    rewardInterval: Yup.number()
+      .when(['includeReward', 'rewardRecurring'], {
+        is: true,
+        then: minMaxInt(MIN_REWARD_INTERVAL, MAX_REWARD_INTERVAL, 'Reward interval')
+      })
+  }),
+  DecreaseWorkingGroupLeaderStake: () => ({
+    workingGroup: Yup.string(),
+    amount: Yup.number()
+      .required('Amount is required!')
+      .min(DECREASE_LEAD_STAKE_MIN, `Amount must be greater than ${DECREASE_LEAD_STAKE_MIN}`)
+  }),
+  SlashWorkingGroupLeaderStake: () => ({
+    workingGroup: Yup.string(),
+    amount: Yup.number()
+      .required('Amount is required!')
+      .min(SLASH_LEAD_STAKE_MIN, `Amount must be greater than ${SLASH_LEAD_STAKE_MIN}`)
+  }),
+  SetWorkingGroupLeaderReward: () => ({
+    workingGroup: Yup.string(),
+    amount: minMaxInt(MIN_REWARD_AMOUNT, MAX_REWARD_AMOUNT, 'Reward amount')
+  }),
+  TerminateWorkingGroupLeaderRole: ({ min, max }: 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`),
+    slashStake: Yup.boolean()
   })
 };
 

+ 19 - 20
pioneer/packages/joy-roles/src/classifiers.ts

@@ -7,7 +7,7 @@ import {
   Application,
   AcceptingApplications, ReviewPeriod,
   WaitingToBeingOpeningStageVariant,
-  ActiveOpeningStageVariant, ActiveOpeningStageKeys,
+  ActiveOpeningStageVariant,
   Opening,
   OpeningStageKeys,
   Deactivated, OpeningDeactivationCauseKeys,
@@ -133,25 +133,24 @@ async function classifyActiveOpeningStage (
   queryer: IBlockQueryer,
   stage: ActiveOpeningStageVariant
 ): Promise<OpeningStageClassification> {
-  switch (stage.stage.type) {
-    case ActiveOpeningStageKeys.AcceptingApplications:
-      return classifyActiveOpeningStageAcceptingApplications(
-        queryer,
-        stage.stage.value as AcceptingApplications
-      );
-
-    case ActiveOpeningStageKeys.ReviewPeriod:
-      return classifyActiveOpeningStageReviewPeriod(
-        opening,
-        queryer,
-        stage.stage.value as ReviewPeriod
-      );
-
-    case ActiveOpeningStageKeys.Deactivated:
-      return classifyActiveOpeningStageDeactivated(
-        queryer,
-        stage.stage.value as Deactivated
-      );
+  if (stage.stage.isOfType('AcceptingApplications')) {
+    return classifyActiveOpeningStageAcceptingApplications(
+      queryer,
+      stage.stage.asType('AcceptingApplications')
+    );
+  }
+  if (stage.stage.isOfType('ReviewPeriod')) {
+    return classifyActiveOpeningStageReviewPeriod(
+      opening,
+      queryer,
+      stage.stage.asType('ReviewPeriod')
+    );
+  }
+  if (stage.stage.isOfType('Deactivated')) {
+    return classifyActiveOpeningStageDeactivated(
+      queryer,
+      stage.stage.value as Deactivated
+    );
   }
 
   throw new Error('Unknown active opening stage: ' + stage.stage.type);

+ 18 - 8
pioneer/packages/joy-utils/src/MemberProfilePreview.tsx

@@ -25,23 +25,26 @@ const StyledProfilePreview = styled.div`
   }
 `;
 
-const Details = styled.div``;
+const Details = styled.div`
+  margin-left: 1rem;
+  display: grid;
+  grid-row-gap: 0.25rem;
+  grid-template-columns: 100%;
+`;
 
 const DetailsHandle = styled.h4`
   margin: 0;
-  margin-left: 1rem;
   font-weight: bold;
   color: #333;
 `;
 
 const DetailsID = styled.div`
-  margin: 0;
-  margin-top: 0.25rem;
-  margin-left: 1rem;
   color: #777;
 `;
 
-export default function ProfilePreview ({ id, avatar_uri, root_account, handle, link = false }: ProfileItemProps) {
+export default function ProfilePreview (
+  { id, avatar_uri, root_account, handle, link = false, children }: React.PropsWithChildren<ProfileItemProps>
+) {
   const Preview = (
     <StyledProfilePreview>
       {avatar_uri.toString() ? (
@@ -52,6 +55,7 @@ export default function ProfilePreview ({ id, avatar_uri, root_account, handle,
       <Details>
         <DetailsHandle>{handle.toString()}</DetailsHandle>
         { id !== undefined && <DetailsID>ID: {id.toString()}</DetailsID> }
+        { children }
       </Details>
     </StyledProfilePreview>
   );
@@ -69,7 +73,13 @@ type ProfilePreviewFromStructProps = {
   id?: number | MemberId;
 };
 
-export function ProfilePreviewFromStruct ({ profile, link, id }: ProfilePreviewFromStructProps) {
+export function ProfilePreviewFromStruct (
+  { profile, link, id, children }: React.PropsWithChildren<ProfilePreviewFromStructProps>
+) {
   const { avatar_uri, root_account, handle } = profile;
-  return <ProfilePreview {...{ avatar_uri, root_account, handle, link, id }} />;
+  return (
+    <ProfilePreview {...{ avatar_uri, root_account, handle, link, id }}>
+      {children}
+    </ProfilePreview>
+  );
 }

+ 99 - 14
pioneer/packages/joy-utils/src/consts/proposals.ts

@@ -3,12 +3,13 @@ import { ProposalType, ProposalMeta } from '../types/proposals';
 export const metadata: { [k in ProposalType]: ProposalMeta } = {
   EvictStorageProvider: {
     description: 'Evicting Storage Provider Proposal',
-    category: 'Storage',
+    category: 'Other',
     stake: 25000,
     approvalQuorum: 50,
     approvalThreshold: 75,
     slashingQuorum: 60,
-    slashingThreshold: 80
+    slashingThreshold: 80,
+    outdated: true
   },
   Text: {
     description: 'Signal Proposal',
@@ -21,12 +22,13 @@ export const metadata: { [k in ProposalType]: ProposalMeta } = {
   },
   SetStorageRoleParameters: {
     description: 'Set Storage Role Params Proposal',
-    category: 'Storage',
+    category: 'Other',
     stake: 100000,
     approvalQuorum: 66,
     approvalThreshold: 80,
     slashingQuorum: 60,
-    slashingThreshold: 80
+    slashingThreshold: 80,
+    outdated: true
   },
   SetValidatorCount: {
     description: 'Set Max Validator Count Proposal',
@@ -84,12 +86,75 @@ export const metadata: { [k in ProposalType]: ProposalMeta } = {
   },
   AddWorkingGroupLeaderOpening: {
     description: 'Add Working Group Leader Opening Proposal',
-    category: 'Other',
+    category: 'Working Groups',
     stake: 100000,
     approvalQuorum: 60,
     approvalThreshold: 80,
     slashingQuorum: 60,
     slashingThreshold: 80
+  },
+  SetWorkingGroupMintCapacity: {
+    description: 'Set Working Group Mint Capacity Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  BeginReviewWorkingGroupLeaderApplication: {
+    description: 'Begin Working Group Leader Applications Review Proposal',
+    category: 'Working Groups',
+    stake: 25000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  FillWorkingGroupLeaderOpening: {
+    description: 'Fill Working Group Leader Opening Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  DecreaseWorkingGroupLeaderStake: {
+    description: 'Decrease Working Group Leader Stake Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  SlashWorkingGroupLeaderStake: {
+    description: 'Slash Working Group Leader Stake Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  SetWorkingGroupLeaderReward: {
+    description: 'Set Working Group Leader Reward Proposal',
+    category: 'Working Groups',
+    stake: 50000,
+    approvalQuorum: 60,
+    approvalThreshold: 75,
+    slashingQuorum: 60,
+    slashingThreshold: 80
+  },
+  TerminateWorkingGroupLeaderRole: {
+    description: 'Terminate Working Group Leader Role Proposal',
+    category: 'Working Groups',
+    stake: 100000,
+    approvalQuorum: 66,
+    approvalThreshold: 80,
+    slashingQuorum: 60,
+    slashingThreshold: 80
   }
 };
 
@@ -97,19 +162,11 @@ type ProposalsApiMethodNames = {
   votingPeriod: string;
   gracePeriod: string;
 }
-export const apiMethods: { [k in ProposalType]: ProposalsApiMethodNames } = {
-  EvictStorageProvider: {
-    votingPeriod: 'evictStorageProviderProposalVotingPeriod',
-    gracePeriod: 'evictStorageProviderProposalPeriod'
-  },
+export const apiMethods: { [k in ProposalType]?: ProposalsApiMethodNames } = {
   Text: {
     votingPeriod: 'textProposalVotingPeriod',
     gracePeriod: 'textProposalGracePeriod'
   },
-  SetStorageRoleParameters: {
-    votingPeriod: 'setStorageRoleParametersProposalVotingPeriod',
-    gracePeriod: 'setStorageRoleParametersProposalGracePeriod'
-  },
   SetValidatorCount: {
     votingPeriod: 'setValidatorCountProposalVotingPeriod',
     gracePeriod: 'setValidatorCountProposalGracePeriod'
@@ -137,6 +194,34 @@ export const apiMethods: { [k in ProposalType]: ProposalsApiMethodNames } = {
   AddWorkingGroupLeaderOpening: {
     votingPeriod: 'addWorkingGroupOpeningProposalVotingPeriod',
     gracePeriod: 'addWorkingGroupOpeningProposalGracePeriod'
+  },
+  SetWorkingGroupMintCapacity: {
+    votingPeriod: 'setWorkingGroupMintCapacityProposalVotingPeriod',
+    gracePeriod: 'setWorkingGroupMintCapacityProposalGracePeriod'
+  },
+  BeginReviewWorkingGroupLeaderApplication: {
+    votingPeriod: 'beginReviewWorkingGroupLeaderApplicationsProposalVotingPeriod',
+    gracePeriod: 'beginReviewWorkingGroupLeaderApplicationsProposalGracePeriod'
+  },
+  FillWorkingGroupLeaderOpening: {
+    votingPeriod: 'fillWorkingGroupLeaderOpeningProposalVotingPeriod',
+    gracePeriod: 'fillWorkingGroupLeaderOpeningProposalGracePeriod'
+  },
+  DecreaseWorkingGroupLeaderStake: {
+    votingPeriod: 'decreaseWorkingGroupLeaderStakeProposalVotingPeriod',
+    gracePeriod: 'decreaseWorkingGroupLeaderStakeProposalGracePeriod'
+  },
+  SlashWorkingGroupLeaderStake: {
+    votingPeriod: 'slashWorkingGroupLeaderStakeProposalVotingPeriod',
+    gracePeriod: 'slashWorkingGroupLeaderStakeProposalGracePeriod'
+  },
+  SetWorkingGroupLeaderReward: {
+    votingPeriod: 'setWorkingGroupLeaderRewardProposalVotingPeriod',
+    gracePeriod: 'setWorkingGroupLeaderRewardProposalGracePeriod'
+  },
+  TerminateWorkingGroupLeaderRole: {
+    votingPeriod: 'terminateWorkingGroupLeaderRoleProposalVotingPeriod',
+    gracePeriod: 'terminateWorkingGroupLeaderRoleProposalGracePeriod'
   }
 } as const;
 

+ 2 - 2
pioneer/packages/joy-utils/src/consts/workingGroups.ts

@@ -1,4 +1,4 @@
-import { WorkingGroupKeys } from '@joystream/types/common';
-export const apiModuleByGroup: { [k in WorkingGroupKeys]: string } = {
+import { WorkingGroupKey } from '@joystream/types/common';
+export const apiModuleByGroup: { [k in WorkingGroupKey]: string } = {
   Storage: 'storageWorkingGroup'
 };

+ 22 - 0
pioneer/packages/joy-utils/src/functions/format.ts

@@ -0,0 +1,22 @@
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
+import { formatBalance } from '@polkadot/util';
+import { Option } from '@polkadot/types';
+import { RewardPolicy } from '@joystream/types/working-group';
+
+export const formatReward = (
+  {
+    amount_per_payout: amount,
+    payout_interval: interval,
+    next_payment_at_block
+  }: RewardRelationship | RewardPolicy,
+  showNextPaymentBlock = false
+) => {
+  const nextPaymentBlock = (next_payment_at_block instanceof Option)
+    ? next_payment_at_block.unwrapOr(null)
+    : next_payment_at_block;
+
+  return (
+    `${formatBalance(amount)}${interval.isSome ? ` / ${interval.unwrap()} block(s)` : ''}` +
+    ((showNextPaymentBlock && nextPaymentBlock) ? ` (Next payment: #${nextPaymentBlock})` : '')
+  );
+};

+ 102 - 0
pioneer/packages/joy-utils/src/react/components/working-groups/ApplicationDetails.tsx

@@ -0,0 +1,102 @@
+import React, { useState } from 'react';
+import { ParsedApplication } from '../../../types/workingGroups';
+import { ProfilePreviewFromStruct as MemberPreview } from '../../../MemberProfilePreview';
+import { useTransport, usePromise } from '../../hooks';
+import { Item, Label, Button } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+import { WorkingGroupKey } from '@joystream/types/common';
+import PromiseComponent from '../PromiseComponent';
+
+type ApplicationsDetailsProps = {
+  applications: ParsedApplication[];
+  acceptedIds?: number[];
+}
+
+export const ApplicationsDetails = ({ applications, acceptedIds }: ApplicationsDetailsProps) => {
+  const rejectedApplications = acceptedIds !== undefined ? applications.filter(a => !acceptedIds.includes(a.wgApplicationId)) : [];
+  const [showAll, setShowAll] = useState(!rejectedApplications.length);
+  const shownApplications = applications.filter(a => showAll || acceptedIds?.includes(a.wgApplicationId));
+  return (<>
+    <Item.Group>
+      {
+        shownApplications.map(({ member, stakes, wgApplicationId, humanReadableText }) => {
+          let HRT = humanReadableText.toString();
+          const accepted = acceptedIds?.includes(wgApplicationId);
+          try { HRT = JSON.stringify(JSON.parse(HRT), undefined, 4); } catch (e) { /* Do nothing */ }
+          return (
+            <Item key={wgApplicationId} style={{
+              background: 'white',
+              padding: '1em 1.5em',
+              boxShadow: `0 0 0.5rem 1px ${accepted ? '#21ba45' : (acceptedIds !== undefined ? '#db282899' : '#00000050')}`
+            }}>
+              <Item.Content>
+                <Item.Header><MemberPreview profile={member}></MemberPreview></Item.Header>
+                <Item.Meta>
+                  <Label>Application id: {wgApplicationId}</Label>
+                  { stakes.application > 0 && <Label>Appl. stake: {formatBalance(stakes.application)}</Label> }
+                  { stakes.role > 0 && <Label>Role stake: {formatBalance(stakes.role)}</Label> }
+                </Item.Meta>
+                <Item.Description>
+                  <pre style={{
+                    whiteSpace: 'pre-wrap',
+                    fontWeight: 'normal'
+                  }}>
+                    {HRT}
+                  </pre>
+                </Item.Description>
+              </Item.Content>
+            </Item>
+          );
+        })
+      }
+    </Item.Group>
+    {rejectedApplications.length > 0 && (
+      <Button fluid onClick={() => setShowAll(current => !current)}>
+        { showAll ? 'Hide rejected applications' : 'Show rejected applications' }
+      </Button>
+    )}
+  </>);
+};
+
+type ApplicationsDetailsByIdsProps = {
+  group: WorkingGroupKey;
+  ids: number[];
+  acceptedIds?: number[];
+};
+
+export const ApplicationsDetailsByIds = ({ group, ids, acceptedIds }: ApplicationsDetailsByIdsProps) => {
+  const transport = useTransport();
+  const [applications, error, loading] = usePromise<ParsedApplication[]>(
+    () => Promise.all(ids.map(id => transport.workingGroups.parsedApplicationById(group, id))),
+    [],
+    [ids]
+  );
+
+  return (
+    <PromiseComponent {...{ error, loading }} message="Fetching application(s)...">
+      <ApplicationsDetails applications={applications} acceptedIds={acceptedIds}/>
+    </PromiseComponent>
+  );
+};
+
+type ApplicationsDetailsByOpeningProps = {
+  group: WorkingGroupKey;
+  openingId: number;
+  acceptedIds?: number[];
+};
+
+export const ApplicationsDetailsByOpening = ({ group, openingId, acceptedIds }: ApplicationsDetailsByOpeningProps) => {
+  const transport = useTransport();
+  const [applications, error, loading] = usePromise<ParsedApplication[]>(
+    // Cannot filter by active, otherwise the details will be broken once opening is filled!
+    () => transport.workingGroups.openingApplications(group, openingId),
+    [],
+    [openingId]
+  );
+
+  return (
+    <PromiseComponent {...{ error, loading }} message="Fetching applications...">
+      <ApplicationsDetails applications={applications} acceptedIds={acceptedIds}/>
+    </PromiseComponent>
+  );
+};

+ 56 - 0
pioneer/packages/joy-utils/src/react/components/working-groups/LeadInfo.tsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import { WorkerData } from '../../../types/workingGroups';
+import { ProfilePreviewFromStruct as MemberPreview } from '../../../MemberProfilePreview';
+import { Label, Message } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+import { WorkingGroupKey } from '@joystream/types/common';
+import { useTransport, usePromise } from '../../hooks';
+import PromiseComponent from '../PromiseComponent';
+import { formatReward } from '@polkadot/joy-utils/functions/format';
+
+type LeadInfoProps = {
+  lead: WorkerData | null;
+  group?: WorkingGroupKey;
+  header?: boolean;
+  emptyMessage?: string;
+};
+
+export const LeadInfo = ({ lead, group, header = false, emptyMessage = 'NONE' }: LeadInfoProps) => (
+  <Message>
+    <Message.Content>
+      { header && <Message.Header>Current {group && `${group} `}Working Group lead:</Message.Header> }
+      <div style={{ padding: '0.5rem 0' }}>
+        { lead
+          ? (
+            <MemberPreview profile={lead.profile}>
+              <div>
+                <Label>Role stake: <b>{ lead.stake ? formatBalance(lead.stake) : 'NONE'}</b></Label>
+                <Label>Reward: <b>{ lead.reward ? formatReward(lead.reward) : 'NONE' }</b></Label>
+              </div>
+            </MemberPreview>
+          ) : emptyMessage
+        }
+      </div>
+    </Message.Content>
+  </Message>
+);
+
+type LeadInfoFromIdProps = {
+  leadId: number;
+  group: WorkingGroupKey;
+};
+
+export const LeadInfoFromId = ({ leadId, group }: LeadInfoFromIdProps) => {
+  const transport = useTransport();
+  const [lead, error, loading] = usePromise<WorkerData | null>(
+    () => transport.workingGroups.groupMemberById(group, leadId),
+    null,
+    [leadId]
+  );
+
+  return (
+    <PromiseComponent error={error} loading={loading} message="Fetching current lead...">
+      <LeadInfo lead={lead} group={group} header={false} emptyMessage="Leader no longer active!"/>
+    </PromiseComponent>
+  );
+};

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

@@ -2,7 +2,12 @@ import { useState, useEffect, useCallback } from 'react';
 
 export type UsePromiseReturnValues<T> = [T, any, boolean, () => Promise<void|null>];
 
-export default function usePromise<T> (promise: () => Promise<T>, defaultValue: T, dependsOn: any[] = []): UsePromiseReturnValues<T> {
+export default function usePromise<T> (
+  promise: () => Promise<T>,
+  defaultValue: T,
+  dependsOn: any[] = [],
+  onUpdate?: (newValue: T) => void
+): UsePromiseReturnValues<T> {
   const [state, setState] = useState<{
     value: T;
     error: any;
@@ -12,8 +17,22 @@ export default function usePromise<T> (promise: () => Promise<T>, defaultValue:
   let isSubscribed = true;
   const execute = useCallback(() => {
     return promise()
-      .then(value => (isSubscribed ? setState({ value, error: null, isPending: false }) : null))
-      .catch(error => (isSubscribed ? setState({ value: defaultValue, error: error, isPending: false }) : null));
+      .then(value => {
+        if (isSubscribed) {
+          setState({ value, error: null, isPending: false });
+          if (onUpdate) {
+            onUpdate(value);
+          }
+        }
+      })
+      .catch(error => {
+        if (isSubscribed) {
+          setState({ value: defaultValue, error: error, isPending: false });
+          if (onUpdate) {
+            onUpdate(defaultValue); // This should represent an empty value in most cases
+          }
+        }
+      });
   }, [promise]);
 
   useEffect(() => {

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

@@ -44,6 +44,18 @@ export default abstract class BaseTransport {
     return this.api.query.minting;
   }
 
+  protected get hiring () {
+    return this.api.query.hiring;
+  }
+
+  protected get stake () {
+    return this.api.query.stake;
+  }
+
+  protected get recurringRewards () {
+    return this.api.query.recurringRewards;
+  }
+
   protected queryMethodByName (name: string) {
     const [module, method] = name.split('.');
     return this.api.query[module][method];

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

@@ -154,15 +154,14 @@ export default class ProposalsTransport extends BaseTransport {
   }
 
   async parametersFromProposalType (type: ProposalType) {
-    const { votingPeriod: votingPeriodMethod, gracePeriod: gracePeriodMethod } = proposalsApiMethods[type];
-    // TODO: Remove the fallback after outdated proposals are removed
-    const votingPeriod = this.proposalsCodex[votingPeriodMethod]
-      ? ((await this.proposalsCodex[votingPeriodMethod]()) as u32).toNumber()
-      : 0;
-    const gracePeriod = this.proposalsCodex[gracePeriodMethod]
-      ? ((await this.proposalsCodex[gracePeriodMethod]()) as u32).toNumber()
-      : 0;
-    // Currently it's same for all types, but this will change soon
+    const methods = proposalsApiMethods[type];
+    let votingPeriod = 0;
+    let gracePeriod = 0;
+    if (methods) {
+      votingPeriod = ((await this.proposalsCodex[methods.votingPeriod]()) as u32).toNumber();
+      gracePeriod = ((await this.proposalsCodex[methods.gracePeriod]()) as u32).toNumber();
+    }
+    // Currently it's same for all types, but this will change soon (?)
     const cancellationFee = this.cancellationFee();
     return {
       type,

+ 140 - 15
pioneer/packages/joy-utils/src/transport/workingGroups.ts

@@ -1,12 +1,17 @@
 import { Option } from '@polkadot/types/';
+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 } from '@joystream/types/working-group';
+import { Worker, WorkerId, Opening as WGOpening, Application as WGApplication, OpeningTypeKey } from '@joystream/types/working-group';
 import { apiModuleByGroup } from '../consts/workingGroups';
-import { WorkingGroupKeys } from '@joystream/types/common';
-import { LeadWithProfile } from '../types/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';
 
 export default class WorkingGroupsTransport extends BaseTransport {
   private membersT: MembersTransport;
@@ -16,32 +21,152 @@ export default class WorkingGroupsTransport extends BaseTransport {
     this.membersT = membersTransport;
   }
 
-  protected queryByGroup (group: WorkingGroupKeys) {
+  protected queryByGroup (group: WorkingGroupKey) {
     const module = apiModuleByGroup[group];
     return this.api.query[module];
   }
 
-  public async currentLead (group: WorkingGroupKeys): Promise <LeadWithProfile | null> {
+  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;
+
+    if (!worker.is_active) {
+      return null;
+    }
+
+    const stake = worker.role_stake_profile.isSome
+      ? (await this.stakeValue(worker.role_stake_profile.unwrap().stake_id)).toNumber()
+      : undefined;
+
+    const reward = worker.reward_relationship.isSome
+      ? (await this.rewardRelationship(worker.reward_relationship.unwrap()))
+      : undefined;
+
+    const profile = await this.membersT.expectedMemberProfile(worker.member_id);
+
+    return { group, workerId, worker, profile, stake, reward };
+  }
+
+  public async currentLead (group: WorkingGroupKey): Promise<WorkerData | null> {
     const optLeadId = (await this.queryByGroup(group).currentLead()) as Option<WorkerId>;
 
     if (!optLeadId.isSome) {
       return null;
     }
 
-    const leadWorkerId = optLeadId.unwrap();
-    const leadWorkerLink = new SingleLinkedMapEntry(
-      Worker,
-      await this.queryByGroup(group).workerById(leadWorkerId)
-    );
-    const leadWorker = leadWorkerLink.value;
+    const leadWorkerId = optLeadId.unwrap().toNumber();
 
-    if (!leadWorker.is_active) {
-      return null;
+    return this.groupMemberById(group, leadWorkerId);
+  }
+
+  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 { 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));
+  }
+
+  public async activeOpenings (group: WorkingGroupKey, substage?: ActiveOpeningStageKey, type?: OpeningTypeKey) {
+    return (await this.allOpenings(group, type))
+      .filter(od =>
+        od.hiringOpening.stage.isOfType('Active') &&
+        (!substage || od.hiringOpening.stage.asType('Active').stage.isOfType(substage))
+      );
+  }
+
+  async wgApplicationById (group: WorkingGroupKey, wgApplicationId: number | ApplicationId): Promise<WGApplication> {
+    const nextAppId = await this.queryByGroup(group).nextApplicationId() as ApplicationId;
+
+    if (wgApplicationId < 0 || wgApplicationId >= nextAppId.toNumber()) {
+      throw new Error(`Invalid working group application 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;
+  }
+
+  protected async stakeValue (stakeId: StakeId): Promise<Balance> {
+    return new SingleLinkedMapEntry(
+      Stake,
+      await this.stake.stakes(stakeId)
+    ).value.value;
+  }
+
+  protected async rewardRelationship (relationshipId: RewardRelationshipId): Promise<RewardRelationship> {
+    return new SingleLinkedMapEntry(
+      RewardRelationship,
+      await this.recurringRewards.rewardRelationships(relationshipId)
+    ).value;
+  }
+
+  protected async parseApplication (wgApplicationId: number, wgApplication: WGApplication): Promise<ParsedApplication> {
+    const appId = wgApplication.application_id;
+    const application = await this.hiringApplicationById(appId);
+
+    const { active_role_staking_id: roleStakingId, active_application_staking_id: appStakingId } = application;
+
     return {
-      worker: leadWorker,
-      profile: await this.membersT.expectedMemberProfile(leadWorker.member_id)
+      wgApplicationId,
+      applicationId: appId.toNumber(),
+      member: await this.membersT.expectedMemberProfile(wgApplication.member_id),
+      roleAccout: wgApplication.role_account_id,
+      stakes: {
+        application: appStakingId.isSome ? (await this.stakeValue(appStakingId.unwrap())).toNumber() : 0,
+        role: roleStakingId.isSome ? (await this.stakeValue(roleStakingId.unwrap())).toNumber() : 0
+      },
+      humanReadableText: application.human_readable_text.toString(),
+      stage: application.stage
     };
   }
+
+  async parsedApplicationById (group: WorkingGroupKey, wgApplicationId: number): Promise<ParsedApplication> {
+    const wgApplication = await this.wgApplicationById(group, wgApplicationId);
+    return this.parseApplication(wgApplicationId, wgApplication);
+  }
+
+  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;
+  }
+
+  async openingActiveApplications (group: WorkingGroupKey, wgOpeningId: number): Promise<ParsedApplication[]> {
+    return (await this.openingApplications(group, wgOpeningId))
+      .filter(a => a.stage.isOfType('Active'));
+  }
 }

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

@@ -13,7 +13,14 @@ export const ProposalTypes = [
   'EvictStorageProvider',
   'SetValidatorCount',
   'SetStorageRoleParameters',
-  'AddWorkingGroupLeaderOpening'
+  'AddWorkingGroupLeaderOpening',
+  'SetWorkingGroupMintCapacity',
+  'BeginReviewWorkingGroupLeaderApplication',
+  'FillWorkingGroupLeaderOpening',
+  'SlashWorkingGroupLeaderStake',
+  'DecreaseWorkingGroupLeaderStake',
+  'SetWorkingGroupLeaderReward',
+  'TerminateWorkingGroupLeaderRole'
 ] as const;
 
 export type ProposalType = typeof ProposalTypes[number];
@@ -53,10 +60,10 @@ export type ProposalVotes = {
 };
 
 export const Categories = {
-  storage: 'Storage',
   council: 'Council',
   validators: 'Validators',
   cwg: 'Content Working Group',
+  wg: 'Working Groups',
   other: 'Other'
 } as const;
 
@@ -70,6 +77,7 @@ export type ProposalMeta = {
   approvalThreshold: number;
   slashingQuorum: number;
   slashingThreshold: number;
+  outdated?: boolean;
 }
 
 export type ParsedPost = {

+ 29 - 2
pioneer/packages/joy-utils/src/types/workingGroups.ts

@@ -1,7 +1,34 @@
-import { Worker } from '@joystream/types/working-group';
+import { Worker, Opening as WGOpening } from '@joystream/types/working-group';
 import { Profile } from '@joystream/types/members';
+import { OpeningId, Opening, ApplicationStage } from '@joystream/types/hiring';
+import { AccountId } from '@polkadot/types/interfaces';
+import { WorkingGroupKey } from '@joystream/types/common';
+import { RewardRelationship } from '@joystream/types/recurring-rewards';
 
-export type LeadWithProfile = {
+export type WorkerData = {
+  workerId: number;
   worker: Worker;
   profile: Profile;
+  stake?: number;
+  reward?: RewardRelationship;
+  group: WorkingGroupKey;
 };
+
+export type OpeningData = {
+  id: OpeningId;
+  opening: WGOpening;
+  hiringOpening: Opening;
+}
+
+export type ParsedApplication = {
+  wgApplicationId: number;
+  applicationId: number;
+  member: Profile;
+  roleAccout: AccountId;
+  stakes: {
+    application: number;
+    role: number;
+  };
+  humanReadableText: string;
+  stage: ApplicationStage;
+}

+ 8 - 1
types/src/common.ts

@@ -96,6 +96,13 @@ export class InputValidationLengthConstraint extends JoyStruct<InputValidationLe
       }, value);
     }
 
+    static createWithMaxAllowed() {
+      return new InputValidationLengthConstraint({
+        min: new u16(1),
+        max_min_diff: new u16(65534) // Max allowed without causing u16 overflow
+      })
+    }
+
     get min (): u16 {
       return this.getField('min');
     }
@@ -112,7 +119,7 @@ export class InputValidationLengthConstraint extends JoyStruct<InputValidationLe
 export const WorkingGroupDef = {
   Storage: Null
 } as const;
-export type WorkingGroupKeys = keyof typeof WorkingGroupDef;
+export type WorkingGroupKey = keyof typeof WorkingGroupDef;
 export class WorkingGroup extends JoyEnum(WorkingGroupDef) { };
 
 export function registerCommonTypes() {

+ 14 - 25
types/src/hiring/index.ts

@@ -91,23 +91,17 @@ export class InactiveApplicationStage extends JoyStruct<InactiveApplicationStage
 
 export class ActiveApplicationStage extends Null { };
 
+// TODO: Find usages and replace with "JoyEnum-standard"
 export enum ApplicationStageKeys {
   Active = 'Active',
   Unstaking = 'Unstaking',
   Inactive = 'Inactive',
 }
-
-export class ApplicationStage extends Enum {
-  constructor(value?: any, index?: number) {
-    super(
-      {
-        [ApplicationStageKeys.Active]: ActiveApplicationStage,
-        [ApplicationStageKeys.Unstaking]: UnstakingApplicationStage,
-        [ApplicationStageKeys.Inactive]: InactiveApplicationStage,
-      },
-      value, index);
-  }
-}
+export class ApplicationStage extends JoyEnum({
+  Active: ActiveApplicationStage,
+  Unstaking: UnstakingApplicationStage,
+  Inactive: InactiveApplicationStage
+} as const) { };
 
 export type IApplicationRationingPolicy = {
   max_active_applicants: u32,
@@ -231,14 +225,14 @@ export class Deactivated extends JoyStruct<IDeactivated> {
   }
 };
 
-// TODO: Find usages and replace them with JoyEnum helpers
-export enum ActiveOpeningStageKeys {
-  AcceptingApplications = 'AcceptingApplications',
-  ReviewPeriod = 'ReviewPeriod',
-  Deactivated = 'Deactivated',
-}
+export const ActiveOpeningStageDef = {
+  AcceptingApplications: AcceptingApplications,
+  ReviewPeriod: ReviewPeriod,
+  Deactivated: Deactivated
+} as const;
+export type ActiveOpeningStageKey = keyof typeof ActiveOpeningStageDef;
 
-export class ActiveOpeningStage extends JoyEnum({AcceptingApplications, ReviewPeriod, Deactivated} as const) { }
+export class ActiveOpeningStage extends JoyEnum(ActiveOpeningStageDef) { }
 
 export type ActiveOpeningStageVariantType = {
   stage: ActiveOpeningStage,
@@ -263,12 +257,7 @@ export class ActiveOpeningStageVariant extends JoyStruct<ActiveOpeningStageVaria
   }
 
   get is_active(): boolean {
-    switch (this.stage.type) {
-      case ActiveOpeningStageKeys.AcceptingApplications:
-        return true
-    }
-
-	  return false
+    return this.stage.isOfType('AcceptingApplications');
   }
 }
 

+ 13 - 2
types/src/working-group/index.ts

@@ -243,10 +243,12 @@ export class WorkingGroupOpeningPolicyCommitment extends JoyStruct<IWorkingGroup
 
 export class OpeningType_Leader extends Null { };
 export class OpeningType_Worker extends Null { };
-export class OpeningType extends JoyEnum({
+export const OpeningTypeDef = {
   Leader: OpeningType_Leader,
   Worker: OpeningType_Worker
-} as const) { };
+} as const;
+export type OpeningTypeKey = keyof typeof OpeningTypeDef;
+export class OpeningType extends JoyEnum(OpeningTypeDef) { };
 
 export type IOpening = {
   hiring_opening_id: OpeningId,
@@ -300,6 +302,15 @@ export class RewardPolicy extends JoyStruct<IRewardPolicy> {
       payout_interval: 'Option<BlockNumber>',
     }, value);
   }
+  get amount_per_payout(): Balance {
+    return this.getField<Balance>('amount_per_payout');
+  }
+  get next_payment_at_block(): BlockNumber {
+    return this.getField<BlockNumber>('next_payment_at_block');
+  }
+  get payout_interval(): Option<BlockNumber> {
+    return this.getField<Option<BlockNumber>>('payout_interval');
+  }
 };
 
 export function registerWorkingGroupTypes() {