Add new custom form field for single value selection (#504)

* created BoxSelection component

* wip

* design changes

* integrate with form

* refactoring

* used inside appearance component

* WIP, added space between options

* adde new fields to BoxSelection used for chain selection

* fixed errors

* removed access type option from publish.json

* updated component for chain selection

* updated for compute type on publish dataset

* added component to dockerImageOpions

* removed Dotdotdot component from options

* remove space

* styling updates, fix React warning for terms checkbox

Co-authored-by: Norbi <katunanorbert@gmai.com>
Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
Norbi 2021-05-17 16:32:01 +03:00 committed by GitHub
parent 82acbba5ce
commit f0ed9f68cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 261 additions and 60 deletions

View File

@ -28,8 +28,8 @@
"label": "Docker Image",
"placeholder": "e.g. python3.7",
"help": "Please select an image to run your algorithm.",
"type": "select",
"options": ["node:latest", "python:latest", "custom image"],
"type": "boxSelection",
"options": [],
"required": true
},
{

View File

@ -34,7 +34,7 @@
"name": "access",
"label": "Access Type",
"help": "Choose how you want your files to be accessible for the specified price.",
"type": "select",
"type": "boxSelection",
"options": ["Download", "Compute"],
"required": true
},

View File

@ -4,6 +4,9 @@ import styles from './InputElement.module.css'
import { InputProps } from '.'
import FilesInput from '../../molecules/FormFields/FilesInput'
import Terms from '../../molecules/FormFields/Terms'
import BoxSelection, {
BoxSelectionOption
} from '../../molecules/FormFields/BoxSelection'
import Datatoken from '../../molecules/FormFields/Datatoken'
import classNames from 'classnames/bind'
import AssetSelection, {
@ -91,7 +94,7 @@ export default function InputElement({
id={slugify(option)}
type={type}
name={name}
checked={props.defaultChecked}
defaultChecked={props.defaultChecked}
{...props}
/>
<label className={styles.radioLabel} htmlFor={slugify(option)}>
@ -125,6 +128,15 @@ export default function InputElement({
return <Datatoken name={name} {...field} {...props} />
case 'terms':
return <Terms name={name} options={options} {...field} {...props} />
case 'boxSelection':
return (
<BoxSelection
name={name}
options={(options as unknown) as BoxSelectionOption[]}
{...field}
{...props}
/>
)
default:
return prefix || postfix ? (
<div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>

View File

@ -0,0 +1,57 @@
.boxSelectionsWrapper {
display: flex;
justify-content: space-between;
gap: 0 calc(var(--spacer) / 4);
}
.boxSelectionsWrapper > div {
width: 100%;
}
.boxSelection {
display: block;
flex: 1 1 0px;
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 2)
calc(var(--spacer) / 4) calc(var(--spacer) / 2) !important;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
color: var(--color-secondary);
cursor: pointer;
}
.disabled {
pointer-events: none;
opacity: 0.5;
}
.title {
font-weight: var(--font-weight-bold);
text-align: center;
}
.boxSelectionsWrapper input[type='radio'] {
position: fixed;
opacity: 0;
pointer-events: none;
}
input[type='radio']:checked + label {
color: var(--font-color-text);
border-color: var(--color-secondary);
}
.boxSelection svg {
width: var(--font-size-h4);
height: var(--font-size-h4);
fill: currentColor;
margin-bottom: calc(var(--spacer) / 5);
}
.boxSelectionsWrapper label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
color: var(--color-secondary);
font-weight: normal;
}

View File

@ -0,0 +1,70 @@
import React, { ChangeEvent } from 'react'
import classNames from 'classnames/bind'
import Loader from '../../atoms/Loader'
import styles from './BoxSelection.module.css'
const cx = classNames.bind(styles)
export interface BoxSelectionOption {
name: string
checked: boolean
title: JSX.Element | string
icon?: JSX.Element
text?: JSX.Element | string
}
export default function BoxSelection({
name,
options,
disabled,
handleChange,
...props
}: {
name: string
options: BoxSelectionOption[]
disabled?: boolean
handleChange?: (event: ChangeEvent<HTMLInputElement>) => void
}): JSX.Element {
const styleClassesWrapper = cx({
boxSelectionsWrapper: true,
[styles.disabled]: disabled
})
const styleClassesInput = cx({
input: true,
radio: true
})
return (
<div className={styleClassesWrapper}>
{!options ? (
<Loader />
) : (
options.map((value: BoxSelectionOption) => (
<div key={value.name}>
<input
id={value.name}
type="radio"
className={styleClassesInput}
defaultChecked={value.checked}
onChange={(event) => handleChange(event)}
{...props}
disabled={disabled}
value={value.name}
name={name}
/>
<label
className={`${styles.boxSelection} ${styles.label}`}
htmlFor={value.name}
title={value.name}
>
{value.icon}
<span className={styles.title}>{value.title}</span>
{value.text}
</label>
</div>
))
)}
</div>
)
}

View File

@ -30,3 +30,7 @@
color: var(--font-color-text);
border-color: var(--color-secondary);
}
.appearances div[class*='boxSelectionsWrapper'] {
padding-bottom: calc(var(--spacer) / 8);
}

View File

@ -1,42 +1,44 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, ChangeEvent } from 'react'
import { DarkMode } from 'use-dark-mode'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label'
import styles from './Appearance.module.css'
import { ReactComponent as Moon } from '../../../images/moon.svg'
import { ReactComponent as Sun } from '../../../images/sun.svg'
const buttons = ['Light', 'Dark']
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Appearance.module.css'
export default function Appearance({
darkMode
}: {
darkMode: DarkMode
}): ReactElement {
return (
<li>
<Label htmlFor="">Appearance</Label>
<div className={styles.buttons}>
{buttons.map((button) => {
const isDark = button === 'Dark'
const selected =
(isDark && darkMode.value) || (!isDark && !darkMode.value)
const options: BoxSelectionOption[] = [
{
name: 'Light',
checked: !darkMode.value,
title: 'Light',
icon: <Sun />
},
{
name: 'Dark',
checked: darkMode.value,
title: 'Dark',
icon: <Moon />
}
]
return (
<Button
key={button}
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small"
style="text"
onClick={() => (isDark ? darkMode.enable() : darkMode.disable())}
>
{isDark ? <Moon /> : <Sun />}
{button}
</Button>
)
})}
</div>
function handleChange(event: ChangeEvent<HTMLInputElement>) {
event.target.value === 'Dark' ? darkMode.enable() : darkMode.disable()
}
return (
<li className={styles.appearances}>
<Label htmlFor="">Appearance</Label>
<BoxSelection
options={options}
name="appearanceMode"
handleChange={handleChange}
/>
<FormHelp>Defaults to your OS setting, select to override.</FormHelp>
</li>
)

View File

@ -17,3 +17,14 @@
.selected {
composes: selected from './Appearance.module.css';
}
.chains div[class*='boxSelectionsWrapper'] {
display: grid;
gap: calc(var(--spacer) / 4);
grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
padding-bottom: calc(var(--spacer) / 8);
}
.chains label[class*='boxSelection'] {
padding: calc(var(--spacer) / 3) calc(var(--spacer) / 4) !important;
}

View File

@ -1,53 +1,58 @@
import { ConfigHelperConfig } from '@oceanprotocol/lib'
import React, { ReactElement } from 'react'
import React, { ReactElement, ChangeEvent } from 'react'
import { useOcean } from '../../../providers/Ocean'
import { useWeb3 } from '../../../providers/Web3'
import { getOceanConfig } from '../../../utils/ocean'
import Button from '../../atoms/Button'
import FormHelp from '../../atoms/Input/Help'
import Label from '../../atoms/Input/Label'
import BoxSelection, { BoxSelectionOption } from '../FormFields/BoxSelection'
import styles from './Chain.module.css'
export default function Chain(): ReactElement {
const { web3 } = useWeb3()
const { config, connect } = useOcean()
async function connectOcean(networkName: string) {
const config = getOceanConfig(networkName)
async function connectOcean(event: ChangeEvent<HTMLInputElement>) {
const config = getOceanConfig(event.target.value)
await connect(config)
}
const chains = [
{ name: 'ETH', oceanConfig: 'mainnet', label: 'Mainnet' },
{ name: 'Polygon/Matic', oceanConfig: 'polygon', label: 'Mainnet' },
{ name: 'Moonbase Alpha', oceanConfig: 'moonbeamalpha', label: 'Testnet' }
function isNetworkSelected(oceanConfig: string) {
return (config as ConfigHelperConfig).network === oceanConfig
}
const options: BoxSelectionOption[] = [
{
name: 'mainnet',
checked: isNetworkSelected('mainnet'),
title: 'ETH',
text: 'Mainnet'
},
{
name: 'polygon',
checked: isNetworkSelected('polygon'),
title: 'Polygon/Matic',
text: 'Mainnet'
},
{
name: 'moonbeamalpha',
checked: isNetworkSelected('moonbeamalpha'),
title: 'Moonbase Alpha',
text: 'Testnet'
}
]
// TODO: to fully solve https://github.com/oceanprotocol/market/issues/432
// there are more considerations for users with a wallet connected (wallet network vs. setting network).
// For now, only show the setting for non-wallet users.
return !web3 ? (
<li>
<li className={styles.chains}>
<Label htmlFor="">Chain</Label>
<div className={styles.buttons}>
{chains.map((button) => {
const selected =
(config as ConfigHelperConfig).network === button.oceanConfig
return (
<Button
key={button.name}
className={`${styles.button} ${selected ? styles.selected : ''}`}
size="small"
style="text"
onClick={() => connectOcean(button.oceanConfig)}
>
{button.name}
<span>{button.label}</span>
</Button>
)
})}
</div>
<BoxSelection
options={options}
name="chain"
handleChange={connectOcean}
/>
<FormHelp>Switch the data source for the interface.</FormHelp>
</li>
) : null

View File

@ -14,7 +14,6 @@ import Button from '../../atoms/Button'
import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormAlgorithm } from '../../../@types/MetaData'
import { initialValues as initialValuesAlgorithm } from '../../../models/FormAlgoPublish'
import stylesIndex from './index.module.css'
const query = graphql`
@ -63,6 +62,24 @@ export default function FormPublish(): ReactElement {
initialValues.dockerImage
)
const dockerImageOptions = [
{
name: 'node:latest',
title: 'node:latest',
checked: true
},
{
name: 'python:latest',
title: 'python:latest',
checked: false
},
{
name: 'custom image',
title: 'custom image',
checked: false
}
]
// reset form validation on every mount
useEffect(() => {
setErrors({})
@ -135,6 +152,11 @@ export default function FormPublish(): ReactElement {
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection'
? dockerImageOptions
: field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)

View File

@ -7,6 +7,8 @@ import { FormContent, FormFieldProps } from '../../../@types/Form'
import { MetadataPublishFormDataset } from '../../../@types/MetaData'
import { initialValues as initialValuesDataset } from '../../../models/FormAlgoPublish'
import { useOcean } from '../../../providers/Ocean'
import { ReactComponent as Download } from '../../../images/download.svg'
import { ReactComponent as Compute } from '../../../images/compute.svg'
import stylesIndex from './index.module.css'
import styles from './FormPublish.module.css'
@ -61,6 +63,19 @@ export default function FormPublish(): ReactElement {
// setSubmitting(false)
}, [setErrors, setTouched])
const accessTypeOptions = [
{
name: 'Download',
title: 'Download',
icon: <Download />
},
{
name: 'Compute',
title: 'Compute',
icon: <Compute />
}
]
// Manually handle change events instead of using `handleChange` from Formik.
// Workaround for default `validateOnChange` not kicking in
function handleFieldChange(
@ -94,6 +109,9 @@ export default function FormPublish(): ReactElement {
<Field
key={field.name}
{...field}
options={
field.type === 'boxSelection' ? accessTypeOptions : field.options
}
component={Input}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
handleFieldChange(e, field)