Adjust components placement and styling in bounties list (#4460)

This commit is contained in:
MiZiet 2021-01-22 12:06:49 +01:00 committed by GitHub
parent db80609f3e
commit b09bc9cbc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 917 additions and 289 deletions

View File

@ -8,6 +8,9 @@
"Approval under voting": "Approval under voting",
"Approve": "Approve",
"Assign curator": "Assign curator",
"Aye": "Aye",
"Aye: {{count}}": "Aye: {{count}}",
"Beneficiary": "Beneficiary",
"Bond": "Bond",
"Bounties": "Bounties",
"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.",
@ -22,6 +25,8 @@
"Extend Expiry": "Extend Expiry",
"How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.": "How much should be paid out for completed Bounty. Upon funding, the amount will be reserved in treasury.",
"Initiate Voting": "Initiate Voting",
"Nay": "Nay",
"Nay: {{count}}": "Nay: {{count}}",
"No open bounties": "No open bounties",
"Only curator can extend the bounty time.": "Only curator can extend the bounty time.",
"Part of the bounty value that will go to the Curator as a reward for their work": "Part of the bounty value that will go to the Curator as a reward for their work",
@ -62,6 +67,7 @@
"curator": "curator",
"curator account": "curator account",
"curator's fee": "curator's fee",
"next action": "next action",
"next bounty funding in": "next bounty funding in",
"next burn": "next burn",
"past": "past",
@ -69,6 +75,7 @@
"proposing account": "proposing account",
"select curator": "select curator",
"spend period": "spend period",
"status": "status",
"submit with account": "submit with account",
"title": "title",
"treasury": "treasury",

View File

@ -17,6 +17,8 @@
"Approval under voting": "",
"Approving of all or none of the options is equivalent and will not affect the outcome of the poll.": "",
"Authorize transaction": "",
"Aye": "",
"Aye: {{count}}": "",
"Bond": "",
"Cancel": "",
"Cancel all": "",
@ -59,6 +61,8 @@
"My On-Chain Name": "",
"My stashes": "",
"My validators": "",
"Nay": "",
"Nay: {{count}}": "",
"Network": "",
"Never execute JS snippets from untrusted sources.": "",
"Next session": "",
@ -214,6 +218,7 @@
"motions": "",
"multisig signatory": "",
"nay": "",
"next action": "",
"nominators": "",
"other": "",
"other stake": "",

View File

@ -7,7 +7,9 @@ import type { ThemeDef } from '@polkadot/react-components/types';
const FONT = {
fontMono: '0.9em Consolas, monaco, "Ubuntu Mono", "Liberation Mono", "Courier New", Courier, monospace',
fontSans: '1em "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontWeightBold: 700,
fontWeightLight: 200,
fontWeightMedium: 500,
fontWeightNormal: 400
};
@ -38,7 +40,7 @@ export const darkTheme: ThemeDef = {
export const lightTheme: ThemeDef = {
...FONT,
bgInput: '#fff',
bgInput: '#ffffff',
bgInputError: '#fff6f6',
bgInverse: 'rgba(244, 242, 240, 0.91)',
bgMenu: '#fff',
@ -52,7 +54,7 @@ export const lightTheme: ThemeDef = {
color: '#4e4e4e',
colorCheckbox: 'rgba(34, 36, 38, 0.15)',
colorError: 'rgba(139, 0, 0)',
colorLabel: 'rgba(78, 78, 78, 0.66)',
colorLabel: 'rgba(78,78,78,0.66)',
colorSummary: 'rgba(0, 0, 0, 0.6)',
contentHalfWidth: `${MAX_WIDTH / 2}px`,
contentMaxWidth: `${MAX_WIDTH}px`,

View File

@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { DeriveBounties, DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { BlockNumber, Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
import { fireEvent, render } from '@testing-library/react';
import BN from 'bn.js';
@ -11,133 +11,88 @@ import React, { Suspense } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { ApiPromise } from '@polkadot/api';
import { lightTheme } from '@polkadot/apps/themes';
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
import { Metadata } from '@polkadot/metadata';
import metaStatic from '@polkadot/metadata/static';
import { ApiContext } from '@polkadot/react-api';
import { ApiProps } from '@polkadot/react-api/types';
import i18next from '@polkadot/react-components/i18n';
import { QueueProvider } from '@polkadot/react-components/Status/Context';
import { QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
import { createAugmentedApi } from '@polkadot/test-support/api';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { BountyFactory } from '@polkadot/test-support/creation/bounties/bountyFactory';
import { aliceSigner, MemoryStore } from '@polkadot/test-support/keyring';
import { TypeRegistry } from '@polkadot/types/create';
import { keyring } from '@polkadot/ui-keyring';
import { defaultAccounts,
defaultBalance,
defaultBountyApi,
defaultMembers,
defaultTreasury } from '../test/hooks/defaults';
import Bounties from './Bounties';
import { BountyApi } from './hooks';
function bountyStatus (status: string): BountyStatus {
return new TypeRegistry().createType('BountyStatus', status);
}
const alice = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const bob = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty';
function aBounty ({ value = balanceOf(1), status = bountyStatus('Proposed') }: Partial<Bounty> = {}): Bounty {
return new TypeRegistry().createType('Bounty', { status, value });
}
const mockMembers = defaultMembers;
const mockAccounts = defaultAccounts;
const mockTreasury = defaultTreasury;
let mockBountyApi = defaultBountyApi;
let mockBalance = defaultBalance;
function anIndex (index = 0): BountyIndex {
return new TypeRegistry().createType('BountyIndex', index);
}
jest.mock('./hooks', () => ({
useBalance: () => mockBalance,
useBounties: () => mockBountyApi
}));
function balanceOf (number: number) {
return new TypeRegistry().createType('Balance', new BN(number));
}
jest.mock('@polkadot/react-hooks/useTreasury', () => ({
useTreasury: () => mockTreasury
}));
jest.mock('@polkadot/react-hooks/useMembers', () => ({
useMembers: () => mockMembers
}));
jest.mock('@polkadot/react-hooks/useAccounts', () => ({
useAccounts: () => mockAccounts
}));
function aGenesisHash () {
return new TypeRegistry().createType('Hash', POLKADOT_GENESIS);
}
const extendBountyExpiry = jest.fn().mockReturnValue('mockProposeExtrinsic');
let mockBountyApi: BountyApi = {
approveBounty: jest.fn(),
bestNumber: new BN(1) as BlockNumber,
bounties: [] as DeriveBounties,
bountyDepositBase: new BN(1),
bountyValueMinimum: new BN(1),
closeBounty: jest.fn(),
dataDepositPerByte: new BN(1),
extendBountyExpiry,
maximumReasonLength: 100,
proposeBounty: jest.fn(),
proposeCurator: jest.fn()
};
let mockBalance = balanceOf(1);
let apiWithAugmentations: ApiPromise;
const mockMembers = { isMember: true };
const mockAccounts = { allAccounts: ['5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'] };
const mockTreasury = {
burn: new BN(1),
spendPeriod: new BN(0),
value: balanceOf(1)
};
jest.mock('./hooks', () => {
return {
useBalance: () => mockBalance,
useBounties: () => mockBountyApi
};
});
jest.mock('@polkadot/react-hooks/useTreasury', () => {
return {
useTreasury: () => mockTreasury
};
});
export function createApiWithAugmentations (): ApiPromise {
const registry = new TypeRegistry();
const metadata = new Metadata(registry, metaStatic);
registry.setMetadata(metadata);
const api = new ApiPromise({ provider: new WsProvider('ws://', false), registry });
api.injectMetadata(metadata, true);
return api;
}
function bountyInStatus (status: string) {
return new TypeRegistry().createType('Bounty', { status, value: new BN(151) });
}
function aProposal (extrinsic: SubmittableExtrinsic<'promise'>) {
function aProposal (extrinsic: SubmittableExtrinsic<'promise'>, ayes: string[] = [alice], nays: string[] = [bob]) {
return {
hash: new TypeRegistry().createType('Hash'),
proposal: apiWithAugmentations
proposal: augmentedApi
.registry.createType('Proposal', extrinsic),
votes: apiWithAugmentations.registry.createType('Votes', {
ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'],
votes: augmentedApi.registry.createType('Votes', {
ayes: ayes,
index: 0,
nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'],
nays: nays,
threshold: 4
})
};
}
jest.mock('@polkadot/react-hooks/useMembers', () => {
return {
useMembers: () => mockMembers
};
});
jest.mock('@polkadot/react-hooks/useAccounts', () => {
return {
useAccounts: () => mockAccounts
};
});
const propose = jest.fn().mockReturnValue('mockProposeExtrinsic');
let augmentedApi: ApiPromise;
let queueExtrinsic: QueueTxExtrinsicAdd;
let aBountyStatus: (status: string) => BountyStatus;
let aBountyIndex: (index?:number) => BountyIndex;
let aBounty: ({ status, value }?: Partial<Bounty>) => Bounty;
let bountyWith: ({ status, value }: { status?: string, value?: number }) => Bounty;
describe('Bounties', () => {
beforeAll(async () => {
await i18next.changeLanguage('en');
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
apiWithAugmentations = createApiWithAugmentations();
augmentedApi = createAugmentedApi();
({ aBounty, aBountyIndex, aBountyStatus, bountyWith } = new BountyFactory(augmentedApi));
});
beforeEach(() => {
queueExtrinsic = jest.fn() as QueueTxExtrinsicAdd;
@ -180,12 +135,12 @@ describe('Bounties', () => {
);
};
function renderOneBounty (bounty: Bounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = anIndex()) {
function renderOneBounty (bounty: Bounty, proposals: DeriveCollectiveProposal[] = [], description = '', index = aBountyIndex()) {
return renderBounties({ bounties: [{ bounty, description, index, proposals }] });
}
it('creates correct bounty with proposal', () => {
const bounty = apiWithAugmentations.registry.createType('Bounty', {
const bounty = augmentedApi.registry.createType('Bounty', {
curatorDeposit: new BN(1),
status: 'Funded',
value: new BN(10)
@ -197,7 +152,7 @@ describe('Bounties', () => {
});
it('creates correct proposal', () => {
const proposal = apiWithAugmentations.registry.createType('Proposal', apiWithAugmentations.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1));
const proposal = augmentedApi.registry.createType('Proposal', augmentedApi.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1));
expect(proposal.args[0].eq(new BN(0))).toBeTruthy();
expect(proposal.args[1].toString()).toEqual('5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z');
@ -206,7 +161,7 @@ describe('Bounties', () => {
});
it('creates correct votes', () => {
const votes = apiWithAugmentations.registry.createType('Votes', { ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], index: 0, nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'], threshold: 4 });
const votes = augmentedApi.registry.createType('Votes', { ayes: ['5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'], index: 0, nays: ['5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'], threshold: 4 });
expect(votes.ayes.length).toEqual(1);
expect(votes.ayes[0].toString()).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
@ -230,14 +185,14 @@ describe('Bounties', () => {
});
it('renders bounties in order from newest to oldest', async () => {
const bounty1 = bountyInStatus('Proposed');
const bounty2 = bountyInStatus('Proposed');
const bounty3 = bountyInStatus('Proposed');
const bounty1 = bountyWith({ status: 'Proposed' });
const bounty2 = bountyWith({ status: 'Proposed' });
const bounty3 = bountyWith({ status: 'Proposed' });
const { findAllByTestId } = renderBounties({ bounties: [
{ bounty: bounty1, description: 'bounty 2', index: anIndex(2), proposals: [] },
{ bounty: bounty2, description: 'bounty 1', index: anIndex(1), proposals: [] },
{ bounty: bounty3, description: 'bounty 3', index: anIndex(3), proposals: [] }
{ bounty: bounty1, description: 'bounty 2', index: aBountyIndex(2), proposals: [] },
{ bounty: bounty2, description: 'bounty 1', index: aBountyIndex(1), proposals: [] },
{ bounty: bounty3, description: 'bounty 3', index: aBountyIndex(3), proposals: [] }
] });
const descriptions = await findAllByTestId('description');
@ -285,7 +240,7 @@ describe('Bounties', () => {
describe('propose curator modal', () => {
it('shows an error if fee is greater than bounty value', async () => {
const bounty = { status: bountyStatus('Funded'), value: balanceOf(5) };
const bounty = { status: aBountyStatus('Funded'), value: balanceOf(5) };
const { findByTestId, findByText } = renderOneBounty(aBounty(bounty));
const proposeCuratorButton = await findByText('Propose Curator');
@ -301,7 +256,7 @@ describe('Bounties', () => {
});
it('disables Assign Curator button if validation fails', async () => {
const bounty = { status: bountyStatus('Funded'), value: balanceOf(5) };
const bounty = { status: aBountyStatus('Funded'), value: balanceOf(5) };
const { findByTestId, findByText } = renderOneBounty(aBounty(bounty));
const proposeCuratorButton = await findByText('Propose Curator');
@ -319,7 +274,7 @@ describe('Bounties', () => {
});
it('queues propose extrinsic on submit', async () => {
const bounty = { status: bountyStatus('Funded') };
const bounty = { status: aBountyStatus('Funded') };
const { findByTestId, findByText, getAllByRole } = renderOneBounty(aBounty(bounty));
const proposeCuratorButton = await findByText('Propose Curator');
@ -349,7 +304,7 @@ describe('Bounties', () => {
describe('extend bounty expiry action modal', () => {
it('queues extend bounty expiry extrinsic on submit', async () => {
const bounty = bountyInStatus('Active');
const bounty = bountyWith({ status: 'Active' });
const { findByTestId, findByText } = renderOneBounty(bounty);
@ -367,14 +322,14 @@ describe('Bounties', () => {
fireEvent.click(acceptButton);
expect(queueExtrinsic).toHaveBeenCalledWith(expect.objectContaining({ accountId: '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM', extrinsic: 'mockProposeExtrinsic' }));
expect(extendBountyExpiry).toHaveBeenCalledWith(anIndex(0), 'The bounty extend expiry remark');
expect(mockBountyApi.extendBountyExpiry).toHaveBeenCalledWith(aBountyIndex(0), 'The bounty extend expiry remark');
});
});
describe('status is extended when voting', () => {
describe('status is extended', () => {
it('on proposed curator', async () => {
const bounty = bountyInStatus('Funded');
const proposals = [aProposal(apiWithAugmentations.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))];
const bounty = bountyWith({ status: 'Funded' });
const proposals = [aProposal(augmentedApi.tx.bounties.proposeCurator(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z', 1))];
const { findByText } = renderOneBounty(bounty, proposals);
@ -382,8 +337,8 @@ describe('Bounties', () => {
});
it('on bounty approval in proposed status', async () => {
const bounty = bountyInStatus('Proposed');
const proposals = [aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))];
const bounty = bountyWith({ status: 'Proposed' });
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
const { findByText } = renderOneBounty(bounty, proposals);
@ -391,10 +346,10 @@ describe('Bounties', () => {
});
it('on parallel bounty approval and bounty close in proposed status', async () => {
const bounty = bountyInStatus('Proposed');
const bounty = bountyWith({ status: 'Proposed' });
const proposals = [
aProposal(apiWithAugmentations.tx.bounties.closeBounty(0)),
aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))
aProposal(augmentedApi.tx.bounties.closeBounty(0)),
aProposal(augmentedApi.tx.bounties.approveBounty(0))
];
const { findByText } = renderOneBounty(bounty, proposals);
@ -403,8 +358,8 @@ describe('Bounties', () => {
});
it('on close bounty in active status', async () => {
const bounty = bountyInStatus('Active');
const proposals = [aProposal(apiWithAugmentations.tx.bounties.closeBounty(0))];
const bounty = bountyWith({ status: 'Active' });
const proposals = [aProposal(augmentedApi.tx.bounties.closeBounty(0))];
const { findByText } = renderOneBounty(bounty, proposals);
@ -412,8 +367,8 @@ describe('Bounties', () => {
});
it('on unassign curator in active state', async () => {
const bounty = bountyInStatus('Active');
const proposals = [aProposal(apiWithAugmentations.tx.bounties.unassignCurator(0))];
const bounty = bountyWith({ status: 'Active' });
const proposals = [aProposal(augmentedApi.tx.bounties.unassignCurator(0))];
const { findByText } = renderOneBounty(bounty, proposals);
@ -421,12 +376,88 @@ describe('Bounties', () => {
});
it('on bounty approval in active state', async () => {
const bounty = bountyInStatus('Active');
const proposals = [aProposal(apiWithAugmentations.tx.bounties.approveBounty(0))];
const bounty = bountyWith({ status: 'Active' });
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
const { findByTestId } = renderOneBounty(bounty, proposals);
await expect(findByTestId('extendedStatus')).rejects.toThrow();
await expect(findByTestId('extendedVotingStatus')).rejects.toThrow();
});
it('when bounty is claimable', async () => {
const bounty = bountyWith({ status: 'PendingPayout' });
const { findByTestId } = renderOneBounty(bounty);
expect((await findByTestId('extendedActionStatus')).textContent).toEqual('Claimable');
});
});
describe('Voters', () => {
it('aye and nay', async () => {
const bounty = bountyWith({ status: 'Proposed' });
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0))];
const { findAllByTestId } = renderOneBounty(bounty, proposals);
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
const nayVoters = await findAllByTestId((testId) => testId.startsWith('voters_nays'));
expect(ayeVoters).toHaveLength(1);
expect(nayVoters).toHaveLength(1);
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
expect(nayVoters[0].getAttribute('data-testid')).toContain(bob);
});
it('multiple aye and no nay', async () => {
const bounty = bountyWith({ status: 'Proposed' });
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
const { findAllByTestId } = renderOneBounty(bounty, proposals);
const ayeVoters = await findAllByTestId((testId) => testId.startsWith('voters_ayes'));
expect(ayeVoters).toHaveLength(2);
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
expect(ayeVoters[0].getAttribute('data-testid')).toContain(alice);
expect(ayeVoters[1].getAttribute('data-testid')).toContain(bob);
});
it('not displayed when no voting', async () => {
const bounty = bountyWith({ status: 'Proposed' });
const proposals: DeriveCollectiveProposal[] = [];
const { findAllByTestId } = renderOneBounty(bounty, proposals);
await expect(findAllByTestId((testId) => testId.startsWith('voters_ayes'))).rejects.toThrow();
await expect(findAllByTestId((testId) => testId.startsWith('voters_nays'))).rejects.toThrow();
});
});
describe('Voting summary', () => {
it('is displayed when voting', async () => {
const bounty = bountyWith({ status: 'Proposed' });
const proposals = [aProposal(augmentedApi.tx.bounties.approveBounty(0), [alice, bob], [])];
const { findByTestId } = renderOneBounty(bounty, proposals);
expect((await findByTestId('voting-summary')).textContent).toEqual('Aye 2/4Nay 0/0Voting results');
});
it('is not displayed when no voting', async () => {
const bounty = bountyWith({ status: 'Proposed' });
const proposals: DeriveCollectiveProposal[] = [];
const { findByTestId } = renderOneBounty(bounty, proposals);
await expect(findByTestId('voting-summary')).rejects.toThrow();
});
});
describe('Description', () => {
it('Beneficiary', async () => {
const bounty = bountyWith({ status: 'PendingPayout' });
const proposals = [aProposal(augmentedApi.tx.bounties.awardBounty(0, '5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z'))];
const { findByText } = renderOneBounty(bounty, proposals);
expect(await findByText('Beneficiary')).toBeTruthy();
});
});
});

View File

@ -2,40 +2,46 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
import styled from 'styled-components';
import Summary from '@polkadot/app-bounties/Summary';
import { Button, Table } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import Bounty from './Bounty';
import BountyCreate from './BountyCreate';
import { useBounties } from './hooks';
import { useTranslation } from './translate';
function Bounties (): React.ReactElement {
interface Props {
className?: string;
}
function Bounties ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const { bestNumber, bounties } = useBounties();
const headerRef = useRef([
[t('bounties'), 'start'],
[t('status'), 'start'],
[t('title'), 'start'],
[],
[t('value'), 'start'],
[t('curator'), 'start'],
[t('update due'), 'start'],
[t('beneficiary'), 'start'],
[t('payout due'), 'start'],
[],
[t('next action'), 'start'],
[],
[]
]);
return (
<>
<div className={className}>
<Summary activeBounties={bounties?.length}/>
<Button.Group>
<BountyCreate />
</Button.Group>
<Table
className='bounties-table-wrapper'
empty={bounties && t<string>('No open bounties')}
hasTitle={false}
header={headerRef.current}
>
{bounties && bestNumber &&
@ -51,8 +57,55 @@ function Bounties (): React.ReactElement {
/>
))}
</Table>
</>
</div>
);
}
export default React.memo(Bounties);
export default React.memo(styled(Bounties)(({ theme }: ThemeProps) => `
.bounties-table-wrapper table {
background: none;
&::before {
background: none;
}
tr {
td,
&:not(.filter) th {
&:first-child {
padding-left: 1.14rem;
}
&:last-child {
padding-right: 1.14rem;
}
}
}
}
.ui--IdentityIcon {
margin-right: 0.42rem;
}
.via-identity .name {
font-size: 1rem;
line-height: 1.7rem;
text-transform: initial;
filter: initial;
opacity: 1;
}
.ui--Table th:first-child h1 {
padding: 0;
font-weight: ${theme.fontWeightBold};
font-size: 0.7rem;
line-height: 0.85rem;
text-transform: uppercase;
color: ${theme.colorLabel};
border: none;
svg {
display: none;
}
}
`));

View File

@ -9,12 +9,15 @@ import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { AddressSmall, Icon, LinkExternal } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
import VotingResultsColumn from './Voting/VotersColumn';
import { BountyActions } from './BountyActions';
import BountyInfos from './BountyInfos';
import BountyStatusView from './BountyStatusView';
import { getBountyStatus, truncateTitle } from './helpers';
import { getBountyStatus } from './helpers';
import { useTranslation } from './translate';
interface Props {
@ -28,6 +31,7 @@ interface Props {
interface DueProps {
dueBlocks: BN | undefined;
until: 'update' | 'payout';
}
const EMPTY_CELL = '-';
@ -55,144 +59,261 @@ function Bounty ({ bestNumber, bounty, className = '', description, index, propo
<tr className={className}>
<td>
<BountyStatusView
blocksUntilPayout={blocksUntilPayout}
bountyStatus={bountyStatus}
proposals={proposals}
status={status}
/>
</td>
<td data-testid='description'>{truncateTitle(description, 30)}</td>
<td
className='description-column'
colSpan={2}
data-testid='description'
>
<div title={description}>
{description}
</div>
</td>
<td><FormatBalance value={value} /></td>
<td>{curator ? <AddressSmall value={curator} /> : EMPTY_CELL}</td>
<td><DueBlocks dueBlocks={blocksUntilUpdate} /></td>
<td>{beneficiary ? <AddressSmall value={beneficiary} /> : EMPTY_CELL}</td>
<td><DueBlocks dueBlocks={blocksUntilPayout} /></td>
<td>{curator && <AddressSmall value={curator} />}</td>
<td>
<BountyActions
bestNumber={bestNumber}
description={description}
index={index}
proposals={proposals}
status={status}
value={value}
/>
{blocksUntilPayout
? <DueBlocks
dueBlocks={blocksUntilPayout}
until={'payout'}
/>
: ''}
{blocksUntilUpdate
? <DueBlocks
dueBlocks={blocksUntilUpdate}
until={'update'}
/>
: ''}
</td>
<td className='table-column-icon'>
<LinkExternal
data={index}
isLogo
type='bounty'
/>
<td>
<div className='td-row'>
<BountyInfos
beneficiary={beneficiary}
proposals={proposals}
status={status}
/>
<div className='bounty-action-row'>
<BountyActions
bestNumber={bestNumber}
description={description}
index={index}
proposals={proposals}
status={status}
value={value}
/>
</div>
</div>
</td>
<td className='table-column-icon'>
<div onClick={handleOnIconClick}>
<Icon
icon={
<td className='fast-actions'>
<div className='fast-actions-row'>
<LinkExternal
data={index}
isLogo
type='bounty'
/>
<div className='table-column-icon'
onClick={handleOnIconClick}>
<Icon icon={
isExpanded
? 'caret-up'
: 'caret-down'
}
/>
/>
</div>
</div>
</td>
</tr>
<tr className={className}
style={{ visibility: isExpanded ? 'visible' : 'collapse' }}>
<td className='proposer'
colSpan={2}>
<div className='proposer-row'>
<div className='label'>{t('Proposer')}</div>
<AddressSmall value={proposer} />
</div>
</td>
<td className='column-with-label'
colSpan={2}>
<div className='column-with-label-row'>
<div className='label'>{t('Bond')}</div>
<div className='inline-balance'><FormatBalance value={bond} /></div>
</div>
<div className='column-with-label-row'>
<div className='label'>{t("Curator's fee")}</div>
<div className='inline-balance'>{curator ? <FormatBalance value={fee} /> : EMPTY_CELL}</div>
</div>
<div className='column-with-label-row'>
<div className='label'>{t("Curator's deposit")}</div>
<div className='inline-balance'>{curator ? <FormatBalance value={curatorDeposit} /> : EMPTY_CELL}</div>
</div>
</td>
<td />
<td />
<td className='proposer'>
<div className='label'>{t('Proposer')}</div>
<AddressSmall value={proposer} />
</td>
<td className='column-with-label'>
<div className='label'>{t('Value')}</div>
<div className='label'>{t('Bond')}</div>
</td>
<td >
<div className='inline-balance'><FormatBalance value={value} /></div>
<div className='inline-balance'><FormatBalance value={bond} /></div>
</td>
<td className='column-with-label'>
<div className='label'>{t("Curator's fee")}</div>
<div className='label'>{t("Curator's deposit")}</div>
</td>
<td>
<div className='inline-balance'>{curator ? <FormatBalance value={fee} /> : EMPTY_CELL}</div>
<div className='inline-balance'>{curator ? <FormatBalance value={curatorDeposit} /> : EMPTY_CELL}</div>
{proposals && (
<div className='votes-table'>
<VotingResultsColumn
option={'ayes'}
proposals={proposals}
status={status}
/>
<VotingResultsColumn
option={'nays'}
proposals={proposals}
status={status}
/>
</div>
)}
</td>
<td />
<td />
<td />
<td />
</tr>
</>
);
}
function DueBlocks ({ dueBlocks }: DueProps): React.ReactElement<DueProps> {
function DueBlocks ({ dueBlocks, until }: DueProps): React.ReactElement<DueProps> {
const { t } = useTranslation();
if (!dueBlocks) {
return <>{EMPTY_CELL}</>;
}
return dueBlocks.gtn(0)
? <>
<BlockToTime blocks={dueBlocks} />
{t<string>('{{blocks}} blocks', { replace: { blocks: formatNumber(dueBlocks) } })}
return (
<>
{dueBlocks && dueBlocks.gtn(0) && (
<>
{t<string>('{{blocks}} blocks', { replace: { blocks: formatNumber(dueBlocks) } })}
<BlockToTime blocks={dueBlocks}
className='block-to-time'> until {until}</BlockToTime>
</>
)}
</>
: <>{t('Claimable')}</>;
);
}
export default React.memo(styled(Bounty)`
export default React.memo(styled(Bounty)(({ theme }: ThemeProps) => `
.description-column {
max-width: 200px;
div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
& .links {
display: inline-flex;
}
& .table-column-icon {
width: 52px;
padding: 0;
& .fast-actions {
.fast-actions-row {
display: flex;
align-items: center;
justify-content: flex-end;
div {
padding: 0.75rem;
& > * + * {
margin-left: 0.285rem;
}
}
.table-column-icon {
display: flex;
align-items: center;
justify-content: center;
width: 1.7rem;
height: 1.7rem;
border: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
border-radius: 4px;
cursor: pointer;
}
}
& .inline-balance {
display: inline-block;
width: 100%;
margin: 12px 0;
font-size: 1rem;
line-height: normal;
}
.settings-button {
width: 24px;
height: 24px;
padding: 0;
border-radius: 4px;
& .column-with-label {
vertical-align: middle;
padding: 0;
div {
text-align: right;
padding: 12px 0 12px 0;
font-size: 0.7rem;
line-height: normal;
text-transform: uppercase;
color: #4D4D4D;
&:nth-child(2){
padding: 18px 0 10px 0;
svg {
padding: 0;
margin: 0;
color: #000 !important;
}
&:hover {
background: #fff;
}
&:focus {
background: #fff;
border: 1px solid #616161;
}
}
}
& .proposer {
vertical-align: middle;
div {
display: inline-block;
}
& .inline-balance {
width: 50%;
font-size: 1rem;
line-height: normal;
}
.label {
text-align: right;
padding: 0 1.7rem 0 0;
font-weight: 500;
font-size: 0.7rem;
line-height: normal;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
text-transform: uppercase;
}
& .column-with-label-row {
display: flex;
align-items: center;
padding: 0 0 1.7rem;
.label {
text-align: right;
padding: 12px 12px 12px 0;
font-size: 0.7rem;
line-height: normal;
text-transform: uppercase;
color: #4D4D4D;
width: 50%;
}
}
`);
.proposer-row {
display: flex;
align-items: center;
}
.td-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.bounty-action-row {
display: flex;
justify-content: flex-end;
align-items: center;
margin-left: auto;
& > * + * {
margin-left: 0.6rem;
}
}
.block-to-time {
margin-top: 0.28rem;
font-size: 0.7rem;
line-height: 0.85rem;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
}
& .votes-table {
display: flex;
justify-content: space-between;
}
& .ui--FormatBalance {
font-size: 0.85rem;
line-height: 1.4rem;
}
`));

View File

@ -6,8 +6,9 @@ import type { AccountId, BountyIndex } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import React, { useMemo } from 'react';
import { useBounties } from '@polkadot/app-bounties/hooks';
import { TxButton } from '@polkadot/react-components';
import { useAccounts, useApi } from '@polkadot/react-hooks';
import { useAccounts } from '@polkadot/react-hooks';
import { isClaimable } from './helpers';
import { useTranslation } from './translate';
@ -20,7 +21,7 @@ interface Props {
function BountyClaimAction ({ beneficiaryId, index, payoutDue }: Props) {
const { t } = useTranslation();
const { api } = useApi();
const { claimBounty } = useBounties();
const { allAccounts } = useAccounts();
const isBountyClaimable = useMemo(
@ -35,7 +36,7 @@ function BountyClaimAction ({ beneficiaryId, index, payoutDue }: Props) {
icon='plus'
label={t<string>('Claim')}
params={[index]}
tx={(api.tx.bounties || api.tx.treasury).claimBounty}
tx={claimBounty}
/>
)
: null;

View File

@ -0,0 +1,45 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveCollectiveProposal } from '@polkadot/api-derive/types';
import type { AccountId, BountyStatus } from '@polkadot/types/interfaces';
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';
interface Props {
beneficiary?: AccountId;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function BountyInfos ({ beneficiary, proposals, status }: Props): JSX.Element {
const { t } = useTranslation();
return (
<>
{proposals && (
<VotingSummary
proposals={proposals}
status={status}
/>
)}
{beneficiary && (
<div>
<AddressSmall value={beneficiary} />
<Description description={t<string>('Beneficiary')} />
</div>
)}
</>
);
}
export default React.memo(styled(BountyInfos)`
`);

View File

@ -4,24 +4,26 @@
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';
import VotingDescription from './VotingDescription';
interface Props {
blocksUntilPayout?: BN
bountyStatus: StatusName;
className?: string;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function BountyStatusView ({ bountyStatus, className = '', proposals, status }: Props): React.ReactElement<Props> {
function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', proposals, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const statusHelpMessages = useRef<HelpMessages>({
@ -43,7 +45,8 @@ function BountyStatusView ({ bountyStatus, className = '', proposals, status }:
</div>
{proposals && (
<div>
<VotingDescription
<ExtendedStatus
blocksUntilPayout={blocksUntilPayout}
proposals={proposals}
status={status}
/>
@ -56,6 +59,6 @@ function BountyStatusView ({ bountyStatus, className = '', proposals, status }:
export default React.memo(styled(BountyStatusView)`
.bountyStatus {
display: flex;
align-items: flex-end;
align-items: center;
}
`);

View File

@ -0,0 +1,30 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
import { ThemeProps } from '@polkadot/react-components/types';
interface Props {
className?: string;
dataTestId?: string;
description: string;
}
function Description ({ className = '', dataTestId = '', description }: Props): JSX.Element {
return (
<div
className={className}
data-testid={dataTestId}>
{description}
</div>
);
}
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'};
`));

View File

@ -7,7 +7,7 @@ import { useBounties } from '@polkadot/app-bounties/hooks';
import { useTranslation } from '@polkadot/app-treasury/translate';
import { CardSummary, SummaryBox } from '@polkadot/react-components';
import Progress from '@polkadot/react-components/Progress';
import { useTreasury } from '@polkadot/react-hooks/useTreasury';
import { useTreasury } from '@polkadot/react-hooks';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
interface Props {

View File

@ -0,0 +1,53 @@
// 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 { BN_ZERO } from '@polkadot/util';
import Description from '../Description';
import { bestValidProposalName } from '../helpers/extendedStatuses';
import { useTranslation } from '../translate';
interface Props {
blocksUntilPayout?: BN ;
className?: string;
proposals?: DeriveCollectiveProposal[];
status: BountyStatus;
}
function ExtendedStatus ({ blocksUntilPayout, className = '', proposals, status }: Props): React.ReactElement<Props> {
const bestProposalName = proposals ? bestValidProposalName(proposals, status) : null;
const { t } = useTranslation();
const votingDescriptions = 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')
});
return (
<>
{bestProposalName && votingDescriptions.current[bestProposalName] &&
<Description
className={className}
dataTestId='extendedVotingStatus'
description={votingDescriptions.current[bestProposalName]}
/>
}
{blocksUntilPayout?.lt(BN_ZERO) &&
<Description
className={className}
dataTestId='extendedActionStatus'
description={t<string>('Claimable')}
/>
}
</>
);
}
export default React.memo(ExtendedStatus);

View File

@ -0,0 +1,80 @@
// 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, { useMemo } from 'react';
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 { useTranslation } from '../translate';
interface Props {
className?: string;
option: 'ayes' | 'nays';
proposals: DeriveCollectiveProposal[];
status: BountyStatus;
}
const icons = {
ayes: 'check',
nays: 'times'
};
function VotersColumn ({ className, option, proposals, status }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const proposal = useMemo(() => getProposalToDisplay(proposals, status), [proposals, status]);
const votes = useMemo(() => option === 'ayes' ? proposal?.votes?.ayes : proposal?.votes?.nays, [proposal, option]);
const voters = useMemo(() => votes?.map((accountId) =>
<div
className='voter'
data-testid={`voters_${option}_${accountId.toString()}`}
key={accountId.toString()}
>
<AddressSmall value={accountId}/>
</div>),
[option, votes]);
return (
<>
{proposal &&
<div className={className}>
<div className='vote-numbers'>
<span className='vote-numbers-icon'><Icon icon={icons[option]} /></span>
<span className='vote-numbers-label'>
{option === 'ayes' && t('Aye: {{count}}', { replace: { count: votes?.length } })}
{option === 'nays' && t('Nay: {{count}}', { replace: { count: votes?.length } })}
</span>
</div>
{voters}
</div>}
</>
);
}
export default React.memo(styled(VotersColumn)(({ theme }: ThemeProps) => `
.vote-numbers {
display: flex;
align-items: center;
margin-bottom: 0.85rem;
}
.vote-numbers-icon svg {
max-width: 10px;
color: ${theme.theme === 'dark' ? '#757575' : '#8B8B8B'};
}
.vote-numbers-label {
margin-left: 0.75rem;
font-weight: bold;
font-size: 0.7rem;
line-height: 0.85rem;
text-transform: uppercase;
color: ${theme.colorLabel};
}
`));

View File

@ -0,0 +1,59 @@
// 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, { 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';
interface Props {
className?: string;
proposals: DeriveCollectiveProposal[];
status: BountyStatus;
}
function VotingSummary ({ className, proposals, status }: Props): JSX.Element {
const { members } = useMembers();
const { t } = useTranslation();
const proposal = useMemo(() => getProposalToDisplay(proposals, status), [proposals, status]);
const ayes = useMemo(() => proposal?.votes?.ayes?.length, [proposal]);
const nays = useMemo(() => proposal?.votes?.nays?.length, [proposal]);
const threshold = useMemo(() => proposal?.votes?.threshold.toNumber(), [proposal]);
const nayThreshold = useMemo(() => members?.length && threshold ? (members.length - threshold + 1) : 0, [members, threshold]);
return (
<>
{proposal && (
<div
className={className}
data-testid='voting-summary'
>
<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='Voting results' />
</div>
)}
</>
);
}
export default React.memo(styled(VotingSummary)(({ theme }: ThemeProps) => `
.voting-summary-text {
font-size: 0.85rem;
line-height: 0.5rem;
color: ${theme.theme === 'dark' ? '#eeeeee' : '#1a1b20'}
span {
min-width: 0.5rem;
margin-right: 0.5rem;
}
}
`));

View File

@ -1,47 +0,0 @@
// 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 { bestValidProposalName } from './helpers/extendedStatuses';
import { useTranslation } from './translate';
interface Props {
className?: string;
proposals: DeriveCollectiveProposal[];
status: BountyStatus;
}
function VotingDescription ({ className = '', proposals, status }: Props): React.ReactElement<Props> {
const bestProposalName = bestValidProposalName(proposals, status);
const { t } = useTranslation();
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')
});
return (
<>
{bestProposalName && (
<div
className={className}
data-testid='extendedStatus'
>
<>{descriptions.current[bestProposalName] || null}</>
</div>
)}
</>
);
}
export default React.memo(styled(VotingDescription)`
font-size: 0.7rem;
color: #8B8B8B;
margin-top: 10px;
`);

View File

@ -16,6 +16,7 @@ export type BountyApi = {
bountyDepositBase: BN,
bountyIndex?: BN,
bountyValueMinimum: BN,
claimBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
closeBounty: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
dataDepositPerByte: BN,
extendBountyExpiry: ((...args: any[]) => SubmittableExtrinsic<'promise'>);
@ -36,6 +37,7 @@ export function useBounties (): BountyApi {
const dataDepositPerByte = (constsBase.dataDepositPerByte as BalanceOf).toBn();
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;
const approveBounty = (api.tx.bounties || api.tx.treasury).approveBounty;
const closeBounty = (api.tx.bounties || api.tx.treasury).closeBounty;
const extendBountyExpiry = (api.tx.bounties || api.tx.treasury).extendBountyExpiry;
@ -47,6 +49,7 @@ export function useBounties (): BountyApi {
bountyDepositBase,
bountyIndex,
bountyValueMinimum,
claimBounty,
closeBounty,
dataDepositPerByte,
extendBountyExpiry,

View File

@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
import styled from 'styled-components';
import { Tabs } from '@polkadot/react-components';
import { ThemeProps } from '@polkadot/react-components/types';
import Bounties from './Bounties';
import { useTranslation } from './translate';
@ -12,9 +14,10 @@ export { default as useCounter } from './useCounter';
interface Props {
basePath: string;
className?: string;
}
function BountiesApp ({ basePath }: Props): React.ReactElement<Props> {
function BountiesApp ({ basePath, className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const itemsRef = useRef([
@ -26,7 +29,7 @@ function BountiesApp ({ basePath }: Props): React.ReactElement<Props> {
]);
return (
<main className='bounties--App'>
<main className={`bounties--App ${className}`}>
<header>
<Tabs
basePath={basePath}
@ -38,4 +41,77 @@ function BountiesApp ({ basePath }: Props): React.ReactElement<Props> {
);
}
export default React.memo(BountiesApp);
export default React.memo(styled(BountiesApp)(({ theme }: ThemeProps) => `
&& thead th {
padding: 0.75rem 1rem 0.57rem;
font-weight: bold;
font-size: 0.7rem;
line-height: 0.85rem;
text-transform: uppercase;
color: ${theme.colorLabel};
border: none;
}
thead tr {
border: none;
background: none;
}
tbody td {
padding: 1.4rem 1rem;
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'};
&:first-child {
border-left: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
}
&:last-child {
border-right: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
}
}
tbody tr {
&:nth-child(odd) {
background: ${theme.theme === 'dark' ? '#111218' : '#fff'};
}
&:nth-child(odd) td {
vertical-align: middle;
}
&:nth-child(even) {
background: ${theme.bgPage};
}
&:first-child {
td {
border-top: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
}
td:first-child {
border-top-left-radius: 0.285rem;
}
td:last-child {
border-top-right-radius: 0.285rem;
}
}
&:last-child {
td {
border-bottom: 1px solid ${theme.theme === 'dark' ? '#2f313c' : '#dfdfdf'};
}
td:first-child {
border-bottom-left-radius: 0.285rem;
}
td:last-child {
border-bottom-right-radius: 0.285rem;
}
}
}
`));

View File

@ -0,0 +1,36 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
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';
export const defaultBountyApi: BountyApi = {
approveBounty: jest.fn(),
bestNumber: new BN(1) as BlockNumber,
bounties: [] as DeriveBounties,
bountyDepositBase: new BN(1),
bountyValueMinimum: new BN(1),
claimBounty: jest.fn(),
closeBounty: jest.fn(),
dataDepositPerByte: new BN(1),
extendBountyExpiry: jest.fn().mockReturnValue('mockProposeExtrinsic'),
maximumReasonLength: 100,
proposeBounty: jest.fn(),
proposeCurator: jest.fn()
};
export const defaultBalance = balanceOf(1);
export const defaultTreasury = {
burn: new BN(1),
spendPeriod: new BN(0),
value: balanceOf(1)
};
export const defaultMembers = { isMember: true };
export const defaultAccounts = { allAccounts: ['5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'] };

View File

@ -7,8 +7,7 @@ import BN from 'bn.js';
import React from 'react';
import { CardSummary, SummaryBox } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import { useTreasury } from '@polkadot/react-hooks/useTreasury';
import { useApi, useCall, useTreasury } from '@polkadot/react-hooks';
import { FormatBalance } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';

View File

@ -15,9 +15,10 @@ interface Props {
filter?: React.ReactNode;
header?: (null | undefined | HeaderDef)[];
isEmpty: boolean;
hasTitle: boolean;
}
function Head ({ className = '', filter, header, isEmpty }: Props): React.ReactElement<Props> | null {
function Head ({ className = '', filter, hasTitle, header, isEmpty }: Props): React.ReactElement<Props> | null {
if (!header?.length) {
return null;
}
@ -37,7 +38,7 @@ function Head ({ className = '', filter, header, isEmpty }: Props): React.ReactE
key={index}
onClick={onClick}
>
{index === 0
{index === 0 && hasTitle
? (
<h1>
<Icon

View File

@ -16,6 +16,7 @@ interface TableProps {
filter?: React.ReactNode;
footer?: React.ReactNode;
header?: [React.ReactNode?, string?, number?, (() => void)?][];
hasTitle: boolean;
isFixed?: boolean;
legend?: React.ReactNode;
}
@ -31,7 +32,7 @@ function extractKids (children: React.ReactNode): [boolean, React.ReactNode] {
return [isEmpty, isEmpty ? null : kids];
}
function Table ({ children, className = '', empty, emptySpinner, filter, footer, header, isFixed, legend }: TableProps): React.ReactElement<TableProps> {
function Table ({ children, className = '', empty, emptySpinner, filter, footer, hasTitle = true, header, isFixed, legend }: TableProps): React.ReactElement<TableProps> {
const [isEmpty, kids] = extractKids(children);
return (
@ -40,6 +41,7 @@ function Table ({ children, className = '', empty, emptySpinner, filter, footer,
<table className={`${(isFixed && !isEmpty) ? 'isFixed' : 'isNotFixed'} highlight--bg-faint`}>
<Head
filter={filter}
hasTitle={hasTitle}
header={header}
isEmpty={isEmpty}
/>

View File

@ -93,6 +93,8 @@ export interface ThemeDef {
fontMono: string;
fontWeightLight: number;
fontWeightNormal: number;
fontWeightMedium: number;
fontWeightBold: number;
theme: 'dark' | 'light';
}

View File

@ -36,5 +36,6 @@ export { useRegistrars } from './useRegistrars';
export { useSavedFlags } from './useSavedFlags';
export { useSudo } from './useSudo';
export { useToggle } from './useToggle';
export { useTreasury } from './useTreasury';
export { useVotingStatus } from './useVotingStatus';
export { useWeight } from './useWeight';

View File

@ -0,0 +1,20 @@
// Copyright 2017-2021 @polkadot/test-support authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Metadata } from '@polkadot/metadata';
import metaStatic from '@polkadot/metadata/static';
import { TypeRegistry } from '@polkadot/types/create';
export function createAugmentedApi (): ApiPromise {
const registry = new TypeRegistry();
const metadata = new Metadata(registry, metaStatic);
registry.setMetadata(metadata);
const api = new ApiPromise({ provider: new WsProvider('ws://', false), registry });
api.injectMetadata(metadata, true);
return api;
}

View File

@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0
export * from './createApi';
export * from './createAugmentedApi';

View File

@ -0,0 +1,12 @@
// Copyright 2017-2021 @polkadot/app-bounties authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Balance } from '@polkadot/types/interfaces';
import BN from 'bn.js';
import { TypeRegistry } from '@polkadot/types/create';
export function balanceOf (number: number): Balance {
return new TypeRegistry().createType('Balance', new BN(number));
}

View File

@ -0,0 +1,32 @@
// Copyright 2017-2020 @polkadot/api authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ApiPromise } from '@polkadot/api';
import { balanceOf } from '@polkadot/test-support/creation/balance';
import { Bounty, BountyIndex, BountyStatus } from '@polkadot/types/interfaces';
import { Registry } from '@polkadot/types/types';
export class BountyFactory {
readonly #api: ApiPromise;
readonly #registry: Registry;
constructor (api: ApiPromise) {
this.#api = api;
this.#registry = this.#api.registry;
}
public aBountyIndex = (index = 0): BountyIndex =>
this.#registry.createType('BountyIndex', index);
public defaultBounty = (): Bounty =>
this.#registry.createType('Bounty');
public aBountyStatus = (status: string): BountyStatus =>
this.#registry.createType('BountyStatus', status);
public bountyWith = ({ status = 'Proposed', value = 1 } = {}): Bounty =>
this.aBounty({ status: this.aBountyStatus(status), value: balanceOf(value) })
public aBounty = ({ status = this.aBountyStatus('Proposed'), value = balanceOf(1) }: Partial<Bounty> = {}): Bounty =>
this.#registry.createType('Bounty', { status, value });
}