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

feat(wallet-dashboard): style selected visual Assets. #4085

Open
wants to merge 19 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4074f2b
feat(wallet-dashboard): style selected visual Assets.
panteleymonchuk Nov 15, 2024
9bb5dd0
refactor(core): destructure metaKeys and metaValues from attributes
panteleymonchuk Nov 25, 2024
1f2eb82
refactor(wallet): move Collapsible component to core.
panteleymonchuk Nov 25, 2024
f9530b1
feat(dashboard): integrate useAssetsDialog for asset details view
panteleymonchuk Nov 25, 2024
934cd96
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Nov 26, 2024
d055db0
fix(assets): update import path and enhance text styling in DetailsView
panteleymonchuk Nov 26, 2024
2a2daeb
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Nov 27, 2024
6304492
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Nov 27, 2024
c22a756
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Nov 27, 2024
124ddec
Merge branch 'develop' into tooling-dashboard/style-selected-asset
panteleymonchuk Nov 28, 2024
fc404d8
refactor(wallet-dashboard): move state to page
panteleymonchuk Nov 28, 2024
7bd0c26
Merge branch 'develop' into tooling-dashboard/style-selected-asset
panteleymonchuk Nov 28, 2024
61a13f8
refactor(wallet-dashboard): rename handler functions for consistency …
panteleymonchuk Nov 28, 2024
ed3e381
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Nov 29, 2024
336468e
fix(dashboard): PR conflicts
panteleymonchuk Nov 29, 2024
4ce390b
refactor(dashboard): update state for asset view, improve code
panteleymonchuk Nov 29, 2024
16110bb
Merge branch 'develop' into tooling-dashboard/style-selected-asset
brancoder Nov 29, 2024
0bd5c50
Merge remote-tracking branch 'origin/develop' into tooling-dashboard/…
panteleymonchuk Dec 2, 2024
84b2dba
fix(wallet-dashboard): unify asset transfer success and error handlin…
panteleymonchuk Dec 2, 2024
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
4 changes: 4 additions & 0 deletions apps/core/src/components/collapsible/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './Collapsible';
2 changes: 1 addition & 1 deletion apps/core/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export * from './coin';
export * from './icon';
export * from './Inputs';
export * from './QR';

export * from './collapsible';
export * from './providers';
3 changes: 3 additions & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,8 @@ export * from './useTransactionData';
export * from './useGetStakingValidatorDetails';
export * from './useCursorPagination';
export * from './useTheme';
export * from './useNFTBasicData';
export * from './useOwnedNFT';
export * from './useNftDetails';

export * from './stake';
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { IotaObjectData } from '@iota/iota-sdk/client';
import useFileExtensionType from './useFileExtensionType';
import useMediaUrl from './useMediaUrl';

