Editor.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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. const prefix = window.localStorage.getItem('inDataService') ?? ''
  158. const prefixPath = prefix ? '/' + prefix : prefix
  159. history.push(`/project/${projectId}${prefixPath}/schedules`)
  160. }, [])
  161. const {
  162. portals,
  163. displays,
  164. loading,
  165. editingSchedule,
  166. onLoadDisplaySlides,
  167. onLoadDashboards
  168. } = props
  169. const loadVizDetail = useCallback(
  170. (
  171. type: IScheduleVizConfigItem['contentType'],
  172. schedule: ISchedule,
  173. vizs: IPortal[] | IDisplayFormed[]
  174. ) => {
  175. if (!schedule.id || !vizs.length) {
  176. return
  177. }
  178. const { contentList } = schedule.config
  179. // initial Viz loading by contentList Portal or Display setting
  180. contentList.forEach(({ contentType, id: vizId }) => {
  181. if (contentType !== type) {
  182. return
  183. }
  184. if (~vizs.findIndex(({ id }) => id === vizId)) {
  185. switch (type) {
  186. case 'portal':
  187. onLoadDashboards(vizId)
  188. break
  189. case 'display':
  190. onLoadDisplaySlides(vizId)
  191. break
  192. }
  193. }
  194. })
  195. },
  196. []
  197. )
  198. useEffect(() => {
  199. loadVizDetail('portal', editingSchedule, portals)
  200. }, [portals, editingSchedule])
  201. useEffect(() => {
  202. loadVizDetail('display', editingSchedule, displays)
  203. }, [displays, editingSchedule])
  204. const {
  205. suggestMails,
  206. portalDashboards,
  207. displaySlides,
  208. onAddSchedule,
  209. onEditSchedule,
  210. onCheckUniqueName,
  211. onLoadSuggestMails,
  212. onChangeJobType
  213. } = props
  214. const { jobStatus, config } = editingSchedule
  215. const { contentList } = config
  216. const [localContentList, setLocalContentList] = useState(contentList)
  217. useEffect(() => {
  218. setLocalContentList([...contentList])
  219. }, [contentList])
  220. let baseConfigForm: FormComponentProps<ScheduleBaseFormProps> = null
  221. let mailConfigForm: FormComponentProps<IScheduleMailConfig> = null
  222. let weChatWorkConfigForm: FormComponentProps<IScheduleWeChatWorkConfig> = null
  223. const saveSchedule = () => {
  224. if (!localContentList.length) {
  225. message.error('请勾选发送内容')
  226. return
  227. }
  228. baseConfigForm.form.validateFieldsAndScroll((err1, value1) => {
  229. if (err1) {
  230. return
  231. }
  232. const { setCronExpressionManually, ...scheduleBase } = value1
  233. const [startDate, endDate] = baseConfigForm.form.getFieldValue(
  234. 'dateRange'
  235. ) as ScheduleBaseFormProps['dateRange']
  236. delete scheduleBase.dateRange
  237. const schedule: ISchedule = {
  238. ...scheduleBase,
  239. cronExpression: setCronExpressionManually
  240. ? scheduleBase.cronExpression
  241. : getCronExpressionByPartition(scheduleBase),
  242. startDate: moment(startDate).format('YYYY-MM-DD HH:mm:ss'),
  243. endDate: moment(endDate).format('YYYY-MM-DD HH:mm:ss'),
  244. projectId: +projectId
  245. }
  246. if (editingSchedule.jobType === 'email') {
  247. mailConfigForm.form.validateFieldsAndScroll((err2, value2) => {
  248. if (err2) {
  249. return
  250. }
  251. schedule.config = {
  252. ...value2,
  253. contentList: localContentList,
  254. setCronExpressionManually
  255. }
  256. schedule.config.content = serialize(
  257. schedule.config.content as RichTextNode[]
  258. )
  259. })
  260. } else {
  261. weChatWorkConfigForm.form.validateFieldsAndScroll((err3, value3) => {
  262. if (err3) {
  263. return
  264. }
  265. schedule.config = {
  266. ...value3,
  267. contentList: localContentList,
  268. setCronExpressionManually
  269. }
  270. })
  271. }
  272. if (editingSchedule.id) {
  273. schedule.id = editingSchedule.id
  274. onEditSchedule(schedule, goBack)
  275. } else {
  276. onAddSchedule(schedule, goBack)
  277. }
  278. })
  279. }
  280. return (
  281. <>
  282. <Helmet title="Schedule" />
  283. <div className={Styles.scheduleEditor}>
  284. <div className={StylesHeader.editorHeader}>
  285. <Icon type="left" className={StylesHeader.back} onClick={goBack} />
  286. <div className={StylesHeader.title}>
  287. <span className={StylesHeader.name}>{`${
  288. scheduleId ? '修改' : '新增'
  289. } Schedule`}</span>
  290. </div>
  291. <div className={StylesHeader.actions}>
  292. <Tooltip
  293. placement="bottom"
  294. title={jobStatus === 'started' ? '停止后允许修改' : ''}
  295. >
  296. <Button
  297. type="primary"
  298. disabled={loading.edit || jobStatus === 'started'}
  299. onClick={saveSchedule}
  300. >
  301. 保存
  302. </Button>
  303. </Tooltip>
  304. </div>
  305. </div>
  306. <div className={Styles.containerVertical}>
  307. <Row gutter={8}>
  308. <Col span={12}>
  309. <Card title="基本设置" size="small">
  310. <ScheduleBaseConfig
  311. wrappedComponentRef={(inst) => {
  312. baseConfigForm = inst
  313. }}
  314. schedule={editingSchedule}
  315. loading={loading.schedule}
  316. onCheckUniqueName={onCheckUniqueName}
  317. onChangeJobType={onChangeJobType}
  318. />
  319. </Card>
  320. {editingSchedule.jobType === 'email' ? (
  321. <Card title="邮件设置" size="small" style={{ marginTop: 8 }}>
  322. <ScheduleMailConfig
  323. wrappedComponentRef={(inst) => {
  324. mailConfigForm = inst
  325. }}
  326. config={config as IScheduleMailConfig}
  327. loading={loading.schedule}
  328. mailList={suggestMails}
  329. onLoadMailList={onLoadSuggestMails}
  330. />
  331. </Card>
  332. ) : (
  333. <Card
  334. title="企业微信设置"
  335. size="small"
  336. style={{ marginTop: 8 }}
  337. >
  338. <ScheduleWeChatWorkConfig
  339. wrappedComponentRef={(inst) => {
  340. weChatWorkConfigForm = inst
  341. }}
  342. config={config as IScheduleWeChatWorkConfig}
  343. />
  344. </Card>
  345. )}
  346. </Col>
  347. <Col span={12}>
  348. <Card title="发送内容设置" size="small">
  349. <ScheduleVizConfig
  350. displays={displays}
  351. portals={portals}
  352. portalDashboards={portalDashboards}
  353. displaySlides={displaySlides}
  354. value={localContentList}
  355. onLoadDisplaySlides={onLoadDisplaySlides}
  356. onLoadPortalDashboards={onLoadDashboards}
  357. onChange={setLocalContentList}
  358. />
  359. </Card>
  360. </Col>
  361. </Row>
  362. </div>
  363. </div>
  364. </>
  365. )
  366. }
  367. const mapStateToProps = createStructuredSelector({
  368. displays: makeSelectDisplays(),
  369. portals: makeSelectPortals(),
  370. portalDashboards: makeSelectPortalDashboards(),
  371. displaySlides: makeSelectDisplaySlides(),
  372. loading: makeSelectLoading(),
  373. editingSchedule: makeSelectEditingSchedule(),
  374. suggestMails: makeSelectSuggestMails()
  375. })
  376. const mapDispatchToProps = (dispatch) => ({
  377. onHideNavigator: () => dispatch(hideNavigator()),
  378. onLoadDisplays: (projectId) => dispatch(VizActions.loadDisplays(projectId)),
  379. onLoadPortals: (projectId) => dispatch(VizActions.loadPortals(projectId)),
  380. onLoadDisplaySlides: (displayId) =>
  381. dispatch(VizActions.loadDisplaySlides(displayId)),
  382. // @REFACTOR to use viz reducer portalDashboards
  383. onLoadDashboards: (portalId) =>
  384. dispatch(
  385. VizActions.loadPortalDashboards(
  386. portalId,
  387. (dashboards) => {
  388. dispatch(ScheduleActions.portalDashboardsLoaded(portalId, dashboards))
  389. },
  390. false
  391. )
  392. ),
  393. onLoadScheduleDetail: (scheduleId) =>
  394. dispatch(ScheduleActions.loadScheduleDetail(scheduleId)),
  395. onAddSchedule: (schedule, resolve) =>
  396. dispatch(ScheduleActions.addSchedule(schedule, resolve)),
  397. onEditSchedule: (schedule, resolve) =>
  398. dispatch(ScheduleActions.editSchedule(schedule, resolve)),
  399. onResetState: () => dispatch(ScheduleActions.resetScheduleState()),
  400. onCheckUniqueName: (data, resolve, reject) =>
  401. dispatch(checkNameUniqueAction('cronjob', data, resolve, reject)),
  402. onLoadSuggestMails: (keyword) =>
  403. dispatch(ScheduleActions.loadSuggestMails(keyword)),
  404. onChangeJobType: (jobType) =>
  405. dispatch(ScheduleActions.changeScheduleJobType(jobType))
  406. })
  407. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  408. const withReducer = injectReducer({ key: 'schedule', reducer })
  409. const withSaga = injectSaga({ key: 'schedule', saga })
  410. const withVizReducer = injectReducer({
  411. key: 'viz',
  412. reducer: vizReducer
  413. })
  414. const withVizSaga = injectSaga({ key: 'viz', saga: vizSaga })
  415. const withDashboardSaga = injectSaga({ key: 'dashboard', saga: dashboardSaga })
  416. export default compose(
  417. withReducer,
  418. withSaga,
  419. withVizReducer,
  420. withVizSaga,
  421. withDashboardSaga,
  422. withConnect
  423. )(ScheduleEditor)