1
0
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:
Jamie Hewitt 2022-11-07 23:09:19 +03:00 committed by GitHub
parent 124fd1d137
commit 81341cd914
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 254 additions and 35 deletions

View 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 })
}))

View File

@ -1,23 +1,3 @@
import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia'
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 })
}))
import './__mocks__/hooksMocks'

2
package-lock.json generated
View File

@ -97,7 +97,7 @@
"typescript": "^4.8.4"
},
"engines": {
"node": ">=14"
"node": "16"
}
},
"node_modules/@adobe/css-tools": {

View File

@ -24,6 +24,8 @@ export declare type AssetListProps = {
onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string
noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
}
export default function AssetList({
@ -34,7 +36,9 @@ export default function AssetList({
isLoading,
onPageChange,
className,
noPublisher
noPublisher,
noDescription,
noPrice
}: AssetListProps): ReactElement {
const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] =
@ -74,6 +78,8 @@ export default function AssetList({
asset={assetWithPrice}
key={assetWithPrice.id}
noPublisher={noPublisher}
noDescription={noDescription}
noPrice={noPrice}
/>
))
) : (

View File

@ -21,7 +21,7 @@
}
.detailLine {
margin-bottom: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 4);
}
.content {
@ -43,8 +43,12 @@
overflow-wrap: break-word;
}
.price {
margin-top: calc(var(--spacer) / 12);
}
.footer {
margin-top: calc(var(--spacer) / 4);
margin-top: calc(var(--spacer) / 12);
}
.typeLabel {

View File

@ -14,11 +14,15 @@ import { useUserPreferences } from '@context/UserPreferences'
export declare type AssetTeaserProps = {
asset: AssetExtended
noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
}
export default function AssetTeaser({
asset,
noPublisher
noPublisher,
noDescription,
noPrice
}: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata
const { datatokens } = asset
@ -53,16 +57,23 @@ export default function AssetTeaser({
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
{!noDescription && (
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot>
</div>
)}
{!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}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>

View File

@ -15,6 +15,7 @@ import NetworkName from '@shared/NetworkName'
import content from '../../../../content/purgatory.json'
import Web3 from 'web3'
import Button from '@shared/atoms/Button'
import RelatedAssets from '../RelatedAssets'
export default function AssetContent({
asset
@ -78,6 +79,7 @@ export default function AssetContent({
</Button>
</div>
)}
<RelatedAssets />
</div>
</article>
</>

View 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
}

View File

@ -0,0 +1,8 @@
.section {
margin-top: calc(var(--spacer) * 2);
}
.section > h3 {
font-size: var(--font-size-large);
color: var(--color-secondary);
}

View 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
})
})

View 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>
)
}