Account sidebar - single button for editing name and tags (#6049)
* WIP - merge edition of account name and tags * [WIP] - tests * Apply test improvements * Improve tests * Improve tests and use useCallback * WIP - add cancel button * Outside click * Add more tests * Apply styling * Add opacity on not editable elements * Remove unnecessary code and fix type of sidebar ref * Disable eslint rule * Place edit button within account menu buttons * Adjust test setup * Ato-lint fixes * Update styles * Apply review suggestions * Remove unused styles and change type of sidebar ref * Remove onCancel from useAccountInfo hook * Fix issue with edit button * Apply review suggestions, remove tests * Adjust tests * Simplify tests * Apply lint fixes * Change layout of buttons Co-authored-by: MiZiet <michalzietalak@o2.pl> Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
parent
fb30cdd356
commit
ad8226a5c3
@ -32,6 +32,7 @@
|
||||
"Approve this call hash": "Approve this call hash",
|
||||
"Backup": "Backup",
|
||||
"Backup account": "Backup account",
|
||||
"Cancel": "Cancel",
|
||||
"Cancel this call hash": "Cancel this call hash",
|
||||
"Change": "Change",
|
||||
"Change account password": "Change account password",
|
||||
@ -55,6 +56,7 @@
|
||||
"Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use \"ed25519\".": "Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use \"ed25519\".",
|
||||
"Development": "Development",
|
||||
"Download": "Download",
|
||||
"Edit": "Edit",
|
||||
"Either approve or reject this call.": "Either approve or reject this call.",
|
||||
"Ensure that the extension has accounts, some accounts are visible globally and available for this chain and that you gave the application permission to access accounts from the extension to use them.": "Ensure that the extension has accounts, some accounts are visible globally and available for this chain and that you gave the application permission to access accounts from the extension to use them.",
|
||||
"Ensure that you utilized the \"Save\" functionality before using a generated address to receive funds. Without saving the address any funds and the associated seed any funds sent to it will be lost.": "Ensure that you utilized the \"Save\" functionality before using a generated address to receive funds. Without saving the address any funds and the associated seed any funds sent to it will be lost.",
|
||||
|
||||
@ -3,13 +3,13 @@
|
||||
|
||||
import type { Balance } from '@polkadot/types/interfaces';
|
||||
|
||||
import { fireEvent, screen, within } from '@testing-library/react';
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
|
||||
import BN from 'bn.js';
|
||||
|
||||
import { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types';
|
||||
import i18next from '@polkadot/react-components/i18n';
|
||||
import toShortAddress from '@polkadot/react-components/util/toShortAddress';
|
||||
import { UseAccountInfo } from '@polkadot/react-hooks/types';
|
||||
import { AddressFlags, UseAccountInfo } from '@polkadot/react-hooks/types';
|
||||
import { balanceOf } from '@polkadot/test-support/creation/balance';
|
||||
import { makeStakingLedger as ledger } from '@polkadot/test-support/creation/stakingInfo/stakingLedger';
|
||||
import { MemoryStore } from '@polkadot/test-support/keyring';
|
||||
@ -17,6 +17,8 @@ import { keyring } from '@polkadot/ui-keyring';
|
||||
import { KeyringJson$Meta } from '@polkadot/ui-keyring/types';
|
||||
|
||||
import { AccountOverrides, Override } from '../../test/hooks/default';
|
||||
import { AccountRow } from '../../test/pageElements/AccountRow';
|
||||
import { Sidebar } from '../../test/pageElements/Sidebar';
|
||||
import { AccountsPage, format } from '../../test/pages/accountsPage';
|
||||
|
||||
describe('Accounts page', () => {
|
||||
@ -374,6 +376,121 @@ describe('Accounts page', () => {
|
||||
await accountsPage.checkOrderAndRowsColoring([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidebar', () => {
|
||||
const initialName = 'INITIAL_NAME';
|
||||
const newName = 'NEW_NAME';
|
||||
const defaultTag = 'Default';
|
||||
const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]';
|
||||
|
||||
let accountRows: AccountRow[];
|
||||
let sideBar: Sidebar;
|
||||
|
||||
describe('changes name', () => {
|
||||
beforeEach(async () => {
|
||||
accountsPage.renderPage([[aliceAddress, anAccountWithMeta({ isDevelopment: false, name: initialName })]]);
|
||||
sideBar = await openSidebarForAccountRow(0);
|
||||
await sideBar.changeAccountName(newName);
|
||||
});
|
||||
|
||||
it('within keyring', () => {
|
||||
const changedAccount = keyring.getAccount(aliceAddress);
|
||||
|
||||
expect(changedAccount?.meta?.name).toEqual(newName);
|
||||
});
|
||||
|
||||
it('within sidebar', async () => {
|
||||
await sideBar.assertAccountName(newName);
|
||||
});
|
||||
|
||||
it('within account row', async () => {
|
||||
await accountRows[0].assertAccountName(newName);
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot be edited if edit button has not been pressed', async () => {
|
||||
await sideBar.clickByText('no tags');
|
||||
expect(sideBar.queryByRole('combobox')).toBeFalsy();
|
||||
|
||||
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
|
||||
});
|
||||
|
||||
it('when isEditable is false account name is not editable', async () => {
|
||||
renderAccountsWithDefaultAddresses(
|
||||
anAccountWithInfo({ flags: { isEditable: false } as AddressFlags })
|
||||
);
|
||||
sideBar = await openSidebarForAccountRow(0);
|
||||
sideBar.edit();
|
||||
await expect(sideBar.typeAccountName(newName)).rejects.toThrowError(nameInputNotFoundError);
|
||||
});
|
||||
|
||||
describe('on edit cancel', () => {
|
||||
beforeEach(async () => {
|
||||
accountsPage.renderPage([[aliceAddress, anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })]]);
|
||||
|
||||
sideBar = await openSidebarForAccountRow(0);
|
||||
await sideBar.assertTags('no tags');
|
||||
sideBar.edit();
|
||||
});
|
||||
|
||||
it('restores tags and name to state from keyring', async () => {
|
||||
await sideBar.typeAccountName(newName);
|
||||
await sideBar.selectTag(defaultTag);
|
||||
|
||||
sideBar.cancel();
|
||||
await sideBar.assertTags('no tags');
|
||||
await sideBar.assertAccountName(initialName);
|
||||
});
|
||||
|
||||
it('Cancel button disappears', () => {
|
||||
sideBar.cancel();
|
||||
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('outside click', () => {
|
||||
beforeEach(async () => {
|
||||
renderAccountsWithDefaultAddresses(
|
||||
anAccountWithMeta({ name: 'alice' }),
|
||||
anAccountWithMeta({ name: 'bob' })
|
||||
);
|
||||
|
||||
sideBar = await openSidebarForAccountRow(0);
|
||||
sideBar.edit();
|
||||
});
|
||||
|
||||
it('cancels editing', async () => {
|
||||
await sideBar.typeAccountName(newName);
|
||||
await sideBar.selectTag(defaultTag);
|
||||
|
||||
fireEvent.click(await screen.findByText('accounts'));
|
||||
|
||||
await sideBar.assertTags('no tags');
|
||||
await sideBar.assertAccountName('ALICE');
|
||||
|
||||
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('within sidebar does not cancel editing', async () => {
|
||||
await sideBar.clickByText('Tags');
|
||||
|
||||
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('cancels editing and changes name when opening sidebar for another account', async () => {
|
||||
await waitFor(() => sideBar.assertAccountInput('alice'));
|
||||
|
||||
sideBar = await openSidebarForAccountRow(1);
|
||||
await sideBar.assertAccountName('BOB');
|
||||
});
|
||||
});
|
||||
|
||||
async function openSidebarForAccountRow (rowIndex: number) {
|
||||
accountRows = await accountsPage.findAccountRows();
|
||||
|
||||
return accountRows[rowIndex].openSidebar();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const anAccount = (): AccountOverrides => ({});
|
||||
|
||||
136
packages/page-accounts/src/Sidebar/AccountMenuButtons.tsx
Normal file
136
packages/page-accounts/src/Sidebar/AccountMenuButtons.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import Transfer from '@polkadot/app-accounts/modals/Transfer';
|
||||
import { useTranslation } from '@polkadot/app-accounts/translate';
|
||||
import { Button } from '@polkadot/react-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
import { AddressFlags } from '@polkadot/react-hooks/types';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
flags: AddressFlags;
|
||||
isEditingName: boolean;
|
||||
isEditing: boolean;
|
||||
toggleIsEditingName: () => void;
|
||||
toggleIsEditingTags: () => void;
|
||||
onCancel: () => void;
|
||||
onSaveName: () => void;
|
||||
onSaveTags: () => void;
|
||||
onForgetAddress: () => void;
|
||||
onUpdateName: () => void;
|
||||
recipientId: string;
|
||||
}
|
||||
|
||||
function AccountMenuButtons ({ className = '', flags, isEditing, isEditingName, onCancel, onForgetAddress, onSaveName, onSaveTags, onUpdateName, recipientId, toggleIsEditingName, toggleIsEditingTags }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [isTransferOpen, toggleIsTransferOpen] = useToggle();
|
||||
|
||||
const _onForgetAddress = useCallback(
|
||||
(): void => {
|
||||
onForgetAddress();
|
||||
onUpdateName && onUpdateName();
|
||||
},
|
||||
[onForgetAddress, onUpdateName]
|
||||
);
|
||||
|
||||
const toggleIsEditing = useCallback(() => {
|
||||
flags.isEditable && toggleIsEditingName();
|
||||
toggleIsEditingTags();
|
||||
}, [flags.isEditable, toggleIsEditingName, toggleIsEditingTags]);
|
||||
|
||||
const _onUpdateName = useCallback(
|
||||
(): void => {
|
||||
onSaveName();
|
||||
onUpdateName && onUpdateName();
|
||||
},
|
||||
[onSaveName, onUpdateName]
|
||||
);
|
||||
|
||||
const updateName = useCallback(() => {
|
||||
if (isEditingName && (flags.isInContacts || flags.isOwned)) {
|
||||
_onUpdateName();
|
||||
toggleIsEditingName();
|
||||
}
|
||||
}, [isEditingName, flags.isInContacts, flags.isOwned, _onUpdateName, toggleIsEditingName]);
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
if (isEditing) {
|
||||
updateName();
|
||||
onSaveTags();
|
||||
}
|
||||
|
||||
toggleIsEditing();
|
||||
}, [isEditing, toggleIsEditing, updateName, onSaveTags]);
|
||||
|
||||
return (
|
||||
<div className={`${className} ui--AddressMenu-buttons`}>
|
||||
{isEditing
|
||||
? (
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='times'
|
||||
label={t<string>('Cancel')}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
<Button
|
||||
icon='save'
|
||||
label={t<string>('Save')}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</Button.Group>
|
||||
)
|
||||
: (
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='paper-plane'
|
||||
isDisabled={isEditing}
|
||||
label={t<string>('Send')}
|
||||
onClick={toggleIsTransferOpen}
|
||||
/>
|
||||
{!flags.isOwned && !flags.isInContacts && (
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={isEditing}
|
||||
label={t<string>('Save')}
|
||||
onClick={_onUpdateName}
|
||||
/>
|
||||
)}
|
||||
{!flags.isOwned && flags.isInContacts && (
|
||||
<Button
|
||||
icon='ban'
|
||||
isDisabled={isEditing}
|
||||
label={t<string>('Remove')}
|
||||
onClick={_onForgetAddress}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon='edit'
|
||||
label={t<string>('Edit')}
|
||||
onClick={onEdit}
|
||||
/>
|
||||
</Button.Group>)}
|
||||
{isTransferOpen && (
|
||||
<Transfer
|
||||
key='modal-transfer'
|
||||
onClose={toggleIsTransferOpen}
|
||||
recipientId={recipientId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(AccountMenuButtons)`
|
||||
width: 100%;
|
||||
|
||||
.ui--Button-Group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`);
|
||||
@ -1,55 +1,44 @@
|
||||
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AccountName, Button, Icon, IdentityIcon, Input, LinkExternal, Sidebar, Tags } from '@polkadot/react-components';
|
||||
import SidebarEditableSection from '@polkadot/app-accounts/Sidebar/SidebarEditableSection';
|
||||
import { IdentityIcon, LinkExternal, Sidebar } from '@polkadot/react-components';
|
||||
import { colorLink } from '@polkadot/react-components/styles/theme';
|
||||
import { useAccountInfo, useToggle } from '@polkadot/react-hooks';
|
||||
import { useAccountInfo } from '@polkadot/react-hooks';
|
||||
|
||||
import Transfer from '../modals/Transfer';
|
||||
import { useTranslation } from '../translate';
|
||||
import Balances from './Balances';
|
||||
import Flags from './Flags';
|
||||
import Identity from './Identity';
|
||||
import Multisig from './Multisig';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
className?: string;
|
||||
dataTestId?: string;
|
||||
onClose: () => void;
|
||||
onUpdateName: () => void;
|
||||
}
|
||||
|
||||
function FullSidebar ({ address, className = '', onClose, onUpdateName }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { accountIndex, flags, identity, isEditingName, isEditingTags, meta, name, onForgetAddress, onSaveName, onSaveTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags } = useAccountInfo(address);
|
||||
const [isTransferOpen, toggleIsTransferOpen] = useToggle();
|
||||
function FullSidebar ({ address, className = '', dataTestId, onClose, onUpdateName }: Props): React.ReactElement<Props> {
|
||||
const [inEditMode, setInEditMode] = useState<boolean>(false);
|
||||
const { accountIndex, flags, identity, meta } = useAccountInfo(address);
|
||||
|
||||
const _onForgetAddress = useCallback(
|
||||
(): void => {
|
||||
onForgetAddress();
|
||||
onUpdateName && onUpdateName();
|
||||
},
|
||||
[onForgetAddress, onUpdateName]
|
||||
);
|
||||
|
||||
const _onUpdateName = useCallback(
|
||||
(): void => {
|
||||
onSaveName();
|
||||
onUpdateName && onUpdateName();
|
||||
},
|
||||
[onSaveName, onUpdateName]
|
||||
);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className={className}
|
||||
className={`${className}${inEditMode ? ' inEditMode' : ''}`}
|
||||
dataTestId={dataTestId}
|
||||
onClose={onClose}
|
||||
position='right'
|
||||
sidebarRef={ref}
|
||||
>
|
||||
<div className='ui--AddressMenu-header'>
|
||||
<div
|
||||
className='ui--AddressMenu-header'
|
||||
data-testid='sidebar-address-menu'
|
||||
>
|
||||
<IdentityIcon
|
||||
size={80}
|
||||
value={address}
|
||||
@ -62,84 +51,12 @@ function FullSidebar ({ address, className = '', onClose, onUpdateName }: Props)
|
||||
{accountIndex}
|
||||
</div>
|
||||
)}
|
||||
<AccountName
|
||||
onClick={(flags.isEditable && !isEditingName) ? toggleIsEditingName : undefined}
|
||||
override={
|
||||
isEditingName
|
||||
? (
|
||||
<Input
|
||||
autoFocus
|
||||
className='name--input'
|
||||
defaultValue={name}
|
||||
onBlur={(flags.isInContacts || flags.isOwned) ? _onUpdateName : undefined}
|
||||
onChange={setName}
|
||||
withLabel={false}
|
||||
/>
|
||||
)
|
||||
: flags.isEditable
|
||||
? (name.toUpperCase() || t<string>('<unknown>'))
|
||||
: undefined
|
||||
}
|
||||
value={address}
|
||||
withSidebar={false}
|
||||
>
|
||||
{(!isEditingName && flags.isEditable) && (
|
||||
<Icon
|
||||
className='inline-icon'
|
||||
icon='edit'
|
||||
/>
|
||||
)}
|
||||
</AccountName>
|
||||
<div className='ui--AddressMenu-tags'>
|
||||
<Tags
|
||||
isEditable
|
||||
isEditing={isEditingTags}
|
||||
onChange={setTags}
|
||||
onSave={onSaveTags}
|
||||
onToggleIsEditing={toggleIsEditingTags}
|
||||
size='tiny'
|
||||
value={tags}
|
||||
withTitle
|
||||
/>
|
||||
</div>
|
||||
<Flags flags={flags} />
|
||||
<div className='ui--AddressMenu-buttons'>
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='paper-plane'
|
||||
label={t<string>('Send')}
|
||||
onClick={toggleIsTransferOpen}
|
||||
/>
|
||||
{flags.isOwned && (
|
||||
<Button
|
||||
icon='check'
|
||||
isBasic
|
||||
label={t<string>('Owned')}
|
||||
/>
|
||||
)}
|
||||
{!flags.isOwned && !flags.isInContacts && (
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t<string>('Save')}
|
||||
onClick={_onUpdateName}
|
||||
/>
|
||||
)}
|
||||
{!flags.isOwned && flags.isInContacts && (
|
||||
<Button
|
||||
icon='ban'
|
||||
label={t<string>('Remove')}
|
||||
onClick={_onForgetAddress}
|
||||
/>
|
||||
)}
|
||||
</Button.Group>
|
||||
{isTransferOpen && (
|
||||
<Transfer
|
||||
key='modal-transfer'
|
||||
onClose={toggleIsTransferOpen}
|
||||
recipientId={address}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<SidebarEditableSection
|
||||
address={address}
|
||||
isBeingEdited={setInEditMode}
|
||||
onUpdateName={onUpdateName}
|
||||
sidebarRef={ref}
|
||||
/>
|
||||
</div>
|
||||
<Balances address={address} />
|
||||
<Identity
|
||||
@ -288,12 +205,13 @@ export default React.memo(styled(FullSidebar)`
|
||||
margin: 0 !important;
|
||||
|
||||
> input {
|
||||
padding: 0 !important;
|
||||
background: rgba(230, 230, 230, 0.8) !important;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: 0 3px 3px rgba(0,0,0,.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.inEditMode {
|
||||
.ui--AddressMenu-flags {
|
||||
opacity: 60%;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import AccountMenuButtons from '@polkadot/app-accounts/Sidebar/AccountMenuButtons';
|
||||
import Flags from '@polkadot/app-accounts/Sidebar/Flags';
|
||||
import { useTranslation } from '@polkadot/app-accounts/translate';
|
||||
import { AccountName, Input, Tags } from '@polkadot/react-components';
|
||||
import { useAccountInfo, useOutsideClick } from '@polkadot/react-hooks';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
isBeingEdited: (arg: boolean) => void;
|
||||
onUpdateName: () => void;
|
||||
sidebarRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function SidebarEditableSection ({ address, isBeingEdited, onUpdateName, sidebarRef }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { flags, isEditing, isEditingName, isEditingTags, name, onForgetAddress, onSaveName, onSaveTags, setIsEditingName, setIsEditingTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags } = useAccountInfo(address);
|
||||
|
||||
useEffect(() => {
|
||||
isBeingEdited(isEditing());
|
||||
}, [isBeingEdited, isEditing]);
|
||||
|
||||
const onCancel = useCallback(
|
||||
(): void => {
|
||||
if (isEditing()) {
|
||||
try {
|
||||
const accountOrAddress = keyring.getAccount(address) || keyring.getAddress(address);
|
||||
|
||||
setName(accountOrAddress?.meta.name || '');
|
||||
setTags(accountOrAddress?.meta.tags ? (accountOrAddress.meta.tags as string[]).sort() : []);
|
||||
setIsEditingName(false);
|
||||
setIsEditingTags(false);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}, [isEditing, setName, setTags, setIsEditingName, setIsEditingTags, address]);
|
||||
|
||||
useOutsideClick([sidebarRef], onCancel);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountName
|
||||
override={
|
||||
isEditingName
|
||||
? (
|
||||
<Input
|
||||
className='name--input'
|
||||
defaultValue={name}
|
||||
label='name-input'
|
||||
onChange={setName}
|
||||
withLabel={false}
|
||||
/>
|
||||
)
|
||||
: flags.isEditable
|
||||
? (name.toUpperCase() || t<string>('<unknown>'))
|
||||
: undefined
|
||||
}
|
||||
value={address}
|
||||
withSidebar={false}
|
||||
/>
|
||||
<div
|
||||
className='ui--AddressMenu-tags'
|
||||
data-testid='sidebar-tags'
|
||||
>
|
||||
<Tags
|
||||
isEditable
|
||||
isEditing={isEditingTags}
|
||||
onChange={setTags}
|
||||
size='tiny'
|
||||
value={tags}
|
||||
withEditButton={false}
|
||||
withTitle
|
||||
/>
|
||||
</div>
|
||||
<Flags flags={flags} />
|
||||
<AccountMenuButtons
|
||||
flags={flags}
|
||||
isEditing={isEditing()}
|
||||
isEditingName={isEditingName}
|
||||
onCancel={onCancel}
|
||||
onForgetAddress={onForgetAddress}
|
||||
onSaveName={onSaveName}
|
||||
onSaveTags={onSaveTags}
|
||||
onUpdateName={onUpdateName}
|
||||
recipientId={address}
|
||||
toggleIsEditingName={toggleIsEditingName}
|
||||
toggleIsEditingTags={toggleIsEditingTags}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(SidebarEditableSection);
|
||||
@ -29,6 +29,7 @@ function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
|
||||
{address && (
|
||||
<Sidebar
|
||||
address={address}
|
||||
dataTestId='account-sidebar'
|
||||
onClose={onClose}
|
||||
onUpdateName={onUpdateName}
|
||||
/>
|
||||
|
||||
70
packages/page-accounts/test/pageElements/AccountRow.ts
Normal file
70
packages/page-accounts/test/pageElements/AccountRow.ts
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent, screen, within } from '@testing-library/react';
|
||||
|
||||
import { Balance } from '@polkadot/types/interfaces';
|
||||
|
||||
import { format } from '../pages/accountsPage';
|
||||
import { Sidebar } from './Sidebar';
|
||||
|
||||
// utility wrapper over an account item in accounts table, serves basic assertions about an account row
|
||||
|
||||
export class AccountRow {
|
||||
public primaryRow: HTMLElement;
|
||||
public detailsRow: HTMLElement;
|
||||
|
||||
constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) {
|
||||
this.primaryRow = primaryRow;
|
||||
this.detailsRow = detailsRow;
|
||||
}
|
||||
|
||||
async assertBalancesTotal (expected: Balance): Promise<void> {
|
||||
const balanceActual = await within(this.primaryRow).findByTestId('balance-summary');
|
||||
const balanceExpected = format(expected);
|
||||
|
||||
expect(balanceActual).toHaveTextContent(balanceExpected);
|
||||
}
|
||||
|
||||
async assertBalancesDetails (expected: {name: string, amount: Balance}[]): Promise<void> {
|
||||
for (const expectedBalanceDetailsItem of expected) {
|
||||
const labelElement = await within(this.detailsRow).findByText(expectedBalanceDetailsItem.name);
|
||||
const balanceElement = labelElement.nextSibling;
|
||||
const amount = format(expectedBalanceDetailsItem.amount);
|
||||
|
||||
expect(balanceElement).toHaveTextContent(amount);
|
||||
}
|
||||
}
|
||||
|
||||
async assertAccountName (expectedName: string): Promise<void> {
|
||||
const accountName = await within(this.primaryRow).findByTestId('account-name');
|
||||
|
||||
expect(accountName).toHaveTextContent(expectedName);
|
||||
}
|
||||
|
||||
async assertParentAccountName (expectedParentAccount: string): Promise<void> {
|
||||
const parentAccount = await within(this.primaryRow).findByTestId('parent');
|
||||
|
||||
expect(parentAccount).toHaveTextContent(expectedParentAccount);
|
||||
}
|
||||
|
||||
async assertTags (tagsContent: string): Promise<void> {
|
||||
const tagsActual = await within(this.detailsRow).findByTestId('tags');
|
||||
|
||||
expect(tagsActual).toHaveTextContent(tagsContent);
|
||||
}
|
||||
|
||||
async assertShortAddress (expectedShortAddress: string): Promise<void> {
|
||||
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
|
||||
|
||||
expect(actualShortAddress).toHaveTextContent(expectedShortAddress);
|
||||
}
|
||||
|
||||
async openSidebar (): Promise<Sidebar> {
|
||||
const accountName = await within(this.primaryRow).findByTestId('account-name');
|
||||
|
||||
fireEvent.click(accountName);
|
||||
|
||||
return new Sidebar(await screen.findByTestId('account-sidebar'));
|
||||
}
|
||||
}
|
||||
124
packages/page-accounts/test/pageElements/Sidebar.ts
Normal file
124
packages/page-accounts/test/pageElements/Sidebar.ts
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright 2017-2021 @polkadot/page-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { fireEvent, within } from '@testing-library/react';
|
||||
|
||||
export class Sidebar {
|
||||
public sidebar: HTMLElement
|
||||
|
||||
constructor (sidebar: HTMLElement) {
|
||||
this.sidebar = sidebar;
|
||||
}
|
||||
|
||||
async changeAccountName (accountName: string): Promise<void> {
|
||||
this.edit();
|
||||
await this.typeAccountName(accountName);
|
||||
this.save();
|
||||
}
|
||||
|
||||
async typeAccountName (accountName: string): Promise<void> {
|
||||
const accountNameInput = await this.findByTestId('name-input');
|
||||
|
||||
fireEvent.change(accountNameInput, { target: { value: accountName } });
|
||||
}
|
||||
|
||||
async selectTag (tagName: string): Promise<void> {
|
||||
const tagsCombobox = this.openTagsDropdown();
|
||||
const tagOptions = await within(tagsCombobox).findAllByRole('option');
|
||||
const tag = tagOptions.find((tag) => tag.textContent === tagName) as HTMLElement;
|
||||
|
||||
fireEvent.click(tag);
|
||||
}
|
||||
|
||||
async assertAccountInput (expectedInput: string): Promise<void> {
|
||||
const sideBarName = await this.findByTestId('name-input');
|
||||
|
||||
expect(sideBarName).toHaveProperty('value', expectedInput);
|
||||
}
|
||||
|
||||
async assertAccountName (expectedAccountName: string): Promise<void> {
|
||||
const sideBarName = await this.findByTestId('account-name');
|
||||
|
||||
expect(sideBarName).toHaveTextContent(expectedAccountName);
|
||||
}
|
||||
|
||||
async assertTags (tagsContent: string): Promise<void> {
|
||||
const sideBarTags = await this.findByTestId('sidebar-tags');
|
||||
|
||||
expect(sideBarTags).toHaveTextContent(tagsContent);
|
||||
}
|
||||
|
||||
close (): Promise<void> {
|
||||
return this.clickByTestId('close-sidebar-button');
|
||||
}
|
||||
|
||||
cancel (): void {
|
||||
this.clickButton('Cancel');
|
||||
}
|
||||
|
||||
edit (): void {
|
||||
this.clickButton('Edit');
|
||||
}
|
||||
|
||||
save (): void {
|
||||
this.clickButton('Save');
|
||||
}
|
||||
|
||||
async clickByText (text: string): Promise<void> {
|
||||
const htmlElement = await this.findByText(text);
|
||||
|
||||
fireEvent.click(htmlElement);
|
||||
}
|
||||
|
||||
async clickByTestId (testId: string): Promise<void> {
|
||||
const htmlElement = await this.findByTestId(testId);
|
||||
|
||||
fireEvent.click(htmlElement);
|
||||
}
|
||||
|
||||
async findByText (text: string): Promise<HTMLElement> {
|
||||
return within(this.sidebar).findByText(text);
|
||||
}
|
||||
|
||||
async findByTestId (testId: string): Promise<HTMLElement> {
|
||||
return within(this.sidebar).findByTestId(testId);
|
||||
}
|
||||
|
||||
async findByRole (role: string): Promise<HTMLElement> {
|
||||
return within(this.sidebar).findByRole(role);
|
||||
}
|
||||
|
||||
async findAllByRole (role: string): Promise<HTMLElement[]> {
|
||||
return within(this.sidebar).findAllByRole(role);
|
||||
}
|
||||
|
||||
getByTestId (testId: string): HTMLElement {
|
||||
return within(this.sidebar).getByTestId(testId);
|
||||
}
|
||||
|
||||
getByRole (roleName: string, options?: Record<string, unknown>): HTMLElement {
|
||||
return within(this.sidebar).getByRole(roleName, options);
|
||||
}
|
||||
|
||||
queryByRole (roleName: string, options?: Record<string, unknown>): HTMLElement | null {
|
||||
return within(this.sidebar).queryByRole(roleName, options);
|
||||
}
|
||||
|
||||
queryByTestId (testId: string): HTMLElement | null {
|
||||
return within(this.sidebar).queryByTestId(testId);
|
||||
}
|
||||
|
||||
private clickButton (buttonName: string) {
|
||||
const saveButton = this.getByRole('button', { name: buttonName });
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
}
|
||||
|
||||
private openTagsDropdown (): HTMLElement {
|
||||
const tagsDropdown = this.getByRole('combobox', { expanded: false });
|
||||
|
||||
fireEvent.click(tagsDropdown);
|
||||
|
||||
return tagsDropdown;
|
||||
}
|
||||
}
|
||||
@ -8,18 +8,21 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { balance } from '@polkadot/app-accounts/Accounts/index.spec';
|
||||
import AccountSidebar from '@polkadot/app-accounts/Sidebar';
|
||||
import { lightTheme } from '@polkadot/apps/themes';
|
||||
import { POLKADOT_GENESIS } from '@polkadot/apps-config';
|
||||
import { ApiContext } from '@polkadot/react-api';
|
||||
import { ApiProps } from '@polkadot/react-api/types';
|
||||
import { QueueProvider } from '@polkadot/react-components/Status/Context';
|
||||
import { PartialQueueTxExtrinsic, QueueProps, QueueTxExtrinsicAdd } from '@polkadot/react-components/Status/types';
|
||||
import { UseAccountInfo } from '@polkadot/react-hooks/types';
|
||||
import { TypeRegistry } from '@polkadot/types/create';
|
||||
import { Balance, BlockNumber } from '@polkadot/types/interfaces';
|
||||
import { keyring } from '@polkadot/ui-keyring';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import { AccountOverrides, mockAccountHooks } from '../hooks/default';
|
||||
import { AccountRow } from '../pageElements/AccountRow';
|
||||
import Overview from '../pages/../../src/Accounts/index';
|
||||
|
||||
let queueExtrinsic: (value: PartialQueueTxExtrinsic) => void;
|
||||
@ -31,60 +34,10 @@ function noop (): void {
|
||||
class NotYetRendered extends Error {
|
||||
}
|
||||
|
||||
// utility wrapper over an account item in accounts table, serves basic assertions about an account row
|
||||
export class AccountRow {
|
||||
public primaryRow: HTMLElement;
|
||||
public detailsRow: HTMLElement;
|
||||
|
||||
constructor (primaryRow: HTMLElement, detailsRow: HTMLElement) {
|
||||
this.primaryRow = primaryRow;
|
||||
this.detailsRow = detailsRow;
|
||||
}
|
||||
|
||||
async assertBalancesTotal (expected: Balance): Promise<void> {
|
||||
const balanceActual = await within(this.primaryRow).findByTestId('balance-summary');
|
||||
const balanceExpected = format(expected);
|
||||
|
||||
expect(balanceActual).toHaveTextContent(balanceExpected);
|
||||
}
|
||||
|
||||
async assertBalancesDetails (expected: {name: string, amount: Balance}[]): Promise<void> {
|
||||
for (const expectedBalanceDetailsItem of expected) {
|
||||
const labelElement = await within(this.detailsRow).findByText(expectedBalanceDetailsItem.name);
|
||||
const balanceElement = labelElement.nextSibling;
|
||||
const amount = format(expectedBalanceDetailsItem.amount);
|
||||
|
||||
expect(balanceElement).toHaveTextContent(amount);
|
||||
}
|
||||
}
|
||||
|
||||
async assertParentAccountName (expectedParentAccount: string): Promise<void> {
|
||||
const parentAccount = await within(this.primaryRow).findByTestId('parent');
|
||||
|
||||
expect(parentAccount).toHaveTextContent(expectedParentAccount);
|
||||
}
|
||||
|
||||
async assertTags (tagsContent: string): Promise<void> {
|
||||
const tagsActual = await within(this.detailsRow).findByTestId('tags');
|
||||
|
||||
expect(tagsActual).toHaveTextContent(tagsContent);
|
||||
}
|
||||
|
||||
async assertShortAddress (expectedShortAddress: string): Promise<void> {
|
||||
const actualShortAddress = await within(this.primaryRow).findByTestId('short-address');
|
||||
|
||||
expect(actualShortAddress).toHaveTextContent(expectedShortAddress);
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useAccounts', () => ({
|
||||
useAccounts: () => mockAccountHooks.useAccounts
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useAccountInfo', () => ({
|
||||
useAccountInfo: (address: string) => mockAccountHooks.accountsMap[address]?.info || {}
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useLoadingDelay', () => ({
|
||||
useLoadingDelay: () => false
|
||||
}));
|
||||
@ -101,6 +54,25 @@ jest.mock('@polkadot/react-hooks/useBestNumber', () => ({
|
||||
useBestNumber: () => 1
|
||||
}));
|
||||
|
||||
jest.mock('@polkadot/react-hooks/useAccountInfo', () => {
|
||||
// eslint-disable-next-line func-call-spacing
|
||||
const actual = jest.requireActual<{useAccountInfo: (address: string) => UseAccountInfo}>('@polkadot/react-hooks/useAccountInfo');
|
||||
|
||||
return ({
|
||||
useAccountInfo: (address: string) => {
|
||||
const mockInfo = mockAccountHooks.accountsMap[address];
|
||||
|
||||
return mockInfo
|
||||
? {
|
||||
...actual.useAccountInfo(address),
|
||||
flags: { ...actual.useAccountInfo(address).flags, ...(mockInfo.info.flags) },
|
||||
tags: [...actual.useAccountInfo(address).tags, ...(mockInfo.info.tags)]
|
||||
}
|
||||
: actual.useAccountInfo(address);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export class AccountsPage {
|
||||
private renderResult?: RenderResult
|
||||
|
||||
@ -115,8 +87,7 @@ export class AccountsPage {
|
||||
api: {
|
||||
derive: {
|
||||
accounts: {
|
||||
info: () => Promise.resolve(() => { /**/
|
||||
})
|
||||
info: () => { /**/ }
|
||||
},
|
||||
balances: {
|
||||
all: () => ({
|
||||
@ -155,7 +126,9 @@ export class AccountsPage {
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<ApiContext.Provider value={mockApi}>
|
||||
<Overview onStatusChange={noop} />
|
||||
<AccountSidebar>
|
||||
<Overview onStatusChange={noop} />
|
||||
</AccountSidebar>
|
||||
</ApiContext.Provider>
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
|
||||
@ -171,6 +171,7 @@ function AccountName ({ children, className = '', defaultName, label, onClick, o
|
||||
return (
|
||||
<div
|
||||
className={`ui--AccountName${withSidebar ? ' withSidebar' : ''} ${className}`}
|
||||
data-testid='account-name'
|
||||
onClick={
|
||||
withSidebar
|
||||
? _onToggleSidebar
|
||||
|
||||
@ -42,7 +42,7 @@ function Button ({ activeOnEnter, children, className = '', dataTestId = '', ico
|
||||
onMouseLeave={onMouseLeave}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
{icon && <Icon icon={icon} />}
|
||||
{label}
|
||||
{children}
|
||||
<Spinner
|
||||
|
||||
@ -11,7 +11,7 @@ export interface ButtonProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
dataTestId?: string;
|
||||
icon: IconName;
|
||||
icon?: IconName;
|
||||
isBasic?: boolean;
|
||||
isBusy?: boolean;
|
||||
isCircular?: boolean;
|
||||
|
||||
@ -24,9 +24,16 @@ interface Props {
|
||||
library.add(fas);
|
||||
|
||||
function Icon ({ className = '', color = 'normal', icon, isPadded, isSpinning, onClick, size = '1x', tooltip }: Props): React.ReactElement<Props> {
|
||||
const extraProps = tooltip
|
||||
? { 'data-for': tooltip, 'data-testid': icon, 'data-tip': true }
|
||||
: {};
|
||||
const extraProps: Record<string, unknown> = {
|
||||
'data-testid': icon,
|
||||
...(tooltip
|
||||
? {
|
||||
'data-for': tooltip,
|
||||
'data-tip': true
|
||||
}
|
||||
: {}
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
|
||||
@ -10,17 +10,24 @@ interface Props {
|
||||
button?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
dataTestId?: string;
|
||||
offset?: number | string;
|
||||
onClose: () => void;
|
||||
position: 'left' | 'right';
|
||||
sidebarRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
function Sidebar ({ button, children, className = '', onClose }: Props): React.ReactElement<Props> {
|
||||
function Sidebar ({ button, children, className = '', dataTestId = '', onClose, sidebarRef }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={`ui--Sidebar ${className}`}>
|
||||
<div
|
||||
className={`ui--Sidebar ${className}`}
|
||||
data-testid={dataTestId}
|
||||
ref={sidebarRef}
|
||||
>
|
||||
<Button.Group className='ui--Sidebar-buttons'>
|
||||
{button}
|
||||
<Button
|
||||
dataTestId='close-sidebar-button'
|
||||
icon='times'
|
||||
isBasic
|
||||
isCircular
|
||||
|
||||
@ -18,10 +18,11 @@ interface Props {
|
||||
onToggleIsEditing?: () => void;
|
||||
onSave?: () => void;
|
||||
value: string[];
|
||||
withEditButton?: boolean;
|
||||
withTitle?: boolean;
|
||||
}
|
||||
|
||||
function Tags ({ children, className = '', isEditable, isEditing, onChange, onSave, onToggleIsEditing, value, withTitle }: Props): React.ReactElement<Props> {
|
||||
function Tags ({ children, className = '', isEditable, isEditing, onChange, onSave, onToggleIsEditing, value, withEditButton = true, withTitle }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const contents = useMemo(
|
||||
@ -57,12 +58,12 @@ function Tags ({ children, className = '', isEditable, isEditing, onChange, onSa
|
||||
onChange={onChange}
|
||||
onClose={_onSave}
|
||||
openOnFocus
|
||||
searchInput={{ autoFocus: true }}
|
||||
searchInput={{ autoFocus: false }}
|
||||
value={value}
|
||||
withLabel={false}
|
||||
/>
|
||||
)
|
||||
: isEditable
|
||||
: isEditable && withEditButton
|
||||
? (
|
||||
<EditButton
|
||||
className={value.length === 0 ? 'center' : 'left'}
|
||||
|
||||
@ -122,12 +122,15 @@ export interface UseAccountInfo {
|
||||
meta?: KeyringJson$Meta;
|
||||
toggleIsEditingName: () => void;
|
||||
isEditingTags: boolean;
|
||||
isEditing: () => boolean;
|
||||
isNull: boolean;
|
||||
toggleIsEditingTags: () => void;
|
||||
onSaveName: () => void;
|
||||
onSaveTags: () => void;
|
||||
onSetGenesisHash: (genesisHash: string | null) => void;
|
||||
onForgetAddress: () => void;
|
||||
setIsEditingName: (isEditing: boolean) => void;
|
||||
setIsEditingTags: (isEditing: boolean) => void;
|
||||
}
|
||||
|
||||
export interface StakerState {
|
||||
|
||||
@ -51,8 +51,8 @@ export function useAccountInfo (value: string | null, isContract = false): UseAc
|
||||
const [identity, setIdentity] = useState<AddressIdentity | undefined>();
|
||||
const [flags, setFlags] = useState<AddressFlags>(IS_NONE);
|
||||
const [meta, setMeta] = useState<KeyringJson$Meta | undefined>();
|
||||
const [isEditingName, toggleIsEditingName] = useToggle();
|
||||
const [isEditingTags, toggleIsEditingTags] = useToggle();
|
||||
const [isEditingName, toggleIsEditingName, setIsEditingName] = useToggle();
|
||||
const [isEditingTags, toggleIsEditingTags, setIsEditingTags] = useToggle();
|
||||
|
||||
useEffect((): void => {
|
||||
validator && setFlags((flags) => ({
|
||||
@ -251,11 +251,14 @@ export function useAccountInfo (value: string | null, isContract = false): UseAc
|
||||
[]
|
||||
);
|
||||
|
||||
const isEditing = useCallback(() => isEditingName || isEditingTags, [isEditingName, isEditingTags]);
|
||||
|
||||
return {
|
||||
accountIndex,
|
||||
flags,
|
||||
genesisHash,
|
||||
identity,
|
||||
isEditing,
|
||||
isEditingName,
|
||||
isEditingTags,
|
||||
isNull: !value,
|
||||
@ -265,6 +268,8 @@ export function useAccountInfo (value: string | null, isContract = false): UseAc
|
||||
onSaveName,
|
||||
onSaveTags,
|
||||
onSetGenesisHash,
|
||||
setIsEditingName,
|
||||
setIsEditingTags,
|
||||
setName,
|
||||
setTags,
|
||||
tags,
|
||||
|
||||
@ -15,10 +15,10 @@ export const useOutsideClick = (elements: RefObject<HTMLDivElement>[], callback:
|
||||
}, [elements, callback]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClick);
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick);
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
};
|
||||
}, [handleClick, callback]);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user