diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4adee417f..9523c1b69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
- [Backup NG] Explicit error if a VM is missing [#3434](https://github.com/vatesfr/xen-orchestra/issues/3434) (PR [#3522](https://github.com/vatesfr/xen-orchestra/pull/3522))
- [Backup NG] Show all advanced settings with non-default values in overview [#3549](https://github.com/vatesfr/xen-orchestra/issues/3549) (PR [#3554](https://github.com/vatesfr/xen-orchestra/pull/3554))
- [Backup NG] Collapse advanced settings by default [#3551](https://github.com/vatesfr/xen-orchestra/issues/3551) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3559))
+- [Scheduling] Merge selection and interval tabs [#1902](https://github.com/vatesfr/xen-orchestra/issues/1902) (PR [#3519](https://github.com/vatesfr/xen-orchestra/pull/3519))
### Bug fixes
diff --git a/packages/xo-web/src/common/form/index.js b/packages/xo-web/src/common/form/index.js
index 126247aef..077371e4b 100644
--- a/packages/xo-web/src/common/form/index.js
+++ b/packages/xo-web/src/common/form/index.js
@@ -92,15 +92,16 @@ export class Range extends Component {
max: PropTypes.number.isRequired,
min: PropTypes.number.isRequired,
onChange: PropTypes.func,
+ required: PropTypes.boolean,
step: PropTypes.number,
value: PropTypes.number,
}
componentDidMount () {
- const { min, onChange, value } = this.props
+ const { min, onChange, required, value } = this.props
- if (!value) {
- onChange && onChange(min)
+ if (value === undefined && required) {
+ onChange !== undefined && onChange(min)
}
}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
index 20216dd1a..884583d0d 100644
--- a/packages/xo-web/src/common/intl/messages.js
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -251,19 +251,11 @@ const messages = {
// --- Dates/Scheduler ---
schedulingMonth: 'Month',
- schedulingEveryNMonth: 'Every N month',
- schedulingEachSelectedMonth: 'Each selected month',
schedulingDay: 'Day',
- schedulingEveryNDay: 'Every N day',
- schedulingEachSelectedDay: 'Each selected day',
schedulingSetWeekDayMode: 'Switch to week days',
schedulingSetMonthDayMode: 'Switch to month days',
schedulingHour: 'Hour',
- schedulingEachSelectedHour: 'Each selected hour',
- schedulingEveryNHour: 'Every N hour',
schedulingMinute: 'Minute',
- schedulingEachSelectedMinute: 'Each selected minute',
- schedulingEveryNMinute: 'Every N minute',
selectTableAllMonth: 'Every month',
selectTableAllDay: 'Every day',
selectTableAllHour: 'Every hour',
diff --git a/packages/xo-web/src/common/scheduling.js b/packages/xo-web/src/common/scheduling.js
index 69ba5a18d..5a6a8bfdd 100644
--- a/packages/xo-web/src/common/scheduling.js
+++ b/packages/xo-web/src/common/scheduling.js
@@ -2,14 +2,14 @@ import classNames from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
-import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
import { FormattedDate, FormattedTime } from 'react-intl'
+import { injectState, provideState } from '@julien-f/freactal'
+import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'
import _ from './intl'
import Button from './button'
import Component from './base-component'
import TimezonePicker from './timezone-picker'
-import Icon from './icon'
import Tooltip from './tooltip'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
@@ -25,10 +25,10 @@ const PREVIEW_SLIDER_STYLE = { width: '400px' }
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
-const MINUTES_RANGE = [2, 30]
-const HOURS_RANGE = [2, 12]
-const MONTH_DAYS_RANGE = [2, 15]
-const MONTHS_RANGE = [2, 6]
+const MINUTES_RANGE = [1, 30]
+const HOURS_RANGE = [1, 12]
+const MONTH_DAYS_RANGE = [1, 15]
+const MONTHS_RANGE = [1, 6]
const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20
@@ -146,7 +146,8 @@ export class SchedulePreview extends Component {
min={MIN_PREVIEWS}
max={MAX_PREVIEWS}
onChange={this.linkState('value')}
- value={+value}
+ value={value && +value}
+ required
/>
@@ -193,47 +194,30 @@ class ToggleTd extends Component {
// ===================================================================
-class TableSelect extends Component {
- static propTypes = {
- labelId: PropTypes.string.isRequired,
- options: PropTypes.array.isRequired,
- optionRenderer: PropTypes.func,
- onChange: PropTypes.func.isRequired,
- value: PropTypes.array.isRequired,
- }
-
- static defaultProps = {
- optionRenderer: value => value,
- }
-
- _reset = () => {
- this.props.onChange([])
- }
-
- _handleChange = (tdId, tdValue) => {
- const { props } = this
-
- const newValue = props.value.slice()
- const index = sortedIndex(newValue, tdId)
-
- if (tdValue) {
- // Add
- if (newValue[index] !== tdId) {
- newValue.splice(index, 0, tdId)
- }
- } else {
- // Remove
- if (newValue[index] === tdId) {
- newValue.splice(index, 1)
- }
- }
-
- props.onChange(newValue)
- }
-
- render () {
- const { labelId, options, optionRenderer, value } = this.props
-
+const TableSelect = [
+ provideState({
+ effects: {
+ onChange: (_, tdId, add) => (_, { value, onChange }) => {
+ const newValue = [...value]
+ const index = sortedIndex(newValue, tdId)
+ if (add) {
+ newValue[index] !== tdId && newValue.splice(index, 0, tdId)
+ } else {
+ newValue[index] === tdId && newValue.splice(index, 1)
+ }
+ onChange(newValue)
+ },
+ selectAll: () => ({ optionsValues }, { onChange }) => {
+ onChange(optionsValues)
+ },
+ },
+ computed: {
+ optionsValues: (_, { options }) => flatten(options),
+ },
+ }),
+ injectState,
+ ({ state, effects, labelId, options, optionRenderer = identity, value }) => {
+ let k = 0
return (
@@ -245,147 +229,103 @@ class TableSelect extends Component {
children={optionRenderer(tdOption)}
tdId={tdOption}
key={tdOption}
- onChange={this._handleChange}
- value={includes(value, tdOption)}
+ onChange={effects.onChange}
+ value={
+ k < value.length && value[k] === tdOption && (++k, true)
+ }
/>
))}
))}
-
)
- }
+ },
+].reduceRight((value, decorator) => decorator(value))
+
+TableSelect.propTypes = {
+ labelId: PropTypes.string.isRequired,
+ options: PropTypes.array.isRequired,
+ optionRenderer: PropTypes.func,
+ onChange: PropTypes.func.isRequired,
+ value: PropTypes.array.isRequired,
}
// ===================================================================
-// "2,7" => [2,7] "*/2" => 2 "*" => []
-const cronToValue = (cron, range) => {
- if (cron.indexOf('/') === 1) {
- return +cron.split('/')[1]
- }
+const TimePicker = [
+ provideState({
+ effects: {
+ onChange: (_, value) => ({ optionsValues }, { onChange }) => {
+ if (isArray(value)) {
+ value = value.length === optionsValues.length ? '*' : value.join(',')
+ } else {
+ value = `*/${value}`
+ }
- if (cron === '*') {
- return []
- }
+ onChange(value)
+ },
+ },
+ computed: {
+ step: (_, { value }) =>
+ value.indexOf('/') === 1 ? +value.split('/')[1] : undefined,
+ optionsValues: (_, { options }) => flatten(options),
- return map(cron.split(','), Number)
-}
+ // '*' or '*/1' => all values
+ // '2,7' => [2,7]
+ // '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]
+ tableValue: ({ optionsValues, step }, { value }) =>
+ value === '*' || step === 1
+ ? optionsValues
+ : step !== undefined
+ ? optionsValues.filter((_, i) => i % step === 0)
+ : value.split(',').map(Number),
-// [2,7] => "2,7" 2 => "*/2" [] => "*"
-const valueToCron = value => {
- if (!isArray(value)) {
- return `*/${value}`
- }
+ // '*' => 1
+ // '*/2' => 2
+ rangeValue: ({ step }, { value }) => (value === '*' ? 1 : step),
+ },
+ }),
+ injectState,
+ ({ state, effects, ...props }) => (
+
+
+ {_(`scheduling${props.labelId}`)}
+ {props.headerAddon}
+
+
+
+ {props.range !== undefined && (
+
+ )}
+
+
+ ),
+].reduceRight((value, decorator) => decorator(value))
- if (!value.length) {
- return '*'
- }
-
- return value.join(',')
-}
-
-class TimePicker extends Component {
- static propTypes = {
- headerAddon: PropTypes.node,
- optionRenderer: PropTypes.func,
- onChange: PropTypes.func.isRequired,
- range: PropTypes.array,
- labelId: PropTypes.string.isRequired,
- value: PropTypes.any.isRequired,
- }
-
- _update = cron => {
- const { tableValue, rangeValue } = this.state
-
- const newValue = cronToValue(cron)
- const periodic = !isArray(newValue)
-
- this.setState({
- periodic,
- tableValue: periodic ? tableValue : newValue,
- rangeValue: periodic ? newValue : rangeValue,
- })
- }
-
- componentWillReceiveProps (props) {
- if (props.value !== this.props.value) {
- this._update(props.value)
- }
- }
-
- componentDidMount () {
- this._update(this.props.value)
- }
-
- _onChange = value => {
- this.props.onChange(valueToCron(value))
- }
-
- _tableTab = () => this._onChange(this.state.tableValue || [])
- _periodicTab = () =>
- this._onChange(this.state.rangeValue || this.props.range[0])
-
- render () {
- const { headerAddon, labelId, options, optionRenderer, range } = this.props
-
- const { periodic, tableValue, rangeValue } = this.state
-
- return (
-
-
- {_(`scheduling${labelId}`)}
- {headerAddon}
-
-
- {range && (
-
- )}
- {periodic ? (
-
- ) : (
-
- )}
-
-
- )
- }
+TimePicker.propTypes = {
+ headerAddon: PropTypes.node,
+ labelId: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ optionRenderer: PropTypes.func,
+ options: PropTypes.array.isRequired,
+ range: PropTypes.array,
+ value: PropTypes.string.isRequired,
}
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
@@ -420,12 +360,7 @@ class DayPicker extends Component {
}
_onChange = cron => {
- const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
-
- this.props.onChange([
- isMonthDayPattern ? cron : '*',
- isMonthDayPattern ? '*' : cron,
- ])
+ this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])
}
render () {
@@ -453,11 +388,10 @@ class DayPicker extends Component {
headerAddon={dayModeToggle}
key={weekDayMode ? 'week' : 'month'}
labelId='Day'
+ onChange={this._onChange}
optionRenderer={weekDayMode ? getDayName : undefined}
options={weekDayMode ? WEEK_DAYS : DAYS}
- onChange={this._onChange}
range={MONTH_DAYS_RANGE}
- setWeekDayMode={this._setWeekDayMode}
value={weekDayMode ? weekDayPattern : monthDayPattern}
/>
)