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

new publish form data setup

This commit is contained in:
Matthias Kretschmann 2021-10-21 19:58:55 +01:00
parent 70470a9459
commit 99453623d2
Signed by: m
GPG Key ID: 606EEEF3C479A91F
49 changed files with 788 additions and 743 deletions

View File

@ -1,89 +0,0 @@
{
"title": "Publish a Data Set",
"data": [
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"help": "Enter a concise title.",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics). You can change the description at any time. If you provide personal data, please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"type": "textarea",
"required": true
},
{
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "boxSelection",
"options": ["Download", "Compute"],
"required": true,
"disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.",
"disclaimerValues": ["Download"]
},
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this data set will be created with this name & symbol.",
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "terms",
"options": ["I agree to these Terms and Conditions"],
"required": true
}
],
"success": "Asset Created!"
}

98
content/publish/form.json Normal file
View File

@ -0,0 +1,98 @@
{
"metadata": {
"title": "Enter details",
"fields": [
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics). You can change the description at any time. If you provide personal data, please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"type": "textarea",
"required": true
},
{
"name": "files",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please enter the URL to your data set file and click \"ADD FILE\" to validate the data. This URL will be stored encrypted after publishing. For a compute data set, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size.",
"type": "files",
"required": true
},
{
"name": "links",
"label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json",
"help": "Please enter the URL to a sample of your data set file and click \"ADD FILE\" to validate the data. This file should reveal the data structure of your data set, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing.",
"type": "files"
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set. You are welcome to use a pseudonym, and you can change your author name at any time. Please note that it will remain in the transaction history. For more information on how personal data is handled within the metadata, please refer to our [privacy policy](/privacy/en).",
"required": true
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "terms",
"options": ["I agree to these Terms and Conditions"],
"required": true
}
]
},
"services": {
"title": "Create services",
"fields": [
{
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "boxSelection",
"options": ["Download", "Compute"],
"required": true,
"disclaimer": "Please do not provide downloadable personal data without the consent of the data subjects.",
"disclaimerValues": ["Download"]
},
{
"name": "providerUri",
"label": "Custom Provider URL",
"type": "providerUri",
"help": "Enter the URL for your custom provider or leave blank to use the default provider. [Learn more](https://github.com/oceanprotocol/provider/).",
"placeholder": "https://provider.polygon.oceanprotocol.com/",
"advanced": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",
"type": "datatoken",
"help": "The datatoken for this data set will be created with this name & symbol.",
"required": true
}
]
},
"pricing": {
"title": "Create pricing schema",
"fields": [{ "name": "dummy content" }]
}
}

View File

@ -1,6 +1,6 @@
{
"title": "Publish",
"description": "Highlight the important features of your data set or algorithm to make it more discoverable and catch the interest of data consumers.",
"warning": "Given the beta status, publishing on Ropsten or Rinkeby first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
"warning": "Publishing into a test network first is strongly recommended. Please familiarize yourself with [the market](https://oceanprotocol.com/technology/marketplaces), [the risks](https://blog.oceanprotocol.com/on-staking-on-data-in-ocean-market-3d8e09eb0a13), and the [Terms of Use](/terms).",
"tooltipNetwork": "Assets are published into the network your wallet is connected to. Switch your wallet's network to publish into another one."
}

View File

