Skip to content

Commit dbcc89c

Browse files
committed
feat(AddTaskDialog): add end date datetime picker
- Add DateTimePicker for end date in AddTaskDialog - Mark task as completed when end date is set via `task done` command - Show inline warning when end date is selected - Update and add tests
1 parent 7eea998 commit dbcc89c

3 files changed

Lines changed: 60 additions & 90 deletions

File tree

backend/utils/tw/add_task.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
6464
}
6565
cmdArgs = append(cmdArgs, "wait:"+wait)
6666
}
67-
if req.End != "" {
68-
cmdArgs = append(cmdArgs, "end:"+req.End)
69-
}
7067
if req.Recur != "" && dueDate != "" {
7168
cmdArgs = append(cmdArgs, "recur:"+req.Recur)
7269
}
@@ -83,7 +80,8 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
8380
return fmt.Errorf("failed to add task: %v\n %v", err, cmdArgs)
8481
}
8582

86-
if len(req.Annotations) > 0 {
83+
var taskID string
84+
if req.End != "" || len(req.Annotations) > 0 {
8785
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "export")
8886
if err != nil {
8987
return fmt.Errorf("failed to export tasks: %v", err)
@@ -100,7 +98,20 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
10098

10199
lastTask := tasks[len(tasks)-1]
102100
taskID := fmt.Sprintf("%d", lastTask.ID)
101+
}
103102

103+
if req.End != "" {
104+
end, err := utils.ConvertISOToTaskwarriorFormat(req.End)
105+
if err != nil {
106+
return fmt.Errorf("unexpected end date format error: %v", err)
107+
}
108+
doneArgs := []string{"rc.confirmation=off", taskID, "done", "end:" + end}
109+
if err := utils.ExecCommandInDir(tempDir, "task", doneArgs...); err != nil {
110+
return fmt.Errorf("failed to complete task with end date: %v", err)
111+
}
112+
}
113+
114+
if len(req.Annotations) > 0 {
104115
for _, annotation := range req.Annotations {
105116
if annotation.Description != "" {
106117
annotateArgs := []string{"rc.confirmation=off", taskID, "annotate", annotation.Description}

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useState, useEffect, useRef } from 'react';
22
import { Badge } from '@/components/ui/badge';
33
import { Button } from '@/components/ui/button';
4-
import { DatePicker } from '@/components/ui/date-picker';
54
import { DateTimePicker } from '@/components/ui/date-time-picker';
65
import {
76
Dialog,
@@ -401,17 +400,36 @@ export const AddTaskdialog = ({
401400
End
402401
</Label>
403402
<div className="col-span-3">
404-
<DatePicker
403+
<DateTimePicker
405404
ref={(element) => (inputRefs.current.end = element)}
406-
date={newTask.end ? new Date(newTask.end) : undefined}
407-
onDateChange={(date) => {
405+
date={
406+
newTask.end
407+
? new Date(
408+
newTask.end.includes('T')
409+
? newTask.end
410+
: `${newTask.end}T00:00:00`
411+
)
412+
: undefined
413+
}
414+
onDateTimeChange={(date, hasTime) => {
408415
setNewTask({
409416
...newTask,
410-
end: date ? format(date, 'yyyy-MM-dd') : '',
417+
end: date
418+
? hasTime
419+
? date.toISOString()
420+
: format(date, 'yyyy-MM-dd')
421+
: '',
411422
});
412423
}}
413-
placeholder="Select an end date"
424+
placeholder="Select end date and time"
414425
/>
426+
{newTask.end && (
427+
<div className="mt-1.5 pl-2.5 border-l-2 border-amber-500/60">
428+
<p className="text-xs text-amber-400 leading-tight">
429+
Task will be marked as completed
430+
</p>
431+
</div>
432+
)}
415433
</div>
416434
</div>
417435
<div

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

Lines changed: 21 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,6 @@ jest.mock('date-fns', () => ({
1212
}),
1313
}));
1414

15-
jest.mock('@/components/ui/date-picker', () => ({
16-
DatePicker: ({ onDateChange, placeholder }: any) => (
17-
<input
18-
data-testid="date-picker"
19-
placeholder={placeholder}
20-
onChange={(e) => {
21-
if (e.target.value) {
22-
onDateChange(new Date(e.target.value));
23-
} else {
24-
onDateChange(null);
25-
}
26-
}}
27-
/>
28-
),
29-
}));
30-
3115
jest.mock('@/components/ui/date-time-picker', () => ({
3216
DateTimePicker: ({ onDateTimeChange, placeholder }: any) => (
3317
<div>
@@ -431,6 +415,11 @@ describe('AddTaskDialog Component', () => {
431415
label: 'Entry',
432416
placeholder: 'Select entry date and time',
433417
},
418+
{
419+
name: 'end',
420+
label: 'End',
421+
placeholder: 'Select end date and time',
422+
},
434423
];
435424

436425
test.each(dateTimeFields)(
@@ -533,75 +522,27 @@ describe('AddTaskDialog Component', () => {
533522
);
534523
});
535524

536-
describe('DatePicker fields', () => {
537-
const dateOnlyFields = [
538-
{ name: 'end', label: 'End', placeholder: 'Select an end date' },
539-
];
540-
541-
test.each(dateOnlyFields)(
542-
'renders $name date picker with correct placeholder',
543-
({ placeholder }) => {
544-
mockProps.isOpen = true;
545-
render(<AddTaskdialog {...mockProps} />);
525+
describe('End date warning message', () => {
526+
test('should show warning when end date is selected', () => {
527+
mockProps.isOpen = true;
528+
mockProps.newTask.end = '2026-01-18';
546529

547-
const datePicker = screen.getByPlaceholderText(placeholder);
548-
expect(datePicker).toBeInTheDocument();
549-
}
550-
);
530+
render(<AddTaskdialog {...mockProps} />);
551531

552-
test.each(dateOnlyFields)(
553-
'updates $name when user selects a date',
554-
({ name, placeholder }) => {
555-
mockProps.isOpen = true;
556-
render(<AddTaskdialog {...mockProps} />);
557-
558-
const datePicker = screen.getByPlaceholderText(placeholder);
559-
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
560-
561-
expect(mockProps.setNewTask).toHaveBeenCalledWith({
562-
...mockProps.newTask,
563-
[name]: '2025-12-25',
564-
});
565-
}
566-
);
567-
568-
test.each(dateOnlyFields)(
569-
'allows empty $name date (optional field)',
570-
({ name, placeholder }) => {
571-
mockProps.isOpen = true;
572-
render(<AddTaskdialog {...mockProps} />);
573-
574-
const datePicker = screen.getByPlaceholderText(placeholder);
575-
576-
fireEvent.change(datePicker, { target: { value: '2025-12-25' } });
577-
mockProps.setNewTask.mockClear();
578-
fireEvent.change(datePicker, { target: { value: '' } });
579-
580-
expect(mockProps.setNewTask).toHaveBeenCalledWith({
581-
...mockProps.newTask,
582-
[name]: '',
583-
});
584-
}
585-
);
532+
expect(
533+
screen.getByText(/task will be marked as completed/i)
534+
).toBeInTheDocument();
535+
});
586536

587-
test.each(dateOnlyFields)(
588-
'submits task with $name date when provided',
589-
({ name }) => {
590-
mockProps.isOpen = true;
591-
mockProps.newTask = {
592-
...mockProps.newTask,
593-
[name]: '2025-12-25',
594-
};
595-
render(<AddTaskdialog {...mockProps} />);
537+
test('should not show warning when end date is empty', () => {
538+
mockProps.isOpen = true;
596539

597-
const submitButton = screen.getByRole('button', {
598-
name: /add task/i,
599-
});
600-
fireEvent.click(submitButton);
540+
render(<AddTaskdialog {...mockProps} />);
601541

602-
expect(mockProps.onSubmit).toHaveBeenCalledWith(mockProps.newTask);
603-
}
604-
);
542+
expect(
543+
screen.queryByText(/task will be marked as completed/i)
544+
).not.toBeInTheDocument();
545+
});
605546
});
606547
});
607548

0 commit comments

Comments
 (0)