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

Set, edit, and display timeout (#324)

* added timeout to publish asset

* add timeout to edit asses(wip)

* added timout to edit metadata form

* fixed wrong constant name

* fix options autosorting

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

* Fixed autosorting in edit form

* Added "1 day" to timeout options

* Changed ternary operators to switch

* Feature/asset timeout (#325)

* Compute asset timeout

* Code styled

* Deleted unused import

* Display timeout for buy/download

* Switch case for timeout values

* Moved mapping function to /utils/metadata

* display timeout option not matching defined ones, map seconds to string

* handle update with no predefined timeout value, add weeks to map method

* Display timeout on button

* consume button text logic change

* whoops, revert wrong change

* small millisecondsToStr refactor

* copy tweaks

* template literal logic restore

* keep tweaking help text logic

* abstract into method
* change whole condition logic
* tweak hasDatatoken/hasPreviousOrder combination condition

* Unified seconds to string conversion methods

* getHelpText tweaks, small refactor

* copy editing, limit hardcoded timeout list

* fix mixup of map & filter

* use Timeout as label and be done with it

Co-authored-by: mihaisc <mihai.scarlat@smartcontrol.ro>
Co-authored-by: claudiaHash <49017601+claudiaHash@users.noreply.github.com>
Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
Co-authored-by: Claudia Holhos <clawww1996@gmail.com>
This commit is contained in:
Norby 2021-01-22 18:12:48 +02:00 committed by GitHub
parent 7b854e09dd
commit a2fe2fdee0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 234 additions and 32 deletions

View File

@ -19,6 +19,15 @@
"type": "textarea",
"rows": 10,
"required": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
}
]
}

View File

