Skip to content

Commit

Permalink
explorer: support for nft standard (MystenLabs#9700)
Browse files Browse the repository at this point in the history
## Description 

* shows all nfts in address page even if display does not exist to allow
listing all owned objects for an address
* removes existing handling of displaying nfts meta - only objects with
display will show images and other display related fields


https://user-images.githubusercontent.com/10210143/226921696-7188c326-4ad6-4fb5-9220-cac4c3bc6ac6.mov


## Test Plan 

manually - mint an nft that has display nft standard and check it's
display in object and address details view

closes APPS-654
  • Loading branch information
pchrysochoidis authored Mar 22, 2023
1 parent 91c6fd0 commit f249d2f
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 108 deletions.
20 changes: 10 additions & 10 deletions apps/explorer/src/components/ownedobjects/OwnedObjects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { useRpcClient } from '@mysten/core';
import {
Coin,
getObjectFields,
getObjectId,
PaginatedObjectsResponse,
is,
Expand Down Expand Up @@ -60,6 +59,7 @@ function OwnedObject({ id, byAddress }: { id: string; byAddress: boolean }) {
options: {
showType: true,
showContent: true,
showDisplay: true,
},
})
.then((results) => {
Expand All @@ -68,21 +68,21 @@ function OwnedObject({ id, byAddress }: { id: string; byAddress: boolean }) {
.filter(({ status }) => status === 'Exists')
.map(
(resp) => {
const contents = getObjectFields(resp);
const url = parseImageURL(contents);

const name = extractName(contents);
const objType = parseObjectType(resp);
const balanceValue = Coin.getBalance(resp);
const displayMeta =
typeof resp.details === 'object' &&
'display' in resp.details
? resp.details.display
: undefined;
const url = parseImageURL(displayMeta);
return {
id: getObjectId(resp),
Type: objType,
Type: parseObjectType(resp),
_isCoin: Coin.isCoin(resp),
display: url
? transformURL(url)
: undefined,
balance: balanceValue,
name: name,
balance: Coin.getBalance(resp),
name: extractName(displayMeta) || '',
};
}
// TODO - add back version
Expand Down
9 changes: 5 additions & 4 deletions apps/explorer/src/hooks/useGetObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ export function useGetSystemObject() {
}

export function useGetObject(
objectId: string
objectId?: string | null
): UseQueryResult<SuiObjectResponse, unknown> {
const rpc = useRpcClient();
const normalizedObjId = normalizeSuiAddress(objectId);
const normalizedObjId = objectId && normalizeSuiAddress(objectId);
const response = useQuery(
['object', normalizedObjId],
async () =>
rpc.getObject({
id: normalizedObjId,
id: normalizedObjId!,
options: {
showType: true,
showContent: true,
showOwner: true,
showPreviousTransaction: true,
showStorageRebate: true,
showDisplay: true,
},
}),
{ enabled: !!objectId }
{ enabled: !!normalizedObjId }
);

return response;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { type ReactNode } from 'react';

import { DescriptionItem } from '~/ui/DescriptionList';
import { Link } from '~/ui/Link';
import { Text } from '~/ui/Text';
import { getDisplayUrl } from '~/utils/objectUtils';

export type LinkOrTextDescriptionItemProps = {
title: ReactNode;
value: ReactNode;
parseUrl?: boolean;
};

export function LinkOrTextDescriptionItem({
title,
value,
parseUrl = false,
}: LinkOrTextDescriptionItemProps) {
let urlData = null;
if (parseUrl && typeof value === 'string') {
urlData = getDisplayUrl(value);
}
return value ? (
<DescriptionItem title={title}>
{urlData && typeof urlData === 'object' ? (
<Link href={urlData.href} variant="textHeroDark">
{urlData.display}
</Link>
) : typeof value === 'string' ? (
<Text variant="p1/medium" color="steel-darker">
{value}
</Text>
) : (
value
)}
</DescriptionItem>
) : null;
}
3 changes: 0 additions & 3 deletions apps/explorer/src/pages/object-result/ObjectResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useParams } from 'react-router-dom';

import { ErrorBoundary } from '../../components/error-boundary/ErrorBoundary';
import { useGetObject } from '../../hooks/useGetObject';
import { extractName } from '../../utils/objectUtils';
import { translate, type DataType } from './ObjectResultType';
import PkgView from './views/PkgView';
import { TokenView } from './views/TokenView';
Expand Down Expand Up @@ -43,15 +42,13 @@ export function ObjectResult() {
}

const resp = translate(data);
const name = extractName(resp.data?.contents);
const isPackage = resp.objType === PACKAGE_TYPE_NAME;

return (
<div className="mt-5 mb-10">
<PageHeader
type={isPackage ? 'Package' : 'Object'}
title={resp.id}
subtitle={name}
/>

<ErrorBoundary>
Expand Down
6 changes: 6 additions & 0 deletions apps/explorer/src/pages/object-result/ObjectResultType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export type DataType = {
tx_digest?: string;
};
loadState?: string;
display?: Record<string, string>;
};

export function instanceOfDataType(object: any): object is DataType {
Expand All @@ -56,6 +57,11 @@ export function translate(o: SuiObjectResponse): DataType {
contents: getObjectFields(o) ?? getMovePackageContent(o)!,
tx_digest: getObjectPreviousTransactionDigest(o),
},
display:
(typeof o.details === 'object' &&
'display' in o.details &&
o.details.display) ||
undefined,
};
}
case 'NotExists': {
Expand Down
142 changes: 76 additions & 66 deletions apps/explorer/src/pages/object-result/views/TokenView.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { ArrowRight12 } from '@mysten/icons';
import { normalizeSuiAddress } from '@mysten/sui.js';
import { useState, useEffect, useCallback } from 'react';

import { ReactComponent as PreviewMediaIcon } from '../../../assets/SVGIcons/preview-media.svg';
import DisplayBox from '../../../components/displaybox/DisplayBox';
import ModulesWrapper from '../../../components/module/ModulesWrapper';
import OwnedObjects from '../../../components/ownedobjects/OwnedObjects';
Expand All @@ -14,6 +14,7 @@ import {
extractName,
} from '../../../utils/objectUtils';
import { trimStdLibPrefix, genFileTypeMsg } from '../../../utils/stringUtils';
import { LinkOrTextDescriptionItem } from '../LinkOrTextDescriptionItem';
import { type DataType } from '../ObjectResultType';

import styles from './ObjectView.module.css';
Expand All @@ -27,8 +28,8 @@ import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '~/ui/Tabs';
import { Text } from '~/ui/Text';

export function TokenView({ data }: { data: DataType }) {
const imgUrl = parseImageURL(data.data.contents);
const name = extractName(data?.data?.contents);
const imgUrl = parseImageURL(data.display);
const name = extractName(data.display);

const properties = Object.entries(data.data?.contents).filter(
([key, value]) => key !== 'name' && checkIsPropertyType(value)
Expand Down Expand Up @@ -135,76 +136,85 @@ export function TokenView({ data }: { data: DataType }) {
</DescriptionItem>
</DescriptionList>
</div>
{name || data.data.contents.description ? (
{data.display ? (
<div className="pt-2 pr-10 md:pt-2.5">
<DescriptionList>
{name && (
<DescriptionItem title="Name">
<Text
variant="body/medium"
color="steel-darker"
>
{name}
</Text>
</DescriptionItem>
)}
{data.data.contents.description ? (
<DescriptionItem title="Description">
<Text
variant="p1/medium"
color="steel-darker"
>
{
data.data.contents
.description
}
</Text>
</DescriptionItem>
) : null}
<LinkOrTextDescriptionItem
title="Name"
value={name}
/>
<LinkOrTextDescriptionItem
title="Description"
value={data.display.description}
/>
<LinkOrTextDescriptionItem
title="Creator"
value={data.display.creator}
parseUrl
/>
<LinkOrTextDescriptionItem
title="Link"
value={data.display.link}
parseUrl
/>
<LinkOrTextDescriptionItem
title="Website"
value={data.display.project_url}
parseUrl
/>
</DescriptionList>
</div>
) : null}
</div>
{imgUrl !== '' && (
<div className="flex flex-col gap-5 border-0 border-t border-solid border-gray-45 pt-6 md:basis-1/3 md:border-t-0 md:pl-10">
<div className="flex w-40 justify-center md:w-50">
<DisplayBox
display={imgUrl}
caption={
name ||
trimStdLibPrefix(data.objType)
}
fileInfo={fileType}
modalImage={[
isImageFullScreen,
setImageFullScreen,
]}
/>
</div>
<div className="flex flex-col gap-2.5">
{name && (
<Heading
variant="heading4/semibold"
color="gray-90"
>
{name}
</Heading>
)}
{fileType && (
<Text
variant="bodySmall/medium"
color="steel-darker"
>
{fileType}
</Text>
)}
<button
type="button"
onClick={handlePreviewClick}
className="flex gap-1 text-caption font-semibold uppercase text-steel-dark"
>
Preview <PreviewMediaIcon />
</button>
<div className="border-0 border-t border-solid border-gray-45 pt-6 md:basis-1/3 md:border-t-0 md:pl-10">
<div className="flex flex-row flex-nowrap gap-5">
<div className="flex w-40 justify-center md:w-50">
<DisplayBox
display={imgUrl}
caption={
name ||
trimStdLibPrefix(
data.objType
)
}
fileInfo={fileType}
modalImage={[
isImageFullScreen,
setImageFullScreen,
]}
/>
</div>
<div className="flex flex-col justify-center gap-2.5">
{name && (
<Heading
variant="heading4/semibold"
color="gray-90"
>
{name}
</Heading>
)}
{fileType && (
<Text
variant="bodySmall/medium"
color="steel-darker"
>
{fileType}
</Text>
)}
<div>
<Link
size="captionSmall"
uppercase
onClick={handlePreviewClick}
after={
<ArrowRight12 className="-rotate-45" />
}
>
Preview
</Link>
</div>
</div>
</div>
</div>
)}
Expand Down
11 changes: 9 additions & 2 deletions apps/explorer/src/ui/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ const linkStyles = cva([], {
variants: {
variant: {
text: 'text-body font-semibold text-steel-dark hover:text-steel-darker active:text-steel disabled:text-gray-60',
mono: 'font-mono text-bodySmall font-medium text-sui-dark break-all',
mono: 'font-mono text-bodySmall font-medium text-hero-dark hover:text-hero-darkest break-all',
textHeroDark:
'text-p1 font-medium text-hero-dark hover:text-hero-darkest',
},
uppercase: {
true: 'uppercase',
},
size: {
md: '!text-body',
sm: '!text-bodySmall',
captionSmall: '!text-captionSmall',
},
},
defaultVariants: {
Expand All @@ -34,14 +37,18 @@ export interface LinkProps

export function Link({
variant,
uppercase,
size,
before,
after,
children,
...props
}: LinkProps) {
return (
<ButtonOrLink className={linkStyles({ variant, size })} {...props}>
<ButtonOrLink
className={linkStyles({ variant, size, uppercase })}
{...props}
>
<div className="inline-flex flex-nowrap items-center gap-2">
{before}
{children}
Expand Down
Loading

0 comments on commit f249d2f

Please sign in to comment.