Skip to content

Commit

Permalink
[Frontend] Improve select date widgets (#860)
Browse files Browse the repository at this point in the history
  • Loading branch information
johanah29 authored Jun 14, 2024
1 parent dae2a50 commit 3a89f05
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ const IndexScenarioComponent: FunctionComponent<{ scenario: ScenarioStore }> = (
</Tabs>
<div className={classes.scheduling}>
{!cronExpression && (
<IconButton size="small" style={{ cursor: 'default', marginRight: 5 }} disabled={true}>
<UpdateOutlined />
<IconButton size="small" onClick={() => setOpenScenarioRecurringFormDialog(true)} style={{ marginRight: 5 }}>
<UpdateOutlined color="primary" />
</IconButton>
)}
{cronExpression && !scenario.scenario_recurrence && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const ScenarioHeader = ({
size="small"
onClick={() => setOpenScenarioRecurringFormDialog(true)}
>
{t('Simulate Now')}
{t('Launch')}
</Button>
)}
<ScenarioPopover scenario={scenario} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Stack, Switch } from '@mui/material';
import { DateTimePicker, TimePicker } from '@mui/x-date-pickers';
import { TimePicker, DateTimePicker } from '@mui/x-date-pickers';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
Expand Down Expand Up @@ -103,16 +103,31 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
},
).refine(
(data) => {
if (data.endDate) {
return new Date(data.endDate).getTime() > new Date(data.startDate).getTime();
if (['daily', 'weekly', 'monthly'].includes(selectRecurring)) {
if (data.endDate) {
return new Date(data.endDate).getTime() > new Date(data.startDate).getTime();
}
}

return true;
},
{
message: t('End date need to be stricly after start date'),
path: ['endDate'],
},
),
)
.refine(
(data) => {
if (data.startDate) {
return new Date(data.startDate).getTime() >= new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime();
}
return true;
},
{
message: t('Start date should be at least today'),
path: ['startDate'],
},
),
),
});

Expand All @@ -137,6 +152,7 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
reset(defaultFormValues());
setOpen(false);
};

