Просмотр исходного кода

Merge pull request #405 from smiclea/fields-refactor

Refactor Wizard and Endpoint options fields
Dorin Paslaru 6 лет назад
Родитель
Сommit
8febc26ee5
32 измененных файлов с 243 добавлено и 766 удалено
  1. 2 3
      src/components/atoms/InfoIcon/InfoIcon.jsx
  2. 1 1
      src/components/atoms/TextInput/TextInput.jsx
  3. 0 4
      src/components/molecules/Dropdown/Dropdown.jsx
  4. 0 6
      src/components/molecules/EndpointField/package.json
  5. 0 73
      src/components/molecules/EndpointField/story.jsx
  6. 0 87
      src/components/molecules/EndpointField/test.jsx
  7. 128 81
      src/components/molecules/FieldInput/FieldInput.jsx
  8. 0 0
      src/components/molecules/FieldInput/images/asterisk.svg
  9. 6 0
      src/components/molecules/FieldInput/package.json
  10. 1 4
      src/components/molecules/PropertiesTable/PropertiesTable.jsx
  11. 2 2
      src/components/molecules/PropertiesTable/test.jsx
  12. 0 257
      src/components/molecules/WizardOptionsField/WizardOptionsField.jsx
  13. 0 6
      src/components/molecules/WizardOptionsField/package.json
  14. 0 84
      src/components/molecules/WizardOptionsField/story.jsx
  15. 0 78
      src/components/molecules/WizardOptionsField/test.jsx
  16. 32 18
      src/components/organisms/AssessmentMigrationOptions/AssessmentMigrationOptions.jsx
  17. 2 9
      src/components/organisms/ChooseProvider/test.jsx
  18. 4 4
      src/components/organisms/EndpointDuplicateOptions/EndpointDuplicateOptions.jsx
  19. 11 11
      src/components/organisms/ProjectMemberModal/ProjectMemberModal.jsx
  20. 1 1
      src/components/organisms/ProjectMemberModal/test.jsx
  21. 5 3
      src/components/organisms/ProjectModal/ProjectModal.jsx
  22. 5 4
      src/components/organisms/ReplicaExecutionOptions/ReplicaExecutionOptions.jsx
  23. 7 6
      src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.jsx
  24. 6 6
      src/components/organisms/UserModal/UserModal.jsx
  25. 1 1
      src/components/organisms/WizardNetworks/WizardNetworks.jsx
  26. 16 9
      src/components/organisms/WizardOptions/WizardOptions.jsx
  27. 1 1
      src/components/organisms/WizardStorage/WizardStorage.jsx
  28. 1 1
      src/constants.js
  29. 1 1
      src/plugins/endpoint/azure/ContentPlugin.jsx
  30. 6 4
      src/plugins/endpoint/default/ContentPlugin.jsx
  31. 3 1
      src/plugins/endpoint/openstack/ContentPlugin.jsx
  32. 1 0
      src/types/Field.js

+ 2 - 3
src/components/atoms/InfoIcon/InfoIcon.jsx

@@ -31,10 +31,9 @@ const Wrapper = styled.div`
 `
 type Props = {
   text: string,
-  marginLeft?: number,
-  marginBottom?: number,
+  marginLeft?: ?number,
+  marginBottom?: ?number,
   className?: string,
-  marginLeft?: number,
   warning?: boolean,
 }
 @observer

+ 1 - 1
src/components/atoms/TextInput/TextInput.jsx

