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

adding ipfs / arweave support (#1765)

* support storage type publish

* adding storageType to edit form

* add IPFS type

* fix testst and rollback ipfs typing

* update oceanjs lib

* fix Ipfs type

* Update package-lock.json

* added graphql and smartcontract options UI (WIP)

* update package.json

* removed graphql and smartcontracts

* make various fixes for edit and publish with IPFS (missing Arweave)

* removed no-case-declarations lines

* moved ipfs utils

* renamed getFileUrlInfo to getFileInfo

* added is-ipfs to jest mock

* Update package-lock.json

* fix things

* npm is fun

* rename url to file in getFileInfo

* refactor publish form (storage type field)

* fix tab value when changing tabs

* refactoring edit form

* more refactor edit form

* fix edit validation

* fix validation when input is empty on edit form

* fix validation when loading asset in edit form

* change URL to file confirmed

* change messages based on service type

* Update form.json

* fix FileInput tests and added ipfs / arweave tests

* removed unnecessary comment

* Update index.tsx

* cleanup logic

* update @oceanprotocol/lib

* Update package-lock.json

* fix test error

* fix test assetsWithAccessDetails

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
EnzoVezzaro 2022-11-25 10:07:55 -04:00 committed by GitHub
parent 071948e0e6
commit f6d11e5e6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1355 additions and 45547 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -29,19 +29,62 @@
}, },
{ {
"name": "files", "name": "files",
"label": "New file", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"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.", "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, "prominentHelp": true,
"type": "files" "type": "files",
"required": true
}, },
{ {
"name": "links", "value": "arweave",
"label": "New sample file", "title": "Arweave",
"placeholder": "e.g. https://file.com/samplefile.json", "label": "Transaction ID",
"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.", "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, "prominentHelp": true,
"type": "files" "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",
"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
}, },
{ {

View File

@ -104,19 +104,61 @@
{ {
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"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. ", "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, "prominentHelp": true,
"type": "files", "type": "files",
"required": true "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", "name": "links",
"label": "Sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "prominentHelp": false,
"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.**", "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, "prominentHelp": true,
"type": "files" "type": "files",
"required": false
}],
"required": false
}, },
{ {
"name": "algorithmPrivacy", "name": "algorithmPrivacy",

45915
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@
"@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.4.0", "@oceanprotocol/lib": "^2.5.2",
"@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",
@ -41,6 +41,7 @@
"filesize": "^10.0.5", "filesize": "^10.0.5",
"formik": "^2.2.9", "formik": "^2.2.9",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"is-ipfs": "^7.0.3",
"is-url-superb": "^6.1.0", "is-url-superb": "^6.1.0",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"match-sorter": "^6.3.1", "match-sorter": "^6.3.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,9 +1,11 @@
import { import {
Arweave,
ComputeAlgorithm, ComputeAlgorithm,
ComputeAsset, ComputeAsset,
ComputeEnvironment, ComputeEnvironment,
downloadFileBrowser, downloadFileBrowser,
FileInfo, FileInfo,
Ipfs,
LoggerInstance, LoggerInstance,
ProviderComputeInitializeResults, ProviderComputeInitializeResults,
ProviderInstance, ProviderInstance,
@ -83,18 +85,45 @@ export async function getFileDidInfo(
} }
} }
export async function getFileUrlInfo( export async function getFileInfo(
url: string, file: string,
providerUrl: string providerUrl: string,
storageType: string
): Promise<FileInfo[]> { ): Promise<FileInfo[]> {
try { try {
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 = { const fileUrl: UrlFile = {
type: 'url', type: 'url',
index: 0, index: 0,
url, url: file,
method: 'get' method: 'get'
} }
const response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
response = await ProviderInstance.getFileInfo(fileUrl, providerUrl)
break
}
}
return response return response
} catch (error) { } catch (error) {
LoggerInstance.error(error.message) LoggerInstance.error(error.message)

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

@ -0,0 +1,44 @@
import { isCID } from '@utils/ipfs'
import isUrl from 'is-url-superb'
import * as Yup from 'yup'
export function testLinks() {
return Yup.string().test((value, context) => {
const { type } = context.parent
let validField
let errorMessage
switch (type) {
case 'url':
validField = isUrl(value?.toString() || '')
if (!validField) {
errorMessage = 'Must be a valid url.'
} else {
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

@ -75,6 +75,7 @@ export default function ContainerInput(props: InputProps): ReactElement {
name={`${field.name}[0].url`} name={`${field.name}[0].url`}
checkUrl={false} checkUrl={false}
isLoading={isLoading} isLoading={isLoading}
storageType={'url'}
handleButtonClick={handleValidation} handleButtonClick={handleValidation}
/> />
)} )}

View File

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

View File

@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react' import React from 'react'
import FilesInput from './index' import FilesInput from './index'
import { useField } from 'formik' import { useField } from 'formik'
import { getFileUrlInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
jest.mock('formik') jest.mock('formik')
jest.mock('@utils/provider') jest.mock('@utils/provider')
@ -48,16 +48,17 @@ const mockForm = {
describe('@shared/FormInput/InputElement/FilesInput', () => { describe('@shared/FormInput/InputElement/FilesInput', () => {
it('renders without crashing', async () => { it('renders without crashing', async () => {
;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers]) ;(useField as jest.Mock).mockReturnValue([mockField, mockMeta, mockHelpers])
;(getFileUrlInfo as jest.Mock).mockReturnValue([ ;(getFileInfo as jest.Mock).mockReturnValue([
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentType: 'text/html', contentType: 'text/html',
contentLength: 100 contentLength: 100
} }
]) ])
render(<FilesInput form={mockForm} {...props} />) render(<FilesInput form={mockForm} field={mockField} {...props} />)
expect(screen.getByText('Validate')).toBeInTheDocument() expect(screen.getByText('Validate')).toBeInTheDocument()
fireEvent.click(screen.getByText('Validate')) fireEvent.click(screen.getByText('Validate'))
@ -67,13 +68,14 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
expect(mockHelpers.setValue).toHaveBeenCalled() expect(mockHelpers.setValue).toHaveBeenCalled()
}) })
it('renders fileinfo when file is valid', () => { it('renders fileinfo when file url is valid', () => {
;(useField as jest.Mock).mockReturnValue([ ;(useField as jest.Mock).mockReturnValue([
{ {
value: [ value: [
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentType: 'text/html', contentType: 'text/html',
contentLength: 100 contentLength: 100
} }
@ -82,10 +84,52 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
expect(screen.getByText('https://hello.com')).toBeInTheDocument() 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', () => { it('renders fileinfo without contentType', () => {
;(useField as jest.Mock).mockReturnValue([ ;(useField as jest.Mock).mockReturnValue([
{ {
@ -93,6 +137,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
{ {
valid: true, valid: true,
url: 'https://hello.com', url: 'https://hello.com',
type: 'url',
contentLength: 100 contentLength: 100
} }
] ]
@ -100,7 +145,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
}) })
it('renders fileinfo placeholder when hideUrl is passed', () => { it('renders fileinfo placeholder when hideUrl is passed', () => {
@ -117,7 +162,7 @@ describe('@shared/FormInput/InputElement/FilesInput', () => {
mockMeta, mockMeta,
mockHelpers mockHelpers
]) ])
render(<FilesInput {...props} />) render(<FilesInput {...props} field={mockField} />)
expect( expect(
screen.getByText('https://oceanprotocol/placeholder') screen.getByText('https://oceanprotocol/placeholder')
).toBeInTheDocument() ).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 { 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 { getFileInfo } from '@utils/provider'
import { LoggerInstance } from '@oceanprotocol/lib' import { LoggerInstance } from '@oceanprotocol/lib'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
@ -12,14 +12,17 @@ export default function FilesInput(props: InputProps): ReactElement {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { asset } = useAsset() 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) { 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 = props.form?.values?.services
? props.form?.values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint
setIsLoading(true) setIsLoading(true)
// TODO: handled on provider // TODO: handled on provider
@ -29,7 +32,7 @@ export default function FilesInput(props: InputProps): ReactElement {
) )
} }
const checkedFile = await getFileUrlInfo(url, providerUrl) const checkedFile = await getFileInfo(url, providerUrl, storageType)
// error if something's not right from response // error if something's not right from response
if (!checkedFile) if (!checkedFile)
@ -39,7 +42,7 @@ export default function FilesInput(props: InputProps): ReactElement {
throw Error('✗ No valid file detected. Check your URL and try again.') throw Error('✗ No valid file detected. Check your URL and try again.')
// if all good, add file to formik state // if all good, add file to formik state
helpers.setValue([{ url, ...checkedFile[0] }]) helpers.setValue([{ url, type: storageType, ...checkedFile[0] }])
} catch (error) { } catch (error) {
props.form.setFieldError(`${field.name}[0].url`, error.message) props.form.setFieldError(`${field.name}[0].url`, error.message)
LoggerInstance.error(error.message) LoggerInstance.error(error.message)
@ -50,7 +53,9 @@ export default function FilesInput(props: InputProps): ReactElement {
function handleClose() { function handleClose() {
helpers.setTouched(false) helpers.setTouched(false)
helpers.setValue(meta.initialValue) helpers.setValue([
{ url: '', type: storageType === 'hidden' ? 'ipfs' : storageType }
])
} }
return ( return (
@ -64,7 +69,9 @@ export default function FilesInput(props: InputProps): ReactElement {
{...props} {...props}
name={`${field.name}[0].url`} name={`${field.name}[0].url`}
isLoading={isLoading} isLoading={isLoading}
checkUrl={true}
handleButtonClick={handleValidation} handleButtonClick={handleValidation}
storageType={storageType}
/> />
)} )}
</> </>

View File

@ -6,6 +6,7 @@ import styles from './index.module.css'
import InputGroup from '@shared/FormInput/InputGroup' 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'
import { isCID } from '@utils/ipfs'
export interface URLInputProps { export interface URLInputProps {
submitText: string submitText: string
@ -13,6 +14,7 @@ export interface URLInputProps {
isLoading: boolean isLoading: boolean
name: string name: string
checkUrl?: boolean checkUrl?: boolean
storageType?: string
} }
export default function URLInput({ export default function URLInput({
@ -21,6 +23,7 @@ export default function URLInput({
isLoading, isLoading,
name, name,
checkUrl, checkUrl,
storageType,
...props ...props
}: URLInputProps): ReactElement { }: URLInputProps): ReactElement {
const [field, meta] = useField(name) const [field, meta] = useField(name)
@ -32,7 +35,8 @@ export default function URLInput({
setIsButtonDisabled( setIsButtonDisabled(
!field?.value || !field?.value ||
field.value === '' || field.value === '' ||
(checkUrl && !isUrl(field.value)) || (checkUrl && storageType === 'url' && !isUrl(field.value)) ||
(checkUrl && storageType === 'ipfs' && !isCID(field.value)) ||
field.value.includes('javascript:') || field.value.includes('javascript:') ||
meta?.error 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 styles from './index.module.css'
import { InputProps } from '..' import { InputProps } from '..'
import FilesInput from './FilesInput' import FilesInput from './FilesInput'
@ -11,6 +11,7 @@ import Nft from './Nft'
import InputRadio from './Radio' import InputRadio from './Radio'
import ContainerInput from '@shared/FormInput/InputElement/ContainerInput' import ContainerInput from '@shared/FormInput/InputElement/ContainerInput'
import TagsAutoComplete from './TagsAutoComplete' import TagsAutoComplete from './TagsAutoComplete'
import TabsFile from '@shared/atoms/TabsFile'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
@ -55,6 +56,7 @@ export default function InputElement({
...props ...props
}: InputProps): ReactElement { }: InputProps): ReactElement {
const styleClasses = cx({ select: true, [size]: size }) const styleClasses = cx({ select: true, [size]: size })
switch (props.type) { switch (props.type) {
case 'select': { case 'select': {
const sortedOptions = const sortedOptions =
@ -80,6 +82,26 @@ export default function InputElement({
</select> </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': case 'textarea':
return <textarea id={props.name} className={styles.textarea} {...props} /> return <textarea id={props.name} className={styles.textarea} {...props} />

View File

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

View File

@ -0,0 +1,87 @@
.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;
}
.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) => {
if (isHidden) return null
return (
<Tab
className={styles.tab}
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

@ -7,7 +7,7 @@ import { compareAsBN } from '@utils/numbers'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import Web3Feedback from '@shared/Web3Feedback' import Web3Feedback from '@shared/Web3Feedback'
import { getFileDidInfo, getFileUrlInfo } from '@utils/provider' import { getFileDidInfo, getFileInfo } from '@utils/provider'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
@ -52,14 +52,20 @@ export default function AssetActions({
formikState?.values?.services[0].providerUrl.url || formikState?.values?.services[0].providerUrl.url ||
asset?.services[0]?.serviceEndpoint asset?.services[0]?.serviceEndpoint
const storageType = formikState?.values?.services
? formikState?.values?.services[0].files[0].type
: null
try { try {
const fileInfoResponse = formikState?.values?.services?.[0].files?.[0] const fileInfoResponse = formikState?.values?.services?.[0].files?.[0]
.url .url
? await getFileUrlInfo( ? await getFileInfo(
formikState?.values?.services?.[0].files?.[0].url, formikState?.values?.services?.[0].files?.[0].url,
providerUrl providerUrl,
storageType
) )
: await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl) : await getFileDidInfo(asset?.id, asset?.services[0]?.id, providerUrl)
fileInfoResponse && setFileMetadata(fileInfoResponse[0]) fileInfoResponse && setFileMetadata(fileInfoResponse[0])
// set the content type in the Dataset Schema // set the content type in the Dataset Schema

View File

@ -115,7 +115,7 @@ export default function Edit({
datatokenAddress: asset.services[0].datatokenAddress, datatokenAddress: asset.services[0].datatokenAddress,
files: [ files: [
{ {
type: 'url', type: values.files[0].type,
index: 0, index: 0,
url: values.files[0].url, url: values.files[0].url,
method: 'GET' method: 'GET'

View File

@ -1,10 +1,11 @@
import React, { ReactElement, useEffect } from 'react' import React, { ReactElement, useEffect } from 'react'
import { Field, Form, useFormikContext } from 'formik' import { Field, Form, useFormikContext } from 'formik'
import Input, { InputProps } from '@shared/FormInput' import Input from '@shared/FormInput'
import FormActions from './FormActions' import FormActions from './FormActions'
import { useAsset } from '@context/Asset' import { useAsset } from '@context/Asset'
import { FormPublishData } from 'src/components/Publish/_types' import { FormPublishData } from 'src/components/Publish/_types'
import { getFileUrlInfo } from '@utils/provider' import { getFileInfo } from '@utils/provider'
import { getFieldContent } from '@utils/form'
export function checkIfTimeoutInPredefinedValues( export function checkIfTimeoutInPredefinedValues(
timeout: string, timeout: string,
@ -21,11 +22,11 @@ export default function FormEditMetadata({
showPrice, showPrice,
isComputeDataset isComputeDataset
}: { }: {
data: InputProps[] data: FormFieldContent[]
showPrice: boolean showPrice: boolean
isComputeDataset: boolean isComputeDataset: boolean
}): ReactElement { }): ReactElement {
const { oceanConfig, asset } = useAsset() const { asset } = useAsset()
const { values, setFieldValue } = useFormikContext<FormPublishData>() const { values, setFieldValue } = useFormikContext<FormPublishData>()
// This component is handled by Formik so it's not rendered like a "normal" react component, // This component is handled by Formik so it's not rendered like a "normal" react component,
@ -56,9 +57,10 @@ export default function FormEditMetadata({
const providerUrl = values?.services const providerUrl = values?.services
? values?.services[0].providerUrl.url ? values?.services[0].providerUrl.url
: asset.services[0].serviceEndpoint : asset.services[0].serviceEndpoint
// if we have a sample file, we need to get the files' info before setting defaults links value // if we have a sample file, we need to get the files' info before setting defaults links value
asset?.metadata?.links?.[0] && asset?.metadata?.links?.[0] &&
getFileUrlInfo(asset.metadata.links[0], providerUrl).then( getFileInfo(asset.metadata.links[0], providerUrl, 'url').then(
(checkedFile) => { (checkedFile) => {
// set valid false if url is using google drive // set valid false if url is using google drive
if (asset.metadata.links[0].includes('drive.google')) { if (asset.metadata.links[0].includes('drive.google')) {
@ -74,6 +76,7 @@ export default function FormEditMetadata({
setFieldValue('links', [ setFieldValue('links', [
{ {
url: asset.metadata.links[0], url: asset.metadata.links[0],
type: 'url',
...checkedFile[0] ...checkedFile[0]
} }
]) ])
@ -83,23 +86,48 @@ export default function FormEditMetadata({
return ( return (
<Form> <Form>
{data.map( <Field {...getFieldContent('name', data)} component={Input} name="name" />
(field: InputProps) =>
(!showPrice && field.name === 'price') || (
<Field <Field
key={field.name} {...getFieldContent('description', data)}
options={
field.name === 'timeout' && isComputeDataset === true
? timeoutOptionsArray
: field.options
}
{...field}
component={Input} component={Input}
prefix={field.name === 'price' && oceanConfig?.oceanTokenSymbol} 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" />
<FormActions /> <FormActions />
</Form> </Form>
) )

View File

@ -12,8 +12,8 @@ export function getInitialValues(
name: metadata?.name, name: metadata?.name,
description: metadata?.description, description: metadata?.description,
price, price,
links: [{ url: '', type: '' }], links: [{ url: '', type: 'url' }],
files: [{ url: '', type: '' }], files: [{ url: '', type: 'ipfs' }],
timeout: secondsToString(timeout), timeout: secondsToString(timeout),
author: metadata?.author, author: metadata?.author,
tags: metadata?.tags, tags: metadata?.tags,

View File

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

View File

@ -5,7 +5,6 @@ import Downloads from './Downloads'
import ComputeJobs from './ComputeJobs' import ComputeJobs from './ComputeJobs'
import styles from './index.module.css' import styles from './index.module.css'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
import { chainIds } from 'app.config'
import { getComputeJobs } from '@utils/compute' import { getComputeJobs } from '@utils/compute'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken' import { useCancelToken } from '@hooks/useCancelToken'

View File

@ -72,8 +72,8 @@ export const initialValues: FormPublishData = {
}, },
services: [ services: [
{ {
files: [{ url: '', type: '' }], files: [{ url: '', type: 'ipfs' }],
links: [{ url: '', type: '' }], links: [{ url: '', type: 'url' }],
dataTokenOptions: { name: '', symbol: '' }, dataTokenOptions: { name: '', symbol: '' },
timeout: '', timeout: '',
access: 'access', access: 'access',

View File

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

View File

@ -2,6 +2,7 @@ import { MAX_DECIMALS } from '@utils/constants'
import * as Yup from 'yup' import * as Yup from 'yup'
import { getMaxDecimalsValidation } from '@utils/numbers' import { getMaxDecimalsValidation } from '@utils/numbers'
import { FileInfo } from '@oceanprotocol/lib' import { FileInfo } from '@oceanprotocol/lib'
import { testLinks } from '../../@utils/yup'
// TODO: conditional validation // TODO: conditional validation
// e.g. when algo is selected, Docker image is required // e.g. when algo is selected, Docker image is required
@ -32,16 +33,7 @@ const validationService = {
files: Yup.array<FileInfo[]>() files: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string() url: testLinks().required('Required'),
.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'),
valid: Yup.boolean().isTrue().required('File must be valid.') valid: Yup.boolean().isTrue().required('File must be valid.')
}) })
@ -51,16 +43,7 @@ const validationService = {
links: Yup.array<FileInfo[]>() links: Yup.array<FileInfo[]>()
.of( .of(
Yup.object().shape({ Yup.object().shape({
url: Yup.string() url: testLinks(),
.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
valid: Yup.boolean() valid: Yup.boolean()
// valid: Yup.boolean().isTrue('File must be valid.') // valid: Yup.boolean().isTrue('File must be valid.')
}) })