ScheduleItem.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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 moment from 'moment'
  19. import Switch from '../../atoms/Switch'
  20. import Dropdown from '../../molecules/Dropdown'
  21. import DatetimePicker from '../../molecules/DatetimePicker'
  22. import Button from '../../atoms/Button'
  23. import type { Schedule } from '../../../types/Schedule'
  24. import { executionOptions } from '../../../config'
  25. import Palette from '../../styleUtils/Palette'
  26. import StyleProps from '../../styleUtils/StyleProps'
  27. import DateUtils from '../../../utils/DateUtils'
  28. import notificationStore from '../../../stores/NotificationStore'
  29. import deleteImage from './images/delete.svg'
  30. import deleteHoverImage from './images/delete-hover.svg'
  31. import saveImage from './images/save.svg'
  32. import saveHoverImage from './images/save-hover.svg'
  33. const Wrapper = styled.div`
  34. display: flex;
  35. border-top: 1px solid ${Palette.grayscale[1]};
  36. padding: 16px 0;
  37. position: relative;
  38. &:last-child {
  39. border-bottom: 1px solid ${Palette.grayscale[1]};
  40. }
  41. `
  42. const Data = styled.div`
  43. width: ${props => props.width};
  44. `
  45. const Label = styled.div`
  46. background: ${Palette.grayscale[7]};
  47. height: 100%;
  48. font-size: 12px;
  49. margin-right: 8px;
  50. border-radius: ${StyleProps.borderRadius};
  51. padding: 0 8px;
  52. white-space: nowrap;
  53. overflow: hidden;
  54. text-overflow: ellipsis;
  55. text-align: center;
  56. line-height: 35px;
  57. margin-bottom: -8px;
  58. `
  59. const DropdownStyled = styled(Dropdown) `
  60. font-size: 12px;
  61. `
  62. const ItemButton = props => css`
  63. width: 16px;
  64. height: 16px;
  65. position: absolute;
  66. cursor: pointer;
  67. top: 24px;
  68. ${props.hidden ? 'display: none;' : ''}
  69. `
  70. const DeleteButton = styled.div`
  71. ${props => ItemButton(props)}
  72. background: url('${deleteImage}') center no-repeat;
  73. right: -32px;
  74. &:hover {
  75. background: url('${deleteHoverImage}') center no-repeat;
  76. }
  77. `
  78. const SaveButton = styled.div`
  79. ${props => ItemButton(props)}
  80. background: url('${saveImage}') center no-repeat;
  81. right: -64px;
  82. &:hover {
  83. background: url('${saveHoverImage}') center no-repeat;
  84. }
  85. `
  86. const padNumber = number => {
  87. if (number < 10) return `0${number}`
  88. return number.toString()
  89. }
  90. type Field = { label: string, value?: any }
  91. type TimezoneValue = 'utc' | 'local'
  92. type Props = {
  93. colWidths: string[],
  94. item: Schedule,
  95. onChange: (schedule: Schedule, forced?: boolean) => void,
  96. onSaveSchedule: () => void,
  97. onShowOptionsClick: () => void,
  98. onDeleteClick: () => void,
  99. unsavedSchedules: Schedule[],
  100. timezone: TimezoneValue,
  101. }
  102. @observer
  103. class ScheduleItem extends React.Component<Props> {
  104. getFieldValue(items: Field[], fieldName: string, zeroBasedIndex?: boolean, defaultSelectedIndex?: number) {
  105. if (this.props.item.schedule === null || this.props.item.schedule === undefined) {
  106. return defaultSelectedIndex !== undefined ? items[defaultSelectedIndex] : items[0]
  107. }
  108. if (this.props.item.schedule[fieldName] === null || this.props.item.schedule[fieldName] === undefined) {
  109. return items[0]
  110. }
  111. if (zeroBasedIndex) {
  112. let value = this.props.item.schedule[fieldName]
  113. if (fieldName === 'hour' && this.props.timezone === 'local') {
  114. value = DateUtils.getLocalHour(value)
  115. }
  116. return items[value + 1]
  117. }
  118. return items[this.props.item.schedule[fieldName]]
  119. }
  120. handleMonthChange(item: Field) {
  121. let month = item.value || 1
  122. let maxNumDays = moment().month(month - 1).daysInMonth()
  123. let change: Schedule = { schedule: { month: item.value } }
  124. if (this.props.item.schedule && this.props.item.schedule.dom && this.props.item.schedule.dom > maxNumDays) {
  125. // $FlowIssue
  126. change.schedule.dom = maxNumDays
  127. }
  128. this.props.onChange(change)
  129. }
  130. handleExpirationDateChange(date: Date) {
  131. let newDate = moment(date)
  132. if (newDate.diff(new Date(), 'minutes') < 60) {
  133. notificationStore.alert('Please select a further expiration date.', 'error')
  134. return
  135. }
  136. this.props.onChange({ expiration_date: newDate.toDate() })
  137. }
  138. handleHourChange(hour: number) {
  139. if (this.props.timezone === 'local' && hour !== null && hour !== undefined) {
  140. hour = DateUtils.getUtcHour(hour)
  141. }
  142. this.props.onChange({ schedule: { hour } })
  143. }
  144. shouldUseBold(fieldName: string, isRootField?: boolean) {
  145. const unsavedSchedule = this.props.unsavedSchedules.find(s => s.id === this.props.item.id)
  146. if (!unsavedSchedule) {
  147. return false
  148. }
  149. let data = isRootField ? unsavedSchedule : unsavedSchedule.schedule
  150. if (data && data[fieldName] !== undefined) {
  151. return true
  152. }
  153. return false
  154. }
  155. areExecutionOptionsChanged() {
  156. let isChanged = false
  157. executionOptions.forEach(o => {
  158. let scheduleValue = this.props.item[o.name]
  159. let optionValue = o.value !== undefined ? o.value : false
  160. if (scheduleValue !== undefined && scheduleValue !== null && scheduleValue !== optionValue) {
  161. isChanged = true
  162. }
  163. })
  164. return isChanged
  165. }
  166. renderLabel(value: Field) {
  167. return <Label>{value.label}</Label>
  168. }
  169. renderMonthValue() {
  170. let items = [{ label: 'Any', value: null }]
  171. let months = moment.months()
  172. months.forEach((label, value) => {
  173. items.push({ label, value: value + 1 })
  174. })
  175. if (this.props.item.enabled) {
  176. return this.renderLabel(this.getFieldValue(items, 'month'))
  177. }
  178. return (
  179. <DropdownStyled
  180. centered
  181. width={160}
  182. items={items}
  183. useBold={this.shouldUseBold('month')}
  184. selectedItem={this.getFieldValue(items, 'month')}
  185. onChange={item => { this.handleMonthChange(item) }}
  186. data-test-id="scheduleItem-monthDropdown"
  187. />
  188. )
  189. }
  190. renderDayOfMonthValue() {
  191. let month = this.props.item.schedule && this.props.item.schedule.month ? this.props.item.schedule.month : 1
  192. let items = [{ label: 'Any', value: null }]
  193. for (let i = 1; i <= moment().month(month - 1).daysInMonth(); i += 1) {
  194. items.push({ label: i.toString(), value: i })
  195. }
  196. if (this.props.item.enabled) {
  197. return this.renderLabel(this.getFieldValue(items, 'dom'))
  198. }
  199. return (
  200. <DropdownStyled
  201. centered
  202. width={86}
  203. items={items}
  204. useBold={this.shouldUseBold('dom')}
  205. selectedItem={this.getFieldValue(items, 'dom')}
  206. onChange={item => { this.props.onChange({ schedule: { dom: item.value } }) }}
  207. data-test-id="scheduleItem-dayOfMonthDropdown"
  208. />
  209. )
  210. }
  211. renderDayOfWeekValue() {
  212. let items = [{ label: 'Any', value: null }]
  213. // $FlowIssue
  214. let days = moment.weekdays(true)
  215. days.forEach((label, value) => {
  216. items.push({ label, value })
  217. })
  218. if (this.props.item.enabled) {
  219. return this.renderLabel(this.getFieldValue(items, 'dow', true))
  220. }
  221. return (
  222. <DropdownStyled
  223. centered
  224. width={160}
  225. items={items}
  226. useBold={this.shouldUseBold('dow')}
  227. selectedItem={this.getFieldValue(items, 'dow', true)}
  228. onChange={item => { this.props.onChange({ schedule: { dow: item.value } }) }}
  229. data-test-id="scheduleItem-dayOfWeekDropdown"
  230. />
  231. )
  232. }
  233. renderHourValue() {
  234. let items = [{ label: 'Any', value: null }]
  235. for (let i = 0; i <= 23; i += 1) {
  236. items.push({ label: padNumber(i), value: i })
  237. }
  238. if (this.props.item.enabled) {
  239. return this.renderLabel(this.getFieldValue(items, 'hour', true, 1))
  240. }
  241. return (
  242. <DropdownStyled
  243. centered
  244. width={86}
  245. items={items}
  246. useBold={this.shouldUseBold('hour')}
  247. selectedItem={this.getFieldValue(items, 'hour', true, 1)}
  248. onChange={item => { this.handleHourChange(item.value) }}
  249. data-test-id="scheduleItem-hourDropdown"
  250. />
  251. )
  252. }
  253. renderMinuteValue() {
  254. let items = [{ label: 'Any', value: null }]
  255. for (let i = 0; i <= 59; i += 1) {
  256. items.push({ label: padNumber(i), value: i })
  257. }
  258. if (this.props.item.enabled) {
  259. return this.renderLabel(this.getFieldValue(items, 'minute', true, 1))
  260. }
  261. return (
  262. <DropdownStyled
  263. centered
  264. width={86}
  265. items={items}
  266. useBold={this.shouldUseBold('minute')}
  267. selectedItem={this.getFieldValue(items, 'minute', true, 1)}
  268. onChange={item => { this.props.onChange({ schedule: { minute: item.value } }) }}
  269. data-test-id="scheduleItem-minuteDropdown"
  270. />
  271. )
  272. }
  273. renderExpirationValue() {
  274. let date = this.props.item.expiration_date && moment(this.props.item.expiration_date)
  275. if (this.props.item.enabled) {
  276. let labelDate = date
  277. if (this.props.timezone === 'utc' && date) {
  278. labelDate = DateUtils.getUtcTime(date)
  279. }
  280. return this.renderLabel({ label: (labelDate && labelDate.format('DD/MM/YYYY hh:mm A')) || '-' })
  281. }
  282. return (
  283. <DatetimePicker
  284. value={date ? date.toDate() : null}
  285. timezone={this.props.timezone}
  286. useBold={this.shouldUseBold('expiration_date', true)}
  287. onChange={date => { this.handleExpirationDateChange(date) }}
  288. isValidDate={date => moment(date).isAfter(moment())}
  289. />
  290. )
  291. }
  292. render() {
  293. let enabled = typeof this.props.item.enabled !== 'undefined' && this.props.item.enabled !== null ? this.props.item.enabled : false
  294. return (
  295. <Wrapper data-test-id="scheduleItem">
  296. <Data width={this.props.colWidths[0]}>
  297. <Switch
  298. noLabel
  299. height={16}
  300. checked={enabled}
  301. onChange={enabled => { this.props.onChange({ enabled }, true) }}
  302. data-test-id="scheduleItem-enabled"
  303. />
  304. </Data>
  305. <Data width={this.props.colWidths[1]}>
  306. {this.renderMonthValue()}
  307. </Data>
  308. <Data width={this.props.colWidths[2]}>
  309. {this.renderDayOfMonthValue()}
  310. </Data>
  311. <Data width={this.props.colWidths[3]}>
  312. {this.renderDayOfWeekValue()}
  313. </Data>
  314. <Data width={this.props.colWidths[4]}>
  315. {this.renderHourValue()}
  316. </Data>
  317. <Data width={this.props.colWidths[5]}>
  318. {this.renderMinuteValue()}
  319. </Data>
  320. <Data width={this.props.colWidths[6]}>
  321. {this.renderExpirationValue()}
  322. </Data>
  323. <Data width={this.props.colWidths[7]}>
  324. <Button
  325. onClick={this.props.onShowOptionsClick}
  326. secondary
  327. hollow={!this.areExecutionOptionsChanged()}
  328. width="40px"
  329. style={{
  330. fontSize: '9px',
  331. letterSpacing: '1px',
  332. padding: '0 0 1px 3px',
  333. }}
  334. data-test-id="scheduleItem-optionsButton"
  335. >•••</Button>
  336. </Data>
  337. <DeleteButton
  338. data-test-id="scheduleItem-deleteButton"
  339. onClick={this.props.onDeleteClick}
  340. hidden={this.props.item.enabled}
  341. />
  342. <SaveButton
  343. data-test-id="scheduleItem-saveButton"
  344. onClick={this.props.onSaveSchedule}
  345. hidden={this.props.item.enabled || !this.props.unsavedSchedules.find(us => us.id === this.props.item.id)}
  346. />
  347. </Wrapper>
  348. )
  349. }
  350. }
  351. export default ScheduleItem