From 40b25396265840c6fd5a2c8f5ebb6275d1ac8354 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 27 Mar 2019 10:42:06 -0400 Subject: [PATCH] rework org edit form to use Formik --- package-lock.json | 126 ++++++++- package.json | 1 + .../AnsibleSelect/AnsibleSelect.jsx | 22 +- src/components/FormActionGroup.jsx | 4 +- src/components/FormField.jsx | 56 ++++ .../screens/Organization/OrganizationEdit.jsx | 258 +++++++----------- 6 files changed, 292 insertions(+), 175 deletions(-) create mode 100644 src/components/FormField.jsx diff --git a/package-lock.json b/package-lock.json index c346715216..3c0bf4de43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1657,7 +1657,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "requires": { "ansi-wrap": "^0.1.0" @@ -2185,6 +2185,11 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2567,12 +2572,12 @@ }, "babel-plugin-syntax-class-properties": { "version": "6.13.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=" }, "babel-plugin-syntax-flow": { "version": "6.18.0", - "resolved": "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz", "integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0=" }, "babel-plugin-syntax-jsx": { @@ -4140,6 +4145,15 @@ "sha.js": "^2.4.8" } }, + "create-react-context": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz", + "integrity": "sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==", + "requires": { + "fbjs": "^0.8.0", + "gud": "^1.0.0" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -4380,6 +4394,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, "default-gateway": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-2.7.2.tgz", @@ -4711,7 +4730,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -4852,7 +4871,6 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "dev": true, "requires": { "iconv-lite": "~0.4.13" } @@ -5686,6 +5704,27 @@ "bser": "^2.0.0" } }, + "fbjs": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", + "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", + "requires": { + "core-js": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + }, + "dependencies": { + "core-js": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", + "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" + } + } + }, "fbjs-scripts": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/fbjs-scripts/-/fbjs-scripts-0.8.3.tgz", @@ -5736,7 +5775,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -6001,6 +6040,22 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/formik/-/formik-1.5.1.tgz", + "integrity": "sha512-FBWGBKQkcCE4d5b5l2fKccD9d1QxNxw/0bQTRvp3EjzA8Bnjmsm9H/Oy0375UA8P3FPmfJkF4cXLLdEqK7fP5A==", + "requires": { + "create-react-context": "^0.2.2", + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^2.5.5", + "lodash": "^4.17.11", + "lodash-es": "^4.17.11", + "prop-types": "^15.6.1", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.9.3" + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -6831,6 +6886,11 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gud": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", + "integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==" + }, "handle-thing": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", @@ -8031,8 +8091,7 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, "is-string": { "version": "1.0.4", @@ -8103,6 +8162,15 @@ } } }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "^1.0.1", + "whatwg-fetch": ">=0.10.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -9264,7 +9332,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=" }, "kleur": { @@ -9358,6 +9426,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, + "lodash-es": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz", + "integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q==" + }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -10117,7 +10190,6 @@ "version": "1.6.3", "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz", "integrity": "sha1-3CNO3WSJmC1Y6PDbT2lQKavNjAQ=", - "dev": true, "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -11305,6 +11377,14 @@ "integrity": "sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==", "dev": true }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "requires": { + "asap": "~2.0.3" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -11554,6 +11634,11 @@ "scheduler": "^0.10.0" } }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-hot-loader": { "version": "4.3.11", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz", @@ -13014,8 +13099,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { "version": "1.1.0", @@ -13939,6 +14023,11 @@ "setimmediate": "^1.0.4" } }, + "tiny-warning": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.2.tgz", + "integrity": "sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q==" + }, "tippy.js": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-3.4.1.tgz", @@ -14111,8 +14200,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tty-browserify": { "version": "0.0.0", @@ -14163,6 +14251,11 @@ "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", "dev": true }, + "ua-parser-js": { + "version": "0.7.19", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.19.tgz", + "integrity": "sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==" + }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", @@ -15562,6 +15655,11 @@ "iconv-lite": "0.4.24" } }, + "whatwg-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + }, "whatwg-mimetype": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz", diff --git a/package.json b/package.json index 61c168857b..c6ff3dabca 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@patternfly/react-icons": "^3.5.0", "@patternfly/react-tokens": "^2.0.4", "axios": "^0.18.0", + "formik": "^1.5.1", "prop-types": "^15.6.2", "react": "^16.4.1", "react-dom": "^16.4.1", diff --git a/src/components/AnsibleSelect/AnsibleSelect.jsx b/src/components/AnsibleSelect/AnsibleSelect.jsx index 90e1fddccc..c4de1c51e7 100644 --- a/src/components/AnsibleSelect/AnsibleSelect.jsx +++ b/src/components/AnsibleSelect/AnsibleSelect.jsx @@ -16,7 +16,7 @@ class AnsibleSelect extends React.Component { onSelectChange (val, event) { const { onChange, name } = this.props; event.target.name = name; - onChange(val, event); + onChange(event, val); } render () { @@ -24,10 +24,22 @@ class AnsibleSelect extends React.Component { return ( {({ i18n }) => ( - - {data.map((datum) => (datum === defaultSelected - ? () : ())) - } + + {data.map((datum) => ( + datum === defaultSelected ? ( + + ) : ( + + ) + ))} )} diff --git a/src/components/FormActionGroup.jsx b/src/components/FormActionGroup.jsx index 451987e847..675633163e 100644 --- a/src/components/FormActionGroup.jsx +++ b/src/components/FormActionGroup.jsx @@ -28,10 +28,10 @@ const FormActionGroup = ({ onSubmit, submitDisabled, onCancel }) => ( - + - + diff --git a/src/components/FormField.jsx b/src/components/FormField.jsx new file mode 100644 index 0000000000..177f8eeaa1 --- /dev/null +++ b/src/components/FormField.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field } from 'formik'; +import { FormGroup, TextInput } from '@patternfly/react-core'; + +function FormField (props) { + const { id, name, label, validate, isRequired, ...rest } = props; + + return ( + { + const isValid = !form.touched[field.name] || !form.errors[field.name]; + + return ( + + { + field.onChange(event); + }} + /> + + ); + }} + /> + ); +} + +FormField.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + type: PropTypes.string, + validate: PropTypes.func, + isRequired: PropTypes.bool, +}; + +FormField.defaultProps = { + type: 'text', + validate: () => {}, + isRequired: false, +}; + +export default FormField; diff --git a/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx b/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx index c8e22a4542..38d1c255bc 100644 --- a/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx +++ b/src/pages/Organizations/screens/Organization/OrganizationEdit.jsx @@ -1,27 +1,35 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withRouter } from 'react-router-dom'; +import { Formik, Field } from 'formik'; import { I18n, i18nMark } from '@lingui/react'; import { t } from '@lingui/macro'; import { CardBody, Form, FormGroup, - TextInput, } from '@patternfly/react-core'; import { ConfigContext } from '../../../../context'; +import FormField from '../../../../components/FormField'; import FormActionGroup from '../../../../components/FormActionGroup'; import AnsibleSelect from '../../../../components/AnsibleSelect'; import InstanceGroupsLookup from '../../components/InstanceGroupsLookup'; +function required (message) { + return value => { + if (!value.trim()) { + return message || i18nMark('This field must not be blank'); + } + return undefined; + }; +} + class OrganizationEdit extends Component { constructor (props) { super(props); this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this); - this.checkValidity = this.checkValidity.bind(this); - this.handleFieldChange = this.handleFieldChange.bind(this); this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.postInstanceGroups = this.postInstanceGroups.bind(this); @@ -29,48 +37,25 @@ class OrganizationEdit extends Component { this.handleSuccess = this.handleSuccess.bind(this); this.state = { - form: { - name: { - value: '', - isValid: true, - validation: { - required: true - }, - helperTextInvalid: i18nMark('This field must not be blank') - }, - description: { - value: '' - }, - instanceGroups: { - value: [], - initialValue: [] - }, - custom_virtualenv: { - value: '', - defaultValue: '/venv/ansible/' - } - }, + initialInstanceGroups: [], + instanceGroups: [], error: '', - formIsValid: true + formIsValid: true, }; } async componentDidMount () { - const { organization } = this.props; - const { form: formData } = this.state; - - formData.name.value = organization.name; - formData.description.value = organization.description; - formData.custom_virtualenv.value = organization.custom_virtualenv; - + let instanceGroups; try { - formData.instanceGroups.value = await this.getRelatedInstanceGroups(); - formData.instanceGroups.initialValue = [...formData.instanceGroups.value]; + instanceGroups = await this.getRelatedInstanceGroups(); } catch (err) { this.setState({ error: err }); } - this.setState({ form: formData }); + this.setState({ + instanceGroups, + initialInstanceGroups: instanceGroups, + }); } async getRelatedInstanceGroups () { @@ -79,56 +64,19 @@ class OrganizationEdit extends Component { organization: { id } } = this.props; const { data } = await api.getOrganizationInstanceGroups(id); - const { results } = data; - return results; + return data.results; } - checkValidity = (value, validation) => { - const isValid = (validation.required) - ? (value.trim() !== '') : true; - - return isValid; + handleInstanceGroupsChange (instanceGroups) { + this.setState({ instanceGroups }); } - handleFieldChange (val, evt) { - const targetName = evt.target.name; - const value = val; - - const { form: updatedForm } = this.state; - const updatedFormEl = { ...updatedForm[targetName] }; - - updatedFormEl.value = value; - updatedForm[targetName] = updatedFormEl; - - updatedFormEl.isValid = (updatedFormEl.validation) - ? this.checkValidity(updatedFormEl.value, updatedFormEl.validation) : true; - - const formIsValid = (updatedFormEl.validation) ? updatedFormEl.isValid : true; - - this.setState({ form: updatedForm, formIsValid }); - } - - handleInstanceGroupsChange (val, targetName) { - const { form: updatedForm } = this.state; - updatedForm[targetName].value = val; - - this.setState({ form: updatedForm }); - } - - async handleSubmit () { + async handleSubmit (values) { const { api, organization } = this.props; - const { form: { name, description, custom_virtualenv } } = this.state; - const formData = { name, description, custom_virtualenv }; - - const updatedData = {}; - Object.keys(formData) - .forEach(formId => { - updatedData[formId] = formData[formId].value; - }); - + const { instanceGroups } = this.state; try { - await api.updateOrganizationDetails(organization.id, updatedData); - await this.postInstanceGroups(); + await api.updateOrganizationDetails(organization.id, values); + await this.postInstanceGroups(instanceGroups); } catch (err) { this.setState({ error: err }); } finally { @@ -146,18 +94,18 @@ class OrganizationEdit extends Component { history.push(`/organizations/${id}`); } - async postInstanceGroups () { + async postInstanceGroups (instanceGroups) { const { api, organization } = this.props; - const { form: { instanceGroups } } = this.state; + const { initialInstanceGroups } = this.state; const url = organization.related.instance_groups; - const initialInstanceGroups = instanceGroups.initialValue.map(ig => ig.id); - const updatedInstanceGroups = instanceGroups.value.map(ig => ig.id); + const initialIds = initialInstanceGroups.map(ig => ig.id); + const updatedIds = instanceGroups.map(ig => ig.id); - const groupsToAssociate = [...updatedInstanceGroups] - .filter(x => !initialInstanceGroups.includes(x)); - const groupsToDisassociate = [...initialInstanceGroups] - .filter(x => !updatedInstanceGroups.includes(x)); + const groupsToAssociate = [...updatedIds] + .filter(x => !initialIds.includes(x)); + const groupsToDisassociate = [...initialIds] + .filter(x => !updatedIds.includes(x)); try { await Promise.all(groupsToAssociate.map(async id => { @@ -172,83 +120,85 @@ class OrganizationEdit extends Component { } render () { - const { api } = this.props; + const { api, organization } = this.props; const { - form: { - name, - description, - instanceGroups, - custom_virtualenv - }, + instanceGroups, formIsValid, - error + error, } = this.state; + const defaultVenv = '/venv/ansible/'; return ( {({ i18n }) => ( -
-
- - ( + +
+ + + + {({ custom_virtualenvs }) => ( + custom_virtualenvs && custom_virtualenvs.length > 1 && ( + ( + + + + )} + /> + ) + )} + +
+ -
- - - - - {({ custom_virtualenvs }) => ( - custom_virtualenvs && custom_virtualenvs.length > 1 && ( - - - - ) - )} - -
- - - { error ?
error
: '' } - + { error ?
error
: '' } + + )} + /> )}