Browse Source

Propose tech committee motions (#1996)

* Propose tech committe motions

* bug, ignore .vscode

* linting

* Some cleanups

* Move Propose back
Keith Ingram 5 years ago
parent
commit
a69bdfeef1

+ 1 - 0
.gitignore

@@ -14,3 +14,4 @@ npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
 .idea/
+.vscode/

+ 2 - 5
packages/app-tech-comm/src/Overview/Summary.tsx

@@ -4,7 +4,7 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { I18nProps } from '@polkadot/react-components/types';
-import { AccountId, Hash } from '@polkadot/types/interfaces';
+import { ComponentProps } from '../types';
 
 import React from 'react';
 import { SummaryBox, CardSummary } from '@polkadot/react-components';
@@ -14,10 +14,7 @@ import { formatNumber } from '@polkadot/util';
 
 import translate from '../translate';
 
-interface Props extends I18nProps {
-  members?: AccountId[];
-  proposals?: Hash[];
-}
+interface Props extends ComponentProps, I18nProps {}
 
 function Summary ({ className, members, proposals, t }: Props): React.ReactElement<Props> {
   const { api } = useApi();

+ 7 - 12
packages/app-tech-comm/src/Overview/index.tsx

@@ -2,32 +2,27 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { AccountId, Hash } from '@polkadot/types/interfaces';
+import { I18nProps } from '@polkadot/react-components/types';
+import { ComponentProps } from '../types';
 
 import React from 'react';
 
+import translate from '../translate';
 import Members from './Members';
 import Summary from './Summary';
 
-interface Props {
-  className?: string;
-  members?: AccountId[];
-  proposals?: Hash[];
-}
+interface Props extends I18nProps, ComponentProps {}
 
-export default function Overview ({ className, members, proposals }: Props): React.ReactElement<Props> {
+function Overview ({ className, members, proposals }: Props): React.ReactElement<Props> {
   return (
     <div className={className}>
       <Summary
         members={members}
         proposals={proposals}
       />
-      {/* <Button.Group>
-        <SubmitCandidacy electionsInfo={electionsInfo} />
-        <Button.Or />
-        <Vote electionsInfo={electionsInfo} />
-      </Button.Group> */}
       <Members members={members} />
     </div>
   );
 }
+
+export default translate(Overview);

+ 0 - 17
packages/app-tech-comm/src/Overview/types.ts

@@ -1,17 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-democracy authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import { SetIndex } from '@polkadot/types/interfaces';
-import { DerivedElectionsInfo } from '@polkadot/api-derive/types';
-
-import BN from 'bn.js';
-
-export interface ComponentProps {
-  electionsInfo: DerivedElectionsInfo;
-}
-
-export interface VoterPosition {
-  setIndex: SetIndex;
-  globalIndex: BN;
-}

+ 3 - 3
packages/app-tech-comm/src/Proposals/Proposal.tsx

@@ -21,14 +21,14 @@ interface Props extends I18nProps {
 
 function Proposal ({ className, hash, t }: Props): React.ReactElement<Props> | null {
   const { api } = useApi();
-  const _proposal = trackStream<Option<ProposalType>>(api.query.technicalCommittee.proposalOf, [hash]);
+  const optProposal = trackStream<Option<ProposalType>>(api.query.technicalCommittee.proposalOf, [hash]);
   const votes = trackStream<Option<Votes>>(api.query.technicalCommittee.voting, [hash]);
 
-  if (!votes?.isSome) {
+  if (!optProposal?.isSome || !votes?.isSome) {
     return null;
   }
 
-  const proposal = _proposal?.unwrapOr(null);
+  const proposal = optProposal.unwrap();
   const { ayes, index, nays, threshold } = votes.unwrap();
 
   return (

+ 83 - 0
packages/app-tech-comm/src/Proposals/Propose.tsx

@@ -0,0 +1,83 @@
+// Copyright 2017-2019 @polkadot/ui-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/react-components/types';
+import { TxSource, TxDef } from '@polkadot/react-hooks/types';
+import { Call, Proposal } from '@polkadot/types/interfaces';
+
+import BN from 'bn.js';
+import React, { useEffect, useState } from 'react';
+import { registry } from '@polkadot/react-api';
+import { Extrinsic, InputNumber, TxModalNew as TxModal } from '@polkadot/react-components';
+import { useApi, useTx } from '@polkadot/react-hooks';
+import { createType } from '@polkadot/types';
+
+import translate from '../translate';
+
+interface Props extends I18nProps {
+  memberCount?: number;
+  onClose: () => void;
+}
+
+function Propose ({ t, onClose, memberCount = 0 }: Props): React.ReactElement<Props> {
+  const _hasThreshold = (threshold?: BN | null): boolean =>
+    !!threshold && !threshold.isZero() && threshold.lten(memberCount);
+
+  const { apiDefaultTxSudo } = useApi();
+  const [proposal, setProposal] = useState<Proposal | null>(null);
+  const [[threshold, hasThreshold], setThreshold] = useState<[BN | null, boolean]>([
+    new BN(memberCount / 2 + 1),
+    true
+  ]);
+
+  // FIXME Rework this, unless you know, you can never figure out what all these options mean here
+  const txState = useTx(
+    (): TxSource<TxDef> => [
+      [
+        'technicalCommittee.propose',
+        [threshold, proposal]
+      ],
+      !!proposal && hasThreshold
+    ],
+    [memberCount, proposal, threshold, hasThreshold],
+    {}
+  );
+
+  useEffect((): void => {
+    setThreshold([threshold, _hasThreshold(threshold)]);
+  }, [memberCount]);
+
+  const _onChangeExtrinsic = (method?: Call): void =>
+    setProposal(method ? createType(registry, 'Proposal', method) : null);
+  const _onChangeThreshold = (threshold?: BN): void =>
+    setThreshold([threshold || null, _hasThreshold(threshold)]);
+
+  return (
+    <TxModal
+      isOpen
+      onClose={onClose}
+      {...txState}
+      header={t('Propose a committee motion')}
+    >
+      <InputNumber
+        className='medium'
+        label={t('threshold')}
+        help={t('The minimum number of committee votes required to approve this motion')}
+        isError={!hasThreshold}
+        onChange={_onChangeThreshold}
+        onEnter={txState.sendTx}
+        placeholder={t('Positive number between 1 and {{memberCount}}', { replace: { memberCount } })}
+        value={threshold || undefined}
+      />
+      <Extrinsic
+        defaultValue={apiDefaultTxSudo}
+        label={t('proposal')}
+        onChange={_onChangeExtrinsic}
+        onEnter={txState.sendTx}
+      />
+    </TxModal>
+  );
+}
+
+export default translate(Propose);

+ 25 - 9
packages/app-tech-comm/src/Proposals/index.tsx

@@ -2,23 +2,39 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { Hash } from '@polkadot/types/interfaces';
 import { I18nProps } from '@polkadot/react-components/types';
+import { Hash } from '@polkadot/types/interfaces';
+import { ComponentProps } from '../types';
 
-import React from 'react';
-import { Table } from '@polkadot/react-components';
+import React, { useState } from 'react';
+import { Button, Table } from '@polkadot/react-components';
 
-import Proposal from './Proposal';
 import translate from '../translate';
+import Proposal from './Proposal';
+import Propose from './Propose';
 
-interface Props extends I18nProps {
-  proposals?: Hash[];
-}
+interface Props extends ComponentProps, I18nProps {}
+
+function Proposals ({ className, members, proposals, t }: Props): React.ReactElement<Props> {
+  const [isProposeOpen, setIsProposeOpen] = useState(false);
+  const _toggleProposal = (): void => setIsProposeOpen(!isProposeOpen);
 
-function Proposals ({ className, proposals, t }: Props): React.ReactElement<Props> {
   return (
     <div className={className}>
-      {/* <Propose /> */}
+      {isProposeOpen && (
+        <Propose
+          memberCount={members?.length}
+          onClose={_toggleProposal}
+        />
+      )}
+      <Button.Group>
+        <Button
+          isPrimary
+          label={t('Submit proposal')}
+          icon='add'
+          onClick={_toggleProposal}
+        />
+      </Button.Group>
       {proposals?.length
         ? (
           <Table>

+ 4 - 1
packages/app-tech-comm/src/index.tsx

@@ -43,7 +43,10 @@ function App ({ basePath, className, t }: Props): React.ReactElement<Props> {
       </header>
       <Switch>
         <Route path={`${basePath}/proposals`}>
-          <Proposals proposals={proposals} />
+          <Proposals
+            members={members}
+            proposals={proposals}
+          />
         </Route>
         <Route path={basePath}>
           <Overview

+ 11 - 0
packages/app-tech-comm/src/types.ts

@@ -0,0 +1,11 @@
+// Copyright 2017-2019 @polkadot/app-democracy authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { AccountId, Hash } from '@polkadot/types/interfaces';
+
+export interface ComponentProps {
+  className?: string;
+  proposals?: Hash[];
+  members?: AccountId[];
+}

+ 2 - 2
packages/react-components/src/InputNumber.tsx

@@ -213,8 +213,8 @@ function InputNumber (props: Props): React.ReactElement<Props> {
   }, [value, si, bitLength, maxValue]);
 
   useEffect((): void => {
-    onChange && isValid && onChange(valueBn);
-  }, [isValid, valueBn]);
+    onChange && onChange(valueBn);
+  }, [valueBn]);
 
   const _onChange = (input: string): void => {
     setValues(getValuesFromString(input, si, props));

+ 8 - 4
packages/react-components/src/TxModalNew.tsx

@@ -19,7 +19,7 @@ function TxModal<P extends Props> ({
   isSending,
   trigger: Trigger,
   header = t('Submit signed extrinsic'),
-  content,
+  children,
   preContent,
   isDisabled = false,
   isSubmittable = true,
@@ -49,6 +49,11 @@ function TxModal<P extends Props> ({
     props.onClose && props.onClose();
   };
 
+  const onSend = (): void => {
+    sendTx();
+    onClose();
+  };
+
   // const onStart = (): void => {
   //   props.onSubmit && props.onSubmit();
   // };
@@ -108,7 +113,7 @@ function TxModal<P extends Props> ({
             type='account'
             {...inputAddressProps}
           />
-          {content}
+          {children}
         </Modal.Content>
         <Modal.Actions>
           <Button.Group>
@@ -120,12 +125,11 @@ function TxModal<P extends Props> ({
             />
             <Button.Or />
             <Button
-              isLoading={isSending}
               isDisabled={isDisabled || isSending || !accountId || !isSubmittable}
               isPrimary
               label={submitButtonLabel}
               icon={submitButtonIcon}
-              onClick={sendTx}
+              onClick={onSend}
               {...submitButtonProps}
             />
           </Button.Group>

+ 1 - 2
packages/react-components/src/types.ts

@@ -113,8 +113,7 @@ export interface TxModalProps extends I18nProps, TxState {
   isDisabled?: boolean;
   isOpen?: boolean;
   isUnsigned?: boolean;
-  isSubmittable?: boolean;
-  content: React.ReactNode;
+  children: React.ReactNode;
   preContent?: React.ReactNode;
   trigger?: TxTrigger;
   onSubmit?: () => void;

+ 6 - 1
packages/react-hooks/src/types.ts

@@ -7,7 +7,11 @@ import { AccountId, Balance, BlockNumber, Call, Hash, SessionIndex } from '@polk
 import { IExtrinsic } from '@polkadot/types/types';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
 
-export type TxSources = SubmittableExtrinsic | IExtrinsic | Call | [string, any[] | ConstructTxFn] | null;
+export type TxDef = [string, any[] | ConstructTxFn];
+
+export type TxDefs = SubmittableExtrinsic | IExtrinsic | Call | TxDef | null;
+
+export type TxSource<T extends TxDefs> = [T, boolean];
 
 export interface Slash {
   accountId: AccountId;
@@ -25,6 +29,7 @@ export interface SessionRewards {
 
 export interface ExtrinsicAndSenders {
   extrinsic: SubmittableExtrinsic | null;
+  isSubmittable: boolean;
   sendTx: () => void;
   sendUnsigned: () => void;
 }

+ 38 - 46
packages/react-hooks/src/useTx.ts

@@ -4,7 +4,7 @@
 
 import { StringOrNull } from '@polkadot/react-components/types';
 import { Call } from '@polkadot/types/interfaces';
-import { ExtrinsicAndSenders, TxSources, TxProps, TxState } from './types';
+import { ExtrinsicAndSenders, TxDef, TxDefs, TxSource, TxProps, TxState } from './types';
 
 import { useContext, useMemo, useState } from 'react';
 import { ApiPromise } from '@polkadot/api';
@@ -13,82 +13,73 @@ import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
 import { assert, isFunction } from '@polkadot/util';
 import useApi from './useApi';
 
-function getExtrinsic<T extends TxSources> (api: ApiPromise, tx: T): SubmittableExtrinsic | null {
-  if (!tx) {
+function getExtrinsic<T extends TxDefs> (api: ApiPromise, txDef: T): SubmittableExtrinsic | null {
+  if (!txDef) {
     return null;
   }
-  if (Array.isArray(tx)) {
-    const [section, method] = tx[0].split('.');
+
+  if (Array.isArray(txDef)) {
+    const [section, method] = txDef[0].split('.');
 
     assert(api.tx[section] && api.tx[section][method], `Unable to find api.tx.${section}.${method}`);
     try {
       return api.tx[section][method](...(
-        isFunction(tx[1])
-          ? tx[1]()
-          : tx[1]
+        isFunction(txDef[1])
+          ? txDef[1]()
+          : txDef[1]
       )) as any as SubmittableExtrinsic;
     } catch (e) {
       return null;
     }
   } else {
-    if ((tx as Call).callIndex) {
-      const fn = api.findCall(tx.callIndex);
+    if ((txDef as Call).callIndex) {
+      const fn = api.findCall(txDef.callIndex);
 
-      return api.tx[fn.section][fn.method](...tx.args);
+      return api.tx[fn.section][fn.method](...txDef.args);
     }
 
-    return tx as any as SubmittableExtrinsic;
+    return txDef as any as SubmittableExtrinsic;
   }
 }
 
-export default function useTx<T extends TxSources> (source: T, { accountId: anAccountId, onChangeAccountId, onStart, onSuccess, onFailed, onUpdate }: TxProps = {}): TxState {
+export default function useTx<T extends TxDef> (memoFn: (...args: any[]) => TxSource<T>, memoArr: any[], { accountId: anAccountId, onChangeAccountId, onStart, onSuccess, onFailed, onUpdate }: TxProps = {}): TxState {
   const { api } = useApi();
   const { queueExtrinsic } = useContext(StatusContext);
 
+  const txSource = useMemo(memoFn, memoArr);
+
   const [accountId, setAccountId] = useState<StringOrNull>(anAccountId || null);
   const [isSending, setIsSending] = useState(false);
 
-  const _onStart = useMemo(
-    (): () => void => (): void => {
-      setIsSending(true);
+  const _onStart = (): void => {
+    setIsSending(true);
 
-      onStart && onStart();
-    },
-    [onStart]
-  );
+    onStart && onStart();
+  };
 
-  const _onSuccess = useMemo(
-    (): () => void => (): void => {
-      setIsSending(false);
+  const _onSuccess = (): void => {
+    setIsSending(false);
 
-      onSuccess && onSuccess();
-    },
-    [onSuccess]
-  );
+    onSuccess && onSuccess();
+  };
 
-  const _onFailed = useMemo(
-    (): () => void => (): void => {
-      setIsSending(false);
+  const _onFailed = (): void => {
+    setIsSending(false);
 
-      onFailed && onFailed();
-    },
-    [onFailed]
-  );
+    onFailed && onFailed();
+  };
 
-  const _onUpdate = useMemo(
-    (): () => void => (): void => {
-      setIsSending(false);
+  const _onUpdate = (): void => {
+    setIsSending(false);
 
-      onUpdate && onUpdate();
-    },
-    [onUpdate]
-  );
+    onUpdate && onUpdate();
+  };
 
-  function getExtrinsicAndSenders (api: ApiPromise, accountId: StringOrNull, tx: T): ExtrinsicAndSenders {
-    const extrinsic = getExtrinsic<T>(api, tx);
+  function getExtrinsicAndSenders (api: ApiPromise, accountId: StringOrNull, [txDef, isSubmittable]: TxSource<T>): ExtrinsicAndSenders {
+    const extrinsic = getExtrinsic<T>(api, txDef);
 
     function _sendTx (isUnsigned?: boolean): void {
-      !!extrinsic && queueExtrinsic({
+      !!extrinsic && isSubmittable && queueExtrinsic({
         accountId,
         extrinsic,
         isUnsigned,
@@ -101,14 +92,15 @@ export default function useTx<T extends TxSources> (source: T, { accountId: anAc
 
     return {
       extrinsic,
+      isSubmittable: !!accountId && !!extrinsic && isSubmittable,
       sendTx: (): void => _sendTx(),
       sendUnsigned: (): void => _sendTx(true)
     };
   }
 
   const txAndSenders = useMemo<ExtrinsicAndSenders>(
-    (): ExtrinsicAndSenders => getExtrinsicAndSenders(api, accountId, source),
-    [accountId, source]
+    (): ExtrinsicAndSenders => getExtrinsicAndSenders(api, accountId, txSource),
+    [accountId, txSource]
   );
 
   return {