diff --git a/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.html b/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.html deleted file mode 100644 index a2352e6470..0000000000 --- a/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.html +++ /dev/null @@ -1,84 +0,0 @@ -
-
-
-
-
-

-
-

-
- -
-
-

${HeaderTaskTriggers}

- -
-
-
-
-
-
-
-
-

${ButtonAddScheduledTaskTrigger}

-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - -
-
-
-
-
diff --git a/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js b/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js deleted file mode 100644 index 7d46af171a..0000000000 --- a/src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js +++ /dev/null @@ -1,236 +0,0 @@ -import loading from 'components/loading/loading'; -import datetime from 'scripts/datetime'; -import dom from 'scripts/dom'; -import globalize from 'lib/globalize'; -import 'elements/emby-input/emby-input'; -import 'elements/emby-button/emby-button'; -import 'elements/emby-select/emby-select'; -import confirm from 'components/confirm/confirm'; -import { getParameterByName } from 'utils/url.ts'; - -function fillTimeOfDay(select) { - const options = []; - - for (let i = 0; i < 86400000; i += 900000) { - options.push({ - name: ScheduledTaskPage.getDisplayTime(i * 10000), - value: i * 10000 - }); - } - - select.innerHTML = options.map(function (o) { - return ''; - }).join(''); -} - -const ScheduledTaskPage = { - refreshScheduledTask: function (view) { - loading.show(); - const id = getParameterByName('id'); - ApiClient.getScheduledTask(id).then(function (task) { - ScheduledTaskPage.loadScheduledTask(view, task); - }); - }, - loadScheduledTask: function (view, task) { - view.querySelector('.taskName').innerHTML = task.Name; - view.querySelector('#pTaskDescription').innerHTML = task.Description; - - import('components/listview/listview.scss').then(() => { - ScheduledTaskPage.loadTaskTriggers(view, task); - }); - - loading.hide(); - }, - loadTaskTriggers: function (context, task) { - let html = ''; - html += '
'; - - for (let i = 0, length = task.Triggers.length; i < length; i++) { - const trigger = task.Triggers[i]; - - html += '
'; - html += ''; - if (trigger.MaxRuntimeTicks) { - html += '
'; - } else { - html += '
'; - } - html += "
" + ScheduledTaskPage.getTriggerFriendlyName(trigger) + '
'; - if (trigger.MaxRuntimeTicks) { - html += '
'; - const hours = trigger.MaxRuntimeTicks / 36e9; - if (hours == 1) { - html += globalize.translate('ValueTimeLimitSingleHour'); - } else { - html += globalize.translate('ValueTimeLimitMultiHour', hours); - } - html += '
'; - } - - html += '
'; - html += ''; - html += '
'; - } - - html += '
'; - context.querySelector('.taskTriggers').innerHTML = html; - }, - // TODO: Replace this mess with date-fns and remove datetime completely - getTriggerFriendlyName: function (trigger) { - if (trigger.Type == 'DailyTrigger') { - return globalize.translate('DailyAt', ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks)); - } - - if (trigger.Type == 'WeeklyTrigger') { - // TODO: The day of week isn't localised as well - return globalize.translate('WeeklyAt', trigger.DayOfWeek, ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks)); - } - - if (trigger.Type == 'SystemEventTrigger' && trigger.SystemEvent == 'WakeFromSleep') { - return globalize.translate('OnWakeFromSleep'); - } - - if (trigger.Type == 'IntervalTrigger') { - const hours = trigger.IntervalTicks / 36e9; - - if (hours == 0.25) { - return globalize.translate('EveryXMinutes', '15'); - } - if (hours == 0.5) { - return globalize.translate('EveryXMinutes', '30'); - } - if (hours == 0.75) { - return globalize.translate('EveryXMinutes', '45'); - } - if (hours == 1) { - return globalize.translate('EveryHour'); - } - - return globalize.translate('EveryXHours', hours); - } - - if (trigger.Type == 'StartupTrigger') { - return globalize.translate('OnApplicationStartup'); - } - - return trigger.Type; - }, - getDisplayTime: function (ticks) { - const ms = ticks / 1e4; - const now = new Date(); - now.setHours(0, 0, 0, 0); - now.setTime(now.getTime() + ms); - return datetime.getDisplayTime(now); - }, - showAddTriggerPopup: function (view) { - view.querySelector('#selectTriggerType').value = 'DailyTrigger'; - view.querySelector('#selectTriggerType').dispatchEvent(new CustomEvent('change', {})); - view.querySelector('#popupAddTrigger').classList.remove('hide'); - }, - confirmDeleteTrigger: function (view, index) { - confirm(globalize.translate('MessageDeleteTaskTrigger'), globalize.translate('HeaderDeleteTaskTrigger')).then(function () { - ScheduledTaskPage.deleteTrigger(view, index); - }); - }, - deleteTrigger: function (view, index) { - loading.show(); - const id = getParameterByName('id'); - ApiClient.getScheduledTask(id).then(function (task) { - task.Triggers.splice(index, 1); - ApiClient.updateScheduledTaskTriggers(task.Id, task.Triggers).then(function () { - ScheduledTaskPage.refreshScheduledTask(view); - }); - }); - }, - refreshTriggerFields: function (page, triggerType) { - if (triggerType == 'DailyTrigger') { - page.querySelector('#fldTimeOfDay').classList.remove('hide'); - page.querySelector('#fldDayOfWeek').classList.add('hide'); - page.querySelector('#fldSelectSystemEvent').classList.add('hide'); - page.querySelector('#fldSelectInterval').classList.add('hide'); - page.querySelector('#selectTimeOfDay').setAttribute('required', 'required'); - } else if (triggerType == 'WeeklyTrigger') { - page.querySelector('#fldTimeOfDay').classList.remove('hide'); - page.querySelector('#fldDayOfWeek').classList.remove('hide'); - page.querySelector('#fldSelectSystemEvent').classList.add('hide'); - page.querySelector('#fldSelectInterval').classList.add('hide'); - page.querySelector('#selectTimeOfDay').setAttribute('required', 'required'); - } else if (triggerType == 'SystemEventTrigger') { - page.querySelector('#fldTimeOfDay').classList.add('hide'); - page.querySelector('#fldDayOfWeek').classList.add('hide'); - page.querySelector('#fldSelectSystemEvent').classList.remove('hide'); - page.querySelector('#fldSelectInterval').classList.add('hide'); - page.querySelector('#selectTimeOfDay').removeAttribute('required'); - } else if (triggerType == 'IntervalTrigger') { - page.querySelector('#fldTimeOfDay').classList.add('hide'); - page.querySelector('#fldDayOfWeek').classList.add('hide'); - page.querySelector('#fldSelectSystemEvent').classList.add('hide'); - page.querySelector('#fldSelectInterval').classList.remove('hide'); - page.querySelector('#selectTimeOfDay').removeAttribute('required'); - } else if (triggerType == 'StartupTrigger') { - page.querySelector('#fldTimeOfDay').classList.add('hide'); - page.querySelector('#fldDayOfWeek').classList.add('hide'); - page.querySelector('#fldSelectSystemEvent').classList.add('hide'); - page.querySelector('#fldSelectInterval').classList.add('hide'); - page.querySelector('#selectTimeOfDay').removeAttribute('required'); - } - }, - getTriggerToAdd: function (page) { - const trigger = { - Type: page.querySelector('#selectTriggerType').value - }; - - if (trigger.Type == 'DailyTrigger') { - trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value; - } else if (trigger.Type == 'WeeklyTrigger') { - trigger.DayOfWeek = page.querySelector('#selectDayOfWeek').value; - trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value; - } else if (trigger.Type == 'SystemEventTrigger') { - trigger.SystemEvent = page.querySelector('#selectSystemEvent').value; - } else if (trigger.Type == 'IntervalTrigger') { - trigger.IntervalTicks = page.querySelector('#selectInterval').value; - } - - let timeLimit = page.querySelector('#txtTimeLimit').value || '0'; - timeLimit = parseFloat(timeLimit) * 3600000; - - trigger.MaxRuntimeTicks = timeLimit * 1e4 || null; - - return trigger; - } -}; -export default function (view) { - function onSubmit(e) { - loading.show(); - const id = getParameterByName('id'); - ApiClient.getScheduledTask(id).then(function (task) { - task.Triggers.push(ScheduledTaskPage.getTriggerToAdd(view)); - ApiClient.updateScheduledTaskTriggers(task.Id, task.Triggers).then(function () { - document.querySelector('#popupAddTrigger').classList.add('hide'); - ScheduledTaskPage.refreshScheduledTask(view); - }); - }); - e.preventDefault(); - } - - view.querySelector('.addTriggerForm').addEventListener('submit', onSubmit); - fillTimeOfDay(view.querySelector('#selectTimeOfDay')); - view.querySelector('#popupAddTrigger').parentNode.trigger(new Event('create')); - view.querySelector('.selectTriggerType').addEventListener('change', function () { - ScheduledTaskPage.refreshTriggerFields(view, this.value); - }); - view.querySelector('.btnAddTrigger').addEventListener('click', function () { - ScheduledTaskPage.showAddTriggerPopup(view); - }); - view.addEventListener('click', function (e) { - const btnDeleteTrigger = dom.parentWithClass(e.target, 'btnDeleteTrigger'); - - if (btnDeleteTrigger) { - ScheduledTaskPage.confirmDeleteTrigger(view, parseInt(btnDeleteTrigger.getAttribute('data-index'), 10)); - } - }); - view.addEventListener('viewshow', function () { - ScheduledTaskPage.refreshScheduledTask(view); - }); -} - diff --git a/src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts b/src/apps/dashboard/features/tasks/api/useStartTask.ts similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/api/useStartTask.ts rename to src/apps/dashboard/features/tasks/api/useStartTask.ts diff --git a/src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts b/src/apps/dashboard/features/tasks/api/useStopTask.ts similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/api/useStopTask.ts rename to src/apps/dashboard/features/tasks/api/useStopTask.ts diff --git a/src/apps/dashboard/features/tasks/api/useTask.ts b/src/apps/dashboard/features/tasks/api/useTask.ts new file mode 100644 index 0000000000..9df4adaefa --- /dev/null +++ b/src/apps/dashboard/features/tasks/api/useTask.ts @@ -0,0 +1,29 @@ +import type { ScheduledTasksApiGetTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; +import type { AxiosRequestConfig } from 'axios'; +import type { Api } from '@jellyfin/sdk'; +import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'hooks/useApi'; +import { QUERY_KEY } from './useTasks'; + +const fetchTask = async ( + api: Api, + params: ScheduledTasksApiGetTaskRequest, + options?: AxiosRequestConfig +) => { + const response = await getScheduledTasksApi(api).getTask(params, options); + + return response.data; +}; + +export const useTask = (params: ScheduledTasksApiGetTaskRequest) => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ QUERY_KEY, params.taskId ], + queryFn: ({ signal }) => + fetchTask(api!, params, { signal }), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/scheduledtasks/api/useTasks.ts b/src/apps/dashboard/features/tasks/api/useTasks.ts similarity index 96% rename from src/apps/dashboard/features/scheduledtasks/api/useTasks.ts rename to src/apps/dashboard/features/tasks/api/useTasks.ts index 928ec4d639..7a4b6e1c4d 100644 --- a/src/apps/dashboard/features/scheduledtasks/api/useTasks.ts +++ b/src/apps/dashboard/features/tasks/api/useTasks.ts @@ -22,7 +22,7 @@ export const useTasks = (params?: ScheduledTasksApiGetTasksRequest) => { const { api } = useApi(); return useQuery({ - queryKey: [QUERY_KEY], + queryKey: [ QUERY_KEY ], queryFn: ({ signal }) => fetchTasks(api!, params, { signal }), enabled: !!api diff --git a/src/apps/dashboard/features/tasks/api/useUpdateTask.ts b/src/apps/dashboard/features/tasks/api/useUpdateTask.ts new file mode 100644 index 0000000000..dd0e8ac72b --- /dev/null +++ b/src/apps/dashboard/features/tasks/api/useUpdateTask.ts @@ -0,0 +1,22 @@ +import { ScheduledTasksApiUpdateTaskRequest } from '@jellyfin/sdk/lib/generated-client/api/scheduled-tasks-api'; +import { getScheduledTasksApi } from '@jellyfin/sdk/lib/utils/api/scheduled-tasks-api'; +import { useMutation } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import { queryClient } from 'utils/query/queryClient'; +import { QUERY_KEY } from './useTasks'; + +export const useUpdateTask = () => { + const { api } = useApi(); + + return useMutation({ + mutationFn: (params: ScheduledTasksApiUpdateTaskRequest) => ( + getScheduledTasksApi(api!) + .updateTask(params) + ), + onSuccess: (_data, params) => { + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY, params.taskId ] + }); + } + }); +}; diff --git a/src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx b/src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx new file mode 100644 index 0000000000..4cec4b0474 --- /dev/null +++ b/src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx @@ -0,0 +1,171 @@ +import React, { FunctionComponent, useCallback, useMemo, useState } from 'react'; +import Dialog from '@mui/material/Dialog'; +import Button from '@mui/material/Button'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import MenuItem from '@mui/material/MenuItem'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; +import { TaskTriggerInfoType } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info-type'; +import { DayOfWeek } from '@jellyfin/sdk/lib/generated-client/models/day-of-week'; +import globalize from 'lib/globalize'; +import { getIntervalOptions, getTimeOfDayOptions } from '../utils/edit'; +import { useLocale } from 'hooks/useLocale'; + +type IProps = { + open: boolean, + title: string, + onClose?: () => void, + onAdd?: (trigger: TaskTriggerInfo) => void +}; + +const NewTriggerForm: FunctionComponent = ({ open, title, onClose, onAdd }: IProps) => { + const { dateFnsLocale } = useLocale(); + const [triggerType, setTriggerType] = useState(TaskTriggerInfoType.DailyTrigger); + + const timeOfDayOptions = useMemo(() => getTimeOfDayOptions(dateFnsLocale), [dateFnsLocale]); + const intervalOptions = useMemo(() => getIntervalOptions(dateFnsLocale), [dateFnsLocale]); + + const onTriggerTypeChange = useCallback((e: React.ChangeEvent) => { + setTriggerType(e.target.value as TaskTriggerInfoType); + }, []); + + const onSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + + const formData = new FormData(e.currentTarget); + const data = Object.fromEntries(formData.entries()); + const trigger: TaskTriggerInfo = { + Type: data.TriggerType.toString() as TaskTriggerInfoType + }; + + if (trigger.Type == TaskTriggerInfoType.WeeklyTrigger) { + trigger.DayOfWeek = data.DayOfWeek.toString() as DayOfWeek; + } + + if (trigger.Type == TaskTriggerInfoType.DailyTrigger || trigger.Type == TaskTriggerInfoType.WeeklyTrigger) { + trigger.TimeOfDayTicks = parseInt(data.TimeOfDay.toString(), 10); + } + + if (trigger.Type == TaskTriggerInfoType.IntervalTrigger) { + trigger.IntervalTicks = parseInt(data.Interval.toString(), 10); + } + + if (data.TimeLimit.toString()) { + trigger.MaxRuntimeTicks = parseFloat(data.TimeLimit.toString()) * 36e9; + } + + if (onAdd) { + onAdd(trigger); + } + }, [ onAdd ]); + + return ( + + {title} + + + + + {globalize.translate('OptionDaily')} + {globalize.translate('OptionWeekly')} + {globalize.translate('OptionOnInterval')} + {globalize.translate('OnApplicationStartup')} + + + {triggerType == TaskTriggerInfoType.WeeklyTrigger && ( + + {globalize.translate('Sunday')} + {globalize.translate('Monday')} + {globalize.translate('Tuesday')} + {globalize.translate('Wednesday')} + {globalize.translate('Thursday')} + {globalize.translate('Friday')} + {globalize.translate('Saturday')} + + )} + + {(triggerType == TaskTriggerInfoType.DailyTrigger || triggerType == TaskTriggerInfoType.WeeklyTrigger) && ( + + {timeOfDayOptions.map((option) => { + return {option.name}; + })} + + )} + + {triggerType == TaskTriggerInfoType.IntervalTrigger && ( + + {intervalOptions.map((option) => { + return {option.name}; + })} + + )} + + + + + + + + + + + ); +}; + +export default NewTriggerForm; diff --git a/src/apps/dashboard/features/scheduledtasks/components/Task.tsx b/src/apps/dashboard/features/tasks/components/Task.tsx similarity index 82% rename from src/apps/dashboard/features/scheduledtasks/components/Task.tsx rename to src/apps/dashboard/features/tasks/components/Task.tsx index 0c5fda280b..ef140c0cb3 100644 --- a/src/apps/dashboard/features/scheduledtasks/components/Task.tsx +++ b/src/apps/dashboard/features/tasks/components/Task.tsx @@ -2,11 +2,9 @@ import React, { FunctionComponent, useCallback } from 'react'; import ListItem from '@mui/material/ListItem'; import Avatar from '@mui/material/Avatar'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; -import ListItemButton from '@mui/material/ListItemButton'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import ListItemText from '@mui/material/ListItemText'; import Typography from '@mui/material/Typography'; -import Dashboard from 'utils/dashboard'; import { TaskProps } from '../types/taskProps'; import TaskProgress from './TaskProgress'; import TaskLastRan from './TaskLastRan'; @@ -15,18 +13,12 @@ import PlayArrow from '@mui/icons-material/PlayArrow'; import Stop from '@mui/icons-material/Stop'; import { useStartTask } from '../api/useStartTask'; import { useStopTask } from '../api/useStopTask'; +import ListItemLink from 'components/ListItemLink'; const Task: FunctionComponent = ({ task }: TaskProps) => { const startTask = useStartTask(); const stopTask = useStopTask(); - const navigateTaskEdit = useCallback(() => { - Dashboard.navigate(`/dashboard/tasks/edit?id=${task.Id}`) - .catch(err => { - console.error('[Task] failed to navigate to task edit page', err); - }); - }, [task]); - const handleStartTask = useCallback(() => { if (task.Id) { startTask.mutate({ taskId: task.Id }); @@ -48,7 +40,7 @@ const Task: FunctionComponent = ({ task }: TaskProps) => { } > - + @@ -59,7 +51,7 @@ const Task: FunctionComponent = ({ task }: TaskProps) => { secondary={task.State == 'Running' ? : } disableTypography /> - + ); }; diff --git a/src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx b/src/apps/dashboard/features/tasks/components/TaskLastRan.tsx similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/components/TaskLastRan.tsx rename to src/apps/dashboard/features/tasks/components/TaskLastRan.tsx diff --git a/src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx b/src/apps/dashboard/features/tasks/components/TaskProgress.tsx similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/components/TaskProgress.tsx rename to src/apps/dashboard/features/tasks/components/TaskProgress.tsx diff --git a/src/apps/dashboard/features/tasks/components/TaskTriggerCell.tsx b/src/apps/dashboard/features/tasks/components/TaskTriggerCell.tsx new file mode 100644 index 0000000000..9f4f8502c9 --- /dev/null +++ b/src/apps/dashboard/features/tasks/components/TaskTriggerCell.tsx @@ -0,0 +1,34 @@ +import React, { FC } from 'react'; +import type { MRT_Cell, MRT_RowData } from 'material-react-table'; +import { useLocale } from 'hooks/useLocale'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { getTriggerFriendlyName } from '../utils/edit'; +import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; +import globalize from 'lib/globalize'; + +interface CellProps { + cell: MRT_Cell +} + +const TaskTriggerCell: FC = ({ cell }) => { + const { dateFnsLocale } = useLocale(); + const trigger = cell.getValue(); + + const timeLimitHours = trigger.MaxRuntimeTicks ? trigger.MaxRuntimeTicks / 36e9 : false; + + return ( + + {getTriggerFriendlyName(trigger, dateFnsLocale)} + {timeLimitHours && ( + + {timeLimitHours == 1 ? + globalize.translate('ValueTimeLimitSingleHour') : + globalize.translate('ValueTimeLimitMultiHour', timeLimitHours)} + + )} + + ); +}; + +export default TaskTriggerCell; diff --git a/src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx b/src/apps/dashboard/features/tasks/components/Tasks.tsx similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/components/Tasks.tsx rename to src/apps/dashboard/features/tasks/components/Tasks.tsx diff --git a/src/apps/dashboard/features/tasks/constants/intervalDurations.ts b/src/apps/dashboard/features/tasks/constants/intervalDurations.ts new file mode 100644 index 0000000000..f90d0f0ddd --- /dev/null +++ b/src/apps/dashboard/features/tasks/constants/intervalDurations.ts @@ -0,0 +1,13 @@ +export const INTERVAL_DURATIONS: number[] = [ + 9000000000, // 15 minutes + 18000000000, // 30 minutes + 27000000000, // 45 minutes + 36000000000, // 1 hour + 72000000000, // 2 hours + 108000000000, // 3 hours + 144000000000, // 4 hours + 216000000000, // 6 hours + 288000000000, // 8 hours + 432000000000, // 12 hours + 864000000000 // 24 hours +]; diff --git a/src/apps/dashboard/features/scheduledtasks/types/taskProps.ts b/src/apps/dashboard/features/tasks/types/taskProps.ts similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/types/taskProps.ts rename to src/apps/dashboard/features/tasks/types/taskProps.ts diff --git a/src/apps/dashboard/features/tasks/utils/edit.ts b/src/apps/dashboard/features/tasks/utils/edit.ts new file mode 100644 index 0000000000..536146e9c5 --- /dev/null +++ b/src/apps/dashboard/features/tasks/utils/edit.ts @@ -0,0 +1,80 @@ +import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; +import { format, formatDistanceStrict, Locale, parse } from 'date-fns'; +import globalize from 'lib/globalize'; +import { INTERVAL_DURATIONS } from '../constants/intervalDurations'; + +function getDisplayTime(ticks: number, locale: Locale) { + const ms = ticks / 1e4; + const now = new Date(); + now.setHours(0, 0, 0, 0); + now.setTime(now.getTime() + ms); + return format(now, 'p', { locale: locale }); +} + +export function getTimeOfDayOptions(locale: Locale) { + const options = []; + + for (let i = 0; i < 86400000; i += 900000) { + options.push({ + name: getDisplayTime(i * 10000, locale), + value: i * 10000 + }); + } + + return options; +} + +export function getIntervalOptions(locale: Locale) { + const options = []; + + for (const ticksDuration of INTERVAL_DURATIONS) { + const durationMs = Math.floor(ticksDuration / 1e4); + const unit = durationMs < 36e5 ? 'minute' : 'hour'; + options.push({ + name: formatDistanceStrict(0, durationMs, { locale: locale, unit: unit }), + value: ticksDuration + }); + } + + return options; +} + +function getIntervalTriggerTime(ticks: number) { + const hours = ticks / 36e9; + + switch (hours) { + case 0.25: + return globalize.translate('EveryXMinutes', '15'); + case 0.5: + return globalize.translate('EveryXMinutes', '30'); + case 0.75: + return globalize.translate('EveryXMinutes', '45'); + case 1: + return globalize.translate('EveryHour'); + default: + return globalize.translate('EveryXHours', hours); + } +} + +function localizeDayOfWeek(dayOfWeek: string | null | undefined, locale: Locale) { + if (!dayOfWeek) return ''; + + const parsedDayOfWeek = parse(dayOfWeek, 'cccc', new Date()); + + return format(parsedDayOfWeek, 'cccc', { locale: locale }); +} + +export function getTriggerFriendlyName(trigger: TaskTriggerInfo, locale: Locale) { + switch (trigger.Type) { + case 'DailyTrigger': + return globalize.translate('DailyAt', getDisplayTime(trigger.TimeOfDayTicks || 0, locale)); + case 'WeeklyTrigger': + return globalize.translate('WeeklyAt', localizeDayOfWeek(trigger.DayOfWeek, locale), getDisplayTime(trigger.TimeOfDayTicks || 0, locale)); + case 'IntervalTrigger': + return getIntervalTriggerTime(trigger.IntervalTicks || 0); + case 'StartupTrigger': + return globalize.translate('OnApplicationStartup'); + default: + return trigger.Type; + } +} diff --git a/src/apps/dashboard/features/scheduledtasks/utils/tasks.ts b/src/apps/dashboard/features/tasks/utils/tasks.ts similarity index 100% rename from src/apps/dashboard/features/scheduledtasks/utils/tasks.ts rename to src/apps/dashboard/features/tasks/utils/tasks.ts diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 5a7398da39..4ba4d5e5e1 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -15,6 +15,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'playback/trickplay', type: AppType.Dashboard }, { path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard }, { path: 'tasks', type: AppType.Dashboard }, + { path: 'tasks/:id', page: 'tasks/task', type: AppType.Dashboard }, { path: 'users', type: AppType.Dashboard }, { path: 'users/access', type: AppType.Dashboard }, { path: 'users/add', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index b690b45440..6cd162d88d 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -93,12 +93,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'plugins/installed/index', view: 'plugins/installed/index.html' } - }, { - path: 'tasks/edit', - pageProps: { - appType: AppType.Dashboard, - controller: 'scheduledtasks/scheduledtask', - view: 'scheduledtasks/scheduledtask.html' - } } ]; diff --git a/src/apps/dashboard/routes/tasks/index.tsx b/src/apps/dashboard/routes/tasks/index.tsx index f742ef8ede..232fb34950 100644 --- a/src/apps/dashboard/routes/tasks/index.tsx +++ b/src/apps/dashboard/routes/tasks/index.tsx @@ -3,10 +3,10 @@ import Page from 'components/Page'; import globalize from 'lib/globalize'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; -import { QUERY_KEY, useTasks } from '../../features/scheduledtasks/api/useTasks'; -import { getCategories, getTasksByCategory } from '../../features/scheduledtasks/utils/tasks'; +import { QUERY_KEY, useTasks } from '../../features/tasks/api/useTasks'; +import { getCategories, getTasksByCategory } from '../../features/tasks/utils/tasks'; import Loading from 'components/loading/LoadingComponent'; -import Tasks from '../../features/scheduledtasks/components/Tasks'; +import Tasks from '../../features/tasks/components/Tasks'; import type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info'; import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type'; import serverNotifications from 'scripts/serverNotifications'; diff --git a/src/apps/dashboard/routes/tasks/task.tsx b/src/apps/dashboard/routes/tasks/task.tsx new file mode 100644 index 0000000000..2aa00f886c --- /dev/null +++ b/src/apps/dashboard/routes/tasks/task.tsx @@ -0,0 +1,172 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Page from 'components/Page'; +import { useParams } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import AddIcon from '@mui/icons-material/Add'; +import IconButton from '@mui/material/IconButton'; +import Tooltip from '@mui/material/Tooltip'; +import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'; +import Loading from 'components/loading/LoadingComponent'; +import { MRT_ColumnDef, MRT_Table, useMaterialReactTable } from 'material-react-table'; +import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; +import globalize from '../../../../lib/globalize'; +import { useTask } from 'apps/dashboard/features/tasks/api/useTask'; +import { useUpdateTask } from 'apps/dashboard/features/tasks/api/useUpdateTask'; +import ConfirmDialog from 'components/ConfirmDialog'; +import TaskTriggerCell from 'apps/dashboard/features/tasks/components/TaskTriggerCell'; +import NewTriggerForm from 'apps/dashboard/features/tasks/components/NewTriggerForm'; + +export const Component = () => { + const { id: taskId } = useParams(); + const updateTask = useUpdateTask(); + const { data: task, isLoading } = useTask({ taskId: taskId || '' }); + const [ isAddTriggerDialogOpen, setIsAddTriggerDialogOpen ] = useState(false); + const [ isRemoveConfirmOpen, setIsRemoveConfirmOpen ] = useState(false); + const [ pendingDeleteTrigger, setPendingDeleteTrigger ] = useState(null); + + const onCloseRemoveConfirmDialog = useCallback(() => { + setPendingDeleteTrigger(null); + setIsRemoveConfirmOpen(false); + }, []); + + const onDeleteTrigger = useCallback((trigger: TaskTriggerInfo | null | undefined) => { + if (trigger) { + setPendingDeleteTrigger(trigger); + setIsRemoveConfirmOpen(true); + } + }, []); + + const onConfirmDelete = useCallback(() => { + const triggersRemaining = task?.Triggers?.filter(trigger => trigger != pendingDeleteTrigger); + + if (task?.Id && triggersRemaining) { + updateTask.mutate({ + taskId: task.Id, + taskTriggerInfo: triggersRemaining + }); + setIsRemoveConfirmOpen(false); + } + }, [task, pendingDeleteTrigger, updateTask]); + + const showAddTriggerDialog = useCallback(() => { + setIsAddTriggerDialogOpen(true); + }, []); + + const handleNewTriggerDialogClose = useCallback(() => { + setIsAddTriggerDialogOpen(false); + }, []); + + const onNewTriggerAdd = useCallback((trigger: TaskTriggerInfo) => { + if (task?.Triggers && task?.Id) { + const triggers = [...task.Triggers, trigger]; + + updateTask.mutate({ + taskId: task.Id, + taskTriggerInfo: triggers + }); + setIsAddTriggerDialogOpen(false); + } + }, [task, updateTask]); + + const columns = useMemo[]>(() => [ + { + id: 'TriggerTime', + accessorFn: row => row, + Cell: TaskTriggerCell, + header: globalize.translate('LabelTime') + } + ], []); + + const table = useMaterialReactTable({ + columns, + data: task?.Triggers || [], + + enableSorting: false, + enableFilters: false, + enableColumnActions: false, + enablePagination: false, + + state: { + isLoading + }, + + muiTableContainerProps: { + sx: { + maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer + } + }, + + // Custom actions + enableRowActions: true, + positionActionsColumn: 'last', + displayColumnDefOptions: { + 'mrt-row-actions': { + header: '' + } + }, + renderRowActions: ({ row }) => { + return ( + + + onDeleteTrigger(row.original)} + > + + + + + ); + } + }); + + if (isLoading || !task) { + return ; + } + + return ( + + + + + + + {task.Name} + {task.Description} + + + + + + + ); +}; + +Component.displayName = 'TaskPage'; diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index 08efb3dd2b..bc3c31ced1 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -27,7 +27,7 @@ const ConfirmDialog: FC = ({ onConfirm, ...dialogProps }) => ( - + {title}