Previously both Planning and Planned rendered the same amber badge because a single <Border class="badge planning"> was used. Split into two borders gated by IsPlanning / IsPlanned so Planned picks up the blue badge.planned style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
6.1 KiB
C#
143 lines
6.1 KiB
C#
using System.Collections.Generic;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using ClaudeDo.Data.Models;
|
||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||
|
||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||
|
||
public sealed partial class TaskRowViewModel : ViewModelBase
|
||
{
|
||
public required string Id { get; init; }
|
||
[ObservableProperty] private string _title = "";
|
||
[ObservableProperty] private string _listName = "";
|
||
[ObservableProperty] private bool _done;
|
||
[ObservableProperty] private bool _isStarred;
|
||
[ObservableProperty] private bool _isMyDay;
|
||
[ObservableProperty] private bool _isSelected;
|
||
[ObservableProperty] private TaskStatus _status;
|
||
[ObservableProperty] private string? _branch;
|
||
[ObservableProperty] private string? _diffStat;
|
||
[ObservableProperty] private string? _liveTail;
|
||
[ObservableProperty] private DateTime? _scheduledFor;
|
||
[ObservableProperty] private int _diffAdditions;
|
||
[ObservableProperty] private int _diffDeletions;
|
||
[ObservableProperty] private bool _dropHintAbove;
|
||
[ObservableProperty] private bool _dropHintBelow;
|
||
[ObservableProperty] private string? _parentTaskId;
|
||
[ObservableProperty] private bool _isExpanded = true;
|
||
|
||
public DateTime CreatedAt { get; init; }
|
||
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
||
|
||
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||
public int StepsCount { get; init; }
|
||
public int StepsCompleted { get; init; }
|
||
|
||
public bool IsChild => !string.IsNullOrEmpty(ParentTaskId);
|
||
public bool IsPlanningParent => Status == TaskStatus.Planning || Status == TaskStatus.Planned;
|
||
public bool IsPlanning => Status == TaskStatus.Planning;
|
||
public bool IsPlanned => Status == TaskStatus.Planned;
|
||
public bool IsDraft => Status == TaskStatus.Draft;
|
||
|
||
public bool CanOpenPlanningSession => Status == TaskStatus.Manual && !IsChild;
|
||
public bool CanResumeOrDiscardPlanning => Status == TaskStatus.Planning;
|
||
|
||
public string? PlanningBadge => Status switch
|
||
{
|
||
TaskStatus.Planning => "PLANNING",
|
||
TaskStatus.Planned => "PLANNED",
|
||
_ => null,
|
||
};
|
||
|
||
public bool HasBranch => !string.IsNullOrWhiteSpace(Branch);
|
||
public bool HasDiff => DiffAdditions > 0 || DiffDeletions > 0;
|
||
public bool HasTags => Tags.Count > 0;
|
||
public bool HasSteps => StepsCount > 0;
|
||
public bool IsOverdue => ScheduledFor is { } d && d.Date < DateTime.Today && !Done;
|
||
public bool IsRunning => Status == TaskStatus.Running;
|
||
public bool IsQueued => Status == TaskStatus.Queued;
|
||
public bool HasSchedule => ScheduledFor.HasValue;
|
||
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
||
|
||
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||
public string StepsText => $"{StepsCompleted}/{StepsCount} steps";
|
||
|
||
public string StatusChipClass => Status switch
|
||
{
|
||
TaskStatus.Running => "running",
|
||
TaskStatus.Failed => "error",
|
||
TaskStatus.Done => "review",
|
||
TaskStatus.Queued => "queued",
|
||
_ => "idle",
|
||
};
|
||
|
||
partial void OnStatusChanged(TaskStatus value)
|
||
{
|
||
OnPropertyChanged(nameof(StatusChipClass));
|
||
OnPropertyChanged(nameof(IsRunning));
|
||
OnPropertyChanged(nameof(IsQueued));
|
||
OnPropertyChanged(nameof(HasLiveTail));
|
||
OnPropertyChanged(nameof(IsPlanningParent));
|
||
OnPropertyChanged(nameof(IsPlanning));
|
||
OnPropertyChanged(nameof(IsPlanned));
|
||
OnPropertyChanged(nameof(PlanningBadge));
|
||
OnPropertyChanged(nameof(IsDraft));
|
||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||
OnPropertyChanged(nameof(CanResumeOrDiscardPlanning));
|
||
}
|
||
|
||
partial void OnParentTaskIdChanged(string? value)
|
||
{
|
||
OnPropertyChanged(nameof(IsChild));
|
||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||
}
|
||
|
||
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
|
||
partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail));
|
||
partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue));
|
||
partial void OnScheduledForChanged(DateTime? value)
|
||
{
|
||
OnPropertyChanged(nameof(IsOverdue));
|
||
OnPropertyChanged(nameof(HasSchedule));
|
||
}
|
||
partial void OnDiffAdditionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffAdditionsText)); }
|
||
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
||
|
||
public static TaskRowViewModel FromEntity(TaskEntity t)
|
||
{
|
||
var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
|
||
return new TaskRowViewModel
|
||
{
|
||
Id = t.Id,
|
||
Title = t.Title,
|
||
ListName = t.List?.Name ?? "",
|
||
Done = t.Status == TaskStatus.Done,
|
||
IsStarred = t.IsStarred,
|
||
IsMyDay = t.IsMyDay,
|
||
Status = t.Status,
|
||
Branch = t.Worktree?.BranchName,
|
||
DiffStat = t.Worktree?.DiffStat,
|
||
ScheduledFor = t.ScheduledFor,
|
||
DiffAdditions = add,
|
||
DiffDeletions = del,
|
||
CreatedAt = t.CreatedAt,
|
||
ParentTaskId = t.ParentTaskId,
|
||
};
|
||
}
|
||
|
||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||
private static (int add, int del) ParseDiffStat(string? s)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(s)) return (0, 0);
|
||
int add = 0, del = 0;
|
||
var parts = s.Split(new[] { ' ', ',', '\t' }, StringSplitOptions.RemoveEmptyEntries);
|
||
foreach (var p in parts)
|
||
{
|
||
if (p.Length > 1 && p[0] == '+' && int.TryParse(p.AsSpan(1), out var a)) add = a;
|
||
else if (p.Length > 1 && (p[0] == '-' || p[0] == '\u2212') && int.TryParse(p.AsSpan(1), out var d)) del = d;
|
||
}
|
||
return (add, del);
|
||
}
|
||
}
|