From b09bc9cbc938088adabc5f64e02e2f5ebacd64ec Mon Sep 17 00:00:00 2001 From: MiZiet <55240109+MiZiet@users.noreply.github.com> Date: Fri, 22 Jan 2021 12:06:49 +0100 Subject: [PATCH] Adjust components placement and styling in bounties list (#4460) --- .../apps/public/locales/en/app-bounties.json | 7 + .../apps/public/locales/en/translation.json | 5 + packages/apps/src/themes.ts | 6 +- packages/page-bounties/src/Bounties.spec.tsx | 273 +++++++++------- packages/page-bounties/src/Bounties.tsx | 71 +++- packages/page-bounties/src/Bounty.tsx | 309 ++++++++++++------ .../page-bounties/src/BountyClaimAction.tsx | 7 +- packages/page-bounties/src/BountyInfos.tsx | 45 +++ .../page-bounties/src/BountyStatusView.tsx | 11 +- packages/page-bounties/src/Description.tsx | 30 ++ packages/page-bounties/src/Summary.tsx | 2 +- .../src/Voting/ExtendedStatus.tsx | 53 +++ .../page-bounties/src/Voting/VotersColumn.tsx | 80 +++++ .../src/Voting/VotingSummary.tsx | 59 ++++ .../page-bounties/src/VotingDescription.tsx | 47 --- .../page-bounties/src/hooks/useBounties.tsx | 3 + packages/page-bounties/src/index.tsx | 82 ++++- packages/page-bounties/test/hooks/defaults.ts | 36 ++ .../page-treasury/src/Overview/Summary.tsx | 3 +- packages/react-components/src/Table/Head.tsx | 5 +- packages/react-components/src/Table/index.tsx | 4 +- packages/react-components/src/types.ts | 2 + packages/react-hooks/src/index.ts | 1 + .../src/api/createAugmentedApi.ts | 20 ++ packages/test-support/src/api/index.ts | 1 + packages/test-support/src/creation/balance.ts | 12 + .../src/creation/bounties/bountyFactory.ts | 32 ++ 27 files changed, 917 insertions(+), 289 deletions(-) create mode 100644 packages/page-bounties/src/BountyInfos.tsx create mode 100644 packages/page-bounties/src/Description.tsx create mode 100644 packages/page-bounties/src/Voting/ExtendedStatus.tsx create mode 100644 packages/page-bounties/src/Voting/VotersColumn.tsx create mode 100644 packages/page-bounties/src/Voting/VotingSummary.tsx delete mode 100644 packages/page-bounties/src/VotingDescription.tsx create mode 100644 packages/page-bounties/test/hooks/defaults.ts create mode 100644 packages/test-support/src/api/createAugmentedApi.ts create mode 100644 packages/test-support/src/creation/balance.ts create mode 100644 packages/test-support/src/creation/bounties/bountyFactory.ts diff --git a/packages/apps/public/locales/en/app-bounties.json b/packages/apps/public/locales/en/app-bounties.json index 3a6d97efb7..bc3ba61cbb 100644 --- a/packages/apps/public/locales/en/app-bounties.json +++ b/packages/apps/public/locales/en/app-bounties.json @@ -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", diff --git a/packages/apps/public/locales/en/translation.json b/packages/apps/public/locales/en/translation.json index 63c214406b..4eb19e3d13 100644 --- a/packages/apps/public/locales/en/translation.json +++ b/packages/apps/public/locales/en/translation.json @@ -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": "", diff --git a/packages/apps/src/themes.ts b/packages/apps/src/themes.ts index 1728376990..8566b4f928 100644 --- a/packages/apps/src/themes.ts +++ b/packages/apps/src/themes.ts @@ -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`, diff --git a/packages/page-bounties/src/Bounties.spec.tsx b/packages/page-bounties/src/Bounties.spec.tsx index b4b0a00d5e..4538613165 100644 --- a/packages/page-bounties/src/Bounties.spec.tsx +++ b/packages/page-bounties/src/Bounties.spec.tsx @@ -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 { - 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; +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(); }); }); }); diff --git a/packages/page-bounties/src/Bounties.tsx b/packages/page-bounties/src/Bounties.tsx index 2a6ccc8778..ca011a792a 100644 --- a/packages/page-bounties/src/Bounties.tsx +++ b/packages/page-bounties/src/Bounties.tsx @@ -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 ( - <> +
('No open bounties')} + hasTitle={false} header={headerRef.current} > {bounties && bestNumber && @@ -51,8 +57,55 @@ function Bounties (): React.ReactElement { /> ))}
- +
); } -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; + } + } +`)); diff --git a/packages/page-bounties/src/Bounty.tsx b/packages/page-bounties/src/Bounty.tsx index bb7030c2de..8dbd26d5e0 100644 --- a/packages/page-bounties/src/Bounty.tsx +++ b/packages/page-bounties/src/Bounty.tsx @@ -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 - {truncateTitle(description, 30)} + +
+ {description} +
+ - {curator ? : EMPTY_CELL} - - {beneficiary ? : EMPTY_CELL} - + {curator && } - + {blocksUntilPayout + ? + : ''} + {blocksUntilUpdate + ? + : ''} - - + +
+ +
+ +
+
- -
- +
+ +
+ + /> +
+ +
+
{t('Proposer')}
+ +
+ + +
+
{t('Bond')}
+
+
+
+
{t("Curator's fee")}
+
{curator ? : EMPTY_CELL}
+
+
+
{t("Curator's deposit")}
+
{curator ? : EMPTY_CELL}
+
+ + - -
{t('Proposer')}
- - - -
{t('Value')}
-
{t('Bond')}
- - -
-
- - -
{t("Curator's fee")}
-
{t("Curator's deposit")}
- -
{curator ? : EMPTY_CELL}
-
{curator ? : EMPTY_CELL}
+ {proposals && ( +
+ + +
+ )} - - - ); } -function DueBlocks ({ dueBlocks }: DueProps): React.ReactElement { +function DueBlocks ({ dueBlocks, until }: DueProps): React.ReactElement { const { t } = useTranslation(); - if (!dueBlocks) { - return <>{EMPTY_CELL}; - } - - return dueBlocks.gtn(0) - ? <> - - {t('{{blocks}} blocks', { replace: { blocks: formatNumber(dueBlocks) } })} + return ( + <> + {dueBlocks && dueBlocks.gtn(0) && ( + <> + {t('{{blocks}} blocks', { replace: { blocks: formatNumber(dueBlocks) } })} + until {until} + + )} - : <>{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; + } +`)); diff --git a/packages/page-bounties/src/BountyClaimAction.tsx b/packages/page-bounties/src/BountyClaimAction.tsx index c0a0dc2888..87a2854acc 100644 --- a/packages/page-bounties/src/BountyClaimAction.tsx +++ b/packages/page-bounties/src/BountyClaimAction.tsx @@ -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('Claim')} params={[index]} - tx={(api.tx.bounties || api.tx.treasury).claimBounty} + tx={claimBounty} /> ) : null; diff --git a/packages/page-bounties/src/BountyInfos.tsx b/packages/page-bounties/src/BountyInfos.tsx new file mode 100644 index 0000000000..c7346e4632 --- /dev/null +++ b/packages/page-bounties/src/BountyInfos.tsx @@ -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 && ( + + )} + {beneficiary && ( +
+ + ('Beneficiary')} /> +
+ )} + + ); +} + +export default React.memo(styled(BountyInfos)` + +`); diff --git a/packages/page-bounties/src/BountyStatusView.tsx b/packages/page-bounties/src/BountyStatusView.tsx index ba4564cecd..ca57cd1cc1 100644 --- a/packages/page-bounties/src/BountyStatusView.tsx +++ b/packages/page-bounties/src/BountyStatusView.tsx @@ -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 { +function BountyStatusView ({ blocksUntilPayout, bountyStatus, className = '', proposals, status }: Props): React.ReactElement { const { t } = useTranslation(); const statusHelpMessages = useRef({ @@ -43,7 +45,8 @@ function BountyStatusView ({ bountyStatus, className = '', proposals, status }:
{proposals && (
- @@ -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; } `); diff --git a/packages/page-bounties/src/Description.tsx b/packages/page-bounties/src/Description.tsx new file mode 100644 index 0000000000..1209177450 --- /dev/null +++ b/packages/page-bounties/src/Description.tsx @@ -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 ( +
+ {description} +
+ ); +} + +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'}; +`)); diff --git a/packages/page-bounties/src/Summary.tsx b/packages/page-bounties/src/Summary.tsx index c824117b78..7e7076fe7f 100644 --- a/packages/page-bounties/src/Summary.tsx +++ b/packages/page-bounties/src/Summary.tsx @@ -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 { diff --git a/packages/page-bounties/src/Voting/ExtendedStatus.tsx b/packages/page-bounties/src/Voting/ExtendedStatus.tsx new file mode 100644 index 0000000000..db048101ec --- /dev/null +++ b/packages/page-bounties/src/Voting/ExtendedStatus.tsx @@ -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 { + const bestProposalName = proposals ? bestValidProposalName(proposals, status) : null; + const { t } = useTranslation(); + const votingDescriptions = useRef>({ + 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] && + + } + {blocksUntilPayout?.lt(BN_ZERO) && + ('Claimable')} + /> + } + + ); +} + +export default React.memo(ExtendedStatus); diff --git a/packages/page-bounties/src/Voting/VotersColumn.tsx b/packages/page-bounties/src/Voting/VotersColumn.tsx new file mode 100644 index 0000000000..048d4b76af --- /dev/null +++ b/packages/page-bounties/src/Voting/VotersColumn.tsx @@ -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 { + 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) => +
+ +
), + [option, votes]); + + return ( + <> + {proposal && +
+
+ + + {option === 'ayes' && t('Aye: {{count}}', { replace: { count: votes?.length } })} + {option === 'nays' && t('Nay: {{count}}', { replace: { count: votes?.length } })} + +
+ {voters} +
} + + ); +} + +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}; + } +`)); diff --git a/packages/page-bounties/src/Voting/VotingSummary.tsx b/packages/page-bounties/src/Voting/VotingSummary.tsx new file mode 100644 index 0000000000..e35ee694f2 --- /dev/null +++ b/packages/page-bounties/src/Voting/VotingSummary.tsx @@ -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 && ( +
+

{t('Aye')} {ayes}/{threshold}

+

{t('Nay')} {nays}/{nayThreshold}

+ +
+ )} + + ); +} + +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; + } + } +`)); diff --git a/packages/page-bounties/src/VotingDescription.tsx b/packages/page-bounties/src/VotingDescription.tsx deleted file mode 100644 index fef6ae351c..0000000000 --- a/packages/page-bounties/src/VotingDescription.tsx +++ /dev/null @@ -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 { - const bestProposalName = bestValidProposalName(proposals, status); - const { t } = useTranslation(); - const descriptions = useRef>({ - approveBounty: t('Approval under voting'), - closeBounty: t('Rejection under voting'), - proposeCurator: t('Curator under voting'), - unassignCurator: t('Unassign curator under voting') - }); - - return ( - <> - {bestProposalName && ( -
- <>{descriptions.current[bestProposalName] || null} -
- )} - - ); -} - -export default React.memo(styled(VotingDescription)` - font-size: 0.7rem; - color: #8B8B8B; - margin-top: 10px; -`); diff --git a/packages/page-bounties/src/hooks/useBounties.tsx b/packages/page-bounties/src/hooks/useBounties.tsx index f5ab71014d..27d88c33d2 100644 --- a/packages/page-bounties/src/hooks/useBounties.tsx +++ b/packages/page-bounties/src/hooks/useBounties.tsx @@ -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, diff --git a/packages/page-bounties/src/index.tsx b/packages/page-bounties/src/index.tsx index 7b847c2808..43708c4427 100644 --- a/packages/page-bounties/src/index.tsx +++ b/packages/page-bounties/src/index.tsx @@ -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 { +function BountiesApp ({ basePath, className = '' }: Props): React.ReactElement { const { t } = useTranslation(); const itemsRef = useRef([ @@ -26,7 +29,7 @@ function BountiesApp ({ basePath }: Props): React.ReactElement { ]); return ( -
+
{ ); } -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; + } + } + } +`)); diff --git a/packages/page-bounties/test/hooks/defaults.ts b/packages/page-bounties/test/hooks/defaults.ts new file mode 100644 index 0000000000..70e07d4d4c --- /dev/null +++ b/packages/page-bounties/test/hooks/defaults.ts @@ -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'] }; diff --git a/packages/page-treasury/src/Overview/Summary.tsx b/packages/page-treasury/src/Overview/Summary.tsx index cbffdce2af..f02eb4d2d9 100644 --- a/packages/page-treasury/src/Overview/Summary.tsx +++ b/packages/page-treasury/src/Overview/Summary.tsx @@ -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'; diff --git a/packages/react-components/src/Table/Head.tsx b/packages/react-components/src/Table/Head.tsx index 9b31455b92..3412d29969 100644 --- a/packages/react-components/src/Table/Head.tsx +++ b/packages/react-components/src/Table/Head.tsx @@ -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 | null { +function Head ({ className = '', filter, hasTitle, header, isEmpty }: Props): React.ReactElement | 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 ? (

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 { +function Table ({ children, className = '', empty, emptySpinner, filter, footer, hasTitle = true, header, isFixed, legend }: TableProps): React.ReactElement { const [isEmpty, kids] = extractKids(children); return ( @@ -40,6 +41,7 @@ function Table ({ children, className = '', empty, emptySpinner, filter, footer, diff --git a/packages/react-components/src/types.ts b/packages/react-components/src/types.ts index 37ee3033ec..85ad0728e6 100644 --- a/packages/react-components/src/types.ts +++ b/packages/react-components/src/types.ts @@ -93,6 +93,8 @@ export interface ThemeDef { fontMono: string; fontWeightLight: number; fontWeightNormal: number; + fontWeightMedium: number; + fontWeightBold: number; theme: 'dark' | 'light'; } diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 859cf018b4..62c4169745 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -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'; diff --git a/packages/test-support/src/api/createAugmentedApi.ts b/packages/test-support/src/api/createAugmentedApi.ts new file mode 100644 index 0000000000..a623e99973 --- /dev/null +++ b/packages/test-support/src/api/createAugmentedApi.ts @@ -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; +} diff --git a/packages/test-support/src/api/index.ts b/packages/test-support/src/api/index.ts index 4629571e25..bae38709b2 100644 --- a/packages/test-support/src/api/index.ts +++ b/packages/test-support/src/api/index.ts @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export * from './createApi'; +export * from './createAugmentedApi'; diff --git a/packages/test-support/src/creation/balance.ts b/packages/test-support/src/creation/balance.ts new file mode 100644 index 0000000000..fa5af3cd34 --- /dev/null +++ b/packages/test-support/src/creation/balance.ts @@ -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)); +} diff --git a/packages/test-support/src/creation/bounties/bountyFactory.ts b/packages/test-support/src/creation/bounties/bountyFactory.ts new file mode 100644 index 0000000000..579a44f966 --- /dev/null +++ b/packages/test-support/src/creation/bounties/bountyFactory.ts @@ -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 => + this.#registry.createType('Bounty', { status, value }); +}