Skip to content

Commit 1efa560

Browse files
authored
Allow ending a recurrence while completing a task (#219)
1 parent 0403cd9 commit 1efa560

File tree

8 files changed

+63
-27
lines changed

8 files changed

+63
-27
lines changed

apiserver/internal/apis/task.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,16 @@ func (h *TasksAPIHandler) completeTask(c *gin.Context) {
166166
return
167167
}
168168

169-
status, response := h.tService.CompleteTask(c, currentIdentity.UserID, id)
169+
endRecurrenceStr := c.DefaultQuery("endRecurrence", "false")
170+
endRecurrence, err := strconv.ParseBool(endRecurrenceStr)
171+
if err != nil {
172+
c.JSON(http.StatusBadRequest, gin.H{
173+
"error": "Invalid endRecurrence value",
174+
})
175+
return
176+
}
177+
178+
status, response := h.tService.CompleteTask(c, currentIdentity.UserID, id, endRecurrence)
170179
c.JSON(status, response)
171180
}
172181

apiserver/internal/services/tasks/handlers.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,19 @@ func (h *TasksMessageHandler) updateDueDate(ctx context.Context, userID int, msg
157157
}
158158

159159
func (h *TasksMessageHandler) completeTask(ctx context.Context, userID int, msg ws.WSMessage) *ws.WSResponse {
160-
var id int
161-
if err := json.Unmarshal(msg.Data, &id); err != nil {
160+
var req struct {
161+
ID int `json:"id"`
162+
EndRecurrence bool `json:"endRecurrence"`
163+
}
164+
if err := json.Unmarshal(msg.Data, &req); err != nil {
162165
return &ws.WSResponse{
163166
Status: http.StatusBadRequest,
164167
Data: gin.H{
165-
"error": "Invalid task ID",
168+
"error": "Invalid request data",
166169
},
167170
}
168171
}
169-
status, response := h.ts.CompleteTask(ctx, userID, id)
172+
status, response := h.ts.CompleteTask(ctx, userID, req.ID, req.EndRecurrence)
170173
return &ws.WSResponse{
171174
Status: status,
172175
Data: response,

apiserver/internal/services/tasks/task.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ func (s *TaskService) UpdateDueDate(ctx context.Context, userID, taskID int, req
382382
}
383383
}
384384

385-
func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int) (int, interface{}) {
385+
func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int, endRecurrence bool) (int, interface{}) {
386386
log := logging.FromContext(ctx)
387387

388388
task, err := s.t.GetTask(ctx, taskID)
@@ -394,12 +394,15 @@ func (s *TaskService) CompleteTask(ctx context.Context, userID, taskID int) (int
394394
}
395395

396396
var completedDate time.Time = time.Now().UTC()
397-
var nextDueDate *time.Time
398-
nextDueDate, err = tRepo.ScheduleNextDueDate(task, completedDate)
399-
if err != nil {
400-
log.Errorf("error scheduling next due date: %s", err.Error())
401-
return http.StatusInternalServerError, gin.H{
402-
"error": fmt.Sprintf("Error scheduling next due date: %s", err),
397+
var nextDueDate *time.Time = nil
398+
399+
if !endRecurrence {
400+
nextDueDate, err = tRepo.ScheduleNextDueDate(task, completedDate)
401+
if err != nil {
402+
log.Errorf("error scheduling next due date: %s", err.Error())
403+
return http.StatusInternalServerError, gin.H{
404+
"error": fmt.Sprintf("Error scheduling next due date: %s", err),
405+
}
403406
}
404407
}
405408

frontend/src/api/tasks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const GetTasks = async (): Promise<TasksResponse> =>
2525
export const GetCompletedTasks = async (): Promise<TasksResponse> =>
2626
await Request<TasksResponse>(`/tasks/completed`)
2727

28-
export const MarkTaskComplete = async (id: number): Promise<SingleTaskResponse> =>
29-
await Request<SingleTaskResponse>(`/tasks/${id}/do`, 'POST')
28+
export const MarkTaskComplete = async (id: number, endRecurrence: boolean): Promise<SingleTaskResponse> =>
29+
await Request<SingleTaskResponse>(`/tasks/${id}/do?endRecurrence=${endRecurrence}`, 'POST')
3030

3131
export const SkipTask = async (id: number): Promise<SingleTaskResponse> =>
3232
await Request<SingleTaskResponse>(`/tasks/${id}/skip`, 'POST')

frontend/src/store/tasksSlice.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export const fetchCompletedTasks = createAsyncThunk(
8484

8585
export const completeTask = createAsyncThunk(
8686
'tasks/completeTask',
87-
async (taskId: number) => {
88-
const response = await MarkTaskComplete(taskId)
87+
async (req: { taskId: number, endRecurrence: boolean }) => {
88+
const response = await MarkTaskComplete(req.taskId, req.endRecurrence)
8989
return response.task
9090
},
9191
)

frontend/src/views/Tasks/MyTasks.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { skipTask, deleteTask, updateDueDate, setGroupBy, toggleGroup, toggleShowCompleted, fetchCompletedTasks } from '@/store/tasksSlice'
1+
import { skipTask, deleteTask, updateDueDate, setGroupBy, toggleGroup, toggleShowCompleted, fetchCompletedTasks, completeTask } from '@/store/tasksSlice'
22
import { Task, TASK_UPDATE_EVENT } from '@/models/task'
33
import {
44
ExpandCircleDown,
@@ -12,6 +12,7 @@ import {
1212
SwitchAccessShortcut,
1313
Visibility,
1414
VisibilityOff,
15+
PublishedWithChanges,
1516
} from '@mui/icons-material'
1617
import {
1718
Container,
@@ -54,6 +55,7 @@ type MyTasksProps = {
5455
toggleShowCompleted: () => void
5556
fetchCompletedTasks: () => Promise<any>
5657

58+
completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
5759
deleteTask: (taskId: number) => Promise<any>
5860
skipTask: (taskId: number) => Promise<any>
5961
updateDueDate: (taskId: number, dueDate: string) => Promise<any>
@@ -187,6 +189,18 @@ class MyTasksImpl extends React.Component<MyTasksProps, MyTasksState> {
187189
this.props.navigate(NavigationPaths.TaskHistory(task.id))
188190
}
189191

192+
private onCompleteAndStopRecurrenceClicked = async () => {
193+
const { contextMenuTask: task } = this.state
194+
if (task === null) {
195+
throw new Error('Attempted to complete and stop recurrence without a reference')
196+
}
197+
198+
await this.props.completeTask(task.id, true)
199+
200+
this.dismissMoreMenu()
201+
this.onEvent('completed')
202+
}
203+
190204
private onSkipTaskClicked = async () => {
191205
const { contextMenuTask: task } = this.state
192206
if (task === null) {
@@ -384,10 +398,16 @@ class MyTasksImpl extends React.Component<MyTasksProps, MyTasksState> {
384398
ref={this.menuRef}
385399
>
386400
{contextMenuTask && contextMenuTask.frequency.type !== 'once' && (
387-
<MenuItem onClick={this.onSkipTaskClicked}>
388-
<SwitchAccessShortcut />
389-
Skip to next due date
390-
</MenuItem>
401+
<>
402+
<MenuItem onClick={this.onCompleteAndStopRecurrenceClicked}>
403+
<PublishedWithChanges />
404+
Complete and end recurrence
405+
</MenuItem>
406+
<MenuItem onClick={this.onSkipTaskClicked}>
407+
<SwitchAccessShortcut />
408+
Skip to next due date
409+
</MenuItem>
410+
</>
391411
)}
392412
<MenuItem onClick={this.onChangeDueDateClicked}>
393413
<MoreTime />
@@ -511,6 +531,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
511531
toggleShowCompleted: () => dispatch(toggleShowCompleted()),
512532
fetchCompletedTasks: () => dispatch(fetchCompletedTasks()),
513533

534+
completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
514535
deleteTask: (taskId: number) => dispatch(deleteTask(taskId)),
515536
skipTask: (taskId: number) => dispatch(skipTask(taskId)),
516537
updateDueDate: (taskId: number, dueDate: string) => dispatch(updateDueDate({ taskId, dueDate })),

frontend/src/views/Tasks/TaskCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { TaskUI } from '@/utils/marshalling'
3030
type TaskCardProps = WithNavigate & {
3131
task: TaskUI
3232

33-
completeTask: (taskId: number) => Promise<any>
33+
completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
3434
onTaskUpdate: (event: TASK_UPDATE_EVENT) => void
3535
onContextMenu: (event: React.MouseEvent<HTMLDivElement>, task: TaskUI) => void
3636
}
@@ -46,7 +46,7 @@ class TaskCardImpl extends React.Component<TaskCardProps> {
4646

4747
private handleTaskCompletion = async () => {
4848
const { task, onTaskUpdate } = this.props
49-
await this.props.completeTask(task.id)
49+
await this.props.completeTask(task.id, false)
5050

5151
// Play the task completion sound
5252
playSound(SoundEffect.TaskComplete)
@@ -234,7 +234,7 @@ class TaskCardImpl extends React.Component<TaskCardProps> {
234234
}
235235

236236
const mapDispatchToProps = (dispatch: AppDispatch) => ({
237-
completeTask: (taskId: number) => dispatch(completeTask(taskId)),
237+
completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
238238
})
239239

240240
export const TaskCard = connect(

frontend/src/views/Tasks/TasksOverview.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type TasksOverviewProps = {
4444
filterTasks: (searchQuery: string) => void
4545
toggleShowCompleted: () => void
4646
fetchCompletedTasks: () => Promise<any>
47-
completeTask: (taskId: number) => Promise<any>
47+
completeTask: (taskId: number, endRecurrence: boolean) => Promise<any>
4848
deleteTask: (taskId: number) => Promise<any>
4949
updateDueDate: (taskId: number, dueDate: string) => Promise<any>
5050
pushStatus: (status: Status) => void
@@ -91,7 +91,7 @@ class TasksOverviewImpl extends React.Component<TasksOverviewProps> {
9191
}
9292

9393
private onCompleteTaskClicked = (task: TaskUI) => async () => {
94-
await this.props.completeTask(task.id)
94+
await this.props.completeTask(task.id, false)
9595

9696
playSound(SoundEffect.TaskComplete)
9797
this.props.pushStatus({
@@ -445,7 +445,7 @@ const mapDispatchToProps = (dispatch: AppDispatch) => ({
445445
filterTasks: (searchQuery: string) => dispatch(filterTasks(searchQuery)),
446446
toggleShowCompleted: () => dispatch(toggleShowCompleted()),
447447
fetchCompletedTasks: () => dispatch(fetchCompletedTasks()),
448-
completeTask: (taskId: number) => dispatch(completeTask(taskId)),
448+
completeTask: (taskId: number, endRecurrence: boolean) => dispatch(completeTask({ taskId, endRecurrence })),
449449
deleteTask: (taskId: number) => dispatch(deleteTask(taskId)),
450450
updateDueDate: (taskId: number, dueDate: string) => dispatch(updateDueDate({ taskId, dueDate })),
451451
pushStatus: (status: Status) => dispatch(pushStatus(status)),

0 commit comments

Comments
 (0)