Editor.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, { useEffect, useState, useCallback } from 'react'
  21. import moment, { Moment } from 'moment'
  22. import Helmet from 'react-helmet'
  23. import { connect } from 'react-redux'
  24. import { compose } from 'redux'
  25. import { RouteComponentWithParams } from 'utils/types'
  26. import injectReducer from 'utils/injectReducer'
  27. import injectSaga from 'utils/injectSaga'
  28. import { createStructuredSelector } from 'reselect'
  29. import {
  30. makeSelectLoading,
  31. makeSelectEditingSchedule,
  32. makeSelectSuggestMails,
  33. makeSelectPortalDashboards
  34. } from './selectors'
  35. import {
  36. makeSelectPortals,
  37. makeSelectDisplays,
  38. makeSelectDisplaySlides
  39. } from 'containers/Viz/selectors'
  40. import { checkNameUniqueAction } from 'containers/App/actions'
  41. import { ScheduleActions } from './actions'
  42. import { hideNavigator } from 'containers/App/actions'
  43. import { VizActions } from 'containers/Viz/actions'
  44. import reducer from './reducer'
  45. import saga, { editSchedule } from './sagas'
  46. import vizReducer from 'containers/Viz/reducer'
  47. import vizSaga from 'containers/Viz/sagas'
  48. import dashboardSaga from 'containers/Dashboard/sagas'
  49. import { Row, Col, Card, Button, Icon, Tooltip, message } from 'antd'
  50. import { FormComponentProps } from 'antd/lib/form/Form'
  51. import ScheduleBaseConfig, {
  52. ScheduleBaseFormProps
  53. } from './components/ScheduleBaseConfig'
  54. import ScheduleMailConfig from './components/ScheduleMailConfig'
  55. import ScheduleVizConfig from './components/ScheduleVizConfig'
  56. import {
  57. IPortal,
  58. IDashboard,
  59. IDisplayFormed,
  60. ISlideFormed
  61. } from 'containers/Viz/types'
  62. import { IProject } from 'containers/Projects/types'
  63. import { ISchedule, IScheduleLoading } from './types'
  64. import {
  65. IUserInfo,
  66. IScheduleMailConfig,
  67. SchedulePeriodUnit,
  68. ICronExpressionPartition,
  69. IScheduleVizConfigItem,
  70. IScheduleWeChatWorkConfig,
  71. JobType
  72. } from './components/types'
  73. import { serialize } from 'components/RichText/Serializer'
  74. import { RichTextNode } from 'app/components/RichText'
  75. import Styles from './Schedule.less'
  76. import StylesHeader from 'components/EditorHeader/EditorHeader.less'
  77. import ScheduleWeChatWorkConfig from './components/ScheduleWeChatWorkConfig'
  78. const getCronExpressionByPartition = (partition: ICronExpressionPartition) => {
  79. const { periodUnit, minute, hour, day, weekDay, month } = partition
  80. let cronExpression = ''
  81. switch (periodUnit as SchedulePeriodUnit) {
  82. case 'Minute':
  83. cronExpression = `0 */${minute} * * * ?`
  84. break
  85. case 'Hour':
  86. cronExpression = `0 ${minute} * * * ?`
  87. break
  88. case 'Day':
  89. cronExpression = `0 ${minute} ${hour} * * ?`
  90. break
  91. case 'Week':
  92. cronExpression = `0 ${minute} ${hour} ? * ${weekDay}`
  93. break
  94. case 'Month':
  95. cronExpression = `0 ${minute} ${hour} ${day} * ?`
  96. break
  97. case 'Year':
  98. cronExpression = `0 ${minute} ${hour} ${day} ${month} ?`
  99. break
  100. }
  101. return cronExpression
  102. }
  103. interface IScheduleEditorStateProps {
  104. displays: IDisplayFormed[]
  105. portals: IPortal[]
  106. portalDashboards: { [key: number]: IDashboard[] }
  107. displaySlides: { [key: number]: ISlideFormed[] }
  108. loading: IScheduleLoading
  109. editingSchedule: ISchedule
  110. suggestMails: IUserInfo[]
  111. currentProject: IProject
  112. }
  113. interface IScheduleEditorDispatchProps {
  114. onHideNavigator: () => void
  115. onLoadDisplays: (projectId: number) => void
  116. onLoadPortals: (projectId: number) => void
  117. onLoadDisplaySlides: (displayId: number) => void
  118. onLoadDashboards: (portalId: number) => void
  119. onLoadScheduleDetail: (scheduleId: number) => void
  120. onAddSchedule: (schedule: ISchedule, resolve: () => void) => any
  121. onEditSchedule: (schedule: ISchedule, resolve: () => void) => any
  122. onResetState: () => void
  123. onCheckUniqueName: (
  124. data: any,
  125. resolve: () => any,
  126. reject: (error: string) => any
  127. ) => any
  128. onLoadSuggestMails: (keyword: string) => any
  129. onChangeJobType: (jobType: JobType) => any
  130. }
  131. type ScheduleEditorProps = IScheduleEditorStateProps &
  132. IScheduleEditorDispatchProps &
  133. RouteComponentWithParams
  134. const ScheduleEditor: React.FC<ScheduleEditorProps> = (props) => {
  135. const {
  136. onHideNavigator,
  137. onLoadDisplays,
  138. onLoadPortals,
  139. onLoadScheduleDetail,
  140. onResetState,
  141. match,
  142. history
  143. } = props
  144. const { projectId, scheduleId } = match.params
  145. useEffect(() => {
  146. onHideNavigator()
  147. onLoadDisplays(+projectId)
  148. onLoadPortals(+projectId)
  149. if (+scheduleId) {
  150. onLoadScheduleDetail(+scheduleId)
  151. }
  152. return () => {
  153. onResetState()
  154. }
  155. }, [])
  156. const goBack = useCallback(() => {
  157. history.push(`/project/${projectId}/schedules`)
  158. }, [])
  159. const {
  160. portals,
  161. displays,
  162. loading,
  163. editingSchedule,
  164. onLoadDisplaySlides,
  165. onLoadDashboards
  166. } = props
  167. const loadVizDetail = useCallback(
  168. (
  169. type: IScheduleVizConfigItem['contentType'],
  170. schedule: ISchedule,
  171. vizs: IPortal[] | IDisplayFormed[]
  172. ) => {
  173. if (!schedule.id || !vizs.length) {
  174. return
  175. }
  176. const { contentList } = schedule.config
  177. // initial Viz loading by contentList Portal or Display setting
  178. contentList.forEach(({ contentType, id: vizId }) => {
  179. if (contentType !== type) {
  180. return
  181. }
  182. if (~vizs.findIndex(({ id }) => id === vizId)) {
  183. switch (type) {
  184. case 'portal':
  185. onLoadDashboards(vizId)
  186. break
  187. case 'display':
  188. onLoadDisplaySlides(vizId)
  189. break
  190. }
  191. }
  192. })
  193. },
  194. []
  195. )
  196. useEffect(() => {
  197. loadVizDetail('portal', editingSchedule, portals)
  198. }, [portals, editingSchedule])
  199. useEffect(() => {
  200. loadVizDetail('display', editingSchedule, displays)
  201. }, [displays, editingSchedule])
  202. const {
  203. suggestMails,
  204. portalDashboards,
  205. displaySlides,
  206. onAddSchedule,
  207. onEditSchedule,
  208. onCheckUniqueName,
  209. onLoadSuggestMails,
  210. onChangeJobType
  211. } = props
  212. const { jobStatus, config } = editingSchedule
  213. const { contentList } = config
  214. const [localContentList, setLocalContentList] = useState(contentList)
  215. useEffect(() => {
  216. setLocalContentList([...contentList])
  217. }, [contentList])
  218. let baseConfigForm: FormComponentProps<ScheduleBaseFormProps> = null
  219. let mailConfigForm: FormComponentProps<IScheduleMailConfig> = null
  220. let weChatWorkConfigForm: FormComponentProps<IScheduleWeChatWorkConfig> = null
  221. const saveSchedule = () => {
  222. if (!localContentList.length) {
  223. message.error('请勾选发送内容')
  224. return
  225. }
  226. baseConfigForm.form.validateFieldsAndScroll((err1, value1) => {
  227. if (err1) {
  228. return
  229. }
  230. const { setCronExpressionManually, ...scheduleBase } = value1
  231. const [startDate, endDate] = baseConfigForm.form.getFieldValue(
  232. 'dateRange'
  233. ) as ScheduleBaseFormProps['dateRange']
  234. delete scheduleBase.dateRange
  235. const schedule: ISchedule = {
  236. ...scheduleBase,
  237. cronExpression: setCronExpressionManually
  238. ? scheduleBase.cronExpression
  239. : getCronExpressionByPartition(scheduleBase),
  240. startDate: moment(startDate).format('YYYY-MM-DD HH:mm:ss'),
  241. endDate: moment(endDate).format('YYYY-MM-DD HH:mm:ss'),
  242. projectId: +projectId
  243. }
  244. if (editingSchedule.jobType === 'email') {
  245. mailConfigForm.form.validateFieldsAndScroll((err2, value2) => {
  246. if (err2) {
  247. return
  248. }
  249. schedule.config = {
  250. ...value2,
  251. contentList: localContentList,
  252. setCronExpressionManually
  253. }
  254. schedule.config.content = serialize(
  255. schedule.config.content as RichTextNode[]
  256. )
  257. })
  258. } else {
  259. weChatWorkConfigForm.form.validateFieldsAndScroll((err3, value3) => {
  260. if (err3) {
  261. return
  262. }
  263. schedule.config = {
  264. ...value3,
  265. contentList: localContentList,
  266. setCronExpressionManually
  267. }
  268. })
  269. }
  270. if (editingSchedule.id) {
  271. schedule.id = editingSchedule.id
  272. onEditSchedule(schedule, goBack)
  273. } else {
  274. onAddSchedule(schedule, goBack)
  275. }
  276. })
  277. }
  278. return (
  279. <>
  280. <Helmet title="Schedule" />
  281. <div className={Styles.scheduleEditor}>
  282. <div className={StylesHeader.editorHeader}>
  283. <Icon type="left" className={StylesHeader.back} onClick={goBack} />
  284. <div className={StylesHeader.title}>
  285. <span className={StylesHeader.name}>{`${
  286. scheduleId ? '修改' : '新增'
  287. } Schedule`}</span>
  288. </div>
  289. <div className={StylesHeader.actions}>
  290. <Tooltip
  291. placement="bottom"
  292. title={jobStatus === 'started' ? '停止后允许修改' : ''}
  293. >
  294. <Button
  295. type="primary"
  296. disabled={loading.edit || jobStatus === 'started'}
  297. onClick={saveSchedule}
  298. >
  299. 保存
  300. </Button>
  301. </Tooltip>
  302. </div>
  303. </div>
  304. <div className={Styles.containerVertical}>
  305. <Row gutter={8}>
  306. <Col span={12}>
  307. <Card title="基本设置" size="small">
  308. <ScheduleBaseConfig
  309. wrappedComponentRef={(inst) => {
  310. baseConfigForm = inst
  311. }}
  312. schedule={editingSchedule}
  313. loading={loading.schedule}
  314. onCheckUniqueName={onCheckUniqueName}
  315. onChangeJobType={onChangeJobType}
  316. />
  317. </Card>
  318. {editingSchedule.jobType === 'email' ? (
  319. <Card title="邮件设置" size="small" style={{ marginTop: 8 }}>
  320. <ScheduleMailConfig
  321. wrappedComponentRef={(inst) => {
  322. mailConfigForm = inst
  323. }}
  324. config={config as IScheduleMailConfig}
  325. loading={loading.schedule}
  326. mailList={suggestMails}
  327. onLoadMailList={onLoadSuggestMails}
  328. />
  329. </Card>
  330. ) : (
  331. <Card
  332. title="企业微信设置"
  333. size="small"
  334. style={{ marginTop: 8 }}
  335. >
  336. <ScheduleWeChatWorkConfig
  337. wrappedComponentRef={(inst) => {
  338. weChatWorkConfigForm = inst
  339. }}
  340. config={config as IScheduleWeChatWorkConfig}
  341. />
  342. </Card>
  343. )}
  344. </Col>
  345. <Col span={12}>
  346. <Card title="发送内容设置" size="small">
  347. <ScheduleVizConfig
  348. displays={displays}
  349. portals={portals}
  350. portalDashboards={portalDashboards}
  351. displaySlides={displaySlides}
  352. value={localContentList}
  353. onLoadDisplaySlides={onLoadDisplaySlides}
  354. onLoadPortalDashboards={onLoadDashboards}
  355. onChange={setLocalContentList}
  356. />
  357. </Card>
  358. </Col>
  359. </Row>
  360. </div>
  361. </div>
  362. </>
  363. )
  364. }
  365. const mapStateToProps = createStructuredSelector({
  366. displays: makeSelectDisplays(),
  367. portals: makeSelectPortals(),
  368. portalDashboards: makeSelectPortalDashboards(),
  369. displaySlides: makeSelectDisplaySlides(),
  370. loading: makeSelectLoading(),
  371. editingSchedule: makeSelectEditingSchedule(),
  372. suggestMails: makeSelectSuggestMails()
  373. })
  374. const mapDispatchToProps = (dispatch) => ({
  375. onHideNavigator: () => dispatch(hideNavigator()),
  376. onLoadDisplays: (projectId) => dispatch(VizActions.loadDisplays(projectId)),
  377. onLoadPortals: (projectId) => dispatch(VizActions.loadPortals(projectId)),
  378. onLoadDisplaySlides: (displayId) =>
  379. dispatch(VizActions.loadDisplaySlides(displayId)),
  380. // @REFACTOR to use viz reducer portalDashboards
  381. onLoadDashboards: (portalId) =>
  382. dispatch(
  383. VizActions.loadPortalDashboards(
  384. portalId,
  385. (dashboards) => {
  386. dispatch(ScheduleActions.portalDashboardsLoaded(portalId, dashboards))
  387. },
  388. false
  389. )
  390. ),
  391. onLoadScheduleDetail: (scheduleId) =>
  392. dispatch(ScheduleActions.loadScheduleDetail(scheduleId)),
  393. onAddSchedule: (schedule, resolve) =>
  394. dispatch(ScheduleActions.addSchedule(schedule, resolve)),
  395. onEditSchedule: (schedule, resolve) =>
  396. dispatch(ScheduleActions.editSchedule(schedule, resolve)),
  397. onResetState: () => dispatch(ScheduleActions.resetScheduleState()),
  398. onCheckUniqueName: (data, resolve, reject) =>
  399. dispatch(checkNameUniqueAction('cronjob', data, resolve, reject)),
  400. onLoadSuggestMails: (keyword) =>
  401. dispatch(ScheduleActions.loadSuggestMails(keyword)),
  402. onChangeJobType: (jobType) =>
  403. dispatch(ScheduleActions.changeScheduleJobType(jobType))
  404. })
  405. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  406. const withReducer = injectReducer({ key: 'schedule', reducer })
  407. const withSaga = injectSaga({ key: 'schedule', saga })
  408. const withVizReducer = injectReducer({
  409. key: 'viz',
  410. reducer: vizReducer
  411. })
  412. const withVizSaga = injectSaga({ key: 'viz', saga: vizSaga })
  413. const withDashboardSaga = injectSaga({ key: 'dashboard', saga: dashboardSaga })
  414. export default compose(
  415. withReducer,
  416. withSaga,
  417. withVizReducer,
  418. withVizSaga,
  419. withDashboardSaga,
  420. withConnect
  421. )(ScheduleEditor)