From b6daddcb7a3c32fcff7a50707161ded73ba26dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9sar=20Rol=C3=B3n?= <37310205+Angatupyry@users.noreply.github.com> Date: Fri, 25 Aug 2023 03:35:50 -0400 Subject: [PATCH] Delete the entire recurring event or only the selected event (#742) * Add routes to delete only one job Signed-off-by: angatupyry * Add except date to cancel a job Signed-off-by: angatupyry * Testing clear only one job Signed-off-by: angatupyry * Remove unused endpoint Signed-off-by: angatupyry * Delete all jobs and create again when except date is included this week Signed-off-by: angatupyry * Show event or not depending of the except date Signed-off-by: angatupyry * Fix comment to explain the code better Signed-off-by: angatupyry * Add except date to the Schedule task and remove from ScheduleTaskschedule Signed-off-by: angatupyry * Create a endpoint to add a except date to the array and check whether or not the curretn date is include in the array of except_dates Signed-off-by: angatupyry * Adding control in order to show events Signed-off-by: angatupyry * Add confirmation popup to delete one event or all events Signed-off-by: angatupyry * Sort alphabetical order Signed-off-by: angatupyry * Remove console.log Signed-off-by: angatupyry * Using throw error to show message in console Signed-off-by: angatupyry * Change the endpoint and endpoint parameters to make them more accurate and understandable. Signed-off-by: angatupyry --------- Signed-off-by: angatupyry --- packages/api-client/lib/openapi/api.ts | 117 ++++++++++++++++ packages/api-client/lib/version.ts | 2 +- packages/api-client/schema/index.ts | 34 +++++ .../models/tortoise_models/scheduled_task.py | 1 + .../routes/tasks/scheduled_tasks.py | 25 ++++ .../src/components/tasks/tasks-app.tsx | 125 ++++++++++++++---- 6 files changed, 279 insertions(+), 25 deletions(-) diff --git a/packages/api-client/lib/openapi/api.ts b/packages/api-client/lib/openapi/api.ts index fa09c4733..b509ca3a2 100644 --- a/packages/api-client/lib/openapi/api.ts +++ b/packages/api-client/lib/openapi/api.ts @@ -321,6 +321,12 @@ export interface ApiServerModelsTortoiseModelsScheduledTaskScheduledTask { * @memberof ApiServerModelsTortoiseModelsScheduledTaskScheduledTask */ last_ran?: string | null; + /** + * + * @type {any} + * @memberof ApiServerModelsTortoiseModelsScheduledTaskScheduledTask + */ + except_dates?: any; /** * * @type {Array} @@ -7340,6 +7346,60 @@ export class LiftsApi extends BaseAPI { */ export const TasksApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @summary Del Scheduled Tasks Event + * @param {number} taskId + * @param {string} eventDate + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delScheduledTasksEventScheduledTasksTaskIdClearPut: async ( + taskId: number, + eventDate: string, + options: AxiosRequestConfig = {}, + ): Promise => { + // verify required parameter 'taskId' is not null or undefined + assertParamExists('delScheduledTasksEventScheduledTasksTaskIdClearPut', 'taskId', taskId); + // verify required parameter 'eventDate' is not null or undefined + assertParamExists( + 'delScheduledTasksEventScheduledTasksTaskIdClearPut', + 'eventDate', + eventDate, + ); + const localVarPath = `/scheduled_tasks/{task_id}/clear`.replace( + `{${'task_id'}}`, + encodeURIComponent(String(taskId)), + ); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (eventDate !== undefined) { + localVarQueryParameter['event_date'] = + (eventDate as any) instanceof Date ? (eventDate as any).toISOString() : eventDate; + } + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Del Scheduled Tasks @@ -8386,6 +8446,27 @@ export const TasksApiAxiosParamCreator = function (configuration?: Configuration export const TasksApiFp = function (configuration?: Configuration) { const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration); return { + /** + * + * @summary Del Scheduled Tasks Event + * @param {number} taskId + * @param {string} eventDate + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async delScheduledTasksEventScheduledTasksTaskIdClearPut( + taskId: number, + eventDate: string, + options?: AxiosRequestConfig, + ): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = + await localVarAxiosParamCreator.delScheduledTasksEventScheduledTasksTaskIdClearPut( + taskId, + eventDate, + options, + ); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Del Scheduled Tasks @@ -8824,6 +8905,23 @@ export const TasksApiFactory = function ( ) { const localVarFp = TasksApiFp(configuration); return { + /** + * + * @summary Del Scheduled Tasks Event + * @param {number} taskId + * @param {string} eventDate + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + delScheduledTasksEventScheduledTasksTaskIdClearPut( + taskId: number, + eventDate: string, + options?: any, + ): AxiosPromise { + return localVarFp + .delScheduledTasksEventScheduledTasksTaskIdClearPut(taskId, eventDate, options) + .then((request) => request(axios, basePath)); + }, /** * * @summary Del Scheduled Tasks @@ -9183,6 +9281,25 @@ export const TasksApiFactory = function ( * @extends {BaseAPI} */ export class TasksApi extends BaseAPI { + /** + * + * @summary Del Scheduled Tasks Event + * @param {number} taskId + * @param {string} eventDate + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof TasksApi + */ + public delScheduledTasksEventScheduledTasksTaskIdClearPut( + taskId: number, + eventDate: string, + options?: AxiosRequestConfig, + ) { + return TasksApiFp(this.configuration) + .delScheduledTasksEventScheduledTasksTaskIdClearPut(taskId, eventDate, options) + .then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Del Scheduled Tasks diff --git a/packages/api-client/lib/version.ts b/packages/api-client/lib/version.ts index 62334887f..091417ba6 100644 --- a/packages/api-client/lib/version.ts +++ b/packages/api-client/lib/version.ts @@ -3,6 +3,6 @@ import { version as rmfModelVer } from 'rmf-models'; export const version = { rmfModels: rmfModelVer, - rmfServer: '0aa093dff56cb468ad8d8e51f28b930fe88492d9', + rmfServer: 'c8c43e395caae7ccc858008b7663d30914b2cc62', openapiGenerator: '6.2.1', }; diff --git a/packages/api-client/schema/index.ts b/packages/api-client/schema/index.ts index 12033db33..0d7a8e22a 100644 --- a/packages/api-client/schema/index.ts +++ b/packages/api-client/schema/index.ts @@ -1250,6 +1250,39 @@ export default { }, }, }, + '/scheduled_tasks/{task_id}/clear': { + put: { + tags: ['Tasks'], + summary: 'Del Scheduled Tasks Event', + operationId: 'del_scheduled_tasks_event_scheduled_tasks__task_id__clear_put', + parameters: [ + { + required: true, + schema: { title: 'Task Id', type: 'integer' }, + name: 'task_id', + in: 'path', + }, + { + required: true, + schema: { title: 'Event Date', type: 'string', format: 'date-time' }, + name: 'event_date', + in: 'query', + }, + ], + responses: { + '200': { + description: 'Successful Response', + content: { 'application/json': { schema: {} } }, + }, + '422': { + description: 'Validation Error', + content: { + 'application/json': { schema: { $ref: '#/components/schemas/HTTPValidationError' } }, + }, + }, + }, + }, + }, '/favorite_tasks': { get: { tags: ['Tasks'], @@ -3669,6 +3702,7 @@ export default { task_request: { title: 'Task Request' }, created_by: { title: 'Created By', maxLength: 255, type: 'string' }, last_ran: { title: 'Last Ran', type: 'string', format: 'date-time', nullable: true }, + except_dates: { title: 'Except Dates' }, schedules: { title: 'Schedules', type: 'array', diff --git a/packages/api-server/api_server/models/tortoise_models/scheduled_task.py b/packages/api-server/api_server/models/tortoise_models/scheduled_task.py index 6b733feea..2d1d2a9b8 100644 --- a/packages/api-server/api_server/models/tortoise_models/scheduled_task.py +++ b/packages/api-server/api_server/models/tortoise_models/scheduled_task.py @@ -28,6 +28,7 @@ class ScheduledTask(Model): created_by = CharField(255) schedules: ReverseRelation["ScheduledTaskSchedule"] last_ran: Optional[datetime] = DatetimeField(null=True) + except_dates = JSONField(null=True, default=list) class ScheduledTaskSchedule(Model): diff --git a/packages/api-server/api_server/routes/tasks/scheduled_tasks.py b/packages/api-server/api_server/routes/tasks/scheduled_tasks.py index 452ed2d06..e524cd604 100644 --- a/packages/api-server/api_server/routes/tasks/scheduled_tasks.py +++ b/packages/api-server/api_server/routes/tasks/scheduled_tasks.py @@ -25,6 +25,10 @@ class PostScheduledTaskRequest(BaseModel): schedules: list[ttm.ScheduledTaskSchedulePydantic] +def datetime_to_date_format(date: datetime) -> str: + return date.date().strftime("%m/%d/%Y").lstrip("0") + + async def schedule_task(task: ttm.ScheduledTask, task_repo: TaskRepository): await task.fetch_related("schedules") jobs: list[tuple[ttm.ScheduledTaskSchedule, schedule.Job]] = [] @@ -49,6 +53,8 @@ async def run(): def do(): logger.info(f"starting task {task.pk}") + if datetime_to_date_format(datetime.now()) in task.except_dates: + return asyncio.get_event_loop().create_task(run()) for _, j in jobs: @@ -129,6 +135,25 @@ async def get_scheduled_task(task_id: int) -> ttm.ScheduledTask: return task +@router.put("/{task_id}/clear") +async def del_scheduled_tasks_event( + task_id: int, + event_date: datetime, + task_repo: TaskRepository = Depends(task_repo_dep), +): + task = await get_scheduled_task(task_id) + if task is None: + raise HTTPException(404) + + task.except_dates.append(datetime_to_date_format(event_date)) + await task.save() + + for sche in task.schedules: + schedule.clear(sche.get_id()) + + await schedule_task(task, task_repo) + + @router.delete("/{task_id}") async def del_scheduled_tasks(task_id: int): async with tortoise.transactions.in_transaction(): diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index b29172f2e..292201bfe 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -4,9 +4,13 @@ import DownloadIcon from '@mui/icons-material/Download'; import RefreshIcon from '@mui/icons-material/Refresh'; import { Box, + FormControl, + FormControlLabel, IconButton, Menu, MenuItem, + Radio, + RadioGroup, Tab, Tabs, TableContainer, @@ -39,6 +43,7 @@ import { } from 'date-fns'; import React from 'react'; import { + ConfirmationDialog, FilterFields, MuiMouseEvent, SortFields, @@ -59,6 +64,11 @@ interface TabPanelProps { selectedTabIndex: number; } +enum ScheduleDeleteOptions { + ALL = 'all', + CURRENT = 'current', +} + function tabId(index: number): string { return `simple-tab-${index}`; } @@ -99,6 +109,7 @@ function scheduleToEvents( start: Date, end: Date, schedule: ApiSchedule, + task: ScheduledTask, getEventId: () => number, getEventTitle: () => string, ): ProcessedEvent[] { @@ -157,13 +168,16 @@ function scheduleToEvents( (scheStartFrom == null || scheStartFrom <= cur) && (scheUntil == null || scheUntil >= cur) ) { - events.push({ - start: cur, - end: addMinutes(cur, 45), - event_id: getEventId(), - title: getEventTitle(), - }); + if (!task.except_dates.includes(cur.toLocaleDateString())) { + events.push({ + start: cur, + end: addMinutes(cur, 45), + event_id: getEventId(), + title: getEventTitle(), + }); + } } + cur = new Date(cur.valueOf() + period); } return events; @@ -189,6 +203,13 @@ export const TasksApp = React.memo( const [openTaskSummary, setOpenTaskSummary] = React.useState(false); const [selectedTask, setSelectedTask] = React.useState(null); + const [openDeleteScheduleDialog, setOpenDeleteScheduleDialog] = React.useState(false); + const [scheduleDeleteValue, setScheduleDeleteValue] = React.useState( + ScheduleDeleteOptions.CURRENT, + ); + const [currentEventId, setCurrentEventId] = React.useState(-1); + const exceptDateRef = React.useRef(new Date()); + const [tasksState, setTasksState] = React.useState({ isLoading: true, data: [], @@ -350,7 +371,7 @@ export const TasksApp = React.memo( eventsMap.current = {}; return tasks.flatMap((t: ScheduledTask) => t.schedules.flatMap((s: ApiSchedule) => { - const events = scheduleToEvents(params.start, params.end, s, getEventId, () => + const events = scheduleToEvents(params.start, params.end, s, t, getEventId, () => getScheduledTaskTitle(t), ); events.forEach((ev) => { @@ -368,6 +389,37 @@ export const TasksApp = React.memo( setSelectedTabIndex(newSelectedTabIndex); }; + const handleSubmitDeleteSchedule: React.MouseEventHandler = async (ev) => { + ev.preventDefault(); + try { + const task = eventsMap.current[Number(currentEventId)]; + + if (!task) { + throw new Error(`unable to find task for event ${currentEventId}`); + } + if (!rmf) { + throw new Error('tasks api not available'); + } + + if (scheduleDeleteValue === ScheduleDeleteOptions.CURRENT) { + await rmf.tasksApi.delScheduledTasksEventScheduledTasksTaskIdClearPut( + task.id, + exceptDateRef.current.toISOString(), + ); + } else { + await rmf.tasksApi.delScheduledTasksScheduledTasksTaskIdDelete(task.id); + } + AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + + // Set the default values + setOpenDeleteScheduleDialog(false); + setCurrentEventId(-1); + setScheduleDeleteValue(ScheduleDeleteOptions.CURRENT); + } catch (e) { + console.error(`Failed to delete scheduled task: ${e}`); + } + }; + return ( { - const task = eventsMap.current[Number(deletedId)]; - if (!task) { - console.error( - `Failed to delete scheduled task: unable to find task for event ${deletedId}`, - ); - return; - } - if (!rmf) { - return; - } - try { - await rmf.tasksApi.delScheduledTasksScheduledTasksTaskIdDelete(task.id); - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); - } catch (e) { - console.error(`Failed to delete scheduled task: ${e}`); - } + onEventClick={(event: ProcessedEvent) => { + exceptDateRef.current = event.start; + }} + onDelete={async (deletedId: number) => { + setCurrentEventId(Number(deletedId)); + setOpenDeleteScheduleDialog(true); }} /> @@ -494,6 +535,42 @@ export const TasksApp = React.memo( {openTaskSummary && ( setOpenTaskSummary(false)} /> )} + {openDeleteScheduleDialog && ( + { + setOpenDeleteScheduleDialog(false); + setScheduleDeleteValue(ScheduleDeleteOptions.CURRENT); + }} + onSubmit={handleSubmitDeleteSchedule} + > + + ) => + setScheduleDeleteValue(event.target.value) + } + > + } + label="This event" + /> + } + label="All events" + /> + + + + )} {children} );