1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-11-15 01:34:57 +01:00

Merge pull request #29 from oceanprotocol/feature/forms

Simplify publish form with Formik
This commit is contained in:
Matthias Kretschmann 2020-07-13 14:40:40 +02:00 committed by GitHub
commit 81950413ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 1083 additions and 2159 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ public
storybook-static
public/storybook
.artifacts
.vercel

View File

@ -1,4 +1,94 @@
{
"title": "Publish Data",
"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": {
"title": "Publish",
"data": [
{
"name": "name",
"label": "Title",
"placeholder": "e.g. Shapes of Desert Plants",
"help": "Enter a concise title.",
"required": true
},
{
"name": "files",
"label": "Files",
"placeholder": "e.g. https://file.com/file.json",
"help": "Please provide a URL to your data set file.",
"type": "files",
"required": true
},
{
"name": "description",
"label": "Description",
"help": "Add a thorough description with as much detail as possible.",
"type": "textarea",
"required": true
},
{
"name": "price",
"label": "Price",
"help": "Set your price in Ocean Tokens.",
"type": "price",
"min": 1,
"required": true
},
{
"name": "access",
"label": "Access Type",
"type": "select",
"options": ["Download", "Compute"],
"required": true
},
{
"name": "author",
"label": "Author",
"placeholder": "e.g. Jelly McJellyfish",
"help": "Give proper attribution for your data set.",
"required": true
},
{
"name": "copyrightHolder",
"label": "Copyright Holder",
"placeholder": "e.g. Marine Institute of Jellyfish"
},
{
"name": "tags",
"label": "Tags",
"placeholder": "e.g. logistics, ai",
"help": "Separate tags with comma."
},
{
"name": "license",
"label": "License",
"type": "select",
"options": [
"Public Domain",
"PDDL: Public Domain Dedication and License",
"ODC-By: Attribution License",
"ODC-ODbL: Open Database License",
"CDLA-Sharing: Community Data License Agreement",
"CDLA-Permissive: Community Data License Agreement",
"CC0: Public Domain Dedication",
"CC BY: Attribution 4.0 International",
"CC BY-SA: Attribution-ShareAlike 4.0 International",
"CC BY-ND: Attribution-NoDerivatives 4.0 International",
"CC BY-NC: Attribution-NonCommercial 4.0 International",
"CC BY-NC-SA: Attribution-NonCommercial-ShareAlike 4.0 International",
"CC BY-NC-ND: Attribution-NonCommercial-NoDerivatives 4.0 International",
"No License Specified"
],
"required": true
},
{
"name": "termsAndConditions",
"label": "Terms & Conditions",
"type": "checkbox",
"options": ["I agree to these Terms and Conditions"],
"required": true
}
],
"success": "Asset Created!"
}
}

