1
0
Fork 0
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:
viown 2025-02-22 16:45:48 +03:00
parent e80b890bd2
commit 524d1b6574
20 changed files with 501 additions and 330 deletions

View file

@ -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>

View file

@ -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);
});
}

View 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
});
};

View 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 ]
});
}
});
};

View 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;

View file

@ -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;

View 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;
}
}

View file

@ -15,6 +15,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'playback/trickplay', type: AppType.Dashboard },
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
{ path: 'tasks', type: AppType.Dashboard },
{ path: 'tasks/edit', type: AppType.Dashboard },
{ path: 'users', type: AppType.Dashboard },
{ path: 'users/access', type: AppType.Dashboard },
{ path: 'users/add', type: AppType.Dashboard },

View file

@ -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'
}
}
];

View 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;

View file

@ -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';