export default function useNFTBasicData(nftObj: IotaObjectData | null) {
export function useNFTBasicData(nftObj: IotaObjectData | null) {
const nftObjectID = nftObj?.objectId || null;
const filePath = useMediaUrl(nftObj?.content || null);
let objType = null;
Expand Down
85 changes: 85 additions & 0 deletions apps/core/src/hooks/useNftDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import { useGetNFTMeta, useOwnedNFT, useNFTBasicData, useGetKioskContents } from './';
import { formatAddress } from '@iota/iota-sdk/utils';
import { isAssetTransferable, truncateString } from '../utils';

type NftFields = {
metadata?: { fields?: { attributes?: { fields?: { keys: string[]; values: string[] } } } };
};

export function useNftDetails(nftId: string, accountAddress: string | null) {
const { data: objectData, isPending: isNftLoading } = useOwnedNFT(nftId || '', accountAddress);
const { data } = useGetKioskContents(accountAddress);

const isContainedInKiosk = data?.lookup.get(nftId!);
const kioskItem = data?.list.find((k) => k.data?.objectId === nftId);

const isTransferable = isAssetTransferable(objectData);

const { nftFields } = useNFTBasicData(objectData);

const { data: nftMeta, isPending: isPendingMeta } = useGetNFTMeta(nftId);

const nftName = nftMeta?.name || formatAddress(nftId);
const nftImageUrl = nftMeta?.imageUrl || '';

// Extract either the attributes, or use the top-level NFT fields:
const { keys: metaKeys, values: metaValues } =
(nftFields as NftFields)?.metadata?.fields?.attributes?.fields ||
Object.entries(nftFields ?? {})
.filter(([key]) => key !== 'id')
.reduce(
(acc, [key, value]) => {
acc.keys.push(key);
acc.values.push(value as string);
return acc;
},
{ keys: [] as string[], values: [] as string[] },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Line 8 and this one could share a type

);

const ownerAddress =
(objectData?.owner &&
typeof objectData?.owner === 'object' &&
'AddressOwner' in objectData.owner &&
objectData.owner.AddressOwner) ||
'';

function formatMetaValue(value: string | object) {
if (typeof value === 'object') {
return {
value: JSON.stringify(value),
valueLink: undefined,
};
} else {
if (value.includes('http')) {
return {
value: value.startsWith('http')
? truncateString(value, 20, 8)
: formatAddress(value),
valueLink: value,
};
}
return {
value: value,
valueLink: undefined,
};
}
}

return {
objectData,
isNftLoading,
nftName,
nftImageUrl,
ownerAddress,
isTransferable,
metaKeys,
metaValues,
formatMetaValue,
isContainedInKiosk,
kioskItem,
nftMeta,
isPendingMeta,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { useGetKioskContents, useGetObject } from '@iota/core';
import { useGetKioskContents, useGetObject } from './';
import { useMemo } from 'react';

export function useOwnedNFT(nftObjectId: string | null, address: string | null) {
Expand Down
1 change: 1 addition & 0 deletions apps/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './getDelegationDataByStakeId';
export * from './api-env';
export * from './getExplorerPaths';
export * from './getExplorerLink';
export * from './truncateString';

export * from './stake';
export * from './transaction';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ArrowDown } from '@iota/ui-icons';
import { Button, ButtonType } from '@/lib';
import { ICON_STYLE } from './accordion.classes';

interface AccordionHeaderProps {
export interface AccordionHeaderProps {
/**
* Flag for show/hide content
*/
Expand Down
23 changes: 23 additions & 0 deletions apps/wallet-dashboard/app/(protected)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IotaObjectData } from '@iota/iota-sdk/client';
import { useState } from 'react';
import { AssetCategory } from '@/lib/enums';
import { AssetList } from '@/components/AssetsList';
import { AssetsDialog, AssetsDialogView } from '@/components/Dialogs/Assets';

const OBJECTS_PER_REQ = 50;

Expand All @@ -25,6 +26,8 @@ const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [
];

export default function AssetsDashboardPage(): React.JSX.Element {
const [view, setView] = useState<AssetsDialogView | undefined>(AssetsDialogView.Details);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the view here is not necessary, it could be moved inside the AssetsDialog

const [selectedAsset, setSelectedAsset] = useState<IotaObjectData | null>(null);
const [selectedCategory, setSelectedCategory] = useState<AssetCategory>(AssetCategory.Visual);
const account = useCurrentAccount();
const { data, isFetching, fetchNextPage, hasNextPage } = useGetOwnedObjects(
Expand All @@ -49,6 +52,16 @@ export default function AssetsDashboardPage(): React.JSX.Element {
}
}

function onClickAsset(asset: IotaObjectData) {
setSelectedAsset(asset);
setView(AssetsDialogView.Details);
}

function onCloseDialog() {
setSelectedAsset(null);
setView(undefined);
}

return (
<Panel>
<Title title="Assets" size={TitleSize.Medium} />
Expand All @@ -67,10 +80,20 @@ export default function AssetsDashboardPage(): React.JSX.Element {
<AssetList
assets={assets}
selectedCategory={selectedCategory}
onClick={onClickAsset}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetching}
fetchNextPage={fetchNextPage}
/>
{view && (
<AssetsDialog
view={view}
setView={setView}
isOpen={!!selectedAsset}
onClose={onCloseDialog}
asset={selectedAsset}
/>
)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{view && (
<AssetsDialog
view={view}
setView={setView}
isOpen={!!selectedAsset}
onClose={onCloseDialog}
asset={selectedAsset}
/>
)}
{selectedAsset && (
<AssetsDialog
onClose={() => setSelectedAsset(null)}
asset={selectedAsset}
/>
)}

</div>
</Panel>
);
Expand Down
9 changes: 8 additions & 1 deletion apps/wallet-dashboard/components/AssetsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface AssetListProps {
hasNextPage: boolean;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
onClick: (asset: IotaObjectData) => void;
}

const ASSET_LAYOUT: Record<AssetCategory, string> = {
Expand All @@ -29,6 +30,7 @@ export function AssetList({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
onClick,
}: AssetListProps): React.JSX.Element {
const observerElem = useRef<HTMLDivElement | null>(null);
const { isIntersecting } = useOnScreen(observerElem);
Expand All @@ -43,7 +45,12 @@ export function AssetList({
return (
<div className={cl('max-h-[600px]', ASSET_LAYOUT[selectedCategory])}>
{assets.map((asset) => (
<AssetTileLink key={asset.digest} asset={asset} type={selectedCategory} />
<AssetTileLink
key={asset.digest}
asset={asset}
type={selectedCategory}
onClick={onClick}
/>
))}
<div ref={observerElem}>
{isSpinnerVisible ? (
Expand Down
103 changes: 103 additions & 0 deletions apps/wallet-dashboard/components/Dialogs/Assets/AssetsDialog.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name should be singular, since it refers to a specific Asset, not multiple. Maybe even VisualAssetDialog or simply AssetDialog

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { Dialog } from '@iota/apps-ui-kit';
import { FormikProvider, useFormik } from 'formik';
import { useCurrentAccount } from '@iota/dapp-kit';
import { createNftSendValidationSchema } from '@iota/core';
import { DetailsView, SendView } from './views';
import { IotaObjectData } from '@iota/iota-sdk/client';
import { AssetsDialogView } from './constants';
import { useCreateSendAssetTransaction, useNotifications } from '@/hooks';
import { NotificationType } from '@/stores/notificationStore';
import { ASSETS_ROUTE } from '@/lib/constants/routes.constants';
import { useRouter } from 'next/navigation';

interface AssetsDialogProps {
isOpen: boolean;
onClose: () => void;
asset: IotaObjectData | null;
view: AssetsDialogView;
setView: (view: AssetsDialogView | undefined) => void;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface AssetsDialogProps {
isOpen: boolean;
onClose: () => void;
asset: IotaObjectData | null;
view: AssetsDialogView;
setView: (view: AssetsDialogView | undefined) => void;
}
interface AssetsDialogProps {
onClose: () => void;
asset: IotaObjectData;
}


export interface FormValues {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this interface needs too be exported I would say to rename it to something more specific

to: string;
}

const INITIAL_VALUES: FormValues = {
to: '',
};

export function AssetsDialog({
isOpen,
onClose,
asset,
setView,
view,
}: AssetsDialogProps): JSX.Element {
const account = useCurrentAccount();
const activeAddress = account?.address ?? '';
const objectId = asset?.objectId ?? '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function AssetsDialog({
isOpen,
onClose,
asset,
setView,
view,
}: AssetsDialogProps): JSX.Element {
const account = useCurrentAccount();
const activeAddress = account?.address ?? '';
const objectId = asset?.objectId ?? '';
export function AssetsDialog({
onClose,
asset,
}: AssetsDialogProps): React.JSX.Element {
const [view, setView] = useState(AssetsDialogView.Details);
const account = useCurrentAccount();
const activeAddress = account?.address ?? '';
const objectId = asset?.objectId ?? '';

const router = useRouter();
const { addNotification } = useNotifications();
const validationSchema = createNftSendValidationSchema(activeAddress, objectId);

function onDetailsSend() {
setView(AssetsDialogView.Send);
}

const { mutation: sendAsset } = useCreateSendAssetTransaction(
objectId,
onSendAssetSuccess,
onSendAssetError,
);

const formik = useFormik<FormValues>({
initialValues: INITIAL_VALUES,
validationSchema: validationSchema,
onSubmit: onSubmit,
validateOnChange: true,
});

function onSendAssetSuccess() {
addNotification('Transfer transaction successful', NotificationType.Success);
router.push(ASSETS_ROUTE.path + '/assets');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not necessary, it's the same view, isn't it?

}

function onSendAssetError() {
addNotification('Transfer transaction failed', NotificationType.Error);
}

async function onSubmit(values: FormValues) {
try {
await sendAsset.mutateAsync(values.to);
} catch (error) {
addNotification('Transfer transaction failed', NotificationType.Error);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After sending, first the dialog should be closed and then shown a notification, no?


function onSendClose() {
setView(undefined);
}

function onSendBack() {
setView(AssetsDialogView.Details);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel it's a bit weird that the functions are defined by their view, rather than what they do, like, the function onSendClose should be called handleCloseDialog for example, and onSendBack should be called setDetailsView


return (
<Dialog open={isOpen} onOpenChange={() => onClose()}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<Dialog open={isOpen} onOpenChange={() => onClose()}>
<Dialog open onOpenChange={() => onClose()}>

<FormikProvider value={formik}>
<>
{view === AssetsDialogView.Details && asset && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way asset will always be defined

<DetailsView asset={asset} onClose={onClose} onSend={onDetailsSend} />
)}
{view === AssetsDialogView.Send && asset && (
<SendView asset={asset} onClose={onSendClose} onBack={onSendBack} />
)}
</>
</FormikProvider>
</Dialog>
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of the folder and the file are wrong, this should be an enum folder, and the file shouldn't be pascal case

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export enum AssetsDialogView {
Details = 'Details',
Send = 'Send',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './AssetsDialogView';
5 changes: 5 additions & 0 deletions apps/wallet-dashboard/components/Dialogs/Assets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './AssetsDialog';
export * from './constants';
Loading
Loading