@@ -35,7 +35,7 @@ const Required = styled.div`
 `
 const getInputWidth = props => {
   if (props.width) {
-    return props.width
+    return typeof props.width === 'number' ? `${props.width}px` : props.width
   }
 
   if (props.large) {

+ 0 - 4
src/components/molecules/Dropdown/Dropdown.jsx

@@ -31,10 +31,6 @@ import tipImage from './images/tip'
 import requiredImage from './images/required.svg'
 
 const getWidth = props => {
-  if (props.large) {
-    return StyleProps.inputSizes.large.width - 2
-  }
-
   if (props.width) {
     return props.width - 2
   }

+ 0 - 6
src/components/molecules/EndpointField/package.json

@@ -1,6 +0,0 @@
-{
-  "name": "EndpointField",
-  "version": "0.0.0",
-  "private": true,
-  "main":"./EndpointField.jsx"
-}

+ 0 - 73
src/components/molecules/EndpointField/story.jsx

@@ -1,73 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import EndpointField from '.'
-
-class Wrapper extends React.Component {
-  constructor() {
-    super()
-    this.state = { value: null }
-  }
-
-  handleChange(value) {
-    this.setState({ value })
-  }
-
-  render() {
-    return (
-      <EndpointField
-        {...this.props}
-        value={this.state.value}
-        onChange={value => { this.handleChange(value) }}
-      />
-    )
-  }
-}
-
-storiesOf('EndpointField', module)
-  .add('text input', () => (
-    <Wrapper name="field_name" type="string" />
-  ))
-  .add('text input large', () => (
-    <Wrapper large name="field_name" type="string" />
-  ))
-  .add('text input disabled', () => (
-    <Wrapper name="field_name" type="string" disabled />
-  ))
-  .add('text input highlight', () => (
-    <Wrapper name="field_name" type="string" highlight />
-  ))
-  .add('text input password', () => (
-    <Wrapper name="field_name" type="string" password />
-  ))
-  .add('switch', () => (
-    <Wrapper name="migr_worker_use_config_drive" type="boolean" />
-  ))
-  .add('number dropdown', () => (
-    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} />
-  ))
-  .add('number dropdown large', () => (
-    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} large />
-  ))
-  .add('number dropdown disabled', () => (
-    <Wrapper name="field_name" type="integer" minimum={1} maximum={5} disabled />
-  ))
-  .add('radio', () => (
-    <Wrapper name="field_name" type="radio" />
-  ))
-  .add('radio disabled', () => (
-    <Wrapper name="field_name" type="radio" disabled />
-  ))

+ 0 - 87
src/components/molecules/EndpointField/test.jsx

@@ -1,87 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import React from 'react'
-import { shallow } from 'enzyme'
-import TestWrapper from '../../../utils/TestWrapper'
-import EndpointField from '.'
-
-const wrap = props => new TestWrapper(shallow(<EndpointField {...props} />), 'endpointField')
-
-describe('EndpointField Component', () => {
-  it('renders label', () => {
-    const wrapper = wrap({ type: 'boolean', value: true, name: 'the_name' })
-    expect(wrapper.findText('label')).toBe('The Name')
-  })
-
-  it('renders boolean field with correct value', () => {
-    let wrapper = wrap({ type: 'boolean', name: 'the_name', value: true })
-    expect(wrapper.find('switch-the_name').length).toBe(1)
-    expect(wrapper.find('switch-the_name').prop('checked')).toBe(true)
-  })
-
-  it('renders boolean field disabled', () => {
-    let wrapper = wrap({ type: 'boolean', name: 'the_name', disabled: true })
-    expect(wrapper.find('switch-the_name').prop('disabled')).toBe(true)
-  })
-
-  it('renders text input field with correct label and value', () => {
-    let wrapper = wrap({ type: 'string', name: 'the_name', value: 'the_value' })
-    expect(wrapper.findText('label')).toBe('The Name')
-    expect(wrapper.find('textInput-the_name').length).toBe(1)
-    expect(wrapper.find('textInput-the_name').prop('value')).toBe('the_value')
-  })
-
-  it('renders text input field with password, large, disabled, highlighted and required', () => {
-    let wrapper = wrap({
-      type: 'string',
-      name: 'the_name',
-      value: 'the_value',
-      password: true,
-      large: true,
-      disabled: true,
-      highlight: true,
-      required: true,
-    })
-    let textInput = wrapper.find('textInput-the_name')
-    expect(textInput.prop('type')).toBe('password')
-    expect(textInput.prop('large')).toBe(true)
-    expect(textInput.prop('disabled')).toBe(true)
-    expect(textInput.prop('highlight')).toBe(true)
-    expect(textInput.prop('required')).toBe(true)
-  })
-
-  it('renders integer dropdown field with correct items', () => {
-    let wrapper = wrap({
-      type: 'integer',
-      name: 'the_name',
-      value: 11,
-      minimum: 10,
-      maximum: 15,
-    })
-    let dropdown = wrapper.find('dropdown-the_name')
-    expect(dropdown.prop('selectedItem')).toBe(11)
-    expect(dropdown.prop('items')[3].value).toBe(13)
-    expect(dropdown.prop('items')[5].value).toBe(15)
-  })
-
-  it('renders radio input field with correct value', () => {
-    let wrapper = wrap({ type: 'radio', name: 'the_name', value: true })
-    let radioInput = wrapper.find('radioInput-the_name')
-    expect(radioInput.length).toBe(1)
-    expect(radioInput.prop('checked')).toBe(true)
-  })
-})

+ 128 - 81
src/components/molecules/EndpointField/EndpointField.jsx → src/components/molecules/FieldInput/FieldInput.jsx

@@ -16,73 +16,99 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from 'react'
 import { observer } from 'mobx-react'
-import styled from 'styled-components'
+import styled, { css } from 'styled-components'
 
-import Switch from '../../atoms/Switch'
-import TextInput from '../../atoms/TextInput'
-import RadioInput from '../../atoms/RadioInput'
-import InfoIcon from '../../atoms/InfoIcon'
-import Dropdown from '../../molecules/Dropdown'
-import DropdownInput from '../../molecules/DropdownInput'
-import TextArea from '../../atoms/TextArea'
-import PropertiesTable from '../../molecules/PropertiesTable'
+import Switch from '../../atoms/Switch/Switch'
+import TextInput from '../../atoms/TextInput/TextInput'
+import RadioInput from '../../atoms/RadioInput/RadioInput'
+import InfoIcon from '../../atoms/InfoIcon/InfoIcon'
+import Dropdown from '../Dropdown/Dropdown'
+import DropdownInput from '../DropdownInput/DropdownInput'
+import TextArea from '../../atoms/TextArea/TextArea'
+import PropertiesTable from '../PropertiesTable/PropertiesTable'
+import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
 
-import type { Field as FieldType } from '../../../types/Field'
+import type { Field } from '../../../types/Field'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import StyleProps from '../../styleUtils/StyleProps'
 import Palette from '../../styleUtils/Palette'
 
-const Wrapper = styled.div``
+import asteriskImage from './images/asterisk.svg'
+
+const Wrapper = styled.div`
+  ${props => props.layout === 'page' ? css`
+    display: flex;
+    flex-direction: ${props.inline ? 'row' : 'column'};
+    ${props.inline ? '' : css`justify-content: center;`}
+  ` : ''}
+`
+
 const Label = styled.div`
-  font-size: 10px;
   font-weight: ${StyleProps.fontWeights.medium};
-  color: ${Palette.grayscale[3]};
-  text-transform: uppercase;
-  margin-bottom: 2px;
-  display: flex;
-  align-items: center;
+  ${props => props.layout === 'page' ? css`
+    margin-bottom: 8px;
+  ` : css`
+    margin-bottom: 2px;
+    font-size: 10px;
+    color: ${Palette.grayscale[3]};
+    text-transform: uppercase;
+    display: flex;
+    align-items: center;
+  `}
 `
-const LabelText = styled.span`
-  margin-right: 24px;
+const LabelText = styled.span``
+const Asterisk = styled.div`
+  ${StyleProps.exactSize('16px')}
+  display: inline-block;
+  background: url('${asteriskImage}') center no-repeat;
+  margin-bottom: -3px;
+  margin-left: ${props => props.marginLeft || '0px'};
 `
 
 type Props = {
   name: string,
   type: string,
   value: any,
-  onChange?: (value: any, fieldName?: string) => void,
-  valueCallback?: (fieldName: string) => void,
+  onChange?: (value: any, field?: Field) => void,
+  valueCallback?: (field: Field) => any,
   getFieldValue?: (fieldName: string) => string,
   onFieldChange?: (fieldName: string, fieldValue: string) => void,
   className?: string,
+  properties?: Field[],
+  // $FlowIgnore
+  enum?: string[] | { label: string, value: string }[] | { name: string, id: string }[],
+  required?: boolean,
   minimum?: number,
   maximum?: number,
   password?: boolean,
-  required?: boolean,
-  large?: boolean,
   highlight?: boolean,
-  properties?: FieldType[],
   disabled?: boolean,
-  // $FlowIssue
-  enum?: string[] | { label: string, value: string }[],
   items?: any[],
   useTextArea?: boolean,
   noSelectionMessage?: string,
   noItemsMessage?: string,
-  selectedItems?: string[],
-  'data-test-id'?: string,
+  layout: 'modal' | 'page',
+  width?: number,
+  label?: string,
+  addNullValue?: boolean,
+  nullableBoolean?: boolean,
+  style?: { [string]: mixed },
 }
 @observer
-class Field extends React.Component<Props> {
+class FieldInput extends React.Component<Props> {
   renderSwitch(propss: { triState: boolean }) {
     return (
       <Switch
-        data-test-id={`endpointField-switch-${this.props.name}`}
+        width={this.props.layout === 'page' ? '112px' : ''}
+        height={this.props.layout === 'page' ? 16 : 24}
+        justifyContent={this.props.layout === 'page' ? 'flex-end' : ''}
         disabled={this.props.disabled}
         triState={propss.triState}
         checked={this.props.value}
         onChange={checked => { if (this.props.onChange) this.props.onChange(checked) }}
+        leftLabel={this.props.layout === 'page'}
+        style={this.props.layout === 'page' ? { marginTop: '-8px' } : {}}
       />
     )
   }
@@ -90,15 +116,14 @@ class Field extends React.Component<Props> {
   renderTextInput() {
     return (
       <TextInput
-        data-test-id={`endpointField-textInput-${this.props.name}`}
+        width={this.props.width}
         highlight={this.props.highlight}
         type={this.props.password ? 'password' : 'text'}
-        large={this.props.large}
         value={this.props.value}
         onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
         placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
-        required={this.props.required}
+        required={this.props.layout === 'page' ? false : this.props.required}
       />
     )
   }
@@ -107,7 +132,7 @@ class Field extends React.Component<Props> {
     return (
       <TextInput
         highlight={this.props.highlight}
-        large={this.props.large}
+        width={this.props.width}
         value={this.props.value}
         onChange={e => {
           let value = Number(e.target.value.replace(/\D/g, '')) || ''
@@ -129,13 +154,13 @@ class Field extends React.Component<Props> {
     return (
       <PropertiesTable
         properties={this.props.properties}
-        valueCallback={field => { if (this.props.valueCallback) { this.props.valueCallback(field.name) } }}
+        valueCallback={field => this.props.valueCallback && this.props.valueCallback(field)}
         onChange={(field, value) => {
-          let fieldName = field.name.substr(field.name.lastIndexOf('/') + 1)
           if (this.props.onChange) {
-            this.props.onChange(value, fieldName)
+            this.props.onChange(value, field)
           }
         }}
+        hideRequiredSymbol={this.props.layout === 'page'}
       />
     )
   }
@@ -143,7 +168,6 @@ class Field extends React.Component<Props> {
   renderTextArea() {
     return (
       <TextArea
-        data-test-id={`endpointField-textArea-${this.props.name}`}
         style={{ width: '100%' }}
         highlight={this.props.highlight}
         value={this.props.value}
@@ -156,51 +180,73 @@ class Field extends React.Component<Props> {
   }
 
   renderEnumDropdown() {
-    if (!this.props.enum) {
-      return null
-    }
-
+    const useDictionary = LabelDictionary.enumFields.find(f => f === this.props.name)
     let items = this.props.enum.map(e => {
-      if (typeof e === 'string') {
-        return {
-          label: LabelDictionary.get(e),
-          value: e,
-        }
+      if (typeof e !== 'string' && e.separator === true) {
+        return e
+      }
+
+      return {
+        label: typeof e === 'string' ? (useDictionary ? LabelDictionary.get(e) : e) : e.name || e.label,
+        value: typeof e === 'string' ? e : e.id || e.value,
       }
-      return e
     })
+    if (this.props.addNullValue) {
+      items = [
+        { label: 'Choose a value', value: null },
+        ...items,
+      ]
+    }
+
     let selectedItem = items.find(i => i.value === this.props.value)
+    let commonProps = {
+      width: this.props.width,
+      selectedItem,
+      items,
+      onChange: item => this.props.onChange && this.props.onChange(item.value),
+    }
 
+    if (items.length < 10) {
+      return (
+        <Dropdown
+          {...commonProps}
+          noSelectionMessage="Choose a value"
+          dimFirstItem={this.props.addNullValue}
+        />
+      )
+    }
     return (
-      <Dropdown
-        data-test-id={`endpointField-dropdown-${this.props.name}`}
-        large={this.props.large}
-        selectedItem={selectedItem}
-        noSelectionMessage={this.props.noSelectionMessage}
-        noItemsMessage={this.props.noItemsMessage}
-        items={items}
-        onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
-        disabled={this.props.disabled}
-        highlight={this.props.highlight}
-        required={this.props.required}
+      <AutocompleteDropdown
+        {...commonProps}
+        dimNullValue
       />
     )
   }
 
   renderArrayDropdown() {
+    let items = this.props.enum.map(e => {
+      if (typeof e !== 'string' && e.separator === true) {
+        return e
+      }
+
+      return {
+        label: typeof e === 'string' ? LabelDictionary.get(e) : e.name || e.label,
+        value: typeof e === 'string' ? e : e.id || e.value,
+      }
+    })
+    let selectedItems = this.props.value || []
     return (
       <Dropdown
-        data-test-id={`endpointField-multidropdown-${this.props.name}`}
         multipleSelection
-        large={this.props.large}
+        width={this.props.width}
         disabled={this.props.disabled}
-        noSelectionMessage={this.props.noSelectionMessage}
+        noSelectionMessage="Choose values"
         noItemsMessage={this.props.noItemsMessage}
-        items={this.props.items}
-        selectedItems={this.props.selectedItems}
+        items={items}
+        selectedItems={selectedItems}
         onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
         highlight={this.props.highlight}
-        required={this.props.required}
+        required={this.props.layout === 'page' ? false : this.props.required}
       />
     )
   }
@@ -221,8 +267,7 @@ class Field extends React.Component<Props> {
 
     return (
       <Dropdown
-        data-test-id={`endpointField-dropdown-${this.props.name}`}
-        large={this.props.large}
+        width={this.props.width}
         selectedItem={this.props.value}
         items={items}
         onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
@@ -236,7 +281,6 @@ class Field extends React.Component<Props> {
   renderRadioInput() {
     return (
       <RadioInput
-        data-test-id={`endpointField-radioInput-${this.props.name}`}
         checked={this.props.value}
         label={LabelDictionary.get(this.props.name)}
         onChange={e => { if (this.props.onChange) this.props.onChange(e.target.checked) }}
@@ -278,11 +322,9 @@ class Field extends React.Component<Props> {
       case 'input-choice':
         return this.renderDropdownInput()
       case 'boolean':
-        return this.renderSwitch({ triState: false })
-      case 'optional-boolean':
-        return this.renderSwitch({ triState: true })
+        return this.renderSwitch({ triState: Boolean(this.props.nullableBoolean) })
       case 'string':
-        if (this.props.enum) {
+        if (this.props.enum && this.props.enum.length) {
           return this.renderEnumDropdown()
         }
         if (this.props.useTextArea) {
@@ -311,22 +353,27 @@ class Field extends React.Component<Props> {
     }
 
     let description = LabelDictionary.getDescription(this.props.name)
-    let infoIcon = null
-    if (description) {
-      infoIcon = <InfoIcon text={description} marginLeft={-20} marginBottom={0} />
-    }
+    let marginRight = this.props.layout === 'modal' || description || this.props.required ? '24px' : 0
 
     return (
-      <Label>
-        <LabelText data-test-id="endpointField-label">{LabelDictionary.get(this.props.name)}</LabelText>
-        {infoIcon}
+      <Label layout={this.props.layout}>
+        <LabelText style={{ marginRight }}>
+          {this.props.label || LabelDictionary.get(this.props.name)}
+        </LabelText>
+        {description ? <InfoIcon text={description} marginLeft={-20} marginBottom={this.props.layout === 'page' ? null : 0} /> : null}
+        {this.props.layout === 'page' && Boolean(this.props.required) ? <Asterisk marginLeft={description ? '4px' : '-16px'} /> : null}
       </Label>
     )
   }
 
   render() {
     return (
-      <Wrapper data-test-id={this.props['data-test-id'] || 'endpointField-wrapper'} className={this.props.className}>
+      <Wrapper
+        className={this.props.className}
+        inline={this.props.type === 'boolean'}
+        style={this.props.style}
+        layout={this.props.layout}
+      >
         {this.renderLabel()}
         {this.renderInput()}
       </Wrapper>
@@ -334,4 +381,4 @@ class Field extends React.Component<Props> {
   }
 }
 
-export default Field
+export default FieldInput

+ 0 - 0
src/components/molecules/WizardOptionsField/images/asterisk.svg → src/components/molecules/FieldInput/images/asterisk.svg


+ 6 - 0
src/components/molecules/FieldInput/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "FieldInput",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./FieldInput.jsx"
+}

+ 1 - 4
src/components/molecules/PropertiesTable/PropertiesTable.jsx

@@ -145,10 +145,7 @@ class PropertiesTable extends React.Component<Props> {
     let input = null
     switch (prop.type) {
       case 'boolean':
-        input = this.renderSwitch(prop, { triState: true })
-        break
-      case 'strict-boolean':
-        input = this.renderSwitch(prop, { triState: false })
+        input = this.renderSwitch(prop, { triState: Boolean(prop.nullableBoolean) })
         break
       case 'string':
         if (prop.enum) {

+ 2 - 2
src/components/molecules/PropertiesTable/test.jsx

@@ -22,8 +22,8 @@ import PropertiesTable from '.'
 const wrap = props => new TW(shallow(<PropertiesTable onChange={() => { }} {...props} />), 'propertiesTable')
 
 let properties = [
-  { type: 'boolean', name: 'prop_1', label: 'Boolean', value: true },
-  { type: 'strict-boolean', name: 'prop_2', label: 'Strict Boolean', value: false },
+  { type: 'boolean', name: 'prop_1', label: 'Nullabel Boolean', value: true, nullableBoolean: true },
+  { type: 'boolean', name: 'prop_2', label: 'Boolean', value: false },
   { type: 'string', name: 'prop_3', label: 'String', value: 'value-3' },
   { type: 'string', name: 'prop_3a', label: 'String', required: true, value: 'value-4' },
   // $FlowIgnore

+ 0 - 257
src/components/molecules/WizardOptionsField/WizardOptionsField.jsx

@@ -1,257 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import React from 'react'
-import styled from 'styled-components'
-import { observer } from 'mobx-react'
-
-import Switch from '../../atoms/Switch'
-import TextInput from '../../atoms/TextInput'
-import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
-import Dropdown from '../../molecules/Dropdown'
-import InfoIcon from '../../atoms/InfoIcon'
-import PropertiesTable from '../../molecules/PropertiesTable'
-
-import StyleProps from '../../styleUtils/StyleProps'
-import LabelDictionary from '../../../utils/LabelDictionary'
-
-import type { Field } from '../../../types/Field'
-
-import asteriskImage from './images/asterisk.svg'
-
-const getDirection = props => {
-  if (props.type === 'strict-boolean' || props.type === 'boolean') {
-    return 'row'
-  }
-
-  return 'column'
-}
-const Wrapper = styled.div`
-  display: flex;
-  flex-direction: ${props => getDirection(props)};
-  ${props => getDirection(props) === 'row' ? '' : 'justify-content: center;'}
-`
-const Label = styled.div`
-  font-weight: ${StyleProps.fontWeights.medium};
-  margin-bottom: 8px;
-  ${props => getDirection(props) === 'column' ? 'margin-bottom: 8px;' : ''}
-`
-const LabelText = styled.span`
-  margin-right: ${props => props.noMargin ? 0 : 24}px;
-`
-const Asterisk = styled.div`
-  ${StyleProps.exactSize('16px')}
-  display: inline-block;
-  background: url('${asteriskImage}') center no-repeat;
-  margin-bottom: -3px;
-  margin-left: ${props => props.marginLeft || '0px'};
-`
-type Props = {
-  type: 'replica' | 'migration',
-  name: string,
-  value: any,
-  onChange: (value: any) => void,
-  valueCallback: (prop: Field, value: any) => void,
-  className?: string,
-  properties: Field[],
-  enum: string[],
-  required: boolean,
-  width?: number,
-  label?: string,
-  skipNullValue?: boolean,
-  'data-test-id'?: string,
-  style?: { [string]: mixed },
-}
-@observer
-class WizardOptionsField extends React.Component<Props> {
-  renderSwitch(propss: { triState: boolean }) {
-    return (
-      <Switch
-        width="112px"
-        justifyContent="flex-end"
-        height={16}
-        triState={propss.triState}
-        checked={this.props.value}
-        onChange={checked => { this.props.onChange(checked) }}
-        style={{ marginTop: '-8px' }}
-        leftLabel
-        data-test-id="wOptionsField-switch"
-      />
-    )
-  }
-
-  renderTextInput() {
-    return (
-      <TextInput
-        width={`${this.props.width || StyleProps.inputSizes.wizard.width}px`}
-        value={this.props.value}
-        onChange={e => { this.props.onChange(e.target.value) }}
-        placeholder={LabelDictionary.get(this.props.name)}
-        data-test-id="wOptionsField-textInput"
-      />
-    )
-  }
-
-  renderObjectTable() {
-    if (!this.props.properties || !this.props.properties.length) {
-      return null
-    }
-
-    return (
-      <PropertiesTable
-        properties={this.props.properties}
-        valueCallback={this.props.valueCallback}
-        onChange={this.props.onChange}
-        hideRequiredSymbol
-        data-test-id="wOptionsField-propertiesTable"
-      />
-    )
-  }
-
-  renderEnumDropdown() {
-    const useDictionary = LabelDictionary.enumFields.find(f => f === this.props.name)
-    let items = this.props.enum.map(e => {
-      if (typeof e !== 'string' && e.separator === true) {
-        return e
-      }
-
-      return {
-        label: typeof e === 'string' ? (useDictionary ? LabelDictionary.get(e) : e) : e.name,
-        value: typeof e === 'string' ? e : e.id,
-      }
-    })
-
-    if (!this.props.skipNullValue) {
-      items = [
-        { label: 'Choose a value', value: null },
-        ...items,
-      ]
-    }
-
-    let selectedItem = items.find(i => i.value === this.props.value)
-
-    if (items.length < 10) {
-      return (
-        <Dropdown
-          data-test-id={`wOptionsField-enumDropdown-${this.props.name}`}
-          width={this.props.width || StyleProps.inputSizes.wizard.width}
-          noSelectionMessage="Choose a value"
-          selectedItem={selectedItem}
-          items={items}
-          dimFirstItem
-          onChange={item => this.props.onChange(item.value)}
-        />
-      )
-    }
-
-    return (
-      <AutocompleteDropdown
-        width={this.props.width || StyleProps.inputSizes.wizard.width}
-        selectedItem={selectedItem}
-        items={items}
-        dimNullValue
-        onChange={item => this.props.onChange(item.value)}
-      />
-    )
-  }
-
-  renderArrayDropdown() {
-    let items = this.props.enum.map(e => {
-      if (typeof e !== 'string' && e.separator === true) {
-        return e
-      }
-
-      return {
-        label: typeof e === 'string' ? LabelDictionary.get(e) : e.name,
-        value: typeof e === 'string' ? e : e.id,
-      }
-    })
-    let selectedItems = this.props.value || []
-    return (
-      <Dropdown
-        multipleSelection
-        items={items}
-        selectedItems={selectedItems}
-        width={this.props.width || StyleProps.inputSizes.wizard.width}
-        noSelectionMessage="Choose values"
-        onChange={item => this.props.onChange(item.value)}
-      />
-    )
-  }
-
-  renderField() {
-    let field = null
-    switch (this.props.type) {
-      case 'strict-boolean':
-        field = this.renderSwitch({ triState: false })
-        break
-      case 'boolean':
-        field = this.renderSwitch({ triState: true })
-        break
-      case 'string':
-        if (this.props.enum && this.props.enum.length) {
-          field = this.renderEnumDropdown()
-        } else {
-          field = this.renderTextInput()
-        }
-        break
-      case 'object':
-        field = this.renderObjectTable()
-        break
-      case 'array':
-        field = this.renderArrayDropdown()
-        break
-      default:
-    }
-
-    return field
-  }
-
-  renderLabel() {
-    let description = LabelDictionary.getDescription(this.props.name)
-    return (
-      <Label>
-        <LabelText data-test-id="wOptionsField-label" noMargin={!description && !this.props.required}>
-          {this.props.label || LabelDictionary.get(this.props.name)}
-        </LabelText>
-        {description ? <InfoIcon text={description} marginLeft={-20} /> : null}
-        {this.props.required ? <Asterisk data-test-id="wOptionsField-required" marginLeft={description ? '4px' : '-16px'} /> : null}
-      </Label>
-    )
-  }
-
-  render() {
-    let field = this.renderField()
-
-    if (!field) {
-      return null
-    }
-
-    return (
-      <Wrapper
-        data-test-id={this.props['data-test-id'] || 'wOptionsField-wrapper'}
-        type={this.props.type}
-        className={this.props.className}
-        style={this.props.style}
-      >
-        {this.renderLabel()}
-        {field}
-      </Wrapper>
-    )
-  }
-}
-
-export default WizardOptionsField

+ 0 - 6
src/components/molecules/WizardOptionsField/package.json

@@ -1,6 +0,0 @@
-{
-  "name": "WizardOptionsField",
-  "version": "0.0.0",
-  "private": true,
-  "main":"./WizardOptionsField.jsx"
-}

+ 0 - 84
src/components/molecules/WizardOptionsField/story.jsx

@@ -1,84 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-import React from 'react'
-import { storiesOf } from '@storybook/react'
-import styled from 'styled-components'
-import WizardOptionsField from '.'
-
-const WizardOptionsFieldStyled = styled(WizardOptionsField) `
-  width: 319px;
-  justify-content: space-between;
-`
-
-class Wrapper extends React.Component {
-  constructor() {
-    super()
-    this.state = { value: null }
-  }
-
-  handleChange(value) {
-    this.setState({ value })
-  }
-
-  render() {
-    return (
-      <div style={{ width: '800px' }}>
-        <WizardOptionsFieldStyled
-          {...this.props}
-          value={this.state.value}
-          onChange={value => { this.handleChange(value) }}
-        />
-      </div>
-    )
-  }
-}
-
-storiesOf('WizardOptionsField', module)
-  .add('string', () => (
-    <Wrapper
-      name="String input"
-      type="string"
-    />
-  ))
-  .add('switch with boolean', () => (
-    <Wrapper
-      name="Switch"
-      type="boolean"
-    />
-  ))
-  .add('switch with strict-boolean', () => (
-    <Wrapper
-      name="Switch"
-      type="strict-boolean"
-    />
-  ))
-  .add('enum dropdown', () => (
-    <Wrapper
-      type="string"
-      name="Port Reuse"
-      enum={['keep_mac', 'reuse_ports', 'replace_mac']}
-    />
-  ))
-  .add('object table', () => (
-    <Wrapper
-      type="object"
-      name="Object table"
-      properties={[
-        { type: 'boolean', name: 'prop-1', label: 'Property 1' },
-        { type: 'boolean', name: 'prop-2', label: 'Property 2' },
-      ]}
-      valueCallback={prop => prop.name === 'prop-2'}
-    />
-  ))

+ 0 - 78
src/components/molecules/WizardOptionsField/test.jsx

@@ -1,78 +0,0 @@
-/*
-Copyright (C) 2017  Cloudbase Solutions SRL
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU Affero General Public License for more details.
-You should have received a copy of the GNU Affero General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-// @flow
-
-import React from 'react'
-import { shallow } from 'enzyme'
-import TW from '../../../utils/TestWrapper'
-import WizardOptionsField from '.'
-
-// $FlowIgnore
-const wrap = props => new TW(shallow(<WizardOptionsField {...props} />), 'wOptionsField')
-
-describe('WizardOptionsField Component', () => {
-  it('renders label', () => {
-    let wrapper = wrap({ name: 'the_name', type: 'string', value: 'the_value' })
-    expect(wrapper.findText('label')).toBe('The Name')
-  })
-
-  it('renders string input with correct value', () => {
-    let wrapper = wrap({ name: 'the_name', type: 'string', value: 'the_value' })
-    expect(wrapper.find('textInput').prop('value')).toBe('the_value')
-  })
-
-  it('renders required field', () => {
-    let wrapper = wrap({ name: 'the_name', type: 'string', value: 'the_value', required: true })
-    expect(wrapper.find('required').length).toBe(1)
-    wrapper = wrap({ name: 'the_name', type: 'string', value: 'the_value', required: false })
-    expect(wrapper.find('required').length).toBe(0)
-  })
-
-  it('renders strict boolean with correct value', () => {
-    let wrapper = wrap({ name: 'the_name', type: 'strict-boolean', value: true })
-    expect(wrapper.find('switch').prop('triState')).toBe(false)
-    expect(wrapper.find('switch').prop('checked')).toBe(true)
-  })
-
-  it('renders boolean with correct value', () => {
-    let wrapper = wrap({ name: 'the_name', type: 'boolean', value: true })
-    expect(wrapper.find('switch').prop('triState')).toBe(true)
-    expect(wrapper.find('switch').prop('checked')).toBe(true)
-  })
-
-  it('renders enum string', () => {
-    let wrapper = wrap({
-      name: 'port_reuse_policy',
-      type: 'string',
-      value: 'reuse_ports',
-      enum: ['keep_mac', 'reuse_ports', 'replace_mac'],
-    })
-    expect(wrapper.find('enumDropdown-port_reuse_policy').prop('selectedItem').label).toBe('Reuse Existing Ports')
-    expect(wrapper.find('enumDropdown-port_reuse_policy').prop('items')[3].value).toBe('replace_mac')
-  })
-
-  it('renders object table', () => {
-    let wrapper = wrap({
-      name: 'test',
-      type: 'object',
-      properties: [
-        { type: 'boolean', name: 'prop-1', label: 'Property 1' },
-        { type: 'boolean', name: 'prop-2', label: 'Property 2' },
-      ],
-      valueCallback: prop => prop.name === 'prop-2',
-    })
-    expect(wrapper.find('propertiesTable').prop('properties')[1].name).toBe('prop-2')
-  })
-})

+ 32 - 18
src/components/organisms/AssessmentMigrationOptions/AssessmentMigrationOptions.jsx

@@ -19,7 +19,7 @@ import { observer } from 'mobx-react'
 import styled from 'styled-components'
 
 import Button from '../../atoms/Button'
-import EndpointField from '../../molecules/EndpointField'
+import FieldInput from '../../molecules/FieldInput'
 import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
 
 import type { Field } from '../../../types/Field'
@@ -51,8 +51,8 @@ const Fields = styled.div`
   width: 100%;
   min-height: 0;
 `
-const FieldStyled = styled(EndpointField)`
-  ${StyleProps.exactWidth('224px')}
+const FieldStyled = styled(FieldInput)`
+  ${StyleProps.exactWidth(`${StyleProps.inputSizes.large.width}px`)}
   margin-bottom: 16px;
 `
 const Row = styled.div`
@@ -71,23 +71,23 @@ const Buttons = styled.div`
 const generalFields = [
   {
     name: 'use_replica',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
   {
     name: 'separate_vm',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
 ]
 const replicaFields = [
   {
     name: 'shutdown_instances',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
 ]
 const migrationFields = [
   {
     name: 'skip_os_morphing',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
 ]
 
@@ -157,24 +157,35 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
     let fields = generalFields
     let useReplica = this.getFieldValue('use_replica')
     let skipFields = ['location', 'resource_group', 'network_map', 'storage_map', 'vm_size', 'worker_size']
+    let cleanup = fields => fields.filter(f => !skipFields.find(n => n === f.name)).map(f => {
+      if (f.type === 'boolean') {
+        f.nullableBoolean = true
+      }
+      return { ...f }
+    })
 
     if (useReplica) {
       fields = [...fields, ...replicaFields]
       if (this.state.showAdvancedOptions) {
-        fields = [...fields, ...this.props.replicaSchema.filter(f => !skipFields.find(n => n === f.name))]
+        fields = [
+          ...fields,
+          ...cleanup(this.props.replicaSchema),
+        ]
       }
     } else {
       fields = [...fields, ...migrationFields]
       if (this.state.showAdvancedOptions) {
-        fields = [...fields, ...this.props.migrationSchema.filter(f => !skipFields.find(n => n === f.name))]
+        fields = [
+          ...fields,
+          ...cleanup(this.props.migrationSchema),
+        ]
       }
     }
 
     const sortPriority: any = {
-      'strict-boolean': 1,
-      boolean: 2,
-      string: 3,
-      object: 4,
+      boolean: 1,
+      string: 2,
+      object: 3,
     }
     fields.sort((a, b) => {
       if (sortPriority[a.type] && sortPriority[b.type]) {
@@ -195,22 +206,25 @@ class AssessmentMigrationOptions extends React.Component<Props, State> {
       let additionalProps
       if (field.type === 'object' && field.properties) {
         additionalProps = {
-          valueCallback: propName => this.getObjectFieldValue(field.name, propName),
-          onChange: (value, propName) => { this.handleObjectValueChange(field.name, propName, value) },
-          properties: field.properties,
+          valueCallback: callbackField => this.getObjectFieldValue(field.name, callbackField.name),
+          onChange: (value, callbackField) => {
+            let propName = callbackField.name.substr(callbackField.name.lastIndexOf('/') + 1)
+            this.handleObjectValueChange(field.name, propName, value)
+          },
+          properties: field.properties.map(p => ({ ...p, required: false })),
         }
       } else {
         let value = this.getFieldValue(field.name)
         additionalProps = {
           value,
           onChange: value => { this.handleValueChange(field.name, value) },
-          type: field.type === 'strict-boolean' ? 'boolean' : field.type === 'boolean' ? 'optional-boolean' : field.type,
+          type: field.type,
         }
       }
 
       const currentField = (
         <FieldStyled
-          large
+          width={StyleProps.inputSizes.large.width}
           {...field}
           {...additionalProps}
         />

+ 2 - 9
src/components/organisms/ChooseProvider/test.jsx

@@ -25,19 +25,12 @@ const wrap = props => new TW(shallow(
   <ChooseProvider {...props} />
 ), 'cProvider')
 
-let providers = {
-  azure: {},
-  openstack: {},
-  opc: {},
-  oracle_vm: {},
-  vmware_vsphere: {},
-  aws: {},
-}
+let providers = ['azure', 'openstack', 'opc', 'oracle_vm', 'vmware_vsphere', 'aws']
 
 describe('ChooseProvider Component', () => {
   it('renders all given providers', () => {
     let wrapper = wrap({ providers })
-    Object.keys(providers).forEach(key => {
+    providers.forEach(key => {
       expect(wrapper.find(`endpointLogo-${key}`).prop('endpoint')).toBe(key)
     })
   })

+ 4 - 4
src/components/organisms/EndpointDuplicateOptions/EndpointDuplicateOptions.jsx

@@ -20,7 +20,7 @@ import styled from 'styled-components'
 
 import StatusImage from '../../atoms/StatusImage'
 import Button from '../../atoms/Button'
-import WizardOptionsField from '../../molecules/WizardOptionsField'
+import FieldInput from '../../molecules/FieldInput'
 
 import KeyboardManager from '../../../utils/KeyboardManager'
 import type { Project } from '../../../types/Project'
@@ -72,7 +72,7 @@ const Buttons = styled.div`
   justify-content: space-between;
   width: 100%;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField)`
+const FieldInputStyled = styled(FieldInput)`
   width: 319px;
   justify-content: space-between;
 `
@@ -120,15 +120,15 @@ class EndpointDuplicateOptions extends React.Component<Props, State> {
       <Options>
         <Image />
         <Form>
-          <WizardOptionsFieldStyled
+          <FieldInputStyled
             data-test-id={`${testName}-field-project`}
             name="duplicate_to_project"
             type="string"
             enum={this.props.projects}
-            skipNullValue
             value={this.state.selectedProjectId}
             onChange={projectId => { this.setState({ selectedProjectId: projectId }) }}
             width={318}
+            layout="page"
           />
         </Form>
         <Buttons>

+ 11 - 11
src/components/organisms/ProjectMemberModal/ProjectMemberModal.jsx

@@ -23,7 +23,7 @@ import type { User } from '../../../types/User'
 import type { Project, Role } from '../../../types/Project'
 import Button from '../../atoms/Button'
 import Modal from '../../molecules/Modal'
-import Field from '../../molecules/EndpointField'
+import FieldInput from '../../molecules/FieldInput'
 import ToggleButtonBar from '../../atoms/ToggleButtonBar'
 import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
 import StyleProps from '../../styleUtils/StyleProps'
@@ -58,8 +58,8 @@ const Form = styled.div`
     margin-top: 16px;
   }
 `
-const FieldStyled = styled(Field)`
-  ${StyleProps.exactWidth('224px')}
+const FieldStyled = styled(FieldInput)`
+  ${StyleProps.exactWidth(`${StyleProps.inputSizes.large.width}px`)}
 `
 const FormField = styled.div``
 const FormLabel = styled.div`
@@ -226,7 +226,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
     let highlighFieldName = this.state.isNew ? 'rolesNew' : 'rolesExisting'
 
     return (
-      <Field
+      <FieldInput
         data-test-id={`${testName}-roles`}
         key="roles"
         name="role(s)"
@@ -238,11 +238,11 @@ class ProjectMemberModal extends React.Component<Props, State> {
             setSelectedRoles([...selectedRoles, roleId])
           }
         }}
-        selectedItems={selectedRoles}
-        value={null}
-        large
+        value={selectedRoles}
+        width={StyleProps.inputSizes.large.width}
+        layout="modal"
         disabled={this.props.loading}
-        items={this.props.roles.filter(r => r.name !== 'key-manager:service-admin').map(r => { return { label: r.name, value: r.id } })}
+        enum={this.props.roles.filter(r => r.name !== 'key-manager:service-admin').map(r => { return { name: r.name, id: r.id } })}
         required
         highlight={Boolean(this.state.highlightFieldNames.find(n => n === highlighFieldName))}
         noSelectionMessage="Choose role(s)"
@@ -260,7 +260,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
         type={field.type || 'string'}
         value={value}
         onChange={onChange}
-        large
+        width={StyleProps.inputSizes.large.width}
         disabled={this.props.loading}
         enum={field.enum}
         password={field.name === 'password' || field.name === 'confirm_password'}
@@ -273,7 +273,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
   }
 
   renderNewForm() {
-    const userProjects = this.props.projects.map(p => { return { label: p.name, value: p.id } })
+    const userProjects = this.props.projects.map(p => ({ name: p.name, id: p.id }))
     const fields = [
       this.renderField(
         { name: 'username', required: true },
@@ -289,7 +289,7 @@ class ProjectMemberModal extends React.Component<Props, State> {
         {
           name: 'Primary Project',
           // $FlowIssue
-          enum: [{ label: 'Choose a project', value: null }].concat(userProjects),
+          enum: [{ name: 'Choose a project', id: null }].concat(userProjects),
         },
         this.state.projectId,
         projectId => { this.setState({ projectId }) },

+ 1 - 1
src/components/organisms/ProjectMemberModal/test.jsx

@@ -55,7 +55,7 @@ describe('ProjectMemberModal Component', () => {
       onAddClick: () => { },
     })
     expect(wrapper.find('users').prop('items')[1].value).toBe(users[1].id)
-    expect(wrapper.find('roles').prop('items')[1].value).toBe(roles[1].id)
+    expect(wrapper.find('roles').prop('enum')[1].id).toBe(roles[1].id)
     expect(wrapper.find('users').prop('highlight')).toBe(false)
     expect(wrapper.find('roles').prop('highlight')).toBe(false)
     expect(wrapper.find('users').prop('disabled')).toBe(false)

+ 5 - 3
src/components/organisms/ProjectModal/ProjectModal.jsx

@@ -22,10 +22,11 @@ import type { Project } from '../../../types/Project'
 import type { Field as FieldType } from '../../../types/Field'
 import Button from '../../atoms/Button'
 import Modal from '../../molecules/Modal'
-import Field from '../../molecules/EndpointField'
+import FieldInput from '../../molecules/FieldInput'
 
 import projectImage from './images/project.svg'
 import KeyboardManager from '../../../utils/KeyboardManager'
+import StyleProps from '../../styleUtils/StyleProps'
 
 const Wrapper = styled.div`
   padding: 48px 32px 32px 32px;
@@ -116,14 +117,15 @@ class ProjectModal extends React.Component<Props, State> {
 
   renderField(field: FieldType, value: any, onChange: (value: any) => void) {
     return (
-      <Field
+      <FieldInput
+        layout="modal"
         data-test-id={`${testName}-field-${field.name}`}
         key={field.name}
         name={field.name}
         type={field.type || 'string'}
         value={value}
         onChange={onChange}
-        large
+        width={StyleProps.inputSizes.large.width}
         disabled={this.props.loading}
         required={field.required}
         highlight={Boolean(this.state.highlightFieldNames.find(n => n === field.name))}

+ 5 - 4
src/components/organisms/ReplicaExecutionOptions/ReplicaExecutionOptions.jsx

@@ -19,7 +19,7 @@ import { observer } from 'mobx-react'
 import styled from 'styled-components'
 
 import Button from '../../atoms/Button'
-import WizardOptionsField from '../../molecules/WizardOptionsField'
+import FieldInput from '../../molecules/FieldInput'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import KeyboardManager from '../../../utils/KeyboardManager'
@@ -49,7 +49,7 @@ const Buttons = styled.div`
   justify-content: space-between;
   width: 100%;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField)`
+const FieldInputStyled = styled(FieldInput)`
   width: 319px;
   justify-content: space-between;
 `
@@ -110,10 +110,11 @@ class ReplicaExecutionOptions extends React.Component<Props, State> {
         <Form>
           {this.state.fields.map(field => {
             return (
-              <WizardOptionsFieldStyled
+              <FieldInputStyled
                 key={field.name}
                 name={field.name}
-                type="strict-boolean"
+                type="boolean"
+                layout="page"
                 value={this.getFieldValue(field)}
                 label={LabelDictionary.get(field.name)}
                 onChange={value => this.handleValueChange(field, value)}

+ 7 - 6
src/components/organisms/ReplicaMigrationOptions/ReplicaMigrationOptions.jsx

@@ -19,7 +19,7 @@ import { observer } from 'mobx-react'
 import styled from 'styled-components'
 
 import Button from '../../atoms/Button'
-import WizardOptionsField from '../../molecules/WizardOptionsField'
+import FieldInput from '../../molecules/FieldInput'
 
 import LabelDictionary from '../../../utils/LabelDictionary'
 import KeyboardManager from '../../../utils/KeyboardManager'
@@ -50,7 +50,7 @@ const Buttons = styled.div`
   justify-content: space-between;
   width: 100%;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField)`
+const FieldInputStyled = styled(FieldInput)`
   width: 319px;
   justify-content: space-between;
   margin-bottom: 32px;
@@ -66,16 +66,16 @@ type State = {
 let defaultFields: Field[] = [
   {
     name: 'clone_disks',
-    type: 'strict-boolean',
+    type: 'boolean',
     value: true,
   },
   {
     name: 'force',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
   {
     name: 'skip_os_morphing',
-    type: 'strict-boolean',
+    type: 'boolean',
   },
 ]
 @observer
@@ -109,11 +109,12 @@ class ReplicaMigrationOptions extends React.Component<Props, State> {
         <Form>
           {this.state.fields.map(field => {
             return (
-              <WizardOptionsFieldStyled
+              <FieldInputStyled
                 key={field.name}
                 name={field.name}
                 type={field.type}
                 value={field.value}
+                layout="page"
                 label={LabelDictionary.get(field.name)}
                 onChange={value => this.handleValueChange(field, value)}
               />

+ 6 - 6
src/components/organisms/UserModal/UserModal.jsx

@@ -23,10 +23,11 @@ import type { Project } from '../../../types/Project'
 import type { Field as FieldType } from '../../../types/Field'
 import Button from '../../atoms/Button'
 import Modal from '../../molecules/Modal'
-import Field from '../../molecules/EndpointField'
+import FieldInput from '../../molecules/FieldInput'
 
 import userImage from './images/user.svg'
 import KeyboardManager from '../../../utils/KeyboardManager'
+import StyleProps from '../../styleUtils/StyleProps'
 
 const Wrapper = styled.div`
   padding: 48px 0 32px 0;
@@ -173,14 +174,15 @@ class UserModal extends React.Component<Props, State> {
     let disabled = this.props.loading || (this.props.isLoggedUser && field.name === 'enabled')
 
     return (
-      <Field
+      <FieldInput
+        layout="modal"
         data-test-id={`${testName}-field-${field.name}`}
         key={field.name}
         name={field.name}
         type={field.type || 'string'}
         value={value}
         onChange={onChange}
-        large
+        width={StyleProps.inputSizes.large.width}
         disabled={disabled}
         enum={field.enum}
         password={field.name === 'new_password' || field.name === 'confirm_password'}
@@ -193,8 +195,6 @@ class UserModal extends React.Component<Props, State> {
 
   renderForm() {
     let fields
-    const userProjects = this.props.projects.map(p => { return { label: p.name, value: p.id } })
-
     const passwordFields = [
       this.renderField(
         { name: 'new_password', required: true },
@@ -227,7 +227,7 @@ class UserModal extends React.Component<Props, State> {
         {
           name: 'Primary Project',
           // $FlowIssue
-          enum: [{ label: 'Choose a project', value: null }].concat(userProjects),
+          enum: [{ name: 'Choose a project', id: null }].concat(this.props.projects.concat([])),
         },
         this.state.projectId,
         projectId => { this.setState({ projectId }) },

+ 1 - 1
src/components/organisms/WizardNetworks/WizardNetworks.jsx

@@ -196,7 +196,7 @@ class WizardNetworks extends React.Component<Props> {
               ) :
                 (
                   <Dropdown
-                    large
+                    width={StyleProps.inputSizes.large.width}
                     centered
                     noSelectionMessage="Select ..."
                     noItemsMessage={this.props.loading ? 'Loading ...' : 'No networks found'}

+ 16 - 9
src/components/organisms/WizardOptions/WizardOptions.jsx

@@ -21,7 +21,7 @@ import autobind from 'autobind-decorator'
 
 import StyleProps from '../../styleUtils/StyleProps'
 import ToggleButtonBar from '../../atoms/ToggleButtonBar'
-import WizardOptionsField from '../../molecules/WizardOptionsField'
+import FieldInput from '../../molecules/FieldInput'
 import StatusImage from '../../atoms/StatusImage'
 import type { Field } from '../../../types/Field'
 import type { Instance } from '../../../types/Instance'
@@ -50,7 +50,7 @@ const Column = styled.div`
   ${props => props.left ? 'margin-right: 160px;' : ''}
   margin-top: -16px;
 `
-const WizardOptionsFieldStyled = styled(WizardOptionsField)`
+const FieldInputStyled = styled(FieldInput)`
   width: ${props => props.width || StyleProps.inputSizes.wizard.width}px;
   justify-content: space-between;
   margin-top: 16px;
@@ -121,17 +121,17 @@ class WizardOptions extends React.Component<Props> {
     }
 
     if (this.props.wizardType === 'migration') {
-      fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'strict-boolean', default: false })
+      fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'boolean', default: false })
     }
 
     if (this.props.selectedInstances && this.props.selectedInstances.length > 1) {
       let dictionaryLabel = LabelDictionary.get('separate_vm')
       let label = this.props.wizardType === 'migration' ? dictionaryLabel : dictionaryLabel.replace('Migration', 'Replica')
-      fieldsSchema.unshift({ name: 'separate_vm', label, type: 'strict-boolean', default: true })
+      fieldsSchema.unshift({ name: 'separate_vm', label, type: 'boolean', default: true })
     }
 
     if (this.props.wizardType === 'replica') {
-      fieldsSchema.push({ name: 'execute_now', type: 'strict-boolean', default: true })
+      fieldsSchema.push({ name: 'execute_now', type: 'boolean', default: true })
       let executeNowValue = this.getFieldValue('execute_now', true)
       if (executeNowValue) {
         fieldsSchema = [
@@ -167,7 +167,7 @@ class WizardOptions extends React.Component<Props> {
     if (field.type === 'object' && field.properties) {
       additionalProps = {
         valueCallback: f => this.getFieldValue(f.name, f.default),
-        onChange: (f, value) => { this.props.onChange(f, value) },
+        onChange: (value, f) => { this.props.onChange(f, value) },
         properties: field.properties,
       }
     } else {
@@ -177,22 +177,26 @@ class WizardOptions extends React.Component<Props> {
       }
     }
     return (
-      <WizardOptionsFieldStyled
+      <FieldInputStyled
+        layout="page"
         key={field.name}
         name={field.name}
         type={field.type}
         enum={field.enum}
+        addNullValue
         required={field.required}
         data-test-id={`wOptions-field-${field.name}`}
-        width={this.props.fieldWidth}
+        width={this.props.fieldWidth || StyleProps.inputSizes.wizard.width}
         label={field.label}
+        nullableBoolean={field.nullableBoolean}
         {...additionalProps}
       />
     )
   }
 
   renderOptionsFields() {
-    let fieldsSchema = this.getDefaultFieldsSchema()
+    let fieldsSchema: Field[] = this.getDefaultFieldsSchema()
+    let nonNullableBooleans: string[] = fieldsSchema.filter(f => f.type === 'boolean').map(f => f.name)
 
     fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => f.required))
 
@@ -209,6 +213,9 @@ class WizardOptions extends React.Component<Props> {
       if (field.name === 'execute_now_options') {
         column = executeNowColumn
       }
+      if (field.type === 'boolean' && !nonNullableBooleans.find(name => name === field.name)) {
+        field.nullableBoolean = true
+      }
 
       return {
         column,

+ 1 - 1
src/components/organisms/WizardStorage/WizardStorage.jsx

@@ -207,7 +207,7 @@ class WizardStorage extends React.Component<Props> {
                 ) :
                   (
                     <Dropdown
-                      large
+                      width={StyleProps.inputSizes.large.width}
                       centered
                       noSelectionMessage="Default"
                       noItemsMessage="No storage found"

+ 1 - 1
src/constants.js

@@ -79,7 +79,7 @@ export const env = {
 export const executionOptions = [
   {
     name: 'shutdown_instances',
-    type: 'strict-boolean',
+    type: 'boolean',
     defaultValue: false,
   },
 ]

+ 1 - 1
src/plugins/endpoint/azure/ContentPlugin.jsx

@@ -210,7 +210,7 @@ class ContentPlugin extends React.Component<Props, State> {
     return (
       <FieldStyled
         {...field}
-        large
+        width={StyleProps.inputSizes.large.width}
         disabled={this.props.disabled}
         key={field.name}
         password={field.name === 'password'}

+ 6 - 4
src/plugins/endpoint/default/ContentPlugin.jsx

@@ -17,9 +17,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import * as React from 'react'
 import styled from 'styled-components'
 
-import EndpointField from '../../../components/molecules/EndpointField'
+import FieldInput from '../../../components/molecules/FieldInput'
 import type { Field } from '../../../types/Field'
 
+import StyleProps from '../../../components/styleUtils/StyleProps'
+
 export const Wrapper = styled.div`
   display: flex;
   flex-direction: column;
@@ -32,9 +34,9 @@ export const Fields = styled.div`
   flex-direction: column;
   overflow: auto;
 `
-export const FieldStyled = styled(EndpointField)`
+export const FieldStyled = styled(FieldInput)`
   min-width: ${props => props.useTextArea ? '100%' : '224px'};
-  max-width: 224px;
+  max-width: ${StyleProps.inputSizes.large.width}px;
   margin-bottom: 16px;
 `
 export const Row = styled.div`
@@ -85,7 +87,7 @@ class ContentPlugin extends React.Component<Props> {
       const currentField = (
         <FieldStyled
           {...field}
-          large
+          width={StyleProps.inputSizes.large.width}
           disabled={this.props.disabled}
           password={isPassword}
           highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}

+ 3 - 1
src/plugins/endpoint/openstack/ContentPlugin.jsx

@@ -23,6 +23,8 @@ import ToggleButtonBar from '../../../components/atoms/ToggleButtonBar'
 import type { Field } from '../../../types/Field'
 import { Wrapper, Fields, FieldStyled, Row } from '../default/ContentPlugin'
 
+import StyleProps from '../../../components/styleUtils/StyleProps'
+
 const ToggleButtonBarStyled = styled(ToggleButtonBar)`
   margin-top: 16px;
 `
@@ -169,7 +171,7 @@ class ContentPlugin extends React.Component<Props, State> {
         <FieldStyled
           {...field}
           required={required}
-          large
+          width={StyleProps.inputSizes.large.width}
           disabled={disabled}
           password={field.name === 'password'}
           highlight={this.props.invalidFields.findIndex(fn => fn === field.name) > -1}

+ 1 - 0
src/types/Field.js

@@ -22,6 +22,7 @@ export type Field = {
   // $FlowIssue
   enum?: string[] | { id: string, name: string, [string]: mixed }[],
   default?: any,
+  nullableBoolean?: boolean,
   items?: Field[],
   fields?: Field[],
   minimum?: number,