mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-04-22 21:47:57 +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,74 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AccountName from '../AccountName.js';
|
||||
import IdentityIcon from '../IdentityIcon/index.js';
|
||||
import { styled } from '../styled.js';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
className?: string;
|
||||
isUppercase: boolean;
|
||||
name: string;
|
||||
style?: Record<string, string>;
|
||||
}
|
||||
|
||||
function KeyPair ({ address, className = '' }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<StyledDiv className={`${className} ui--KeyPair`}>
|
||||
<IdentityIcon
|
||||
className='icon'
|
||||
value={address}
|
||||
/>
|
||||
<div className='name'>
|
||||
<AccountName value={address} />
|
||||
</div>
|
||||
<div className='address'>
|
||||
{address}
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
||||
> .address {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
font-size: var(--font-size-small);
|
||||
margin-left: 1rem;
|
||||
max-width: var(--width-shortaddr);
|
||||
min-width: var(--width-shortaddr);
|
||||
opacity: var(--opacity-light);
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
> .name {
|
||||
display: inline-block;
|
||||
flex: 1 0;
|
||||
margin-left: 3rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(KeyPair);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeyringSectionOption } from '@pezkuwi/ui-keyring/options/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Dropdown from '../Dropdown.js';
|
||||
|
||||
export default function createHeader (option: KeyringSectionOption): React.ReactNode {
|
||||
return (
|
||||
<Dropdown.Header
|
||||
content={option.name}
|
||||
key={option.key || option.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { KeyringSectionOption } from '@pezkuwi/ui-keyring/options/types';
|
||||
import type { Option } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { decodeAddress } from '@pezkuwi/util-crypto';
|
||||
|
||||
import KeyPair from './KeyPair.js';
|
||||
|
||||
export default function createItem (option: KeyringSectionOption, isUppercase = true): Option | null {
|
||||
const allowedLength = keyring.keyring.type === 'ethereum'
|
||||
? 20
|
||||
: 32;
|
||||
|
||||
try {
|
||||
if (decodeAddress(option.key).length >= allowedLength) {
|
||||
return {
|
||||
...option,
|
||||
text: (
|
||||
<KeyPair
|
||||
address={option.key || ''}
|
||||
isUppercase={isUppercase}
|
||||
name={option.name}
|
||||
/>
|
||||
)
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DropdownItemProps } from 'semantic-ui-react';
|
||||
import type { KeyringOption$Type, KeyringOptions, KeyringSectionOption } from '@pezkuwi/ui-keyring/options/types';
|
||||
import type { Option } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import store from 'store';
|
||||
|
||||
import { withMulti, withObservable } from '@pezkuwi/react-api/hoc';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { createOptionItem } from '@pezkuwi/ui-keyring/options/item';
|
||||
import { isNull, isUndefined } from '@pezkuwi/util';
|
||||
import { isAddress } from '@pezkuwi/util-crypto';
|
||||
|
||||
import Dropdown from '../Dropdown.js';
|
||||
import Static from '../Static.js';
|
||||
import { styled } from '../styled.js';
|
||||
import { getAddressName, toAddress } from '../util/index.js';
|
||||
import createHeader from './createHeader.js';
|
||||
import createItem from './createItem.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
defaultValue?: Uint8Array | string | null;
|
||||
filter?: string[] | null;
|
||||
hideAddress?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isError?: boolean;
|
||||
isInput?: boolean;
|
||||
isMultiple?: boolean;
|
||||
label?: React.ReactNode;
|
||||
labelExtra?: React.ReactNode;
|
||||
onChange?: (value: string | null) => void;
|
||||
onChangeMulti?: (value: string[]) => void;
|
||||
options?: KeyringSectionOption[] | null;
|
||||
optionsAll?: Record<string, Option[]>;
|
||||
placeholder?: string;
|
||||
type?: KeyringOption$Type;
|
||||
value?: string | Uint8Array | string[] | null;
|
||||
withEllipsis?: boolean;
|
||||
withExclude?: boolean;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
|
||||
type ExportedType = React.ComponentType<Props> & {
|
||||
createOption: (option: KeyringSectionOption, isUppercase?: boolean) => Option | null;
|
||||
setLastValue: (type: KeyringOption$Type, value: string) => void;
|
||||
};
|
||||
|
||||
interface State {
|
||||
lastValue?: string;
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'options:InputAddress';
|
||||
const DEFAULT_TYPE = 'all';
|
||||
const MULTI_DEFAULT: string[] = [];
|
||||
|
||||
function transformToAddress (value?: string | Uint8Array | null): string | null {
|
||||
try {
|
||||
return toAddress(value, false, keyring.keyring.type === 'ethereum' ? 20 : 32) || null;
|
||||
} catch {
|
||||
// noop, handled by return
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function transformToAccountId (value: string): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountId = transformToAddress(value);
|
||||
|
||||
return !accountId
|
||||
? null
|
||||
: accountId;
|
||||
}
|
||||
|
||||
function createOption (address: string): Option | null {
|
||||
let isRecent: boolean | undefined;
|
||||
const pair = keyring.getAccount(address);
|
||||
let name: string | undefined;
|
||||
|
||||
if (pair) {
|
||||
name = pair.meta.name;
|
||||
} else {
|
||||
const addr = keyring.getAddress(address);
|
||||
|
||||
if (addr) {
|
||||
name = addr.meta.name;
|
||||
isRecent = addr.meta.isRecent;
|
||||
} else {
|
||||
isRecent = true;
|
||||
}
|
||||
}
|
||||
|
||||
return createItem(createOptionItem(address, name), !isRecent);
|
||||
}
|
||||
|
||||
function readOptions (): Record<string, Record<string, string>> {
|
||||
return store.get(STORAGE_KEY) as Record<string, Record<string, string>> || { defaults: {} };
|
||||
}
|
||||
|
||||
function getLastValue (type: KeyringOption$Type = DEFAULT_TYPE): string {
|
||||
const options = readOptions();
|
||||
|
||||
return options.defaults[type];
|
||||
}
|
||||
|
||||
function setLastValue (type: KeyringOption$Type = DEFAULT_TYPE, value: string): void {
|
||||
const options = readOptions();
|
||||
|
||||
options.defaults[type] = value;
|
||||
store.set(STORAGE_KEY, options);
|
||||
}
|
||||
|
||||
function dedupe (options: Option[]): Option[] {
|
||||
return options.reduce<Option[]>((all, o, index) => {
|
||||
const hasDupe = all.some(({ key }, eindex) =>
|
||||
eindex !== index &&
|
||||
key === o.key
|
||||
);
|
||||
|
||||
if (!hasDupe) {
|
||||
all.push(o);
|
||||
}
|
||||
|
||||
return all;
|
||||
}, []);
|
||||
}
|
||||
|
||||
class InputAddress extends React.PureComponent<Props, State> {
|
||||
public override state: State = {};
|
||||
|
||||
public static getDerivedStateFromProps ({ type, value }: Props, { lastValue }: State): Pick<State, never> | null {
|
||||
try {
|
||||
return {
|
||||
lastValue: lastValue || getLastValue(type),
|
||||
value: Array.isArray(value)
|
||||
? value.map((v) => toAddress(v))
|
||||
: (toAddress(value) || undefined)
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override render (): React.ReactNode {
|
||||
const { className = '', defaultValue, hideAddress = false, isDisabled = false, isError, isMultiple, label, labelExtra, options, optionsAll, placeholder, type = DEFAULT_TYPE, withEllipsis, withLabel } = this.props;
|
||||
const hasOptions = (options && options.length !== 0) || (optionsAll && Object.keys(optionsAll[type]).length !== 0);
|
||||
|
||||
// the options could be delayed, don't render without
|
||||
if (!hasOptions && !isDisabled && !['allPlus'].includes(type)) {
|
||||
// This is nasty, but since this things is non-functional, there is not much
|
||||
// we can do (well, wrap it, however that approach is deprecated here)
|
||||
return (
|
||||
<Static
|
||||
className={className}
|
||||
label={label}
|
||||
>
|
||||
No accounts are available for selection.
|
||||
</Static>
|
||||
);
|
||||
}
|
||||
|
||||
const { lastValue, value } = this.state;
|
||||
const lastOption = this.getLastOptionValue();
|
||||
const actualValue = transformToAddress(
|
||||
isDisabled || (defaultValue && defaultValue !== '0x' && (this.hasValue(defaultValue) || type === 'allPlus'))
|
||||
? defaultValue
|
||||
: this.hasValue(lastValue)
|
||||
? lastValue
|
||||
: lastOption?.value
|
||||
);
|
||||
const actualOptions: Option[] = options
|
||||
? dedupe(
|
||||
options
|
||||
.map((o) => createItem(o))
|
||||
.filter((o): o is Option => !!o)
|
||||
)
|
||||
: isDisabled && actualValue
|
||||
? [createOption(actualValue)].filter((o): o is Option => !!o)
|
||||
: actualValue
|
||||
? this.addActual(actualValue)
|
||||
: this.getFiltered();
|
||||
const _defaultValue = (isMultiple || !isUndefined(value))
|
||||
? undefined
|
||||
: actualValue;
|
||||
|
||||
return (
|
||||
<StyledDropdown
|
||||
className={`${className} ui--InputAddress ${hideAddress ? 'hideAddress' : ''}`}
|
||||
defaultValue={_defaultValue}
|
||||
isDisabled={isDisabled}
|
||||
isError={isError}
|
||||
isMultiple={isMultiple}
|
||||
label={label}
|
||||
labelExtra={labelExtra}
|
||||
onChange={
|
||||
isMultiple
|
||||
? this.onChangeMulti
|
||||
: this.onChange
|
||||
}
|
||||
onSearch={this.onSearch}
|
||||
options={
|
||||
// FIXME: this is a "bit" of a HACK - the issue is that the "null"
|
||||
// value from Option is not correct for the supplied type. (This
|
||||
// originates in the ui repo for the KeyringOption)
|
||||
actualOptions as unknown as React.ReactNode[]
|
||||
}
|
||||
placeholder={placeholder}
|
||||
renderLabel={
|
||||
isMultiple
|
||||
? this.renderLabel
|
||||
: undefined
|
||||
}
|
||||
value={
|
||||
isMultiple && !value
|
||||
? MULTI_DEFAULT
|
||||
: value
|
||||
}
|
||||
withEllipsis={withEllipsis}
|
||||
withLabel={withLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private addActual (actualValue: string): Option[] {
|
||||
const base = this.getFiltered();
|
||||
|
||||
return this.hasValue(actualValue)
|
||||
? base
|
||||
: base.concat(...[createOption(actualValue)].filter((o): o is Option => !!o));
|
||||
}
|
||||
|
||||
private renderLabel = ({ value }: KeyringSectionOption): React.ReactNode => {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getAddressName(value);
|
||||
};
|
||||
|
||||
private getLastOptionValue (): KeyringSectionOption | undefined {
|
||||
const available = this.getFiltered();
|
||||
|
||||
return available.length
|
||||
? available[available.length - 1]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private hasValue (test?: Uint8Array | string | null): boolean {
|
||||
const address = test?.toString();
|
||||
|
||||
return this.getFiltered().some(({ value }) => value === address);
|
||||
}
|
||||
|
||||
private getFiltered (): Option[] {
|
||||
const { filter, optionsAll, type = DEFAULT_TYPE, withExclude = false } = this.props;
|
||||
|
||||
return !optionsAll
|
||||
? []
|
||||
: dedupe(optionsAll[type]).filter(({ value }) =>
|
||||
!filter || (
|
||||
!!value && (
|
||||
withExclude
|
||||
? !filter.includes(value)
|
||||
: filter.includes(value)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private onChange = (address: string): void => {
|
||||
const { filter, onChange, type } = this.props;
|
||||
|
||||
!filter && setLastValue(type, address);
|
||||
|
||||
onChange && onChange(
|
||||
!!address && (this.hasValue(address) || (type === 'allPlus' && isAddress(address)))
|
||||
? transformToAccountId(address)
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
private onChangeMulti = (addresses: string[]): void => {
|
||||
const { onChangeMulti } = this.props;
|
||||
|
||||
if (onChangeMulti) {
|
||||
onChangeMulti(
|
||||
addresses
|
||||
.map(transformToAccountId)
|
||||
.filter((address): address is string => !!address)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private onSearch = (filteredOptions: DropdownItemProps[], _query: string): DropdownItemProps[] => {
|
||||
const { isInput = true } = this.props;
|
||||
const query = _query.trim();
|
||||
const queryLower = query.toLowerCase();
|
||||
const matches = filteredOptions.filter((item): boolean =>
|
||||
!!item.value && typeof item.name === 'string' && (
|
||||
(item.name.toLowerCase?.().includes(queryLower)) ||
|
||||
item.value.toString().toLowerCase().includes(queryLower)
|
||||
)
|
||||
);
|
||||
|
||||
if (isInput && matches.length === 0) {
|
||||
const accountId = transformToAccountId(query);
|
||||
|
||||
if (accountId) {
|
||||
const account = keyring.getAccount(accountId);
|
||||
|
||||
if (account) {
|
||||
matches.push({
|
||||
key: account.address,
|
||||
name: account.meta.name,
|
||||
value: account.address
|
||||
});
|
||||
} else {
|
||||
const item = keyring.saveRecent(
|
||||
accountId.toString()
|
||||
).option;
|
||||
|
||||
matches.push({
|
||||
key: item.key,
|
||||
name: item.name,
|
||||
value: item.value || undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME The return here is _very_ suspect, but it actually does exactly
|
||||
// what it is meant to do... filter and return the options for clicking
|
||||
//
|
||||
// (effectively it seems to be the value type that should allow undefined
|
||||
// instead of null in there...)
|
||||
return matches.filter((item, index): boolean => {
|
||||
const isLast = index === matches.length - 1;
|
||||
const nextItem = matches[index + 1];
|
||||
const hasNext = nextItem?.value;
|
||||
|
||||
return !(isNull(item.value) || isUndefined(item.value)) || (!isLast && !!hasNext);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const StyledDropdown = styled(Dropdown)`
|
||||
.ui.dropdown .text {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ui.disabled.search {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.ui.search.selection.dropdown {
|
||||
> .text > .ui--KeyPair {
|
||||
.ui--IdentityIcon {
|
||||
left: -2.75rem;
|
||||
top: -1.05rem;
|
||||
|
||||
> div,
|
||||
img,
|
||||
svg {
|
||||
height: 32px !important;
|
||||
width: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 0;
|
||||
|
||||
> .ui--AccountName {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .menu > div.item > .ui--KeyPair > .name > .ui--AccountName {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.hideAddress .ui.search.selection.dropdown > .text > .ui--KeyPair .address {
|
||||
flex: 0;
|
||||
max-width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExportedComponent = withMulti(
|
||||
InputAddress,
|
||||
withObservable(keyring.keyringOption.optionsSubject, {
|
||||
propName: 'optionsAll',
|
||||
transform: (optionsAll: KeyringOptions): Record<string, (Option | React.ReactNode)[]> =>
|
||||
Object.entries(optionsAll).reduce((result: Record<string, (Option | React.ReactNode)[]>, [type, options]): Record<string, (Option | React.ReactNode)[]> => {
|
||||
result[type] = options
|
||||
.map((option): Option | React.ReactNode | null =>
|
||||
option.value === null
|
||||
? createHeader(option)
|
||||
: createItem(option)
|
||||
)
|
||||
.filter((o): o is Option | React.ReactNode => !!o);
|
||||
|
||||
return result;
|
||||
}, {})
|
||||
})
|
||||
) as ExportedType;
|
||||
|
||||
ExportedComponent.createOption = createItem;
|
||||
ExportedComponent.setLastValue = setLastValue;
|
||||
|
||||
export default ExportedComponent;
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
import type { KeyringOption$Type, KeyringSectionOption } from '@pezkuwi/ui-keyring/options/types';
|
||||
|
||||
export interface Option extends KeyringSectionOption {
|
||||
className?: string;
|
||||
text: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface InputAddressProps {
|
||||
className?: string;
|
||||
defaultValue?: Uint8Array | string | null;
|
||||
hideAddress?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isError?: boolean;
|
||||
isInput?: boolean;
|
||||
isMultiple?: boolean;
|
||||
label?: React.ReactNode;
|
||||
labelExtra?: React.ReactNode;
|
||||
onChange?: (value: string | null) => void;
|
||||
onChangeMulti?: (value: string[]) => void;
|
||||
options?: KeyringSectionOption[];
|
||||
optionsAll?: Record<string, Option[]>;
|
||||
placeholder?: string;
|
||||
type?: KeyringOption$Type;
|
||||
value?: string | Uint8Array | string[] | null;
|
||||
withEllipsis?: boolean;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user