From 524d1b6574d663e25de276ec35bedaf9b4c6eb90 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:45:48 +0300 Subject: [PATCH] Migrate tasks edit page to react --- .../scheduledtasks/scheduledtask.html | 84 ------- .../scheduledtasks/scheduledtask.js | 236 ------------------ .../api/useStartTask.ts | 0 .../api/useStopTask.ts | 0 .../dashboard/features/tasks/api/useTask.ts | 35 +++ .../{scheduledtasks => tasks}/api/useTasks.ts | 0 .../features/tasks/api/useUpdateTask.ts | 22 ++ .../tasks/components/NewTriggerForm.tsx | 169 +++++++++++++ .../components/Task.tsx | 0 .../components/TaskLastRan.tsx | 0 .../components/TaskProgress.tsx | 0 .../tasks/components/TaskTriggerCell.tsx | 34 +++ .../components/Tasks.tsx | 0 .../types/taskProps.ts | 0 .../dashboard/features/tasks/utils/edit.ts | 64 +++++ .../{scheduledtasks => tasks}/utils/tasks.ts | 0 src/apps/dashboard/routes/_asyncRoutes.ts | 1 + src/apps/dashboard/routes/_legacyRoutes.ts | 7 - src/apps/dashboard/routes/tasks/edit.tsx | 173 +++++++++++++ src/apps/dashboard/routes/tasks/index.tsx | 6 +- 20 files changed, 501 insertions(+), 330 deletions(-) delete mode 100644 src/apps/dashboard/controllers/scheduledtasks/scheduledtask.html delete mode 100644 src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js rename src/apps/dashboard/features/{scheduledtasks => tasks}/api/useStartTask.ts (100%) rename src/apps/dashboard/features/{scheduledtasks => tasks}/api/useStopTask.ts (100%) create mode 100644 src/apps/dashboard/features/tasks/api/useTask.ts rename src/apps/dashboard/features/{scheduledtasks => tasks}/api/useTasks.ts (100%) create mode 100644 src/apps/dashboard/features/tasks/api/useUpdateTask.ts create mode 100644 src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx rename src/apps/dashboard/features/{scheduledtasks => tasks}/components/Task.tsx (100%) rename src/apps/dashboard/features/{scheduledtasks => tasks}/components/TaskLastRan.tsx (100%) rename src/apps/dashboard/features/{scheduledtasks => tasks}/components/TaskProgress.tsx (100%) create mode 100644 src/apps/dashboard/features/tasks/components/TaskTriggerCell.tsx rename src/apps/dashboard/features/{scheduledtasks => tasks}/components/Tasks.tsx (100%) rename src/apps/dashboard/features/{scheduledtasks => tasks}/types/taskProps.ts (100%) create mode 100644 src/apps/dashboard/features/tasks/utils/edit.ts rename src/apps/dashboard/features/{scheduledtasks => tasks}/utils/tasks.ts (100%) create mode 100644 src/apps/dashboard/routes/tasks/edit.tsx 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..98800420b4 --- /dev/null +++ b/src/apps/dashboard/features/tasks/api/useTask.ts @@ -0,0 +1,35 @@ +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'; + +export const QUERY_KEY = 'Task'; + +const fetchTask = async ( + api: Api, + params: ScheduledTasksApiGetTaskRequest, + options?: AxiosRequestConfig +) => { + if (!api) { + console.warn('[fetchTasks] No API instance available'); + return; + } + + 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 100% rename from src/apps/dashboard/features/scheduledtasks/api/useTasks.ts rename to src/apps/dashboard/features/tasks/api/useTasks.ts 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..8f34c0df38 --- /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: () => { + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY ] + }); + } + }); +}; 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..53404885fc --- /dev/null +++ b/src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx @@ -0,0 +1,169 @@ +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 { getTimeOfDayOptions } from '../utils/edit'; +import { useLocale } from 'hooks/useLocale'; + +type IProps = { + open: boolean, + title: string, + onClose?: () => void, + onSubmit?: (trigger: TaskTriggerInfo) => void +}; + +const NewTriggerForm: FunctionComponent = ({ open, title, onClose, onSubmit }: IProps) => { + const { dateFnsLocale } = useLocale(); + const [triggerType, setTriggerType] = useState('DailyTrigger'); + + const timeOfDayOptions = useMemo(() => getTimeOfDayOptions(dateFnsLocale), [dateFnsLocale]); + + const onTriggerTypeChange = useCallback((e: React.ChangeEvent) => { + setTriggerType(e.target.value); + }, []); + + return ( + ) => { + 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()) * 3600000 * 1e4; + } + + if (onSubmit) { + onSubmit(trigger); + } + } + }} + > + {title} + + + + + {globalize.translate('OptionDaily')} + {globalize.translate('OptionWeekly')} + {globalize.translate('OptionOnInterval')} + {globalize.translate('OnApplicationStartup')} + + + {triggerType == 'WeeklyTrigger' && ( + + {globalize.translate('Sunday')} + {globalize.translate('Monday')} + {globalize.translate('Tuesday')} + {globalize.translate('Wednesday')} + {globalize.translate('Thursday')} + {globalize.translate('Friday')} + {globalize.translate('Saturday')} + + )} + + {['DailyTrigger', 'WeeklyTrigger'].includes(triggerType) && ( + + {timeOfDayOptions.map((option) => { + return {option.name}; + })} + + )} + + {triggerType == 'IntervalTrigger' && ( + + 15 minutes + 30 minutes + 45 minutes + 1 hour + 2 hours + 3 hours + 4 hours + 6 hours + 8 hours + 12 hours + 24 hours + + )} + + + + + + + + + + + ); +}; + +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 100% rename from src/apps/dashboard/features/scheduledtasks/components/Task.tsx rename to src/apps/dashboard/features/tasks/components/Task.tsx 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/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..2637465710 --- /dev/null +++ b/src/apps/dashboard/features/tasks/utils/edit.ts @@ -0,0 +1,64 @@ +import type { TaskTriggerInfo } from '@jellyfin/sdk/lib/generated-client/models/task-trigger-info'; +import { format, Locale, parse } from 'date-fns'; +import globalize from 'lib/globalize'; + +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; +} + +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..d66c4ed13d 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/edit', 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/edit.tsx b/src/apps/dashboard/routes/tasks/edit.tsx new file mode 100644 index 0000000000..5120f80f05 --- /dev/null +++ b/src/apps/dashboard/routes/tasks/edit.tsx @@ -0,0 +1,173 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Page from 'components/Page'; +import { useSearchParams } 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'; + +const TaskEdit = () => { + const [ searchParams ] = useSearchParams(); + const updateTask = useUpdateTask(); + const taskId = searchParams.get('id'); + 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 onNewTriggerSubmit = 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} + + + + + + + ); +}; + +export default TaskEdit; 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';