Skip to content

Commit 0789dc4

Browse files
feat(AddTaskDialog): add DateTimePicker for start field (#376)
- Replace DatePicker with DateTimePicker for start field in AddTaskDialog - Add DateTimePicker mock in AddTaskDialog.test.tsx - Add comprehensive tests for DateTime fields (render, update, clear, submit) - Support both date-only and datetime formats Contributes: #325
1 parent bf7b859 commit 0789dc4

3 files changed

Lines changed: 207 additions & 86 deletions

File tree

backend/utils/tw/add_task.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
4040
cmdArgs = append(cmdArgs, "due:"+dueDate)
4141
}
4242
if req.Start != "" {
43-
cmdArgs = append(cmdArgs, "start:"+req.Start)
43+
start, err := utils.ConvertISOToTaskwarriorFormat(req.Start)
44+
if err != nil {
45+
return fmt.Errorf("unexpected date format error: %v", err)
46+
}
47+
cmdArgs = append(cmdArgs, "start:"+start)
4448
}
4549
if len(req.Depends) > 0 {
4650
dependsStr := strings.Join(req.Depends, ",")

frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,15 +288,27 @@ export const AddTaskdialog = ({
288288
Start
289289
</Label>
290290
<div className="col-span-3">
291-
<DatePicker
292-
date={newTask.start ? new Date(newTask.start) : undefined}
293-
onDateChange={(date) => {
291+
<DateTimePicker
292+
date={
293+
newTask.start
294+
? new Date(
295+
newTask.start.includes('T')
296+
? newTask.start
297+
: `${newTask.start}T00:00:00`
298+
)
299+
: undefined
300+
}
301+
onDateTimeChange={(date, hasTime) => {
294302
setNewTask({
295303
...newTask,
296-
start: date ? format(date, 'yyyy-MM-dd') : '',
304+
start: date
305+
? hasTime
306+
? date.toISOString()
307+
: format(date, 'yyyy-MM-dd')
308+
: '',
297309
});
298310
}}
299-
placeholder="Select a start date"
311+
placeholder="Select start date and time"
300312
/>
301313
</div>
302314
</div>

frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx

Lines changed: 185 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ jest.mock('@/components/ui/date-picker', () => ({
2828
),
2929
}));
3030

31+
jest.mock('@/components/ui/date-time-picker', () => ({
32+
DateTimePicker: ({ onDateTimeChange, placeholder }: any) => (
33+
<div>
34+
<input
35+
data-testid="date-time-picker"
36+
placeholder={placeholder}
37+
onChange={(e) => {
38+
if (e.target.value) {
39+
const hasTime = e.target.value.includes('T');
40+
onDateTimeChange(new Date(e.target.value), hasTime);
41+
} else {
42+
onDateTimeChange(undefined, false);
43+
}
44+
}}
45+
/>
46+
</div>
47+
),
48+
}));
49+
3150
jest.mock('@/components/ui/select', () => {
3251
return {
3352
Select: ({ children, onValueChange, value }: any) => {
@@ -374,102 +393,188 @@ describe('AddTaskDialog Component', () => {
374393
});
375394

376395
describe('Date Fields', () => {
377-
const dateFields = [
378-
{ name: 'due', label: 'Due', placeholder: 'Select a due date' },
379-
{ name: 'start', label: 'Start', placeholder: 'Select a start date' },
380-
{ name: 'end', label: 'End', placeholder: 'Select an end date' },
381-
{ name: 'entry', label: 'Entry', placeholder: 'Select an entry date' },
382-
{ name: 'wait', label: 'Wait', placeholder: 'Select a wait date' },
383-
];
384-
385-
test.each(dateFields.filter((field) => field.name !== 'due'))(
386-
'renders $name date picker with correct placeholder',
387-
({ placeholder }) => {
388-
mockProps.isOpen = true;
389-
render(<AddTaskdialog {...mockProps} />);
390-
391-
const datePicker = screen.getByPlaceholderText(placeholder);
392-
expect(datePicker).toBeInTheDocument();
393-
}
394-
);
395-
396-
test('renders due date picker with correct placeholder', () => {
397-
mockProps.isOpen = true;
398-
render(<AddTaskdialog {...mockProps} />);
396+
describe('DateTime Fields', () => {
397+
const dateTimeFields = [
398+
{ name: 'due', label: 'Due', placeholder: 'Select due date and time' },
399+
{
400+
name: 'start',
401+
label: 'Start',
402+
placeholder: 'Select start date and time',
403+
},
404+
];
399405

400-
const dueDateButton = screen.getByText('Select due date and time');
401-
expect(dueDateButton).toBeInTheDocument();
402-
});
406+
test.each(dateTimeFields)(
407+
'renders $name date-time picker with correct placeholder',
408+
({ placeholder }) => {
409+
mockProps.isOpen = true;
410+
render(<AddTaskdialog {...mockProps} />);
403411

404-
test.each(dateFields.filter((field) => field.name !== 'due'))(
405-
'updates $name when user selects a date',
406-
({ name, placeholder }) => {
407-
mockProps.isOpen = true;
408-
render(<AddTaskdialog {...mockProps} />);
412+
const picker = screen.getByPlaceholderText(placeholder);
413+
expect(picker).toBeInTheDocument();
414+
}
415+
);
409416

410-
const datePicker = screen.getByPlaceholderText(placeholder);
411-
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
417+
test.each(dateTimeFields)(
418+
'updates $name with date only when no time is selected',
419+
({ name, placeholder }) => {
420+
mockProps.isOpen = true;
421+
render(<AddTaskdialog {...mockProps} />);
412422

413-
expect(mockProps.setNewTask).toHaveBeenCalledWith({
414-
...mockProps.newTask,
415-
[name]: '2025-12-25',
416-
});
417-
}
418-
);
423+
const picker = screen.getByPlaceholderText(placeholder);
424+
fireEvent.change(picker, { target: { value: '2025-12-25' } });
419425

420-
// Special test for due date with DateTimePicker
421-
test('updates due when user selects a date and time', () => {
422-
mockProps.isOpen = true;
423-
render(<AddTaskdialog {...mockProps} />);
426+
expect(mockProps.setNewTask).toHaveBeenLastCalledWith({
427+
...mockProps.newTask,
428+
[name]: '2025-12-25',
429+
});
430+
}
431+
);
432+
433+
test.each(dateTimeFields)(
434+
'updates $name with full datetime when time is selected',
435+
({ name, placeholder }) => {
436+
mockProps.isOpen = true;
437+
render(<AddTaskdialog {...mockProps} />);
438+
const picker = screen.getByPlaceholderText(placeholder);
439+
440+
fireEvent.change(picker, {
441+
target: { value: '2025-12-25T14:30:00' },
442+
});
443+
expect(mockProps.setNewTask).toHaveBeenLastCalledWith(
444+
expect.objectContaining({
445+
[name]: expect.any(String),
446+
})
447+
);
448+
449+
const callArgs = mockProps.setNewTask.mock.calls.at(-1)![0];
450+
expect(callArgs[name]).toContain('T');
451+
}
452+
);
453+
454+
test.each(dateTimeFields)(
455+
'allows empty $name date (optional field)',
456+
({ name, placeholder }) => {
457+
mockProps.isOpen = true;
458+
render(<AddTaskdialog {...mockProps} />);
459+
460+
const picker = screen.getByPlaceholderText(placeholder);
461+
462+
fireEvent.change(picker, {
463+
target: { value: '2025-12-25T14:30:00' },
464+
});
465+
mockProps.setNewTask.mockClear();
466+
fireEvent.change(picker, { target: { value: '' } });
424467

425-
const dueDateButton = screen.getByText('Select due date and time');
426-
expect(dueDateButton).toBeInTheDocument();
468+
expect(mockProps.setNewTask).toHaveBeenLastCalledWith({
469+
...mockProps.newTask,
470+
[name]: '',
471+
});
472+
}
473+
);
474+
475+
test.each(dateTimeFields)(
476+
'submits task with $name date when provided',
477+
({ name }) => {
478+
mockProps.isOpen = true;
479+
mockProps.newTask = {
480+
...mockProps.newTask,
481+
[name]: '2025-12-25T14:30:00.000Z',
482+
};
483+
render(<AddTaskdialog {...mockProps} />);
484+
485+
const picker = screen.getByPlaceholderText(
486+
`Select ${name} date and time`
487+
);
488+
fireEvent.change(picker, {
489+
target: { value: '2025-12-25T14:30:00' },
490+
});
491+
492+
const submitButton = screen.getByRole('button', {
493+
name: /add task/i,
494+
});
495+
fireEvent.click(submitButton);
496+
497+
expect(mockProps.onSubmit).toHaveBeenLastCalledWith(
498+
expect.objectContaining({
499+
[name]: '2025-12-25T14:30:00.000Z',
500+
})
501+
);
502+
}
503+
);
427504
});
428505

429-
test.each(dateFields.filter((field) => field.name !== 'due'))(
430-
'allows empty $name date (optional field)',
431-
({ name, placeholder }) => {
432-
mockProps.isOpen = true;
433-
render(<AddTaskdialog {...mockProps} />);
506+
describe('DatePicker fields', () => {
507+
const dateOnlyFields = [
508+
{ name: 'end', label: 'End', placeholder: 'Select an end date' },
509+
{ name: 'entry', label: 'Entry', placeholder: 'Select an entry date' },
510+
{ name: 'wait', label: 'Wait', placeholder: 'Select a wait date' },
511+
];
434512

435-
const datePicker = screen.getByPlaceholderText(placeholder);
513+
test.each(dateOnlyFields)(
514+
'renders $name date picker with correct placeholder',
515+
({ placeholder }) => {
516+
mockProps.isOpen = true;
517+
render(<AddTaskdialog {...mockProps} />);
436518

437-
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
438-
mockProps.setNewTask.mockClear();
439-
fireEvent.change(datePicker, { target: { value: '' } });
519+
const datePicker = screen.getByPlaceholderText(placeholder);
520+
expect(datePicker).toBeInTheDocument();
521+
}
522+
);
440523

441-
expect(mockProps.setNewTask).toHaveBeenCalledWith({
442-
...mockProps.newTask,
443-
[name]: '',
444-
});
445-
}
446-
);
524+
test.each(dateOnlyFields)(
525+
'updates $name when user selects a date',
526+
({ name, placeholder }) => {
527+
mockProps.isOpen = true;
528+
render(<AddTaskdialog {...mockProps} />);
447529

448-
// Special test for due date with DateTimePicker
449-
test('allows empty due date (optional field)', () => {
450-
mockProps.isOpen = true;
451-
render(<AddTaskdialog {...mockProps} />);
530+
const datePicker = screen.getByPlaceholderText(placeholder);
531+
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
452532

453-
const dueDateButton = screen.getByText('Select due date and time');
454-
expect(dueDateButton).toBeInTheDocument();
455-
});
533+
expect(mockProps.setNewTask).toHaveBeenCalledWith({
534+
...mockProps.newTask,
535+
[name]: '2025-12-25',
536+
});
537+
}
538+
);
539+
540+
test.each(dateOnlyFields)(
541+
'allows empty $name date (optional field)',
542+
({ name, placeholder }) => {
543+
mockProps.isOpen = true;
544+
render(<AddTaskdialog {...mockProps} />);
545+
546+
const datePicker = screen.getByPlaceholderText(placeholder);
456547

457-
test.each(dateFields)(
458-
'submits task with $name date when provided',
459-
({ name }) => {
460-
mockProps.isOpen = true;
461-
mockProps.newTask = {
462-
...mockProps.newTask,
463-
[name]: '2025-12-25',
464-
};
465-
render(<AddTaskdialog {...mockProps} />);
548+
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
549+
mockProps.setNewTask.mockClear();
550+
fireEvent.change(datePicker, { target: { value: '' } });
466551

467-
const submitButton = screen.getByRole('button', { name: /add task/i });
468-
fireEvent.click(submitButton);
552+
expect(mockProps.setNewTask).toHaveBeenCalledWith({
553+
...mockProps.newTask,
554+
[name]: '',
555+
});
556+
}
557+
);
469558

470-
expect(mockProps.onSubmit).toHaveBeenCalledWith(mockProps.newTask);
471-
}
472-
);
559+
test.each(dateOnlyFields)(
560+
'submits task with $name date when provided',
561+
({ name }) => {
562+
mockProps.isOpen = true;
563+
mockProps.newTask = {
564+
...mockProps.newTask,
565+
[name]: '2025-12-25',
566+
};
567+
render(<AddTaskdialog {...mockProps} />);
568+
569+
const submitButton = screen.getByRole('button', {
570+
name: /add task/i,
571+
});
572+
fireEvent.click(submitButton);
573+
574+
expect(mockProps.onSubmit).toHaveBeenCalledWith(mockProps.newTask);
575+
}
576+
);
577+
});
473578
});
474579

475580
describe('Depends Field', () => {

0 commit comments

Comments
 (0)