1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

Merge pull request #23 from oceanprotocol/feature/publish-wizard

New publish flow
This commit is contained in:
Jernej Pregelj 2019-03-05 14:56:28 +01:00 committed by GitHub
commit 7f39b4eeb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 727 additions and 248 deletions

View File

@ -37,10 +37,9 @@
}
&:disabled {
color: rgba($brand-white, .7);
cursor: not-allowed;
pointer-events: none;
opacity: .8;
opacity: .5;
}
}

View File

@ -9,6 +9,7 @@ interface ButtonProps {
link?: boolean
href?: string
onClick?: any
disabled?: boolean
}
export default class Button extends PureComponent<ButtonProps, any> {

View File

@ -2,6 +2,10 @@
.form {
width: 100%;
background: $brand-white;
padding: $spacer;
border: 1px solid $brand-grey-lighter;
border-radius: $border-radius;
fieldset {
border: 0;
@ -9,6 +13,13 @@
}
}
.formMinimal {
composes: form;
background: none;
padding: 0;
border: 0;
}
.formHeader {
margin-bottom: $spacer;
}

View File

@ -6,14 +6,20 @@ const Form = ({
description,
children,
onSubmit,
minimal,
...props
}: {
title?: string
description?: string
children: any
onSubmit?: any
minimal?: boolean
}) => (
<form className={styles.form} onSubmit={onSubmit} {...props}>
<form
className={minimal ? styles.formMinimal : styles.form}
onSubmit={onSubmit}
{...props}
>
{title && (
<header className={styles.formHeader}>
<h1 className={styles.formTitle}>{title}</h1>

View File

@ -49,7 +49,15 @@ export default class Input extends PureComponent<InputProps, InputState> {
}
public InputComponent = () => {
const { type, options, group, name, required, onChange } = this.props
const {
type,
options,
group,
name,
required,
onChange,
value
} = this.props
const wrapClass = this.inputWrapClasses()
@ -64,19 +72,22 @@ export default class Input extends PureComponent<InputProps, InputState> {
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
onChange={onChange}
value={value}
>
<option value="">---</option>
{options &&
options.map((option: string, index: number) => (
<option
key={index}
value={slugify(option, {
lower: true
})}
>
{option}
</option>
))}
options
.sort((a, b) => a.localeCompare(b))
.map((option: string, index: number) => (
<option
key={index}
value={slugify(option, {
lower: true
})}
>
{option}
</option>
))}
</select>
</div>
)

View File

@ -3,7 +3,8 @@
.spinner {
position: relative;
text-align: center;
margin-bottom: $spacer / 2;
margin-bottom: $spacer;
margin-top: $spacer * 2;
&:before {
content: '';
@ -13,7 +14,7 @@
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-top: -20px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid $brand-purple;
@ -24,7 +25,6 @@
.spinnerMessage {
color: $brand-grey-light;
margin-top: $spacer / 2;
}
@keyframes spinner {

View File

@ -1,90 +1,114 @@
{
"title": "Publish a new data asset",
"description": "A cool form description",
"fields": {
"name": {
"label": "Title",
"placeholder": "i.e. My cool data set",
"type": "text",
"required": true,
"help": "Help me Obiwan"
"steps": [
{
"title": "Essentials",
"description": "Start by adding a title and URLs for your data set.",
"fields": {
"name": {
"label": "Title",
"placeholder": "i.e. Almond sales data",
"type": "text",
"required": true,
"help": "Enter a concise title. You will be able to enter a more thorough description in the next step."
},
"files": {
"label": "Files",
"placeholder": "i.e. https://file.com/file.json",
"type": "text",
"required": true,
"help": "Provide one or multiple urls to your data set files."
}
}
},
"files": {
"label": "Files",
"placeholder": "i.e. https://file.com/file.json",
"type": "text",
"required": true,
"help": "Provide one or multiple links to your data files."
{
"title": "Information",
"description": "Further describe and categorize your data set to help people discover it.",
"fields": {
"description": {
"label": "Description",
"description": "Add a thorough description with as much detail as possible.",
"placeholder": "i.e. Almond sales data ",
"type": "textarea",
"required": true,
"rows": 5
},
"categories": {
"label": "Categories",
"description": "Pick a category which best fits your data set.",
"type": "select",
"required": true,
"options": [
"Image Recognition",
"Dataset Of Datasets",
"Language",
"Performing Arts",
"Visual Arts & Design",
"Philosophy",
"History",
"Theology",
"Anthropology & Archeology",
"Sociology",
"Psychology",
"Politics",
"Interdisciplinary",
"Economics & Finance",
"Demography",
"Biology",
"Chemistry",
"Physics & Energy",
"Earth & Climate",
"Space & Astronomy",
"Mathematics",
"Computer Technology",
"Engineering",
"Agriculture & Bio Engineering",
"Transportation",
"Urban Planning",
"Medicine",
"Business & Management",
"Sports & Recreation",
"Communication & Journalism",
"Other"
]
}
}
},
"description": {
"label": "Description",
"placeholder": "i.e. My cool data set",
"type": "textarea",
"required": true,
"rows": 5
{
"title": "Authorship",
"description": "Give proper attribution for your data set.",
"fields": {
"author": {
"label": "Author",
"placeholder": "i.e. Jelly McJellyfish",
"type": "text",
"required": true
},
"copyrightHolder": {
"label": "Copyright Holder",
"placeholder": "i.e. Marine Institute of Jellyfish",
"type": "text",
"required": true
},
"license": {
"label": "License",
"type": "select",
"required": true,
"options": [
"Public Domain",
"CC BY: Attribution",
"CC BY-SA: Attribution ShareAlike",
"CC BY-ND: Attribution-NoDerivs",
"CC BY-NC: Attribution-NonCommercial",
"CC BY-NC-SA: Attribution-NonCommercial-ShareAlike",
"CC BY-NC-ND: Attribution-NonCommercial-NoDerivs"
]
}
}
},
"categories": {
"label": "Categories",
"type": "select",
"options": [
"Image Recognition",
"Dataset Of Datasets",
"Language",
"Performing Arts",
"Visual Arts & Design",
"Philosophy",
"History",
"Theology",
"Anthropology & Archeology",
"Sociology",
"Psychology",
"Politics",
"Interdisciplinary",
"Economics & Finance",
"Demography",
"Biology",
"Chemistry",
"Physics & Energy",
"Earth & Climate",
"Space & Astronomy",
"Mathematics",
"Computer Technology",
"Engineering",
"Agriculture & Bio Engineering",
"Transportation",
"Urban Planning",
"Medicine",
"Business & Management",
"Sports & Recreation",
"Communication & Journalism",
"Other"
]
},
"copyrightHolder": {
"label": "Copyright Holder",
"placeholder": "i.e. fwhfiw",
"type": "text",
"required": true
},
"author": {
"label": "Author",
"placeholder": "i.e. Jelly McJellyfish",
"type": "text",
"required": true
},
"license": {
"label": "License",
"type": "select",
"required": true,
"options": [
"Public Domain",
"CC BY: Attribution",
"CC BY-SA: Attribution ShareAlike",
"CC BY-ND: Attribution-NoDerivs",
"CC BY-NC: Attribution-NonCommercial",
"CC BY-NC-SA: Attribution-NonCommercial-ShareAlike",
"CC BY-NC-ND: Attribution-NonCommercial-NoDerivs"
]
{
"title": "Register",
"description": "Splendid, we got all the data. Now let's register your data set.",
"content": "After clicking the button below you will be asked by your wallet to sign this request."
}
}
]
}

View File

@ -2,22 +2,9 @@
.home {
display: block;
}
.published {
margin-top: $spacer * 3;
margin-bottom: $spacer;
> div {
text-align: center;
margin-top: $spacer;
margin-bottom: $spacer;
form {
margin-top: $spacer * 2;
margin-bottom: $spacer * 4;
}
}
.subTitle {
font-size: $font-size-h4;
color: $brand-grey-light;
border-bottom: 1px solid $brand-grey-lighter;
padding-bottom: $spacer / 2;
}

View File

@ -26,26 +26,21 @@ class Home extends Component<HomeProps, HomeState> {
description={meta.description}
className={styles.home}
>
<Form onSubmit={this.searchAssets}>
<Form onSubmit={this.searchAssets} minimal>
<Input
type="search"
name="search"
label="Search"
label="Search for data sets"
placeholder="i.e. almond sales data"
value={this.state.search}
onChange={this.inputChange}
group={<Button>Search</Button>}
group={
<Button primary disabled={!this.state.search}>
Search
</Button>
}
/>
</Form>
<div className={styles.published}>
<h2 className={styles.subTitle}>Your Data Sets</h2>
<div>
<p>None yet.</p>
<Link to="/publish">+ Publish A Data Set</Link>
</div>
</div>
</Route>
)
}

View File

@ -12,7 +12,6 @@ interface FilesProps {
help?: string
name: string
onChange: any
// resetForm: any
}
interface FilesStates {
@ -32,12 +31,26 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
public addItem = (value: string) => {
this.props.files.push({ url: value })
// this.props.resetForm()
const event = {
currentTarget: {
name: 'files',
value: this.props.files
}
}
this.props.onChange(event as any)
this.setState({ isFormShown: !this.state.isFormShown })
}
public removeItem = (index: number) => {
this.props.files.splice(index, 1)
const event = {
currentTarget: {
name: 'files',
value: this.props.files
}
}
this.props.onChange(event as any)
this.forceUpdate()
}
public render() {
@ -52,7 +65,7 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
<input
type="hidden"
name={name}
value={files}
value={JSON.stringify(files)}
onChange={onChange}
/>

View File

@ -0,0 +1,76 @@
@import '../../styles/variables';
.progress {
display: block;
padding: 0;
position: relative;
margin-top: $spacer * 1.5;
margin-bottom: $spacer;
}
.item {
display: inline-block;
width: 25%;
text-align: center;
color: $brand-grey-light;
&:before {
content: '';
display: block;
width: 60%;
height: .1rem;
background: $brand-grey-lighter;
position: absolute;
top: 20%;
left: -30%;
}
&:first-child {
&:before {
display: none;
}
}
span {
display: block;
}
}
.active {
composes: item;
font-family: $font-family-button;
font-weight: $font-weight-bold;
color: $brand-black;
.number {
background: $brand-black;
}
&:before {
background: $green;
}
}
.completed {
composes: active;
.number {
background: $green;
}
}
.label {
margin-top: $spacer / 8;
font-size: $font-size-small;
}
.number {
width: 1.6rem;
height: 1.6rem;
margin: auto;
border-radius: 50%;
background: $brand-grey-light;
color: $brand-white;
font-family: $font-family-button;
font-weight: $font-weight-bold;
}

View File

@ -0,0 +1,32 @@
import React from 'react'
import styles from './Progress.module.scss'
const Progress = ({
currentStep,
steps
}: {
currentStep: number
steps: any[]
}) => {
return (
<ul className={styles.progress}>
{steps.map(({ title }, index) => (
<li
key={index}
className={
currentStep === index + 1
? styles.active
: currentStep > index + 1
? styles.completed
: styles.item
}
>
<span className={styles.number}>{index + 1}</span>
<span className={styles.label}>{title}</span>
</li>
))}
</ul>
)
}
export default Progress

View File

@ -0,0 +1,26 @@
@import '../../styles/variables';
.header {
margin-bottom: $spacer;
}
.title {
font-size: $font-size-h2;
margin: 0;
}
.description {
margin-top: $spacer / 4;
margin-bottom: 0;
}
.actions {
display: flex;
justify-content: space-between;
padding-top: $spacer / 2;
button:last-child {
min-width: 12rem;
margin-left: auto;
}
}

164
src/routes/Publish/Step.tsx Normal file
View File

@ -0,0 +1,164 @@
import React, { PureComponent } from 'react'
import Input from '../../components/atoms/Form/Input'
import Label from '../../components/atoms/Form/Label'
import Row from '../../components/atoms/Form/Row'
import Button from '../../components/atoms/Button'
import { User } from '../../context/User'
import Files from './Files/'
import StepRegisterContent from './StepRegisterContent'
import styles from './Step.module.scss'
interface StepProps {
currentStep: number
index: number
inputChange: any
inputToArrayChange: any
fields?: any[]
state: any
title: string
description: string
next: any
prev: any
totalSteps: number
tryAgain: any
toStart: any
publishedDid?: string
content?: string
}
export default class Step extends PureComponent<StepProps, {}> {
public previousButton() {
const { currentStep, prev } = this.props
if (currentStep !== 1) {
return (
<Button link onClick={prev}>
Previous
</Button>
)
}
return null
}
public nextButton() {
const { currentStep, next, totalSteps, state } = this.props
if (currentStep < totalSteps) {
return (
<Button
disabled={
!state.validationStatus[currentStep].allFieldsValid
}
onClick={next}
>
Next
</Button>
)
}
return null
}
public render() {
const {
currentStep,
index,
title,
description,
fields,
inputChange,
inputToArrayChange,
state,
totalSteps,
tryAgain,
toStart,
content
} = this.props
if (currentStep !== index + 1) {
return null
}
const lastStep = currentStep === totalSteps
return (
<>
<header className={styles.header}>
<h2 className={styles.title}>{title}</h2>
<p className={styles.description}>{description}</p>
</header>
{fields &&
Object.entries(fields).map(([key, value]) => {
let onChange = inputChange
if (key === 'categories') {
onChange = inputToArrayChange
}
if (key === 'files') {
return (
<Row key={key}>
<Label htmlFor={key} required>
{value.label}
</Label>
<Files
placeholder={value.placeholder}
name={key}
help={value.help}
files={state.files}
onChange={onChange}
/>
</Row>
)
}
return (
<Input
key={key}
name={key}
label={value.label}
placeholder={value.placeholder}
required={value.required}
type={value.type}
help={value.help}
options={value.options}
onChange={onChange}
rows={value.rows}
value={(state as any)[key]}
/>
)
})}
{lastStep && (
<StepRegisterContent
tryAgain={tryAgain}
toStart={toStart}
state={state}
content={content}
/>
)}
<div className={styles.actions}>
{this.previousButton()}
{this.nextButton()}
{lastStep && (
<User.Consumer>
{states =>
states.isLogged ? (
<Button primary>Register asset</Button>
) : (
<Button onClick={states.startLogin}>
Register asset (login first)
</Button>
)
}
</User.Consumer>
)}
</div>
</>
)
}
}
Step.contextType = User

View File

@ -0,0 +1,5 @@
@import '../../styles/variables';
.message {
margin-bottom: $spacer;
}

View File

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react'
import Web3message from '../../components/Web3message'
import Spinner from '../../components/atoms/Spinner'
import styles from './StepRegisterContent.module.scss'
interface StepRegisterContentProps {
tryAgain: any
toStart: any
state: any
content?: string
}
export default class StepRegisterContent extends PureComponent<
StepRegisterContentProps,
{}
> {
public publishingState = () => (
<Spinner message={'Please sign with your crypto wallet'} />
)
public errorState = () => (
<div className={styles.message}>
Something went wrong,{' '}
<a onClick={() => this.props.tryAgain()}>try again</a>
</div>
)
public publishedState = () => (
<div className={styles.message}>
Your asset is published! See it{' '}
<a href={'/asset/' + this.props.state.publishedDid}>here</a>, submit
another asset by clicking{' '}
<a onClick={() => this.props.toStart()}>here</a>
</div>
)
public render() {
return (
<>
<Web3message />
{this.props.state.isPublishing ? (
this.publishingState()
) : this.props.state.publishingError ? (
this.errorState()
) : this.props.state.isPublished ? (
this.publishedState()
) : (
<p>{this.props.content}</p>
)}
</>
)
}
}

View File

@ -1,17 +1,13 @@
import React, { ChangeEvent, Component, FormEvent } from 'react'
import { Logger } from '@oceanprotocol/squid'
import Route from '../../components/templates/Route'
import Button from '../../components/atoms/Button'
import Form from '../../components/atoms/Form/Form'
import Input from '../../components/atoms/Form/Input'
import Label from '../../components/atoms/Form/Label'
import Row from '../../components/atoms/Form/Row'
import { User } from '../../context/User'
import AssetModel from '../../models/AssetModel'
import Web3message from '../../components/Web3message'
import Files from './Files/'
import { User } from '../../context/User'
import Step from './Step'
import Progress from './Progress'
import form from '../../data/form-publish.json'
import { steps } from '../../data/form-publish.json'
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
@ -19,22 +15,25 @@ interface PublishState {
name?: string
dateCreated?: Date
description?: string
files?: any[]
files?: string[]
price?: number
author?: string
type?: AssetType
license?: string
copyrightHolder?: string
categories?: string[]
categories?: string
tags?: string[]
isPublishing?: boolean
isPublished?: boolean
publishedDid?: string
publishingError?: string
currentStep?: number
validationStatus?: any
}
class Publish extends Component<{}, PublishState> {
public state = {
currentStep: 1,
name: '',
dateCreated: new Date(),
description: '',
@ -44,58 +43,28 @@ class Publish extends Component<{}, PublishState> {
type: 'dataset' as AssetType,
license: '',
copyrightHolder: '',
categories: [],
categories: '',
isPublishing: false,
isPublished: false,
publishedDid: '',
publishingError: ''
publishingError: '',
validationStatus: {
1: { name: false, files: false, allFieldsValid: false },
2: { description: false, categories: false, allFieldsValid: false },
3: {
author: false,
copyrightHolder: false,
license: false,
allFieldsValid: false
}
}
}
public formFields = (entries: any[]) =>
entries.map(([key, value]) => {
let onChange = this.inputChange
if (key === 'files' || key === 'categories') {
onChange = this.inputToArrayChange
}
if (key === 'files') {
return (
<Row key={key}>
<Label htmlFor={key} required>
{value.label}
</Label>
<Files
placeholder={value.placeholder}
name={value.name}
help={value.help}
files={this.state.files}
onChange={onChange}
/>
</Row>
)
}
return (
<Input
key={key}
name={key}
label={value.label}
placeholder={value.placeholder}
required={value.required}
type={value.type}
help={value.help}
options={value.options}
onChange={onChange}
rows={value.rows}
value={(this.state as any)[key]}
/>
)
})
private inputChange = (
event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLSelectElement>
) => {
this.validateInputs(event.currentTarget.name, event.currentTarget.value)
this.setState({
[event.currentTarget.name]: event.currentTarget.value
})
@ -104,11 +73,28 @@ class Publish extends Component<{}, PublishState> {
private inputToArrayChange = (
event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLSelectElement>
) => {
this.validateInputs(event.currentTarget.name, event.currentTarget.value)
this.setState({
[event.currentTarget.name]: [event.currentTarget.value]
})
}
private next = () => {
let { currentStep } = this.state
const totalSteps = steps.length
currentStep =
currentStep >= totalSteps - 1 ? totalSteps : currentStep + 1
this.setState({ currentStep })
}
private prev = () => {
let { currentStep } = this.state
currentStep = currentStep <= 1 ? 1 : currentStep - 1
this.setState({ currentStep })
}
private tryAgain = () => {
this.setState({ publishingError: '' })
}
@ -124,19 +110,140 @@ class Publish extends Component<{}, PublishState> {
type: 'dataset' as AssetType,
license: '',
copyrightHolder: '',
categories: [],
categories: '',
isPublishing: false,
isPublished: false
isPublished: false,
currentStep: 1
})
}
private validateInputs = (name: string, value: any) => {
let hasContent = value.length > 0
// Setting state for all fields
if (hasContent) {
this.setState(
prevState => ({
validationStatus: {
...prevState.validationStatus,
[this.state.currentStep]: {
...prevState.validationStatus[
this.state.currentStep
],
[name]: true
}
}
}),
this.runValidation
)
} else {
this.setState(
prevState => ({
validationStatus: {
...prevState.validationStatus,
[this.state.currentStep]: {
...prevState.validationStatus[
this.state.currentStep
],
[name]: false
}
}
}),
this.runValidation
)
}
}
private runValidation = () => {
let { validationStatus } = this.state
//
// Step 1
//
if (validationStatus[1].name && validationStatus[1].files) {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
1: {
...prevState.validationStatus[1],
allFieldsValid: true
}
}
}))
} else {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
1: {
...prevState.validationStatus[1],
allFieldsValid: false
}
}
}))
}
//
// Step 2
//
if (validationStatus[2].description && validationStatus[2].categories) {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
2: {
...prevState.validationStatus[2],
allFieldsValid: true
}
}
}))
} else {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
2: {
...prevState.validationStatus[2],
allFieldsValid: false
}
}
}))
}
//
// Step 3
//
if (
validationStatus[3].author &&
validationStatus[3].copyrightHolder &&
validationStatus[3].license
) {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
3: {
...prevState.validationStatus[3],
allFieldsValid: true
}
}
}))
} else {
this.setState(prevState => ({
validationStatus: {
...prevState.validationStatus,
3: {
...prevState.validationStatus[3],
allFieldsValid: false
}
}
}))
}
}
private registerAsset = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
this.setState({
publishingError: '',
isPublishing: true
})
const account = await this.context.ocean.getAccounts()
const { ocean } = this.context
const account = await ocean.getAccounts()
const newAsset = {
// OEP-08 Attributes
// https://github.com/oceanprotocol/OEPs/tree/master/8
@ -166,10 +273,7 @@ class Publish extends Component<{}, PublishState> {
}
try {
const asset = await this.context.ocean.registerAsset(
newAsset,
account[0]
)
const asset = await ocean.registerAsset(newAsset, account[0])
this.setState({
publishedDid: asset.id,
isPublished: true
@ -187,66 +291,37 @@ class Publish extends Component<{}, PublishState> {
}
public render() {
const entries = Object.entries(form.fields)
return (
<Route title="Publish">
<Web3message />
<Route
title="Publish"
description="Publish a new data set into the Ocean Protocol Network."
>
<Progress steps={steps} currentStep={this.state.currentStep} />
{this.state.isPublishing ? (
this.publishingState()
) : this.state.publishingError ? (
this.errorState()
) : this.state.isPublished ? (
this.publishedState()
) : (
<Form
title={form.title}
description={form.description}
onSubmit={this.registerAsset}
>
{this.formFields(entries)}
<User.Consumer>
{states =>
states.isLogged ? (
<Button primary>Register asset</Button>
) : (
<Button onClick={states.startLogin}>
Register asset (login first)
</Button>
)
}
</User.Consumer>
</Form>
)}
<Form onSubmit={this.registerAsset}>
{steps.map((step: any, index: number) => (
<Step
key={index}
index={index}
title={step.title}
description={step.description}
currentStep={this.state.currentStep}
fields={step.fields}
inputChange={this.inputChange}
inputToArrayChange={this.inputToArrayChange}
state={this.state}
next={this.next}
prev={this.prev}
totalSteps={steps.length}
tryAgain={this.tryAgain}
toStart={this.toStart}
content={step.content}
/>
))}
</Form>
</Route>
)
}
public publishingState = () => {
return <div>Please sign with your crypto wallet</div>
}
public errorState = () => {
return (
<div>
Something went wrong,{' '}
<a onClick={() => this.tryAgain()}>try again</a>
</div>
)
}
public publishedState = () => {
return (
<div>
Your asset is published! See it{' '}
<a href={'/asset/' + this.state.publishedDid}>here</a>, submit
another asset by clicking{' '}
<a onClick={() => this.toStart()}>here</a>
</div>
)
}
}
Publish.contextType = User