FieldInput.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import { observer } from 'mobx-react'
  17. import styled, { css } from 'styled-components'
  18. import Switch from '../../atoms/Switch/Switch'
  19. import TextInput from '../../atoms/TextInput/TextInput'
  20. import RadioInput from '../../atoms/RadioInput/RadioInput'
  21. import InfoIcon from '../../atoms/InfoIcon/InfoIcon'
  22. import Dropdown from '../Dropdown/Dropdown'
  23. import DropdownInput from '../DropdownInput/DropdownInput'
  24. import TextArea from '../../atoms/TextArea/TextArea'
  25. import PropertiesTable from '../PropertiesTable/PropertiesTable'
  26. import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
  27. import Stepper from '../../atoms/Stepper'
  28. import type { Field } from '../../../types/Field'
  29. import LabelDictionary from '../../../utils/LabelDictionary'
  30. import StyleProps from '../../styleUtils/StyleProps'
  31. import Palette from '../../styleUtils/Palette'
  32. import asteriskImage from './images/asterisk.svg'
  33. const Wrapper = styled.div`
  34. ${props => props.layout === 'page' ? css`
  35. display: flex;
  36. flex-direction: ${props.inline ? 'row' : 'column'};
  37. ${props.inline ? '' : css`justify-content: center;`}
  38. ` : ''}
  39. `
  40. const Label = styled.div`
  41. font-weight: ${StyleProps.fontWeights.medium};
  42. flex-grow: 1;
  43. ${props => props.layout === 'page' ? css`
  44. margin-bottom: 8px;
  45. ` : css`
  46. margin-bottom: 2px;
  47. font-size: 10px;
  48. color: ${Palette.grayscale[3]};
  49. text-transform: uppercase;
  50. display: flex;
  51. align-items: center;
  52. `}
  53. ${props => props.disabledLoading ? StyleProps.animations.disabledLoading : ''}
  54. `
  55. const LabelText = styled.span``
  56. const Asterisk = styled.div`
  57. ${StyleProps.exactSize('16px')}
  58. display: inline-block;
  59. background: url('${asteriskImage}') center no-repeat;
  60. margin-bottom: -3px;
  61. margin-left: ${props => props.marginLeft || '0px'};
  62. `
  63. type Props = {
  64. name: string,
  65. type: string,
  66. value: any,
  67. onChange?: (value: any, field?: Field) => void,
  68. valueCallback?: (field: Field) => any,
  69. getFieldValue?: (fieldName: string) => string,
  70. onFieldChange?: (fieldName: string, fieldValue: string) => void,
  71. className?: string,
  72. properties?: Field[],
  73. // $FlowIgnore
  74. enum?: string[] | { label: string, value: string }[] | { name: string, id: string }[],
  75. required?: boolean,
  76. minimum?: number,
  77. maximum?: number,
  78. password?: boolean,
  79. highlight?: boolean,
  80. disabled?: boolean,
  81. disabledLoading?: boolean,
  82. items?: any[],
  83. useTextArea?: boolean,
  84. noSelectionMessage?: string,
  85. noItemsMessage?: string,
  86. layout: 'modal' | 'page',
  87. width?: number,
  88. label?: string,
  89. addNullValue?: boolean,
  90. nullableBoolean?: boolean,
  91. description?: string,
  92. style?: { [string]: mixed },
  93. }
  94. @observer
  95. class FieldInput extends React.Component<Props> {
  96. renderSwitch(propss: { triState: boolean }) {
  97. return (
  98. <Switch
  99. width={this.props.layout === 'page' ? '112px' : ''}
  100. height={this.props.layout === 'page' ? 16 : 24}
  101. justifyContent={this.props.layout === 'page' ? 'flex-end' : ''}
  102. disabled={this.props.disabled}
  103. disabledLoading={this.props.disabledLoading}
  104. triState={propss.triState}
  105. checked={this.props.value}
  106. onChange={checked => { if (this.props.onChange) this.props.onChange(checked) }}
  107. leftLabel={this.props.layout === 'page'}
  108. style={this.props.layout === 'page' ? { marginTop: '-8px' } : {}}
  109. />
  110. )
  111. }
  112. renderTextInput() {
  113. return (
  114. <TextInput
  115. width={this.props.width}
  116. highlight={this.props.highlight}
  117. type={this.props.password ? 'password' : 'text'}
  118. value={this.props.value}
  119. onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
  120. placeholder={LabelDictionary.get(this.props.name)}
  121. disabled={this.props.disabled}
  122. required={this.props.layout === 'page' ? false : this.props.required}
  123. disabledLoading={this.props.disabledLoading}
  124. />
  125. )
  126. }
  127. renderIntInput() {
  128. if (this.props.minimum && this.props.maximum && this.props.maximum - this.props.minimum <= 10) {
  129. let items = []
  130. for (let i = this.props.minimum; i <= this.props.maximum; i += 1) {
  131. items.push({
  132. label: i.toString(),
  133. value: i,
  134. })
  135. }
  136. return (
  137. <Dropdown
  138. width={this.props.width}
  139. selectedItem={this.props.value}
  140. items={items}
  141. onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
  142. disabled={this.props.disabled}
  143. disabledLoading={this.props.disabledLoading}
  144. highlight={this.props.highlight}
  145. required={this.props.layout === 'page' ? false : this.props.required}
  146. />
  147. )
  148. }
  149. return (
  150. <Stepper
  151. highlight={Boolean(this.props.highlight)}
  152. width={this.props.width ? `${this.props.width}px` : undefined}
  153. value={this.props.value}
  154. disabled={this.props.disabled}
  155. disabledLoading={this.props.disabledLoading}
  156. required={this.props.layout === 'page' ? false : this.props.required}
  157. onChange={value => { if (this.props.onChange) this.props.onChange(value) }}
  158. minimum={this.props.minimum}
  159. maximum={this.props.maximum}
  160. />
  161. )
  162. }
  163. renderObjectTable() {
  164. if (!this.props.properties || !this.props.properties.length) {
  165. return null
  166. }
  167. return (
  168. <PropertiesTable
  169. properties={this.props.properties}
  170. valueCallback={field => this.props.valueCallback && this.props.valueCallback(field)}
  171. onChange={(field, value) => {
  172. if (this.props.onChange) {
  173. this.props.onChange(value, field)
  174. }
  175. }}
  176. hideRequiredSymbol={this.props.layout === 'page'}
  177. disabledLoading={this.props.disabledLoading}
  178. />
  179. )
  180. }
  181. renderTextArea() {
  182. return (
  183. <TextArea
  184. style={{ width: '100%' }}
  185. highlight={this.props.highlight}
  186. value={this.props.value}
  187. onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
  188. placeholder={LabelDictionary.get(this.props.name)}
  189. disabled={this.props.disabled}
  190. disabledLoading={this.props.disabledLoading}
  191. required={this.props.layout === 'page' ? false : this.props.required}
  192. />
  193. )
  194. }
  195. renderEnumDropdown() {
  196. const useDictionary = LabelDictionary.enumFields.find(f => f === this.props.name)
  197. let items = this.props.enum.map(e => {
  198. if (typeof e !== 'string' && e.separator === true) {
  199. return e
  200. }
  201. return {
  202. label: typeof e === 'string' ? (useDictionary ? LabelDictionary.get(e) : e) : e.name || e.label,
  203. value: typeof e === 'string' ? e : e.id || e.value,
  204. }
  205. })
  206. if (this.props.addNullValue) {
  207. items = [
  208. { label: 'Choose a value', value: null },
  209. ...items,
  210. ]
  211. }
  212. let selectedItem = items.find(i => i.value === this.props.value)
  213. let commonProps = {
  214. width: this.props.width,
  215. required: this.props.layout === 'page' ? false : this.props.required,
  216. selectedItem,
  217. items,
  218. disabledLoading: this.props.disabledLoading,
  219. disabled: this.props.disabled,
  220. onChange: item => this.props.onChange && this.props.onChange(item.value),
  221. }
  222. if (items.length < 10) {
  223. return (
  224. <Dropdown
  225. {...commonProps}
  226. noSelectionMessage="Choose a value"
  227. dimFirstItem={this.props.addNullValue}
  228. />
  229. )
  230. }
  231. return (
  232. <AutocompleteDropdown
  233. {...commonProps}
  234. dimNullValue
  235. />
  236. )
  237. }
  238. renderArrayDropdown() {
  239. let items = this.props.enum.map(e => {
  240. if (typeof e !== 'string' && e.separator === true) {
  241. return e
  242. }
  243. return {
  244. label: typeof e === 'string' ? e : e.name || e.label,
  245. value: typeof e === 'string' ? e : e.id || e.value,
  246. }
  247. })
  248. let selectedItems = this.props.value || []
  249. return (
  250. <Dropdown
  251. multipleSelection
  252. width={this.props.width}
  253. disabled={this.props.disabled}
  254. disabledLoading={this.props.disabledLoading}
  255. noSelectionMessage="Choose values"
  256. noItemsMessage={this.props.noItemsMessage}
  257. items={items}
  258. selectedItems={selectedItems}
  259. onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
  260. highlight={this.props.highlight}
  261. required={this.props.layout === 'page' ? false : this.props.required}
  262. />
  263. )
  264. }
  265. renderRadioInput() {
  266. return (
  267. <RadioInput
  268. checked={this.props.value}
  269. label={LabelDictionary.get(this.props.name)}
  270. onChange={checked => { if (this.props.onChange) this.props.onChange(checked) }}
  271. disabled={this.props.disabled}
  272. disabledLoading={this.props.disabledLoading}
  273. />
  274. )
  275. }
  276. renderDropdownInput() {
  277. if (!this.props.items) {
  278. return null
  279. }
  280. let items = this.props.items.map(field => {
  281. return {
  282. value: field.name,
  283. label: field.label || LabelDictionary.get(field.name),
  284. }
  285. })
  286. let fieldName = this.props.value || items[0].value
  287. return (
  288. <DropdownInput
  289. items={items}
  290. selectedItem={fieldName}
  291. onItemChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
  292. inputValue={this.props.getFieldValue ? this.props.getFieldValue(fieldName) : ''}
  293. onInputChange={value => { if (this.props.onFieldChange) this.props.onFieldChange(fieldName, value) }}
  294. placeholder={LabelDictionary.get(fieldName)}
  295. highlight={this.props.highlight}
  296. disabled={this.props.disabled}
  297. disabledLoading={this.props.disabledLoading}
  298. required={this.props.layout === 'page' ? false : this.props.required}
  299. />
  300. )
  301. }
  302. renderInput() {
  303. switch (this.props.type) {
  304. case 'input-choice':
  305. return this.renderDropdownInput()
  306. case 'boolean':
  307. return this.renderSwitch({ triState: Boolean(this.props.nullableBoolean) })
  308. case 'string':
  309. if (this.props.enum && this.props.enum.length) {
  310. return this.renderEnumDropdown()
  311. }
  312. if (this.props.useTextArea) {
  313. return this.renderTextArea()
  314. }
  315. return this.renderTextInput()
  316. case 'integer':
  317. return this.renderIntInput()
  318. case 'radio':
  319. return this.renderRadioInput()
  320. case 'array':
  321. return this.renderArrayDropdown()
  322. case 'object':
  323. return this.renderObjectTable()
  324. default:
  325. return null
  326. }
  327. }
  328. renderLabel() {
  329. if (this.props.type === 'radio') {
  330. return null
  331. }
  332. let description = LabelDictionary.getDescription(this.props.name) || this.props.description
  333. let marginRight = this.props.layout === 'modal' || description || this.props.required ? '24px' : 0
  334. return (
  335. <Label layout={this.props.layout} disabledLoading={this.props.disabledLoading}>
  336. <LabelText style={{ marginRight }}>
  337. {this.props.label || LabelDictionary.get(this.props.name)}
  338. </LabelText>
  339. {description ? <InfoIcon text={description} marginLeft={-20} marginBottom={this.props.layout === 'page' ? null : 0} /> : null}
  340. {this.props.layout === 'page' && Boolean(this.props.required) ? <Asterisk marginLeft={description ? '4px' : '-16px'} /> : null}
  341. </Label>
  342. )
  343. }
  344. render() {
  345. return (
  346. <Wrapper
  347. className={this.props.className}
  348. inline={this.props.type === 'boolean'}
  349. style={this.props.style}
  350. layout={this.props.layout}
  351. >
  352. {this.renderLabel()}
  353. {this.renderInput()}
  354. </Wrapper>
  355. )
  356. }
  357. }
  358. export default FieldInput