Adjust components placement and styling in bounties list (#4460)
This commit is contained in:
parent
db80609f3e
commit
b09bc9cbc9
@ -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",
|
||||
|
||||
@ -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": "",
|
||||
|
||||
@ -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`,
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`));
|
||||
|
||||
@ -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;
|
||||
|
||||
45
packages/page-bounties/src/BountyInfos.tsx
Normal file
45
packages/page-bounties/src/BountyInfos.tsx
Normal 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)`
|
||||
|
||||
`);
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
30
packages/page-bounties/src/Description.tsx
Normal file
30
packages/page-bounties/src/Description.tsx
Normal 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'};
|
||||
`));
|
||||
@ -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 {
|
||||
|
||||
53
packages/page-bounties/src/Voting/ExtendedStatus.tsx
Normal file
53
packages/page-bounties/src/Voting/ExtendedStatus.tsx
Normal 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);
|
||||
80
packages/page-bounties/src/Voting/VotersColumn.tsx
Normal file
80
packages/page-bounties/src/Voting/VotersColumn.tsx
Normal 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};
|
||||
}
|
||||
`));
|
||||
59
packages/page-bounties/src/Voting/VotingSummary.tsx
Normal file
59
packages/page-bounties/src/Voting/VotingSummary.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
`));
|
||||
@ -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;
|
||||
`);
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`));
|
||||
|
||||
36
packages/page-bounties/test/hooks/defaults.ts
Normal file
36
packages/page-bounties/test/hooks/defaults.ts
Normal 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'] };
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -93,6 +93,8 @@ export interface ThemeDef {
|
||||
fontMono: string;
|
||||
fontWeightLight: number;
|
||||
fontWeightNormal: number;
|
||||
fontWeightMedium: number;
|
||||
fontWeightBold: number;
|
||||
theme: 'dark' | 'light';
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
20
packages/test-support/src/api/createAugmentedApi.ts
Normal file
20
packages/test-support/src/api/createAugmentedApi.ts
Normal 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;
|
||||
}
|
||||
@ -2,3 +2,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './createApi';
|
||||
export * from './createAugmentedApi';
|
||||
|
||||
12
packages/test-support/src/creation/balance.ts
Normal file
12
packages/test-support/src/creation/balance.ts
Normal 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));
|
||||
}
|
||||
32
packages/test-support/src/creation/bounties/bountyFactory.ts
Normal file
32
packages/test-support/src/creation/bounties/bountyFactory.ts
Normal 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 });
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user