Skip to content

Commit a756bbc

Browse files
feat(explorer): make field values copyable across the app (#6849)
# Description of change Please write a summary of your changes and why you made them. ## Links to any relevant issues fixes #6829 ## Type of change Choose a type of change, and delete any options that are not relevant. - Bug fix (a non-breaking change which fixes an issue) - Enhancement (a non-breaking change which adds functionality) - Breaking change (fix or feature that would cause existing functionality to not work as expected) - Documentation Fix ## How the change has been tested Describe the tests that you ran to verify your changes. Make sure to provide instructions for the maintainer as well as any relevant configurations. - [ ] Basic tests (linting, compilation, formatting, unit/integration tests) - [ ] Patch-specific tests (correctness, functionality coverage) ### Infrastructure QA (only required for crates that are maintained by @iotaledger/infrastructure) - [ ] Synchronization of the indexer from genesis for a network including migration objects. - [ ] Restart of indexer synchronization locally without resetting the database. - [ ] Restart of indexer synchronization on a production-like database. - [ ] Deployment of services using Docker. - [ ] Verification of API backward compatibility. ## Change checklist Tick the boxes that are relevant to your changes, and delete any items that are not. - [ ] I have followed the contribution guidelines for this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have checked that new and existing unit tests pass locally with my changes ### Release Notes <!-- Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. --> - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] REST API: --------- Co-authored-by: Bran <[email protected]>
1 parent be15d98 commit a756bbc

File tree

25 files changed

+241
-72
lines changed

25 files changed

+241
-72
lines changed

.changeset/light-dryers-matter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@iota/apps-ui-kit': patch
3+
---
4+
5+
add copy button to DisplayStats component

apps/explorer/src/components/gas-breakdown/GasBreakdown.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,10 @@
33
// SPDX-License-Identifier: Apache-2.0
44

55
import { Accordion, AccordionContent, Title, Divider } from '@iota/apps-ui-kit';
6-
import {
7-
CoinFormat,
8-
type TransactionSummaryType,
9-
useCopyToClipboard,
10-
useFormatCoin,
11-
toast,
12-
} from '@iota/core';
13-
import { Copy } from '@iota/apps-ui-icons';
6+
import { CoinFormat, type TransactionSummaryType, useFormatCoin } from '@iota/core';
147
import { AddressLink, CollapsibleCard, ObjectLink } from '~/components/ui';
158
import { Fragment } from 'react';
9+
import { onCopySuccess } from '~/lib/utils';
1610

1711
interface GasProps {
1812
amount?: bigint | number | string;
@@ -53,20 +47,14 @@ function GasAmount({ amount, burnedAmount }: GasProps): JSX.Element | null {
5347
}
5448

5549
function GasPaymentLinks({ objectIds }: { objectIds: string[] }): JSX.Element {
56-
const copyToClipBoard = useCopyToClipboard(() => toast('Copied'));
57-
58-
const handleCopy = async (objectId: string) => {
59-
await copyToClipBoard(objectId);
60-
};
61-
6250
return (
6351
<div className="flex max-h-20 min-h-[20px] flex-wrap items-center gap-x-4 gap-y-2 overflow-y-auto">
6452
{objectIds.map((objectId, index) => (
6553
<div key={index} className="flex items-center gap-x-1.5">
66-
<ObjectLink objectId={objectId} />
67-
<Copy
68-
className="shrink-0 cursor-pointer text-neutral-70"
69-
onClick={() => handleCopy(objectId)}
54+
<ObjectLink
55+
objectId={objectId}
56+
copyText={objectId}
57+
onCopySuccess={onCopySuccess}
7058
/>
7159
</div>
7260
))}

apps/explorer/src/components/object/DynamicFieldsCard.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Panel,
1515
LoadingIndicator,
1616
} from '@iota/apps-ui-kit';
17+
import { onCopySuccess } from '~/lib/utils';
1718

1819
interface DynamicFieldRowProps {
1920
id: string;
@@ -35,7 +36,11 @@ function DynamicFieldRow({ id, result, defaultOpen }: DynamicFieldRowProps): JSX
3536
String(result.name.value)
3637
) : null}
3738
</div>
38-
<ObjectLink objectId={result.objectId} />
39+
<ObjectLink
40+
objectId={result.objectId}
41+
copyText={result.objectId}
42+
onCopySuccess={onCopySuccess}
43+
/>
3944
</div>
4045
</AccordionHeader>
4146
<AccordionContent isExpanded={open}>

apps/explorer/src/components/object/FieldItem.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { type IotaMoveNormalizedType } from '@iota/iota-sdk/client';
66
import { SyntaxHighlighter } from '~/components';
77
import { AddressLink, Link, ObjectLink } from '~/components/ui';
88
import { getFieldTypeValue } from '~/lib/ui';
9+
import { onCopySuccess } from '~/lib/utils';
910

1011
interface FieldItemProps {
1112
value: string | number | object | boolean;
@@ -34,15 +35,25 @@ export function FieldItem({
3435
if (normalizedType === TYPE_ADDRESS) {
3536
return (
3637
<div className="break-all">
37-
<AddressLink address={value.toString()} noTruncate={!truncate} />
38+
<AddressLink
39+
address={value.toString()}
40+
noTruncate={!truncate}
41+
copyText={value.toString()}
42+
onCopySuccess={onCopySuccess}
43+
/>
3844
</div>
3945
);
4046
}
4147

4248
if (normalizedType === 'string' && TYPE_OBJECT_ID.includes(normalizedType)) {
4349
return (
4450
<div className="break-all">
45-
<ObjectLink objectId={value.toString()} noTruncate={!truncate} />
51+
<ObjectLink
52+
objectId={value.toString()}
53+
noTruncate={!truncate}
54+
copyText={value.toString()}
55+
onCopySuccess={onCopySuccess}
56+
/>
4657
</div>
4758
);
4859
}

apps/explorer/src/components/owned-coins/CoinItem.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CoinFormat, useFormatCoin } from '@iota/core';
77
import { type CoinStruct } from '@iota/iota-sdk/client';
88
import { formatAddress } from '@iota/iota-sdk/utils';
99
import { ObjectLink } from '../ui';
10+
import { onCopySuccess } from '~/lib/utils';
1011

1112
interface CoinItemProps {
1213
coin: CoinStruct;
@@ -23,7 +24,12 @@ export function CoinItem({ coin }: CoinItemProps): JSX.Element {
2324
keyText={`${formattedBalance} ${symbol}`}
2425
isReverse
2526
value={
26-
<ObjectLink objectId={coin.coinObjectId} label={formatAddress(coin.coinObjectId)} />
27+
<ObjectLink
28+
objectId={coin.coinObjectId}
29+
label={formatAddress(coin.coinObjectId)}
30+
copyText={coin.coinObjectId}
31+
onCopySuccess={onCopySuccess}
32+
/>
2733
}
2834
fullwidth
2935
/>

apps/explorer/src/components/top-packages/TopPackagesTable.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type MoveCallMetric } from '@iota/iota-sdk/client';
77
import { type ColumnDef } from '@tanstack/react-table';
88

99
import { ObjectLink, PlaceholderTable, TableCard } from '~/components/ui';
10+
import { onCopySuccess } from '~/lib/utils';
1011

1112
interface TopPackagesTableProps {
1213
data: MoveCallMetric[];
@@ -47,7 +48,7 @@ const tableColumns: ColumnDef<MoveCallMetric>[] = [
4748
const item = metric[0].package;
4849
return (
4950
<TableCellBase>
50-
<ObjectLink objectId={item}>
51+
<ObjectLink objectId={item} copyText={item} onCopySuccess={onCopySuccess}>
5152
<TableCellText>{item}</TableCellText>
5253
</ObjectLink>
5354
</TableCellBase>

apps/explorer/src/components/ui/InternalLink.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Modifications Copyright (c) 2024 IOTA Stiftung
33
// SPDX-License-Identifier: Apache-2.0
44

5+
import { Copy } from '@iota/apps-ui-icons';
6+
import { ButtonUnstyled } from '@iota/apps-ui-kit';
57
import { formatAddress, formatDigest, formatType } from '@iota/iota-sdk/utils';
68
import { type ReactNode } from 'react';
79

@@ -11,6 +13,9 @@ interface BaseInternalLinkProps extends LinkProps {
1113
noTruncate?: boolean;
1214
label?: string | ReactNode;
1315
queryStrings?: Record<string, string>;
16+
copyText?: string;
17+
onCopySuccess?: (e: React.MouseEvent<HTMLButtonElement>, text: string) => void;
18+
onCopyError?: (e: unknown, text: string) => void;
1419
}
1520

1621
function createInternalLink<T extends string>(
@@ -23,21 +28,47 @@ function createInternalLink<T extends string>(
2328
noTruncate,
2429
label,
2530
queryStrings = {},
31+
copyText,
32+
onCopySuccess,
33+
onCopyError,
2634
...props
2735
}: BaseInternalLinkProps & Record<T, string>) => {
2836
const truncatedAddress = noTruncate ? id : formatter(id);
2937
const queryString = new URLSearchParams(queryStrings).toString();
3038
const queryStringPrefix = queryString ? `?${queryString}` : '';
3139

40+
async function handleCopyClick(event: React.MouseEvent<HTMLButtonElement>) {
41+
event.stopPropagation();
42+
if (!navigator.clipboard) {
43+
return;
44+
}
45+
if (copyText) {
46+
try {
47+
await navigator.clipboard.writeText(copyText);
48+
onCopySuccess?.(event, copyText);
49+
} catch (error) {
50+
console.error('Failed to copy:', error);
51+
onCopyError?.(error, copyText);
52+
}
53+
}
54+
}
55+
3256
return (
33-
<Link
34-
className="text-primary-30 dark:text-primary-80"
35-
variant="mono"
36-
to={`/${base}/${encodeURI(id)}${queryStringPrefix}`}
37-
{...props}
38-
>
39-
{label || truncatedAddress}
40-
</Link>
57+
<div className="flex flex-row items-center gap-x-xxs">
58+
<Link
59+
className="text-primary-30 dark:text-primary-80"
60+
variant="mono"
61+
to={`/${base}/${encodeURI(id)}${queryStringPrefix}`}
62+
{...props}
63+
>
64+
{label || truncatedAddress}
65+
</Link>
66+
{copyText && (
67+
<ButtonUnstyled onClick={handleCopyClick}>
68+
<Copy className="text-neutral-60 dark:text-neutral-40" />
69+
</ButtonUnstyled>
70+
)}
71+
</div>
4172
);
4273
};
4374
}

apps/explorer/src/components/ui/PageHeader.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Placeholder,
1313
} from '@iota/apps-ui-kit';
1414
import { Copy, Warning } from '@iota/apps-ui-icons';
15-
import { useCopyToClipboard, toast } from '@iota/core';
15+
import { onCopySuccess } from '~/lib/utils';
1616

1717
type PageHeaderType = 'Transaction' | 'Checkpoint' | 'Address' | 'Object' | 'Package';
1818

@@ -35,12 +35,6 @@ export function PageHeader({
3535
after,
3636
status,
3737
}: PageHeaderProps): JSX.Element {
38-
const copyToClipBoard = useCopyToClipboard(() => toast('Copied'));
39-
40-
const handleCopy = async () => {
41-
await copyToClipBoard(title);
42-
};
43-
4438
return (
4539
<Panel>
4640
<div className="flex w-full items-center p-md--rs">
@@ -83,7 +77,7 @@ export function PageHeader({
8377
{title}
8478
</span>
8579
<Copy
86-
onClick={handleCopy}
80+
onClick={onCopySuccess}
8781
className="shrink-0 cursor-pointer"
8882
/>
8983
</div>

apps/explorer/src/components/validator/ValidatorMeta.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { Badge, BadgeType, KeyValueInfo, Panel } from '@iota/apps-ui-kit';
55
import { type IotaValidatorSummary } from '@iota/iota-sdk/client';
66
import { ArrowTopRight } from '@iota/apps-ui-icons';
77
import { AddressLink } from '~/components/ui';
8-
import { ImageIcon, ImageIconSize, toast } from '@iota/core';
8+
import { ImageIcon, ImageIconSize } from '@iota/core';
99
import type { InactiveValidatorData } from '~/pages/validator/ValidatorDetails';
10+
import { onCopySuccess } from '~/lib/utils';
1011

1112
type ValidatorMetaProps = {
1213
validatorData: IotaValidatorSummary;
@@ -25,9 +26,6 @@ export function InactiveValidators({
2526
}: {
2627
validatorData: InactiveValidatorData;
2728
}): JSX.Element {
28-
function handleOnCopy() {
29-
toast('Copied to clipboard');
30-
}
3129
return (
3230
<div className="flex flex-col gap-y-md">
3331
<Panel>
@@ -71,18 +69,19 @@ export function InactiveValidators({
7169
keyText="Pool ID"
7270
value={validatorStakingPoolId}
7371
copyText={validatorStakingPoolId}
74-
onCopySuccess={handleOnCopy}
72+
onCopySuccess={onCopySuccess}
7573
/>
7674
<KeyValueInfo
7775
keyText="Address"
7876
value={<AddressLink address={validatorAddress} label={validatorAddress} />}
7977
copyText={validatorAddress}
80-
onCopySuccess={handleOnCopy}
78+
onCopySuccess={onCopySuccess}
8179
/>
8280
<KeyValueInfo
8381
keyText="Public Key"
8482
value={validatorPublicKey}
8583
copyText={validatorPublicKey}
84+
onCopySuccess={onCopySuccess}
8685
/>
8786
</div>
8887
</Panel>
@@ -97,10 +96,6 @@ export function ValidatorMeta({ validatorData }: ValidatorMetaProps): JSX.Elemen
9796
const description = validatorData.description;
9897
const projectUrl = validatorData.projectUrl;
9998

100-
function handleOnCopy() {
101-
toast('Copied to clipboard');
102-
}
103-
10499
return (
105100
<div className="flex flex-col gap-y-md">
106101
<Panel>
@@ -144,7 +139,7 @@ export function ValidatorMeta({ validatorData }: ValidatorMetaProps): JSX.Elemen
144139
keyText="Pool ID"
145140
value={validatorData.stakingPoolId}
146141
copyText={validatorData.stakingPoolId}
147-
onCopySuccess={handleOnCopy}
142+
onCopySuccess={onCopySuccess}
148143
/>
149144
<KeyValueInfo
150145
keyText="Address"
@@ -155,7 +150,7 @@ export function ValidatorMeta({ validatorData }: ValidatorMetaProps): JSX.Elemen
155150
/>
156151
}
157152
copyText={validatorData.iotaAddress}
158-
onCopySuccess={handleOnCopy}
153+
onCopySuccess={onCopySuccess}
159154
/>
160155
<KeyValueInfo
161156
keyText="Public Key"

apps/explorer/src/index.css

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,9 @@ body,
103103
'node-map';
104104
}
105105

106-
@screen md {
107-
.home-page-grid-container-bottom {
108-
@apply gap-10;
109-
}
110-
}
111-
112106
@screen lg {
113107
.home-page-grid-container-bottom {
114-
@apply grid grid-cols-2 gap-x-20 gap-y-10;
108+
@apply grid grid-cols-2 gap-x-8 gap-y-8;
115109
grid-template-areas:
116110
'activity activity'
117111
'packages validators'

apps/explorer/src/lib/ui/utils/generateCheckpointsTableColumns.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TableCellBase, TableCellText } from '@iota/apps-ui-kit';
66
import type { Checkpoint } from '@iota/iota-sdk/client';
77
import type { ColumnDef } from '@tanstack/react-table';
88
import { CheckpointSequenceLink, CheckpointLink } from '~/components';
9+
import { onCopySuccess } from '~/lib/utils';
910
import { getElapsedTime } from '~/pages/epochs/utils';
1011

1112
/**
@@ -23,6 +24,8 @@ export function generateCheckpointsTableColumns(): ColumnDef<Checkpoint>[] {
2324
<CheckpointLink
2425
digest={digest}
2526
label={<TableCellText>{digest}</TableCellText>}
27+
copyText={digest}
28+
onCopySuccess={onCopySuccess}
2629
/>
2730
</TableCellBase>
2831
);

apps/explorer/src/lib/ui/utils/generateTransactionsTableColumns.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ColumnDef } from '@tanstack/react-table';
1010
import { AddressLink, TransactionLink } from '../../../components/ui';
1111
import { formatAddress, formatDigest, NANOS_PER_IOTA } from '@iota/iota-sdk/utils';
1212
import { getElapsedTime } from '~/pages/epochs/utils';
13+
import { onCopySuccess } from '~/lib/utils';
1314

1415
/**
1516
* Generate table columns renderers for the transactions data.
@@ -26,6 +27,8 @@ export function generateTransactionsTableColumns(): ColumnDef<IotaTransactionBlo
2627
<TransactionLink
2728
digest={digest}
2829
label={<TableCellText>{formatDigest(digest)}</TableCellText>}
30+
copyText={digest}
31+
onCopySuccess={onCopySuccess}
2932
/>
3033
</TableCellBase>
3134
);
@@ -41,6 +44,8 @@ export function generateTransactionsTableColumns(): ColumnDef<IotaTransactionBlo
4144
<AddressLink
4245
address={address}
4346
label={<TableCellText>{formatAddress(address)}</TableCellText>}
47+
copyText={address}
48+
onCopySuccess={onCopySuccess}
4449
/>
4550
</TableCellBase>
4651
);

apps/explorer/src/lib/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './stringUtils';
1515
export * from './iotaMoveTypeConverters';
1616
export * from './getSupplyChangeAfterEpochEnd';
1717
export * from './sanitizePendingValidators';
18+
export * from './onCopySuccess';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) 2025 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { toast } from '@iota/core';
5+
6+
export function onCopySuccess() {
7+
toast.success('Copied to clipboard');
8+
}

0 commit comments

Comments
 (0)