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:
Arjun Porwal 2025-03-05 22:52:59 +05:30 committed by GitHub
parent 79fe7b080d
commit 331fc4b9a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 232 additions and 4 deletions

View File

@ -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} />

View File

@ -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}
/>

View 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 && <>&nbsp;/&nbsp;{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);

View File

@ -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);

View File

@ -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> {