Propose tech committee motions (#1996)

* Propose tech committe motions

* bug, ignore .vscode

* linting

* Some cleanups

* Move Propose back
This commit is contained in:
Keith Ingram 2019-12-04 06:22:19 -05:00 committed by Jaco Greeff
parent f4e2c78cf9
commit a69bdfeef1
14 changed files with 191 additions and 102 deletions

1
.gitignore vendored
View File

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

View File

@ -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();

View File

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

View File

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

View File

@ -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 (

View File

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

View File

@ -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>

View File

@ -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

View File

@ -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[];
}

View File

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

View File

@ -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>

View File

@ -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;

View File

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

View File

@ -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 {