diff --git a/packages/apps/public/locales/en/app-alliance.json b/packages/apps/public/locales/en/app-alliance.json index 411df1df31..779b3e1926 100644 --- a/packages/apps/public/locales/en/app-alliance.json +++ b/packages/apps/public/locales/en/app-alliance.json @@ -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" } \ No newline at end of file diff --git a/packages/page-alliance/src/Announcements/Accouncement.tsx b/packages/page-alliance/src/Announcements/Accouncement.tsx index 352f4dd402..f30320b591 100644 --- a/packages/page-alliance/src/Announcements/Accouncement.tsx +++ b/packages/page-alliance/src/Announcements/Accouncement.tsx @@ -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 { - // TODO IPFS link +function Website ({ className, value: { cid: { codec, hash_: { code }, version }, ipfs } }: Props): React.ReactElement { + const ipfsLink = useIpfsLink(ipfs); + return ( - {ipfs} + {ipfsLink && ( + {ipfsLink.ipfsHash} + )} {version.type} - {codec.toString()} + 0x{codec.toString(16)} + + + 0x{code.toString(16)} ); diff --git a/packages/page-alliance/src/Announcements/index.tsx b/packages/page-alliance/src/Announcements/index.tsx index 2e6f4a323b..cde3890fdf 100644 --- a/packages/page-alliance/src/Announcements/index.tsx +++ b/packages/page-alliance/src/Announcements/index.tsx @@ -21,7 +21,8 @@ function Announcements ({ accouncements, className }: Props): React.ReactElement const annRef = useRef([ [t('annoucements'), 'start'], [t('version'), 'number'], - [t('codec'), 'number'] + [t('codec'), 'number'], + [t('code'), 'number'] ]); return ( diff --git a/packages/page-alliance/src/Overview/Member.tsx b/packages/page-alliance/src/Overview/Member.tsx index 67a897145d..36ef470d79 100644 --- a/packages/page-alliance/src/Overview/Member.tsx +++ b/packages/page-alliance/src/Overview/Member.tsx @@ -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 { +function Member ({ className, info: { accountId, role }, isPrime, isVoter }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); const { allAccounts } = useAccounts(); @@ -39,17 +41,30 @@ function Member ({ className, info: { accountId, role } }: Props): React.ReactEl return ( - - {info && info.isUpForKicking && ( - - )} - - + + + {(info && info.isUpForKicking && ( + ('Up for kicking')} + label={t('kicking')} + /> + )) || (isPrime && ( + ('Current prime member, default voting')} + label={t('prime voter')} + /> + )) || (isVoter && ( + ('Allowed to vote on motions')} + label={t('voter')} + /> + ))} + {info && info.deposit && ( diff --git a/packages/page-alliance/src/Overview/Members.tsx b/packages/page-alliance/src/Overview/Members.tsx index ed4c2c4d82..e63f84a72b 100644 --- a/packages/page-alliance/src/Overview/Members.tsx +++ b/packages/page-alliance/src/Overview/Members.tsx @@ -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 { +function Members ({ className, members, prime, voters }: Props): React.ReactElement { const { t } = useTranslation(); const hdrRef = useRef([ @@ -34,6 +36,8 @@ function Members ({ className, members }: Props): React.ReactElement { {members && members.map((m) => ( ))} diff --git a/packages/page-alliance/src/Overview/Summary.tsx b/packages/page-alliance/src/Overview/Summary.tsx index 1f0f6bd732..d8a659a414 100644 --- a/packages/page-alliance/src/Overview/Summary.tsx +++ b/packages/page-alliance/src/Overview/Summary.tsx @@ -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 { const { t } = useTranslation(); + const ipfsLink = useIpfsLink(rule && rule.cid && rule.cid.ipfs); - // TODO Turn rule into IPFS link return ( ('rule')}> {rule ? rule.hasRule - ? t('yes') + ? ipfsLink + ? ( + {ipfsLink.ipfsShort} + ) + : t('yes') : t('no') : } diff --git a/packages/page-alliance/src/Overview/index.tsx b/packages/page-alliance/src/Overview/index.tsx index 81e30f9cae..d0308c026c 100644 --- a/packages/page-alliance/src/Overview/index.tsx +++ b/packages/page-alliance/src/Overview/index.tsx @@ -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 { +function Overview ({ className, members, prime, rule, unscrupelous, voters }: Props): React.ReactElement { const { t } = useTranslation(); const [isJoinOpen, toggleJoin] = useToggle(); @@ -47,7 +48,11 @@ function Overview ({ className, members, rule, unscrupelous }: Props): React.Rea /> )} - + ); } diff --git a/packages/page-alliance/src/index.tsx b/packages/page-alliance/src/index.tsx index 8452e21811..577bad34c3 100644 --- a/packages/page-alliance/src/index.tsx +++ b/packages/page-alliance/src/index.tsx @@ -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 { const { t } = useTranslation(); - const { isMember: isVoter, members: voters } = useCollectiveMembers('allianceMotion'); + const { api } = useApi(); + const proposalHashes = useCall(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('Overview') }, + { + name: 'motions', + text: t('Motions ({{count}})', { replace: { count: (proposalHashes && proposalHashes.length) || 0 } }) + }, { name: 'announcements', - text: t('Announcements') + text: t('Announcements ({{count}})', { replace: { count: (accouncements && accouncements.length) || 0 } }) }, { name: 'unscrupelous', text: t('Unscrupelous') } - ]); + ], [accouncements, proposalHashes, t]); return (
+ + + @@ -62,6 +91,7 @@ function AllianceApp ({ basePath, className }: Props): React.ReactElement , upForKicking: bool): MemberInfo { - return { - accountId, - deposit: depositOf.unwrapOr(null), - isUpForKicking: upForKicking.isTrue - }; -} - function useMemberInfoImpl (accountId: string): MemberInfo | undefined { const { api } = useApi(); const upForKicking = useCall(api.query.alliance.upForKicking, [accountId]); const depositOf = useCall>(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] ); } diff --git a/packages/page-alliance/src/useMembers.ts b/packages/page-alliance/src/useMembers.ts index 1891d079ce..984a9c838e 100644 --- a/packages/page-alliance/src/useMembers.ts +++ b/packages/page-alliance/src/useMembers.ts @@ -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 = ['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(); - const query = useCallMulti([ - [api.query.alliance.members, ROLES[0]], - [api.query.alliance.members, ROLES[1]], - [api.query.alliance.members, ROLES[2]] - ]); + const role0 = useCall(api.query.alliance.members, [ROLES[0]]); + const role1 = useCall(api.query.alliance.members, [ROLES[1]]); + const role2 = useCall(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; } diff --git a/packages/page-alliance/src/useUnscrupelous.ts b/packages/page-alliance/src/useUnscrupelous.ts index 1a5d0bdcc9..ee3a4d5847 100644 --- a/packages/page-alliance/src/useUnscrupelous.ts +++ b/packages/page-alliance/src/useUnscrupelous.ts @@ -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(api.query.alliance.unscrupulousAccounts, [], OPT_ACC); + const websites = useCall(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); diff --git a/packages/page-council/src/Motions/ProposeExternal.tsx b/packages/page-council/src/Motions/ProposeExternal.tsx index 8c98f9007b..61f166007b 100644 --- a/packages/page-council/src/Motions/ProposeExternal.tsx +++ b/packages/page-council/src/Motions/ProposeExternal.tsx @@ -37,7 +37,7 @@ function ProposeExternal ({ className = '', isMember, members }: Props): React.R const [{ hash, isHashValid }, setHash] = useState({ 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) }), diff --git a/packages/page-council/src/Motions/ProposeMotion.tsx b/packages/page-council/src/Motions/ProposeMotion.tsx index 1be6e971dc..750837ca95 100644 --- a/packages/page-council/src/Motions/ProposeMotion.tsx +++ b/packages/page-council/src/Motions/ProposeMotion.tsx @@ -39,7 +39,7 @@ function Propose ({ isMember, members }: Props): React.ReactElement | 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]); diff --git a/packages/page-tech-comm/src/Proposals/Close.tsx b/packages/page-tech-comm/src/Proposals/Close.tsx index d3f66b23f1..8fe45598a6 100644 --- a/packages/page-tech-comm/src/Proposals/Close.tsx +++ b/packages/page-tech-comm/src/Proposals/Close.tsx @@ -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 | null { diff --git a/packages/page-tech-comm/src/Proposals/Proposal.tsx b/packages/page-tech-comm/src/Proposals/Proposal.tsx index fe898e3678..243d017633 100644 --- a/packages/page-tech-comm/src/Proposals/Proposal.tsx +++ b/packages/page-tech-comm/src/Proposals/Proposal.tsx @@ -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 | null { const { api } = useApi(); - const derive = useCall(api.derive[type].proposal, [imageHash]); + const derive = useCall(api.derive[type as 'technicalCommittee'].proposal, [imageHash]); const { hasFailed, isCloseable, isVoteable, remainingBlocks } = useVotingStatus(derive?.votes, members.length, type); const modLocation = useCollectiveInstance(type); diff --git a/packages/page-tech-comm/src/Proposals/Propose.tsx b/packages/page-tech-comm/src/Proposals/Propose.tsx index 291ed1d248..7c5cf9fb3f 100644 --- a/packages/page-tech-comm/src/Proposals/Propose.tsx +++ b/packages/page-tech-comm/src/Proposals/Propose.tsx @@ -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 | 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 | null { const { t } = useTranslation(); const { api, apiDefaultTxSudo } = useApi(); const { isOpen, onClose, onOpen } = useModal(); const [accountId, setAcountId] = useState(null); const [{ proposal, proposalLength }, setProposal] = useState({ 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 value={threshold || undefined} /> ('proposal')} onChange={_onChangeExtrinsic} /> diff --git a/packages/page-tech-comm/src/Proposals/Voting.tsx b/packages/page-tech-comm/src/Proposals/Voting.tsx index b19dd04bc6..1c115b7a08 100644 --- a/packages/page-tech-comm/src/Proposals/Voting.tsx +++ b/packages/page-tech-comm/src/Proposals/Voting.tsx @@ -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 | null { diff --git a/packages/page-tech-comm/src/Proposals/index.tsx b/packages/page-tech-comm/src/Proposals/index.tsx index 9949d58469..3610806ec9 100644 --- a/packages/page-tech-comm/src/Proposals/index.tsx +++ b/packages/page-tech-comm/src/Proposals/index.tsx @@ -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 { +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 { const { t } = useTranslation(); const headerRef = useRef([ @@ -28,6 +35,9 @@ function Proposals ({ className = '', isMember, members, prime, proposalHashes,
; + 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 { +function ExtrinsicDisplay ({ defaultArgs, defaultValue, filter, isDisabled, isError, isPrivate, label, onChange, onEnter, onError, onEscape, withLabel }: Props): React.ReactElement { const [{ extrinsic, values }, setDisplay] = useState(() => getCallState(defaultValue, defaultArgs)); useEffect((): void => { @@ -116,6 +117,7 @@ function ExtrinsicDisplay ({ defaultArgs, defaultValue, isDisabled, isError, isP
; + 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 { +function InputExtrinsic ({ className = '', defaultValue, filter, help, isDisabled, label, onChange, withLabel }: Props): React.ReactElement { const { api } = useApi(); - const [optionsMethod, setOptionsMethod] = useState(() => methodOptions(api, defaultValue.section)); - const [optionsSection] = useState(() => sectionOptions(api)); + const [optionsMethod, setOptionsMethod] = useState(() => methodOptions(api, defaultValue.section, filter)); + const [optionsSection] = useState(() => sectionOptions(api, filter)); const [value, setValue] = useState>((): 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 ( diff --git a/packages/react-components/src/InputExtrinsic/options/method.tsx b/packages/react-components/src/InputExtrinsic/options/method.tsx index eebd39bba0..aa577a7a84 100644 --- a/packages/react-components/src/InputExtrinsic/options/method.tsx +++ b/packages/react-components/src/InputExtrinsic/options/method.tsx @@ -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]; diff --git a/packages/react-components/src/InputExtrinsic/options/section.ts b/packages/react-components/src/InputExtrinsic/options/section.ts index 3d0b75ae41..e3e2e1582a 100644 --- a/packages/react-components/src/InputExtrinsic/options/section.ts +++ b/packages/react-components/src/InputExtrinsic/options/section.ts @@ -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 } => ({ diff --git a/packages/react-components/src/Status/checks.ts b/packages/react-components/src/Status/checks.ts index 65a3354be7..a89188602e 100644 --- a/packages/react-components/src/Status/checks.ts +++ b/packages/react-components/src/Status/checks.ts @@ -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 = { + Executed: dispatchResultCouncil +}; + const xcmPallet: Record = { Attempted: xcmAttempted }; const CHECKS: Record> = { + allianceMotion: collective, + council: collective, + membership: collective, multisig: { MultisigExecuted: dispatchResultMulti }, @@ -59,6 +70,7 @@ const CHECKS: Record> = { Sudid: dispatchResult, SudoAsDone: dispatchResult }, + technicalCommittee: collective, utility: { BatchInterrupted: batchInterrupted, DispatchedAs: dispatchResult diff --git a/packages/react-components/src/Tag.tsx b/packages/react-components/src/Tag.tsx index f7ea47b67b..3e5572f2e9 100644 --- a/packages/react-components/src/Tag.tsx +++ b/packages/react-components/src/Tag.tsx @@ -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; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 8c4306889b..4a30ebbd3c 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -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'; diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index a87af81e06..994bfcdd0b 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -27,7 +27,7 @@ export type TxDefs = SubmittableExtrinsic<'promise'> | IExtrinsic | Call | TxDef export type TxSource = [T, boolean]; -export type CollectiveType = 'allianceMotion' | 'council' | 'membership' | 'technicalCommittee'; +export type CollectiveType = 'alliance' | 'council' | 'membership' | 'technicalCommittee'; export interface ModalState { isOpen: boolean; diff --git a/packages/react-hooks/src/useIpfsLink.ts b/packages/react-hooks/src/useIpfsLink.ts new file mode 100644 index 0000000000..17654ad255 --- /dev/null +++ b/packages/react-hooks/src/useIpfsLink.ts @@ -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); diff --git a/packages/react-hooks/src/useVotingStatus.ts b/packages/react-hooks/src/useVotingStatus.ts index af30d3172d..8267820da5 100644 --- a/packages/react-hooks/src/useVotingStatus.ts +++ b/packages/react-hooks/src/useVotingStatus.ts @@ -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(); diff --git a/packages/react-params/src/Param/Cid.tsx b/packages/react-params/src/Param/Cid.tsx index 732cf2d7f1..5c752455de 100644 --- a/packages/react-params/src/Param/Cid.tsx +++ b/packages/react-params/src/Param/Cid.tsx @@ -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 { const { className = '', defaultValue, isDisabled, isError, label, onChange, withLabel } = props; const [isValid, setIsValid] = useState(false); + const [ipfsCid] = useState(() => + isDisabled && defaultValue && isCodec(defaultValue.value) + ? toIpfsCid(defaultValue.value as PalletAllianceCid) + : null + ); const [isStruct] = useState(() => isDisabled || !defaultValue || isCodec(defaultValue.value)); const _onChange = useCallback( @@ -31,7 +38,20 @@ function Cid (props: Props): React.ReactElement { [onChange] ); - if (isStruct) { + if (ipfsCid) { + return ( + + + + ); + } else if (isStruct) { return ; }