mirror of
https://github.com/oceanprotocol/market.git
synced 2024-11-15 01:34:57 +01:00
Merge pull request #29 from oceanprotocol/feature/forms
Simplify publish form with Formik
This commit is contained in:
commit
81950413ad
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,4 +9,5 @@ public
|
|||||||
.cache
|
.cache
|
||||||
storybook-static
|
storybook-static
|
||||||
public/storybook
|
public/storybook
|
||||||
.artifacts
|
.artifacts
|
||||||
|
.vercel
|
@ -1,4 +1,94 @@
|
|||||||
{
|
{
|
||||||
"title": "Publish Data",
|
"title": "Publish Data",
|
||||||
"description": "Highlight the important features of your data set to make it more discoverable and catch the interest of data consumers."
|
"description": "Highlight the important features of your data set to make it more discoverable and catch the interest of data consumers.",
|
||||||
|
"form": {
|
||||||
|
"title": "Publish",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"label": "Title",
|
||||||
|
"placeholder": "e.g. Shapes of Desert Plants",
|
||||||
|
"help": "Enter a concise title.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "files",
|
||||||
|
"label": "Files",
|
||||||
|
"placeholder": "e.g. https://file.com/file.json",
|
||||||
|
"help": "Please provide a URL to your data set file.",
|
||||||
|
"type": "files",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"label": "Description",
|
||||||
|
"help": "Add a thorough description with as much detail as possible.",
|
||||||
|
"type": "textarea",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "price",
|
||||||
|
"label": "Price",
|
||||||
|
"help": "Set your price in Ocean Tokens.",
|
||||||
|
"type": "price",
|
||||||
|
"min": 1,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "access",
|
||||||
|
"label": "Access Type",
|
||||||
|
"type": "select",
|
||||||
|
"options": ["Download", "Compute"],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "author",
|
||||||
|
"label": "Author",
|
||||||
|
"placeholder": "e.g. Jelly McJellyfish",
|
||||||
|
"help": "Give proper attribution for your data set.",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "copyrightHolder",
|
||||||
|
"label": "Copyright Holder",
|
||||||
|
"placeholder": "e.g. Marine Institute of Jellyfish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tags",
|
||||||
|
"label": "Tags",
|
||||||
|
"placeholder": "e.g. logistics, ai",
|
||||||
|
"help": "Separate tags with comma."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "license",
|
||||||
|
"label": "License",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
"Public Domain",
|
||||||
|
"PDDL: Public Domain Dedication and License",
|
||||||
|
"ODC-By: Attribution License",
|
||||||
|
"ODC-ODbL: Open Database License",
|
||||||
|
"CDLA-Sharing: Community Data License Agreement",
|
||||||
|
"CDLA-Permissive: Community Data License Agreement",
|
||||||
|
"CC0: Public Domain Dedication",
|
||||||
|
"CC BY: Attribution 4.0 International",
|
||||||
|
"CC BY-SA: Attribution-ShareAlike 4.0 International",
|
||||||
|
"CC BY-ND: Attribution-NoDerivatives 4.0 International",
|
||||||
|
"CC BY-NC: Attribution-NonCommercial 4.0 International",
|
||||||
|
"CC BY-NC-SA: Attribution-NonCommercial-ShareAlike 4.0 International",
|
||||||
|
"CC BY-NC-ND: Attribution-NonCommercial-NoDerivatives 4.0 International",
|
||||||
|
"No License Specified"
|
||||||
|
],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "termsAndConditions",
|
||||||
|
"label": "Terms & Conditions",
|
||||||
|
"type": "checkbox",
|
||||||
|
"options": ["I agree to these Terms and Conditions"],
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"success": "Asset Created!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1120
package-lock.json
generated
1120
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@ -20,7 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@loadable/component": "^5.13.1",
|
"@loadable/component": "^5.13.1",
|
||||||
"@now/node": "^1.7.1",
|
"@now/node": "^1.7.2",
|
||||||
"@oceanprotocol/art": "^3.0.0",
|
"@oceanprotocol/art": "^3.0.0",
|
||||||
"@oceanprotocol/react": "0.0.11",
|
"@oceanprotocol/react": "0.0.11",
|
||||||
"@oceanprotocol/squid": "^2.2.0",
|
"@oceanprotocol/squid": "^2.2.0",
|
||||||
@ -34,31 +34,31 @@
|
|||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"gatsby": "^2.23.22",
|
"formik": "^2.1.4",
|
||||||
"gatsby-image": "^2.4.12",
|
"gatsby": "^2.24.2",
|
||||||
"gatsby-plugin-manifest": "^2.4.17",
|
"gatsby-image": "^2.4.13",
|
||||||
"gatsby-plugin-react-helmet": "^3.3.9",
|
"gatsby-plugin-manifest": "^2.4.18",
|
||||||
"gatsby-plugin-remove-trailing-slashes": "^2.3.10",
|
"gatsby-plugin-react-helmet": "^3.3.10",
|
||||||
"gatsby-plugin-sharp": "^2.6.18",
|
"gatsby-plugin-remove-trailing-slashes": "^2.3.11",
|
||||||
|
"gatsby-plugin-sharp": "^2.6.19",
|
||||||
"gatsby-plugin-svgr": "^2.0.2",
|
"gatsby-plugin-svgr": "^2.0.2",
|
||||||
"gatsby-plugin-webpack-size": "^1.0.0",
|
"gatsby-plugin-webpack-size": "^1.0.0",
|
||||||
"gatsby-source-filesystem": "^2.3.18",
|
"gatsby-source-filesystem": "^2.3.19",
|
||||||
"gatsby-source-graphql": "^2.6.1",
|
"gatsby-source-graphql": "^2.6.2",
|
||||||
"gatsby-transformer-json": "^2.4.10",
|
"gatsby-transformer-json": "^2.4.11",
|
||||||
"gatsby-transformer-remark": "^2.8.23",
|
"gatsby-transformer-remark": "^2.8.25",
|
||||||
"gatsby-transformer-sharp": "^2.5.10",
|
"gatsby-transformer-sharp": "^2.5.11",
|
||||||
"intersection-observer": "^0.11.0",
|
"intersection-observer": "^0.11.0",
|
||||||
"is-url-superb": "^4.0.0",
|
"is-url-superb": "^4.0.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"query-string": "^6.13.1",
|
"query-string": "^6.13.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-data-table-component": "^6.9.6",
|
"react-data-table-component": "^6.9.6",
|
||||||
"react-datepicker": "^3.0.0",
|
"react-datepicker": "^3.1.3",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"react-dotdotdot": "^1.3.1",
|
"react-dotdotdot": "^1.3.1",
|
||||||
"react-dropzone": "^11.0.1",
|
"react-dropzone": "^11.0.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-jsonschema-form": "^1.8.1",
|
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
"react-paginate": "^6.3.2",
|
"react-paginate": "^6.3.2",
|
||||||
"react-rating": "^2.0.5",
|
"react-rating": "^2.0.5",
|
||||||
@ -68,28 +68,29 @@
|
|||||||
"react-toastify": "^6.0.8",
|
"react-toastify": "^6.0.8",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"slugify": "^1.4.4",
|
"slugify": "^1.4.4",
|
||||||
"web3connect": "^1.0.0-beta.33"
|
"web3connect": "^1.0.0-beta.33",
|
||||||
|
"yup": "^0.29.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.10.3",
|
"@babel/core": "^7.10.3",
|
||||||
"@babel/preset-typescript": "^7.10.1",
|
"@babel/preset-typescript": "^7.10.1",
|
||||||
"@storybook/addon-actions": "^6.0.0-beta.45",
|
"@storybook/addon-actions": "^6.0.0-rc.3",
|
||||||
"@storybook/addon-storyshots": "^6.0.0-beta.45",
|
"@storybook/addon-storyshots": "^6.0.0-rc.3",
|
||||||
"@storybook/react": "^6.0.0-beta.45",
|
"@storybook/react": "^6.0.0-rc.3",
|
||||||
"@svgr/webpack": "^5.4.0",
|
"@svgr/webpack": "^5.4.0",
|
||||||
"@testing-library/jest-dom": "^5.11.0",
|
"@testing-library/jest-dom": "^5.11.0",
|
||||||
"@testing-library/react": "^10.4.4",
|
"@testing-library/react": "^10.4.5",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
"@types/loadable__component": "^5.10.0",
|
"@types/loadable__component": "^5.13.0",
|
||||||
"@types/node": "^14.0.19",
|
"@types/node": "^14.0.22",
|
||||||
"@types/numeral": "^0.0.28",
|
"@types/numeral": "^0.0.28",
|
||||||
"@types/react": "^16.9.41",
|
"@types/react": "^16.9.43",
|
||||||
"@types/react-datepicker": "^3.0.2",
|
"@types/react-datepicker": "^3.0.2",
|
||||||
"@types/react-helmet": "^6.0.0",
|
"@types/react-helmet": "^6.0.0",
|
||||||
"@types/react-jsonschema-form": "^1.7.3",
|
|
||||||
"@types/react-paginate": "^6.2.1",
|
"@types/react-paginate": "^6.2.1",
|
||||||
"@types/react-tabs": "^2.3.2",
|
"@types/react-tabs": "^2.3.2",
|
||||||
"@types/shortid": "0.0.29",
|
"@types/shortid": "0.0.29",
|
||||||
|
"@types/yup": "^0.29.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.6.0",
|
"@typescript-eslint/eslint-plugin": "^3.6.0",
|
||||||
"@typescript-eslint/parser": "^3.6.0",
|
"@typescript-eslint/parser": "^3.6.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
|
18
src/@types/Form.d.ts
vendored
Normal file
18
src/@types/Form.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface FormFieldProps {
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
type?: string
|
||||||
|
options?: string[]
|
||||||
|
required?: boolean
|
||||||
|
help?: string
|
||||||
|
placeholder?: string
|
||||||
|
pattern?: string
|
||||||
|
min?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormContent {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
success: string
|
||||||
|
data: FormFieldProps[]
|
||||||
|
}
|
27
src/@types/MetaData.d.ts
vendored
27
src/@types/MetaData.d.ts
vendored
@ -1,25 +1,36 @@
|
|||||||
import { MetaData, AdditionalInformation } from '@oceanprotocol/squid'
|
import { File, MetaData, AdditionalInformation } from '@oceanprotocol/squid'
|
||||||
import { ServiceMetadata } from '@oceanprotocol/squid/dist/node/ddo/Service'
|
import { ServiceMetadata } from '@oceanprotocol/squid/dist/node/ddo/Service'
|
||||||
|
|
||||||
export interface Sample {
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare type AccessType = 'Download' | 'Compute'
|
export declare type AccessType = 'Download' | 'Compute'
|
||||||
|
|
||||||
export interface AdditionalInformationMarket extends AdditionalInformation {
|
export interface AdditionalInformationMarket extends AdditionalInformation {
|
||||||
description: string
|
description: string
|
||||||
links?: Sample[] // redefine existing key, cause not specific enough in Squid
|
links?: File[] // redefine existing key, cause not specific enough in Squid
|
||||||
termsAndConditions: boolean
|
termsAndConditions: boolean
|
||||||
dateRange?: [string, string]
|
dateRange?: [string, string]
|
||||||
access: AccessType
|
access: AccessType | string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetaDataMarket extends MetaData {
|
export interface MetaDataMarket extends MetaData {
|
||||||
additionalInformation: AdditionalInformationMarket
|
additionalInformation: AdditionalInformationMarket
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MetaDataPublishForm {
|
||||||
|
// ---- required fields ----
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
files: string | File[]
|
||||||
|
author: string
|
||||||
|
license: string
|
||||||
|
price: string
|
||||||
|
access: string
|
||||||
|
termsAndConditions: boolean
|
||||||
|
// ---- optional fields ----
|
||||||
|
copyrightHolder?: string
|
||||||
|
tags?: string
|
||||||
|
links?: string | File[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServiceMetaDataMarket extends ServiceMetadata {
|
export interface ServiceMetaDataMarket extends ServiceMetadata {
|
||||||
attributes: MetaDataMarket
|
attributes: MetaDataMarket
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, ReactElement } from 'react'
|
import React, { ReactNode, ReactElement } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
|
||||||
import Header from './organisms/Header'
|
import Header from './organisms/Header'
|
||||||
import Footer from './organisms/Footer'
|
import Footer from './organisms/Footer'
|
||||||
import PageHeader from './molecules/PageHeader'
|
import PageHeader from './molecules/PageHeader'
|
||||||
@ -24,12 +23,6 @@ export default function Layout({
|
|||||||
}: LayoutProps): ReactElement {
|
}: LayoutProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<Helmet>
|
|
||||||
<link rel="icon" href="/icons/icon-96x96.png" />
|
|
||||||
<link rel="apple-touch-icon" href="icons/icon-256x256.png" />
|
|
||||||
<meta name="theme-color" content="#ca2935" />
|
|
||||||
</Helmet>
|
|
||||||
|
|
||||||
<Seo title={title} description={description} uri={uri} />
|
<Seo title={title} description={description} uri={uri} />
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Center } from '../../../.storybook/helpers'
|
import { Center } from '../../../.storybook/helpers'
|
||||||
import { Alert } from './Alert'
|
import Alert from './Alert'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Atoms/Alert',
|
title: 'Atoms/Alert',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import styles from './Alert.module.css'
|
import styles from './Alert.module.css'
|
||||||
|
|
||||||
export function Alert({
|
export default function Alert({
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
state
|
state
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
.button:disabled {
|
.button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
background: var(--brand-grey-lighter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary,
|
.primary,
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
.label {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Checkbox from './Checkbox'
|
|
||||||
import { Center } from '../../../.storybook/helpers'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Atoms/Checkbox',
|
|
||||||
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Checked = () => (
|
|
||||||
<Checkbox
|
|
||||||
name="someName"
|
|
||||||
checked
|
|
||||||
onChange={() => null}
|
|
||||||
label="Example checkbox"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Unchecked = () => (
|
|
||||||
<Checkbox
|
|
||||||
name="someName"
|
|
||||||
checked={false}
|
|
||||||
onChange={() => null}
|
|
||||||
label="Example checkbox"
|
|
||||||
/>
|
|
||||||
)
|
|
@ -1,31 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import styles from './Checkbox.module.css'
|
|
||||||
|
|
||||||
interface CheckboxProps {
|
|
||||||
name: string
|
|
||||||
checked: boolean
|
|
||||||
onChange?: (evt: React.ChangeEvent) => void
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Checkbox: React.FC<CheckboxProps> = ({
|
|
||||||
name,
|
|
||||||
checked,
|
|
||||||
onChange,
|
|
||||||
label
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<label className={styles.label}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name={name}
|
|
||||||
checked={checked}
|
|
||||||
onChange={onChange}
|
|
||||||
className={styles.checkbox}
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Checkbox
|
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { File as FileMetaData } from '@oceanprotocol/squid'
|
import { File as FileMetaData } from '@oceanprotocol/squid'
|
||||||
import filesize from 'filesize'
|
import filesize from 'filesize'
|
||||||
import cleanupContentType from '../../utils/cleanupContentType'
|
import cleanupContentType from '../../utils/cleanupContentType'
|
||||||
import styles from './File.module.css'
|
import styles from './File.module.css'
|
||||||
|
|
||||||
export default function File({ file }: { file: FileMetaData }) {
|
export default function File({ file }: { file: FileMetaData }): ReactElement {
|
||||||
if (!file) return null
|
if (!file) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
.dateRange {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator {
|
|
||||||
margin: 0 var(--spacer);
|
|
||||||
height: inherit;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox {
|
|
||||||
composes: checkbox from '../../molecules/Form/FieldTemplate.module.css';
|
|
||||||
margin-top: calc(var(--spacer) / 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
composes: label from '../../molecules/Form/FieldTemplate.module.css';
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Center } from '../../../../.storybook/helpers'
|
|
||||||
import DateRangeWidget from './DateRangeWidget'
|
|
||||||
import { PublishFormSchema } from '../../../models/PublishForm'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Atoms/DateRangeWidget',
|
|
||||||
decorators: [(storyFn: () => React.FC) => <Center>{storyFn()}</Center>]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DateRange = () => (
|
|
||||||
<DateRangeWidget
|
|
||||||
schema={PublishFormSchema}
|
|
||||||
id="1"
|
|
||||||
autofocus={false}
|
|
||||||
disabled={false}
|
|
||||||
label="Date Range"
|
|
||||||
formContext={{}}
|
|
||||||
readonly={false}
|
|
||||||
value="[]"
|
|
||||||
onBlur={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
onChange={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
options={{}}
|
|
||||||
required={false}
|
|
||||||
/>
|
|
||||||
)
|
|
@ -1,87 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { WidgetProps } from 'react-jsonschema-form'
|
|
||||||
import loadable from '@loadable/component'
|
|
||||||
import styles from './DateRangeWidget.module.css'
|
|
||||||
import { toStringNoMS } from '../../../utils'
|
|
||||||
|
|
||||||
// lazy load this module, it's huge
|
|
||||||
const LazyDatePicker = loadable(() => import('react-datepicker'))
|
|
||||||
|
|
||||||
export function getWidgetValue(
|
|
||||||
date1: Date,
|
|
||||||
date2: Date,
|
|
||||||
range: boolean
|
|
||||||
): string {
|
|
||||||
let [initial, final] = [toStringNoMS(date1), toStringNoMS(date2)]
|
|
||||||
|
|
||||||
if (!range) {
|
|
||||||
final = initial
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSON.stringify([initial, final])
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DateRangeWidget(props: WidgetProps) {
|
|
||||||
const { onChange } = props
|
|
||||||
const [startDate, setStartDate] = useState<Date>(new Date())
|
|
||||||
const [endDate, setEndDate] = useState<Date>(new Date())
|
|
||||||
const [range, setRange] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If the range checkbox is clicked we update the value of the picker
|
|
||||||
onChange(getWidgetValue(startDate, endDate, range))
|
|
||||||
}, [range])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.dateRange}>
|
|
||||||
{range ? (
|
|
||||||
<>
|
|
||||||
<LazyDatePicker
|
|
||||||
selected={startDate}
|
|
||||||
onChange={(date: Date) => {
|
|
||||||
setStartDate(date)
|
|
||||||
onChange(getWidgetValue(date, endDate, range))
|
|
||||||
}}
|
|
||||||
startDate={startDate}
|
|
||||||
selectsStart
|
|
||||||
endDate={endDate}
|
|
||||||
/>
|
|
||||||
<div className={styles.separator}>–</div>
|
|
||||||
<LazyDatePicker
|
|
||||||
selected={endDate}
|
|
||||||
selectsEnd
|
|
||||||
onChange={(date: Date) => {
|
|
||||||
setEndDate(date)
|
|
||||||
onChange(getWidgetValue(startDate, date, range))
|
|
||||||
}}
|
|
||||||
minDate={startDate}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<LazyDatePicker
|
|
||||||
selected={startDate}
|
|
||||||
onChange={(date: Date) => {
|
|
||||||
setStartDate(date)
|
|
||||||
onChange(getWidgetValue(date, date, range))
|
|
||||||
}}
|
|
||||||
startDate={startDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.checkbox}>
|
|
||||||
<input
|
|
||||||
id="range"
|
|
||||||
type="checkbox"
|
|
||||||
onChange={(ev) => setRange(ev.target.checked)}
|
|
||||||
checked={range}
|
|
||||||
/>
|
|
||||||
<label className={styles.label} htmlFor="range">
|
|
||||||
Date Range
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
.terms {
|
|
||||||
padding: calc(var(--spacer) / 2);
|
|
||||||
border: 1px solid var(--brand-grey-light);
|
|
||||||
background-color: var(--brand-grey-lighter);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: calc(var(--spacer) / 2);
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
|
|
||||||
max-height: 250px;
|
|
||||||
/* smooth overflow scrolling for pre-iOS 13 */
|
|
||||||
overflow: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms h1 {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
margin-bottom: calc(var(--spacer) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms h2 {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
composes: label from '../../molecules/Form/FieldTemplate.module.css';
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.req {
|
|
||||||
composes: req from '../../molecules/Form/FieldTemplate.module.css';
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { WidgetProps } from 'react-jsonschema-form'
|
|
||||||
import styles from './TermsWidget.module.css'
|
|
||||||
|
|
||||||
export default function TermsWidget(props: WidgetProps) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
disabled,
|
|
||||||
readonly,
|
|
||||||
label,
|
|
||||||
autofocus,
|
|
||||||
onBlur,
|
|
||||||
onFocus,
|
|
||||||
onChange,
|
|
||||||
required
|
|
||||||
// DescriptionField
|
|
||||||
} = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* <Markdown text={terms} className={styles.terms} /> */}
|
|
||||||
<label
|
|
||||||
htmlFor={id}
|
|
||||||
className={required ? `${styles.label} ${styles.req}` : styles.label}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id={id}
|
|
||||||
checked={typeof value === 'undefined' ? false : value}
|
|
||||||
disabled={disabled || readonly}
|
|
||||||
autoFocus={autofocus}
|
|
||||||
onChange={(event) => onChange(event.target.checked)}
|
|
||||||
onBlur={onBlur && ((event) => onBlur(id, event.target.checked))}
|
|
||||||
onFocus={onFocus && ((event) => onFocus(id, event.target.checked))}
|
|
||||||
/>
|
|
||||||
<span>{label}</span>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -69,34 +69,10 @@
|
|||||||
|
|
||||||
.radioGroup {
|
.radioGroup {
|
||||||
margin-top: calc(var(--spacer) / 2);
|
margin-top: calc(var(--spacer) / 2);
|
||||||
margin-bottom: -2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 40rem) {
|
|
||||||
.radioGroup {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radioWrap {
|
.radioWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: calc(var(--spacer) / 2);
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 40rem) {
|
|
||||||
.radioWrap {
|
|
||||||
flex: 0 0 49%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio:checked + label {
|
|
||||||
border-color: var(--brand-pink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.radioLabel {
|
.radioLabel {
|
||||||
@ -104,19 +80,8 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: var(--font-weight-bold);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
line-height: 1.2;
|
|
||||||
border: 1px solid var(--brand-grey-lighter);
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
color: var(--brand-grey);
|
color: var(--brand-grey);
|
||||||
text-align: left;
|
padding-left: 0.5rem;
|
||||||
padding-left: 2.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Size modifiers */
|
/* Size modifiers */
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import slugify from '@sindresorhus/slugify'
|
import slugify from '@sindresorhus/slugify'
|
||||||
import styles from './InputElement.module.css'
|
import styles from './InputElement.module.css'
|
||||||
import { InputProps } from '.'
|
import { InputProps } from '.'
|
||||||
|
import FilesInput from '../../molecules/FilesInput'
|
||||||
|
|
||||||
export default function InputElement(props: InputProps) {
|
export default function InputElement(props: InputProps): ReactElement {
|
||||||
const { type, options, rows, name } = props
|
const { type, options, rows, name } = props
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -52,6 +53,8 @@ export default function InputElement(props: InputProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
case 'files':
|
||||||
|
return <FilesInput name={name} {...props} />
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
|
@ -1,31 +1,43 @@
|
|||||||
.inputGroup {
|
.inputGroup input {
|
||||||
width: 100%;
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $break-point--small) {
|
.inputGroup button {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
margin-top: -1px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputGroup button:hover,
|
||||||
|
.inputGroup button:focus,
|
||||||
|
.inputGroup input:focus + button:hover,
|
||||||
|
.inputGroup input:focus + button:focus {
|
||||||
|
background: var(--brand-gradient);
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 30rem) {
|
||||||
|
.inputGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.inputGroup > input {
|
.inputGroup input {
|
||||||
@media screen and (min-width: $break-point--small) {
|
border-bottom-left-radius: var(--border-radius);
|
||||||
width: 75%;
|
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.inputGroup > button {
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: -120%;
|
|
||||||
|
|
||||||
@media screen and (min-width: $break-point--small) {
|
.inputGroup button {
|
||||||
position: relative;
|
border-top-right-radius: var(--border-radius);
|
||||||
bottom: auto;
|
|
||||||
width: 25%;
|
|
||||||
height: 100%;
|
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
box-shadow: none;
|
margin-top: 0;
|
||||||
|
margin-left: -1px;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 20%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
import styles from './InputGroup.module.css'
|
import styles from './InputGroup.module.css'
|
||||||
|
|
||||||
const InputGroup = ({ children }: { children: any }) => (
|
const InputGroup = ({ children }: { children: ReactNode }): ReactElement => (
|
||||||
<div className={styles.inputGroup}>{children}</div>
|
<div className={styles.inputGroup}>{children}</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,3 +1,17 @@
|
|||||||
.field {
|
.field {
|
||||||
margin-bottom: var(--spacer);
|
margin-bottom: var(--spacer);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field .field {
|
||||||
|
margin-bottom: calc(var(--spacer) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
color: var(--brand-alert-red);
|
||||||
|
position: absolute;
|
||||||
|
text-align: right;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import InputElement from './InputElement'
|
|||||||
import Help from './Help'
|
import Help from './Help'
|
||||||
import Label from './Label'
|
import Label from './Label'
|
||||||
import styles from './index.module.css'
|
import styles from './index.module.css'
|
||||||
|
import { ErrorMessage } from 'formik'
|
||||||
|
|
||||||
export interface InputProps {
|
export interface InputProps {
|
||||||
name: string
|
name: string
|
||||||
@ -35,15 +36,8 @@ export interface InputProps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Input(props: InputProps) {
|
export default function Input(props: Partial<InputProps>): ReactElement {
|
||||||
const {
|
const { required, name, label, help, additionalComponent, field } = props
|
||||||
required,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
help,
|
|
||||||
additionalComponent,
|
|
||||||
field
|
|
||||||
} = props as Partial<InputProps>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.field}>
|
<div className={styles.field}>
|
||||||
@ -52,6 +46,12 @@ export default function Input(props: InputProps) {
|
|||||||
</Label>
|
</Label>
|
||||||
<InputElement {...field} {...props} />
|
<InputElement {...field} {...props} />
|
||||||
|
|
||||||
|
{field && (
|
||||||
|
<div className={styles.error}>
|
||||||
|
<ErrorMessage name={field.name} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{help && <Help>{help}</Help>}
|
{help && <Help>{help}</Help>}
|
||||||
{additionalComponent && additionalComponent}
|
{additionalComponent && additionalComponent}
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,3 @@ export const Normal = () => <Loader />
|
|||||||
export const WithMessage = () => (
|
export const WithMessage = () => (
|
||||||
<Loader message="Crunching all the tech for you..." />
|
<Loader message="Crunching all the tech for you..." />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const WithMessageHorizontal = () => (
|
|
||||||
<Loader message="Crunching all the tech for you..." isHorizontal />
|
|
||||||
)
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import Eye from '../../../images/eye.svg'
|
import Eye from '../../../images/eye.svg'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Tooltip from '../Tooltip'
|
import Tooltip from '../Tooltip'
|
||||||
@ -10,7 +10,7 @@ export declare type ActionsCellProps = {
|
|||||||
|
|
||||||
export default function ActionsCell({
|
export default function ActionsCell({
|
||||||
handleOnClickViewJobDetails
|
handleOnClickViewJobDetails
|
||||||
}: ActionsCellProps) {
|
}: ActionsCellProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{handleOnClickViewJobDetails && (
|
{handleOnClickViewJobDetails && (
|
||||||
|
@ -7,4 +7,9 @@ export default {
|
|||||||
title: 'Molecules/Asset Teaser'
|
title: 'Molecules/Asset Teaser'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Default = () => <AssetTeaser ddo={new DDO(ddo)} />
|
export const Default = () => (
|
||||||
|
<AssetTeaser
|
||||||
|
did={ddo.id}
|
||||||
|
metadata={new DDO(ddo).findServiceByType('metadata').attributes as any}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
.info {
|
.info {
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: calc(var(--spacer) / 2);
|
padding: calc(var(--spacer) / 2);
|
||||||
border: 1px solid var(--brand-grey-light);
|
border: 1px solid var(--brand-grey-lighter);
|
||||||
background-color: var(--brand-grey-lighter);
|
background-color: var(--brand-grey-dimmed);
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.url {
|
.url {
|
||||||
@ -34,6 +35,6 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
font-size: var(--font-size-h3);
|
font-size: var(--font-size-h3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-secondary);
|
color: var(--brand-grey);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
27
src/components/molecules/FilesInput/Info.tsx
Normal file
27
src/components/molecules/FilesInput/Info.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import { File } from '@oceanprotocol/squid'
|
||||||
|
import { prettySize } from '../../../utils'
|
||||||
|
import cleanupContentType from '../../../utils/cleanupContentType'
|
||||||
|
import styles from './Info.module.css'
|
||||||
|
|
||||||
|
export default function FileInfo({
|
||||||
|
file,
|
||||||
|
removeItem
|
||||||
|
}: {
|
||||||
|
file: File
|
||||||
|
removeItem(): void
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
<h3 className={styles.url}>{file.url}</h3>
|
||||||
|
<ul>
|
||||||
|
<li>URL confirmed</li>
|
||||||
|
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
|
||||||
|
{file.contentType && <li>{cleanupContentType(file.contentType)}</li>}
|
||||||
|
</ul>
|
||||||
|
<button className={styles.removeButton} onClick={() => removeItem()}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
3
src/components/molecules/FilesInput/Input.module.css
Normal file
3
src/components/molecules/FilesInput/Input.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.input {
|
||||||
|
composes: input from '../../atoms/Input/InputElement.module.css';
|
||||||
|
}
|
38
src/components/molecules/FilesInput/Input.tsx
Normal file
38
src/components/molecules/FilesInput/Input.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import isUrl from 'is-url-superb'
|
||||||
|
import Button from '../../atoms/Button'
|
||||||
|
import { useField } from 'formik'
|
||||||
|
import Loader from '../../atoms/Loader'
|
||||||
|
import InputElement from '../../atoms/Input/InputElement'
|
||||||
|
import { InputProps } from '../../atoms/Input'
|
||||||
|
import styles from './Input.module.css'
|
||||||
|
import InputGroup from '../../atoms/Input/InputGroup'
|
||||||
|
|
||||||
|
export default function FileInput({
|
||||||
|
handleButtonClick,
|
||||||
|
isLoading,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
handleButtonClick(e: React.SyntheticEvent, data: string): void
|
||||||
|
isLoading: boolean
|
||||||
|
}): ReactElement {
|
||||||
|
const [field, meta] = useField(props as InputProps)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputGroup>
|
||||||
|
<input className={styles.input} {...props} type="url" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)}
|
||||||
|
disabled={
|
||||||
|
!field.value ||
|
||||||
|
// weird static page build fix so is-url-superb won't error
|
||||||
|
!isUrl(typeof field.value === 'string' ? field.value : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader /> : 'Add File'}
|
||||||
|
</Button>
|
||||||
|
</InputGroup>
|
||||||
|
)
|
||||||
|
}
|
51
src/components/molecules/FilesInput/index.tsx
Normal file
51
src/components/molecules/FilesInput/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React, { ReactElement, useState } from 'react'
|
||||||
|
import { useField } from 'formik'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import FileInfo from './Info'
|
||||||
|
import FileInput from './Input'
|
||||||
|
import { getFileInfo } from '../../../utils'
|
||||||
|
import { InputProps } from '../../atoms/Input'
|
||||||
|
|
||||||
|
interface Values {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FilesInput(props: InputProps): ReactElement {
|
||||||
|
const [field, meta, helpers] = useField(props)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
|
||||||
|
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const newFileInfo = await getFileInfo(url)
|
||||||
|
newFileInfo && helpers.setValue([newFileInfo])
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Could not fetch file info. Please check url and try again')
|
||||||
|
console.error(error.message)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeItem() {
|
||||||
|
helpers.setValue(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{typeof field.value === 'object' ? (
|
||||||
|
<FileInfo file={field.value[0]} removeItem={removeItem} />
|
||||||
|
) : (
|
||||||
|
<FileInput
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
isLoading={isLoading}
|
||||||
|
handleButtonClick={handleButtonClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,140 +0,0 @@
|
|||||||
.row {
|
|
||||||
margin-bottom: var(--spacer);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input,
|
|
||||||
.row input:not([type='radio']):not([type='checkbox']),
|
|
||||||
.row select,
|
|
||||||
.row textarea {
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
font-family: var(--font-family-base);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
/* font-weight: var(--font-weight-bold); */
|
|
||||||
color: var(--brand-grey-dark);
|
|
||||||
border: 2px solid var(--brand-pink);
|
|
||||||
box-shadow: none;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--brand-white);
|
|
||||||
padding: calc(var(--spacer) / 3);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: 0.2s ease-out;
|
|
||||||
min-height: 43px;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input:focus,
|
|
||||||
.row input:focus:not([type='radio']):not([type='checkbox']),
|
|
||||||
.row select:focus,
|
|
||||||
.row textarea:focus {
|
|
||||||
box-shadow: none;
|
|
||||||
outline: 0;
|
|
||||||
border-color: var(--brand-pink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input::placeholder,
|
|
||||||
.row input::placeholder,
|
|
||||||
.row textarea::placeholder {
|
|
||||||
font-family: var(--font-family-base);
|
|
||||||
font-size: var(--font-size-base);
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-weight: var(--font-weight-base);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input[readonly],
|
|
||||||
.input[disabled],
|
|
||||||
.row input[readonly],
|
|
||||||
.row input[disabled] {
|
|
||||||
background-color: var(--brand-grey-light);
|
|
||||||
cursor: not-allowed;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row textarea {
|
|
||||||
min-height: 5rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row select {
|
|
||||||
padding-right: 3rem;
|
|
||||||
|
|
||||||
/* custom arrow */
|
|
||||||
background-image: linear-gradient(
|
|
||||||
45deg,
|
|
||||||
transparent 50%,
|
|
||||||
var(--brand-pink) 50%
|
|
||||||
),
|
|
||||||
linear-gradient(135deg, var(--brand-pink) 50%, transparent 50%),
|
|
||||||
linear-gradient(to right, var(--brand-pink) 0px, var(--brand-white) 2px);
|
|
||||||
background-position: calc(100% - 18px) calc(1rem + 5px),
|
|
||||||
calc(100% - 13px) calc(1rem + 5px), 100% 0;
|
|
||||||
background-size: 5px 5px, 5px 5px, 2.5rem 4rem;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox label,
|
|
||||||
.radio label,
|
|
||||||
.row input[type='radio'] + span,
|
|
||||||
.row input[type='checkbox'] + span {
|
|
||||||
display: inline-block;
|
|
||||||
font-weight: var(--font-weight-base);
|
|
||||||
margin-bottom: calc(var(--spacer) / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.row :global(.field-radio-group) {
|
|
||||||
border: 2px solid var(--brand-pink);
|
|
||||||
padding: calc(var(--spacer) / 3);
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
.labelHolder {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
margin-bottom: calc(var(--spacer) / 8);
|
|
||||||
color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.req:after {
|
|
||||||
content: '*';
|
|
||||||
padding-left: calc(var(--spacer) / 8);
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.help {
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
color: var(--color-secondary);
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors {
|
|
||||||
padding-top: calc(var(--spacer) / 8);
|
|
||||||
font-size: var(--font-size-mini);
|
|
||||||
color: var(--color-primary);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error input:not([type='radio']):not([type='checkbox']),
|
|
||||||
.error select,
|
|
||||||
.error textarea {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Size Modifiers */
|
|
||||||
.large,
|
|
||||||
.large::placeholder,
|
|
||||||
.large + button {
|
|
||||||
font-size: var(--font-size-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
|
||||||
padding: calc(var(--spacer) / 2);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { FieldTemplateProps } from 'react-jsonschema-form'
|
|
||||||
import styles from './FieldTemplate.module.css'
|
|
||||||
|
|
||||||
const noLabelFields = ['root', 'root_termsAndConditions', 'root_files_0']
|
|
||||||
|
|
||||||
// Ref: https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#field-template
|
|
||||||
export const FieldTemplate = ({
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
rawHelp,
|
|
||||||
required,
|
|
||||||
rawErrors,
|
|
||||||
children
|
|
||||||
}: FieldTemplateProps) => {
|
|
||||||
const noLabel = id !== noLabelFields.filter((f) => id === f)[0]
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
key={id}
|
|
||||||
className={
|
|
||||||
rawErrors !== undefined && rawErrors.length > 0
|
|
||||||
? `${styles.row} ${styles.error}`
|
|
||||||
: `${styles.row}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles.labelHolder}>
|
|
||||||
{noLabel && (
|
|
||||||
<label
|
|
||||||
className={
|
|
||||||
required ? `${styles.label} ${styles.req}` : styles.label
|
|
||||||
}
|
|
||||||
htmlFor={id}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{rawErrors && <span className={styles.errors}>{rawErrors}</span>}
|
|
||||||
{rawHelp && <div className={styles.help}>{rawHelp}</div>}
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { File } from '@oceanprotocol/squid'
|
|
||||||
import { prettySize } from '../../../../utils'
|
|
||||||
import cleanupContentType from '../../../../utils/cleanupContentType'
|
|
||||||
import styles from './Info.module.css'
|
|
||||||
|
|
||||||
const FileInfo = ({ info, removeItem }: { info: File; removeItem(): void }) => (
|
|
||||||
<div className={styles.info}>
|
|
||||||
<h3 className={styles.url}>{info.url}</h3>
|
|
||||||
<ul>
|
|
||||||
<li>URL confirmed</li>
|
|
||||||
{info.contentLength && <li>{prettySize(+info.contentLength)}</li>}
|
|
||||||
{info.contentType && <li>{cleanupContentType(info.contentType)}</li>}
|
|
||||||
</ul>
|
|
||||||
<button className={styles.removeButton} onClick={() => removeItem()}>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default FileInfo
|
|
@ -1,34 +0,0 @@
|
|||||||
import React, { ReactElement, ReactNode } from 'react'
|
|
||||||
import isUrl from 'is-url-superb'
|
|
||||||
import Loader from '../../../atoms/Loader'
|
|
||||||
import Button from '../../../atoms/Button'
|
|
||||||
import styles from './index.module.css'
|
|
||||||
|
|
||||||
const FileInput = ({
|
|
||||||
formData,
|
|
||||||
handleButtonClick,
|
|
||||||
isLoading,
|
|
||||||
children,
|
|
||||||
i
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
i: number
|
|
||||||
formData: string[]
|
|
||||||
handleButtonClick(e: React.SyntheticEvent, data: string): void
|
|
||||||
isLoading: boolean
|
|
||||||
}): ReactElement => (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
{formData[i] && (
|
|
||||||
<Button
|
|
||||||
className={styles.addButton}
|
|
||||||
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, formData[i])}
|
|
||||||
disabled={!isUrl(formData[i])}
|
|
||||||
>
|
|
||||||
{isLoading ? <Loader /> : 'Add File'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default FileInput
|
|
@ -1,16 +0,0 @@
|
|||||||
.arrayField {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrayField > section {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addButton {
|
|
||||||
margin-top: calc(var(--spacer) / 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import React, { useState } from 'react'
|
|
||||||
import { ArrayFieldTemplateProps } from 'react-jsonschema-form'
|
|
||||||
import { File } from '@oceanprotocol/squid'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import useStoredValue from '../../../../hooks/useStoredValue'
|
|
||||||
import { getFileInfo } from '../../../../utils'
|
|
||||||
import FileInfo from './Info'
|
|
||||||
import FileInput from './Input'
|
|
||||||
import styles from './index.module.css'
|
|
||||||
|
|
||||||
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
|
|
||||||
|
|
||||||
const FileField = ({ items, formData }: ArrayFieldTemplateProps) => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
// in order to access fileInfo as an array of objects upon formSubmit we need to keep it in localStorage
|
|
||||||
const [fileInfo, setFileInfo] = useStoredValue<File[]>(
|
|
||||||
FILES_DATA_LOCAL_STORAGE_KEY,
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleButtonClick = async (e: React.SyntheticEvent, url: string) => {
|
|
||||||
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
const newFileInfo = await getFileInfo(url)
|
|
||||||
newFileInfo && setFileInfo([newFileInfo])
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('Could not fetch file info. Please check url and try again')
|
|
||||||
console.error(error.message)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeItem = () => {
|
|
||||||
setFileInfo([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{items.map(({ children, key }, i) => (
|
|
||||||
<div key={key} className={styles.arrayField}>
|
|
||||||
{fileInfo[i] ? (
|
|
||||||
<FileInfo info={fileInfo[i]} removeItem={removeItem} />
|
|
||||||
) : (
|
|
||||||
<FileInput
|
|
||||||
formData={formData}
|
|
||||||
handleButtonClick={handleButtonClick}
|
|
||||||
i={i}
|
|
||||||
isLoading={isLoading}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</FileInput>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileField
|
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { ObjectFieldTemplateProps } from 'react-jsonschema-form'
|
|
||||||
|
|
||||||
// Template to render form
|
|
||||||
// https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#object-field-template
|
|
||||||
const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => (
|
|
||||||
<>
|
|
||||||
<h3>{props.title}</h3>
|
|
||||||
{props.properties.map(
|
|
||||||
(element: { content: React.ReactElement }) => element.content
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
export { ObjectFieldTemplate }
|
|
@ -1,115 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import FormJsonSchema, {
|
|
||||||
UiSchema,
|
|
||||||
IChangeEvent,
|
|
||||||
ISubmitEvent,
|
|
||||||
ErrorSchema,
|
|
||||||
AjvError
|
|
||||||
} from 'react-jsonschema-form'
|
|
||||||
import { JSONSchema6 } from 'json-schema'
|
|
||||||
import Button from '../../atoms/Button'
|
|
||||||
import styles from './index.module.css'
|
|
||||||
|
|
||||||
import { FieldTemplate } from './FieldTemplate'
|
|
||||||
|
|
||||||
import {
|
|
||||||
customWidgets,
|
|
||||||
PublishFormDataInterface
|
|
||||||
} from '../../../models/PublishForm'
|
|
||||||
// Overwrite default input fields
|
|
||||||
/*
|
|
||||||
AltDateTimeWidget
|
|
||||||
AltDateWidget
|
|
||||||
CheckboxWidget
|
|
||||||
ColorWidget
|
|
||||||
DateTimeWidget
|
|
||||||
DateWidget
|
|
||||||
EmailWidget
|
|
||||||
FileWidget
|
|
||||||
HiddenWidget
|
|
||||||
RadioWidget
|
|
||||||
RangeWidget
|
|
||||||
SelectWidget
|
|
||||||
CheckboxesWidget
|
|
||||||
UpDownWidget
|
|
||||||
TextareaWidget
|
|
||||||
PasswordWidget
|
|
||||||
TextWidget
|
|
||||||
URLWidget
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Example of Custom Error
|
|
||||||
// REF: react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-error-messages
|
|
||||||
export const transformErrors = (errors: AjvError[]) => {
|
|
||||||
return errors.map((error: AjvError) => {
|
|
||||||
if (error.property === '.termsAndConditions') {
|
|
||||||
console.log('ERROR')
|
|
||||||
error.message = 'Required Field'
|
|
||||||
}
|
|
||||||
return error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const validate = (formData: PublishFormDataInterface, errors: any) => {
|
|
||||||
if (!formData.termsAndConditions) {
|
|
||||||
errors.termsAndConditions.addError('Required Field')
|
|
||||||
}
|
|
||||||
return errors
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare type FormProps = {
|
|
||||||
buttonDisabled?: boolean
|
|
||||||
children?: React.ReactNode
|
|
||||||
schema: JSONSchema6
|
|
||||||
uiSchema: UiSchema
|
|
||||||
formData: PublishFormDataInterface
|
|
||||||
onChange: (
|
|
||||||
e: IChangeEvent<PublishFormDataInterface>,
|
|
||||||
es?: ErrorSchema
|
|
||||||
) => void
|
|
||||||
onSubmit: (e: ISubmitEvent<PublishFormDataInterface>) => void
|
|
||||||
onError: (e: AjvError) => void
|
|
||||||
showErrorList?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Form({
|
|
||||||
children,
|
|
||||||
schema,
|
|
||||||
uiSchema,
|
|
||||||
formData,
|
|
||||||
onChange,
|
|
||||||
onSubmit,
|
|
||||||
onError,
|
|
||||||
showErrorList,
|
|
||||||
buttonDisabled
|
|
||||||
}: FormProps) {
|
|
||||||
return (
|
|
||||||
<FormJsonSchema
|
|
||||||
className={styles.form}
|
|
||||||
schema={schema}
|
|
||||||
formData={formData}
|
|
||||||
uiSchema={uiSchema}
|
|
||||||
onChange={(event: IChangeEvent<PublishFormDataInterface>) =>
|
|
||||||
onChange(event)
|
|
||||||
}
|
|
||||||
onSubmit={(event: ISubmitEvent<PublishFormDataInterface>) =>
|
|
||||||
onSubmit(event)
|
|
||||||
}
|
|
||||||
FieldTemplate={FieldTemplate}
|
|
||||||
onError={onError}
|
|
||||||
widgets={customWidgets}
|
|
||||||
noHtml5Validate
|
|
||||||
showErrorList={showErrorList}
|
|
||||||
validate={validate} // REF: https://react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-validation
|
|
||||||
// liveValidate
|
|
||||||
transformErrors={transformErrors}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Button disabled={buttonDisabled} style="primary">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</FormJsonSchema>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
.error {
|
|
||||||
background-color: var(--red);
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
background-color: var(--green);
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
background-color: var(--yellow);
|
|
||||||
}
|
|
@ -1,203 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useNavigate } from '@reach/router'
|
|
||||||
import Form from '../../molecules/Form/index'
|
|
||||||
import {
|
|
||||||
PublishFormSchema,
|
|
||||||
PublishFormUiSchema,
|
|
||||||
publishFormData,
|
|
||||||
PublishFormDataInterface
|
|
||||||
} from '../../../models/PublishForm'
|
|
||||||
import useStoredValue from '../../../hooks/useStoredValue'
|
|
||||||
import { MetaDataMarket } from '../../../@types/MetaData'
|
|
||||||
import { File, MetaData } from '@oceanprotocol/squid'
|
|
||||||
import { isBrowser, toStringNoMS } from '../../../utils'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import styles from './PublishForm.module.css'
|
|
||||||
import utils from 'web3-utils'
|
|
||||||
import AssetModel from '../../../models/Asset'
|
|
||||||
import { useWeb3, useOcean } from '@oceanprotocol/react'
|
|
||||||
import {
|
|
||||||
Service,
|
|
||||||
ServiceCompute
|
|
||||||
} from '@oceanprotocol/squid/dist/node/ddo/Service'
|
|
||||||
|
|
||||||
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
|
|
||||||
const PUBLISH_FORM_LOCAL_STORAGE_KEY = 'publishForm'
|
|
||||||
|
|
||||||
export function getFilesData() {
|
|
||||||
let localFileData: File[] = []
|
|
||||||
if (isBrowser) {
|
|
||||||
const storedData = localStorage.getItem(FILES_DATA_LOCAL_STORAGE_KEY)
|
|
||||||
if (storedData) {
|
|
||||||
localFileData = localFileData.concat(JSON.parse(storedData) as File[])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return localFileData
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearFilesData() {
|
|
||||||
if (isBrowser)
|
|
||||||
localStorage.setItem(FILES_DATA_LOCAL_STORAGE_KEY, JSON.stringify([]))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformPublishFormToMetadata(
|
|
||||||
data: PublishFormDataInterface
|
|
||||||
): MetaDataMarket {
|
|
||||||
const currentTime = toStringNoMS(new Date())
|
|
||||||
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
price,
|
|
||||||
author,
|
|
||||||
license,
|
|
||||||
summary,
|
|
||||||
holder,
|
|
||||||
keywords,
|
|
||||||
termsAndConditions,
|
|
||||||
supportName,
|
|
||||||
supportEmail,
|
|
||||||
dateRange,
|
|
||||||
access
|
|
||||||
} = data
|
|
||||||
|
|
||||||
const metadata: MetaDataMarket = {
|
|
||||||
main: {
|
|
||||||
...AssetModel.main,
|
|
||||||
name: title,
|
|
||||||
price: utils.toWei(price.toString()),
|
|
||||||
author,
|
|
||||||
dateCreated: currentTime,
|
|
||||||
datePublished: currentTime,
|
|
||||||
files: getFilesData(),
|
|
||||||
license
|
|
||||||
},
|
|
||||||
// ------- additional information -------
|
|
||||||
additionalInformation: {
|
|
||||||
...AssetModel.additionalInformation,
|
|
||||||
description: summary,
|
|
||||||
copyrightHolder: holder,
|
|
||||||
tags: keywords?.split(','),
|
|
||||||
termsAndConditions,
|
|
||||||
access: access || 'Download'
|
|
||||||
},
|
|
||||||
// ------- curation -------
|
|
||||||
curation: AssetModel.curation
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateRange) {
|
|
||||||
const newDateRange = JSON.parse(dateRange)
|
|
||||||
if (newDateRange.length > 1) {
|
|
||||||
metadata.additionalInformation.dateRange = JSON.parse(dateRange)
|
|
||||||
} else if (newDateRange.length === 1) {
|
|
||||||
// eslint-disable-next-line prefer-destructuring
|
|
||||||
metadata.main.dateCreated = newDateRange[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
const PublishForm: React.FC<any> = () => {
|
|
||||||
const [buttonDisabled, setButtonDisabled] = useState(false)
|
|
||||||
const { web3Connect } = useWeb3()
|
|
||||||
const { ocean, account } = useOcean()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const [data, updateData] = useStoredValue(
|
|
||||||
PUBLISH_FORM_LOCAL_STORAGE_KEY,
|
|
||||||
publishFormData
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setButtonDisabled(!ocean)
|
|
||||||
}, [ocean])
|
|
||||||
|
|
||||||
const handleChange = ({
|
|
||||||
formData
|
|
||||||
}: {
|
|
||||||
formData: PublishFormDataInterface
|
|
||||||
}) => {
|
|
||||||
updateData(formData)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async ({
|
|
||||||
formData
|
|
||||||
}: {
|
|
||||||
formData: PublishFormDataInterface
|
|
||||||
}) => {
|
|
||||||
setButtonDisabled(true)
|
|
||||||
const submittingToast = toast.info('submitting asset', {
|
|
||||||
className: styles.info
|
|
||||||
})
|
|
||||||
|
|
||||||
if (ocean == null) {
|
|
||||||
await web3Connect.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ocean) {
|
|
||||||
const metadata = transformPublishFormToMetadata(formData)
|
|
||||||
|
|
||||||
// if services array stays empty, the default access service
|
|
||||||
// will be created by squid-js
|
|
||||||
let services: Service[] = []
|
|
||||||
|
|
||||||
if (metadata.additionalInformation.access === 'Compute') {
|
|
||||||
const computeService: ServiceCompute = await ocean.compute.createComputeServiceAttributes(
|
|
||||||
account,
|
|
||||||
metadata.main.price,
|
|
||||||
// Note: a hack without consequences.
|
|
||||||
// Will make metadata.main.datePublished (automatically created by Aquarius)
|
|
||||||
// go out of sync with this service.main.datePublished.
|
|
||||||
toStringNoMS(new Date(Date.now()))
|
|
||||||
)
|
|
||||||
services = [computeService]
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const asset = await ocean.assets.create(
|
|
||||||
(metadata as unknown) as MetaData,
|
|
||||||
account,
|
|
||||||
services
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reset the form to initial values
|
|
||||||
|
|
||||||
updateData(publishFormData)
|
|
||||||
clearFilesData()
|
|
||||||
|
|
||||||
// User feedback and redirect
|
|
||||||
toast.success('asset created successfully', {
|
|
||||||
className: styles.success
|
|
||||||
})
|
|
||||||
toast.dismiss(submittingToast)
|
|
||||||
navigate(`/asset/${asset.id}`)
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e)
|
|
||||||
} finally {
|
|
||||||
setButtonDisabled(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the form to initial values
|
|
||||||
|
|
||||||
// User feedback and redirect
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleError = () =>
|
|
||||||
toast.error('Please check form. There are some errors', {
|
|
||||||
className: styles.error
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
schema={PublishFormSchema}
|
|
||||||
uiSchema={PublishFormUiSchema}
|
|
||||||
formData={data}
|
|
||||||
onChange={handleChange}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onError={handleError}
|
|
||||||
showErrorList={false}
|
|
||||||
buttonDisabled={buttonDisabled}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PublishForm
|
|
@ -2,52 +2,10 @@
|
|||||||
margin-bottom: var(--spacer);
|
margin-bottom: var(--spacer);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputGroup > div {
|
.form > div > div {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputGroup label {
|
.form label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputGroup input {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputGroup button {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
margin-top: -1px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputGroup button:hover,
|
|
||||||
.inputGroup button:focus,
|
|
||||||
.inputGroup input:focus + button:hover,
|
|
||||||
.inputGroup input:focus + button:focus {
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
transform: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 30rem) {
|
|
||||||
.inputGroup {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputGroup input {
|
|
||||||
border-bottom-left-radius: var(--border-radius);
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inputGroup button {
|
|
||||||
border-top-right-radius: var(--border-radius);
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: -1px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,7 @@ import styles from './SearchBar.module.css'
|
|||||||
import Loader from '../atoms/Loader'
|
import Loader from '../atoms/Loader'
|
||||||
import Button from '../atoms/Button'
|
import Button from '../atoms/Button'
|
||||||
import Input from '../atoms/Input'
|
import Input from '../atoms/Input'
|
||||||
|
import InputGroup from '../atoms/Input/InputGroup'
|
||||||
|
|
||||||
export default function SearchBar({
|
export default function SearchBar({
|
||||||
placeholder,
|
placeholder,
|
||||||
@ -35,7 +36,7 @@ export default function SearchBar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={styles.form}>
|
<form className={styles.form}>
|
||||||
<div className={styles.inputGroup}>
|
<InputGroup>
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
name="search"
|
name="search"
|
||||||
@ -47,7 +48,7 @@ export default function SearchBar({
|
|||||||
<Button onClick={(e: FormEvent<HTMLButtonElement>) => startSearch(e)}>
|
<Button onClick={(e: FormEvent<HTMLButtonElement>) => startSearch(e)}>
|
||||||
{searchStarted ? <Loader /> : 'Search'}
|
{searchStarted ? <Loader /> : 'Search'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</InputGroup>
|
||||||
|
|
||||||
{filters && <fieldset className={styles.filters}>Type, Price</fieldset>}
|
{filters && <fieldset className={styles.filters}>Type, Price</fieldset>}
|
||||||
</form>
|
</form>
|
||||||
|
@ -15,7 +15,7 @@ import styles from './Compute.module.css'
|
|||||||
import Button from '../../atoms/Button'
|
import Button from '../../atoms/Button'
|
||||||
import Input from '../../atoms/Input'
|
import Input from '../../atoms/Input'
|
||||||
import { MetaDataMarket } from '../../../@types/MetaData'
|
import { MetaDataMarket } from '../../../@types/MetaData'
|
||||||
import { Alert } from '../../atoms/Alert'
|
import Alert from '../../atoms/Alert'
|
||||||
|
|
||||||
export default function Compute({
|
export default function Compute({
|
||||||
did,
|
did,
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PublishForm from '../molecules/PublishForm/PublishForm'
|
|
||||||
import styles from './Publish.module.css'
|
|
||||||
import Web3Feedback from '../molecules/Wallet/Feedback'
|
|
||||||
|
|
||||||
const PublishPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<article className={styles.grid}>
|
|
||||||
<PublishForm />
|
|
||||||
<aside>
|
|
||||||
<div className={styles.sticky}>
|
|
||||||
<Web3Feedback />
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</article>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default PublishPage
|
|
@ -1,4 +1,3 @@
|
|||||||
.form {
|
.form {
|
||||||
composes: box from '../../atoms/Box.module.css';
|
composes: box from '../../atoms/Box.module.css';
|
||||||
margin-bottom: var(--spacer);
|
|
||||||
}
|
}
|
147
src/components/pages/Publish/PublishForm.tsx
Normal file
147
src/components/pages/Publish/PublishForm.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
import { toStringNoMS } from '../../../utils'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import styles from './PublishForm.module.css'
|
||||||
|
import { useOcean } from '@oceanprotocol/react'
|
||||||
|
import {
|
||||||
|
Service,
|
||||||
|
ServiceCompute
|
||||||
|
} from '@oceanprotocol/squid/dist/node/ddo/Service'
|
||||||
|
import { Formik, Form as FormFormik, Field } from 'formik'
|
||||||
|
import Input from '../../atoms/Input'
|
||||||
|
import Button from '../../atoms/Button'
|
||||||
|
import { transformPublishFormToMetadata } from './utils'
|
||||||
|
import { FormContent, FormFieldProps } from '../../../@types/Form'
|
||||||
|
import { MetaDataPublishForm } from '../../../@types/MetaData'
|
||||||
|
import AssetModel from '../../../models/Asset'
|
||||||
|
import { File } from '@oceanprotocol/squid'
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape<MetaDataPublishForm>({
|
||||||
|
// ---- required fields ----
|
||||||
|
name: Yup.string().required('Required'),
|
||||||
|
author: Yup.string().required('Required'),
|
||||||
|
price: Yup.string().required('Required'),
|
||||||
|
files: Yup.array<File>().required('Required').nullable(),
|
||||||
|
description: Yup.string().required('Required'),
|
||||||
|
license: Yup.string().required('Required'),
|
||||||
|
access: Yup.string().min(4).required('Required'),
|
||||||
|
termsAndConditions: Yup.boolean().required('Required'),
|
||||||
|
|
||||||
|
// ---- optional fields ----
|
||||||
|
copyrightHolder: Yup.string(),
|
||||||
|
tags: Yup.string(),
|
||||||
|
links: Yup.object<File[]>()
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialValues: MetaDataPublishForm = {
|
||||||
|
name: undefined,
|
||||||
|
author: undefined,
|
||||||
|
price: undefined,
|
||||||
|
files: undefined,
|
||||||
|
description: undefined,
|
||||||
|
license: undefined,
|
||||||
|
access: undefined,
|
||||||
|
termsAndConditions: undefined,
|
||||||
|
copyrightHolder: undefined,
|
||||||
|
tags: undefined,
|
||||||
|
links: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PublishForm({
|
||||||
|
content
|
||||||
|
}: {
|
||||||
|
content: FormContent
|
||||||
|
}): ReactElement {
|
||||||
|
const { ocean, account } = useOcean()
|
||||||
|
|
||||||
|
async function handleSubmit(values: MetaDataPublishForm) {
|
||||||
|
const submittingToast = toast.info('submitting asset')
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
Collected form values:
|
||||||
|
----------------------
|
||||||
|
${values}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const metadata = transformPublishFormToMetadata(values)
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
Transformed metadata values:
|
||||||
|
----------------------
|
||||||
|
${metadata}
|
||||||
|
`)
|
||||||
|
|
||||||
|
// if services array stays empty, the default access service
|
||||||
|
// will be created by squid-js
|
||||||
|
// let services: Service[] = []
|
||||||
|
|
||||||
|
// if (metadata.additionalInformation.access === 'Compute') {
|
||||||
|
// const computeService: ServiceCompute = await ocean.compute.createComputeServiceAttributes(
|
||||||
|
// account,
|
||||||
|
// metadata.main.price,
|
||||||
|
// // Note: a hack without consequences.
|
||||||
|
// // Will make metadata.main.datePublished (automatically created by Aquarius)
|
||||||
|
// // go out of sync with this service.main.datePublished.
|
||||||
|
// toStringNoMS(new Date(Date.now()))
|
||||||
|
// )
|
||||||
|
// services = [computeService]
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const asset = await ocean.assets.create(
|
||||||
|
// (metadata as unknown) as MetaData,
|
||||||
|
// account,
|
||||||
|
// services
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // TODO: Reset the form to initial values
|
||||||
|
|
||||||
|
// // User feedback and redirect
|
||||||
|
// toast.success('asset created successfully', {
|
||||||
|
// className: styles.success
|
||||||
|
// })
|
||||||
|
// toast.dismiss(submittingToast)
|
||||||
|
// // navigate(`/asset/${asset.id}`)
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e.message)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
initialStatus="empty"
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={async (values, { setSubmitting }) => {
|
||||||
|
await handleSubmit(values)
|
||||||
|
setSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, isValid, status, setStatus }) => (
|
||||||
|
<FormFormik
|
||||||
|
className={styles.form}
|
||||||
|
onChange={() => status === 'empty' && setStatus(null)}
|
||||||
|
>
|
||||||
|
{content.data.map((field: FormFieldProps) => (
|
||||||
|
<Field key={field.name} {...field} component={Input} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
style="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
!ocean ||
|
||||||
|
!account ||
|
||||||
|
isSubmitting ||
|
||||||
|
!isValid ||
|
||||||
|
status === 'empty'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</FormFormik>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)
|
||||||
|
}
|
22
src/components/pages/Publish/index.tsx
Normal file
22
src/components/pages/Publish/index.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
import PublishForm from './PublishForm'
|
||||||
|
import styles from './index.module.css'
|
||||||
|
import Web3Feedback from '../../molecules/Wallet/Feedback'
|
||||||
|
import { FormContent } from '../../../@types/Form'
|
||||||
|
|
||||||
|
export default function PublishPage({
|
||||||
|
content
|
||||||
|
}: {
|
||||||
|
content: { form: FormContent }
|
||||||
|
}): ReactElement {
|
||||||
|
return (
|
||||||
|
<article className={styles.grid}>
|
||||||
|
<PublishForm content={content.form} />
|
||||||
|
<aside>
|
||||||
|
<div className={styles.sticky}>
|
||||||
|
<Web3Feedback />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
51
src/components/pages/Publish/utils.ts
Normal file
51
src/components/pages/Publish/utils.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { MetaDataMarket, MetaDataPublishForm } from '../../../@types/MetaData'
|
||||||
|
import { toStringNoMS } from '../../../utils'
|
||||||
|
import AssetModel from '../../../models/Asset'
|
||||||
|
import web3Utils from 'web3-utils'
|
||||||
|
|
||||||
|
export function transformPublishFormToMetadata(
|
||||||
|
data: MetaDataPublishForm
|
||||||
|
): MetaDataMarket {
|
||||||
|
const currentTime = toStringNoMS(new Date())
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
author,
|
||||||
|
license,
|
||||||
|
description,
|
||||||
|
copyrightHolder,
|
||||||
|
tags,
|
||||||
|
links,
|
||||||
|
termsAndConditions,
|
||||||
|
files,
|
||||||
|
access
|
||||||
|
} = data
|
||||||
|
|
||||||
|
const metadata: MetaDataMarket = {
|
||||||
|
main: {
|
||||||
|
...AssetModel.main,
|
||||||
|
name,
|
||||||
|
price: `${web3Utils.toWei(price.toString())}`,
|
||||||
|
author,
|
||||||
|
dateCreated: currentTime,
|
||||||
|
datePublished: currentTime,
|
||||||
|
files: typeof files !== 'string' && files,
|
||||||
|
license
|
||||||
|
},
|
||||||
|
additionalInformation: {
|
||||||
|
...AssetModel.additionalInformation,
|
||||||
|
description,
|
||||||
|
copyrightHolder,
|
||||||
|
tags: tags?.split(','),
|
||||||
|
// links: {
|
||||||
|
// url: links
|
||||||
|
// },
|
||||||
|
termsAndConditions,
|
||||||
|
access: access || 'Download'
|
||||||
|
},
|
||||||
|
curation: AssetModel.curation
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
@ -1,12 +1,19 @@
|
|||||||
import React, { ReactElement, ReactNode } from 'react'
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
|
||||||
import '@oceanprotocol/typographies/css/ocean-typo.css'
|
import '@oceanprotocol/typographies/css/ocean-typo.css'
|
||||||
import '../global/styles.css'
|
import '../global/styles.css'
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
|
|
||||||
export default function Styles({
|
export default function Styles({
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
return <>{children}</>
|
return (
|
||||||
|
<>
|
||||||
|
{children}
|
||||||
|
<ToastContainer position="bottom-right" newestOnTop />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
30
src/global/_toast.css
Normal file
30
src/global/_toast.css
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
div.Toastify__toast {
|
||||||
|
font-family: var(--font-family-base);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 6px 15px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
padding: calc(var(--spacer) / 2) var(--spacer);
|
||||||
|
background: var(--brand-white);
|
||||||
|
color: var(--brand-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.Toastify__toast--error {
|
||||||
|
background: var(--brand-alert-red);
|
||||||
|
color: var(--brand-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.Toastify__toast--success {
|
||||||
|
background: var(--brand-alert-green);
|
||||||
|
color: var(--brand-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.Toastify__toast--info {
|
||||||
|
background: var(--brand-white);
|
||||||
|
color: var(--brand-grey);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.Toastify__toast--warning {
|
||||||
|
background: var(--brand-alert-yellow);
|
||||||
|
color: var(--brand-white);
|
||||||
|
}
|
@ -133,3 +133,4 @@ fieldset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import '_code.css';
|
@import '_code.css';
|
||||||
|
@import '_toast.css';
|
||||||
|
@ -15,12 +15,11 @@ const AssetModel: MetaDataMarket = {
|
|||||||
additionalInformation: {
|
additionalInformation: {
|
||||||
description: '',
|
description: '',
|
||||||
copyrightHolder: '',
|
copyrightHolder: '',
|
||||||
tags: undefined,
|
tags: [],
|
||||||
// links: [],
|
links: [],
|
||||||
|
|
||||||
// custom items
|
// custom items
|
||||||
termsAndConditions: false,
|
termsAndConditions: false,
|
||||||
dateRange: undefined,
|
|
||||||
access: 'Download'
|
access: 'Download'
|
||||||
},
|
},
|
||||||
curation: {
|
curation: {
|
||||||
|
@ -1,188 +0,0 @@
|
|||||||
import { UiSchema } from 'react-jsonschema-form'
|
|
||||||
import { JSONSchema6 } from 'json-schema'
|
|
||||||
import TermsWidget from '../components/atoms/FormWidgets/TermsWidget'
|
|
||||||
import DateRangeWidget from '../components/atoms/FormWidgets/DateRangeWidget'
|
|
||||||
import { ObjectFieldTemplate } from '../components/molecules/Form/ObjectFieldTemplate'
|
|
||||||
import { AccessType } from '../@types/MetaData'
|
|
||||||
import FileField from '../components/molecules/Form/FileField'
|
|
||||||
|
|
||||||
export const customWidgets = {
|
|
||||||
TermsWidget,
|
|
||||||
DateRangeWidget
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PublishFormSchema: JSONSchema6 = {
|
|
||||||
type: 'object',
|
|
||||||
required: [
|
|
||||||
'title',
|
|
||||||
'author',
|
|
||||||
'license',
|
|
||||||
'price',
|
|
||||||
'files',
|
|
||||||
'summary',
|
|
||||||
'termsAndConditions',
|
|
||||||
'access'
|
|
||||||
],
|
|
||||||
definitions: {
|
|
||||||
files: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
name: {
|
|
||||||
type: 'string'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
title: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Offer Title'
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Summary',
|
|
||||||
minLength: 24
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
type: 'array',
|
|
||||||
title: 'Data File URL',
|
|
||||||
items: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'File URL',
|
|
||||||
format: 'uri'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
price: {
|
|
||||||
title: 'Price',
|
|
||||||
type: 'number',
|
|
||||||
minimum: 0
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Author'
|
|
||||||
},
|
|
||||||
access: {
|
|
||||||
title: 'Access type',
|
|
||||||
type: 'string',
|
|
||||||
enum: ['Download', 'Compute']
|
|
||||||
},
|
|
||||||
license: {
|
|
||||||
title: 'License',
|
|
||||||
type: 'string',
|
|
||||||
enum: [
|
|
||||||
'Public Domain',
|
|
||||||
'PDDL: Public Domain Dedication and License',
|
|
||||||
'ODC-By: Attribution License',
|
|
||||||
'ODC-ODbL: Open Database License',
|
|
||||||
'CDLA-Sharing: Community Data License Agreement',
|
|
||||||
'CDLA-Permissive: Community Data License Agreement',
|
|
||||||
'CC0: Public Domain Dedication',
|
|
||||||
'CC BY: Attribution 4.0 International',
|
|
||||||
'CC BY-SA: Attribution-ShareAlike 4.0 International',
|
|
||||||
'CC BY-ND: Attribution-NoDerivatives 4.0 International',
|
|
||||||
'CC BY-NC: Attribution-NonCommercial 4.0 International',
|
|
||||||
'CC BY-NC-SA: Attribution-NonCommercial-ShareAlike 4.0 International',
|
|
||||||
'CC BY-NC-ND: Attribution-NonCommercial-NoDerivatives 4.0 International',
|
|
||||||
'No License Specified'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
dateRange: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Creation Date'
|
|
||||||
},
|
|
||||||
holder: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Copyright Holder'
|
|
||||||
},
|
|
||||||
keywords: {
|
|
||||||
type: 'string',
|
|
||||||
title: 'Keywords'
|
|
||||||
},
|
|
||||||
termsAndConditions: {
|
|
||||||
type: 'boolean',
|
|
||||||
title: 'I agree to these Terms and Conditions'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Widgets Ref https://react-jsonschema-form.readthedocs.io/en/latest/form-customization/#alternative-widgets
|
|
||||||
export const PublishFormUiSchema: UiSchema = {
|
|
||||||
'ui:ObjectFieldTemplate': ObjectFieldTemplate,
|
|
||||||
category: {
|
|
||||||
'ui:widget': 'radio'
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
'ui:placeholder': 'e.g. Shapes of Desert Plants',
|
|
||||||
'ui:help': 'Enter a concise title.'
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
'ui:placeholder': 'Max of 1000 characters',
|
|
||||||
'ui:widget': 'textarea',
|
|
||||||
'ui:help': 'Add a thorough description with as much detail as possible.'
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
'ui:ArrayFieldTemplate': FileField,
|
|
||||||
items: {
|
|
||||||
'ui:placeholder': 'e.g. https://file.com/file.json',
|
|
||||||
'ui:widget': 'uri',
|
|
||||||
classNames: 'input-file'
|
|
||||||
},
|
|
||||||
'ui:help': 'Please provide a URL to your data set file.'
|
|
||||||
},
|
|
||||||
price: {
|
|
||||||
'ui:help': 'Set your price in Ocean Tokens.'
|
|
||||||
},
|
|
||||||
access: {
|
|
||||||
'ui:widget': 'select',
|
|
||||||
'ui:help': 'Access Type'
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
'ui:placeholder': 'e.g. Jelly McJellyfish',
|
|
||||||
'ui:help': 'Give proper attribution for your data set.'
|
|
||||||
},
|
|
||||||
license: {
|
|
||||||
'ui:widget': 'select'
|
|
||||||
},
|
|
||||||
dateRange: {
|
|
||||||
'ui:widget': 'DateRangeWidget',
|
|
||||||
'ui:help':
|
|
||||||
'Select the date the asset was created, or was updated for the last time.'
|
|
||||||
},
|
|
||||||
holder: {
|
|
||||||
'ui:placeholder': 'e.g. Marine Institute of Jellyfish'
|
|
||||||
},
|
|
||||||
keywords: {
|
|
||||||
'ui:placeholder': 'shipment, logistics'
|
|
||||||
},
|
|
||||||
termsAndConditions: {
|
|
||||||
'ui:widget': 'TermsWidget'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PublishFormDataInterface {
|
|
||||||
// ---- required fields ----
|
|
||||||
summary: string
|
|
||||||
termsAndConditions: boolean
|
|
||||||
author: string
|
|
||||||
license: string
|
|
||||||
files: string[]
|
|
||||||
price: number
|
|
||||||
title: string
|
|
||||||
access?: AccessType
|
|
||||||
// ---- optional fields ----
|
|
||||||
dateRange?: string
|
|
||||||
holder?: string
|
|
||||||
keywords?: string
|
|
||||||
}
|
|
||||||
// Ref: https://github.com/oceanprotocol/OEPs/blob/master/8/v0.4/README.md#main-attributes
|
|
||||||
export const publishFormData: PublishFormDataInterface = {
|
|
||||||
author: '',
|
|
||||||
price: 0,
|
|
||||||
title: '',
|
|
||||||
files: [''], // should be empty string initially to display the expanded field
|
|
||||||
summary: '',
|
|
||||||
license: '',
|
|
||||||
termsAndConditions: false,
|
|
||||||
dateRange: undefined,
|
|
||||||
holder: undefined,
|
|
||||||
keywords: undefined
|
|
||||||
}
|
|
@ -6,7 +6,7 @@ import { PageProps } from 'gatsby'
|
|||||||
import { MetaDataMarket, ServiceMetaDataMarket } from '../../@types/MetaData'
|
import { MetaDataMarket, ServiceMetaDataMarket } from '../../@types/MetaData'
|
||||||
import { Aquarius, Logger } from '@oceanprotocol/squid'
|
import { Aquarius, Logger } from '@oceanprotocol/squid'
|
||||||
import { oceanConfig } from '../../../app.config'
|
import { oceanConfig } from '../../../app.config'
|
||||||
import { Alert } from '../../components/atoms/Alert'
|
import Alert from '../../components/atoms/Alert'
|
||||||
|
|
||||||
export default function AssetRoute(props: PageProps): ReactElement {
|
export default function AssetRoute(props: PageProps): ReactElement {
|
||||||
const [metadata, setMetadata] = useState<MetaDataMarket>()
|
const [metadata, setMetadata] = useState<MetaDataMarket>()
|
||||||
|
@ -9,7 +9,7 @@ export default function PageGatsbyPublish(props: PageProps): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title={title} description={description} uri={props.uri}>
|
<Layout title={title} description={description} uri={props.uri}>
|
||||||
<PagePublish />
|
<PagePublish content={content} />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -22,6 +22,20 @@ export const contentQuery = graphql`
|
|||||||
childPagesJson {
|
childPagesJson {
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
|
form {
|
||||||
|
title
|
||||||
|
data {
|
||||||
|
name
|
||||||
|
placeholder
|
||||||
|
label
|
||||||
|
help
|
||||||
|
type
|
||||||
|
required
|
||||||
|
options
|
||||||
|
min
|
||||||
|
}
|
||||||
|
success
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export function updateQueryStringParameter(
|
|||||||
uri: string,
|
uri: string,
|
||||||
key: string,
|
key: string,
|
||||||
newValue: string
|
newValue: string
|
||||||
) {
|
): string {
|
||||||
const regex = new RegExp('([?&])' + key + '=.*?(&|$)', 'i')
|
const regex = new RegExp('([?&])' + key + '=.*?(&|$)', 'i')
|
||||||
const separator = uri.indexOf('?') !== -1 ? '&' : '?'
|
const separator = uri.indexOf('?') !== -1 ? '&' : '?'
|
||||||
|
|
||||||
@ -19,7 +19,11 @@ export function updateQueryStringParameter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prettySize(bytes: number, separator = ' ', postFix = '') {
|
export function prettySize(
|
||||||
|
bytes: number,
|
||||||
|
separator = ' ',
|
||||||
|
postFix = ''
|
||||||
|
): string {
|
||||||
if (bytes) {
|
if (bytes) {
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
const i = Math.min(
|
const i = Math.min(
|
||||||
@ -36,7 +40,7 @@ export function prettySize(bytes: number, separator = ' ', postFix = '') {
|
|||||||
// Boolean value that will be true if we are inside a browser, false otherwise
|
// Boolean value that will be true if we are inside a browser, false otherwise
|
||||||
export const isBrowser = typeof window !== 'undefined'
|
export const isBrowser = typeof window !== 'undefined'
|
||||||
|
|
||||||
export function formatNumber(number: number, format?: string) {
|
export function formatNumber(number: number, format?: string): string {
|
||||||
numeral.zeroFormat('0')
|
numeral.zeroFormat('0')
|
||||||
const defaultFormat = '0,0.000'
|
const defaultFormat = '0,0.000'
|
||||||
|
|
||||||
@ -54,13 +58,9 @@ export async function getFileInfo(url: string): Promise<File> {
|
|||||||
data: { url }
|
data: { url }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.status > 299) {
|
if (response.status > 299 || !response.data.result) {
|
||||||
throw new Error(response.statusText)
|
toast.error('Could not connect to File API')
|
||||||
}
|
return
|
||||||
|
|
||||||
if (!response.data.result) {
|
|
||||||
toast.error(response.data.message)
|
|
||||||
return { contentLength: undefined, contentType: '', url }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { contentLength, contentType } = response.data.result
|
const { contentLength, contentType } = response.data.result
|
||||||
@ -72,7 +72,7 @@ export async function getFileInfo(url: string): Promise<File> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDid(did: string | undefined) {
|
export function isDid(did: string | undefined): boolean {
|
||||||
const didMatch = (did as string).match(/^did:op:([a-f0-9]{64})$/i)
|
const didMatch = (did as string).match(/^did:op:([a-f0-9]{64})$/i)
|
||||||
return !!didMatch
|
return !!didMatch
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@ export function setProperty<T extends Record<string, unknown>>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatBytes(a: number, b: number) {
|
export function formatBytes(a: number, b: number): string {
|
||||||
if (a === 0) return '0 Bytes'
|
if (a === 0) return '0 Bytes'
|
||||||
const c = 1024
|
const c = 1024
|
||||||
const d = b || 2
|
const d = b || 2
|
||||||
|
@ -8,7 +8,7 @@ describe('AssetModel', () => {
|
|||||||
name: 'Hello'
|
name: 'Hello'
|
||||||
}),
|
}),
|
||||||
additionalInformation: Object.assign(AssetModel.additionalInformation, {
|
additionalInformation: Object.assign(AssetModel.additionalInformation, {
|
||||||
supportName: 'Jelly McJellyfish'
|
description: 'Jelly McJellyfish'
|
||||||
}),
|
}),
|
||||||
curation: Object.assign(AssetModel.curation, {
|
curation: Object.assign(AssetModel.curation, {
|
||||||
numVotes: 100,
|
numVotes: 100,
|
||||||
@ -18,6 +18,6 @@ describe('AssetModel', () => {
|
|||||||
|
|
||||||
expect(newMeta).toMatchObject(AssetModel)
|
expect(newMeta).toMatchObject(AssetModel)
|
||||||
expect(newMeta.main.name).toBe('Hello')
|
expect(newMeta.main.name).toBe('Hello')
|
||||||
expect(newMeta.additionalInformation.supportName).toBe('Jelly McJellyfish')
|
expect(newMeta.additionalInformation.description).toBe('Jelly McJellyfish')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -72,7 +72,6 @@ const ddo: Partial<DDO> = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
termsAndConditions: true,
|
termsAndConditions: true,
|
||||||
dateRange: ['2018-09-20T08:38:58', '2019-12-11T05:19:42'],
|
|
||||||
access: 'Download'
|
access: 'Download'
|
||||||
},
|
},
|
||||||
curation: {
|
curation: {
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { PublishFormDataInterface } from '../../../src/models/PublishForm'
|
import { MetaDataPublishForm } from '../../../src/@types/MetaData'
|
||||||
|
|
||||||
const testFormData: PublishFormDataInterface = {
|
const testFormData: MetaDataPublishForm = {
|
||||||
author: '',
|
author: '',
|
||||||
files: [],
|
files: [],
|
||||||
license: '',
|
license: '',
|
||||||
price: 0,
|
price: '0',
|
||||||
title: '',
|
name: '',
|
||||||
summary: 'summary',
|
description: 'description',
|
||||||
termsAndConditions: true
|
termsAndConditions: true,
|
||||||
|
access: 'Download'
|
||||||
|
// links: []
|
||||||
}
|
}
|
||||||
|
|
||||||
export default testFormData
|
export default testFormData
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { render, act } from '@testing-library/react'
|
|
||||||
import DateRangeWidget, {
|
|
||||||
getWidgetValue
|
|
||||||
} from '../../../src/components/atoms/FormWidgets/DateRangeWidget'
|
|
||||||
import { PublishFormSchema } from '../../../src/models/PublishForm'
|
|
||||||
|
|
||||||
describe('Date Range Widget', () => {
|
|
||||||
it('renders without crashing', () => {
|
|
||||||
act(() => {
|
|
||||||
const { container } = render(
|
|
||||||
<DateRangeWidget
|
|
||||||
schema={PublishFormSchema}
|
|
||||||
id="1"
|
|
||||||
autofocus={false}
|
|
||||||
disabled={false}
|
|
||||||
label="Date Range"
|
|
||||||
formContext={{}}
|
|
||||||
readonly={false}
|
|
||||||
value={'["2020-03-15T15:13:30Z", "2020-03-18T15:13:30Z"]'}
|
|
||||||
onBlur={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
onChange={() => {
|
|
||||||
/* */
|
|
||||||
}}
|
|
||||||
options={{}}
|
|
||||||
required={false}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('getWidgetValue returns a correctly encoded string', () => {
|
|
||||||
expect(
|
|
||||||
getWidgetValue(
|
|
||||||
new Date('2020-03-15T15:13:30.123Z'),
|
|
||||||
new Date('2020-03-18T15:13:30.456Z'),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
).toEqual('["2020-03-15T15:13:30Z","2020-03-15T15:13:30Z"]')
|
|
||||||
|
|
||||||
expect(
|
|
||||||
getWidgetValue(
|
|
||||||
new Date('2020-03-15T15:13:30.123Z'),
|
|
||||||
new Date('2020-03-18T18:13:30.456Z'),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
).toEqual('["2020-03-15T15:13:30Z","2020-03-18T18:13:30Z"]')
|
|
||||||
})
|
|
||||||
})
|
|
@ -12,7 +12,9 @@ describe('Layout', () => {
|
|||||||
|
|
||||||
testRender(
|
testRender(
|
||||||
<LocationProvider history={history}>
|
<LocationProvider history={history}>
|
||||||
<Layout location={{ href: 'https://demo.com' } as Location}>Hello</Layout>
|
<Layout title="Hello" uri={history.location.href}>
|
||||||
|
Hello
|
||||||
|
</Layout>
|
||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,78 +1,30 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render } from '@testing-library/react'
|
import { render } from '@testing-library/react'
|
||||||
import Form, { transformErrors } from '../../../src/components/molecules/Form'
|
import { transformPublishFormToMetadata } from '../../../src/components/pages/Publish/utils'
|
||||||
import {
|
import {
|
||||||
publishFormData,
|
MetaDataMarket,
|
||||||
PublishFormDataInterface,
|
MetaDataPublishForm
|
||||||
PublishFormSchema,
|
} from '../../../src/@types/MetaData'
|
||||||
PublishFormUiSchema
|
import PublishForm from '../../../src/components/pages/Publish/PublishForm'
|
||||||
} from '../../../src/models/PublishForm'
|
import publishFormData from '../__fixtures__/testFormData'
|
||||||
import testFormData from '../__fixtures__/testFormData'
|
import content from '../../../content/pages/publish.json'
|
||||||
import { transformPublishFormToMetadata } from '../../../src/components/molecules/PublishForm/PublishForm'
|
|
||||||
import { MetaDataMarket } from '../../../src/@types/MetaData'
|
|
||||||
|
|
||||||
describe('PublishForm', () => {
|
describe('PublishForm', () => {
|
||||||
it('renders without crashing', async () => {
|
it('renders without crashing', async () => {
|
||||||
const { container } = render(
|
const { container } = render(<PublishForm content={content.form} />)
|
||||||
<Form
|
|
||||||
schema={PublishFormSchema}
|
|
||||||
formData={testFormData}
|
|
||||||
uiSchema={PublishFormUiSchema}
|
|
||||||
onChange={() => null}
|
|
||||||
onSubmit={() => null}
|
|
||||||
onError={() => null}
|
|
||||||
>
|
|
||||||
Hello
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
expect(container.firstChild).toBeInTheDocument()
|
expect(container.firstChild).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('transformErrors() passes through data', () => {
|
|
||||||
const errorsMock = [
|
|
||||||
{
|
|
||||||
message: 'Hello',
|
|
||||||
name: 'Hello',
|
|
||||||
params: 'Hello',
|
|
||||||
property: 'Hello',
|
|
||||||
stack: 'Hello'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const error = transformErrors(errorsMock)
|
|
||||||
expect(error[0].message).toBe('Hello')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('transformErrors() transforms data', () => {
|
|
||||||
const errorsMock = [
|
|
||||||
{
|
|
||||||
message: 'Hello',
|
|
||||||
name: 'Hello',
|
|
||||||
params: 'Hello',
|
|
||||||
property: '.termsAndConditions',
|
|
||||||
stack: 'Hello'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const error = transformErrors(errorsMock)
|
|
||||||
expect(error[0].message).not.toBe('Hello')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Form data is correctly transformed to asset MetaData', () => {
|
it('Form data is correctly transformed to asset MetaData', () => {
|
||||||
const data: PublishFormDataInterface = publishFormData
|
const data: MetaDataPublishForm = publishFormData
|
||||||
let metadata: MetaDataMarket = transformPublishFormToMetadata(data)
|
let metadata: MetaDataMarket = transformPublishFormToMetadata(data)
|
||||||
|
|
||||||
expect(metadata.additionalInformation).toBeDefined()
|
expect(metadata.additionalInformation).toBeDefined()
|
||||||
expect(metadata.main).toBeDefined()
|
expect(metadata.main).toBeDefined()
|
||||||
|
|
||||||
data.price = 1.3
|
data.price = '1.3'
|
||||||
data.dateRange = '["2020-03-05T15:17:31Z","2020-03-10T16:00:00Z"]'
|
|
||||||
|
|
||||||
metadata = transformPublishFormToMetadata(data)
|
metadata = transformPublishFormToMetadata(data)
|
||||||
expect(metadata.main.price).toBe('1300000000000000000')
|
expect(metadata.main.price).toBe('1300000000000000000')
|
||||||
expect(metadata.additionalInformation.dateRange).toEqual([
|
|
||||||
'2020-03-05T15:17:31Z',
|
|
||||||
'2020-03-10T16:00:00Z'
|
|
||||||
])
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user