,
30 | firstRender: boolean,
31 | locale: Locale,
32 | shortcuts: Shortcuts,
33 | setMinutes: SetValueNumbersOrUndefined,
34 | setHours: SetValueNumbersOrUndefined,
35 | setMonthDays: SetValueNumbersOrUndefined,
36 | setMonths: SetValueNumbersOrUndefined,
37 | setWeekDays: SetValueNumbersOrUndefined,
38 | setPeriod: SetValuePeriod
39 | ) {
40 | onError && onError(undefined)
41 | setInternalError(false)
42 |
43 | let error = false
44 |
45 | // Handle empty cron string
46 | if (!cronString) {
47 | if (
48 | allowEmpty === 'always' ||
49 | (firstRender && allowEmpty === 'for-default-value')
50 | ) {
51 | return
52 | }
53 |
54 | error = true
55 | }
56 |
57 | if (!error) {
58 | // Shortcuts management
59 | if (
60 | shortcuts &&
61 | (shortcuts === true || shortcuts.includes(cronString as any))
62 | ) {
63 | if (cronString === '@reboot') {
64 | setPeriod('reboot')
65 |
66 | return
67 | }
68 |
69 | // Convert a shortcut to a valid cron string
70 | const shortcutObject = SUPPORTED_SHORTCUTS.find(
71 | (supportedShortcut) => supportedShortcut.name === cronString
72 | )
73 |
74 | if (shortcutObject) {
75 | cronString = shortcutObject.value
76 | }
77 | }
78 |
79 | try {
80 | const cronParts = parseCronString(cronString)
81 | const period = getPeriodFromCronParts(cronParts)
82 |
83 | setPeriod(period)
84 | setMinutes(cronParts[0])
85 | setHours(cronParts[1])
86 | setMonthDays(cronParts[2])
87 | setMonths(cronParts[3])
88 | setWeekDays(cronParts[4])
89 | } catch (err) {
90 | // Specific errors are not handle (yet)
91 | error = true
92 | }
93 | }
94 | if (error) {
95 | internalValueRef.current = cronString
96 | setInternalError(true)
97 | setError(onError, locale)
98 | }
99 | }
100 |
101 | /**
102 | * Get cron string from values
103 | */
104 | export function getCronStringFromValues(
105 | period: PeriodType,
106 | months: number[] | undefined,
107 | monthDays: number[] | undefined,
108 | weekDays: number[] | undefined,
109 | hours: number[] | undefined,
110 | minutes: number[] | undefined,
111 | humanizeValue: boolean | undefined,
112 | dropdownsConfig: DropdownsConfig | undefined
113 | ) {
114 | if (period === 'reboot') {
115 | return '@reboot'
116 | }
117 |
118 | const newMonths = period === 'year' && months ? months : []
119 | const newMonthDays =
120 | (period === 'year' || period === 'month') && monthDays ? monthDays : []
121 | const newWeekDays =
122 | (period === 'year' || period === 'month' || period === 'week') && weekDays
123 | ? weekDays
124 | : []
125 | const newHours =
126 | period !== 'minute' && period !== 'hour' && hours ? hours : []
127 | const newMinutes = period !== 'minute' && minutes ? minutes : []
128 |
129 | const parsedArray = parseCronArray(
130 | [newMinutes, newHours, newMonthDays, newMonths, newWeekDays],
131 | humanizeValue,
132 | dropdownsConfig
133 | )
134 |
135 | return cronToString(parsedArray)
136 | }
137 |
138 | /**
139 | * Returns the cron part array as a string.
140 | */
141 | export function partToString(
142 | cronPart: number[],
143 | unit: Unit,
144 | humanize?: boolean,
145 | leadingZero?: LeadingZero,
146 | clockFormat?: ClockFormat
147 | ) {
148 | let retval = ''
149 |
150 | if (isFull(cronPart, unit) || cronPart.length === 0) {
151 | retval = '*'
152 | } else {
153 | const step = getStep(cronPart)
154 |
155 | if (step && isInterval(cronPart, step)) {
156 | if (isFullInterval(cronPart, unit, step)) {
157 | retval = `*/${step}`
158 | } else {
159 | retval = `${formatValue(
160 | getMin(cronPart),
161 | unit,
162 | humanize,
163 | leadingZero,
164 | clockFormat
165 | )}-${formatValue(
166 | getMax(cronPart),
167 | unit,
168 | humanize,
169 | leadingZero,
170 | clockFormat
171 | )}/${step}`
172 | }
173 | } else {
174 | retval = toRanges(cronPart)
175 | .map((range: number | number[]) => {
176 | if (Array.isArray(range)) {
177 | return `${formatValue(
178 | range[0],
179 | unit,
180 | humanize,
181 | leadingZero,
182 | clockFormat
183 | )}-${formatValue(
184 | range[1],
185 | unit,
186 | humanize,
187 | leadingZero,
188 | clockFormat
189 | )}`
190 | }
191 |
192 | return formatValue(range, unit, humanize, leadingZero, clockFormat)
193 | })
194 | .join(',')
195 | }
196 | }
197 | return retval
198 | }
199 |
200 | /**
201 | * Format the value
202 | */
203 | export function formatValue(
204 | value: number,
205 | unit: Unit,
206 | humanize?: boolean,
207 | leadingZero?: LeadingZero,
208 | clockFormat?: ClockFormat
209 | ) {
210 | let cronPartString = value.toString()
211 | const { type, alt, min } = unit
212 | const needLeadingZero =
213 | leadingZero && (leadingZero === true || leadingZero.includes(type as any))
214 | const need24HourClock =
215 | clockFormat === '24-hour-clock' && (type === 'hours' || type === 'minutes')
216 |
217 | if ((humanize && type === 'week-days') || (humanize && type === 'months')) {
218 | cronPartString = alt![value - min]
219 | } else if (value < 10 && (needLeadingZero || need24HourClock)) {
220 | cronPartString = cronPartString.padStart(2, '0')
221 | }
222 |
223 | if (type === 'hours' && clockFormat === '12-hour-clock') {
224 | const suffix = value >= 12 ? 'PM' : 'AM'
225 | let hour: number | string = value % 12 || 12
226 |
227 | if (hour < 10 && needLeadingZero) {
228 | hour = hour.toString().padStart(2, '0')
229 | }
230 |
231 | cronPartString = `${hour}${suffix}`
232 | }
233 |
234 | return cronPartString
235 | }
236 |
237 | /**
238 | * Validates a range of positive integers
239 | */
240 | export function parsePartArray(arr: number[], unit: Unit) {
241 | const values = sort(dedup(fixSunday(arr, unit)))
242 |
243 | if (values.length === 0) {
244 | return values
245 | }
246 |
247 | const value = outOfRange(values, unit)
248 |
249 | if (typeof value !== 'undefined') {
250 | throw new Error(`Value "${value}" out of range for ${unit.type}`)
251 | }
252 |
253 | return values
254 | }
255 |
256 | /**
257 | * Parses a 2-dimensional array of integers as a cron schedule
258 | */
259 | function parseCronArray(
260 | cronArr: number[][],
261 | humanizeValue: boolean | undefined,
262 | dropdownsConfig: DropdownsConfig | undefined
263 | ) {
264 | return cronArr.map((partArr, idx) => {
265 | const unit = UNITS[idx]
266 | const parsedArray = parsePartArray(partArr, unit)
267 | const dropdownOption: DropdownConfig | undefined =
268 | dropdownsConfig?.[unit.type]
269 |
270 | return partToString(
271 | parsedArray,
272 | unit,
273 | dropdownOption?.humanizeValue ?? humanizeValue
274 | )
275 | })
276 | }
277 |
278 | /**
279 | * Returns the cron array as a string
280 | */
281 | function cronToString(parts: string[]) {
282 | return parts.join(' ')
283 | }
284 |
285 | /**
286 | * Find the period from cron parts
287 | */
288 | function getPeriodFromCronParts(cronParts: number[][]): PeriodType {
289 | if (cronParts[3].length > 0) {
290 | return 'year'
291 | } else if (cronParts[2].length > 0) {
292 | return 'month'
293 | } else if (cronParts[4].length > 0) {
294 | return 'week'
295 | } else if (cronParts[1].length > 0) {
296 | return 'day'
297 | } else if (cronParts[0].length > 0) {
298 | return 'hour'
299 | }
300 | return 'minute'
301 | }
302 |
303 | /**
304 | * Parses a cron string to an array of parts
305 | */
306 | export function parseCronString(str: string) {
307 | if (typeof str !== 'string') {
308 | throw new Error('Invalid cron string')
309 | }
310 |
311 | const parts = str.replace(/\s+/g, ' ').trim().split(' ')
312 |
313 | if (parts.length === 5) {
314 | return parts.map((partStr, idx) => {
315 | return parsePartString(partStr, UNITS[idx])
316 | })
317 | }
318 |
319 | throw new Error('Invalid cron string format')
320 | }
321 |
322 | /**
323 | * Parses a string as a range of positive integers
324 | */
325 | function parsePartString(str: string, unit: Unit) {
326 | if (str === '*' || str === '*/1') {
327 | return []
328 | }
329 |
330 | const values = sort(
331 | dedup(
332 | fixSunday(
333 | replaceAlternatives(str, unit.min, unit.alt)
334 | .split(',')
335 | .map((value) => {
336 | const valueParts = value.split('/')
337 |
338 | if (valueParts.length > 2) {
339 | throw new Error(`Invalid value "${str} for "${unit.type}"`)
340 | }
341 |
342 | let parsedValues: number[]
343 | const left = valueParts[0]
344 | const right = valueParts[1]
345 |
346 | if (left === '*') {
347 | parsedValues = range(unit.min, unit.max)
348 | } else {
349 | parsedValues = parseRange(left, str, unit)
350 | }
351 |
352 | const step = parseStep(right, unit)
353 | const intervalValues = applyInterval(parsedValues, step)
354 |
355 | return intervalValues
356 | })
357 | .flat(),
358 | unit
359 | )
360 | )
361 | )
362 |
363 | const value = outOfRange(values, unit)
364 |
365 | if (typeof value !== 'undefined') {
366 | throw new Error(`Value "${value}" out of range for ${unit.type}`)
367 | }
368 |
369 | // Prevent to return full array
370 | // If all values are selected we don't want any selection visible
371 | if (values.length === unit.total) {
372 | return []
373 | }
374 |
375 | return values
376 | }
377 |
378 | /**
379 | * Replaces the alternative representations of numbers in a string
380 | */
381 | function replaceAlternatives(str: string, min: number, alt?: string[]) {
382 | if (alt) {
383 | str = str.toUpperCase()
384 |
385 | for (let i = 0; i < alt.length; i++) {
386 | str = str.replace(alt[i], `${i + min}`)
387 | }
388 | }
389 | return str
390 | }
391 |
392 | /**
393 | * Replace all 7 with 0 as Sunday can be represented by both
394 | */
395 | function fixSunday(values: number[], unit: Unit) {
396 | if (unit.type === 'week-days') {
397 | values = values.map(function (value) {
398 | if (value === 7) {
399 | return 0
400 | }
401 |
402 | return value
403 | })
404 | }
405 |
406 | return values
407 | }
408 |
409 | /**
410 | * Parses a range string
411 | */
412 | function parseRange(rangeStr: string, context: string, unit: Unit) {
413 | const subparts = rangeStr.split('-')
414 |
415 | if (subparts.length === 1) {
416 | const value = convertStringToNumber(subparts[0])
417 |
418 | if (isNaN(value)) {
419 | throw new Error(`Invalid value "${context}" for ${unit.type}`)
420 | }
421 |
422 | return [value]
423 | } else if (subparts.length === 2) {
424 | const minValue = convertStringToNumber(subparts[0])
425 | const maxValue = convertStringToNumber(subparts[1])
426 |
427 | if (isNaN(minValue) || isNaN(maxValue)) {
428 | throw new Error(`Invalid value "${context}" for ${unit.type}`)
429 | }
430 |
431 | // Fix to allow equal min and max range values
432 | // cf: https://github.com/roccivic/cron-converter/pull/15
433 | if (maxValue < minValue) {
434 | throw new Error(
435 | `Max range is less than min range in "${rangeStr}" for ${unit.type}`
436 | )
437 | }
438 |
439 | return range(minValue, maxValue)
440 | } else {
441 | throw new Error(`Invalid value "${rangeStr}" for ${unit.type}`)
442 | }
443 | }
444 |
445 | /**
446 | * Finds an element from values that is outside of the range of unit
447 | */
448 | function outOfRange(values: number[], unit: Unit) {
449 | const first = values[0]
450 | const last = values[values.length - 1]
451 |
452 | if (first < unit.min) {
453 | return first
454 | } else if (last > unit.max) {
455 | return last
456 | }
457 |
458 | return
459 | }
460 |
461 | /**
462 | * Parses the step from a part string
463 | */
464 | function parseStep(step: string, unit: Unit) {
465 | if (typeof step !== 'undefined') {
466 | const parsedStep = convertStringToNumber(step)
467 |
468 | if (isNaN(parsedStep) || parsedStep < 1) {
469 | throw new Error(`Invalid interval step value "${step}" for ${unit.type}`)
470 | }
471 |
472 | return parsedStep
473 | }
474 | }
475 |
476 | /**
477 | * Applies an interval step to a collection of values
478 | */
479 | function applyInterval(values: number[], step?: number) {
480 | if (step) {
481 | const minVal = values[0]
482 |
483 | values = values.filter((value) => {
484 | return value % step === minVal % step || value === minVal
485 | })
486 | }
487 |
488 | return values
489 | }
490 |
491 | /**
492 | * Returns true if range has all the values of the unit
493 | */
494 | function isFull(values: number[], unit: Unit) {
495 | return values.length === unit.max - unit.min + 1
496 | }
497 |
498 | /**
499 | * Returns the difference between first and second elements in the range
500 | */
501 | function getStep(values: number[]) {
502 | if (values.length > 2) {
503 | const step = values[1] - values[0]
504 |
505 | if (step > 1) {
506 | return step
507 | }
508 | }
509 | }
510 |
511 | /**
512 | * Returns true if the range can be represented as an interval
513 | */
514 | function isInterval(values: number[], step: number) {
515 | for (let i = 1; i < values.length; i++) {
516 | const prev = values[i - 1]
517 | const value = values[i]
518 |
519 | if (value - prev !== step) {
520 | return false
521 | }
522 | }
523 |
524 | return true
525 | }
526 |
527 | /**
528 | * Returns true if the range contains all the interval values
529 | */
530 | function isFullInterval(values: number[], unit: Unit, step: number) {
531 | const min = getMin(values)
532 | const max = getMax(values)
533 | const haveAllValues = values.length === (max - min) / step + 1
534 |
535 | if (min === unit.min && max + step > unit.max && haveAllValues) {
536 | return true
537 | }
538 |
539 | return false
540 | }
541 |
542 | /**
543 | * Returns the smallest value in the range
544 | */
545 | function getMin(values: number[]) {
546 | return values[0]
547 | }
548 |
549 | /**
550 | * Returns the largest value in the range
551 | */
552 | function getMax(values: number[]) {
553 | return values[values.length - 1]
554 | }
555 |
556 | /**
557 | * Returns the range as an array of ranges
558 | * defined as arrays of positive integers
559 | */
560 | function toRanges(values: number[]) {
561 | const retval: (number[] | number)[] = []
562 | let startPart: number | null = null
563 |
564 | values.forEach((value, index, self) => {
565 | if (value !== self[index + 1] - 1) {
566 | if (startPart !== null) {
567 | retval.push([startPart, value])
568 | startPart = null
569 | } else {
570 | retval.push(value)
571 | }
572 | } else if (startPart === null) {
573 | startPart = value
574 | }
575 | })
576 |
577 | return retval
578 | }
579 |
--------------------------------------------------------------------------------
/src/fields/Hours.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import CustomSelect from '../components/CustomSelect'
4 | import { UNITS } from '../constants'
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { HoursProps } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function Hours(props: HoursProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | disabled,
16 | readOnly,
17 | leadingZero,
18 | clockFormat,
19 | period,
20 | periodicityOnDoubleClick,
21 | mode,
22 | allowClear,
23 | filterOption,
24 | getPopupContainer,
25 | } = props
26 | const internalClassName = useMemo(
27 | () =>
28 | classNames({
29 | 'react-js-cron-field': true,
30 | 'react-js-cron-hours': true,
31 | [`${className}-field`]: !!className,
32 | [`${className}-hours`]: !!className,
33 | }),
34 | [className]
35 | )
36 |
37 | return (
38 |
39 | {locale.prefixHours !== '' && (
40 | {locale.prefixHours || DEFAULT_LOCALE_EN.prefixHours}
41 | )}
42 |
43 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/src/fields/Minutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import CustomSelect from '../components/CustomSelect'
4 | import { UNITS } from '../constants'
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { MinutesProps } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function Minutes(props: MinutesProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | disabled,
16 | readOnly,
17 | leadingZero,
18 | clockFormat,
19 | period,
20 | periodicityOnDoubleClick,
21 | mode,
22 | allowClear,
23 | filterOption,
24 | getPopupContainer,
25 | } = props
26 | const internalClassName = useMemo(
27 | () =>
28 | classNames({
29 | 'react-js-cron-field': true,
30 | 'react-js-cron-minutes': true,
31 | [`${className}-field`]: !!className,
32 | [`${className}-minutes`]: !!className,
33 | }),
34 | [className]
35 | )
36 |
37 | return (
38 |
39 | {period === 'hour'
40 | ? locale.prefixMinutesForHourPeriod !== '' && (
41 |
42 | {locale.prefixMinutesForHourPeriod ||
43 | DEFAULT_LOCALE_EN.prefixMinutesForHourPeriod}
44 |
45 | )
46 | : locale.prefixMinutes !== '' && (
47 |
48 | {locale.prefixMinutes || DEFAULT_LOCALE_EN.prefixMinutes}
49 |
50 | )}
51 |
52 |
75 |
76 | {period === 'hour' && locale.suffixMinutesForHourPeriod !== '' && (
77 |
78 | {locale.suffixMinutesForHourPeriod ||
79 | DEFAULT_LOCALE_EN.suffixMinutesForHourPeriod}
80 |
81 | )}
82 |
83 | )
84 | }
85 |
--------------------------------------------------------------------------------
/src/fields/MonthDays.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import CustomSelect from '../components/CustomSelect'
4 | import { UNITS } from '../constants'
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { MonthDaysProps } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function MonthDays(props: MonthDaysProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | weekDays,
16 | disabled,
17 | readOnly,
18 | leadingZero,
19 | period,
20 | periodicityOnDoubleClick,
21 | mode,
22 | allowClear,
23 | filterOption,
24 | getPopupContainer,
25 | } = props
26 | const noWeekDays = !weekDays || weekDays.length === 0
27 |
28 | const internalClassName = useMemo(
29 | () =>
30 | classNames({
31 | 'react-js-cron-field': true,
32 | 'react-js-cron-month-days': true,
33 | 'react-js-cron-month-days-placeholder': !noWeekDays,
34 | [`${className}-field`]: !!className,
35 | [`${className}-month-days`]: !!className,
36 | }),
37 | [className, noWeekDays]
38 | )
39 |
40 | const localeJSON = JSON.stringify(locale)
41 | const placeholder = useMemo(
42 | () => {
43 | if (noWeekDays) {
44 | return locale.emptyMonthDays || DEFAULT_LOCALE_EN.emptyMonthDays
45 | }
46 |
47 | return locale.emptyMonthDaysShort || DEFAULT_LOCALE_EN.emptyMonthDaysShort
48 | },
49 | // eslint-disable-next-line react-hooks/exhaustive-deps
50 | [noWeekDays, localeJSON]
51 | )
52 |
53 | const displayMonthDays =
54 | !readOnly ||
55 | (value && value.length > 0) ||
56 | ((!value || value.length === 0) && (!weekDays || weekDays.length === 0))
57 |
58 | return displayMonthDays ? (
59 |
60 | {locale.prefixMonthDays !== '' && (
61 |
62 | {locale.prefixMonthDays || DEFAULT_LOCALE_EN.prefixMonthDays}
63 |
64 | )}
65 |
66 |
83 |
84 | ) : null
85 | }
86 |
--------------------------------------------------------------------------------
/src/fields/Months.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import CustomSelect from '../components/CustomSelect'
4 | import { UNITS } from '../constants'
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { MonthsProps } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function Months(props: MonthsProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | humanizeLabels,
16 | disabled,
17 | readOnly,
18 | period,
19 | periodicityOnDoubleClick,
20 | mode,
21 | allowClear,
22 | filterOption,
23 | getPopupContainer,
24 | } = props
25 | const optionsList = locale.months || DEFAULT_LOCALE_EN.months
26 |
27 | const internalClassName = useMemo(
28 | () =>
29 | classNames({
30 | 'react-js-cron-field': true,
31 | 'react-js-cron-months': true,
32 | [`${className}-field`]: !!className,
33 | [`${className}-months`]: !!className,
34 | }),
35 | [className]
36 | )
37 |
38 | return (
39 |
40 | {locale.prefixMonths !== '' && (
41 | {locale.prefixMonths || DEFAULT_LOCALE_EN.prefixMonths}
42 | )}
43 |
44 |
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/src/fields/Period.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from 'antd'
2 | import { BaseOptionType } from 'antd/es/select'
3 | import React, { useCallback, useMemo } from 'react'
4 |
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { PeriodProps, PeriodType } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function Period(props: PeriodProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | disabled,
16 | readOnly,
17 | shortcuts,
18 | allowedPeriods,
19 | allowClear,
20 | getPopupContainer,
21 | } = props
22 | const options: BaseOptionType[] = []
23 |
24 | if (allowedPeriods.includes('year')) {
25 | options.push({
26 | value: 'year',
27 | label: locale.yearOption || DEFAULT_LOCALE_EN.yearOption,
28 | })
29 | }
30 |
31 | if (allowedPeriods.includes('month')) {
32 | options.push({
33 | value: 'month',
34 | label: locale.monthOption || DEFAULT_LOCALE_EN.monthOption,
35 | })
36 | }
37 |
38 | if (allowedPeriods.includes('week')) {
39 | options.push({
40 | value: 'week',
41 | label: locale.weekOption || DEFAULT_LOCALE_EN.weekOption,
42 | })
43 | }
44 |
45 | if (allowedPeriods.includes('day')) {
46 | options.push({
47 | value: 'day',
48 | label: locale.dayOption || DEFAULT_LOCALE_EN.dayOption,
49 | })
50 | }
51 |
52 | if (allowedPeriods.includes('hour')) {
53 | options.push({
54 | value: 'hour',
55 | label: locale.hourOption || DEFAULT_LOCALE_EN.hourOption,
56 | })
57 | }
58 |
59 | if (allowedPeriods.includes('minute')) {
60 | options.push({
61 | value: 'minute',
62 | label: locale.minuteOption || DEFAULT_LOCALE_EN.minuteOption,
63 | })
64 | }
65 |
66 | if (
67 | allowedPeriods.includes('reboot') &&
68 | shortcuts &&
69 | (shortcuts === true || shortcuts.includes('@reboot'))
70 | ) {
71 | options.push({
72 | value: 'reboot',
73 | label: locale.rebootOption || DEFAULT_LOCALE_EN.rebootOption,
74 | })
75 | }
76 |
77 | const handleChange = useCallback(
78 | (newValue: PeriodType) => {
79 | if (!readOnly) {
80 | setValue(newValue)
81 | }
82 | },
83 | [setValue, readOnly]
84 | )
85 |
86 | const internalClassName = useMemo(
87 | () =>
88 | classNames({
89 | 'react-js-cron-field': true,
90 | 'react-js-cron-period': true,
91 | [`${className}-field`]: !!className,
92 | [`${className}-period`]: !!className,
93 | }),
94 | [className]
95 | )
96 |
97 | const selectClassName = useMemo(
98 | () =>
99 | classNames({
100 | 'react-js-cron-select': true,
101 | 'react-js-cron-select-no-prefix': locale.prefixPeriod === '',
102 | [`${className}-select`]: !!className,
103 | }),
104 | [className, locale.prefixPeriod]
105 | )
106 |
107 | const popupClassName = useMemo(
108 | () =>
109 | classNames({
110 | 'react-js-cron-select-dropdown': true,
111 | 'react-js-cron-select-dropdown-period': true,
112 | [`${className}-select-dropdown`]: !!className,
113 | [`${className}-select-dropdown-period`]: !!className,
114 | }),
115 | [className]
116 | )
117 |
118 | return (
119 |
120 | {locale.prefixPeriod !== '' && (
121 | {locale.prefixPeriod || DEFAULT_LOCALE_EN.prefixPeriod}
122 | )}
123 |
124 |
140 | )
141 | }
142 |
--------------------------------------------------------------------------------
/src/fields/WeekDays.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 |
3 | import CustomSelect from '../components/CustomSelect'
4 | import { UNITS } from '../constants'
5 | import { DEFAULT_LOCALE_EN } from '../locale'
6 | import { WeekDaysProps } from '../types'
7 | import { classNames } from '../utils'
8 |
9 | export default function WeekDays(props: WeekDaysProps) {
10 | const {
11 | value,
12 | setValue,
13 | locale,
14 | className,
15 | humanizeLabels,
16 | monthDays,
17 | disabled,
18 | readOnly,
19 | period,
20 | periodicityOnDoubleClick,
21 | mode,
22 | allowClear,
23 | filterOption,
24 | getPopupContainer,
25 | } = props
26 | const optionsList = locale.weekDays || DEFAULT_LOCALE_EN.weekDays
27 | const noMonthDays = period === 'week' || !monthDays || monthDays.length === 0
28 |
29 | const internalClassName = useMemo(
30 | () =>
31 | classNames({
32 | 'react-js-cron-field': true,
33 | 'react-js-cron-week-days': true,
34 | 'react-js-cron-week-days-placeholder': !noMonthDays,
35 | [`${className}-field`]: !!className,
36 | [`${className}-week-days`]: !!className,
37 | }),
38 | [className, noMonthDays]
39 | )
40 |
41 | const localeJSON = JSON.stringify(locale)
42 | const placeholder = useMemo(
43 | () => {
44 | if (noMonthDays) {
45 | return locale.emptyWeekDays || DEFAULT_LOCALE_EN.emptyWeekDays
46 | }
47 |
48 | return locale.emptyWeekDaysShort || DEFAULT_LOCALE_EN.emptyWeekDaysShort
49 | },
50 | // eslint-disable-next-line react-hooks/exhaustive-deps
51 | [noMonthDays, localeJSON]
52 | )
53 |
54 | const displayWeekDays =
55 | period === 'week' ||
56 | !readOnly ||
57 | (value && value.length > 0) ||
58 | ((!value || value.length === 0) && (!monthDays || monthDays.length === 0))
59 |
60 | const monthDaysIsDisplayed =
61 | !readOnly ||
62 | (monthDays && monthDays.length > 0) ||
63 | ((!monthDays || monthDays.length === 0) && (!value || value.length === 0))
64 |
65 | return displayWeekDays ? (
66 |
67 | {locale.prefixWeekDays !== '' &&
68 | (period === 'week' || !monthDaysIsDisplayed) && (
69 |
70 | {locale.prefixWeekDays || DEFAULT_LOCALE_EN.prefixWeekDays}
71 |
72 | )}
73 |
74 | {locale.prefixWeekDaysForMonthAndYearPeriod !== '' &&
75 | period !== 'week' &&
76 | monthDaysIsDisplayed && (
77 |
78 | {locale.prefixWeekDaysForMonthAndYearPeriod ||
79 | DEFAULT_LOCALE_EN.prefixWeekDaysForMonthAndYearPeriod}
80 |
81 | )}
82 |
83 |
107 |
108 | ) : null
109 | }
110 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Cron from './Cron'
2 | import * as converter from './converter'
3 |
4 | export * from './types'
5 |
6 | // Support "import { Cron } from 'react-js-cron'"
7 | // Support "import { Cron as ReactJSCron } from 'react-js-cron'"
8 | export { Cron, converter }
9 |
10 | // Support "import Cron from 'react-js-cron'"
11 | export default Cron
12 |
--------------------------------------------------------------------------------
/src/locale.ts:
--------------------------------------------------------------------------------
1 | import { DefaultLocale } from './types'
2 |
3 | export const DEFAULT_LOCALE_EN: DefaultLocale = {
4 | everyText: 'every',
5 | emptyMonths: 'every month',
6 | emptyMonthDays: 'every day of the month',
7 | emptyMonthDaysShort: 'day of the month',
8 | emptyWeekDays: 'every day of the week',
9 | emptyWeekDaysShort: 'day of the week',
10 | emptyHours: 'every hour',
11 | emptyMinutes: 'every minute',
12 | emptyMinutesForHourPeriod: 'every',
13 | yearOption: 'year',
14 | monthOption: 'month',
15 | weekOption: 'week',
16 | dayOption: 'day',
17 | hourOption: 'hour',
18 | minuteOption: 'minute',
19 | rebootOption: 'reboot',
20 | prefixPeriod: 'Every',
21 | prefixMonths: 'in',
22 | prefixMonthDays: 'on',
23 | prefixWeekDays: 'on',
24 | prefixWeekDaysForMonthAndYearPeriod: 'and',
25 | prefixHours: 'at',
26 | prefixMinutes: ':',
27 | prefixMinutesForHourPeriod: 'at',
28 | suffixMinutesForHourPeriod: 'minute(s)',
29 | errorInvalidCron: 'Invalid cron expression',
30 | clearButtonText: 'Clear',
31 | weekDays: [
32 | // Order is important, the index will be used as value
33 | 'Sunday', // Sunday must always be first, it's "0"
34 | 'Monday',
35 | 'Tuesday',
36 | 'Wednesday',
37 | 'Thursday',
38 | 'Friday',
39 | 'Saturday',
40 | ],
41 | months: [
42 | // Order is important, the index will be used as value
43 | 'January',
44 | 'February',
45 | 'March',
46 | 'April',
47 | 'May',
48 | 'June',
49 | 'July',
50 | 'August',
51 | 'September',
52 | 'October',
53 | 'November',
54 | 'December',
55 | ],
56 | // Order is important, the index will be used as value
57 | altWeekDays: [
58 | 'SUN', // Sunday must always be first, it's "0"
59 | 'MON',
60 | 'TUE',
61 | 'WED',
62 | 'THU',
63 | 'FRI',
64 | 'SAT',
65 | ],
66 | // Order is important, the index will be used as value
67 | altMonths: [
68 | 'JAN',
69 | 'FEB',
70 | 'MAR',
71 | 'APR',
72 | 'MAY',
73 | 'JUN',
74 | 'JUL',
75 | 'AUG',
76 | 'SEP',
77 | 'OCT',
78 | 'NOV',
79 | 'DEC',
80 | ],
81 | }
82 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // react-testing-library renders your components to document.body,
2 | // this adds jest-dom's custom assertions
3 | import '@testing-library/jest-dom'
4 |
--------------------------------------------------------------------------------
/src/stories/constants.stories.ts:
--------------------------------------------------------------------------------
1 | export const FRENCH_LOCALE = {
2 | everyText: 'chaque',
3 | emptyMonths: 'chaque mois',
4 | emptyMonthDays: 'chaque jour du mois',
5 | emptyMonthDaysShort: 'jour du mois',
6 | emptyWeekDays: 'chaque jour de la semaine',
7 | emptyWeekDaysShort: 'jour de la semaine',
8 | emptyHours: 'chaque heure',
9 | emptyMinutes: 'chaque minute',
10 | emptyMinutesForHourPeriod: 'chaque',
11 | yearOption: 'année',
12 | monthOption: 'mois',
13 | weekOption: 'semaine',
14 | dayOption: 'jour',
15 | hourOption: 'heure',
16 | minuteOption: 'minute',
17 | rebootOption: 'redémarrage',
18 | prefixPeriod: 'Chaque',
19 | prefixMonths: 'en',
20 | prefixMonthDays: 'le',
21 | prefixWeekDays: 'le',
22 | prefixWeekDaysForMonthAndYearPeriod: 'et',
23 | prefixHours: 'à',
24 | prefixMinutes: ':',
25 | prefixMinutesForHourPeriod: 'à',
26 | suffixMinutesForHourPeriod: 'minute(s)',
27 | errorInvalidCron: 'Expression cron invalide',
28 | clearButtonText: 'Effacer',
29 | // Order is important, the index will be used as value
30 | months: [
31 | 'janvier',
32 | 'février',
33 | 'mars',
34 | 'avril',
35 | 'mai',
36 | 'juin',
37 | 'juillet',
38 | 'août',
39 | 'septembre',
40 | 'octobre',
41 | 'novembre',
42 | 'décembre',
43 | ],
44 | // Order is important, the index will be used as value
45 | weekDays: [
46 | 'dimanche',
47 | 'lundi',
48 | 'mardi',
49 | 'mercredi',
50 | 'jeudi',
51 | 'vendredi',
52 | 'samedi',
53 | ],
54 | // cf: https://fr.wikipedia.org/wiki/Mois#Abr%C3%A9viations
55 | // Order is important, the index will be used as value
56 | altMonths: [
57 | 'JAN',
58 | 'FÉV',
59 | 'MAR',
60 | 'AVR',
61 | 'MAI',
62 | 'JUN',
63 | 'JUL',
64 | 'AOÛ',
65 | 'SEP',
66 | 'OCT',
67 | 'NOV',
68 | 'DÉC',
69 | ],
70 | // cf: http://bdl.oqlf.gouv.qc.ca/bdl/gabarit_bdl.asp?id=3617
71 | // Order is important, the index will be used as value
72 | altWeekDays: ['DIM', 'LUN', 'MAR', 'MER', 'JEU', 'VEN', 'SAM'],
73 | }
74 | export const ENGLISH_VARIANT_LOCALE = {
75 | everyText: 'all',
76 | emptyHours: 'all hours',
77 | emptyWeekDays: 'all days of the week',
78 | emptyMonthDays: 'all days of the month',
79 | emptyMonths: 'all months',
80 | emptyMinutes: 'all minutes',
81 | emptyMinutesForHourPeriod: 'all',
82 | yearOption: 'years',
83 | monthOption: 'months',
84 | weekOption: 'weeks',
85 | dayOption: 'days',
86 | hourOption: 'hours',
87 | minuteOption: 'minutes',
88 | rebootOption: 'reboots',
89 | prefixPeriod: 'All',
90 | }
91 | export const NO_PREFIX_SUFFIX_LOCALE = {
92 | prefixPeriod: '',
93 | prefixMonths: '',
94 | prefixMonthDays: '',
95 | prefixWeekDays: '',
96 | prefixWeekDaysForMonthAndYearPeriod: '',
97 | prefixHours: '',
98 | prefixMinutes: '',
99 | prefixMinutesForHourPeriod: '',
100 | suffixMinutesForHourPeriod: '',
101 | }
102 |
--------------------------------------------------------------------------------
/src/stories/styles.stories.css:
--------------------------------------------------------------------------------
1 | .sbdocs-content {
2 | max-width: 1400px !important;
3 | }
4 | .demo-dynamic-settings {
5 | margin-bottom: 10px;
6 | border-bottom: 1px solid #e6e6e6;
7 | }
8 | .demo-dynamic-settings .ant-form-item {
9 | margin-right: 16px;
10 | margin-bottom: 8px;
11 | }
12 | .demo-dynamic-settings p {
13 | width: 100%;
14 | font-size: 12px;
15 | }
16 | .demo-dynamic-settings + div {
17 | padding-bottom: 10px;
18 | margin-bottom: 20px;
19 | border-bottom: 1px solid #e6e6e6;
20 | display: flex;
21 | justify-content: space-between;
22 | align-items: center;
23 | }
24 | .demo-dynamic-settings + div p {
25 | display: inline;
26 | margin: 0;
27 | }
28 |
29 | /* Custom style */
30 | .my-project-cron-field > span {
31 | font-weight: bold;
32 | }
33 | .my-project-cron-error .my-project-cron-field > span {
34 | color: #ff4d4f;
35 | }
36 | .my-project-cron .my-project-cron-select > div:first-child {
37 | border-radius: 10px;
38 | }
39 | .my-project-cron-select-dropdown
40 | .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
41 | background-color: #dadada;
42 | }
43 | .my-project-cron-clear-button {
44 | border-radius: 10px;
45 | }
46 |
--------------------------------------------------------------------------------
/src/stories/utils.stories.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, useReducer } from 'react'
2 |
3 | /**
4 | * Custom hook to update cron value and input value.
5 | *
6 | * Cannot use InputRef to update the value because of a change in antd 4.19.0.
7 | *
8 | * @param defaultValue - The default value of the input and cron component.
9 | * @returns - The cron and input values with the dispatch function.
10 | */
11 | export function useCronReducer(defaultValue: string): [
12 | {
13 | inputValue: string
14 | cronValue: string
15 | },
16 | Dispatch<{
17 | type: 'set_cron_value' | 'set_input_value' | 'set_values'
18 | value: string
19 | }>
20 | ] {
21 | const [values, dispatchValues] = useReducer(
22 | (
23 | prevValues: {
24 | inputValue: string
25 | cronValue: string
26 | },
27 | action: {
28 | type: 'set_cron_value' | 'set_input_value' | 'set_values'
29 | value: string
30 | }
31 | ) => {
32 | switch (action.type) {
33 | case 'set_cron_value':
34 | return {
35 | inputValue: prevValues.inputValue,
36 | cronValue: action.value,
37 | }
38 | case 'set_input_value':
39 | return {
40 | inputValue: action.value,
41 | cronValue: prevValues.cronValue,
42 | }
43 | case 'set_values':
44 | return {
45 | inputValue: action.value,
46 | cronValue: action.value,
47 | }
48 | }
49 | },
50 | {
51 | inputValue: defaultValue,
52 | cronValue: defaultValue,
53 | }
54 | )
55 |
56 | return [values, dispatchValues]
57 | }
58 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .react-js-cron {
2 | display: flex;
3 | align-items: flex-start;
4 | flex-wrap: wrap;
5 | }
6 | .react-js-cron > div,
7 | .react-js-cron-field {
8 | display: flex;
9 | align-items: center;
10 | }
11 | .react-js-cron-field {
12 | margin-bottom: 10px;
13 | }
14 | .react-js-cron-field > span {
15 | margin-left: 5px;
16 | }
17 | div.react-js-cron-select {
18 | margin-left: 5px;
19 | }
20 | .react-js-cron-select.react-js-cron-select-no-prefix {
21 | margin-left: 0;
22 | }
23 | .react-js-cron-select .ant-select-selection-wrap {
24 | position: relative;
25 | align-items: center;
26 | }
27 | /* Absolute position only when there are one child, meaning when no items are selected. */
28 | .react-js-cron-select
29 | .ant-select-selection-overflow:has(> :nth-child(-n + 1):last-child) {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | }
34 | /* Center placeholder vertically. */
35 | .react-js-cron-select .ant-select-selection-placeholder {
36 | margin-top: -2px;
37 | }
38 | div.react-js-cron-error .react-js-cron-select .ant-select-selector {
39 | border-color: #ff4d4f;
40 | background: #fff6f6;
41 | }
42 | div.react-js-cron-custom-select {
43 | min-width: 70px;
44 | z-index: 1;
45 | }
46 | div.react-js-cron-error div.react-js-cron-custom-select {
47 | background: #fff6f6;
48 | }
49 | div.react-js-cron-select.react-js-cron-custom-select.ant-select
50 | div.ant-select-selector {
51 | padding-left: 11px;
52 | padding-right: 30px;
53 | }
54 | .react-js-cron-read-only
55 | div.react-js-cron-select.react-js-cron-custom-select.ant-select
56 | div.ant-select-selector {
57 | padding-right: 11px;
58 | }
59 | div.react-js-cron-custom-select .ant-select-selection-search {
60 | width: 0 !important;
61 | margin: 0 !important;
62 | }
63 | div.react-js-cron-custom-select .ant-select-selection-placeholder {
64 | position: static;
65 | top: 50%;
66 | right: auto;
67 | left: auto;
68 | transform: none;
69 | transition: none;
70 | opacity: 1;
71 | color: inherit;
72 | }
73 | .react-js-cron-week-days-placeholder
74 | .react-js-cron-custom-select
75 | .ant-select-selection-placeholder,
76 | .react-js-cron-month-days-placeholder
77 | .react-js-cron-custom-select
78 | .ant-select-selection-placeholder {
79 | opacity: 0.4;
80 | }
81 | .react-js-cron-custom-select-dropdown {
82 | min-width: 0 !important;
83 | width: 174px !important;
84 | }
85 | .react-js-cron-custom-select-dropdown .rc-virtual-list {
86 | max-height: none !important;
87 | }
88 | .react-js-cron-custom-select-dropdown-grid .rc-virtual-list-holder {
89 | max-height: initial !important;
90 | }
91 | .react-js-cron-custom-select-dropdown-grid .rc-virtual-list-holder-inner {
92 | display: grid !important;
93 | grid-template-columns: repeat(4, 1fr);
94 | }
95 | .react-js-cron-custom-select-dropdown-grid
96 | .rc-virtual-list-holder-inner
97 | .ant-select-item-option-content {
98 | text-align: center;
99 | }
100 | .react-js-cron-custom-select-dropdown-hours-twelve-hour-clock {
101 | width: 260px !important;
102 | }
103 | .react-js-cron-custom-select-dropdown-minutes-large {
104 | width: 300px !important;
105 | }
106 | .react-js-cron-custom-select-dropdown-minutes-large
107 | .rc-virtual-list-holder-inner {
108 | grid-template-columns: repeat(6, 1fr);
109 | }
110 | .react-js-cron-custom-select-dropdown-minutes-medium {
111 | width: 220px !important;
112 | }
113 | .react-js-cron-custom-select-dropdown-minutes-medium
114 | .rc-virtual-list-holder-inner {
115 | grid-template-columns: repeat(5, 1fr);
116 | }
117 | .react-js-cron-period > span:first-child {
118 | margin-left: 0 !important;
119 | }
120 | .react-js-cron-period
121 | .react-js-cron-select.ant-select-single.ant-select-open
122 | .ant-select-selection-item {
123 | opacity: 1;
124 | }
125 | .react-js-cron-select-dropdown-period {
126 | min-width: 0 !important;
127 | width: auto !important;
128 | }
129 | .react-js-cron-clear-button {
130 | margin-left: 10px;
131 | margin-bottom: 10px;
132 | }
133 | .react-js-cron-disabled .react-js-cron-select.ant-select-disabled {
134 | background: #f5f5f5;
135 | }
136 | div.react-js-cron-select.react-js-cron-custom-select.ant-select
137 | div.ant-select-selector
138 | > .ant-select-selection-overflow {
139 | align-items: center;
140 | flex: initial;
141 | }
142 |
--------------------------------------------------------------------------------
/src/tests/Cron.defaultValue.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react'
2 |
3 | import Cron from '../Cron'
4 | import {
5 | AllowEmpty,
6 | ClockFormat,
7 | CronError,
8 | CronType,
9 | DropdownsConfig,
10 | LeadingZero,
11 | PeriodType,
12 | Shortcuts,
13 | } from '../types'
14 |
15 | describe('Cron defaultValue test suite', () => {
16 | const defaultError: CronError = {
17 | description: 'Invalid cron expression',
18 | type: 'invalid_cron',
19 | }
20 |
21 | const cases: {
22 | title: string
23 | defaultValue: string
24 | expectedValue?: string
25 | expectedPeriod?: PeriodType
26 | shortcuts?: Shortcuts
27 | allowEmpty?: AllowEmpty
28 | humanizeLabels?: boolean
29 | humanizeValue?: boolean
30 | leadingZero?: LeadingZero
31 | clockFormat?: ClockFormat
32 | allowedDropdowns?: CronType[]
33 | defaultPeriod?: PeriodType
34 | periodSelect: PeriodType | undefined
35 | monthsSelect: string | undefined
36 | monthDaysSelect: string | undefined
37 | weekDaysSelect: string | undefined
38 | hoursSelect: string | undefined
39 | minutesSelect: string | undefined
40 | dropdownsConfig?: DropdownsConfig
41 | error?: CronError
42 | }[] = [
43 | {
44 | title: 'every minutes',
45 | defaultValue: '* * * * *',
46 | periodSelect: 'minute',
47 | monthsSelect: undefined,
48 | monthDaysSelect: undefined,
49 | weekDaysSelect: undefined,
50 | hoursSelect: undefined,
51 | minutesSelect: undefined,
52 | },
53 | {
54 | title: 'each monday',
55 | defaultValue: '* * * * 1',
56 | periodSelect: 'week',
57 | monthsSelect: undefined,
58 | monthDaysSelect: undefined,
59 | weekDaysSelect: 'MON',
60 | hoursSelect: 'every hour',
61 | minutesSelect: 'every minute',
62 | },
63 | {
64 | title: 'every 2 hours',
65 | defaultValue: '* */2 * * *',
66 | periodSelect: 'day',
67 | monthsSelect: undefined,
68 | monthDaysSelect: undefined,
69 | weekDaysSelect: undefined,
70 | hoursSelect: 'every 2',
71 | minutesSelect: 'every minute',
72 | },
73 | {
74 | title: 'valid cron values with simple range definition',
75 | defaultValue: '0 1 3-5 5 */2',
76 | periodSelect: 'year',
77 | monthsSelect: 'MAY',
78 | monthDaysSelect: '3-5',
79 | weekDaysSelect: 'every 2',
80 | hoursSelect: '1',
81 | minutesSelect: '0',
82 | },
83 | {
84 | title: 'multiple minutes',
85 | defaultValue: '2,5,9,13,22 * * * *',
86 | periodSelect: 'hour',
87 | monthsSelect: undefined,
88 | monthDaysSelect: undefined,
89 | weekDaysSelect: undefined,
90 | hoursSelect: undefined,
91 | minutesSelect: '2,5,9,13,22',
92 | },
93 | {
94 | title: 'a minute and an hour',
95 | defaultValue: '2 7 * * *',
96 | periodSelect: 'day',
97 | monthsSelect: undefined,
98 | monthDaysSelect: undefined,
99 | weekDaysSelect: undefined,
100 | hoursSelect: '7',
101 | minutesSelect: '2',
102 | },
103 | {
104 | title: 'humanized value is allowed by default',
105 | defaultValue: '* * * MAY SUN',
106 | expectedValue: '* * * 5 0',
107 | periodSelect: 'year',
108 | monthsSelect: 'MAY',
109 | monthDaysSelect: 'day of the month',
110 | weekDaysSelect: 'SUN',
111 | hoursSelect: 'every hour',
112 | minutesSelect: 'every minute',
113 | },
114 | {
115 | title: 'humanizeValue at false convert humanized value in cron',
116 | defaultValue: '* * * * MON-WED,sat',
117 | expectedValue: '* * * * 1-3,6',
118 | humanizeValue: false,
119 | periodSelect: 'week',
120 | monthsSelect: undefined,
121 | monthDaysSelect: undefined,
122 | weekDaysSelect: 'MON-WED,SAT',
123 | hoursSelect: 'every hour',
124 | minutesSelect: 'every minute',
125 | },
126 | {
127 | title: 'humanizeValue at true keep humanized value in cron',
128 | defaultValue: '* * * * MON-WED,sat',
129 | expectedValue: '* * * * MON-WED,SAT',
130 | humanizeValue: true,
131 | periodSelect: 'week',
132 | monthsSelect: undefined,
133 | monthDaysSelect: undefined,
134 | weekDaysSelect: 'MON-WED,SAT',
135 | hoursSelect: 'every hour',
136 | minutesSelect: 'every minute',
137 | },
138 | {
139 | title: 'humanized labels is allowed',
140 | defaultValue: '* * * * MON-WED,sat',
141 | expectedValue: '* * * * 1-3,6',
142 | humanizeLabels: true,
143 | periodSelect: 'week',
144 | monthsSelect: undefined,
145 | monthDaysSelect: undefined,
146 | weekDaysSelect: 'MON-WED,SAT',
147 | hoursSelect: 'every hour',
148 | minutesSelect: 'every minute',
149 | },
150 | {
151 | title: 'humanized labels is not allowed',
152 | defaultValue: '* * * * MON-WED,sat',
153 | expectedValue: '* * * * 1-3,6',
154 | humanizeLabels: false,
155 | periodSelect: 'week',
156 | monthsSelect: undefined,
157 | monthDaysSelect: undefined,
158 | weekDaysSelect: '1-3,6',
159 | hoursSelect: 'every hour',
160 | minutesSelect: 'every minute',
161 | },
162 | {
163 | title: 'leading zero is added when props is true',
164 | defaultValue: '1 3,18 6,23 * *',
165 | leadingZero: true,
166 | periodSelect: 'month',
167 | monthsSelect: undefined,
168 | monthDaysSelect: '06,23',
169 | weekDaysSelect: 'day of the week',
170 | hoursSelect: '03,18',
171 | minutesSelect: '01',
172 | },
173 | {
174 | title: 'leading zero is added only for hours',
175 | defaultValue: '1 3,18 6,23 * *',
176 | leadingZero: ['hours'],
177 | periodSelect: 'month',
178 | monthsSelect: undefined,
179 | monthDaysSelect: '6,23',
180 | weekDaysSelect: 'day of the week',
181 | hoursSelect: '03,18',
182 | minutesSelect: '1',
183 | },
184 | {
185 | title:
186 | 'leading zero is added for hours and minutes with clock format 24 hours',
187 | defaultValue: '1 3,18 6,23 * *',
188 | clockFormat: '24-hour-clock',
189 | periodSelect: 'month',
190 | monthsSelect: undefined,
191 | monthDaysSelect: '6,23',
192 | weekDaysSelect: 'day of the week',
193 | hoursSelect: '03,18',
194 | minutesSelect: '01',
195 | },
196 | {
197 | title: 'AM and PM displayed with clock format 12 hours',
198 | defaultValue: '1 3,18 6,23 * *',
199 | clockFormat: '12-hour-clock',
200 | periodSelect: 'month',
201 | monthsSelect: undefined,
202 | monthDaysSelect: '6,23',
203 | weekDaysSelect: 'day of the week',
204 | hoursSelect: '3AM,6PM',
205 | minutesSelect: '1',
206 | },
207 | {
208 | title: 'leading zero with AM and PM displayed with clock format 12 hours',
209 | defaultValue: '1 3,18 6,23 * *',
210 | leadingZero: true,
211 | clockFormat: '12-hour-clock',
212 | periodSelect: 'month',
213 | monthsSelect: undefined,
214 | monthDaysSelect: '06,23',
215 | weekDaysSelect: 'day of the week',
216 | hoursSelect: '03AM,06PM',
217 | minutesSelect: '01',
218 | },
219 | {
220 | title: 'dropdowns config is allowed',
221 | defaultValue: '1 * * * MON-WED,sat',
222 | expectedValue: '1 * * * MON-WED,SAT',
223 | dropdownsConfig: {
224 | 'week-days': {
225 | humanizeLabels: true,
226 | humanizeValue: true,
227 | },
228 | 'minutes': {
229 | leadingZero: true,
230 | },
231 | },
232 | periodSelect: 'week',
233 | monthsSelect: undefined,
234 | monthDaysSelect: undefined,
235 | weekDaysSelect: 'MON-WED,SAT',
236 | hoursSelect: 'every hour',
237 | minutesSelect: '01',
238 | },
239 | {
240 | title: 'that default period can be override when default value is empty',
241 | defaultValue: '',
242 | defaultPeriod: 'year',
243 | periodSelect: 'year',
244 | monthsSelect: 'every month',
245 | monthDaysSelect: 'every day of the month',
246 | weekDaysSelect: 'every day of the week',
247 | hoursSelect: 'every hour',
248 | minutesSelect: 'every minute',
249 | },
250 | {
251 | title: 'that default period is ignored when default value is not empty',
252 | defaultValue: '* * * * *',
253 | defaultPeriod: 'year',
254 | periodSelect: 'minute',
255 | monthsSelect: undefined,
256 | monthDaysSelect: undefined,
257 | weekDaysSelect: undefined,
258 | hoursSelect: undefined,
259 | minutesSelect: undefined,
260 | },
261 | {
262 | title:
263 | 'that undefined for default value is considered like an empty string',
264 | defaultValue: undefined as unknown as string,
265 | periodSelect: 'day',
266 | monthsSelect: undefined,
267 | monthDaysSelect: undefined,
268 | weekDaysSelect: undefined,
269 | hoursSelect: 'every hour',
270 | minutesSelect: 'every minute',
271 | },
272 | {
273 | title:
274 | 'that an empty string is allowed for default value with allowEmpty always',
275 | defaultValue: '',
276 | allowEmpty: 'always',
277 | periodSelect: 'day',
278 | monthsSelect: undefined,
279 | monthDaysSelect: undefined,
280 | weekDaysSelect: undefined,
281 | hoursSelect: 'every hour',
282 | minutesSelect: 'every minute',
283 | },
284 | {
285 | title:
286 | 'that en empty string is allowed for default value with allowEmpty for-default-value',
287 | defaultValue: '',
288 | allowEmpty: 'for-default-value',
289 | periodSelect: 'day',
290 | monthsSelect: undefined,
291 | monthDaysSelect: undefined,
292 | weekDaysSelect: undefined,
293 | hoursSelect: 'every hour',
294 | minutesSelect: 'every minute',
295 | },
296 | {
297 | title:
298 | 'that an empty string is not allowed for default value with allowEmpty never',
299 | defaultValue: '',
300 | allowEmpty: 'never',
301 | periodSelect: 'day',
302 | monthsSelect: undefined,
303 | monthDaysSelect: undefined,
304 | weekDaysSelect: undefined,
305 | hoursSelect: 'every hour',
306 | minutesSelect: 'every minute',
307 | error: defaultError,
308 | },
309 | {
310 | title: 'wrong string value',
311 | defaultValue: 'wrong value',
312 | periodSelect: 'day',
313 | monthsSelect: undefined,
314 | monthDaysSelect: undefined,
315 | weekDaysSelect: undefined,
316 | hoursSelect: 'every hour',
317 | minutesSelect: 'every minute',
318 | error: defaultError,
319 | },
320 | {
321 | title: 'wrong number value',
322 | defaultValue: 200 as unknown as string,
323 | periodSelect: 'day',
324 | monthsSelect: undefined,
325 | monthDaysSelect: undefined,
326 | weekDaysSelect: undefined,
327 | hoursSelect: 'every hour',
328 | minutesSelect: 'every minute',
329 | error: defaultError,
330 | },
331 | {
332 | title: 'all units values filled set year period',
333 | defaultValue: '1 2 3 4 5',
334 | periodSelect: 'year',
335 | monthsSelect: 'APR',
336 | monthDaysSelect: '3',
337 | weekDaysSelect: 'FRI',
338 | hoursSelect: '2',
339 | minutesSelect: '1',
340 | },
341 | {
342 | title: 'month days filled set month period',
343 | defaultValue: '* * 1 * *',
344 | periodSelect: 'month',
345 | monthsSelect: undefined,
346 | monthDaysSelect: '1',
347 | weekDaysSelect: 'day of the week',
348 | hoursSelect: 'every hour',
349 | minutesSelect: 'every minute',
350 | },
351 | {
352 | title: 'that a range is allowed when valid',
353 | defaultValue: '1 2 3-9/3 4 5',
354 | periodSelect: 'year',
355 | monthsSelect: 'APR',
356 | monthDaysSelect: '3-9/3',
357 | weekDaysSelect: 'FRI',
358 | hoursSelect: '2',
359 | minutesSelect: '1',
360 | },
361 | {
362 | title: 'that reboot shortcut is allowed when shortcuts is true',
363 | defaultValue: '@reboot',
364 | shortcuts: true,
365 | periodSelect: 'reboot',
366 | monthsSelect: undefined,
367 | monthDaysSelect: undefined,
368 | weekDaysSelect: undefined,
369 | hoursSelect: undefined,
370 | minutesSelect: undefined,
371 | },
372 | {
373 | title: 'that reboot shortcut is not allowed when shortcuts is false',
374 | defaultValue: '@reboot',
375 | shortcuts: false,
376 | periodSelect: 'day',
377 | monthsSelect: undefined,
378 | monthDaysSelect: undefined,
379 | weekDaysSelect: undefined,
380 | hoursSelect: 'every hour',
381 | minutesSelect: 'every minute',
382 | error: defaultError,
383 | },
384 | {
385 | title: 'that monthly shortcut is allowed when shortcuts is true',
386 | defaultValue: '@monthly',
387 | expectedValue: '0 0 1 * *',
388 | shortcuts: true,
389 | periodSelect: 'month',
390 | monthsSelect: undefined,
391 | monthDaysSelect: '1',
392 | weekDaysSelect: 'day of the week',
393 | hoursSelect: '0',
394 | minutesSelect: '0',
395 | },
396 | {
397 | title:
398 | 'that monthly shortcut is not allowed when shortcuts only accept reboot',
399 | defaultValue: '@monthly',
400 | shortcuts: ['@reboot'],
401 | periodSelect: 'day',
402 | monthsSelect: undefined,
403 | monthDaysSelect: undefined,
404 | weekDaysSelect: undefined,
405 | hoursSelect: 'every hour',
406 | minutesSelect: 'every minute',
407 | error: defaultError,
408 | },
409 | {
410 | title: 'that monthly shortcut is allowed when shortcuts accept @monthly',
411 | defaultValue: '@monthly',
412 | expectedValue: '0 0 1 * *',
413 | shortcuts: ['@monthly'],
414 | periodSelect: 'month',
415 | monthsSelect: undefined,
416 | monthDaysSelect: '1',
417 | weekDaysSelect: 'day of the week',
418 | hoursSelect: '0',
419 | minutesSelect: '0',
420 | },
421 | {
422 | title: 'that a wrong shortcut is not allowed',
423 | defaultValue: '@wrongShortcut',
424 | shortcuts: ['@wrongShortcut'] as unknown as Shortcuts,
425 | periodSelect: 'day',
426 | monthsSelect: undefined,
427 | monthDaysSelect: undefined,
428 | weekDaysSelect: undefined,
429 | hoursSelect: 'every hour',
430 | minutesSelect: 'every minute',
431 | error: defaultError,
432 | },
433 | {
434 | title: '7 for sunday converted to sunday',
435 | defaultValue: '* * * * 7',
436 | expectedValue: '* * * * 0',
437 | periodSelect: 'week',
438 | monthsSelect: undefined,
439 | monthDaysSelect: undefined,
440 | weekDaysSelect: 'SUN',
441 | hoursSelect: 'every hour',
442 | minutesSelect: 'every minute',
443 | },
444 | {
445 | title: 'wrong range with double "/" throw an error',
446 | defaultValue: '2/2/2 * * * *',
447 | periodSelect: 'day',
448 | monthsSelect: undefined,
449 | monthDaysSelect: undefined,
450 | weekDaysSelect: undefined,
451 | hoursSelect: 'every hour',
452 | minutesSelect: 'every minute',
453 | error: defaultError,
454 | },
455 | {
456 | title: 'full week days definition set every',
457 | defaultValue: '* * * * 0,1,2,3,4,5,6',
458 | expectedValue: '* * * * *',
459 | periodSelect: 'minute',
460 | monthsSelect: undefined,
461 | monthDaysSelect: undefined,
462 | weekDaysSelect: undefined,
463 | hoursSelect: undefined,
464 | minutesSelect: undefined,
465 | },
466 | {
467 | title: 'that an out of range value too low is not allowed',
468 | defaultValue: '* * * * -1',
469 | periodSelect: 'day',
470 | monthsSelect: undefined,
471 | monthDaysSelect: undefined,
472 | weekDaysSelect: undefined,
473 | hoursSelect: 'every hour',
474 | minutesSelect: 'every minute',
475 | error: defaultError,
476 | },
477 | {
478 | title: 'that an out of range value too big is not allowed',
479 | defaultValue: '* * * * 200',
480 | periodSelect: 'day',
481 | monthsSelect: undefined,
482 | monthDaysSelect: undefined,
483 | weekDaysSelect: undefined,
484 | hoursSelect: 'every hour',
485 | minutesSelect: 'every minute',
486 | error: defaultError,
487 | },
488 | {
489 | title:
490 | 'that an out of range value too low for range first part is not allowed',
491 | defaultValue: '* * 0-3/4 * *',
492 | periodSelect: 'day',
493 | monthsSelect: undefined,
494 | monthDaysSelect: undefined,
495 | weekDaysSelect: undefined,
496 | hoursSelect: 'every hour',
497 | minutesSelect: 'every minute',
498 | error: defaultError,
499 | },
500 | {
501 | title:
502 | 'that a range first value greater than second value is not allowed',
503 | defaultValue: '* * * * 5-2',
504 | periodSelect: 'day',
505 | monthsSelect: undefined,
506 | monthDaysSelect: undefined,
507 | weekDaysSelect: undefined,
508 | hoursSelect: 'every hour',
509 | minutesSelect: 'every minute',
510 | error: defaultError,
511 | },
512 | {
513 | title: 'wrong range with more than one separator not allowed',
514 | defaultValue: '* * * * 5-2-2',
515 | periodSelect: 'day',
516 | monthsSelect: undefined,
517 | monthDaysSelect: undefined,
518 | weekDaysSelect: undefined,
519 | hoursSelect: 'every hour',
520 | minutesSelect: 'every minute',
521 | error: defaultError,
522 | },
523 | {
524 | title: 'wrong range first part not allowed',
525 | defaultValue: '* * * * error/2',
526 | periodSelect: 'day',
527 | monthsSelect: undefined,
528 | monthDaysSelect: undefined,
529 | weekDaysSelect: undefined,
530 | hoursSelect: 'every hour',
531 | minutesSelect: 'every minute',
532 | error: defaultError,
533 | },
534 | {
535 | title: 'wrong range second part not allowed',
536 | defaultValue: '* * * * 2/error',
537 | periodSelect: 'day',
538 | monthsSelect: undefined,
539 | monthDaysSelect: undefined,
540 | weekDaysSelect: undefined,
541 | hoursSelect: 'every hour',
542 | minutesSelect: 'every minute',
543 | error: defaultError,
544 | },
545 | {
546 | title: 'that dropdowns are not visible if not allowed',
547 | defaultValue: '1 1 1 1 1',
548 | expectedPeriod: 'year',
549 | allowedDropdowns: [],
550 | periodSelect: undefined,
551 | monthsSelect: undefined,
552 | monthDaysSelect: undefined,
553 | weekDaysSelect: undefined,
554 | hoursSelect: undefined,
555 | minutesSelect: undefined,
556 | },
557 | {
558 | title: 'custom multiple ranges with one interval',
559 | defaultValue: '* 2-10,19-23/2 * * *',
560 | expectedValue: '* 2-10,19,21,23 * * *',
561 | periodSelect: 'day',
562 | monthsSelect: undefined,
563 | monthDaysSelect: undefined,
564 | weekDaysSelect: undefined,
565 | hoursSelect: '2-10,19,21,23',
566 | minutesSelect: 'every minute',
567 | },
568 | {
569 | title: 'custom multiple ranges with two intervals',
570 | defaultValue: '* 2-6/2,19-23/2 * * *',
571 | expectedValue: '* 2,4,6,19,21,23 * * *',
572 | periodSelect: 'day',
573 | monthsSelect: undefined,
574 | monthDaysSelect: undefined,
575 | weekDaysSelect: undefined,
576 | hoursSelect: '2,4,6,19,21,23',
577 | minutesSelect: 'every minute',
578 | },
579 | {
580 | title: 'wrong end of value with text throw an error',
581 | defaultValue: '1-4/2crash * * * *',
582 | periodSelect: 'day',
583 | monthsSelect: undefined,
584 | monthDaysSelect: undefined,
585 | weekDaysSelect: undefined,
586 | hoursSelect: 'every hour',
587 | minutesSelect: 'every minute',
588 | error: defaultError,
589 | },
590 | {
591 | title: 'wrong end of value with # throw an error',
592 | defaultValue: '* 1#3 * * *',
593 | periodSelect: 'day',
594 | monthsSelect: undefined,
595 | monthDaysSelect: undefined,
596 | weekDaysSelect: undefined,
597 | hoursSelect: 'every hour',
598 | minutesSelect: 'every minute',
599 | error: defaultError,
600 | },
601 | {
602 | title: 'wrong end of value with # and week-day throw an error',
603 | defaultValue: '* * * * Sun#3',
604 | periodSelect: 'day',
605 | monthsSelect: undefined,
606 | monthDaysSelect: undefined,
607 | weekDaysSelect: undefined,
608 | hoursSelect: 'every hour',
609 | minutesSelect: 'every minute',
610 | error: defaultError,
611 | },
612 | {
613 | title: 'wrong end of multiple values with # throw an error',
614 | defaultValue: '* * * 1,4#3 *',
615 | periodSelect: 'day',
616 | monthsSelect: undefined,
617 | monthDaysSelect: undefined,
618 | weekDaysSelect: undefined,
619 | hoursSelect: 'every hour',
620 | minutesSelect: 'every minute',
621 | error: defaultError,
622 | },
623 | {
624 | title: 'wrong value with # in the middle throw an error',
625 | defaultValue: '* 1#2 * * *',
626 | periodSelect: 'day',
627 | monthsSelect: undefined,
628 | monthDaysSelect: undefined,
629 | weekDaysSelect: undefined,
630 | hoursSelect: 'every hour',
631 | minutesSelect: 'every minute',
632 | error: defaultError,
633 | },
634 | {
635 | title: 'wrong value with text in the middle throw an error',
636 | defaultValue: '1crash5 * * * *',
637 | periodSelect: 'day',
638 | monthsSelect: undefined,
639 | monthDaysSelect: undefined,
640 | weekDaysSelect: undefined,
641 | hoursSelect: 'every hour',
642 | minutesSelect: 'every minute',
643 | error: defaultError,
644 | },
645 | {
646 | title: 'wrong null value throw an error',
647 | defaultValue: 'null * * * *',
648 | periodSelect: 'day',
649 | monthsSelect: undefined,
650 | monthDaysSelect: undefined,
651 | weekDaysSelect: undefined,
652 | hoursSelect: 'every hour',
653 | minutesSelect: 'every minute',
654 | error: defaultError,
655 | },
656 | {
657 | title: 'wrong empty value throw an error',
658 | defaultValue: '1,,2 * * * *',
659 | periodSelect: 'day',
660 | monthsSelect: undefined,
661 | monthDaysSelect: undefined,
662 | weekDaysSelect: undefined,
663 | hoursSelect: 'every hour',
664 | minutesSelect: 'every minute',
665 | error: defaultError,
666 | },
667 | {
668 | title: 'wrong interval value throw an error',
669 | defaultValue: '1-/4 * * * *',
670 | periodSelect: 'day',
671 | monthsSelect: undefined,
672 | monthDaysSelect: undefined,
673 | weekDaysSelect: undefined,
674 | hoursSelect: 'every hour',
675 | minutesSelect: 'every minute',
676 | error: defaultError,
677 | },
678 | {
679 | title: 'wrong false value throw an error',
680 | defaultValue: 'false * * * *',
681 | periodSelect: 'day',
682 | monthsSelect: undefined,
683 | monthDaysSelect: undefined,
684 | weekDaysSelect: undefined,
685 | hoursSelect: 'every hour',
686 | minutesSelect: 'every minute',
687 | error: defaultError,
688 | },
689 | {
690 | title: 'wrong true value throw an error',
691 | defaultValue: 'true * * * *',
692 | periodSelect: 'day',
693 | monthsSelect: undefined,
694 | monthDaysSelect: undefined,
695 | weekDaysSelect: undefined,
696 | hoursSelect: 'every hour',
697 | minutesSelect: 'every minute',
698 | error: defaultError,
699 | },
700 | {
701 | title: 'wrong number with e value throw an error',
702 | defaultValue: '2e1 * * * *',
703 | periodSelect: 'day',
704 | monthsSelect: undefined,
705 | monthDaysSelect: undefined,
706 | weekDaysSelect: undefined,
707 | hoursSelect: 'every hour',
708 | minutesSelect: 'every minute',
709 | error: defaultError,
710 | },
711 | {
712 | title: 'wrong number with x value throw an error',
713 | defaultValue: '0xF * * * *',
714 | periodSelect: 'day',
715 | monthsSelect: undefined,
716 | monthDaysSelect: undefined,
717 | weekDaysSelect: undefined,
718 | hoursSelect: 'every hour',
719 | minutesSelect: 'every minute',
720 | error: defaultError,
721 | },
722 | {
723 | title: 'leading 0 in value is accepted',
724 | defaultValue: '010 * * * *',
725 | expectedValue: '10 * * * *',
726 | periodSelect: 'hour',
727 | monthsSelect: undefined,
728 | monthDaysSelect: undefined,
729 | weekDaysSelect: undefined,
730 | hoursSelect: undefined,
731 | minutesSelect: '10',
732 | },
733 | ]
734 |
735 | test.each(cases)(
736 | 'should check $title',
737 | ({
738 | defaultValue,
739 | expectedValue,
740 | expectedPeriod,
741 | allowEmpty,
742 | shortcuts,
743 | humanizeLabels,
744 | humanizeValue,
745 | leadingZero,
746 | clockFormat,
747 | allowedDropdowns,
748 | defaultPeriod,
749 | periodSelect,
750 | monthsSelect,
751 | monthDaysSelect,
752 | weekDaysSelect,
753 | hoursSelect,
754 | minutesSelect,
755 | dropdownsConfig,
756 | error,
757 | }) => {
758 | const setValue = jest.fn()
759 | const onError = jest.fn()
760 |
761 | render(
762 |
776 | )
777 |
778 | //
779 | // Check error management
780 |
781 | if (error) {
782 | expect(onError).toHaveBeenLastCalledWith(error)
783 | } else {
784 | expect(onError).toHaveBeenLastCalledWith(undefined)
785 | }
786 |
787 | //
788 | // Check value after Cron component validation
789 |
790 | if (defaultValue && !error) {
791 | const valueToCheck = expectedValue || defaultValue
792 | const selectedPeriodToCheck = expectedPeriod || periodSelect
793 |
794 | expect(setValue).toHaveBeenLastCalledWith(valueToCheck, {
795 | selectedPeriod: selectedPeriodToCheck,
796 | })
797 | }
798 |
799 | //
800 | // Check period dropdown
801 |
802 | if (periodSelect) {
803 | expect(screen.getByTestId('select-period')).toBeVisible()
804 | expect(screen.getByTestId('select-period').textContent).toContain(
805 | periodSelect
806 | )
807 | } else {
808 | expect(screen.queryByTestId(/select-period/i)).toBeNull()
809 | }
810 |
811 | //
812 | // Check months dropdown
813 |
814 | if (monthsSelect) {
815 | expect(screen.queryByTestId('custom-select-months')).toBeVisible()
816 | expect(
817 | screen.getByTestId('custom-select-months').textContent
818 | ).toContain(monthsSelect)
819 | } else {
820 | expect(screen.queryByTestId(/custom-select-months/i)).toBeNull()
821 | }
822 |
823 | //
824 | // Check month days dropdown
825 |
826 | if (monthDaysSelect) {
827 | expect(screen.queryByTestId('custom-select-month-days')).toBeVisible()
828 | expect(
829 | screen.getByTestId('custom-select-month-days').textContent
830 | ).toContain(monthDaysSelect)
831 | } else {
832 | expect(screen.queryByTestId(/custom-select-month-days/i)).toBeNull()
833 | }
834 |
835 | //
836 | // Check week days dropdown
837 |
838 | if (weekDaysSelect) {
839 | expect(screen.queryByTestId('custom-select-week-days')).toBeVisible()
840 | expect(
841 | screen.getByTestId('custom-select-week-days').textContent
842 | ).toContain(weekDaysSelect)
843 | } else {
844 | expect(screen.queryByTestId(/custom-select-week-days/i)).toBeNull()
845 | }
846 |
847 | //
848 | // Check hours dropdown
849 |
850 | if (hoursSelect) {
851 | expect(screen.queryByTestId('custom-select-hours')).toBeVisible()
852 | expect(screen.getByTestId('custom-select-hours').textContent).toContain(
853 | hoursSelect
854 | )
855 | } else {
856 | expect(screen.queryByTestId(/custom-select-hours/i)).toBeNull()
857 | }
858 |
859 | //
860 | // Check minutes dropdown
861 |
862 | if (minutesSelect) {
863 | expect(screen.queryByTestId('custom-select-minutes')).toBeVisible()
864 | expect(
865 | screen.getByTestId('custom-select-minutes').textContent
866 | ).toContain(minutesSelect)
867 | } else {
868 | expect(screen.queryByTestId(/custom-select-minutes/i)).toBeNull()
869 | }
870 | }
871 | )
872 | })
873 |
--------------------------------------------------------------------------------
/src/tests/Cron.updateValue.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, waitFor } from '@testing-library/react'
2 | import userEvent from '@testing-library/user-event'
3 |
4 | import Cron from '../Cron'
5 |
6 | describe('Cron update value test suite', () => {
7 | it("should check that it's possible to change the period from minute to year", async () => {
8 | const user = userEvent.setup()
9 | const value = '* * * * *'
10 | const setValue = jest.fn()
11 |
12 | render()
13 |
14 | // Open Period dropdown
15 | await waitFor(() => {
16 | user.click(screen.getByText('minute'))
17 | })
18 |
19 | // Select year period
20 | await waitFor(() => {
21 | user.click(screen.getByText('year'))
22 | })
23 |
24 | // Check dropdowns values
25 | await waitFor(() => {
26 | expect(screen.getByTestId('select-period').textContent).toContain('year')
27 | expect(screen.getByTestId('custom-select-months').textContent).toContain(
28 | 'every month'
29 | )
30 | expect(
31 | screen.getByTestId('custom-select-month-days').textContent
32 | ).toContain('every day of the month')
33 | expect(
34 | screen.getByTestId('custom-select-week-days').textContent
35 | ).toContain('every day of the week')
36 | expect(screen.getByTestId('custom-select-hours').textContent).toContain(
37 | 'every hour'
38 | )
39 | expect(screen.getByTestId('custom-select-minutes').textContent).toContain(
40 | 'every minute'
41 | )
42 | })
43 | })
44 |
45 | it("should check that it's possible to select specific minutes", async () => {
46 | const user = userEvent.setup()
47 | const value = '1,4 * * * *'
48 | const setValue = jest.fn()
49 |
50 | render()
51 |
52 | // Open minute dropdown
53 | await waitFor(() => user.click(screen.getByText('1,4')))
54 |
55 | // Select another minute value
56 | await waitFor(() => user.click(screen.getByText('59')))
57 |
58 | // Check dropdowns values
59 | expect(await screen.findByText('1,4,59')).toBeVisible()
60 | })
61 |
62 | it("should check that it's possible to select a periodicity with double click", async () => {
63 | const user = userEvent.setup()
64 | const value = '1,4 * * * *'
65 | const setValue = jest.fn()
66 |
67 | render()
68 |
69 | // Open minute dropdown
70 | await waitFor(() => {
71 | user.click(screen.getByText('1,4'))
72 | })
73 |
74 | // Select another minute value
75 | await waitFor(() => {
76 | user.dblClick(screen.getByText('2'))
77 | })
78 |
79 | // Check dropdowns values
80 | await waitFor(() => {
81 | expect(screen.getByTestId('custom-select-minutes').textContent).toContain(
82 | 'every 2'
83 | )
84 | })
85 | })
86 |
87 | it("should check that it's possible to change a periodicity with double click", async () => {
88 | const user = userEvent.setup()
89 | const value = '*/2 * * * *'
90 | const setValue = jest.fn()
91 |
92 | render()
93 |
94 | // Open minute dropdown
95 | await waitFor(() => {
96 | user.click(screen.getByText('every 2'))
97 | })
98 |
99 | // Select another minute value
100 | await waitFor(() => {
101 | user.dblClick(screen.getByText('4'))
102 | })
103 |
104 | // Check dropdowns values
105 | await waitFor(() => {
106 | expect(screen.getByTestId('custom-select-minutes').textContent).toContain(
107 | 'every 4'
108 | )
109 | })
110 | })
111 |
112 | it("should check that it's possible to clear cron value", async () => {
113 | const user = userEvent.setup()
114 | const value = '1 1 1 1 1'
115 | const setValue = jest.fn()
116 |
117 | render()
118 |
119 | // Clear cron value
120 | await waitFor(() => {
121 | user.click(screen.getByText('Clear'))
122 | })
123 |
124 | // Check dropdowns values
125 | await waitFor(() => {
126 | expect(setValue).toHaveBeenNthCalledWith(2, '* * * * *', {
127 | selectedPeriod: 'year',
128 | })
129 | })
130 | })
131 |
132 | it("should check that it's possible to clear cron value with empty", async () => {
133 | const user = userEvent.setup()
134 | const value = '1 1 1 1 1'
135 | const setValue = jest.fn()
136 |
137 | render()
138 |
139 | // Clear cron value
140 | await waitFor(() => {
141 | user.click(screen.getByText('Clear'))
142 | })
143 |
144 | // Check dropdowns values
145 | await waitFor(() => {
146 | expect(setValue).toHaveBeenNthCalledWith(2, '', {
147 | selectedPeriod: 'year',
148 | })
149 | })
150 | })
151 |
152 | it("should check that it's possible to clear cron value when @reboot is set", async () => {
153 | const user = userEvent.setup()
154 | const value = '@reboot'
155 | const setValue = jest.fn()
156 |
157 | render()
158 |
159 | // Clear cron value
160 | await waitFor(() => {
161 | user.click(screen.getByText('Clear'))
162 | })
163 |
164 | // Check dropdowns values
165 | await waitFor(() => {
166 | expect(setValue).toHaveBeenNthCalledWith(2, '* * * * *', {
167 | selectedPeriod: 'day',
168 | })
169 | })
170 | })
171 |
172 | it('should check that pressing clear setting an empty value throw an error if not allowed', async () => {
173 | const user = userEvent.setup()
174 | const value = '1 1 1 1 1'
175 | const setValue = jest.fn()
176 | const onError = jest.fn()
177 |
178 | render(
179 |
186 | )
187 |
188 | // Clear cron value
189 | await waitFor(() => {
190 | user.click(screen.getByText('Clear'))
191 | })
192 |
193 | // Check dropdowns values and error
194 | await waitFor(() => {
195 | expect(setValue).toHaveBeenNthCalledWith(2, '', {
196 | selectedPeriod: 'year',
197 | })
198 | expect(onError).toHaveBeenNthCalledWith(3, {
199 | description: 'Invalid cron expression',
200 | type: 'invalid_cron',
201 | })
202 | })
203 | })
204 |
205 | it("should check that pressing clear setting an empty value don't throw an error if not allowed", async () => {
206 | const user = userEvent.setup()
207 | const value = '1 1 1 1 1'
208 | const setValue = jest.fn()
209 | const onError = jest.fn()
210 |
211 | render(
212 |
219 | )
220 |
221 | // Clear cron value
222 | await waitFor(() => {
223 | user.click(screen.getByText('Clear'))
224 | })
225 |
226 | // Check dropdowns values and error
227 | await waitFor(() => {
228 | expect(setValue).toHaveBeenNthCalledWith(2, '', {
229 | selectedPeriod: 'year',
230 | })
231 | expect(onError).toHaveBeenNthCalledWith(3, undefined)
232 | })
233 | })
234 |
235 | it("should check that it's not possible to update value when it's readOnly mode", async () => {
236 | const user = userEvent.setup()
237 | const value = '1,4 * * * *'
238 | const setValue = jest.fn()
239 |
240 | render()
241 |
242 | // Open minute dropdown
243 | await waitFor(() => user.click(screen.getByText('1,4')))
244 |
245 | // Check dropdown is not visible
246 | await waitFor(() => {
247 | expect(screen.queryByText('59')).not.toBeInTheDocument()
248 | })
249 |
250 | // Check dropdowns values still the sane
251 | expect(await screen.findByText('1,4')).toBeVisible()
252 | })
253 |
254 | it("should check that it's not possible to update value when it's disabled mode", async () => {
255 | const user = userEvent.setup()
256 | const value = '1,4 * * * *'
257 | const setValue = jest.fn()
258 |
259 | render()
260 |
261 | // Open minute dropdown
262 | await waitFor(() => user.click(screen.getByText('1,4')))
263 |
264 | // Check dropdown is not visible
265 | await waitFor(() => {
266 | expect(screen.queryByText('59')).not.toBeInTheDocument()
267 | })
268 |
269 | // Check dropdowns values still the sane
270 | expect(await screen.findByText('1,4')).toBeVisible()
271 | })
272 |
273 | it('should check that week-days and minutes options are filtered with dropdownConfig', async () => {
274 | const user = userEvent.setup()
275 | const value = '4,6 * * * 1'
276 | const setValue = jest.fn()
277 |
278 | render(
279 | Number(value) < 58,
286 | },
287 | 'week-days': {
288 | // Remove sunday
289 | filterOption: ({ value }) => Number(value) !== 0,
290 | },
291 | }}
292 | />
293 | )
294 |
295 | // Open minutes dropdown
296 | await waitFor(() => {
297 | user.click(screen.getByText('4,6'))
298 | })
299 |
300 | // Check minutes
301 | await waitFor(() => {
302 | for (let i = 0; i < 60; i++) {
303 | if (i < 58) {
304 | expect(screen.getByText(i)).toBeVisible()
305 | } else {
306 | expect(screen.queryByText(i)).not.toBeInTheDocument()
307 | }
308 | }
309 | })
310 |
311 | // Open week-days dropdown
312 | await waitFor(() => {
313 | user.click(screen.getByText('MON'))
314 | })
315 |
316 | // Check days of the week
317 | await waitFor(() => {
318 | const days = [
319 | 'Sunday',
320 | 'Monday',
321 | 'Tuesday',
322 | 'Wednesday',
323 | 'Thursday',
324 | 'Friday',
325 | 'Saturday',
326 | ]
327 | for (let i = 0; i < 7; i++) {
328 | if (i === 0) {
329 | expect(screen.queryByText(days[i])).not.toBeInTheDocument()
330 | } else {
331 | expect(screen.getByText(days[i])).toBeVisible()
332 | }
333 | }
334 | })
335 | })
336 | })
337 |
--------------------------------------------------------------------------------
/src/tests/__snapshots__/fields.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Fields matches the original snapshot 1`] = `
4 |
5 |
8 |
9 | at
10 |
11 |
15 |
18 |
21 |
24 |
28 |
32 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
60 | every hour
61 |
62 |
63 |
64 |
70 |
75 |
88 |
89 |
90 |
91 |
92 |
93 | `;
94 |
95 | exports[`Fields matches the original snapshot 1`] = `
96 |
97 |
100 |
101 | :
102 |
103 |
107 |
110 |
113 |
116 |
120 |
124 |
140 |
144 |
145 |
146 |
147 |
148 |
149 |
152 | every minute
153 |
154 |
155 |
156 |
162 |
167 |
180 |
181 |
182 |
183 |
184 |
185 | `;
186 |
187 | exports[`Fields matches the original snapshot 1`] = `
188 |
189 |
192 |
193 | on
194 |
195 |
199 |
202 |
205 |
208 |
212 |
216 |
232 |
236 |
237 |
238 |
239 |
240 |
241 |
244 | every day of the month
245 |
246 |
247 |
248 |
254 |
259 |
272 |
273 |
274 |
275 |
276 |
277 | `;
278 |
279 | exports[`Fields matches the original snapshot 1`] = `
280 |
281 |
284 |
285 | in
286 |
287 |
291 |
294 |
297 |
300 |
304 |
308 |
324 |
328 |
329 |
330 |
331 |
332 |
333 |
336 | every month
337 |
338 |
339 |
340 |
346 |
351 |
364 |
365 |
366 |
367 |
368 |
369 | `;
370 |
371 | exports[`Fields matches the original snapshot 1`] = `
372 |
373 |
376 |
377 | Every
378 |
379 |
383 |
386 |
389 |
392 |
408 |
409 |
413 | year
414 |
415 |
416 |
417 |
423 |
428 |
441 |
442 |
443 |
444 |
445 |
446 | `;
447 |
448 | exports[`Fields matches the original snapshot 1`] = `
449 |
450 |
453 |
454 | on
455 |
456 |
460 |
463 |
466 |
469 |
473 |
477 |
493 |
497 |
498 |
499 |
500 |
501 |
502 |
505 | every day of the week
506 |
507 |
508 |
509 |
515 |
520 |
533 |
534 |
535 |
536 |
537 |
538 | `;
539 |
--------------------------------------------------------------------------------
/src/tests/fields.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react'
2 |
3 | import Hours from '../fields/Hours'
4 | import Minutes from '../fields/Minutes'
5 | import MonthDays from '../fields/MonthDays'
6 | import Months from '../fields/Months'
7 | import Period from '../fields/Period'
8 | import WeekDays from '../fields/WeekDays'
9 | import { DEFAULT_LOCALE_EN } from '../locale'
10 |
11 | describe('Fields', () => {
12 | it(' matches the original snapshot', () => {
13 | const { asFragment } = render(
14 | value}
16 | locale={DEFAULT_LOCALE_EN}
17 | mode='multiple'
18 | period='month'
19 | disabled={false}
20 | readOnly={false}
21 | periodicityOnDoubleClick
22 | leadingZero
23 | />
24 | )
25 |
26 | expect(asFragment()).toMatchSnapshot()
27 | })
28 |
29 | it(' matches the original snapshot', () => {
30 | const { asFragment } = render(
31 | value}
33 | locale={DEFAULT_LOCALE_EN}
34 | mode='multiple'
35 | period='month'
36 | disabled={false}
37 | readOnly={false}
38 | periodicityOnDoubleClick
39 | leadingZero
40 | />
41 | )
42 |
43 | expect(asFragment()).toMatchSnapshot()
44 | })
45 |
46 | it(' matches the original snapshot', () => {
47 | const { asFragment } = render(
48 | value}
50 | locale={DEFAULT_LOCALE_EN}
51 | mode='multiple'
52 | period='month'
53 | disabled={false}
54 | readOnly={false}
55 | periodicityOnDoubleClick
56 | leadingZero
57 | />
58 | )
59 |
60 | expect(asFragment()).toMatchSnapshot()
61 | })
62 |
63 | it(' matches the original snapshot', () => {
64 | const { asFragment } = render(
65 | value}
67 | locale={DEFAULT_LOCALE_EN}
68 | mode='multiple'
69 | period='year'
70 | disabled={false}
71 | readOnly={false}
72 | periodicityOnDoubleClick
73 | humanizeLabels
74 | />
75 | )
76 |
77 | expect(asFragment()).toMatchSnapshot()
78 | })
79 |
80 | it(' matches the original snapshot', () => {
81 | const { asFragment } = render(
82 | value}
84 | locale={DEFAULT_LOCALE_EN}
85 | disabled={false}
86 | readOnly={false}
87 | value='year'
88 | allowedPeriods={[
89 | 'minute',
90 | 'hour',
91 | 'day',
92 | 'week',
93 | 'month',
94 | 'year',
95 | 'reboot',
96 | ]}
97 | shortcuts
98 | />
99 | )
100 |
101 | expect(asFragment()).toMatchSnapshot()
102 | })
103 |
104 | it(' matches the original snapshot', () => {
105 | const { asFragment } = render(
106 | value}
108 | locale={DEFAULT_LOCALE_EN}
109 | disabled={false}
110 | readOnly={false}
111 | mode='multiple'
112 | period='week'
113 | humanizeLabels
114 | periodicityOnDoubleClick
115 | />
116 | )
117 |
118 | expect(asFragment()).toMatchSnapshot()
119 | })
120 | })
121 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { ButtonProps, SelectProps } from 'antd'
2 | import { Dispatch, SetStateAction } from 'react'
3 |
4 | // External props
5 |
6 | export interface CronProps {
7 | /**
8 | * Cron value, the component is by design a controlled component.
9 | * The first value will be the default value.
10 | *
11 | * required
12 | */
13 | value: string
14 |
15 | /**
16 | * Set the cron value, similar to onChange.
17 | * The naming tells you that you have to set the value by yourself.
18 | *
19 | * required
20 | */
21 | setValue: SetValue
22 |
23 | /**
24 | * Set the container className and used as a prefix for other selectors.
25 | * Available selectors: https://xrutayisire.github.io/react-js-cron/?path=/story/reactjs-cron--custom-style
26 | */
27 | className?: string
28 |
29 | /**
30 | * Humanize the labels in the cron component, SUN-SAT and JAN-DEC.
31 | *
32 | * Default: true
33 | */
34 | humanizeLabels?: boolean
35 |
36 | /**
37 | * Humanize the value, SUN-SAT and JAN-DEC.
38 | *
39 | * Default: false
40 | */
41 | humanizeValue?: boolean
42 |
43 | /**
44 | * Add a "0" before numbers lower than 10.
45 | *
46 | * Default: false
47 | */
48 | leadingZero?: LeadingZero
49 |
50 | /**
51 | * Define the default period when the default value is empty.
52 | *
53 | * Default: 'day'
54 | */
55 | defaultPeriod?: PeriodType
56 |
57 | /**
58 | * Disable the cron component.
59 | *
60 | * Default: false
61 | */
62 | disabled?: boolean
63 |
64 | /**
65 | * Make the cron component read-only.
66 | *
67 | * Default: false
68 | */
69 | readOnly?: boolean
70 |
71 | /**
72 | * Show clear button for each dropdown.
73 | *
74 | * Default: true
75 | */
76 | allowClear?: boolean
77 |
78 | /**
79 | * Define if empty should trigger an error.
80 | *
81 | * Default: 'for-default-value'
82 | */
83 | allowEmpty?: AllowEmpty
84 |
85 | /**
86 | * Support cron shortcuts.
87 | *
88 | * Default: ['@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly']
89 | */
90 | shortcuts?: Shortcuts
91 |
92 | /**
93 | * Define the clock format.
94 | *
95 | * Default: undefined
96 | */
97 | clockFormat?: ClockFormat
98 |
99 | /**
100 | * Display the clear button.
101 | *
102 | * Default: true
103 | */
104 | clearButton?: boolean
105 |
106 | /**
107 | * antd button props to customize the clear button.
108 | */
109 | clearButtonProps?: ClearButtonProps
110 |
111 | /**
112 | * Define the clear button action.
113 | *
114 | * Default: 'fill-with-every'
115 | */
116 | clearButtonAction?: ClearButtonAction
117 |
118 | /**
119 | * Display error style (red border and background).
120 | *
121 | * Display: true
122 | */
123 | displayError?: boolean
124 |
125 | /**
126 | * Triggered when the cron component detects an error with the value.
127 | */
128 | onError?: OnError
129 |
130 | /**
131 | * Define if a double click on a dropdown option should automatically
132 | * select / unselect a periodicity.
133 | *
134 | * Default: true
135 | */
136 | periodicityOnDoubleClick?: boolean
137 |
138 | /**
139 | * Define if it's possible to select only one or multiple values for each dropdowns.
140 | *
141 | * Even in single mode, if you want to disable the double click on a dropdown option that
142 | * automatically select / unselect a periodicity, set 'periodicityOnDoubleClick'
143 | * prop at false.
144 | *
145 | * When single mode is active and 'periodicityOnDoubleClick' is false,
146 | * each dropdown will automatically close after selecting a value
147 | *
148 | * Default: 'multiple'
149 | */
150 | mode?: Mode
151 |
152 | /**
153 | * Define which dropdowns need to be displayed.
154 | *
155 | * Default: ['period', 'months', 'month-days', 'week-days', 'hours', 'minutes']
156 | */
157 | allowedDropdowns?: CronType[]
158 |
159 | /**
160 | * Define the list of periods available.
161 | *
162 | * Default: ['year', 'month', 'week', 'day', 'hour', 'minute', 'reboot']
163 | */
164 | allowedPeriods?: PeriodType[]
165 |
166 | /**
167 | * Define specific configuration that is used for each dropdown specifically.
168 | * Configuring a dropdown will override any global configuration for the same property.
169 | *
170 | * Configuration available:
171 | *
172 | * // See global configuration
173 | * // For 'months' and 'week-days'
174 | * humanizeLabels?: boolean
175 | *
176 | * // See global configuration
177 | * // For 'months' and 'week-days'
178 | * humanizeValue?: boolean
179 | *
180 | * // See global configuration
181 | * // For 'month-days', 'hours' and 'minutes'
182 | * leadingZero?: boolean
183 | *
184 | * // See global configuration
185 | * For 'period', 'months', 'month-days', 'week-days', 'hours' and 'minutes'
186 | * disabled?: boolean
187 | *
188 | * // See global configuration
189 | * For 'period', 'months', 'month-days', 'week-days', 'hours' and 'minutes'
190 | * readOnly?: boolean
191 | *
192 | * // See global configuration
193 | * // For 'period', 'months', 'month-days', 'week-days', 'hours' and 'minutes'
194 | * allowClear?: boolean
195 | *
196 | * // See global configuration
197 | * // For 'months', 'month-days', 'week-days', 'hours' and 'minutes'
198 | * periodicityOnDoubleClick?: boolean
199 | *
200 | * // See global configuration
201 | * // For 'months', 'month-days', 'week-days', 'hours' and 'minutes'
202 | * mode?: Mode
203 | *
204 | * // The function will receive one argument, an object with value and label.
205 | * // If the function returns true, the option will be included in the filtered set.
206 | * // Otherwise, it will be excluded.
207 | * // For 'months', 'month-days', 'week-days', 'hours' and 'minutes'
208 | * filterOption?: FilterOption
209 | *
210 | * Default: undefined
211 | */
212 | dropdownsConfig?: DropdownsConfig
213 |
214 | /**
215 | * Change the component language.
216 | * Can also be used to remove prefix and suffix.
217 | *
218 | * When setting 'humanizeLabels' you can change the language of the
219 | * alternative labels with 'altWeekDays' and 'altMonths'.
220 | *
221 | * The order of the 'locale' properties 'weekDays', 'months', 'altMonths'
222 | * and 'altWeekDays' is important! The index will be used as value.
223 | *
224 | * Default './src/locale.ts'
225 | */
226 | locale?: Locale
227 |
228 | /**
229 | * Define the container for the dropdowns.
230 | * By default, the dropdowns will be rendered in the body.
231 | * This is useful when you want to render the dropdowns in a specific
232 | * container, for example, when using a modal or a specific layout.
233 | */
234 | getPopupContainer?: () => HTMLElement
235 | }
236 | export interface Locale {
237 | everyText?: string
238 | emptyMonths?: string
239 | emptyMonthDays?: string
240 | emptyMonthDaysShort?: string
241 | emptyWeekDays?: string
242 | emptyWeekDaysShort?: string
243 | emptyHours?: string
244 | emptyMinutes?: string
245 | emptyMinutesForHourPeriod?: string
246 | yearOption?: string
247 | monthOption?: string
248 | weekOption?: string
249 | dayOption?: string
250 | hourOption?: string
251 | minuteOption?: string
252 | rebootOption?: string
253 | prefixPeriod?: string
254 | prefixMonths?: string
255 | prefixMonthDays?: string
256 | prefixWeekDays?: string
257 | prefixWeekDaysForMonthAndYearPeriod?: string
258 | prefixHours?: string
259 | prefixMinutes?: string
260 | prefixMinutesForHourPeriod?: string
261 | suffixMinutesForHourPeriod?: string
262 | errorInvalidCron?: string
263 | clearButtonText?: string
264 | weekDays?: string[]
265 | months?: string[]
266 | altWeekDays?: string[]
267 | altMonths?: string[]
268 | }
269 | export type SetValueFunction = (
270 | value: string,
271 | extra: SetValueFunctionExtra
272 | ) => void
273 | export interface SetValueFunctionExtra {
274 | selectedPeriod: PeriodType
275 | }
276 | export type SetValue = SetValueFunction | Dispatch>
277 | export type CronError =
278 | | {
279 | type: 'invalid_cron'
280 | description: string
281 | }
282 | | undefined
283 | export type OnErrorFunction = (error: CronError) => void
284 | export type OnError =
285 | | OnErrorFunction
286 | | Dispatch>
287 | | undefined
288 | export interface ClearButtonProps extends Omit {}
289 | export type ClearButtonAction = 'empty' | 'fill-with-every'
290 | export type PeriodType =
291 | | 'year'
292 | | 'month'
293 | | 'week'
294 | | 'day'
295 | | 'hour'
296 | | 'minute'
297 | | 'reboot'
298 | export type AllowEmpty = 'always' | 'never' | 'for-default-value'
299 | export type CronType =
300 | | 'period'
301 | | 'months'
302 | | 'month-days'
303 | | 'week-days'
304 | | 'hours'
305 | | 'minutes'
306 | export type LeadingZeroType = 'month-days' | 'hours' | 'minutes'
307 | export type LeadingZero = boolean | LeadingZeroType[]
308 | export type ClockFormat = '24-hour-clock' | '12-hour-clock'
309 | export type ShortcutsType =
310 | | '@yearly'
311 | | '@annually'
312 | | '@monthly'
313 | | '@weekly'
314 | | '@daily'
315 | | '@midnight'
316 | | '@hourly'
317 | | '@reboot'
318 | export type Shortcuts = boolean | ShortcutsType[]
319 | export type Mode = 'multiple' | 'single'
320 | export type DropdownConfig = {
321 | humanizeLabels?: boolean
322 | humanizeValue?: boolean
323 | leadingZero?: boolean
324 | disabled?: boolean
325 | readOnly?: boolean
326 | allowClear?: boolean
327 | periodicityOnDoubleClick?: boolean
328 | mode?: Mode
329 | filterOption?: FilterOption
330 | }
331 | export type DropdownsConfig = {
332 | 'period'?: Pick
333 | 'months'?: Omit
334 | 'month-days'?: Omit
335 | 'week-days'?: Omit
336 | 'hours'?: Omit
337 | 'minutes'?: Omit
338 | }
339 |
340 | // Internal props
341 |
342 | export interface FieldProps {
343 | value?: number[]
344 | setValue: SetValueNumbersOrUndefined
345 | locale: Locale
346 | className?: string
347 | disabled: boolean
348 | readOnly: boolean
349 | period: PeriodType
350 | periodicityOnDoubleClick: boolean
351 | mode: Mode
352 | allowClear?: boolean
353 | filterOption?: FilterOption
354 | getPopupContainer?: () => HTMLElement
355 | }
356 | export interface PeriodProps
357 | extends Omit<
358 | FieldProps,
359 | | 'value'
360 | | 'setValue'
361 | | 'period'
362 | | 'periodicityOnDoubleClick'
363 | | 'mode'
364 | | 'filterOption'
365 | > {
366 | value: PeriodType
367 | setValue: SetValuePeriod
368 | shortcuts: Shortcuts
369 | allowedPeriods: PeriodType[]
370 | getPopupContainer?: () => HTMLElement
371 | }
372 | export interface MonthsProps extends FieldProps {
373 | humanizeLabels: boolean
374 | }
375 | export interface MonthDaysProps extends FieldProps {
376 | weekDays?: number[]
377 | leadingZero: LeadingZero
378 | }
379 | export interface WeekDaysProps extends FieldProps {
380 | humanizeLabels: boolean
381 | monthDays?: number[]
382 | }
383 | export interface HoursProps extends FieldProps {
384 | leadingZero: LeadingZero
385 | clockFormat?: ClockFormat
386 | }
387 | export interface MinutesProps extends FieldProps {
388 | leadingZero: LeadingZero
389 | clockFormat?: ClockFormat
390 | }
391 | export interface CustomSelectProps
392 | extends Omit<
393 | SelectProps,
394 | | 'mode'
395 | | 'tokenSeparators'
396 | | 'virtual'
397 | | 'onClick'
398 | | 'onBlur'
399 | | 'tagRender'
400 | | 'dropdownRender'
401 | | 'showSearch'
402 | | 'suffixIcon'
403 | | 'onChange'
404 | | 'dropdownMatchSelectWidth'
405 | | 'options'
406 | | 'onSelect'
407 | | 'onDeselect'
408 | | 'filterOption'
409 | > {
410 | grid?: boolean
411 | setValue: SetValueNumbersOrUndefined
412 | optionsList?: string[]
413 | locale: Locale
414 | value?: number[]
415 | humanizeLabels?: boolean
416 | disabled: boolean
417 | readOnly: boolean
418 | leadingZero?: LeadingZero
419 | clockFormat?: ClockFormat
420 | period: PeriodType
421 | unit: Unit
422 | periodicityOnDoubleClick: boolean
423 | mode: Mode
424 | filterOption?: FilterOption
425 | getPopupContainer?: () => HTMLElement
426 | }
427 | export type SetValueNumbersOrUndefined = Dispatch<
428 | SetStateAction
429 | >
430 | export type SetValuePeriod = Dispatch>
431 | export type SetInternalError = Dispatch>
432 | export interface DefaultLocale {
433 | everyText: string
434 | emptyMonths: string
435 | emptyMonthDays: string
436 | emptyMonthDaysShort: string
437 | emptyWeekDays: string
438 | emptyWeekDaysShort: string
439 | emptyHours: string
440 | emptyMinutes: string
441 | emptyMinutesForHourPeriod: string
442 | yearOption: string
443 | monthOption: string
444 | weekOption: string
445 | dayOption: string
446 | hourOption: string
447 | minuteOption: string
448 | rebootOption: string
449 | prefixPeriod: string
450 | prefixMonths: string
451 | prefixMonthDays: string
452 | prefixWeekDays: string
453 | prefixWeekDaysForMonthAndYearPeriod: string
454 | prefixHours: string
455 | prefixMinutes: string
456 | prefixMinutesForHourPeriod: string
457 | suffixMinutesForHourPeriod: string
458 | errorInvalidCron: string
459 | clearButtonText: string
460 | weekDays: string[]
461 | months: string[]
462 | altWeekDays: string[]
463 | altMonths: string[]
464 | }
465 | export interface Classes {
466 | [key: string]: boolean
467 | }
468 | export interface ShortcutsValues {
469 | name: ShortcutsType
470 | value: string
471 | }
472 | export interface Unit {
473 | type: CronType
474 | min: number
475 | max: number
476 | total: number
477 | alt?: string[]
478 | }
479 | export interface Clicks {
480 | time: number
481 | value: number
482 | }
483 |
484 | export type FilterOption = ({
485 | value,
486 | label,
487 | }: {
488 | value: string
489 | label: string
490 | }) => boolean
491 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | import { DEFAULT_LOCALE_EN } from './locale'
4 | import { Classes, Locale, OnError } from './types'
5 |
6 | /**
7 | * Creates an array of integers from start to end, inclusive
8 | */
9 | export function range(start: number, end: number) {
10 | const array: number[] = []
11 |
12 | for (let i = start; i <= end; i++) {
13 | array.push(i)
14 | }
15 |
16 | return array
17 | }
18 |
19 | /**
20 | * Sorts an array of numbers
21 | */
22 | export function sort(array: number[]) {
23 | array.sort(function (a, b) {
24 | return a - b
25 | })
26 |
27 | return array
28 | }
29 |
30 | /**
31 | * Removes duplicate entries from an array
32 | */
33 | export function dedup(array: number[]) {
34 | const result: number[] = []
35 |
36 | array.forEach(function (i) {
37 | if (result.indexOf(i) < 0) {
38 | result.push(i)
39 | }
40 | })
41 |
42 | return result
43 | }
44 |
45 | /**
46 | * Simple classNames util function to prevent adding external library 'classnames'
47 | */
48 | export function classNames(classes: Classes) {
49 | return Object.entries(classes)
50 | .filter(([key, value]) => key && value)
51 | .map(([key]) => key)
52 | .join(' ')
53 | }
54 |
55 | /**
56 | * Handle onError prop to set the error
57 | */
58 | export function setError(onError: OnError, locale: Locale) {
59 | onError &&
60 | onError({
61 | type: 'invalid_cron',
62 | description:
63 | locale.errorInvalidCron || DEFAULT_LOCALE_EN.errorInvalidCron,
64 | })
65 | }
66 |
67 | /**
68 | * React useEffect hook to return the previous value
69 | */
70 | export function usePrevious(value: any) {
71 | const ref = useRef(value)
72 |
73 | useEffect(() => {
74 | ref.current = value
75 | }, [value])
76 |
77 | return ref.current
78 | }
79 |
80 | /**
81 | * Convert a string to number but fail if not valid for cron
82 | */
83 | export function convertStringToNumber(str: string) {
84 | const parseIntValue = parseInt(str, 10)
85 | const numberValue = Number(str)
86 |
87 | return parseIntValue === numberValue ? numberValue : NaN
88 | }
89 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationDir": "types",
5 | "emitDeclarationOnly": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "lib": ["dom", "dom.iterable", "esnext"],
9 | "module": "ESNext",
10 | "moduleResolution": "node",
11 | "noImplicitAny": true,
12 | "outDir": "dist",
13 | "removeComments": true,
14 | "rootDir": "src",
15 | "strictNullChecks": true,
16 | "target": "es5"
17 | },
18 | "include": ["./src/**/*"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------