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:
Jaco 2022-07-29 11:08:00 +02:00 committed by GitHub
parent 16389f21e7
commit 2c69be6ebc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 268 additions and 91 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 } => ({

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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