mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate tasks edit page to react
This commit is contained in:
parent
e80b890bd2
commit
524d1b6574
20 changed files with 501 additions and 330 deletions
|
@ -1,84 +0,0 @@
|
||||||
<div id="scheduledTaskPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage">
|
|
||||||
<div>
|
|
||||||
<div class="content-primary">
|
|
||||||
<div class="verticalSection">
|
|
||||||
<div class="sectionTitleContainer flex align-items-center">
|
|
||||||
<h2 class="sectionTitle taskName"></h2>
|
|
||||||
</div>
|
|
||||||
<p id="pTaskDescription"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="readOnlyContent">
|
|
||||||
<div>
|
|
||||||
<h2 style="vertical-align: middle; display: inline-block;">${HeaderTaskTriggers}</h2>
|
|
||||||
<button is="emby-button" type="button" class="fab fab-mini btnAddTrigger submit" style="margin-left: 1em;" title="${ButtonAddScheduledTaskTrigger}">
|
|
||||||
<span class="material-icons add" aria-hidden="true"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="taskTriggers"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%; z-index: 999999;">
|
|
||||||
<form class="addTriggerForm" style="padding:1em;">
|
|
||||||
<div class="ui-bar-a">
|
|
||||||
<h3>${ButtonAddScheduledTaskTrigger}</h3>
|
|
||||||
</div>
|
|
||||||
<div data-role="content">
|
|
||||||
<div class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectTriggerType" class="selectTriggerType" label="${LabelTriggerType}">
|
|
||||||
<option value="DailyTrigger">${OptionDaily}</option>
|
|
||||||
<option value="WeeklyTrigger">${OptionWeekly}</option>
|
|
||||||
<option value="IntervalTrigger">${OptionOnInterval}</option>
|
|
||||||
<option value="StartupTrigger">${OnApplicationStartup}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="fldDayOfWeek" class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectDayOfWeek" name="selectDayOfWeek" label="${LabelDay}">
|
|
||||||
<option value="Sunday">${Sunday}</option>
|
|
||||||
<option value="Monday">${Monday}</option>
|
|
||||||
<option value="Tuesday">${Tuesday}</option>
|
|
||||||
<option value="Wednesday">${Wednesday}</option>
|
|
||||||
<option value="Thursday">${Thursday}</option>
|
|
||||||
<option value="Friday">${Friday}</option>
|
|
||||||
<option value="Saturday">${Saturday}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="fldTimeOfDay" class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectTimeOfDay" label="${LabelTime}"></select>
|
|
||||||
</div>
|
|
||||||
<div id="fldSelectSystemEvent" class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectSystemEvent" name="selectSystemEvent" label="${LabelEvent}">
|
|
||||||
<option value="WakeFromSleep">${OptionWakeFromSleep}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div id="fldSelectInterval" class="selectContainer">
|
|
||||||
<select is="emby-select" id="selectInterval" label="${LabelEveryXMinutes}">
|
|
||||||
<option value="9000000000">15 minutes</option>
|
|
||||||
<option value="18000000000">30 minutes</option>
|
|
||||||
<option value="27000000000">45 minutes</option>
|
|
||||||
<option value="36000000000">1 hour</option>
|
|
||||||
<option value="72000000000">2 hours</option>
|
|
||||||
<option value="108000000000">3 hours</option>
|
|
||||||
<option value="144000000000">4 hours</option>
|
|
||||||
<option value="216000000000">6 hours</option>
|
|
||||||
<option value="288000000000">8 hours</option>
|
|
||||||
<option value="432000000000">12 hours</option>
|
|
||||||
<option value="864000000000">24 hours</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="inputContainer">
|
|
||||||
<input is="emby-input" id="txtTimeLimit" type="number" pattern="[0-9]*" min="1" step=".5" label="${LabelTimeLimitHours}" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button is="emby-button" type="submit" class="raised button-submit block" data-icon="check">
|
|
||||||
<span>${Add}</span>
|
|
||||||
</button>
|
|
||||||
<button is="emby-button" type="button" class="raised button-cancel block" data-icon="delete" onclick="$(this).parents('.dialog').addClass('hide');">
|
|
||||||
<span>${ButtonCancel}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -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 '<option value="' + o.value + '">' + o.name + '</option>';
|
|
||||||
}).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 += '<div class="paperList">';
|
|
||||||
|
|
||||||
for (let i = 0, length = task.Triggers.length; i < length; i++) {
|
|
||||||
const trigger = task.Triggers[i];
|
|
||||||
|
|
||||||
html += '<div class="listItem listItem-border">';
|
|
||||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
|
||||||
if (trigger.MaxRuntimeTicks) {
|
|
||||||
html += '<div class="listItemBody two-line">';
|
|
||||||
} else {
|
|
||||||
html += '<div class="listItemBody">';
|
|
||||||
}
|
|
||||||
html += "<div class='listItemBodyText'>" + ScheduledTaskPage.getTriggerFriendlyName(trigger) + '</div>';
|
|
||||||
if (trigger.MaxRuntimeTicks) {
|
|
||||||
html += '<div class="listItemBodyText secondary">';
|
|
||||||
const hours = trigger.MaxRuntimeTicks / 36e9;
|
|
||||||
if (hours == 1) {
|
|
||||||
html += globalize.translate('ValueTimeLimitSingleHour');
|
|
||||||
} else {
|
|
||||||
html += globalize.translate('ValueTimeLimitMultiHour', hours);
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
html += '<button class="btnDeleteTrigger" data-index="' + i + '" type="button" is="paper-icon-button-light" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
35
src/apps/dashboard/features/tasks/api/useTask.ts
Normal file
35
src/apps/dashboard/features/tasks/api/useTask.ts
Normal file
|
@ -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
|
||||||
|
});
|
||||||
|
};
|
22
src/apps/dashboard/features/tasks/api/useUpdateTask.ts
Normal file
22
src/apps/dashboard/features/tasks/api/useUpdateTask.ts
Normal file
|
@ -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 ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
169
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
169
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
|
@ -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<IProps> = ({ 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<HTMLInputElement>) => {
|
||||||
|
setTriggerType(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
maxWidth={'xs'}
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
component: 'form',
|
||||||
|
onSubmit: (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<TextField
|
||||||
|
name='TriggerType'
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
value={triggerType}
|
||||||
|
onChange={onTriggerTypeChange}
|
||||||
|
label={globalize.translate('LabelTriggerType')}
|
||||||
|
>
|
||||||
|
<MenuItem value='DailyTrigger'>{globalize.translate('OptionDaily')}</MenuItem>
|
||||||
|
<MenuItem value='WeeklyTrigger'>{globalize.translate('OptionWeekly')}</MenuItem>
|
||||||
|
<MenuItem value='IntervalTrigger'>{globalize.translate('OptionOnInterval')}</MenuItem>
|
||||||
|
<MenuItem value='StartupTrigger'>{globalize.translate('OnApplicationStartup')}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
|
||||||
|
{triggerType == 'WeeklyTrigger' && (
|
||||||
|
<TextField
|
||||||
|
name='DayOfWeek'
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
defaultValue={'Sunday'}
|
||||||
|
label={globalize.translate('LabelDay')}
|
||||||
|
>
|
||||||
|
<MenuItem value='Sunday'>{globalize.translate('Sunday')}</MenuItem>
|
||||||
|
<MenuItem value='Monday'>{globalize.translate('Monday')}</MenuItem>
|
||||||
|
<MenuItem value='Tuesday'>{globalize.translate('Tuesday')}</MenuItem>
|
||||||
|
<MenuItem value='Wednesday'>{globalize.translate('Wednesday')}</MenuItem>
|
||||||
|
<MenuItem value='Thursday'>{globalize.translate('Thursday')}</MenuItem>
|
||||||
|
<MenuItem value='Friday'>{globalize.translate('Friday')}</MenuItem>
|
||||||
|
<MenuItem value='Saturday'>{globalize.translate('Saturday')}</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{['DailyTrigger', 'WeeklyTrigger'].includes(triggerType) && (
|
||||||
|
<TextField
|
||||||
|
name='TimeOfDay'
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
defaultValue={'0'}
|
||||||
|
label={globalize.translate('LabelTime')}
|
||||||
|
>
|
||||||
|
{timeOfDayOptions.map((option) => {
|
||||||
|
return <MenuItem key={option.value} value={option.value}>{option.name}</MenuItem>;
|
||||||
|
})}
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{triggerType == 'IntervalTrigger' && (
|
||||||
|
<TextField
|
||||||
|
name='Interval'
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
defaultValue={'9000000000'}
|
||||||
|
label={globalize.translate('LabelEveryXMinutes')}
|
||||||
|
>
|
||||||
|
<MenuItem value='9000000000'>15 minutes</MenuItem>
|
||||||
|
<MenuItem value='18000000000'>30 minutes</MenuItem>
|
||||||
|
<MenuItem value='27000000000'>45 minutes</MenuItem>
|
||||||
|
<MenuItem value='36000000000'>1 hour</MenuItem>
|
||||||
|
<MenuItem value='72000000000'>2 hours</MenuItem>
|
||||||
|
<MenuItem value='108000000000'>3 hours</MenuItem>
|
||||||
|
<MenuItem value='144000000000'>4 hours</MenuItem>
|
||||||
|
<MenuItem value='216000000000'>6 hours</MenuItem>
|
||||||
|
<MenuItem value='288000000000'>8 hours</MenuItem>
|
||||||
|
<MenuItem value='432000000000'>12 hours</MenuItem>
|
||||||
|
<MenuItem value='864000000000'>24 hours</MenuItem>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
name='TimeLimit'
|
||||||
|
fullWidth
|
||||||
|
defaultValue={''}
|
||||||
|
type='number'
|
||||||
|
inputProps={{
|
||||||
|
min: 1,
|
||||||
|
step: 0.5
|
||||||
|
}}
|
||||||
|
label={globalize.translate('LabelTimeLimitHours')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={onClose}
|
||||||
|
color='error'
|
||||||
|
>{globalize.translate('ButtonCancel')}</Button>
|
||||||
|
<Button type='submit'>{globalize.translate('Add')}</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewTriggerForm;
|
|
@ -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<MRT_RowData>
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskTriggerCell: FC<CellProps> = ({ cell }) => {
|
||||||
|
const { dateFnsLocale } = useLocale();
|
||||||
|
const trigger = cell.getValue<TaskTriggerInfo>();
|
||||||
|
|
||||||
|
const timeLimitHours = trigger.MaxRuntimeTicks ? trigger.MaxRuntimeTicks / 36e9 : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Typography variant='body1'>{getTriggerFriendlyName(trigger, dateFnsLocale)}</Typography>
|
||||||
|
{timeLimitHours && (
|
||||||
|
<Typography variant='body2' color={'text.secondary'}>
|
||||||
|
{timeLimitHours == 1 ?
|
||||||
|
globalize.translate('ValueTimeLimitSingleHour') :
|
||||||
|
globalize.translate('ValueTimeLimitMultiHour', timeLimitHours)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskTriggerCell;
|
64
src/apps/dashboard/features/tasks/utils/edit.ts
Normal file
64
src/apps/dashboard/features/tasks/utils/edit.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'playback/trickplay', type: AppType.Dashboard },
|
{ path: 'playback/trickplay', type: AppType.Dashboard },
|
||||||
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
||||||
{ path: 'tasks', type: AppType.Dashboard },
|
{ path: 'tasks', type: AppType.Dashboard },
|
||||||
|
{ path: 'tasks/edit', type: AppType.Dashboard },
|
||||||
{ path: 'users', type: AppType.Dashboard },
|
{ path: 'users', type: AppType.Dashboard },
|
||||||
{ path: 'users/access', type: AppType.Dashboard },
|
{ path: 'users/access', type: AppType.Dashboard },
|
||||||
{ path: 'users/add', type: AppType.Dashboard },
|
{ path: 'users/add', type: AppType.Dashboard },
|
||||||
|
|
|
@ -93,12 +93,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'plugins/installed/index',
|
controller: 'plugins/installed/index',
|
||||||
view: 'plugins/installed/index.html'
|
view: 'plugins/installed/index.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'tasks/edit',
|
|
||||||
pageProps: {
|
|
||||||
appType: AppType.Dashboard,
|
|
||||||
controller: 'scheduledtasks/scheduledtask',
|
|
||||||
view: 'scheduledtasks/scheduledtask.html'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
173
src/apps/dashboard/routes/tasks/edit.tsx
Normal file
173
src/apps/dashboard/routes/tasks/edit.tsx
Normal file
|
@ -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<TaskTriggerInfo | null>(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<MRT_ColumnDef<TaskTriggerInfo>[]>(() => [
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}>
|
||||||
|
<Tooltip disableInteractive title={globalize.translate('ButtonRemove')}>
|
||||||
|
<IconButton
|
||||||
|
color='error'
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onDeleteTrigger(row.original)}
|
||||||
|
>
|
||||||
|
<RemoveCircleIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading || !task) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='scheduledTaskPage'
|
||||||
|
className='mainAnimatedPage type-interior'
|
||||||
|
>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={isRemoveConfirmOpen}
|
||||||
|
title={globalize.translate('HeaderDeleteTaskTrigger')}
|
||||||
|
text={globalize.translate('MessageDeleteTaskTrigger')}
|
||||||
|
onCancel={onCloseRemoveConfirmDialog}
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
confirmButtonColor='error'
|
||||||
|
confirmButtonText={globalize.translate('ButtonRemove')}
|
||||||
|
/>
|
||||||
|
<NewTriggerForm
|
||||||
|
open={isAddTriggerDialogOpen}
|
||||||
|
title={globalize.translate('ButtonAddScheduledTaskTrigger')}
|
||||||
|
onClose={handleNewTriggerDialogClose}
|
||||||
|
onSubmit={onNewTriggerSubmit}
|
||||||
|
/>
|
||||||
|
<Box className='content-primary'>
|
||||||
|
<Box className='readOnlyContent'>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Typography variant='h2'>{task.Name}</Typography>
|
||||||
|
<Typography variant='body1'>{task.Description}</Typography>
|
||||||
|
<Button
|
||||||
|
sx={{ alignSelf: 'flex-start' }}
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={showAddTriggerDialog}
|
||||||
|
>{globalize.translate('ButtonAddScheduledTaskTrigger')}</Button>
|
||||||
|
<MRT_Table table={table} />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskEdit;
|
|
@ -3,10 +3,10 @@ import Page from 'components/Page';
|
||||||
import globalize from 'lib/globalize';
|
import globalize from 'lib/globalize';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { QUERY_KEY, useTasks } from '../../features/scheduledtasks/api/useTasks';
|
import { QUERY_KEY, useTasks } from '../../features/tasks/api/useTasks';
|
||||||
import { getCategories, getTasksByCategory } from '../../features/scheduledtasks/utils/tasks';
|
import { getCategories, getTasksByCategory } from '../../features/tasks/utils/tasks';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
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 type { TaskInfo } from '@jellyfin/sdk/lib/generated-client/models/task-info';
|
||||||
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type';
|
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models/session-message-type';
|
||||||
import serverNotifications from 'scripts/serverNotifications';
|
import serverNotifications from 'scripts/serverNotifications';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue