1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Merge branch 'main' into feature/issue-1657-shacl-schema

This commit is contained in:
Matthias Kretschmann 2022-11-07 20:45:55 +00:00 committed by GitHub
commit b400f73d04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 6714 additions and 2173 deletions

View File

@ -0,0 +1,71 @@
import { Asset } from '@oceanprotocol/lib'
export const assetAquarius: Asset = {
'@context': ['https://w3id.org/did/v1'],
id: 'did:op:6654b0793765b269696cec8d2f0d077d9bbcdd3c4f033d941ab9684e8ad06630',
nftAddress: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
version: '4.1.0',
chainId: 5,
metadata: {
created: '2022-09-29T11:30:26Z',
updated: '2022-09-29T11:30:26Z',
type: 'dataset',
name: 'Testitest',
description: 'This is a test.',
tags: [],
author: 'Test User',
license: 'https://market.oceanprotocol.com/terms',
additionalInformation: {
termsAndConditions: true
}
},
services: [
{
id: 'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1',
type: 'access',
files:
'0x04022ef1afafe340f41b261ef721b8dd55dee094975cc70330803d760beef38871948ce572ff1c533d56cda2665749ed2eb8283e243ec5ee19011f510b6b263b2da0af537e3f1fdff7ddd90fa26c7a4761a6d26928bc1348a302634012aac7998e92c84456ab73e9a847120c44ebda15781787e8c382391b2eaefc8b8d36998f3998d1c4647f4f7bb28f4278093c1d231f66e78f81452049443b9e540aeb42ebbdc1b748c024eb10218532814736e241efa1c2a687685b4e2ea7a877685aa0ea325d1a8cf765d1b423b32d81ec3c3e22fc9c15c6b9b71f2862edaec4e4cf7c3a638ffc0ecb88ede3cabb511d4780543a53c001a95f42de1877796e13c997b57bc671507e92198934b4ea7c2e6554993388421253e8c2f10458dec872a7ebfa71b6e77ed359222c93261ba252028c5da06ccf8defcd529885b2125816325a47e23728b513',
datatokenAddress: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
serviceEndpoint: 'https://v4.provider.goerli.oceanprotocol.com',
timeout: 604800
}
],
event: {
tx: '0x3e07a75c1cc5d4146222a93ab4319144e60ecca3ebfb8b15f1ff339d6f479dc9',
block: 7680195,
from: '0x903322C7E45A60d7c8C3EA236c5beA9Af86310c7',
contract: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
datetime: '2022-09-29T11:31:12'
},
nft: {
address: '0xbA5BA7B09e2FA1eb0258f647503F81D2Af5cb07d',
name: 'Ocean Data NFT',
symbol: 'OCEAN-NFT',
state: 0,
tokenURI:
'data:application/json;base64,eyJuYW1lIjoiT2NlYW4gRGF0YSBORlQiLCJzeW1ib2wiOiJPQ0VBTi1ORlQiLCJkZXNjcmlwdGlvbiI6IlRoaXMgTkZUIHJlcHJlc2VudHMgYW4gYXNzZXQgaW4gdGhlIE9jZWFuIFByb3RvY29sIHY0IGVjb3N5c3RlbS5cblxuVmlldyBvbiBPY2VhbiBNYXJrZXQ6IGh0dHBzOi8vbWFya2V0Lm9jZWFucHJvdG9jb2wuY29tL2Fzc2V0L2RpZDpvcDo2NjU0YjA3OTM3NjViMjY5Njk2Y2VjOGQyZjBkMDc3ZDliYmNkZDNjNGYwMzNkOTQxYWI5Njg0ZThhZDA2NjMwIiwiZXh0ZXJuYWxfdXJsIjoiaHR0cHM6Ly9tYXJrZXQub2NlYW5wcm90b2NvbC5jb20vYXNzZXQvZGlkOm9wOjY2NTRiMDc5Mzc2NWIyNjk2OTZjZWM4ZDJmMGQwNzdkOWJiY2RkM2M0ZjAzM2Q5NDFhYjk2ODRlOGFkMDY2MzAiLCJiYWNrZ3JvdW5kX2NvbG9yIjoiMTQxNDE0IiwiaW1hZ2VfZGF0YSI6ImRhdGE6aW1hZ2Uvc3ZnK3htbCwlM0Nzdmcgdmlld0JveD0nMCAwIDk5IDk5JyBmaWxsPSd1bmRlZmluZWQnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyclM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5Mjc3JyBkPSdNMCw5OUwwLDI5QzksMjUgMTksMjIgMjksMjFDMzgsMTkgNDksMTkgNjEsMjFDNzIsMjIgODUsMjUgOTksMjlMOTksOTlaJy8lM0UlM0NwYXRoIGZpbGw9JyUyM2ZmNDA5MmJiJyBkPSdNMCw5OUwwLDU1QzgsNDkgMTcsNDQgMjgsNDNDMzgsNDEgNTAsNDIgNjMsNDNDNzUsNDMgODcsNDIgOTksNDJMOTksOTlaJyUzRSUzQy9wYXRoJTNFJTNDcGF0aCBmaWxsPSclMjNmZjQwOTJmZicgZD0nTTAsOTlMMCw2OEMxMSw2NiAyMiw2NSAzMiw2N0M0MSw2OCA1MCw3MyA2MSw3NkM3MSw3OCA4NSw3OCA5OSw3OUw5OSw5OVonJTNFJTNDL3BhdGglM0UlM0Mvc3ZnJTNFIn0=',
owner: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
created: '2022-09-29T11:31:12'
},
datatokens: [
{
address: '0x067e1E6ec580F3F0f6781679A4A5AB07A6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59',
serviceId:
'dbc42f4c62d2452f8731fd023eacfae74e9c7a42fbd12ce84310f13342e4aab1'
}
],
stats: {
orders: 22
// price: {
// value: 3231343254,
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN'
// }
},
purgatory: {
state: false,
reason: ''
}
}

View File

