feat: initial Pezkuwi Apps rebrand from polkadot-apps

Rebranded terminology:
- Polkadot → Pezkuwi
- Kusama → Dicle
- Westend → Zagros
- Rococo → PezkuwiChain
- Substrate → Bizinikiwi
- parachain → teyrchain

Custom logos with Kurdistan brand colors (#e6007a → #86e62a):
- bizinikiwi-hexagon.svg
- sora-bizinikiwi.svg
- hezscanner.svg
- heztreasury.svg
- pezkuwiscan.svg
- pezkuwistats.svg
- pezkuwiassembly.svg
- pezkuwiholic.svg
This commit is contained in:
Kurdistan Tech Ministry committed 2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 files changed
+329019

No files matched your search

@@ -0,0 +1,147 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AddressFlags } from '@pezkuwi/react-hooks/types';
import React, { useCallback } from 'react';
import { useApi, useToggle } from '@pezkuwi/react-hooks';
import { isFunction } from '@pezkuwi/util';
import Button from '../Button/index.js';
import { TransferModal } from '../modals/index.js';
import { styled } from '../styled.js';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
flags: AddressFlags;
isEditingName: boolean;
isEditing: boolean;
toggleIsEditingName: () => void;
toggleIsEditingTags: () => void;
onCancel: () => void;
onSaveName: () => void;
onSaveTags: () => void;
onForgetAddress: () => void;
onUpdateName?: (() => void) | null;
recipientId: string;
}
function AccountMenuButtons ({ className = '', flags, isEditing, isEditingName, onCancel, onForgetAddress, onSaveName, onSaveTags, onUpdateName, recipientId, toggleIsEditingName, toggleIsEditingTags }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isTransferOpen, toggleIsTransferOpen] = useToggle();
const api = useApi();
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 (
<StyledDiv className={`${className} ui--AddressMenu-buttons`}>
{isEditing
? (
<Button.Group>
<Button
icon='times'
label={t('Cancel')}
onClick={onCancel}
/>
<Button
icon='save'
label={t('Save')}
onClick={onEdit}
/>
</Button.Group>
)
: (
<Button.Group>
{(isFunction(api.api.tx.balances?.transferAllowDeath) || isFunction(api.api.tx.balances?.transfer)) && (
<Button
icon='paper-plane'
isDisabled={isEditing}
label={t('Send')}
onClick={toggleIsTransferOpen}
/>
)}
{!flags.isOwned && !flags.isInContacts && (
<Button
icon='plus'
isDisabled={isEditing}
label={t('Save')}
onClick={_onUpdateName}
/>
)}
{!flags.isOwned && flags.isInContacts && (
<Button
icon='ban'
isDisabled={isEditing}
label={t('Remove')}
onClick={_onForgetAddress}
/>
)}
<Button
icon='edit'
isDisabled={!flags.isEditable}
label={t('Edit')}
onClick={onEdit}
/>
</Button.Group>
)
}
{isTransferOpen && (
<TransferModal
key='modal-transfer'
onClose={toggleIsTransferOpen}
recipientId={recipientId}
/>
)}
</StyledDiv>
);
}
const StyledDiv = styled.div`
width: 100%;
.ui--Button-Group {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: 0;
}
`;
export default React.memo(AccountMenuButtons);
@@ -0,0 +1,88 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AddressFlags } from '@pezkuwi/react-hooks/types';
import React from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useToggle } from '@pezkuwi/react-hooks';
import AccountName from '../AccountName.js';
import Button from '../Button/index.js';
import IdentityIcon from '../IdentityIcon/index.js';
import Input from '../Input.js';
import { useTranslation } from '../translate.js';
interface Props {
value: string,
editingName: boolean,
defaultValue: string,
onChange: (value: string) => void,
flags: AddressFlags,
accountIndex: string | undefined,
}
function AddressSection ({ accountIndex, defaultValue, editingName, flags, onChange, value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isCopyShown, toggleIsCopyShown] = useToggle();
const NOOP = () => undefined;
return (
<div className='ui--AddressSection'>
<IdentityIcon
size={80}
value={value}
/>
<div className='ui--AddressSection__AddressColumn'>
<AccountName
override={
editingName
? (
<Input
className='name--input'
defaultValue={defaultValue}
label='name-input'
onChange={onChange}
withLabel={false}
/>
)
: flags.isEditable
? (defaultValue.toUpperCase() || t('<unknown>'))
: undefined
}
value={value}
withSidebar={false}
/>
<div className='ui--AddressMenu-addr'>
{value}
</div>
{accountIndex && (
<div className='ui--AddressMenu-index'>
<label>{t('index')}:</label> {accountIndex}
</div>
)}
</div>
<div className='ui--AddressSection__CopyColumn'>
<div className='ui--AddressMenu-copyaddr'>
<CopyToClipboard
text={value}
>
<span>
<Button.Group>
<Button
icon={isCopyShown ? 'check' : 'copy'}
label={isCopyShown ? t('Copied') : t('Copy')}
onClick={isCopyShown ? NOOP : toggleIsCopyShown }
onMouseLeave={isCopyShown ? toggleIsCopyShown : NOOP }
/>
</Button.Group>
</span>
</CopyToClipboard>
</div>
</div>
</div>
);
}
export default React.memo(AddressSection);
@@ -0,0 +1,56 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import AddressInfo from '../AddressInfo.js';
import { styled } from '../styled.js';
import { useTranslation } from '../translate.js';
interface Props {
address: string;
className?: string;
}
const WITH_BALANCE = { available: true, bonded: true, free: true, locked: true, reserved: true, total: true };
function Balances ({ address, className }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
return (
<StyledSection className={className}>
<div className='ui--AddressMenu-sectionHeader'>
{t('balance')}
</div>
<AddressInfo
address={address}
className='balanceExpander'
key={address}
withBalance={WITH_BALANCE}
withLabel
/>
</StyledSection>
);
}
const StyledSection = styled.section`
.balanceExpander {
justify-content: flex-start;
.column {
width: auto;
max-width: 18.57rem;
label {
text-align: left;
color: inherit;
}
.ui--Expander-content .ui--FormatBalance-value {
font-size: var(--font-size-small);
}
}
}
`;
export default React.memo(Balances);
@@ -0,0 +1,114 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AddressFlags } from '@pezkuwi/react-hooks/types';
import React from 'react';
import Flag from '../Flag.js';
import { styled } from '../styled.js';
import { useTranslation } from '../translate.js';
interface Props {
flags: AddressFlags;
className?: string;
}
function Flags ({ className = '', flags: { isCouncil, isDevelopment, isExternal, isInjected, isMultisig, isNominator, isProxied, isSociety, isSudo, isTechCommittee, isValidator } }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const hasFlags = isCouncil || isDevelopment || isExternal || isInjected || isMultisig || isProxied || isSociety || isSudo || isTechCommittee || isValidator || isNominator;
if (!hasFlags) {
return null;
}
return (
<StyledDiv className={`${className} ui--AddressMenu-flags`}>
{
hasFlags && (
<h5>{t('Flags')}</h5>
)
}
<div>
{isValidator && (
<Flag
color='theme'
label={t('Validator')}
/>
)}
{isNominator && (
<Flag
color='theme'
label={t('Nominator')}
/>
)}
{isExternal && (
isMultisig
? (
<Flag
color='green'
label={t('Multisig')}
/>
)
: isProxied
? (
<Flag
color='grey'
label={t('Proxied')}
/>
)
: (
<Flag
color='grey'
label={t('External')}
/>
)
)}
{isInjected && (
<Flag
color='grey'
label={t('Injected')}
/>
)}
{isDevelopment && (
<Flag
color='grey'
label={t('Test account')}
/>
)}
{isCouncil && (
<Flag
color='blue'
label={t('Council')}
/>
)}
{isSociety && (
<Flag
color='green'
label={t('Society')}
/>
)}
{isTechCommittee && (
<Flag
color='orange'
label={t('Technical committee')}
/>
)}
{isSudo && (
<Flag
color='pink'
label={t('Sudo key')}
/>
)}
</div>
</StyledDiv>
);
}
const StyledDiv = styled.div`
.ui--Tag {
margin: 0.2rem 1rem 0.2rem 0.571rem;
}
`;
export default React.memo(Flags);
@@ -0,0 +1,237 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useMemo } from 'react';
import { useApi, useRegistrars, useSubidentities, useToggle } from '@pezkuwi/react-hooks';
import { AddressIdentityOtherDiscordKey } from '@pezkuwi/react-hooks/constants';
import { type AddressIdentity } from '@pezkuwi/react-hooks/types';
import { isHex } from '@pezkuwi/util';
import AddressMini from '../AddressMini.js';
import AvatarItem from '../AvatarItem.js';
import Expander from '../Expander.js';
import IconLink from '../IconLink.js';
import { useTranslation } from '../translate.js';
import Judgements from './Judgements.js';
import RegistrarJudgement from './RegistrarJudgement.js';
import UserIcon from './UserIcon.js';
interface Props {
address: string;
identity?: AddressIdentity;
}
const SUBS_DISPLAY_THRESHOLD = 4;
function Identity ({ address, identity }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { apiIdentity } = useApi();
const { isRegistrar, registrars } = useRegistrars();
const [isJudgementOpen, toggleIsJudgementOpen] = useToggle();
const subs = useSubidentities(address);
const subsList = useMemo(() =>
subs?.map((sub) =>
<AddressMini
className='subs'
isPadded={false}
key={sub.toString()}
value={sub}
/>
)
, [subs]
);
if (!identity || !identity.isExistent || !apiIdentity.query.identity?.identityOf) {
return null;
}
return (
<section
className='withDivider'
data-testid='identity-section'
>
<div className='ui--AddressMenu-section ui--AddressMenu-identity'>
<div className='ui--AddressMenu-sectionHeader'>
{t('identity')}
</div>
<div>
<AvatarItem
icon={
// This won't work - images are IPFS hashes
// identity.image
// ? <img src={identity.image} />
// : <i className='icon user ui--AddressMenu-identityIcon' />
//
<UserIcon />
}
subtitle={identity.legal}
title={identity.display}
/>
<Judgements address={address} />
<div className='ui--AddressMenu-identityTable'>
{identity.parent && (
<div className='tr parent'>
<div className='th'>{t('parent')}</div>
<div className='td'>
<AddressMini
className='parent'
isPadded={false}
value={identity.parent}
/>
</div>
</div>
)}
{identity.email && (
<div className='tr'>
<div className='th'>{t('email')}</div>
<div className='td'>
{isHex(identity.email) || !identity.isKnownGood
? identity.email
: (
<a
href={`mailto:${identity.email}`}
rel='noopener noreferrer'
target='_blank'
>
{identity.email}
</a>
)}
</div>
</div>
)}
{identity.web && (
<div className='tr'>
<div className='th'>{t('website')}</div>
<div className='td'>
{isHex(identity.web) || !identity.isKnownGood
? identity.web
: (
<a
href={(identity.web).replace(/^(https?:\/\/)?/g, 'https://')}
rel='noopener noreferrer'
target='_blank'
>
{identity.web}
</a>
)}
</div>
</div>
)}
{identity.twitter && (
<div className='tr'>
<div className='th'>{t('twitter')}</div>
<div className='td'>
{isHex(identity.twitter) || !identity.isKnownGood
? identity.twitter
: (
<a
href={
(identity.twitter).startsWith('https://twitter.com/')
? (identity.twitter)
: `https://twitter.com/${identity.twitter}`
}
rel='noopener noreferrer'
target='_blank'
>
{identity.twitter}
</a>
)}
</div>
</div>
)}
{identity.other && AddressIdentityOtherDiscordKey in identity.other && (
<div className='tr'>
<div className='th'>{t('discord')}</div>
<div className='td'>
{identity.other[AddressIdentityOtherDiscordKey]}
</div>
</div>
)}
{identity.github && (
<div className='tr'>
<div className='th'>{t('github')}</div>
<div className='td'>
{identity.github}
</div>
</div>
)}
{identity.matrix && (
<div className='tr'>
<div className='th'>{t('matrix')}</div>
<div className='td'>
{identity.matrix}
</div>
</div>
)}
{identity.discord && (
<div className='tr'>
<div className='th'>{t('discord')}</div>
<div className='td'>
{identity.discord}
</div>
</div>
)}
{identity.riot && (
<div className='tr'>
<div className='th'>{t('riot')}</div>
<div className='td'>
{identity.riot}
</div>
</div>
)}
{!!subs?.length && (
<div className='tr'>
<div className='th top'>{t('subs')}</div>
<div
className='td'
data-testid='subs'
>
{subs.length > SUBS_DISPLAY_THRESHOLD
? (
<Expander summary={subs.length}>
{subsList}
</Expander>
)
: (
<>
<div className='subs-number'>{subs.length}</div>
{subsList}
</>
)
}
</div>
</div>
)}
</div>
</div>
</div>
{isRegistrar && (
<div className='ui--AddressMenu-section'>
<div className='ui--AddressMenu-actions'>
<ul>
<li>
<IconLink
icon='address-card'
label={t('Add identity judgment')}
onClick={toggleIsJudgementOpen}
/>
</li>
</ul>
</div>
</div>
)}
{isJudgementOpen && isRegistrar && (
<RegistrarJudgement
address={address}
key='modal-judgement'
registrars={registrars}
toggleJudgement={toggleIsJudgementOpen}
/>
)}
</section>
);
}
export default React.memo(Identity);
@@ -0,0 +1,50 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DisplayedJudgement, Judgement } from '../types.js';
import React, { useMemo } from 'react';
import AddressSmall from '../AddressSmall.js';
import Menu from '../Menu/index.js';
import Popup from '../Popup/index.js';
import Tag from '../Tag.js';
interface Props {
judgement: Judgement
}
export function getJudgementColor (name: DisplayedJudgement): 'green' | 'red' {
return (name === 'Erroneous' || name === 'Low quality')
? 'red'
: 'green';
}
function JudgementTag ({ judgement: { judgementName, registrars } }: Props): React.ReactElement<Props> {
const judgementColor = useMemo(() => getJudgementColor(judgementName), [judgementName]);
return (
<Popup
closeOnScroll
position='middle'
value={
<Menu>
{registrars.map((registrar) => registrar && (
<AddressSmall
key={registrar.address}
value={registrar.address}
/>
))}
</Menu>
}
>
<Tag
color={judgementColor}
label={`${registrars.length} ${judgementName}`}
size='tiny'
/>
</Popup>
);
}
export default React.memo(JudgementTag);
@@ -0,0 +1,64 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { useJudgements } from '@pezkuwi/react-hooks';
import { styled } from '../styled.js';
import Tag from '../Tag.js';
import { useTranslation } from '../translate.js';
import JudgementTag from './JudgementTag.js';
interface Props {
address: string;
className?: string;
}
function Judgements ({ address, className = '' }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const judgements = useJudgements(address);
if (judgements.length === 0) {
return (
<div
className={`${className} no-judgements`}
data-testid='judgements'
>
<Tag
color='yellow'
key='NoJudgements'
label={t('No judgements')}
size='tiny'
/>
</div>
);
}
return (
<StyledDiv
className={className}
data-testid='judgements'
>
{judgements.map((judgement) =>
<JudgementTag
judgement={judgement}
key={`${address}${judgement.judgementName}`}
/>
)}
</StyledDiv>
);
}
const StyledDiv = styled.div`
margin-top: 0.714rem;
&:not(.no-judgements) {
.ui--Tag:hover {
cursor: pointer;
}
}
`;
export default React.memo(Judgements);
@@ -0,0 +1,53 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringJson$Meta } from '@pezkuwi/ui-keyring/types';
import React from 'react';
import AddressMini from '../AddressMini.js';
import { useTranslation } from '../translate.js';
interface Props {
isMultisig: boolean;
meta?: KeyringJson$Meta;
}
function Multisig ({ isMultisig, meta }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
if (!isMultisig || !meta) {
return null;
}
const { threshold, who } = meta;
return (
<section className='ui--AddressMenu-multisig withDivider'>
<div className='ui--AddressMenu-sectionHeader'>
{t('multisig')}
</div>
<div className='ui--AddressMenu-multisigTable'>
<div className='tr'>
<div className='th'>{t('threshold')}</div>
<div className='td'>
{threshold}/{who?.length}
</div>
</div>
<div className='tr'>
<div className='th signatories'>{t('signatories')}</div>
<div className='td'>
{who?.map((address) => (
<AddressMini
key={address}
value={address}
/>
))}
</div>
</div>
</div>
</section>
);
}
export default React.memo(Multisig);
@@ -0,0 +1,128 @@
// Copyright 2017-2025 @pezkuwi/react-query authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Bytes, Option } from '@pezkuwi/types';
import type { PalletIdentityRegistration } from '@pezkuwi/types/lookup';
import type { ITuple } from '@pezkuwi/types/types';
import type { HexString } from '@pezkuwi/util/types';
import React, { useEffect, useState } from 'react';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import Dropdown from '../Dropdown.js';
import Input from '../Input.js';
import InputAddress from '../InputAddress/index.js';
import MarkError from '../MarkError.js';
import Modal from '../Modal/index.js';
import Spinner from '../Spinner.js';
import { useTranslation } from '../translate.js';
import TxButton from '../TxButton.js';
interface Props {
address: string;
registrars: { address: string; index: number }[];
toggleJudgement: () => void;
}
const JUDGEMENT_ENUM = [
{ text: 'Unknown', value: 0 },
{ text: 'Fee paid', value: 1 },
{ text: 'Reasonable', value: 2 },
{ text: 'Known good', value: 3 },
{ text: 'Out of date', value: 4 },
{ text: 'Low quality', value: 5 }
];
const OPT_ID = {
transform: (optId: Option<ITuple<[PalletIdentityRegistration, Option<Bytes>]>>): HexString | null => {
const id = optId.isSome
? optId.unwrap()
: null;
// Backwards compatibility - https://github.com/pezkuwi-js/apps/issues/10493
return !id
? null
: Array.isArray(id)
? id[0].info.hash.toHex()
: (id as unknown as PalletIdentityRegistration).info.hash.toHex();
}
};
function RegistrarJudgement ({ address, registrars, toggleJudgement }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { apiIdentity, enableIdentity } = useApi();
const identityHash = useCall(apiIdentity.query.identity.identityOf, [address], OPT_ID);
const [addresses] = useState(() => registrars.map(({ address }) => address));
const [judgementAccountId, setJudgementAccountId] = useState<string | null>(null);
const [judgementEnum, setJudgementEnum] = useState(2); // Reasonable
const [registrarIndex, setRegistrarIndex] = useState(-1);
// find the id of our registrar in the list
useEffect((): void => {
const registrar = registrars.find(({ address }) => judgementAccountId === address);
setRegistrarIndex(
registrar
? registrar.index
: -1
);
}, [judgementAccountId, registrars]);
return (
<Modal
header={t('Provide judgement')}
onClose={toggleJudgement}
size='small'
>
<Modal.Content>
<InputAddress
filter={addresses}
label={t('registrar account')}
onChange={setJudgementAccountId}
type='account'
/>
<Input
isDisabled
label={t('registrar index')}
value={registrarIndex === -1 ? t('invalid/unknown registrar account') : registrarIndex.toString()}
/>
<Dropdown
label={t('judgement')}
onChange={setJudgementEnum}
options={JUDGEMENT_ENUM}
value={judgementEnum}
/>
{identityHash
? (
<Input
defaultValue={identityHash}
isDisabled
label={t('identity hash')}
/>
)
: identityHash === null
? <MarkError content={t('No identity associated with account')} />
: <Spinner noLabel />
}
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={judgementAccountId}
icon='check'
isDisabled={!enableIdentity || !identityHash || registrarIndex === -1}
label={t('Judge')}
onStart={toggleJudgement}
params={
apiIdentity.tx.identity.provideJudgement.meta.args.length === 4
? [registrarIndex, address, judgementEnum, identityHash]
: [registrarIndex, address, judgementEnum]
}
tx={apiIdentity.tx.identity.provideJudgement}
/>
</Modal.Actions>
</Modal>
);
}
export default React.memo(RegistrarJudgement);
@@ -0,0 +1,268 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
import type { AddressFlags } from '@pezkuwi/react-hooks/types';
import type { Sidebar } from '@pezkuwi/test-support/pagesElements';
import type { RegistrationJudgement } from '@pezkuwi/types/interfaces';
import { fireEvent, screen, waitFor, within } from '@testing-library/react';
import i18next from '@pezkuwi/react-components/i18n';
import { anAccount, anAccountWithInfo, anAccountWithMeta } from '@pezkuwi/test-support/creation/account';
import { alice, bob, MemoryStore } from '@pezkuwi/test-support/keyring';
import { charlieShortAddress, ferdieShortAddress, mockRegistration, registrars } from '@pezkuwi/test-support/mockData';
import { mockApiHooks } from '@pezkuwi/test-support/utils';
import { keyring } from '@pezkuwi/ui-keyring';
import { AccountsPage } from '../../../page-accounts/test/pages/accountsPage.js';
// FIXME: these all need to be wrapped in waitFor ....
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Sidebar', () => {
let accountsPage: AccountsPage;
let sideBar: Sidebar;
beforeAll(async () => {
await i18next.changeLanguage('en');
if (keyring.getAccounts().length === 0) {
keyring.loadAll({ isDevelopment: true, store: new MemoryStore() });
}
});
beforeEach(() => {
accountsPage = new AccountsPage();
accountsPage.clearAccounts();
});
describe('editing', () => {
const initialName = 'INITIAL_NAME';
const newName = 'NEW_NAME';
const defaultTag = 'Default';
const nameInputNotFoundError = 'Unable to find an element by: [data-testid="name-input"]';
describe('changes name', () => {
beforeEach(async () => {
// Cannot get this to work on React 18 ... the first one fails :(
// However... with a delay, it seems to get through the queue
accountsPage.render([[alice, anAccountWithMeta({ isDevelopment: false, name: initialName })]]);
await new Promise((resolve) => setTimeout(resolve, 1000));
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.changeAccountName(newName);
});
it('within keyring', () => {
const changedAccount = keyring.getAccount(alice);
expect(changedAccount?.meta?.name).toEqual(newName);
});
// eslint-disable-next-line jest/expect-expect
it('within sidebar', async () => {
await sideBar.assertAccountName(newName);
});
// eslint-disable-next-line jest/expect-expect
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);
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.clickByText('none');
expect(sideBar.queryByRole('combobox')).toBeFalsy();
await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError);
});
it('when isEditable is false account name is not editable', async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithInfo({ flags: { isEditable: false } as AddressFlags })
);
sideBar = await accountsPage.openSidebarForRow(0);
sideBar.edit();
await expect(sideBar.typeAccountName(newName)).rejects.toThrow(nameInputNotFoundError);
});
describe('on edit cancel', () => {
beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ isDevelopment: false, name: initialName, tags: [] })
);
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.assertTags('none');
sideBar.edit();
});
// eslint-disable-next-line jest/expect-expect
it('restores tags and name to state from keyring', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
sideBar.cancel();
await sideBar.assertTags('none');
await sideBar.assertAccountName(initialName);
});
it('Cancel button disappears', () => {
sideBar.cancel();
expect(sideBar.queryByRole('button', { name: 'Cancel' })).toBeFalsy();
});
});
describe('outside click', () => {
beforeEach(async () => {
accountsPage.renderAccountsWithDefaultAddresses(
anAccountWithMeta({ name: 'alice' }),
anAccountWithMeta({ name: 'bob' })
);
sideBar = await accountsPage.openSidebarForRow(0);
sideBar.edit();
});
it('cancels editing', async () => {
await sideBar.typeAccountName(newName);
await sideBar.selectTag(defaultTag);
fireEvent.click(await screen.findByText('accounts'));
await sideBar.assertTags('none');
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();
});
// eslint-disable-next-line jest/expect-expect
it('cancels editing and changes name when opening sidebar for another account', async () => {
await waitFor(() => sideBar.assertAccountInput('alice'));
sideBar = await accountsPage.openSidebarForRow(1);
await sideBar.assertAccountName('BOB');
});
});
});
describe('identity section', () => {
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);
});
});
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');
});
// eslint-disable-next-line jest/expect-expect
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');
});
});
});
describe('judgements', () => {
// eslint-disable-next-line jest/expect-expect
it('displays several judgements', async () => {
mockApiHooks.setJudgements(mockRegistration.judgements as RegistrationJudgement[]);
accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.assertJudgement('1 Known good');
await sideBar.assertJudgement('2 Reasonable');
await sideBar.assertJudgement('1 Erroneous');
});
// eslint-disable-next-line jest/expect-expect
it('displays no judgements', async () => {
accountsPage.renderDefaultAccounts(1);
sideBar = await accountsPage.openSidebarForRow(0);
await sideBar.assertJudgement('No judgements');
});
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);
});
// eslint-disable-next-line jest/expect-expect
it('singular registrar', async () => {
const judgementTag = await sideBar.getJudgement('1 Known good');
await judgementTag.assertRegistrars([charlieShortAddress]);
});
// eslint-disable-next-line jest/expect-expect
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([]);
});
});
});
});
@@ -0,0 +1,331 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useRef, useState } from 'react';
import { useAccountInfo } from '@pezkuwi/react-hooks';
import LinkExternal from '../LinkExternal.js';
import Sidebar from '../Sidebar.js';
import { styled } from '../styled.js';
import { colorLink } from '../styles/theme.js';
import Balances from './Balances.js';
import Identity from './Identity.js';
import Multisig from './Multisig.js';
import SidebarEditableSection from './SidebarEditableSection.js';
interface Props {
address: string;
className?: string;
dataTestId?: string;
onClose?: () => void;
onUpdateName?: (() => void) | null;
}
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 sidebarRef = useRef<HTMLDivElement>(null);
return (
<StyledSidebar
className={`${className}${inEditMode ? ' inEditMode' : ''}`}
dataTestId={dataTestId}
onClose={onClose}
position='right'
sidebarRef={sidebarRef}
>
<div
className='ui--AddressMenu-header'
data-testid='sidebar-address-menu'
>
<SidebarEditableSection
accountIndex={accountIndex}
address={address}
isBeingEdited={setInEditMode}
onUpdateName={onUpdateName}
sidebarRef={sidebarRef}
/>
</div>
<div className='ui--ScrollSection'>
<Balances address={address} />
<Identity
address={address}
identity={identity}
/>
<Multisig
isMultisig={flags.isMultisig}
meta={meta}
/>
</div>
<section className='ui--LinkSection'>
<LinkExternal
data={address}
isSidebar
type='address'
/>
</section>
</StyledSidebar>
);
}
const StyledSidebar = styled(Sidebar)`
display: flex;
flex-direction: column;
background-color: var(--bg-sidebar);
max-width: 30.42rem;
min-width: 30.42rem;
overflow-y: hidden;
padding: 0 0 3.286rem;
input {
width: auto !important;
}
.ui--AddressMenu-header {
align-items: center;
background: var(--bg-tabs);
border-bottom: 1px solid var(--border-table);
display: flex;
flex-direction: column;
justify-content: center;
padding: 1.35rem 1rem 1rem 1rem;
}
.ui--AddressSection {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
width: 100%;
.ui--AddressSection__AddressColumn {
flex: 1;
margin-left: 1rem;
.ui--AccountName {
max-width: 21.5rem;
overflow: hidden;
white-space: normal;
}
}
.ui--AddressSection__CopyColumn {
margin-left: 1rem;
.ui--AccountName {
max-width: 10rem;
overflow: hidden;
}
}
}
.ui--AddressMenu-addr,
.ui--AddressMenu-index {
text-align: left;
font-size: var(--font-size-small);
}
.ui--AddressMenu-addr {
word-break: break-all;
width: 24ch;
margin: 0.571rem 0;
color: var(--color-label);
}
.ui--AddressMenu-copyaddr,
.ui--AddressMenu-index {
text-align: left;
font-size: var(--font-size-small);
}
.ui--AddressMenu-copyaaddr {
word-break: break-all;
width: 12ch;
margin: 0.371rem 0;
color: var(--color-label);
}
.ui--AddressMenu-index {
display: flex;
flex-direction: row;
label {
font-size: var(--font-size-small);
margin-right: 0.4rem;
text-transform: capitalize;
}
}
section {
position: relative;
&:not(:last-child) {
margin-bottom: 1rem;
}
.ui--AddressMenu-sectionHeader {
display: flex;
justify-content: space-between;
align-items: center;
text-transform: capitalize;
margin-bottom: 0.57rem;
width: 100%;
color: var(--color-text);
font-size: 1.143rem;
}
&.withDivider {
padding-top: 1rem;
::before {
position: absolute;
top: 0;
left: 0;
content: '';
width: 100%;
height: 1px;
background-color: var(--border-table);
}
}
}
.ui--AddressMenu-identity,
.ui--AddressMenu-multisig {
.ui--AddressMenu-identityTable,
.ui--AddressMenu-multisigTable {
font-size: var(--font-size-small);
margin-top: 0.6rem;
.tr {
padding: 0.25rem 0;
display: inline-flex;
align-items: center;
width: 100%;
.th {
text-transform: uppercase;
color: var(--color-label);
font-weight: var(--font-weight-normal);
text-align: left;
flex-basis: 25%;
font-size: var(--font-size-tiny);
&.top {
align-self: flex-start;
}
}
.td {
flex: 1;
overflow: hidden;
padding-left: 0.6rem;
text-overflow: ellipsis;
}
}
.ui--AddressMini, .subs-number {
margin-bottom: 0.4rem;
padding: 0;
}
.subs-number {
font-size: var(--font-size-base);
margin-bottom: 0.714rem;
}
}
.parent {
padding: 0 !important;
}
}
&& .column {
align-items: center;
.ui--FormatBalance:first-of-type {
margin-bottom: 0.4rem;
}
.ui--FormatBalance {
line-height: 1rem;
}
}
.ui--AddressMenu-buttons {
.ui--Button-Group {
margin-bottom: 0;
}
}
.ui--AddressMenu-tags,
.ui--AddressMenu-flags {
margin: 0.75rem 0 0;
width: 100%;
}
.ui--AddressMenu-identityIcon {
background: ${colorLink}66;
}
.ui--AddressMenu-actions {
ul {
list-style-type: none;
margin-block-start: 0;
margin-block-end: 0;
padding-inline-start: 1rem;
li {
margin: 0.2rem 0;
}
}
}
.inline-icon {
cursor: pointer;
margin: 0 0 0 0.5rem;
color: ${colorLink};
}
.name--input {
.ui.input {
margin: 0 !important;
> input {
}
}
}
&.inEditMode {
.ui--AddressMenu-flags {
opacity: 60%;
}
}
.ui--AddressMenu-multisig .th.signatories {
align-self: flex-start;
}
.ui--ScrollSection {
padding: 1rem;
overflow: auto;
}
.ui--LinkSection {
border-top: 1px solid var(--border-table);
padding: 0.5rem 0 0.571rem;
width: 100%;
position: absolute;
bottom: 0;
span {
margin: 0 0.5rem;
}
}
`;
export default React.memo(FullSidebar);
@@ -0,0 +1,93 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useEffect, useMemo } from 'react';
import { useAccountInfo, useOutsideClick } from '@pezkuwi/react-hooks';
import { keyring } from '@pezkuwi/ui-keyring';
import Tags from '../Tags.js';
import AccountMenuButtons from './AccountMenuButtons.js';
import AddressSection from './AddressSection.js';
import Flags from './Flags.js';
interface Props {
accountIndex: string | undefined;
address: string;
isBeingEdited: (arg: boolean) => void;
onUpdateName?: (() => void) | null;
sidebarRef: React.RefObject<HTMLDivElement>;
}
function SidebarEditableSection ({ accountIndex, address, isBeingEdited, onUpdateName, sidebarRef }: Props): React.ReactElement<Props> {
const { flags, isEditing, isEditingName, isEditingTags, name, onForgetAddress, onSaveName, onSaveTags, setIsEditingName, setIsEditingTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags } = useAccountInfo(address);
const refs = useMemo(
() => [sidebarRef],
[sidebarRef]
);
useEffect((): void => {
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).sort() : []);
setIsEditingName(false);
setIsEditingTags(false);
} catch {
// ignore
}
}
}, [isEditing, setName, setTags, setIsEditingName, setIsEditingTags, address]);
useOutsideClick(refs, onCancel);
return (
<>
<AddressSection
accountIndex={accountIndex}
defaultValue={name}
editingName={isEditingName}
flags={flags}
onChange={setName}
value={address}
/>
<div
className='ui--AddressMenu-tags'
data-testid='sidebar-tags'
>
<Tags
isEditable
isEditing={isEditingTags}
onChange={setTags}
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);
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
function UserIcon (): React.ReactElement {
return (
<svg
fill='none'
height='40'
viewBox='0 0 40 40'
width='40'
xmlns='http://www.w3.org/2000/svg'
xmlnsXlink='http://www.w3.org/1999/xlink'
>
<circle
cx='20'
cy='20'
// eslint-disable-next-line react/no-unknown-property
fill='url(#pattern0)'
r='20'
/>
<defs>
<pattern
height='1'
id='pattern0'
patternContentUnits='objectBoundingBox'
width='1'
>
<use
transform='scale(0.00277778)'
xlinkHref='#image0'
/>
</pattern>
<image
height='360'
id='image0'
width='360'
xlinkHref='data:image/png;base64,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 Line truncated
/>
</defs>
</svg>
);
}
export default React.memo(UserIcon);
@@ -0,0 +1,41 @@
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useState } from 'react';
import { AccountSidebarCtx } from '@pezkuwi/react-hooks/ctx/AccountSidebar';
import Sidebar from './Sidebar.js';
interface Props {
children: React.ReactNode;
}
type State = [string | null, (() => void) | null];
const EMPTY_STATE: State = [null, null];
function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
const [[address, onUpdateName], setAddress] = useState<State>(EMPTY_STATE);
const onClose = useCallback(
() => setAddress([null, null]),
[]
);
return (
<AccountSidebarCtx.Provider value={setAddress}>
{children}
{address && (
<Sidebar
address={address}
dataTestId='account-sidebar'
onClose={onClose}
onUpdateName={onUpdateName}
/>
)}
</AccountSidebarCtx.Provider>
);
}
export default React.memo(AccountSidebar);