feat(ui): status/tag context menu + ThemedDatePicker in task row

Adds "Set status" and "Tags" submenus to the row context menu (tags
list is built lazily on Opening from AllTags ∪ row tags). Replaces
the schedule flyout's separate DATE / TIME pickers with a single
ThemedDatePicker in date+time mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-29 10:40:09 +02:00
parent c1856657b5
commit 9ba238f4ad
2 changed files with 61 additions and 16 deletions

View File

@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView" x:Class="ClaudeDo.Ui.Views.Islands.TaskRowView"
x:DataType="vm:TaskRowViewModel"> x:DataType="vm:TaskRowViewModel">
<Grid> <Grid>
@@ -30,7 +31,7 @@
Classes.selected="{Binding IsSelected}" Classes.selected="{Binding IsSelected}"
Classes.done="{Binding Done}"> Classes.done="{Binding Done}">
<Border.ContextMenu> <Border.ContextMenu>
<ContextMenu> <ContextMenu Opening="OnContextMenuOpening">
<MenuItem Header="Send to queue" <MenuItem Header="Send to queue"
IsVisible="{Binding !IsQueued}" IsVisible="{Binding !IsQueued}"
Click="OnSendToQueueClick"/> Click="OnSendToQueueClick"/>
@@ -38,6 +39,16 @@
IsVisible="{Binding CanRemoveFromQueue}" IsVisible="{Binding CanRemoveFromQueue}"
Click="OnRemoveFromQueueClick"/> Click="OnRemoveFromQueueClick"/>
<Separator/> <Separator/>
<MenuItem Header="Set status">
<MenuItem Header="Idle" Tag="Idle" Click="OnSetStatusClick"/>
<MenuItem Header="Queued" Tag="Queued" Click="OnSetStatusClick"/>
<MenuItem Header="Running" Tag="Running" Click="OnSetStatusClick"/>
<MenuItem Header="Done" Tag="Done" Click="OnSetStatusClick"/>
<MenuItem Header="Failed" Tag="Failed" Click="OnSetStatusClick"/>
<MenuItem Header="Cancelled" Tag="Cancelled" Click="OnSetStatusClick"/>
</MenuItem>
<MenuItem Header="Tags" x:Name="TagsMenu"/>
<Separator/>
<MenuItem Header="Run interactively" <MenuItem Header="Run interactively"
Click="OnRunInteractivelyClick"/> Click="OnRunInteractivelyClick"/>
<MenuItem Header="Open planning Session" <MenuItem Header="Open planning Session"
@@ -224,15 +235,9 @@
Foreground="{DynamicResource TextBrush}"/> Foreground="{DynamicResource TextBrush}"/>
<StackPanel Spacing="6"> <StackPanel Spacing="6">
<TextBlock Text="DATE" FontSize="10" Opacity="0.6" <TextBlock Text="WHEN" FontSize="10" Opacity="0.6"
Foreground="{DynamicResource TextDimBrush}"/> Foreground="{DynamicResource TextDimBrush}"/>
<DatePicker x:Name="ScheduleDate" HorizontalAlignment="Stretch"/> <ctl:ThemedDatePicker x:Name="ScheduleDate" ShowTime="True"
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="TIME" FontSize="10" Opacity="0.6"
Foreground="{DynamicResource TextDimBrush}"/>
<TimePicker x:Name="ScheduleTime" ClockIdentifier="24HourClock"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>

View File

@@ -3,11 +3,14 @@ using Avalonia;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Easings; using Avalonia.Animation.Easings;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Islands; using ClaudeDo.Ui.ViewModels.Islands;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.Views.Islands; namespace ClaudeDo.Ui.Views.Islands;
@@ -69,13 +72,52 @@ public partial class TaskRowView : UserControl
await vm.QueuePlanningSubtasksCommand.ExecuteAsync(row); await vm.QueuePlanningSubtasksCommand.ExecuteAsync(row);
} }
private async void OnSetStatusClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem mi) return;
if (DataContext is not TaskRowViewModel row) return;
if (FindTasksVm() is not { } vm) return;
if (mi.Tag is not string tag) return;
if (!System.Enum.TryParse<TaskStatus>(tag, ignoreCase: true, out var status)) return;
await vm.SetStatusOnRowAsync(row, status);
}
private void OnContextMenuOpening(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return;
// Build the union of all known tags + tags currently on this row, so a row's
// own tags stay reachable from the menu even if the global list is stale.
var rowTags = row.Tags.ToHashSet();
var union = vm.AllTags.Concat(rowTags).Distinct().OrderBy(t => t).ToList();
TagsMenu.Items.Clear();
if (union.Count == 0)
{
TagsMenu.Items.Add(new MenuItem { Header = "(no tags yet)", IsEnabled = false });
return;
}
foreach (var name in union)
{
var prefix = rowTags.Contains(name) ? "✓ " : " ";
var item = new MenuItem { Header = prefix + name, Tag = name };
item.Click += OnToggleTagClick;
TagsMenu.Items.Add(item);
}
}
private async void OnToggleTagClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem mi || mi.Tag is not string name) return;
if (DataContext is not TaskRowViewModel row || FindTasksVm() is not { } vm) return;
await vm.ToggleTagOnRowAsync(row, name);
}
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;
_pendingScheduleRow = row; _pendingScheduleRow = row;
var seed = row.ScheduledFor ?? DateTime.Now.AddHours(1); ScheduleDate.SelectedDate = row.ScheduledFor ?? DateTime.Now.AddHours(1);
ScheduleDate.SelectedDate = new DateTimeOffset(seed.Date, TimeSpan.Zero);
ScheduleTime.SelectedTime = seed.TimeOfDay;
ScheduleAnchor.Flyout?.ShowAt(ScheduleAnchor); ScheduleAnchor.Flyout?.ShowAt(ScheduleAnchor);
} }
@@ -83,9 +125,7 @@ public partial class TaskRowView : UserControl
{ {
ScheduleAnchor.Flyout?.Hide(); ScheduleAnchor.Flyout?.Hide();
if (_pendingScheduleRow is null || ScheduleDate.SelectedDate is null) return; if (_pendingScheduleRow is null || ScheduleDate.SelectedDate is null) return;
var date = ScheduleDate.SelectedDate.Value.Date; var when = ScheduleDate.SelectedDate.Value;
var time = ScheduleTime.SelectedTime ?? TimeSpan.FromHours(9);
var when = date + time;
if (FindTasksVm() is { } tvm) if (FindTasksVm() is { } tvm)
await tvm.SetScheduledForAsync(_pendingScheduleRow, when); await tvm.SetScheduledForAsync(_pendingScheduleRow, when);
_pendingScheduleRow = null; _pendingScheduleRow = null;