Enhance Preimages Page with Improved Usability (#11342)
* enhance: Add user preimages component and update preimage handling * refactor: user preimages handling and improve UI components * style: enhance table row styling in Preimage component * feat: unnote multiple preimages at once * chore: check if id is not undefined
This commit is contained in:
parent
79fe7b080d
commit
331fc4b9a2
@ -1,9 +1,10 @@
|
||||
// Copyright 2017-2025 @polkadot/app-preimages authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Preimage as TPreimage } from '@polkadot/react-hooks/types';
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { usePreimage } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
@ -15,11 +16,16 @@ import Hash from './Hash.js';
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: HexString;
|
||||
cb?: (info: TPreimage) => void;
|
||||
}
|
||||
|
||||
function Preimage ({ className, value }: Props): React.ReactElement<Props> {
|
||||
function Preimage ({ cb, className, value }: Props): React.ReactElement<Props> {
|
||||
const info = usePreimage(value);
|
||||
|
||||
useEffect(() => {
|
||||
info && cb?.(info);
|
||||
}, [cb, info]);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<Hash value={value} />
|
||||
|
||||
@ -2,14 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsicFunction } from '@polkadot/api/types';
|
||||
import type { Preimage as TPreimage } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Button, styled, Table } from '@polkadot/react-components';
|
||||
import { useAccounts } from '@polkadot/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import usePreimages from '../usePreimages.js';
|
||||
import Add from './Add/index.js';
|
||||
import UserPreimages from './userPreimages/index.js';
|
||||
import Preimage from './Preimage.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
@ -21,8 +24,30 @@ interface Props {
|
||||
|
||||
function Hashes ({ className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
const [allPreImagesInfo, setAllPreImagesInfo] = useState<TPreimage[]>([]);
|
||||
const hashes = usePreimages();
|
||||
|
||||
// HACK to concat all preimages info without creating a new hook, just for multiple hashes
|
||||
const onSetAllPreImagesInfo = useCallback((info: TPreimage) => {
|
||||
setAllPreImagesInfo((preimages) => ([
|
||||
...preimages.filter((e) => e.proposalHash !== info.proposalHash),
|
||||
info
|
||||
]));
|
||||
}, []);
|
||||
|
||||
const groupedUserPreimages = useMemo(() => {
|
||||
return allPreImagesInfo.reduce((result: Record<string, TPreimage[]>, current) => {
|
||||
if (current.deposit?.who && allAccounts.includes(current.deposit?.who)) {
|
||||
const newItems = [...(result[current.deposit?.who] || []), current];
|
||||
|
||||
result[current.deposit?.who] = newItems;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, {} as Record<string, TPreimage[]>);
|
||||
}, [allAccounts, allPreImagesInfo]);
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('preimages'), 'start', 2],
|
||||
[undefined, 'media--1300'],
|
||||
@ -36,6 +61,7 @@ function Hashes ({ className }: Props): React.ReactElement<Props> {
|
||||
<Button.Group>
|
||||
<Add />
|
||||
</Button.Group>
|
||||
<UserPreimages userPreimages={groupedUserPreimages} />
|
||||
<Table
|
||||
className={className}
|
||||
empty={hashes && t('No hashes found')}
|
||||
@ -43,6 +69,7 @@ function Hashes ({ className }: Props): React.ReactElement<Props> {
|
||||
>
|
||||
{hashes?.map((h) => (
|
||||
<Preimage
|
||||
cb={onSetAllPreImagesInfo}
|
||||
key={h}
|
||||
value={h}
|
||||
/>
|
||||
|
||||
149
packages/page-preimages/src/Preimages/userPreimages/Preimage.tsx
Normal file
149
packages/page-preimages/src/Preimages/userPreimages/Preimage.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
// Copyright 2017-2025 @polkadot/app-preimages authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Preimage as TPreimage } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { AddressMini, Checkbox, styled, TxButton } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Call from '../Call.js';
|
||||
import Hash from '../Hash.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
depositor: string,
|
||||
preimageInfos: TPreimage[];
|
||||
}
|
||||
|
||||
interface SelectPreimageProps {
|
||||
proposalHash: TPreimage['proposalHash'],
|
||||
onSelectPreimage: React.Dispatch<React.SetStateAction<TPreimage['proposalHash'][]>>
|
||||
}
|
||||
|
||||
const SelectPreimage = ({ onSelectPreimage, proposalHash }: SelectPreimageProps) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const onChange = React.useCallback((value: boolean) => {
|
||||
setChecked(value);
|
||||
onSelectPreimage((prevHashes) =>
|
||||
// Add preimage hash if checked else filter it out
|
||||
value ? [...prevHashes, proposalHash] : prevHashes.filter((i) => i !== proposalHash)
|
||||
);
|
||||
}, [onSelectPreimage, proposalHash]);
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
onChange={onChange}
|
||||
value={checked}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Preimage = ({ className, depositor, preimageInfos }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
|
||||
const [selectedPreimages, onSelectPreimage] = useState<TPreimage['proposalHash'][]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{preimageInfos.map((info, index) => {
|
||||
return (
|
||||
<StyledTr
|
||||
className={`isExpanded ${className}`}
|
||||
isFirstItem={index === 0}
|
||||
isLastItem={false}
|
||||
key={info.proposalHash}
|
||||
>
|
||||
<td
|
||||
className='address all'
|
||||
style={{ paddingTop: 15, verticalAlign: 'top' }}
|
||||
>
|
||||
{index === 0 && <AddressMini value={depositor} />}
|
||||
</td>
|
||||
<Call value={info} />
|
||||
<td style={{ alignItems: 'center', display: 'flex' }}>
|
||||
<SelectPreimage
|
||||
onSelectPreimage={onSelectPreimage}
|
||||
proposalHash={info.proposalHash}
|
||||
/>
|
||||
<Hash value={info.proposalHash} />
|
||||
</td>
|
||||
<td className='number media--1000'>
|
||||
{info?.proposalLength
|
||||
? formatNumber(info.proposalLength)
|
||||
: <span className='--tmp'>999,999</span>}
|
||||
</td>
|
||||
<td className='preimageStatus start media--1100 together'>
|
||||
{info
|
||||
? (
|
||||
<>
|
||||
{info.status && (<div>{info.status?.type}{info.count !== 0 && <> / {formatNumber(info.count)}</>}</div>)}
|
||||
</>
|
||||
)
|
||||
: <span className='--tmp'>Unrequested</span>}
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
})}
|
||||
<StyledTr
|
||||
className={`isExpanded ${className}`}
|
||||
isFirstItem={false}
|
||||
isLastItem
|
||||
>
|
||||
<td className='all' />
|
||||
<td className='all' />
|
||||
<td className='media--1300' />
|
||||
<td>
|
||||
<TxButton
|
||||
accountId={depositor}
|
||||
className={className}
|
||||
icon='minus'
|
||||
isToplevel
|
||||
label={t('Unnote')}
|
||||
params={[selectedPreimages.map((i) => api.tx.preimage.unnotePreimage(i))]}
|
||||
tx={api.tx.utility.batchAll}
|
||||
/>
|
||||
</td>
|
||||
<td className='media--1000' />
|
||||
<td className='media--1100' />
|
||||
</StyledTr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BASE_BORDER = 0.125;
|
||||
const BORDER_TOP = `${BASE_BORDER * 3}rem solid var(--bg-page)`;
|
||||
const BORDER_RADIUS = `${BASE_BORDER * 4}rem`;
|
||||
|
||||
const StyledTr = styled.tr<{isFirstItem: boolean; isLastItem: boolean}>`
|
||||
.ui--Icon {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: ${(props) => props.isFirstItem && BORDER_TOP};
|
||||
border-radius: 0rem !important;
|
||||
|
||||
&:first-child {
|
||||
padding-block: 1rem !important;
|
||||
border-top-left-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
border-bottom-left-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
border-bottom-right-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
}
|
||||
|
||||
td {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Preimage);
|
||||
@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2025 @polkadot/app-preimages authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Preimage as TPreimage } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Preimage from './Preimage.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
userPreimages: Record<string, TPreimage[]>
|
||||
}
|
||||
|
||||
const UserPreimages = ({ className, userPreimages }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('my preimages'), 'start', 2],
|
||||
[undefined, 'media--1300'],
|
||||
[t('hash'), 'start'],
|
||||
[t('length'), 'media--1000'],
|
||||
[t('status'), 'start media--1100']
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={Object.values(userPreimages) && t('No hashes found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{Object.keys(userPreimages)?.map((depositor) => (
|
||||
<Preimage
|
||||
depositor={depositor}
|
||||
key={depositor}
|
||||
preimageInfos={userPreimages[depositor]}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UserPreimages);
|
||||
@ -11,7 +11,7 @@ const EMPTY_PARAMS: unknown[] = [];
|
||||
|
||||
const OPT_KEY = {
|
||||
transform: (keys: StorageKey<[u32]>[]): u32[] =>
|
||||
keys.map(({ args: [id] }) => id)
|
||||
keys.map(({ args: [id] }) => id).filter((id) => id !== undefined)
|
||||
};
|
||||
|
||||
function filter (records: EventRecord[]): Changes<u32> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user