Disable ranked voting (based on compares) (#8493)

This commit is contained in:
Jaco 2022-12-06 14:42:33 +02:00 committed by GitHub
parent fd2a1ceee4
commit 7ee315dad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 178 additions and 55 deletions

View File

@ -1,30 +1,40 @@
// Copyright 2017-2022 @polkadot/app-config authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import type { TrackInfo } from './types';
function compareFellowshipRank (trackId: number): (rank: BN) => boolean {
return (rank: BN): boolean =>
rank.gten(trackId);
}
export const kusama: Record<string, TrackInfo[]> = {
fellowshipReferenda: [
{
compare: compareFellowshipRank(0),
id: 0,
name: 'candidates',
origin: { Origins: 'FellowshipInitiates' },
text: 'Origin commanded by any members of the Polkadot Fellowship (no Dan grade needed)'
},
{
compare: compareFellowshipRank(1),
id: 1,
name: 'members',
origin: { Origins: 'Fellowship1Dan' },
text: 'Origin commanded by rank 1 of the Polkadot Fellowship and with a success of 1'
},
{
compare: compareFellowshipRank(2),
id: 2,
name: 'proficients',
origin: { Origins: 'Fellowship2Dan' },
text: 'Origin commanded by rank 2 of the Polkadot Fellowship and with a success of 2'
},
{
compare: compareFellowshipRank(3),
id: 3,
name: 'fellows',
origin: [
@ -34,12 +44,14 @@ export const kusama: Record<string, TrackInfo[]> = {
text: 'Origin commanded by Polkadot Fellows (3rd Dan fellows or greater)'
},
{
compare: compareFellowshipRank(4),
id: 4,
name: 'senior fellows',
origin: { Origins: 'Fellowship4Dan' },
text: 'Origin commanded by rank 4 of the Polkadot Fellowship and with a success of 4'
},
{
compare: compareFellowshipRank(5),
id: 5,
name: 'experts',
origin: [
@ -49,12 +61,14 @@ export const kusama: Record<string, TrackInfo[]> = {
text: 'Origin commanded by Polkadot Experts (5th Dan fellows or greater)'
},
{
compare: compareFellowshipRank(6),
id: 6,
name: 'senior experts',
origin: { Origins: 'Fellowship6Dan' },
text: 'Origin commanded by rank 6 of the Polkadot Fellowship and with a success of 6'
},
{
compare: compareFellowshipRank(7),
id: 7,
name: 'masters',
origin: [
@ -64,12 +78,14 @@ export const kusama: Record<string, TrackInfo[]> = {
text: 'Origin commanded by Polkadot Masters (7th Dan fellows of greater)'
},
{
compare: compareFellowshipRank(8),
id: 8,
name: 'senior masters',
origin: { Origins: 'Fellowship8Dan' },
text: 'Origin commanded by rank 8 of the Polkadot Fellowship and with a success of 8'
},
{
compare: compareFellowshipRank(9),
id: 9,
name: 'grand masters',
origin: { Origins: 'Fellowship9Dan' },

View File

@ -1,9 +1,13 @@
// Copyright 2017-2022 @polkadot/app-config authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
export type Origin = { system: string } | { Origins: string };
export interface TrackInfo {
// TODO We would want to make this generic for non-fellowship
compare?: (memberInfo: BN) => boolean;
id: number;
name: string;
origin: Origin | Origin[];

View File

@ -52,6 +52,7 @@ function App ({ basePath, className, palletColl, palletPoll }: Props): React.Rea
members={members && members.memberIds}
palletReferenda={palletPoll}
palletVote={palletColl}
ranks={members && members.memberRanks}
/>
</Route>
<Route>

View File

@ -4,6 +4,7 @@
import type { Option } from '@polkadot/types';
import type { AccountId32 } from '@polkadot/types/interfaces';
import type { PalletRankedCollectiveMemberRecord } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { Member, PalletColl } from './types';
import { useMemo } from 'react';
@ -14,6 +15,7 @@ import useMembersIds from './useMemberIds';
interface Result {
memberIds: string[];
memberRanks: BN[];
members: Member[];
}
@ -27,10 +29,10 @@ const OPT_MEM = {
accountId: accountId.toString(),
info
}));
const memberIds = members.map(({ accountId }) => accountId);
return {
memberIds,
memberIds: members.map(({ accountId }) => accountId),
memberRanks: members.map(({ info }) => info.rank),
members
};
},
@ -44,7 +46,7 @@ function useMembersImpl (collective: PalletColl): Result | undefined {
return useMemo(
() => ids && ids.length === 0
? { memberIds: [], members: [] }
? { memberIds: [], memberRanks: [], members: [] }
: result,
[ids, result]
);

View File

@ -0,0 +1,74 @@
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { PalletReferenda, PalletVote, ReferendaGroup } from '../types';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { Table } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import { getTrackInfo } from '../util';
import Referendum from './Referendum';
interface Props extends ReferendaGroup {
className?: string;
isMember: boolean;
members?: string[];
palletReferenda: PalletReferenda;
palletVote: PalletVote;
ranks?: BN[];
tracks?: [BN, PalletReferendaTrackInfo][] | undefined;
}
function Group ({ className, isMember, members, palletReferenda, palletVote, ranks, referenda, trackId, trackName, tracks }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, specName } = useApi();
const trackInfo = useMemo(
() => getTrackInfo(api, specName, palletReferenda, tracks, trackId?.toNumber()),
[api, specName, palletReferenda, tracks, trackId]
);
return (
<Table
className={className}
empty={referenda && t<string>('No active referenda')}
header={[
[trackName ? <>{trackName}<div>{trackInfo?.text}</div></> : t('referenda'), 'start', 7],
[undefined, undefined, 1]
]}
key={
trackName
? `track:${trackName}`
: 'untracked'
}
>
{referenda && referenda.map((r) => (
<Referendum
isMember={isMember}
key={r.key}
members={members}
palletReferenda={palletReferenda}
palletVote={palletVote}
ranks={ranks}
trackInfo={trackInfo}
value={r}
/>
))}
</Table>
);
}
export default React.memo(styled(Group)`
th > h1 > div {
display: inline-block;
font-size: 1rem;
padding-left: 1.5rem;
text-overflow: ellipsis;
}
`);

View File

@ -71,7 +71,7 @@ function expandOngoing (info: Referendum['info'], track?: PalletReferendaTrackIn
};
}
function Ongoing ({ isMember, members, palletReferenda, palletVote, value: { id, info, isConvictionVote, track } }: Props): React.ReactElement<Props> {
function Ongoing ({ isMember, members, palletReferenda, palletVote, ranks, trackInfo, value: { id, info, isConvictionVote, track } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { ongoing: { decisionDeposit, submissionDeposit, tally }, periods: { confirmEnd, decideEnd, periodEnd }, proposalHash, shortHash, tallyTotal } = useMemo(
@ -133,6 +133,8 @@ function Ongoing ({ isMember, members, palletReferenda, palletVote, value: { id,
members={members}
palletVote={palletVote}
preimage={preimage}
ranks={ranks}
trackInfo={trackInfo}
/>
</td>
</>

View File

@ -3,9 +3,9 @@
import type { Preimage } from '@polkadot/app-preimages/types';
import type { BN } from '@polkadot/util';
import type { PalletVote } from '../types';
import type { PalletVote, TrackInfo } from '../types';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Button, ConvictionDropdown, Modal, ProposedAction, TxButton, VoteAccount, VoteValue } from '@polkadot/react-components';
import { useAccounts, useApi, useToggle } from '@polkadot/react-hooks';
@ -19,17 +19,49 @@ interface Props {
members?: string[];
palletVote: PalletVote;
preimage?: Preimage;
ranks?: BN[];
trackInfo?: TrackInfo;
}
function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage }: Props): React.ReactElement<Props> | null {
function filterMembers (allAccounts: string[], members?: string[], ranks?: BN[], trackInfo?: TrackInfo): string[] | undefined {
if (members) {
const accounts = members.filter((a) => allAccounts.includes(a));
if (ranks && trackInfo && trackInfo.compare) {
const cmp = trackInfo.compare;
return accounts.filter((_, i) => cmp(ranks[i]));
}
return accounts;
}
return members;
}
function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage, ranks, trackInfo }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { hasAccounts } = useAccounts();
const { allAccounts, hasAccounts } = useAccounts();
const [accountId, setAccountId] = useState<string | null>(null);
const [balance, setBalance] = useState<BN | undefined>();
const [conviction, setConviction] = useState(0);
const [isVotingOpen, toggleVoting] = useToggle();
const filteredMembers = useMemo(
() => filterMembers(allAccounts, members, ranks, trackInfo),
[allAccounts, members, ranks, trackInfo]
);
const isDisabled = useMemo(
() => !isMember || (
members && filteredMembers
? !filteredMembers.length
: false
),
[filteredMembers, isMember, members]
);
if (!hasAccounts) {
return null;
}
@ -53,7 +85,7 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage
)}
<Modal.Columns hint={t<string>('The vote will be recorded for this account. If another account delegated to this one, the delegated votes will also be counted.')}>
<VoteAccount
filter={members}
filter={filteredMembers}
onChange={setAccountId}
/>
</Modal.Columns>
@ -84,7 +116,6 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage
<TxButton
accountId={accountId}
icon='ban'
isDisabled={!isMember}
label={t<string>('Vote Nay')}
onStart={toggleVoting}
params={
@ -97,7 +128,6 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage
<TxButton
accountId={accountId}
icon='check'
isDisabled={!isMember}
label={t<string>('Vote Aye')}
onStart={toggleVoting}
params={
@ -112,6 +142,7 @@ function Voting ({ id, isConvictionVote, isMember, members, palletVote, preimage
)}
<Button
icon='check-to-slot'
isDisabled={isDisabled}
label={t<string>('Vote')}
onClick={toggleVoting}
/>

View File

@ -1,20 +1,20 @@
// Copyright 2017-2022 @polkadot/app-referenda authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';
import type { PalletReferenda, PalletVote, ReferendaGroup } from '../types';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import AddPreimage from '@polkadot/app-preimages/Preimages/Add';
import { Button, Dropdown, Table } from '@polkadot/react-components';
import { useAccounts, useApi } from '@polkadot/react-hooks';
import { Button, Dropdown } from '@polkadot/react-components';
import { useAccounts } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import useReferenda from '../useReferenda';
import useSummary from '../useSummary';
import { getTrackInfo } from '../util';
import Referendum from './Referendum';
import Group from './Group';
import Submit from './Submit';
import Summary from './Summary';
@ -25,11 +25,11 @@ interface Props {
members?: string[];
palletReferenda: PalletReferenda;
palletVote: PalletVote;
ranks?: BN[];
}
function Referenda ({ className, members, palletReferenda, palletVote }: Props): React.ReactElement<Props> {
function Referenda ({ className, members, palletReferenda, palletVote, ranks }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, specName } = useApi();
const { allAccounts } = useAccounts();
const [grouped, tracks] = useReferenda(palletReferenda);
const summary = useSummary(palletReferenda, grouped);
@ -80,43 +80,25 @@ function Referenda ({ className, members, palletReferenda, palletVote }: Props):
tracks={tracks}
/>
</Button.Group>
{filtered.map(({ referenda, trackId, trackName }: ReferendaGroup) => (
<Table
empty={referenda && t<string>('No active referenda')}
header={[
[trackName ? <>{trackName}<div>{getTrackInfo(api, specName, palletReferenda, tracks, trackId?.toNumber())?.text}</div></> : t('referenda'), 'start', 7],
[undefined, undefined, 1]
]}
key={
trackName
? `track:${trackName}`
: 'untracked'
}
>
{referenda && referenda.map((r) => (
<Referendum
isMember={isMember}
key={r.key}
members={members}
palletReferenda={palletReferenda}
palletVote={palletVote}
value={r}
/>
))}
</Table>
{filtered.map(({ key, referenda, trackId, trackName }: ReferendaGroup) => (
<Group
isMember={isMember}
key={key}
members={members}
palletReferenda={palletReferenda}
palletVote={palletVote}
ranks={ranks}
referenda={referenda}
trackId={trackId}
trackName={trackName}
tracks={tracks}
/>
))}
</div>
);
}
export default React.memo(styled(Referenda)`
th > h1 > div {
display: inline-block;
font-size: 1rem;
padding-left: 1.5rem;
text-overflow: ellipsis;
}
.ui--Dropdown.topDropdown {
min-width: 25rem;
padding-left: 0;

View File

@ -9,6 +9,7 @@ export type PalletReferenda = 'referenda' | 'rankedPolls' | 'fellowshipReferenda
export type PalletVote = 'convictionVoting' | 'rankedCollective' | 'fellowshipCollective';
export interface ReferendaGroup {
key: string;
track?: PalletReferendaTrackInfo;
trackId?: BN;
trackName?: string;
@ -34,6 +35,8 @@ export interface ReferendumProps {
members?: string[];
palletReferenda: PalletReferenda;
palletVote: PalletVote;
ranks?: BN[];
trackInfo?: TrackInfo;
value: Referendum;
}
@ -42,3 +45,9 @@ export interface Summary {
refActive?: number;
refCount?: BN;
}
export interface TrackInfo {
compare?: (input: BN) => boolean;
origin: Record<string, string> | Record<string, string>[];
text?: string;
}

View File

@ -92,14 +92,14 @@ const OPT_MULTI = {
function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo][]): ReferendaGroup[] {
if (!referenda) {
// return an empty group when we have no referenda
return [{}];
return [{ key: 'empty' }];
} else if (!tracks) {
// if we have no tracks, we just return the referenda sorted
return [{ referenda: referenda.sort(sortReferenda) }];
return [{ key: 'referenda', referenda: referenda.sort(sortReferenda) }];
}
const grouped: ReferendaGroupKnown[] = [];
const other: ReferendaGroupKnown = { referenda: [] };
const other: ReferendaGroupKnown = { key: 'referenda', referenda: [] };
// sort the referenda by track inside groups
for (let i = 0; i < referenda.length; i++) {
@ -119,6 +119,7 @@ function group (referenda?: Referendum[], tracks?: [BN, PalletReferendaTrackInfo
if (!group) {
// we don't have a group as of yet, create one
grouped.push({
key: `track:${ref.trackId.toString()}`,
referenda: [ref],
track: ref.track,
trackId: ref.trackId,
@ -157,7 +158,7 @@ function useReferendaImpl (palletReferenda: PalletReferenda): [ReferendaGroup[],
return useMemo(
() => [
(ids && ids.length === 0)
? [{ referenda: [] }]
? [{ key: 'referenda', referenda: [] }]
: group(referenda, tracks),
tracks
],

View File

@ -4,6 +4,7 @@
import type { ApiPromise } from '@polkadot/api';
import type { PalletReferendaTrackInfo } from '@polkadot/types/lookup';
import type { BN } from '@polkadot/util';
import type { TrackInfo } from './types';
import { getGovernanceTracks } from '@polkadot/apps-config';
import { formatNumber, stringPascalCase } from '@polkadot/util';
@ -20,8 +21,8 @@ export function getTrackName (trackId: BN, { name }: PalletReferendaTrackInfo):
}`;
}
export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda: string, tracks?: [BN, PalletReferendaTrackInfo][], trackId?: number): { origin: Record<string, string> | Record<string, string>[], text?: string } | undefined {
let info: { origin: Record<string, string> | Record<string, string>[], text?: string } | undefined;
export function getTrackInfo (api: ApiPromise, specName: string, palletReferenda: string, tracks?: [BN, PalletReferendaTrackInfo][], trackId?: number): TrackInfo | undefined {
let info: TrackInfo | undefined;
if (tracks && trackId !== undefined) {
const originMap = getGovernanceTracks(api, specName, palletReferenda);