mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6565 from viown/react-tasks-edit
Migrate tasks edit page to react
This commit is contained in:
commit
3c62c1dc51
22 changed files with 530 additions and 343 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);
|
||||
});
|
||||
}
|
||||
|
29
src/apps/dashboard/features/tasks/api/useTask.ts
Normal file
29
src/apps/dashboard/features/tasks/api/useTask.ts
Normal file
|
@ -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
|
||||
});
|
||||
};
|
|
@ -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
|
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: (_data, params) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [ QUERY_KEY, params.taskId ]
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
171
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
171
src/apps/dashboard/features/tasks/components/NewTriggerForm.tsx
Normal file
|
@ -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<IProps> = ({ open, title, onClose, onAdd }: IProps) => {
|
||||
const { dateFnsLocale } = useLocale();
|
||||
const [triggerType, setTriggerType] = useState<TaskTriggerInfoType>(TaskTriggerInfoType.DailyTrigger);
|
||||
|
||||
const timeOfDayOptions = useMemo(() => getTimeOfDayOptions(dateFnsLocale), [dateFnsLocale]);
|
||||
const intervalOptions = useMemo(() => getIntervalOptions(dateFnsLocale), [dateFnsLocale]);
|
||||
|
||||
const onTriggerTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTriggerType(e.target.value as TaskTriggerInfoType);
|
||||
}, []);
|
||||
|
||||
const onSubmit = useCallback((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()) * 36e9;
|
||||
}
|
||||
|
||||
if (onAdd) {
|
||||
onAdd(trigger);
|
||||
}
|
||||
}, [ onAdd ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
maxWidth={'xs'}
|
||||
fullWidth
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
component: 'form',
|
||||
onSubmit: onSubmit
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
name='TriggerType'
|
||||
select
|
||||
fullWidth
|
||||
value={triggerType}
|
||||
onChange={onTriggerTypeChange}
|
||||
label={globalize.translate('LabelTriggerType')}
|
||||
>
|
||||
<MenuItem value={TaskTriggerInfoType.DailyTrigger}>{globalize.translate('OptionDaily')}</MenuItem>
|
||||
<MenuItem value={TaskTriggerInfoType.WeeklyTrigger}>{globalize.translate('OptionWeekly')}</MenuItem>
|
||||
<MenuItem value={TaskTriggerInfoType.IntervalTrigger}>{globalize.translate('OptionOnInterval')}</MenuItem>
|
||||
<MenuItem value={TaskTriggerInfoType.StartupTrigger}>{globalize.translate('OnApplicationStartup')}</MenuItem>
|
||||
</TextField>
|
||||
|
||||
{triggerType == TaskTriggerInfoType.WeeklyTrigger && (
|
||||
<TextField
|
||||
name='DayOfWeek'
|
||||
select
|
||||
fullWidth
|
||||
defaultValue={DayOfWeek.Sunday}
|
||||
label={globalize.translate('LabelDay')}
|
||||
>
|
||||
<MenuItem value={DayOfWeek.Sunday}>{globalize.translate('Sunday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Monday}>{globalize.translate('Monday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Tuesday}>{globalize.translate('Tuesday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Wednesday}>{globalize.translate('Wednesday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Thursday}>{globalize.translate('Thursday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Friday}>{globalize.translate('Friday')}</MenuItem>
|
||||
<MenuItem value={DayOfWeek.Saturday}>{globalize.translate('Saturday')}</MenuItem>
|
||||
</TextField>
|
||||
)}
|
||||
|
||||
{(triggerType == TaskTriggerInfoType.DailyTrigger || triggerType == TaskTriggerInfoType.WeeklyTrigger) && (
|
||||
<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 == TaskTriggerInfoType.IntervalTrigger && (
|
||||
<TextField
|
||||
name='Interval'
|
||||
select
|
||||
fullWidth
|
||||
defaultValue={intervalOptions[0].value}
|
||||
label={globalize.translate('LabelEveryXMinutes')}
|
||||
>
|
||||
{intervalOptions.map((option) => {
|
||||
return <MenuItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>{option.name}</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;
|
|
@ -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<TaskProps> = ({ 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<TaskProps> = ({ task }: TaskProps) => {
|
|||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemButton onClick={navigateTaskEdit}>
|
||||
<ListItemLink to={`/dashboard/tasks/${task.Id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar sx={{ bgcolor: 'primary.main' }}>
|
||||
<AccessTimeIcon sx={{ color: '#fff' }} />
|
||||
|
@ -59,7 +51,7 @@ const Task: FunctionComponent<TaskProps> = ({ task }: TaskProps) => {
|
|||
secondary={task.State == 'Running' ? <TaskProgress task={task} /> : <TaskLastRan task={task} />}
|
||||
disableTypography
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItemLink>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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
|
||||
];
|
80
src/apps/dashboard/features/tasks/utils/edit.ts
Normal file
80
src/apps/dashboard/features/tasks/utils/edit.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 },
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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';
|
||||
|
|
172
src/apps/dashboard/routes/tasks/task.tsx
Normal file
172
src/apps/dashboard/routes/tasks/task.tsx
Normal file
|
@ -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<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 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<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}
|
||||
onAdd={onNewTriggerAdd}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'TaskPage';
|
|
@ -27,7 +27,7 @@ const ConfirmDialog: FC<ConfirmDialogProps> = ({
|
|||
onConfirm,
|
||||
...dialogProps
|
||||
}) => (
|
||||
<Dialog {...dialogProps}>
|
||||
<Dialog onClose={onCancel} {...dialogProps}>
|
||||
<DialogTitle>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue