feat(ui): move a task to another list via drag & drop
Dragging a task row onto a user list in the Lists island now reassigns it to that list. The task drag holds the pointer capture, so the release is resolved geometrically (new case between the Mission Control and reorder cases) instead of going through the Lists island's own DragDrop path, which never sees a DragEventArgs during a task drag. TaskRepository.MoveToListAsync reassigns the task plus every descendant (a child must never sit in a different list than its parent) and appends the task at the end of the target list. Guards: running tasks and tasks holding an Active/Kept worktree are rejected to the footer error strip; a move that changes repo asks for confirmation naming both repos. The source repo is read from the task's own list rather than the island's current list, which is a smart/virtual list with no working dir of its own whenever one of those is shown.
This commit is contained in:
@@ -84,6 +84,60 @@ public sealed class TaskRepository
|
||||
public Task<List<TaskEntity>> GetByListAsync(string listId, CancellationToken ct = default)
|
||||
=> GetByListIdAsync(listId, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ids of every descendant of <paramref name="taskId"/> (children, grandchildren, ...),
|
||||
/// walking the ParentTaskId chain breadth-first. Does not include the task itself.
|
||||
/// </summary>
|
||||
public async Task<List<string>> GetDescendantIdsAsync(string taskId, CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<string>();
|
||||
// Guards against a corrupt parent chain (a self-parent or a cycle would loop forever).
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal) { taskId };
|
||||
var frontier = new List<string> { taskId };
|
||||
while (frontier.Count > 0)
|
||||
{
|
||||
var children = await _context.Tasks.AsNoTracking()
|
||||
.Where(t => t.ParentTaskId != null && frontier.Contains(t.ParentTaskId))
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(ct);
|
||||
var fresh = children.Where(seen.Add).ToList();
|
||||
if (fresh.Count == 0) break;
|
||||
result.AddRange(fresh);
|
||||
frontier = fresh;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a task (and every descendant, so a child never ends up in a different list than its
|
||||
/// parent) to <paramref name="targetListId"/>, appending it at the end of the target list's
|
||||
/// order. ListId is init-only, so the move goes through ExecuteUpdate rather than a tracked
|
||||
/// entity mutation.
|
||||
/// </summary>
|
||||
public async Task MoveToListAsync(string taskId, string targetListId, CancellationToken ct = default)
|
||||
{
|
||||
var exists = await _context.Tasks.AsNoTracking().AnyAsync(t => t.Id == taskId, ct);
|
||||
if (!exists)
|
||||
throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
|
||||
var descendantIds = await GetDescendantIdsAsync(taskId, ct);
|
||||
var movedIds = new List<string> { taskId };
|
||||
movedIds.AddRange(descendantIds);
|
||||
|
||||
var maxSort = await _context.Tasks
|
||||
.Where(t => t.ListId == targetListId)
|
||||
.Select(t => (int?)t.SortOrder)
|
||||
.MaxAsync(ct);
|
||||
|
||||
await _context.Tasks
|
||||
.Where(t => movedIds.Contains(t.Id))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.ListId, targetListId), ct);
|
||||
|
||||
await _context.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.SortOrder, (maxSort ?? -1) + 1), ct);
|
||||
}
|
||||
|
||||
public async Task<List<TaskEntity>> GetByCreatorAsync(string createdBy, CancellationToken ct = default)
|
||||
{
|
||||
return await _context.Tasks
|
||||
|
||||
Reference in New Issue
Block a user