-
Notifications
You must be signed in to change notification settings - Fork 1.3k
WIP: Task sorting persistence and drag-drop reordering #4248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Rename 'Actions' to 'Tasks' in macOS menu - Add persistent task sorting that survives app restarts - Tasks keep their position when moving between categories (e.g., tomorrow to today) - Add drag-and-drop reordering for macOS tasks page (matching mobile behavior) - Store sort order per category in SharedPreferences
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces persistent task sorting and drag-and-drop reordering, which are great enhancements for task management. However, I've identified a few critical issues that need to be addressed. There's a potential race condition in how the sort order is persisted to SharedPreferences due to un-awaited asynchronous calls, which could lead to data loss. Additionally, the drag-and-drop implementation in both the desktop and mobile versions contains a bug in how it calculates the drop position, which will prevent reordering from working as expected. Addressing these issues will ensure the new features are robust and reliable.
app/lib/backend/preferences.dart
Outdated
| /// Set task sort order map | ||
| set taskSortOrder(Map<String, int> value) { | ||
| saveString('taskSortOrder', jsonEncode(value)); | ||
| } | ||
|
|
||
| /// Update sort order for a single task | ||
| void updateTaskSortOrder(String taskId, int sortOrder) { | ||
| final current = taskSortOrder; | ||
| current[taskId] = sortOrder; | ||
| taskSortOrder = current; | ||
| } | ||
|
|
||
| /// Update sort orders for multiple tasks | ||
| void updateTaskSortOrders(Map<String, int> updates) { | ||
| final current = taskSortOrder; | ||
| current.addAll(updates); | ||
| taskSortOrder = current; | ||
| } | ||
|
|
||
| /// Remove sort order for a task (when task is deleted) | ||
| void removeTaskSortOrder(String taskId) { | ||
| final current = taskSortOrder; | ||
| current.remove(taskId); | ||
| taskSortOrder = current; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setter taskSortOrder and the update methods (updateTaskSortOrder, updateTaskSortOrders, removeTaskSortOrder) call saveString, which is an asynchronous operation, without awaiting its completion. This fire-and-forget approach can lead to race conditions and data loss, especially with rapid updates like those from drag-and-drop operations. For example, if two updates happen in quick succession, the second one might start before the first one has finished writing to storage, causing the first update to be overwritten.
/// Set task sort order map
Future<void> setTaskSortOrder(Map<String, int> value) async {
await saveString('taskSortOrder', jsonEncode(value));
}
/// Update sort order for a single task
Future<void> updateTaskSortOrder(String taskId, int sortOrder) async {
final current = taskSortOrder;
current[taskId] = sortOrder;
await setTaskSortOrder(current);
}
/// Update sort orders for multiple tasks
Future<void> updateTaskSortOrders(Map<String, int> updates) async {
final current = taskSortOrder;
current.addAll(updates);
await setTaskSortOrder(current);
}
/// Remove sort order for a task (when task is deleted)
Future<void> removeTaskSortOrder(String taskId) async {
final current = taskSortOrder;
current.remove(taskId);
await setTaskSortOrder(current);
}| onMove: (details) { | ||
| final RenderBox? box = context.findRenderObject() as RenderBox?; | ||
| if (box != null) { | ||
| final localPosition = box.globalToLocal(details.offset); | ||
| final isAbove = localPosition.dy < 20; | ||
| if (_hoveredItemId != item.id || _hoverAbove != isAbove) { | ||
| setState(() { | ||
| _hoveredItemId = item.id; | ||
| _hoverAbove = isAbove; | ||
| }); | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The onMove callback for the DragTarget has a critical bug that will cause incorrect drag-and-drop behavior.
context.findRenderObject()is fetching theRenderBoxfor the entire page, not the specific list item being hovered over. This meansbox.globalToLocal(details.offset)will calculate coordinates relative to the page, not the item, making the hover detection logic faulty.- The line
final isAbove = localPosition.dy < 20;uses a hardcoded magic number20to decide the drop position. This is unreliable as item heights can vary. It should be based on the item's actual height (e.g.,box.size.height / 2).
To fix this, you need to obtain the RenderBox of the specific DragTarget item. A common approach is to use a GlobalKey for each item's DragTarget widget to get its context and RenderBox.
Here's a conceptual example of how you could adjust the onMove logic:
// In onMove callback:
// 1. Get the RenderBox of the target item (e.g., via a GlobalKey).
final RenderBox? itemBox = ...; // Get item's RenderBox
if (itemBox != null) {
final localPosition = itemBox.globalToLocal(details.offset);
// 2. Use half the item's height for comparison.
final isAbove = localPosition.dy < itemBox.size.height / 2;
if (_hoveredItemId != item.id || _hoverAbove != isAbove) {
setState(() {
_hoveredItemId = item.id;
_hoverAbove = isAbove;
});
}
}Keep only the 'Actions' -> 'Tasks' rename, remove sorting persistence that was causing tasks not to render.
Summary
Status
🚧 Work in Progress - Still testing
Testing needed