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

Merge branch 'main' into feature/calica-integration

This commit is contained in:
Matthias Kretschmann 2022-12-06 10:07:50 +00:00
commit 85e183929f
Signed by: m
GPG Key ID: 606EEEF3C479A91F
95 changed files with 6948 additions and 4152 deletions

View File

@ -42,7 +42,7 @@ exclude_patterns:
- '**/*.d.ts'
- '**/@types/'
- '**/_types.*'
- '**/*.stories.tsx'
- '**/*.test.tsx'
- '**/*.stories.*'
- '**/*.test.*'
- '.storybook/'
- '.jest/'

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @mihaisc @kremalicious @claudiaHash @bogdanfazakas @EnzoVezzaro
* @jamiehewitt15 @mihaisc @kremalicious @bogdanfazakas @EnzoVezzaro

View File

@ -4,7 +4,6 @@ on:
push:
branches:
- main
- v4
- v3
tags:
- '**'
@ -20,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3
@ -47,7 +46,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3
@ -82,7 +81,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '18'
- name: Cache node_modules
uses: actions/cache@v3
env:
@ -110,7 +109,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16']
node: ['18']
steps:
- uses: actions/checkout@v3

View File

@ -12,6 +12,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run build:static

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ coverage
.next
.artifacts
.vercel
.swc
repo-metadata.json
networks-metadata.json
src/@types/subgraph

View File

@ -57,12 +57,12 @@ export const assetAquarius: Asset = {
}
],
stats: {
orders: 22
// price: {
// value: 3231343254,
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN'
// }
orders: 22,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
purgatory: {
state: false,

View File

@ -68,8 +68,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 0
// price: {}
orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -151,12 +155,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 45554.69921875,
orders: 1
// price: {
// tokenAddress: '0x282d8efCe846A88B159800bd4130ad77443Fa1A1',
// tokenSymbol: 'mOCEAN',
// value: 100
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -245,12 +249,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 7
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -339,12 +343,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 50.2051887512,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 10
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -439,12 +443,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 6
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -569,12 +573,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 5
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0'
},
@ -657,12 +661,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 5
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -751,12 +755,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 5
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -844,10 +848,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 114.1658859253,
orders: 1
// price: {
// value: 0
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -927,12 +933,12 @@ export const assets: AssetExtended[] = [
}
],
stats: {
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 2
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1018,10 +1024,12 @@ export const assets: AssetExtended[] = [
}
],
stats: {
orders: 1
// price: {
// value: 0
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1103,10 +1111,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 11159.279296875,
orders: 1
// price: {
// value: 0
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1186,10 +1196,12 @@ export const assets: AssetExtended[] = [
}
],
stats: {
orders: 0
// price: {
// value: 0
// }
orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1270,8 +1282,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 0,
orders: 0
// price: {}
orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1339,12 +1355,12 @@ export const assets: AssetExtended[] = [
}
],
stats: {
orders: 1
// price: {
// tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
// tokenSymbol: 'OCEAN',
// value: 1
// }
orders: 1,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1430,10 +1446,12 @@ export const assets: AssetExtended[] = [
}
],
stats: {
orders: 0
// price: {
// value: 0
// }
orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1525,7 +1543,12 @@ export const assets: AssetExtended[] = [
],
stats: {
allocated: 422.9883117676,
orders: 0
orders: 0,
price: {
value: 3231343254,
tokenAddress: '0xCfDdA22C9837aE76E0faA845354f33C62E03653a',
tokenSymbol: 'OCEAN'
}
},
version: '4.1.0',
accessDetails: {
@ -1551,4 +1574,4 @@ export const assets: AssetExtended[] = [
validOrderTx: null
}
}
]
]

View File

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

View File

@ -1 +1,3 @@
export default true
export default function isUrl() {
return true
}

3
.jest/__mocks__/tar.ts Normal file
View File

@ -0,0 +1,3 @@
// mocked, as this module makes Jest go all
// "Uncaught SyntaxError: Octal escape sequences are not allowed in strict mode"
export default jest.fn().mockImplementation(() => 'hello')

View File

@ -9,13 +9,13 @@ const createJestConfig = nextJest({
const customJestConfig = {
rootDir: '../',
// Add more setup options before each test is run
setupFilesAfterEnv: ['<rootDir>/.jest/jest.setup.js'],
setupFilesAfterEnv: ['<rootDir>/.jest/jest.setup.tsx'],
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
moduleDirectories: ['node_modules', '<rootDir>/src'],
testEnvironment: 'jest-environment-jsdom',
testEnvironment: 'jsdom',
moduleNameMapper: {
'^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
// '^@/components/(.*)$': '<rootDir>/components/$1',
'@components/(.*)$': '<rootDir>/src/components/$1',
'@shared(.*)$': '<rootDir>/src/components/@shared/$1',
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
'@context/(.*)$': '<rootDir>/src/@context/$1',
@ -29,8 +29,25 @@ const customJestConfig = {
'!src/**/*.{stories,test}.{ts,tsx}',
'!src/@types/**/*.{ts,tsx}'
],
testPathIgnorePatterns: ['node_modules', '\\.cache', '.next', 'coverage']
// Add ignores so ESM packages are not transformed by Jest
// note: this does not work with Next.js, hence workaround further down
// see: https://github.com/vercel/next.js/issues/35634#issuecomment-1115250297
// transformIgnorePatterns: ['node_modules/(?!(uuid|remark)/)'],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/.next/',
'<rootDir>/coverage'
]
}
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
// https://github.com/vercel/next.js/issues/35634#issuecomment-1115250297
async function jestConfig() {
const nextJestConfig = await createJestConfig(customJestConfig)()
// Add ignores for specific ESM packages so they are transformed by Jest
// /node_modules/ is the first pattern
nextJestConfig.transformIgnorePatterns[0] = '/node_modules/(?!uuid|remark)/'
return nextJestConfig
}
module.exports = jestConfig

View File

@ -1,3 +0,0 @@
import '@testing-library/jest-dom/extend-expect'
import './__mocks__/matchMedia'
import './__mocks__/hooksMocks'

20
.jest/jest.setup.tsx Normal file
View File

@ -0,0 +1,20 @@
import '@testing-library/jest-dom/extend-expect'
import { jest } from '@jest/globals'
import './__mocks__/matchMedia'
import './__mocks__/hooksMocks'
jest.mock('next/router', () => ({
useRouter: jest.fn().mockImplementation(() => ({
route: '/',
pathname: '/'
}))
}))
// jest.mock('next/head', () => {
// return {
// __esModule: true,
// default: ({ children }: { children: Array<React.ReactElement> }) => {
// return <>{children}</>
// }
// }
// })

2
.nvmrc
View File

@ -1 +1 @@
16
18

View File

@ -253,7 +253,7 @@ export default function NetworkName(): ReactElement {
const { networkId, isTestnet } = useWeb3()
const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId)
const networkName = getNetworkDisplayName(networkData)
return (
<>

View File

@ -29,19 +29,62 @@
},
{
"name": "files",
"label": "New file",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. Leaving this field empty will not remove the current value.",
"prominentHelp": true,
"type": "files"
},
{
"label": "File",
"prominentHelp": false,
"type": "tabs",
"fields": [{
"value": "ipfs",
"title": "IPFS",
"label": "CID",
"placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq",
"help": "This CID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}],
"sortOptions": false,
"required": true
},{
"name": "links",
"label": "New sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please provide a URL to a sample of your dataset file. This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** Leaving this field empty will not remove the current value.",
"prominentHelp": true,
"type": "files"
"label": "Sample file",
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": false
}],
"required": false
},
{
@ -66,6 +109,13 @@
"type": "tags",
"placeholder": "e.g. logistics",
"required": false
},
{
"name": "paymentCollector",
"label": "Payment Collector Address",
"placeholder": "e.g. 0X123ABC...",
"help": "This address will receive the revenue from all sales. More info available in our [docs](https://docs.oceanprotocol.com/core-concepts/datanft-and-datatoken#revenue).",
"required": false
}
]
}

View File

