Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Preimages Page with Improved Usability #11342

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/page-preimages/src/Preimages/Preimage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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} />
Expand Down
29 changes: 28 additions & 1 deletion packages/page-preimages/src/Preimages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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'],
Expand All @@ -36,13 +61,15 @@ function Hashes ({ className }: Props): React.ReactElement<Props> {
<Button.Group>
<Add />
</Button.Group>
<UserPreimages userPreimages={groupedUserPreimages} />
<Table
className={className}
empty={hashes && t('No hashes found')}
header={headerRef.current}
>
{hashes?.map((h) => (
<Preimage
cb={onSetAllPreImagesInfo}
key={h}
value={h}
/>
Expand Down
149 changes: 149 additions & 0 deletions packages/page-preimages/src/Preimages/userPreimages/Preimage.tsx
Original file line number Diff line number Diff line change
@@ -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);
46 changes: 46 additions & 0 deletions packages/page-preimages/src/Preimages/userPreimages/index.tsx
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 1 addition & 1 deletion packages/react-hooks/src/useAssetIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down