Skip to content

Commit

Permalink
feat: delete task group with swipe and show confirmation dialog (#89)
Browse files Browse the repository at this point in the history
* fix: wrap tasks overview widget with listview

* feat: delete task group with swipe and show confirmation dialog

* fix: extract stuff into functions
  • Loading branch information
invertedEcho authored Jul 27, 2024
1 parent 7d494a6 commit 7a34856
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 108 deletions.
25 changes: 24 additions & 1 deletion backend/src/db/functions/task-group.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { count, eq } from 'drizzle-orm';
import { count, eq, inArray } from 'drizzle-orm';
import { db } from '..';
import {
assignmentTable,
recurringTaskGroupTable,
recurringTaskGroupUserTable,
taskTable,
taskUserGroupTable,
userTable,
} from '../schema';
import { CreateTaskGroup } from 'src/tasks/task-group.controller';
Expand Down Expand Up @@ -88,3 +90,24 @@ export async function dbGetTasksOfTaskGroup(taskGroupId: number) {
.from(taskTable)
.where(eq(taskTable.recurringTaskGroupId, taskGroupId));
}

export async function dbDeleteTaskGroup(taskGroupId: number) {
await db
.delete(recurringTaskGroupUserTable)
.where(eq(recurringTaskGroupUserTable.recurringTaskGroupId, taskGroupId));
const taskIds = (await dbGetTasksOfTaskGroup(taskGroupId)).map(
(task) => task.id,
);
if (taskIds.length > 0) {
await db
.delete(assignmentTable)
.where(inArray(assignmentTable.taskId, taskIds));
await db
.delete(taskUserGroupTable)
.where(inArray(taskUserGroupTable.taskId, taskIds));
await db.delete(taskTable).where(inArray(taskTable.id, taskIds));
}
await db
.delete(recurringTaskGroupTable)
.where(eq(recurringTaskGroupTable.id, taskGroupId));
}
2 changes: 1 addition & 1 deletion backend/src/db/functions/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function dbCreateRecurringTask({
}
}

export async function dbDeleteRecurringTask({ taskId }: { taskId: number }) {
export async function dbDeleteTask({ taskId }: { taskId: number }) {
try {
await db.delete(assignmentTable).where(eq(assignmentTable.taskId, taskId));
await db
Expand Down
7 changes: 7 additions & 0 deletions backend/src/tasks/task-group.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Expand All @@ -10,6 +11,7 @@ import {
} from '@nestjs/common';
import {
dbCreateTaskGroup,
dbDeleteTaskGroup,
dbGetTaskGroups,
dbGetTasksOfTaskGroup,
} from 'src/db/functions/task-group';
Expand Down Expand Up @@ -48,4 +50,9 @@ export class TaskGroupController {
}
await dbCreateTaskGroup(taskGroup);
}

@Delete(':taskGroupId')
async deleteTaskGroup(@Param('taskGroupId') taskGroupId: number) {
await dbDeleteTaskGroup(taskGroupId);
}
}
6 changes: 3 additions & 3 deletions backend/src/tasks/task.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import {
dbCreateOneOffTask,
dbCreateRecurringTask,
dbDeleteRecurringTask,
dbDeleteTask,
dbGetAllTasks,
dbUpdateTask,
} from 'src/db/functions/task';
Expand Down Expand Up @@ -61,7 +61,7 @@ export class TasksController {
}

@Delete(':id')
async deleteRecurringTask(@Param('id') id: number) {
await dbDeleteRecurringTask({ taskId: id });
async deleteTask(@Param('id') id: number) {
await dbDeleteTask({ taskId: id });
}
}
2 changes: 2 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

macos/Flutter/ephemeral
9 changes: 9 additions & 0 deletions frontend/lib/fetch/task_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,12 @@ Future<List<Task>> fetchTasksForTaskGroup({required int taskGroupId}) async {
"Failed to load tasks for task group $taskGroupId: ${response.statusCode}");
}
}

Future<void> deleteTaskGroup({required int taskGroupId}) async {
final apiBaseUrl = getApiBaseUrl();
final response = await authenticatedClient
.delete(Uri.parse("$apiBaseUrl/task-group/$taskGroupId"));
if (response.statusCode != 200) {
throw Exception("Failed to delete task group: ${response.statusCode}");
}
}
4 changes: 2 additions & 2 deletions frontend/lib/widgets/screens/edit_task_group.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ class EditTaskGroupScreenState extends State<EditTaskGroupScreen> {
} else if (snapshot.hasData && snapshot.data!.isEmpty) {
return const SafeArea(
child: Text(
"No tasks inside this task group. To create some tasks, use the + Action Button on the bottom right."));
"No tasks inside this task group. To create some tasks, go back to the previous screen and use the + Action Button on the bottom right."));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Tasks in task group",
"Tasks:",
style: Theme.of(context).textTheme.titleLarge,
),
TaskList(
Expand Down
103 changes: 76 additions & 27 deletions frontend/lib/widgets/tasks/task_group_list.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import 'package:flatshare/fetch/task_group.dart';
import 'package:flatshare/models/task_group.dart';
import 'package:flatshare/widgets/screens/edit_task_group.dart';
import 'package:flutter/material.dart';

// TODO: We should probably fix that our backend doesn't return the pg interval type formatted
// like this in the case of month
String formatInterval(String interval) {
if (interval.contains("mon")) {
return interval.replaceAll("mon", "month");
Expand All @@ -18,6 +17,38 @@ class TaskGroupList extends StatelessWidget {
const TaskGroupList(
{super.key, required this.taskGroups, required this.onRefresh});

void handleOnDismissed(
{required BuildContext context, required int taskGroupId}) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("Are you sure?"),
content: const Text(
"Are you really sure you want to delete this task group? This will also delete all tasks attached to this task group, and all assignments attached to these tasks."),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
},
child: const Text("Abort")),
TextButton(
onPressed: () async {
try {
await deleteTaskGroup(taskGroupId: taskGroupId);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$error')),
);
}
Navigator.of(context).pop();
},
child: const Text("Confirm"))
],
);
});
}

@override
Widget build(BuildContext context) {
return ListView.builder(
Expand All @@ -26,33 +57,51 @@ class TaskGroupList extends StatelessWidget {
itemCount: taskGroups.length,
itemBuilder: (context, index) {
final taskGroup = taskGroups[index];
return Card(
child: InkWell(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => EditTaskGroupScreen(
taskGroup: taskGroup, onRefresh: onRefresh)));
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(taskGroup.title,
style: Theme.of(context).textTheme.titleMedium),
Text(taskGroup.description!),
Text("Total tasks: ${taskGroup.numberOfTasks}"),
Row(
return Dismissible(
key: Key(taskGroup.id.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
handleOnDismissed(context: context, taskGroupId: taskGroup.id);
},
background: Container(
decoration: const BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(15))),
padding: const EdgeInsets.all(16),
child: const Align(
alignment: Alignment.centerRight,
child: Icon(
Icons.delete,
color: Colors.black,
),
)),
child: Card(
child: InkWell(
onTap: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => EditTaskGroupScreen(
taskGroup: taskGroup, onRefresh: onRefresh)));
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.repeat),
Text("Every ${formatInterval(taskGroup.interval)}")
Text(taskGroup.title,
style: Theme.of(context).textTheme.titleMedium),
Text(taskGroup.description!),
Text("Total tasks: ${taskGroup.numberOfTasks}"),
Row(
children: [
const Icon(Icons.repeat),
Text("Every ${formatInterval(taskGroup.interval)}")
],
)
],
)
],
),
),
));
),
),
)));
});
}
}
36 changes: 31 additions & 5 deletions frontend/lib/widgets/tasks/task_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,33 @@ class TaskList extends StatelessWidget {

const TaskList({super.key, required this.tasks, required this.refreshState});

void handleOnDismissed({required BuildContext context, required int taskId}) {
showDialog(
context: context,
builder: (BuildContext ctx) {
return AlertDialog(
title: const Text("Are you sure?"),
content: const Text(
"Are you really sure you want to delete this task? This will also delete all current assignments that exist for this task."),
actions: [
TextButton(
onPressed: () async {
refreshState();
Navigator.of(context).pop();
},
child: const Text("Abort")),
TextButton(
onPressed: () async {
await deleteTask(taskId: taskId);
Navigator.of(context).pop();
refreshState();
},
child: const Text("Confirm"))
],
);
});
}

@override
Widget build(BuildContext context) {
return ListView.builder(
Expand All @@ -30,8 +57,7 @@ class TaskList extends StatelessWidget {
direction: DismissDirection.endToStart,
key: Key(task.id.toString()),
onDismissed: (direction) async {
await deleteTask(taskId: task.id);
refreshState();
handleOnDismissed(context: context, taskId: task.id);
},
background: Container(
decoration: const BoxDecoration(
Expand Down Expand Up @@ -70,17 +96,17 @@ class TaskList extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
height: 300,
height: 350,
width: double.infinity,
child: Column(
mainAxisAlignment:
MainAxisAlignment.start,
children: [
Text("Edit task",
Text("Edit Task",
style: Theme.of(context)
.textTheme
.headlineMedium),
const SizedBox(height: 40),
const SizedBox(height: 20),
EditTaskForm(
task: task,
refreshState: refreshState)
Expand Down
Loading

0 comments on commit 7a34856

Please sign in to comment.