feat(ui): add merge-helper task selection dialog
This commit is contained in:
@@ -125,6 +125,7 @@ sealed class Program
|
||||
sc.AddTransient<Func<DiffViewerViewModel>>(sp => () => sp.GetRequiredService<DiffViewerViewModel>());
|
||||
sc.AddTransient<WorktreesOverviewModalViewModel>();
|
||||
sc.AddTransient<Func<WorktreesOverviewModalViewModel>>(sp => () => sp.GetRequiredService<WorktreesOverviewModalViewModel>());
|
||||
sc.AddTransient<MergeHelperSelectionModalViewModel>();
|
||||
sc.AddSingleton<IPrimeScheduleApi, WorkerPrimeScheduleApi>();
|
||||
sc.AddSingleton<INotesApi, WorkerNotesApi>();
|
||||
sc.AddSingleton<IOnlineLoginService, OnlineLoginService>();
|
||||
|
||||
@@ -365,6 +365,20 @@
|
||||
"forgetFolders": "Ordner vergessen",
|
||||
"alreadyAdded": "(bereits hinzugefügt)"
|
||||
},
|
||||
"mergeHelper": {
|
||||
"title": "CLAUDE MACHEN LASSEN",
|
||||
"windowTitle": "Claude machen lassen",
|
||||
"scopeAll": "Alle Listen",
|
||||
"scopeList": "Liste: {0}",
|
||||
"confirm": "Claude machen lassen",
|
||||
"cancel": "Abbrechen",
|
||||
"selectAll": "Alle auswählen",
|
||||
"selectNone": "Keine auswählen",
|
||||
"empty": "Keine Aufgaben zu übergeben — alles erledigt oder abgebrochen.",
|
||||
"columnTask": "AUFGABE",
|
||||
"columnStatus": "STATUS",
|
||||
"columnList": "LISTE"
|
||||
},
|
||||
"unfinishedPlanning": {
|
||||
"title": "UNVOLLENDETE PLANUNGSSITZUNG",
|
||||
"windowTitle": "Unvollendete Planungssitzung",
|
||||
|
||||
@@ -365,6 +365,20 @@
|
||||
"forgetFolders": "Forget folders",
|
||||
"alreadyAdded": "(already added)"
|
||||
},
|
||||
"mergeHelper": {
|
||||
"title": "LET CLAUDE HANDLE IT",
|
||||
"windowTitle": "Let Claude handle it",
|
||||
"scopeAll": "All lists",
|
||||
"scopeList": "List: {0}",
|
||||
"confirm": "Let Claude handle it",
|
||||
"cancel": "Cancel",
|
||||
"selectAll": "Select all",
|
||||
"selectNone": "Select none",
|
||||
"empty": "No tasks to hand over — everything is done or cancelled.",
|
||||
"columnTask": "TASK",
|
||||
"columnStatus": "STATUS",
|
||||
"columnList": "LIST"
|
||||
},
|
||||
"unfinishedPlanning": {
|
||||
"title": "UNFINISHED PLANNING SESSION",
|
||||
"windowTitle": "Unfinished planning session",
|
||||
|
||||
@@ -22,6 +22,9 @@ public interface IDialogService
|
||||
Task ShowListSettingsAsync(ListSettingsModalViewModel vm);
|
||||
Task ShowRepoImportAsync(RepoImportModalViewModel vm);
|
||||
Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm);
|
||||
|
||||
/// <summary>Merge-helper task picker. Returns the ordered selected task ids, or null on cancel.</summary>
|
||||
Task<IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm);
|
||||
Task ShowWorkerConnectionAsync(WorkerConnectionModalViewModel vm);
|
||||
Task ShowConflictResolverAsync(ConflictResolverViewModel vm);
|
||||
Task ShowLogVisualizerAsync(LogVisualizerViewModel vm);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
public sealed partial class MergeHelperTaskRowViewModel : ViewModelBase
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string StatusText { get; init; }
|
||||
public required string ListName { get; init; }
|
||||
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Let Claude handle it" task picker: lists the non-terminal tasks of a list (or all lists),
|
||||
/// pre-ticks the actionable ones, and resolves <see cref="Result"/> with the ordered selected
|
||||
/// task ids (null on cancel).
|
||||
/// </summary>
|
||||
public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private string? _listId;
|
||||
|
||||
public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();
|
||||
|
||||
[ObservableProperty] private string _scopeLabel = "";
|
||||
[ObservableProperty] private bool _isGlobal;
|
||||
|
||||
public bool HasTasks => Tasks.Count > 0;
|
||||
public bool CanConfirm => Tasks.Any(t => t.IsSelected);
|
||||
|
||||
public TaskCompletionSource<IReadOnlyList<string>?> Result { get; } = new();
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public MergeHelperSelectionModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
=> _dbFactory = dbFactory;
|
||||
|
||||
public void Configure(string? listId, string? listName)
|
||||
{
|
||||
_listId = listId;
|
||||
IsGlobal = listId is null;
|
||||
ScopeLabel = listId is null
|
||||
? Loc.T("modals.mergeHelper.scopeAll")
|
||||
: Loc.T("modals.mergeHelper.scopeList", listName ?? "");
|
||||
}
|
||||
|
||||
public async Task LoadAsync(CancellationToken ct = default)
|
||||
{
|
||||
foreach (var row in Tasks) row.PropertyChanged -= OnRowChanged;
|
||||
Tasks.Clear();
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var query = ctx.Tasks.AsNoTracking()
|
||||
.Where(t => t.Status != TaskStatus.Done && t.Status != TaskStatus.Cancelled);
|
||||
if (_listId is not null)
|
||||
query = query.Where(t => t.ListId == _listId);
|
||||
|
||||
var candidates = await query
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.Select(t => new { t.Id, t.Title, t.Status, ListName = t.List.Name })
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var c in candidates)
|
||||
{
|
||||
var row = new MergeHelperTaskRowViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
StatusText = c.Status.ToString(),
|
||||
ListName = c.ListName,
|
||||
IsSelected = IsTickedByDefault(c.Status),
|
||||
};
|
||||
row.PropertyChanged += OnRowChanged;
|
||||
Tasks.Add(row);
|
||||
}
|
||||
OnPropertyChanged(nameof(HasTasks));
|
||||
OnPropertyChanged(nameof(CanConfirm));
|
||||
}
|
||||
|
||||
// Actionable statuses are pre-ticked; Running/WaitingForChildren are listed but unticked
|
||||
// (the helper would only poll them). Done/Cancelled never make it into the list.
|
||||
internal static bool IsTickedByDefault(TaskStatus status) => status
|
||||
is TaskStatus.Idle or TaskStatus.Queued or TaskStatus.WaitingForReview or TaskStatus.Failed;
|
||||
|
||||
private void OnRowChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MergeHelperTaskRowViewModel.IsSelected))
|
||||
OnPropertyChanged(nameof(CanConfirm));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectAll()
|
||||
{
|
||||
foreach (var t in Tasks) t.IsSelected = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectNone()
|
||||
{
|
||||
foreach (var t in Tasks) t.IsSelected = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
Result.TrySetResult(Tasks.Where(t => t.IsSelected).Select(t => t.Id).ToList());
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result.TrySetResult(null);
|
||||
CloseAction?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
x:Class="ClaudeDo.Ui.Views.Modals.MergeHelperSelectionModal"
|
||||
x:DataType="vm:MergeHelperSelectionModalViewModel"
|
||||
Title="{loc:Tr modals.mergeHelper.windowTitle}"
|
||||
Width="560" Height="480" MinWidth="420" MinHeight="320"
|
||||
CanResize="True"
|
||||
WindowDecorations="BorderOnly"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="-1"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource SurfaceBrush}">
|
||||
<Window.KeyBindings>
|
||||
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
|
||||
</Window.KeyBindings>
|
||||
|
||||
<ctl:ModalShell Title="{loc:Tr modals.mergeHelper.title}" CloseCommand="{Binding CancelCommand}">
|
||||
<ctl:ModalShell.Footer>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<Button Classes="btn" Content="{loc:Tr modals.mergeHelper.cancel}" Command="{Binding CancelCommand}" MinWidth="90"/>
|
||||
<Button Content="{loc:Tr modals.mergeHelper.confirm}" Command="{Binding ConfirmCommand}"
|
||||
IsEnabled="{Binding CanConfirm}" MinWidth="140" Classes="primary"/>
|
||||
</StackPanel>
|
||||
</ctl:ModalShell.Footer>
|
||||
|
||||
<!-- Body: scope + select-all/none toolbar, headers, checkbox rows -->
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto,Auto" Margin="20,12,20,6">
|
||||
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding ScopeLabel}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectAll}"
|
||||
Command="{Binding SelectAllCommand}" IsEnabled="{Binding HasTasks}"/>
|
||||
<Button Grid.Column="2" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectNone}"
|
||||
Command="{Binding SelectNoneCommand}" IsEnabled="{Binding HasTasks}" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Column headers -->
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="32,*,120,120" Margin="20,0,20,4"
|
||||
IsVisible="{Binding HasTasks}">
|
||||
<TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.mergeHelper.columnTask}"/>
|
||||
<TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.mergeHelper.columnStatus}"/>
|
||||
<TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.mergeHelper.columnList}"
|
||||
IsVisible="{Binding IsGlobal}"/>
|
||||
</Grid>
|
||||
|
||||
<ScrollViewer Padding="20,2,20,8">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="meta" Margin="0,8"
|
||||
Text="{loc:Tr modals.mergeHelper.empty}"
|
||||
IsVisible="{Binding !HasTasks}"/>
|
||||
<ItemsControl ItemsSource="{Binding Tasks}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:MergeHelperTaskRowViewModel">
|
||||
<Grid ColumnDefinitions="32,*,120,120" Margin="0,1">
|
||||
<CheckBox Grid.Column="0" MinWidth="0"
|
||||
IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Classes="body" Grid.Column="1" Text="{Binding Title}"
|
||||
VerticalAlignment="Center" Margin="4,0,0,0"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<Border Grid.Column="2" Classes="chip"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding StatusText}"/>
|
||||
</Border>
|
||||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding ListName}"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding $parent[Window].((vm:MergeHelperSelectionModalViewModel)DataContext).IsGlobal}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
|
||||
</ctl:ModalShell>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClaudeDo.Ui.Views.Modals;
|
||||
|
||||
public partial class MergeHelperSelectionModal : Window
|
||||
{
|
||||
public MergeHelperSelectionModal()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,15 @@ public sealed class WindowDialogService : IDialogService
|
||||
await dlg.ShowDialog(_owner);
|
||||
}
|
||||
|
||||
public async Task<System.Collections.Generic.IReadOnlyList<string>?> ShowMergeHelperSelectionAsync(MergeHelperSelectionModalViewModel vm)
|
||||
{
|
||||
var dlg = new MergeHelperSelectionModal { DataContext = vm };
|
||||
vm.CloseAction = () => dlg.Close();
|
||||
dlg.Closed += (_, _) => vm.Result.TrySetResult(null); // native close counts as cancel
|
||||
await dlg.ShowDialog(_owner);
|
||||
return await vm.Result.Task;
|
||||
}
|
||||
|
||||
public async Task ShowWorktreesOverviewAsync(WorktreesOverviewModalViewModel vm)
|
||||
{
|
||||
var dlg = new WorktreesOverviewModalView { DataContext = vm };
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Linq;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Modals;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class MergeHelperSelectionModalViewModelTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public MergeHelperSelectionModalViewModelTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_mh_test_{Guid.NewGuid():N}.db");
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private MergeHelperSelectionModalViewModel BuildVm()
|
||||
=> new(new TestDbFactory(NewContext));
|
||||
|
||||
private async Task SeedAllStatusesAsync()
|
||||
{
|
||||
await using var db = NewContext();
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
db.Lists.Add(new ListEntity { Id = "L2", Name = "Home", CreatedAt = DateTime.UtcNow });
|
||||
var order = 0;
|
||||
void Add(string id, string listId, TaskStatus status) =>
|
||||
db.Tasks.Add(new TaskEntity
|
||||
{
|
||||
Id = id, ListId = listId, Title = $"task {id}",
|
||||
Status = status, CreatedAt = DateTime.UtcNow, SortOrder = order++,
|
||||
});
|
||||
|
||||
Add("t-idle", "L1", TaskStatus.Idle);
|
||||
Add("t-queued", "L1", TaskStatus.Queued);
|
||||
Add("t-running", "L1", TaskStatus.Running);
|
||||
Add("t-review", "L1", TaskStatus.WaitingForReview);
|
||||
Add("t-children", "L1", TaskStatus.WaitingForChildren);
|
||||
Add("t-failed", "L1", TaskStatus.Failed);
|
||||
Add("t-done", "L1", TaskStatus.Done);
|
||||
Add("t-cancelled", "L1", TaskStatus.Cancelled);
|
||||
Add("t-other-list", "L2", TaskStatus.Idle);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_ExcludesTerminalStatuses_AndTicksActionableByDefault()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure(null, null);
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.DoesNotContain(vm.Tasks, t => t.Id is "t-done" or "t-cancelled");
|
||||
Assert.Equal(7, vm.Tasks.Count);
|
||||
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-idle").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-queued").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-review").IsSelected);
|
||||
Assert.True(vm.Tasks.Single(t => t.Id == "t-failed").IsSelected);
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "t-running").IsSelected);
|
||||
Assert.False(vm.Tasks.Single(t => t.Id == "t-children").IsSelected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_PerListScope_FiltersToThatList()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L2", "Home");
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.Single(vm.Tasks);
|
||||
Assert.Equal("t-other-list", vm.Tasks[0].Id);
|
||||
Assert.Equal("Home", vm.Tasks[0].ListName);
|
||||
Assert.False(vm.IsGlobal);
|
||||
Assert.Contains("Home", vm.ScopeLabel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanConfirm_FollowsRowSelection()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure(null, null);
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.True(vm.CanConfirm);
|
||||
|
||||
vm.SelectNoneCommand.Execute(null);
|
||||
Assert.False(vm.CanConfirm);
|
||||
Assert.All(vm.Tasks, t => Assert.False(t.IsSelected));
|
||||
|
||||
vm.Tasks[0].IsSelected = true; // single row re-enables via PropertyChanged hook
|
||||
Assert.True(vm.CanConfirm);
|
||||
|
||||
vm.SelectAllCommand.Execute(null);
|
||||
Assert.All(vm.Tasks, t => Assert.True(t.IsSelected));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Confirm_ReturnsSelectedIds_InRowOrder()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure(null, null);
|
||||
await vm.LoadAsync();
|
||||
|
||||
vm.SelectNoneCommand.Execute(null);
|
||||
vm.Tasks.Single(t => t.Id == "t-review").IsSelected = true;
|
||||
vm.Tasks.Single(t => t.Id == "t-idle").IsSelected = true;
|
||||
|
||||
var closed = false;
|
||||
vm.CloseAction = () => closed = true;
|
||||
vm.ConfirmCommand.Execute(null);
|
||||
|
||||
var result = await vm.Result.Task;
|
||||
Assert.NotNull(result);
|
||||
// Row order (SortOrder): t-idle was seeded before t-review.
|
||||
Assert.Equal(new[] { "t-idle", "t-review" }, result);
|
||||
Assert.True(closed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancel_ReturnsNull()
|
||||
{
|
||||
await SeedAllStatusesAsync();
|
||||
var vm = BuildVm();
|
||||
vm.Configure(null, null);
|
||||
await vm.LoadAsync();
|
||||
|
||||
var closed = false;
|
||||
vm.CloseAction = () => closed = true;
|
||||
vm.CancelCommand.Execute(null);
|
||||
|
||||
Assert.Null(await vm.Result.Task);
|
||||
Assert.True(closed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Load_NoCandidates_HasTasksFalse_CannotConfirm()
|
||||
{
|
||||
await using (var db = NewContext())
|
||||
{
|
||||
db.Lists.Add(new ListEntity { Id = "L1", Name = "Work", CreatedAt = DateTime.UtcNow });
|
||||
db.Tasks.Add(new TaskEntity { Id = "t-done", ListId = "L1", Title = "done", Status = TaskStatus.Done, CreatedAt = DateTime.UtcNow });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var vm = BuildVm();
|
||||
vm.Configure("L1", "Work");
|
||||
await vm.LoadAsync();
|
||||
|
||||
Assert.False(vm.HasTasks);
|
||||
Assert.False(vm.CanConfirm);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user