feat(ui): tickets + plan day button, fallback list and prime action
This commit is contained in:
@@ -112,10 +112,11 @@
|
||||
"actionPing": "Ping",
|
||||
"actionFillMyDay": "Mein Tag füllen",
|
||||
"actionCustom": "Eigener Prompt",
|
||||
"actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.",
|
||||
"actionImportTickets": "Tickets + Tag planen",
|
||||
"actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Tickets + Tag planen“ importiert erst deine zugewiesenen Tickets in die passenden Listen und startet dann die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.",
|
||||
"customPromptLabel": "Prompt",
|
||||
"customPromptPlaceholder": "Der vollständige Prompt für diesen Zeitplan (Pflichtfeld)",
|
||||
"maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“.",
|
||||
"maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“ oder „Tickets + Tag planen“.",
|
||||
"promptOverrideLabel": "Prompt-Zusatz",
|
||||
"promptOverridePlaceholder": "Zusätzliche Anweisungen, die für diesen Zeitplan an den Prime-Prompt angehängt werden (optional)",
|
||||
"dayMo": "Mo",
|
||||
@@ -152,7 +153,10 @@
|
||||
"token": "Personal Access Token",
|
||||
"tokenSet": "gespeichert",
|
||||
"testConnection": "Verbindung testen",
|
||||
"clearToken": "Token entfernen"
|
||||
"clearToken": "Token entfernen",
|
||||
"fallbackList": "Fallback-Liste",
|
||||
"fallbackListHint": "Ticket-Items, die Claude keiner Liste zuordnen kann, landen hier. Keine = sie werden übersprungen.",
|
||||
"fallbackNone": "(keine)"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "geerbt · Liste",
|
||||
@@ -285,6 +289,7 @@
|
||||
"copyFormattedTip": "Titel, Beschreibung und offene Schritte kopieren",
|
||||
"toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten",
|
||||
"planDay": "Tag planen",
|
||||
"importTicketsAndPlanDay": "Tickets + Tag planen",
|
||||
"prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen",
|
||||
"roadblockReply": {
|
||||
"placeholder": "Auf den Roadblock antworten…",
|
||||
@@ -937,7 +942,8 @@
|
||||
},
|
||||
"reports": {
|
||||
"generatingWeekly": "Bericht wird erstellt…",
|
||||
"runningDailyPrep": "Tagesvorbereitung läuft…"
|
||||
"runningDailyPrep": "Tagesvorbereitung läuft…",
|
||||
"runningTicketImport": "Tickets werden importiert, danach Tagesplanung…"
|
||||
},
|
||||
"planning": {
|
||||
"finalizing": "Plan wird finalisiert…",
|
||||
|
||||
@@ -112,10 +112,11 @@
|
||||
"actionPing": "Ping",
|
||||
"actionFillMyDay": "Fill My Day",
|
||||
"actionCustom": "Custom prompt",
|
||||
"actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.",
|
||||
"actionImportTickets": "Tickets + plan day",
|
||||
"actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Tickets + plan day first imports your assigned tickets into the matching lists, then runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.",
|
||||
"customPromptLabel": "Prompt",
|
||||
"customPromptPlaceholder": "The full prompt to run for this schedule (required)",
|
||||
"maxTasksDisabledHint": "Only applies to Fill My Day schedules.",
|
||||
"maxTasksDisabledHint": "Only applies to Fill My Day and Tickets + plan day schedules.",
|
||||
"promptOverrideLabel": "Prompt addition",
|
||||
"promptOverridePlaceholder": "Extra instructions appended to the daily prep prompt for this schedule (optional)",
|
||||
"dayMo": "Mo",
|
||||
@@ -152,7 +153,10 @@
|
||||
"token": "Personal access token",
|
||||
"tokenSet": "stored",
|
||||
"testConnection": "Test connection",
|
||||
"clearToken": "Remove token"
|
||||
"clearToken": "Remove token",
|
||||
"fallbackList": "Fallback list",
|
||||
"fallbackListHint": "Imported ticket items Claude can't match to a list land here. None = they are skipped.",
|
||||
"fallbackNone": "(none)"
|
||||
},
|
||||
"inherit": {
|
||||
"inheritedFromList": "inherited · List",
|
||||
@@ -285,6 +289,7 @@
|
||||
"copyFormattedTip": "Copy title, description and open steps",
|
||||
"toggleEditPreviewTip": "Toggle edit/preview",
|
||||
"planDay": "Plan day",
|
||||
"importTicketsAndPlanDay": "Tickets + plan day",
|
||||
"prepEmpty": "No prep run today yet — click Plan day",
|
||||
"roadblockReply": {
|
||||
"placeholder": "Reply to the roadblock…",
|
||||
@@ -937,7 +942,8 @@
|
||||
},
|
||||
"reports": {
|
||||
"generatingWeekly": "Generating report…",
|
||||
"runningDailyPrep": "Running daily prep…"
|
||||
"runningDailyPrep": "Running daily prep…",
|
||||
"runningTicketImport": "Importing tickets, then planning the day…"
|
||||
},
|
||||
"planning": {
|
||||
"finalizing": "Finalizing plan…",
|
||||
|
||||
@@ -190,6 +190,8 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
||||
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
||||
Task<bool> RunDailyPrepNowAsync();
|
||||
/// <summary>Ticket import, then the daily prep, in one Prime run.</summary>
|
||||
Task<bool> RunTicketImportNowAsync();
|
||||
Task RefineTaskAsync(string taskId);
|
||||
|
||||
event Action<string>? RefineStartedEvent;
|
||||
@@ -244,6 +246,8 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync();
|
||||
|
||||
Task<TicketSettingsDto?> GetTicketSettingsAsync();
|
||||
/// <summary>Sets the fallback list for imported ticket items; null clears it.</summary>
|
||||
Task SetTicketFallbackListAsync(string? listId);
|
||||
Task SetTicketApiBaseUrlAsync(string? baseUrl);
|
||||
Task SetTicketTokenAsync(string token);
|
||||
Task ClearTicketTokenAsync();
|
||||
|
||||
@@ -504,6 +504,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<bool> RunDailyPrepNowAsync()
|
||||
=> InvokeTimedAsync<bool>("RunDailyPrepNow", () => _hub.InvokeAsync<bool>("RunDailyPrepNow"));
|
||||
|
||||
public Task<bool> RunTicketImportNowAsync()
|
||||
=> InvokeTimedAsync<bool>("RunTicketImportNow", () => _hub.InvokeAsync<bool>("RunTicketImportNow"));
|
||||
|
||||
public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId));
|
||||
|
||||
public Task ClearMyDayAsync()
|
||||
@@ -758,6 +761,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<TicketSettingsDto?> GetTicketSettingsAsync()
|
||||
=> TryInvokeAsync<TicketSettingsDto>("GetTicketSettings");
|
||||
|
||||
public Task SetTicketFallbackListAsync(string? listId)
|
||||
=> InvokeTimedAsync("SetTicketFallbackList", () => _hub.InvokeAsync("SetTicketFallbackList", listId));
|
||||
|
||||
public Task SetTicketApiBaseUrlAsync(string? baseUrl)
|
||||
=> InvokeTimedAsync("SetTicketApiBaseUrl", () => _hub.InvokeAsync("SetTicketApiBaseUrl", baseUrl));
|
||||
|
||||
|
||||
@@ -581,6 +581,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Bind(null, "prep");
|
||||
IsPrepMode = true;
|
||||
_ = Prep.LoadLastPrepLogIfEmptyAsync();
|
||||
_ = Prep.RefreshTicketsConfiguredAsync();
|
||||
}
|
||||
|
||||
/// <param name="source">What triggered the selection (<see cref="TasksIslandViewModel.SelectionSource"/>);
|
||||
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Ui.Helpers;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
@@ -20,6 +21,9 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
||||
|
||||
[ObservableProperty] private bool _isPrepRunning;
|
||||
|
||||
// The button only makes sense with a configured ticket system — otherwise it would just plan the day.
|
||||
[ObservableProperty] private bool _ticketsConfigured;
|
||||
|
||||
// Covers the gap between the click and the worker's PrepStartedEvent broadcast — IsPrepRunning
|
||||
// stays false until that event arrives, so without this the button click looks like a no-op.
|
||||
public OperationStatus PrepOperation { get; } = new();
|
||||
@@ -57,6 +61,7 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
if (e.PropertyName != nameof(OperationStatus.IsRunning)) return;
|
||||
PlanDayCommand.NotifyCanExecuteChanged();
|
||||
ImportTicketsAndPlanDayCommand.NotifyCanExecuteChanged();
|
||||
OnPropertyChanged(nameof(IsPlanDayEnabled));
|
||||
};
|
||||
}
|
||||
@@ -78,6 +83,25 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
||||
catch { }
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanPlanDay))]
|
||||
private async System.Threading.Tasks.Task ImportTicketsAndPlanDayAsync()
|
||||
{
|
||||
using var op = PrepOperation.Begin(Loc.T("ops.reports.runningTicketImport"));
|
||||
try { await _worker.RunTicketImportNowAsync(); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Reads whether the ticket system has a base URL and a token; keeps the last value when the worker is unreachable.</summary>
|
||||
public async System.Threading.Tasks.Task RefreshTicketsConfiguredAsync()
|
||||
{
|
||||
if (!TicketSettingsTabViewModel.FeatureEnabled) { TicketsConfigured = false; return; }
|
||||
TicketSettingsDto? dto;
|
||||
try { dto = await _worker.GetTicketSettingsAsync(); }
|
||||
catch { return; }
|
||||
if (dto is null) return;
|
||||
TicketsConfigured = !string.IsNullOrWhiteSpace(dto.ApiBaseUrl) && dto.TokenSet;
|
||||
}
|
||||
|
||||
public async System.Threading.Tasks.Task LoadLastPrepLogIfEmptyAsync()
|
||||
{
|
||||
if (IsPrepRunning || PrepLog.Count > 0) return;
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
|
||||
}
|
||||
|
||||
private void UpdateAnyFillMyDay() =>
|
||||
AnyFillMyDay = Rows.Any(r => r.Kind == PrimeActionKind.FillMyDay);
|
||||
AnyFillMyDay = Rows.Any(r => r.Kind is PrimeActionKind.FillMyDay or PrimeActionKind.ImportTickets);
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
/// ItemsControl share one selection.</summary>
|
||||
public string RadioGroup => $"prime-kind-{Id}";
|
||||
|
||||
// Three booleans rather than an enum-to-bool converter: RadioButton.IsChecked binds
|
||||
// One boolean per kind rather than an enum-to-bool converter: RadioButton.IsChecked binds
|
||||
// TwoWay to a bool, and compiled bindings stay simple.
|
||||
public bool IsPing
|
||||
{
|
||||
@@ -68,6 +68,12 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
set { if (value) Kind = PrimeActionKind.Custom; }
|
||||
}
|
||||
|
||||
public bool IsImportTickets
|
||||
{
|
||||
get => Kind == PrimeActionKind.ImportTickets;
|
||||
set { if (value) Kind = PrimeActionKind.ImportTickets; }
|
||||
}
|
||||
|
||||
public bool ShowPrompt => Kind != PrimeActionKind.Ping;
|
||||
|
||||
public string PromptLabel => Kind == PrimeActionKind.Custom
|
||||
@@ -83,6 +89,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
||||
OnPropertyChanged(nameof(IsPing));
|
||||
OnPropertyChanged(nameof(IsFillMyDay));
|
||||
OnPropertyChanged(nameof(IsCustom));
|
||||
OnPropertyChanged(nameof(IsImportTickets));
|
||||
OnPropertyChanged(nameof(ShowPrompt));
|
||||
OnPropertyChanged(nameof(PromptLabel));
|
||||
OnPropertyChanged(nameof(PromptPlaceholder));
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
|
||||
/// <summary>One fallback-list choice; Id null = no fallback.</summary>
|
||||
public sealed record FallbackListOption(string? Id, string Name);
|
||||
|
||||
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
{
|
||||
/// <summary>
|
||||
@@ -21,6 +27,7 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
public const bool FeatureEnabled = true;
|
||||
|
||||
private readonly IWorkerClient _worker;
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
|
||||
[ObservableProperty] private string _apiBaseUrl = "";
|
||||
[ObservableProperty] private string _token = "";
|
||||
@@ -33,7 +40,17 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
// Never shows the stored token — TicketSettingsDto only ever carries the bool.
|
||||
public string TokenPlaceholder => TokenSet ? Loc.T("settings.tickets.tokenSet") : "";
|
||||
|
||||
public TicketSettingsTabViewModel(IWorkerClient worker) => _worker = worker;
|
||||
public ObservableCollection<FallbackListOption> FallbackLists { get; } = new();
|
||||
[ObservableProperty] private FallbackListOption? _selectedFallbackList;
|
||||
|
||||
// Only a successful load may write the fallback back — otherwise an offline load would clear it.
|
||||
private bool _fallbackLoaded;
|
||||
|
||||
public TicketSettingsTabViewModel(IWorkerClient worker, IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
{
|
||||
_worker = worker;
|
||||
_dbFactory = dbFactory;
|
||||
}
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
@@ -45,6 +62,17 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
}
|
||||
ApiBaseUrl = dto.ApiBaseUrl ?? "";
|
||||
TokenSet = dto.TokenSet;
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var lists = await ctx.Lists.AsNoTracking()
|
||||
.OrderBy(l => l.SortOrder).ThenBy(l => l.Name)
|
||||
.Select(l => new FallbackListOption(l.Id, l.Name))
|
||||
.ToListAsync();
|
||||
FallbackLists.Clear();
|
||||
FallbackLists.Add(new FallbackListOption(null, Loc.T("settings.tickets.fallbackNone")));
|
||||
foreach (var option in lists) FallbackLists.Add(option);
|
||||
SelectedFallbackList = FallbackLists.FirstOrDefault(o => o.Id == dto.FallbackListId) ?? FallbackLists[0];
|
||||
_fallbackLoaded = true;
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
@@ -56,6 +84,8 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
|
||||
Token = "";
|
||||
TokenSet = true;
|
||||
}
|
||||
if (_fallbackLoaded)
|
||||
await _worker.SetTicketFallbackListAsync(SelectedFallbackList?.Id);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -108,7 +108,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
Prime = prime;
|
||||
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
||||
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
||||
Tickets = new TicketSettingsTabViewModel(worker);
|
||||
Tickets = new TicketSettingsTabViewModel(worker, dbFactory);
|
||||
|
||||
_categories = new List<SettingsCategory>
|
||||
{
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
Command="{Binding Prep.PlanDayCommand}"
|
||||
IsEnabled="{Binding Prep.IsPlanDayEnabled}"
|
||||
Content="{loc:Tr details.planDay}"/>
|
||||
<Button Classes="btn"
|
||||
Command="{Binding Prep.ImportTicketsAndPlanDayCommand}"
|
||||
IsEnabled="{Binding Prep.IsPlanDayEnabled}"
|
||||
IsVisible="{Binding Prep.TicketsConfigured}"
|
||||
Content="{loc:Tr details.importTicketsAndPlanDay}"/>
|
||||
<ctl:OperationIndicator Status="{Binding Prep.PrepOperation}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -362,6 +362,9 @@
|
||||
<RadioButton GroupName="{Binding RadioGroup}"
|
||||
Content="{loc:Tr settings.prime.actionFillMyDay}"
|
||||
IsChecked="{Binding IsFillMyDay, Mode=TwoWay}"/>
|
||||
<RadioButton GroupName="{Binding RadioGroup}"
|
||||
Content="{loc:Tr settings.prime.actionImportTickets}"
|
||||
IsChecked="{Binding IsImportTickets, Mode=TwoWay}"/>
|
||||
<RadioButton GroupName="{Binding RadioGroup}"
|
||||
Content="{loc:Tr settings.prime.actionCustom}"
|
||||
IsChecked="{Binding IsCustom, Mode=TwoWay}"/>
|
||||
@@ -770,6 +773,18 @@
|
||||
|
||||
<TextBlock Classes="meta" Text="{Binding Tickets.StatusMessage}"
|
||||
IsVisible="{Binding Tickets.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.fallbackList}"/>
|
||||
<ComboBox ItemsSource="{Binding Tickets.FallbackLists}"
|
||||
SelectedItem="{Binding Tickets.SelectedFallbackList, Mode=TwoWay}"
|
||||
DisplayMemberBinding="{Binding Name}"
|
||||
HorizontalAlignment="Left" MinWidth="240"/>
|
||||
<TextBlock Classes="meta" Text="{loc:Tr settings.tickets.fallbackListHint}"
|
||||
TextWrapping="Wrap" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
@@ -169,6 +169,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult<string?>(null);
|
||||
public virtual Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult("");
|
||||
public virtual Task<bool> RunDailyPrepNowAsync() { RunDailyPrepNowCalls++; return Task.FromResult(false); }
|
||||
public int RunTicketImportNowCalls { get; private set; }
|
||||
public virtual Task<bool> RunTicketImportNowAsync() { RunTicketImportNowCalls++; return Task.FromResult(false); }
|
||||
public virtual Task ClearMyDayAsync() { ClearMyDayCalls++; return Task.CompletedTask; }
|
||||
public virtual Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult<AppSettingsDto?>(null);
|
||||
public virtual Task UpdateAppSettingsAsync(AppSettingsDto dto) => Task.CompletedTask;
|
||||
@@ -220,6 +222,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
Task.FromResult<TokenTrackerDashboardDto?>(new TokenTrackerDashboardDto(true, "http://127.0.0.1:7681/", null));
|
||||
|
||||
public virtual Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult<TicketSettingsDto?>(null);
|
||||
public virtual Task SetTicketFallbackListAsync(string? listId) => Task.CompletedTask;
|
||||
public virtual Task SetTicketApiBaseUrlAsync(string? baseUrl) => Task.CompletedTask;
|
||||
public virtual Task SetTicketTokenAsync(string token) => Task.CompletedTask;
|
||||
public virtual Task ClearTicketTokenAsync() => Task.CompletedTask;
|
||||
|
||||
@@ -58,4 +58,39 @@ public class PrepPanelViewModelTests
|
||||
Assert.False(vm.PrepOperation.IsRunning);
|
||||
Assert.True(vm.PlanDayCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
private sealed class TicketWorker : StubWorkerClient
|
||||
{
|
||||
public TicketSettingsDto? Settings { get; set; }
|
||||
public override Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult(Settings);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("http://api", true, true)]
|
||||
[InlineData("http://api", false, false)]
|
||||
[InlineData(null, true, false)]
|
||||
public async Task TicketsConfigured_needs_base_url_and_token(string? url, bool tokenSet, bool expected)
|
||||
{
|
||||
var worker = new TicketWorker { Settings = new TicketSettingsDto(url, tokenSet) };
|
||||
var vm = new PrepPanelViewModel(worker);
|
||||
|
||||
await vm.RefreshTicketsConfiguredAsync();
|
||||
|
||||
Assert.Equal(expected, vm.TicketsConfigured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportTicketsAndPlanDay_calls_the_worker_and_shares_the_plan_day_lock()
|
||||
{
|
||||
var worker = new Worker();
|
||||
var vm = new PrepPanelViewModel(worker);
|
||||
|
||||
var plan = vm.PlanDayCommand.ExecuteAsync(null);
|
||||
Assert.False(vm.ImportTicketsAndPlanDayCommand.CanExecute(null));
|
||||
worker.Gate.SetResult(true);
|
||||
await plan;
|
||||
|
||||
await vm.ImportTicketsAndPlanDayCommand.ExecuteAsync(null);
|
||||
Assert.Equal(1, worker.RunTicketImportNowCalls);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +193,19 @@ public class PrimeClaudeTabViewModelTests
|
||||
Assert.False(vm.AnyFillMyDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Setting_IsImportTickets_Flips_Kind_And_Counts_As_FillMyDay()
|
||||
{
|
||||
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
||||
vm.AddScheduleCommand.Execute(null);
|
||||
|
||||
vm.Rows[0].IsImportTickets = true;
|
||||
|
||||
Assert.Equal(PrimeActionKind.ImportTickets, vm.Rows[0].Kind);
|
||||
Assert.False(vm.Rows[0].IsPing);
|
||||
Assert.True(vm.AnyFillMyDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_Round_Trips_The_Kind()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public sealed class TicketSettingsTabViewModelTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_ticketsettings_{Guid.NewGuid():N}.db");
|
||||
|
||||
public TicketSettingsTabViewModelTests()
|
||||
{
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "Hub", CreatedAt = DateTime.UtcNow, SortOrder = 0 });
|
||||
ctx.Lists.Add(new ListEntity { Id = "l2", Name = "Inbox", CreatedAt = DateTime.UtcNow, SortOrder = 1 });
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||
try { File.Delete(_dbPath + suffix); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext() => new(new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}").Options);
|
||||
|
||||
private sealed class Worker : StubWorkerClient
|
||||
{
|
||||
public TicketSettingsDto? Settings { get; set; }
|
||||
public List<string?> SavedFallbacks { get; } = new();
|
||||
public override Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult(Settings);
|
||||
public override Task SetTicketFallbackListAsync(string? listId) { SavedFallbacks.Add(listId); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_offers_none_plus_every_list_and_selects_the_stored_fallback()
|
||||
{
|
||||
var worker = new Worker { Settings = new TicketSettingsDto("http://api", true, "l2") };
|
||||
var vm = new TicketSettingsTabViewModel(worker, new TestDbFactory(NewContext));
|
||||
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.Equal([null, "l1", "l2"], vm.FallbackLists.Select(o => o.Id));
|
||||
Assert.Equal("l2", vm.SelectedFallbackList?.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_writes_the_selected_fallback_and_none_as_null()
|
||||
{
|
||||
var worker = new Worker { Settings = new TicketSettingsDto("http://api", true, "l2") };
|
||||
var vm = new TicketSettingsTabViewModel(worker, new TestDbFactory(NewContext));
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.SelectedFallbackList = vm.FallbackLists[0];
|
||||
await vm.SaveAsync();
|
||||
|
||||
Assert.Equal([null], worker.SavedFallbacks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Save_does_not_touch_the_fallback_when_the_load_failed()
|
||||
{
|
||||
var worker = new Worker { Settings = null };
|
||||
var vm = new TicketSettingsTabViewModel(worker, new TestDbFactory(NewContext));
|
||||
await vm.LoadAsync();
|
||||
|
||||
await vm.SaveAsync();
|
||||
|
||||
Assert.Empty(worker.SavedFallbacks);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user