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:
parent
c57731cd0b
commit
960c5b3234
25
content/pages/edit.json
Normal file
25
content/pages/edit.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
18
src/components/atoms/DebugOutput.tsx
Normal file
18
src/components/atoms/DebugOutput.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
<button
|
||||
className={styles.removeButton}
|
||||
onClick={() => helpers.setValue(undefined)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.box {
|
||||
composes: box from '../../atoms/Box.module.css';
|
||||
composes: box from '../atoms/Box.module.css';
|
||||
width: 100%;
|
||||
}
|
||||
|
78
src/components/molecules/MetadataFeedback.tsx
Normal file
78
src/components/molecules/MetadataFeedback.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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 {
|
121
src/components/molecules/MetadataPreview.tsx
Normal file
121
src/components/molecules/MetadataPreview.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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'
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
31
src/components/organisms/AssetActions/Edit/Debug.tsx
Normal file
31
src/components/organisms/AssetActions/Edit/Debug.tsx
Normal 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} />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
10
src/components/organisms/AssetActions/Edit/index.module.css
Normal file
10
src/components/organisms/AssetActions/Edit/index.module.css
Normal 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;
|
||||
}
|
139
src/components/organisms/AssetActions/Edit/index.tsx
Normal file
139
src/components/organisms/AssetActions/Edit/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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>()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 />
|
||||
}
|
||||
)
|
||||
|
||||
|
41
src/components/organisms/AssetContent/EditHistory.module.css
Normal file
41
src/components/organisms/AssetContent/EditHistory.module.css
Normal 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;
|
||||
}
|
59
src/components/organisms/AssetContent/EditHistory.tsx
Normal file
59
src/components/organisms/AssetContent/EditHistory.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
13
src/components/organisms/AssetContent/MetaMain.module.css
Normal file
13
src/components/organisms/AssetContent/MetaMain.module.css
Normal 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);
|
||||
}
|
34
src/components/organisms/AssetContent/MetaMain.tsx
Normal file
34
src/components/organisms/AssetContent/MetaMain.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -14,7 +14,3 @@
|
||||
.samples {
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
||||
.date {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
@ -1,16 +1,26 @@
|
||||
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'
|
||||
|
||||
const SampleButton = ({ url }: { url: string }) => (
|
||||
<Button
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download
|
||||
style="text"
|
||||
size="small"
|
||||
>
|
||||
Download Sample
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default function MetaSecondary(): ReactElement {
|
||||
const { metadata } = useAsset()
|
||||
|
||||
export default function MetaSecondary({
|
||||
metadata
|
||||
}: {
|
||||
metadata: MetadataMarket
|
||||
}): ReactElement {
|
||||
return (
|
||||
<aside className={styles.metaSecondary}>
|
||||
{metadata?.additionalInformation?.links?.length > 0 && (
|
||||
@ -18,16 +28,9 @@ export default function MetaSecondary({
|
||||
<MetaItem
|
||||
title="Sample Data"
|
||||
content={
|
||||
<Button
|
||||
href={metadata?.additionalInformation?.links[0].url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
download
|
||||
style="text"
|
||||
size="small"
|
||||
>
|
||||
Download Sample
|
||||
</Button>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
/>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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}>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
20
src/models/FormEditMetadata.ts
Normal file
20
src/models/FormEditMetadata.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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'),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
},
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user