@ -16,9 +16,15 @@ function createTypes(actions) {
desc: String!
cookieName: String!
}
type PublishJsonData implements Node {
type PublishJson implements Node {
metadata: Extensions
services: Extensions
pricing: Extensions
}
type Extensions {
disclaimer: String
disclaimerValues: [String!]
advanced: Boolean
}
`
createTypes(typeDefs)

View File

@ -21,10 +21,9 @@ declare global {
advanced?: boolean
}
interface FormContent {
interface FormStepContent {
title: string
description?: string
success: string
data: FormFieldProps[]
fields: FormFieldProps[]
}
}

View File

@ -5,7 +5,7 @@ import { Field } from 'formik'
import styles from './AdvancedSettings.module.css'
export default function AdvancedSettings(prop: {
content: FormContent
content: FormStepContent
handleFieldChange: (
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps

View File

@ -16,7 +16,7 @@ export default function Terms(props: InputProps): ReactElement {
const data = useStaticQuery(query)
const termsProps: InputProps = {
...props,
defaultChecked: props.value.toString() === 'true'
defaultChecked: props?.value?.toString() === 'true'
}
return (

View File

@ -13,6 +13,8 @@ import styles from './index.module.css'
import { ErrorMessage, FieldInputProps } from 'formik'
import classNames from 'classnames/bind'
import Disclaimer from './Disclaimer'
import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/atoms/Markdown'
const cx = classNames.bind(styles)
@ -97,7 +99,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
data-is-submitting={props.form?.isSubmitting ? true : null}
>
<Label htmlFor={props.name} required={props.required}>
{label}
{label} {help && <Tooltip content={<Markdown text={help} />} />}
</Label>
<InputElement size={size} {...field} {...props} />
@ -107,7 +109,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
</div>
)}
{help && <Help>{help}</Help>}
{/* {help && <Help>{help}</Help>} */}
{disclaimer && (
<Disclaimer visible={disclaimerVisible}>{disclaimer}</Disclaimer>

View File

@ -7,6 +7,7 @@
font-size: var(--font-size-h3);
margin-top: 0;
margin-bottom: 0;
display: inline-flex;
}
@media (min-width: 40rem) {

View File

@ -10,7 +10,7 @@ export default function PageHeader({
description,
center
}: {
title: string
title: ReactElement
description?: string
center?: boolean
}): ReactElement {

View File

@ -4,7 +4,6 @@ import MetaFull from './MetaFull'
import MetaSecondary from './MetaSecondary'
import AssetActions from '../AssetActions'
import { useUserPreferences } from '@context/UserPreferences'
import Pricing from '../../Publish/Pricing'
import Bookmark from './Bookmark'
import { useAsset } from '@context/Asset'
import Alert from '@shared/atoms/Alert'
@ -31,7 +30,6 @@ export default function AssetContent(): ReactElement {
const { debug } = useUserPreferences()
const { accountId } = useWeb3()
const { owner, isInPurgatory, purgatoryData, isAssetNetwork } = useAsset()
const [showPricing, setShowPricing] = useState(false)
const [showEdit, setShowEdit] = useState<boolean>()
const [isComputeType, setIsComputeType] = useState<boolean>(false)
const [showEditCompute, setShowEditCompute] = useState<boolean>()
@ -43,7 +41,6 @@ export default function AssetContent(): ReactElement {
const isOwner = accountId.toLowerCase() === owner.toLowerCase()
setIsOwner(isOwner)
setShowPricing(isOwner && price.type === '')
setIsComputeType(Boolean(ddo.findServiceByType('compute')))
}, [accountId, price, owner, ddo])
@ -70,7 +67,6 @@ export default function AssetContent(): ReactElement {
<article className={styles.grid}>
<div>
{showPricing && <Pricing ddo={ddo} />}
<div className={styles.content}>
<MetaMain />
<Bookmark did={ddo.id} />

View File

@ -10,23 +10,23 @@ export default function Debug({
values: Partial<FormPublishData>
}): ReactElement {
const ddo = {
'@context': 'https://w3id.org/did/v1',
dataTokenInfo: {
...values.dataTokenOptions
},
service: [
{
index: 0,
type: 'metadata',
attributes: { ...transformPublishFormToMetadata(values) }
},
{
index: 1,
type: values.access,
serviceEndpoint: values.providerUri,
attributes: {}
}
]
'@context': 'https://w3id.org/did/v1'
// dataTokenInfo: {
// ...values.dataTokenOptions
// },
// service: [
// {
// index: 0,
// type: 'metadata',
// attributes: { ...transformPublishFormToMetadata(values) }
// },
// {
// index: 1,
// type: values.access,
// serviceEndpoint: values.providerUri,
// attributes: {}
// }
// ]
}
return (

View File

@ -1,7 +0,0 @@
.form {
composes: box from '../../atoms/Box.module.css';
margin-bottom: var(--spacer);
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

View File

@ -1,125 +0,0 @@
import React, {
ReactElement,
useEffect,
FormEvent,
ChangeEvent,
useState
} from 'react'
import { useFormikContext, Field, Form, FormikContextType } from 'formik'
import Input from '@shared/Form/Input'
import Download from '@images/download.svg'
import Compute from '@images/compute.svg'
import FormTitle from './FormTitle'
import FormActions from './FormActions'
import AdvancedSettings from '@shared/Form/FormFields/AdvancedSettings'
import { FormPublishData } from './_types'
import styles from './FormPublish.module.css'
import { initialValues } from './_constants'
import content from '../../../content/pages/publish/form-dataset.json'
export default function FormPublish(): ReactElement {
const {
status,
setStatus,
isValid,
values,
setErrors,
setTouched,
resetForm,
validateField,
setFieldValue
}: FormikContextType<FormPublishData> = useFormikContext()
const [computeTypeSelected, setComputeTypeSelected] = useState<boolean>(false)
// reset form validation on every mount
useEffect(() => {
setErrors({})
setTouched({})
// setSubmitting(false)
}, [setErrors, setTouched])
const accessTypeOptions = [
{
name: 'Download',
title: 'Download',
icon: <Download />
},
{
name: 'Compute',
title: 'Compute',
icon: <Compute />
}
]
const computeTypeOptions = ['1 day', '1 week', '1 month', '1 year']
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
// function handleFieldChange(
// e: ChangeEvent<HTMLInputElement>,
// field: FormFieldProps
// ) {
// const value =
// field.type === 'terms' ? !JSON.parse(e.target.value) : e.target.value
// if (field.name === 'access' && value === 'Compute') {
// setComputeTypeSelected(true)
// if (values.timeout === 'Forever')
// setFieldValue('timeout', computeTypeOptions[0])
// } else {
// if (field.name === 'access' && value === 'Download') {
// setComputeTypeSelected(false)
// }
// }
// validateField(field.name)
// setFieldValue(field.name, value)
// }
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({
values: initialValues as FormPublishData,
status: 'empty'
})
setStatus('empty')
}
return (
<Form
className={styles.form}
// do we need this?
onChange={() => status === 'empty' && setStatus(null)}
>
<FormTitle title={content.title} />
{content.data.map(
(field: FormFieldProps) =>
field.advanced !== true && (
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection'
? accessTypeOptions
: field.name === 'timeout' && computeTypeSelected === true
? computeTypeOptions
: field.options
}
component={Input}
// onChange={(e: ChangeEvent<HTMLInputElement>) =>
// handleFieldChange(e, field)
// }
/>
)
)}
<FormActions
isValid={isValid}
resetFormAndClearStorage={resetFormAndClearStorage}
/>
</Form>
)
}

View File

@ -2,6 +2,8 @@ import React, { FormEvent, ReactElement } from 'react'
import { useOcean } from '@context/Ocean'
import Button from '@shared/atoms/Button'
import styles from './FormActions.module.css'
import { FormikContextType, useFormikContext } from 'formik'
import { FormPublishData } from '../_types'
export default function FormActions({
isValid,
@ -11,22 +13,22 @@ export default function FormActions({
resetFormAndClearStorage: (e: FormEvent<Element>) => void
}): ReactElement {
const { ocean, account } = useOcean()
const { status }: FormikContextType<FormPublishData> = useFormikContext()
return (
<footer className={styles.actions}>
<Button
style="primary"
type="submit"
disabled={!ocean || !account || !isValid || status === 'empty'}
>
Submit
</Button>
{status !== 'empty' && (
<Button style="text" size="small" onClick={resetFormAndClearStorage}>
Reset Form
</Button>
)}
<Button
style="primary"
disabled={!ocean || !account || !isValid || status === 'empty'}
>
Submit
</Button>
</footer>
)
}

View File

@ -1,5 +1,4 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Coin.module.css'
import InputElement from '@shared/Form/Input/InputElement'
import Logo from '@images/logo.svg'
@ -46,7 +45,7 @@ export default function Coin({
{...field}
/>
{datatokenOptions?.symbol === 'OCEAN' && (
<Conversion price={field.value} className={stylesIndex.conversion} />
<Conversion price={field.value} />
)}
<Error meta={meta} />
</div>

View File

@ -1,7 +1,3 @@
.dynamic {
composes: content from './index.module.css';
}
.wallet {
display: flex;
align-items: center;

View File

@ -7,27 +7,21 @@ import Wallet from '../../../Header/Wallet'
import Coin from './Coin'
import styles from './Dynamic.module.css'
import Fees from './Fees'
import stylesIndex from './index.module.css'
import { FormikContextType, useFormikContext } from 'formik'
import { DDO } from '@oceanprotocol/lib'
import Price from './Price'
import Decimal from 'decimal.js'
import { useOcean } from '@context/Ocean'
import { useWeb3 } from '@context/Web3'
import { FormPublishData } from '../../_types'
export default function Dynamic({
ddo,
content
}: {
ddo: DDO
content: any
}): ReactElement {
export default function Dynamic({ content }: { content: any }): ReactElement {
const { networkId, balance } = useWeb3()
const { account } = useOcean()
const [firstPrice, setFirstPrice] = useState<string>()
// Connect with form
const { values }: FormikContextType<PriceOptions> = useFormikContext()
const { values }: FormikContextType<FormPublishData> = useFormikContext()
const { dataTokenOptions } = values.services[0]
const {
price,
@ -36,7 +30,7 @@ export default function Dynamic({
swapFee,
dtAmount,
oceanAmount
} = values
} = values.pricing
const [error, setError] = useState<string>()
@ -69,8 +63,8 @@ export default function Dynamic({
}, [price, networkId, account, balance])
return (
<div className={styles.dynamic}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<>
<FormHelp>{content.info}</FormHelp>
<aside className={styles.wallet}>
{balance?.ocean && (
@ -88,7 +82,7 @@ export default function Dynamic({
Price <Tooltip content={content.tooltips.poolInfo} />
</h4>
<Price ddo={ddo} firstPrice={firstPrice} />
<Price firstPrice={firstPrice} />
<h4 className={styles.title}>
Datatoken Liquidity Pool <Tooltip content={content.tooltips.poolInfo} />
@ -103,8 +97,8 @@ export default function Dynamic({
<Coin
name="dtAmount"
datatokenOptions={{
symbol: ddo.dataTokenInfo.symbol,
name: ddo.dataTokenInfo.name
symbol: dataTokenOptions.symbol,
name: dataTokenOptions.name
}}
weight={`${Number(weightOnDataToken) * 10}%`}
readOnly
@ -123,6 +117,6 @@ export default function Dynamic({
<Alert text={error} state="error" />
</div>
)}
</div>
</>
)
}

View File

@ -0,0 +1,14 @@
import React, { ReactElement } from 'react'
import FormHelp from '@shared/Form/Input/Help'
import Price from './Price'
import Fees from './Fees'
export default function Fixed({ content }: { content: any }): ReactElement {
return (
<>
<FormHelp>{content.info}</FormHelp>
<Price />
<Fees tooltips={content.tooltips} pricingType="fixed" />
</>
)
}

View File

@ -0,0 +1,12 @@
import React, { ReactElement } from 'react'
import FormHelp from '@shared/Form/Input/Help'
import Price from './Price'
export default function Free({ content }: { content: any }): ReactElement {
return (
<>
<FormHelp>{content.info}</FormHelp>
<Price free />
</>
)
}

View File

@ -1,40 +1,23 @@
import Conversion from '@shared/Price/Conversion'
import { useField } from 'formik'
import React, { ReactElement, useState, useEffect } from 'react'
import { useField, useFormikContext } from 'formik'
import React, { ReactElement } from 'react'
import Input from '@shared/Form/Input'
import Error from './Error'
import { DDO } from '@oceanprotocol/lib'
import PriceUnit from '@shared/Price/PriceUnit'
import usePricing from '@hooks/usePricing'
import styles from './Price.module.css'
import { FormPublishData } from '../../_types'
export default function Price({
ddo,
firstPrice,
free
}: {
ddo: DDO
firstPrice?: string
free?: boolean
}): ReactElement {
const [field, meta] = useField('price')
const { getDTName, getDTSymbol } = usePricing()
const [dtSymbol, setDtSymbol] = useState<string>()
const [dtName, setDtName] = useState<string>()
useEffect(() => {
if (!ddo) return
async function setDatatokenSymbol(ddo: DDO) {
const dtSymbol = await getDTSymbol(ddo)
setDtSymbol(dtSymbol)
}
async function setDatatokenName(ddo: DDO) {
const dtName = await getDTName(ddo)
setDtName(dtName)
}
setDatatokenSymbol(ddo)
setDatatokenName(ddo)
}, [])
const { values } = useFormikContext<FormPublishData>()
const { dataTokenOptions } = values.services[0]
return (
<div className={styles.price}>
@ -62,7 +45,7 @@ export default function Price({
</div>
<div className={styles.datatoken}>
<h4>
= <strong>1</strong> {dtSymbol}{' '}
= <strong>1</strong> {dataTokenOptions.symbol}{' '}
<Conversion price={field.value} className={styles.conversion} />
</h4>
</div>

View File

@ -0,0 +1,152 @@
import React, { ReactElement, useEffect } from 'react'
import { useFormikContext } from 'formik'
import { DDO } from '@oceanprotocol/lib'
import { graphql, useStaticQuery } from 'gatsby'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import Tabs from '@shared/atoms/Tabs'
import { isValidNumber } from '@utils/numbers'
import Decimal from 'decimal.js'
import { FormPublishData } from '../../_types'
import Dynamic from './Dynamic'
import Fixed from './Fixed'
import Free from './Free'
const query = graphql`
query PricingQuery {
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
edges {
node {
childContentJson {
create {
empty {
title
info
action {
name
help
}
}
fixed {
title
info
tooltips {
communityFee
marketplaceFee
}
}
dynamic {
title
info
tooltips {
poolInfo
swapFee
communityFee
marketplaceFee
}
}
free {
title
info
}
}
}
}
}
}
}
`
export default function Pricing(): ReactElement {
// Get content
const data = useStaticQuery(query)
const content = data.content.edges[0].node.childContentJson.create
const { appConfig } = useSiteMetadata()
// Connect with main publish form
const { values, setFieldValue } = useFormikContext<FormPublishData>()
const { pricing } = values
const { price, oceanAmount, weightOnOcean, weightOnDataToken, type } = pricing
// Switch type value upon tab change
function handleTabChange(tabName: string) {
const type = tabName.toLowerCase()
setFieldValue('pricing.type', type)
type === 'fixed' && setFieldValue('pricing.dtAmount', 1000)
type === 'free' && price < 1 && setFieldValue('pricing.price', 1)
}
// Always update everything when price value changes
useEffect(() => {
if (type === 'fixed') return
const dtAmount =
isValidNumber(oceanAmount) &&
isValidNumber(weightOnOcean) &&
isValidNumber(price) &&
isValidNumber(weightOnDataToken)
? new Decimal(oceanAmount)
.dividedBy(new Decimal(weightOnOcean))
.dividedBy(new Decimal(price))
.mul(new Decimal(weightOnDataToken))
: 0
setFieldValue('pricing.dtAmount', dtAmount)
}, [price, oceanAmount, weightOnOcean, weightOnDataToken, type])
const tabs = [
appConfig.allowFixedPricing === 'true'
? {
title: content.fixed.title,
content: <Fixed content={content.fixed} />
}
: undefined,
appConfig.allowDynamicPricing === 'true'
? {
title: content.dynamic.title,
content: <Dynamic content={content.dynamic} />
}
: undefined,
appConfig.allowFreePricing === 'true'
? {
title: content.free.title,
content: <Free content={content.free} />
}
: undefined
].filter((tab) => tab !== undefined)
return (
<Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={type === 'fixed' ? 0 : 1}
/>
)
// async function handleCreatePricing(values: PriceOptions) {
// try {
// const priceOptions = {
// ...values,
// // swapFee is tricky: to get 0.1% you need to send 0.001 as value
// swapFee: `${values.swapFee / 100}`
// }
// const tx = await createPricing(priceOptions, ddo)
// // Pricing failed
// if (!tx || pricingError) {
// toast.error(pricingError || 'Price creation failed.')
// Logger.error(pricingError || 'Price creation failed.')
// return
// }
// // Pricing succeeded
// setSuccess(
// `🎉 Successfully created a ${values.type} price. 🎉 Reload the page to get all updates.`
// )
// Logger.log(`Transaction: ${tx}`)
// } catch (error) {
// toast.error(error.message)
// Logger.error(error.message)
// }
// }
}

View File

@ -0,0 +1,4 @@
.form {
composes: box from '@shared/atoms/Box.module.css';
margin-bottom: var(--spacer);
}

View File

@ -0,0 +1,224 @@
import React, {
ReactElement,
useEffect,
FormEvent,
ChangeEvent,
useState
} from 'react'
import { useStaticQuery, graphql } from 'gatsby'
import { useFormikContext, Field, Form, FormikContextType } from 'formik'
import Input from '@shared/Form/Input'
import { ReactComponent as Download } from '@images/download.svg'
import { ReactComponent as Compute } from '@images/compute.svg'
import FormActions from './FormActions'
import AdvancedSettings from '@shared/Form/FormFields/AdvancedSettings'
import { FormPublishData } from '../_types'
import styles from './index.module.css'
import { initialValues } from '../_constants'
import Tabs from '@shared/atoms/Tabs'
import Pricing from './Pricing'
import Debug from '../Debug'
const query = graphql`
query {
content: publishJson {
metadata {
title
fields {
name
placeholder
label
help
type
required
options
disclaimer
disclaimerValues
advanced
}
}
services {
title
fields {
name
placeholder
label
help
type
required
options
disclaimer
disclaimerValues
advanced
}
}
pricing {
title
fields {
name
placeholder
label
help
type
required
options
disclaimer
disclaimerValues
advanced
}
}
warning
}
}
`
const accessTypeOptions = [
{
name: 'Download',
title: 'Download',
icon: <Download />
},
{
name: 'Compute',
title: 'Compute',
icon: <Compute />
}
]
export default function FormPublish(): ReactElement {
const { content } = useStaticQuery(query)
const {
setStatus,
isValid,
values,
setErrors,
setTouched,
resetForm,
validateField,
setFieldValue
}: FormikContextType<FormPublishData> = useFormikContext()
const [computeTypeSelected, setComputeTypeSelected] = useState<boolean>(false)
// reset form validation on every mount
useEffect(() => {
setErrors({})
setTouched({})
// setSubmitting(false)
}, [setErrors, setTouched])
const computeTypeOptions = ['1 day', '1 week', '1 month', '1 year']
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
const value =
field.type === 'terms' ? !JSON.parse(e.target.value) : e.target.value
if (field.name === 'access' && value === 'Compute') {
setComputeTypeSelected(true)
if (values.timeout === 'Forever')
setFieldValue('timeout', computeTypeOptions[0])
} else {
if (field.name === 'access' && value === 'Download') {
setComputeTypeSelected(false)
}
}
validateField(field.name)
setFieldValue(field.name, value)
}
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({
values: initialValues as FormPublishData,
status: 'empty'
})
setStatus('empty')
}
function getStepContentFields(contentStep: FormStepContent) {
return contentStep.fields.map(
(field: FormFieldProps) =>
field.advanced !== true && (
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection'
? accessTypeOptions
: field.name === 'timeout' && computeTypeSelected === true
? computeTypeOptions
: field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
)
)
}
const tabs = [
{
title: content.metadata.title,
content: (
<>
{getStepContentFields(content.metadata)}
<AdvancedSettings
content={content.metadata}
handleFieldChange={handleFieldChange}
/>
<FormActions
isValid={isValid}
resetFormAndClearStorage={resetFormAndClearStorage}
/>
</>
)
},
{
title: content.services.title,
content: (
<>
{getStepContentFields(content.services)}
<AdvancedSettings
content={content.services}
handleFieldChange={handleFieldChange}
/>
<FormActions
isValid={isValid}
resetFormAndClearStorage={resetFormAndClearStorage}
/>
</>
)
},
{
title: content.pricing.title,
content: (
<>
<Pricing />
<FormActions
isValid={isValid}
resetFormAndClearStorage={resetFormAndClearStorage}
/>
</>
)
}
]
return (
<>
<Form className={styles.form}>
<Tabs items={tabs} />
</Form>
<Debug values={values} />
</>
)
}

View File

@ -1,21 +0,0 @@
.title {
font-size: var(--font-size-h4);
display: inline-flex;
}
.network {
color: var(--font-color-heading);
margin-left: calc(var(--spacer) / 8);
}
.network svg {
width: 1em;
height: 1em;
margin-top: -0.25em;
fill: var(--color-secondary);
}
.tooltip {
width: 0.75em;
height: 0.75em;
}

View File

@ -1,8 +0,0 @@
.feedback {
width: 100%;
min-height: 20vh;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}

View File

@ -1,34 +0,0 @@
import Loader from '@shared/atoms/Loader'
import SuccessConfetti from '@shared/SuccessConfetti'
import React, { ReactElement } from 'react'
import styles from './Feedback.module.css'
import Button from '@shared/atoms/Button'
export default function Feedback({
success,
pricingStepText
}: {
success: string
pricingStepText: string
}): ReactElement {
const SuccessAction = () => (
<Button
style="primary"
size="small"
className={styles.action}
onClick={() => window?.location.reload()}
>
Reload Page
</Button>
)
return (
<div className={styles.feedback}>
{success ? (
<SuccessConfetti success={success} action={<SuccessAction />} />
) : (
<Loader message={pricingStepText} />
)}
</div>
)
}

View File

@ -1,3 +0,0 @@
.fixed {
composes: content from './index.module.css';
}

View File

@ -1,23 +0,0 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Fixed.module.css'
import FormHelp from '@shared/Form/Input/Help'
import { DDO } from '@oceanprotocol/lib'
import Price from './Price'
import Fees from './Fees'
export default function Fixed({
ddo,
content
}: {
ddo: DDO
content: any
}): ReactElement {
return (
<div className={styles.fixed}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<Price ddo={ddo} />
<Fees tooltips={content.tooltips} pricingType="fixed" />
</div>
)
}

View File

@ -1,3 +0,0 @@
.free {
composes: content from './index.module.css';
}

View File

@ -1,21 +0,0 @@
import React, { ReactElement } from 'react'
import stylesIndex from './index.module.css'
import styles from './Free.module.css'
import FormHelp from '@shared/Form/Input/Help'
import { DDO } from '@oceanprotocol/lib'
import Price from './Price'
export default function Free({
ddo,
content
}: {
ddo: DDO
content: any
}): ReactElement {
return (
<div className={styles.free}>
<FormHelp className={stylesIndex.help}>{content.info}</FormHelp>
<Price ddo={ddo} free />
</div>
)
}

View File

@ -1,52 +0,0 @@
.content {
padding: 0;
}
.content label {
color: var(--color-secondary);
}
.content input {
text-align: center;
}
.content p {
margin-bottom: 0;
}
.content [class*='error'] {
text-align: left;
top: 100%;
}
.conversion {
width: 100%;
display: block;
text-align: center;
margin-top: calc(var(--spacer) / 6);
}
.help {
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}
.actions {
text-align: center;
}
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}
.actionsHelp {
margin-top: calc(var(--spacer) / 2);
padding-left: var(--spacer);
padding-right: var(--spacer);
}
.free {
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}

View File

@ -1,109 +0,0 @@
import React, { ReactElement, useEffect } from 'react'
import styles from './index.module.css'
import Tabs from '@shared/atoms/Tabs'
import Fixed from './Fixed'
import Dynamic from './Dynamic'
import Free from './Free'
import { useFormikContext } from 'formik'
import { useUserPreferences } from '@context/UserPreferences'
import Button from '@shared/atoms/Button'
import { DDO } from '@oceanprotocol/lib'
import FormHelp from '@shared/Form/Input/Help'
import { useSiteMetadata } from '@hooks/useSiteMetadata'
import { isValidNumber } from '@utils/numbers'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
export default function FormPricing({
ddo,
setShowPricing,
content
}: {
ddo: DDO
setShowPricing: (value: boolean) => void
content: any
}): ReactElement {
const { debug } = useUserPreferences()
const { appConfig } = useSiteMetadata()
// Connect with form
const { values, setFieldValue, submitForm } = useFormikContext()
const { price, oceanAmount, weightOnOcean, weightOnDataToken, type } =
values as PriceOptions
// Switch type value upon tab change
function handleTabChange(tabName: string) {
const type = tabName.toLowerCase()
setFieldValue('type', type)
type === 'fixed' && setFieldValue('dtAmount', 1000)
type === 'free' && price < 1 && setFieldValue('price', 1)
}
// Always update everything when price value changes
useEffect(() => {
if (type === 'fixed') return
const dtAmount =
isValidNumber(oceanAmount) &&
isValidNumber(weightOnOcean) &&
isValidNumber(price) &&
isValidNumber(weightOnDataToken)
? new Decimal(oceanAmount)
.dividedBy(new Decimal(weightOnOcean))
.dividedBy(new Decimal(price))
.mul(new Decimal(weightOnDataToken))
: 0
setFieldValue('dtAmount', dtAmount)
}, [price, oceanAmount, weightOnOcean, weightOnDataToken, type])
const tabs = [
appConfig.allowFixedPricing === 'true'
? {
title: content.fixed.title,
content: <Fixed content={content.fixed} ddo={ddo} />
}
: undefined,
appConfig.allowDynamicPricing === 'true'
? {
title: content.dynamic.title,
content: <Dynamic content={content.dynamic} ddo={ddo} />
}
: undefined,
appConfig.allowFreePricing === 'true'
? {
title: content.free.title,
content: <Free content={content.free} ddo={ddo} />
}
: undefined
].filter((tab) => tab !== undefined)
return (
<>
<Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={type === 'fixed' ? 0 : 1}
/>
<div className={styles.actions}>
<Button style="primary" onClick={() => submitForm()}>
{content.empty.action.name}
</Button>
<Button style="text" size="small" onClick={() => setShowPricing(false)}>
Cancel
</Button>
<FormHelp className={styles.actionsHelp}>
{content.empty.action.help}
</FormHelp>
</div>
{debug === true && (
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
)}
</>
)
}

View File

@ -1,39 +0,0 @@
import { allowDynamicPricing, allowFixedPricing } from '../../../../app.config'
import * as Yup from 'yup'
export const validationSchema: Yup.SchemaOf<PriceOptions> = Yup.object().shape({
price: Yup.number()
.min(1, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
dtAmount: Yup.number()
.min(9, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
oceanAmount: Yup.number()
.min(21, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
type: Yup.string()
.matches(/fixed|dynamic|free/g, { excludeEmptyString: true })
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
weightOnOcean: Yup.string().required('Required'),
swapFee: Yup.number()
.min(0.1, (param) => `Must be more or equal to ${param.min}`)
.max(10, 'Maximum is 10%')
.required('Required')
.nullable()
})
export const initialValues: PriceOptions = {
price: 1,
type:
allowDynamicPricing === 'true'
? 'dynamic'
: allowFixedPricing === 'true'
? 'fixed'
: 'free',
dtAmount: allowDynamicPricing === 'true' ? 9 : 1000,
oceanAmount: 21,
weightOnOcean: '7', // 70% on OCEAN
weightOnDataToken: '3', // 30% on datatoken
swapFee: 0.1 // in %
}

View File

@ -1,11 +0,0 @@
.pricing {
composes: box from '@shared/atoms/Box.module.css';
padding: 0;
padding-bottom: var(--spacer);
margin-top: var(--spacer);
}
.pricing [class*='alert'] {
margin: var(--spacer);
margin-bottom: 0;
}

View File

@ -0,0 +1,16 @@
.network {
color: var(--font-color-heading);
margin-left: calc(var(--spacer) / 3);
}
.network svg {
width: 1em;
height: 1em;
margin-top: -0.25em;
fill: currentColor;
}
.tooltip {
width: 0.5em;
height: 0.5em;
}

View File

@ -0,0 +1,43 @@
import React, { ReactElement } from 'react'
import NetworkName from '@shared/NetworkName'
import Tooltip from '@shared/atoms/Tooltip'
import { useWeb3 } from '@context/Web3'
import styles from './Title.module.css'
import { graphql, useStaticQuery } from 'gatsby'
const query = graphql`
query {
content: allFile(filter: { relativePath: { eq: "publish/index.json" } }) {
edges {
node {
childPublishJson {
title
tooltipNetwork
}
}
}
}
}
`
export default function Title(): ReactElement {
const data = useStaticQuery(query)
const content = data.content.edges[0].node.childPublishJson
const { networkId } = useWeb3()
return (
<>
{content.title}{' '}
{networkId && (
<>
into <NetworkName networkId={networkId} className={styles.network} />
<Tooltip
content={content.tooltipNetwork}
className={styles.tooltip}
/>
</>
)}
</>
)
}

View File

@ -1,101 +1,149 @@
import { File as FileMetadata } from '@oceanprotocol/lib'
import * as Yup from 'yup'
import { allowDynamicPricing, allowFixedPricing } from '../../../app.config'
import { FormPublishData } from './_types'
export const initialValues: Partial<FormPublishData> = {
metadata: {
name: '',
author: '',
description: '',
termsAndConditions: false,
tags: ''
},
services: [
{
files: '',
links: '',
dataTokenOptions: { name: '', symbol: '' },
timeout: 'Forever',
access: '',
providerUri: ''
}
],
pricing: {
price: 1,
type:
allowDynamicPricing === 'true'
? 'dynamic'
: allowFixedPricing === 'true'
? 'fixed'
: 'free',
dtAmount: allowDynamicPricing === 'true' ? 9 : 1000,
oceanAmount: 21,
weightOnOcean: '7', // 70% on OCEAN
weightOnDataToken: '3', // 30% on datatoken
swapFee: 0.1 // in %
}
}
const validationMetadata = {
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
description: Yup.string().min(10).required('Required'),
author: Yup.string().required('Required'),
tags: Yup.string().nullable(),
termsAndConditions: Yup.boolean().required('Required')
}
const validationService = {
files: Yup.array<FileMetadata>()
.required('Enter a valid URL and click "ADD FILE"')
.nullable(),
links: Yup.array<FileMetadata[]>().nullable(),
dataTokenOptions: Yup.object()
.shape({
name: Yup.string(),
symbol: Yup.string()
})
.required('Required'),
timeout: Yup.string().required('Required'),
access: Yup.string()
.matches(/Compute|Download/g, { excludeEmptyString: true })
.required('Required'),
providerUri: Yup.string().url().nullable()
}
const validationPricing = {
price: Yup.number()
.min(1, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
dtAmount: Yup.number()
.min(9, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
oceanAmount: Yup.number()
.min(21, (param) => `Must be more or equal to ${param.min}`)
.required('Required'),
type: Yup.string()
.matches(/fixed|dynamic|free/g, { excludeEmptyString: true })
.required('Required'),
weightOnDataToken: Yup.string().required('Required'),
weightOnOcean: Yup.string().required('Required'),
swapFee: Yup.number()
.min(0.1, (param) => `Must be more or equal to ${param.min}`)
.max(10, 'Maximum is 10%')
.required('Required')
.nullable()
}
export const validationSchema: Yup.SchemaOf<FormPublishData> = Yup.object()
.shape({
// ---- required fields ----
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
author: Yup.string().required('Required'),
dataTokenOptions: Yup.object()
.shape({
name: Yup.string(),
symbol: Yup.string()
})
.required('Required'),
files: Yup.array<FileMetadata>()
.required('Enter a valid URL and click "ADD FILE"')
.nullable(),
description: Yup.string().min(10).required('Required'),
timeout: Yup.string().required('Required'),
access: Yup.string()
.matches(/Compute|Download/g, { excludeEmptyString: true })
.required('Required'),
termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ----
tags: Yup.string().nullable(),
links: Yup.array<FileMetadata[]>().nullable(),
providerUri: Yup.string().url().nullable()
metadata: Yup.object().shape(validationMetadata),
services: Yup.array().of(Yup.object().shape(validationService)),
pricing: Yup.object().shape(validationPricing)
})
.defined()
export const initialValues: Partial<FormPublishData> = {
name: '',
author: '',
dataTokenOptions: {
name: '',
symbol: ''
},
files: '',
description: '',
timeout: 'Forever',
access: '',
termsAndConditions: false,
tags: '',
providerUri: ''
}
// export const validationSchemaAlgo: Yup.SchemaOf<MetadataPublishFormAlgorithm> =
// Yup.object()
// .shape({
// // ---- required fields ----
// name: Yup.string()
// .min(4, (param) => `Title must be at least ${param.min} characters`)
// .required('Required'),
// description: Yup.string().min(10).required('Required'),
// files: Yup.array<FileMetadata>().required('Required').nullable(),
// timeout: Yup.string().required('Required'),
// dataTokenOptions: Yup.object()
// .shape({
// name: Yup.string(),
// symbol: Yup.string()
// })
// .required('Required'),
// dockerImage: Yup.string()
// .matches(/node:latest|python:latest|custom image/g, {
// excludeEmptyString: true
// })
// .required('Required'),
// image: Yup.string().required('Required'),
// containerTag: Yup.string().required('Required'),
// entrypoint: Yup.string().required('Required'),
// author: Yup.string().required('Required'),
// termsAndConditions: Yup.boolean().required('Required'),
// // ---- optional fields ----
// algorithmPrivacy: Yup.boolean().nullable(),
// tags: Yup.string().nullable(),
// links: Yup.array<FileMetadata[]>().nullable()
// })
// .defined()
export const validationSchemaAlgo: Yup.SchemaOf<MetadataPublishFormAlgorithm> =
Yup.object()
.shape({
// ---- required fields ----
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
description: Yup.string().min(10).required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(),
timeout: Yup.string().required('Required'),
dataTokenOptions: Yup.object()
.shape({
name: Yup.string(),
symbol: Yup.string()
})
.required('Required'),
dockerImage: Yup.string()
.matches(/node:latest|python:latest|custom image/g, {
excludeEmptyString: true
})
.required('Required'),
image: Yup.string().required('Required'),
containerTag: Yup.string().required('Required'),
entrypoint: Yup.string().required('Required'),
author: Yup.string().required('Required'),
termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ----
algorithmPrivacy: Yup.boolean().nullable(),
tags: Yup.string().nullable(),
links: Yup.array<FileMetadata[]>().nullable()
})
.defined()
export const initialValuesAlgo: Partial<MetadataPublishFormAlgorithm> = {
name: '',
author: '',
dataTokenOptions: {
name: '',
symbol: ''
},
dockerImage: 'node:latest',
image: 'node',
containerTag: 'latest',
entrypoint: 'node $ALGO',
files: '',
description: '',
algorithmPrivacy: false,
termsAndConditions: false,
tags: '',
timeout: 'Forever',
providerUri: ''
}
// export const initialValuesAlgo: Partial<MetadataPublishFormAlgorithm> = {
// name: '',
// author: '',
// dataTokenOptions: {
// name: '',
// symbol: ''
// },
// dockerImage: 'node:latest',
// image: 'node',
// containerTag: 'latest',
// entrypoint: 'node $ALGO',
// files: '',
// description: '',
// algorithmPrivacy: false,
// termsAndConditions: false,
// tags: '',
// timeout: 'Forever',
// providerUri: ''
// }

View File

@ -2,9 +2,12 @@ import { DataTokenOptions } from '@hooks/usePublish'
import { EditableMetadataLinks } from '@oceanprotocol/lib'
export interface FormPublishService {
files: string | File[]
links?: string | EditableMetadataLinks[]
timeout: string
dataTokenOptions: DataTokenOptions
access: 'Download' | 'Compute' | string
providerUri?: string
}
export interface FormPublishData {
@ -12,12 +15,9 @@ export interface FormPublishData {
metadata: {
name: string
description: string
files: string | File[]
author: string
termsAndConditions: boolean
tags?: string
links?: string | EditableMetadataLinks[]
providerUri?: string
}
services: FormPublishService[]
pricing: PriceOptions

View File

@ -2,7 +2,6 @@ import React, { ReactElement, useState, useEffect } from 'react'
import Permission from '@shared/Permission'
import { Formik, FormikState } from 'formik'
import { usePublish } from '@hooks/usePublish'
import styles from './index.module.css'
import { initialValues, validationSchema } from './_constants'
// import {
// transformPublishFormToMetadata,
@ -13,13 +12,17 @@ import { Logger, Metadata } from '@oceanprotocol/lib'
import { useAccountPurgatory } from '@hooks/useAccountPurgatory'
import { useWeb3 } from '@context/Web3'
import { FormPublishData } from './_types'
import PageHeader from '@shared/Page/PageHeader'
import Title from './Title'
import styles from './index.module.css'
import FormPublish from './FormPublish'
const formName = 'ocean-publish-form'
export default function PublishPage({
content
}: {
content: { warning: string }
content: { title: string; description: string; warning: string }
}): ReactElement {
const { accountId } = useWeb3()
const { isInPurgatory, purgatoryData } = useAccountPurgatory(accountId)
@ -78,21 +81,19 @@ export default function PublishPage({
// }
// }
return isInPurgatory && purgatoryData ? null : (
<Permission eventType="publish">
<Formik
initialValues={initialValues}
initialStatus="empty"
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
// kick off publishing
// await handleSubmit(values, resetForm)
}}
>
{({ values }) => {
return <>Hello</>
}}
</Formik>
</Permission>
{isInPurgatory && purgatoryData ? null : (
<Formik
initialValues={initialValues}
initialStatus="empty"
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
// kick off publishing
// await handleSubmit(values, resetForm)
}}
>
<FormPublish />
</Formik>
)}
</>
)
}

View File

@ -15,4 +15,4 @@ export default function PagePublish(): ReactElement {
</Page>
</OceanProvider>
)
}
}