@ -0,0 +1,26 @@
import { assetAquarius } from './assetAquarius'
export const asset: AssetExtended = {
...assetAquarius,
accessDetails: {
publisherMarketOrderFee: '0',
type: 'fixed',
addressOrId:
'0x00e3b740e4d8bf6e97010ecb5b14d1b7efc0913bfa291fcf5adb8eb9e6c29e93',
price: '3231343254',
isPurchasable: true,
isOwned: false,
validOrderTx: null,
baseToken: {
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
name: 'Ocean Token',
symbol: 'OCEAN',
decimals: 18
},
datatoken: {
address: '0x067e1e6ec580f3f0f6781679a4a5ab07a6464b08',
name: 'Stupendous Orca Token',
symbol: 'STUORC-59'
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
export default {
prices: {
h2o: {
btc: 0.00009013,
cad: 2.38,
cny: 12.36,
eth: 0.00132713,
eur: 1.78,
gbp: 1.55,
hkd: 13.53,
inr: 141.78,
jpy: 253.21,
link: 0.24050954,
rub: 109.81,
sgd: 2.47,
usd: 1.72
},
'matic-network': {
btc: 0.0000414,
cad: 1.093,
cny: 5.67,
eth: 0.00061102,
eur: 0.816959,
gbp: 0.715112,
hkd: 6.21,
inr: 65.06,
jpy: 116.23,
link: 0.11072143,
rub: 50.4,
sgd: 1.14,
usd: 0.790922
},
ethereum: {
btc: 0.06775775,
cad: 1789.03,
cny: 9288.09,
eth: 1,
eur: 1337.1,
gbp: 1170.41,
hkd: 10161.71,
inr: 106490,
jpy: 190239,
link: 181.216,
rub: 82491,
sgd: 1859.58,
usd: 1294.49
},
'ocean-protocol': {
btc: 0.00000809,
cad: 0.213554,
cny: 1.11,
eth: 0.00011937,
eur: 0.159608,
gbp: 0.13971,
hkd: 1.21,
inr: 12.71,
jpy: 22.71,
link: 0.02163146,
rub: 9.85,
sgd: 0.221976,
usd: 0.154521
}
}
}

View File

@ -0,0 +1,41 @@
export const columns = [
{
name: 'Name',
selector: (row: any) => row.name,
maxWidth: '45rem',
grow: 1
},
{
name: 'Symbol',
selector: (row: any) => row.symbol,
maxWidth: '10rem'
},
{
name: 'Price',
selector: (row: any) => row.price,
right: true
}
]
export const data = [
{
name: 'Title asset',
symbol: 'DATA-70',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
},
{
name: 'Title asset',
symbol: 'DATA-72',
price: '1.011'
},
{
name: 'Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset Title asset',
symbol: 'DATA-71',
price: '1.011'
}
]

View File

@ -0,0 +1,10 @@
export default {
debug: true,
currency: 'EUR',
locale: 'en-US',
chainIds: [5, 1, 137, 56, 1285, 246],
bookmarks: [],
privacyPolicySlug: '/privacy/en',
showPPC: true,
infiniteApproval: false
}

View File

@ -0,0 +1,33 @@
export default {
accountEns: 'jellymcjellyfish.eth',
accountEnsAvatar:
'https://metadata.ens.domains/mainnet/avatar/jellymcjellyfish.eth',
accountId: '0x99840Df5Cb42faBE0Feb8811Aaa4BC99cA6C84e0',
approvedBaseTokens: [
{
address: '0xcfdda22c9837ae76e0faa845354f33c62e03653a',
symbol: 'OCEAN',
name: 'Ocean Token',
decimals: 18
}
],
balance: { eth: '0', ocean: '1000' },
block: 7751969,
chainId: 5,
connect: jest.fn(),
isSupportedOceanNetwork: true,
isTestnet: true,
logout: jest.fn(),
networkData: { name: 'Görli', title: 'Ethereum Testnet Görli', chain: 'ETH' },
networkDisplayName: 'ETH Görli',
networkId: 5,
web3: { currentProvider: {} },
web3Loading: false,
web3Modal: { show: false, eventController: {}, connect: jest.fn() },
web3Provider: {},
web3ProviderInfo: {
id: 'injected',
name: 'MetaMask',
logo: ''
}
}

View File

@ -0,0 +1,3 @@
import { assets } from '../../__fixtures__/assetsWithAccessDetails'
export const getAccessDetailsForAssets = jest.fn().mockResolvedValue(assets)

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

@ -0,0 +1 @@
export default true

View File

@ -1,7 +1,3 @@
import '@testing-library/jest-dom/extend-expect' import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia' import './__mocks__/matchMedia'
import marketMetadataMock from './__mocks__/MarketMetadata' import './__mocks__/hooksMocks'
jest.mock('../../src/@context/MarketMetadata', () => ({
useMarketMetadata: () => marketMetadataMock
}))

View File

@ -14,7 +14,7 @@ module.exports = {
chainIds: [1, 137, 56, 246, 1285, 8996], chainIds: [1, 137, 56, 246, 1285, 8996],
// List of all supported chainIds. Used to populate the Chains user preferences list. // List of all supported chainIds. Used to populate the Chains user preferences list.
chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001, 1287, 8996], chainIdsSupported: [1, 137, 56, 246, 1285, 5, 80001],
infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx', infuraProjectId: process.env.NEXT_PUBLIC_INFURA_PROJECT_ID || 'xxx',

4424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@
"format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write", "format": "prettier --ignore-path .gitignore './**/*.{css,yml,js,ts,tsx,json}' --write",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"deploy:s3": "bash scripts/deploy-s3.sh", "deploy:s3": "bash scripts/deploy-s3.sh",
"postinstall": "husky install && rm -r node_modules/apollo-language-server/node_modules/graphql", "postinstall": "husky install",
"codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/", "codegen:apollo": "apollo client:codegen --endpoint=https://v4.subgraph.goerli.oceanprotocol.com/subgraphs/name/oceanprotocol/ocean-subgraph --target typescript --tsFileExtension=d.ts --outputFlat src/@types/subgraph/",
"storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet", "storybook": "cross-env NODE_ENV=test start-storybook -p 6006 --quiet",
"storybook:build": "cross-env NODE_ENV=test build-storybook" "storybook:build": "cross-env NODE_ENV=test build-storybook"
@ -26,13 +26,13 @@
"@coingecko/cryptoformat": "^0.5.4", "@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2", "@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0", "@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.2.3", "@oceanprotocol/lib": "^2.4.0",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3", "@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@urql/exchange-refocus": "^1.0.0", "@urql/exchange-refocus": "^1.0.0",
"@walletconnect/web3-provider": "^1.8.0", "@walletconnect/web3-provider": "^1.8.0",
"axios": "^0.27.2", "axios": "^1.1.3",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"decimal.js": "^10.3.1", "decimal.js": "^10.3.1",
@ -43,8 +43,6 @@
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0",
"match-sorter": "^6.3.1", "match-sorter": "^6.3.1",
"myetherwallet-blockies": "^0.1.1", "myetherwallet-blockies": "^0.1.1",
"next": "12.3.1", "next": "12.3.1",
@ -72,53 +70,54 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^6.5.12", "@storybook/addon-essentials": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.12", "@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.12", "@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.12", "@storybook/react": "^6.5.13",
"@svgr/webpack": "^6.3.1", "@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@types/js-cookie": "^3.0.2", "@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.4", "@types/loadable__component": "^5.13.4",
"@types/lodash.debounce": "^4.0.7", "@types/node": "^18.8.5",
"@types/lodash.omit": "^4.5.7",
"@types/node": "^18.7.18",
"@types/react": "^18.0.21", "@types/react": "^18.0.21",
"@types/react-dom": "^18.0.5", "@types/react-dom": "^18.0.5",
"@types/react-modal": "^3.13.1", "@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1", "@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1", "@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.38.1", "@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.38.1", "@typescript-eslint/parser": "^5.42.0",
"apollo": "^2.34.0", "apollo": "^2.34.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.23.1", "eslint": "^8.25.0",
"eslint-config-oceanprotocol": "^2.0.4", "eslint-config-oceanprotocol": "^2.0.4",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2", "eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.8", "eslint-plugin-react": "^7.31.10",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.7.0", "eslint-plugin-testing-library": "^5.7.2",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"jest": "^29.1.2", "jest": "^29.1.2",
"jest-environment-jsdom": "^29.0.3", "jest-environment-jsdom": "^29.2.2",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"process": "^0.11.10", "process": "^0.11.10",
"serve": "^14.0.1", "serve": "^14.0.1",
"stream-http": "^3.2.0", "stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.3" "typescript": "^4.8.4"
},
"overrides": {
"graphql": "15.8.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/oceanprotocol/market" "url": "https://github.com/oceanprotocol/market"
}, },
"engines": { "engines": {
"node": ">=14" "node": "16"
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",

View File

@ -76,8 +76,9 @@ function AssetProvider({
LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset) LoggerInstance.error(`[asset] Failed getting asset for ${did}`, asset)
return return
} }
if (asset.nft.state !== 0 && asset.nft.state !== 4) {
// handle nft states as documented in https://docs.oceanprotocol.com/concepts/did-ddo/#state if ([1, 2, 3].includes(asset.nft.state)) {
// handle nft states as documented in https://docs.oceanprotocol.com/core-concepts/did-ddo/#state
let state let state
switch (asset.nft.state) { switch (asset.nft.state) {
case 1: case 1:
@ -119,7 +120,7 @@ function AssetProvider({
// Helper: Get and set asset access details // Helper: Get and set asset access details
// ----------------------------------- // -----------------------------------
const fetchAccessDetails = useCallback(async (): Promise<void> => { const fetchAccessDetails = useCallback(async (): Promise<void> => {
if (!asset?.chainId || !asset?.services) return if (!asset?.chainId || !asset?.services?.length) return
const accessDetails = await getAccessDetails( const accessDetails = await getAccessDetails(
asset.chainId, asset.chainId,

View File

@ -29,6 +29,7 @@ interface ProfileProviderValue {
downloadsTotal: number downloadsTotal: number
isDownloadsLoading: boolean isDownloadsLoading: boolean
sales: number sales: number
ownAccount: boolean
} }
const ProfileContext = createContext({} as ProfileProviderValue) const ProfileContext = createContext({} as ProfileProviderValue)
@ -46,17 +47,18 @@ const clearedProfile: Profile = {
function ProfileProvider({ function ProfileProvider({
accountId, accountId,
accountEns, accountEns,
ownAccount,
children children
}: { }: {
accountId: string accountId: string
accountEns: string accountEns: string
ownAccount: boolean
children: ReactNode children: ReactNode
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { appConfig } = useMarketMetadata() const { appConfig } = useMarketMetadata()
const [isEthAddress, setIsEthAddress] = useState<boolean>() const [isEthAddress, setIsEthAddress] = useState<boolean>()
// //
// Do nothing in all following effects // Do nothing in all following effects
// when accountId is no ETH address // when accountId is no ETH address
@ -111,7 +113,8 @@ function ProfileProvider({
const result = await getPublishedAssets( const result = await getPublishedAssets(
accountId, accountId,
chainIds, chainIds,
cancelTokenSource.token cancelTokenSource.token,
ownAccount
) )
setAssets(result.results) setAssets(result.results)
setAssetsTotal(result.totalResults) setAssetsTotal(result.totalResults)
@ -134,7 +137,13 @@ function ProfileProvider({
return () => { return () => {
cancelTokenSource.cancel() cancelTokenSource.cancel()
} }
}, [accountId, appConfig.metadataCacheUri, chainIds, isEthAddress]) }, [
accountId,
appConfig.metadataCacheUri,
chainIds,
isEthAddress,
ownAccount
])
// //
// DOWNLOADS // DOWNLOADS
@ -154,11 +163,13 @@ function ProfileProvider({
for (let i = 0; i < tokenOrders?.length; i++) { for (let i = 0; i < tokenOrders?.length; i++) {
dtList.push(tokenOrders[i].datatoken.address) dtList.push(tokenOrders[i].datatoken.address)
} }
const downloads = await getDownloadAssets( const downloads = await getDownloadAssets(
dtList, dtList,
tokenOrders, tokenOrders,
chainIds, chainIds,
cancelToken cancelToken,
ownAccount
) )
setDownloads(downloads) setDownloads(downloads)
setDownloadsTotal(downloads.length) setDownloadsTotal(downloads.length)
@ -167,7 +178,7 @@ function ProfileProvider({
downloads downloads
) )
}, },
[accountId, chainIds] [accountId, chainIds, ownAccount]
) )
useEffect(() => { useEffect(() => {
@ -230,6 +241,7 @@ function ProfileProvider({
downloads, downloads,
downloadsTotal, downloadsTotal,
isDownloadsLoading, isDownloadsLoading,
ownAccount,
sales sales
}} }}
> >

View File

@ -27,4 +27,9 @@ declare global {
computeJobs: ComputeJobMetaData[] computeJobs: ComputeJobMetaData[]
isLoaded: boolean isLoaded: boolean
} }
interface totalPriceMap {
value: string
symbol: string
}
} }

View File

@ -12,4 +12,5 @@ interface BaseQueryParams {
aggs?: any aggs?: any
filters?: FilterTerm[] filters?: FilterTerm[]
ignorePurgatory?: boolean ignorePurgatory?: boolean
ignoreState?: boolean
} }

View File

@ -323,7 +323,7 @@ export async function getAccessDetailsForAssets(
}, },
queryContext queryContext
) )
tokenQueryResult.data?.tokens.forEach((token) => { tokenQueryResult?.data?.tokens?.forEach((token) => {
const currentAsset = assetsExtended.find( const currentAsset = assetsExtended.find(
(asset) => (asset) =>
asset.services[0].datatokenAddress.toLowerCase() === token.id asset.services[0].datatokenAddress.toLowerCase() === token.id

View File

@ -1,5 +1,5 @@
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios' import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config' import { metadataCacheUri } from '../../app.config'
@ -60,7 +60,22 @@ export function generateBaseQuery(
getFilterTerm('_index', 'aquarius'), getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory ...(baseQueryParams.ignorePurgatory
? [] ? []
: [getFilterTerm('purgatory.state', false)]) : [getFilterTerm('purgatory.state', false)]),
...(baseQueryParams.ignoreState
? []
: [
{
bool: {
must_not: [
{
term: {
'nft.state': 5
}
}
]
}
}
])
] ]
} }
} }
@ -203,7 +218,7 @@ export async function getAssetsFromDidList(
cancelToken: CancelToken cancelToken: CancelToken
): Promise<PagedAssets> { ): Promise<PagedAssets> {
try { try {
if (!(didList.length > 0)) return if (!didList.length) return
const baseParams = { const baseParams = {
chainIds, chainIds,
@ -225,7 +240,7 @@ export async function getAssetsFromDtList(
cancelToken: CancelToken cancelToken: CancelToken
): Promise<Asset[]> { ): Promise<Asset[]> {
try { try {
if (!(dtList.length > 0)) return if (!dtList.length) return
const baseParams = { const baseParams = {
chainIds, chainIds,
@ -302,7 +317,7 @@ export async function getAlgorithmDatasetsForCompute(
must: { must: {
match: { match: {
'services.compute.publisherTrustedAlgorithms.did': { 'services.compute.publisherTrustedAlgorithms.did': {
query: escapeEsReservedCharacters(algorithmId) query: algorithmId
} }
} }
} }
@ -315,7 +330,6 @@ export async function getAlgorithmDatasetsForCompute(
const query = generateBaseQuery(baseQueryParams) const query = generateBaseQuery(baseQueryParams)
const computeDatasets = await queryMetadata(query, cancelToken) const computeDatasets = await queryMetadata(query, cancelToken)
if (computeDatasets?.totalResults === 0) return [] if (computeDatasets?.totalResults === 0) return []
const datasets = await transformAssetToAssetSelection( const datasets = await transformAssetToAssetSelection(
@ -330,6 +344,7 @@ export async function getPublishedAssets(
accountId: string, accountId: string,
chainIds: number[], chainIds: number[],
cancelToken: CancelToken, cancelToken: CancelToken,
ignoreState = false,
page?: number, page?: number,
type?: string, type?: string,
accesType?: string accesType?: string
@ -358,6 +373,7 @@ export async function getPublishedAssets(
} }
}, },
ignorePurgatory: true, ignorePurgatory: true,
ignoreState,
esPaginationOptions: { esPaginationOptions: {
from: (Number(page) - 1 || 0) * 9, from: (Number(page) - 1 || 0) * 9,
size: 9 size: 9
@ -471,14 +487,17 @@ export async function getDownloadAssets(
dtList: string[], dtList: string[],
tokenOrders: OrdersData[], tokenOrders: OrdersData[],
chainIds: number[], chainIds: number[],
cancelToken: CancelToken cancelToken: CancelToken,
ignoreState = false
): Promise<DownloadedAsset[]> { ): Promise<DownloadedAsset[]> {
const baseQueryparams = { const baseQueryparams = {
chainIds, chainIds,
filters: [ filters: [
getFilterTerm('services.datatokenAddress', dtList), getFilterTerm('services.datatokenAddress', dtList),
getFilterTerm('services.type', 'access') getFilterTerm('services.type', 'access')
] ],
ignorePurgatory: true,
ignoreState
} as BaseQueryParams } as BaseQueryParams
const query = generateBaseQuery(baseQueryparams) const query = generateBaseQuery(baseQueryparams)
try { try {

View File

@ -1,6 +1,6 @@
import { getAccessDetailsForAssets } from './accessDetailsAndPricing' import { getAccessDetailsForAssets } from './accessDetailsAndPricing'
import { PublisherTrustedAlgorithm, Asset } from '@oceanprotocol/lib' import { PublisherTrustedAlgorithm, Asset } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { getServiceByName } from './ddo' import { getServiceByName } from './ddo'
export async function transformAssetToAssetSelection( export async function transformAssetToAssetSelection(
@ -14,11 +14,12 @@ export async function transformAssetToAssetSelection(
const algorithmList: AssetSelectionAsset[] = [] const algorithmList: AssetSelectionAsset[] = []
for (const asset of extendedAssets) { for (const asset of extendedAssets) {
const algoComputeService = getServiceByName(asset, 'compute') const algoService =
getServiceByName(asset, 'compute') || getServiceByName(asset, 'access')
if ( if (
asset?.accessDetails?.price && asset?.accessDetails?.price &&
algoComputeService?.serviceEndpoint === datasetProviderEndpoint algoService?.serviceEndpoint === datasetProviderEndpoint
) { ) {
let selected = false let selected = false
selectedAlgorithms?.forEach((algorithm: PublisherTrustedAlgorithm) => { selectedAlgorithms?.forEach((algorithm: PublisherTrustedAlgorithm) => {

View File

@ -21,10 +21,10 @@ import {
} from './aquarius' } from './aquarius'
import { fetchDataForMultipleChains } from './subgraph' import { fetchDataForMultipleChains } from './subgraph'
import { getServiceById, getServiceByName } from './ddo' import { getServiceById, getServiceByName } from './ddo'
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery' import { SortTermOptions } from '../@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { transformAssetToAssetSelection } from './assetConvertor' import { transformAssetToAssetSelection } from './assetConvertor'
import { ComputeEditForm } from 'src/components/Asset/Edit/_types' import { ComputeEditForm } from '../components/Asset/Edit/_types'
import { getFileDidInfo } from './provider' import { getFileDidInfo } from './provider'
const getComputeOrders = gql` const getComputeOrders = gql`

View File

@ -1,18 +1,13 @@
import { LoggerInstance, Dispenser, Datatoken } from '@oceanprotocol/lib' import { LoggerInstance, Datatoken } from '@oceanprotocol/lib'
import Web3 from 'web3' import Web3 from 'web3'
import { TransactionReceipt } from 'web3-core' import { TransactionReceipt } from 'web3-core'
export async function setMinterToPublisher( export async function setMinterToPublisher(
web3: Web3, web3: Web3,
dispenserAddress: string,
datatokenAddress: string, datatokenAddress: string,
accountId: string, accountId: string,
setError: (msg: string) => void setError: (msg: string) => void
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const dispenserInstance = new Dispenser(dispenserAddress, web3)
const status = await dispenserInstance.status(datatokenAddress)
if (!status?.active) return
const datatokenInstance = new Datatoken(web3) const datatokenInstance = new Datatoken(web3)
const response = await datatokenInstance.removeMinter( const response = await datatokenInstance.removeMinter(
@ -20,6 +15,7 @@ export async function setMinterToPublisher(
accountId, accountId,
accountId accountId
) )
if (!response) { if (!response) {
setError('Updating DDO failed.') setError('Updating DDO failed.')
LoggerInstance.error('Failed at cancelMinter') LoggerInstance.error('Failed at cancelMinter')

View File

@ -6,7 +6,8 @@ import {
FileInfo, FileInfo,
LoggerInstance, LoggerInstance,
ProviderComputeInitializeResults, ProviderComputeInitializeResults,
ProviderInstance ProviderInstance,
UrlFile
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import Web3 from 'web3' import Web3 from 'web3'
import { getValidUntilTime } from './compute' import { getValidUntilTime } from './compute'
@ -87,7 +88,13 @@ export async function getFileUrlInfo(
providerUrl: string providerUrl: string
): Promise<FileInfo[]> { ): Promise<FileInfo[]> {
try { try {
const response = await ProviderInstance.checkFileUrl(url, providerUrl) const fileUrl: UrlFile = {
type: 'url',
index: 0,
url,
method: 'get'
}
const response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
return response return response
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)

View File

@ -13,7 +13,7 @@ import {
} from '@hooks/useNetworkMetadata' } from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius' import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config' import { chainIdsSupported } from 'app.config'
import { Asset, LoggerInstance } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
const AllLocked = gql` const AllLocked = gql`
query AllLocked { query AllLocked {
@ -42,7 +42,7 @@ const NftOwnAllocation = gql`
} }
` `
const OceanLocked = gql` const OceanLocked = gql`
query OceanLocked($address: String) { query OceanLocked($address: ID!) {
veOCEAN(id: $address) { veOCEAN(id: $address) {
id id
lockedAmount lockedAmount

View File

@ -1,24 +0,0 @@
.accountList {
display: grid;
grid-template-columns: 1fr;
gap: calc(var(--spacer) / 2);
}
@media screen and (min-width: 25rem) {
.accountList {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
gap: var(--spacer);
}
}
.empty {
color: var(--color-secondary);
font-size: var(--font-size-small);
font-style: italic;
}
.loaderWrap {
display: flex;
justify-content: center;
align-items: center;
}

View File

@ -0,0 +1,29 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddToken, { AddTokenProps } from '@shared/AddToken'
export default {
title: 'Component/@shared/AddToken',
component: AddToken
} as ComponentMeta<typeof AddToken>
const Template: ComponentStory<typeof AddToken> = (args: AddTokenProps) => {
return <AddToken {...args} />
}
interface Props {
args: AddTokenProps
}
export const Default: Props = Template.bind({})
Default.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
export const Minimal: Props = Template.bind({})
Minimal.args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN',
minimal: true
}

View File

@ -0,0 +1,24 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AddToken from './index'
jest.mock('../../../@utils/web3', () => ({ addTokenToWallet: jest.fn() }))
describe('@shared/AddToken', () => {
const propsBase = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8',
symbol: 'OCEAN'
}
testRender(<AddToken {...propsBase} />)
it('renders with custom text', () => {
render(<AddToken {...propsBase} text="Hello Text" />)
expect(screen.getByText('Hello Text')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button'))
})
it('renders minimal', () => {
render(<AddToken {...propsBase} minimal />)
})
})

View File

@ -8,19 +8,21 @@ import styles from './index.module.css'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
export interface AddTokenProps {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}
export default function AddToken({ export default function AddToken({
address, address,
symbol, symbol,
text, text,
className, className,
minimal minimal
}: { }: AddTokenProps): ReactElement {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}): ReactElement {
const { web3Provider } = useWeb3() const { web3Provider } = useWeb3()
const styleClasses = cx({ const styleClasses = cx({

View File

@ -0,0 +1,65 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import AddAnnouncementBanner, {
AnnouncementBannerProps
} from '@shared/AnnouncementBanner'
export default {
title: 'Component/@shared/AnnouncementBanner',
component: AddAnnouncementBanner
} as ComponentMeta<typeof AddAnnouncementBanner>
const Template: ComponentStory<typeof AddAnnouncementBanner> = (
args: AnnouncementBannerProps
) => <AddAnnouncementBanner {...args} />
interface Props {
args: AnnouncementBannerProps
}
export const Default: Props = Template.bind({})
Default.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Success: Props = Template.bind({})
Success.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'success',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Warning: Props = Template.bind({})
Warning.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'warning',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}
export const Error: Props = Template.bind({})
Error.args = {
text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum ac enim auctor placerat.',
state: 'error',
action: {
name: 'see more',
handleAction: () => {
alert('Link clicked!')
}
}
}

View File

@ -0,0 +1,13 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AnnouncementBanner from './index'
describe('@shared/AnnouncementBanner', () => {
testRender(
<AnnouncementBanner
text="# Hello World!"
action={{ name: 'hello', handleAction: jest.fn() }}
state="success"
/>
)
})

View File

@ -12,17 +12,19 @@ export interface AnnouncementAction {
handleAction: () => void handleAction: () => void
} }
export interface AnnouncementBannerProps {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}
export default function AnnouncementBanner({ export default function AnnouncementBanner({
text, text,
action, action,
state, state,
className className
}: { }: AnnouncementBannerProps): ReactElement {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}): ReactElement {
const styleClasses = cx({ const styleClasses = cx({
banner: true, banner: true,
error: state === 'error', error: state === 'error',

View File

@ -1,59 +0,0 @@
.display {
composes: selection from '@shared/FormFields/AssetSelection/index.module.css';
}
.display [class*='loaderWrap'] {
margin: calc(var(--spacer) / 3);
}
.scroll {
composes: scroll from '@shared/FormFields/AssetSelection/index.module.css';
margin-top: 0;
border-top: none;
width: 100%;
}
.row {
composes: row from '@shared/FormFields/AssetSelection/index.module.css';
}
.row:last-child {
border-bottom: none;
}
.row:first-child {
border-top: none;
}
.row:hover {
background-color: var(--background-content);
}
.info {
display: block;
width: 100%;
}
.title {
composes: title from '@shared/FormFields/AssetSelection/index.module.css';
}
.hover:hover {
color: var(--color-primary);
}
.price {
composes: price from '@shared/FormFields/AssetSelection/index.module.css';
}
.price [class*='symbol'] {
font-size: calc(var(--font-size-small) / 1.2) !important;
}
.did {
composes: did from '@shared/FormFields/AssetSelection/index.module.css';
}
.empty {
composes: empty from '@shared/FormFields/AssetSelection/index.module.css';
}

View File

@ -0,0 +1,35 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import MarketMetadataProvider from '@context/MarketMetadata'
import { UserPreferencesProvider } from '@context/UserPreferences'
import AssetList, { AssetListProps } from '.'
import { assets } from '../../../../.jest/__fixtures__/assetsWithAccessDetails'
export default {
title: 'Component/@shared/AssetList',
component: AssetList
} as ComponentMeta<typeof AssetList>
const Template: ComponentStory<typeof AssetList> = (args: AssetListProps) => {
return (
<MarketMetadataProvider>
<UserPreferencesProvider>
<AssetList {...args} />
</UserPreferencesProvider>
</MarketMetadataProvider>
)
}
export const Default: { args: AssetListProps } = Template.bind({})
Default.args = {
assets,
showPagination: true,
page: 1,
totalPages: 10
}
export const Empty: { args: AssetListProps } = Template.bind({})
Empty.args = {
assets: [],
showPagination: false
}

View File

@ -0,0 +1,32 @@
import { render, screen, fireEvent } from '@testing-library/react'
import React from 'react'
import AssetList from './index'
import { assetAquarius } from '../../../../.jest/__fixtures__/assetAquarius'
describe('@shared/AssetList', () => {
it('renders without crashing', async () => {
const onPageChange = jest.fn()
render(
<AssetList
assets={[assetAquarius]}
showPagination
page={1}
totalPages={10}
onPageChange={onPageChange}
/>
)
await screen.findAllByText('OCEAN')
fireEvent.click(screen.getByLabelText('Page 2'))
expect(onPageChange).toBeCalled()
})
it('renders empty', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading={false} />)
await screen.findByText('No results found')
})
it('renders loading', async () => {
render(<AssetList assets={[]} showPagination={false} isLoading />)
})
})

View File

@ -2,15 +2,11 @@ import AssetTeaser from '@shared/AssetTeaser'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination' import Pagination from '@shared/Pagination'
import styles from './index.module.css' import styles from './index.module.css'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing' import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
const cx = classNames.bind(styles)
function LoaderArea() { function LoaderArea() {
return ( return (
<div className={styles.loaderWrap}> <div className={styles.loaderWrap}>
@ -19,7 +15,7 @@ function LoaderArea() {
) )
} }
declare type AssetListProps = { export declare type AssetListProps = {
assets: AssetExtended[] assets: AssetExtended[]
showPagination: boolean showPagination: boolean
page?: number page?: number
@ -28,6 +24,8 @@ 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({
@ -38,16 +36,18 @@ export default function AssetList({
isLoading, isLoading,
onPageChange, onPageChange,
className, className,
noPublisher noPublisher,
noDescription,
noPrice
}: AssetListProps): ReactElement { }: AssetListProps): ReactElement {
const { chainIds } = useUserPreferences()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>() const [assetsWithPrices, setAssetsWithPrices] =
useState<AssetExtended[]>(assets)
const [loading, setLoading] = useState<boolean>(isLoading) const [loading, setLoading] = useState<boolean>(isLoading)
const isMounted = useIsMounted() const isMounted = useIsMounted()
useEffect(() => { useEffect(() => {
if (!assets) return if (!assets || !assets.length) return
setAssetsWithPrices(assets as AssetExtended[]) setAssetsWithPrices(assets as AssetExtended[])
setLoading(false) setLoading(false)
@ -67,16 +67,9 @@ export default function AssetList({
onPageChange(selected + 1) onPageChange(selected + 1)
} }
const styleClasses = cx({ const styleClasses = `${styles.assetList} ${className || ''}`
assetList: true,
[className]: className
})
return chainIds.length === 0 ? ( return assetsWithPrices && !loading ? (
<div className={styleClasses}>
<div className={styles.empty}>No network selected</div>
</div>
) : assetsWithPrices && !loading ? (
<> <>
<div className={styleClasses}> <div className={styleClasses}>
{assetsWithPrices.length > 0 ? ( {assetsWithPrices.length > 0 ? (
@ -85,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}
/> />
)) ))
) : ( ) : (

View File

@ -0,0 +1,24 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetListTitle from '.'
import { render } from '@testing-library/react'
jest.mock('../../../@utils/aquarius', () => ({
getAssetsNames: () => Promise.resolve('Test')
}))
describe('AssetListTitle', () => {
testRender(
<AssetListTitle asset={{ metadata: { name: 'Hello world' } } as any} />
)
it('renders with passed title', () => {
render(<AssetListTitle title="Hello Title" />)
})
it('renders with passed DID', () => {
render(
<AssetListTitle did="did:op:764b81877039fa2651b919fc91c399799acb837f270e6d17bfb7973fbe6e9408" />
)
})
})

View File

@ -1,7 +1,7 @@
import Link from 'next/link' import Link from 'next/link'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '@utils/aquarius' import { getAssetsNames } from '@utils/aquarius'
import styles from './AssetListTitle.module.css' import styles from './index.module.css'
import axios from 'axios' import axios from 'axios'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'

View File

@ -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 {

View File

@ -0,0 +1,8 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import AssetTeaser from './index'
import { asset } from '../../../../.jest/__fixtures__/assetWithAccessDetails'
describe('@shared/AssetTeaser', () => {
testRender(<AssetTeaser asset={asset} />)
})

View File

@ -11,14 +11,18 @@ import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit' import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
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>
{!noDescription && (
<div className={styles.content}> <div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}> <Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')} {removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot> </Dotdotdot>
</div> </div>
{isUnsupportedPricing ? ( )}
{!noPrice && (
<div className={styles.price}>
{isUnsupportedPricing || !asset.services.length ? (
<strong>No pricing schema available</strong> <strong>No pricing schema available</strong>
) : ( ) : (
<Price accessDetails={asset.accessDetails} size="small" /> <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}>

View File

@ -0,0 +1,7 @@
import React from 'react'
import testRender from '../../../../.jest/testRender'
import DebugOutput from './index'
describe('@shared/DebugOutput', () => {
testRender(<DebugOutput title="Debug" output="Hello Output" />)
})

View File

@ -0,0 +1,21 @@
import testRender from '../../../../.jest/testRender'
import { render, screen } from '@testing-library/react'
import React from 'react'
import ExplorerLink from './index'
describe('@shared/ExplorerLink', () => {
testRender(
<ExplorerLink networkId={1} path="/tx">
Hello Link
</ExplorerLink>
)
it('renders without networkId', () => {
render(
<ExplorerLink networkId={null} path="/tx">
Hello Link
</ExplorerLink>
)
expect(screen.getByRole('link')).toHaveTextContent('Hello Link')
})
})

View File

@ -1,12 +1,9 @@
import React, { ReactElement, ReactNode, useEffect, useState } from 'react' import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import External from '@images/external.svg' import External from '@images/external.svg'
import classNames from 'classnames/bind'
import { Config } from '@oceanprotocol/lib' import { Config } from '@oceanprotocol/lib'
import styles from './index.module.css' import styles from './index.module.css'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
const cx = classNames.bind(styles)
export default function ExplorerLink({ export default function ExplorerLink({
networkId, networkId,
path, path,
@ -20,10 +17,6 @@ export default function ExplorerLink({
}): ReactElement { }): ReactElement {
const [url, setUrl] = useState<string>() const [url, setUrl] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<Config>() const [oceanConfig, setOceanConfig] = useState<Config>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => { useEffect(() => {
if (!networkId) return if (!networkId) return
@ -39,7 +32,7 @@ export default function ExplorerLink({
title={`View on ${oceanConfig?.explorerUri}`} title={`View on ${oceanConfig?.explorerUri}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={styleClasses} className={`${styles.link} ${className || ''}`}
> >
{children} <External /> {children} <External />
</a> </a>

View File

@ -0,0 +1,37 @@
import testRender from '../../../../.jest/testRender'
import { FileInfo } from '@oceanprotocol/lib'
import { render } from '@testing-library/react'
import React from 'react'
import FileIcon from './index'
describe('@shared/FileIcon', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '123'
}
testRender(<FileIcon file={file} />)
it('renders small', () => {
render(<FileIcon file={file} small />)
})
it('renders loading', () => {
render(<FileIcon file={file} isLoading />)
})
it('renders empty', () => {
const file: FileInfo = { type: 'url' }
render(<FileIcon file={file} />)
})
it('renders with 0 contentLength', () => {
const file: FileInfo = {
type: 'url',
contentType: 'text/plain',
contentLength: '0'
}
render(<FileIcon file={file} />)
})
})

View File

@ -30,9 +30,9 @@ export default function FileIcon({
return ( return (
<ul className={styleClasses}> <ul className={styleClasses}>
{!isLoading && file ? ( {!isLoading ? (
<> <>
{file.contentType || file.contentLength ? ( {file?.contentType || file?.contentLength ? (
<> <>
<li>{cleanupContentType(file.contentType)}</li> <li>{cleanupContentType(file.contentType)}</li>
<li> <li>

View File

@ -1,17 +0,0 @@
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
sizes.length - 1
)
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)}${separator}${
sizes[i]
}${postFix}`
}
return 'n/a'
}

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Error from './Error'
describe('@shared/FormInput/Error', () => {
const propsBase = {
value: '',
touched: false,
initialTouched: false
}
it('renders without crashing', () => {
render(<Error meta={{ ...propsBase, error: 'Hello Error' }} />)
expect(screen.getByText('Hello Error')).toBeInTheDocument()
})
it('renders nothing without error passed', () => {
render(<Error meta={{ ...propsBase }} />)
expect(screen.queryByText('Hello Error')).not.toBeInTheDocument()
})
})

View File

@ -1,9 +1,6 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styles from './Help.module.css' import styles from './Help.module.css'
import Markdown from '@shared/Markdown' import Markdown from '@shared/Markdown'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const FormHelp = ({ const FormHelp = ({
children, children,
@ -12,12 +9,9 @@ const FormHelp = ({
children: string children: string
className?: string className?: string
}): ReactElement => { }): ReactElement => {
const styleClasses = cx({ return (
help: true, <Markdown className={`${styles.help} ${className || ''}`} text={children} />
[className]: className )
})
return <Markdown className={styleClasses} text={children} />
} }
export default FormHelp export default FormHelp

View File

@ -58,11 +58,11 @@
} }
.radio { .radio {
composes: radio from '@shared/FormInput/InputRadio.module.css'; composes: radio from '@shared/FormInput/InputElement/Radio/index.module.css';
} }
.checkbox { .checkbox {
composes: checkbox from '@shared/FormInput/InputRadio.module.css'; composes: checkbox from '@shared/FormInput/InputElement/Radio/index.module.css';
} }
.title { .title {

View File

@ -0,0 +1,52 @@
import AssetSelection, { AssetSelectionAsset } from './'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
describe('@shared/FormInput/InputElement/AssetSelection', () => {
const assets: AssetSelectionAsset[] = [
{
did: 'did:op:xxx',
name: 'Asset',
price: '10',
checked: false,
symbol: 'OCEAN'
},
{
did: 'did:op:yyy',
name: 'Asset',
price: '10',
checked: true,
symbol: 'OCEAN'
},
{
did: 'did:op:zzz',
name: 'Asset',
price: '0',
checked: false,
symbol: 'OCEAN'
}
]
it('renders without crashing', () => {
render(<AssetSelection assets={assets} />)
const searchInput = screen.getByPlaceholderText(
'Search by title, datatoken, or DID...'
)
fireEvent.change(searchInput, { target: { value: 'Assets' } })
fireEvent.change(searchInput, { target: { value: '' } })
})
it('renders empty assetSelection', () => {
render(<AssetSelection assets={[]} />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders disabled assetSelection', () => {
render(<AssetSelection assets={[]} disabled />)
expect(screen.getByText('No assets found.')).toBeInTheDocument()
})
it('renders assetSelectionMultiple', () => {
render(<AssetSelection assets={assets} multiple />)
})
})

View File

@ -1,15 +1,12 @@
import React, { ChangeEvent, useState } from 'react' import React, { ChangeEvent, useState } from 'react'
import Dotdotdot from 'react-dotdotdot' import Dotdotdot from 'react-dotdotdot'
import slugify from 'slugify' import slugify from 'slugify'
import classNames from 'classnames/bind'
import PriceUnit from '@shared/Price/PriceUnit' import PriceUnit from '@shared/Price/PriceUnit'
import External from '@images/external.svg' import External from '@images/external.svg'
import InputElement from '@shared/FormInput/InputElement' import InputElement from '@shared/FormInput/InputElement'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import styles from './index.module.css' import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface AssetSelectionAsset { export interface AssetSelectionAsset {
did: string did: string
name: string name: string
@ -34,18 +31,19 @@ export default function AssetSelection({
}): JSX.Element { }): JSX.Element {
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const styleClassesInput = cx({ const styleClassesWrapper = `${styles.selection} ${
input: true, disabled ? styles.disabled : ''
[styles.checkbox]: multiple, }`
[styles.radio]: !multiple const styleClassesInput = `${styles.input} ${
}) multiple ? styles.checkbox : styles.radio
}`
function handleSearchInput(e: ChangeEvent<HTMLInputElement>) { function handleSearchInput(e: ChangeEvent<HTMLInputElement>) {
setSearchValue(e.target.value) setSearchValue(e.target.value)
} }
return ( return (
<div className={`${styles.selection} ${disabled ? styles.disabled : ''}`}> <div className={styleClassesWrapper}>
<InputElement <InputElement
type="search" type="search"
name="search" name="search"

View File

@ -0,0 +1,38 @@
import BoxSelection, { BoxSelectionOption } from './'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
describe('@shared/FormInput/InputElement/BoxSelection', () => {
const handleChange = jest.fn()
const options: BoxSelectionOption[] = [
{
name: 'option1',
value: 'option1',
title: 'Option 1',
checked: true,
text: 'Option 1 Text',
icon: <div>Icon</div>
},
{
name: 'option2',
title: 'Option 2 Text',
checked: false
}
]
it('renders without crashing', () => {
render(
<BoxSelection name="box" options={options} handleChange={handleChange} />
)
fireEvent.click(screen.getByText('Option 2 Text'))
expect(handleChange).toHaveBeenCalled()
})
it('renders disabled', () => {
render(<BoxSelection name="box" options={options} disabled />)
})
it('renders loader without options', () => {
render(<BoxSelection name="box" options={null} />)
})
})

View File

@ -1,10 +1,7 @@
import React, { ChangeEvent } from 'react' import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import styles from './index.module.css' import styles from './index.module.css'
const cx = classNames.bind(styles)
export interface BoxSelectionOption { export interface BoxSelectionOption {
name: string name: string
value?: string value?: string
@ -26,15 +23,10 @@ export default function BoxSelection({
disabled?: boolean disabled?: boolean
handleChange?: (event: ChangeEvent<HTMLInputElement>) => void handleChange?: (event: ChangeEvent<HTMLInputElement>) => void
}): JSX.Element { }): JSX.Element {
const styleClassesWrapper = cx({ const styleClassesWrapper = `${styles.boxSelectionsWrapper} ${
boxSelectionsWrapper: true, disabled ? styles.disabled : ''
[styles.disabled]: disabled }`
}) const styleClassesInput = `${styles.input} ${styles.radio}`
const styleClassesInput = cx({
input: true,
radio: true
})
return ( return (
<div className={styleClassesWrapper}> <div className={styleClassesWrapper}>
@ -51,7 +43,7 @@ export default function BoxSelection({
type="radio" type="radio"
className={styleClassesInput} className={styleClassesInput}
disabled={disabled} disabled={disabled}
value={option.value ? option.value : option.name} value={option.value || option.name}
name={name} name={name}
/> />
<label <label

View File

@ -0,0 +1,59 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Datatoken from './index'
import { useField } from 'formik'
jest.mock('formik')
const props = {
name: 'Datatoken'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: {
name: '',
symbol: ''
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
}
const mockHelpers = {
setValue: jest.fn()
}
describe('@shared/FormInput/InputElement/Datatoken', () => {
it('renders without crashing', () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
render(<Datatoken {...props} />)
fireEvent.click(screen.getByRole('button'))
})
it('does nothing when data already present', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: {
name: 'Hello Name'
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
},
mockMeta,
mockHelpers
])
render(<Datatoken {...props} />)
})
})

View File

@ -31,6 +31,11 @@
padding-right: calc(var(--spacer) / 2); padding-right: calc(var(--spacer) / 2);
} }
.hideUrl {
filter: blur(0.2rem);
user-select: none;
}
.warning { .warning {
margin-top: calc(var(--spacer) / 3); margin-top: calc(var(--spacer) / 3);
margin-left: 0; margin-left: 0;

View File

@ -15,9 +15,13 @@ export default function FileInfo({
? cleanupContentType(file.contentType) ? cleanupContentType(file.contentType)
: null : null
const hideUrl = file.type === 'hidden' || false
return ( return (
<div className={styles.info}> <div className={`${styles.info}`}>
<h3 className={styles.url}>{file.url}</h3> <h3 className={`${styles.url} ${hideUrl ? styles.hideUrl : null}`}>
{hideUrl ? 'https://oceanprotocol/placeholder' : file.url}
</h3>
<ul> <ul>
<li className={styles.success}> URL confirmed</li> <li className={styles.success}> URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>} {file.contentLength && <li>{prettySize(+file.contentLength)}</li>}

View File

@ -0,0 +1,125 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import FilesInput from './index'
import { useField } from 'formik'
import { getFileUrlInfo } from '@utils/provider'
jest.mock('formik')
jest.mock('@utils/provider')
const props = {
name: 'File'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: 'https://hello.com',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
const mockHelpers = {
setValue: jest.fn(),
setTouched: jest.fn()
}
const mockForm = {
values: {
services: [{ providerUrl: 'https://provider.url' }]
},
errors: {},
touched: {},
isSubmitting: false,
isValidating: false,
submitCount: 0,
setFieldError: jest.fn()
}
describe('@shared/FormInput/InputElement/FilesInput', () => {
it('renders without crashing', async () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
;(getFileUrlInfo as jest.Mock).mockReturnValue([
{
valid: true,
url: 'https://hello.com',
contentType: 'text/html',
contentLength: 100
}
])
render(<FilesInput form={mockForm} {...props} />)
expect(screen.getByText('Validate')).toBeInTheDocument()
fireEvent.click(screen.getByText('Validate'))
// can't really re-mock our helpers.setValue() behavior switching
// to Info component, so we just wait for Validate button to be back again.
await screen.findByText('Validate')
expect(mockHelpers.setValue).toHaveBeenCalled()
})
it('renders fileinfo when file is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
contentType: 'text/html',
contentLength: 100
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
expect(screen.getByText('https://hello.com')).toBeInTheDocument()
})
it('renders fileinfo without contentType', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
contentLength: 100
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
})
it('renders fileinfo placeholder when hideUrl is passed', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
type: 'hidden'
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
expect(
screen.getByText('https://oceanprotocol/placeholder')
).toBeInTheDocument()
})
})

View File

@ -1,26 +1,24 @@
import React, { ReactElement, useState } from 'react' import React, { ReactElement, useState } from 'react'
import { useField, useFormikContext } from 'formik' import { useField } from 'formik'
import FileInfo from './Info' import FileInfo from './Info'
import UrlInput from '../URLInput' import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput' import { InputProps } from '@shared/FormInput'
import { getFileUrlInfo } from '@utils/provider' import { getFileUrlInfo } from '@utils/provider'
import { FormPublishData } from 'src/components/Publish/_types'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
export default function FilesInput(props: InputProps): ReactElement { export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props.name) const [field, meta, helpers] = useField(props.name)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { values, setFieldError } = useFormikContext<FormPublishData>()
const { asset } = useAsset() const { asset } = useAsset()
async function handleValidation(e: React.SyntheticEvent, url: string) { async function handleValidation(e: React.SyntheticEvent, url: string) {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf' // File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault() e?.preventDefault()
try { try {
const providerUrl = values?.services const providerUrl = props.form?.values?.services
? values?.services[0].providerUrl.url ? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint : asset.services[0].serviceEndpoint
setIsLoading(true) setIsLoading(true)
const checkedFile = await getFileUrlInfo(url, providerUrl) const checkedFile = await getFileUrlInfo(url, providerUrl)
@ -35,7 +33,7 @@ export default function FilesInput(props: InputProps): ReactElement {
// if all good, add file to formik state // if all good, add file to formik state
helpers.setValue([{ url, ...checkedFile[0] }]) helpers.setValue([{ url, ...checkedFile[0] }])
} catch (error) { } catch (error) {
setFieldError(`${field.name}[0].url`, error.message) props.form.setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -43,13 +41,14 @@ export default function FilesInput(props: InputProps): ReactElement {
} }
function handleClose() { function handleClose() {
helpers.setValue(meta.initialValue)
helpers.setTouched(false) helpers.setTouched(false)
helpers.setValue(meta.initialValue)
} }
return ( return (
<> <>
{field?.value?.[0]?.valid === true ? ( {field?.value?.[0]?.valid === true ||
field?.value?.[0]?.type === 'hidden' ? (
<FileInfo file={field.value[0]} handleClose={handleClose} /> <FileInfo file={field.value[0]} handleClose={handleClose} />
) : ( ) : (
<UrlInput <UrlInput

View File

@ -0,0 +1,16 @@
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (!bytes) return 'n/a'
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
sizes.length - 1
)
return `${(bytes / 1024 ** i).toFixed(i ? 1 : 0)}${separator}${
sizes[i]
}${postFix}`
}

View File

@ -0,0 +1,63 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Nft from './index'
import { useField } from 'formik'
jest.mock('formik')
const props = {
name: 'NFT'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
const mockField = {
value: {
name: '',
symbol: '',
description: '',
external_url: '',
background_color: '',
image_data: ''
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
}
const mockHelpers = {
setValue: jest.fn()
}
describe('@shared/FormInput/InputElement/Nft', () => {
it('renders without crashing', () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
render(<Nft {...props} />)
fireEvent.click(screen.getByRole('button'))
})
it('does nothing when data already present', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: {
name: 'Hello Name'
},
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'NFT'
},
mockMeta,
mockHelpers
])
render(<Nft {...props} />)
})
})

View File

@ -16,7 +16,7 @@
.radio, .radio,
.checkbox { .checkbox {
composes: input from './InputElement.module.css'; composes: input from '../index.module.css';
position: relative; position: relative;
padding: 0; padding: 0;
width: 18px; width: 18px;

View File

@ -1,7 +1,7 @@
import React, { InputHTMLAttributes, ReactElement } from 'react' import React, { InputHTMLAttributes, ReactElement } from 'react'
import slugify from 'slugify' import slugify from 'slugify'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import styles from './InputRadio.module.css' import styles from './index.module.css'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)

View File

@ -2,11 +2,11 @@ import React, { ReactElement, useEffect, useState } from 'react'
import CreatableSelect from 'react-select/creatable' import CreatableSelect from 'react-select/creatable'
import { OnChangeValue } from 'react-select' import { OnChangeValue } from 'react-select'
import { useField } from 'formik' import { useField } from 'formik'
import { InputProps } from '.' import { InputProps } from '../..'
import { getTagsList } from '@utils/aquarius' import { getTagsList } from '@utils/aquarius'
import { chainIds } from 'app.config' import { chainIds } from '../../../../../../app.config'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import styles from './TagsAutoComplete.module.css' import styles from './index.module.css'
import { matchSorter } from 'match-sorter' import { matchSorter } from 'match-sorter'
interface AutoCompleteOption { interface AutoCompleteOption {

View File

@ -1,5 +1,5 @@
.input { .input {
composes: input from '@shared/FormInput/InputElement.module.css'; composes: input from '@shared/FormInput/InputElement/index.module.css';
} }
.hasError { .hasError {

View File

@ -0,0 +1,57 @@
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import URLInput, { URLInputProps } from './index'
import { useField } from 'formik'
jest.mock('formik')
const props: URLInputProps = {
submitText: 'Submit',
handleButtonClick: jest.fn(),
isLoading: false,
name: 'Hello Name'
}
const mockMeta = {
touched: false,
error: '',
initialError: '',
initialTouched: false,
initialValue: '',
value: ''
}
describe('@shared/FormInput/InputElement/URLInput', () => {
it('renders without crashing', () => {
const mockField = {
value: '',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta])
render(<URLInput {...props} />)
expect(screen.getByRole('button')).toBeDisabled()
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'https://google.com' }
})
})
it('renders button enabled with value', () => {
const mockField = {
value: 'https://google.com',
checked: false,
onChange: jest.fn(),
onBlur: jest.fn(),
name: 'url'
}
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta])
render(<URLInput {...props} />)
expect(screen.getByRole('button')).toBeEnabled()
fireEvent.click(screen.getByRole('button'))
})
})

View File

@ -7,6 +7,14 @@ import InputGroup from '@shared/FormInput/InputGroup'
import InputElement from '@shared/FormInput/InputElement' import InputElement from '@shared/FormInput/InputElement'
import isUrl from 'is-url-superb' import isUrl from 'is-url-superb'
export interface URLInputProps {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
name: string
checkUrl?: boolean
}
export default function URLInput({ export default function URLInput({
submitText, submitText,
handleButtonClick, handleButtonClick,
@ -14,13 +22,7 @@ export default function URLInput({
name, name,
checkUrl, checkUrl,
...props ...props
}: { }: URLInputProps): ReactElement {
submitText: string
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
name: string
checkUrl?: boolean
}): ReactElement {
const [field, meta] = useField(name) const [field, meta] = useField(name)
const [isButtonDisabled, setIsButtonDisabled] = useState(true) const [isButtonDisabled, setIsButtonDisabled] = useState(true)

View File

@ -1,17 +1,15 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styles from './InputElement.module.css' import styles from './index.module.css'
import { InputProps } from '.' import { InputProps } from '..'
import FilesInput from '../FormFields/FilesInput' import FilesInput from './FilesInput'
import CustomProvider from '../FormFields/Provider' import CustomProvider from './Provider'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection' import BoxSelection, { BoxSelectionOption } from './BoxSelection'
import Datatoken from '../FormFields/Datatoken' import Datatoken from './Datatoken'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import AssetSelection, { import AssetSelection, { AssetSelectionAsset } from './AssetSelection'
AssetSelectionAsset import Nft from './Nft'
} from '../FormFields/AssetSelection' import InputRadio from './Radio'
import Nft from '../FormFields/Nft' import ContainerInput from '@shared/FormInput/InputElement/ContainerInput'
import InputRadio from './InputRadio'
import ContainerInput from '@shared/FormFields/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete' import TagsAutoComplete from './TagsAutoComplete'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -87,12 +85,14 @@ export default function InputElement({
case 'radio': case 'radio':
case 'checkbox': case 'checkbox':
return <InputRadio options={options} inputSize={size} {...props} /> return (
<InputRadio options={options as string[]} inputSize={size} {...props} />
)
case 'assetSelection': case 'assetSelection':
return ( return (
<AssetSelection <AssetSelection
assets={options as unknown as AssetSelectionAsset[]} assets={options as AssetSelectionAsset[]}
{...field} {...field}
{...props} {...props}
/> />
@ -101,14 +101,14 @@ export default function InputElement({
case 'assetSelectionMultiple': case 'assetSelectionMultiple':
return ( return (
<AssetSelection <AssetSelection
assets={options as unknown as AssetSelectionAsset[]} assets={options as AssetSelectionAsset[]}
multiple multiple
{...field} {...field}
{...props} {...props}
/> />
) )
case 'files': case 'files':
return <FilesInput {...field} {...props} /> return <FilesInput {...field} form={form} {...props} />
case 'container': case 'container':
return <ContainerInput {...field} {...props} /> return <ContainerInput {...field} {...props} />
case 'providerUrl': case 'providerUrl':
@ -120,7 +120,7 @@ export default function InputElement({
case 'boxSelection': case 'boxSelection':
return ( return (
<BoxSelection <BoxSelection
options={options as unknown as BoxSelectionOption[]} options={options as BoxSelectionOption[]}
{...field} {...field}
{...props} {...props}
/> />

View File

@ -1,3 +0,0 @@
.row {
margin-bottom: var(--spacer);
}

View File

@ -1,8 +0,0 @@
import React, { ReactElement, ReactNode } from 'react'
import styles from './Row.module.css'
const Row = ({ children }: { children: ReactNode }): ReactElement => (
<div className={styles.row}>{children}</div>
)
export default Row

View File

@ -0,0 +1,105 @@
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { render, screen } from '@testing-library/react'
import React from 'react'
import FormInput from './index'
describe('@shared/FormInput', () => {
it('renders without crashing', () => {
render(
<FormInput
type="text"
name="Hello Name"
label="Hello Label"
placeholder="Hello Placeholder"
required
help="Hello Help"
/>
)
expect(screen.getByText('Hello Label')).toBeInTheDocument()
expect(screen.getByPlaceholderText('Hello Placeholder')).toBeInTheDocument()
})
it('renders prominent help', () => {
render(<FormInput type="text" help="Hello Help" prominentHelp />)
expect(screen.getByText('Hello Help')).toBeInTheDocument()
})
it('renders disclaimer', () => {
render(<FormInput type="text" disclaimer="Hello Disclaimer" />)
expect(screen.getByText('Hello Disclaimer')).toBeInTheDocument()
})
it('renders with prefix & postfix', () => {
render(
<FormInput type="text" prefix="Hello Prefix" postfix="Hello Postfix" />
)
expect(screen.getByText('Hello Prefix')).toBeInTheDocument()
expect(screen.getByText('Hello Postfix')).toBeInTheDocument()
})
it('renders textarea', () => {
render(<FormInput type="textarea" />)
})
it('renders radio', () => {
render(<FormInput type="radio" options={['option1', 'option2']} />)
})
it('renders checkbox', () => {
render(<FormInput type="checkbox" options={['option1', 'option2']} />)
})
it('renders select', () => {
render(<FormInput type="select" options={['option1', 'option2']} />)
})
it('renders assetSelection', () => {
const assets: AssetSelectionAsset[] = [
{
did: 'did:op:xxx',
name: 'Asset',
price: '10',
checked: false,
symbol: 'OCEAN'
},
{
did: 'did:op:yyy',
name: 'Asset',
price: '10',
checked: true,
symbol: 'OCEAN'
}
]
render(<FormInput type="assetSelection" options={assets} />)
})
it('renders assetSelectionMultiple', () => {
render(<FormInput type="assetSelectionMultiple" />)
})
it('renders boxSelection', () => {
const options: BoxSelectionOption[] = [
{
name: 'option1',
title: 'Option 1',
checked: true,
text: 'Option 1 Text',
icon: <div>Icon</div>
},
{
name: 'option2',
title: 'Option 2',
checked: true
}
]
render(
<FormInput
type="boxSelection"
options={options}
onChange={() => jest.fn()}
/>
)
})
})

View File

@ -16,6 +16,8 @@ import Disclaimer from './Disclaimer'
import Tooltip from '@shared/atoms/Tooltip' import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown' import Markdown from '@shared/Markdown'
import FormHelp from './Help' import FormHelp from './Help'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { BoxSelectionOption } from '@shared/FormInput/InputElement/BoxSelection'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -28,7 +30,7 @@ export interface InputProps {
prominentHelp?: boolean prominentHelp?: boolean
tag?: string tag?: string
type?: string type?: string
options?: string[] options?: string[] | AssetSelectionAsset[] | BoxSelectionOption[]
sortOptions?: boolean sortOptions?: boolean
additionalComponent?: ReactElement additionalComponent?: ReactElement
value?: string | number value?: string | number

View File

@ -1,73 +0,0 @@
import * as React from 'react'
import { FormikProps, connect } from 'formik'
import debounce from 'lodash.debounce'
import omit from 'lodash.omit'
import isEqual from 'react-fast-compare'
import { LoggerInstance } from '@oceanprotocol/lib'
export interface PersistProps {
name: string
ignoreFields?: string[]
debounce?: number
isSessionStorage?: boolean
}
// TODO: refactor into functional component
class PersistImpl extends React.Component<
PersistProps & { formik: FormikProps<any> },
any
> {
static defaultProps = {
debounce: 300
}
saveForm = debounce((data: FormikProps<any>) => {
const dataToSave = this.omitIgnoredFields(data)
LoggerInstance.log('data to save', dataToSave)
if (this.props.isSessionStorage) {
window.sessionStorage.setItem(this.props.name, JSON.stringify(dataToSave))
} else {
window.localStorage.setItem(this.props.name, JSON.stringify(dataToSave))
}
}, this.props.debounce)
omitIgnoredFields = (data: FormikProps<any>) => {
const { ignoreFields } = this.props
LoggerInstance.log('omitted fields', ignoreFields)
const { values, touched, errors } = data
LoggerInstance.log('values', values, omit(values, ignoreFields))
return ignoreFields
? omit(
{
...data,
values: omit(values, ignoreFields),
touched: omit(touched, ignoreFields),
errors: omit(errors, ignoreFields)
},
ignoreFields
)
: data
}
componentDidUpdate(prevProps: PersistProps & { formik: FormikProps<any> }) {
if (!isEqual(prevProps.formik, this.props.formik)) {
this.saveForm(this.props.formik)
}
}
componentDidMount() {
const maybeState = this.props.isSessionStorage
? window.sessionStorage.getItem(this.props.name)
: window.localStorage.getItem(this.props.name)
if (maybeState && maybeState !== null) {
this.props.formik.setFormikState(JSON.parse(maybeState))
}
}
render(): null {
return null
}
}
export const Persist = connect<PersistProps, any>(PersistImpl)

View File

@ -19,5 +19,5 @@ export function NetworkIcon({ name }: { name: string }): ReactElement {
? EnergywebIcon ? EnergywebIcon
: EthIcon // ETH icon as fallback : EthIcon // ETH icon as fallback
return IconMapped ? <IconMapped className={styles.icon} /> : null return <IconMapped className={styles.icon} />
} }

View File

@ -0,0 +1,33 @@
import { render } from '@testing-library/react'
import React from 'react'
import NetworkName from './index'
describe('@shared/NetworkName', () => {
it('renders without crashing', () => {
render(<NetworkName networkId={1} />)
})
it('renders minimal', () => {
render(<NetworkName networkId={1} minimal />)
})
it('renders Polygon', () => {
render(<NetworkName networkId={137} />)
})
it('renders BSC', () => {
render(<NetworkName networkId={56} />)
})
it('renders Energy Web', () => {
render(<NetworkName networkId={246} />)
})
it('renders Moonriver', () => {
render(<NetworkName networkId={1285} />)
})
it('renders icon fallback', () => {
render(<NetworkName networkId={99999} />)
})
})

View File

@ -0,0 +1,20 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Page from './index'
describe('@shared/Page', () => {
it('renders without crashing', () => {
render(
<Page uri="/hello" title="Hello Title" description="Hello Description">
Hello Children
</Page>
)
expect(screen.getByText('Hello Children')).toBeInTheDocument()
expect(screen.getByText('Hello Title')).toBeInTheDocument()
expect(screen.getByText('Hello Description')).toBeInTheDocument()
})
it('renders without title', () => {
render(<Page uri="/hello">Hello Children</Page>)
})
})

View File

@ -0,0 +1,28 @@
import testRender from '../../../../.jest/testRender'
import { render } from '@testing-library/react'
import React from 'react'
import Pagination from './index'
import { MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS } from '@utils/aquarius'
describe('@shared/Pagination', () => {
testRender(
<Pagination
totalPages={MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS + 1}
currentPage={2}
rowsPerPage={10}
rowCount={30}
onChangePage={() => jest.fn()}
/>
)
it('renders without currentPage prop', () => {
render(
<Pagination
totalPages={10}
rowsPerPage={10}
rowCount={30}
onChangePage={() => jest.fn()}
/>
)
})
})

View File

@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Price from './index'
import { asset } from '../../../../.jest/__fixtures__/assetWithAccessDetails'
import prices from '../../../../.jest/__fixtures__/prices'
jest.mock('../../../@context/Prices', () => ({
usePrices: () => prices,
getCoingeckoTokenId: () => 'ocean-protocol'
}))
describe('@shared/Price', () => {
it('renders fixed price', () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, type: 'fixed', price: '10' }}
/>
)
expect(screen.getByText('10')).toBeInTheDocument()
})
it('renders free price', () => {
render(<Price accessDetails={{ ...asset.accessDetails, type: 'free' }} />)
expect(screen.getByText('Free')).toBeInTheDocument()
})
it('renders null price', () => {
render(<Price accessDetails={{ ...asset.accessDetails, price: null }} />)
expect(screen.getByText('-')).toBeInTheDocument()
})
it('renders conversion', async () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, price: '10' }}
conversion
/>
)
expect(await screen.findByText('≈')).toBeInTheDocument()
})
it('renders no conversion when no price defined', async () => {
render(
<Price
accessDetails={{ ...asset.accessDetails, price: null }}
conversion
/>
)
expect(screen.queryByText('≈')).not.toBeInTheDocument()
})
})

View File

@ -7,7 +7,7 @@ const account = '0x0000000000000000000000000000000000000000'
jest.mock('axios') jest.mock('axios')
describe('Publisher', () => { describe('@shared/Publisher', () => {
test('should return correct markup by default', async () => { test('should return correct markup by default', async () => {
;(axios as any).get.mockImplementationOnce(() => ;(axios as any).get.mockImplementationOnce(() =>
Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } }) Promise.resolve({ data: { name: 'jellymcjellyfish.eth' } })

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react'
import React from 'react'
import testRender from '../../../../.jest/testRender'
import SuccessConfetti from './index'
describe('@shared/SuccessConfetti', () => {
testRender(<SuccessConfetti success="Nice Success!" />)
it('renders without success', () => {
render(<SuccessConfetti success={null} />)
})
})

View File

@ -0,0 +1,18 @@
import { render, fireEvent, screen } from '@testing-library/react'
import React from 'react'
import WalletNetworkSwitcher from './'
jest.mock('../../../@utils/web3', () => ({
addCustomNetwork: () => jest.fn()
}))
describe('@shared/WalletNetworkSwitcher', () => {
it('renders without crashing', () => {
render(<WalletNetworkSwitcher />)
})
it('switching networks can be invoked', () => {
render(<WalletNetworkSwitcher />)
fireEvent.click(screen.getByRole('button'))
})
})

View File

@ -13,6 +13,7 @@ export default function WalletNetworkSwitcher(): ReactElement {
const { networkId, web3Provider } = useWeb3() const { networkId, web3Provider } = useWeb3()
const { asset } = useAsset() const { asset } = useAsset()
const { networksList } = useNetworkMetadata() const { networksList } = useNetworkMetadata()
const ddoNetworkData = getNetworkDataById(networksList, asset.chainId) const ddoNetworkData = getNetworkDataById(networksList, asset.chainId)
const walletNetworkData = getNetworkDataById(networksList, networkId) const walletNetworkData = getNetworkDataById(networksList, networkId)

View File

@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import Web3Feedback from './'
import { useGraphSyncStatus } from '../../../@hooks/useGraphSyncStatus'
jest.mock('../../../@hooks/useGraphSyncStatus')
describe('@shared/Web3Feedback', () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: true,
blockGraph: '333333',
blockHead: '333333'
}))
it('renders without crashing', () => {
render(<Web3Feedback networkId={1} accountId="0xxxx" />)
})
it('renders isAssetNetwork === false', async () => {
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={false} />
)
expect(
await screen.findByText('Not connected to asset network')
).toBeInTheDocument()
})
it('renders isGraphSynced === false', async () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: false
}))
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={true} />
)
expect(await screen.findByText('Data out of sync')).toBeInTheDocument()
})
it('renders no account', async () => {
render(<Web3Feedback networkId={1} accountId={undefined} />)
expect(await screen.findByText('No account connected')).toBeInTheDocument()
})
it('do nothing if nothing to show', async () => {
;(useGraphSyncStatus as jest.Mock).mockImplementation(() => ({
isGraphSynced: true
}))
render(
<Web3Feedback networkId={1} accountId="0xxxx" isAssetNetwork={true} />
)
expect(screen.queryByRole('heading')).not.toBeInTheDocument()
})
})

Some files were not shown because too many files have changed in this diff Show More