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

Update metadata, the proper way (#292)

* prototype view switching

* refactor, more UI

* formik form setup & data flow

* debug output, fixes, refactor

* description preview refactor

* publish/update date changes

* output created & updated date at top of asset
* use ddo.created & ddo.updated everywhere
* stop pushing metadata.main.datePublished

* owner check for edit link

* all the feedback states and switching between them: loading, error, success

* refactor feedback, one component for publish & edit

* action & date output fixes

* move all content, iterate form fields from it

* UI updates

* styling tweaks

* ddo dataflow refactor, more useAsset usage

* more useAsset usage

* form actions styling

* prepare edit history component

* metadata output tweaks

* copy

* safeguard against profile urls without protocol defined

* refetch ddo after edit

Signed-off-by: mihaisc <mihai.scarlat@smartcontrol.ro>

* switch author for dataTokenOptions in metadata preview

* refactor

* copy

* showPricing fix

* validation: minimum characters for title & description

* disable submit button when validation fails

* form validation fixes

* manually trigger onChange validation in publish & edit forms

Co-authored-by: mihaisc <mihai.scarlat@smartcontrol.ro>
This commit is contained in:
Matthias Kretschmann 2020-12-10 14:30:40 +01:00 committed by GitHub
parent c57731cd0b
commit 960c5b3234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 952 additions and 440 deletions

25
content/pages/edit.json Normal file
View File

@ -0,0 +1,25 @@
{
"description": "Update selected metadata of this data set. Updating metadata will create an on-chain transaction you have to approve in your wallet.",
"form": {
"success": "🎉 Successfully updated. 🎉",
"successAction": "Close",
"error": "Updating DDO failed.",
"data": [
{
"name": "name",
"label": "New Title",
"placeholder": "e.g. Shapes of Desert Plants",
"help": "Enter a concise title.",
"required": true
},
{
"name": "description",
"label": "New Description",
"help": "Add a thorough description with as much detail as possible. You can use [Markdown](https://daringfireball.net/projects/markdown/basics).",
"type": "textarea",
"rows": 10,
"required": true
}
]
}
}

View File

@ -0,0 +1,18 @@
import React, { ReactElement } from 'react'
export default function DebugOutput({
title,
output
}: {
title: string
output: any
}): ReactElement {
return (
<div>
<h5>{title}</h5>
<pre>
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
</div>
)
}

View File

@ -44,15 +44,7 @@ export interface InputProps {
}
export default function Input(props: Partial<InputProps>): ReactElement {
const {
required,
name,
label,
help,
additionalComponent,
size,
field
} = props
const { label, help, additionalComponent, size, field } = props
const hasError =
props.form?.touched[field.name] && props.form?.errors[field.name]
@ -67,7 +59,7 @@ export default function Input(props: Partial<InputProps>): ReactElement {
className={styleClasses}
data-is-submitting={props.form?.isSubmitting ? true : null}
>
<Label htmlFor={name} required={required}>
<Label htmlFor={props.name} required={props.required}>
{label}
</Label>
<InputElement size={size} {...field} {...props} />

View File

@ -5,6 +5,14 @@
word-break: break-word;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5 {
margin-bottom: calc(var(--spacer) / 2);
}
.markdown h1 {
font-size: var(--font-size-h3);
}

View File

@ -17,7 +17,9 @@ export default function PublisherLinks({
? `https://twitter.com/${link.value}`
: link.name === 'GitHub'
? `https://github.com/${link.value}`
: link.value
: link.value.includes('http') // safeguard against urls without protocol defined
? link.value
: `//${link.value}`
return (
<a href={href} key={link.name} target="_blank" rel="noreferrer">

View File

@ -1,6 +1,5 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { format, formatDistance } from 'date-fns'
import { setDate } from 'date-fns/esm'
export default function Time({
date,
@ -15,24 +14,26 @@ export default function Time({
}): ReactElement {
const [dateIso, setDateIso] = useState<string>()
const [dateNew, setDateNew] = useState<Date>()
useEffect(() => {
if (!date) return
const dateNew = isUnix ? new Date(Number(date) * 1000) : new Date(date)
setDateIso(dateNew.toISOString())
setDateNew(dateNew)
}, [date])
}, [date, isUnix])
return !dateIso || !dateNew ? (
<></>
) : (
<time
title={relative ? format(dateNew, 'MMMM d, yyyy') : undefined}
title={format(dateNew, 'PPppp')}
dateTime={dateIso}
className={className || undefined}
>
{relative
? formatDistance(dateNew, Date.now(), { addSuffix: true })
: format(dateNew, 'MMMM d, yyyy')}
: format(dateNew, 'PP')}
</time>
)
}

View File

@ -1,16 +1,25 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useEffect } from 'react'
import { File as FileMetadata } from '@oceanprotocol/lib/dist/node/ddo/interfaces/File'
import { prettySize } from '../../../../utils'
import cleanupContentType from '../../../../utils/cleanupContentType'
import styles from './Info.module.css'
import { useField, useFormikContext } from 'formik'
export default function FileInfo({
file,
removeItem
name,
file
}: {
name: string
file: FileMetadata
removeItem?(): void
}): ReactElement {
const { validateField } = useFormikContext()
const [field, meta, helpers] = useField(name)
// On mount, validate the field manually
useEffect(() => {
validateField(name)
}, [name, validateField])
return (
<div className={styles.info}>
<h3 className={styles.url}>{file.url}</h3>
@ -19,11 +28,12 @@ export default function FileInfo({
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{file.contentType && <li>{cleanupContentType(file.contentType)}</li>}
</ul>
{removeItem && (
<button className={styles.removeButton} onClick={() => removeItem()}>
<button
className={styles.removeButton}
onClick={() => helpers.setValue(undefined)}
>
&times;
</button>
)}
</div>
)
}

View File

@ -11,6 +11,10 @@ export default function FilesInput(props: InputProps): ReactElement {
const [isLoading, setIsLoading] = useState(false)
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
// hack so the onBlur-triggered validation does not show,
// like when this field is required
helpers.setTouched(false)
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault()
@ -26,14 +30,10 @@ export default function FilesInput(props: InputProps): ReactElement {
}
}
function removeItem() {
helpers.setValue(undefined)
}
return (
<>
{field?.value && field.value[0] && typeof field.value === 'object' ? (
<FileInfo file={field.value[0]} removeItem={removeItem} />
<FileInfo name={props.name} file={field.value[0]} />
) : (
<FileInput
{...props}

View File

@ -9,7 +9,7 @@
}
.box {
composes: box from '../../atoms/Box.module.css';
composes: box from '../atoms/Box.module.css';
width: 100%;
}

View File

@ -0,0 +1,78 @@
import Alert from '../atoms/Alert'
import Button from '../atoms/Button'
import Loader from '../atoms/Loader'
import React, { ReactElement } from 'react'
import styles from './MetadataFeedback.module.css'
import SuccessConfetti from '../atoms/SuccessConfetti'
interface Action {
name: string
onClick?: () => void
to?: string
}
function ActionSuccess({ action }: { action: Action }) {
const { name, onClick, to } = action
return (
<Button
style="primary"
size="small"
onClick={onClick || null}
to={to || null}
className={styles.action}
>
{name}
</Button>
)
}
function ActionError({ setError }: { setError: (error: string) => void }) {
return (
<Button
style="primary"
size="small"
className={styles.action}
onClick={() => setError(undefined)}
>
Try Again
</Button>
)
}
export default function MetadataFeedback({
title,
error,
success,
loading,
successAction,
setError
}: {
title: string
error: string
success: string
loading?: string
successAction: Action
setError: (error: string) => void
}): ReactElement {
return (
<div className={styles.feedback}>
<div className={styles.box}>
<h3>{title}</h3>
{error ? (
<>
<Alert text={error} state="error" />
<ActionError setError={setError} />
</>
) : success ? (
<SuccessConfetti
success={success}
action={<ActionSuccess action={successAction} />}
/>
) : (
<Loader message={loading} />
)}
</div>
</div>
)
}

View File

@ -24,11 +24,10 @@
margin: 0;
}
.author {
.datatoken {
margin-top: calc(var(--spacer) / 8);
margin-bottom: 0;
color: var(--color-secondary);
font-weight: var(--font-weight-bold);
}
.preview [class*='MetaItem-module--metaItem'] h3 {

View File

@ -0,0 +1,121 @@
import React, { FormEvent, ReactElement, useState } from 'react'
import { File as FileMetadata } from '@oceanprotocol/lib/dist/node/ddo/interfaces/File'
import Markdown from '../atoms/Markdown'
import Tags from '../atoms/Tags'
import MetaItem from '../organisms/AssetContent/MetaItem'
import styles from './MetadataPreview.module.css'
import File from '../atoms/File'
import { MetadataPublishForm } from '../../@types/MetaData'
import Button from '../atoms/Button'
import { transformTags } from '../../utils/metadata'
function Description({ description }: { description: string }) {
const [fullDescription, setFullDescription] = useState<boolean>(false)
const textLimit = 500 // string.length
const descriptionDisplay =
fullDescription === true
? description
: `${description.substring(0, textLimit)}${
description.length > textLimit ? `...` : ''
}`
function handleDescriptionToggle(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
setFullDescription(!fullDescription)
}
return (
<div className={styles.description}>
<Markdown text={descriptionDisplay} />
{description.length > textLimit && (
<Button
style="text"
size="small"
onClick={handleDescriptionToggle}
className={styles.toggle}
>
{fullDescription === true ? 'Close' : 'Expand'}
</Button>
)}
</div>
)
}
function MetaFull({ values }: { values: Partial<MetadataPublishForm> }) {
return (
<div className={styles.metaFull}>
{Object.entries(values)
.filter(
([key, value]) =>
!(
key.includes('name') ||
key.includes('description') ||
key.includes('tags') ||
key.includes('files') ||
key.includes('links') ||
key.includes('termsAndConditions') ||
key.includes('dataTokenOptions') ||
value === undefined ||
value === ''
)
)
.map(([key, value]) => (
<MetaItem key={key} title={key} content={value} />
))}
</div>
)
}
function Sample({ url }: { url: string }) {
return (
<Button
href={url}
target="_blank"
rel="noreferrer"
download
style="text"
size="small"
>
Download Sample
</Button>
)
}
export default function MetadataPreview({
values
}: {
values: Partial<MetadataPublishForm>
}): ReactElement {
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.dataTokenOptions?.name && (
<p
className={styles.datatoken}
>{`${values.dataTokenOptions.name}${values.dataTokenOptions.symbol}`}</p>
)}
{values.description && <Description description={values.description} />}
<div className={styles.asset}>
{values.files?.length > 0 && typeof values.files !== 'string' && (
<File
file={values.files[0] as FileMetadata}
className={styles.file}
small
/>
)}
</div>
{typeof values.links !== 'string' && values.links?.length && (
<Sample url={(values.links[0] as FileMetadata).url} />
)}
{values.tags && <Tags items={transformTags(values.tags)} />}
</header>
<MetaFull values={values} />
</div>
)
}

View File

@ -13,7 +13,6 @@ import {
usePricing
} from '@oceanprotocol/react'
import styles from './Compute.module.css'
import Button from '../../atoms/Button'
import Input from '../../atoms/Input'
import Alert from '../../atoms/Alert'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'

View File

@ -28,19 +28,17 @@ export default function Consume({
const [hasPreviousOrder, setHasPreviousOrder] = useState(false)
const [previousOrderId, setPreviousOrderId] = useState<string>()
const { isInPurgatory, price } = useAsset()
const {
dtSymbol,
buyDT,
pricingStepText,
pricingError,
pricingIsLoading
} = usePricing(ddo)
const { buyDT, pricingStepText, pricingError, pricingIsLoading } = usePricing(
ddo
)
const { consumeStepText, consume, consumeError } = useConsume()
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true)
useEffect(() => {
if (!price) return
setIsConsumable(
price.isConsumable !== undefined ? price.isConsumable === 'true' : true
)
@ -110,14 +108,14 @@ export default function Consume({
</Button>
{hasDatatoken && (
<div className={styles.help}>
You own {dtBalance} {dtSymbol} allowing you to use this data set
without paying again.
You own {dtBalance} {ddo.dataTokenInfo.symbol} allowing you to use
this data set without paying again.
</div>
)}
{(!hasDatatoken || !hasPreviousOrder) && (
<div className={styles.help}>
For using this data set, you will buy 1 {dtSymbol} and immediately
spend it back to the publisher and pool.
For using this data set, you will buy 1 {ddo.dataTokenInfo.symbol}{' '}
and immediately spend it back to the publisher and pool.
</div>
)}
</>

View File

@ -0,0 +1,31 @@
import { DDO } from '@oceanprotocol/lib'
import React, { ReactElement } from 'react'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import { transformPublishFormToMetadata } from '../../../../utils/metadata'
import DebugOutput from '../../../atoms/DebugOutput'
export default function Debug({
values,
ddo
}: {
values: Partial<MetadataPublishForm>
ddo: DDO
}): ReactElement {
const newDdo = {
'@context': 'https://w3id.org/did/v1',
service: [
{
index: 0,
type: 'metadata',
attributes: { ...transformPublishFormToMetadata(values, ddo) }
}
]
}
return (
<>
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed DDO Values" output={newDdo} />
</>
)
}

View File

@ -0,0 +1,24 @@
.form {
composes: box from '../../../atoms/Box.module.css';
}
.actions {
margin-left: -2rem;
margin-right: -2rem;
border-top: 1px solid var(--border-color);
padding: calc(var(--spacer) / 2) var(--spacer) 0;
display: flex;
justify-content: center;
}
@media (min-width: 40rem) {
.actions {
padding-top: var(--spacer);
}
}
.actions a,
.actions button {
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,57 @@
import React, { ChangeEvent, ReactElement } from 'react'
import styles from './FormEditMetadata.module.css'
import { Field, Form, FormikContextType, useFormikContext } from 'formik'
import Button from '../../../atoms/Button'
import Input from '../../../atoms/Input'
import { useOcean } from '@oceanprotocol/react'
import { FormFieldProps } from '../../../../@types/Form'
import { MetadataPublishForm } from '../../../../@types/MetaData'
export default function FormEditMetadata({
data,
setShowEdit
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
}): ReactElement {
const { ocean, accountId } = useOcean()
const {
isValid,
validateField,
setFieldValue
}: FormikContextType<Partial<MetadataPublishForm>> = useFormikContext()
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
setFieldValue(field.name, e.target.value)
}
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>
Cancel
</Button>
</footer>
</Form>
)
}

View File

@ -0,0 +1,10 @@
.grid {
composes: grid from '../../AssetContent/index.module.css';
margin-top: var(--spacer);
}
.description {
font-size: var(--font-size-large);
margin-top: -1.5rem;
max-width: 50rem;
}

View File

@ -0,0 +1,139 @@
import { useOcean } from '@oceanprotocol/react'
import { Formik } from 'formik'
import React, { ReactElement, useState } from 'react'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import {
validationSchema,
getInitialValues
} from '../../../../models/FormEditMetadata'
import { useAsset } from '../../../../providers/Asset'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import MetadataPreview from '../../../molecules/MetadataPreview'
import Debug from './Debug'
import Web3Feedback from '../../../molecules/Wallet/Feedback'
import FormEditMetadata from './FormEditMetadata'
import styles from './index.module.css'
import { Logger } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback'
import { graphql, useStaticQuery } from 'gatsby'
const contentQuery = graphql`
query EditMetadataQuery {
content: allFile(filter: { relativePath: { eq: "pages/edit.json" } }) {
edges {
node {
childPagesJson {
description
form {
success
successAction
error
data {
name
placeholder
label
help
type
required
options
rows
}
}
}
}
}
}
}
`
export default function Edit({
setShowEdit
}: {
setShowEdit: (show: boolean) => void
}): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childPagesJson
const { debug } = useUserPreferences()
const { ocean, account } = useOcean()
const { did, metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const hasFeedback = error || success
async function handleSubmit(
values: Partial<MetadataPublishForm>,
resetForm: () => void
) {
try {
const ddo = await ocean.assets.editMetadata(
did,
{ title: values.name, description: values.description },
account
)
// Edit failed
if (!ddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
}
// Edit succeeded
setSuccess(content.form.success)
resetForm()
} catch (error) {
Logger.error(error.message)
setError(error.message)
}
}
return (
<Formik
initialValues={getInitialValues(metadata)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
// kick off editing
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
isSubmitting || hasFeedback ? (
<MetadataFeedback
title="Updating Data Set"
error={error}
success={success}
setError={setError}
successAction={{
name: content.form.successAction,
onClick: async () => {
await refreshDdo()
setShowEdit(false)
}
}}
/>
) : (
<>
<p className={styles.description}>{content.description}</p>
<article className={styles.grid}>
<FormEditMetadata
data={content.form.data}
setShowEdit={setShowEdit}
/>
<aside>
<MetadataPreview values={values} />
<Web3Feedback />
</aside>
{debug === true && <Debug values={values} ddo={ddo} />}
</article>
</>
)
}
</Formik>
)
}

View File

@ -12,6 +12,7 @@ import Alert from '../../../../atoms/Alert'
import TokenBalance from '../../../../../@types/TokenBalance'
import { useUserPreferences } from '../../../../../providers/UserPreferences'
import Output from './Output'
import DebugOutput from '../../../../atoms/DebugOutput'
const contentQuery = graphql`
query PoolAddQuery {
@ -201,11 +202,7 @@ export default function Add({
action={submitForm}
txId={txId}
/>
{debug && (
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
)}
{debug && <DebugOutput title="Collected values" output={values} />}
</>
)}
</Formik>

View File

@ -1,6 +1,6 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useOcean, useMetadata, usePricing } from '@oceanprotocol/react'
import { DDO, Logger } from '@oceanprotocol/lib'
import { useOcean } from '@oceanprotocol/react'
import { Logger } from '@oceanprotocol/lib'
import styles from './index.module.css'
import stylesActions from './Actions.module.css'
import PriceUnit from '../../../atoms/Price/PriceUnit'
@ -37,15 +37,20 @@ const contentQuery = graphql`
}
`
export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
export default function Pool(): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool
const { ocean, accountId, networkId, config } = useOcean()
const { owner } = useMetadata(ddo)
const { dtSymbol } = usePricing(ddo)
const { isInPurgatory, price, refreshInterval, refreshPrice } = useAsset()
const {
isInPurgatory,
ddo,
owner,
price,
refreshInterval,
refreshPrice
} = useAsset()
const dtSymbol = ddo?.dataTokenInfo.symbol
const [poolTokens, setPoolTokens] = useState<string>()
const [totalPoolTokens, setTotalPoolTokens] = useState<string>()

View File

@ -1,14 +1,13 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useOcean } from '@oceanprotocol/react'
import { DDO } from '@oceanprotocol/lib'
import FormTrade from './FormTrade'
import TokenBalance from '../../../../@types/TokenBalance'
import { useAsset } from '../../../../providers/Asset'
export default function Trade({ ddo }: { ddo: DDO }): ReactElement {
export default function Trade(): ReactElement {
const { ocean, balance, accountId } = useOcean()
const [tokenBalance, setTokenBalance] = useState<TokenBalance>()
const { price } = useAsset()
const { price, ddo } = useAsset()
const [maxDt, setMaxDt] = useState(0)
const [maxOcean, setMaxOcean] = useState(0)

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useState, useEffect } from 'react'
import styles from './index.module.css'
import Compute from './Compute'
import Consume from './Consume'
import { DDO, Logger } from '@oceanprotocol/lib'
import { Logger } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs'
import { useOcean } from '@oceanprotocol/react'
import compareAsBN from '../../../utils/compareAsBN'
@ -10,14 +10,13 @@ import Pool from './Pool'
import Trade from './Trade'
import { useAsset } from '../../../providers/Asset'
export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
export default function AssetActions(): ReactElement {
const { ocean, balance, accountId } = useOcean()
const { price } = useAsset()
const { price, ddo, metadata } = useAsset()
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [dtBalance, setDtBalance] = useState<string>()
const isCompute = Boolean(ddo.findServiceByType('compute'))
const { attributes } = ddo.findServiceByType('metadata')
const isCompute = Boolean(ddo?.findServiceByType('compute'))
// Get and set user DT balance
useEffect(() => {
@ -60,7 +59,7 @@ export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
ddo={ddo}
dtBalance={dtBalance}
isBalanceSufficient={isBalanceSufficient}
file={attributes.main.files[0]}
file={metadata?.main.files[0]}
/>
)
@ -72,17 +71,17 @@ export default function AssetActions({ ddo }: { ddo: DDO }): ReactElement {
]
// Check from metadata, cause that is available earlier
const hasPool = ddo.price?.type === 'pool'
const hasPool = ddo?.price?.type === 'pool'
hasPool &&
tabs.push(
{
title: 'Pool',
content: <Pool ddo={ddo} />
content: <Pool />
},
{
title: 'Trade',
content: <Trade ddo={ddo} />
content: <Trade />
}
)

View File

@ -0,0 +1,41 @@
.title {
composes: title from './MetaItem.module.css';
margin-top: var(--spacer);
margin-bottom: calc(var(--spacer) / 2);
}
.history {
font-size: var(--font-size-small);
padding-left: 1.25rem;
}
.item {
position: relative;
display: block;
margin-bottom: calc(var(--spacer) / 4);
color: var(--color-secondary);
}
.item::before {
content: '▪';
position: absolute;
top: -1px;
left: -1.25rem;
color: var(--color-secondary);
user-select: none;
}
.item::after {
content: '';
position: absolute;
left: -1rem;
top: 62%;
display: block;
width: 1px;
height: 110%;
background-color: var(--color-secondary);
}
.item:last-child::after {
display: none;
}

View File

@ -0,0 +1,59 @@
import { useOcean } from '@oceanprotocol/react'
import React, { ReactElement, useEffect, useState } from 'react'
import { useAsset } from '../../../providers/Asset'
import EtherscanLink from '../../atoms/EtherscanLink'
import Time from '../../atoms/Time'
import styles from './EditHistory.module.css'
interface Receipt {
hash: string
timestamp: string
}
// TODO: fetch for real
const fakeReceipts = [
{
hash: '0xxxxxxxxx',
timestamp: '1607460269'
},
{
hash: '0xxxxxxxxx',
timestamp: '1606460159'
},
{
hash: '0xxxxxxxxx',
timestamp: '1506460159'
}
]
export default function EditHistory(): ReactElement {
const { networkId } = useOcean()
const { ddo } = useAsset()
const [receipts, setReceipts] = useState<Receipt[]>()
useEffect(() => {
setReceipts(fakeReceipts)
}, [])
return (
<>
<h3 className={styles.title}>Metadata History</h3>
<ul className={styles.history}>
{receipts?.map((receipt) => (
<li key={receipt.hash} className={styles.item}>
<EtherscanLink networkId={networkId} path={`/tx/${receipt.hash}`}>
edited <Time date={receipt.timestamp} relative isUnix />
</EtherscanLink>
</li>
))}
<li className={styles.item}>
{/* TODO: get this initial metadata creation tx somehow */}
<EtherscanLink networkId={networkId} path="/tx/xxx">
published <Time date={ddo.created} relative />
</EtherscanLink>
</li>
</ul>
</>
)
}

View File

@ -1,6 +1,5 @@
.metaFull {
margin-top: var(--spacer);
font-size: var(--font-size-small);
display: grid;
gap: var(--spacer);
grid-template-columns: 1fr 1fr;
@ -14,3 +13,8 @@
word-break: break-all;
padding: 0;
}
/* debug output */
.metaFull + div {
margin-top: var(--spacer);
}

View File

@ -2,21 +2,11 @@ import React, { ReactElement } from 'react'
import Time from '../../atoms/Time'
import MetaItem from './MetaItem'
import styles from './MetaFull.module.css'
import { MetadataMarket } from '../../../@types/MetaData'
import { DDO } from '@oceanprotocol/lib'
import Publisher from '../../atoms/Publisher'
import { useAsset } from '../../../providers/Asset'
export default function MetaFull({
ddo,
metadata,
isInPurgatory
}: {
ddo: DDO
metadata: MetadataMarket
isInPurgatory: boolean
}): ReactElement {
const { id, publicKey } = ddo
const { dateCreated, datePublished } = metadata.main
export default function MetaFull(): ReactElement {
const { ddo, metadata, isInPurgatory } = useAsset()
return (
<div className={styles.metaFull}>
@ -25,19 +15,19 @@ export default function MetaFull({
)}
<MetaItem
title="Owner"
content={<Publisher account={publicKey[0].owner} />}
content={<Publisher account={ddo?.publicKey[0].owner} />}
/>
{/* <MetaItem
title="Data Created"
content={<Time date={metadata?.main.dateCreated} />}
/> */}
{metadata?.additionalInformation?.categories && (
<MetaItem
title="Category"
content={metadata?.additionalInformation?.categories[0]}
/>
{/* TODO: remove those 2 date items here when EditHistory component is ready */}
<MetaItem title="Published" content={<Time date={ddo?.created} />} />
{ddo?.created !== ddo?.updated && (
<MetaItem title="Updated" content={<Time date={ddo?.updated} />} />
)}
<MetaItem title="Data Created" content={<Time date={dateCreated} />} />
<MetaItem title="Published" content={<Time date={datePublished} />} />
<MetaItem title="DID" content={<code>{id}</code>} />
<MetaItem title="DID" content={<code>{ddo?.id}</code>} />
</div>
)
}

View File

@ -3,6 +3,10 @@
}
.title {
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
font-size: var(--font-size-small);
margin-bottom: calc(var(--spacer) / 4);
color: var(--color-secondary);
text-transform: uppercase;
}

View File

@ -0,0 +1,13 @@
.meta {
margin-bottom: calc(var(--spacer) / 1.5);
color: var(--color-secondary);
}
.meta p {
margin-bottom: 0;
}
.date {
font-size: var(--font-size-mini);
margin-top: calc(var(--spacer) / 2);
}

View File

@ -0,0 +1,34 @@
import { useOcean } from '@oceanprotocol/react'
import React, { ReactElement } from 'react'
import { useAsset } from '../../../providers/Asset'
import EtherscanLink from '../../atoms/EtherscanLink'
import Publisher from '../../atoms/Publisher'
import Time from '../../atoms/Time'
import styles from './MetaMain.module.css'
export default function MetaMain(): ReactElement {
const { ddo, owner } = useAsset()
const { networkId } = useOcean()
return (
<aside className={styles.meta}>
<p>
<EtherscanLink networkId={networkId} path={`token/${ddo?.dataToken}`}>
{`${ddo?.dataTokenInfo.name}${ddo?.dataTokenInfo.symbol}`}
</EtherscanLink>
</p>
<div>
Published By <Publisher account={owner} />
</div>
<p className={styles.date}>
<Time date={ddo?.created} relative />
{ddo?.created !== ddo?.updated && (
<>
{' — '}
updated <Time date={ddo?.updated} relative />
</>
)}
</p>
</aside>
)
}

View File

@ -14,7 +14,3 @@
.samples {
margin-top: var(--spacer);
}
.date {
color: var(--color-secondary);
}

View File

@ -1,25 +1,13 @@
import React, { ReactElement } from 'react'
import MetaItem from './MetaItem'
import styles from './MetaSecondary.module.css'
import { MetadataMarket } from '../../../@types/MetaData'
import Tags from '../../atoms/Tags'
import Button from '../../atoms/Button'
import Time from '../../atoms/Time'
import { useAsset } from '../../../providers/Asset'
export default function MetaSecondary({
metadata
}: {
metadata: MetadataMarket
}): ReactElement {
return (
<aside className={styles.metaSecondary}>
{metadata?.additionalInformation?.links?.length > 0 && (
<div className={styles.samples}>
<MetaItem
title="Sample Data"
content={
const SampleButton = ({ url }: { url: string }) => (
<Button
href={metadata?.additionalInformation?.links[0].url}
href={url}
target="_blank"
rel="noreferrer"
download
@ -28,6 +16,21 @@ export default function MetaSecondary({
>
Download Sample
</Button>
)
export default function MetaSecondary(): ReactElement {
const { metadata } = useAsset()
return (
<aside className={styles.metaSecondary}>
{metadata?.additionalInformation?.links?.length > 0 && (
<div className={styles.samples}>
<MetaItem
title="Sample Data"
content={
<SampleButton
url={metadata?.additionalInformation?.links[0].url}
/>
}
/>
</div>
@ -36,10 +39,6 @@ export default function MetaSecondary({
{metadata?.additionalInformation?.tags?.length > 0 && (
<Tags items={metadata?.additionalInformation?.tags} />
)}
<p className={styles.date}>
Published <Time date={metadata?.main.datePublished} relative />
</p>
</aside>
)
}

View File

@ -25,8 +25,6 @@ export default function Dynamic({
content: any
}): ReactElement {
const { account, balance, networkId, refreshBalance } = useOcean()
const { dtSymbol, dtName } = usePricing(ddo)
const [firstPrice, setFirstPrice] = useState<string>()
// Connect with form
@ -117,7 +115,10 @@ export default function Dynamic({
/>
<Coin
name="dtAmount"
datatokenOptions={{ symbol: dtSymbol, name: dtName }}
datatokenOptions={{
symbol: ddo.dataTokenInfo.symbol,
name: ddo.dataTokenInfo.name
}}
weight={`${Number(weightOnDataToken) * 10}%`}
readOnly
/>

View File

@ -25,20 +25,20 @@
}
}
.meta {
margin-bottom: var(--spacer);
color: var(--color-secondary);
}
.meta p {
margin-bottom: 0;
}
.datatoken a {
color: var(--color-secondary);
}
.buttonGroup {
.ownerActions {
text-align: center;
margin-top: var(--spacer);
margin-bottom: var(--spacer);
margin-bottom: calc(var(--spacer) * 1.5);
margin-left: -2rem;
margin-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer);
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
}
.ownerActions a,
.ownerActions button {
color: var(--color-secondary);
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
}

View File

@ -1,24 +1,23 @@
import { MetadataMarket } from '../../../@types/MetaData'
import React, { ReactElement, useEffect, useState } from 'react'
import { graphql, Link, useStaticQuery } from 'gatsby'
import { graphql, useStaticQuery } from 'gatsby'
import Markdown from '../../atoms/Markdown'
import MetaFull from './MetaFull'
import MetaSecondary from './MetaSecondary'
import styles from './index.module.css'
import AssetActions from '../AssetActions'
import { DDO } from '@oceanprotocol/lib'
import { useUserPreferences } from '../../../providers/UserPreferences'
import Pricing from './Pricing'
import { useOcean, usePricing } from '@oceanprotocol/react'
import EtherscanLink from '../../atoms/EtherscanLink'
import { useOcean } from '@oceanprotocol/react'
import Bookmark from './Bookmark'
import Publisher from '../../atoms/Publisher'
import { useAsset } from '../../../providers/Asset'
import Alert from '../../atoms/Alert'
import Button from '../../atoms/Button'
import Edit from '../AssetActions/Edit'
import DebugOutput from '../../atoms/DebugOutput'
import MetaMain from './MetaMain'
// import EditHistory from './EditHistory'
export interface AssetContentProps {
metadata: MetadataMarket
ddo: DDO
path?: string
}
@ -39,54 +38,37 @@ const contentQuery = graphql`
}
`
export default function AssetContent({
metadata,
ddo
}: AssetContentProps): ReactElement {
export default function AssetContent(props: AssetContentProps): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.purgatory.edges[0].node.childContentJson.asset
const { debug } = useUserPreferences()
const { accountId, networkId } = useOcean()
const { accountId } = useOcean()
const { owner, isInPurgatory, purgatoryData } = useAsset()
const { dtSymbol, dtName } = usePricing(ddo)
const [showPricing, setShowPricing] = useState(false)
const { price } = useAsset()
const [showEdit, setShowEdit] = useState<boolean>()
const { ddo, price, metadata } = useAsset()
const isOwner = accountId === owner
useEffect(() => {
setShowPricing(accountId === owner && price.isConsumable === '')
}, [accountId, owner, price])
if (!price) return
setShowPricing(isOwner && price.address === '')
}, [isOwner, price])
return (
function handleEditButton() {
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setShowEdit(true)
}
return showEdit ? (
<Edit setShowEdit={setShowEdit} />
) : (
<article className={styles.grid}>
<div>
{showPricing && <Pricing ddo={ddo} />}
<div className={styles.content}>
{metadata?.additionalInformation?.categories?.length && (
<p>
<Link
to={`/search?categories=${metadata?.additionalInformation?.categories[0]}`}
>
{metadata?.additionalInformation?.categories[0]}
</Link>
</p>
)}
<aside className={styles.meta}>
<p className={styles.datatoken}>
<EtherscanLink
networkId={networkId}
path={`token/${ddo.dataToken}`}
>
{dtName ? (
`${dtName}${dtSymbol}`
) : (
<code>{ddo.dataToken}</code>
)}
</EtherscanLink>
</p>
Published By <Publisher account={owner} />
</aside>
<MetaMain />
<Bookmark did={ddo.id} />
{isInPurgatory ? (
<Alert
@ -102,28 +84,26 @@ export default function AssetContent({
text={metadata?.additionalInformation?.description || ''}
/>
<MetaSecondary metadata={metadata} />
<MetaSecondary />
{isOwner && (
<div className={styles.ownerActions}>
<Button style="text" size="small" onClick={handleEditButton}>
Edit Metadata
</Button>
</div>
)}
</>
)}
<MetaFull
ddo={ddo}
metadata={metadata}
isInPurgatory={isInPurgatory}
/>
{debug === true && (
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
)}
<Bookmark did={ddo.id} />
<MetaFull />
{/* <EditHistory /> */}
{debug === true && <DebugOutput title="DDO" output={ddo} />}
</div>
</div>
<div className={styles.actions}>
<AssetActions ddo={ddo} />
<AssetActions />
</div>
</article>
)

View File

@ -1,16 +1,8 @@
import React, { ReactElement } from 'react'
import { MetadataPublishForm } from '../../../@types/MetaData'
import DebugOutput from '../../atoms/DebugOutput'
import styles from './index.module.css'
import { transformPublishFormToMetadata } from './utils'
const Output = ({ title, output }: { title: string; output: any }) => (
<div>
<h5>{title}</h5>
<pre>
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
</div>
)
import { transformPublishFormToMetadata } from '../../../utils/metadata'
export default function Debug({
values
@ -38,8 +30,8 @@ export default function Debug({
return (
<div className={styles.grid}>
<Output title="Collected Form Values" output={values} />
<Output title="Transformed DDO Values" output={ddo} />
<DebugOutput title="Collected Form Values" output={values} />
<DebugOutput title="Transformed DDO Values" output={ddo} />
</div>
)
}

View File

@ -1,57 +0,0 @@
import Alert from '../../atoms/Alert'
import Button from '../../atoms/Button'
import Loader from '../../atoms/Loader'
import React, { ReactElement } from 'react'
import styles from './Feedback.module.css'
import SuccessConfetti from '../../atoms/SuccessConfetti'
import { DDO } from '@oceanprotocol/lib'
export default function Feedback({
error,
success,
ddo,
publishStepText,
setError
}: {
error: string
success: string
ddo: DDO
publishStepText: string
setError: (error: string) => void
}): ReactElement {
const SuccessAction = () => (
<Button
style="primary"
size="small"
to={`/asset/${ddo?.id}`}
className={styles.action}
>
Go to data set
</Button>
)
return (
<div className={styles.feedback}>
<div className={styles.box}>
<h3>Publishing Data Set</h3>
{error ? (
<>
<Alert text={error} state="error" />
<Button
style="primary"
size="small"
className={styles.action}
onClick={() => setError(undefined)}
>
Try Again
</Button>
</>
) : success ? (
<SuccessConfetti success={success} action={<SuccessAction />} />
) : (
<Loader message={publishStepText} />
)}
</div>
</div>
)
}

View File

@ -1,10 +1,11 @@
import React, { ReactElement, useEffect, FormEvent } from 'react'
import React, { ReactElement, useEffect, FormEvent, ChangeEvent } from 'react'
import styles from './FormPublish.module.css'
import { useOcean } from '@oceanprotocol/react'
import { useFormikContext, Field, Form } from 'formik'
import { useFormikContext, Field, Form, FormikContextType } from 'formik'
import Input from '../../atoms/Input'
import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishForm } from '../../../@types/MetaData'
export default function FormPublish({
content
@ -19,8 +20,10 @@ export default function FormPublish({
setErrors,
setTouched,
resetForm,
initialValues
} = useFormikContext()
initialValues,
validateField,
setFieldValue
}: FormikContextType<MetadataPublishForm> = useFormikContext()
// reset form validation on every mount
useEffect(() => {
@ -30,6 +33,16 @@ export default function FormPublish({
// setSubmitting(false)
}, [setErrors, setTouched])
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
e: ChangeEvent<HTMLInputElement>,
field: FormFieldProps
) {
validateField(field.name)
setFieldValue(field.name, e.target.value)
}
const resetFormAndClearStorage = (e: FormEvent<Element>) => {
e.preventDefault()
resetForm({ values: initialValues, status: 'empty' })
@ -43,7 +56,14 @@ export default function FormPublish({
onChange={() => status === 'empty' && setStatus(null)}
>
{content.data.map((field: FormFieldProps) => (
<Field key={field.name} {...field} component={Input} />
<Field
key={field.name}
{...field}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)
}
/>
))}
<footer className={styles.actions}>

View File

@ -1,103 +0,0 @@
import React, { FormEvent, ReactElement, useState } from 'react'
import { File as FileMetadata } from '@oceanprotocol/lib/dist/node/ddo/interfaces/File'
import Markdown from '../../atoms/Markdown'
import Tags from '../../atoms/Tags'
import MetaItem from '../../organisms/AssetContent/MetaItem'
import styles from './Preview.module.css'
import File from '../../atoms/File'
import { MetadataPublishForm } from '../../../@types/MetaData'
import Button from '../../atoms/Button'
import { transformTags } from './utils'
export default function Preview({
values
}: {
values: Partial<MetadataPublishForm>
}): ReactElement {
const [fullDescription, setFullDescription] = useState<boolean>(false)
const textLimit = 500 // string.length
const description =
fullDescription === true
? values.description
: `${values.description.substring(0, textLimit)}${
values.description.length > textLimit ? `...` : ''
}`
function handleDescriptionToggle(e: FormEvent<HTMLButtonElement>) {
e.preventDefault()
setFullDescription(!fullDescription)
}
return (
<div className={styles.preview}>
<h2 className={styles.previewTitle}>Preview</h2>
<header>
{values.name && <h3 className={styles.title}>{values.name}</h3>}
{values.author && <p className={styles.author}>{values.author}</p>}
{values.description && (
<div className={styles.description}>
<Markdown text={description} />
{values.description.length > textLimit && (
<Button
style="text"
size="small"
onClick={handleDescriptionToggle}
className={styles.toggle}
>
{fullDescription === true ? 'Close' : 'Expand'}
</Button>
)}
</div>
)}
<div className={styles.asset}>
{values.files?.length > 0 && typeof values.files !== 'string' && (
<File
file={values.files[0] as FileMetadata}
className={styles.file}
small
/>
)}
</div>
{typeof values.links !== 'string' && values.links?.length && (
<Button
href={(values.links[0] as FileMetadata).url}
target="_blank"
rel="noreferrer"
download
style="text"
size="small"
>
Download Sample
</Button>
)}
{values.tags && <Tags items={transformTags(values.tags)} />}
</header>
<div className={styles.metaFull}>
{Object.entries(values)
.filter(
([key, value]) =>
!(
key.includes('author') ||
key.includes('name') ||
key.includes('description') ||
key.includes('tags') ||
key.includes('files') ||
key.includes('links') ||
key.includes('termsAndConditions') ||
key.includes('dataTokenOptions') ||
value === undefined ||
value === ''
)
)
.map(([key, value]) => (
<MetaItem key={key} title={key} content={value} />
))}
</div>
</div>
)
}

View File

@ -6,15 +6,15 @@ import FormPublish from './FormPublish'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from '../../../models/FormPublish'
import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview'
import { transformPublishFormToMetadata } from '../../../utils/metadata'
import MetadataPreview from '../../molecules/MetadataPreview'
import { MetadataPublishForm } from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { DDO, Logger, Metadata } from '@oceanprotocol/lib'
import { Logger, Metadata } from '@oceanprotocol/lib'
import { Persist } from '../../atoms/FormikPersist'
import Debug from './Debug'
import Feedback from './Feedback'
import Alert from '../../atoms/Alert'
import MetadataFeedback from '../../molecules/MetadataFeedback'
const formName = 'ocean-publish-form'
@ -28,7 +28,7 @@ export default function PublishPage({
const { isInPurgatory, purgatoryData } = useOcean()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const [ddo, setDdo] = useState<DDO>()
const [did, setDid] = useState<string>()
const hasFeedback = isLoading || error || success
@ -61,7 +61,7 @@ export default function PublishPage({
}
// Publish succeeded
setDdo(ddo)
setDid(ddo.id)
setSuccess(
'🎉 Successfully published. 🎉 Now create a price on your data set.'
)
@ -77,12 +77,11 @@ export default function PublishPage({
initialValues={initialValues}
initialStatus="empty"
validationSchema={validationSchema}
onSubmit={async (values, { setSubmitting, resetForm }) => {
onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
// kick off publishing
await handleSubmit(values, resetForm)
setSubmitting(false)
}}
>
{({ values }) => (
@ -90,12 +89,16 @@ export default function PublishPage({
<Persist name={formName} ignoreFields={['isSubmitting']} />
{hasFeedback ? (
<Feedback
<MetadataFeedback
title="Publishing Data Set"
error={error}
success={success}
publishStepText={publishStepText}
ddo={ddo}
loading={publishStepText}
setError={setError}
successAction={{
name: 'Go to data set →',
to: `/asset/${did}`
}}
/>
) : (
<>
@ -109,7 +112,7 @@ export default function PublishPage({
<aside>
<div className={styles.sticky}>
<Preview values={values} />
<MetadataPreview values={values} />
<Web3Feedback />
</div>
</aside>

View File

@ -2,7 +2,6 @@ import React, { useState, useEffect, ReactElement } from 'react'
import { Router } from '@reach/router'
import AssetContent from '../organisms/AssetContent'
import Page from './Page'
import { MetadataMarket } from '../../@types/MetaData'
import Alert from '../atoms/Alert'
import Loader from '../atoms/Loader'
import { useAsset } from '../../providers/Asset'
@ -12,37 +11,29 @@ export default function PageTemplateAssetDetails({
}: {
uri: string
}): ReactElement {
const { isInPurgatory } = useAsset()
const [metadata, setMetadata] = useState<MetadataMarket>()
const [title, setTitle] = useState<string>()
const { ddo, error } = useAsset()
const { ddo, title, error, isInPurgatory } = useAsset()
const [pageTitle, setPageTitle] = useState<string>()
useEffect(() => {
if (!ddo || error) {
setTitle('Could not retrieve asset')
setPageTitle('Could not retrieve asset')
return
}
const { attributes } = ddo.findServiceByType('metadata')
setTitle(isInPurgatory ? '' : attributes.main.name)
setMetadata((attributes as unknown) as MetadataMarket)
}, [ddo, error, isInPurgatory])
setPageTitle(isInPurgatory ? '' : title)
}, [ddo, error, isInPurgatory, title])
return ddo && metadata ? (
return ddo ? (
<>
<Page title={title} uri={uri}>
<Page title={pageTitle} uri={uri}>
<Router basepath="/asset">
<AssetContent
ddo={ddo}
metadata={metadata as MetadataMarket}
path=":did"
/>
<AssetContent path=":did" />
</Router>
</Page>
</>
) : error ? (
<Page title={title} noPageHeader uri={uri}>
<Alert title={title} text={error} state="error" />
<Page title={pageTitle} noPageHeader uri={uri}>
<Alert title={pageTitle} text={error} state="error" />
</Page>
) : (
<Page title={undefined} uri={uri}>

View File

@ -0,0 +1,20 @@
import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData'
import * as Yup from 'yup'
export const validationSchema = Yup.object().shape<
Partial<MetadataPublishForm>
>({
name: Yup.string()
.min(4, (param) => `Title must be at least ${param.min} characters`)
.required('Required'),
description: Yup.string().required('Required').min(10)
})
export function getInitialValues(
metadata: MetadataMarket
): Partial<MetadataPublishForm> {
return {
name: metadata.main.name,
description: metadata.additionalInformation.description
}
}

View File

@ -4,7 +4,9 @@ import * as Yup from 'yup'
export const validationSchema = Yup.object().shape<MetadataPublishForm>({
// ---- required fields ----
name: Yup.string().required('Required'),
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({
@ -13,7 +15,7 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
})
.required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(),
description: Yup.string().required('Required'),
description: Yup.string().min(10).required('Required'),
access: Yup.string()
.matches(/Compute|Download/g)
.required('Required'),

View File

@ -7,25 +7,27 @@ import React, {
useCallback,
ReactNode
} from 'react'
import { Logger, DDO, Metadata, BestPrice } from '@oceanprotocol/lib'
import { Logger, DDO, BestPrice } from '@oceanprotocol/lib'
import { PurgatoryData } from '@oceanprotocol/lib/dist/node/ddo/interfaces/PurgatoryData'
import { getDataTokenPrice, useOcean } from '@oceanprotocol/react'
import getAssetPurgatoryData from '../utils/purgatory'
import { ConfigHelperConfig } from '@oceanprotocol/lib/dist/node/utils/ConfigHelper'
import axios from 'axios'
import axios, { CancelToken } from 'axios'
import { retrieveDDO } from '../utils/aquarius'
import { MetadataMarket } from '../@types/MetaData'
interface AssetProviderValue {
isInPurgatory: boolean
purgatoryData: PurgatoryData
ddo: DDO | undefined
did: string | undefined
metadata: Metadata | undefined
metadata: MetadataMarket | undefined
title: string | undefined
owner: string | undefined
price: BestPrice | undefined
error?: string
refreshInterval: number
refreshDdo: (token?: CancelToken) => Promise<void>
refreshPrice: () => Promise<void>
}
@ -45,7 +47,7 @@ function AssetProvider({
const [purgatoryData, setPurgatoryData] = useState<PurgatoryData>()
const [ddo, setDDO] = useState<DDO>()
const [did, setDID] = useState<string>()
const [metadata, setMetadata] = useState<Metadata>()
const [metadata, setMetadata] = useState<MetadataMarket>()
const [title, setTitle] = useState<string>()
const [price, setPrice] = useState<BestPrice>()
const [owner, setOwner] = useState<string>()
@ -69,6 +71,29 @@ function AssetProvider({
Logger.log(`Refreshed asset price: ${newPrice?.value}`)
}, [ocean, config, ddo, networkId, status])
const fetchDdo = async (token?: CancelToken) => {
Logger.log('Init asset, get ddo')
const ddo = await retrieveDDO(
asset as string,
config.metadataCacheUri,
token
)
if (!ddo) {
setError(
`The DDO for ${asset} was not found in MetadataCache. If you just published a new data set, wait some seconds and refresh this page.`
)
} else {
setError(undefined)
}
return ddo
}
const refreshDdo = async (token?: CancelToken) => {
const ddo = await fetchDdo(token)
Logger.debug('DDO', ddo)
setDDO(ddo)
}
//
// Get and set DDO based on passed DDO or DID
//
@ -79,28 +104,14 @@ function AssetProvider({
let isMounted = true
Logger.log('Init asset, get ddo')
async function init(): Promise<void> {
const ddo = await retrieveDDO(
asset as string,
config.metadataCacheUri,
source.token
)
if (!ddo) {
setError(
`The DDO for ${asset} was not found in MetadataCache. If you just published a new data set, wait some seconds and refresh this page.`
)
} else {
setError(undefined)
}
async function init() {
const ddo = await fetchDdo(source.token)
if (!isMounted) return
Logger.debug('DDO', ddo)
setDDO(ddo)
setDID(asset as string)
}
init()
return () => {
isMounted = false
source.cancel()
@ -130,10 +141,10 @@ function AssetProvider({
if (result?.did !== undefined) {
setIsInPurgatory(true)
setPurgatoryData(result)
} else {
setIsInPurgatory(false)
return
}
setPurgatoryData(result)
setIsInPurgatory(false)
} catch (error) {
Logger.error(error)
}
@ -147,7 +158,7 @@ function AssetProvider({
// Set price & metadata from DDO first
setPrice(ddo.price)
const { attributes } = ddo.findServiceByType('metadata')
setMetadata(attributes)
setMetadata((attributes as unknown) as MetadataMarket)
setTitle(attributes?.main.name)
setOwner(ddo.publicKey[0].owner)
setIsInPurgatory(ddo.isInPurgatory === 'true')
@ -177,6 +188,7 @@ function AssetProvider({
isInPurgatory,
purgatoryData,
refreshInterval,
refreshDdo,
refreshPrice
} as AssetProviderValue
}

View File

@ -1,7 +1,8 @@
import { MetadataMarket, MetadataPublishForm } from '../../../@types/MetaData'
import { toStringNoMS } from '../../../utils'
import AssetModel from '../../../models/Asset'
import { MetadataMarket, MetadataPublishForm } from '../@types/MetaData'
import { toStringNoMS } from '.'
import AssetModel from '../models/Asset'
import slugify from '@sindresorhus/slugify'
import { DDO } from '@oceanprotocol/lib'
export function transformTags(value: string): string[] {
const originalTags = value?.split(',')
@ -10,11 +11,7 @@ export function transformTags(value: string): string[] {
}
export function transformPublishFormToMetadata(
data: Partial<MetadataPublishForm>
): MetadataMarket {
const currentTime = toStringNoMS(new Date())
const {
{
name,
author,
description,
@ -22,15 +19,17 @@ export function transformPublishFormToMetadata(
links,
termsAndConditions,
files
} = data
}: Partial<MetadataPublishForm>,
ddo?: DDO
): MetadataMarket {
const currentTime = toStringNoMS(new Date())
const metadata: MetadataMarket = {
main: {
...AssetModel.main,
name,
author,
dateCreated: currentTime,
datePublished: currentTime,
dateCreated: ddo ? ddo.created : currentTime,
files: typeof files !== 'string' && files,
license: 'https://market.oceanprotocol.com/terms'
},

View File

@ -1,6 +1,6 @@
import React from 'react'
import { render } from '@testing-library/react'
import { transformPublishFormToMetadata } from '../../../src/components/pages/Publish/utils'
import { transformPublishFormToMetadata } from '../../../src/utils/metadata'
import {
MetadataMarket,
MetadataPublishForm