diff --git a/client/src/project/new/ProjectNew.present.js b/client/src/project/new/ProjectNew.present.js index 0c01bd79e2..250419f59e 100644 --- a/client/src/project/new/ProjectNew.present.js +++ b/client/src/project/new/ProjectNew.present.js @@ -33,7 +33,7 @@ import { UncontrolledPopover, PopoverHeader, PopoverBody, Row, Table, UncontrolledTooltip } from "reactstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faInfoCircle, faLink, faQuestionCircle, faSyncAlt } from "@fortawesome/free-solid-svg-icons"; +import { faInfoCircle, faLink, faQuestionCircle, faSyncAlt, faUndo } from "@fortawesome/free-solid-svg-icons"; import { ButtonWithMenu, Clipboard, ErrorAlert, ExternalLink, FieldGroup, Loader, WarnAlert } from "../../utils/UIComponents"; @@ -943,6 +943,32 @@ function TemplateGalleryRow(props) { ); } +/** + * Create a "restore default" button. + * + * @param {function} restore - function to invoke + * @param {string} tip - message to display in the tooltip + * @param {boolean} disabled - whether it's disabled or not + */ +function RestoreButton(props) { + const { restore, name, disabled } = props; + + const id = `restore_${name}`; + const tip = disabled ? + "Default value already selected" : + "Restore default value"; + + return ( +
+ + {tip} +
+ ); +} + class Variables extends Component { render() { const { input, handlers } = this.props; @@ -956,19 +982,111 @@ class Variables extends Component { const template = templates.all.filter(t => t.id === input.template)[0]; if (!template || !template.variables || !Object.keys(template.variables).length) return null; - const variables = Object.keys(template.variables).map(variable => ( - - - handlers.setVariable(variable, e.target.value)} /> - {capitalize(template.variables[variable])} - - )); + const variables = Object.keys(template.variables).map(variable => { + const data = template.variables[variable]; + + // fallback to avoid breaking old variable structure + if (typeof data !== "object") { + return ( + + + handlers.setVariable(variable, e.target.value)} /> + {capitalize(template.variables[variable])} + + ); + } + + // expected `data` properties: default_value, description, enum, type. + // changing enum to enumValues to avoid using js reserved word + return ( + + ); + }); return variables; } } +function Variable(props) { + const { default_value, description, enumValues, handlers, input, name, type } = props; + const id = `parameter-${name}`; + + const descriptionOutput = description ? + ({capitalize(description)}) : + null; + + const defaultOutput = default_value != null ? + `Default: ${default_value}` : + null; + + const restoreButton = default_value != null ? + ( + handlers.setVariable(name, default_value)} + /> + ) : + null; + + let inputElement = null; + if (type === "boolean") { + inputElement = ( + + handlers.setVariable(name, e.target.checked)} + className="form-check-input rounded-pill" /> + + {restoreButton} + + ); + // inputElement = null; + } + else if (type === "enum") { + const enumObjects = enumValues.map(enumObject => { + const enumId = `enum-${id}-${enumObject.toString()}`; + return ( + + ); + }); + inputElement = ( + + {restoreButton} + handlers.setVariable(name, e.target.value)}> + {enumObjects} + + {descriptionOutput} + + ); + } + else { + const inputType = type === "number" ? + "number" : + "text"; + inputElement = ( + + {restoreButton} + handlers.setVariable(name, e.target.value)} + placeholder={defaultOutput} /> + {descriptionOutput} + + ); + } + + return inputElement; +} + class Create extends Component { constructor(props) { super(props); @@ -1078,6 +1196,25 @@ class Create extends Component { "based on " + (templates.all.find(t => t.id === input.template).name) : ""; + const errorFields = meta.validation.errors ? + Object.keys(meta.validation.errors) + .filter(field => !input[`${field}Pristine`]) // don't consider pristine fields + .map(field => capitalize(field)) : + []; + const plural = errorFields.length > 1 ? + "s" : + ""; + const errorMessage = errorFields.length ? + ( + + + To create a new project, please first fix problems with the following field{plural}:{" "} + {errorFields.join(", ")} + + + ) : + null; + return ( {alert} @@ -1093,6 +1230,7 @@ class Create extends Component { meta={meta} createUrl={this.props.handlers.createEncodedUrl} /> + {errorMessage} ); } @@ -1173,7 +1311,7 @@ function ShareLinkModal(props) { if (include.variables) { let variablesObject = {}; for (let variable of Object.keys(input.variables)) { - if (input.variables[variable]) + if (input.variables[variable] != null) variablesObject[variable] = input.variables[variable]; } dataObject.variables = variablesObject; diff --git a/client/src/project/new/ProjectNew.state.js b/client/src/project/new/ProjectNew.state.js index 23cdbf810d..4004e90295 100644 --- a/client/src/project/new/ProjectNew.state.js +++ b/client/src/project/new/ProjectNew.state.js @@ -91,17 +91,32 @@ class NewProjectCoordinator { Object.keys(template.variables) : []; - // preserve already set values + // preserve already set values or set default values when available const oldValues = currentInput.template ? currentInput.variables : {}; const oldVariables = Object.keys(oldValues); const values = variables.reduce((values, variable) => { - const text = oldVariables.includes(variable) ? - oldValues[variable] : - ""; - return { ...values, [variable]: text }; + let value = ""; + + const variableData = template.variables[variable]; + if (typeof variableData === "object") { + // set first value for enum, and "false" for boolean + if (variableData["type"] === "enum" && variableData["enum"] && variableData["enum"].length) + value = variableData["enum"][0]; + + // set default, if any + if (typeof variableData === "object" && variableData["default_value"] != null) + value = variableData["default_value"]; + } + + // set older value, if any + if (oldVariables.includes(variable)) + value = oldValues[variable]; + + return { ...values, [variable]: value }; }, {}); + return values; } @@ -552,10 +567,10 @@ class NewProjectCoordinator { newProjectData.ref = userTemplates.ref; } - // add variables + // add variables after converting to string (renku core accept string only) let parameters = []; for (let variable of Object.keys(input.variables)) - parameters.push({ key: variable, value: input.variables[variable] }); + parameters.push({ key: variable, value: input.variables[variable].toString() }); newProjectData.parameters = parameters; // reset all previous creation progresses and invoke the project creation API