1120
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@
},
"dependencies": {
"@loadable/component": "^5.13.1",
"@now/node": "^1.7.1",
"@now/node": "^1.7.2",
"@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/react": "0.0.11",
"@oceanprotocol/squid": "^2.2.0",
@ -34,31 +34,31 @@
"dotenv": "^8.2.0",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^6.1.0",
"gatsby": "^2.23.22",
"gatsby-image": "^2.4.12",
"gatsby-plugin-manifest": "^2.4.17",
"gatsby-plugin-react-helmet": "^3.3.9",
"gatsby-plugin-remove-trailing-slashes": "^2.3.10",
"gatsby-plugin-sharp": "^2.6.18",
"formik": "^2.1.4",
"gatsby": "^2.24.2",
"gatsby-image": "^2.4.13",
"gatsby-plugin-manifest": "^2.4.18",
"gatsby-plugin-react-helmet": "^3.3.10",
"gatsby-plugin-remove-trailing-slashes": "^2.3.11",
"gatsby-plugin-sharp": "^2.6.19",
"gatsby-plugin-svgr": "^2.0.2",
"gatsby-plugin-webpack-size": "^1.0.0",
"gatsby-source-filesystem": "^2.3.18",
"gatsby-source-graphql": "^2.6.1",
"gatsby-transformer-json": "^2.4.10",
"gatsby-transformer-remark": "^2.8.23",
"gatsby-transformer-sharp": "^2.5.10",
"gatsby-source-filesystem": "^2.3.19",
"gatsby-source-graphql": "^2.6.2",
"gatsby-transformer-json": "^2.4.11",
"gatsby-transformer-remark": "^2.8.25",
"gatsby-transformer-sharp": "^2.5.11",
"intersection-observer": "^0.11.0",
"is-url-superb": "^4.0.0",
"numeral": "^2.0.6",
"query-string": "^6.13.1",
"react": "^16.13.1",
"react-data-table-component": "^6.9.6",
"react-datepicker": "^3.0.0",
"react-datepicker": "^3.1.3",
"react-dom": "^16.13.1",
"react-dotdotdot": "^1.3.1",
"react-dropzone": "^11.0.1",
"react-helmet": "^6.1.0",
"react-jsonschema-form": "^1.8.1",
"react-markdown": "^4.3.1",
"react-paginate": "^6.3.2",
"react-rating": "^2.0.5",
@ -68,28 +68,29 @@
"react-toastify": "^6.0.8",
"shortid": "^2.2.15",
"slugify": "^1.4.4",
"web3connect": "^1.0.0-beta.33"
"web3connect": "^1.0.0-beta.33",
"yup": "^0.29.1"
},
"devDependencies": {
"@babel/core": "^7.10.3",
"@babel/preset-typescript": "^7.10.1",
"@storybook/addon-actions": "^6.0.0-beta.45",
"@storybook/addon-storyshots": "^6.0.0-beta.45",
"@storybook/react": "^6.0.0-beta.45",
"@storybook/addon-actions": "^6.0.0-rc.3",
"@storybook/addon-storyshots": "^6.0.0-rc.3",
"@storybook/react": "^6.0.0-rc.3",
"@svgr/webpack": "^5.4.0",
"@testing-library/jest-dom": "^5.11.0",
"@testing-library/react": "^10.4.4",
"@testing-library/react": "^10.4.5",
"@types/jest": "^26.0.4",
"@types/loadable__component": "^5.10.0",
"@types/node": "^14.0.19",
"@types/loadable__component": "^5.13.0",
"@types/node": "^14.0.22",
"@types/numeral": "^0.0.28",
"@types/react": "^16.9.41",
"@types/react": "^16.9.43",
"@types/react-datepicker": "^3.0.2",
"@types/react-helmet": "^6.0.0",
"@types/react-jsonschema-form": "^1.7.3",
"@types/react-paginate": "^6.2.1",
"@types/react-tabs": "^2.3.2",
"@types/shortid": "0.0.29",
"@types/yup": "^0.29.3",
"@typescript-eslint/eslint-plugin": "^3.6.0",
"@typescript-eslint/parser": "^3.6.0",
"babel-loader": "^8.1.0",

18
src/@types/Form.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
export interface FormFieldProps {
label: string
name: string
type?: string
options?: string[]
required?: boolean
help?: string
placeholder?: string
pattern?: string
min?: string
}
export interface FormContent {
title: string
description?: string
success: string
data: FormFieldProps[]
}

View File

@ -1,25 +1,36 @@
import { MetaData, AdditionalInformation } from '@oceanprotocol/squid'
import { File, MetaData, AdditionalInformation } from '@oceanprotocol/squid'
import { ServiceMetadata } from '@oceanprotocol/squid/dist/node/ddo/Service'
export interface Sample {
name: string
url: string
}
export declare type AccessType = 'Download' | 'Compute'
export interface AdditionalInformationMarket extends AdditionalInformation {
description: string
links?: Sample[] // redefine existing key, cause not specific enough in Squid
links?: File[] // redefine existing key, cause not specific enough in Squid
termsAndConditions: boolean
dateRange?: [string, string]
access: AccessType
access: AccessType | string
}
export interface MetaDataMarket extends MetaData {
additionalInformation: AdditionalInformationMarket
}
export interface MetaDataPublishForm {
// ---- required fields ----
name: string
description: string
files: string | File[]
author: string
license: string
price: string
access: string
termsAndConditions: boolean
// ---- optional fields ----
copyrightHolder?: string
tags?: string
links?: string | File[]
}
export interface ServiceMetaDataMarket extends ServiceMetadata {
attributes: MetaDataMarket
}

View File

@ -1,5 +1,4 @@
import React, { ReactNode, ReactElement } from 'react'
import { Helmet } from 'react-helmet'
import Header from './organisms/Header'
import Footer from './organisms/Footer'
import PageHeader from './molecules/PageHeader'
@ -24,12 +23,6 @@ export default function Layout({
}: LayoutProps): ReactElement {
return (
<div className={styles.app}>
<Helmet>
<link rel="icon" href="/icons/icon-96x96.png" />
<link rel="apple-touch-icon" href="icons/icon-256x256.png" />
<meta name="theme-color" content="#ca2935" />
</Helmet>
<Seo title={title} description={description} uri={uri} />
<Header />

View File

@ -1,6 +1,6 @@
import React from 'react'
import { Center } from '../../../.storybook/helpers'
import { Alert } from './Alert'
import Alert from './Alert'
export default {
title: 'Atoms/Alert',

View File

@ -1,7 +1,7 @@
import React, { ReactElement } from 'react'
import styles from './Alert.module.css'
export function Alert({
export default function Alert({
title,
text,
state

View File

@ -46,7 +46,7 @@
.button:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: 0.5;
background: var(--brand-grey-lighter);
}
.primary,

View File

@ -1,3 +0,0 @@
.label {
cursor: pointer;
}

View File

@ -1,26 +0,0 @@
import React from 'react'
import Checkbox from './Checkbox'
import { Center } from '../../../.storybook/helpers'
export default {
title: 'Atoms/Checkbox',
decorators: [(storyFn: any) => <Center>{storyFn()}</Center>]
}
export const Checked = () => (
<Checkbox
name="someName"
checked
onChange={() => null}
label="Example checkbox"
/>
)
export const Unchecked = () => (
<Checkbox
name="someName"
checked={false}
onChange={() => null}
label="Example checkbox"
/>
)

View File

@ -1,31 +0,0 @@
import React from 'react'
import styles from './Checkbox.module.css'
interface CheckboxProps {
name: string
checked: boolean
onChange?: (evt: React.ChangeEvent) => void
label: string
}
const Checkbox: React.FC<CheckboxProps> = ({
name,
checked,
onChange,
label
}) => {
return (
<label className={styles.label}>
<input
type="checkbox"
name={name}
checked={checked}
onChange={onChange}
className={styles.checkbox}
/>
{label}
</label>
)
}
export default Checkbox

View File

@ -1,10 +1,10 @@
import React from 'react'
import React, { ReactElement } from 'react'
import { File as FileMetaData } from '@oceanprotocol/squid'
import filesize from 'filesize'
import cleanupContentType from '../../utils/cleanupContentType'
import styles from './File.module.css'
export default function File({ file }: { file: FileMetaData }) {
export default function File({ file }: { file: FileMetaData }): ReactElement {
if (!file) return null
return (

View File

@ -1,22 +0,0 @@
.dateRange {
display: flex;
}
.separator {
margin: 0 var(--spacer);
height: inherit;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-base);
}
.checkbox {
composes: checkbox from '../../molecules/Form/FieldTemplate.module.css';
margin-top: calc(var(--spacer) / 8);
}
.label {
composes: label from '../../molecules/Form/FieldTemplate.module.css';
font-size: var(--font-size-small);
}

View File

@ -1,33 +0,0 @@
import React from 'react'
import { Center } from '../../../../.storybook/helpers'
import DateRangeWidget from './DateRangeWidget'
import { PublishFormSchema } from '../../../models/PublishForm'
export default {
title: 'Atoms/DateRangeWidget',
decorators: [(storyFn: () => React.FC) => <Center>{storyFn()}</Center>]
}
export const DateRange = () => (
<DateRangeWidget
schema={PublishFormSchema}
id="1"
autofocus={false}
disabled={false}
label="Date Range"
formContext={{}}
readonly={false}
value="[]"
onBlur={() => {
/* */
}}
onFocus={() => {
/* */
}}
onChange={() => {
/* */
}}
options={{}}
required={false}
/>
)

View File

@ -1,87 +0,0 @@
import React, { useEffect, useState } from 'react'
import { WidgetProps } from 'react-jsonschema-form'
import loadable from '@loadable/component'
import styles from './DateRangeWidget.module.css'
import { toStringNoMS } from '../../../utils'
// lazy load this module, it's huge
const LazyDatePicker = loadable(() => import('react-datepicker'))
export function getWidgetValue(
date1: Date,
date2: Date,
range: boolean
): string {
let [initial, final] = [toStringNoMS(date1), toStringNoMS(date2)]
if (!range) {
final = initial
}
return JSON.stringify([initial, final])
}
export default function DateRangeWidget(props: WidgetProps) {
const { onChange } = props
const [startDate, setStartDate] = useState<Date>(new Date())
const [endDate, setEndDate] = useState<Date>(new Date())
const [range, setRange] = useState(false)
useEffect(() => {
// If the range checkbox is clicked we update the value of the picker
onChange(getWidgetValue(startDate, endDate, range))
}, [range])
return (
<>
<div className={styles.dateRange}>
{range ? (
<>
<LazyDatePicker
selected={startDate}
onChange={(date: Date) => {
setStartDate(date)
onChange(getWidgetValue(date, endDate, range))
}}
startDate={startDate}
selectsStart
endDate={endDate}
/>
<div className={styles.separator}></div>
<LazyDatePicker
selected={endDate}
selectsEnd
onChange={(date: Date) => {
setEndDate(date)
onChange(getWidgetValue(startDate, date, range))
}}
minDate={startDate}
startDate={startDate}
endDate={endDate}
/>
</>
) : (
<LazyDatePicker
selected={startDate}
onChange={(date: Date) => {
setStartDate(date)
onChange(getWidgetValue(date, date, range))
}}
startDate={startDate}
/>
)}
</div>
<div className={styles.checkbox}>
<input
id="range"
type="checkbox"
onChange={(ev) => setRange(ev.target.checked)}
checked={range}
/>
<label className={styles.label} htmlFor="range">
Date Range
</label>
</div>
</>
)
}

View File

@ -1,31 +0,0 @@
.terms {
padding: calc(var(--spacer) / 2);
border: 1px solid var(--brand-grey-light);
background-color: var(--brand-grey-lighter);
border-radius: var(--border-radius);
margin-bottom: calc(var(--spacer) / 2);
font-size: var(--font-size-small);
max-height: 250px;
/* smooth overflow scrolling for pre-iOS 13 */
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.terms h1 {
font-size: var(--font-size-base);
margin-bottom: calc(var(--spacer) / 2);
}
.terms h2 {
font-size: var(--font-size-small);
}
.label {
composes: label from '../../molecules/Form/FieldTemplate.module.css';
margin-bottom: 0;
}
.req {
composes: req from '../../molecules/Form/FieldTemplate.module.css';
}

View File

@ -1,41 +0,0 @@
import React from 'react'
import { WidgetProps } from 'react-jsonschema-form'
import styles from './TermsWidget.module.css'
export default function TermsWidget(props: WidgetProps) {
const {
id,
value,
disabled,
readonly,
label,
autofocus,
onBlur,
onFocus,
onChange,
required
// DescriptionField
} = props
return (
<>
{/* <Markdown text={terms} className={styles.terms} /> */}
<label
htmlFor={id}
className={required ? `${styles.label} ${styles.req}` : styles.label}
>
<input
type="checkbox"
id={id}
checked={typeof value === 'undefined' ? false : value}
disabled={disabled || readonly}
autoFocus={autofocus}
onChange={(event) => onChange(event.target.checked)}
onBlur={onBlur && ((event) => onBlur(id, event.target.checked))}
onFocus={onFocus && ((event) => onFocus(id, event.target.checked))}
/>
<span>{label}</span>
</label>
</>
)
}

View File

@ -69,34 +69,10 @@
.radioGroup {
margin-top: calc(var(--spacer) / 2);
margin-bottom: -2%;
}
@media screen and (min-width: 40rem) {
.radioGroup {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
}
.radioWrap {
position: relative;
padding: calc(var(--spacer) / 2);
text-align: center;
display: flex;
align-items: center;
margin-bottom: 2%;
}
@media screen and (min-width: 40rem) {
.radioWrap {
flex: 0 0 49%;
}
}
.radio:checked + label {
border-color: var(--brand-pink);
}
.radioLabel {
@ -104,19 +80,8 @@
padding: 0;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
line-height: 1.2;
border: 1px solid var(--brand-grey-lighter);
border-radius: 0.2rem;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: var(--brand-grey);
text-align: left;
padding-left: 2.5rem;
display: flex;
align-items: center;
padding-left: 0.5rem;
}
/* Size modifiers */

View File

@ -1,9 +1,10 @@
import React from 'react'
import React, { ReactElement } from 'react'
import slugify from '@sindresorhus/slugify'
import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../../molecules/FilesInput'
export default function InputElement(props: InputProps) {
export default function InputElement(props: InputProps): ReactElement {
const { type, options, rows, name } = props
switch (type) {
@ -52,6 +53,8 @@ export default function InputElement(props: InputProps) {
))}
</div>
)
case 'files':
return <FilesInput name={name} {...props} />
default:
return (
<input

View File

@ -1,31 +1,43 @@
.inputGroup {
width: 100%;
.inputGroup input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@media screen and (min-width: $break-point--small) {
.inputGroup button {
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-top: -1px;
width: 100%;
}
.inputGroup button:hover,
.inputGroup button:focus,
.inputGroup input:focus + button:hover,
.inputGroup input:focus + button:focus {
background: var(--brand-gradient);
transform: none;
box-shadow: none;
}
@media screen and (min-width: 30rem) {
.inputGroup {
display: flex;
}
}
.inputGroup > input {
@media screen and (min-width: $break-point--small) {
width: 75%;
.inputGroup input {
border-bottom-left-radius: var(--border-radius);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.inputGroup > button {
width: 100%;
position: absolute;
left: 0;
bottom: -120%;
@media screen and (min-width: $break-point--small) {
position: relative;
bottom: auto;
width: 25%;
height: 100%;
.inputGroup button {
border-top-right-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: none;
margin-top: 0;
margin-left: -1px;
width: fit-content;
min-width: 20%;
}
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import React, { ReactElement, ReactNode } from 'react'
import styles from './InputGroup.module.css'
const InputGroup = ({ children }: { children: any }) => (
const InputGroup = ({ children }: { children: ReactNode }): ReactElement => (
<div className={styles.inputGroup}>{children}</div>
)

View File

@ -1,3 +1,17 @@
.field {
margin-bottom: var(--spacer);
position: relative;
}
.field .field {
margin-bottom: calc(var(--spacer) / 2);
}
.error {
font-size: var(--font-size-small);
color: var(--brand-alert-red);
position: absolute;
text-align: right;
right: 0;
top: 0;
}

View File

@ -3,6 +3,7 @@ import InputElement from './InputElement'
import Help from './Help'
import Label from './Label'
import styles from './index.module.css'
import { ErrorMessage } from 'formik'
export interface InputProps {
name: string
@ -35,15 +36,8 @@ export interface InputProps {
}
}
export default function Input(props: InputProps) {
const {
required,
name,
label,
help,
additionalComponent,
field
} = props as Partial<InputProps>
export default function Input(props: Partial<InputProps>): ReactElement {
const { required, name, label, help, additionalComponent, field } = props
return (
<div className={styles.field}>
@ -52,6 +46,12 @@ export default function Input(props: InputProps) {
</Label>
<InputElement {...field} {...props} />
{field && (
<div className={styles.error}>
<ErrorMessage name={field.name} />
</div>
)}
{help && <Help>{help}</Help>}
{additionalComponent && additionalComponent}
</div>

View File

@ -12,7 +12,3 @@ export const Normal = () => <Loader />
export const WithMessage = () => (
<Loader message="Crunching all the tech for you..." />
)
export const WithMessageHorizontal = () => (
<Loader message="Crunching all the tech for you..." isHorizontal />
)

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { ReactElement } from 'react'
import Eye from '../../../images/eye.svg'
import Button from '../Button'
import Tooltip from '../Tooltip'
@ -10,7 +10,7 @@ export declare type ActionsCellProps = {
export default function ActionsCell({
handleOnClickViewJobDetails
}: ActionsCellProps) {
}: ActionsCellProps): ReactElement {
return (
<>
{handleOnClickViewJobDetails && (

View File

@ -7,4 +7,9 @@ export default {
title: 'Molecules/Asset Teaser'
}
export const Default = () => <AssetTeaser ddo={new DDO(ddo)} />
export const Default = () => (
<AssetTeaser
did={ddo.id}
metadata={new DDO(ddo).findServiceByType('metadata').attributes as any}
/>
)

View File

@ -1,8 +1,9 @@
.info {
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 2);
border: 1px solid var(--brand-grey-light);
background-color: var(--brand-grey-lighter);
border: 1px solid var(--brand-grey-lighter);
background-color: var(--brand-grey-dimmed);
position: relative;
}
.url {
@ -34,6 +35,6 @@
right: 0;
font-size: var(--font-size-h3);
cursor: pointer;
color: var(--color-secondary);
color: var(--brand-grey);
background-color: transparent;
}

View File

@ -0,0 +1,27 @@
import React, { ReactElement } from 'react'
import { File } from '@oceanprotocol/squid'
import { prettySize } from '../../../utils'
import cleanupContentType from '../../../utils/cleanupContentType'
import styles from './Info.module.css'
export default function FileInfo({
file,
removeItem
}: {
file: File
removeItem(): void
}): ReactElement {
return (
<div className={styles.info}>
<h3 className={styles.url}>{file.url}</h3>
<ul>
<li>URL confirmed</li>
{file.contentLength && <li>{prettySize(+file.contentLength)}</li>}
{file.contentType && <li>{cleanupContentType(file.contentType)}</li>}
</ul>
<button className={styles.removeButton} onClick={() => removeItem()}>
&times;
</button>
</div>
)
}

View File

@ -0,0 +1,3 @@
.input {
composes: input from '../../atoms/Input/InputElement.module.css';
}

View File

@ -0,0 +1,38 @@
import React, { ReactElement } from 'react'
import isUrl from 'is-url-superb'
import Button from '../../atoms/Button'
import { useField } from 'formik'
import Loader from '../../atoms/Loader'
import InputElement from '../../atoms/Input/InputElement'
import { InputProps } from '../../atoms/Input'
import styles from './Input.module.css'
import InputGroup from '../../atoms/Input/InputGroup'
export default function FileInput({
handleButtonClick,
isLoading,
...props
}: {
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
}): ReactElement {
const [field, meta] = useField(props as InputProps)
return (
<InputGroup>
<input className={styles.input} {...props} type="url" />
<Button
size="small"
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, field.value)}
disabled={
!field.value ||
// weird static page build fix so is-url-superb won't error
!isUrl(typeof field.value === 'string' ? field.value : '')
}
>
{isLoading ? <Loader /> : 'Add File'}
</Button>
</InputGroup>
)
}

View File

@ -0,0 +1,51 @@
import React, { ReactElement, useState } from 'react'
import { useField } from 'formik'
import { toast } from 'react-toastify'
import FileInfo from './Info'
import FileInput from './Input'
import { getFileInfo } from '../../../utils'
import { InputProps } from '../../atoms/Input'
interface Values {
url: string
}
export default function FilesInput(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props)
const [isLoading, setIsLoading] = useState(false)
async function handleButtonClick(e: React.SyntheticEvent, url: string) {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault()
try {
setIsLoading(true)
const newFileInfo = await getFileInfo(url)
newFileInfo && helpers.setValue([newFileInfo])
} catch (error) {
toast.error('Could not fetch file info. Please check url and try again')
console.error(error.message)
} finally {
setIsLoading(false)
}
}
function removeItem() {
helpers.setValue(undefined)
}
return (
<>
{typeof field.value === 'object' ? (
<FileInfo file={field.value[0]} removeItem={removeItem} />
) : (
<FileInput
{...props}
{...field}
isLoading={isLoading}
handleButtonClick={handleButtonClick}
/>
)}
</>
)
}

View File

@ -1,140 +0,0 @@
.row {
margin-bottom: var(--spacer);
}
.input,
.row input:not([type='radio']):not([type='checkbox']),
.row select,
.row textarea {
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-bold);
/* font-weight: var(--font-weight-bold); */
color: var(--brand-grey-dark);
border: 2px solid var(--brand-pink);
box-shadow: none;
width: 100%;
background: var(--brand-white);
padding: calc(var(--spacer) / 3);
margin: 0;
border-radius: var(--border-radius);
transition: 0.2s ease-out;
min-height: 43px;
appearance: none;
}
.input:focus,
.row input:focus:not([type='radio']):not([type='checkbox']),
.row select:focus,
.row textarea:focus {
box-shadow: none;
outline: 0;
border-color: var(--brand-pink);
}
.input::placeholder,
.row input::placeholder,
.row textarea::placeholder {
font-family: var(--font-family-base);
font-size: var(--font-size-base);
color: var(--color-secondary);
font-weight: var(--font-weight-base);
opacity: 0.7;
}
.input[readonly],
.input[disabled],
.row input[readonly],
.row input[disabled] {
background-color: var(--brand-grey-light);
cursor: not-allowed;
pointer-events: none;
}
.row textarea {
min-height: 5rem;
display: block;
}
.row select {
padding-right: 3rem;
/* custom arrow */
background-image: linear-gradient(
45deg,
transparent 50%,
var(--brand-pink) 50%
),
linear-gradient(135deg, var(--brand-pink) 50%, transparent 50%),
linear-gradient(to right, var(--brand-pink) 0px, var(--brand-white) 2px);
background-position: calc(100% - 18px) calc(1rem + 5px),
calc(100% - 13px) calc(1rem + 5px), 100% 0;
background-size: 5px 5px, 5px 5px, 2.5rem 4rem;
background-repeat: no-repeat;
cursor: pointer;
}
.checkbox label,
.radio label,
.row input[type='radio'] + span,
.row input[type='checkbox'] + span {
display: inline-block;
font-weight: var(--font-weight-base);
margin-bottom: calc(var(--spacer) / 4);
}
.row :global(.field-radio-group) {
border: 2px solid var(--brand-pink);
padding: calc(var(--spacer) / 3);
border-radius: var(--border-radius);
}
.labelHolder {
display: flex;
justify-content: space-between;
}
.label {
display: block;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin-bottom: calc(var(--spacer) / 8);
color: var(--color-secondary);
}
.req:after {
content: '*';
padding-left: calc(var(--spacer) / 8);
color: var(--color-primary);
}
.help {
font-size: var(--font-size-small);
color: var(--color-secondary);
margin-top: 0.25rem;
font-style: italic;
}
.errors {
padding-top: calc(var(--spacer) / 8);
font-size: var(--font-size-mini);
color: var(--color-primary);
text-transform: capitalize;
}
.error input:not([type='radio']):not([type='checkbox']),
.error select,
.error textarea {
border-color: var(--color-primary);
}
/* Size Modifiers */
.large,
.large::placeholder,
.large + button {
font-size: var(--font-size-large);
}
.large {
padding: calc(var(--spacer) / 2);
}

View File

@ -1,44 +0,0 @@
import React from 'react'
import { FieldTemplateProps } from 'react-jsonschema-form'
import styles from './FieldTemplate.module.css'
const noLabelFields = ['root', 'root_termsAndConditions', 'root_files_0']
// Ref: https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#field-template
export const FieldTemplate = ({
id,
label,
rawHelp,
required,
rawErrors,
children
}: FieldTemplateProps) => {
const noLabel = id !== noLabelFields.filter((f) => id === f)[0]
return (
<section
key={id}
className={
rawErrors !== undefined && rawErrors.length > 0
? `${styles.row} ${styles.error}`
: `${styles.row}`
}
>
<div className={styles.labelHolder}>
{noLabel && (
<label
className={
required ? `${styles.label} ${styles.req}` : styles.label
}
htmlFor={id}
>
{label}
</label>
)}
</div>
{children}
{rawErrors && <span className={styles.errors}>{rawErrors}</span>}
{rawHelp && <div className={styles.help}>{rawHelp}</div>}
</section>
)
}

View File

@ -1,21 +0,0 @@
import React from 'react'
import { File } from '@oceanprotocol/squid'
import { prettySize } from '../../../../utils'
import cleanupContentType from '../../../../utils/cleanupContentType'
import styles from './Info.module.css'
const FileInfo = ({ info, removeItem }: { info: File; removeItem(): void }) => (
<div className={styles.info}>
<h3 className={styles.url}>{info.url}</h3>
<ul>
<li>URL confirmed</li>
{info.contentLength && <li>{prettySize(+info.contentLength)}</li>}
{info.contentType && <li>{cleanupContentType(info.contentType)}</li>}
</ul>
<button className={styles.removeButton} onClick={() => removeItem()}>
&times;
</button>
</div>
)
export default FileInfo

View File

@ -1,34 +0,0 @@
import React, { ReactElement, ReactNode } from 'react'
import isUrl from 'is-url-superb'
import Loader from '../../../atoms/Loader'
import Button from '../../../atoms/Button'
import styles from './index.module.css'
const FileInput = ({
formData,
handleButtonClick,
isLoading,
children,
i
}: {
children: ReactNode
i: number
formData: string[]
handleButtonClick(e: React.SyntheticEvent, data: string): void
isLoading: boolean
}): ReactElement => (
<>
{children}
{formData[i] && (
<Button
className={styles.addButton}
onClick={(e: React.SyntheticEvent) => handleButtonClick(e, formData[i])}
disabled={!isUrl(formData[i])}
>
{isLoading ? <Loader /> : 'Add File'}
</Button>
)}
</>
)
export default FileInput

View File

@ -1,16 +0,0 @@
.arrayField {
position: relative;
}
.arrayField > section {
margin-bottom: 0;
}
.addButton {
margin-top: calc(var(--spacer) / 4);
}
.error {
border-color: var(--color-primary);
text-transform: capitalize;
}

View File

@ -1,63 +0,0 @@
import React, { useState } from 'react'
import { ArrayFieldTemplateProps } from 'react-jsonschema-form'
import { File } from '@oceanprotocol/squid'
import { toast } from 'react-toastify'
import useStoredValue from '../../../../hooks/useStoredValue'
import { getFileInfo } from '../../../../utils'
import FileInfo from './Info'
import FileInput from './Input'
import styles from './index.module.css'
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
const FileField = ({ items, formData }: ArrayFieldTemplateProps) => {
const [isLoading, setIsLoading] = useState(false)
// in order to access fileInfo as an array of objects upon formSubmit we need to keep it in localStorage
const [fileInfo, setFileInfo] = useStoredValue<File[]>(
FILES_DATA_LOCAL_STORAGE_KEY,
[]
)
const handleButtonClick = async (e: React.SyntheticEvent, url: string) => {
// File example 'https://oceanprotocol.com/tech-whitepaper.pdf'
e.preventDefault()
try {
setIsLoading(true)
const newFileInfo = await getFileInfo(url)
newFileInfo && setFileInfo([newFileInfo])
} catch (error) {
toast.error('Could not fetch file info. Please check url and try again')
console.error(error.message)
} finally {
setIsLoading(false)
}
}
const removeItem = () => {
setFileInfo([])
}
return (
<>
{items.map(({ children, key }, i) => (
<div key={key} className={styles.arrayField}>
{fileInfo[i] ? (
<FileInfo info={fileInfo[i]} removeItem={removeItem} />
) : (
<FileInput
formData={formData}
handleButtonClick={handleButtonClick}
i={i}
isLoading={isLoading}
>
{children}
</FileInput>
)}
</div>
))}
</>
)
}
export default FileField

View File

@ -1,15 +0,0 @@
import React from 'react'
import { ObjectFieldTemplateProps } from 'react-jsonschema-form'
// Template to render form
// https://react-jsonschema-form.readthedocs.io/en/latest/advanced-customization/#object-field-template
const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => (
<>
<h3>{props.title}</h3>
{props.properties.map(
(element: { content: React.ReactElement }) => element.content
)}
</>
)
export { ObjectFieldTemplate }

View File

@ -1,115 +0,0 @@
import React from 'react'
import FormJsonSchema, {
UiSchema,
IChangeEvent,
ISubmitEvent,
ErrorSchema,
AjvError
} from 'react-jsonschema-form'
import { JSONSchema6 } from 'json-schema'
import Button from '../../atoms/Button'
import styles from './index.module.css'
import { FieldTemplate } from './FieldTemplate'
import {
customWidgets,
PublishFormDataInterface
} from '../../../models/PublishForm'
// Overwrite default input fields
/*
AltDateTimeWidget
AltDateWidget
CheckboxWidget
ColorWidget
DateTimeWidget
DateWidget
EmailWidget
FileWidget
HiddenWidget
RadioWidget
RangeWidget
SelectWidget
CheckboxesWidget
UpDownWidget
TextareaWidget
PasswordWidget
TextWidget
URLWidget
*/
// Example of Custom Error
// REF: react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-error-messages
export const transformErrors = (errors: AjvError[]) => {
return errors.map((error: AjvError) => {
if (error.property === '.termsAndConditions') {
console.log('ERROR')
error.message = 'Required Field'
}
return error
})
}
const validate = (formData: PublishFormDataInterface, errors: any) => {
if (!formData.termsAndConditions) {
errors.termsAndConditions.addError('Required Field')
}
return errors
}
export declare type FormProps = {
buttonDisabled?: boolean
children?: React.ReactNode
schema: JSONSchema6
uiSchema: UiSchema
formData: PublishFormDataInterface
onChange: (
e: IChangeEvent<PublishFormDataInterface>,
es?: ErrorSchema
) => void
onSubmit: (e: ISubmitEvent<PublishFormDataInterface>) => void
onError: (e: AjvError) => void
showErrorList?: boolean
}
export default function Form({
children,
schema,
uiSchema,
formData,
onChange,
onSubmit,
onError,
showErrorList,
buttonDisabled
}: FormProps) {
return (
<FormJsonSchema
className={styles.form}
schema={schema}
formData={formData}
uiSchema={uiSchema}
onChange={(event: IChangeEvent<PublishFormDataInterface>) =>
onChange(event)
}
onSubmit={(event: ISubmitEvent<PublishFormDataInterface>) =>
onSubmit(event)
}
FieldTemplate={FieldTemplate}
onError={onError}
widgets={customWidgets}
noHtml5Validate
showErrorList={showErrorList}
validate={validate} // REF: https://react-jsonschema-form.readthedocs.io/en/latest/validation/#custom-validation
// liveValidate
transformErrors={transformErrors}
>
<div>
<Button disabled={buttonDisabled} style="primary">
Submit
</Button>
</div>
{children}
</FormJsonSchema>
)
}

View File

@ -1,9 +0,0 @@
.error {
background-color: var(--red);
}
.success {
background-color: var(--green);
}
.info {
background-color: var(--yellow);
}

View File

@ -1,203 +0,0 @@
import React, { useEffect, useState } from 'react'
import { useNavigate } from '@reach/router'
import Form from '../../molecules/Form/index'
import {
PublishFormSchema,
PublishFormUiSchema,
publishFormData,
PublishFormDataInterface
} from '../../../models/PublishForm'
import useStoredValue from '../../../hooks/useStoredValue'
import { MetaDataMarket } from '../../../@types/MetaData'
import { File, MetaData } from '@oceanprotocol/squid'
import { isBrowser, toStringNoMS } from '../../../utils'
import { toast } from 'react-toastify'
import styles from './PublishForm.module.css'
import utils from 'web3-utils'
import AssetModel from '../../../models/Asset'
import { useWeb3, useOcean } from '@oceanprotocol/react'
import {
Service,
ServiceCompute
} from '@oceanprotocol/squid/dist/node/ddo/Service'
const FILES_DATA_LOCAL_STORAGE_KEY = 'filesData'
const PUBLISH_FORM_LOCAL_STORAGE_KEY = 'publishForm'
export function getFilesData() {
let localFileData: File[] = []
if (isBrowser) {
const storedData = localStorage.getItem(FILES_DATA_LOCAL_STORAGE_KEY)
if (storedData) {
localFileData = localFileData.concat(JSON.parse(storedData) as File[])
}
}
return localFileData
}
export function clearFilesData() {
if (isBrowser)
localStorage.setItem(FILES_DATA_LOCAL_STORAGE_KEY, JSON.stringify([]))
}
export function transformPublishFormToMetadata(
data: PublishFormDataInterface
): MetaDataMarket {
const currentTime = toStringNoMS(new Date())
const {
title,
price,
author,
license,
summary,
holder,
keywords,
termsAndConditions,
supportName,
supportEmail,
dateRange,
access
} = data
const metadata: MetaDataMarket = {
main: {
...AssetModel.main,
name: title,
price: utils.toWei(price.toString()),
author,
dateCreated: currentTime,
datePublished: currentTime,
files: getFilesData(),
license
},
// ------- additional information -------
additionalInformation: {
...AssetModel.additionalInformation,
description: summary,
copyrightHolder: holder,
tags: keywords?.split(','),
termsAndConditions,
access: access || 'Download'
},
// ------- curation -------
curation: AssetModel.curation
}
if (dateRange) {
const newDateRange = JSON.parse(dateRange)
if (newDateRange.length > 1) {
metadata.additionalInformation.dateRange = JSON.parse(dateRange)
} else if (newDateRange.length === 1) {
// eslint-disable-next-line prefer-destructuring
metadata.main.dateCreated = newDateRange[0]
}
}
return metadata
}
const PublishForm: React.FC<any> = () => {
const [buttonDisabled, setButtonDisabled] = useState(false)
const { web3Connect } = useWeb3()
const { ocean, account } = useOcean()
const navigate = useNavigate()
const [data, updateData] = useStoredValue(
PUBLISH_FORM_LOCAL_STORAGE_KEY,
publishFormData
)
useEffect(() => {
setButtonDisabled(!ocean)
}, [ocean])
const handleChange = ({
formData
}: {
formData: PublishFormDataInterface
}) => {
updateData(formData)
}
const handleSubmit = async ({
formData
}: {
formData: PublishFormDataInterface
}) => {
setButtonDisabled(true)
const submittingToast = toast.info('submitting asset', {
className: styles.info
})
if (ocean == null) {
await web3Connect.connect()
}
if (ocean) {
const metadata = transformPublishFormToMetadata(formData)
// if services array stays empty, the default access service
// will be created by squid-js
let services: Service[] = []
if (metadata.additionalInformation.access === 'Compute') {
const computeService: ServiceCompute = await ocean.compute.createComputeServiceAttributes(
account,
metadata.main.price,
// Note: a hack without consequences.
// Will make metadata.main.datePublished (automatically created by Aquarius)
// go out of sync with this service.main.datePublished.
toStringNoMS(new Date(Date.now()))
)
services = [computeService]
}
try {
const asset = await ocean.assets.create(
(metadata as unknown) as MetaData,
account,
services
)
// Reset the form to initial values
updateData(publishFormData)
clearFilesData()
// User feedback and redirect
toast.success('asset created successfully', {
className: styles.success
})
toast.dismiss(submittingToast)
navigate(`/asset/${asset.id}`)
} catch (e) {
console.log(e)
} finally {
setButtonDisabled(false)
}
// Reset the form to initial values
// User feedback and redirect
}
}
const handleError = () =>
toast.error('Please check form. There are some errors', {
className: styles.error
})
return (
<Form
schema={PublishFormSchema}
uiSchema={PublishFormUiSchema}
formData={data}
onChange={handleChange}
onSubmit={handleSubmit}
onError={handleError}
showErrorList={false}
buttonDisabled={buttonDisabled}
/>
)
}
export default PublishForm

View File

@ -2,52 +2,10 @@
margin-bottom: var(--spacer);
}
.inputGroup > div {
.form > div > div {
margin: 0;
}
.inputGroup label {
.form label {
display: none;
}
.inputGroup input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.inputGroup button {
border-top-left-radius: 0;
border-top-right-radius: 0;
margin-top: -1px;
width: 100%;
}
.inputGroup button:hover,
.inputGroup button:focus,
.inputGroup input:focus + button:hover,
.inputGroup input:focus + button:focus {
background: var(--brand-gradient);
transform: none;
box-shadow: none;
}
@media screen and (min-width: 30rem) {
.inputGroup {
display: flex;
}
.inputGroup input {
border-bottom-left-radius: var(--border-radius);
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.inputGroup button {
border-top-right-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-top: 0;
margin-left: -1px;
width: auto;
}
}

View File

@ -4,6 +4,7 @@ import styles from './SearchBar.module.css'
import Loader from '../atoms/Loader'
import Button from '../atoms/Button'
import Input from '../atoms/Input'
import InputGroup from '../atoms/Input/InputGroup'
export default function SearchBar({
placeholder,
@ -35,7 +36,7 @@ export default function SearchBar({
return (
<form className={styles.form}>
<div className={styles.inputGroup}>
<InputGroup>
<Input
type="search"
name="search"
@ -47,7 +48,7 @@ export default function SearchBar({
<Button onClick={(e: FormEvent<HTMLButtonElement>) => startSearch(e)}>
{searchStarted ? <Loader /> : 'Search'}
</Button>
</div>
</InputGroup>
{filters && <fieldset className={styles.filters}>Type, Price</fieldset>}
</form>

View File

@ -15,7 +15,7 @@ import styles from './Compute.module.css'
import Button from '../../atoms/Button'
import Input from '../../atoms/Input'
import { MetaDataMarket } from '../../../@types/MetaData'
import { Alert } from '../../atoms/Alert'
import Alert from '../../atoms/Alert'
export default function Compute({
did,

View File

@ -1,18 +0,0 @@
import React from 'react'
import PublishForm from '../molecules/PublishForm/PublishForm'
import styles from './Publish.module.css'
import Web3Feedback from '../molecules/Wallet/Feedback'
const PublishPage: React.FC = () => {
return (
<article className={styles.grid}>
<PublishForm />
<aside>
<div className={styles.sticky}>
<Web3Feedback />
</div>
</aside>
</article>
)
}
export default PublishPage

View File

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

View File

@ -0,0 +1,147 @@
import React, { ReactElement } from 'react'
import * as Yup from 'yup'
import { toStringNoMS } from '../../../utils'
import { toast } from 'react-toastify'
import styles from './PublishForm.module.css'
import { useOcean } from '@oceanprotocol/react'
import {
Service,
ServiceCompute
} from '@oceanprotocol/squid/dist/node/ddo/Service'
import { Formik, Form as FormFormik, Field } from 'formik'
import Input from '../../atoms/Input'
import Button from '../../atoms/Button'
import { transformPublishFormToMetadata } from './utils'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetaDataPublishForm } from '../../../@types/MetaData'
import AssetModel from '../../../models/Asset'
import { File } from '@oceanprotocol/squid'
const validationSchema = Yup.object().shape<MetaDataPublishForm>({
// ---- required fields ----
name: Yup.string().required('Required'),
author: Yup.string().required('Required'),
price: Yup.string().required('Required'),
files: Yup.array<File>().required('Required').nullable(),
description: Yup.string().required('Required'),
license: Yup.string().required('Required'),
access: Yup.string().min(4).required('Required'),
termsAndConditions: Yup.boolean().required('Required'),
// ---- optional fields ----
copyrightHolder: Yup.string(),
tags: Yup.string(),
links: Yup.object<File[]>()
})
const initialValues: MetaDataPublishForm = {
name: undefined,
author: undefined,
price: undefined,
files: undefined,
description: undefined,
license: undefined,
access: undefined,
termsAndConditions: undefined,
copyrightHolder: undefined,
tags: undefined,
links: undefined
}
export default function PublishForm({
content
}: {
content: FormContent
}): ReactElement {
const { ocean, account } = useOcean()
async function handleSubmit(values: MetaDataPublishForm) {
const submittingToast = toast.info('submitting asset')
console.log(`
Collected form values:
----------------------
${values}
`)
const metadata = transformPublishFormToMetadata(values)
console.log(`
Transformed metadata values:
----------------------
${metadata}
`)
// if services array stays empty, the default access service
// will be created by squid-js
// let services: Service[] = []
// if (metadata.additionalInformation.access === 'Compute') {
// const computeService: ServiceCompute = await ocean.compute.createComputeServiceAttributes(
// account,
// metadata.main.price,
// // Note: a hack without consequences.
// // Will make metadata.main.datePublished (automatically created by Aquarius)
// // go out of sync with this service.main.datePublished.
// toStringNoMS(new Date(Date.now()))
// )
// services = [computeService]
// }
// try {
// const asset = await ocean.assets.create(
// (metadata as unknown) as MetaData,
// account,
// services
// )
// // TODO: Reset the form to initial values
// // User feedback and redirect
// toast.success('asset created successfully', {
// className: styles.success
// })
// toast.dismiss(submittingToast)
// // navigate(`/asset/${asset.id}`)
// } catch (e) {
// console.error(e.message)
// }
}
return (
<Formik
initialValues={initialValues}
initialStatus="empty"
validationSchema={validationSchema}
onSubmit={async (values, { setSubmitting }) => {
await handleSubmit(values)
setSubmitting(false)
}}
>
{({ isSubmitting, isValid, status, setStatus }) => (
<FormFormik
className={styles.form}
onChange={() => status === 'empty' && setStatus(null)}
>
{content.data.map((field: FormFieldProps) => (
<Field key={field.name} {...field} component={Input} />
))}
<Button
style="primary"
type="submit"
disabled={
!ocean ||
!account ||
isSubmitting ||
!isValid ||
status === 'empty'
}
>
Submit
</Button>
</FormFormik>
)}
</Formik>
)
}

View File

@ -0,0 +1,22 @@
import React, { ReactElement } from 'react'
import PublishForm from './PublishForm'
import styles from './index.module.css'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form'
export default function PublishPage({
content
}: {
content: { form: FormContent }
}): ReactElement {
return (
<article className={styles.grid}>
<PublishForm content={content.form} />
<aside>
<div className={styles.sticky}>
<Web3Feedback />
</div>
</aside>
</article>
)
}

View File

@ -0,0 +1,51 @@
import { MetaDataMarket, MetaDataPublishForm } from '../../../@types/MetaData'
import { toStringNoMS } from '../../../utils'
import AssetModel from '../../../models/Asset'
import web3Utils from 'web3-utils'
export function transformPublishFormToMetadata(
data: MetaDataPublishForm
): MetaDataMarket {
const currentTime = toStringNoMS(new Date())
const {
name,
price,
author,
license,
description,
copyrightHolder,
tags,
links,
termsAndConditions,
files,
access
} = data
const metadata: MetaDataMarket = {
main: {
...AssetModel.main,
name,
price: `${web3Utils.toWei(price.toString())}`,
author,
dateCreated: currentTime,
datePublished: currentTime,
files: typeof files !== 'string' && files,
license
},
additionalInformation: {
...AssetModel.additionalInformation,
description,
copyrightHolder,
tags: tags?.split(','),
// links: {
// url: links
// },
termsAndConditions,
access: access || 'Download'
},
curation: AssetModel.curation
}
return metadata
}

View File

@ -1,12 +1,19 @@
import React, { ReactElement, ReactNode } from 'react'
import { ToastContainer } from 'react-toastify'
import '@oceanprotocol/typographies/css/ocean-typo.css'
import '../global/styles.css'
import 'react-toastify/dist/ReactToastify.css'
export default function Styles({
children
}: {
children: ReactNode
}): ReactElement {
return <>{children}</>
return (
<>
{children}
<ToastContainer position="bottom-right" newestOnTop />
</>
)
}

30
src/global/_toast.css Normal file
View File

@ -0,0 +1,30 @@
div.Toastify__toast {
font-family: var(--font-family-base);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
border-radius: var(--border-radius);
box-shadow: 0 6px 15px 0 rgba(0, 0, 0, 0.05);
padding: calc(var(--spacer) / 2) var(--spacer);
background: var(--brand-white);
color: var(--brand-grey);
}
div.Toastify__toast--error {
background: var(--brand-alert-red);
color: var(--brand-white);
}
div.Toastify__toast--success {
background: var(--brand-alert-green);
color: var(--brand-white);
}
div.Toastify__toast--info {
background: var(--brand-white);
color: var(--brand-grey);
}
div.Toastify__toast--warning {
background: var(--brand-alert-yellow);
color: var(--brand-white);
}

View File

@ -133,3 +133,4 @@ fieldset {
}
@import '_code.css';
@import '_toast.css';

View File

@ -15,12 +15,11 @@ const AssetModel: MetaDataMarket = {
additionalInformation: {
description: '',
copyrightHolder: '',
tags: undefined,
// links: [],
tags: [],
links: [],
// custom items
termsAndConditions: false,
dateRange: undefined,
access: 'Download'
},
curation: {

View File

@ -1,188 +0,0 @@
import { UiSchema } from 'react-jsonschema-form'
import { JSONSchema6 } from 'json-schema'
import TermsWidget from '../components/atoms/FormWidgets/TermsWidget'
import DateRangeWidget from '../components/atoms/FormWidgets/DateRangeWidget'
import { ObjectFieldTemplate } from '../components/molecules/Form/ObjectFieldTemplate'
import { AccessType } from '../@types/MetaData'
import FileField from '../components/molecules/Form/FileField'
export const customWidgets = {
TermsWidget,
DateRangeWidget
}
export const PublishFormSchema: JSONSchema6 = {
type: 'object',
required: [
'title',
'author',
'license',
'price',
'files',
'summary',
'termsAndConditions',
'access'
],
definitions: {
files: {
type: 'object',
properties: {
name: {
type: 'string'
}
}
}
},
properties: {
title: {
type: 'string',
title: 'Offer Title'
},
summary: {
type: 'string',
title: 'Summary',
minLength: 24
},
files: {
type: 'array',
title: 'Data File URL',
items: {
type: 'string',
title: 'File URL',
format: 'uri'
}
},
price: {
title: 'Price',
type: 'number',
minimum: 0
},
author: {
type: 'string',
title: 'Author'
},
access: {
title: 'Access type',
type: 'string',
enum: ['Download', 'Compute']
},
license: {
title: 'License',
type: 'string',
enum: [
'Public Domain',
'PDDL: Public Domain Dedication and License',
'ODC-By: Attribution License',
'ODC-ODbL: Open Database License',
'CDLA-Sharing: Community Data License Agreement',
'CDLA-Permissive: Community Data License Agreement',
'CC0: Public Domain Dedication',
'CC BY: Attribution 4.0 International',
'CC BY-SA: Attribution-ShareAlike 4.0 International',
'CC BY-ND: Attribution-NoDerivatives 4.0 International',
'CC BY-NC: Attribution-NonCommercial 4.0 International',
'CC BY-NC-SA: Attribution-NonCommercial-ShareAlike 4.0 International',
'CC BY-NC-ND: Attribution-NonCommercial-NoDerivatives 4.0 International',
'No License Specified'
]
},
dateRange: {
type: 'string',
title: 'Creation Date'
},
holder: {
type: 'string',
title: 'Copyright Holder'
},
keywords: {
type: 'string',
title: 'Keywords'
},
termsAndConditions: {
type: 'boolean',
title: 'I agree to these Terms and Conditions'
}
}
}
// Widgets Ref https://react-jsonschema-form.readthedocs.io/en/latest/form-customization/#alternative-widgets
export const PublishFormUiSchema: UiSchema = {
'ui:ObjectFieldTemplate': ObjectFieldTemplate,
category: {
'ui:widget': 'radio'
},
title: {
'ui:placeholder': 'e.g. Shapes of Desert Plants',
'ui:help': 'Enter a concise title.'
},
summary: {
'ui:placeholder': 'Max of 1000 characters',
'ui:widget': 'textarea',
'ui:help': 'Add a thorough description with as much detail as possible.'
},
files: {
'ui:ArrayFieldTemplate': FileField,
items: {
'ui:placeholder': 'e.g. https://file.com/file.json',
'ui:widget': 'uri',
classNames: 'input-file'
},
'ui:help': 'Please provide a URL to your data set file.'
},
price: {
'ui:help': 'Set your price in Ocean Tokens.'
},
access: {
'ui:widget': 'select',
'ui:help': 'Access Type'
},
author: {
'ui:placeholder': 'e.g. Jelly McJellyfish',
'ui:help': 'Give proper attribution for your data set.'
},
license: {
'ui:widget': 'select'
},
dateRange: {
'ui:widget': 'DateRangeWidget',
'ui:help':
'Select the date the asset was created, or was updated for the last time.'
},
holder: {
'ui:placeholder': 'e.g. Marine Institute of Jellyfish'
},
keywords: {
'ui:placeholder': 'shipment, logistics'
},
termsAndConditions: {
'ui:widget': 'TermsWidget'
}
}
export interface PublishFormDataInterface {
// ---- required fields ----
summary: string
termsAndConditions: boolean
author: string
license: string
files: string[]
price: number
title: string
access?: AccessType
// ---- optional fields ----
dateRange?: string
holder?: string
keywords?: string
}
// Ref: https://github.com/oceanprotocol/OEPs/blob/master/8/v0.4/README.md#main-attributes
export const publishFormData: PublishFormDataInterface = {
author: '',
price: 0,
title: '',
files: [''], // should be empty string initially to display the expanded field
summary: '',
license: '',
termsAndConditions: false,
dateRange: undefined,
holder: undefined,
keywords: undefined
}

View File

@ -6,7 +6,7 @@ import { PageProps } from 'gatsby'
import { MetaDataMarket, ServiceMetaDataMarket } from '../../@types/MetaData'
import { Aquarius, Logger } from '@oceanprotocol/squid'
import { oceanConfig } from '../../../app.config'
import { Alert } from '../../components/atoms/Alert'
import Alert from '../../components/atoms/Alert'
export default function AssetRoute(props: PageProps): ReactElement {
const [metadata, setMetadata] = useState<MetaDataMarket>()

View File

@ -9,7 +9,7 @@ export default function PageGatsbyPublish(props: PageProps): ReactElement {
return (
<Layout title={title} description={description} uri={props.uri}>
<PagePublish />
<PagePublish content={content} />
</Layout>
)
}
@ -22,6 +22,20 @@ export const contentQuery = graphql`
childPagesJson {
title
description
form {
title
data {
name
placeholder
label
help
type
required
options
min
}
success
}
}
}
}

View File

@ -8,7 +8,7 @@ export function updateQueryStringParameter(
uri: string,
key: string,
newValue: string
) {
): string {
const regex = new RegExp('([?&])' + key + '=.*?(&|$)', 'i')
const separator = uri.indexOf('?') !== -1 ? '&' : '?'
@ -19,7 +19,11 @@ export function updateQueryStringParameter(
}
}
export function prettySize(bytes: number, separator = ' ', postFix = '') {
export function prettySize(
bytes: number,
separator = ' ',
postFix = ''
): string {
if (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const i = Math.min(
@ -36,7 +40,7 @@ export function prettySize(bytes: number, separator = ' ', postFix = '') {
// Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined'
export function formatNumber(number: number, format?: string) {
export function formatNumber(number: number, format?: string): string {
numeral.zeroFormat('0')
const defaultFormat = '0,0.000'
@ -54,13 +58,9 @@ export async function getFileInfo(url: string): Promise<File> {
data: { url }
})
if (response.status > 299) {
throw new Error(response.statusText)
}
if (!response.data.result) {
toast.error(response.data.message)
return { contentLength: undefined, contentType: '', url }
if (response.status > 299 || !response.data.result) {
toast.error('Could not connect to File API')
return
}
const { contentLength, contentType } = response.data.result
@ -72,7 +72,7 @@ export async function getFileInfo(url: string): Promise<File> {
}
}
export function isDid(did: string | undefined) {
export function isDid(did: string | undefined): boolean {
const didMatch = (did as string).match(/^did:op:([a-f0-9]{64})$/i)
return !!didMatch
}
@ -125,7 +125,7 @@ export function setProperty<T extends Record<string, unknown>>(
}
}
export function formatBytes(a: number, b: number) {
export function formatBytes(a: number, b: number): string {
if (a === 0) return '0 Bytes'
const c = 1024
const d = b || 2

View File

@ -8,7 +8,7 @@ describe('AssetModel', () => {
name: 'Hello'
}),
additionalInformation: Object.assign(AssetModel.additionalInformation, {
supportName: 'Jelly McJellyfish'
description: 'Jelly McJellyfish'
}),
curation: Object.assign(AssetModel.curation, {
numVotes: 100,
@ -18,6 +18,6 @@ describe('AssetModel', () => {
expect(newMeta).toMatchObject(AssetModel)
expect(newMeta.main.name).toBe('Hello')
expect(newMeta.additionalInformation.supportName).toBe('Jelly McJellyfish')
expect(newMeta.additionalInformation.description).toBe('Jelly McJellyfish')
})
})

View File

@ -72,7 +72,6 @@ const ddo: Partial<DDO> = {
}
],
termsAndConditions: true,
dateRange: ['2018-09-20T08:38:58', '2019-12-11T05:19:42'],
access: 'Download'
},
curation: {

View File

@ -1,13 +1,15 @@
import { PublishFormDataInterface } from '../../../src/models/PublishForm'
import { MetaDataPublishForm } from '../../../src/@types/MetaData'
const testFormData: PublishFormDataInterface = {
const testFormData: MetaDataPublishForm = {
author: '',
files: [],
license: '',
price: 0,
title: '',
summary: 'summary',
termsAndConditions: true
price: '0',
name: '',
description: 'description',
termsAndConditions: true,
access: 'Download'
// links: []
}
export default testFormData

View File

@ -1,55 +0,0 @@
import React from 'react'
import { render, act } from '@testing-library/react'
import DateRangeWidget, {
getWidgetValue
} from '../../../src/components/atoms/FormWidgets/DateRangeWidget'
import { PublishFormSchema } from '../../../src/models/PublishForm'
describe('Date Range Widget', () => {
it('renders without crashing', () => {
act(() => {
const { container } = render(
<DateRangeWidget
schema={PublishFormSchema}
id="1"
autofocus={false}
disabled={false}
label="Date Range"
formContext={{}}
readonly={false}
value={'["2020-03-15T15:13:30Z", "2020-03-18T15:13:30Z"]'}
onBlur={() => {
/* */
}}
onFocus={() => {
/* */
}}
onChange={() => {
/* */
}}
options={{}}
required={false}
/>
)
expect(container.firstChild).toBeInTheDocument()
})
})
it('getWidgetValue returns a correctly encoded string', () => {
expect(
getWidgetValue(
new Date('2020-03-15T15:13:30.123Z'),
new Date('2020-03-18T15:13:30.456Z'),
false
)
).toEqual('["2020-03-15T15:13:30Z","2020-03-15T15:13:30Z"]')
expect(
getWidgetValue(
new Date('2020-03-15T15:13:30.123Z'),
new Date('2020-03-18T18:13:30.456Z'),
true
)
).toEqual('["2020-03-15T15:13:30Z","2020-03-18T18:13:30Z"]')
})
})

View File

@ -12,7 +12,9 @@ describe('Layout', () => {
testRender(
<LocationProvider history={history}>
<Layout location={{ href: 'https://demo.com' } as Location}>Hello</Layout>
<Layout title="Hello" uri={history.location.href}>
Hello
</Layout>
</LocationProvider>
)
})

View File

@ -1,78 +1,30 @@
import React from 'react'
import { render } from '@testing-library/react'
import Form, { transformErrors } from '../../../src/components/molecules/Form'
import { transformPublishFormToMetadata } from '../../../src/components/pages/Publish/utils'
import {
publishFormData,
PublishFormDataInterface,
PublishFormSchema,
PublishFormUiSchema
} from '../../../src/models/PublishForm'
import testFormData from '../__fixtures__/testFormData'
import { transformPublishFormToMetadata } from '../../../src/components/molecules/PublishForm/PublishForm'
import { MetaDataMarket } from '../../../src/@types/MetaData'
MetaDataMarket,
MetaDataPublishForm
} from '../../../src/@types/MetaData'
import PublishForm from '../../../src/components/pages/Publish/PublishForm'
import publishFormData from '../__fixtures__/testFormData'
import content from '../../../content/pages/publish.json'
describe('PublishForm', () => {
it('renders without crashing', async () => {
const { container } = render(
<Form
schema={PublishFormSchema}
formData={testFormData}
uiSchema={PublishFormUiSchema}
onChange={() => null}
onSubmit={() => null}
onError={() => null}
>
Hello
</Form>
)
const { container } = render(<PublishForm content={content.form} />)
expect(container.firstChild).toBeInTheDocument()
})
it('transformErrors() passes through data', () => {
const errorsMock = [
{
message: 'Hello',
name: 'Hello',
params: 'Hello',
property: 'Hello',
stack: 'Hello'
}
]
const error = transformErrors(errorsMock)
expect(error[0].message).toBe('Hello')
})
it('transformErrors() transforms data', () => {
const errorsMock = [
{
message: 'Hello',
name: 'Hello',
params: 'Hello',
property: '.termsAndConditions',
stack: 'Hello'
}
]
const error = transformErrors(errorsMock)
expect(error[0].message).not.toBe('Hello')
})
it('Form data is correctly transformed to asset MetaData', () => {
const data: PublishFormDataInterface = publishFormData
const data: MetaDataPublishForm = publishFormData
let metadata: MetaDataMarket = transformPublishFormToMetadata(data)
expect(metadata.additionalInformation).toBeDefined()
expect(metadata.main).toBeDefined()
data.price = 1.3
data.dateRange = '["2020-03-05T15:17:31Z","2020-03-10T16:00:00Z"]'
data.price = '1.3'
metadata = transformPublishFormToMetadata(data)
expect(metadata.main.price).toBe('1300000000000000000')
expect(metadata.additionalInformation.dateRange).toEqual([
'2020-03-05T15:17:31Z',
'2020-03-10T16:00:00Z'
])
})
})