feat(prime): add the action kind to the settings viewmodels
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
public sealed record PrimeScheduleDto(
|
||||
@@ -6,7 +8,8 @@ public sealed record PrimeScheduleDto(
|
||||
TimeSpan TimeOfDay,
|
||||
bool Enabled,
|
||||
DateTimeOffset? LastRunAt,
|
||||
string? PromptOverride);
|
||||
string? PromptOverride,
|
||||
PrimeActionKind Kind);
|
||||
|
||||
public sealed record PrimeFiredEvent(
|
||||
Guid ScheduleId,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -11,13 +14,36 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
||||
private readonly HashSet<Guid> _initialIds = new();
|
||||
|
||||
[ObservableProperty] private int _dailyPrepMaxTasks = 5;
|
||||
[ObservableProperty] private bool _anyFillMyDay;
|
||||
|
||||
public ObservableCollection<PrimeScheduleRowViewModel> Rows { get; } = new();
|
||||
|
||||
public PrimeClaudeTabViewModel(IPrimeScheduleApi api) => _api = api;
|
||||
public PrimeClaudeTabViewModel(IPrimeScheduleApi api)
|
||||
{
|
||||
_api = api;
|
||||
Rows.CollectionChanged += OnRowsChanged;
|
||||
}
|
||||
|
||||
private void OnRowsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.OldItems is not null)
|
||||
foreach (PrimeScheduleRowViewModel r in e.OldItems) r.PropertyChanged -= OnRowChanged;
|
||||
if (e.NewItems is not null)
|
||||
foreach (PrimeScheduleRowViewModel r in e.NewItems) r.PropertyChanged += OnRowChanged;
|
||||
UpdateAnyFillMyDay();
|
||||
}
|
||||
|
||||
private void OnRowChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PrimeScheduleRowViewModel.Kind)) UpdateAnyFillMyDay();
|
||||
}
|
||||
|
||||
private void UpdateAnyFillMyDay() =>
|
||||
AnyFillMyDay = Rows.Any(r => r.Kind == PrimeActionKind.FillMyDay);
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
foreach (var r in Rows) r.PropertyChanged -= OnRowChanged;
|
||||
Rows.Clear();
|
||||
_initialIds.Clear();
|
||||
var list = await _api.ListAsync();
|
||||
@@ -36,6 +62,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
||||
return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day.";
|
||||
if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1))
|
||||
return "Time must be between 00:00 and 23:59.";
|
||||
if (r.Kind == PrimeActionKind.Custom && string.IsNullOrWhiteSpace(r.PromptOverride))
|
||||
return $"Schedule {r.TimeOfDay:hh\\:mm}: a custom action needs a prompt.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -60,7 +88,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
||||
TimeOfDay: new TimeSpan(7, 0, 0),
|
||||
Enabled: true,
|
||||
LastRunAt: null,
|
||||
PromptOverride: null);
|
||||
PromptOverride: null,
|
||||
Kind: PrimeActionKind.Ping);
|
||||
Rows.Add(new PrimeScheduleRowViewModel(dto, isExisting: false));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Globalization;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
@@ -22,6 +24,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
[ObservableProperty] private TimeSpan _timeOfDay;
|
||||
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
||||
[ObservableProperty] private string? _promptOverride;
|
||||
[ObservableProperty] private PrimeActionKind _kind;
|
||||
|
||||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||||
|
||||
@@ -41,6 +44,50 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Per-row group name. A shared GroupName would make every schedule in the
|
||||
/// ItemsControl share one selection.</summary>
|
||||
public string RadioGroup => $"prime-kind-{Id}";
|
||||
|
||||
// Three booleans rather than an enum-to-bool converter: RadioButton.IsChecked binds
|
||||
// TwoWay to a bool, and compiled bindings stay simple.
|
||||
public bool IsPing
|
||||
{
|
||||
get => Kind == PrimeActionKind.Ping;
|
||||
set { if (value) Kind = PrimeActionKind.Ping; }
|
||||
}
|
||||
|
||||
public bool IsFillMyDay
|
||||
{
|
||||
get => Kind == PrimeActionKind.FillMyDay;
|
||||
set { if (value) Kind = PrimeActionKind.FillMyDay; }
|
||||
}
|
||||
|
||||
public bool IsCustom
|
||||
{
|
||||
get => Kind == PrimeActionKind.Custom;
|
||||
set { if (value) Kind = PrimeActionKind.Custom; }
|
||||
}
|
||||
|
||||
public bool ShowPrompt => Kind != PrimeActionKind.Ping;
|
||||
|
||||
public string PromptLabel => Kind == PrimeActionKind.Custom
|
||||
? Loc.T("settings.prime.customPromptLabel")
|
||||
: Loc.T("settings.prime.promptOverrideLabel");
|
||||
|
||||
public string PromptPlaceholder => Kind == PrimeActionKind.Custom
|
||||
? Loc.T("settings.prime.customPromptPlaceholder")
|
||||
: Loc.T("settings.prime.promptOverridePlaceholder");
|
||||
|
||||
partial void OnKindChanged(PrimeActionKind value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsPing));
|
||||
OnPropertyChanged(nameof(IsFillMyDay));
|
||||
OnPropertyChanged(nameof(IsCustom));
|
||||
OnPropertyChanged(nameof(ShowPrompt));
|
||||
OnPropertyChanged(nameof(PromptLabel));
|
||||
OnPropertyChanged(nameof(PromptPlaceholder));
|
||||
}
|
||||
|
||||
partial void OnLastRunAtChanged(DateTimeOffset? value) => OnPropertyChanged(nameof(LastRunLabel));
|
||||
|
||||
partial void OnTimeOfDayChanged(TimeSpan value) => OnPropertyChanged(nameof(TimeText));
|
||||
@@ -60,6 +107,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
TimeOfDay = dto.TimeOfDay;
|
||||
LastRunAt = dto.LastRunAt;
|
||||
PromptOverride = dto.PromptOverride;
|
||||
Kind = dto.Kind;
|
||||
}
|
||||
|
||||
public int DaysMask()
|
||||
@@ -77,5 +125,5 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
|
||||
public PrimeScheduleDto ToDto() =>
|
||||
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt,
|
||||
string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride);
|
||||
string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride, Kind);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
@@ -27,8 +28,9 @@ public class PrimeClaudeTabViewModelTests
|
||||
public Task DeleteAsync(Guid id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) =>
|
||||
new(id, days, time, true, null, null);
|
||||
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time,
|
||||
PrimeActionKind kind = PrimeActionKind.FillMyDay, string? prompt = null) =>
|
||||
new(id, days, time, true, null, prompt, kind);
|
||||
|
||||
[Fact]
|
||||
public async Task Load_Populates_Rows()
|
||||
@@ -116,4 +118,92 @@ public class PrimeClaudeTabViewModelTests
|
||||
var ex = await Assert.ThrowsAsync<Exception>(() => vm.SaveAsync());
|
||||
Assert.Equal(api.ExceptionMessage, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSchedule_Defaults_To_Ping()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
Assert.Equal(PrimeActionKind.Ping, vm.Rows[0].Kind);
|
||||
Assert.True(vm.Rows[0].IsPing);
|
||||
Assert.False(vm.Rows[0].IsFillMyDay);
|
||||
Assert.False(vm.Rows[0].IsCustom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setting_IsCustom_Flips_Kind_And_Clears_The_Others()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
var row = vm.Rows[0];
|
||||
|
||||
row.IsCustom = true;
|
||||
|
||||
Assert.Equal(PrimeActionKind.Custom, row.Kind);
|
||||
Assert.False(row.IsPing);
|
||||
Assert.False(row.IsFillMyDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Row_RadioGroup_Is_Unique_Per_Row()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
Assert.NotEqual(vm.Rows[0].RadioGroup, vm.Rows[1].RadioGroup);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Rejects_Custom_Without_A_Prompt()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
vm.Rows[0].IsCustom = true;
|
||||
vm.Rows[0].PromptOverride = " ";
|
||||
|
||||
Assert.NotNull(vm.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Accepts_Custom_With_A_Prompt()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
vm.Rows[0].IsCustom = true;
|
||||
vm.Rows[0].PromptOverride = "queue the oldest task";
|
||||
|
||||
Assert.Null(vm.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnyFillMyDay_Tracks_Kind_And_Row_Changes()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
Assert.False(vm.AnyFillMyDay);
|
||||
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
Assert.False(vm.AnyFillMyDay);
|
||||
|
||||
vm.Rows[0].IsFillMyDay = true;
|
||||
Assert.True(vm.AnyFillMyDay);
|
||||
|
||||
vm.RemoveScheduleCommand.Execute(vm.Rows[0]);
|
||||
Assert.False(vm.AnyFillMyDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_Round_Trips_The_Kind()
|
||||
{
|
||||
var api = new FakeApi();
|
||||
var vm = new PrimeClaudeTabViewModel(api);
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
vm.Rows[0].IsCustom = true;
|
||||
vm.Rows[0].PromptOverride = "do the thing";
|
||||
|
||||
await vm.SaveAsync();
|
||||
|
||||
Assert.Equal(PrimeActionKind.Custom, api.Upserts[0].Kind);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user