@ -104,19 +104,61 @@
{
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"prominentHelp": false,
"type": "tabs",
"fields": [{
"value": "ipfs",
"title": "IPFS",
"label": "CID",
"placeholder": "e.g. bafkreidgvpkjawlxz6sffxzwgooowe5yt7i6wsyg236mfoks77nywkptdq",
"help": "This CID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}],
"sortOptions": false,
"required": true
},
{
},{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"prominentHelp": true,
"type": "files"
"prominentHelp": false,
"type": "tabs",
"fields": [
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": false
}],
"required": false
},
{
"name": "algorithmPrivacy",

9098
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,47 +26,48 @@
"@coingecko/cryptoformat": "^0.5.4",
"@loadable/component": "^5.15.2",
"@oceanprotocol/art": "^3.2.0",
"@oceanprotocol/lib": "^2.4.0",
"@oceanprotocol/lib": "^2.5.2",
"@oceanprotocol/typographies": "^0.1.0",
"@oceanprotocol/use-dark-mode": "^2.4.3",
"@tippyjs/react": "^4.2.6",
"@urql/exchange-refocus": "^1.0.0",
"@walletconnect/web3-provider": "^1.8.0",
"axios": "^1.1.3",
"axios": "^1.2.0",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"decimal.js": "^10.3.1",
"decimal.js": "^10.4.2",
"dom-confetti": "^0.2.2",
"dotenv": "^16.0.1",
"dotenv": "^16.0.3",
"filesize": "^10.0.5",
"formik": "^2.2.9",
"gray-matter": "^4.0.3",
"is-ipfs": "^7.0.3",
"is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1",
"match-sorter": "^6.3.1",
"myetherwallet-blockies": "^0.1.1",
"next": "12.3.1",
"next": "13.0.5",
"query-string": "^7.1.1",
"react": "^18.2.0",
"react-clipboard.js": "^2.0.16",
"react-data-table-component": "^7.5.2",
"react-dom": "^18.1.0",
"react-data-table-component": "^7.5.3",
"react-dom": "^18.2.0",
"react-dotdotdot": "^1.3.1",
"react-modal": "^3.15.1",
"react-paginate": "^8.1.3",
"react-select": "^5.4.0",
"react-spring": "^9.5.2",
"react-modal": "^3.16.1",
"react-paginate": "^8.1.4",
"react-select": "^5.6.1",
"react-spring": "^9.5.5",
"react-tabs": "^5.1.0",
"react-toastify": "^9.0.4",
"remark": "^13.0.0",
"remark-gfm": "^1.0.0",
"remark-html": "^13.0.1",
"react-toastify": "^9.1.1",
"remark": "^14.0.2",
"remark-gfm": "^3.0.1",
"remark-html": "^15.0.1",
"remove-markdown": "^0.5.0",
"slugify": "^1.6.5",
"swr": "^1.3.0",
"urql": "^3.0.3",
"web3": "^1.8.0",
"web3modal": "^1.9.9",
"web3": "^1.8.1",
"web3modal": "^1.9.10",
"yup": "^0.32.11"
},
"devDependencies": {
@ -77,37 +78,38 @@
"@svgr/webpack": "^6.5.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@types/jest": "^29.2.3",
"@types/js-cookie": "^3.0.2",
"@types/loadable__component": "^5.13.4",
"@types/node": "^18.8.5",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.5",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-modal": "^3.13.1",
"@types/react-paginate": "^7.1.1",
"@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"apollo": "^2.34.0",
"cross-env": "^7.0.3",
"eslint": "^8.25.0",
"eslint": "^8.28.0",
"eslint-config-oceanprotocol": "^2.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jest-dom": "^4.0.2",
"eslint-plugin-jest-dom": "^4.0.3",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^5.7.2",
"eslint-plugin-testing-library": "^5.9.1",
"https-browserify": "^1.0.0",
"husky": "^8.0.1",
"jest": "^29.1.2",
"jest-environment-jsdom": "^29.2.2",
"prettier": "^2.7.1",
"husky": "^8.0.2",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"prettier": "^2.8.0",
"pretty-quick": "^3.1.3",
"process": "^0.11.10",
"serve": "^14.0.1",
"serve": "^14.1.2",
"stream-http": "^3.2.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.4"
"typescript": "^4.9.3"
},
"overrides": {
"graphql": "15.8.0"
@ -117,7 +119,7 @@
"url": "https://github.com/oceanprotocol/market"
},
"engines": {
"node": "16"
"node": "18"
},
"browserslist": [
">0.2%",

View File

@ -9,7 +9,7 @@ import React, {
} from 'react'
import { Config, LoggerInstance, Purgatory } from '@oceanprotocol/lib'
import { CancelToken } from 'axios'
import { retrieveAsset } from '@utils/aquarius'
import { getAsset } from '@utils/aquarius'
import { useWeb3 } from './Web3'
import { useCancelToken } from '@hooks/useCancelToken'
import { getOceanConfig, getDevelopmentConfig } from '@utils/ocean'
@ -66,7 +66,7 @@ function AssetProvider({
LoggerInstance.log('[asset] Fetching asset...')
setLoading(true)
const asset = await retrieveAsset(did, token)
const asset = await getAsset(did, token)
if (!asset) {
setError(

View File

@ -7,7 +7,7 @@ import React, {
useEffect,
useState
} from 'react'
import { OpcQuery } from 'src/@types/subgraph/OpcQuery'
import { OpcQuery } from '../../../src/@types/subgraph/OpcQuery'
import { OperationResult } from 'urql'
import { opcQuery } from './_queries'
import { MarketMetadataProviderValue, OpcFee } from './_types'

View File

@ -307,7 +307,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
)
// Construct network display name
const networkDisplayName = getNetworkDisplayName(networkData, networkId)
const networkDisplayName = getNetworkDisplayName(networkData)
setNetworkDisplayName(networkDisplayName)
setIsTestnet(getNetworkType(networkData) !== NetworkType.Mainnet)

View File

@ -0,0 +1,43 @@
import { getNetworkType, getNetworkDisplayName } from './utils'
describe('useNetworkMetadata/utils', () => {
test('getNetworkType returns mainnet', () => {
const type = getNetworkType({
name: 'Eth',
title: 'Eth'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
expect(type).toBe('mainnet')
})
test('getNetworkType returns testnet if "Test" is in name', () => {
const type = getNetworkType({
name: 'Testnet',
title: 'Testnet'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
expect(type).toBe('testnet')
})
test('getNetworkDisplayName returns correct values', () => {
/* eslint-disable @typescript-eslint/no-explicit-any */
const type1 = getNetworkDisplayName({
chainId: 1,
chain: 'ETH',
name: 'Ethereum Mainnet'
} as any)
expect(type1).toBe('ETH')
const type2 = getNetworkDisplayName({ chainId: 80001 } as any)
expect(type2).toBe('Mumbai')
const type3 = getNetworkDisplayName({ chainId: 8996 } as any)
expect(type3).toBe('Development')
const type4 = getNetworkDisplayName({ chainId: 2021000 } as any)
expect(type4).toBe('GAIA-X')
/* eslint-enable @typescript-eslint/no-explicit-any */
})
})

View File

@ -11,9 +11,8 @@ export function getNetworkType(network: EthereumListsChain): string {
// We hack in mainnet detection for moonriver.
if (
network &&
!network.name.includes('Testnet') &&
!network.title?.includes('Testnet') &&
network.name !== 'Moonbase Alpha'
!network.name?.includes('Testnet') &&
!network.title?.includes('Testnet')
) {
return NetworkType.Mainnet
} else {
@ -21,19 +20,13 @@ export function getNetworkType(network: EthereumListsChain): string {
}
}
export function getNetworkDisplayName(
data: EthereumListsChain,
networkId: number
): string {
export function getNetworkDisplayName(data: EthereumListsChain): string {
let displayName
switch (networkId) {
switch (data.chainId) {
case 137:
displayName = 'Polygon'
break
case 1287:
displayName = 'Moonbase'
break
case 1285:
displayName = 'Moonriver'
break
@ -43,9 +36,6 @@ export function getNetworkDisplayName(
case 8996:
displayName = 'Development'
break
case 3:
displayName = 'Ropsten'
break
case 5:
displayName = 'Görli'
break
@ -54,7 +44,9 @@ export function getNetworkDisplayName(
break
default:
displayName = data
? `${data.chain} ${getNetworkType(data) === 'mainnet' ? '' : data.name}`
? `${data.chain}${
getNetworkType(data) === 'mainnet' ? '' : ` ${data.name}`
}`
: 'Unknown'
break
}

View File

@ -0,0 +1,70 @@
import {
SortDirectionOptions,
SortTermOptions
} from '../../@types/aquarius/SearchQuery'
import { escapeEsReservedCharacters, getFilterTerm, generateBaseQuery } from '.'
const defaultBaseQueryReturn = {
from: 0,
query: {
bool: {
filter: [
{ terms: { chainId: [1, 3] } },
{ term: { _index: 'aquarius' } },
{ term: { 'purgatory.state': false } },
{ bool: { must_not: [{ term: { 'nft.state': 5 } }] } }
]
}
},
size: 1000
}
describe('@utils/aquarius', () => {
test('escapeEsReservedCharacters', () => {
expect(escapeEsReservedCharacters('<')).toBe('\\<')
})
test('getFilterTerm with string value', () => {
expect(getFilterTerm('hello', 'world')).toStrictEqual({
term: { hello: 'world' }
})
})
test('getFilterTerm with array value', () => {
expect(getFilterTerm('hello', ['world', 'domination'])).toStrictEqual({
terms: { hello: ['world', 'domination'] }
})
})
test('generateBaseQuery', () => {
expect(generateBaseQuery({ chainIds: [1, 3] })).toStrictEqual(
defaultBaseQueryReturn
)
})
test('generateBaseQuery aggs are passed through', () => {
expect(
generateBaseQuery({ chainIds: [1, 3], aggs: 'hello world' })
).toStrictEqual({
...defaultBaseQueryReturn,
aggs: 'hello world'
})
})
test('generateBaseQuery sortOptions are passed through', () => {
expect(
generateBaseQuery({
chainIds: [1, 3],
sortOptions: {
sortBy: SortTermOptions.Created,
sortDirection: SortDirectionOptions.Ascending
}
})
).toStrictEqual({
...defaultBaseQueryReturn,
sort: {
'nft.created': 'asc'
}
})
})
})

View File

@ -1,13 +1,13 @@
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config'
import { OrdersData_orders as OrdersData } from '../../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../../app.config'
import {
SortDirectionOptions,
SortTermOptions
} from '../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from './assetConvertor'
} from '../../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from '../assetConvertor'
export interface UserSales {
id: string
@ -19,7 +19,7 @@ export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
export function escapeEsReservedCharacters(value: string): string {
// eslint-disable-next-line no-useless-escape
const pattern = /([\!\*\+\-\=\<\>\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g
return value.replace(pattern, '\\$1')
return value?.replace(pattern, '\\$1')
}
/**
@ -55,13 +55,13 @@ export function generateBaseQuery(
...(baseQueryParams.filters || []),
baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: '',
: [],
getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory
? ''
? []
: [getFilterTerm('purgatory.state', false)]),
...(baseQueryParams.ignoreState
? ''
? []
: [
{
bool: {
@ -143,7 +143,7 @@ export async function queryMetadata(
}
}
export async function retrieveAsset(
export async function getAsset(
did: string,
cancelToken: CancelToken
): Promise<Asset> {
@ -186,73 +186,7 @@ export async function getAssetsNames(
}
}
export async function getAssetsFromDidList(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<PagedAssets> {
try {
if (!didList.length) return
const baseParams = {
chainIds,
filters: [getFilterTerm('_id', didList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function getAssetsFromDtList(
dtList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<Asset[]> {
try {
if (!dtList.length) return
const baseParams = {
chainIds,
filters: [getFilterTerm('services.datatokenAddress', dtList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult?.results
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function getAssetsFromNftList(
nftList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<Asset[]> {
try {
if (!(nftList.length > 0)) return
const baseParams = {
chainIds,
filters: [getFilterTerm('nftAddress', nftList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult?.results
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function retrieveDDOListByDIDs(
export async function getAssetsFromDids(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
@ -368,7 +302,7 @@ export async function getPublishedAssets(
}
}
export async function getTopPublishers(
async function getTopPublishers(
chainIds: number[],
cancelToken: CancelToken,
page?: number,

View File

@ -17,7 +17,7 @@ import {
queryMetadata,
getFilterTerm,
generateBaseQuery,
retrieveDDOListByDIDs
getAssetsFromDids
} from './aquarius'
import { fetchDataForMultipleChains } from './subgraph'
import { getServiceById, getServiceByName } from './ddo'
@ -338,7 +338,7 @@ export async function createTrustedAlgorithmList(
if (!selectedAlgorithms || selectedAlgorithms.length === 0)
return trustedAlgorithms
const selectedAssets = await retrieveDDOListByDIDs(
const selectedAssets = await getAssetsFromDids(
selectedAlgorithms,
[assetChainId],
cancelToken
@ -393,31 +393,3 @@ export async function transformComputeFormToServiceComputeOptions(
return privacy
}
export async function checkComputeResourcesValidity(
asset: Asset,
accountId: string,
computeEnvMaxJobDuration: number,
datasetTimeout?: number,
algorithmTimeout?: number,
cancelToken?: CancelToken
): Promise<boolean> {
const jobs = await getComputeJobs(
[asset?.chainId],
accountId,
asset,
cancelToken
)
if (jobs.computeJobs.length <= 0) return false
const inputValues = []
computeEnvMaxJobDuration && inputValues.push(computeEnvMaxJobDuration * 60)
datasetTimeout && inputValues.push(datasetTimeout)
algorithmTimeout && inputValues.push(algorithmTimeout)
const minValue = Math.min(...inputValues)
const jobStartDate = new Date(
parseInt(jobs.computeJobs[0].dateCreated) * 1000
)
jobStartDate.setMinutes(jobStartDate.getMinutes() + Math.floor(minValue / 60))
const currentTime = new Date().getTime() / 1000
return Math.floor(jobStartDate.getTime() / 1000) > currentTime
}

View File

@ -1,5 +1,7 @@
import { getEnsName, getEnsAddress, getEnsProfile } from './ens'
import { getEnsName, getEnsAddress, getEnsProfile } from '.'
// TODO: this directly hits the ENS registry, which is not ideal
// so we need to rewrite this to mock responses instead for more reliable test runs.
describe('@utils/ens', () => {
jest.setTimeout(10000)
jest.retryTimes(2)

View File

@ -1,4 +1,4 @@
import { fetchData } from './fetch'
import { fetchData } from '../fetch'
const apiUrl = 'https://ens-proxy.oceanprotocol.com/api'

View File

@ -3,12 +3,6 @@ import { Asset } from '@oceanprotocol/lib'
// Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined'
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
const index = arr.indexOf(value)
if (index > -1) {

5
src/@utils/ipfs.ts Normal file
View File

@ -0,0 +1,5 @@
import * as isIPFS from 'is-ipfs'
export function isCID(value: string) {
return isIPFS.cid(value)
}

View File

@ -1,4 +1,4 @@
import remark from 'remark'
import { remark } from 'remark'
import remarkHtml from 'remark-html'
import remarkGfm from 'remark-gfm'
@ -6,7 +6,7 @@ export function markdownToHtml(markdown: string): string {
const result = remark()
.use(remarkGfm)
.use(remarkHtml) // serializes through remark-rehype and rehype-stringify
.processSync(markdown).contents
.processSync(markdown)
return result.toString()
}

View File

@ -1,9 +1,11 @@
import {
Arweave,
ComputeAlgorithm,
ComputeAsset,
ComputeEnvironment,
downloadFileBrowser,
FileInfo,
Ipfs,
LoggerInstance,
ProviderComputeInitializeResults,
ProviderInstance,
@ -83,18 +85,45 @@ export async function getFileDidInfo(
}
}
export async function getFileUrlInfo(
url: string,
providerUrl: string
export async function getFileInfo(
file: string,
providerUrl: string,
storageType: string
): Promise<FileInfo[]> {
try {
const fileUrl: UrlFile = {
type: 'url',
index: 0,
url,
method: 'get'
let response
switch (storageType) {
case 'ipfs': {
const fileIPFS: Ipfs = {
type: 'ipfs',
hash: file
}
response = await ProviderInstance.getFileInfo(fileIPFS, providerUrl)
break
}
case 'arweave': {
const fileArweave: Arweave = {
type: 'arweave',
transactionId: file
}
response = await ProviderInstance.getFileInfo(fileArweave, providerUrl)
break
}
default: {
const fileUrl: UrlFile = {
type: 'url',
index: 0,
url: file,
method: 'get'
}
response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
break
}
}
const response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
return response
} catch (error) {
LoggerInstance.error(error.message)

View File

@ -1,4 +1,4 @@
import { sanitizeUrl } from './url'
import { sanitizeUrl } from '.'
describe('@utils/url', () => {
test('sanitizeUrl', () => {

View File

@ -1,7 +1,7 @@
import { AllLockedQuery } from 'src/@types/subgraph/AllLockedQuery'
import { OwnAllocationsQuery } from 'src/@types/subgraph/OwnAllocationsQuery'
import { NftOwnAllocationQuery } from 'src/@types/subgraph/NftOwnAllocationQuery'
import { OceanLockedQuery } from 'src/@types/subgraph/OceanLockedQuery'
import { AllLockedQuery } from '../../src/@types/subgraph/AllLockedQuery'
import { OwnAllocationsQuery } from '../../src/@types/subgraph/OwnAllocationsQuery'
import { NftOwnAllocationQuery } from '../../src/@types/subgraph/NftOwnAllocationQuery'
import { OceanLockedQuery } from '../../src/@types/subgraph/OceanLockedQuery'
import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios'
@ -11,9 +11,6 @@ import {
getNetworkType,
NetworkType
} from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from '../../app.config'
import { Asset } from '@oceanprotocol/lib'
const AllLocked = gql`
query AllLockedQuery {
@ -80,6 +77,7 @@ export function getVeChainNetworkIds(assetNetworkIds: number[]): number[] {
})
return veNetworkIds
}
export async function getNftOwnAllocation(
userAddress: string,
nftAddress: string,
@ -177,17 +175,3 @@ export async function getOwnAllocations(
return allocations
}
export async function getOwnAssetsWithAllocation(
networkIds: number[],
userAddress: string
): Promise<Asset[]> {
const allocations = await getOwnAllocations(networkIds, userAddress)
const assets = await getAssetsFromNftList(
allocations.map((x) => x.nftAddress),
chainIdsSupported,
null
)
return assets
}

View File

@ -33,7 +33,7 @@ export async function addCustomNetwork(
const newNetworkData = {
chainId: `0x${network.chainId.toString(16)}`,
chainName: getNetworkDisplayName(network, network.chainId),
chainName: getNetworkDisplayName(network),
nativeCurrency: network.nativeCurrency,
rpcUrls: network.rpc,
blockExplorerUrls

57
src/@utils/yup.ts Normal file
View File

@ -0,0 +1,57 @@
import { isCID } from '@utils/ipfs'
import isUrl from 'is-url-superb'
import * as Yup from 'yup'
export function testLinks(isEdit?: boolean) {
return Yup.string().test((value, context) => {
const { type } = context.parent
let validField
let errorMessage
switch (type) {
// we allow submit if the type input is hidden as will be ignore
case 'hidden':
validField = true
break
case 'url':
validField = isUrl(value?.toString() || '')
// if we're in publish, the field must be valid
if (!validField) {
validField = false
errorMessage = 'Must be a valid url.'
}
// we allow submit if we're in the edit page and the field is empty
if (
(!value?.toString() && isEdit) ||
(!value?.toString() && context.path === 'services[0].links[0].url')
) {
validField = true
}
// if the url has google drive, we need to block the user from submit
if (value?.toString().includes('drive.google')) {
validField = false
errorMessage =
'Google Drive is not a supported hosting service. Please use an alternative.'
}
break
case 'ipfs':
validField = isCID(value?.toString())
errorMessage = !value?.toString() ? 'CID required.' : 'CID not valid.'
break
case 'arweave':
validField = !value?.toString().includes('http')
errorMessage = !value?.toString()
? 'Transaction ID required.'
: 'Transaction ID not valid.'
break
}
if (!validField) {
return context.createError({
message: errorMessage
})
}
return true
})
}

View File

@ -41,9 +41,7 @@ export default function AssetListTitle({
return (
<h3 className={styles.title}>
<Link href={`/asset/${did || asset?.id}`}>
<a>{assetTitle}</a>
</Link>
<Link href={`/asset/${did || asset?.id}`}>{assetTitle}</Link>
</h3>
)
}

View File

@ -35,83 +35,77 @@ export default function AssetTeaser({
return (
<article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}>
<a className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>
{datatokens[0]?.symbol.substring(0, 9)}
</span>
<NetworkName
networkId={asset.chainId}
className={styles.typeLabel}
/>
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
<Link href={`/asset/${asset.id}`} className={styles.link}>
<aside className={styles.detailLine}>
<AssetType
className={styles.typeLabel}
type={type}
accessType={accessType}
/>
<span className={styles.typeLabel}>
{datatokens[0]?.symbol.substring(0, 9)}
</span>
<NetworkName networkId={asset.chainId} className={styles.typeLabel} />
</aside>
<header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}>
{name.slice(0, 200)}
</Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />}
</header>
{!noDescription && (
<div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')}
</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>
</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}>
{allocated < 0 ? (
''
) : (
<Price accessDetails={asset.accessDetails} size="small" />
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</div>
)}
<footer className={styles.footer}>
{allocated && allocated > 0 ? (
<span className={styles.typeLabel}>
{allocated < 0 ? (
''
) : (
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0 ? (
'N/A'
) : (
<>
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
</>
)}
</span>
) : null}
{asset.views && asset.views > 0 ? (
<span className={styles.typeLabel}>
{asset.views < 0 ? (
'N/A'
) : (
<>
<strong>{asset.views}</strong>{' '}
{asset.views === 1 ? 'view' : 'views'}
</>
)}
</span>
) : null}
</footer>
</a>
</span>
) : null}
{orders && orders > 0 ? (
<span className={styles.typeLabel}>
{orders < 0 ? (
'N/A'
) : (
<>
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
</>
)}
</span>
) : null}
{asset.views && asset.views > 0 ? (
<span className={styles.typeLabel}>
{asset.views < 0 ? (
'N/A'
) : (
<>
<strong>{asset.views}</strong>{' '}
{asset.views === 1 ? 'view' : 'views'}
</>
)}
</span>
) : null}
</footer>
</Link>
</article>
)

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useState } from 'react'
import { useField, useFormikContext } from 'formik'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { FormPublishData } from 'src/components/Publish/_types'
import { FormPublishData } from '@components/Publish/_types'
import { LoggerInstance } from '@oceanprotocol/lib'
import ImageInfo from './Info'
import { getContainerChecksum } from '@utils/docker'
@ -75,6 +75,7 @@ export default function ContainerInput(props: InputProps): ReactElement {
name={`${field.name}[0].url`}
checkUrl={false}
isLoading={isLoading}
storageType={'url'}
handleButtonClick={handleValidation}
/>
)}

View File

@ -23,7 +23,7 @@ export default function FileInfo({
{hideUrl ? 'https://oceanprotocol/placeholder' : file.url}
</h3>
<ul>
<li className={styles.success}> URL confirmed</li>
<li className={styles.success}> File confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{contentTypeCleaned && <li>{contentTypeCleaned}</li>}
</ul>

View File

@ -2,7 +2,7 @@ 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'
import { getFileInfo } from '@utils/provider'
jest.mock('formik')
jest.mock('@utils/provider')
@ -48,16 +48,17 @@ const mockForm = {
describe('@shared/FormInput/InputElement/FilesInput', () => {
it('renders without crashing', async () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
;(getFileUrlInfo as jest.Mock).mockReturnValue([
;(getFileInfo as jest.Mock).mockReturnValue([
{
valid: true,
url: 'https://hello.com',
type: 'url',
contentType: 'text/html',
contentLength: 100
}
])
render(<FilesInput form={mockForm} {...props} />)
render(<FilesInput form={mockForm} field={mockField} {...props} />)
expect(screen.getByText('Validate')).toBeInTheDocument()
fireEvent.click(screen.getByText('Validate'))
@ -67,13 +68,14 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
expect(mockHelpers.setValue).toHaveBeenCalled()
})
it('renders fileinfo when file is valid', () => {
it('renders fileinfo when file url is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
url: 'https://hello.com',
type: 'url',
contentType: 'text/html',
contentLength: 100
}
@ -82,10 +84,52 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('https://hello.com')).toBeInTheDocument()
})
it('renders fileinfo when ipfs is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
hash: 'bafkreicxccbk4blsx5qtovqfgsuutxjxom47dvyzyz3asi2ggjg5ipwlc4',
type: 'ipfs',
contentLength: '40492',
contentType: 'text/csv',
index: 0
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('✓ File confirmed')).toBeInTheDocument()
})
it('renders fileinfo when arweave is valid', () => {
;(useField as jest.Mock).mockReturnValue([
{
value: [
{
valid: true,
transactionId: 'T6NL8Zc0LCbT3bF9HacAGQC4W0_hW7b3tXbm8OtWtlA',
type: 'arweave',
contentLength: '57043',
contentType: 'image/jpeg',
index: 0
}
]
},
mockMeta,
mockHelpers
])
render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('✓ File confirmed')).toBeInTheDocument()
})
it('renders fileinfo without contentType', () => {
;(useField as jest.Mock).mockReturnValue([
{
@ -93,6 +137,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
{
valid: true,
url: 'https://hello.com',
type: 'url',
contentLength: 100
}
]
@ -100,7 +145,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
render(<FilesInput {...props} field={mockField} />)
})
it('renders fileinfo placeholder when hideUrl is passed', () => {
@ -117,7 +162,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta,
mockHelpers
])
render(<FilesInput {...props} />)
render(<FilesInput {...props} field={mockField} />)
expect(
screen.getByText('https://oceanprotocol/placeholder')
).toBeInTheDocument()

View File

@ -1,9 +1,9 @@
import React, { ReactElement, useState } from 'react'
import React, { ReactElement, useEffect, useState } from 'react'
import { useField } from 'formik'
import FileInfo from './Info'
import UrlInput from '../URLInput'
import { InputProps } from '@shared/FormInput'
import { getFileUrlInfo } from '@utils/provider'
import { getFileInfo } from '@utils/provider'
import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset'
@ -12,16 +12,27 @@ export default function FilesInput(props: InputProps): ReactElement {
const [isLoading, setIsLoading] = useState(false)
const { asset } = useAsset()
const providerUrl = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
const storageType = field.value[0].type
async function handleValidation(e: React.SyntheticEvent, url: string) {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e?.preventDefault()
try {
const providerUrl = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
setIsLoading(true)
const checkedFile = await getFileUrlInfo(url, providerUrl)
// TODO: handled on provider
if (url.includes('drive.google')) {
throw Error(
'Google Drive is not a supported hosting service. Please use an alternative.'
)
}
const checkedFile = await getFileInfo(url, providerUrl, storageType)
// error if something's not right from response
if (!checkedFile)
@ -31,7 +42,7 @@ export default function FilesInput(props: InputProps): ReactElement {
throw Error('✗ No valid file detected. Check your URL and try again.')
// if all good, add file to formik state
helpers.setValue([{ url, ...checkedFile[0] }])
helpers.setValue([{ url, type: storageType, ...checkedFile[0] }])
} catch (error) {
props.form.setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message)
@ -42,7 +53,9 @@ export default function FilesInput(props: InputProps): ReactElement {
function handleClose() {
helpers.setTouched(false)
helpers.setValue(meta.initialValue)
helpers.setValue([
{ url: '', type: storageType === 'hidden' ? 'ipfs' : storageType }
])
}
return (
@ -56,7 +69,9 @@ export default function FilesInput(props: InputProps): ReactElement {
{...props}
name={`${field.name}[0].url`}
isLoading={isLoading}
checkUrl={true}
handleButtonClick={handleValidation}
storageType={storageType}
/>
)}
</>

View File

@ -6,7 +6,7 @@ import FileInfo from '../FilesInput/Info'
import styles from './index.module.css'
import Button from '@shared/atoms/Button'
import { LoggerInstance, ProviderInstance } from '@oceanprotocol/lib'
import { FormPublishData } from 'src/components/Publish/_types'
import { FormPublishData } from '@components/Publish/_types'
import { getOceanConfig } from '@utils/ocean'
import { useWeb3 } from '@context/Web3'
import axios from 'axios'

View File

@ -6,6 +6,7 @@ import styles from './index.module.css'
import InputGroup from '@shared/FormInput/InputGroup'
import InputElement from '@shared/FormInput/InputElement'
import isUrl from 'is-url-superb'
import { isCID } from '@utils/ipfs'
export interface URLInputProps {
submitText: string
@ -13,6 +14,7 @@ export interface URLInputProps {
isLoading: boolean
name: string
checkUrl?: boolean
storageType?: string
}
export default function URLInput({
@ -21,6 +23,7 @@ export default function URLInput({
isLoading,
name,
checkUrl,
storageType,
...props
}: URLInputProps): ReactElement {
const [field, meta] = useField(name)
@ -32,7 +35,8 @@ export default function URLInput({
setIsButtonDisabled(
!field?.value ||
field.value === '' ||
(checkUrl && !isUrl(field.value)) ||
(checkUrl && storageType === 'url' && !isUrl(field.value)) ||
(checkUrl && storageType === 'ipfs' && !isCID(field.value)) ||
field.value.includes('javascript:') ||
meta?.error
)

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useCallback, useState } from 'react'
import styles from './index.module.css'
import { InputProps } from '..'
import FilesInput from './FilesInput'
@ -11,6 +11,7 @@ import Nft from './Nft'
import InputRadio from './Radio'
import ContainerInput from '@shared/FormInput/InputElement/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete'
import TabsFile from '@shared/atoms/TabsFile'
const cx = classNames.bind(styles)
@ -55,6 +56,7 @@ export default function InputElement({
...props
}: InputProps): ReactElement {
const styleClasses = cx({ select: true, [size]: size })
switch (props.type) {
case 'select': {
const sortedOptions =
@ -80,6 +82,26 @@ export default function InputElement({
</select>
)
}
case 'tabs': {
const tabs: any = []
props.fields.map((field: any, i) => {
return tabs.push({
title: field.title,
field,
props,
content: (
<FilesInput
key={`fileInput_${i}`}
{...field}
form={form}
{...props}
/>
)
})
})
return <TabsFile items={tabs} className={styles.pricing} />
}
case 'textarea':
return <textarea id={props.name} className={styles.textarea} {...props} />

View File

@ -32,6 +32,7 @@ export interface InputProps {
type?: string
options?: string[] | AssetSelectionAsset[] | BoxSelectionOption[]
sortOptions?: boolean
fields?: FieldInputProps<any>[]
additionalComponent?: ReactElement
value?: string | number
onChange?(

View File

@ -17,7 +17,7 @@ export default function NetworkName({
}): ReactElement {
const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId)
const networkName = getNetworkDisplayName(networkData)
return (
<span

View File

@ -41,8 +41,8 @@ export default function Publisher({
name
) : (
<>
<Link href={`/profile/${account}`}>
<a title="Show profile page.">{name}</a>
<Link href={`/profile/${account}`} title="Show profile page.">
{name}
</Link>
</>
)}

View File

@ -18,10 +18,10 @@ export default function WalletNetworkSwitcher(): ReactElement {
const walletNetworkData = getNetworkDataById(networksList, networkId)
const ddoNetworkName = (
<strong>{getNetworkDisplayName(ddoNetworkData, asset.chainId)}</strong>
<strong>{getNetworkDisplayName(ddoNetworkData)}</strong>
)
const walletNetworkName = (
<strong>{getNetworkDisplayName(walletNetworkData, networkId)}</strong>
<strong>{getNetworkDisplayName(walletNetworkData)}</strong>
)
async function switchWalletNetwork() {

View File

@ -1,5 +1,4 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '@context/Web3'
import Status from '@shared/atoms/Status'
import styles from './index.module.css'
import WalletNetworkSwitcher from '../WalletNetworkSwitcher'

View File

@ -41,10 +41,8 @@ export default function Button({
})
return to ? (
<Link href={to}>
<a className={styleClasses} {...props}>
{children}
</a>
<Link href={to} className={styleClasses} {...props}>
{children}
</Link>
) : href ? (
<a href={href} className={styleClasses} {...props}>

View File

@ -0,0 +1,91 @@
.tabListContainer {
margin-left: auto;
margin-right: auto;
border-bottom: 1px solid var(--border-color);
max-width: 100%;
}
.tabList {
text-align: center;
padding: calc(var(--spacer) / 2) 0 0 0;
display: flex;
overflow-x: auto;
scrollbar-width: none;
}
.tab {
display: inline-block;
padding: calc(var(--spacer) / 8) calc(var(--spacer) / 2);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
text-transform: uppercase;
color: var(--color-secondary);
margin-right: -1px;
background: none;
border: 0;
}
.tabHidden {
display: none !important;
}
.tab,
.tab label {
cursor: pointer;
}
.tab:focus-visible {
outline: none;
}
.tab[aria-selected='true'] {
background-color: inherit;
color: inherit;
list-style: none;
background-color: var(--background-body);
border: 1px solid var(--border-color);
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
border-bottom: 0;
}
.tab[aria-disabled='true'] {
cursor: not-allowed;
}
.tab > div {
margin: 0;
}
.tabContent {
padding: calc(var(--spacer) / 2);
border: 1px solid var(--border-color);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
border-top: 0;
}
.tabLabel {
color: var(--font-color-text);
font-size: var(--font-size-small);
font-family: var(--font-family-heading);
line-height: 1.2;
display: block;
margin-bottom: calc(var(--spacer) / 2);
}
@media (min-width: 40rem) {
.tabContent {
padding: calc(var(--spacer) / 2);
}
}
@media (min-width: 60rem) {
.tab {
padding: calc(var(--spacer) / 8) var(--spacer);
}
}
.radio {
composes: radio from '../../FormInput/InputElement/Radio/index.module.css';
}

View File

@ -0,0 +1,52 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react'
import Tabs, { TabsProps } from '@shared/atoms/Tabs'
export default {
title: 'Component/@shared/atoms/Tabs',
component: Tabs
} as ComponentMeta<typeof Tabs>
const Template: ComponentStory<typeof Tabs> = (args) => <Tabs {...args} />
interface Props {
args: TabsProps
}
const items = [
{
title: 'First tab',
content: 'this is the content for the first tab'
},
{
title: 'Second tab',
content: 'this is the content for the second tab'
},
{
title: 'Third tab',
content: 'this is the content for the third tab'
}
]
export const Default = Template.bind({})
Default.args = {
items
}
export const WithRadio: Props = Template.bind({})
WithRadio.args = {
items,
showRadio: true
}
export const WithDefaultIndex: Props = Template.bind({})
WithDefaultIndex.args = {
items,
defaultIndex: 1
}
export const LotsOfTabs: Props = Template.bind({})
LotsOfTabs.args = {
items: items.flatMap((i) => [i, i, i])
}

View File

@ -0,0 +1,25 @@
import React from 'react'
import { render, screen, fireEvent } from '@testing-library/react'
import { Default, WithRadio } from './index.stories'
describe('Tabs', () => {
test('should be able to change', async () => {
render(<Default {...Default.args} />)
fireEvent.click(screen.getByText('Second tab'))
const secondTab = await screen.findByText(/content for the second tab/i)
expect(secondTab).toBeInTheDocument()
})
test('should fire custom change handler', async () => {
const handler = jest.fn()
render(<Default {...Default.args} handleTabChange={handler} />)
fireEvent.click(screen.getByText('Second tab'))
expect(handler).toBeCalledTimes(1)
})
test('renders WithRadio', () => {
render(<Default {...WithRadio.args} />)
})
})

View File

@ -0,0 +1,109 @@
import Label from '@shared/FormInput/Label'
import Markdown from '@shared/Markdown'
import { useFormikContext } from 'formik'
import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import { FormPublishData } from 'src/components/Publish/_types'
import Tooltip from '../Tooltip'
import styles from './index.module.css'
export interface TabsItem {
field: any
title: string
content: ReactNode
disabled?: boolean
props: any
}
export interface TabsProps {
items: TabsItem[]
className?: string
}
export default function TabsFile({
items,
className
}: TabsProps): ReactElement {
const { values, setFieldValue } = useFormikContext<FormPublishData>()
const [tabIndex, setTabIndex] = useState(0)
// hide tabs if are hidden
const isHidden = items[tabIndex].props.value[0].type === 'hidden'
const setIndex = (tabName: string) => {
const index = items.findIndex((tab: any) => {
if (tab.title !== tabName) return false
return tab
})
setTabIndex(index)
setFieldValue(`${items[index].props.name}[0]`, {
url: '',
type: items[index].field.value
})
}
const handleTabChange = (tabName: string) => {
setIndex(tabName)
}
let textToolTip = false
if (values?.services) {
textToolTip = values.services[0].access === 'compute'
}
return (
<ReactTabs className={`${className || ''}`} defaultIndex={tabIndex}>
<div className={styles.tabListContainer}>
<TabList className={styles.tabList}>
{items.map((item, index) => {
return (
<Tab
className={`${styles.tab} ${
isHidden ? styles.tabHidden : null
}`}
key={`tab_${items[tabIndex].props.name}_${index}`}
onClick={
handleTabChange ? () => handleTabChange(item.title) : null
}
disabled={item.disabled}
>
{item.title}
</Tab>
)
})}
</TabList>
</div>
<div className={styles.tabContent}>
{items.map((item, index) => {
return (
<>
<TabPanel key={`tabpanel_${items[tabIndex].props.name}_${index}`}>
{!isHidden && (
<label className={styles.tabLabel}>
{item.field.label}
{item.field.required && (
<span title="Required" className={styles.required}>
*
</span>
)}
{item.field.help && item.field.prominentHelp && (
<Tooltip
content={
<Markdown
text={`${item.field.help} ${
textToolTip ? item.field.computeHelp : ''
}`}
/>
}
/>
)}
</label>
)}
{item.content}
</TabPanel>
</>
)
})}
</div>
</ReactTabs>
)
}

View File

@ -15,10 +15,12 @@ const Tag = ({ tag, noLinks }: { tag: string; noLinks?: boolean }) => {
return noLinks ? (
<span className={styles.tag}>{tag}</span>
) : (
<Link href={`/search?tags=${urlEncodedTag}&sort=_score&sortOrder=desc`}>
<a className={styles.tag} title={tag}>
{tag}
</a>
<Link
href={`/search?tags=${urlEncodedTag}&sort=_score&sortOrder=desc`}
className={styles.tag}
title={tag}
>
{tag}
</Link>
)
}

View File

@ -24,24 +24,26 @@ export default function AssetComputeSelection({
<Empty />
) : (
assets.map((asset: AssetSelectionAsset) => (
<Link href={`/asset/${asset.did}`} key={asset.did}>
<a className={styles.row}>
<div className={styles.info}>
<h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name}
</Dotdotdot>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.symbol} | {asset.did}
<Link
href={`/asset/${asset.did}`}
key={asset.did}
className={styles.row}
>
<div className={styles.info}>
<h3 className={styles.title}>
<Dotdotdot clamp={1} tagName="span">
{asset.name}
</Dotdotdot>
</div>
<PriceUnit
price={Number(asset.price)}
size="small"
className={styles.price}
/>
</a>
</h3>
<Dotdotdot clamp={1} tagName="code" className={styles.did}>
{asset.symbol} | {asset.did}
</Dotdotdot>
</div>
<PriceUnit
price={Number(asset.price)}
size="small"
className={styles.price}
/>
</Link>
))
)}

View File

@ -10,7 +10,7 @@ export default function ComputeHistory({
}: {
title: string
children: ReactNode
refetchJobs?: any
refetchJobs?: React.Dispatch<React.SetStateAction<boolean>>
}): ReactElement {
const [open, setOpen] = useState(false)

View File

@ -7,13 +7,13 @@ import { compareAsBN } from '@utils/numbers'
import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback'
import { getFileDidInfo, getFileUrlInfo } from '@utils/provider'
import { getFileDidInfo, getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import styles from './index.module.css'
import { useFormikContext } from 'formik'
import { FormPublishData } from 'src/components/Publish/_types'
import { FormPublishData } from '@components/Publish/_types'
import { getTokenBalanceFromSymbol } from '@utils/web3'
import AssetStats from './AssetStats'
import CalicaIntegration from './CalicaIntegration'
@ -53,14 +53,20 @@ export default function AssetActions({
formikState?.values?.services[0].providerUrl.url ||
asset?.services[0]?.serviceEndpoint
const storageType = formikState?.values?.services
? formikState?.values?.services[0].files[0].type
: null
try {
const fileInfoResponse = formikState?.values?.services?.[0].files?.[0]
.url
? await getFileUrlInfo(
? await getFileInfo(
formikState?.values?.services?.[0].files?.[0].url,
providerUrl
providerUrl,
storageType
)
: await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl)
fileInfoResponse && setFileMetadata(fileInfoResponse[0])
// set the content type in the Dataset Schema

View File

@ -1,12 +1,29 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useState, useEffect } from 'react'
import MetaItem from './MetaItem'
import styles from './MetaFull.module.css'
import Publisher from '@shared/Publisher'
import { useAsset } from '@context/Asset'
import { Asset } from '@oceanprotocol/lib'
import { useWeb3 } from '@context/Web3'
import { Asset, Datatoken, LoggerInstance } from '@oceanprotocol/lib'
export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
const [paymentCollector, setPaymentCollector] = useState<string>()
const { isInPurgatory } = useAsset()
const { web3 } = useWeb3()
useEffect(() => {
async function getInitialPaymentCollector() {
try {
const datatoken = new Datatoken(web3)
setPaymentCollector(
await datatoken.getPaymentCollector(ddo.datatokens[0].address)
)
} catch (error) {
LoggerInstance.error('[MetaFull: getInitialPaymentCollector]', error)
}
}
getInitialPaymentCollector()
}, [ddo, web3])
function DockerImage() {
const containerInfo = ddo?.metadata?.algorithm?.container
@ -23,6 +40,12 @@ export default function MetaFull({ ddo }: { ddo: Asset }): ReactElement {
title="Owner"
content={<Publisher account={ddo?.nft?.owner} />}
/>
{paymentCollector && paymentCollector !== ddo?.nft?.owner && (
<MetaItem
title="Revenue Sent To"
content={<Publisher account={paymentCollector} />}
/>
)}
{ddo?.metadata?.type === 'algorithm' && ddo?.metadata?.algorithm && (
<MetaItem title="Docker Image" content={<DockerImage />} />

View File

@ -3,7 +3,7 @@ import Tooltip from '@shared/atoms/Tooltip'
import { decodeTokenURI } from '@utils/nft'
import { useFormikContext } from 'formik'
import React from 'react'
import { FormPublishData } from 'src/components/Publish/_types'
import { FormPublishData } from '@components/Publish/_types'
import Logo from '@shared/atoms/Logo'
import NftTooltip from './NftTooltip'
import styles from './index.module.css'

View File

@ -1,11 +1,12 @@
import React, { ReactElement, useState } from 'react'
import React, { ReactElement, useState, useEffect } from 'react'
import { Formik } from 'formik'
import {
LoggerInstance,
Metadata,
FixedRateExchange,
Asset,
Service
Service,
Datatoken
} from '@oceanprotocol/lib'
import { validationSchema } from './_validation'
import { getInitialValues } from './_constants'
@ -36,10 +37,28 @@ export default function Edit({
const { accountId, web3 } = useWeb3()
const newAbortController = useAbortController()
const [success, setSuccess] = useState<string>()
const [paymentCollector, setPaymentCollector] = useState<string>()
const [error, setError] = useState<string>()
const isComputeType = asset?.services[0]?.type === 'compute'
const hasFeedback = error || success
useEffect(() => {
async function getInitialPaymentCollector() {
try {
const datatoken = new Datatoken(web3)
setPaymentCollector(
await datatoken.getPaymentCollector(asset?.datatokens[0].address)
)
} catch (error) {
LoggerInstance.error(
'[EditMetadata: getInitialPaymentCollector]',
error
)
}
}
getInitialPaymentCollector()
}, [asset, web3])
async function updateFixedPrice(newPrice: string) {
const config = getOceanConfig(asset.chainId)
@ -81,13 +100,22 @@ export default function Edit({
values.price !== asset.accessDetails.price &&
(await updateFixedPrice(values.price))
if (values.paymentCollector !== paymentCollector) {
const datatoken = new Datatoken(web3)
await datatoken.setPaymentCollector(
asset?.datatokens[0].address,
accountId,
values.paymentCollector
)
}
if (values.files[0]?.url) {
const file = {
nftAddress: asset.nftAddress,
datatokenAddress: asset.services[0].datatokenAddress,
files: [
{
type: 'url',
type: values.files[0].type,
index: 0,
url: values.files[0].url,
method: 'GET'
@ -147,7 +175,8 @@ export default function Edit({
initialValues={getInitialValues(
asset?.metadata,
asset?.services[0]?.timeout,
asset?.accessDetails?.price
asset?.accessDetails?.price,
paymentCollector
)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {

View File

@ -0,0 +1,15 @@
import { render, screen } from '@testing-library/react'
import React from 'react'
import { useFormikContext } from 'formik'
import FormActions from './FormActions'
jest.mock('formik')
describe('@components/Asset/Edit/FormActions.tsx', () => {
it('renders fixed price', () => {
const isValid = true
;(useFormikContext as jest.Mock).mockReturnValue([isValid])
render(<FormActions />)
expect(screen.getByText('Submit')).toBeInTheDocument()
})
})

View File

@ -1,10 +1,11 @@
import React, { ReactElement, useEffect } from 'react'
import { Field, Form, useField, useFormikContext } from 'formik'
import Input, { InputProps } from '@shared/FormInput'
import { Field, Form, useFormikContext } from 'formik'
import Input from '@shared/FormInput'
import FormActions from './FormActions'
import { useAsset } from '@context/Asset'
import { FormPublishData } from 'src/components/Publish/_types'
import { getFileUrlInfo } from '@utils/provider'
import { FormPublishData } from '@components/Publish/_types'
import { getFileInfo } from '@utils/provider'
import { getFieldContent } from '@utils/form'
export function checkIfTimeoutInPredefinedValues(
timeout: string,
@ -21,11 +22,11 @@ export default function FormEditMetadata({
showPrice,
isComputeDataset
}: {
data: InputProps[]
data: FormFieldContent[]
showPrice: boolean
isComputeDataset: boolean
}): ReactElement {
const { oceanConfig, asset } = useAsset()
const { asset } = useAsset()
const { values, setFieldValue } = useFormikContext<FormPublishData>()
// This component is handled by Formik so it's not rendered like a "normal" react component,
@ -46,25 +47,38 @@ export default function FormEditMetadata({
useEffect(() => {
// let's initiate files with empty url (we can't access the asset url) with type hidden (for UI frontend)
setFieldValue('files', [
{
url: '',
type: 'hidden'
}
])
setTimeout(() => {
setFieldValue('files', [
{
url: '',
type: 'hidden'
}
])
}, 500)
const providerUrl = values?.services
? values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
// if we have a sample file, we need to get the files' info before setting defaults links value
asset?.metadata?.links?.[0] &&
getFileUrlInfo(asset.metadata.links[0], providerUrl).then(
getFileInfo(asset.metadata.links[0], providerUrl, 'url').then(
(checkedFile) => {
console.log(checkedFile)
// set valid false if url is using google drive
if (asset.metadata.links[0].includes('drive.google')) {
setFieldValue('links', [
{
url: asset.metadata.links[0],
valid: false
}
])
return
}
// initiate link with values from asset metadata
setFieldValue('links', [
{
url: asset.metadata.links[0],
type: 'url',
...checkedFile[0]
}
])
@ -74,23 +88,53 @@ export default function FormEditMetadata({
return (
<Form>
{data.map(
(field: InputProps) =>
(!showPrice && field.name === 'price') || (
<Field
key={field.name}
options={
field.name === 'timeout' && isComputeDataset === true
? timeoutOptionsArray
: field.options
}
{...field}
component={Input}
prefix={field.name === 'price' && oceanConfig?.oceanTokenSymbol}
/>
)
<Field {...getFieldContent('name', data)} component={Input} name="name" />
<Field
{...getFieldContent('description', data)}
component={Input}
name="description"
/>
{showPrice && (
<Field
{...getFieldContent('price', data)}
component={Input}
name="price"
/>
)}
<Field
{...getFieldContent('files', data)}
component={Input}
name="files"
/>
<Field
{...getFieldContent('links', data)}
component={Input}
name="links"
/>
<Field
{...getFieldContent('timeout', data)}
component={Input}
name="timeout"
/>
<Field
{...getFieldContent('author', data)}
component={Input}
name="author"
/>
<Field {...getFieldContent('tags', data)} component={Input} name="tags" />
<Field
{...getFieldContent('paymentCollector', data)}
component={Input}
name="paymentCollector"
/>
<FormActions />
</Form>
)

View File

@ -5,17 +5,19 @@ import { ComputeEditForm, MetadataEditForm } from './_types'
export function getInitialValues(
metadata: Metadata,
timeout: number,
price: string
price: string,
paymentCollector: string
): Partial<MetadataEditForm> {
return {
name: metadata?.name,
description: metadata?.description,
price,
links: [{ url: '', type: '' }],
files: [{ url: '', type: '' }],
links: [{ url: '', type: 'url' }],
files: [{ url: '', type: 'ipfs' }],
timeout: secondsToString(timeout),
author: metadata?.author,
tags: metadata?.tags
tags: metadata?.tags,
paymentCollector
}
}

View File

@ -3,6 +3,7 @@ export interface MetadataEditForm {
name: string
description: string
timeout: string
paymentCollector: string
price?: string
files: FileInfo[]
links?: FileInfo[]

View File

@ -1,5 +1,7 @@
import { FileInfo } from '@oceanprotocol/lib'
import * as Yup from 'yup'
import web3 from 'web3'
import { testLinks } from '../../../@utils/yup'
export const validationSchema = Yup.object().shape({
name: Yup.string()
@ -10,38 +12,38 @@ export const validationSchema = Yup.object().shape({
files: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string()
.url('Must be a valid URL.')
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
),
valid: Yup.boolean().isTrue()
url: testLinks(true),
valid: Yup.boolean().test((value, context) => {
const { type } = context.parent
// allow user to submit if the value type is hidden
if (type === 'hidden') return true
return value || false
})
})
)
.nullable(),
links: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string()
.url('Must be a valid URL.')
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
),
valid: Yup.boolean().isTrue()
links: Yup.array<FileInfo[]>().of(
Yup.object().shape({
url: testLinks(true),
valid: Yup.boolean().test((value, context) => {
// allow user to submit if the value is null
const { valid, url } = context.parent
// allow user to continue if the url is empty
if (!url) return true
return valid
})
)
.nullable(),
})
),
timeout: Yup.string().required('Required'),
author: Yup.string().nullable(),
tags: Yup.array<string[]>().nullable()
tags: Yup.array<string[]>().nullable(),
paymentCollector: Yup.string().test(
'ValidAddress',
'Must be a valid Ethereum Address.',
(value) => {
return web3.utils.isAddress(value)
}
)
})
export const computeSettingsValidationSchema = Yup.object().shape({

View File

@ -24,17 +24,11 @@ export default function Links() {
</Fragment>
))}
<Link href="/imprint">
<a>Imprint</a>
</Link>
<Link href="/imprint">Imprint</Link>
{' — '}
<Link href="/terms">
<a>Terms</a>
</Link>
<Link href="/terms">Terms</Link>
{' — '}
<Link href={privacyPolicySlug}>
<a>Privacy</a>
</Link>
<Link href={privacyPolicySlug}>Privacy</Link>
{appConfig?.privacyPreferenceCenter === 'true' && (
<>
{' — '}

View File

@ -6,7 +6,7 @@ import useNetworkMetadata, {
} from '@hooks/useNetworkMetadata'
import { LoggerInstance } from '@oceanprotocol/lib'
import styles from './index.module.css'
import { FooterStatsValues_globalStatistics as FooterStatsValuesGlobalStatistics } from 'src/@types/subgraph/FooterStatsValues'
import { FooterStatsValues_globalStatistics as FooterStatsValuesGlobalStatistics } from '../../../../src/@types/subgraph/FooterStatsValues'
import MarketStatsTotal from './Total'
import { queryGlobalStatistics } from './_queries'
import { StatsTotal } from './_types'

View File

@ -24,8 +24,8 @@ function MenuLink({ item }: { item: MenuItem }) {
: styles.link
return (
<Link key={item.name} href={item.link}>
<a className={classes}>{item.name}</a>
<Link key={item.name} href={item.link} className={classes}>
{item.name}
</Link>
)
}
@ -35,11 +35,9 @@ export default function Menu(): ReactElement {
return (
<nav className={styles.menu}>
<Link href="/">
<a className={styles.logo}>
<Logo noWordmark />
<h1 className={styles.title}>{siteContent?.siteTitle}</h1>
</a>
<Link href="/" className={styles.logo}>
<Logo noWordmark />
<h1 className={styles.title}>{siteContent?.siteTitle}</h1>
</Link>
<ul className={styles.navigation}>

View File

@ -17,12 +17,7 @@ async function emptySearch() {
const text = searchParams.get('text')
if (text !== ('' || undefined || null)) {
const url = await addExistingParamsToUrl(location, [
'text',
'owner',
'tags'
])
// router.push(`${url}&text=%20`)
await addExistingParamsToUrl(location, ['text', 'owner', 'tags'])
}
}

View File

@ -5,7 +5,7 @@ import { LoggerInstance } from '@oceanprotocol/lib'
import Price from '@shared/Price'
import Tooltip from '@shared/atoms/Tooltip'
import AssetTitle from '@shared/AssetListTitle'
import { retrieveDDOListByDIDs } from '@utils/aquarius'
import { getAssetsFromDids } from '@utils/aquarius'
import { useCancelToken } from '@hooks/useCancelToken'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3'
@ -59,7 +59,7 @@ export default function Bookmarks(): ReactElement {
setIsLoading(true)
try {
const result = await retrieveDDOListByDIDs(
const result = await getAssetsFromDids(
bookmarks,
chainIds,
newCancelToken()

View File

@ -30,24 +30,25 @@ export default function Account({
}, [account?.id])
return (
<Link href={`/profile/${profile?.name || account.id}`}>
<a className={styles.teaser}>
{place && <span className={styles.place}>{place}</span>}
<Avatar
accountId={account.id}
className={styles.avatar}
src={profile?.avatar}
/>
<div>
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
{profile?.name ? profile?.name : accountTruncate(account.id)}
</Dotdotdot>
<p className={styles.sales}>
<span>{account.totalSales}</span>
{`${account.totalSales === 1 ? ' sale' : ' sales'}`}
</p>
</div>
</a>
<Link
href={`/profile/${profile?.name || account.id}`}
className={styles.teaser}
>
{place && <span className={styles.place}>{place}</span>}
<Avatar
accountId={account.id}
className={styles.avatar}
src={profile?.avatar}
/>
<div>
<Dotdotdot tagName="h4" clamp={2} className={styles.name}>
{profile?.name ? profile?.name : accountTruncate(account.id)}
</Dotdotdot>
<p className={styles.sales}>
<span>{account.totalSales}</span>
{`${account.totalSales === 1 ? ' sale' : ' sales'}`}
</p>
</div>
</Link>
)
}

View File

@ -2,7 +2,7 @@ import React, { ReactElement } from 'react'
import styles from './index.module.css'
import Loader from '../../../@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import Account from 'src/components/Home/TopSales/Account'
import Account from '@components/Home/TopSales/Account'
import { UserSales } from '@utils/aquarius'
function LoaderArea() {

View File

@ -1,6 +1,6 @@
import { useUserPreferences } from '@context/UserPreferences'
import { LoggerInstance } from '@oceanprotocol/lib'
import AccountList from 'src/components/Home/TopSales/AccountList'
import AccountList from '@components/Home/TopSales/AccountList'
import { getTopAssetsPublishers, UserSales } from '@utils/aquarius'
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css'

View File

@ -1,7 +1,7 @@
import { LoggerInstance } from '@oceanprotocol/lib'
import { generateBaseQuery, queryMetadata } from '@utils/aquarius'
import axios, { CancelToken } from 'axios'
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery'
import { SortTermOptions } from '../../../../src/@types/aquarius/SearchQuery'
export async function getTopTags(
chainIds: number[],
@ -32,7 +32,7 @@ export async function getTopTags(
try {
const result = await queryMetadata(query, cancelToken)
const tagsList = result?.aggregations?.topTags?.buckets.map(
(x: { key: any }) => x.key
(x: { key: string }) => x.key
)
return tagsList
} catch (error) {

View File

@ -21,14 +21,13 @@ export default function PrivacyLanguages({
return (
<React.Fragment key={policy.policy}>
{i > 0 && ' — '}
<Link href={slug}>
<a
onClick={() => {
setPrivacyPolicySlug(slug)
}}
>
{policy.language}
</a>
<Link
href={slug}
onClick={() => {
setPrivacyPolicySlug(slug)
}}
>
{policy.language}
</Link>
</React.Fragment>
)

View File

@ -3,7 +3,7 @@ import Time from '@shared/atoms/Time'
import Button from '@shared/atoms/Button'
import Modal from '@shared/atoms/Modal'
import External from '@images/external.svg'
import { retrieveAsset } from '@utils/aquarius'
import { getAsset } from '@utils/aquarius'
import Results from './Results'
import styles from './Details.module.css'
import { useCancelToken } from '@hooks/useCancelToken'
@ -49,7 +49,7 @@ function DetailsAssets({ job }: { job: ComputeJobMetaData }) {
useEffect(() => {
async function getAlgoMetadata() {
const ddo = await retrieveAsset(job.algoDID, newCancelToken())
const ddo = await getAsset(job.algoDID, newCancelToken())
setAlgoDtSymbol(ddo.datatokens[0].symbol)
setAlgoName(ddo?.metadata.name)
}

View File

@ -12,7 +12,7 @@ import FormHelp from '@shared/FormInput/Help'
import content from '../../../../../content/pages/history.json'
import { useWeb3 } from '@context/Web3'
import { useCancelToken } from '@hooks/useCancelToken'
import { retrieveAsset } from '@utils/aquarius'
import { getAsset } from '@utils/aquarius'
export default function Results({
job
@ -21,7 +21,6 @@ export default function Results({
}): ReactElement {
const providerInstance = new Provider()
const { accountId, web3 } = useWeb3()
const [isLoading, setIsLoading] = useState(false)
const isFinished = job.dateFinished !== null
const [datasetProvider, setDatasetProvider] = useState<string>()
@ -29,7 +28,7 @@ export default function Results({
useEffect(() => {
async function getAssetMetadata() {
const ddo = await retrieveAsset(job.inputDID[0], newCancelToken())
const ddo = await getAsset(job.inputDID[0], newCancelToken())
setDatasetProvider(ddo.services[0].serviceEndpoint)
}
getAssetMetadata()
@ -61,7 +60,6 @@ export default function Results({
if (!accountId || !job) return
try {
setIsLoading(true)
const jobResult = await providerInstance.getComputeResultUrl(
datasetProvider,
web3,
@ -72,8 +70,6 @@ export default function Results({
await downloadFileBrowser(jobResult)
} catch (error) {
LoggerInstance.error(error.message)
} finally {
setIsLoading(false)
}
}
@ -97,7 +93,7 @@ export default function Results({
</Button>
</ListItem>
) : (
<ListItem>No results found.</ListItem>
<ListItem key={i}>No results found.</ListItem>
)
)}
</ul>

View File

@ -5,7 +5,6 @@ import Downloads from './Downloads'
import ComputeJobs from './ComputeJobs'
import styles from './index.module.css'
import { useWeb3 } from '@context/Web3'
import { chainIds } from 'app.config'
import { getComputeJobs } from '@utils/compute'
import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken'
@ -17,6 +16,7 @@ interface HistoryTab {
}
const refreshInterval = 10000 // 10 sec.
function getTabs(
accountId: string,
userAccountId: string,

View File

@ -8,7 +8,7 @@ import SuccessConfetti from '@shared/SuccessConfetti'
import { useWeb3 } from '@context/Web3'
import { useRouter } from 'next/router'
import Tooltip from '@shared/atoms/Tooltip'
import AvailableNetworks from 'src/components/Publish/AvailableNetworks'
import AvailableNetworks from '@components/Publish/AvailableNetworks'
import Info from '@images/info.svg'
import Loader from '@shared/atoms/Loader'

View File

@ -1,5 +1,5 @@
import { FormikContextType, useFormikContext } from 'formik'
import React, { ReactElement, useEffect, useState } from 'react'
import React, { ReactElement, useEffect } from 'react'
import { useRouter } from 'next/router'
import { FormPublishData } from '../_types'
import { wizardSteps } from '../_constants'

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css'
import { FormPublishData } from '../_types'
import { useFormikContext } from 'formik'
import AssetContent from 'src/components/Asset/AssetContent'
import AssetContent from '@components/Asset/AssetContent'
import { transformPublishFormToDdo } from '../_utils'
import { ZERO_ADDRESS } from '@oceanprotocol/lib'

View File

@ -14,6 +14,8 @@ export function Steps({
const { values, setFieldValue, touched, setTouched } =
useFormikContext<FormPublishData>()
const isCustomProviderUrl = values?.services?.[0]?.providerUrl.custom
// auto-sync user chainId & account into form data values
useEffect(() => {
if (!chainId || !accountId) return
@ -41,11 +43,7 @@ export function Steps({
// Auto-change default providerUrl on user network change
useEffect(() => {
if (
!values?.user?.chainId ||
values?.services[0]?.providerUrl.custom === true
)
return
if (!values?.user?.chainId || isCustomProviderUrl === true) return
const config = getOceanConfig(values.user.chainId)
if (config) {
@ -57,12 +55,7 @@ export function Steps({
}
setTouched({ ...touched, services: [{ providerUrl: { url: true } }] })
}, [
values?.user?.chainId,
values?.services[0]?.providerUrl.custom,
setFieldValue,
setTouched
])
}, [values?.user?.chainId, isCustomProviderUrl, setFieldValue, setTouched])
const { component } = wizardSteps.filter((stepContent) => {
return stepContent.step === values.user.stepCurrent

View File

@ -5,7 +5,7 @@ import styles from './index.module.css'
import content from '../../../../content/publish/index.json'
import { useWeb3 } from '@context/Web3'
import Info from '@images/info.svg'
import AvailableNetworks from 'src/components/Publish/AvailableNetworks'
import AvailableNetworks from '@components/Publish/AvailableNetworks'
export default function Title({
networkId

View File

@ -1,5 +1,5 @@
import React from 'react'
import { allowFixedPricing } from '../../../app.config.js'
import { allowFixedPricing } from '../../../app.config'
import {
FormPublishData,
MetadataAlgorithmContainer,
@ -72,8 +72,8 @@ export const initialValues: FormPublishData = {
},
services: [
{
files: [{ url: '', type: '' }],
links: [{ url: '', type: '' }],
files: [{ url: '', type: 'ipfs' }],
links: [{ url: '', type: 'url' }],
dataTokenOptions: { name: '', symbol: '' },
timeout: '',
access: 'access',

View File

@ -139,18 +139,24 @@ export async function transformPublishFormToDdo(
}
// this is the default format hardcoded
const file = {
nftAddress,
datatokenAddress,
files: [
{
type: 'url',
type: files[0].type,
index: 0,
url: files[0].url,
[files[0].type === 'ipfs'
? 'hash'
: files[0].type === 'arweave'
? 'transactionId'
: 'url']: files[0].url,
method: 'GET'
}
]
}
const filesEncrypted =
!isPreview &&
files?.length &&

View File

@ -2,6 +2,7 @@ import { MAX_DECIMALS } from '@utils/constants'
import * as Yup from 'yup'
import { getMaxDecimalsValidation } from '@utils/numbers'
import { FileInfo } from '@oceanprotocol/lib'
import { testLinks } from '../../@utils/yup'
// TODO: conditional validation
// e.g. when algo is selected, Docker image is required
@ -32,16 +33,7 @@ const validationService = {
files: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string()
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
)
.url('Must be a valid URL.')
.required('Required'),
url: testLinks().required('Required'),
valid: Yup.boolean().isTrue().required('File must be valid.')
})
@ -51,16 +43,7 @@ const validationService = {
links: Yup.array<FileInfo[]>()
.of(
Yup.object().shape({
url: Yup.string()
.url('Must be a valid URL.')
.test(
'GoogleNotSupported',
'Google Drive is not a supported hosting service. Please use an alternative.',
(value) => {
return !value?.toString().includes('drive.google')
}
),
// TODO: require valid file only when URL is given
url: testLinks(),
valid: Yup.boolean()
// valid: Yup.boolean().isTrue('File must be valid.')
})

View File

@ -79,8 +79,6 @@ export default function SearchPage({
fetchAssets(parsed, chainIds)
}, [parsed, chainIds, newCancelToken, fetchAssets])
console.log(queryResult?.results)
return (
<>
<div className={styles.search}>

View File

@ -1,5 +1,6 @@
import { LoggerInstance } from '@oceanprotocol/lib'
import {
escapeEsReservedCharacters,
generateBaseQuery,
getFilterTerm,
queryMetadata
@ -26,10 +27,6 @@ export function updateQueryStringParameter(
}
}
export function escapeESReservedChars(text: string): string {
return text?.replace(/([!*+\-=<>&|()\\[\]{}^~?:\\/"])/g, '\\$1')
}
export function getSearchQuery(
chainIds: number[],
text?: string,
@ -42,7 +39,7 @@ export function getSearchQuery(
serviceType?: string,
accessType?: string
): SearchQuery {
text = escapeESReservedChars(text)
text = escapeEsReservedCharacters(text)
const emptySearchTerm = text === undefined || text === ''
const filters: FilterTerm[] = []
let searchTerm = text || ''
@ -150,7 +147,7 @@ export async function getResults(
},
chainIds: number[],
cancelToken?: CancelToken
): Promise<any> {
): Promise<PagedAssets> {
const {
text,
owner,

View File

@ -6,7 +6,7 @@ import { UserPreferencesProvider } from '@context/UserPreferences'
import PricesProvider from '@context/Prices'
import UrqlProvider from '@context/UrqlProvider'
import ConsentProvider from '@context/CookieConsent'
import App from 'src/components/App'
import App from '../../src/components/App'
import '@oceanprotocol/typographies/css/ocean-typo.css'
import '../stylesGlobal/styles.css'

View File

@ -1,33 +1,30 @@
{
"compilerOptions": {
"target": "esnext",
"module": "ES2020",
"lib": ["dom", "ES2020"],
"resolveJsonModule": true,
"moduleResolution": "node",
"jsx": "preserve",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".", // This must be specified if "paths" is.
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"sourceMap": true,
"noImplicitAny": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"paths": {
"@components/*": ["./src/components/*"],
"@shared/*": ["./src/components/@shared/*"],
"@hooks/*": ["./src/@hooks/*"],
"@context/*": ["./src/@context/*"],
"@images/*": ["./src/@images/*"],
"@utils/*": ["./src/@utils/*"],
"@content/*": ["./@content/*"]
},
"baseUrl": ".",
"strict": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"skipLibCheck": true,
"allowJs": true,
"esModuleInterop": true,
"incremental": true
}
},
"exclude": ["node_modules", ".next", "*.js"],
"include": ["./src/**/*", "./.jest/**/*", "./next-env.d.ts", "./content/**/*"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", ".next", "*.js"]
}