Bounties - Additional info (#4563)

This commit is contained in:
MiZiet 2021-02-02 12:04:44 +01:00 committed by GitHub
parent 43d2d1b665
commit 9de151e5a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 216 additions and 28 deletions

View File

@ -21,6 +21,7 @@
"Claimable": "Claimable",
"Close": "Close",
"Close Bounty": "Close Bounty",
"Close deadline": "Close deadline",
"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 account that will award a bounty to the Beneficiary.": "Curator's account that will award a bounty to the Beneficiary.",
@ -36,6 +37,7 @@
"Id": "Id",
"If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.": "If the motion is outvoted, Curator's deposit will be slashed and Curator will be unassigned. Bounty will return to the Funded state.",
"If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.": "If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.",
"Info": "Info",
"Initiate Voting": "Initiate Voting",
"Nay": "Nay",
"Nay: {{count}}": "Nay: {{count}}",
@ -93,6 +95,10 @@
"Unassign Curator": "Unassign Curator",
"Unassign curator": "Unassign curator",
"Unassign curator under voting": "Unassign curator under voting",
"Value": "Value",
"Waiting for Bounty Funding": "Waiting for Bounty Funding",
"Waiting for Curator's acceptance": "Waiting for Curator's acceptance",
"Warning": "Warning",
"You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.": "You are giving up your curator role, the bounty will return to the Funded state. You will get your deposit back.",
"active": "active",
"award bounty": "award bounty",
@ -122,4 +128,4 @@
"value": "value",
"vote with account": "vote with account",
"{{blocks}} blocks": "{{blocks}} blocks"
}
}

View File

@ -58,6 +58,7 @@
"If the motion is outvoted, the current Curator will be unassigned and the Bounty will return to the Funded state.": "",
"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.": "",
@ -167,6 +168,7 @@
"Voting": "",
"Voting costs nothing other than the transaction fee and can be done from all accounts with a non-zero spendable balance.": "",
"Voting is made on a per-account basis; a single account must all vote the same way and cannot split its vote.": "",
"Warning": "",
"Warning: we did not find any attest statement for {{chain}}": "",
"With the keep-alive option set, the account is protected against removal due to low balances.": "",
"Yes": "",

View File

@ -27,10 +27,12 @@ import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';
import { extractTime } from '@polkadot/util';
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from '../src/BountyInfos';
import { alice,
bob,
defaultBalance,
defaultBountyApi,
defaultBountyUpdatePeriod,
defaultMembers,
defaultTreasury,
ferdie } from '../test/hooks/defaults';
@ -633,4 +635,53 @@ describe('Bounties', () => {
await expect(findByText('Beneficiary')).rejects.toThrow();
});
});
describe('Show', () => {
it('warning when update time is close', async () => {
const bounty = aBounty({ status: bountyStatusWith(
{
curator: alice,
status: 'Active',
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() - 1
}) });
const { findByText } = renderOneBounty(bounty);
expect(await findByText('Warning')).toBeTruthy();
expect(await findByText('Close deadline')).toBeTruthy();
});
it('info when waiting for bounty funding', async () => {
const bounty = bountyWith({ status: 'Approved' });
const { findByText } = renderOneBounty(bounty);
expect(await findByText('Info')).toBeTruthy();
expect(await findByText('Waiting for Bounty Funding')).toBeTruthy();
});
it('info when waiting for curator acceptance', async () => {
const bounty = bountyWith({ status: 'CuratorProposed' });
const { findByText } = renderOneBounty(bounty);
expect(await findByText('Info')).toBeTruthy();
expect(await findByText("Waiting for Curator's acceptance")).toBeTruthy();
});
it('no warning or info when requirements are not met', async () => {
const bounty = aBounty({ status: bountyStatusWith({
curator: alice,
status: 'Active',
updateDue: defaultBountyUpdatePeriod.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100).toNumber() + 1
}) });
const { findByText } = renderOneBounty(bounty);
await expect(findByText('Warning')).rejects.toThrow();
await expect(findByText('Close deadline')).rejects.toThrow();
await expect(findByText('Info')).rejects.toThrow();
await expect(findByText('Waiting for Bounty Funding')).rejects.toThrow();
await expect(findByText("Waiting for Curator's acceptance")).rejects.toThrow();
});
});
});

