<template> <div class="w-full py-2"> <div class="flex -mb-px"> <div class="w-1/2 h-8 rounded-tl-md relative border border-black-200 flex items-center justify-center cursor-pointer" :class="!showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = false"> <p class="text-sm">{{ $strings.HeaderSchedule }}</p> </div> <div class="w-1/2 h-8 rounded-tr-md relative border border-black-200 flex items-center justify-center -ml-px cursor-pointer" :class="showAdvancedView ? 'text-white bg-bg hover:bg-opacity-60 border-b-bg' : 'text-gray-400 hover:text-gray-300 bg-primary bg-opacity-70 hover:bg-opacity-60'" @click="showAdvancedView = true"> <p class="text-sm">{{ $strings.HeaderAdvanced }}</p> </div> </div> <div class="px-2 py-4 md:p-4 border border-black-200 rounded-b-md mr-px" style="min-height: 280px"> <template v-if="!showAdvancedView"> <ui-dropdown v-model="selectedInterval" @input="updateCron" :label="$strings.LabelInterval" :items="intervalOptions" class="mb-2" /> <ui-multi-select-dropdown v-if="selectedInterval === 'custom'" v-model="selectedWeekdays" @input="updateCron" :label="$strings.LabelWeekdaysToRun" :items="weekdays" /> <div v-if="(selectedWeekdays.length && selectedInterval === 'custom') || selectedInterval === 'daily'" class="flex items-center py-2"> <ui-text-input-with-label v-model="selectedHour" @input="updateCron" @blur="hourBlur" type="number" :label="$strings.LabelHour" class="max-w-20" /> <p class="text-xl px-2 mt-4">:</p> <ui-text-input-with-label v-model="selectedMinute" @input="updateCron" @blur="minuteBlur" type="number" :label="$strings.LabelMinute" class="max-w-20" /> </div> <div v-if="description" class="w-full bg-primary bg-opacity-75 rounded-xl p-2 md:p-4 text-center mt-2"> <p class="text-base md:text-lg text-gray-200" v-html="description" /> </div> </template> <template v-else> <p class="px-1 text-sm font-semibold">{{ $strings.LabelCronExpression }}</p> <ui-text-input ref="customExpressionInput" v-model="customCronExpression" @blur="cronExpressionBlur" :padding-y="2" text-center class="w-full text-2xl md:text-4xl -tracking-widest mb-4 font-mono" /> <div class="flex items-center justify-center"> <widgets-loading-spinner v-if="isValidating" class="mr-2" /> <span v-else class="material-symbols-outlined mr-2 text-xl" :class="isValid ? 'text-success' : 'text-error'">{{ isValid ? 'check_circle_outline' : 'error_outline' }}</span> <p v-if="isValidating" class="text-gray-300 text-base md:text-lg text-center">{{ $strings.MessageCheckingCron }}</p> <p v-else-if="customCronError" class="text-error text-base md:text-lg text-center">{{ customCronError }}</p> <p v-else class="text-success text-base md:text-lg text-center">{{ $strings.MessageValidCronExpression }}</p> </div> </template> <div v-if="cronExpression && isValid" class="flex items-center justify-center text-yellow-400 mt-2"> <span class="material-symbols-outlined mr-2 text-xl">event</span> <p>{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}</p> </div> </div> </div> </template> <script> export default { props: { value: { type: String, default: null } }, data() { return { selectedInterval: 'custom', showAdvancedView: false, selectedHour: 0, selectedMinute: 0, selectedWeekdays: [], cronExpression: '0 0 * * *', customCronExpression: '0 0 * * *', customCronError: '', isValidating: false, validatedCron: null, isValid: true } }, watch: { value: { immediate: true, handler(newVal) { this.init() } } }, computed: { minuteIsValid() { return !(isNaN(this.selectedMinute) || this.selectedMinute === '' || this.selectedMinute < 0 || this.selectedMinute > 59) }, hourIsValid() { return !(isNaN(this.selectedHour) || this.selectedHour === '' || this.selectedHour < 0 || this.selectedHour > 23) }, nextRun() { if (!this.cronExpression) return '' const parsed = this.$getNextScheduledDate(this.cronExpression) return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || '' }, description() { if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return '' if (!this.hourIsValid) { return `<span class="text-error">Invalid hour must be 0-23 | ${this.selectedHour < 0 || this.selectedHour > 23}</span>` } if (!this.minuteIsValid) { return `<span class="text-error">Invalid minute must be 0-59</span>` } var description = 'Run every ' var weekdayTexts = '' if (this.selectedWeekdays.length === 7 || this.selectedInterval === 'daily') { weekdayTexts = 'day' } else { weekdayTexts = this.selectedWeekdays .map((weekday) => { return this.weekdays.find((w) => w.value === weekday).text }) .join(', ') } description += `<span class="font-bold text-white">${weekdayTexts}</span>` const hourString = this.selectedHour.toString() const minuteString = this.selectedMinute.toString().padStart(2, '0') description += ` at <span class="font-bold text-white">${hourString}:${minuteString}</span>` return description }, intervalOptions() { return [ { text: this.$strings.LabelIntervalCustomDailyWeekly, value: 'custom' }, { text: this.$strings.LabelIntervalEveryDay, value: 'daily' }, { text: this.$strings.LabelIntervalEvery12Hours, value: '0 */12 * * *' }, { text: this.$strings.LabelIntervalEvery6Hours, value: '0 */6 * * *' }, { text: this.$strings.LabelIntervalEvery2Hours, value: '0 */2 * * *' }, { text: this.$strings.LabelIntervalEveryHour, value: '0 * * * *' }, { text: this.$strings.LabelIntervalEvery30Minutes, value: '*/30 * * * *' }, { text: this.$strings.LabelIntervalEvery15Minutes, value: '*/15 * * * *' } ] }, weekdays() { return [ { text: this.$formatJsDate(new Date(2023, 0, 1), 'EEEE'), value: 0 }, { text: this.$formatJsDate(new Date(2023, 0, 2), 'EEEE'), value: 1 }, { text: this.$formatJsDate(new Date(2023, 0, 3), 'EEEE'), value: 2 }, { text: this.$formatJsDate(new Date(2023, 0, 4), 'EEEE'), value: 3 }, { text: this.$formatJsDate(new Date(2023, 0, 5), 'EEEE'), value: 4 }, { text: this.$formatJsDate(new Date(2023, 0, 6), 'EEEE'), value: 5 }, { text: this.$formatJsDate(new Date(2023, 0, 7), 'EEEE'), value: 6 } ] } }, methods: { checkBlurExpressionInput() { if (!this.showAdvancedView || !this.$refs.customExpressionInput) return false if (this.$refs.customExpressionInput.isFocused) { this.$refs.customExpressionInput.blur() return true } return false }, updateCron() { if (this.selectedInterval === 'custom') { if (!this.minuteIsValid || !this.hourIsValid || !this.selectedWeekdays.length) { this.cronExpression = null return } this.selectedWeekdays.sort() const daysOfWeekPiece = this.selectedWeekdays.length === 7 ? '*' : this.selectedWeekdays.join(',') this.cronExpression = `${this.selectedMinute} ${this.selectedHour} * * ${daysOfWeekPiece}` } else if (this.selectedInterval === 'daily') { if (!this.minuteIsValid || !this.hourIsValid) { this.cronExpression = null return } this.cronExpression = `${this.selectedMinute} ${this.selectedHour} * * *` } else { this.cronExpression = this.selectedInterval } this.customCronExpression = this.cronExpression this.validatedCron = this.cronExpression this.isValid = true this.customCronError = '' this.$emit('input', this.cronExpression) }, minuteBlur() { const v = this.selectedMinute if (v === '' || v === null || isNaN(v) || v < 0) { this.selectedMinute = 0 } else if (v > 59) { this.selectedMinute = 59 } else { this.selectedMinute = Number(v) } this.updateCron() }, hourBlur() { const v = this.selectedHour if (v === '' || v === null || isNaN(v) || v < 0) { this.selectedHour = 0 } else if (v > 23) { this.selectedHour = 23 } else { this.selectedHour = Number(v) } this.updateCron() }, async cronExpressionBlur() { this.customCronError = '' if (!this.customCronExpression || this.customCronExpression.split(' ').length !== 5) { this.customCronError = 'Invalid cron expression' this.isValid = false return } if (this.customCronExpression !== this.cronExpression) { this.selectedWeekdays = [] this.selectedHour = 0 this.selectedMinute = 0 this.cronExpression = this.customCronExpression } if (!this.validatedCron || this.validatedCron !== this.cronExpression) { const validationPayload = await this.validateCron() this.isValid = validationPayload.isValid this.validatedCron = this.cronExpression this.customCronError = validationPayload.error || '' } if (this.isValid) { this.$emit('input', this.cronExpression) } }, validateCron() { this.isValidating = true return this.$axios .$post('/api/validate-cron', { expression: this.customCronExpression }) .then(() => { this.isValidating = false return { isValid: true } }) .catch((error) => { console.error('Invalid cron', error) var errMsg = error.response ? error.response.data || '' : '' this.isValidating = false return { isValid: false, error: errMsg || 'Invalid cron expression' } }) }, init() { this.selectedInterval = 'custom' this.selectedHour = 0 this.selectedMinute = 0 this.selectedWeekdays = [] if (!this.value) return const pieces = this.value.split(' ') if (pieces.length !== 5) { console.error('Invalid cron expression input', this.value) return } const intervalMatch = this.intervalOptions.find((opt) => opt.value === this.value) if (intervalMatch) { this.selectedInterval = this.value } else { var isCustomCron = false if (isNaN(pieces[0]) || isNaN(pieces[1])) { isCustomCron = true } else if (pieces[2] !== '*' || pieces[3] !== '*') { isCustomCron = true } else if (pieces[4] !== '*' && pieces[4].split(',').some((num) => isNaN(num))) { isCustomCron = true } if (isCustomCron) { this.showAdvancedView = true } else { if (pieces[4] === '*') this.selectedInterval = 'daily' this.selectedWeekdays = pieces[4] === '*' ? [0, 1, 2, 3, 4, 5, 6] : pieces[4].split(',').map((num) => Number(num)) this.selectedHour = pieces[1] this.selectedMinute = pieces[0] } } this.cronExpression = this.value this.customCronExpression = this.value } }, mounted() { this.init() } } </script>