Add tooltips for bounty status (#4450)

* Add BountyStatusDisplay component

* Implement help tooltip in BountyStatusDisplay

* Add BountyStatusDisplay translations

* Add styling to BountyStatusDisplay

* Update translations

* Add missing types

* Add missing translation

* Update local imports

* Update local imports

* Rename BountyStatusDisplay to BountyStatusView

* Rewrite bounty status helpers

* Add review suggestion

* Change useMemo to useRef in BountyStatusView
This commit is contained in:
MiZiet 2021-01-20 15:33:23 +01:00 committed by GitHub
parent 90cc0839ff
commit 708aa6cf95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 103 additions and 41 deletions

View File

@ -3,6 +3,7 @@
"Accept": "Accept",
"Account does not have enough funds.": "Account does not have enough funds.",
"Add Bounty": "Add Bounty",
"After a bounty was proposed the council decides whether to fund it or not.": "After a bounty was proposed the council decides whether to fund it or not.",
"Allocation value is smaller than the minimum bounty value.": "Allocation value is smaller than the minimum bounty value.",
"Approval under voting": "Approval under voting",
"Approve": "Approve",
@ -12,6 +13,7 @@
"Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.": "Choose a curator whose background and expertise is such that they are capable of determining when the task is complete.",
"Claim": "Claim",
"Claimable": "Claimable",
"Curator has been proposed by council. The bounty is waiting for curator to accept the role.": "Curator has been proposed by council. The bounty is waiting for curator to accept the role.",
"Curator under voting": "Curator under voting",
"Curator's deposit": "Curator's deposit",
"Curator's fee": "Curator's fee",
@ -43,6 +45,10 @@
"This action will create a Council motion to either approve or reject the Bounty.": "This action will create a Council motion to either approve or reject the Bounty.",
"This action will extend expiry time of the selected bounty.": "This action will extend expiry time of the selected bounty.",
"This amount will be reserved from origin account and returned on approval or slashed upon rejection.": "This amount will be reserved from origin account and returned on approval or slashed upon rejection.",
"This bounty has received approval and is currently being implemented.": "This bounty has received approval and is currently being implemented.",
"This bounty is approved and funded.": "This bounty is approved and funded.",
"This bounty was approved by the council. When next treasury spend period comes, it will get funded.": "This bounty was approved by the council. When next treasury spend period comes, it will get funded.",
"This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.": "This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.",
"Title too long": "Title too long",
"Unassign curator under voting": "Unassign curator under voting",
"Value": "Value",

View File

@ -9,13 +9,13 @@
"Add an existing contract": "",
"Add proxy": "",
"Address index {{index}}": "",
"After a bounty was proposed the council decides whether to fund it or not.": "",
"All stashes": "",
"All voters may alter their votes any number of times prior to the close of the poll.": "",
"An account that is to receive the rewards": "",
"Any combination of the four options may be approved of by the voter. There is no need to select only one option!": "",
"Approval under voting": "",
"Approving of all or none of the options is equivalent and will not affect the outcome of the poll.": "",
"Asset ID": "",
"Authorize transaction": "",
"Bond": "",
"Cancel": "",
@ -30,6 +30,7 @@
"Contracts": "",
"Controller account": "",
"Create a backup file for this account": "",
"Curator has been proposed by council. The bounty is waiting for curator to accept the role.": "",
"Curator under voting": "",
"Curator's deposit": "",
"Curator's fee": "",
@ -49,7 +50,6 @@
"Governance": "",
"If you have not claimed rewards straight after the end of the era, the validator is in the active set and you are seeing no rewards, this would mean that the reward payout transaction was made by another account on your behalf. Always check your favorite explorer to see any historic payouts made to your accounts.": "",
"Inactive": "",
"Info": "",
"Initiate recovery for another": "",
"Light theme (default)": "",
"Locked funds (e.g. for staking) are counted.": "",
@ -66,7 +66,6 @@
"No change": "",
"No change from the original 2017 sale definitions; will mean a total of 10 million DOT from genesis.": "",
"No discretionary lock-voting is in place; all DOT used to vote counts the same.": "",
"No items": "",
"No sub identities set.": "",
"Nominating": "",
"Nominators": "",
@ -82,7 +81,6 @@
"Retrieving nominators": "",
"Retrieving validators": "",
"Save": "",
"Select the account to use for this action.": "",
"Set on-chain identity": "",
"Set on-chain sub-identities": "",
"Settings": "",
@ -98,7 +96,6 @@
"Split of 10x from the original sale; will mean a total of 100 million DOT from genesis. Apparent DOT price would be 10x lower and apparent account balances 10x higher.": "",
"Stash account (do not increase the amount at stake)": "",
"Stash account (increase the amount at stake)": "",
"Submit": "",
"Submit (no signature)": "",
"Swap to a non-executing approval type, with subsequent calls providing the actual call data.": "",
"The account type (derivation) to use": "",
@ -123,6 +120,10 @@
"The sending account that will be used to send this transaction. Any applicable fees will be paid by this account.": "",
"The signatory is one of the allowed accounts on the multisig, making a recorded approval for the transaction.": "",
"The specified value is too large and does not allow funds to pay future transaction fees.": "",
"This bounty has received approval and is currently being implemented.": "",
"This bounty is approved and funded.": "",
"This bounty was approved by the council. When next treasury spend period comes, it will get funded.": "",
"This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.": "",
"This could either be an approval for the hash or with full call details. The call as last approval triggers execution.": "",
"This is a developer tool that allows you to execute selected snippets in a limited context.": "",
"This is not a valid JSON object.": "",
@ -153,7 +154,6 @@
"activate": "",
"address copied": "",
"address index": "",
"address {{index}}": "",
"available": "",
"available to be unlocked": "",
"aye": "",
@ -220,7 +220,6 @@
"overall/validator": "",
"own": "",
"own stake": "",
"paid": "",
"parachain": "",
"parachains": "",
"parent": "",
@ -241,14 +240,12 @@
"recent blocks": "",
"recently": "",
"referenda": "",
"relay parent": "",
"remaining": "",
"reporters": "",
"rewards": "",
"role": "",
"runners up": "",
"scheduled": "",
"scheduling": "",
"secret": "",
"sending from my account": "",
"signer": "",
@ -257,8 +254,6 @@
"state": "",
"status": "",
"strikes": "",
"sub name": "",
"swap to id": "",
"tags": "",
"the payment account": "",
"threshold": "",
@ -274,7 +269,6 @@
"upcoming": "",
"update due": "",
"upgrade": "",
"using my account": "",
"validation": "",
"validator {{index}}": "",
"validators": "",
@ -282,7 +276,6 @@
"vote using my account": "",
"votes": "",
"voting end": "",
"watermark": "",
"yesterday": "",
"{{balance}} voted": "",
"{{count}} key/value pairs encoded for submission": "",

View File

@ -13,9 +13,9 @@ import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import { BountyActions } from './BountyActions';
import BountyStatusView from './BountyStatusView';
import { getBountyStatus, truncateTitle } from './helpers';
import { useTranslation } from './translate';
import VotingDescription from './VotingDescription';
interface Props {
bestNumber: BlockNumber;
@ -54,17 +54,11 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<>
<tr className={className}>
<td>
<div>
<div>{bountyStatus}</div>
{proposals && (
<div>
<VotingDescription
proposals={proposals}
status={status}
/>
</div>
)}
</div>
<BountyStatusView
bountyStatus={bountyStatus}
proposals={proposals}
status={status}
/>
</td>
<td data-testid='description'>{truncateTitle(description, 30)}</td>
<td><FormatBalance value={value} /></td>

View File

@ -0,0 +1,61 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
import React, { useRef } from 'react';
import styled from 'styled-components';
import { LabelHelp } from '@polkadot/react-components';
import { insertSpaceBeforeCapitalLetter } from './helpers';
import { useTranslation } from './translate';
import { HelpMessages, StatusName } from './types';
import VotingDescription from './VotingDescription';
interface Props {
bountyStatus: StatusName;
className?: string;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function BountyStatusView ({ bountyStatus, className = '', proposals, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const statusHelpMessages = useRef<HelpMessages>({
Active: t('This bounty has received approval and is currently being implemented.'),
Approved: t('This bounty was approved by the council. When next treasury spend period comes, it will get funded.'),
CuratorProposed: t('Curator has been proposed by council. The bounty is waiting for curator to accept the role.'),
Funded: t('This bounty is approved and funded.'),
PendingPayout: t('This bounty was completed and the beneficiary was awarded by the curator. Claiming the payout will be possible after delay has passed.'),
Proposed: t('After a bounty was proposed the council decides whether to fund it or not.')
});
return (
<div className={className}>
<div className='bountyStatus'>
{insertSpaceBeforeCapitalLetter(bountyStatus)}
<LabelHelp
help={statusHelpMessages.current[bountyStatus]}
/>
</div>
{proposals && (
<div>
<VotingDescription
proposals={proposals}
status={status}
/>
</div>
)}
</div>
);
}
export default React.memo(styled(BountyStatusView)`
.bountyStatus {
display: flex;
align-items: flex-end;
}
`);

View File

@ -4,7 +4,7 @@
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import React, { useRef } from 'react';
import styled from 'styled-components';
import { bestValidProposalName } from './helpers/extendedStatuses';
@ -19,12 +19,12 @@ interface Props {
function VotingDescription ({ className = '', proposals, status }: Props): React.ReactElement<Props> {
const bestProposalName = bestValidProposalName(proposals, status);
const { t } = useTranslation();
const descriptions: Record<string, string> = useMemo(() => ({
const descriptions = useRef<Record<string, string>>({
approveBounty: t('Approval under voting'),
closeBounty: t('Rejection under voting'),
proposeCurator: t('Curator under voting'),
unassignCurator: t('Unassign curator under voting')
}), [t]);
});
return (
<>
@ -33,7 +33,7 @@ function VotingDescription ({ className = '', proposals, status }: Props): React
className={className}
data-testid='extendedStatus'
>
<>{descriptions[bestProposalName] || null}</>
<>{descriptions.current[bestProposalName] || null}</>
</div>
)}
</>

View File

@ -4,8 +4,7 @@
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BountyStatus } from '@polkadot/types/interfaces';
type BountyVotingStatus = 'Active' | 'Approved' | 'CuratorProposed' | 'Funded' | 'PendingPayout' | 'Proposed';
type BountyVotingStatuses = { [status in BountyVotingStatus]: string[] };
import { BountyVotingStatuses, StatusName } from '../types';
const validProposalNames: BountyVotingStatuses = {
Active: ['closeBounty', 'unassignCurator'],
@ -17,7 +16,7 @@ const validProposalNames: BountyVotingStatuses = {
};
function validMethods (status: BountyStatus): string[] {
return validProposalNames[status.type as BountyVotingStatus];
return validProposalNames[status.type as StatusName];
}
function getProposalByMethod (bountyProposals: DeriveCollectiveProposal[], method: string | undefined): DeriveCollectiveProposal | undefined {

View File

@ -15,7 +15,7 @@ describe('get bounty status', () => {
it('for CuratorProposed state it has curator defined', () => {
const bountyStatus = getBountyStatus(registry.createType('BountyStatus', 'CuratorProposed'));
expect(bountyStatus.bountyStatus).toEqual('Curator Proposed');
expect(bountyStatus.bountyStatus).toEqual('CuratorProposed');
expect(bountyStatus.curator).toBeDefined();
});
@ -30,7 +30,7 @@ describe('get bounty status', () => {
it('for PendingPayout state it has curator, beneficiary and unlock_at defined', () => {
const bountyStatus = getBountyStatus(registry.createType('BountyStatus', 'PendingPayout'));
expect(bountyStatus.bountyStatus).toEqual('Pending Payout');
expect(bountyStatus.bountyStatus).toEqual('PendingPayout');
expect(bountyStatus.curator).toBeDefined();
expect(bountyStatus.beneficiary).toBeDefined();
expect(bountyStatus.unlockAt).toBeDefined();

View File

@ -3,10 +3,10 @@
import type { BountyStatus } from '@polkadot/types/interfaces';
import { BountyStatusType } from '../types';
import { BountyStatusType, StatusName } from '../types';
export const getBountyStatus = (status: BountyStatus): BountyStatusType => {
const statusAsString = status.type;
const statusAsString = status.type as StatusName;
let result: BountyStatusType = {
beneficiary: undefined,
@ -19,7 +19,7 @@ export const getBountyStatus = (status: BountyStatus): BountyStatusType => {
if (status.isCuratorProposed) {
result = {
...result,
bountyStatus: 'Curator Proposed',
bountyStatus: 'CuratorProposed',
curator: status.asCuratorProposed.curator
};
}
@ -36,7 +36,7 @@ export const getBountyStatus = (status: BountyStatus): BountyStatusType => {
result = {
...result,
beneficiary: status.asPendingPayout.beneficiary,
bountyStatus: 'Pending Payout',
bountyStatus: 'PendingPayout',
curator: status.asPendingPayout.curator,
unlockAt: status.asPendingPayout.unlockAt
};

View File

@ -4,4 +4,4 @@
export * from './calculateBountyBond';
export * from './getBountyStatus';
export * from './isClaimable';
export * from './truncateTitle';
export * from './stringHelpers';

View File

@ -1,6 +1,10 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
export function insertSpaceBeforeCapitalLetter (str: string): string {
return str.replace(/([a-z])([A-Z])/g, '$1 $2');
}
export function truncateTitle (str: string, maxLength: number): string {
const ellipsis = String.fromCharCode(8230);

View File

@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { truncateTitle } from '@polkadot/app-bounties/helpers/truncateTitle';
import { truncateTitle } from './stringHelpers';
describe('Truncate title', () => {
it('does not truncate short title', () => {

View File

@ -5,8 +5,13 @@ import type { AccountId, BlockNumber } from '@polkadot/types/interfaces/runtime'
export interface BountyStatusType {
beneficiary: AccountId | undefined;
bountyStatus: string;
bountyStatus: StatusName;
curator: AccountId | undefined;
unlockAt: BlockNumber | undefined;
updateDue: BlockNumber | undefined;
}
export type HelpMessages = Record<StatusName, string>;
export type StatusName = 'Active' | 'Approved' | 'CuratorProposed' | 'Funded' | 'PendingPayout' | 'Proposed';
export type BountyVotingStatuses = { [status in StatusName]: string[] };