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

Merge pull request #93 from oceanprotocol/feature/publish-feedback

Publish flow changes
This commit is contained in:
Matthias Kretschmann 2020-10-05 15:54:18 +02:00 committed by GitHub
commit 93a981e013
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 275 additions and 104 deletions

View File

@ -1,5 +1,5 @@
{ {
"title": "Publish Data", "title": "Publish",
"description": "Highlight the important features of your data set to make it more discoverable and catch the interest of data consumers.", "description": "Highlight the important features of your data set to make it more discoverable and catch the interest of data consumers.",
"form": { "form": {
"title": "Publish", "title": "Publish",

5
package-lock.json generated
View File

@ -12911,6 +12911,11 @@
"integrity": "sha512-yfqzAi1GFxK6EoJIZKgxqJyK6j/OjEFEUi2qkNThD/kUhoCFSG1izq31B5xuxzbJBGw9/67uPtkPMYAzWL7L7Q==", "integrity": "sha512-yfqzAi1GFxK6EoJIZKgxqJyK6j/OjEFEUi2qkNThD/kUhoCFSG1izq31B5xuxzbJBGw9/67uPtkPMYAzWL7L7Q==",
"dev": true "dev": true
}, },
"dom-confetti": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/dom-confetti/-/dom-confetti-0.2.2.tgz",
"integrity": "sha512-+UVH9Y85qmpTnbmFURwLWjqLIykyIrsNSRkPX/eFlBuOURz9RDX8JoZHnajZHyFuCV0w/K3+tZK0ztfoTw6ejg=="
},
"dom-converter": { "dom-converter": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",

View File

@ -35,6 +35,7 @@
"classnames": "^2.2.6", "classnames": "^2.2.6",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"decimal.js": "^10.2.1", "decimal.js": "^10.2.1",
"dom-confetti": "^0.2.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0", "filesize": "^6.1.0",

View File

@ -10,14 +10,19 @@ interface TabsItem {
export default function Tabs({ export default function Tabs({
items, items,
className, className,
handleTabChange handleTabChange,
defaultIndex
}: { }: {
items: TabsItem[] items: TabsItem[]
className?: string className?: string
handleTabChange?: (tabName: string) => void handleTabChange?: (tabName: string) => void
defaultIndex?: number
}): ReactElement { }): ReactElement {
return ( return (
<ReactTabs className={`${className && className}`}> <ReactTabs
className={`${className && className}`}
defaultIndex={defaultIndex}
>
<TabList className={styles.tabList}> <TabList className={styles.tabList}>
{items.map((item) => ( {items.map((item) => (
<Tab <Tab

View File

@ -5,7 +5,7 @@ import styles from './index.module.css'
import Tabs from '../../../atoms/Tabs' import Tabs from '../../../atoms/Tabs'
import Fixed from './Fixed' import Fixed from './Fixed'
import Dynamic from './Dynamic' import Dynamic from './Dynamic'
import { useField } from 'formik' import { useField, useFormikContext } from 'formik'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import { PriceOptionsMarket } from '../../../../@types/MetaData' import { PriceOptionsMarket } from '../../../../@types/MetaData'
@ -68,8 +68,9 @@ export default function Price(props: InputProps): ReactElement {
helpers.setValue({ ...field.value, tokensToMint }) helpers.setValue({ ...field.value, tokensToMint })
}, [price]) }, [price])
// Generate new DT name & symbol // Generate new DT name & symbol, but only once automatically
useEffect(() => { useEffect(() => {
if (!ocean || typeof field?.value?.datatoken?.name !== 'undefined') return
generateName() generateName()
}, [ocean]) }, [ocean])
@ -100,7 +101,11 @@ export default function Price(props: InputProps): ReactElement {
return ( return (
<div className={styles.price}> <div className={styles.price}>
<Tabs items={tabs} handleTabChange={handleTabChange} /> <Tabs
items={tabs}
handleTabChange={handleTabChange}
defaultIndex={field?.value?.type === 'fixed' ? 0 : 1}
/>
{debug === true && ( {debug === true && (
<pre> <pre>
<code>{JSON.stringify(field.value, null, 2)}</code> <code>{JSON.stringify(field.value, null, 2)}</code>

View File

@ -0,0 +1,30 @@
import React, { ReactElement } from 'react'
import { MetadataPublishForm } from '../../../@types/MetaData'
import styles from './index.module.css'
import { transformPublishFormToMetadata } from './utils'
export default function Debug({
values
}: {
values: Partial<MetadataPublishForm>
}): ReactElement {
return (
<div className={styles.grid}>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div>
<div>
<h5>Transformed Values</h5>
<pre>
<code>
{JSON.stringify(transformPublishFormToMetadata(values), null, 2)}
</code>
</pre>
</div>
</div>
)
}

View File

@ -0,0 +1,40 @@
.feedback {
width: 100%;
min-height: 40vh;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
text-align: center;
}
.box {
composes: box from '../../atoms/Box.module.css';
width: 100%;
}
.feedback h3 {
font-size: var(--font-size-large);
text-align: center;
margin-bottom: calc(var(--spacer) / 2);
}
.feedback h3 + div {
text-align: left;
}
@media (min-width: 40rem) {
.feedback {
max-width: 30rem;
margin: 0 auto;
}
}
.feedback [class*='loaderWrap'] {
justify-content: center;
text-align: center;
}
.action {
margin-top: calc(var(--spacer) / 1.5);
}

View File

@ -0,0 +1,45 @@
import Alert from '../../atoms/Alert'
import Success from './Success'
import Button from '../../atoms/Button'
import Loader from '../../atoms/Loader'
import React, { ReactElement } from 'react'
import styles from './Feedback.module.css'
export default function Feedback({
error,
success,
did,
publishStepText,
setError
}: {
error: string
success: string
did: string
publishStepText: string
setError: (error: string) => void
}): ReactElement {
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 ? (
<Success success={success} did={did} />
) : (
<Loader message={publishStepText} />
)}
</div>
</div>
)
}

View File

@ -5,17 +5,11 @@ import { useFormikContext, Form, Field } from 'formik'
import Input from '../../atoms/Input' import Input from '../../atoms/Input'
import Button from '../../atoms/Button' import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form' import { FormContent, FormFieldProps } from '../../../@types/Form'
import Loader from '../../atoms/Loader'
import { Persist } from '../../atoms/FormikPersist'
export default function PublishForm({ export default function PublishForm({
content, content
publishStepText,
isLoading
}: { }: {
content: FormContent content: FormContent
publishStepText?: string
isLoading: boolean
}): ReactElement { }): ReactElement {
const { ocean, account } = useOcean() const { ocean, account } = useOcean()
const { const {
@ -27,7 +21,6 @@ export default function PublishForm({
resetForm, resetForm,
initialValues initialValues
} = useFormikContext() } = useFormikContext()
const formName = 'ocean-publish-form'
// reset form validation on every mount // reset form validation on every mount
useEffect(() => { useEffect(() => {
@ -52,30 +45,22 @@ export default function PublishForm({
{content.data.map((field: FormFieldProps) => ( {content.data.map((field: FormFieldProps) => (
<Field key={field.name} {...field} component={Input} /> <Field key={field.name} {...field} component={Input} />
))} ))}
{isLoading ? (
<Loader message={publishStepText} />
) : (
<footer className={styles.actions}>
<Button
style="primary"
type="submit"
disabled={!ocean || !account || !isValid || status === 'empty'}
>
Submit
</Button>
{status !== 'empty' && ( <footer className={styles.actions}>
<Button <Button
style="text" style="primary"
size="small" type="submit"
onClick={resetFormAndClearStorage} disabled={!ocean || !account || !isValid || status === 'empty'}
> >
Reset Form Submit
</Button> </Button>
)}
</footer> {status !== 'empty' && (
)} <Button style="text" size="small" onClick={resetFormAndClearStorage}>
<Persist name={formName} ignoreFields={['isSubmitting']} /> Reset Form
</Button>
)}
</footer>
</Form> </Form>
) )
} }

View File

@ -0,0 +1,3 @@
.action {
margin-top: calc(var(--spacer) / 1.5);
}

View File

@ -0,0 +1,56 @@
import Alert from '../../atoms/Alert'
import Button from '../../atoms/Button'
import React, { ReactElement, useEffect } from 'react'
import { confetti } from 'dom-confetti'
import styles from './Success.module.css'
const confettiConfig = {
angle: 90,
spread: 360,
startVelocity: 40,
elementCount: 70,
dragFriction: 0.12,
duration: 3000,
stagger: 3,
width: '10px',
height: '10px',
perspective: '500px',
colors: [
'var(--brand-pink)',
'var(--brand-purple)',
'var(--brand-violet)',
'var(--brand-grey-light)',
'var(--brand-grey-lighter)'
]
}
export default function Success({
success,
did
}: {
success: string
did: string
}): ReactElement {
// Have some confetti upon success
useEffect(() => {
if (!success || typeof window === 'undefined') return
const startElement: HTMLElement = document.querySelector('a[data-confetti]')
confetti(startElement, confettiConfig)
}, [success])
return (
<>
<Alert text={success} state="success" />
<Button
style="primary"
size="small"
href={`/asset/${did}`}
className={styles.action}
data-confetti
>
Go to data set
</Button>
</>
)
}

View File

@ -1,6 +1,4 @@
import React, { ReactElement } from 'react' import React, { ReactElement, useState } from 'react'
import { useNavigate } from '@reach/router'
import { toast } from 'react-toastify'
import { Formik } from 'formik' import { Formik } from 'formik'
import { usePublish } from '@oceanprotocol/react' import { usePublish } from '@oceanprotocol/react'
import styles from './index.module.css' import styles from './index.module.css'
@ -10,9 +8,14 @@ import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from '../../../models/FormPublish' import { initialValues, validationSchema } from '../../../models/FormPublish'
import { transformPublishFormToMetadata } from './utils' import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview' import Preview from './Preview'
import { MetadataMarket, MetadataPublishForm } from '../../../@types/MetaData' import { MetadataPublishForm } from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences' import { useUserPreferences } from '../../../providers/UserPreferences'
import { Logger, Metadata } from '@oceanprotocol/lib' import { Logger, Metadata } from '@oceanprotocol/lib'
import { Persist } from '../../atoms/FormikPersist'
import Debug from './Debug'
import Feedback from './Feedback'
const formName = 'ocean-publish-form'
export default function PublishPage({ export default function PublishPage({
content content
@ -21,7 +24,12 @@ export default function PublishPage({
}): ReactElement { }): ReactElement {
const { debug } = useUserPreferences() const { debug } = useUserPreferences()
const { publish, publishError, isLoading, publishStepText } = usePublish() const { publish, publishError, isLoading, publishStepText } = usePublish()
const navigate = useNavigate()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const [did, setDid] = useState<string>()
const hasFeedback = isLoading || error || success
async function handleSubmit( async function handleSubmit(
values: Partial<MetadataPublishForm>, values: Partial<MetadataPublishForm>,
@ -36,80 +44,70 @@ export default function PublishPage({
const ddo = await publish( const ddo = await publish(
(metadata as unknown) as Metadata, (metadata as unknown) as Metadata,
{ { ...price, swapFee: `${price.swapFee}` },
...price,
swapFee: `${price.swapFee}`
},
serviceType, serviceType,
price.datatoken price.datatoken
) )
// Publish failed
if (publishError) { if (publishError) {
toast.error(publishError) && console.error(publishError) setError(publishError)
return null Logger.error(publishError)
return
} }
// User feedback and redirect to new asset detail page // Publish succeeded
ddo && toast.success('Asset created successfully.') && resetForm() if (ddo) {
// Go to new asset detail page setDid(ddo.id)
navigate(`/asset/${ddo.id}`) setSuccess('🎉 Successfully published your data set. 🎉')
resetForm()
}
} catch (error) { } catch (error) {
console.error(error.message) setError(error.message)
toast.error(error.message) Logger.error(error.message)
} }
} }
return ( return (
<article className={styles.grid}> <Formik
<Formik initialValues={initialValues}
initialValues={initialValues} initialStatus="empty"
initialStatus="empty" validationSchema={validationSchema}
validationSchema={validationSchema} onSubmit={async (values, { setSubmitting, resetForm }) => {
onSubmit={async (values, { setSubmitting, resetForm }) => { // move user's focus to top of screen
await handleSubmit(values, resetForm) window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
setSubmitting(false) // kick off publishing
}} await handleSubmit(values, resetForm)
> setSubmitting(false)
{({ values }) => ( }}
<> >
<PublishForm {({ values }) => (
content={content.form} <>
isLoading={isLoading} <Persist name={formName} ignoreFields={['isSubmitting']} />
{hasFeedback ? (
<Feedback
error={error}
success={success}
publishStepText={publishStepText} publishStepText={publishStepText}
did={did}
setError={setError}
/> />
<aside> ) : (
<div className={styles.sticky}> <article className={styles.grid}>
<Preview values={values} /> <PublishForm content={content.form} />
<Web3Feedback /> <aside>
</div> <div className={styles.sticky}>
</aside> <Preview values={values} />
<Web3Feedback />
{debug === true && (
<>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div> </div>
</aside>
</article>
)}
<div> {debug === true && <Debug values={values} />}
<h5>Transformed Values</h5> </>
<pre> )}
<code> </Formik>
{JSON.stringify(
transformPublishFormToMetadata(values),
null,
2
)}
</code>
</pre>
</div>
</>
)}
</>
)}
</Formik>
</article>
) )
} }

View File

@ -11,9 +11,7 @@ import content from '../../../content/pages/publish.json'
describe('PublishForm', () => { describe('PublishForm', () => {
it('renders without crashing', async () => { it('renders without crashing', async () => {
const { container } = render( const { container } = render(<PublishForm content={content.form} />)
<PublishForm content={content.form} isLoading={null} />
)
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })