From 274ca26c467b0ea0c3e301163fadb3eeea74c348 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 31 Aug 2023 12:32:49 +0800 Subject: [PATCH 01/28] Selecting a favorite task changes taskRequests, instead of append (#761) Signed-off-by: Aaron Chong --- .../lib/tasks/create-task.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index eb9c689e0..0e7617b92 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -973,17 +973,14 @@ export function CreateTaskForm({ setOpenDialog={setOpenFavoriteDialog} listItemClick={() => { setFavoriteTaskBuffer(favoriteTask); - setTaskRequests((prev) => { - return [ - ...prev, - { - category: favoriteTask.category, - description: favoriteTask.description, - unix_millis_earliest_start_time: Date.now(), - priority: favoriteTask.priority, - }, - ]; - }); + setTaskRequests([ + { + category: favoriteTask.category, + description: favoriteTask.description, + unix_millis_earliest_start_time: Date.now(), + priority: favoriteTask.priority, + }] + ); }} /> ); From 58f48695c84ccf0ff12afbda78d28c4496bb4b81 Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 7 Sep 2023 10:06:13 +0800 Subject: [PATCH 02/28] Feature/task table auto update (#763) * Sent refresh counter app event to be void, introduced interval event Signed-off-by: Aaron Chong * Fix prepend addition Signed-off-by: Aaron Chong * Using 5 second periodic query interval Signed-off-by: Aaron Chong * Refactor alert event to use void subject as well Signed-off-by: Aaron Chong --------- Signed-off-by: Aaron Chong --- .../dashboard/src/components/alert-store.tsx | 4 +- .../dashboard/src/components/app-events.ts | 4 +- packages/dashboard/src/components/appbar.tsx | 39 ++++++++++--------- .../src/components/tasks/tasks-app.tsx | 25 ++++++++++-- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/packages/dashboard/src/components/alert-store.tsx b/packages/dashboard/src/components/alert-store.tsx index 68ee27b41..95e0db09e 100644 --- a/packages/dashboard/src/components/alert-store.tsx +++ b/packages/dashboard/src/components/alert-store.tsx @@ -17,7 +17,6 @@ enum AlertCategory { export const AlertStore = React.memo(() => { const rmf = React.useContext(RmfAppContext); const [taskAlerts, setTaskAlerts] = React.useState>({}); - const refreshAlertCount = React.useRef(0); const categorizeAndPushAlerts = (alert: Alert) => { // We check if an existing alert has been acknowledged, remove it before @@ -51,8 +50,7 @@ export const AlertStore = React.memo(() => { } const sub = rmf.alertObsStore.subscribe(async (alert) => { categorizeAndPushAlerts(alert); - refreshAlertCount.current += 1; - AppEvents.refreshAlertCount.next(refreshAlertCount.current); + AppEvents.refreshAlert.next(); }); return () => sub.unsubscribe(); }, [rmf]); diff --git a/packages/dashboard/src/components/app-events.ts b/packages/dashboard/src/components/app-events.ts index 1edafe08c..21c1e7d02 100644 --- a/packages/dashboard/src/components/app-events.ts +++ b/packages/dashboard/src/components/app-events.ts @@ -15,8 +15,8 @@ export const AppEvents = { ingestorSelect: new Subject(), robotSelect: new Subject<[fleetName: string, robotName: string] | null>(), taskSelect: new Subject(), - refreshTaskAppCount: new Subject(), - refreshAlertCount: new Subject(), + refreshTaskApp: new Subject(), + refreshAlert: new Subject(), alertListOpenedAlert: new Subject(), disabledLayers: new ReplaySubject>(), zoom: new BehaviorSubject(null), diff --git a/packages/dashboard/src/components/appbar.tsx b/packages/dashboard/src/components/appbar.tsx index b3c6d85e3..0d3a86f58 100644 --- a/packages/dashboard/src/components/appbar.tsx +++ b/packages/dashboard/src/components/appbar.tsx @@ -203,8 +203,8 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea }, [rmf]); React.useEffect(() => { - const sub = AppEvents.refreshTaskAppCount.subscribe((currentValue) => { - setRefreshTaskAppCount(currentValue); + const sub = AppEvents.refreshTaskApp.subscribe({ + next: () => setRefreshTaskAppCount((oldValue) => ++oldValue), }); return () => sub.unsubscribe(); }, []); @@ -249,18 +249,21 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea }), ); subs.push( - AppEvents.refreshAlertCount.subscribe((_) => { - (async () => { - const resp = await rmf.alertsApi.getAlertsAlertsGet(); - const alerts = resp.data as Alert[]; - setUnacknowledgedAlertsNum( - alerts.filter( - (alert) => !(alert.acknowledged_by && alert.unix_millis_acknowledged_time), - ).length, - ); - })(); + AppEvents.refreshAlert.subscribe({ + next: () => { + (async () => { + const resp = await rmf.alertsApi.getAlertsAlertsGet(); + const alerts = resp.data as Alert[]; + setUnacknowledgedAlertsNum( + alerts.filter( + (alert) => !(alert.acknowledged_by && alert.unix_millis_acknowledged_time), + ).length, + ); + })(); + }, }), ); + // Get the initial number of unacknowledged alerts (async () => { const resp = await rmf.alertsApi.getAlertsAlertsGet(); @@ -293,9 +296,9 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea scheduleRequests.map((req) => rmf.tasksApi.postScheduledTaskScheduledTasksPost(req)), ); } - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + AppEvents.refreshTaskApp.next(); }, - [rmf, refreshTaskAppCount], + [rmf], ); const uploadFileInputRef = React.useRef(null); @@ -352,9 +355,9 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea throw new Error('tasks api not available'); } await rmf.tasksApi.postFavoriteTaskFavoriteTasksPost(taskFavoriteRequest); - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + AppEvents.refreshTaskApp.next(); }, - [rmf, refreshTaskAppCount], + [rmf], ); const deleteFavoriteTask = React.useCallback['deleteFavoriteTask']>( @@ -367,9 +370,9 @@ export const AppBar = React.memo(({ extraToolbarItems }: AppBarProps): React.Rea } await rmf.tasksApi.deleteFavoriteTaskFavoriteTasksFavoriteTaskIdDelete(favoriteTask.id); - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + AppEvents.refreshTaskApp.next(); }, - [rmf, refreshTaskAppCount], + [rmf], ); //#endregion 'Favorite Task' diff --git a/packages/dashboard/src/components/tasks/tasks-app.tsx b/packages/dashboard/src/components/tasks/tasks-app.tsx index 292201bfe..1822058ee 100644 --- a/packages/dashboard/src/components/tasks/tasks-app.tsx +++ b/packages/dashboard/src/components/tasks/tasks-app.tsx @@ -58,6 +58,8 @@ import { RmfAppContext } from '../rmf-app'; import { TaskSummary } from './task-summary'; import { downloadCsvFull, downloadCsvMinimal } from './utils'; +const RefreshTaskQueueTableInterval = 5000; + interface TabPanelProps { children?: React.ReactNode; index: number; @@ -222,12 +224,27 @@ export const TasksApp = React.memo( const [sortFields, setSortFields] = React.useState({ model: undefined }); React.useEffect(() => { - const sub = AppEvents.refreshTaskAppCount.subscribe((currentValue) => { - setRefreshTaskAppCount(currentValue); + const sub = AppEvents.refreshTaskApp.subscribe({ + next: () => { + setRefreshTaskAppCount((oldValue) => ++oldValue); + }, }); return () => sub.unsubscribe(); }, []); + React.useEffect(() => { + const refreshTaskQueueTable = async () => { + AppEvents.refreshTaskApp.next(); + }; + const refreshInterval = window.setInterval( + refreshTaskQueueTable, + RefreshTaskQueueTableInterval, + ); + return () => { + clearInterval(refreshInterval); + }; + }, []); + // TODO: parameterize this variable const GET_LIMIT = 10; React.useEffect(() => { @@ -409,7 +426,7 @@ export const TasksApp = React.memo( } else { await rmf.tasksApi.delScheduledTasksScheduledTasksTaskIdDelete(task.id); } - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + AppEvents.refreshTaskApp.next(); // Set the default values setOpenDeleteScheduleDialog(false); @@ -472,7 +489,7 @@ export const TasksApp = React.memo( { - AppEvents.refreshTaskAppCount.next(refreshTaskAppCount + 1); + AppEvents.refreshTaskApp.next(); }} aria-label="Refresh" > From 3388e03eff12a39ee29c3d06c72049e8e002f50e Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Thu, 7 Sep 2023 17:35:53 +0800 Subject: [PATCH 03/28] Reverting to latest pnpm version when #6994 is fixed (#754) Signed-off-by: Aaron Chong --- .github/actions/bootstrap/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/bootstrap/action.yml b/.github/actions/bootstrap/action.yml index 545dff8e3..81425f590 100644 --- a/.github/actions/bootstrap/action.yml +++ b/.github/actions/bootstrap/action.yml @@ -13,7 +13,7 @@ runs: steps: - uses: pnpm/action-setup@v2.2.2 with: - version: '8.6.12' + version: 'latest' - uses: actions/setup-node@v2 with: node-version: '16' From 8a4415e68c35750ba75374a1795471dd3886ae5d Mon Sep 17 00:00:00 2001 From: Aaron Chong Date: Fri, 8 Sep 2023 20:13:36 +0800 Subject: [PATCH 04/28] Feature/form input required (#764) * Making form fields required Signed-off-by: Aaron Chong * Basic checks for each task description to allow submission Signed-off-by: Aaron Chong * Fix e2e tests, select coe for patrol explicitly Signed-off-by: Aaron Chong * Splitting interface and refactoring validity check Signed-off-by: Aaron Chong * Delivery quantity to positive int field, and sku to text field, monitor null or empty value Signed-off-by: Aaron Chong * Disable submit when clean zone is removed Signed-off-by: Aaron Chong --------- Signed-off-by: Aaron Chong --- .../create-task-from-any-tab.test.ts | 32 +- .../tests/ui-interactions/submit-task.test.ts | 15 +- packages/react-components/lib/form-inputs.tsx | 2 +- .../lib/tasks/create-task.tsx | 298 +++++++++--------- 4 files changed, 195 insertions(+), 152 deletions(-) diff --git a/packages/dashboard-e2e/tests/ui-interactions/create-task-from-any-tab.test.ts b/packages/dashboard-e2e/tests/ui-interactions/create-task-from-any-tab.test.ts index d1041f0b5..12bd2070b 100644 --- a/packages/dashboard-e2e/tests/ui-interactions/create-task-from-any-tab.test.ts +++ b/packages/dashboard-e2e/tests/ui-interactions/create-task-from-any-tab.test.ts @@ -5,7 +5,37 @@ describe('submit task', () => { const appBar = await getAppBar(); await (await appBar.$('button[aria-label="System Overview"]')).click(); await (await appBar.$('button[aria-label="new task"]')).click(); + await (await $('#task-type')).click(); + const getPatrolOption = async () => { + const options = await $$('[role=option]'); + for (const opt of options) { + const text = await opt.getText(); + if (text === 'Patrol') { + return opt; + } + } + return null; + }; + await browser.waitUntil(async () => !!(await getPatrolOption())); + const patrolOption = (await getPatrolOption())!; + await patrolOption.click(); + + await (await $('#place-input')).click(); + const getCoeOption = async () => { + const options = await $$('[role=option]'); + for (const opt of options) { + const text = await opt.getText(); + if (text === 'coe') { + return opt; + } + } + return null; + }; + await browser.waitUntil(async () => !!(await getCoeOption())); + const coeOption = (await getCoeOption())!; + await coeOption.click(); + await (await $('button[aria-label="Submit Now"]')).click(); await expect($('div=Successfully created task')).toBeDisplayed(); - }); + }).timeout(60000); }); diff --git a/packages/dashboard-e2e/tests/ui-interactions/submit-task.test.ts b/packages/dashboard-e2e/tests/ui-interactions/submit-task.test.ts index 7c7531244..d95124118 100644 --- a/packages/dashboard-e2e/tests/ui-interactions/submit-task.test.ts +++ b/packages/dashboard-e2e/tests/ui-interactions/submit-task.test.ts @@ -20,7 +20,20 @@ describe('submit task', () => { const patrolOption = (await getPatrolOption())!; await patrolOption.click(); - await (await $('#place-input')).setValue('coe'); + await (await $('#place-input')).click(); + const getCoeOption = async () => { + const options = await $$('[role=option]'); + for (const opt of options) { + const text = await opt.getText(); + if (text === 'coe') { + return opt; + } + } + return null; + }; + await browser.waitUntil(async () => !!(await getCoeOption())); + const coeOption = (await getCoeOption())!; + await coeOption.click(); await (await $('button[aria-label="Submit Now"]')).click(); await expect($('div=Successfully created task')).toBeDisplayed(); diff --git a/packages/react-components/lib/form-inputs.tsx b/packages/react-components/lib/form-inputs.tsx index 430d3eb0a..69510eafb 100644 --- a/packages/react-components/lib/form-inputs.tsx +++ b/packages/react-components/lib/form-inputs.tsx @@ -19,7 +19,7 @@ export function PositiveIntField({ value = 0, onChange, ...props }: PositiveIntF {...props} type="number" value={valueInput} - inputProps={{ min: 0 }} + inputProps={{ min: 1 }} onKeyDown={(ev) => { if ('-+.'.indexOf(ev.key) >= 0) { ev.preventDefault(); diff --git a/packages/react-components/lib/tasks/create-task.tsx b/packages/react-components/lib/tasks/create-task.tsx index 0e7617b92..2b74bf63b 100644 --- a/packages/react-components/lib/tasks/create-task.tsx +++ b/packages/react-components/lib/tasks/create-task.tsx @@ -42,23 +42,20 @@ import { ConfirmationDialog, ConfirmationDialogProps } from '../confirmation-dia import { PositiveIntField } from '../form-inputs'; // A bunch of manually defined descriptions to avoid using `any`. +interface Payload { + sku: string; + quantity: number; +} + +interface TaskPlace { + place: string; + handler: string; + payload: Payload; +} + interface DeliveryTaskDescription { - pickup: { - place: string; - handler: string; - payload: { - sku: string; - quantity: number; - }; - }; - dropoff: { - place: string; - handler: string; - payload: { - sku: string; - quantity: number; - }; - }; + pickup: TaskPlace; + dropoff: TaskPlace; } interface PatrolTaskDescription { @@ -72,6 +69,38 @@ interface CleanTaskDescription { type TaskDescription = DeliveryTaskDescription | PatrolTaskDescription | CleanTaskDescription; +const isNonEmptyString = (value: string): boolean => value.length > 0; +const isPositiveNumber = (value: number): boolean => value > 0; + +const isTaskPlaceValid = (place: TaskPlace): boolean => { + return ( + isNonEmptyString(place.place) && + isNonEmptyString(place.handler) && + isNonEmptyString(place.payload.sku) && + isPositiveNumber(place.payload.quantity) + ); +}; + +const isDeliveryTaskDescriptionValid = (taskDescription: DeliveryTaskDescription): boolean => { + return isTaskPlaceValid(taskDescription.pickup) && isTaskPlaceValid(taskDescription.dropoff); +}; + +const isPatrolTaskDescriptionValid = (taskDescription: PatrolTaskDescription): boolean => { + if (taskDescription.places.length === 0) { + return false; + } + for (const place of taskDescription.places) { + if (place.length === 0) { + return false; + } + } + return taskDescription.rounds > 0; +}; + +const isCleanTaskDescriptionValid = (taskDescription: CleanTaskDescription): boolean => { + return taskDescription.zone.length !== 0; +}; + const classes = { title: 'dialogue-info-value', selectFileBtn: 'create-task-selected-file-btn', @@ -139,6 +168,7 @@ interface DeliveryTaskFormProps { pickupPoints: Record; dropoffPoints: Record; onChange(taskDesc: TaskDescription): void; + allowSubmit(allow: boolean): void; } function DeliveryTaskForm({ @@ -146,8 +176,13 @@ function DeliveryTaskForm({ pickupPoints = {}, dropoffPoints = {}, onChange, + allowSubmit, }: DeliveryTaskFormProps) { const theme = useTheme(); + const onInputChange = (desc: DeliveryTaskDescription) => { + allowSubmit(isDeliveryTaskDescriptionValid(desc)); + onChange(desc); + }; return ( @@ -158,21 +193,22 @@ function DeliveryTaskForm({ fullWidth options={Object.keys(pickupPoints)} value={taskDesc.pickup.place} - onChange={(_ev, newValue) => - newValue !== null && - pickupPoints[newValue] && - onChange({ + onChange={(_ev, newValue) => { + const place = newValue ?? ''; + const handler = + newValue !== null && pickupPoints[newValue] ? pickupPoints[newValue] : ''; + onInputChange({ ...taskDesc, pickup: { ...taskDesc.pickup, - place: newValue, - handler: pickupPoints[newValue], + place: place, + handler: handler, }, - }) - } + }); + }} onBlur={(ev) => pickupPoints[(ev.target as HTMLInputElement).value] && - onChange({ + onInputChange({ ...taskDesc, pickup: { ...taskDesc.pickup, @@ -181,77 +217,50 @@ function DeliveryTaskForm({ }, }) } - renderInput={(params) => } + renderInput={(params) => ( + + )} /> - - newValue !== null && - onChange({ - ...taskDesc, - pickup: { - ...taskDesc.pickup, - payload: { - ...taskDesc.pickup.payload, - sku: newValue, - }, - }, - }) - } - onBlur={(ev) => - onChange({ + required + onChange={(ev) => { + onInputChange({ ...taskDesc, pickup: { ...taskDesc.pickup, payload: { ...taskDesc.pickup.payload, - sku: (ev.target as HTMLInputElement).value, + sku: ev.target.value, }, }, - }) - } - renderInput={(params) => } + }); + }} /> - - newValue !== null && - onChange({ - ...taskDesc, - pickup: { - ...taskDesc.pickup, - payload: { - ...taskDesc.pickup.payload, - quantity: typeof newValue == 'string' ? parseInt(newValue) : newValue, - }, - }, - }) - } - onBlur={(ev) => - onChange({ + label="Quantity" + value={taskDesc.pickup.payload.quantity} + onChange={(_ev, val) => { + console.log(val); + onInputChange({ ...taskDesc, pickup: { ...taskDesc.pickup, payload: { ...taskDesc.pickup.payload, - quantity: parseInt((ev.target as HTMLInputElement).value), + quantity: val, }, }, - }) - } - renderInput={(params) => } + }); + }} /> @@ -261,21 +270,22 @@ function DeliveryTaskForm({ fullWidth options={Object.keys(dropoffPoints)} value={taskDesc.dropoff.place} - onChange={(_ev, newValue) => - newValue !== null && - dropoffPoints[newValue] && - onChange({ + onChange={(_ev, newValue) => { + const place = newValue ?? ''; + const handler = + newValue !== null && dropoffPoints[newValue] ? dropoffPoints[newValue] : ''; + onInputChange({ ...taskDesc, dropoff: { ...taskDesc.dropoff, - place: newValue, - handler: dropoffPoints[newValue], + place: place, + handler: handler, }, - }) - } + }); + }} onBlur={(ev) => dropoffPoints[(ev.target as HTMLInputElement).value] && - onChange({ + onInputChange({ ...taskDesc, dropoff: { ...taskDesc.dropoff, @@ -284,77 +294,49 @@ function DeliveryTaskForm({ }, }) } - renderInput={(params) => } + renderInput={(params) => ( + + )} /> - - newValue !== null && - onChange({ + required + onChange={(ev) => { + onInputChange({ ...taskDesc, dropoff: { ...taskDesc.dropoff, payload: { ...taskDesc.dropoff.payload, - sku: newValue, + sku: ev.target.value, }, }, - }) - } - onBlur={(ev) => - onChange({ - ...taskDesc, - dropoff: { - ...taskDesc.dropoff, - payload: { - ...taskDesc.dropoff.payload, - sku: (ev.target as HTMLInputElement).value, - }, - }, - }) - } - renderInput={(params) => } + }); + }} /> - - newValue !== null && - onChange({ - ...taskDesc, - dropoff: { - ...taskDesc.dropoff, - payload: { - ...taskDesc.dropoff.payload, - quantity: typeof newValue == 'string' ? parseInt(newValue) : newValue, - }, - }, - }) - } - onBlur={(ev) => - onChange({ + label="Quantity" + value={taskDesc.dropoff.payload.quantity} + onChange={(_ev, val) => { + onInputChange({ ...taskDesc, dropoff: { ...taskDesc.dropoff, payload: { ...taskDesc.dropoff.payload, - quantity: parseInt((ev.target as HTMLInputElement).value), + quantity: val, }, }, - }) - } - renderInput={(params) => } + }); + }} /> @@ -400,10 +382,16 @@ interface PatrolTaskFormProps { taskDesc: PatrolTaskDescription; patrolWaypoints: string[]; onChange(patrolTaskDescription: PatrolTaskDescription): void; + allowSubmit(allow: boolean): void; } -function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange }: PatrolTaskFormProps) { +function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange, allowSubmit }: PatrolTaskFormProps) { const theme = useTheme(); + const onInputChange = (desc: PatrolTaskDescription) => { + allowSubmit(isPatrolTaskDescriptionValid(desc)); + onChange(desc); + }; + return ( @@ -414,14 +402,12 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange }: PatrolTaskFormP options={patrolWaypoints} onChange={(_ev, newValue) => newValue !== null && - onChange({ + onInputChange({ ...taskDesc, - places: taskDesc.places.concat(newValue).filter( - (el: string) => el, // filter null and empty str in places array - ), + places: taskDesc.places.concat(newValue).filter((el: string) => el), }) } - renderInput={(params) => } + renderInput={(params) => } /> @@ -430,7 +416,7 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange }: PatrolTaskFormP label="Loops" value={taskDesc.rounds} onChange={(_ev, val) => { - onChange({ + onInputChange({ ...taskDesc, rounds: val, }); @@ -442,7 +428,7 @@ function PatrolTaskForm({ taskDesc, patrolWaypoints, onChange }: PatrolTaskFormP places={taskDesc && taskDesc.places ? taskDesc.places : []} onClick={(places_index) => taskDesc.places.splice(places_index, 1) && - onChange({ + onInputChange({ ...taskDesc, }) } @@ -456,9 +442,15 @@ interface CleanTaskFormProps { taskDesc: CleanTaskDescription; cleaningZones: string[]; onChange(cleanTaskDescription: CleanTaskDescription): void; + allowSubmit(allow: boolean): void; } -function CleanTaskForm({ taskDesc, cleaningZones, onChange }: CleanTaskFormProps) { +function CleanTaskForm({ taskDesc, cleaningZones, onChange, allowSubmit }: CleanTaskFormProps) { + const onInputChange = (desc: CleanTaskDescription) => { + allowSubmit(isCleanTaskDescriptionValid(desc)); + onChange(desc); + }; + return ( - newValue !== null && - onChange({ + onChange={(_ev, newValue) => { + const zone = newValue ?? ''; + onInputChange({ ...taskDesc, - zone: newValue, - }) - } - onBlur={(ev) => onChange({ ...taskDesc, zone: (ev.target as HTMLInputElement).value })} - renderInput={(params) => } + zone: zone, + }); + }} + onBlur={(ev) => onInputChange({ ...taskDesc, zone: (ev.target as HTMLInputElement).value })} + renderInput={(params) => } /> ); } @@ -732,6 +724,7 @@ export function CreateTaskForm({ [taskRequests], ); const [submitting, setSubmitting] = React.useState(false); + const [formFullyFilled, setFormFullyFilled] = React.useState(false); const taskRequest = taskRequests[selectedTaskIdx]; const [openSchedulingDialog, setOpenSchedulingDialog] = React.useState(false); const [schedule, setSchedule] = React.useState({ @@ -776,6 +769,10 @@ export function CreateTaskForm({ updateTasks(); }; + const allowSubmit = (allow: boolean) => { + setFormFullyFilled(allow); + }; + const renderTaskDescriptionForm = () => { switch (taskRequest.category) { case 'clean': @@ -784,6 +781,7 @@ export function CreateTaskForm({ taskDesc={taskRequest.description as CleanTaskDescription} cleaningZones={cleaningZones} onChange={(desc) => handleTaskDescriptionChange('clean', desc)} + allowSubmit={allowSubmit} /> ); case 'patrol': @@ -792,6 +790,7 @@ export function CreateTaskForm({ taskDesc={taskRequest.description as PatrolTaskDescription} patrolWaypoints={patrolWaypoints} onChange={(desc) => handleTaskDescriptionChange('patrol', desc)} + allowSubmit={allowSubmit} /> ); case 'delivery': @@ -801,6 +800,7 @@ export function CreateTaskForm({ pickupPoints={pickupPoints} dropoffPoints={dropoffPoints} onChange={(desc) => handleTaskDescriptionChange('delivery', desc)} + allowSubmit={allowSubmit} /> ); default: @@ -979,8 +979,8 @@ export function CreateTaskForm({ description: favoriteTask.description, unix_millis_earliest_start_time: Date.now(), priority: favoriteTask.priority, - }] - ); + }, + ]); }} /> ); @@ -1127,7 +1127,7 @@ export function CreateTaskForm({ ) : null} - {acknowledged || onAcknowledge === undefined ? ( + {acknowledged ? ( - ) : ( + ) : onAcknowledge === undefined ? null : (