FieldConfigModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import React from 'react'
  2. import classnames from 'classnames'
  3. import { findDOMNode } from 'react-dom'
  4. import { Icon, Row, Col, Form, Input, Checkbox, Select, Button, Popover, Modal, message } from 'antd'
  5. import { FormComponentProps } from 'antd/lib/form'
  6. import { WrappedFormUtils } from 'antd/lib/form/Form'
  7. import { CheckboxChangeEvent } from 'antd/lib/checkbox'
  8. const FormItem = Form.Item
  9. const { TextArea } = Input
  10. const { Option } = Select
  11. import 'codemirror/lib/codemirror.css'
  12. import 'assets/override/codemirror_theme.css'
  13. import 'codemirror/addon/hint/show-hint.css'
  14. const codeMirror = require('codemirror/lib/codemirror')
  15. require('codemirror/addon/edit/matchbrackets')
  16. require('codemirror/mode/javascript/javascript')
  17. require('codemirror/addon/hint/show-hint')
  18. require('codemirror/addon/hint/javascript-hint')
  19. require('codemirror/addon/display/placeholder')
  20. import { IFieldConfig } from './types'
  21. import { getDefaultFieldConfig, extractQueryVariableNames, getFieldAlias } from './util'
  22. import { IQueryVariableMap } from 'containers/Dashboard/types'
  23. import AliasExpressionTestModal from './AliasExpressionTest'
  24. const utilStyles = require('assets/less/util.less')
  25. interface IFieldConfigProps extends FormComponentProps {
  26. visible: boolean
  27. fieldConfig: IFieldConfig
  28. queryInfo: string[]
  29. onSave: (config: IFieldConfig) => void
  30. onCancel: () => void
  31. }
  32. interface IFieldConfigStates {
  33. localConfig: IFieldConfig
  34. queryVariableNames: string[]
  35. testResult: string
  36. testModalVisible: boolean
  37. }
  38. class FieldConfig extends React.PureComponent<IFieldConfigProps, IFieldConfigStates> {
  39. private codeEditor = React.createRef<any>()
  40. private codeMirrorInst: any
  41. private isInitEditor: boolean = true
  42. constructor (props: IFieldConfigProps) {
  43. super(props)
  44. const { fieldConfig } = this.props
  45. this.state = {
  46. localConfig: fieldConfig ? { ...fieldConfig } : getDefaultFieldConfig(),
  47. queryVariableNames: [],
  48. testResult: '',
  49. testModalVisible: false
  50. }
  51. }
  52. public componentDidMount () {
  53. const { form } = this.props
  54. const { localConfig } = this.state
  55. this.setFieldsValue(form, localConfig)
  56. }
  57. public componentDidUpdate () {
  58. if (this.state.localConfig.useExpression) {
  59. // @FIXME refactor to remove setTimeout usage
  60. const tId = setTimeout(() => {
  61. this.isInitEditor = this.initCodeEditor(this.isInitEditor)
  62. clearTimeout(tId)
  63. }, 0)
  64. }
  65. }
  66. public componentWillReceiveProps (nextProps: IFieldConfigProps) {
  67. const { fieldConfig, form } = nextProps
  68. if (fieldConfig !== this.props.fieldConfig) {
  69. this.isInitEditor = true
  70. form.resetFields()
  71. this.setState({
  72. localConfig: fieldConfig ? { ...fieldConfig } : getDefaultFieldConfig(),
  73. queryVariableNames: [],
  74. testResult: ''
  75. }, () => {
  76. this.setFieldsValue(form, this.state.localConfig)
  77. })
  78. }
  79. }
  80. private useExpressionChange = (e: CheckboxChangeEvent) => {
  81. const useExpression = e.target.checked
  82. this.setState({
  83. localConfig: {
  84. ...this.state.localConfig,
  85. useExpression
  86. }
  87. })
  88. }
  89. private setFieldsValue = (form: WrappedFormUtils, config: IFieldConfig) => {
  90. const { alias, desc, useExpression } = config
  91. form.setFieldsValue({
  92. [`alias_${useExpression ? 1 : 0}`]: alias,
  93. desc,
  94. useExpression
  95. })
  96. }
  97. private getFieldsValue (form: WrappedFormUtils): IFieldConfig {
  98. const fieldsValue: any = form.getFieldsValue()
  99. const { useExpression, desc } = fieldsValue
  100. const alias = useExpression
  101. ? this.codeMirrorInst.doc.getValue()
  102. : fieldsValue['alias_0']
  103. const config: IFieldConfig = {
  104. alias,
  105. useExpression,
  106. desc
  107. }
  108. return config
  109. }
  110. private initCodeEditor = (isInit: boolean) => {
  111. if (!this.codeMirrorInst) {
  112. // @FIXME ref is null in componentDidUpdate
  113. if (!this.codeEditor.current) { return true }
  114. const codeEditorDom = findDOMNode(this.codeEditor.current)
  115. this.codeMirrorInst = codeMirror.fromTextArea(codeEditorDom, {
  116. mode: 'text/javascript',
  117. theme: '3024-day',
  118. lineNumbers: true,
  119. lineWrapping: true
  120. })
  121. this.codeMirrorInst.setSize('100%', 300)
  122. }
  123. if (isInit && this.state.localConfig.useExpression) {
  124. this.codeMirrorInst.doc.setValue(this.state.localConfig.alias)
  125. }
  126. return false
  127. }
  128. private addQueryVariable = () => {
  129. const { form } = this.props
  130. const queryVariable = form.getFieldValue('queryVariable')
  131. this.codeMirrorInst.replaceSelection(` $${queryVariable}$ `)
  132. }
  133. private testExpression = () => {
  134. const expression: string = this.codeMirrorInst.doc.getValue()
  135. const queryVariableNames = extractQueryVariableNames(expression)
  136. const testModalVisible = queryVariableNames.length > 0
  137. this.setState({
  138. queryVariableNames,
  139. testModalVisible
  140. })
  141. if (!testModalVisible) {
  142. this.testExpressionResult()
  143. }
  144. }
  145. private testExpressionResult = (queryVariableMap: IQueryVariableMap = {}) => {
  146. const { form } = this.props
  147. const fieldConfig = this.getFieldsValue(form)
  148. const testResult = getFieldAlias(fieldConfig, queryVariableMap)
  149. this.setState({
  150. testResult,
  151. testModalVisible: false
  152. })
  153. return testResult
  154. }
  155. private clearExpressionResult = () => {
  156. this.setState({
  157. testResult: ''
  158. })
  159. }
  160. private closeTestModal = () => {
  161. this.setState({ testModalVisible: false })
  162. }
  163. private save = () => {
  164. const { form, onSave } = this.props
  165. form.validateFieldsAndScroll((err) => {
  166. if (err) { return }
  167. const config = this.getFieldsValue(form)
  168. onSave(config)
  169. })
  170. }
  171. private cancel = () => {
  172. this.props.onCancel()
  173. }
  174. private modalFooter = [(
  175. <Button
  176. key="cancel"
  177. size="large"
  178. onClick={this.cancel}
  179. >
  180. 取 消
  181. </Button>
  182. ), (
  183. <Button
  184. key="submit"
  185. size="large"
  186. type="primary"
  187. onClick={this.save}
  188. >
  189. 保 存
  190. </Button>
  191. )]
  192. private useExprInstr = (
  193. <div>
  194. <pre>
  195. {`1. 编辑器中编辑 JavaScript Function 代码片段,最后需要 return 别名值。
  196. 2. 若当前编辑的 Widget 对应的 View 中含有查询变量 queryVar 的定义,则字段别名中可添加查询变量。
  197. 在使用该 Widget 时,由界面中提供的查询变量值参与动态别名运算,生成对应的别名结果。
  198. 3. 可使用时间辅助函数生成格式时间字符串参与别名运算。
  199. Moment() // 取当前时间
  200. Moment('2018-01-01') // 转换指定的日期
  201. Moment().add(1, 'days').add(-1, 'months') // 日期推算,可链式调用,其中参数分别为数量和单位('hours', days', 'weeks', 'months', 'years' 等)
  202. Moment().format('YYYY-MM-DD HH:mm:ss') // 时间的格式化,其中两位的重复字母依次代表“年”、“月”、“日”、“时”、“分”、“秒”
  203. 4. 编写完毕后,可点击“测试”按钮查看显示效果,若含有参数,则需要在弹窗中需要测试的参数值,若报错,请调整编写。
  204. 5. 例子
  205. var province = $province$ // $province 为 View 中查询变量
  206. var currentYearMonth = Moment().format('YYYY年MM月') // 格式化当前年月
  207. var alias = province + '(' + currentYearMonth + ')' // 拼写最后显示的别名
  208. return alias
  209. `}
  210. </pre>
  211. </div>
  212. )
  213. public render () {
  214. const { visible, queryInfo, form } = this.props
  215. const { getFieldDecorator } = form
  216. const { localConfig, testResult, testModalVisible, queryVariableNames } = this.state
  217. const { desc, useExpression } = localConfig
  218. const variableOptions = (queryInfo || []).map((q) => (
  219. <Option key={q} value={q}>{q}</Option>
  220. ))
  221. const textAreaCls = classnames({ [utilStyles.hide]: !useExpression })
  222. const inputCls = classnames({ [utilStyles.hide]: useExpression })
  223. return (
  224. <Modal
  225. title="字段设置"
  226. wrapClassName="ant-modal-medium"
  227. footer={this.modalFooter}
  228. visible={visible}
  229. maskClosable={false}
  230. onCancel={this.cancel}
  231. >
  232. <Form>
  233. <FormItem label="字段别名" className={inputCls}>
  234. {getFieldDecorator('alias_0')(<Input />)}
  235. </FormItem>
  236. <FormItem label="字段别名" className={textAreaCls} style={{ height: '325px' }}>
  237. <TextArea ref={this.codeEditor} placeholder="请输入动态表达式" />
  238. </FormItem>
  239. {useExpression && <Row type="flex" align="middle" gutter={8}>
  240. {variableOptions.length > 0 && (
  241. <>
  242. <Col span={9}>
  243. <FormItem label="查询变量" labelCol={{span: 8}} wrapperCol={{span: 16}}>
  244. {getFieldDecorator('queryVariable', {
  245. initialValue: queryInfo[0]
  246. })(
  247. <Select>
  248. {variableOptions}
  249. </Select>
  250. )}
  251. </FormItem>
  252. </Col>
  253. <Col span={4}>
  254. <Row type="flex" align="middle">
  255. <FormItem>
  256. <Button onClick={this.addQueryVariable}>添加</Button>
  257. </FormItem>
  258. </Row>
  259. </Col>
  260. </>
  261. )}
  262. <Col span={3}>
  263. <FormItem>
  264. <Button type="primary" onClick={this.testExpression}>测试</Button>
  265. </FormItem>
  266. </Col>
  267. <Col span={8}>
  268. <FormItem>
  269. <Input
  270. readOnly
  271. placeholder="别名结果"
  272. value={testResult}
  273. addonAfter={<Icon type="close" onClick={this.clearExpressionResult} title="清除结果" />}
  274. />
  275. </FormItem>
  276. </Col>
  277. </Row>}
  278. <Row gutter={8} align="middle" justify="center">
  279. <Col span={9}>
  280. <FormItem>
  281. {getFieldDecorator('useExpression', {
  282. initialValue: useExpression,
  283. valuePropName: 'checked'
  284. })(<Checkbox onChange={this.useExpressionChange}>动态别名</Checkbox>)}
  285. <Popover
  286. title="动态别名使用说明"
  287. content={this.useExprInstr}
  288. >
  289. <Icon type="info-circle" />
  290. </Popover>
  291. </FormItem>
  292. </Col>
  293. </Row>
  294. <FormItem label="字段描述">
  295. {getFieldDecorator('desc', {
  296. initialValue: desc
  297. })(<TextArea rows={4} />)}
  298. </FormItem>
  299. </Form>
  300. <AliasExpressionTestModal
  301. visible={testModalVisible}
  302. queryVariableNames={queryVariableNames}
  303. onClose={this.closeTestModal}
  304. onTest={this.testExpressionResult}
  305. />
  306. </Modal>
  307. )
  308. }
  309. }
  310. export default Form.create<IFieldConfigProps>()(FieldConfig)