View File

@ -21,6 +21,7 @@ import BountyInfos from './BountyInfos';
import BountyStatusView from './BountyStatusView';
import Curator from './Curator';
import { useBountyStatus } from './hooks';
import { bountyBorderColor, bountyLabelColor } from './theme';
import { useTranslation } from './translate';
interface Props {
@ -113,6 +114,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<div className='td-row'>
<BountyInfos
beneficiary={beneficiary}
blocksUntilUpdate={blocksUntilUpdate}
proposals={proposals}
status={status}
/>
@ -254,7 +256,7 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
justify-content: center;
width: 1.7rem;
height: 1.7rem;
border: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border: 1px solid ${bountyBorderColor[theme.theme]};
border-radius: 4px;
cursor: pointer;
}
@ -272,7 +274,7 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
font-weight: 500;
font-size: 0.7rem;
line-height: normal;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
color: ${bountyLabelColor[theme.theme]};
text-transform: uppercase;
}
@ -314,7 +316,7 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
margin-top: 0.28rem;
font-size: 0.7rem;
line-height: 0.85rem;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
color: ${bountyLabelColor[theme.theme]};
}
& .votes-table {

View File

@ -5,9 +5,6 @@ import React, { useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import { determineUnassignCuratorAction } from '@polkadot/app-bounties/helpers';
import { useBountyStatus, useUserRole } from '@polkadot/app-bounties/hooks';
import { ValidUnassignCuratorAction } from '@polkadot/app-bounties/types';
import { Button, Menu, Popup } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { useMembers, useToggle } from '@polkadot/react-hooks';
@ -17,7 +14,11 @@ import CloseBounty from './BountyActions/CloseBounty';
import ExtendBountyExpiryAction from './BountyActions/ExtendBountyExpiryAction';
import SlashCurator from './BountyActions/SlashCurator';
import BountyRejectCurator from './BountyRejectCurator';
import { determineUnassignCuratorAction } from './helpers';
import { useBountyStatus, useUserRole } from './hooks';
import { bountySvgColor } from './theme';
import { useTranslation } from './translate';
import { ValidUnassignCuratorAction } from './types';
interface Props {
bestNumber: BlockNumber;
@ -157,7 +158,7 @@ function BountyExtraActions ({ bestNumber, className, description, index, propos
}
export default React.memo(styled(BountyExtraActions)(({ theme }: ThemeProps) => `
.settings-button {
&& .ui--Button:not(.isDisabled):not(.isIcon).settings-button {
width: 24px;
height: 24px;
padding: 0;
@ -166,7 +167,7 @@ export default React.memo(styled(BountyExtraActions)(({ theme }: ThemeProps) =>
svg {
padding: 0;
margin: 0;
color: ${theme.theme === 'dark' ? 'rgba(244,242,240,0.9)' : '#000 !important'};
color: ${bountySvgColor[theme.theme]};
}
&:focus {

View File

@ -0,0 +1,53 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
import { Icon } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { bountyColor, bountyLabelColor, bountySvgColor } from './theme';
import { useTranslation } from './translate';
interface Props {
className: '';
description: string;
type?: 'info' | 'warning';
}
function BountyInfo ({ className = '', description, type = 'info' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<div className={className}>
<div className='title'>
{ type === 'warning' && <Icon icon={'exclamation-triangle'}/> } {type === 'info' ? t('Info') : t('Warning')}
</div>
<div className='description'>
{description}
</div>
</div>
);
}
export default React.memo(styled(BountyInfo)(({ theme }: ThemeProps) => `
font-weight: 500;
font-size: 0.857rem;
line-height: 1.714rem;
color: ${bountyColor[theme.theme]};
.title{
margin-bottom: 0.285rem;
svg {
color: ${bountySvgColor[theme.theme]};
}
}
.description {
font-weight: 400;
font-size: 0.714rem;
line-height: 0.864rem;
color: ${bountyLabelColor[theme.theme]};
}
`));

View File

@ -4,24 +4,35 @@
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { AccountId, BountyStatus } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import Description from '@polkadot/app-bounties/Description';
import { useTranslation } from '@polkadot/app-bounties/translate';
import { AddressSmall } from '@polkadot/react-components';
import VotingSummary from './Voting/VotingSummary';
import BountyInfo from './BountyInfo';
import Description from './Description';
import { useBounties } from './hooks';
import { useTranslation } from './translate';
interface Props {
beneficiary?: AccountId;
blocksUntilUpdate?: BN;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element {
export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10;
const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000');
function BountyInfos ({ beneficiary, blocksUntilUpdate, proposals, status }: Props): JSX.Element {
const { t } = useTranslation();
const { bountyUpdatePeriod } = useBounties();
const blocksPercentageLeftToShowWarning = bountyUpdatePeriod?.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).divn(100);
const blocksToShowWarning = blocksPercentageLeftToShowWarning ?? BLOCKS_LEFT_TO_SHOW_WARNING;
return (
<>
{proposals && (
@ -36,10 +47,26 @@ function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element {
<Description description={t<string>('Beneficiary')} />
</div>
)}
{blocksUntilUpdate?.lt(blocksToShowWarning) && (
<BountyInfo
description={t<string>('Close deadline')}
type='warning'
/>
)}
{status.isApproved && (
<BountyInfo
description={t<string>('Waiting for Bounty Funding')}
type='info'
/>
)}
{status.isCuratorProposed && (
<BountyInfo
description={t<string>("Waiting for Curator's acceptance")}
type='info'
/>
)}
</>
);
}
export default React.memo(styled(BountyInfos)`
`);
export default React.memo(BountyInfos);

View File

@ -6,6 +6,8 @@ import styled from 'styled-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { bountyLabelColor } from './theme';
interface Props {
className?: string;
dataTestId?: string;
@ -26,5 +28,5 @@ export default React.memo(styled(Description)(({ theme }: ThemeProps) => `
margin-top: 0.28rem;
font-size: 0.7rem;
line-height: 0.85rem;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
color: ${bountyLabelColor[theme.theme]};
`));

View File

@ -11,6 +11,7 @@ import { AddressSmall, Icon } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { getProposalToDisplay } from '../helpers/extendedStatuses';
import { bountyLabelColor } from '../theme';
import { useTranslation } from '../translate';
interface Props {
@ -66,7 +67,7 @@ export default React.memo(styled(VotersColumn)(({ theme }: ThemeProps) => `
.vote-numbers-icon svg {
max-width: 10px;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
color: ${bountyLabelColor[theme.theme]};
}
.vote-numbers-label {

View File

@ -7,12 +7,13 @@ import type { BountyStatus } from '@polkadot/types/interfaces';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { useTranslation } from '@polkadot/app-bounties/translate';
import { ThemeProps } from '@polkadot/react-components/types';
import { useMembers } from '@polkadot/react-hooks';
import Description from '../Description';
import { getProposalToDisplay } from '../helpers/extendedStatuses';
import { bountyLabelColor } from '../theme';
import { useTranslation } from '../translate';
interface Props {
className?: string;
@ -46,10 +47,12 @@ function VotingSummary ({ className, proposals, status }: Props): JSX.Element {
}
export default React.memo(styled(VotingSummary)(({ theme }: ThemeProps) => `
margin: 0 1rem 0 0;
.voting-summary-text {
font-size: 0.85rem;
line-height: 0.5rem;
color: ${theme.theme === 'dark' ? '#eeeeee' : '#1a1b20'}
color: ${bountyLabelColor[theme.theme]}
span {
min-width: 0.5rem;

View File

@ -17,6 +17,7 @@ export type BountyApi = {
bounties?: DeriveBounties,
bountyDepositBase: BN,
bountyIndex?: BN,
bountyUpdatePeriod?: BN,
bountyValueMinimum: BN,
claimBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
closeBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
@ -38,6 +39,7 @@ export function useBounties (): BountyApi {
const bountyValueMinimum = (constsBase.bountyValueMinimum as BalanceOf).toBn();
const maximumReasonLength = constsBase.maximumReasonLength.toNumber();
const dataDepositPerByte = (constsBase.dataDepositPerByte as BalanceOf).toBn();
const bountyUpdatePeriod = constsBase.bountyUpdatePeriod;
const proposeBounty = (api.tx.bounties || api.tx.treasury).proposeBounty;
const proposeCurator = (api.tx.bounties || api.tx.treasury).proposeCurator;
const claimBounty = (api.tx.bounties || api.tx.treasury).claimBounty;
@ -56,6 +58,7 @@ export function useBounties (): BountyApi {
bounties,
bountyDepositBase,
bountyIndex,
bountyUpdatePeriod,
bountyValueMinimum,
claimBounty,
closeBounty,

View File

@ -8,6 +8,7 @@ import { Tabs } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import Bounties from './Bounties';
import { bountyBorderColor, bountyColor, bountyOddRowBackground } from './theme';
import { useTranslation } from './translate';
export { default as useCounter } from './useCounter';
@ -62,21 +63,21 @@ export default React.memo(styled(BountiesApp)(({ theme }: ThemeProps) => `
vertical-align: baseline;
font-size: 1rem;
line-height: 1.7rem;
color: ${theme.theme === 'dark' ? '#eeeeee' : '#1a1b20'}
border-bottom: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
color: ${bountyColor[theme.theme]};
border-bottom: 1px solid ${bountyBorderColor[theme.theme]};
&:first-child {
border-left: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border-left: 1px solid ${bountyBorderColor[theme.theme]};
}
&:last-child {
border-right: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border-right: 1px solid ${bountyBorderColor[theme.theme]};
}
}
tbody tr {
&:nth-child(odd) {
background: ${theme.theme === 'dark' ? '#111218' : '#fff'};
background: ${bountyOddRowBackground[theme.theme]};
}
&:nth-child(odd) td {
@ -89,7 +90,7 @@ export default React.memo(styled(BountiesApp)(({ theme }: ThemeProps) => `
&:first-child {
td {
border-top: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border-top: 1px solid ${bountyBorderColor[theme.theme]};
}
td:first-child {
@ -103,7 +104,7 @@ export default React.memo(styled(BountiesApp)(({ theme }: ThemeProps) => `
&:last-child {
td {
border-bottom: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border-bottom: 1px solid ${bountyBorderColor[theme.theme]};
}
td:first-child {

View File

@ -0,0 +1,32 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
type bountyTheme = {
dark:string;
light:string;
}
export const bountyBorderColor: bountyTheme = {
dark: '#2f313c',
light: '#dfdfdf'
};
export const bountyColor: bountyTheme = {
dark: '#BDBDBD',
light: '#1a1b20'
};
export const bountyOddRowBackground: bountyTheme = {
dark: '#111218',
light: '#fff'
};
export const bountyLabelColor: bountyTheme = {
dark: '#757575',
light: '#8B8B8B'
};
export const bountySvgColor: bountyTheme = {
dark: '#8E8E8E',
light: '#4D4D4D'
};

View File

@ -4,10 +4,13 @@
import BN from 'bn.js';
import { DeriveBounties } from '@polkadot/api-derive/types';
import { BountyApi } from '@polkadot/app-bounties/hooks';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { BlockNumber } from '@polkadot/types/interfaces';
import { BountyApi } from '../../src/hooks';
export const defaultBountyUpdatePeriod = new BN(100000);
export const defaultBountyApi: BountyApi = {
acceptCurator: jest.fn(),
approveBounty: jest.fn(),
@ -15,6 +18,7 @@ export const defaultBountyApi: BountyApi = {
bestNumber: new BN(1) as BlockNumber,
bounties: [] as DeriveBounties,
bountyDepositBase: new BN(1),
bountyUpdatePeriod: defaultBountyUpdatePeriod,
bountyValueMinimum: new BN(1),
claimBounty: jest.fn(),
closeBounty: jest.fn().mockReturnValue({ length: 4 }),