Council motions (#1358)
* council motions * continue * council motions > propose * travis ci * latest api * add council votes, proposal views * linting * remove settings * new yarn.lock * base-x * Only pick text-transform from Labelled changes * collapse voting view, aye/yay * move button * lint * fixes * approve <details> tag * aye/nay lists
This commit is contained in:
parent
8b41c495d6
commit
e691868d76
171
packages/app-council/src/Motions/Motion.tsx
Normal file
171
packages/app-council/src/Motions/Motion.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
// 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 { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { Option, Proposal, Votes } from '@polkadot/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ActionItem, InputAddress, Labelled, Voting } from '@polkadot/ui-app';
|
||||
import { withCalls, withMulti } from '@polkadot/ui-api';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
chain_bestNumber?: BN,
|
||||
hash: string,
|
||||
proposal: Proposal | null,
|
||||
votes: Votes | null
|
||||
};
|
||||
|
||||
type State = {
|
||||
votedTotal: number,
|
||||
votedNay: number,
|
||||
votedAye: number
|
||||
};
|
||||
|
||||
class Motion extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
votedTotal: 0,
|
||||
votedAye: 0,
|
||||
votedNay: 0
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps ({ votes }: Props): State | null {
|
||||
if (!votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ayes, nays } = votes;
|
||||
|
||||
const newState: State = {
|
||||
votedNay: nays.length,
|
||||
votedAye: ayes.length,
|
||||
votedTotal: ayes.length + nays.length
|
||||
};
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, hash, proposal, votes } = this.props;
|
||||
|
||||
if (!proposal || !votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { index } = votes;
|
||||
|
||||
return (
|
||||
<ActionItem
|
||||
className={className}
|
||||
accessory={
|
||||
<Voting
|
||||
hash={hash}
|
||||
isCouncil
|
||||
idNumber={index}
|
||||
proposal={proposal}
|
||||
/>
|
||||
}
|
||||
expandNested
|
||||
idNumber={index}
|
||||
proposal={proposal}
|
||||
>
|
||||
{this.renderInfo()}
|
||||
</ActionItem>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInfo () {
|
||||
const { votes, t } = this.props;
|
||||
|
||||
if (!votes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { ayes, nays, threshold } = votes;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
{t(
|
||||
'ayes ({{ayes}}/{{threshold}} to approve)',
|
||||
{
|
||||
replace: {
|
||||
ayes: ayes.length,
|
||||
threshold: threshold.toString()
|
||||
}
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
{ayes.map((address, index) => (
|
||||
<Labelled
|
||||
key={`${index}:${address}`}
|
||||
label={t('Aye')}
|
||||
>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
value={address}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
))}
|
||||
<h4>
|
||||
{t(
|
||||
'nays ({{nays}})',
|
||||
{
|
||||
replace: {
|
||||
nays: nays.length
|
||||
}
|
||||
}
|
||||
)}
|
||||
</h4>
|
||||
{nays.map((address, index) => (
|
||||
<Labelled
|
||||
key={`${index}:${address}`}
|
||||
label={t('Nay')}
|
||||
>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
value={address}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
styled(Motion as React.ComponentClass<Props>)`
|
||||
.democracy--Motion-results {
|
||||
margin-bottom: 1em;
|
||||
|
||||
&.chart {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
`,
|
||||
translate,
|
||||
withCalls<Props>(
|
||||
[
|
||||
'query.councilMotions.proposalOf',
|
||||
{
|
||||
paramName: 'hash',
|
||||
propName: 'proposal',
|
||||
transform: (value: Option<Proposal>) => value.unwrapOr(null)
|
||||
}
|
||||
],
|
||||
[
|
||||
'query.councilMotions.voting',
|
||||
{
|
||||
paramName: 'hash',
|
||||
propName: 'votes',
|
||||
transform: (value: Option<Votes>) => value.unwrapOr(null)
|
||||
}
|
||||
]
|
||||
)
|
||||
);
|
||||
144
packages/app-council/src/Motions/Propose.tsx
Normal file
144
packages/app-council/src/Motions/Propose.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
// 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 { ApiProps } from '@polkadot/ui-api/types';
|
||||
import { Method, Proposal } from '@polkadot/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Extrinsic, InputNumber } from '@polkadot/ui-app';
|
||||
import TxModal, { TxModalState, TxModalProps } from '@polkadot/ui-app/TxModal';
|
||||
import { withApi, withCalls, withMulti } from '@polkadot/ui-api';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = TxModalProps & ApiProps & {
|
||||
memberCount: number
|
||||
};
|
||||
|
||||
type State = TxModalState & {
|
||||
method: Method | null,
|
||||
threshold: BN | null
|
||||
};
|
||||
|
||||
class Propose extends TxModal<Props, State> {
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
this.defaultState = {
|
||||
...this.defaultState,
|
||||
method: null,
|
||||
threshold: props.memberCount ? new BN((props.memberCount / 2) + 1) : null
|
||||
};
|
||||
this.state = this.defaultState;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps ({ memberCount }: Props, { threshold }: State) {
|
||||
if (!threshold && memberCount > 0) {
|
||||
const simpleMajority = new BN((memberCount / 2) + 1);
|
||||
return { threshold: simpleMajority };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
headerText = () => this.props.t('Propose a council motion');
|
||||
|
||||
txMethod = () => 'councilMotions.propose';
|
||||
txParams = () => {
|
||||
const { method, threshold } = this.state;
|
||||
|
||||
return [
|
||||
threshold,
|
||||
...(method ? [new Proposal(method)] : [])
|
||||
];
|
||||
}
|
||||
|
||||
isDisabled = () => {
|
||||
const { memberCount = 0 } = this.props;
|
||||
const { accountId, method, threshold } = this.state;
|
||||
|
||||
const hasThreshold = !!threshold && threshold.gtn(0) && threshold.ltn(memberCount + 1);
|
||||
const hasMethod = !!method;
|
||||
|
||||
return !accountId || !hasMethod || !hasThreshold;
|
||||
}
|
||||
|
||||
renderTrigger = () => {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Propose a council motion')}
|
||||
labelIcon='add'
|
||||
onClick={this.showModal}
|
||||
/>
|
||||
</Button.Group>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { apiDefaultTxSudo, memberCount = 0, t } = this.props;
|
||||
const { threshold } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputNumber
|
||||
className='medium'
|
||||
label={t('threshold')}
|
||||
help={t('The minimum number of council votes required to approve this motion')}
|
||||
isError={!threshold || threshold.eqn(0) || threshold.gtn(memberCount)}
|
||||
onChange={this.onChangeThreshold}
|
||||
onEnter={this.sendTx}
|
||||
placeholder={
|
||||
t(
|
||||
'Positive number between 1 and {{memberCount}}',
|
||||
{ replace: { memberCount } }
|
||||
)
|
||||
}
|
||||
value={threshold || new BN(0)}
|
||||
/>
|
||||
<Extrinsic
|
||||
defaultValue={apiDefaultTxSudo}
|
||||
label={t('proposal')}
|
||||
onChange={this.onChangeExtrinsic}
|
||||
onEnter={this.sendTx}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onChangeThreshold = (threshold?: BN): void => {
|
||||
const { memberCount = 0 } = this.props;
|
||||
if (memberCount > 0 && !this.defaultState.threshold) {
|
||||
this.defaultState.threshold = new BN((memberCount / 2) + 1);
|
||||
}
|
||||
this.setState({ threshold } as State);
|
||||
}
|
||||
|
||||
private onChangeExtrinsic = (method: Method): void => {
|
||||
if (!method) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ method } as State);
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
Propose,
|
||||
translate,
|
||||
withApi,
|
||||
withCalls(
|
||||
[
|
||||
'query.council.activeCouncil',
|
||||
{
|
||||
propName: 'memberCount',
|
||||
transform: (value: Array<any>) => value.length
|
||||
}
|
||||
]
|
||||
)
|
||||
);
|
||||
0
packages/app-council/src/Motions/Summary.tsx
Normal file
0
packages/app-council/src/Motions/Summary.tsx
Normal file
53
packages/app-council/src/Motions/index.tsx
Normal file
53
packages/app-council/src/Motions/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
// 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 { Hash } from '@polkadot/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import React from 'react';
|
||||
import { withCalls } from '@polkadot/ui-api';
|
||||
import { CardGrid } from '@polkadot/ui-app';
|
||||
|
||||
import Motion from './Motion';
|
||||
import Propose from './Propose';
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
councilMotions_proposals?: Array<Hash>
|
||||
};
|
||||
|
||||
class Motions extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<CardGrid
|
||||
emptyText={t('No council motions')}
|
||||
headerText={t('Motions')}
|
||||
buttons={
|
||||
<Propose />
|
||||
}
|
||||
>
|
||||
{this.renderMotions()}
|
||||
</CardGrid>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMotions () {
|
||||
const { councilMotions_proposals = [] } = this.props;
|
||||
|
||||
return councilMotions_proposals.map((hash) => (
|
||||
<Motion
|
||||
hash={hash.toHex()}
|
||||
key={hash.toHex()}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(
|
||||
withCalls<Props>(
|
||||
'query.councilMotions.proposals'
|
||||
)(Motions)
|
||||
);
|
||||
@ -10,6 +10,7 @@ import { Route, Switch } from 'react-router';
|
||||
import { Tabs } from '@polkadot/ui-app';
|
||||
|
||||
import Overview from './Overview';
|
||||
import Motions from './Motions';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = AppProps & BareProps & I18nProps;
|
||||
@ -30,6 +31,10 @@ class App extends React.PureComponent<Props, State> {
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Council overview')
|
||||
},
|
||||
{
|
||||
name: 'motions',
|
||||
text: t('Motions')
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -48,6 +53,7 @@ class App extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</header>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/motions`} component={Motions} />
|
||||
<Route component={Overview} />
|
||||
</Switch>
|
||||
</main>
|
||||
|
||||
@ -10,14 +10,13 @@ import { RawParam } from '@polkadot/ui-params/types';
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ActionItem, Chart, Static } from '@polkadot/ui-app';
|
||||
import { ActionItem, Chart, Static, Voting } from '@polkadot/ui-app';
|
||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||
import settings from '@polkadot/ui-settings';
|
||||
import VoteThreshold from '@polkadot/ui-params/Param/VoteThreshold';
|
||||
import { withCalls, withMulti } from '@polkadot/ui-api';
|
||||
|
||||
import translate from '../translate';
|
||||
import Voting from './Voting';
|
||||
|
||||
const COLORS_AYE = settings.uiTheme === 'substrate'
|
||||
? ['#4d4', '#4e4']
|
||||
@ -100,7 +99,10 @@ class Referendum extends React.PureComponent<Props, State> {
|
||||
idNumber={value.index}
|
||||
proposal={value.proposal}
|
||||
accessory={
|
||||
<Voting referendumId={value.index} />
|
||||
<Voting
|
||||
idNumber={value.index}
|
||||
proposal={value.proposal}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{this.renderInfo()}
|
||||
|
||||
@ -82,7 +82,7 @@ class Voting extends React.PureComponent<Props, State> {
|
||||
<Modal.Header>{t('Vote on proposal')}</Modal.Header>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
help={t('Select the account you wish to vote with. You can approve "yay" or deny "nay" the proposal.')}
|
||||
help={t('Select the account you wish to vote with. You can approve "aye" or deny "nay" the proposal.')}
|
||||
label={t('vote with account')}
|
||||
onChange={this.onChangeAccount}
|
||||
type='account'
|
||||
|
||||
@ -9,5 +9,5 @@ This lists the proposals that you can not vote on yet. The proposal with the hig
|
||||
|
||||
# Referendum
|
||||
|
||||
This lists the proposal that you can now vote for "yay" or against "nay".
|
||||
This lists the proposal that you can now vote for "aye" or against "nay".
|
||||
At the end of the launch period, and depending on the outcome of the referendum, the action that was subjected to vote will be executed or rejected.
|
||||
|
||||
@ -2,18 +2,22 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ApiProps } from '@polkadot/ui-api/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { AccountId, BlockNumber } from '@polkadot/types';
|
||||
|
||||
import React from 'react';
|
||||
import { Dropdown } from '@polkadot/ui-app';
|
||||
import { Button, Dropdown } from '@polkadot/ui-app';
|
||||
import { withMulti, withApi, withCalls } from '@polkadot/ui-api';
|
||||
import TxModal, { TxModalProps, TxModalState } from '@polkadot/ui-app/TxModal';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = I18nProps & TxModalProps & {
|
||||
type Props = I18nProps & ApiProps & TxModalProps & {
|
||||
isApproved?: boolean,
|
||||
proposalInfo?: React.ReactNode,
|
||||
proposalId: string
|
||||
proposalId: string,
|
||||
threshold: number
|
||||
};
|
||||
|
||||
type State = TxModalState & {
|
||||
@ -33,19 +37,40 @@ class Approve extends TxModal<Props, State> {
|
||||
|
||||
headerText = () => this.props.t('Approve or reject proposal');
|
||||
|
||||
txMethod = () => this.state.isApproving ? 'treasury.approveProposal' : 'treasury.rejectProposal';
|
||||
txParams = () => [this.props.proposalId];
|
||||
txMethod = () => 'councilMotions.propose';
|
||||
txParams = () => {
|
||||
const { api, proposalId, threshold } = this.props;
|
||||
const { isApproving } = this.state;
|
||||
|
||||
const method = isApproving ? 'approveProposal' : 'rejectProposal';
|
||||
const spendProposal = api.tx.treasury[method](proposalId);
|
||||
return [threshold, spendProposal];
|
||||
}
|
||||
|
||||
renderTrigger = () => {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div className='ui--Row-buttons'>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Respond')}
|
||||
labelIcon='reply'
|
||||
onClick={this.showModal}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPreContent = () => {
|
||||
const { proposalInfo = null } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{proposalInfo}
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
if (!proposalInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return proposalInfo;
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
@ -54,8 +79,8 @@ class Approve extends TxModal<Props, State> {
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
help={t('Select your vote preference for this spend proposal, either to approve or disapprove')}
|
||||
label={t('action')}
|
||||
help={t('Propose a majority council motion to either approve or reject this spend proposal')}
|
||||
label={t('proposed council action')}
|
||||
options={this.approveOptions()}
|
||||
onChange={this.onChangeApproving}
|
||||
value={isApproving}
|
||||
@ -68,4 +93,17 @@ class Approve extends TxModal<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Approve);
|
||||
export default withMulti(
|
||||
Approve,
|
||||
translate,
|
||||
withApi,
|
||||
withCalls(
|
||||
[
|
||||
'query.council.activeCouncil',
|
||||
{
|
||||
propName: 'threshold',
|
||||
transform: (value: Array<[AccountId, BlockNumber]>) => 1 + (value.length / 2)
|
||||
}
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
@ -1,61 +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 { I18nProps } from '@polkadot/ui-app/types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { TreasuryProposal } from '@polkadot/types';
|
||||
import { Card } from '@polkadot/ui-app';
|
||||
import { styles as rowStyles } from '@polkadot/ui-app/Row';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
children?: React.ReactNode,
|
||||
isApproved?: boolean,
|
||||
proposal: TreasuryProposal,
|
||||
proposalExtra?: React.ReactNode,
|
||||
proposalId: string
|
||||
};
|
||||
|
||||
class Item extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { children, className, proposalExtra, proposalId } = this.props;
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<div className='ui--Row'>
|
||||
<div className='ui--Row-base'>
|
||||
<div className='ui--Row-details treasury--Item-header'>
|
||||
<h3 className='treasury--Item-header-id'>
|
||||
#{proposalId}
|
||||
</h3>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
{proposalExtra}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(styled(Item as React.ComponentClass<Props>)`
|
||||
${rowStyles}
|
||||
|
||||
.treasury--Item-header {
|
||||
margin: -0.5rem -0.5rem 0 0;
|
||||
}
|
||||
|
||||
.treasury--Approve-approved {
|
||||
color: green;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.treasury--Item-header-id {
|
||||
font-size: 2rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
`);
|
||||
@ -6,20 +6,27 @@ import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||
|
||||
import React from 'react';
|
||||
import { Option, TreasuryProposal } from '@polkadot/types';
|
||||
import { ActionItem, Button, Icon, InputAddress, Labelled, Static } from '@polkadot/ui-app';
|
||||
import styled from 'styled-components';
|
||||
import { Option, TreasuryProposal as TreasuryProposalType } from '@polkadot/types';
|
||||
import { ActionItem, Icon, TreasuryProposal } from '@polkadot/ui-app';
|
||||
import { withCalls, withMulti, withObservable } from '@polkadot/ui-api';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import translate from '../translate';
|
||||
import Approve from './Approve';
|
||||
|
||||
const Approved = styled.h3`
|
||||
color: green;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
type Props = I18nProps & {
|
||||
allAccounts?: SubjectInfo,
|
||||
isApproved: boolean,
|
||||
proposal?: TreasuryProposal | null,
|
||||
proposalId: string
|
||||
proposal?: TreasuryProposalType | null,
|
||||
proposalId: string,
|
||||
onPopulate: () => void,
|
||||
onRespond: () => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
@ -27,6 +34,23 @@ type State = {
|
||||
};
|
||||
|
||||
class ProposalDisplay extends React.PureComponent<Props, State> {
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
const { proposal, onPopulate } = props;
|
||||
if (proposal) {
|
||||
onPopulate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ proposal }: Props) {
|
||||
const { onPopulate } = this.props;
|
||||
|
||||
if (proposal && !this.props.proposal) {
|
||||
onPopulate();
|
||||
}
|
||||
}
|
||||
|
||||
state: State = {
|
||||
isApproveOpen: false
|
||||
};
|
||||
@ -43,22 +67,21 @@ class ProposalDisplay extends React.PureComponent<Props, State> {
|
||||
accessory={this.renderAccessory()}
|
||||
idNumber={proposalId}
|
||||
>
|
||||
{this.renderInfo()}
|
||||
<TreasuryProposal proposal={proposal} />
|
||||
</ActionItem>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAccessory () {
|
||||
const { allAccounts, isApproved, proposalId, t } = this.props;
|
||||
const { isApproveOpen } = this.state;
|
||||
const { allAccounts, isApproved, onRespond, proposal, proposalId, t } = this.props;
|
||||
|
||||
if (isApproved) {
|
||||
return (
|
||||
<h3 className='treasury--Approve-approved'>
|
||||
<Approved>
|
||||
<Icon name='check' />
|
||||
{' '}
|
||||
{t('Approved')}
|
||||
</h3>
|
||||
</Approved>
|
||||
);
|
||||
}
|
||||
|
||||
@ -68,73 +91,21 @@ class ProposalDisplay extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='ui--Row-buttons'>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Respond')}
|
||||
labelIcon='reply'
|
||||
onClick={this.showApprove}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<Approve
|
||||
isOpen={isApproveOpen}
|
||||
onClose={this.hideApprove}
|
||||
proposalInfo={this.renderInfo()}
|
||||
proposalId={proposalId}
|
||||
/>
|
||||
</>
|
||||
<Approve
|
||||
proposalInfo={
|
||||
<>
|
||||
<h3>Proposal #{proposalId}</h3>
|
||||
<details>
|
||||
<TreasuryProposal proposal={proposal} />
|
||||
</details>
|
||||
<br />
|
||||
</>
|
||||
}
|
||||
proposalId={proposalId}
|
||||
onSuccess={onRespond}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInfo () {
|
||||
const { isApproved, proposal, t } = this.props;
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { bond, beneficiary, proposer, value } = proposal;
|
||||
|
||||
return (
|
||||
<div className={['treasury--Proposal-info', !isApproved && 'with-children'].join(' ')}>
|
||||
<Labelled label={t('proposed by')}>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
value={proposer}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
<Labelled label={t('beneficiary')}>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
value={beneficiary}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
<Static label={t('value')}>
|
||||
{formatBalance(value)}
|
||||
</Static>
|
||||
<Static label={t('bond')}>
|
||||
{formatBalance(bond)}
|
||||
</Static>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private showApprove = (): void => {
|
||||
this.setState({
|
||||
isApproveOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
private hideApprove = (): void => {
|
||||
this.setState({
|
||||
isApproveOpen: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
@ -144,7 +115,7 @@ export default withMulti(
|
||||
['query.treasury.proposals', {
|
||||
paramName: 'proposalId',
|
||||
propName: 'proposal',
|
||||
transform: (value: Option<TreasuryProposal>) =>
|
||||
transform: (value: Option<TreasuryProposalType>) =>
|
||||
value.unwrapOr(null)
|
||||
}]
|
||||
),
|
||||
|
||||
@ -7,27 +7,29 @@ import { ProposalIndex } from '@polkadot/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withCalls, withMulti } from '@polkadot/ui-api';
|
||||
import { Button, Column } from '@polkadot/ui-app';
|
||||
import { Column } from '@polkadot/ui-app';
|
||||
|
||||
import Proposal from './Proposal';
|
||||
import Propose from './Propose';
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = I18nProps & {
|
||||
type Props = I18nProps & RouteComponentProps & {
|
||||
isApprovals?: boolean,
|
||||
treasury_approvals?: Array<BN>,
|
||||
treasury_proposalCount?: BN
|
||||
};
|
||||
|
||||
type State = {
|
||||
isEmpty: boolean,
|
||||
isProposeOpen: boolean,
|
||||
proposalIndices: Array<BN>
|
||||
};
|
||||
|
||||
class ProposalsBase extends React.PureComponent<Props> {
|
||||
state: State = {
|
||||
isEmpty: true,
|
||||
isProposeOpen: false,
|
||||
proposalIndices: [] as Array<BN>
|
||||
};
|
||||
@ -39,7 +41,9 @@ class ProposalsBase extends React.PureComponent<Props> {
|
||||
proposalIndices = treasury_approvals;
|
||||
} else {
|
||||
for (let i = 0; i < treasury_proposalCount.toNumber(); i++) {
|
||||
proposalIndices.push(new BN(i));
|
||||
if (!treasury_approvals.find(index => index.eqn(i))) {
|
||||
proposalIndices.push(new BN(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
return { proposalIndices };
|
||||
@ -47,32 +51,17 @@ class ProposalsBase extends React.PureComponent<Props> {
|
||||
|
||||
render () {
|
||||
const { isApprovals, t } = this.props;
|
||||
const { isProposeOpen } = this.state;
|
||||
const { isEmpty } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column
|
||||
emptyText={t(isApprovals ? 'No approved proposals' : 'No pending proposals')}
|
||||
headerText={t(isApprovals ? 'Approved' : 'Proposals')}
|
||||
buttons={!isApprovals && (
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Submit a spend proposal')}
|
||||
labelIcon='add'
|
||||
onClick={this.togglePropose(true)}
|
||||
/>
|
||||
</Button.Group>
|
||||
)}
|
||||
isEmpty={isEmpty}
|
||||
>
|
||||
{this.renderProposals()}
|
||||
</Column>
|
||||
{!isApprovals && (
|
||||
<Propose
|
||||
isOpen={isProposeOpen}
|
||||
onClose={this.togglePropose(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -84,26 +73,32 @@ class ProposalsBase extends React.PureComponent<Props> {
|
||||
return proposalIndices.map((proposalId) => (
|
||||
<Proposal
|
||||
isApproved={isApprovals}
|
||||
onPopulate={this.onPopulateProposal}
|
||||
onRespond={this.onRespond}
|
||||
proposalId={proposalId.toString()}
|
||||
key={proposalId.toString()}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
private togglePropose = (isProposeOpen: boolean) => () => {
|
||||
this.setState({
|
||||
isProposeOpen
|
||||
onRespond = () => {
|
||||
const { history } = this.props;
|
||||
|
||||
history.push('/council/motions');
|
||||
}
|
||||
|
||||
private onPopulateProposal = () => {
|
||||
this.setState(({ isEmpty }: State) => {
|
||||
if (isEmpty) {
|
||||
return { isEmpty: false };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const Proposals = withMulti(
|
||||
styled(ProposalsBase as React.ComponentClass<Props, State>)`
|
||||
.treasury--Proposals-inner {
|
||||
display: flex;
|
||||
flex-wrap: column;
|
||||
}
|
||||
`,
|
||||
ProposalsBase,
|
||||
translate,
|
||||
withCalls<Props>(
|
||||
[
|
||||
@ -113,7 +108,8 @@ const Proposals = withMulti(
|
||||
}
|
||||
],
|
||||
'query.treasury.proposalCount'
|
||||
)
|
||||
),
|
||||
withRouter
|
||||
);
|
||||
|
||||
export default Proposals;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
|
||||
import { InputAddress, InputBalance } from '@polkadot/ui-app';
|
||||
import { Button, InputAddress, InputBalance } from '@polkadot/ui-app';
|
||||
import TxModal, { TxModalState, TxModalProps } from '@polkadot/ui-app/TxModal';
|
||||
|
||||
import translate from '../translate';
|
||||
@ -42,6 +42,21 @@ class Propose extends TxModal<Props, State> {
|
||||
return !accountId || !hasValue || !hasBeneficiary;
|
||||
}
|
||||
|
||||
renderTrigger = () => {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Submit a spend proposal')}
|
||||
labelIcon='add'
|
||||
onClick={this.showModal}
|
||||
/>
|
||||
</Button.Group>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { t } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
@ -9,6 +9,7 @@ import { Columar } from '@polkadot/ui-app';
|
||||
|
||||
import Summary from './Summary';
|
||||
import Proposals, { Approvals } from './Proposals';
|
||||
import Propose from './Propose';
|
||||
|
||||
type Props = AppProps & BareProps & I18nProps;
|
||||
|
||||
@ -25,6 +26,7 @@ export default class Overview extends React.PureComponent<Props, State> {
|
||||
return (
|
||||
<>
|
||||
<Summary />
|
||||
<Propose />
|
||||
<Columar>
|
||||
<Proposals />
|
||||
<Approvals />
|
||||
|
||||
@ -11,7 +11,7 @@ import { HelpOverlay, Tabs } from '@polkadot/ui-app';
|
||||
|
||||
import basicMd from './md/basic.md';
|
||||
import Overview from './Overview';
|
||||
import Settings from './Settings';
|
||||
// import Settings from './Settings';
|
||||
|
||||
import translate from './translate';
|
||||
|
||||
@ -33,11 +33,11 @@ class App extends React.PureComponent<Props, State> {
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Treasury overview')
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
text: t('Edit settings')
|
||||
}
|
||||
// {
|
||||
// name: 'settings',
|
||||
// text: t('Edit settings')
|
||||
// }
|
||||
]
|
||||
};
|
||||
}
|
||||
@ -56,7 +56,7 @@ class App extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</header>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/settings`} component={Settings} />
|
||||
{/*<Route path={`${basePath}/settings`} component={Settings} />*/}
|
||||
<Route component={Overview} />
|
||||
</Switch>
|
||||
</main>
|
||||
|
||||
@ -2,82 +2,42 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { Method, Proposal } from '@polkadot/types';
|
||||
import { Proposal } from '@polkadot/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import Call from './Call';
|
||||
import Card from './Card';
|
||||
import ProposedAction, { styles as proposedActionStyles } from './ProposedAction';
|
||||
import { styles as rowStyles } from './Row';
|
||||
|
||||
type Props = {
|
||||
type Props = RouteComponentProps & {
|
||||
className?: string,
|
||||
children?: React.ReactNode,
|
||||
accessory?: React.ReactNode,
|
||||
proposal?: Proposal,
|
||||
idNumber: BN | number | string
|
||||
proposal?: Proposal | null,
|
||||
idNumber: BN | number | string,
|
||||
expandNested?: boolean
|
||||
};
|
||||
|
||||
export const styles = `
|
||||
${rowStyles}
|
||||
|
||||
.ui--ActionItem-extrinsic {
|
||||
margin-top: 1rem;
|
||||
|
||||
.ui--Params-Content {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--ActionItem-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ui--ActionItem-buttons {
|
||||
|
||||
}
|
||||
${proposedActionStyles}
|
||||
`;
|
||||
|
||||
class ActionItem extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { className, children, accessory, proposal } = this.props;
|
||||
|
||||
const idNumber = typeof this.props.idNumber === 'string'
|
||||
? this.props.idNumber
|
||||
: formatNumber(this.props.idNumber);
|
||||
const { className, children, accessory } = this.props;
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<div className='ui--Row'>
|
||||
<div className='ui--Row-base'>
|
||||
<div className='ui--Row-details'>
|
||||
{
|
||||
proposal ?
|
||||
(() => {
|
||||
const { meta, method, section } = Method.findFunction(proposal.callIndex);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>#{idNumber}: {section}.{method}</h3>
|
||||
{meta && meta.documentation && (
|
||||
<details>
|
||||
<summary>{meta.documentation.join(' ')}</summary>
|
||||
</details>
|
||||
)}
|
||||
<Call
|
||||
className='ui--ActionItem-extrinsic'
|
||||
value={proposal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})() :
|
||||
(
|
||||
<h3>#{idNumber}</h3>
|
||||
)
|
||||
}
|
||||
{this.renderProposal()}
|
||||
</div>
|
||||
{accessory}
|
||||
</div>
|
||||
@ -86,6 +46,19 @@ class ActionItem extends React.PureComponent<Props> {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
private renderProposal () {
|
||||
const { idNumber, proposal, expandNested } = this.props;
|
||||
|
||||
return (
|
||||
<ProposedAction
|
||||
idNumber={idNumber}
|
||||
proposal={proposal}
|
||||
withLinks={expandNested}
|
||||
expandNested={expandNested}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default styled(ActionItem as React.ComponentClass<Props>)`${styles}`;
|
||||
export default withRouter(styled(ActionItem as React.ComponentClass<Props>)`${styles}`);
|
||||
|
||||
@ -14,9 +14,9 @@ type Props = CollectionProps;
|
||||
type State = CollectionState;
|
||||
|
||||
class CardGrid extends Collection<Props, State> {
|
||||
static getDerivedStateFromProps ({ children, isEmpty }: Props) {
|
||||
if (isEmpty || !children || (children as Array<any>).length <= 0) {
|
||||
return { isEmpty: true, showHeader: false };
|
||||
static getDerivedStateFromProps ({ children, headerText }: Props) {
|
||||
if (!children || (children as Array<any>).length <= 0) {
|
||||
return { isEmpty: true, showHeader: !!headerText };
|
||||
}
|
||||
return { isEmpty: false, showHeader: true };
|
||||
}
|
||||
|
||||
@ -101,7 +101,6 @@ export default class Collection<P extends CollectionProps, S extends CollectionS
|
||||
|
||||
protected renderCollection (): React.ReactNode {
|
||||
const { children } = this.props;
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,8 @@ class Column extends Collection<Props, State> {
|
||||
return (
|
||||
<div className={`ui--Column ${className}`}>
|
||||
{this.renderHeader()}
|
||||
{isEmpty ?
|
||||
this.renderEmpty() :
|
||||
this.renderCollection()
|
||||
}
|
||||
{isEmpty && this.renderEmpty()}
|
||||
{this.renderCollection()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -50,6 +48,7 @@ export default translate(
|
||||
${collectionStyles}
|
||||
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
flex: 1 1;
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
|
||||
@ -91,7 +91,7 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, isError = false, maxLength, maxValue, onEnter, style, withMax, t } = this.props;
|
||||
const { bitLength = DEFAULT_BITLENGTH, className, help, isSi, isDisabled, isError = false, maxLength, maxValue, onEnter, placeholder, style, withMax, t } = this.props;
|
||||
const { isValid, value } = this.state;
|
||||
const maxValueLength = this.maxValue(bitLength).toString().length - 1;
|
||||
|
||||
@ -109,7 +109,7 @@ class InputNumber extends React.PureComponent<Props, State> {
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onPaste={this.onPaste}
|
||||
placeholder={t('Positive number')}
|
||||
placeholder={placeholder || t('Positive number')}
|
||||
style={style}
|
||||
type='text'
|
||||
value={value}
|
||||
|
||||
153
packages/ui-app/src/Inset.tsx
Normal file
153
packages/ui-app/src/Inset.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
// 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 React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Icon from './Icon';
|
||||
import { classes } from './util';
|
||||
|
||||
export type InsetProps = RouteComponentProps & {
|
||||
className?: string,
|
||||
children?: React.ReactNode
|
||||
isCollapsible?: boolean,
|
||||
header?: React.ReactNode,
|
||||
href?: string | null,
|
||||
withTopMargin?: boolean,
|
||||
withBottomMargin?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
isCollapsed: boolean
|
||||
};
|
||||
|
||||
class Inset extends React.PureComponent<InsetProps, State> {
|
||||
state: State = {
|
||||
isCollapsed: true
|
||||
};
|
||||
|
||||
render () {
|
||||
const { children, className, isCollapsible, header, href, withBottomMargin, withTopMargin } = this.props;
|
||||
const { isCollapsed } = this.state;
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
classes(
|
||||
'ui--Inset',
|
||||
href && 'as-link',
|
||||
isCollapsible && 'collapsible',
|
||||
withBottomMargin && 'bottom-margin',
|
||||
withTopMargin && 'top-margin',
|
||||
className
|
||||
)
|
||||
}
|
||||
>
|
||||
{isCollapsible && (
|
||||
<div
|
||||
className='header'
|
||||
onClick={this.toggleCollapsed}
|
||||
>
|
||||
<h3>{header}</h3>
|
||||
<Icon
|
||||
className={classes(isCollapsed && 'collapsed')}
|
||||
name='angle up'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classes('children', (isCollapsible && isCollapsed) && 'collapsed')}
|
||||
onClick={href ? this.onClick : undefined}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onClick = () => {
|
||||
const { history, href } = this.props;
|
||||
|
||||
history.push(href!);
|
||||
}
|
||||
|
||||
private toggleCollapsed = () => {
|
||||
this.setState(({ isCollapsed }: State) => {
|
||||
return {
|
||||
isCollapsed: !isCollapsed
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
styled(Inset as React.ComponentClass<InsetProps, State>)`
|
||||
& {
|
||||
box-shadow: 0 3px 3px rgba(0,0,0,.2);
|
||||
position: relative;
|
||||
background: #fefefe;
|
||||
padding: 1rem;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.bottom-margin {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
&.top-margin {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
cursor: pointer;
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
line-height: 2rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
font-size: 2rem;
|
||||
color: rgba(0,0,0,0.35);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
line-height: 4rem;
|
||||
transition: all 0.2s;
|
||||
transform-origin: center center;
|
||||
|
||||
&.collapsed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.children {
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.as-link {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 5px 5px rgba(0,0,0,.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
);
|
||||
@ -35,6 +35,10 @@ const Wrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> label {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
&.label-small {
|
||||
display: block;
|
||||
|
||||
|
||||
120
packages/ui-app/src/ProposedAction.tsx
Normal file
120
packages/ui-app/src/ProposedAction.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
// 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 { Method, Proposal } from '@polkadot/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import Call from './Call';
|
||||
import Inset, { InsetProps } from './Inset';
|
||||
import TreasuryProposal from './TreasuryProposal';
|
||||
import { isTreasuryProposalVote } from './util';
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
asInset?: boolean,
|
||||
insetProps?: Partial<InsetProps>,
|
||||
proposal?: Proposal | null,
|
||||
idNumber: BN | number | string,
|
||||
isCollapsible?: boolean,
|
||||
withLinks?: boolean,
|
||||
expandNested?: boolean
|
||||
};
|
||||
|
||||
export const styles = `
|
||||
.ui--ProposedAction-extrinsic {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.ui--Params-Content {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--ProposedAction-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
class ProposedAction extends React.PureComponent<Props> {
|
||||
render () {
|
||||
const { asInset, insetProps, isCollapsible, proposal, withLinks, expandNested } = this.props;
|
||||
|
||||
const idNumber = typeof this.props.idNumber === 'string'
|
||||
? this.props.idNumber
|
||||
: formatNumber(this.props.idNumber);
|
||||
|
||||
if (!proposal) {
|
||||
return (
|
||||
<h3>#{idNumber}</h3>
|
||||
);
|
||||
}
|
||||
|
||||
const { meta, method, section } = Method.findFunction(proposal.callIndex);
|
||||
|
||||
const header = `#${idNumber}: ${section}.${method}`;
|
||||
const documentation = (meta && meta.documentation)
|
||||
? (
|
||||
<summary>{meta.documentation.join(' ')}</summary>
|
||||
)
|
||||
: null;
|
||||
const params = (isTreasuryProposalVote(proposal) && expandNested) ? (
|
||||
<TreasuryProposal
|
||||
className='ui--ProposedAction-extrinsic'
|
||||
asInset={withLinks}
|
||||
insetProps={{
|
||||
withTopMargin: true,
|
||||
withBottomMargin: true,
|
||||
...(withLinks ? { href: '/treasury' } : {})
|
||||
}}
|
||||
proposalId={proposal.args[0].toString()}
|
||||
/>
|
||||
) : (
|
||||
<Call
|
||||
className='ui--ProposedAction-extrinsic'
|
||||
value={proposal}
|
||||
/>
|
||||
);
|
||||
|
||||
if (asInset) {
|
||||
return (
|
||||
<Inset
|
||||
header={header}
|
||||
isCollapsible
|
||||
{...insetProps}
|
||||
>
|
||||
<>
|
||||
{documentation}
|
||||
{params}
|
||||
</>
|
||||
</Inset>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>{header}</h3>
|
||||
{isCollapsible
|
||||
? (
|
||||
<details>
|
||||
{documentation}
|
||||
{params}
|
||||
</details>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<details>
|
||||
{documentation}
|
||||
</details>
|
||||
{params}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default styled(ProposedAction as React.ComponentClass<Props>)`${styles}`;
|
||||
@ -84,8 +84,9 @@ export const styles = `
|
||||
}
|
||||
|
||||
.ui--Row-buttons {
|
||||
flex: 0;
|
||||
margin: -0.75rem -1rem 0 0;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
white-space: nowrap;
|
||||
|
||||
button.ui.button:last-child {
|
||||
@ -103,6 +104,7 @@ export const styles = `
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
padding: 0.25rem 0 0;
|
||||
width: 100%;
|
||||
|
||||
.account-label{
|
||||
margin: -0.75rem 0 0 0
|
||||
|
||||
132
packages/ui-app/src/TreasuryProposal.tsx
Normal file
132
packages/ui-app/src/TreasuryProposal.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
// 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 { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { ApiProps } from '@polkadot/ui-api/types';
|
||||
import { Option, TreasuryProposal as TreasuryProposalType } from '@polkadot/types';
|
||||
|
||||
import React from 'react';
|
||||
import { InputAddress, Labelled, Static } from '@polkadot/ui-app';
|
||||
import { withMulti, withApi } from '@polkadot/ui-api';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import Inset, { InsetProps } from './Inset';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = I18nProps & ApiProps & {
|
||||
className?: string,
|
||||
asInset?: boolean,
|
||||
insetProps?: Partial<InsetProps>,
|
||||
onClick?: () => void,
|
||||
proposalId: string,
|
||||
proposal?: TreasuryProposalType | null,
|
||||
withLink?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
proposal?: TreasuryProposalType | null
|
||||
};
|
||||
|
||||
class TreasuryProposal extends React.PureComponent<Props, State> {
|
||||
state: State = {};
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
if (!props.proposal) {
|
||||
this.fetchProposal();
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps ({ proposal }: Props, state: State) {
|
||||
if (!state.proposal && proposal) {
|
||||
return { proposal };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.fetchProposal();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, asInset, insetProps, onClick, t } = this.props;
|
||||
const { proposal } = this.state;
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { bond, beneficiary, proposer, value } = proposal;
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<Labelled label={t('proposed by')}>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
defaultValue={proposer}
|
||||
value={proposer}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
<Labelled label={t('beneficiary')}>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
defaultValue={beneficiary}
|
||||
value={beneficiary}
|
||||
withLabel={false}
|
||||
/>
|
||||
</Labelled>
|
||||
<Static label={t('value')}>
|
||||
{formatBalance(value)}
|
||||
</Static>
|
||||
<Static label={t('bond')}>
|
||||
{formatBalance(bond)}
|
||||
</Static>
|
||||
</>
|
||||
);
|
||||
|
||||
if (asInset) {
|
||||
return (
|
||||
<Inset
|
||||
className={className}
|
||||
{...insetProps}
|
||||
>
|
||||
{inner}
|
||||
</Inset>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private fetchProposal = () => {
|
||||
const { api, proposalId } = this.props;
|
||||
|
||||
if (!this.state.proposal && proposalId) {
|
||||
api.query.treasury.proposals(proposalId)
|
||||
.then((proposal) => {
|
||||
this.setState({
|
||||
proposal: (proposal as Option<TreasuryProposalType>).unwrapOr(null)
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Error fetching proposal');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
TreasuryProposal,
|
||||
translate,
|
||||
withApi
|
||||
);
|
||||
@ -8,7 +8,6 @@ import React from 'react';
|
||||
import { Button, InputAddress, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
|
||||
|
||||
export type TxModalProps = I18nProps & {
|
||||
isOpen: boolean,
|
||||
onSubmit?: () => void,
|
||||
onClose?: () => void
|
||||
onSuccess?: () => void,
|
||||
@ -17,44 +16,44 @@ export type TxModalProps = I18nProps & {
|
||||
|
||||
export type TxModalState = {
|
||||
accountId?: string | null,
|
||||
isBusy: boolean
|
||||
isBusy: boolean,
|
||||
isOpen: boolean
|
||||
};
|
||||
|
||||
class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponent<P, S> {
|
||||
protected defaultState: S = {
|
||||
accountId: null
|
||||
accountId: null,
|
||||
isOpen: false,
|
||||
isBusy: false
|
||||
} as S;
|
||||
|
||||
state: S = this.defaultState;
|
||||
|
||||
componentWillReceiveProps ({ isOpen }: P, _: S) {
|
||||
if (isOpen && !this.props.isOpen && !this.state.isBusy) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isOpen } = this.props;
|
||||
const { isOpen } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='ui--Modal'
|
||||
dimmer='inverted'
|
||||
onClose={this.onClose}
|
||||
open={isOpen}
|
||||
>
|
||||
<Modal.Header>
|
||||
{this.headerText()}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
{this.renderPreContent()}
|
||||
{this.renderInputAccount()}
|
||||
{this.renderContent()}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
{this.renderButtons()}
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
<>
|
||||
{this.renderTrigger && this.renderTrigger()}
|
||||
<Modal
|
||||
className='ui--Modal'
|
||||
dimmer='inverted'
|
||||
onClose={this.hideModal}
|
||||
open={isOpen}
|
||||
>
|
||||
<Modal.Header>
|
||||
{this.headerText()}
|
||||
</Modal.Header>
|
||||
<Modal.Content>
|
||||
{this.renderPreContent()}
|
||||
{this.renderInputAccount()}
|
||||
{this.renderContent()}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
{this.renderButtons()}
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -64,12 +63,6 @@ class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponen
|
||||
protected submitLabel = (): React.ReactNode => this.props.t('Submit');
|
||||
protected cancelLabel = (): React.ReactNode => this.props.t('Cancel');
|
||||
|
||||
protected onClose = () => {
|
||||
const { onClose } = this.props;
|
||||
|
||||
onClose && onClose();
|
||||
}
|
||||
|
||||
protected onChangeAccount = (accountId: string | null): void => {
|
||||
this.setState({ accountId });
|
||||
}
|
||||
@ -84,9 +77,9 @@ class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponen
|
||||
protected onSuccess = (): void => {
|
||||
const { onClose, onSuccess } = this.props;
|
||||
|
||||
this.toggleBusy(false);
|
||||
onSuccess && onSuccess();
|
||||
onClose && onClose();
|
||||
this.hideModal();
|
||||
}
|
||||
|
||||
protected onFailed = (): void => {
|
||||
@ -113,6 +106,7 @@ class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponen
|
||||
|
||||
protected renderContent: () => React.ReactNode | null = () => null;
|
||||
protected renderPreContent: () => React.ReactNode | null = () => null;
|
||||
protected renderTrigger?: () => React.ReactNode | null = () => null;
|
||||
|
||||
protected renderButtons: () => React.ReactNode | null = () => {
|
||||
return (
|
||||
@ -166,7 +160,7 @@ class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponen
|
||||
<>
|
||||
<Button
|
||||
isNegative
|
||||
onClick={this.onClose}
|
||||
onClick={this.hideModal}
|
||||
label={t('Cancel')}
|
||||
/>
|
||||
<Button.Or />
|
||||
@ -179,6 +173,21 @@ class TxModal<P extends TxModalProps, S extends TxModalState> extends TxComponen
|
||||
this.defaultState
|
||||
);
|
||||
}
|
||||
|
||||
protected showModal = () => {
|
||||
this.setState({
|
||||
isOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
protected hideModal = () => {
|
||||
const { onClose } = this.props;
|
||||
|
||||
onClose && onClose();
|
||||
this.setState(
|
||||
this.defaultState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TxModal;
|
||||
|
||||
133
packages/ui-app/src/Voting.tsx
Normal file
133
packages/ui-app/src/Voting.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
// 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 { Proposal } from '@polkadot/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React from 'react';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import { withMulti, withObservable } from '@polkadot/ui-api';
|
||||
|
||||
import translate from './translate';
|
||||
import Button from './Button';
|
||||
import Dropdown from './Dropdown';
|
||||
import ProposedAction from './ProposedAction';
|
||||
import TxModal, { TxModalProps, TxModalState } from './TxModal';
|
||||
import { isTreasuryProposalVote } from './util';
|
||||
|
||||
type Props = I18nProps & TxModalProps & {
|
||||
allAccounts?: SubjectInfo,
|
||||
hash?: string,
|
||||
idNumber: BN | number,
|
||||
isCouncil: boolean,
|
||||
proposal?: Proposal | null,
|
||||
preContent?: React.ReactNode
|
||||
};
|
||||
|
||||
type State = TxModalState & {
|
||||
voteOptions: Array<{ text: React.ReactNode, value: boolean }>,
|
||||
voteValue: boolean
|
||||
};
|
||||
|
||||
class Voting extends TxModal<Props, State> {
|
||||
state: State;
|
||||
|
||||
headerText = () => {
|
||||
const { isCouncil, t } = this.props;
|
||||
|
||||
return t(isCouncil ? 'Vote on council motion' : 'Vote on proposal');
|
||||
}
|
||||
|
||||
accountLabel = () => this.props.t('Vote with account');
|
||||
accountHelp = () => this.props.t('Select the account you wish to vote with. You can approve "aye" or deny "nay" the proposal.');
|
||||
|
||||
txMethod = () => {
|
||||
const { isCouncil } = this.props;
|
||||
|
||||
return isCouncil ? 'councilMotions.vote' : 'democracy.vote';
|
||||
}
|
||||
|
||||
txParams = () => {
|
||||
const { hash, idNumber, isCouncil } = this.props;
|
||||
const { voteValue } = this.state;
|
||||
|
||||
return isCouncil
|
||||
? [hash!, idNumber, voteValue]
|
||||
: [idNumber, voteValue];
|
||||
}
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
const { t } = props;
|
||||
|
||||
this.state = {
|
||||
...this.defaultState,
|
||||
voteOptions: [
|
||||
{ text: t('Aye, I approve'), value: true },
|
||||
{ text: t('Nay, I do not approve'), value: false }
|
||||
],
|
||||
voteValue: true
|
||||
};
|
||||
}
|
||||
|
||||
renderPreContent = () => {
|
||||
const { idNumber, proposal } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProposedAction
|
||||
expandNested={isTreasuryProposalVote(proposal)}
|
||||
idNumber={idNumber}
|
||||
isCollapsible
|
||||
proposal={proposal}
|
||||
/>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent = () => {
|
||||
const { t } = this.props;
|
||||
const { voteOptions, voteValue } = this.state;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
help={t('Select your vote preferences for this proposal, either to approve or disapprove')}
|
||||
label={t('record my vote as')}
|
||||
options={voteOptions}
|
||||
onChange={this.onChangeVote}
|
||||
value={voteValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTrigger = () => {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className='ui--Row-buttons'>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Vote')}
|
||||
labelIcon='check'
|
||||
onClick={this.showModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onChangeVote = (voteValue: boolean) => {
|
||||
this.setState({ voteValue });
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
Voting,
|
||||
translate,
|
||||
withObservable(keyring.accounts.subject, { propName: 'allAccounts' })
|
||||
);
|
||||
@ -43,6 +43,7 @@ export { default as InputNumber } from './InputNumber';
|
||||
export { default as InputRpc } from './InputRpc';
|
||||
export { default as InputStorage } from './InputStorage';
|
||||
export { default as InputTags } from './InputTags';
|
||||
export { default as Inset } from './Inset';
|
||||
export { default as Label } from './Label';
|
||||
export { default as LabelHelp } from './LabelHelp';
|
||||
export { default as Labelled } from './Labelled';
|
||||
@ -54,6 +55,7 @@ export { default as Nonce } from './Nonce';
|
||||
export { default as Output } from './Output';
|
||||
export { default as Password } from './Password';
|
||||
export { default as Progress } from './Progress';
|
||||
export { default as ProposedAction } from './ProposedAction';
|
||||
export { default as RecentlyOffline } from './RecentlyOffline';
|
||||
export { default as Row } from './Row';
|
||||
export { default as Static } from './Static';
|
||||
@ -62,8 +64,10 @@ export { default as SummaryBox } from './SummaryBox';
|
||||
export { default as Tabs } from './Tabs';
|
||||
export { default as Toggle } from './Toggle';
|
||||
export { default as Tooltip } from './Tooltip';
|
||||
export { default as TreasuryProposal } from './TreasuryProposal';
|
||||
export { default as TxButton } from './TxButton';
|
||||
export { default as TxComponent } from './TxComponent';
|
||||
export { default as TxModal } from './TxModal';
|
||||
export { default as Voting } from './Voting';
|
||||
|
||||
export { default as media } from './media';
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
.ui.dropdown {
|
||||
&.disabled {
|
||||
background: #fefefe;
|
||||
background: transparent;
|
||||
border-color: #eee;
|
||||
border-style: dashed;
|
||||
opacity: 1;
|
||||
|
||||
@ -7,5 +7,6 @@ export { default as getAddressMeta } from './getAddressMeta';
|
||||
export { default as getAddressName } from './getAddressName';
|
||||
export { default as getAddressTags } from './getAddressTags';
|
||||
export { default as getContractAbi } from './getContractAbi';
|
||||
export { default as isTreasuryProposalVote } from './isTreasuryProposalVote';
|
||||
export { default as toAddress } from './toAddress';
|
||||
export { default as toShortAddress } from './toShortAddress';
|
||||
|
||||
17
packages/ui-app/src/util/isTreasuryProposalVote.ts
Normal file
17
packages/ui-app/src/util/isTreasuryProposalVote.ts
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2019 @polkadot/ui-app 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 { Method, Proposal } from '@polkadot/types';
|
||||
|
||||
export default function isTreasuryProposalVote (proposal?: Proposal | null): boolean {
|
||||
if (!proposal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { method, section } = Method.findFunction(proposal.callIndex);
|
||||
|
||||
return section === 'treasury'
|
||||
&& ['approveProposal', 'rejectProposal'].includes(method)
|
||||
&& !!proposal.args[0];
|
||||
}
|
||||
@ -6,6 +6,7 @@
|
||||
border-left: 0.25rem solid #f2f2f2;
|
||||
|
||||
label {
|
||||
text-transform: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
|
||||
@ -8320,6 +8320,13 @@ is-relative@^1.0.0:
|
||||
dependencies:
|
||||
is-unc-path "^1.0.0"
|
||||
|
||||
is-relative@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
|
||||
integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==
|
||||
dependencies:
|
||||
is-unc-path "^1.0.0"
|
||||
|
||||
is-resolvable@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user