FieldInput.jsx 12 KB

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