mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Showing related assets (#1748)
* Creating related assets component * Ensuring that related assets doesn't show the same asset * Adjusting query to show assets from the same publisher but not the exact same asset * modifying search term * Removing logs and unused import * Removing log * Updating query * Fixes * updating query * Updating query to include both related tag assets anbd related owner assests when <3. SHowing results as a list of links * creating minimal asset teaser * removing duplicate filters * Changing minimal to noDescription * Removing unneccessary use of noDescription prop * Adding minimal prop back into Publisher * Removing props from RelatedAssets component * Getting data from asset context * refactor * space-saving asset teaser changes * remove price from output * increase to 4 * refactor for better loading experience * css cleanup * filter out duplicates when merging results * basic render test * try/catch, secure against null query responses * different test tactic Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
parent
124fd1d137
commit
81341cd914
20
.jest/__mocks__/hooksMocks.ts
Normal file
20
.jest/__mocks__/hooksMocks.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import marketMetadata from '../__fixtures__/marketMetadata'
|
||||||
|
import userPreferences from '../__fixtures__/userPreferences'
|
||||||
|
import web3 from '../__fixtures__/web3'
|
||||||
|
import { asset } from '../__fixtures__/assetWithAccessDetails'
|
||||||
|
|
||||||
|
jest.mock('../../src/@context/MarketMetadata', () => ({
|
||||||
|
useMarketMetadata: () => marketMetadata
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../../src/@context/UserPreferences', () => ({
|
||||||
|
useUserPreferences: () => userPreferences
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../../src/@context/Web3', () => ({
|
||||||
|
useWeb3: () => web3
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('../../../@context/Asset', () => ({
|
||||||
|
useAsset: () => ({ asset })
|
||||||
|
}))
|
@ -1,23 +1,3 @@
|
|||||||
import '@testing-library/jest-dom/extend-expect'
|
import '@testing-library/jest-dom/extend-expect'
|
||||||
import './__mocks__/matchMedia'
|
import './__mocks__/matchMedia'
|
||||||
|
import './__mocks__/hooksMocks'
|
||||||
import marketMetadata from './__fixtures__/marketMetadata'
|
|
||||||
import userPreferences from './__fixtures__/userPreferences'
|
|
||||||
import web3 from './__fixtures__/web3'
|
|
||||||
import { asset } from './__fixtures__/assetWithAccessDetails'
|
|
||||||
|
|
||||||
jest.mock('../../src/@context/MarketMetadata', () => ({
|
|
||||||
useMarketMetadata: () => marketMetadata
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../../src/@context/UserPreferences', () => ({
|
|
||||||
useUserPreferences: () => userPreferences
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../../src/@context/Web3', () => ({
|
|
||||||
useWeb3: () => web3
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('../../../@context/Asset', () => ({
|
|
||||||
useAsset: () => ({ asset })
|
|
||||||
}))
|
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -97,7 +97,7 @@
|
|||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": "16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
|
@ -24,6 +24,8 @@ export declare type AssetListProps = {
|
|||||||
onPageChange?: React.Dispatch<React.SetStateAction<number>>
|
onPageChange?: React.Dispatch<React.SetStateAction<number>>
|
||||||
className?: string
|
className?: string
|
||||||
noPublisher?: boolean
|
noPublisher?: boolean
|
||||||
|
noDescription?: boolean
|
||||||
|
noPrice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssetList({
|
export default function AssetList({
|
||||||
@ -34,7 +36,9 @@ export default function AssetList({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onPageChange,
|
onPageChange,
|
||||||
className,
|
className,
|
||||||
noPublisher
|
noPublisher,
|
||||||
|
noDescription,
|
||||||
|
noPrice
|
||||||
}: AssetListProps): ReactElement {
|
}: AssetListProps): ReactElement {
|
||||||
const { accountId } = useWeb3()
|
const { accountId } = useWeb3()
|
||||||
const [assetsWithPrices, setAssetsWithPrices] =
|
const [assetsWithPrices, setAssetsWithPrices] =
|
||||||
@ -74,6 +78,8 @@ export default function AssetList({
|
|||||||
asset={assetWithPrice}
|
asset={assetWithPrice}
|
||||||
key={assetWithPrice.id}
|
key={assetWithPrice.id}
|
||||||
noPublisher={noPublisher}
|
noPublisher={noPublisher}
|
||||||
|
noDescription={noDescription}
|
||||||
|
noPrice={noPrice}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.detailLine {
|
.detailLine {
|
||||||
margin-bottom: calc(var(--spacer) / 2);
|
margin-bottom: calc(var(--spacer) / 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@ -43,8 +43,12 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
margin-top: calc(var(--spacer) / 12);
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: calc(var(--spacer) / 4);
|
margin-top: calc(var(--spacer) / 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.typeLabel {
|
.typeLabel {
|
||||||
|
@ -14,11 +14,15 @@ import { useUserPreferences } from '@context/UserPreferences'
|
|||||||
export declare type AssetTeaserProps = {
|
export declare type AssetTeaserProps = {
|
||||||
asset: AssetExtended
|
asset: AssetExtended
|
||||||
noPublisher?: boolean
|
noPublisher?: boolean
|
||||||
|
noDescription?: boolean
|
||||||
|
noPrice?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssetTeaser({
|
export default function AssetTeaser({
|
||||||
asset,
|
asset,
|
||||||
noPublisher
|
noPublisher,
|
||||||
|
noDescription,
|
||||||
|
noPrice
|
||||||
}: AssetTeaserProps): ReactElement {
|
}: AssetTeaserProps): ReactElement {
|
||||||
const { name, type, description } = asset.metadata
|
const { name, type, description } = asset.metadata
|
||||||
const { datatokens } = asset
|
const { datatokens } = asset
|
||||||
@ -53,16 +57,23 @@ export default function AssetTeaser({
|
|||||||
</Dotdotdot>
|
</Dotdotdot>
|
||||||
{!noPublisher && <Publisher account={owner} minimal />}
|
{!noPublisher && <Publisher account={owner} minimal />}
|
||||||
</header>
|
</header>
|
||||||
<div className={styles.content}>
|
{!noDescription && (
|
||||||
<Dotdotdot tagName="p" clamp={3}>
|
<div className={styles.content}>
|
||||||
{removeMarkdown(description?.substring(0, 300) || '')}
|
<Dotdotdot tagName="p" clamp={3}>
|
||||||
</Dotdotdot>
|
{removeMarkdown(description?.substring(0, 300) || '')}
|
||||||
</div>
|
</Dotdotdot>
|
||||||
{isUnsupportedPricing || !asset.services.length ? (
|
</div>
|
||||||
<strong>No pricing schema available</strong>
|
|
||||||
) : (
|
|
||||||
<Price accessDetails={asset.accessDetails} size="small" />
|
|
||||||
)}
|
)}
|
||||||
|
{!noPrice && (
|
||||||
|
<div className={styles.price}>
|
||||||
|
{isUnsupportedPricing || !asset.services.length ? (
|
||||||
|
<strong>No pricing schema available</strong>
|
||||||
|
) : (
|
||||||
|
<Price accessDetails={asset.accessDetails} size="small" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<footer className={styles.footer}>
|
<footer className={styles.footer}>
|
||||||
{allocated && allocated > 0 ? (
|
{allocated && allocated > 0 ? (
|
||||||
<span className={styles.typeLabel}>
|
<span className={styles.typeLabel}>
|
||||||
|
@ -15,6 +15,7 @@ import NetworkName from '@shared/NetworkName'
|
|||||||
import content from '../../../../content/purgatory.json'
|
import content from '../../../../content/purgatory.json'
|
||||||
import Web3 from 'web3'
|
import Web3 from 'web3'
|
||||||
import Button from '@shared/atoms/Button'
|
import Button from '@shared/atoms/Button'
|
||||||
|
import RelatedAssets from '../RelatedAssets'
|
||||||
|
|
||||||
export default function AssetContent({
|
export default function AssetContent({
|
||||||
asset
|
asset
|
||||||
@ -78,6 +79,7 @@ export default function AssetContent({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<RelatedAssets />
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</>
|
</>
|
||||||
|
33
src/components/Asset/RelatedAssets/_utils.ts
Normal file
33
src/components/Asset/RelatedAssets/_utils.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { SortTermOptions } from '../../../@types/aquarius/SearchQuery'
|
||||||
|
|
||||||
|
export function generateQuery(
|
||||||
|
chainIds: number[],
|
||||||
|
nftAddress: string,
|
||||||
|
size: number,
|
||||||
|
tags?: string[],
|
||||||
|
owner?: string
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
chainIds,
|
||||||
|
esPaginationOptions: {
|
||||||
|
size
|
||||||
|
},
|
||||||
|
nestedQuery: {
|
||||||
|
must_not: {
|
||||||
|
term: { 'nftAddress.keyword': nftAddress }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filters: [
|
||||||
|
tags && {
|
||||||
|
terms: { 'metadata.tags.keyword': tags }
|
||||||
|
},
|
||||||
|
owner && { term: { 'nft.owner.keyword': owner } }
|
||||||
|
],
|
||||||
|
sort: {
|
||||||
|
'stats.orders': 'desc'
|
||||||
|
},
|
||||||
|
sortOptions: {
|
||||||
|
sortBy: SortTermOptions.Orders
|
||||||
|
} as SortOptions
|
||||||
|
} as BaseQueryParams
|
||||||
|
}
|
8
src/components/Asset/RelatedAssets/index.module.css
Normal file
8
src/components/Asset/RelatedAssets/index.module.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.section {
|
||||||
|
margin-top: calc(var(--spacer) * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section > h3 {
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
67
src/components/Asset/RelatedAssets/index.test.tsx
Normal file
67
src/components/Asset/RelatedAssets/index.test.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
import RelatedAssets from '.'
|
||||||
|
import { assets } from '../../../../.jest/__fixtures__/assetsWithAccessDetails'
|
||||||
|
import { queryMetadata } from '../../../@utils/aquarius'
|
||||||
|
// import * as userPreferencesMock from '../../../@context/UserPreferences'
|
||||||
|
|
||||||
|
jest.mock('../../../@utils/aquarius')
|
||||||
|
// jest.mock('../../src/@context/UserPreferences', () => ({
|
||||||
|
// useUserPreferences: () => ({ chainIds: [1, 2, 3] })
|
||||||
|
// }))
|
||||||
|
|
||||||
|
const queryMetadataBaseReturn: PagedAssets = {
|
||||||
|
results: assets,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
totalResults: 10,
|
||||||
|
aggregations: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Asset/RelatedAssets', () => {
|
||||||
|
beforeAll(() => jest.resetAllMocks())
|
||||||
|
|
||||||
|
it('renders with more than 4 queryMetadata() results', async () => {
|
||||||
|
;(queryMetadata as jest.Mock).mockReturnValue(queryMetadataBaseReturn)
|
||||||
|
render(<RelatedAssets />)
|
||||||
|
await screen.findByText(assets[0].metadata.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with 4 queryMetadata() results', async () => {
|
||||||
|
;(queryMetadata as jest.Mock).mockReturnValue({
|
||||||
|
...queryMetadataBaseReturn,
|
||||||
|
results: assets.slice(0, 4),
|
||||||
|
totalResults: 4
|
||||||
|
})
|
||||||
|
render(<RelatedAssets />)
|
||||||
|
await screen.findByText(assets[0].metadata.name)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: figure out how to overwrite already mocked module
|
||||||
|
// it('does nothing when no chainIds selected', async () => {
|
||||||
|
// jest
|
||||||
|
// .spyOn(userPreferencesMock, 'useUserPreferences')
|
||||||
|
// .mockReturnValue({ chainIds: [] } as any)
|
||||||
|
|
||||||
|
// render(<RelatedAssets />)
|
||||||
|
// await screen.findByText('No results found')
|
||||||
|
// })
|
||||||
|
|
||||||
|
it('catches queryMetadata errors', async () => {
|
||||||
|
;(queryMetadata as jest.Mock).mockImplementation(() => {
|
||||||
|
throw new Error('Hello error')
|
||||||
|
})
|
||||||
|
|
||||||
|
// prevent console error from showing up in test log
|
||||||
|
const originalError = console.error
|
||||||
|
console.error = jest.fn()
|
||||||
|
|
||||||
|
try {
|
||||||
|
render(<RelatedAssets />)
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toEqual({ message: 'Hello error' })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error = originalError
|
||||||
|
})
|
||||||
|
})
|
88
src/components/Asset/RelatedAssets/index.tsx
Normal file
88
src/components/Asset/RelatedAssets/index.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import React, { ReactElement, useEffect, useState } from 'react'
|
||||||
|
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
|
||||||
|
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
|
||||||
|
import { useUserPreferences } from '@context/UserPreferences'
|
||||||
|
import { useAsset } from '@context/Asset'
|
||||||
|
import styles from './index.module.css'
|
||||||
|
import { useCancelToken } from '@hooks/useCancelToken'
|
||||||
|
import AssetList from '@shared/AssetList'
|
||||||
|
import { generateQuery } from './_utils'
|
||||||
|
|
||||||
|
export default function RelatedAssets(): ReactElement {
|
||||||
|
const { asset } = useAsset()
|
||||||
|
const { chainIds } = useUserPreferences()
|
||||||
|
const newCancelToken = useCancelToken()
|
||||||
|
|
||||||
|
const [relatedAssets, setRelatedAssets] = useState<Asset[]>()
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!chainIds?.length ||
|
||||||
|
!asset?.nftAddress ||
|
||||||
|
!asset?.nft ||
|
||||||
|
!asset?.metadata
|
||||||
|
) {
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAssets() {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tagQuery = generateBaseQuery(
|
||||||
|
generateQuery(chainIds, asset.nftAddress, 4, asset.metadata.tags)
|
||||||
|
)
|
||||||
|
const tagResults = (await queryMetadata(tagQuery, newCancelToken()))
|
||||||
|
?.results
|
||||||
|
|
||||||
|
if (tagResults.length === 4) {
|
||||||
|
setRelatedAssets(tagResults)
|
||||||
|
} else {
|
||||||
|
const ownerQuery = generateBaseQuery(
|
||||||
|
generateQuery(
|
||||||
|
chainIds,
|
||||||
|
asset.nftAddress,
|
||||||
|
4 - tagResults.length,
|
||||||
|
null,
|
||||||
|
asset.nft.owner
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const ownerResults = (
|
||||||
|
await queryMetadata(ownerQuery, newCancelToken())
|
||||||
|
)?.results
|
||||||
|
|
||||||
|
// combine both results, and filter out duplicates
|
||||||
|
// stolen from: https://stackoverflow.com/a/70326769/733677
|
||||||
|
const bothResults = tagResults.concat(
|
||||||
|
ownerResults.filter(
|
||||||
|
(asset2) => !tagResults.find((asset1) => asset1.id === asset2.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setRelatedAssets(bothResults)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
LoggerInstance.error(error.message)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getAssets()
|
||||||
|
}, [chainIds, asset, newCancelToken])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h3>Related Assets</h3>
|
||||||
|
<AssetList
|
||||||
|
assets={relatedAssets}
|
||||||
|
showPagination={false}
|
||||||
|
isLoading={isLoading}
|
||||||
|
noDescription
|
||||||
|
noPublisher
|
||||||
|
noPrice
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user