feat(ui): roadblock badge on the task card; relocate review actions off the row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,9 @@
|
|||||||
<!-- Icon.ArrowOut — filled arrow for "open external" button -->
|
<!-- Icon.ArrowOut — filled arrow for "open external" button -->
|
||||||
<StreamGeometry x:Key="Icon.ArrowOut">M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z</StreamGeometry>
|
<StreamGeometry x:Key="Icon.ArrowOut">M13 4 H20 V11 H18 V7.4 L11.4 14 L10 12.6 L16.6 6 H13 Z M4 6 H10 V8 H6 V18 H16 V14 H18 V20 H4 Z</StreamGeometry>
|
||||||
|
|
||||||
|
<!-- Icon.Warning — filled triangle with exclamation (roadblock badge) -->
|
||||||
|
<StreamGeometry x:Key="Icon.Warning">M12 3 L22 20 H2 Z M11 9 H13 V14 H11 Z M11 16 H13 V18 H11 Z</StreamGeometry>
|
||||||
|
|
||||||
<!-- Icon.Settings (gear) -->
|
<!-- Icon.Settings (gear) -->
|
||||||
<StreamGeometry x:Key="Icon.Settings">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z</StreamGeometry>
|
<StreamGeometry x:Key="Icon.Settings">M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65a.5.5 0 0 0 .12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1a7.03 7.03 0 0 0-1.69-.98l-.38-2.65a.5.5 0 0 0-.5-.42h-4a.5.5 0 0 0-.5.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.5.5 0 0 0-.61.22l-2 3.46a.5.5 0 0 0 .12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65a.5.5 0 0 0-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65a.5.5 0 0 0 .5.42h4a.5.5 0 0 0 .5-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1a.5.5 0 0 0 .61-.22l2-3.46a.5.5 0 0 0-.12-.64l-2.11-1.65z</StreamGeometry>
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _hasQueuedSubtasks;
|
[ObservableProperty] private bool _hasQueuedSubtasks;
|
||||||
[ObservableProperty] private bool _showListChip = true;
|
[ObservableProperty] private bool _showListChip = true;
|
||||||
[ObservableProperty] private bool _parentFinalized;
|
[ObservableProperty] private bool _parentFinalized;
|
||||||
|
[ObservableProperty] private int _roadblockCount;
|
||||||
|
|
||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
public string CreatedAtFormatted => CreatedAt == default ? "—" : Loc.T("vm.taskRow.createdPrefix", CreatedAt.ToString("MMM d"));
|
||||||
@@ -75,6 +76,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
&& PlanningPhase == PlanningPhase.Finalized
|
&& PlanningPhase == PlanningPhase.Finalized
|
||||||
&& !HasQueuedSubtasks;
|
&& !HasQueuedSubtasks;
|
||||||
public bool HasSchedule => ScheduledFor.HasValue;
|
public bool HasSchedule => ScheduledFor.HasValue;
|
||||||
|
public bool HasRoadblock => RoadblockCount > 0;
|
||||||
|
public string RoadblockTooltip => RoadblockCount == 1
|
||||||
|
? "1 roadblock reported during the run — see details"
|
||||||
|
: $"{RoadblockCount} roadblocks reported during the run — see details";
|
||||||
|
|
||||||
public string DiffAdditionsText => $"+{DiffAdditions}";
|
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||||||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||||
@@ -174,6 +179,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
||||||
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
||||||
|
partial void OnRoadblockCountChanged(int value) { OnPropertyChanged(nameof(HasRoadblock)); OnPropertyChanged(nameof(RoadblockTooltip)); }
|
||||||
|
|
||||||
public void RefreshLocalized()
|
public void RefreshLocalized()
|
||||||
{
|
{
|
||||||
@@ -207,6 +213,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
DiffDeletions = del;
|
DiffDeletions = del;
|
||||||
ParentTaskId = t.ParentTaskId;
|
ParentTaskId = t.ParentTaskId;
|
||||||
BlockedByTaskId = t.BlockedByTaskId;
|
BlockedByTaskId = t.BlockedByTaskId;
|
||||||
|
RoadblockCount = t.RoadblockCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||||
|
|||||||
@@ -129,6 +129,13 @@
|
|||||||
<!-- Chip row -->
|
<!-- Chip row -->
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
|
||||||
|
<!-- Roadblock badge -->
|
||||||
|
<PathIcon Width="13" Height="13" VerticalAlignment="Center"
|
||||||
|
Data="{StaticResource Icon.Warning}"
|
||||||
|
Foreground="#E0A800"
|
||||||
|
IsVisible="{Binding HasRoadblock}"
|
||||||
|
ToolTip.Tip="{Binding RoadblockTooltip}"/>
|
||||||
|
|
||||||
<!-- Status chip -->
|
<!-- Status chip -->
|
||||||
<Border Classes="chip"
|
<Border Classes="chip"
|
||||||
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
Classes.running="{Binding Status, Converter={StaticResource EqStatus}, ConverterParameter=Running}"
|
||||||
@@ -139,23 +146,6 @@
|
|||||||
<TextBlock Text="{Binding StatusLabel}"/>
|
<TextBlock Text="{Binding StatusLabel}"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Review actions (visible when WaitingForReview) -->
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4"
|
|
||||||
IsVisible="{Binding IsWaitingForReview}">
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.approve}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.approveTip}"
|
|
||||||
Click="OnApproveReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.reject}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.rejectTip}"
|
|
||||||
Click="OnRejectReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.park}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.parkTip}"
|
|
||||||
Click="OnParkReviewClick"/>
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" MinWidth="0" Padding="8,2"
|
|
||||||
ToolTip.Tip="{loc:Tr tasks.cancelTip}"
|
|
||||||
Click="OnCancelReviewClick"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Dequeue button (visible when row is Queued, or planning parent has queued subtasks) -->
|
<!-- 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 CanRemoveFromQueue}"
|
IsVisible="{Binding CanRemoveFromQueue}"
|
||||||
@@ -247,35 +237,5 @@
|
|||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- Hidden reject-feedback anchor (its Flyout is shown from the Reject button) -->
|
|
||||||
<Button Grid.Row="1" x:Name="RejectAnchor"
|
|
||||||
Width="1" Height="1" Opacity="0"
|
|
||||||
HorizontalAlignment="Left" VerticalAlignment="Top"
|
|
||||||
IsHitTestVisible="False" Focusable="False">
|
|
||||||
<Button.Flyout>
|
|
||||||
<Flyout Placement="Bottom" ShowMode="Standard">
|
|
||||||
<Border Background="{DynamicResource Surface2Brush}"
|
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="10"
|
|
||||||
Padding="16" Width="320">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Classes="title" Text="{loc:Tr tasks.rejectRerunTitle}"/>
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="eyebrow" Text="{loc:Tr tasks.feedbackLabel}"
|
|
||||||
Foreground="{DynamicResource TextDimBrush}" Opacity="0.6"/>
|
|
||||||
<TextBox x:Name="RejectFeedback"
|
|
||||||
AcceptsReturn="True" TextWrapping="Wrap"
|
|
||||||
MinHeight="80" PlaceholderText="{loc:Tr tasks.feedbackPlaceholder}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8"
|
|
||||||
HorizontalAlignment="Right" Margin="0,4,0,0">
|
|
||||||
<Button Classes="btn" Content="{loc:Tr tasks.cancel}" Click="OnRejectCancelClick" MinWidth="76"/>
|
|
||||||
<Button Content="{loc:Tr tasks.rerun}" Classes="accent" Click="OnRejectConfirmClick" MinWidth="76"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Flyout>
|
|
||||||
</Button.Flyout>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -88,43 +88,6 @@ public partial class TaskRowView : UserControl
|
|||||||
await vm.SetStatusOnRowAsync(row, status);
|
await vm.SetStatusOnRowAsync(row, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnApproveReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.ApproveReviewCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnParkReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.RejectReviewToIdleCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnCancelReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
|
||||||
await vm.CancelReviewCommand.ExecuteAsync(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRejectReviewClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not TaskRowViewModel) return;
|
|
||||||
RejectFeedback.Text = "";
|
|
||||||
RejectAnchor.Flyout?.ShowAt(RejectAnchor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnRejectConfirmClick(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
RejectAnchor.Flyout?.Hide();
|
|
||||||
if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return;
|
|
||||||
var feedback = RejectFeedback.Text ?? "";
|
|
||||||
if (string.IsNullOrWhiteSpace(feedback)) return;
|
|
||||||
await vm.RejectReviewToQueueAsync(row, feedback);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRejectCancelClick(object? sender, RoutedEventArgs e)
|
|
||||||
=> RejectAnchor.Flyout?.Hide();
|
|
||||||
|
|
||||||
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not TaskRowViewModel row) return;
|
if (DataContext is not TaskRowViewModel row) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user