diff --git a/packages/apps/public/locales/en/app-accounts.json b/packages/apps/public/locales/en/app-accounts.json index 464769c6c2..6bf66768c8 100644 --- a/packages/apps/public/locales/en/app-accounts.json +++ b/packages/apps/public/locales/en/app-accounts.json @@ -103,6 +103,7 @@ "Name given to this multisig. You can edit it at any later point in time.": "Name given to this multisig. You can edit it at any later point in time.", "Name given to this proxied account. You can edit it at any later point in time.": "Name given to this proxied account. You can edit it at any later point in time.", "Next": "Next", + "No judgements": "No judgements", "No judgments": "No judgments", "No matches found": "No matches found", "No sub identities set.": "No sub identities set.", @@ -356,4 +357,4 @@ "website": "website", "your current password": "your current password", "your new password": "your new password" -} \ No newline at end of file +} diff --git a/packages/page-accounts/src/Sidebar/Identity.tsx b/packages/page-accounts/src/Sidebar/Identity.tsx index f90c4bb06c..6b8f3e2e63 100644 --- a/packages/page-accounts/src/Sidebar/Identity.tsx +++ b/packages/page-accounts/src/Sidebar/Identity.tsx @@ -5,11 +5,12 @@ import type { AddressIdentity } from '@polkadot/react-hooks/types'; import React, { useMemo } from 'react'; -import { AddressMini, AvatarItem, Expander, IconLink, Tag } from '@polkadot/react-components'; +import { AddressMini, AvatarItem, Expander, IconLink } from '@polkadot/react-components'; import { useApi, useRegistrars, useSubidentities, useToggle } from '@polkadot/react-hooks'; import { isHex } from '@polkadot/util'; import { useTranslation } from '../translate'; +import Judgements from './Judgements'; import RegistrarJudgement from './RegistrarJudgement'; import UserIcon from './UserIcon'; @@ -51,33 +52,6 @@ function Identity ({ address, identity }: Props): React.ReactElement | nu
{t('identity')} - - {identity.judgements.length}  - { - identity.judgements.length - ? identity.isBad - ? identity.isErroneous - ? t('Erroneous') - : t('Low quality') - : identity.isKnownGood - ? t('Known good') - : t('Reasonable') - : t('No judgments') - } - - } - size='tiny' - />
| nu subtitle={identity.legal} title={identity.display} /> +
{identity.parent && (
diff --git a/packages/page-accounts/src/Sidebar/JudgementTag.tsx b/packages/page-accounts/src/Sidebar/JudgementTag.tsx new file mode 100644 index 0000000000..4f983a2132 --- /dev/null +++ b/packages/page-accounts/src/Sidebar/JudgementTag.tsx @@ -0,0 +1,44 @@ +// Copyright 2017-2021 @polkadot/app-accounts authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Judgement } from '@polkadot/react-hooks/types'; + +import React, { useMemo } from 'react'; + +import { AddressSmall, Menu, Popup, Tag } from '@polkadot/react-components'; + +import { getJudgementColor } from '../util'; + +interface Props { + judgement: Judgement +} + +function JudgementTag ({ judgement: { judgementName, registrars } }: Props): React.ReactElement { + const judgementColor = useMemo(() => getJudgementColor(judgementName), [judgementName]); + + return ( + + {registrars.map((registrar) => registrar && ( + + ))} + + } + > + + + ); +} + +export default React.memo(JudgementTag); diff --git a/packages/page-accounts/src/Sidebar/Judgements.tsx b/packages/page-accounts/src/Sidebar/Judgements.tsx new file mode 100644 index 0000000000..c74721fbde --- /dev/null +++ b/packages/page-accounts/src/Sidebar/Judgements.tsx @@ -0,0 +1,63 @@ +// Copyright 2017-2021 @polkadot/app-accounts authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import styled from 'styled-components'; + +import { Tag } from '@polkadot/react-components'; +import { useJudgements } from '@polkadot/react-hooks'; + +import { useTranslation } from '../translate'; +import JudgementTag from './JudgementTag'; + +interface Props { + address: string; + className?: string; +} + +function Judgements ({ address, className = '' }: Props): React.ReactElement { + const { t } = useTranslation(); + + const judgements = useJudgements(address); + + if (judgements.length === 0) { + return ( +
+ ('No judgements')} + size='tiny' + /> +
+ ); + } + + return ( +
+ {judgements.map((judgement) => + + )} +
+ ); +} + +export default React.memo(styled(Judgements)` + margin-top: 0.714rem; + + &:not(.no-judgements) { + .ui--Tag:hover { + cursor: pointer; + } + } +`); diff --git a/packages/page-accounts/src/Sidebar/Sidebar.spec.tsx b/packages/page-accounts/src/Sidebar/Sidebar.spec.tsx index 4e6e4cd0af..91079cde92 100644 --- a/packages/page-accounts/src/Sidebar/Sidebar.spec.tsx +++ b/packages/page-accounts/src/Sidebar/Sidebar.spec.tsx @@ -3,22 +3,25 @@ import { fireEvent, screen, waitFor, within } from '@testing-library/react'; +import i18next from '@polkadot/react-components/i18n'; import { AddressFlags } from '@polkadot/react-hooks/types'; import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@polkadot/test-support/creation/account'; import { alice, bob, MemoryStore } from '@polkadot/test-support/keyring'; +import { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } from '@polkadot/test-support/mockData/registrations'; import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar'; import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks'; +import { RegistrationJudgement } from '@polkadot/types/interfaces'; import { keyring } from '@polkadot/ui-keyring'; -import { AccountRow } from '../../test/pageElements/AccountRow'; import { AccountsPage } from '../../test/pages/accountsPage'; describe('Sidebar', () => { let accountsPage: AccountsPage; - let accountRows: AccountRow[]; let sideBar: Sidebar; - beforeAll(() => { + beforeAll(async () => { + await i18next.changeLanguage('en'); + if (keyring.getAccounts().length === 0) { keyring.loadAll({ isDevelopment: true, store: new MemoryStore() }); } @@ -38,8 +41,7 @@ describe('Sidebar', () => { describe('changes name', () => { beforeEach(async () => { accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName })]]); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); + sideBar = await accountsPage.openSidebarForRow(0); await sideBar.changeAccountName(newName); }); @@ -54,14 +56,15 @@ describe('Sidebar', () => { }); it('within account row', async () => { + const accountRows = await accountsPage.getAccountRows(); + await accountRows[0].assertAccountName(newName); }); }); it('cannot be edited if edit button has not been pressed', async () => { accountsPage.renderDefaultAccounts(1); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); + sideBar = await accountsPage.openSidebarForRow(0); await sideBar.clickByText('no tags'); expect(sideBar.queryByRole('combobox')).toBeFalsy(); @@ -72,8 +75,7 @@ describe('Sidebar', () => { accountsPage.renderAccountsWithDefaultAddresses( anAccountWithInfo({ flags: { isEditable: false } as AddressFlags }) ); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); + sideBar = await accountsPage.openSidebarForRow(0); sideBar.edit(); await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError); @@ -81,9 +83,10 @@ describe('Sidebar', () => { describe('on edit cancel', () => { beforeEach(async () => { - accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })]]); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); + accountsPage.renderAccountsWithDefaultAddresses( + anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] }) + ); + sideBar = await accountsPage.openSidebarForRow(0); await sideBar.assertTags('no tags'); sideBar.edit(); @@ -110,9 +113,7 @@ describe('Sidebar', () => { anAccountWithMeta({ name: 'alice' }), anAccountWithMeta({ name: 'bob' }) ); - - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); + sideBar = await accountsPage.openSidebarForRow(0); sideBar.edit(); }); @@ -137,48 +138,111 @@ describe('Sidebar', () => { it('cancels editing and changes name when opening sidebar for another account', async () => { await waitFor(() => sideBar.assertAccountInput('alice')); - sideBar = await accountRows[1].openSidebar(); + sideBar = await accountsPage.openSidebarForRow(1); await sideBar.assertAccountName('BOB'); }); }); }); describe('identity section', () => { - it('does not display subs when account has zero subs', async () => { - accountsPage.render([[alice, anAccount()]]); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); - const subs = await sideBar.findSubs(); + describe('subs', () => { + describe('when do not exist', () => { + it('does not display subs', async () => { + accountsPage.renderDefaultAccounts(1); + sideBar = await accountsPage.openSidebarForRow(0); + const subs = await sideBar.findSubs(); - expect(subs).toHaveLength(0); + expect(subs).toHaveLength(0); + }); + }); + + describe('when exist', () => { + let subs: HTMLElement[]; + + beforeEach(async () => { + mockApiHooks.setSubs([bob]); + accountsPage.renderAccountsWithDefaultAddresses( + anAccount(), + anAccountWithMeta({ name: 'Bob' }) + ); + sideBar = await accountsPage.openSidebarForRow(0); + subs = await sideBar.findSubs(); + }); + + it('displays count of subs and account names', () => { + const subsNumber = subs[0].childNodes[0]; + const subAccount = subs[0].childNodes[1]; + + expect(subsNumber).toHaveClass('subs-number'); + expect(subsNumber).toHaveTextContent('1'); + expect(subAccount).toHaveTextContent('BOB'); + }); + + it('displays picked sub in sidebar', async () => { + const subAccount = subs[0].childNodes[1]; + + fireEvent.click(await within(subAccount as HTMLElement).findByTestId('account-name')); + + await sideBar.assertAccountName('BOB'); + }); + }); }); - it('displays count of subs and account names', async () => { - mockApiHooks.setSubs([bob]); - accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); - const subs = await sideBar.findSubs(); + describe('judgements', () => { + it('displays several judgements', async () => { + mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]); + accountsPage.renderDefaultAccounts(1); + sideBar = await accountsPage.openSidebarForRow(0); - const subsNumber = subs[0].childNodes[0]; - const subAccount = subs[0].childNodes[1]; + await sideBar.assertJudgement('1 Known good'); + await sideBar.assertJudgement('2 Reasonable'); + await sideBar.assertJudgement('1 Erroneous'); + }); - expect(subsNumber).toHaveClass('subs-number'); - expect(subsNumber).toHaveTextContent('1'); - expect(subAccount).toHaveTextContent('BOB'); - }); + it('displays no judgements', async () => { + accountsPage.renderDefaultAccounts(1); + sideBar = await accountsPage.openSidebarForRow(0); - it('displays picked sub in sidebar', async () => { - mockApiHooks.setSubs([bob]); - accountsPage.render([[alice, anAccount()], [bob, anAccountWithMeta({ name: 'Bob' })]]); - accountRows = await accountsPage.getAccountRows(); - sideBar = await accountRows[0].openSidebar(); - const subs = await sideBar.findSubs(); - const subAccount = subs[0].childNodes[1]; + await sideBar.assertJudgement('No judgements'); + }); - fireEvent.click(await within(subAccount as HTMLElement).findByTestId('account-name')); + describe('displays registrars', () => { + beforeEach(async () => { + mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]); + mockApiHooks.setRegistrars(registrars); + accountsPage.render([ + [alice, anAccountWithMeta({ name: 'Alice' })], + [bob, anAccountWithMeta({ name: 'Bob' })] + ]); + sideBar = await accountsPage.openSidebarForRow(0); + }); - await sideBar.assertAccountName('BOB'); + it('singular registrar', async () => { + const judgementTag = await sideBar.getJudgement('1 Known good'); + + await judgementTag.assertRegistrars([charlieShortAddress]); + }); + + it('multiple registrars', async () => { + const judgementTag = await sideBar.getJudgement('2 Reasonable'); + + await judgementTag.assertRegistrars(['BOB', ferdieShortAddress]); + }); + + it('opens clicked registrar in sidebar and closes popup', async () => { + const judgementTag = await sideBar.getJudgement('2 Reasonable'); + + await judgementTag.clickRegistrar('BOB'); + + expect(screen.queryByTestId('popup-window')).toBeFalsy(); + + await sideBar.assertAccountName('BOB'); + }); + }); + + afterEach(() => { + mockApiHooks.setJudgements([]); + }); }); }); }); diff --git a/packages/page-accounts/src/util.tsx b/packages/page-accounts/src/util.tsx index 90ebcfb221..edcd73c7ce 100644 --- a/packages/page-accounts/src/util.tsx +++ b/packages/page-accounts/src/util.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import type { ActionStatus } from '@polkadot/react-components/Status/types'; +import type { DisplayedJudgement } from '@polkadot/react-components/types'; import type { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces'; import type { CreateResult } from '@polkadot/ui-keyring/types'; import type { AccountBalance, SortedAccount } from './types'; @@ -113,3 +114,7 @@ export function sortAccounts (accountsList: SortedAccount[], accountsMap: Record ? 1 : -1); } + +export const getJudgementColor = (name: DisplayedJudgement): 'green' | 'red' => { + return (name === 'Erroneous' || name === 'Low quality') ? 'red' : 'green'; +}; diff --git a/packages/page-accounts/test/pages/accountsPage.tsx b/packages/page-accounts/test/pages/accountsPage.tsx index ad5a9936ba..75701fb107 100644 --- a/packages/page-accounts/test/pages/accountsPage.tsx +++ b/packages/page-accounts/test/pages/accountsPage.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { anAccount } from '@polkadot/test-support/creation/account'; import { Page } from '@polkadot/test-support/pages/Page'; +import { Sidebar } from '@polkadot/test-support/pagesElements/Sidebar'; import { AccountOverrides } from '@polkadot/test-support/types'; import { assertText, clickButton } from '@polkadot/test-support/utils/renderedScreenUtils'; @@ -74,6 +75,12 @@ export class AccountsPage extends Page { this.render(accounts); } + async openSidebarForRow (accountRowIndex: number): Promise { + const accountRows = await this.getAccountRows(); + + return accountRows[accountRowIndex].openSidebar(); + } + private async getSortCategory (categoryName: string): Promise { const sortByComponent = await this.getSortByComponent(); const availableCategories = await within(sortByComponent).findAllByRole('option'); diff --git a/packages/react-components/src/Popup/PopupWindow.tsx b/packages/react-components/src/Popup/PopupWindow.tsx index efd57500a3..7e7791a5ca 100644 --- a/packages/react-components/src/Popup/PopupWindow.tsx +++ b/packages/react-components/src/Popup/PopupWindow.tsx @@ -15,8 +15,9 @@ function PopupWindow ({ children, className = '', position, triggerRef, windowRe return createPortal(
{children}
, diff --git a/packages/react-components/src/Popup/index.tsx b/packages/react-components/src/Popup/index.tsx index a63a6f73d8..75bdb6fe55 100644 --- a/packages/react-components/src/Popup/index.tsx +++ b/packages/react-components/src/Popup/index.tsx @@ -3,7 +3,7 @@ import type { PopupProps } from './types'; -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import styled from 'styled-components'; import { Button } from '@polkadot/react-components/index'; @@ -11,13 +11,21 @@ import { useOutsideClick, useToggle } from '@polkadot/react-hooks'; import PopupWindow from './PopupWindow'; -function Popup ({ children, className = '', isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) { +function Popup ({ children, className = '', closeOnScroll = false, isDisabled = false, onCloseAction, position = 'left', value }: PopupProps) { const [isOpen, toggleIsOpen, setIsOpen] = useToggle(false); - const triggerRef = useRef(null); const dropdownRef = useRef(null); + const closeWindow = useCallback(() => setIsOpen(false), [setIsOpen]); - useOutsideClick([triggerRef, dropdownRef], () => setIsOpen(false)); + useOutsideClick([triggerRef, dropdownRef], closeWindow); + + useEffect(() => { + if (closeOnScroll) { + document.addEventListener('scroll', closeWindow, true); + } + + return () => document.removeEventListener('scroll', closeWindow, true); + }, [closeOnScroll, closeWindow, setIsOpen]); useEffect(() => { if (!isOpen && onCloseAction) { diff --git a/packages/react-components/src/Popup/types.ts b/packages/react-components/src/Popup/types.ts index ab9c69b018..ce261d6af5 100644 --- a/packages/react-components/src/Popup/types.ts +++ b/packages/react-components/src/Popup/types.ts @@ -17,6 +17,7 @@ export interface PopupWindowProps { export interface PopupProps { isDisabled?: boolean; className?: string; + closeOnScroll?: boolean; value?: React.ReactNode; children?: React.ReactNode; position?: HorizontalPosition; diff --git a/packages/react-components/src/types.ts b/packages/react-components/src/types.ts index f76cac43c0..544800b0fe 100644 --- a/packages/react-components/src/types.ts +++ b/packages/react-components/src/types.ts @@ -83,3 +83,5 @@ export interface ThemeProps { export type FlagColor = 'blue' | 'green' | 'grey' | 'orange' | 'pink' | 'red' | 'yellow' | 'theme'; export type AccountIdIsh = AccountId | AccountIndex | Address | string | Uint8Array | null; + +export type DisplayedJudgement = 'Erroneous' | 'Low quality' | 'Known good' | 'Reasonable'; diff --git a/packages/react-components/src/util/types.ts b/packages/react-components/src/util/types.ts index a9bd36ca23..22858d0e50 100644 --- a/packages/react-components/src/util/types.ts +++ b/packages/react-components/src/util/types.ts @@ -1,6 +1,10 @@ // Copyright 2017-2021 @polkadot/react-components authors & contributors // SPDX-License-Identifier: Apache-2.0 +import { RegistrarIndex } from '@polkadot/types/interfaces/identity/types'; + +import { DisplayedJudgement } from '../types'; + export interface DropdownOption { className?: string; key?: string; @@ -9,3 +13,4 @@ export interface DropdownOption { } export type DropdownOptions = DropdownOption[]; +export type SortedJudgements = ({ judgementName: DisplayedJudgement, registrarsIndexes: RegistrarIndex[] })[]; diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 680fa6b702..732faa415e 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -27,6 +27,7 @@ export { useIncrement } from './useIncrement'; export { useInflation } from './useInflation'; export { useIpfs } from './useIpfs'; export { useIsMountedRef } from './useIsMountedRef'; +export { useJudgements } from './useJudgements'; export { useLedger } from './useLedger'; export { useLoadingDelay } from './useLoadingDelay'; export { useMapEntries } from './useMapEntries'; diff --git a/packages/react-hooks/src/types.ts b/packages/react-hooks/src/types.ts index 79f1856a9b..ffcdc16d3a 100644 --- a/packages/react-hooks/src/types.ts +++ b/packages/react-hooks/src/types.ts @@ -3,6 +3,7 @@ import type { SubmittableExtrinsic } from '@polkadot/api/types'; import type { DeriveAccountFlags, DeriveAccountRegistration } from '@polkadot/api-derive/types'; +import type { DisplayedJudgement } from '@polkadot/react-components/types'; import type { AccountId, Balance, BlockNumber, Call, Exposure, Hash, RewardDestination, SessionIndex, StakingLedger, ValidatorPrefs } from '@polkadot/types/interfaces'; import type { IExtrinsic } from '@polkadot/types/types'; import type { KeyringJson$Meta } from '@polkadot/ui-keyring/types'; @@ -99,13 +100,8 @@ export interface AddressFlags extends DeriveAccountFlags { } export interface AddressIdentity extends DeriveAccountRegistration { - isGood: boolean; - isBad: boolean; - isKnownGood: boolean; - isReasonable: boolean; - isErroneous: boolean; - isLowQuality: boolean; isExistent: boolean; + isKnownGood: boolean; waitCount: number; } @@ -150,3 +146,15 @@ export interface StakerState { stashId: string; validatorPrefs?: ValidatorPrefs; } + +export interface Registrar { + address: string; + index: number; +} + +export interface Judgement { + judgementName: DisplayedJudgement; + registrars: (Registrar | undefined)[]; +} + +export type UseJudgements = Judgement[] diff --git a/packages/react-hooks/src/useAccountInfo.ts b/packages/react-hooks/src/useAccountInfo.ts index 9f0a34dbef..ffe4577189 100644 --- a/packages/react-hooks/src/useAccountInfo.ts +++ b/packages/react-hooks/src/useAccountInfo.ts @@ -100,19 +100,11 @@ export function useAccountInfo (value: string | null, isContract = false): UseAc if (identity) { const judgements = identity.judgements.filter(([, judgement]) => !judgement.isFeePaid); const isKnownGood = judgements.some(([, judgement]) => judgement.isKnownGood); - const isReasonable = judgements.some(([, judgement]) => judgement.isReasonable); - const isErroneous = judgements.some(([, judgement]) => judgement.isErroneous); - const isLowQuality = judgements.some(([, judgement]) => judgement.isLowQuality); setIdentity({ ...identity, - isBad: isErroneous || isLowQuality, - isErroneous, isExistent: !!identity.display, - isGood: isKnownGood || isReasonable, isKnownGood, - isLowQuality, - isReasonable, judgements, waitCount: identity.judgements.length - judgements.length }); diff --git a/packages/react-hooks/src/useJudgements.tsx b/packages/react-hooks/src/useJudgements.tsx new file mode 100644 index 0000000000..90ff206ed6 --- /dev/null +++ b/packages/react-hooks/src/useJudgements.tsx @@ -0,0 +1,21 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { useMemo } from 'react'; + +import { getJudgements } from './utils/getJudgements'; +import { matchRegistrarAccountsWithIndexes } from './utils/matchRegistrarAccountsWithIndexes'; +import { UseJudgements } from './types'; +import { useAccountInfo, useRegistrars } from './'; + +export function useJudgements (address: string): UseJudgements { + const { identity } = useAccountInfo(address); + const { registrars: allRegistrars } = useRegistrars(); + + const judgementsWithRegistrarIndexes = useMemo(() => getJudgements(identity), [identity]); + + return useMemo( + () => matchRegistrarAccountsWithIndexes(judgementsWithRegistrarIndexes, allRegistrars), + [allRegistrars, judgementsWithRegistrarIndexes] + ); +} diff --git a/packages/react-hooks/src/useRegistrars.ts b/packages/react-hooks/src/useRegistrars.ts index 2b59d54d78..2893e7cbad 100644 --- a/packages/react-hooks/src/useRegistrars.ts +++ b/packages/react-hooks/src/useRegistrars.ts @@ -3,6 +3,7 @@ import type { Option } from '@polkadot/types'; import type { RegistrarInfo } from '@polkadot/types/interfaces'; +import type { Registrar } from './types'; import { useMemo } from 'react'; @@ -15,11 +16,6 @@ interface RegistrarNull { index: number; } -interface Registrar { - address: string; - index: number; -} - interface State { isRegistrar: boolean; registrars: Registrar[]; @@ -29,7 +25,7 @@ interface State { export function useRegistrars (skipQuery?: boolean): State { const { api } = useApi(); const { allAccounts, hasAccounts } = useAccounts(); - const query = useCall[]>(!skipQuery && hasAccounts && api.query.identity?.registrars); + const query = useCall[]>(!skipQuery && api.query.identity?.registrars); // determine if we have a registrar or not - registrars are allowed to approve return useMemo( @@ -44,10 +40,10 @@ export function useRegistrars (skipQuery?: boolean): State { .filter((registrar): registrar is Registrar => !!registrar.address); return { - isRegistrar: registrars.some(({ address }) => allAccounts.includes(address)), + isRegistrar: hasAccounts && registrars.some(({ address }) => allAccounts.includes(address)), registrars }; }, - [allAccounts, query] + [allAccounts, hasAccounts, query] ); } diff --git a/packages/react-hooks/src/utils/getJudgements.ts b/packages/react-hooks/src/utils/getJudgements.ts new file mode 100644 index 0000000000..bae8c7dead --- /dev/null +++ b/packages/react-hooks/src/utils/getJudgements.ts @@ -0,0 +1,64 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { DeriveAccountRegistration } from '@polkadot/api-derive/types'; +import type { DisplayedJudgement } from '@polkadot/react-components/types'; +import type { SortedJudgements } from '@polkadot/react-components/util/types'; +import type { RegistrationJudgement } from '@polkadot/types/interfaces'; + +function extractIndexes (registrars: RegistrationJudgement[]) { + return registrars.map((judgement) => judgement[0]); +} + +export function getJudgements (identity: DeriveAccountRegistration | undefined): SortedJudgements { + if (!identity) return []; + + const judgements = groupJudgements(identity.judgements); + + const result = []; + + for (const name in judgements) { + const judgementName = name as DisplayedJudgement; + + if (judgements[judgementName].length !== 0) { + result.push({ judgementName, registrarsIndexes: extractIndexes(judgements[judgementName]) }); + } + } + + return result; +} + +export function groupJudgements (judgements: RegistrationJudgement[]): Record { + const consideredJudgements = filterConsideredJudgements(judgements); + const knownGoodJudgements = filterKnownGood(consideredJudgements); + const reasonableJudgements = filterReasonable(consideredJudgements); + const erroneousJudgements = filterErroneous(consideredJudgements); + const lowQualityJudgements = filterLowQuality(consideredJudgements); + + return { + Erroneous: erroneousJudgements, + 'Known good': knownGoodJudgements, + 'Low quality': lowQualityJudgements, + Reasonable: reasonableJudgements + }; +} + +const filterConsideredJudgements = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => { + return judgements.filter(([, judgement]) => !judgement.isFeePaid); +}; + +const filterKnownGood = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => { + return judgements.filter(([, judgement]) => judgement.isKnownGood); +}; + +const filterReasonable = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => { + return judgements.filter(([, judgement]) => judgement.isReasonable); +}; + +const filterErroneous = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => { + return judgements.filter(([, judgement]) => judgement.isErroneous); +}; + +const filterLowQuality = (judgements: RegistrationJudgement[]): RegistrationJudgement[] => { + return judgements.filter(([, judgement]) => judgement.isLowQuality); +}; diff --git a/packages/react-hooks/src/utils/matchRegistrarAccountsWithIndexes.ts b/packages/react-hooks/src/utils/matchRegistrarAccountsWithIndexes.ts new file mode 100644 index 0000000000..79219272d6 --- /dev/null +++ b/packages/react-hooks/src/utils/matchRegistrarAccountsWithIndexes.ts @@ -0,0 +1,19 @@ +// Copyright 2017-2021 @polkadot/react-hooks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { SortedJudgements } from '@polkadot/react-components/util/types'; +import type { Judgement, Registrar } from '../types'; + +export function matchRegistrarAccountsWithIndexes ( + judgementsWithRegistrarIndexes: SortedJudgements, + allRegistrars: Registrar[] +): Judgement[] { + return judgementsWithRegistrarIndexes.map(({ judgementName, registrarsIndexes }) => { + const findRegistrarByIndex = (index: number) => allRegistrars.find((registrar) => registrar.index === index); + + return { + judgementName, + registrars: registrarsIndexes.map((index) => findRegistrarByIndex(index.toNumber())) + }; + }); +} diff --git a/packages/test-support/src/mockData/registrations.ts b/packages/test-support/src/mockData/registrations.ts new file mode 100644 index 0000000000..e440a00eb1 --- /dev/null +++ b/packages/test-support/src/mockData/registrations.ts @@ -0,0 +1,47 @@ +// Copyright 2017-2021 @polkadot/test-supports authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Registrar } from '@polkadot/react-hooks/types'; + +import registry from '@polkadot/react-api/typeRegistry'; + +import { bob, charlie, ferdie } from '../keyring'; + +export const mockRegistration = { + judgements: [ + [ + registry.createType('RegistrarIndex', '0'), + { + isReasonable: true + } + ], + [ + registry.createType('RegistrarIndex', '1'), + { + isKnownGood: true + } + ], + [ + registry.createType('RegistrarIndex', '2'), + { + isErroneous: true + } + ], + [ + registry.createType('RegistrarIndex', '3'), + { + isReasonable: true + } + ] + ] +}; + +export const bobRegistrar: Registrar = { address: bob, index: 0 }; +export const charlieRegistrar: Registrar = { address: charlie, index: 1 }; +export const ferdieRegistrar: Registrar = { address: ferdie, index: 3 }; + +export const registrars: Registrar[] = [bobRegistrar, charlieRegistrar, ferdieRegistrar]; + +export const bobShortAddress = '5FHneW…M694ty'; +export const charlieShortAddress = '5DAAnr…3PTXFy'; +export const ferdieShortAddress = '5CiPPs…SK2DjL'; diff --git a/packages/test-support/src/pages/Page.tsx b/packages/test-support/src/pages/Page.tsx index 3142c5e5df..b6f3d0928f 100644 --- a/packages/test-support/src/pages/Page.tsx +++ b/packages/test-support/src/pages/Page.tsx @@ -18,7 +18,7 @@ import { mockApiHooks } from '@polkadot/test-support/utils/mockApiHooks'; import { TypeRegistry } from '@polkadot/types/create'; import { keyring } from '@polkadot/ui-keyring'; -import { alice, bob, charlie } from '../keyring'; +import { alice, bob, charlie, ferdie } from '../keyring'; import { Table } from '../pagesElements'; import { AccountOverrides, mockAccountHooks } from '../utils/accountDefaults'; @@ -43,7 +43,14 @@ jest.mock('@polkadot/react-hooks/useAccountInfo', () => { ? { ...actual.useAccountInfo(address), flags: { ...actual.useAccountInfo(address).flags, ...(mockInfo.info.flags) }, - identity: { ...actual.useAccountInfo(address).identity, ...(mockInfo.info.identity) }, + identity: { + ...actual.useAccountInfo(address).identity, + ...(mockInfo.info.identity), + judgements: [ + ...(actual.useAccountInfo(address).identity?.judgements || []), + ...(mockApiHooks.judgements || []) + ] + }, tags: [...actual.useAccountInfo(address).tags, ...(mockInfo.info.tags)] } : actual.useAccountInfo(address); @@ -71,9 +78,20 @@ jest.mock('@polkadot/react-hooks/useSubidentities', () => ({ useSubidentities: () => mockApiHooks.subs })); +jest.mock('@polkadot/react-hooks/useSubidentities', () => ({ + useSubidentities: () => mockApiHooks.subs +})); + +jest.mock('@polkadot/react-hooks/useRegistrars', () => ({ + useRegistrars: () => ({ + isRegistrar: false, + registrars: mockApiHooks.registrars + }) +})); + export abstract class Page { private renderResult?: RenderResult - protected readonly defaultAddresses = [alice, bob, charlie]; + protected readonly defaultAddresses = [alice, bob, charlie, ferdie]; protected constructor (private readonly overview: React.ReactElement, private readonly rowClassName: string) { this.overview = overview; @@ -97,6 +115,9 @@ export abstract class Page { balances: { all: noop }, + chain: { + bestNumber: noop + }, democracy: { locks: noop }, diff --git a/packages/test-support/src/pagesElements/JudgementTag.ts b/packages/test-support/src/pagesElements/JudgementTag.ts new file mode 100644 index 0000000000..dcbe527fec --- /dev/null +++ b/packages/test-support/src/pagesElements/JudgementTag.ts @@ -0,0 +1,36 @@ +// Copyright 2017-2021 @polkadot/test-supports authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { fireEvent, screen, within } from '@testing-library/react'; + +export class JudgementTag { + public judgementTag: HTMLElement + + constructor (judgementTag: HTMLElement) { + this.judgementTag = judgementTag; + } + + async assertRegistrars (expectedRegistrars: string[]): Promise { + const popup = await this.openPopup(); + + for (let index = 0; index < expectedRegistrars.length; index++) { + await within(popup).findByText(expectedRegistrars[index]); + } + } + + async clickRegistrar (registrarName: string): Promise { + const popup = await this.openPopup(); + + const registrars = await within(popup).findAllByTestId('account-name'); + + const registrar = registrars.find((reg) => reg.textContent === registrarName) ?? fail('Registrar not found'); + + fireEvent.click(registrar); + } + + private async openPopup (): Promise { + fireEvent.click(this.judgementTag); + + return screen.findByTestId('popup-window'); + } +} diff --git a/packages/test-support/src/pagesElements/Sidebar.ts b/packages/test-support/src/pagesElements/Sidebar.ts index ada865cba8..bab517b60b 100644 --- a/packages/test-support/src/pagesElements/Sidebar.ts +++ b/packages/test-support/src/pagesElements/Sidebar.ts @@ -1,8 +1,10 @@ -// Copyright 2017-2021 @polkadot/page-accounts authors & contributors +// Copyright 2017-2021 @polkadot/test-supports authors & contributors // SPDX-License-Identifier: Apache-2.0 import { fireEvent, screen, within } from '@testing-library/react'; +import { JudgementTag } from './JudgementTag'; + export class Sidebar { public sidebar: HTMLElement @@ -43,6 +45,12 @@ export class Sidebar { expect(sideBarName).toHaveTextContent(expectedAccountName); } + async assertJudgement (judgement: string): Promise { + const judgementsSection = await this.findByTestId('judgements'); + + expect(judgementsSection).toHaveTextContent(judgement); + } + async assertTags (tagsContent: string): Promise { const sideBarTags = await this.findByTestId('sidebar-tags'); @@ -124,6 +132,12 @@ export class Sidebar { return screen.findByTestId('modal'); } + async getJudgement (judgementName: string): Promise { + const judgements = await this.findByTestId('judgements'); + + return new JudgementTag(await within(judgements).findByText(judgementName)); + } + private clickButton (buttonName: string) { const button = this.getByRole('button', { name: buttonName }); diff --git a/packages/test-support/src/utils/mockApiHooks.ts b/packages/test-support/src/utils/mockApiHooks.ts index 9d26b404b6..7098dd50e9 100644 --- a/packages/test-support/src/utils/mockApiHooks.ts +++ b/packages/test-support/src/utils/mockApiHooks.ts @@ -1,11 +1,25 @@ // Copyright 2017-2021 @polkadot/page-accounts authors & contributors // SPDX-License-Identifier: Apache-2.0 +import type { Registrar } from '@polkadot/react-hooks/types'; +import type { RegistrationJudgement } from '@polkadot/types/interfaces'; + class MockApiHooks { - public subs: string[] | undefined = [] + public subs: string[] | undefined = []; + public judgements: RegistrationJudgement[] | undefined = []; + public registrars: Registrar[] = []; + public setSubs (subs: string[] | undefined) { this.subs = subs; } + + public setJudgements (judgements: RegistrationJudgement[] | undefined) { + this.judgements = judgements; + } + + public setRegistrars (registrars: Registrar[]) { + this.registrars = registrars; + } } export const mockApiHooks = new MockApiHooks();