Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task/FP-119: Allow list of parameters #674

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
85 changes: 64 additions & 21 deletions client/src/components/Applications/AppForm/AppFormSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ const FormSchema = (app) => {
schema: { inputs: {}, parameters: {} },
};

Yup.addMethod(Yup.array, 'oneOfSchemas', function (schemas) {
return this.test(
'one-of-schemas',
'Not all items in ${path} match one of the allowed schemas',
(items) =>
items.every((item) => {
return schemas.some((schema) =>
schema.isValidSync(item, { strict: true })
);
})
);
});

(app.definition.parameters || []).forEach((parameter) => {
const param = parameter;
if (!param.value.visible || param.id.startsWith('_')) {
Expand All @@ -25,46 +38,72 @@ const FormSchema = (app) => {
required: param.value.required,
};

field.multiinput = false;
if (param.semantics.maxCardinality > 1) {
field.multiinput = true;
field.maxitems = param.semantics.maxCardinality;
field.minitems = param.semantics.minCardinality;
}

switch (param.value.type) {
case 'bool':
case 'flag':
field.type = 'checkbox';
field.checked = param.value.default || false;
appFields.schema.parameters[param.id] = Yup.boolean();
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(Yup.boolean())
: Yup.boolean();
break;

case 'enumeration':
field.type = 'select';
field.options = param.value.enum_values;
appFields.schema.parameters[param.id] = Yup.string().oneOf(
field.options.map((enumVal) => {
if (typeof enumVal === 'string') {
return enumVal;
}
return Object.keys(enumVal)[0];
})
);
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(
Yup.string().oneOf(
field.options.map((enumVal) => {
if (typeof enumVal === 'string') {
return enumVal;
}
return Object.keys(enumVal)[0];
})
)
)
: Yup.string().oneOf(
field.options.map((enumVal) => {
if (typeof enumVal === 'string') {
return enumVal;
}
return Object.keys(enumVal)[0];
})
);
break;

case 'number':
appFields.schema.parameters[param.id] = Yup.number();
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(Yup.number())
: Yup.number();
field.type = 'number';
break;

case 'string':
field.agaveFile = param.semantics.ontology.includes('agaveFile');
if (param.semantics.ontology.includes('email')) {
field.type = 'email';
appFields.schema.parameters[param.id] = Yup.string().email(
'Must be a valid email.'
);
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(Yup.string().email('Must be a valid email.\n'))
: Yup.string().email('Must be a valid email.');
} else {
field.type = 'text';
appFields.schema.parameters[param.id] = Yup.string();
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(Yup.string())
: Yup.string();
}
break;
default:
appFields.schema.parameters[param.id] = Yup.string();
appFields.schema.parameters[param.id] = field.multiinput
? Yup.array().of(Yup.string())
: Yup.string();
field.type = 'text';
}

Expand Down Expand Up @@ -99,12 +138,8 @@ const FormSchema = (app) => {
description: input.details.description,
required: input.value.required,
};
if (input.semantics.maxCardinality === 1) {
field.type = 'text';
} else {
field.type = 'array';
field.maxItems = input.semantics.maxCardinality;
}

field.type = 'text';
appFields.schema.inputs[input.id] = Yup.string();
if (input.value.required) {
appFields.schema.inputs[input.id] =
Expand All @@ -127,6 +162,14 @@ const FormSchema = (app) => {
input.value.default === null || typeof input.value.default === 'undefined'
? ''
: input.value.default;

field.multiinput = false;
if (input.semantics.maxCardinality > 1) {
field.multiinput = true;
field.maxitems = input.semantics.maxCardinality;
field.minitems = input.semantics.minCardinality;
appFields.schema.inputs[input.id] = Yup.array().of(Yup.string());
}
});
return appFields;
};
Expand Down
123 changes: 119 additions & 4 deletions client/src/components/_common/Form/FormField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from 'reactstrap';
import { Button } from '_common';

import { useField } from 'formik';
import { useField, Field, FieldArray } from 'formik';
import PropTypes from 'prop-types';
import './FormField.scss';

Expand Down Expand Up @@ -63,7 +63,8 @@ const FormField = ({
// which we can spread on <input> and also replace ErrorMessage entirely.
const [field, meta, helpers] = useField(props);
const [openAgaveFileModal, setOpenAgaveFileModal] = useState(false);
const { id, name } = props;
const [openMultiAgaveFileModal, setOpenMultiAgaveFileModal] = useState([]);
const { id, name, multiinput, maxitems, minitems } = props;
const hasAddon = addon !== undefined;
const wrapperType = hasAddon ? 'InputGroup' : '';

Expand All @@ -85,7 +86,12 @@ const FormField = ({
const FieldNote = () => (
<>
{meta.touched && meta.error ? (
<div className="form-field__validation-error">{meta.error}</div>
<div
style={{ whiteSpace: 'pre-wrap' }}
className="form-field__validation-error"
>
{meta.error}
</div>
) : (
description && (
<FormText className="form-field__help" color="muted">
Expand All @@ -111,7 +117,116 @@ const FormField = ({
{label && hasAddon ? <FieldLabel /> : null}
<FormFieldWrapper type={wrapperType}>
{label && !hasAddon ? <FieldLabel /> : null}
{agaveFile ? (
{multiinput ? (
<FieldArray
name={name}
render={(arrayHelpers) => (
<div>
{field.value &&
Array.isArray(field.value) &&
field.value.length > 0 ? (
field.value.map((_, index) => (
<div key={index}>
{agaveFile ? (
<>
<SelectModal
isOpen={openMultiAgaveFileModal.includes(index)}
toggle={() => {
setOpenMultiAgaveFileModal([]);
}}
onSelect={(system, path) => {
arrayHelpers.replace(
index,
`agave://${system}${path}`
);
}}
/>

<InputGroup className="multi-form-field">
<InputGroupAddon addonType="prepend">
<Button
size="sm"
color="secondary"
type="button"
onClick={() =>
setOpenMultiAgaveFileModal([index])
}
>
Select
</Button>
</InputGroupAddon>

<Input
tag={Field}
name={`${name}.${index}`}
type={props.type}
placeholder={props.placeholder}
component="input"
bsSize="sm"
/>

<InputGroupAddon addonType="append">
<Button
size="sm"
color="secondary"
type="button"
onClick={() => arrayHelpers.remove(index)}
>
X
</Button>
</InputGroupAddon>
</InputGroup>
</>
) : (
<>
{hasAddon && addonType === 'prepend' ? addon : null}
<InputGroup className="multi-form-field">
{props.type === 'select' ? (
<Input
{...props}
tag={Field}
name={`${name}.${index}`}
component={props.type}
bsSize="sm"
/>
) : (
<Input
{...props}
tag={Field}
name={`${name}.${index}`}
bsSize="sm"
/>
)}
<InputGroupAddon addonType="append">
<Button
size="sm"
color="secondary"
type="button"
onClick={() => arrayHelpers.remove(index)}
>
X
</Button>
</InputGroupAddon>
</InputGroup>
{hasAddon && addonType === 'append' ? addon : null}
</>
)}
</div>
))
) : (
<div>No Inputs</div>
)}
<Button
style={{ display: 'flex', justifyContent: 'right' }}
color="link"
onClick={() => arrayHelpers.push('')}
>
<b>+ Add Input</b>
</Button>
</div>
)}
/>
) : agaveFile ? (
<>
<SelectModal
isOpen={openAgaveFileModal}
Expand Down
4 changes: 4 additions & 0 deletions client/src/components/_common/Form/FormField.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@
color: #eb6e6e;
padding-top: 5px;
}

.multi-form-field {
padding-bottom: 10px;
}