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 @@
-
';
-
- 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 (
+
+ );
+};
+
+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}
+ }
+ onClick={showAddTriggerDialog}
+ >{globalize.translate('ButtonAddScheduledTaskTrigger')}
+
+
+
+
+
+ );
+};
+
+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
}) => (
-