return (
<Dialog
open={open}
Expand Down Expand Up @@ -166,7 +182,7 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
render={({ field, fieldState }) => (
<DateTimePicker
views={['year', 'month', 'day']}
value={field.value}
value={(field.value)}
minDate={new Date(new Date().setUTCHours(0, 0, 0, 0)).toISOString()}
onChange={(startDate) => {
return (startDate ? field.onChange(new Date(startDate).toISOString()) : field.onChange(''));
Expand Down Expand Up @@ -254,8 +270,11 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
name="time"
render={({ field, fieldState }) => (
<TimePicker
label={t('Hour')}
label={t('Scheduling_time')}
openTo="hours"
timeSteps={{ minutes: 15 }}
skipDisabled
thresholdToRenderTimeInASingleColumn={100}
closeOnSelect={false}
value={field.value}
minTime={['noRepeat'].includes(selectRecurring) && new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime() === new Date(getValues('startDate')).getTime() ? new Date().toISOString() : null}
Expand All @@ -278,7 +297,7 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
render={({ field, fieldState }) => (
<DateTimePicker
views={['year', 'month', 'day']}
value={field.value || null}
value={(field.value || null)}
minDate={new Date(new Date().setUTCHours(24, 0, 0, 0)).toISOString()}
onChange={(endDate) => {
return (endDate ? field.onChange(new Date(new Date(endDate).setUTCHours(0, 0, 0, 0)).toISOString()) : field.onChange(''));
Expand All @@ -305,7 +324,7 @@ const ScenarioRecurringFormDialog: React.FC<Props> = ({ onSubmit, selectRecurrin
color="secondary"
type="submit"
>
{t('Start')}
{t('Save')}
</Button>
</DialogActions>
</form>
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState } from 'react';
import { Button, FormControlLabel, Switch, Stack } from '@mui/material';
import { DatePicker, TimePicker } from '@mui/x-date-pickers';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ExerciseUpdateStartDateInput } from '../../../../utils/api-types';
import { useFormatter } from '../../../../components/i18n';
import { zodImplement } from '../../../../utils/Zod';
import { minutesInFuture } from '../../../../utils/Time';

interface Props {
onSubmit: SubmitHandler<ExerciseUpdateStartDateInput>;
initialValues?: ExerciseUpdateStartDateInput;
handleClose: () => void;
}

interface ExerciseStartDateAndTime {
date: string;
time: string;
}

// eslint-disable-next-line no-underscore-dangle
const _MS_DELAY_TOO_CLOSE = 1000 * 60 * 2;

const ExerciseDateForm: React.FC<Props> = ({
onSubmit,
handleClose,
initialValues,
}) => {
const { t } = useFormatter();

const defaultFormValues = () => {
if (initialValues?.exercise_start_date) {
return ({ date: initialValues.exercise_start_date, time: initialValues.exercise_start_date });
}
return ({
date: new Date(new Date().setUTCHours(0, 0, 0, 0)).toISOString(),
time: minutesInFuture(5).toISOString(),
});
};

const [checked, setChecked] = useState(!initialValues?.exercise_start_date);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setChecked(event.target.checked);
};

const submit = (data: ExerciseStartDateAndTime) => {
if (checked) {
onSubmit({ exercise_start_date: '' });
} else {
const date = new Date(data.date);
const time = new Date(data.time);
date.setHours(time.getHours());
date.setMinutes(time.getMinutes());
date.setSeconds(time.getSeconds());
onSubmit({ exercise_start_date: date.toISOString() });
}
};

const {
control,
handleSubmit,
clearErrors,
getValues,
} = useForm<ExerciseStartDateAndTime>({
defaultValues: defaultFormValues(),
resolver: zodResolver(
zodImplement<ExerciseStartDateAndTime>().with({
date: z.string().min(1, t('Required')),
time: z.string().min(1, t('Required')),
}).refine(
(data) => {
if (data.date) {
return new Date(data.date).getTime() >= new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime();
}
return true;
},
{
message: t('Date should be at least today'),
path: ['date'],
},
)
.refine(
(data) => {
if (data.time) {
return (new Date().getTime() + _MS_DELAY_TOO_CLOSE) < new Date(data.time).getTime();
}
return true;
},
{
message: t('The time and start date do not match, as the time provided is either too close to the current moment or in the past'),
path: ['time'],
},
),
),
});

return (
<form id="exerciseDateForm" onSubmit={handleSubmit(submit)}>
<FormControlLabel
control={<Switch onChange={handleChange} checked={checked} />}
label={t('Manual launch')}
/>

<Stack spacing={{ xs: 2 }}>
<Controller
control={control}
name="date"
render={({ field, fieldState }) => (
<DatePicker
views={['year', 'month', 'day']}
label={t('Start date (optional)')}
disabled={checked}
minDate={new Date(new Date().setUTCHours(0, 0, 0, 0)).toISOString()}
value={(field.value)}
onChange={(date) => {
return (date ? field.onChange(new Date(date).toISOString()) : field.onChange(''));
}}
onAccept={() => {
clearErrors('time');
}}
slotProps={{
textField: {
fullWidth: true,
error: !!fieldState.error,
helperText: fieldState.error && fieldState.error?.message,
},
}}
/>
)}
/>

<Controller
control={control}
name="time"
render={({ field, fieldState }) => (
<TimePicker
label={t('Scheduling_time')}
openTo="hours"
timeSteps={{ minutes: 15 }}
skipDisabled
thresholdToRenderTimeInASingleColumn={100}
disabled={checked}
closeOnSelect={false}
value={field.value}
minTime={new Date(new Date().setUTCHours(0, 0, 0, 0)).getTime() === new Date(getValues('date')).getTime() ? new Date().toISOString() : null}
onChange={(time) => (time ? field.onChange(new Date(time).toISOString()) : field.onChange(''))}
slotProps={{
textField: {
fullWidth: true,
error: !!fieldState.error,
helperText: fieldState.error && fieldState.error?.message,
},
}}
/>
)}
/>
</Stack>

<div style={{ float: 'right', marginTop: 20 }}>
{handleClose && (
<Button
onClick={handleClose.bind(this)}
style={{ marginRight: 10 }}
>
{t('Cancel')}
</Button>
)}
<Button
color="secondary"
type="submit"
>
{t('Save')}
</Button>
</div>
</form>
);
};

export default ExerciseDateForm;
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,22 @@ const ExerciseDatePopover: React.FC<Props> = ({ exercise }) => {
return (
<>
<Tooltip title={(t('Modify the scheduling'))}>
<IconButton size="small" onClick={() => setOpenEdit(true)} style={{ marginRight: 5 }}>
<UpdateOutlined color="primary" />
<IconButton size="small" color="primary" onClick={() => setOpenEdit(true)} style={{ marginRight: 5 }} disabled={exercise.exercise_status !== 'SCHEDULED'}>
<UpdateOutlined />
</IconButton>
</Tooltip>
<Dialog
TransitionComponent={Transition}
open={openEdit}
onClose={() => setOpenEdit(false)}
PaperProps={{ elevation: 1 }}
maxWidth="xs"
fullWidth
>
<DialogTitle>{t('Update the simulation')}</DialogTitle>
<DialogContent>
<ExerciseDateForm
initialValues={initialValues}
editing
onSubmit={onSubmitEdit}
handleClose={() => setOpenEdit(false)}
/>
Expand Down
Loading

0 comments on commit 3a89f05

Please sign in to comment.