FieldInput.tsx 13 KB

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