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:
kwingram25 2019-07-10 17:49:46 +01:00 committed by GitHub
parent 8b41c495d6
commit e691868d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1188 additions and 297 deletions

View 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)
}
]
)
);

View 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
}
]
)
);

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}]
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,7 +101,6 @@ export default class Collection<P extends CollectionProps, S extends CollectionS
protected renderCollection (): React.ReactNode {
const { children } = this.props;
return children;
}
}

View File

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

View File

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

View 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);
}
}
}
`
);

View File

@ -35,6 +35,10 @@ const Wrapper = styled.div`
white-space: nowrap;
}
> label {
text-transform: lowercase;
}
&.label-small {
display: block;

View 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}`;

View File

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

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

View File

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

View 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' })
);

View File

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

View File

@ -26,7 +26,7 @@
.ui.dropdown {
&.disabled {
background: #fefefe;
background: transparent;
border-color: #eee;
border-style: dashed;
opacity: 1;

View File

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

View 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];
}

View File

@ -6,6 +6,7 @@
border-left: 0.25rem solid #f2f2f2;
label {
text-transform: none;
font-family: monospace;
}

View File

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