feat(ui): NotesEditorViewModel with day navigation and bullet CRUD
This commit is contained in:
83
src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs
Normal file
83
src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using ClaudeDo.Ui.Services.Interfaces;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
public sealed partial class NoteBulletViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly Func<NoteBulletViewModel, Task> _save;
|
||||||
|
private readonly Func<NoteBulletViewModel, Task> _delete;
|
||||||
|
|
||||||
|
public string Id { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private string _text;
|
||||||
|
|
||||||
|
public NoteBulletViewModel(string id, string text,
|
||||||
|
Func<NoteBulletViewModel, Task> save, Func<NoteBulletViewModel, Task> delete)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
_text = text;
|
||||||
|
_save = save;
|
||||||
|
_delete = delete;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private Task Save() => _save(this);
|
||||||
|
[RelayCommand] private Task Delete() => _delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class NotesEditorViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly INotesApi _api;
|
||||||
|
|
||||||
|
public NotesEditorViewModel(INotesApi api) => _api = api;
|
||||||
|
|
||||||
|
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
[ObservableProperty] private string _newBulletText = "";
|
||||||
|
|
||||||
|
public DateTime CurrentDate
|
||||||
|
{
|
||||||
|
get => CurrentDay.ToDateTime(TimeOnly.MinValue);
|
||||||
|
set { var d = DateOnly.FromDateTime(value); if (d != CurrentDay) _ = LoadDayAsync(d); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CurrentDayLabel => CurrentDay.ToString("dddd, dd.MM.yyyy");
|
||||||
|
|
||||||
|
public async Task LoadDayAsync(DateOnly day)
|
||||||
|
{
|
||||||
|
CurrentDay = day;
|
||||||
|
OnPropertyChanged(nameof(CurrentDate));
|
||||||
|
OnPropertyChanged(nameof(CurrentDayLabel));
|
||||||
|
Bullets.Clear();
|
||||||
|
foreach (var dto in await _api.ListAsync(day))
|
||||||
|
Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private NoteBulletViewModel MakeBullet(string id, string text) =>
|
||||||
|
new(id, text, SaveBulletAsync, DeleteBulletAsync);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddBullet()
|
||||||
|
{
|
||||||
|
var text = NewBulletText.Trim();
|
||||||
|
if (text.Length == 0) return;
|
||||||
|
var dto = await _api.AddAsync(CurrentDay, text);
|
||||||
|
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||||
|
NewBulletText = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
||||||
|
[RelayCommand] private Task NextDay() => LoadDayAsync(CurrentDay.AddDays(1));
|
||||||
|
[RelayCommand] private Task Today() => LoadDayAsync(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
|
||||||
|
private Task SaveBulletAsync(NoteBulletViewModel b) => _api.UpdateAsync(b.Id, b.Text);
|
||||||
|
|
||||||
|
private async Task DeleteBulletAsync(NoteBulletViewModel b)
|
||||||
|
{
|
||||||
|
await _api.DeleteAsync(b.Id);
|
||||||
|
Bullets.Remove(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.Services.Interfaces;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||||
|
|
||||||
|
public class NotesEditorViewModelTests
|
||||||
|
{
|
||||||
|
private sealed class FakeNotes : INotesApi
|
||||||
|
{
|
||||||
|
public readonly List<DailyNoteDto> Store = new();
|
||||||
|
private int _seq;
|
||||||
|
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) =>
|
||||||
|
Task.FromResult(Store.Where(n => n.Date == day.ToString("yyyy-MM-dd")).ToList());
|
||||||
|
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text)
|
||||||
|
{
|
||||||
|
var dto = new DailyNoteDto($"id{_seq++}", day.ToString("yyyy-MM-dd"), text, Store.Count);
|
||||||
|
Store.Add(dto);
|
||||||
|
return Task.FromResult<DailyNoteDto?>(dto);
|
||||||
|
}
|
||||||
|
public Task UpdateAsync(string id, string text)
|
||||||
|
{
|
||||||
|
var i = Store.FindIndex(n => n.Id == id);
|
||||||
|
if (i >= 0) Store[i] = Store[i] with { Text = text };
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
public Task DeleteAsync(string id) { Store.RemoveAll(n => n.Id == id); return Task.CompletedTask; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddBullet_PersistsAndAppears_ForCurrentDay()
|
||||||
|
{
|
||||||
|
var api = new FakeNotes();
|
||||||
|
var vm = new NotesEditorViewModel(api);
|
||||||
|
await vm.LoadDayAsync(new DateOnly(2026, 6, 1));
|
||||||
|
|
||||||
|
vm.NewBulletText = "Standup vorbereitet";
|
||||||
|
await vm.AddBulletCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Single(vm.Bullets);
|
||||||
|
Assert.Equal("Standup vorbereitet", vm.Bullets[0].Text);
|
||||||
|
Assert.Equal("", vm.NewBulletText);
|
||||||
|
Assert.Single(api.Store);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PrevAndNextDay_NavigateAndReload()
|
||||||
|
{
|
||||||
|
var api = new FakeNotes();
|
||||||
|
await api.AddAsync(new DateOnly(2026, 5, 31), "gestern");
|
||||||
|
var vm = new NotesEditorViewModel(api);
|
||||||
|
await vm.LoadDayAsync(new DateOnly(2026, 6, 1));
|
||||||
|
Assert.Empty(vm.Bullets);
|
||||||
|
|
||||||
|
await vm.PrevDayCommand.ExecuteAsync(null);
|
||||||
|
Assert.Equal(new DateOnly(2026, 5, 31), vm.CurrentDay);
|
||||||
|
Assert.Single(vm.Bullets);
|
||||||
|
|
||||||
|
await vm.NextDayCommand.ExecuteAsync(null);
|
||||||
|
Assert.Equal(new DateOnly(2026, 6, 1), vm.CurrentDay);
|
||||||
|
Assert.Empty(vm.Bullets);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteBullet_RemovesFromStoreAndList()
|
||||||
|
{
|
||||||
|
var api = new FakeNotes();
|
||||||
|
var vm = new NotesEditorViewModel(api);
|
||||||
|
await vm.LoadDayAsync(new DateOnly(2026, 6, 1));
|
||||||
|
vm.NewBulletText = "weg damit";
|
||||||
|
await vm.AddBulletCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
await vm.Bullets[0].DeleteCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Empty(vm.Bullets);
|
||||||
|
Assert.Empty(api.Store);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user