Bounties - list layout improvements (#4644)
This commit is contained in:
parent
a180bbc4ff
commit
a580ac4d5e
@ -101,6 +101,7 @@
|
||||
"Title too long": "Title too long",
|
||||
"Unassign curator": "Unassign curator",
|
||||
"Unassign curator under voting": "Unassign curator under voting",
|
||||
"Update overdue": "Update overdue",
|
||||
"Voting": "Voting",
|
||||
"Voting results": "Voting results",
|
||||
"Waiting for Bounty Funding": "Waiting for Bounty Funding",
|
||||
|
||||
@ -21,7 +21,7 @@ import { keyring } from '@polkadot/ui-keyring';
|
||||
|
||||
import { defaultBountyUpdatePeriod, mockBountyHooks } from '../test/hooks/defaults';
|
||||
import { BountiesPage } from '../test/pages/bountiesPage';
|
||||
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyInfos';
|
||||
import { BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING } from './BountyNextActionInfo/BountyActionMessage';
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useTreasury', () => ({
|
||||
useTreasury: () => mockHooks.treasury
|
||||
@ -100,7 +100,7 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Curator proposal under voting');
|
||||
await bountiesPage.expectVotingDescription('Curator proposal under voting');
|
||||
});
|
||||
|
||||
it('when voting on bounty approval', async () => {
|
||||
@ -109,7 +109,7 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty approval under voting');
|
||||
await bountiesPage.expectVotingDescription('Bounty approval under voting');
|
||||
});
|
||||
|
||||
it('when simultaneous close and approve motions exist, show approved', async () => {
|
||||
@ -121,7 +121,7 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty approval under voting');
|
||||
await bountiesPage.expectVotingDescription('Bounty approval under voting');
|
||||
});
|
||||
|
||||
it('when voting on close bounty', async () => {
|
||||
@ -130,7 +130,7 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Bounty rejection under voting');
|
||||
await bountiesPage.expectVotingDescription('Bounty rejection under voting');
|
||||
});
|
||||
|
||||
it('when voting on unassign curator', async () => {
|
||||
@ -139,7 +139,7 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await bountiesPage.expectText('Curator slash under voting');
|
||||
await bountiesPage.expectVotingDescription('Curator slash under voting');
|
||||
});
|
||||
|
||||
it('when a motion exists that would fail on execution, show nothing', async () => {
|
||||
@ -148,14 +148,7 @@ describe('Bounties', () => {
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
await expect(findByTestId('extendedVotingStatus')).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('when bounty is claimable', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty);
|
||||
|
||||
expect((await findByTestId('extendedActionStatus')).textContent).toEqual('Claimable');
|
||||
await expect(findByTestId('voting-description')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@ -215,7 +208,7 @@ describe('Bounties', () => {
|
||||
|
||||
const { findByTestId } = bountiesPage.renderOne(bounty, proposals);
|
||||
|
||||
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting results');
|
||||
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting');
|
||||
});
|
||||
|
||||
it('is not displayed when not voting', async () => {
|
||||
@ -457,16 +450,28 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Warning');
|
||||
await bountiesPage.expectText('Close deadline');
|
||||
});
|
||||
|
||||
it('warning when update time is overdue', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith(
|
||||
{
|
||||
curator: alice,
|
||||
status: 'Active',
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
updateDue: mockBountyHooks.bountyApi.bestNumber!.toNumber()
|
||||
}) });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Update overdue');
|
||||
});
|
||||
|
||||
it('info when waiting for bounty funding', async () => {
|
||||
const bounty = bountyWith({ status: 'Approved' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Info');
|
||||
await bountiesPage.expectText('Waiting for Bounty Funding');
|
||||
});
|
||||
|
||||
@ -475,10 +480,17 @@ describe('Bounties', () => {
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Info');
|
||||
await bountiesPage.expectText('Waiting for Curator\'s acceptance');
|
||||
});
|
||||
|
||||
it('info when bounty is claimable', async () => {
|
||||
const bounty = bountyWith({ status: 'PendingPayout' });
|
||||
|
||||
bountiesPage.renderOne(bounty);
|
||||
|
||||
await bountiesPage.expectText('Waiting for implementer to claim');
|
||||
});
|
||||
|
||||
it('no warning or info when requirements are not met', async () => {
|
||||
const bounty = aBounty({ status: bountyStatusWith({
|
||||
curator: alice,
|
||||
@ -489,11 +501,11 @@ describe('Bounties', () => {
|
||||
bountiesPage.renderOne(bounty);
|
||||
await bountiesPage.rendered();
|
||||
|
||||
bountiesPage.expectTextAbsent('Warning');
|
||||
bountiesPage.expectTextAbsent('Close deadline');
|
||||
bountiesPage.expectTextAbsent('Info');
|
||||
bountiesPage.expectTextAbsent('Update overdue');
|
||||
bountiesPage.expectTextAbsent('Waiting for Bounty Funding');
|
||||
bountiesPage.expectTextAbsent("Waiting for Curator's acceptance");
|
||||
bountiesPage.expectTextAbsent('Waiting for implementer to claim');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -13,8 +13,8 @@ import { ThemeProps } from '@polkadot/react-components/types';
|
||||
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import BountyActionMessage from './BountyNextActionInfo/BountyActionMessage';
|
||||
import { getProposalToDisplay } from './helpers/extendedStatuses';
|
||||
import VotersColumn from './Voting/VotersColumn';
|
||||
import { BountyActions } from './BountyActions';
|
||||
import BountyExtraActions from './BountyExtraActions';
|
||||
import BountyInfos from './BountyInfos';
|
||||
@ -23,6 +23,7 @@ import Curator from './Curator';
|
||||
import { useBountyStatus } from './hooks';
|
||||
import { bountyBorderColor, bountyLabelColor } from './theme';
|
||||
import { useTranslation } from './translate';
|
||||
import VotersColumn from './VotersColumn';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
@ -71,12 +72,7 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
<>
|
||||
<tr className={className}>
|
||||
<td>
|
||||
<BountyStatusView
|
||||
blocksUntilPayout={blocksUntilPayout}
|
||||
bountyStatus={bountyStatus}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
<BountyStatusView bountyStatus={bountyStatus}/>
|
||||
</td>
|
||||
<td
|
||||
className='description-column'
|
||||
@ -109,12 +105,16 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
|
||||
until={'update'}
|
||||
/>
|
||||
: ''}
|
||||
<BountyActionMessage
|
||||
bestNumber={bestNumber}
|
||||
blocksUntilUpdate={blocksUntilUpdate}
|
||||
status={status}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<td className='td-info-action-row'>
|
||||
<div className='td-row'>
|
||||
<BountyInfos
|
||||
beneficiary={beneficiary}
|
||||
blocksUntilUpdate={blocksUntilUpdate}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
@ -240,7 +240,10 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
& .fast-actions {
|
||||
& td.fast-actions {
|
||||
padding-left: 0.2rem;
|
||||
width: 1%;
|
||||
|
||||
.fast-actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -296,17 +299,24 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
|
||||
padding: 0 0 1.3rem;
|
||||
}
|
||||
|
||||
& .td-info-action-row {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.td-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& :only-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.bounty-action-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
||||
& > * + * {
|
||||
margin-left: 0.6rem;
|
||||
@ -314,9 +324,8 @@ export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
|
||||
}
|
||||
|
||||
.block-to-time {
|
||||
margin-top: 0.28rem;
|
||||
font-size: 0.7rem;
|
||||
line-height: 0.85rem;
|
||||
line-height: 1.5rem;
|
||||
color: ${bountyLabelColor[theme.theme]};
|
||||
}
|
||||
|
||||
|
||||
@ -4,24 +4,22 @@
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { BN_ZERO } from '@polkadot/util';
|
||||
import { LabelHelp } from '@polkadot/react-components';
|
||||
|
||||
import Description from '../Description';
|
||||
import { proposalNameToDisplay } from '../helpers/extendedStatuses';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
blocksUntilPayout?: BN ;
|
||||
className?: string;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
proposal: DeriveCollectiveProposal;
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
function ExtendedStatus ({ blocksUntilPayout, className = '', proposals, status }: Props): React.ReactElement<Props> {
|
||||
const bestProposalName = proposals ? proposalNameToDisplay(proposals, status) : null;
|
||||
function VotingDescriptionInfo ({ className, proposal, status }: Props): React.ReactElement<Props> {
|
||||
const bestProposalName = proposalNameToDisplay(proposal, status);
|
||||
const { t } = useTranslation();
|
||||
const votingDescriptions = useRef<Record<string, string>>({
|
||||
approveBounty: t('Bounty approval under voting'),
|
||||
@ -32,23 +30,17 @@ function ExtendedStatus ({ blocksUntilPayout, className = '', proposals, status
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={className}
|
||||
data-testid='voting-description'
|
||||
>
|
||||
{bestProposalName && votingDescriptions.current[bestProposalName] &&
|
||||
<Description
|
||||
className={className}
|
||||
dataTestId='extendedVotingStatus'
|
||||
description={votingDescriptions.current[bestProposalName]}
|
||||
/>
|
||||
<LabelHelp help={votingDescriptions.current[bestProposalName]}/>
|
||||
}
|
||||
{blocksUntilPayout?.lt(BN_ZERO) &&
|
||||
<Description
|
||||
className={className}
|
||||
dataTestId='extendedActionStatus'
|
||||
description={t<string>('Claimable')}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ExtendedStatus);
|
||||
export default React.memo(styled(VotingDescriptionInfo)`
|
||||
margin-left: 0.2rem;
|
||||
`);
|
||||
@ -4,7 +4,6 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Description from '../Description';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
@ -15,13 +14,15 @@ function VotingLink ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<a href='#/council/motions'>{t<string>('Voting')}</a>
|
||||
<Description description={t<string>('Go to motions panel')} />
|
||||
</div>
|
||||
<a className={className}
|
||||
href='#/council/motions'>
|
||||
{t<string>('Voting')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(VotingLink)`
|
||||
margin: 0 1rem 0 0
|
||||
line-height: 0.85rem;
|
||||
font-size: 0.7rem;
|
||||
text-decoration: underline;
|
||||
`);
|
||||
@ -2,23 +2,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import VotingDescriptionInfo from '@polkadot/app-bounties/BountyInfos/VotingDescriptionInfo';
|
||||
import { ThemeProps } from '@polkadot/react-components/types';
|
||||
import { useMembers } from '@polkadot/react-hooks';
|
||||
|
||||
import Description from '../Description';
|
||||
import { bountyLabelColor } from '../theme';
|
||||
import { useTranslation } from '../translate';
|
||||
import VotingLink from './VotingLink';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
proposal: DeriveCollectiveProposal;
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
function VotingSummary ({ className, proposal }: Props): JSX.Element {
|
||||
function VotingSummary ({ className, proposal, status }: Props): JSX.Element {
|
||||
const { members } = useMembers();
|
||||
const { t } = useTranslation();
|
||||
const ayes = useMemo(() => proposal?.votes?.ayes?.length, [proposal]);
|
||||
@ -33,10 +36,14 @@ function VotingSummary ({ className, proposal }: Props): JSX.Element {
|
||||
className={className}
|
||||
data-testid='voting-summary'
|
||||
>
|
||||
<div className='votes'>
|
||||
<p className='voting-summary-text'><span>{t('Aye')}</span> <b>{ayes}/{threshold}</b></p>
|
||||
<p className='voting-summary-text'><span>{t('Nay')}</span> <b>{nays}/{nayThreshold}</b></p>
|
||||
<Description description={t<string>('Voting results')} />
|
||||
<div className='voting-summary-text'><span>{t('Aye')}</span> <b>{ayes}/{threshold}</b></div>
|
||||
<div className='voting-summary-text'><span>{t('Nay')}</span> <b>{nays}/{nayThreshold}</b></div>
|
||||
<div className='link-info'>
|
||||
<VotingLink/>
|
||||
<VotingDescriptionInfo
|
||||
proposal={proposal}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -45,11 +52,9 @@ function VotingSummary ({ className, proposal }: 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;
|
||||
line-height: 1.5rem;
|
||||
color: ${bountyLabelColor[theme.theme]}
|
||||
|
||||
span {
|
||||
@ -57,4 +62,11 @@ export default React.memo(styled(VotingSummary)(({ theme }: ThemeProps) => `
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.link-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
`));
|
||||
|
||||
@ -4,75 +4,42 @@
|
||||
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
|
||||
import type { AccountId, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AddressSmall } from '@polkadot/react-components';
|
||||
import { BN_HUNDRED } from '@polkadot/util';
|
||||
|
||||
import Description from '../Description';
|
||||
import { getProposalToDisplay } from '../helpers/extendedStatuses';
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
import VotingLink from '../Voting/VotingLink';
|
||||
import BountyInfo from './BountyInfo';
|
||||
import VotingSummary from './VotingSummary';
|
||||
|
||||
interface Props {
|
||||
beneficiary?: AccountId;
|
||||
blocksUntilUpdate?: BN;
|
||||
className?: string;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10;
|
||||
const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000');
|
||||
|
||||
function BountyInfos ({ beneficiary, blocksUntilUpdate, className, proposals, status }: Props): JSX.Element {
|
||||
function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { bountyUpdatePeriod } = useBounties();
|
||||
|
||||
const blocksPercentageLeftToShowWarning = bountyUpdatePeriod?.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).div(BN_HUNDRED);
|
||||
const blocksToShowWarning = blocksPercentageLeftToShowWarning ?? BLOCKS_LEFT_TO_SHOW_WARNING;
|
||||
const proposalToDisplay = useMemo(() => proposals && getProposalToDisplay(proposals, status), [proposals, status]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{proposalToDisplay && <VotingSummary proposal={proposalToDisplay}/>}
|
||||
{proposalToDisplay && <VotingLink />}
|
||||
<>
|
||||
{proposalToDisplay &&
|
||||
<VotingSummary
|
||||
proposal={proposalToDisplay}
|
||||
status={status}
|
||||
/>
|
||||
}
|
||||
{beneficiary && (
|
||||
<div>
|
||||
<AddressSmall value={beneficiary} />
|
||||
<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'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(BountyInfos)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`);
|
||||
export default React.memo(BountyInfos);
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BlockNumber, BountyStatus } from '@polkadot/types/interfaces';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useBountyStatus } from '@polkadot/app-bounties/hooks';
|
||||
import { BN_HUNDRED, BN_ZERO } from '@polkadot/util';
|
||||
|
||||
import { useBounties } from '../hooks';
|
||||
import { useTranslation } from '../translate';
|
||||
import BountyInfo from './BountyInfo';
|
||||
|
||||
interface Props {
|
||||
bestNumber: BlockNumber;
|
||||
blocksUntilUpdate?: BN;
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
export const BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING = 10;
|
||||
const BLOCKS_LEFT_TO_SHOW_WARNING = new BN('10000');
|
||||
|
||||
function BountyActionMessage ({ bestNumber, blocksUntilUpdate, status }: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { unlockAt } = useBountyStatus(status);
|
||||
const { bountyUpdatePeriod } = useBounties();
|
||||
|
||||
const blocksUntilPayout = useMemo(() => unlockAt?.sub(bestNumber), [bestNumber, unlockAt]);
|
||||
|
||||
const blocksPercentageLeftToShowWarning = bountyUpdatePeriod?.muln(BLOCKS_PERCENTAGE_LEFT_TO_SHOW_WARNING).div(BN_HUNDRED);
|
||||
const blocksToShowWarning = blocksPercentageLeftToShowWarning ?? BLOCKS_LEFT_TO_SHOW_WARNING;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{blocksUntilUpdate?.lte(BN_ZERO) && (
|
||||
<BountyInfo
|
||||
description={t<string>('Update overdue')}
|
||||
type='warning'
|
||||
/>
|
||||
)}
|
||||
{blocksUntilUpdate?.lt(blocksToShowWarning) && blocksUntilUpdate?.gt(BN_ZERO) && (
|
||||
<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'
|
||||
/>
|
||||
)}
|
||||
{blocksUntilPayout?.lt(BN_ZERO) &&
|
||||
<BountyInfo
|
||||
description={t<string>('Waiting for implementer to claim')}
|
||||
type='info'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(BountyActionMessage);
|
||||
@ -8,7 +8,6 @@ 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: '';
|
||||
@ -17,13 +16,13 @@ interface Props {
|
||||
}
|
||||
|
||||
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>
|
||||
{type === 'warning' && (
|
||||
<div className='info-icon'>
|
||||
<Icon icon={'exclamation-triangle'}/>
|
||||
</div>
|
||||
)}
|
||||
<div className='description'>
|
||||
{description}
|
||||
</div>
|
||||
@ -32,13 +31,15 @@ function BountyInfo ({ className = '', description, type = 'info' }: Props): Rea
|
||||
}
|
||||
|
||||
export default React.memo(styled(BountyInfo)(({ theme }: ThemeProps) => `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
font-size: 0.857rem;
|
||||
line-height: 1.714rem;
|
||||
line-height: 1.5rem;
|
||||
color: ${bountyColor[theme.theme]};
|
||||
|
||||
.title{
|
||||
margin-bottom: 0.285rem;
|
||||
.info-icon{
|
||||
margin-right: 0.2rem;
|
||||
svg {
|
||||
color: ${bountySvgColor[theme.theme]};
|
||||
}
|
||||
@ -49,5 +50,6 @@ export default React.memo(styled(BountyInfo)(({ theme }: ThemeProps) => `
|
||||
font-size: 0.714rem;
|
||||
line-height: 0.864rem;
|
||||
color: ${bountyLabelColor[theme.theme]};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
`));
|
||||
@ -1,29 +1,21 @@
|
||||
// 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 BN from 'bn.js';
|
||||
import React, { useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { LabelHelp } from '@polkadot/react-components';
|
||||
|
||||
import ExtendedStatus from './Voting/ExtendedStatus';
|
||||
import { insertSpaceBeforeCapitalLetter } from './helpers';
|
||||
import { useTranslation } from './translate';
|
||||
import { HelpMessages, StatusName } from './types';
|
||||
|
||||
interface Props {
|
||||
blocksUntilPayout?: BN
|
||||
bountyStatus: StatusName;
|
||||
className?: string;
|
||||
proposals?: DeriveCollectiveProposal[];
|
||||
status: BountyStatus;
|
||||
}
|
||||
|
||||
function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', proposals, status }: Props): React.ReactElement<Props> {
|
||||
function BountyStatusView ({ bountyStatus, className = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const statusHelpMessages = useRef<HelpMessages>({
|
||||
@ -36,30 +28,19 @@ function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', pr
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='bountyStatus'
|
||||
data-testid={'bountyStatus'}>
|
||||
{insertSpaceBeforeCapitalLetter(bountyStatus)}
|
||||
<LabelHelp
|
||||
help={statusHelpMessages.current[bountyStatus]}
|
||||
/>
|
||||
</div>
|
||||
{proposals && (
|
||||
<div>
|
||||
<ExtendedStatus
|
||||
blocksUntilPayout={blocksUntilPayout}
|
||||
proposals={proposals}
|
||||
status={status}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={className}
|
||||
data-testid={'bountyStatus'}
|
||||
>
|
||||
{insertSpaceBeforeCapitalLetter(bountyStatus)}
|
||||
<LabelHelp
|
||||
help={statusHelpMessages.current[bountyStatus]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(BountyStatusView)`
|
||||
.bountyStatus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`);
|
||||
|
||||
@ -10,9 +10,9 @@ import styled from 'styled-components';
|
||||
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';
|
||||
import { getProposalToDisplay } from './helpers/extendedStatuses';
|
||||
import { bountyLabelColor } from './theme';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -29,10 +29,8 @@ function bestValidProposalName (bountyProposals: DeriveCollectiveProposal[], sta
|
||||
return validMethods(status).find((method) => methods.includes(method));
|
||||
}
|
||||
|
||||
export function proposalNameToDisplay (bountyProposals: DeriveCollectiveProposal[], status: BountyStatus): string | undefined {
|
||||
const bestProposalName = bestValidProposalName(bountyProposals, status);
|
||||
|
||||
if (bestProposalName !== 'unassignCurator') { return bestProposalName; }
|
||||
export function proposalNameToDisplay (bountyProposal: DeriveCollectiveProposal, status: BountyStatus): string | undefined {
|
||||
if (bountyProposal.proposal.method !== 'unassignCurator') { return bountyProposal.proposal.method; }
|
||||
|
||||
return status.isCuratorProposed ? 'unassignCurator' : 'slashCurator';
|
||||
}
|
||||
|
||||
@ -114,17 +114,20 @@ export class BountiesPage {
|
||||
} as QueueProps;
|
||||
|
||||
return render(
|
||||
<Suspense fallback='...'>
|
||||
<QueueProvider value={queue}>
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ApiContext.Provider value={mockApi}>
|
||||
<Bounties/>
|
||||
</ApiContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
</QueueProvider>
|
||||
</Suspense>
|
||||
<>
|
||||
<div id='tooltips'/>
|
||||
<Suspense fallback='...'>
|
||||
<QueueProvider value={queue}>
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ApiContext.Provider value={mockApi}>
|
||||
<Bounties/>
|
||||
</ApiContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
</QueueProvider>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -305,4 +308,13 @@ export class BountiesPage {
|
||||
fireEvent.change(beneficiaryAccountInput, { target: { value: beneficiary } });
|
||||
fireEvent.keyDown(beneficiaryAccountInput, { code: 'Enter', key: 'Enter' });
|
||||
}
|
||||
|
||||
async expectVotingDescription (description: string): Promise<void> {
|
||||
this.assertRendered();
|
||||
const votingInfo = await this.findByTestId('voting-description');
|
||||
const icon = await within(votingInfo).findByTestId('question-circle');
|
||||
|
||||
fireEvent.mouseEnter(icon);
|
||||
expect(await this.findByText(description)).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ library.add(fas);
|
||||
|
||||
function Icon ({ className = '', color = 'normal', icon, isSpinning, onClick, size = '1x', tooltip }: Props): React.ReactElement<Props> {
|
||||
const extraProps = tooltip
|
||||
? { 'data-for': tooltip, 'data-tip': true }
|
||||
? { 'data-for': tooltip, 'data-testid': icon, 'data-tip': true }
|
||||
: {};
|
||||
|
||||
return (
|
||||
|
||||
@ -6,9 +6,11 @@ import ReactDOM from 'react-dom';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const rootElement = typeof document === 'undefined'
|
||||
? null // This hack is required for server side rendering
|
||||
: document.getElementById('tooltips');
|
||||
function rootElement () {
|
||||
return typeof document === 'undefined'
|
||||
? null // This hack is required for server side rendering
|
||||
: document.getElementById('tooltips');
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -33,10 +35,12 @@ function Tooltip ({ className = '', effect = 'solid', offset, place = 'top', tex
|
||||
);
|
||||
|
||||
useEffect((): () => void => {
|
||||
rootElement && rootElement.appendChild(tooltipContainer);
|
||||
const root = rootElement();
|
||||
|
||||
root && root.appendChild(tooltipContainer);
|
||||
|
||||
return (): void => {
|
||||
rootElement && rootElement.removeChild(tooltipContainer);
|
||||
root && root.removeChild(tooltipContainer);
|
||||
};
|
||||
}, [tooltipContainer]);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user