@ -42,6 +42,15 @@
"options": ["Download"],
"required": true
},
{
"name": "timeout",
"label": "Timeout",
"help": "Define how long buyers should be able to download the data set again after the initial purchase.",
"type": "select",
"options": ["Forever", "1 day", "1 week", "1 month", "1 year"],
"sortOptions": false,
"required": true
},
{
"name": "dataTokenOptions",
"label": "Datatoken Name & Symbol",

View File

@ -3,6 +3,7 @@ export interface FormFieldProps {
name: string
type?: string
options?: string[]
sortOptions?: boolean
required?: boolean
help?: string
placeholder?: string

View File

@ -30,6 +30,7 @@ export interface MetadataPublishForm {
description: string
files: string | File[]
author: string
timeout: string
dataTokenOptions: DataTokenOptions
access: 'Download' | 'Compute' | string
termsAndConditions: boolean

View File

@ -21,6 +21,10 @@
composes: help from './index.module.css';
}
.help:not(:empty) {
margin-top: calc(var(--spacer) / 2);
}
.feedback {
width: 100%;
}

View File

@ -11,6 +11,27 @@ import { useOcean, useConsume, usePricing } from '@oceanprotocol/react'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import checkPreviousOrder from '../../../utils/checkPreviousOrder'
import { useAsset } from '../../../providers/Asset'
import { secondsToString } from '../../../utils/metadata'
function getHelpText(
token: {
dtBalance: string
dtSymbol: string
},
hasDatatoken: boolean,
hasPreviousOrder: boolean,
timeout: string
) {
const { dtBalance, dtSymbol } = token
const assetTimeout = timeout === 'Forever' ? '' : ` for ${timeout}`
const text = hasPreviousOrder
? `You bought this data set already allowing you to download it without paying again${assetTimeout}.`
: hasDatatoken
? `You own ${dtBalance} ${dtSymbol} allowing you to use this data set by spending 1 ${dtSymbol}, but without paying OCEAN again.`
: `For using this data set, you will buy 1 ${dtSymbol} and immediately spend it back to the publisher and pool.`
return text
}
export default function Consume({
ddo,
@ -35,6 +56,12 @@ export default function Consume({
const [isDisabled, setIsDisabled] = useState(true)
const [hasDatatoken, setHasDatatoken] = useState(false)
const [isConsumable, setIsConsumable] = useState(true)
const [assetTimeout, setAssetTimeout] = useState('')
useEffect(() => {
const { timeout } = ddo.findServiceByType('access').attributes.main
setAssetTimeout(secondsToString(timeout))
}, [ddo])
useEffect(() => {
if (!price) return
@ -72,6 +99,7 @@ export default function Consume({
if (!ocean || !accountId) return
async function checkOrders() {
// HEADS UP! checkPreviousOrder() also checks for expiration of possible set timeout.
const orderId = await checkPreviousOrder(ocean, accountId, ddo, 'access')
setPreviousOrderId(orderId)
setHasPreviousOrder(!!orderId)
@ -104,20 +132,20 @@ export default function Consume({
) : (
<>
<Button style="primary" onClick={handleConsume} disabled={isDisabled}>
{hasDatatoken || hasPreviousOrder ? 'Download' : 'Buy'}
{hasPreviousOrder
? 'Download'
: `Buy ${
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`
}`}
</Button>
{hasDatatoken && (
<div className={styles.help}>
You own {dtBalance} {ddo.dataTokenInfo.symbol} allowing you to use
this data set without paying again.
</div>
{getHelpText(
{ dtBalance, dtSymbol: ddo.dataTokenInfo.symbol },
hasDatatoken,
hasPreviousOrder,
assetTimeout
)}
{(!hasDatatoken || !hasPreviousOrder) && (
<div className={styles.help}>
For using this data set, you will buy 1 {ddo.dataTokenInfo.symbol}{' '}
and immediately spend it back to the publisher and pool.
</div>
)}
</>
)}
</div>

View File

@ -6,13 +6,52 @@ import Input from '../../../atoms/Input'
import { useOcean } from '@oceanprotocol/react'
import { FormFieldProps } from '../../../../@types/Form'
import { MetadataPublishForm } from '../../../../@types/MetaData'
import { checkIfTimeoutInPredefinedValues } from '../../../../utils/metadata'
function handleTimeoutCustomOption(
data: FormFieldProps[],
values: Partial<MetadataPublishForm>
) {
const timeoutFieldContent = data.filter(
(field) => field.name === 'timeout'
)[0]
const timeoutInputIndex = data.findIndex(
(element) => element.name === 'timeout'
)
if (
data[timeoutInputIndex].options.length < 6 &&
!checkIfTimeoutInPredefinedValues(
values.timeout,
timeoutFieldContent.options
)
) {
data[timeoutInputIndex].options.push(values.timeout)
} else if (
data[timeoutInputIndex].options.length === 6 &&
checkIfTimeoutInPredefinedValues(
values.timeout,
timeoutFieldContent.options
)
) {
data[timeoutInputIndex].options.pop()
} else if (
data[timeoutInputIndex].options.length === 6 &&
data[timeoutInputIndex].options[5] !== values.timeout
) {
data[timeoutInputIndex].options[5] = values.timeout
}
}
export default function FormEditMetadata({
data,
setShowEdit
setShowEdit,
setTimeoutStringValue,
values
}: {
data: FormFieldProps[]
setShowEdit: (show: boolean) => void
setTimeoutStringValue: (value: string) => void
values: Partial<MetadataPublishForm>
}): ReactElement {
const { ocean, accountId } = useOcean()
const {
@ -31,6 +70,11 @@ export default function FormEditMetadata({
setFieldValue(field.name, e.target.value)
}
// This component is handled by Formik so it's not rendered like a "normal" react component,
// so handleTimeoutCustomOption is called only once.
// https://github.com/oceanprotocol/market/pull/324#discussion_r561132310
if (data && values) handleTimeoutCustomOption(data, values)
return (
<Form className={styles.form}>
{data.map((field: FormFieldProps) => (
@ -45,7 +89,11 @@ export default function FormEditMetadata({
))}
<footer className={styles.actions}>
<Button style="primary" disabled={!ocean || !accountId || !isValid}>
<Button
style="primary"
disabled={!ocean || !accountId || !isValid}
onClick={() => setTimeoutStringValue(values.timeout)}
>
Submit
</Button>
<Button style="text" onClick={() => setShowEdit(false)}>

View File

@ -12,6 +12,7 @@ import MetadataPreview from '../../../molecules/MetadataPreview'
import Debug from './Debug'
import Web3Feedback from '../../../molecules/Wallet/Feedback'
import FormEditMetadata from './FormEditMetadata'
import { mapTimeoutStringToSeconds } from '../../../../utils/metadata'
import styles from './index.module.css'
import { Logger } from '@oceanprotocol/lib'
import MetadataFeedback from '../../../molecules/MetadataFeedback'
@ -35,6 +36,7 @@ const contentQuery = graphql`
help
type
required
sortOptions
options
rows
}
@ -59,6 +61,7 @@ export default function Edit({
const { metadata, ddo, refreshDdo } = useAsset()
const [success, setSuccess] = useState<string>()
const [error, setError] = useState<string>()
const [timeoutStringValue, setTimeoutStringValue] = useState<string>()
const hasFeedback = error || success
@ -68,24 +71,46 @@ export default function Edit({
) {
try {
// Construct new DDO with new values
const newDdo = await ocean.assets.editMetadata(ddo, {
const ddoEditedMetdata = await ocean.assets.editMetadata(ddo, {
title: values.name,
description: values.description
})
// Update DDO on-chain
const tx = await ocean.assets.updateMetadata(newDdo, accountId)
if (!ddoEditedMetdata) {
setError(content.form.error)
Logger.error(content.form.error)
return
}
let ddoEditedTimeout = ddoEditedMetdata
if (timeoutStringValue !== values.timeout) {
const service = ddoEditedMetdata.findServiceByType('access')
const timeout = mapTimeoutStringToSeconds(values.timeout)
ddoEditedTimeout = await ocean.assets.editServiceTimeout(
ddoEditedMetdata,
service.index,
timeout
)
}
// Edit failed
if (!newDdo || !tx) {
if (!ddoEditedTimeout) {
setError(content.form.error)
Logger.error(content.form.error)
return
}
const storedddo = await ocean.assets.updateMetadata(
ddoEditedTimeout,
accountId
)
if (!storedddo) {
setError(content.form.error)
Logger.error(content.form.error)
return
} else {
// Edit succeeded
setSuccess(content.form.success)
resetForm()
}
} catch (error) {
Logger.error(error.message)
setError(error.message)
@ -94,7 +119,10 @@ export default function Edit({
return (
<Formik
initialValues={getInitialValues(metadata)}
initialValues={getInitialValues(
metadata,
ddo.findServiceByType('access').attributes.main.timeout
)}
validationSchema={validationSchema}
onSubmit={async (values, { resetForm }) => {
// move user's focus to top of screen
@ -103,7 +131,7 @@ export default function Edit({
await handleSubmit(values, resetForm)
}}
>
{({ isSubmitting, values }) =>
{({ isSubmitting, values, initialValues }) =>
isSubmitting || hasFeedback ? (
<MetadataFeedback
title="Updating Data Set"
@ -125,6 +153,8 @@ export default function Edit({
<FormEditMetadata
data={content.form.data}
setShowEdit={setShowEdit}
setTimeoutStringValue={setTimeoutStringValue}
values={initialValues}
/>
<aside>

View File

@ -6,7 +6,10 @@ import FormPublish from './FormPublish'
import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from '../../../models/FormPublish'
import { transformPublishFormToMetadata } from '../../../utils/metadata'
import {
transformPublishFormToMetadata,
mapTimeoutStringToSeconds
} from '../../../utils/metadata'
import MetadataPreview from '../../molecules/MetadataPreview'
import { MetadataPublishForm } from '../../../@types/MetaData'
import { useUserPreferences } from '../../../providers/UserPreferences'
@ -37,6 +40,8 @@ export default function PublishPage({
resetForm: () => void
): Promise<void> {
const metadata = transformPublishFormToMetadata(values)
const timeout = mapTimeoutStringToSeconds(values.timeout)
const serviceType = values.access === 'Download' ? 'access' : 'compute'
try {
@ -50,7 +55,8 @@ export default function PublishPage({
const ddo = await publish(
(metadata as unknown) as Metadata,
serviceType,
values.dataTokenOptions
values.dataTokenOptions,
timeout
)
// Publish failed

View File

@ -31,6 +31,7 @@
--border-color: #e2e2e2;
--box-shadow-color: rgba(0, 0, 0, 0.05);
--font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif;
--font-family-heading: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,

View File

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

View File

@ -17,6 +17,7 @@ export const validationSchema: Yup.SchemaOf<MetadataPublishForm> = Yup.object()
.required('Required'),
files: Yup.array<FileMetadata>().required('Required').nullable(),
description: Yup.string().min(10).required('Required'),
timeout: Yup.string().required('Required'),
access: Yup.string()
.matches(/Compute|Download/g, { excludeEmptyString: true })
.required('Required'),
@ -37,6 +38,8 @@ export const initialValues: Partial<MetadataPublishForm> = {
},
files: '',
description: '',
timeout: 'Forever',
access: '',
termsAndConditions: false
termsAndConditions: false,
tags: ''
}

View File

@ -32,6 +32,7 @@ export const contentQuery = graphql`
help
type
required
sortOptions
options
}
success

View File

@ -5,7 +5,7 @@ export default async function checkPreviousOrder(
accountId: string,
ddo: DDO,
serviceType: ServiceType
) {
): Promise<string> {
if (!ocean) return
const service = ddo.findServiceByType(serviceType)

View File

@ -10,6 +10,62 @@ export function transformTags(value: string): string[] {
return transformedTags
}
export function mapTimeoutStringToSeconds(timeout: string): number {
switch (timeout) {
case 'Forever':
return 0
case '1 day':
return 86400
case '1 week':
return 604800
case '1 month':
return 2630000
case '1 year':
return 31556952
default:
return 0
}
}
function numberEnding(number: number): string {
return number > 1 ? 's' : ''
}
export function secondsToString(numberOfSeconds: number): string {
if (numberOfSeconds === 0) return 'Forever'
const years = Math.floor(numberOfSeconds / 31536000)
const weeks = Math.floor((numberOfSeconds %= 31536000) / 604800)
const days = Math.floor((numberOfSeconds %= 604800) / 86400)
const hours = Math.floor((numberOfSeconds %= 86400) / 3600)
const minutes = Math.floor((numberOfSeconds %= 3600) / 60)
const seconds = numberOfSeconds % 60
return years
? `${years} year${numberEnding(years)}`
: weeks
? `${weeks} week${numberEnding(weeks)}`
: days
? `${days} day${numberEnding(days)}`
: hours
? `${hours} hour${numberEnding(hours)}`
: minutes
? `${minutes} minute${numberEnding(minutes)}`
: seconds
? `${seconds} second${numberEnding(seconds)}`
: 'less than a second'
}
export function checkIfTimeoutInPredefinedValues(
timeout: string,
timeoutOptions: string[]
): boolean {
if (timeoutOptions.indexOf(timeout) > -1) {
return true
}
return false
}
export function transformPublishFormToMetadata(
{
name,

View File

@ -8,6 +8,7 @@ const testFormData: MetadataPublishForm = {
symbol: ''
},
name: '',
timeout: '',
description: 'description',
termsAndConditions: true,
access: 'Download'