mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(jobs): show current job frequency in edit modal (#3008)
* fix(jobs): reset job schedule edit modal values when closed * feat(jobs): show job's current frequency * fix(jobs): reset job schedule edit modal values when cancelled * chore: rebase * refactor(jobs): use reducer instead of several react states * fix(jobs): reset modal state when opening instead of closing the modal This prevents the modal state from glitching when saving/closing the modal * feat(jobs): parse job schedule cron string unavailable locale will fallback to english
This commit is contained in:

committed by
GitHub

parent
611ceeb5f4
commit
99fc9a2da0
@@ -47,6 +47,7 @@
|
|||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.6",
|
||||||
"copy-to-clipboard": "3.3.2",
|
"copy-to-clipboard": "3.3.2",
|
||||||
"country-flag-icons": "1.5.5",
|
"country-flag-icons": "1.5.5",
|
||||||
|
"cronstrue": "^2.11.0",
|
||||||
"csurf": "1.11.0",
|
"csurf": "1.11.0",
|
||||||
"date-fns": "2.29.1",
|
"date-fns": "2.29.1",
|
||||||
"email-templates": "9.0.0",
|
"email-templates": "9.0.0",
|
||||||
|
@@ -14,6 +14,7 @@ interface ScheduledJob {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'process' | 'command';
|
type: 'process' | 'command';
|
||||||
interval: 'short' | 'long' | 'fixed';
|
interval: 'short' | 'long' | 'fixed';
|
||||||
|
cronSchedule: string;
|
||||||
running?: () => boolean;
|
running?: () => boolean;
|
||||||
cancelFn?: () => void;
|
cancelFn?: () => void;
|
||||||
}
|
}
|
||||||
@@ -29,6 +30,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Plex Recently Added Scan',
|
name: 'Plex Recently Added Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'short',
|
interval: 'short',
|
||||||
|
cronSchedule: jobs['plex-recently-added-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Recently Added Scan', {
|
logger.info('Starting scheduled job: Plex Recently Added Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
@@ -45,6 +47,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Plex Full Library Scan',
|
name: 'Plex Full Library Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'long',
|
interval: 'long',
|
||||||
|
cronSchedule: jobs['plex-full-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
@@ -61,6 +64,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Plex Watchlist Sync',
|
name: 'Plex Watchlist Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'long',
|
interval: 'long',
|
||||||
|
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
@@ -75,6 +79,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Radarr Scan',
|
name: 'Radarr Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'long',
|
interval: 'long',
|
||||||
|
cronSchedule: jobs['radarr-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
||||||
radarrScanner.run();
|
radarrScanner.run();
|
||||||
@@ -89,6 +94,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Sonarr Scan',
|
name: 'Sonarr Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'long',
|
interval: 'long',
|
||||||
|
cronSchedule: jobs['sonarr-scan'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
|
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
||||||
sonarrScanner.run();
|
sonarrScanner.run();
|
||||||
@@ -103,6 +109,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Download Sync',
|
name: 'Download Sync',
|
||||||
type: 'command',
|
type: 'command',
|
||||||
interval: 'fixed',
|
interval: 'fixed',
|
||||||
|
cronSchedule: jobs['download-sync'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
|
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
|
||||||
logger.debug('Starting scheduled job: Download Sync', {
|
logger.debug('Starting scheduled job: Download Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
@@ -117,6 +124,7 @@ export const startJobs = (): void => {
|
|||||||
name: 'Download Sync Reset',
|
name: 'Download Sync Reset',
|
||||||
type: 'command',
|
type: 'command',
|
||||||
interval: 'long',
|
interval: 'long',
|
||||||
|
cronSchedule: jobs['download-sync-reset'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
|
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Download Sync Reset', {
|
logger.info('Starting scheduled job: Download Sync Reset', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
|
@@ -433,6 +433,7 @@ settingsRoutes.get('/jobs', (_req, res) => {
|
|||||||
name: job.name,
|
name: job.name,
|
||||||
type: job.type,
|
type: job.type,
|
||||||
interval: job.interval,
|
interval: job.interval,
|
||||||
|
cronSchedule: job.cronSchedule,
|
||||||
nextExecutionTime: job.job.nextInvocation(),
|
nextExecutionTime: job.job.nextInvocation(),
|
||||||
running: job.running ? job.running() : false,
|
running: job.running ? job.running() : false,
|
||||||
}))
|
}))
|
||||||
@@ -453,6 +454,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
|
|||||||
name: scheduledJob.name,
|
name: scheduledJob.name,
|
||||||
type: scheduledJob.type,
|
type: scheduledJob.type,
|
||||||
interval: scheduledJob.interval,
|
interval: scheduledJob.interval,
|
||||||
|
cronSchedule: scheduledJob.cronSchedule,
|
||||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
@@ -478,6 +480,7 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
name: scheduledJob.name,
|
name: scheduledJob.name,
|
||||||
type: scheduledJob.type,
|
type: scheduledJob.type,
|
||||||
interval: scheduledJob.interval,
|
interval: scheduledJob.interval,
|
||||||
|
cronSchedule: scheduledJob.cronSchedule,
|
||||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
@@ -502,11 +505,14 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||||
settings.save();
|
settings.save();
|
||||||
|
|
||||||
|
scheduledJob.cronSchedule = req.body.schedule;
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
id: scheduledJob.id,
|
id: scheduledJob.id,
|
||||||
name: scheduledJob.name,
|
name: scheduledJob.name,
|
||||||
type: scheduledJob.type,
|
type: scheduledJob.type,
|
||||||
interval: scheduledJob.interval,
|
interval: scheduledJob.interval,
|
||||||
|
cronSchedule: scheduledJob.cronSchedule,
|
||||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
|
@@ -5,6 +5,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
|
|||||||
import Modal from '@app/components/Common/Modal';
|
import Modal from '@app/components/Common/Modal';
|
||||||
import PageTitle from '@app/components/Common/PageTitle';
|
import PageTitle from '@app/components/Common/PageTitle';
|
||||||
import Table from '@app/components/Common/Table';
|
import Table from '@app/components/Common/Table';
|
||||||
|
import useLocale from '@app/hooks/useLocale';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import { formatBytes } from '@app/utils/numberHelpers';
|
import { formatBytes } from '@app/utils/numberHelpers';
|
||||||
import { Transition } from '@headlessui/react';
|
import { Transition } from '@headlessui/react';
|
||||||
@@ -13,7 +14,8 @@ import { PencilIcon } from '@heroicons/react/solid';
|
|||||||
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
|
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
|
||||||
import type { JobId } from '@server/lib/settings';
|
import type { JobId } from '@server/lib/settings';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Fragment, useState } from 'react';
|
import cronstrue from 'cronstrue/i18n';
|
||||||
|
import { Fragment, useReducer, useState } from 'react';
|
||||||
import type { MessageDescriptor } from 'react-intl';
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
@@ -55,7 +57,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
|||||||
editJobSchedule: 'Modify Job',
|
editJobSchedule: 'Modify Job',
|
||||||
jobScheduleEditSaved: 'Job edited successfully!',
|
jobScheduleEditSaved: 'Job edited successfully!',
|
||||||
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
jobScheduleEditFailed: 'Something went wrong while saving the job.',
|
||||||
editJobSchedulePrompt: 'Frequency',
|
editJobScheduleCurrent: 'Current Frequency',
|
||||||
|
editJobSchedulePrompt: 'New Frequency',
|
||||||
editJobScheduleSelectorHours:
|
editJobScheduleSelectorHours:
|
||||||
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}',
|
||||||
editJobScheduleSelectorMinutes:
|
editJobScheduleSelectorMinutes:
|
||||||
@@ -67,12 +70,56 @@ interface Job {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'process' | 'command';
|
type: 'process' | 'command';
|
||||||
interval: 'short' | 'long' | 'fixed';
|
interval: 'short' | 'long' | 'fixed';
|
||||||
|
cronSchedule: string;
|
||||||
nextExecutionTime: string;
|
nextExecutionTime: string;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JobModalState = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
job?: Job;
|
||||||
|
scheduleHours: number;
|
||||||
|
scheduleMinutes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type JobModalAction =
|
||||||
|
| { type: 'set'; hours?: number; minutes?: number }
|
||||||
|
| {
|
||||||
|
type: 'close';
|
||||||
|
}
|
||||||
|
| { type: 'open'; job?: Job };
|
||||||
|
|
||||||
|
const jobModalReducer = (
|
||||||
|
state: JobModalState,
|
||||||
|
action: JobModalAction
|
||||||
|
): JobModalState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'close':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'open':
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
job: action.job,
|
||||||
|
scheduleHours: 1,
|
||||||
|
scheduleMinutes: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'set':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
scheduleHours: action.hours ?? state.scheduleHours,
|
||||||
|
scheduleMinutes: action.minutes ?? state.scheduleMinutes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsJobs = () => {
|
const SettingsJobs = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { locale } = useLocale();
|
||||||
const { addToast } = useToasts();
|
const { addToast } = useToasts();
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
@@ -88,15 +135,12 @@ const SettingsJobs = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [jobEditModal, setJobEditModal] = useState<{
|
const [jobModalState, dispatch] = useReducer(jobModalReducer, {
|
||||||
isOpen: boolean;
|
|
||||||
job?: Job;
|
|
||||||
}>({
|
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
|
scheduleHours: 1,
|
||||||
|
scheduleMinutes: 5,
|
||||||
});
|
});
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5);
|
|
||||||
const [jobScheduleHours, setJobScheduleHours] = useState(1);
|
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
@@ -146,10 +190,10 @@ const SettingsJobs = () => {
|
|||||||
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
|
const jobScheduleCron = ['0', '0', '*', '*', '*', '*'];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (jobEditModal.job?.interval === 'short') {
|
if (jobModalState.job?.interval === 'short') {
|
||||||
jobScheduleCron[1] = `*/${jobScheduleMinutes}`;
|
jobScheduleCron[1] = `*/${jobModalState.scheduleMinutes}`;
|
||||||
} else if (jobEditModal.job?.interval === 'long') {
|
} else if (jobModalState.job?.interval === 'long') {
|
||||||
jobScheduleCron[2] = `*/${jobScheduleHours}`;
|
jobScheduleCron[2] = `*/${jobModalState.scheduleHours}`;
|
||||||
} else {
|
} else {
|
||||||
// jobs with interval: fixed should not be editable
|
// jobs with interval: fixed should not be editable
|
||||||
throw new Error();
|
throw new Error();
|
||||||
@@ -157,16 +201,18 @@ const SettingsJobs = () => {
|
|||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`/api/v1/settings/jobs/${jobEditModal.job?.id}/schedule`,
|
`/api/v1/settings/jobs/${jobModalState.job.id}/schedule`,
|
||||||
{
|
{
|
||||||
schedule: jobScheduleCron.join(' '),
|
schedule: jobScheduleCron.join(' '),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
addToast(intl.formatMessage(messages.jobScheduleEditSaved), {
|
||||||
appearance: 'success',
|
appearance: 'success',
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
});
|
});
|
||||||
setJobEditModal({ isOpen: false });
|
|
||||||
|
dispatch({ type: 'close' });
|
||||||
revalidate();
|
revalidate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
addToast(intl.formatMessage(messages.jobScheduleEditFailed), {
|
addToast(intl.formatMessage(messages.jobScheduleEditFailed), {
|
||||||
@@ -194,7 +240,7 @@ const SettingsJobs = () => {
|
|||||||
leave="opacity-100 transition duration-300"
|
leave="opacity-100 transition duration-300"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
show={jobEditModal.isOpen}
|
show={jobModalState.isOpen}
|
||||||
>
|
>
|
||||||
<Modal
|
<Modal
|
||||||
title={intl.formatMessage(messages.editJobSchedule)}
|
title={intl.formatMessage(messages.editJobSchedule)}
|
||||||
@@ -203,24 +249,43 @@ const SettingsJobs = () => {
|
|||||||
? intl.formatMessage(globalMessages.saving)
|
? intl.formatMessage(globalMessages.saving)
|
||||||
: intl.formatMessage(globalMessages.save)
|
: intl.formatMessage(globalMessages.save)
|
||||||
}
|
}
|
||||||
onCancel={() => setJobEditModal({ isOpen: false })}
|
onCancel={() => dispatch({ type: 'close' })}
|
||||||
okDisabled={isSaving}
|
okDisabled={isSaving}
|
||||||
onOk={() => scheduleJob()}
|
onOk={() => scheduleJob()}
|
||||||
>
|
>
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<form>
|
<form className="mb-6">
|
||||||
<div className="form-row pb-6">
|
<div className="form-row">
|
||||||
|
<label className="text-label">
|
||||||
|
{intl.formatMessage(messages.editJobScheduleCurrent)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area mt-2 mb-1">
|
||||||
|
<div>
|
||||||
|
{jobModalState.job &&
|
||||||
|
cronstrue.toString(jobModalState.job.cronSchedule, {
|
||||||
|
locale,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{jobModalState.job?.cronSchedule}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
<label htmlFor="jobSchedule" className="text-label">
|
<label htmlFor="jobSchedule" className="text-label">
|
||||||
{intl.formatMessage(messages.editJobSchedulePrompt)}
|
{intl.formatMessage(messages.editJobSchedulePrompt)}
|
||||||
</label>
|
</label>
|
||||||
<div className="form-input-area">
|
<div className="form-input-area">
|
||||||
{jobEditModal.job?.interval === 'short' ? (
|
{jobModalState.job?.interval === 'short' ? (
|
||||||
<select
|
<select
|
||||||
name="jobScheduleMinutes"
|
name="jobScheduleMinutes"
|
||||||
className="inline"
|
className="inline"
|
||||||
value={jobScheduleMinutes}
|
value={jobModalState.scheduleMinutes}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setJobScheduleMinutes(Number(e.target.value))
|
dispatch({
|
||||||
|
type: 'set',
|
||||||
|
minutes: Number(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{[5, 10, 15, 20, 30, 60].map((v) => (
|
{[5, 10, 15, 20, 30, 60].map((v) => (
|
||||||
@@ -238,9 +303,12 @@ const SettingsJobs = () => {
|
|||||||
<select
|
<select
|
||||||
name="jobScheduleHours"
|
name="jobScheduleHours"
|
||||||
className="inline"
|
className="inline"
|
||||||
value={jobScheduleHours}
|
value={jobModalState.scheduleHours}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setJobScheduleHours(Number(e.target.value))
|
dispatch({
|
||||||
|
type: 'set',
|
||||||
|
hours: Number(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{[1, 2, 3, 4, 6, 8, 12, 24, 48, 72].map((v) => (
|
{[1, 2, 3, 4, 6, 8, 12, 24, 48, 72].map((v) => (
|
||||||
@@ -319,9 +387,7 @@ const SettingsJobs = () => {
|
|||||||
<Button
|
<Button
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
buttonType="warning"
|
buttonType="warning"
|
||||||
onClick={() =>
|
onClick={() => dispatch({ type: 'open', job })}
|
||||||
setJobEditModal({ isOpen: true, job: job })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<PencilIcon />
|
<PencilIcon />
|
||||||
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
||||||
|
@@ -639,7 +639,8 @@
|
|||||||
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
|
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
|
||||||
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
|
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
|
||||||
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
|
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
|
||||||
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Frequency",
|
"components.Settings.SettingsJobsCache.editJobScheduleCurrent": "Current Frequency",
|
||||||
|
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "New Frequency",
|
||||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
||||||
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||||
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
||||||
|
@@ -4847,6 +4847,11 @@ cron-parser@^3.5.0:
|
|||||||
is-nan "^1.3.2"
|
is-nan "^1.3.2"
|
||||||
luxon "^1.26.0"
|
luxon "^1.26.0"
|
||||||
|
|
||||||
|
cronstrue@^2.11.0:
|
||||||
|
version "2.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.11.0.tgz#18ff1b95a836b9b4e06854f796db2dc8fa98ce41"
|
||||||
|
integrity sha512-iIBCSis5yqtFYWtJAmNOiwDveFWWIn+8uV5UYuPHYu/Aeu5CSSJepSbaHMyfc+pPFgnsCcGzfPQEo7LSGmWbTg==
|
||||||
|
|
||||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
|
Reference in New Issue
Block a user