feat(ui): show dequeue affordance on planning parents with queued children
Planning parents stay in Planning/Planned status while their children are Queued/Waiting, so the existing IsQueued-only visibility rule hid the dequeue button. Add HasQueuedSubtasks tracking and a CanRemoveFromQueue helper; the parent-row dequeue cascades to all queued/waiting children. Also attach the 'agent' tag on explicit enqueue so the queue picker accepts the task.
This commit is contained in:
@@ -26,6 +26,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private string? _parentTaskId;
|
[ObservableProperty] private string? _parentTaskId;
|
||||||
[ObservableProperty] private bool _isExpanded = true;
|
[ObservableProperty] private bool _isExpanded = true;
|
||||||
[ObservableProperty] private bool _hasPlanningChildren;
|
[ObservableProperty] private bool _hasPlanningChildren;
|
||||||
|
[ObservableProperty] private bool _hasQueuedSubtasks;
|
||||||
|
|
||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
||||||
@@ -58,6 +59,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
public bool IsRunning => Status == TaskStatus.Running;
|
public bool IsRunning => Status == TaskStatus.Running;
|
||||||
public bool IsQueued => Status == TaskStatus.Queued;
|
public bool IsQueued => Status == TaskStatus.Queued;
|
||||||
public bool IsWaiting => Status == TaskStatus.Waiting;
|
public bool IsWaiting => Status == TaskStatus.Waiting;
|
||||||
|
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
|
||||||
public bool HasSchedule => ScheduledFor.HasValue;
|
public bool HasSchedule => ScheduledFor.HasValue;
|
||||||
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
||||||
|
|
||||||
@@ -87,8 +89,12 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsDraft));
|
OnPropertyChanged(nameof(IsDraft));
|
||||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||||
OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
|
OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
|
||||||
|
OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnHasQueuedSubtasksChanged(bool value)
|
||||||
|
=> OnPropertyChanged(nameof(CanRemoveFromQueue));
|
||||||
|
|
||||||
partial void OnParentTaskIdChanged(string? value)
|
partial void OnParentTaskIdChanged(string? value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(IsChild));
|
OnPropertyChanged(nameof(IsChild));
|
||||||
|
|||||||
@@ -100,6 +100,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
else return;
|
else return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the parent's HasQueuedSubtasks flag in sync when a child's status flips.
|
||||||
|
if (entity is not null && !string.IsNullOrEmpty(entity.ParentTaskId))
|
||||||
|
{
|
||||||
|
var parent = Items.FirstOrDefault(r => r.Id == entity.ParentTaskId);
|
||||||
|
if (parent is not null)
|
||||||
|
parent.HasQueuedSubtasks = Items.Any(r =>
|
||||||
|
r.ParentTaskId == parent.Id && (r.IsQueued || r.IsWaiting));
|
||||||
|
}
|
||||||
|
|
||||||
Regroup();
|
Regroup();
|
||||||
UpdateSubtitle();
|
UpdateSubtitle();
|
||||||
}
|
}
|
||||||
@@ -207,6 +216,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
if (parentsWithChildren.Contains(r.Id))
|
if (parentsWithChildren.Contains(r.Id))
|
||||||
r.HasPlanningChildren = true;
|
r.HasPlanningChildren = true;
|
||||||
|
|
||||||
|
// Mark planning parents whose children are currently queued/waiting,
|
||||||
|
// so the dequeue affordance is visible on the parent row.
|
||||||
|
var parentsWithQueuedKids = Items
|
||||||
|
.Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)
|
||||||
|
&& (r.IsQueued || r.IsWaiting))
|
||||||
|
.Select(r => r.ParentTaskId!)
|
||||||
|
.ToHashSet();
|
||||||
|
foreach (var r in Items)
|
||||||
|
r.HasQueuedSubtasks = parentsWithQueuedKids.Contains(r.Id);
|
||||||
|
|
||||||
Regroup();
|
Regroup();
|
||||||
UpdateSubtitle();
|
UpdateSubtitle();
|
||||||
}
|
}
|
||||||
@@ -446,9 +465,15 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
if (row is null || row.IsRunning) return;
|
if (row is null || row.IsRunning) return;
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
var entity = await db.Tasks.Include(t => t.Tags).FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||||
if (entity is null) return;
|
if (entity is null) return;
|
||||||
entity.Status = TaskStatus.Queued;
|
entity.Status = TaskStatus.Queued;
|
||||||
|
// Worker queue picker requires the "agent" tag — attach it on explicit enqueue.
|
||||||
|
if (!entity.Tags.Any(t => t.Name == "agent"))
|
||||||
|
{
|
||||||
|
var agentTag = await db.Tags.FirstOrDefaultAsync(t => t.Name == "agent");
|
||||||
|
if (agentTag is not null) entity.Tags.Add(agentTag);
|
||||||
|
}
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
row.Status = TaskStatus.Queued;
|
row.Status = TaskStatus.Queued;
|
||||||
if (_worker is not null)
|
if (_worker is not null)
|
||||||
@@ -467,9 +492,30 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
|
||||||
if (entity is null) return;
|
if (entity is null) return;
|
||||||
entity.Status = TaskStatus.Manual;
|
|
||||||
await db.SaveChangesAsync();
|
// For a planning parent the dequeue button targets queued/waiting children,
|
||||||
row.Status = TaskStatus.Manual;
|
// not the parent itself (whose Status is Planning/Planned).
|
||||||
|
if (entity.Status == TaskStatus.Planning || entity.Status == TaskStatus.Planned)
|
||||||
|
{
|
||||||
|
var children = await db.Tasks
|
||||||
|
.Where(t => t.ParentTaskId == row.Id
|
||||||
|
&& (t.Status == TaskStatus.Queued || t.Status == TaskStatus.Waiting))
|
||||||
|
.ToListAsync();
|
||||||
|
foreach (var c in children) c.Status = TaskStatus.Manual;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
foreach (var c in children)
|
||||||
|
{
|
||||||
|
var childRow = Items.FirstOrDefault(r => r.Id == c.Id);
|
||||||
|
if (childRow is not null) childRow.Status = TaskStatus.Manual;
|
||||||
|
}
|
||||||
|
row.HasQueuedSubtasks = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entity.Status = TaskStatus.Manual;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
row.Status = TaskStatus.Manual;
|
||||||
|
}
|
||||||
Regroup();
|
Regroup();
|
||||||
UpdateSubtitle();
|
UpdateSubtitle();
|
||||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
IsVisible="{Binding !IsQueued}"
|
IsVisible="{Binding !IsQueued}"
|
||||||
Click="OnSendToQueueClick"/>
|
Click="OnSendToQueueClick"/>
|
||||||
<MenuItem Header="Remove from queue"
|
<MenuItem Header="Remove from queue"
|
||||||
IsVisible="{Binding IsQueued}"
|
IsVisible="{Binding CanRemoveFromQueue}"
|
||||||
Click="OnRemoveFromQueueClick"/>
|
Click="OnRemoveFromQueueClick"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="Run interactively"
|
<MenuItem Header="Run interactively"
|
||||||
@@ -119,9 +119,9 @@
|
|||||||
<TextBlock Text="{Binding Status}"/>
|
<TextBlock Text="{Binding Status}"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Dequeue button (only when Queued) -->
|
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
||||||
<Button Classes="icon-btn dequeue-btn"
|
<Button Classes="icon-btn dequeue-btn"
|
||||||
IsVisible="{Binding IsQueued}"
|
IsVisible="{Binding CanRemoveFromQueue}"
|
||||||
ToolTip.Tip="Remove from queue"
|
ToolTip.Tip="Remove from queue"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RemoveFromQueueCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).RemoveFromQueueCommand}"
|
||||||
CommandParameter="{Binding}">
|
CommandParameter="{Binding}">
|
||||||
|
|||||||
Reference in New Issue
Block a user