From c8e0e14bf516751e863bbf7f192e0647b70e5478 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Tue, 3 Sep 2024 09:20:04 +0800 Subject: [PATCH] Task prioritization and cleanup of task form (#1000) * Basic implementation of priority based on legacy implementation Signed-off-by: Aaron Chong * Use icons for favorite Signed-off-by: Aaron Chong * Fix warning time, use switch and made buttons nicer Signed-off-by: Aaron Chong * To display priority Signed-off-by: Aaron Chong * Lint Signed-off-by: Aaron Chong * Helper function to handle null and undefined priority, clean up creation Signed-off-by: Aaron Chong * Use switch for priority, flip low priority icon Signed-off-by: Aaron Chong * Remove getDefaultTaskPriorty Signed-off-by: Aaron Chong --------- Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 114 +++++++++++------- .../lib/tasks/task-table-datagrid.tsx | 64 ++++++---- packages/react-components/lib/tasks/utils.ts | 25 +++- 3 files changed, 133 insertions(+), 70 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 86c8b652b..549ad08c1 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -5,10 +5,13 @@ import UpdateIcon from '@mui/icons-material/Create'; import DeleteIcon from '@mui/icons-material/Delete'; +import FavoriteBorder from '@mui/icons-material/FavoriteBorder'; +import SaveIcon from '@mui/icons-material/Save'; +import ScheduleSendIcon from '@mui/icons-material/ScheduleSend'; +import SendIcon from '@mui/icons-material/Send'; import { Box, Button, - Checkbox, Chip, Dialog, DialogActions, @@ -29,7 +32,9 @@ import { Radio, RadioGroup, styled, + Switch, TextField, + Tooltip, Typography, useMediaQuery, useTheme, @@ -83,6 +88,7 @@ import { PatrolTaskForm, } from './types/patrol'; import { getDefaultTaskDescription, getTaskRequestCategory } from './types/utils'; +import { createTaskPriority, parseTaskPriority } from './utils'; export interface TaskDefinition { taskDefinitionId: string; @@ -214,7 +220,7 @@ function getDefaultTaskRequest(taskDefinitionId: string): TaskRequest | null { description, unix_millis_earliest_start_time: 0, unix_millis_request_time: Date.now(), - priority: { type: 'binary', value: 0 }, + priority: createTaskPriority(false), requester: '', }; } @@ -462,7 +468,7 @@ export function CreateTaskForm({ } } const [warnTime, setWarnTime] = React.useState(existingWarnTime); - const handleWarnTimeCheckboxChange = (event: React.ChangeEvent) => { + const handleWarnTimeSwitchChange = (event: React.ChangeEvent) => { if (event.target.checked) { setWarnTime(new Date()); } else { @@ -630,10 +636,8 @@ export function CreateTaskForm({ return; } - const requester = scheduling ? `${user}__scheduled` : user; - const request = { ...taskRequest }; - request.requester = requester; + request.requester = user; request.unix_millis_request_time = Date.now(); if (taskDefinitionId === CustomComposeTaskDefinition.taskDefinitionId) { @@ -685,6 +689,10 @@ export function CreateTaskForm({ requestBookingLabel['unix_millis_warn_time'] = `${warnTime.valueOf()}`; } + if (scheduling) { + requestBookingLabel['scheduled'] = 'true'; + } + request.labels = serializeTaskBookingLabel(requestBookingLabel); console.log(`labels: ${request.labels}`); } catch (e) { @@ -786,6 +794,15 @@ export function CreateTaskForm({ } }; + const handlePrioritySwitchChange = (event: React.ChangeEvent) => { + setTaskRequest((prev) => { + return { + ...prev, + priority: createTaskPriority(event.target.checked), + }; + }); + }; + return ( <> - + - - 0} - label="Start Time" - disabled - /> - - + + + - + + + + + } + label="Prioritize" + sx={{ + color: parseTaskPriority(taskRequest.priority) + ? undefined + : theme.palette.action.disabled, + }} + /> + + {renderTaskDescriptionForm(taskDefinitionId)} - - - + + + @@ -960,6 +984,7 @@ export function CreateTaskForm({ aria-label="Submit Now" onClick={handleSubmitNow} size={isScreenHeightLessThan800 ? 'small' : 'medium'} + startIcon={} > )} {callToDeleteFavoriteTask && ( diff --git a/packages/react-components/lib/tasks/task-table-datagrid.tsx b/packages/react-components/lib/tasks/task-table-datagrid.tsx index ae8d55612..d91933e20 100644 --- a/packages/react-components/lib/tasks/task-table-datagrid.tsx +++ b/packages/react-components/lib/tasks/task-table-datagrid.tsx @@ -1,4 +1,8 @@ -import { InsertInvitation as ScheduleIcon, Person as UserIcon } from '@mui/icons-material/'; +import { + InsertInvitation as ScheduleIcon, + LowPriority, + Person as UserIcon, +} from '@mui/icons-material/'; import { Box, Stack, styled, Tooltip, Typography } from '@mui/material'; import { DataGrid, @@ -21,6 +25,7 @@ import * as React from 'react'; import { TaskBookingLabels } from './booking-label'; import { getTaskBookingLabelFromTaskState } from './task-booking-label-utils'; +import { parseTaskPriority } from './utils'; const classes = { taskActiveCell: 'MuiDataGrid-cell-active-cell', @@ -110,33 +115,32 @@ export interface TableDataGridState { setSortFields: React.Dispatch>; } -const TaskRequester = (requester: string | undefined | null): JSX.Element => { +const TaskRequester = ( + requester: string | undefined | null, + scheduled: boolean, + prioritized: boolean, +): JSX.Element => { if (!requester) { return n/a; } - /** When a task is created as scheduled, - we save the requester as USERNAME__scheduled. - Therefore, we remove the __schedule because the different icon is enough indicator to know - if the task was adhoc or scheduled. - */ return ( - {requester.includes('scheduled') ? ( - <> - - - - {requester.split('__')[0]} - + {prioritized ? ( + + + + ) : null} + {scheduled ? ( + + + ) : ( - <> - - - - {requester} - + + + )} + {requester} ); }; @@ -192,7 +196,19 @@ export function TaskDataGridTable({ headerName: 'Requester', width: 150, editable: false, - renderCell: (cellValues) => TaskRequester(cellValues.row.state.booking.requester), + renderCell: (cellValues) => { + let prioritized = false; + if (cellValues.row.state.booking.priority) { + prioritized = parseTaskPriority(cellValues.row.state.booking.priority); + } + + let scheduled = false; + if (cellValues.row.requestLabel && 'scheduled' in cellValues.row.requestLabel) { + scheduled = cellValues.row.requestLabel.scheduled === 'true'; + } + + return TaskRequester(cellValues.row.state.booking.requester, scheduled, prioritized); + }, flex: 1, filterOperators: getMinimalStringFilterOperators, filterable: true, @@ -272,7 +288,7 @@ export function TaskDataGridTable({ renderCell: (cellValues) => { let warnDateTime: Date | undefined = undefined; if (cellValues.row.requestLabel && 'unix_millis_warn_time' in cellValues.row.requestLabel) { - const warnMillisNum = parseInt(cellValues.row.requestLabel['unix_millis_warn_time']); + const warnMillisNum = parseInt(cellValues.row.requestLabel.unix_millis_warn_time); if (!Number.isNaN(warnMillisNum)) { warnDateTime = new Date(warnMillisNum); } @@ -407,9 +423,7 @@ export function TaskDataGridTable({ let warnDateTime: Date | undefined = undefined; if (params.row.requestLabel && 'unix_millis_warn_time' in params.row.requestLabel) { - const warnMillisNum = parseInt( - params.row.requestLabel.description['unix_millis_warn_time'], - ); + const warnMillisNum = parseInt(params.row.requestLabel.unix_millis_warn_time); if (!Number.isNaN(warnMillisNum)) { warnDateTime = new Date(warnMillisNum); } diff --git a/packages/react-components/lib/tasks/utils.ts b/packages/react-components/lib/tasks/utils.ts index 02136da53..6a7c6ed40 100644 --- a/packages/react-components/lib/tasks/utils.ts +++ b/packages/react-components/lib/tasks/utils.ts @@ -1,4 +1,4 @@ -import type { TaskRequest, TaskStateOutput as TaskState } from 'api-client'; +import type { Priority, TaskRequest, TaskStateOutput as TaskState } from 'api-client'; import { TaskType as RmfTaskType } from 'rmf-models/ros/rmf_task_msgs/msg'; export function taskTypeToStr(taskType: number): string { @@ -211,3 +211,26 @@ export function parseDestination(state?: TaskState, request?: TaskRequest): stri return 'n/a'; } + +export function createTaskPriority(prioritize: boolean): Priority { + return { type: 'binary', value: prioritize ? 1 : 0 }; +} + +// FIXME(ac): This method of parsing is crude, and will be fixed using schemas +// when we migrate to jsonforms. +export function parseTaskPriority(priority: Priority | null | undefined): boolean { + if (!priority) { + return false; + } + + if ( + typeof priority == 'object' && + 'type' in priority && + priority['type'] === 'binary' && + 'value' in priority && + typeof priority['value'] == 'number' + ) { + return (priority['value'] as number) > 0; + } + return false; +}