mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-22 14:47:58 +00:00
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:
@@ -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);
|
||||
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
Reference in New Issue
Block a user