Introduce alliance motions (#7925)
* Introduce alliance motions * Quick-and-dirty motion import * Adjust motion submission with filtering * Adjust hooks * Thresholds * Align tech comm threshold * Adjust threshold calcs * Display announcements * Adjust incomplete * url calc * Display encoded ipfsCid * useIpfsLink with rule link * Adjust tags * Cleanups * Adjust for prime type
This commit is contained in:
parent
16389f21e7
commit
2c69be6ebc
@ -1,7 +1,11 @@
|
||||
{
|
||||
"Allowed to vote on motions": "Allowed to vote on motions",
|
||||
"Announcements": "Announcements",
|
||||
"Announcements ({{count}})": "Announcements ({{count}})",
|
||||
"Current prime member, default voting": "Current prime member, default voting",
|
||||
"Join": "Join",
|
||||
"Join alliance": "Join alliance",
|
||||
"Motions ({{count}})": "Motions ({{count}})",
|
||||
"No accounts": "No accounts",
|
||||
"No annoucements": "No annoucements",
|
||||
"No members": "No members",
|
||||
@ -13,17 +17,22 @@
|
||||
"The deposit that is reserved": "The deposit that is reserved",
|
||||
"This account will be submitted to join the aliance. It will be allocated one of the alliance roles upon joining, starting with Ally.": "This account will be submitted to join the aliance. It will be allocated one of the alliance roles upon joining, starting with Ally.",
|
||||
"Unscrupelous": "Unscrupelous",
|
||||
"Up for kicking": "Up for kicking",
|
||||
"accounts": "accounts",
|
||||
"alliance account": "alliance account",
|
||||
"alliance deposit": "alliance deposit",
|
||||
"annoucements": "annoucements",
|
||||
"code": "code",
|
||||
"codec": "codec",
|
||||
"deposit": "deposit",
|
||||
"kicking": "kicking",
|
||||
"members": "members",
|
||||
"no": "no",
|
||||
"prime voter": "prime voter",
|
||||
"role": "role",
|
||||
"rule": "rule",
|
||||
"version": "version",
|
||||
"voter": "voter",
|
||||
"websites": "websites",
|
||||
"yes": "yes"
|
||||
}
|
||||
@ -5,23 +5,35 @@ import type { Cid } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useIpfsLink } from '@polkadot/react-hooks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: Cid;
|
||||
}
|
||||
|
||||
function Website ({ className, value: { cid: { codec, version }, ipfs } }: Props): React.ReactElement<Props> {
|
||||
// TODO IPFS link
|
||||
function Website ({ className, value: { cid: { codec, hash_: { code }, version }, ipfs } }: Props): React.ReactElement<Props> {
|
||||
const ipfsLink = useIpfsLink(ipfs);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='start all'>
|
||||
{ipfs}
|
||||
{ipfsLink && (
|
||||
<a
|
||||
href={ipfsLink.ipfsUrl}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>{ipfsLink.ipfsHash}</a>
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{version.type}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{codec.toString()}
|
||||
0x{codec.toString(16)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
0x{code.toString(16)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@ -21,7 +21,8 @@ function Announcements ({ accouncements, className }: Props): React.ReactElement
|
||||
const annRef = useRef([
|
||||
[t<string>('annoucements'), 'start'],
|
||||
[t<string>('version'), 'number'],
|
||||
[t<string>('codec'), 'number']
|
||||
[t<string>('codec'), 'number'],
|
||||
[t<string>('code'), 'number']
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@ -5,7 +5,7 @@ import type { Member as MemberType } from '../types';
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, Badge, Menu, Popup, StatusContext } from '@polkadot/react-components';
|
||||
import { AddressSmall, Menu, Popup, StatusContext, Tag } from '@polkadot/react-components';
|
||||
import { useAccounts, useApi } from '@polkadot/react-hooks';
|
||||
import { FormatBalance } from '@polkadot/react-query';
|
||||
|
||||
@ -15,9 +15,11 @@ import useMemberInfo from '../useMemberInfo';
|
||||
interface Props {
|
||||
className?: string;
|
||||
info: MemberType;
|
||||
isPrime: boolean;
|
||||
isVoter: boolean;
|
||||
}
|
||||
|
||||
function Member ({ className, info: { accountId, role } }: Props): React.ReactElement<Props> {
|
||||
function Member ({ className, info: { accountId, role }, isPrime, isVoter }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
@ -39,17 +41,30 @@ function Member ({ className, info: { accountId, role } }: Props): React.ReactEl
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td className='badge'>
|
||||
{info && info.isUpForKicking && (
|
||||
<Badge
|
||||
color='red'
|
||||
icon='ban'
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='address all'>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td className='all'>
|
||||
{(info && info.isUpForKicking && (
|
||||
<Tag
|
||||
color='red'
|
||||
hover={t<string>('Up for kicking')}
|
||||
label={t<string>('kicking')}
|
||||
/>
|
||||
)) || (isPrime && (
|
||||
<Tag
|
||||
color='green'
|
||||
hover={t<string>('Current prime member, default voting')}
|
||||
label={t<string>('prime voter')}
|
||||
/>
|
||||
)) || (isVoter && (
|
||||
<Tag
|
||||
color='green'
|
||||
hover={t<string>('Allowed to vote on motions')}
|
||||
label={t<string>('voter')}
|
||||
/>
|
||||
))}
|
||||
</td>
|
||||
<td className='number'>
|
||||
{info && info.deposit && (
|
||||
<FormatBalance value={info.deposit} />
|
||||
|
||||
@ -13,9 +13,11 @@ import Member from './Member';
|
||||
interface Props {
|
||||
className?: string;
|
||||
members?: MemberType[];
|
||||
prime?: string | null;
|
||||
voters?: string[];
|
||||
}
|
||||
|
||||
function Members ({ className, members }: Props): React.ReactElement<Props> {
|
||||
function Members ({ className, members, prime, voters }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const hdrRef = useRef([
|
||||
@ -34,6 +36,8 @@ function Members ({ className, members }: Props): React.ReactElement<Props> {
|
||||
{members && members.map((m) => (
|
||||
<Member
|
||||
info={m}
|
||||
isPrime={prime === m.accountId}
|
||||
isVoter={!!voters && voters.includes(m.accountId)}
|
||||
key={m.accountId}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -6,6 +6,7 @@ import type { Member as MemberType, Rule } from '../types';
|
||||
import React from 'react';
|
||||
|
||||
import { CardSummary, Spinner, SummaryBox } from '@polkadot/react-components';
|
||||
import { useIpfsLink } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
@ -18,14 +19,22 @@ interface Props {
|
||||
|
||||
function Summary ({ className, members, rule }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const ipfsLink = useIpfsLink(rule && rule.cid && rule.cid.ipfs);
|
||||
|
||||
// TODO Turn rule into IPFS link
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t<string>('rule')}>
|
||||
{rule
|
||||
? rule.hasRule
|
||||
? t<string>('yes')
|
||||
? ipfsLink
|
||||
? (
|
||||
<a
|
||||
href={ipfsLink.ipfsUrl}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>{ipfsLink.ipfsShort}</a>
|
||||
)
|
||||
: t<string>('yes')
|
||||
: t<string>('no')
|
||||
: <Spinner />
|
||||
}
|
||||
|
||||
@ -17,12 +17,13 @@ interface Props {
|
||||
className?: string;
|
||||
isVoter?: boolean;
|
||||
members?: MemberType[];
|
||||
prime?: string | null;
|
||||
rule?: Rule;
|
||||
unscrupelous?: Unscrupelous;
|
||||
voters?: string[];
|
||||
}
|
||||
|
||||
function Overview ({ className, members, rule, unscrupelous }: Props): React.ReactElement<Props> {
|
||||
function Overview ({ className, members, prime, rule, unscrupelous, voters }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [isJoinOpen, toggleJoin] = useToggle();
|
||||
|
||||
@ -47,7 +48,11 @@ function Overview ({ className, members, rule, unscrupelous }: Props): React.Rea
|
||||
/>
|
||||
)}
|
||||
</Button.Group>
|
||||
<Members members={members} />
|
||||
<Members
|
||||
members={members}
|
||||
prime={prime}
|
||||
voters={voters}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
// Copyright 2017-2022 @polkadot/app-alliance authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
|
||||
import Motions from '@polkadot/app-tech-comm/Proposals';
|
||||
import { Tabs } from '@polkadot/react-components';
|
||||
import { useCollectiveMembers } from '@polkadot/react-hooks';
|
||||
import { useApi, useCall, useCollectiveMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import Announcements from './Announcements';
|
||||
import Overview from './Overview';
|
||||
@ -21,40 +24,66 @@ interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO Make configurable
|
||||
const DEFAULT_THRESHOLD = 2 / 3;
|
||||
|
||||
function AllianceApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { isMember: isVoter, members: voters } = useCollectiveMembers('allianceMotion');
|
||||
const { api } = useApi();
|
||||
const proposalHashes = useCall<Hash[]>(api.derive.alliance.proposalHashes);
|
||||
const { isMember: isVoter, members: voters, prime } = useCollectiveMembers('alliance');
|
||||
const accouncements = useAnnoucements();
|
||||
const members = useMembers();
|
||||
const rule = useRule();
|
||||
const unscrupelous = useUnscrupelous();
|
||||
|
||||
const itemsRef = useRef([
|
||||
const motionFilter = useCallback(
|
||||
(section: string) => section === 'alliance',
|
||||
[]
|
||||
);
|
||||
|
||||
const items = useMemo(() => [
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t<string>('Overview')
|
||||
},
|
||||
{
|
||||
name: 'motions',
|
||||
text: t<string>('Motions ({{count}})', { replace: { count: (proposalHashes && proposalHashes.length) || 0 } })
|
||||
},
|
||||
{
|
||||
name: 'announcements',
|
||||
text: t<string>('Announcements')
|
||||
text: t<string>('Announcements ({{count}})', { replace: { count: (accouncements && accouncements.length) || 0 } })
|
||||
},
|
||||
{
|
||||
name: 'unscrupelous',
|
||||
text: t<string>('Unscrupelous')
|
||||
}
|
||||
]);
|
||||
], [accouncements, proposalHashes, t]);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
items={items}
|
||||
/>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/announcements`}>
|
||||
<Announcements accouncements={accouncements} />
|
||||
</Route>
|
||||
<Route path={`${basePath}/motions`}>
|
||||
<Motions
|
||||
defaultProposal={api.tx.alliance.addUnscrupulousItems}
|
||||
defaultThreshold={DEFAULT_THRESHOLD}
|
||||
filter={motionFilter}
|
||||
isMember={isVoter}
|
||||
members={voters}
|
||||
prime={prime}
|
||||
proposalHashes={proposalHashes}
|
||||
type='alliance'
|
||||
/>
|
||||
</Route>
|
||||
<Route path={`${basePath}/unscrupelous`}>
|
||||
<Unscrupelous unscrupelous={unscrupelous} />
|
||||
</Route>
|
||||
@ -62,6 +91,7 @@ function AllianceApp ({ basePath, className }: Props): React.ReactElement<Props>
|
||||
<Overview
|
||||
isVoter={isVoter}
|
||||
members={members}
|
||||
prime={prime}
|
||||
rule={rule}
|
||||
unscrupelous={unscrupelous}
|
||||
voters={voters}
|
||||
|
||||
@ -8,24 +8,17 @@ import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
function transform (accountId: string, depositOf: Option<UInt>, upForKicking: bool): MemberInfo {
|
||||
return {
|
||||
accountId,
|
||||
deposit: depositOf.unwrapOr(null),
|
||||
isUpForKicking: upForKicking.isTrue
|
||||
};
|
||||
}
|
||||
|
||||
function useMemberInfoImpl (accountId: string): MemberInfo | undefined {
|
||||
const { api } = useApi();
|
||||
const upForKicking = useCall<bool>(api.query.alliance.upForKicking, [accountId]);
|
||||
const depositOf = useCall<Option<UInt>>(api.query.alliance.depositOf, [accountId]);
|
||||
|
||||
return useMemo(
|
||||
(): MemberInfo | undefined =>
|
||||
upForKicking && depositOf
|
||||
? transform(accountId, depositOf, upForKicking)
|
||||
: undefined,
|
||||
() => upForKicking && depositOf && {
|
||||
accountId,
|
||||
deposit: depositOf.unwrapOr(null),
|
||||
isUpForKicking: upForKicking.isTrue
|
||||
},
|
||||
[accountId, upForKicking, depositOf]
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,11 +6,11 @@ import type { Member } from './types';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@polkadot/react-hooks';
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
const ROLES = <const> ['Founder', 'Fellow', 'Ally'];
|
||||
|
||||
function addMembers (prev: Member[], query: AccountId32[][]): Member[] {
|
||||
function addMembers (prev: Member[], ...query: AccountId32[][]): Member[] {
|
||||
const all: Member[] = [];
|
||||
|
||||
for (let i = 0; i < ROLES.length; i++) {
|
||||
@ -37,16 +37,14 @@ function addMembers (prev: Member[], query: AccountId32[][]): Member[] {
|
||||
function useMembersImpl (): Member[] | undefined {
|
||||
const { api } = useApi();
|
||||
const [state, setState] = useState<Member[] | undefined>();
|
||||
const query = useCallMulti<AccountId32[][]>([
|
||||
[api.query.alliance.members, ROLES[0]],
|
||||
[api.query.alliance.members, ROLES[1]],
|
||||
[api.query.alliance.members, ROLES[2]]
|
||||
]);
|
||||
const role0 = useCall<AccountId32[]>(api.query.alliance.members, [ROLES[0]]);
|
||||
const role1 = useCall<AccountId32[]>(api.query.alliance.members, [ROLES[1]]);
|
||||
const role2 = useCall<AccountId32[]>(api.query.alliance.members, [ROLES[2]]);
|
||||
|
||||
useEffect((): void => {
|
||||
query && query.length === ROLES.length &&
|
||||
setState((prev = []) => addMembers(prev, query));
|
||||
}, [query]);
|
||||
role0 && role1 && role2 &&
|
||||
setState((prev = []) => addMembers(prev, role0, role1, role2));
|
||||
}, [role0, role1, role2]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@ -5,26 +5,33 @@ import type { Bytes } from '@polkadot/types';
|
||||
import type { AccountId32 } from '@polkadot/types/interfaces';
|
||||
import type { Unscrupelous } from './types';
|
||||
|
||||
import { createNamedHook, useApi, useCallMulti } from '@polkadot/react-hooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
function mapString (all: { toString: () => string }[]): string[] {
|
||||
return all.map((a) => a.toString());
|
||||
}
|
||||
|
||||
const OPT_MULTI = {
|
||||
transform: ([accounts, websites]: [AccountId32[], Bytes[]]): Unscrupelous => ({
|
||||
accounts: mapString(accounts),
|
||||
websites: mapString(websites.filter((b) => b.isAscii))
|
||||
})
|
||||
const OPT_ACC = {
|
||||
transform: (accounts: AccountId32[]): string[] =>
|
||||
mapString(accounts)
|
||||
};
|
||||
|
||||
const OPT_WEB = {
|
||||
transform: (websites: Bytes[]): string[] =>
|
||||
mapString(websites.filter((b) => b.isAscii))
|
||||
};
|
||||
|
||||
function useUnscrupelousImpl (): Unscrupelous | undefined {
|
||||
const { api } = useApi();
|
||||
const accounts = useCall<string[]>(api.query.alliance.unscrupulousAccounts, [], OPT_ACC);
|
||||
const websites = useCall<string[]>(api.query.alliance.unscrupulousWebsites, [], OPT_WEB);
|
||||
|
||||
return useCallMulti([
|
||||
api.query.alliance.unscrupulousAccounts,
|
||||
api.query.alliance.unscrupulousWebsites
|
||||
], OPT_MULTI);
|
||||
return useMemo(
|
||||
() => accounts && websites && { accounts, websites },
|
||||
[accounts, websites]
|
||||
);
|
||||
}
|
||||
|
||||
export default createNamedHook('useUnscrupelous', useUnscrupelousImpl);
|
||||
|
||||
@ -37,7 +37,7 @@ function ProposeExternal ({ className = '', isMember, members }: Props): React.R
|
||||
const [{ hash, isHashValid }, setHash] = useState<HashState>({ hash: '', isHashValid: false });
|
||||
const modLocation = useCollectiveInstance('council');
|
||||
|
||||
const threshold = Math.ceil((members.length || 0) * getProposalThreshold(api));
|
||||
const threshold = Math.min(members.length, Math.ceil((members.length || 0) * getProposalThreshold(api)));
|
||||
|
||||
const _onChangeHash = useCallback(
|
||||
(hash?: string): void => setHash({ hash, isHashValid: isHex(hash, 256) }),
|
||||
|
||||
@ -39,7 +39,7 @@ function Propose ({ isMember, members }: Props): React.ReactElement<Props> | nul
|
||||
useEffect((): void => {
|
||||
members && setThreshold({
|
||||
isThresholdValid: members.length !== 0,
|
||||
threshold: new BN(Math.ceil(members.length * getProposalThreshold(api)))
|
||||
threshold: new BN(Math.min(members.length, Math.ceil(members.length * getProposalThreshold(api))))
|
||||
});
|
||||
}, [api, members]);
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Copyright 2017-2022 @polkadot/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@polkadot/react-hooks/types';
|
||||
import type { Hash, Proposal, ProposalIndex } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
@ -15,7 +16,7 @@ interface Props {
|
||||
hash: Hash;
|
||||
idNumber: ProposalIndex;
|
||||
proposal: Proposal | null;
|
||||
type: 'membership' | 'technicalCommittee';
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Close ({ hasFailed, hash, idNumber, proposal, type }: Props): React.ReactElement<Props> | null {
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { CollectiveType } from '@polkadot/react-hooks/types';
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
|
||||
import React from 'react';
|
||||
@ -21,12 +22,12 @@ interface Props {
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
prime?: string | null;
|
||||
type: 'membership' | 'technicalCommittee';
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Proposal ({ className = '', imageHash, isMember, members, prime, type }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const derive = useCall<DeriveCollectiveProposal>(api.derive[type].proposal, [imageHash]);
|
||||
const derive = useCall<DeriveCollectiveProposal>(api.derive[type as 'technicalCommittee'].proposal, [imageHash]);
|
||||
const { hasFailed, isCloseable, isVoteable, remainingBlocks } = useVotingStatus(derive?.votes, members.length, type);
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// Copyright 2017-2022 @polkadot/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
import type { SubmittableExtrinsic, SubmittableExtrinsicFunction } from '@polkadot/api/types';
|
||||
import type { CollectiveType } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
@ -12,9 +13,12 @@ import { BN } from '@polkadot/util';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
defaultThreshold?: number;
|
||||
defaultValue?: SubmittableExtrinsicFunction<'promise'>;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
isMember: boolean;
|
||||
members: string[];
|
||||
type: 'membership' | 'technicalCommittee';
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
interface ProposalState {
|
||||
@ -22,14 +26,17 @@ interface ProposalState {
|
||||
proposalLength: number;
|
||||
}
|
||||
|
||||
function Propose ({ isMember, members, type }: Props): React.ReactElement<Props> | null {
|
||||
// TODO We probably want to pull this from config
|
||||
const DEFAULT_THRESHOLD = 1 / 2;
|
||||
|
||||
function Propose ({ defaultThreshold = DEFAULT_THRESHOLD, defaultValue, filter, isMember, members, type }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api, apiDefaultTxSudo } = useApi();
|
||||
const { isOpen, onClose, onOpen } = useModal();
|
||||
const [accountId, setAcountId] = useState<string | null>(null);
|
||||
const [{ proposal, proposalLength }, setProposal] = useState<ProposalState>({ proposalLength: 0 });
|
||||
const [[threshold, hasThreshold], setThreshold] = useState<[BN | null, boolean]>([
|
||||
new BN(members.length / 2 + 1),
|
||||
new BN(Math.min(members.length, (members.length * defaultThreshold) + 1)),
|
||||
true
|
||||
]);
|
||||
const modLocation = useCollectiveInstance(type);
|
||||
@ -82,7 +89,8 @@ function Propose ({ isMember, members, type }: Props): React.ReactElement<Props>
|
||||
value={threshold || undefined}
|
||||
/>
|
||||
<Extrinsic
|
||||
defaultValue={apiDefaultTxSudo}
|
||||
defaultValue={defaultValue || apiDefaultTxSudo}
|
||||
filter={filter}
|
||||
label={t<string>('proposal')}
|
||||
onChange={_onChangeExtrinsic}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Copyright 2017-2022 @polkadot/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@polkadot/react-hooks/types';
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
import type { BN } from '@polkadot/util';
|
||||
|
||||
@ -17,7 +18,7 @@ interface Props {
|
||||
members: string[];
|
||||
prime?: string | null;
|
||||
proposalId: BN | number;
|
||||
type: 'membership' | 'technicalCommittee';
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
function Voting ({ hash, isMember, members, prime, proposalId, type }: Props): React.ReactElement<Props> | null {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
// Copyright 2017-2022 @polkadot/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsicFunction } from '@polkadot/api/types';
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
import type { ComponentProps as Props } from '../types';
|
||||
import type { ComponentProps } from '../types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
@ -12,7 +13,13 @@ import { useTranslation } from '../translate';
|
||||
import Proposal from './Proposal';
|
||||
import Propose from './Propose';
|
||||
|
||||
function Proposals ({ className = '', isMember, members, prime, proposalHashes, type }: Props): React.ReactElement<Props> {
|
||||
interface Props extends ComponentProps {
|
||||
defaultProposal?: SubmittableExtrinsicFunction<'promise'>;
|
||||
defaultThreshold?: number;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
}
|
||||
|
||||
function Proposals ({ className = '', defaultProposal, defaultThreshold, filter, isMember, members, prime, proposalHashes, type }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef([
|
||||
@ -28,6 +35,9 @@ function Proposals ({ className = '', isMember, members, prime, proposalHashes,
|
||||
<div className={className}>
|
||||
<Button.Group>
|
||||
<Propose
|
||||
defaultThreshold={defaultThreshold}
|
||||
defaultValue={defaultProposal}
|
||||
filter={filter}
|
||||
isMember={isMember}
|
||||
members={members}
|
||||
type={type}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Copyright 2017-2022 @polkadot/app-tech-comm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CollectiveType } from '@polkadot/react-hooks/types';
|
||||
import type { Hash } from '@polkadot/types/interfaces';
|
||||
|
||||
export interface ComponentProps {
|
||||
@ -9,5 +10,5 @@ export interface ComponentProps {
|
||||
prime?: string | null;
|
||||
proposalHashes?: Hash[];
|
||||
members: string[];
|
||||
type: 'membership' | 'technicalCommittee';
|
||||
type: CollectiveType;
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ interface Props {
|
||||
className?: string;
|
||||
defaultArgs?: RawParam[];
|
||||
defaultValue: SubmittableExtrinsicFunction<'promise'>;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
isDisabled?: boolean;
|
||||
isError?: boolean;
|
||||
isPrivate?: boolean;
|
||||
@ -74,7 +75,7 @@ function getCallState (fn: SubmittableExtrinsicFunction<'promise'>, values: RawP
|
||||
};
|
||||
}
|
||||
|
||||
function ExtrinsicDisplay ({ defaultArgs, defaultValue, isDisabled, isError, isPrivate, label, onChange, onEnter, onError, onEscape, withLabel }: Props): React.ReactElement<Props> {
|
||||
function ExtrinsicDisplay ({ defaultArgs, defaultValue, filter, isDisabled, isError, isPrivate, label, onChange, onEnter, onError, onEscape, withLabel }: Props): React.ReactElement<Props> {
|
||||
const [{ extrinsic, values }, setDisplay] = useState<CallState>(() => getCallState(defaultValue, defaultArgs));
|
||||
|
||||
useEffect((): void => {
|
||||
@ -116,6 +117,7 @@ function ExtrinsicDisplay ({ defaultArgs, defaultValue, isDisabled, isError, isP
|
||||
<div className='extrinsics--Extrinsic'>
|
||||
<InputExtrinsic
|
||||
defaultValue={defaultValue}
|
||||
filter={filter}
|
||||
help={meta?.docs.join(' ')}
|
||||
isDisabled={isDisabled}
|
||||
isError={isError}
|
||||
|
||||
@ -17,6 +17,7 @@ import SelectSection from './SelectSection';
|
||||
interface Props {
|
||||
className?: string;
|
||||
defaultValue: SubmittableExtrinsicFunction<'promise'>;
|
||||
filter?: (section: string, method?: string) => boolean;
|
||||
help?: React.ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isError?: boolean;
|
||||
@ -26,10 +27,10 @@ interface Props {
|
||||
withLabel?: boolean;
|
||||
}
|
||||
|
||||
function InputExtrinsic ({ className = '', defaultValue, help, isDisabled, label, onChange, withLabel }: Props): React.ReactElement<Props> {
|
||||
function InputExtrinsic ({ className = '', defaultValue, filter, help, isDisabled, label, onChange, withLabel }: Props): React.ReactElement<Props> {
|
||||
const { api } = useApi();
|
||||
const [optionsMethod, setOptionsMethod] = useState<DropdownOptions>(() => methodOptions(api, defaultValue.section));
|
||||
const [optionsSection] = useState<DropdownOptions>(() => sectionOptions(api));
|
||||
const [optionsMethod, setOptionsMethod] = useState<DropdownOptions>(() => methodOptions(api, defaultValue.section, filter));
|
||||
const [optionsSection] = useState<DropdownOptions>(() => sectionOptions(api, filter));
|
||||
const [value, setValue] = useState<SubmittableExtrinsicFunction<'promise'>>((): SubmittableExtrinsicFunction<'promise'> => defaultValue);
|
||||
const [{ defaultMethod, defaultSection }] = useState(() => ({ defaultMethod: defaultValue.method, defaultSection: defaultValue.section }));
|
||||
|
||||
@ -47,13 +48,13 @@ function InputExtrinsic ({ className = '', defaultValue, help, isDisabled, label
|
||||
const _onSectionChange = useCallback(
|
||||
(newSection: string): void => {
|
||||
if (newSection !== value.section) {
|
||||
const optionsMethod = methodOptions(api, newSection);
|
||||
const optionsMethod = methodOptions(api, newSection, filter);
|
||||
|
||||
setOptionsMethod(optionsMethod);
|
||||
_onKeyChange(api.tx[newSection][optionsMethod[0].value]);
|
||||
}
|
||||
},
|
||||
[_onKeyChange, api, value]
|
||||
[_onKeyChange, api, filter, value]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -7,16 +7,20 @@ import React from 'react';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
|
||||
export default function createOptions (api: ApiPromise, sectionName: string): DropdownOptions {
|
||||
export default function createOptions (api: ApiPromise, sectionName: string, filter?: (section: string, method?: string) => boolean): DropdownOptions {
|
||||
const section = api.tx[sectionName];
|
||||
const isAllowed = !filter || filter(sectionName);
|
||||
|
||||
if (!section || Object.keys(section).length === 0) {
|
||||
if (!section || Object.keys(section).length === 0 || !isAllowed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object
|
||||
.keys(section)
|
||||
.filter((s) => !s.startsWith('$'))
|
||||
.filter((s) =>
|
||||
!s.startsWith('$') &&
|
||||
(!filter || filter(sectionName, s))
|
||||
)
|
||||
.sort()
|
||||
.map((value): DropdownOption => {
|
||||
const method = section[value];
|
||||
|
||||
@ -5,10 +5,13 @@ import type { DropdownOptions } from '../../util/types';
|
||||
|
||||
import { ApiPromise } from '@polkadot/api';
|
||||
|
||||
export default function createOptions (api: ApiPromise): DropdownOptions {
|
||||
export default function createOptions (api: ApiPromise, filter?: (section: string, method?: string) => boolean): DropdownOptions {
|
||||
return Object
|
||||
.keys(api.tx)
|
||||
.filter((s) => !s.startsWith('$'))
|
||||
.filter((s) =>
|
||||
!s.startsWith('$') &&
|
||||
(!filter || filter(s))
|
||||
)
|
||||
.sort()
|
||||
.filter((name): number => Object.keys(api.tx[name]).length)
|
||||
.map((name): { text: string; value: string } => ({
|
||||
|
||||
@ -26,6 +26,10 @@ function dispatchResult ({ data: [result] }: Event): string | null {
|
||||
return extractError(result as DispatchResult);
|
||||
}
|
||||
|
||||
function dispatchResultCouncil ({ data: [, result] }: Event): string | null {
|
||||
return extractError(result as DispatchResult);
|
||||
}
|
||||
|
||||
// [approving, timepoint, multisig, callHash, result]
|
||||
function dispatchResultMulti ({ data: [,,,, result] }: Event): string | null {
|
||||
return extractError(result as DispatchResult);
|
||||
@ -43,11 +47,18 @@ function xcmAttempted ({ data: [outcome] }: Event): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const collective: Record<string, EventCheck> = {
|
||||
Executed: dispatchResultCouncil
|
||||
};
|
||||
|
||||
const xcmPallet: Record<string, EventCheck> = {
|
||||
Attempted: xcmAttempted
|
||||
};
|
||||
|
||||
const CHECKS: Record<string, Record<string, EventCheck>> = {
|
||||
allianceMotion: collective,
|
||||
council: collective,
|
||||
membership: collective,
|
||||
multisig: {
|
||||
MultisigExecuted: dispatchResultMulti
|
||||
},
|
||||
@ -59,6 +70,7 @@ const CHECKS: Record<string, Record<string, EventCheck>> = {
|
||||
Sudid: dispatchResult,
|
||||
SudoAsDone: dispatchResult
|
||||
},
|
||||
technicalCommittee: collective,
|
||||
utility: {
|
||||
BatchInterrupted: batchInterrupted,
|
||||
DispatchedAs: dispatchResult
|
||||
|
||||
@ -50,7 +50,7 @@ export default React.memo(styled(Tag)`
|
||||
font-weight: var(--font-weight-normal);
|
||||
line-height: 1.143rem;
|
||||
margin: 0 0.125rem;
|
||||
padding: 0.571em 0.857em;
|
||||
padding: 0.25em 0.75em;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
|
||||
@ -35,6 +35,7 @@ export { useIncrement } from './useIncrement';
|
||||
export { useInflation } from './useInflation';
|
||||
export { useIpfs } from './useIpfs';
|
||||
export { useIpfsFetch } from './useIpfsFetch';
|
||||
export { useIpfsLink } from './useIpfsLink';
|
||||
export { useIsMountedRef } from './useIsMountedRef';
|
||||
export { useJudgements } from './useJudgements';
|
||||
export { useLedger } from './useLedger';
|
||||
|
||||
@ -27,7 +27,7 @@ export type TxDefs = SubmittableExtrinsic<'promise'> | IExtrinsic | Call | TxDef
|
||||
|
||||
export type TxSource<T extends TxDefs> = [T, boolean];
|
||||
|
||||
export type CollectiveType = 'allianceMotion' | 'council' | 'membership' | 'technicalCommittee';
|
||||
export type CollectiveType = 'alliance' | 'council' | 'membership' | 'technicalCommittee';
|
||||
|
||||
export interface ModalState {
|
||||
isOpen: boolean;
|
||||
|
||||
28
packages/react-hooks/src/useIpfsLink.ts
Normal file
28
packages/react-hooks/src/useIpfsLink.ts
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2022 @polkadot/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook } from './createNamedHook';
|
||||
|
||||
interface Result {
|
||||
ipfsHash: string;
|
||||
ipfsShort: string;
|
||||
ipfsUrl: string;
|
||||
}
|
||||
|
||||
function useIpfsLinkImpl (ipfsHash?: string | null): Result | null {
|
||||
return useMemo(
|
||||
() => ipfsHash
|
||||
? {
|
||||
ipfsHash,
|
||||
ipfsShort: `${ipfsHash.substring(0, 4)}…${ipfsHash.slice(-4)}`,
|
||||
// ipfsUrl: `https://cloudflare-ipfs.com/ipfs/${ipfs}`
|
||||
ipfsUrl: `https://ipfs.io/ipfs/${ipfsHash}`
|
||||
}
|
||||
: null,
|
||||
[ipfsHash]
|
||||
);
|
||||
}
|
||||
|
||||
export const useIpfsLink = createNamedHook('useIpfsLink', useIpfsLinkImpl);
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
import type { BlockNumber, Votes } from '@polkadot/types/interfaces';
|
||||
import type { BN } from '@polkadot/util';
|
||||
import type { CollectiveType } from './types';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@ -23,7 +24,7 @@ interface State {
|
||||
|
||||
const DEFAULT_STATUS = { hasFailed: false, hasPassed: false, isCloseable: false, isVoteable: false, remainingBlocks: null };
|
||||
|
||||
function getStatus (api: ApiPromise, bestNumber: BlockNumber, votes: Votes, numMembers: number, section: 'council' | 'membership' | 'technicalCommittee'): State {
|
||||
function getStatus (api: ApiPromise, bestNumber: BlockNumber, votes: Votes, numMembers: number, section: CollectiveType): State {
|
||||
const [instance] = api.registry.getModuleInstances(api.runtimeVersion.specName.toString(), section) || [section];
|
||||
const modLocation = isFunction(api.tx[instance as 'technicalCommittee']?.close)
|
||||
? instance
|
||||
@ -56,7 +57,7 @@ function getStatus (api: ApiPromise, bestNumber: BlockNumber, votes: Votes, numM
|
||||
};
|
||||
}
|
||||
|
||||
function useVotingStatusImpl (votes: Votes | null | undefined, numMembers: number, section: 'council' | 'membership' | 'technicalCommittee'): State {
|
||||
function useVotingStatusImpl (votes: Votes | null | undefined, numMembers: number, section: CollectiveType): State {
|
||||
const { api } = useApi();
|
||||
const bestNumber = useBestNumber();
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Copyright 2017-2022 @polkadot/react-params authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletAllianceCid } from '@polkadot/types/lookup';
|
||||
import type { Props } from '../types';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
@ -8,13 +9,19 @@ import React, { useCallback, useState } from 'react';
|
||||
import { Input } from '@polkadot/react-components';
|
||||
import { isCodec } from '@polkadot/util';
|
||||
|
||||
import { fromIpfsCid } from '../util';
|
||||
import { fromIpfsCid, toIpfsCid } from '../util';
|
||||
import Bare from './Bare';
|
||||
import Static from './Static';
|
||||
import Struct from './Struct';
|
||||
|
||||
function Cid (props: Props): React.ReactElement<Props> {
|
||||
const { className = '', defaultValue, isDisabled, isError, label, onChange, withLabel } = props;
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const [ipfsCid] = useState<string | null>(() =>
|
||||
isDisabled && defaultValue && isCodec(defaultValue.value)
|
||||
? toIpfsCid(defaultValue.value as PalletAllianceCid)
|
||||
: null
|
||||
);
|
||||
const [isStruct] = useState<boolean>(() => isDisabled || !defaultValue || isCodec(defaultValue.value));
|
||||
|
||||
const _onChange = useCallback(
|
||||
@ -31,7 +38,20 @@ function Cid (props: Props): React.ReactElement<Props> {
|
||||
[onChange]
|
||||
);
|
||||
|
||||
if (isStruct) {
|
||||
if (ipfsCid) {
|
||||
return (
|
||||
<Static {...props}>
|
||||
<Input
|
||||
className='full'
|
||||
isDisabled
|
||||
label='ipfs'
|
||||
type='text'
|
||||
value={ipfsCid}
|
||||
withLabel={withLabel}
|
||||
/>
|
||||
</Static>
|
||||
);
|
||||
} else if (isStruct) {
|
||||
return <Struct {...props} />;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user