Conviction voting delegation (#8721)
* Conviction voting delegation * Extract option creation * TrackDropdown (& component consolidation) * Adjust option extraction * Adjust * "a" modal * Retrieve voting activity for persons of interest * Debug logs ... all is ok * Adjust check ordering * Really make identity optional * Extract voting details * Add delegation targets to hooks * Use param types * Apply master font changes * WIP, start populating * Some progress in the UI stuff * Adjust * The pieces are mostly in-place * Seems to do the trick... * Correct pallet for voting * Use palletVote
This commit is contained in:
parent
cf9e406e31
commit
3a8ea22d4b
79
packages/page-referenda/src/Referenda/Delegate/Activity.tsx
Normal file
79
packages/page-referenda/src/Referenda/Delegate/Activity.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletReferenda, TrackDescription, TrackInfoExt } from '../../types';
|
||||
import type { VoteResultItem } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { MarkWarning, Table } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
import { getTrackInfo } from '../../util';
|
||||
|
||||
interface Props {
|
||||
allowEmpty?: boolean;
|
||||
className?: string;
|
||||
palletReferenda: PalletReferenda;
|
||||
trackId: number;
|
||||
tracks: TrackDescription[];
|
||||
value?: VoteResultItem[] | null | false;
|
||||
}
|
||||
|
||||
function Activity ({ allowEmpty, className, palletReferenda, tracks, value }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api, specName } = useApi();
|
||||
|
||||
const infos = useMemo(
|
||||
() => value && value.map((v): [VoteResultItem, TrackInfoExt | undefined] =>
|
||||
[v, getTrackInfo(api, specName, palletReferenda, tracks, v.classId.toNumber())]
|
||||
),
|
||||
[api, palletReferenda, specName, tracks, value]
|
||||
);
|
||||
|
||||
if (!infos) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
{infos.length
|
||||
? (
|
||||
<Table isInline>
|
||||
{infos.map(([{ casting, classId, delegating }, info], index) => (
|
||||
<tr key={index}>
|
||||
<td className='all'>
|
||||
{(info && info.trackName) || classId.toString()}
|
||||
</td>
|
||||
<td className='together'>
|
||||
{
|
||||
(delegating &&
|
||||
t<string>('delegating')) ||
|
||||
(casting &&
|
||||
`${casting.length} ${casting.length === 1 ? t<string>('vote') : t<string>('votes')}`)
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</Table>
|
||||
)
|
||||
: <MarkWarning content={t<string>('This account has no voting/delating activity in the chain state')} />
|
||||
}
|
||||
{!allowEmpty && infos.some(([{ delegating }]) => delegating) && (
|
||||
<MarkWarning content={t<string>('This account has some delegations in itself')} />
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.ui--Table {
|
||||
font-size: var(--font-percent-small);
|
||||
opacity: var(--opacity-light);
|
||||
padding-left: 2rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Activity);
|
||||
285
packages/page-referenda/src/Referenda/Delegate/index.tsx
Normal file
285
packages/page-referenda/src/Referenda/Delegate/index.tsx
Normal file
@ -0,0 +1,285 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletReferenda, PalletVote, TrackDescription } from '../../types';
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, ConvictionDropdown, InputAddress, Modal, Toggle, ToggleGroup, TxButton, VoteValue } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useStepper, useToggle } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
import TrackDropdown from '../Submit/TrackDropdown';
|
||||
import Activity from './Activity';
|
||||
import useActivityAccount from './useActivityAccount';
|
||||
import useActivityFellows from './useActivityFellows';
|
||||
import useActivityNominators from './useActivityNominators';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
palletReferenda: PalletReferenda;
|
||||
palletVote: PalletVote;
|
||||
tracks: TrackDescription[];
|
||||
}
|
||||
|
||||
interface Option {
|
||||
key: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function Delegate ({ className, palletReferenda, palletVote, tracks }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [isAllTracks, toggleAllTracks] = useToggle();
|
||||
const [step, nextStep, prevStep] = useStepper();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [toAccount, setToAccount] = useState<string | null>(null);
|
||||
const [trackId, setTrackId] = useState<number>(0);
|
||||
const [balance, setBalance] = useState<BN | undefined>();
|
||||
const [conviction, setConviction] = useState(1);
|
||||
const activityFell = useActivityFellows(palletVote);
|
||||
const activityVals = useActivityNominators(palletVote);
|
||||
const activityFrom = useActivityAccount(palletVote, accountId);
|
||||
const activityTo = useActivityAccount(palletVote, toAccount);
|
||||
const [accType, setAccType] = useState({ index: 0, type: 'address' });
|
||||
|
||||
const includeTracks = useMemo(
|
||||
() => activityFrom && tracks
|
||||
.filter((t) => !activityFrom.some((a) => t.id.eq(a.classId)))
|
||||
.map(({ id }) => id),
|
||||
[activityFrom, tracks]
|
||||
);
|
||||
|
||||
const allFell = useMemo(
|
||||
// We also filter the fellows by activity - since there are a number
|
||||
// we just want to skip those that does not have any activity
|
||||
() => activityFell &&
|
||||
Object
|
||||
.entries(activityFell)
|
||||
.map(([key, act]) => (act.length > 0) && ({ key, name: key, value: key }))
|
||||
.filter((a): a is Option => !!a),
|
||||
[activityFell]
|
||||
);
|
||||
|
||||
const allVals = useMemo(
|
||||
() => activityVals &&
|
||||
Object
|
||||
.entries(activityVals)
|
||||
.map(([key]) => ({ key, name: key, value: key }))
|
||||
.filter((a): a is Option => !!a),
|
||||
[activityVals]
|
||||
);
|
||||
|
||||
const typeOpts = useMemo(
|
||||
() => [
|
||||
{ text: t<string>('Addresses'), value: 'address' },
|
||||
isFunction(api.query.staking?.nominators) &&
|
||||
{ isDisabled: !allVals || !allVals.length, text: t<string>('Validators'), value: 'validators' },
|
||||
isFunction(api.query.fellowshipCollective?.members) &&
|
||||
{ isDisabled: !allFell || !allFell.length, text: t<string>('Fellows'), value: 'fellows' }
|
||||
],
|
||||
[allFell, allVals, api, t]
|
||||
);
|
||||
|
||||
const onChangeType = useCallback(
|
||||
(index: number, type: string | number) =>
|
||||
setAccType({ index, type: type.toString() }),
|
||||
[]
|
||||
);
|
||||
|
||||
const extrinsic = useMemo(
|
||||
() => balance && conviction && toAccount && includeTracks
|
||||
? isAllTracks
|
||||
? api.tx.utility.forceBatch(includeTracks.map((trackId) =>
|
||||
api.tx[palletVote as 'convictionVoting'].delegate(trackId, toAccount, conviction, balance)
|
||||
))
|
||||
: api.tx[palletVote as 'convictionVoting'].delegate(trackId, toAccount, conviction, balance)
|
||||
: null,
|
||||
[api, balance, conviction, includeTracks, isAllTracks, palletVote, toAccount, trackId]
|
||||
);
|
||||
|
||||
const isStep1Valid = !!(accountId && activityFrom && includeTracks && (includeTracks.length > 0));
|
||||
const isStep2Valid = !!(toAccount && activityTo);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t<string>('Delegate votes {{step}}/{{numSteps}}', { replace: { numSteps: 2, step } })}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
{(step === 1) && (
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t<string>('Delegate from this account to another. All votes made on the target would count as a delegated vote for this account.')}>
|
||||
<InputAddress
|
||||
label={t<string>('delegate from account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
withLabel
|
||||
/>
|
||||
<Activity
|
||||
allowEmpty
|
||||
palletReferenda={palletReferenda}
|
||||
trackId={-1}
|
||||
tracks={tracks}
|
||||
value={activityFrom}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
align='right'
|
||||
hint={t<string>('Either delegate your votes for a single track as selected or delegate for all available tracks.')}
|
||||
>
|
||||
<Toggle
|
||||
label={t<string>('apply delegation to all tracks')}
|
||||
onChange={toggleAllTracks}
|
||||
value={isAllTracks}
|
||||
/>
|
||||
{!isAllTracks && includeTracks && (includeTracks.length > 0) && (
|
||||
<TrackDropdown
|
||||
include={includeTracks}
|
||||
onChange={setTrackId}
|
||||
palletReferenda={palletReferenda}
|
||||
tracks={tracks}
|
||||
/>
|
||||
)}
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
hint={
|
||||
<>
|
||||
<p>{t<string>('The balance associated with the vote will be locked as per the conviction specified and will not be available for transfer during this period.')}</p>
|
||||
<p>{t<string>('Conviction locks do overlap and are not additive, meaning that funds locked during a previous vote can be locked again.')}</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<VoteValue
|
||||
accountId={accountId}
|
||||
autoFocus
|
||||
label={t<string>('delegated vote value')}
|
||||
onChange={setBalance}
|
||||
/>
|
||||
<ConvictionDropdown
|
||||
label={t<string>('conviction')}
|
||||
onChange={setConviction}
|
||||
value={conviction}
|
||||
voteLockingPeriod={api.consts[palletVote as 'convictionVoting'].voteLockingPeriod}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
)}
|
||||
{(step === 2) && (
|
||||
<Modal.Content>
|
||||
{(typeOpts.length > 1) && (
|
||||
<Modal.Columns
|
||||
align='center'
|
||||
hint={t<string>('Select from a list of pre-propulated accounts (based on your account activity) or supply your own')}
|
||||
>
|
||||
<ToggleGroup
|
||||
onChange={onChangeType}
|
||||
options={typeOpts}
|
||||
value={accType.index}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
)}
|
||||
<Modal.Columns hint={t<string>('The account that you wish to delegate to')}>
|
||||
{accType.type === 'address'
|
||||
? (
|
||||
<InputAddress
|
||||
key='address'
|
||||
label={t<string>('delegate to address')}
|
||||
onChange={setToAccount}
|
||||
type='allPlus'
|
||||
/>
|
||||
)
|
||||
: accType.type === 'validators'
|
||||
? (
|
||||
<InputAddress
|
||||
defaultValue={allVals?.[0].value}
|
||||
key='validators'
|
||||
label={t<string>('delegate to validator')}
|
||||
onChange={setToAccount}
|
||||
options={allVals}
|
||||
type='allPlus'
|
||||
/>
|
||||
)
|
||||
: accType.type === 'fellows'
|
||||
? (
|
||||
<InputAddress
|
||||
defaultValue={allFell?.[0].value}
|
||||
key='fellows'
|
||||
label={t<string>('delegate to fellow')}
|
||||
onChange={setToAccount}
|
||||
options={allFell}
|
||||
type='allPlus'
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<Activity
|
||||
palletReferenda={palletReferenda}
|
||||
trackId={isAllTracks ? -1 : trackId}
|
||||
tracks={tracks}
|
||||
value={
|
||||
accType.type === 'fellows'
|
||||
? activityFell && !!toAccount && activityFell[toAccount]
|
||||
: accType.type === 'validators'
|
||||
? activityVals && !!toAccount && activityVals[toAccount]
|
||||
: activityTo
|
||||
}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
)}
|
||||
<Modal.Actions>
|
||||
{(step !== 1) && (
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
)}
|
||||
{(step !== 2) && (
|
||||
<Button
|
||||
activeOnEnter
|
||||
icon='step-forward'
|
||||
isDisabled={
|
||||
step === 1
|
||||
? !isStep1Valid
|
||||
: !isStep2Valid
|
||||
}
|
||||
label={t<string>('Next')}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
)}
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='code-merge'
|
||||
isDisabled={
|
||||
!isStep1Valid ||
|
||||
!isStep2Valid ||
|
||||
step !== 2
|
||||
}
|
||||
label={t<string>('Delegate')}
|
||||
onStart={toggleOpen}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
<Button
|
||||
icon='code-merge'
|
||||
isDisabled={!hasAccounts}
|
||||
label={t<string>('Delegate')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Delegate);
|
||||
27
packages/page-referenda/src/Referenda/Delegate/types.ts
Normal file
27
packages/page-referenda/src/Referenda/Delegate/types.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletConvictionVotingVoteVoting } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
export interface LockResultItem {
|
||||
classId: BN;
|
||||
}
|
||||
|
||||
export type LockResult = Record<string, LockResultItem[]>;
|
||||
|
||||
export interface VoteResultCasting {
|
||||
refId: BN;
|
||||
}
|
||||
|
||||
export interface VoteResultDelegating {
|
||||
conviction: PalletConvictionVotingVoteVoting['asDelegating']['conviction']['type'];
|
||||
targetId: string;
|
||||
}
|
||||
|
||||
export interface VoteResultItem extends LockResultItem {
|
||||
casting?: VoteResultCasting[];
|
||||
delegating?: VoteResultDelegating;
|
||||
}
|
||||
|
||||
export type VoteResult = Record<string, VoteResultItem[]>;
|
||||
@ -0,0 +1,18 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { VoteResult } from './types';
|
||||
|
||||
import { createNamedHook } from '@polkadot/react-hooks';
|
||||
|
||||
import useSuperIds from './useSuperIds';
|
||||
import useVotingFor from './useVotingFor';
|
||||
|
||||
function useActivityImpl (palletVote: PalletVote, accountIds?: string[] | null): VoteResult | null | undefined {
|
||||
const identities = useSuperIds(accountIds);
|
||||
|
||||
return useVotingFor(palletVote, identities);
|
||||
}
|
||||
|
||||
export default createNamedHook('useActivity', useActivityImpl);
|
||||
@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { VoteResultItem } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook } from '@polkadot/react-hooks';
|
||||
|
||||
import useVotingFor from './useVotingFor';
|
||||
|
||||
function useActivityAccountImpl (palletVote: PalletVote, accountId?: string | null): VoteResultItem[] | null | undefined {
|
||||
const params = useMemo(
|
||||
() => (accountId && [accountId]) || null,
|
||||
[accountId]
|
||||
);
|
||||
const votingFor = useVotingFor(palletVote, params);
|
||||
|
||||
// for a single account (which we assume is user-specified), we
|
||||
// do not lookup the actual parent identity, use as-is
|
||||
return votingFor && accountId
|
||||
? votingFor[accountId] || []
|
||||
: null;
|
||||
}
|
||||
|
||||
export default createNamedHook('useActivityAccount', useActivityAccountImpl);
|
||||
@ -0,0 +1,18 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { VoteResult } from './types';
|
||||
|
||||
import { createNamedHook } from '@polkadot/react-hooks';
|
||||
|
||||
import useActivity from './useActivity';
|
||||
import useFellows from './useFellows';
|
||||
|
||||
function useActivityFellowsImpl (palletVote: PalletVote): VoteResult | null | undefined {
|
||||
const fellows = useFellows();
|
||||
|
||||
return useActivity(palletVote, fellows);
|
||||
}
|
||||
|
||||
export default createNamedHook('useActivityFellows', useActivityFellowsImpl);
|
||||
@ -0,0 +1,18 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { VoteResult } from './types';
|
||||
|
||||
import { createNamedHook } from '@polkadot/react-hooks';
|
||||
|
||||
import useActivity from './useActivity';
|
||||
import useNominators from './useNominators';
|
||||
|
||||
function useActivityNominatorsImpl (palletVote: PalletVote): VoteResult | null | undefined {
|
||||
const nominators = useNominators();
|
||||
|
||||
return useActivity(palletVote, nominators);
|
||||
}
|
||||
|
||||
export default createNamedHook('useActivityNominators', useActivityNominatorsImpl);
|
||||
29
packages/page-referenda/src/Referenda/Delegate/useFellows.ts
Normal file
29
packages/page-referenda/src/Referenda/Delegate/useFellows.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey } from '@polkadot/types';
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useMapKeys } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
const MEMBERS_OPT = {
|
||||
transform: (keys: StorageKey<[AccountId]>[]): string[] =>
|
||||
keys.map(({ args: [id] }) => id.toString())
|
||||
};
|
||||
|
||||
function useFellowsImpl (): string[] | null | undefined {
|
||||
const { api } = useApi();
|
||||
const members = useMapKeys(api.query.fellowshipCollective?.members, [], MEMBERS_OPT);
|
||||
|
||||
return useMemo(
|
||||
() => isFunction(api.query.fellowshipCollective?.members)
|
||||
? members
|
||||
: [],
|
||||
[api, members]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useFellows', useFellowsImpl);
|
||||
@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { PalletStakingNominations } from '@polkadot/types/lookup';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
const NOMINATORS_OPT = {
|
||||
transform: (optNominators: Option<PalletStakingNominations>[]): string[] =>
|
||||
optNominators.reduce<string[]>((all, o) =>
|
||||
o.isSome
|
||||
? all.concat(
|
||||
o.unwrap().targets
|
||||
.map((w) => w.toString())
|
||||
.filter((w) => !all.includes(w))
|
||||
)
|
||||
: all, []
|
||||
)
|
||||
};
|
||||
|
||||
// A list of all validators that any of our accounts nominate
|
||||
// (deduped accross accounts)
|
||||
function useNominatorsImpl (): string[] | null | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const nomineesParam = useMemo(
|
||||
() => [allAccounts],
|
||||
[allAccounts]
|
||||
);
|
||||
|
||||
const nominees = useCall(!!allAccounts.length && nomineesParam && api.query.staking?.nominators?.multi, nomineesParam, NOMINATORS_OPT);
|
||||
|
||||
return useMemo(
|
||||
() => isFunction(api.query.staking?.nominators) && allAccounts.length
|
||||
? nominees
|
||||
: [],
|
||||
[allAccounts, api, nominees]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useNominators', useNominatorsImpl);
|
||||
@ -0,0 +1,57 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@polkadot/types';
|
||||
import type { AccountId } from '@polkadot/types/interfaces';
|
||||
import type { ITuple } from '@polkadot/types/types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
const SUPEROF_OPT = {
|
||||
transform: ([[ids], optSupers]: [[string[]], Option<ITuple<[AccountId]>>[]]): string[] =>
|
||||
optSupers
|
||||
.map((opt, index) =>
|
||||
// if we have a super, use that, otherwise we default to
|
||||
// the actual passed-in identity (which is top-level)
|
||||
opt.isSome
|
||||
? opt.unwrap()[0].toString()
|
||||
: ids[index]
|
||||
)
|
||||
.reduce((all: string[], who): string[] => {
|
||||
// deupe all entries since we may have multiple nominees
|
||||
if (!all.includes(who)) {
|
||||
all.push(who);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, []),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function useSuperIdsImpl (accountIds?: string[] | null): string[] | null | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
// for the supplied accounts, retrieve the de-dupes parent identity
|
||||
const identityParam = useMemo(
|
||||
() => accountIds && [accountIds],
|
||||
[accountIds]
|
||||
);
|
||||
|
||||
const identities = useCall(identityParam && !!identityParam[0].length && api.query.identity?.superOf?.multi, identityParam, SUPEROF_OPT);
|
||||
|
||||
return useMemo(
|
||||
() => identityParam
|
||||
? identityParam[0].length
|
||||
? isFunction(api.query.identity?.superOf)
|
||||
? identities
|
||||
: accountIds
|
||||
: []
|
||||
: null,
|
||||
[api, accountIds, identities, identityParam]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useSuperIds', useSuperIdsImpl);
|
||||
@ -0,0 +1,91 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletConvictionVotingVoteVoting } from '@polkadot/types/lookup';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { LockResult, VoteResult, VoteResultItem } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
import useVotingLocks from './useVotingLocks';
|
||||
|
||||
type ForParam = [accountId: string, classId: BN];
|
||||
|
||||
const FOR_OPT = {
|
||||
transform: ([[ids], votes]: [[ForParam[]], PalletConvictionVotingVoteVoting[]]): VoteResult =>
|
||||
ids.sort((a, b) => a[1].cmp(b[1])).reduce<VoteResult>((all, [accountId, classId], index) => {
|
||||
if (!all[accountId]) {
|
||||
all[accountId] = [];
|
||||
}
|
||||
|
||||
let casting: VoteResultItem['casting'] | undefined;
|
||||
let delegating: VoteResultItem['delegating'] | undefined;
|
||||
|
||||
if (votes[index].isCasting) {
|
||||
casting = votes[index].asCasting.votes.map(([refId]) => ({ refId }));
|
||||
} else if (votes[index].isDelegating) {
|
||||
const { conviction, target } = votes[index].asDelegating;
|
||||
|
||||
delegating = { conviction: conviction.type, targetId: target.toString() };
|
||||
} else {
|
||||
// failsafe log... just in-case
|
||||
console.error(`Unable to handle PalletConvictionVotingVoteVoting type ${votes[index].type}`);
|
||||
}
|
||||
|
||||
all[accountId].push({ casting, classId, delegating });
|
||||
|
||||
return all;
|
||||
}, {}),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function getParams (locks?: LockResult | null): ForParam[] | null | undefined {
|
||||
return locks && Object.entries(locks).reduce<ForParam[]>((all, [accountId, items]) => {
|
||||
return items.reduce<ForParam[]>((all, { classId }) => {
|
||||
all.push(([accountId, classId]));
|
||||
|
||||
return all;
|
||||
}, all);
|
||||
}, []);
|
||||
}
|
||||
|
||||
function combineResult (locks: LockResult, votes: VoteResult): VoteResult {
|
||||
return Object.keys(locks).reduce<VoteResult>((all, accountId) => {
|
||||
if (!all[accountId]) {
|
||||
// when it appears in the original keys, but not here
|
||||
// we just add an empty value to keep track of this one
|
||||
all[accountId] = [];
|
||||
}
|
||||
|
||||
return all;
|
||||
}, votes);
|
||||
}
|
||||
|
||||
function useVotingForImpl (palletVote: PalletVote, accountIds?: string[] | null): VoteResult | null | undefined {
|
||||
const { api } = useApi();
|
||||
const locks = useVotingLocks(palletVote, accountIds);
|
||||
|
||||
const forParam = useMemo(
|
||||
() => [getParams(locks)],
|
||||
[locks]
|
||||
);
|
||||
|
||||
const votes = useCall(forParam && forParam[0] && api.query[palletVote]?.votingFor?.multi, forParam, FOR_OPT);
|
||||
|
||||
return useMemo(
|
||||
() => locks && forParam
|
||||
? forParam[0]
|
||||
? isFunction(api.query[palletVote]?.votingFor)
|
||||
? votes && combineResult(locks, votes)
|
||||
: locks
|
||||
: {}
|
||||
: null,
|
||||
[api, locks, forParam, palletVote, votes]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useVotingFor', useVotingForImpl);
|
||||
@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { INumber } from '@polkadot/types/types';
|
||||
import type { PalletVote } from '../../types';
|
||||
import type { LockResult } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
import { isFunction } from '@polkadot/util';
|
||||
|
||||
const LOCKS_OPT = {
|
||||
transform: ([[ids], locks]: [[string[]], [INumber, INumber][][]]): LockResult =>
|
||||
ids.reduce<LockResult>((all, accountId, index) => ({
|
||||
...all,
|
||||
[accountId]: locks[index].map(([classId, value]) => ({
|
||||
balance: value.toBn(),
|
||||
classId: classId.toBn()
|
||||
}))
|
||||
}), {}),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function useVotingLocksImpl (palletVote: PalletVote, accountIds?: string[] | null): LockResult | null | undefined {
|
||||
const { api } = useApi();
|
||||
|
||||
const locksParam = useMemo(
|
||||
() => accountIds
|
||||
? accountIds.length
|
||||
? [accountIds]
|
||||
: []
|
||||
: undefined,
|
||||
[accountIds]
|
||||
);
|
||||
|
||||
const locks = useCall(locksParam && locksParam[0] && api.query[palletVote]?.classLocksFor?.multi, locksParam, LOCKS_OPT);
|
||||
|
||||
return useMemo(
|
||||
() => locksParam
|
||||
? locksParam[0]
|
||||
? isFunction(api.query[palletVote]?.classLocksFor)
|
||||
? locks
|
||||
: locksParam[0].reduce<LockResult>((all, accountId) => ({ ...all, [accountId]: [] }), {})
|
||||
: {}
|
||||
: null,
|
||||
[api, locks, locksParam, palletVote]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useVotingLocks', useVotingLocksImpl);
|
||||
@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletReferenda, TrackDescription } from '../../types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Dropdown } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../../translate';
|
||||
import useTrackOptions from './useTrackOptions';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
exclude?: (BN | number)[];
|
||||
include?: (BN | number)[];
|
||||
onChange: (trackId: number) => void;
|
||||
palletReferenda: PalletReferenda;
|
||||
tracks: TrackDescription[];
|
||||
}
|
||||
|
||||
function TrackDropdown ({ className, exclude, include, onChange, palletReferenda, tracks }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const trackOpts = useTrackOptions(palletReferenda, tracks, include, exclude);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className={className}
|
||||
defaultValue={trackOpts[0].value}
|
||||
label={t<string>('submission track')}
|
||||
onChange={onChange}
|
||||
options={trackOpts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(TrackDropdown)`
|
||||
.trackOption {
|
||||
.faded {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
margin-top: 0.125rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
`);
|
||||
@ -1,11 +1,10 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { RawParam } from '@polkadot/react-params/types';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
import type { PalletReferenda, TrackDescription } from '../types';
|
||||
import type { PalletReferenda, TrackDescription } from '../../types';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -17,8 +16,9 @@ import { Available } from '@polkadot/react-query';
|
||||
import { getTypeDef } from '@polkadot/types/create';
|
||||
import { BN_HUNDRED, BN_ONE, BN_THOUSAND, BN_ZERO, isHex } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { getTrackInfo, getTrackName } from '../util';
|
||||
import { useTranslation } from '../../translate';
|
||||
import { getTrackInfo } from '../../util';
|
||||
import TrackDropdown from './TrackDropdown';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -39,35 +39,11 @@ interface ImageState {
|
||||
isImageLenValid: boolean;
|
||||
}
|
||||
|
||||
interface TrackOpt {
|
||||
text: React.ReactNode;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function getTrackOptions (api: ApiPromise, specName: string, palletReferenda: string, tracks?: TrackDescription[]): undefined | TrackOpt[] {
|
||||
return tracks && tracks.map(({ id, info }): TrackOpt => {
|
||||
const trackInfo = getTrackInfo(api, specName, palletReferenda, tracks, id.toNumber());
|
||||
const trackName = getTrackName(id, info);
|
||||
|
||||
return {
|
||||
text: trackInfo?.text
|
||||
? (
|
||||
<div className='trackOption'>
|
||||
<div className='normal'>{trackName}</div>
|
||||
<div className='faded'>{trackInfo.text}</div>
|
||||
</div>
|
||||
)
|
||||
: trackName,
|
||||
value: id.toNumber()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function Submit ({ className = '', isMember, members, palletReferenda, tracks }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api, specName } = useApi();
|
||||
const bestNumber = useBestNumber();
|
||||
const [isSubmitOpen, toggleSubmit] = useToggle();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [trackId, setTrack] = useState<number | undefined>(undefined);
|
||||
const [origin, setOrigin] = useState<RawParam['value'] | null>(null);
|
||||
@ -132,11 +108,6 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
[origin, trackInfo]
|
||||
);
|
||||
|
||||
const trackOpts = useMemo(
|
||||
() => getTrackOptions(api, specName, palletReferenda, tracks),
|
||||
[api, palletReferenda, specName, tracks]
|
||||
);
|
||||
|
||||
const enactOpts = useMemo(
|
||||
() => [
|
||||
{ text: t<string>('After delay'), value: 'after' },
|
||||
@ -182,11 +153,11 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
|
||||
return (
|
||||
<>
|
||||
{trackOpts && isSubmitOpen && (
|
||||
{isOpen && (
|
||||
<StyledModal
|
||||
className={className}
|
||||
header={t<string>('Submit proposal')}
|
||||
onClose={toggleSubmit}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
@ -205,19 +176,11 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t<string>('The origin (and by extension track) that you wish to submit for, each has a different period, different root and acceptance criteria.')}>
|
||||
<Dropdown
|
||||
defaultValue={trackOpts[0] && trackOpts[0].value}
|
||||
label={t<string>('submission track')}
|
||||
<TrackDropdown
|
||||
onChange={setTrack}
|
||||
options={trackOpts}
|
||||
palletReferenda={palletReferenda}
|
||||
tracks={tracks}
|
||||
/>
|
||||
{false && trackInfo?.text && (
|
||||
<Input
|
||||
isDisabled
|
||||
label={t<string>('track overview')}
|
||||
value={trackInfo?.text}
|
||||
/>
|
||||
)}
|
||||
{!trackInfo?.origin && (
|
||||
<Params
|
||||
className='originSelect'
|
||||
@ -260,7 +223,7 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns
|
||||
className='centerEnactType'
|
||||
align='center'
|
||||
hint={t<string>('The moment of enactment, either at a specific block, or after a specific number of blocks.')}
|
||||
>
|
||||
<ToggleGroup
|
||||
@ -307,7 +270,7 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
icon='plus'
|
||||
isDisabled={!selectedOrigin || !isImageHashValid || !isImageLenValid || !accountId || isInvalidAt || !preimage?.proposalHash}
|
||||
label={t<string>('Submit proposal')}
|
||||
onStart={toggleSubmit}
|
||||
onStart={toggleOpen}
|
||||
params={[
|
||||
selectedOrigin,
|
||||
{
|
||||
@ -326,9 +289,9 @@ function Submit ({ className = '', isMember, members, palletReferenda, tracks }:
|
||||
)}
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!isMember || !trackOpts}
|
||||
isDisabled={!isMember}
|
||||
label={t<string>('Submit proposal')}
|
||||
onClick={toggleSubmit}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -340,19 +303,6 @@ const StyledModal = styled(Modal)`
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.trackOption {
|
||||
.faded {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
margin-top: 0.125rem;
|
||||
opacity: var(--opacity-light);
|
||||
}
|
||||
}
|
||||
|
||||
.ui--Modal-Columns.centerEnactType > div:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Submit);
|
||||
9
packages/page-referenda/src/Referenda/Submit/types.ts
Normal file
9
packages/page-referenda/src/Referenda/Submit/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
export interface TrackOption {
|
||||
text: React.ReactNode;
|
||||
value: number;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
// Copyright 2017-2023 @polkadot/app-referenda authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { TrackDescription } from '../../types';
|
||||
import type { TrackOption } from './types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi } from '@polkadot/react-hooks';
|
||||
import { bnToBn } from '@polkadot/util';
|
||||
|
||||
import { getTrackInfo, getTrackName } from '../../util';
|
||||
|
||||
function getTrackOptions (api: ApiPromise, specName: string, palletReferenda: string, tracks: TrackDescription[], include?: (BN | number)[], exclude?: (BN | number)[]): TrackOption[] {
|
||||
const includeBn = include && include.map((v) => bnToBn(v));
|
||||
const excludeBn = exclude && exclude.map((v) => bnToBn(v));
|
||||
|
||||
return tracks
|
||||
.filter(({ id }) =>
|
||||
(
|
||||
!includeBn ||
|
||||
includeBn.some((v) => v.eq(id))
|
||||
) && (
|
||||
!excludeBn ||
|
||||
!excludeBn.some((v) => v.eq(id))
|
||||
)
|
||||
)
|
||||
.map(({ id, info }): TrackOption => {
|
||||
const trackInfo = getTrackInfo(api, specName, palletReferenda, tracks, id.toNumber());
|
||||
const trackName = getTrackName(id, info);
|
||||
|
||||
return {
|
||||
text: trackInfo?.text
|
||||
? (
|
||||
<div className='trackOption'>
|
||||
<div className='normal'>{trackName}</div>
|
||||
<div className='faded'>{trackInfo.text}</div>
|
||||
</div>
|
||||
)
|
||||
: trackName,
|
||||
value: id.toNumber()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function useTrackOptionsImpl (palletReferenda: string, tracks: TrackDescription[], include?: (BN | number)[], exclude?: (BN | number)[]): TrackOption[] {
|
||||
const { api, specName } = useApi();
|
||||
|
||||
return useMemo(
|
||||
() => getTrackOptions(api, specName, palletReferenda, tracks, include, exclude),
|
||||
[api, exclude, include, palletReferenda, specName, tracks]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useTrackOptions', useTrackOptionsImpl);
|
||||
@ -2,13 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { VoteTypeProps as Props } from './types';
|
||||
import type { VoteTypeProps as Props } from '../types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Modal, VoteValue } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
function VoteAbstain ({ accountId, id, onChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
@ -2,13 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { VoteTypeProps as Props } from './types';
|
||||
import type { VoteTypeProps as Props } from '../types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Modal, VoteValue } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
function VoteSplit ({ accountId, id, onChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
@ -2,13 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { VoteTypeProps } from './types';
|
||||
import type { VoteTypeProps } from '../types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { ConvictionDropdown, Modal, VoteValue } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { useTranslation } from '../../translate';
|
||||
|
||||
interface Props extends VoteTypeProps {
|
||||
voteLockingPeriod: BN;
|
||||
@ -5,7 +5,7 @@ import type { TFunction } from 'react-i18next';
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { Preimage } from '@polkadot/react-hooks/types';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { PalletVote, TrackInfo } from '../types';
|
||||
import type { PalletVote, TrackInfo } from '../../types';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -13,7 +13,7 @@ import styled from 'styled-components';
|
||||
import { Button, Modal, ProposedAction, ToggleGroup, TxButton, VoteAccount } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import { useTranslation } from '../../translate';
|
||||
import VoteAbstain from './VoteAbstain';
|
||||
import VoteSplit from './VoteSplit';
|
||||
import VoteStandard from './VoteStandard';
|
||||
@ -74,7 +74,7 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
const { api } = useApi();
|
||||
const { allAccounts, hasAccounts } = useAccounts();
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [isVotingOpen, toggleVoting] = useToggle();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [voteTypeIndex, setVoteTypeIndex] = useState(0);
|
||||
const [params, setParams] = useState<unknown[] | undefined>();
|
||||
|
||||
@ -105,11 +105,11 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
|
||||
return (
|
||||
<>
|
||||
{isVotingOpen && (
|
||||
{isOpen && (
|
||||
<StyledModal
|
||||
className={className}
|
||||
header={t<string>('Vote on referendum')}
|
||||
onClose={toggleVoting}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
@ -184,7 +184,7 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
accountId={accountId}
|
||||
icon='check-to-slot'
|
||||
label={t<string>('Vote')}
|
||||
onStart={toggleVoting}
|
||||
onStart={toggleOpen}
|
||||
params={params}
|
||||
tx={api.tx[palletVote].vote}
|
||||
/>
|
||||
@ -195,7 +195,7 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
accountId={accountId}
|
||||
icon='ban'
|
||||
label={t<string>('Vote Nay')}
|
||||
onStart={toggleVoting}
|
||||
onStart={toggleOpen}
|
||||
params={[id, false]}
|
||||
tx={api.tx[palletVote].vote}
|
||||
/>
|
||||
@ -203,7 +203,7 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
accountId={accountId}
|
||||
icon='check'
|
||||
label={t<string>('Vote Aye')}
|
||||
onStart={toggleVoting}
|
||||
onStart={toggleOpen}
|
||||
params={[id, true]}
|
||||
tx={api.tx[palletVote].vote}
|
||||
/>
|
||||
@ -217,7 +217,7 @@ function Voting ({ className, id, isConvictionVote, isMember, members, palletVot
|
||||
icon='check-to-slot'
|
||||
isDisabled={isDisabled}
|
||||
label={t<string>('Vote')}
|
||||
onClick={toggleVoting}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -15,6 +15,7 @@ import { BN_ZERO } from '@polkadot/util';
|
||||
import { useTranslation } from '../translate';
|
||||
import useReferenda from '../useReferenda';
|
||||
import useSummary from '../useSummary';
|
||||
import Delegate from './Delegate';
|
||||
import Group from './Group';
|
||||
import Submit from './Submit';
|
||||
import Summary from './Summary';
|
||||
@ -88,6 +89,13 @@ function Referenda ({ className, isConvictionVote, members, palletReferenda, pal
|
||||
options={trackOpts}
|
||||
value={trackSelected}
|
||||
/>
|
||||
{isConvictionVote && (
|
||||
<Delegate
|
||||
palletReferenda={palletReferenda}
|
||||
palletVote={palletVote}
|
||||
tracks={tracks}
|
||||
/>
|
||||
)}
|
||||
<AddPreimage />
|
||||
<Submit
|
||||
isMember={isMember}
|
||||
|
||||
@ -71,6 +71,7 @@ export interface TrackInfo {
|
||||
|
||||
export interface TrackInfoExt extends TrackInfo {
|
||||
track: TrackDescription;
|
||||
trackName: string;
|
||||
}
|
||||
|
||||
export interface Lock {
|
||||
|
||||
@ -25,7 +25,7 @@ export function getTrackName (trackId: BN, { name }: PalletReferendaTrackInfo):
|
||||
export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda: string, tracks: TrackDescription[], trackId?: number): TrackInfoExt | undefined {
|
||||
let info: TrackInfoExt | undefined;
|
||||
|
||||
if (tracks && trackId !== undefined) {
|
||||
if (tracks && trackId !== undefined && trackId !== -1) {
|
||||
const originMap = getGovernanceTracks(api, specName, palletReferenda);
|
||||
const track = tracks.find(({ id }) => id.eqn(trackId));
|
||||
|
||||
@ -37,7 +37,10 @@ export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda
|
||||
);
|
||||
|
||||
if (base) {
|
||||
info = objectSpread<TrackInfoExt>({ track }, base);
|
||||
info = objectSpread<TrackInfoExt>({
|
||||
track,
|
||||
trackName: getTrackName(track.id, track.info)
|
||||
}, base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ interface Props {
|
||||
labelExtra?: React.ReactNode;
|
||||
onChange?: (value: string | null) => void;
|
||||
onChangeMulti?: (value: string[]) => void;
|
||||
options?: KeyringSectionOption[];
|
||||
options?: KeyringSectionOption[] | null;
|
||||
optionsAll?: Record<string, Option[]>;
|
||||
placeholder?: string;
|
||||
type?: KeyringOption$Type;
|
||||
|
||||
@ -5,17 +5,18 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface Props {
|
||||
align?: 'center' | 'left' | 'right';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
hint?: React.ReactNode;
|
||||
}
|
||||
|
||||
function Columns ({ children, className = '', hint }: Props): React.ReactElement<Props> {
|
||||
function Columns ({ align = 'left', children, className = '', hint }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledDiv className={`${className} ui--Modal-Columns`}>
|
||||
<div>{children}</div>
|
||||
<StyledDiv className={`${className} ui--Modal-Columns ${align}Align`}>
|
||||
<div className='ui--Modal-Columns-content'>{children}</div>
|
||||
{hint && (
|
||||
<div>{hint}</div>
|
||||
<div className='ui--Modal-Columns-hint'>{hint}</div>
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
@ -26,6 +27,14 @@ const StyledDiv = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&.centerAlign > div.ui--Modal-Columns-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.rightAlign > div.ui--Modal-Columns-content {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&+& {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@ -14,8 +14,8 @@ interface Option {
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onChange: (index: number) => void;
|
||||
options: (Option | null | false)[];
|
||||
onChange: (index: number, value: string | number) => void;
|
||||
options: (Option | null | undefined | false)[];
|
||||
value: number;
|
||||
}
|
||||
|
||||
@ -23,21 +23,26 @@ interface ToggleProps {
|
||||
index: number;
|
||||
isDisabled?: boolean;
|
||||
isSelected: boolean;
|
||||
onChange: (index: number) => void;
|
||||
onChange: (index: number, value: string | number) => void;
|
||||
text: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
function ToggleIndex ({ index, isDisabled, isSelected, onChange, text }: ToggleProps): React.ReactElement<ToggleProps> {
|
||||
function ToggleIndex ({ index, isDisabled, isSelected, onChange, text, value }: ToggleProps): React.ReactElement<ToggleProps> {
|
||||
const _onClick = useCallback(
|
||||
(): void => {
|
||||
!isDisabled && onChange(index);
|
||||
!isDisabled && onChange(index, value);
|
||||
},
|
||||
[isDisabled, index, onChange]
|
||||
[isDisabled, index, onChange, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={isSelected ? 'check' : 'circle'}
|
||||
icon={
|
||||
isSelected
|
||||
? 'check'
|
||||
: 'circle'
|
||||
}
|
||||
isBasic
|
||||
isDisabled={isDisabled}
|
||||
isSelected={isSelected}
|
||||
@ -62,7 +67,7 @@ function ToggleGroup ({ className = '', onChange, options, value }: Props): Reac
|
||||
|
||||
return (
|
||||
<StyledDiv className={`${className} ui--ToggleGroup`}>
|
||||
{available.map(({ isDisabled, text }, index): React.ReactNode => (
|
||||
{available.map(({ isDisabled, text, value: optValue }, index): React.ReactNode => (
|
||||
<ToggleIndexMemo
|
||||
index={index}
|
||||
isDisabled={isDisabled}
|
||||
@ -70,6 +75,7 @@ function ToggleGroup ({ className = '', onChange, options, value }: Props): Reac
|
||||
key={index}
|
||||
onChange={onChange}
|
||||
text={text}
|
||||
value={optValue}
|
||||
/>
|
||||
))}
|
||||
</StyledDiv>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user