feat(ui): Settings-Modal auf Sidebar-Kategorien umbauen
TabControl bleibt Content-Host, TabStrip wird retempliert (nur PART_SelectedContentHost) und durch eine zweigruppige Sidebar (BASIS/ERWEITERT) ersetzt. General wird in Allgemein/Ausfuehrung/Berichte gesplittet, Session Skills (Checkbox-Liste aus General + Skills-Tab) zu einer Liste zusammengelegt, und ein Repo-Hinweisstreifen erscheint auf Worktrees/Prime Claude/Session Skills/Berichte solange keine Liste ein WorkingDir hat. Fensterbreite 580 -> 700.
This commit is contained in:
@@ -10,6 +10,21 @@
|
|||||||
"tabFiles": "Dateien",
|
"tabFiles": "Dateien",
|
||||||
"tabPrime": "Prime Claude",
|
"tabPrime": "Prime Claude",
|
||||||
"tabSkills": "Skills",
|
"tabSkills": "Skills",
|
||||||
|
"sidebar": {
|
||||||
|
"groupBasis": "BASIS",
|
||||||
|
"groupAdvanced": "ERWEITERT",
|
||||||
|
"categoryGeneral": "Allgemein",
|
||||||
|
"categoryExecution": "Ausführung",
|
||||||
|
"categoryPrime": "Prime Claude",
|
||||||
|
"categoryWorktrees": "Worktrees",
|
||||||
|
"categoryFiles": "Dateien & Prompts",
|
||||||
|
"categorySkills": "Session Skills",
|
||||||
|
"categoryReports": "Berichte"
|
||||||
|
},
|
||||||
|
"repoHint": {
|
||||||
|
"text": "Wirkt erst mit verknüpftem Repo.",
|
||||||
|
"link": "Repo importieren"
|
||||||
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"defaultInstructions": "Standard-Anweisungen",
|
"defaultInstructions": "Standard-Anweisungen",
|
||||||
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
"defaultInstructionsPlaceholder": "Basis-Anweisungen, die auf jede Aufgabe angewendet werden",
|
||||||
|
|||||||
@@ -10,6 +10,21 @@
|
|||||||
"tabFiles": "Files",
|
"tabFiles": "Files",
|
||||||
"tabPrime": "Prime Claude",
|
"tabPrime": "Prime Claude",
|
||||||
"tabSkills": "Skills",
|
"tabSkills": "Skills",
|
||||||
|
"sidebar": {
|
||||||
|
"groupBasis": "BASICS",
|
||||||
|
"groupAdvanced": "ADVANCED",
|
||||||
|
"categoryGeneral": "General",
|
||||||
|
"categoryExecution": "Execution",
|
||||||
|
"categoryPrime": "Prime Claude",
|
||||||
|
"categoryWorktrees": "Worktrees",
|
||||||
|
"categoryFiles": "Files & Prompts",
|
||||||
|
"categorySkills": "Session Skills",
|
||||||
|
"categoryReports": "Reports"
|
||||||
|
},
|
||||||
|
"repoHint": {
|
||||||
|
"text": "Only takes effect once a repo is linked.",
|
||||||
|
"link": "Import repo"
|
||||||
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"defaultInstructions": "Default instructions",
|
"defaultInstructions": "Default instructions",
|
||||||
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
"defaultInstructionsPlaceholder": "Baseline instructions applied to every task",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using ClaudeDo.Data.Models;
|
|||||||
using ClaudeDo.Localization;
|
using ClaudeDo.Localization;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Agent;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
@@ -33,8 +32,6 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
|
public IReadOnlyList<string> Models { get; } = ModelRegistry.Aliases;
|
||||||
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
|
public IReadOnlyList<string> PermissionModes { get; } = PermissionModeRegistry.Modes;
|
||||||
|
|
||||||
public ObservableCollection<SelectableSkillViewModel> SessionSkills { get; } = new();
|
|
||||||
|
|
||||||
public ObservableCollection<AccentPresetSwatchViewModel> AccentPresetSwatches { get; } = new();
|
public ObservableCollection<AccentPresetSwatchViewModel> AccentPresetSwatches { get; } = new();
|
||||||
private Action<string>? _persistAccent;
|
private Action<string>? _persistAccent;
|
||||||
|
|
||||||
@@ -109,22 +106,6 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase
|
|||||||
return $"Max turns for {row.Model} must be between 1 and 200.";
|
return $"Max turns for {row.Model} must be between 1 and 200.";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Loads the installed-skills checkbox list, reflecting the given selection.</summary>
|
|
||||||
public async Task LoadSessionSkillsAsync(IWorkerClient worker, IReadOnlyCollection<string>? selected)
|
|
||||||
{
|
|
||||||
var installed = await worker.GetSessionSkillsAsync();
|
|
||||||
var selectedSet = selected is null ? new HashSet<string>() : new HashSet<string>(selected);
|
|
||||||
SessionSkills.Clear();
|
|
||||||
foreach (var s in installed)
|
|
||||||
SessionSkills.Add(new SelectableSkillViewModel(s.Name, s.Description, selectedSet.Contains(s.Name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<string>? SelectedSessionSkillNames()
|
|
||||||
{
|
|
||||||
var names = SessionSkills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
|
||||||
return names.Count == 0 ? null : names;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
/// <summary>One row of the per-model preset table. Single consumer, so it lives here.</summary>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isBusy;
|
[ObservableProperty] private bool _isBusy;
|
||||||
[ObservableProperty] private bool _isEmpty = true;
|
[ObservableProperty] private bool _isEmpty = true;
|
||||||
|
|
||||||
public ObservableCollection<SessionSkillDto> Skills { get; } = new();
|
public ObservableCollection<SessionSkillRowViewModel> Skills { get; } = new();
|
||||||
|
|
||||||
public SessionSkillsSettingsTabViewModel(IWorkerClient worker)
|
public SessionSkillsSettingsTabViewModel(IWorkerClient worker)
|
||||||
{
|
{
|
||||||
@@ -33,19 +33,31 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync()
|
/// <summary>Loads the installed skills, marking each row selected per <paramref name="selectedNames"/>.
|
||||||
|
/// When omitted, the current selection is preserved across the refresh (e.g. after install/update/remove).</summary>
|
||||||
|
public async Task LoadAsync(IReadOnlyCollection<string>? selectedNames = null)
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var selected = selectedNames ?? Skills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
||||||
|
var selectedSet = new HashSet<string>(selected);
|
||||||
var skills = await _worker.GetSessionSkillsAsync();
|
var skills = await _worker.GetSessionSkillsAsync();
|
||||||
Skills.Clear();
|
Skills.Clear();
|
||||||
foreach (var s in skills) Skills.Add(s);
|
foreach (var s in skills) Skills.Add(new SessionSkillRowViewModel(s, selectedSet.Contains(s.Name)));
|
||||||
IsEmpty = Skills.Count == 0;
|
IsEmpty = Skills.Count == 0;
|
||||||
}
|
}
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>The globally-selected skill names, or null when none are selected (matches the
|
||||||
|
/// AppSettingsDto.SessionSkills convention of "null means no global selection").</summary>
|
||||||
|
public List<string>? SelectedSkillNames()
|
||||||
|
{
|
||||||
|
var names = Skills.Where(s => s.IsSelected).Select(s => s.Name).ToList();
|
||||||
|
return names.Count == 0 ? null : names;
|
||||||
|
}
|
||||||
|
|
||||||
private bool CanInstall() => !InstallOp.IsRunning;
|
private bool CanInstall() => !InstallOp.IsRunning;
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanInstall))]
|
[RelayCommand(CanExecute = nameof(CanInstall))]
|
||||||
@@ -96,3 +108,27 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase
|
|||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>One installed skill row: identity/management fields from <see cref="SessionSkillDto"/>
|
||||||
|
/// plus the global "active for every task" checkbox state that used to live in a separate
|
||||||
|
/// General-tab list.</summary>
|
||||||
|
public sealed partial class SessionSkillRowViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public string SourceUrl { get; }
|
||||||
|
public string PinnedRef { get; }
|
||||||
|
public DateTimeOffset AddedAt { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
|
||||||
|
public SessionSkillRowViewModel(SessionSkillDto dto, bool isSelected)
|
||||||
|
{
|
||||||
|
Name = dto.Name;
|
||||||
|
Description = dto.Description;
|
||||||
|
SourceUrl = dto.SourceUrl;
|
||||||
|
PinnedRef = dto.PinnedRef;
|
||||||
|
AddedAt = dto.AddedAt;
|
||||||
|
_isSelected = isSelected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,19 @@ using ClaudeDo.Ui.Services;
|
|||||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
/// <summary>One sidebar entry. <c>Group</c> is an internal split key ("Basis"/"Erweitert"), not
|
||||||
|
/// user-facing text — the sidebar renders the two groups as separate lists with their own
|
||||||
|
/// (localized) heading. <c>RequiresRepo</c> marks the categories that show the repo-hint strip.</summary>
|
||||||
|
public sealed record SettingsCategory(string Key, LocalizedString Label, string Group, bool IsVisible, bool RequiresRepo);
|
||||||
|
|
||||||
public sealed partial class SettingsModalViewModel : ViewModelBase
|
public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
|
||||||
public GeneralSettingsTabViewModel General { get; }
|
public GeneralSettingsTabViewModel General { get; }
|
||||||
public WorktreesSettingsTabViewModel Worktrees { get; }
|
public WorktreesSettingsTabViewModel Worktrees { get; }
|
||||||
@@ -19,10 +26,44 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
public PrimeClaudeTabViewModel Prime { get; }
|
public PrimeClaudeTabViewModel Prime { get; }
|
||||||
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
public OnlineInboxSettingsViewModel OnlineInbox { get; }
|
||||||
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
|
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
|
||||||
|
|
||||||
// Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung.
|
// Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung.
|
||||||
public bool ShowOnlineInbox => false;
|
public bool ShowOnlineInbox => false;
|
||||||
|
|
||||||
|
// Fixed order — mirrors the TabItem order in SettingsModalView.axaml 1:1, so
|
||||||
|
// _categories.IndexOf(category) is a valid TabControl.SelectedIndex.
|
||||||
|
private readonly List<SettingsCategory> _categories;
|
||||||
|
public IReadOnlyList<SettingsCategory> Categories => _categories;
|
||||||
|
public IReadOnlyList<SettingsCategory> BasisCategories { get; }
|
||||||
|
public IReadOnlyList<SettingsCategory> ErweitertCategories { get; }
|
||||||
|
|
||||||
|
private SettingsCategory? _selectedCategory;
|
||||||
|
public SettingsCategory? SelectedCategory
|
||||||
|
{
|
||||||
|
get => _selectedCategory;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
// The two sidebar ListBoxes share this property; selecting an item in one pushes null
|
||||||
|
// into the other's binding (item not found in its own ItemsSource) — ignore that noise
|
||||||
|
// instead of letting it clobber the real selection.
|
||||||
|
if (value is null) return;
|
||||||
|
if (SetProperty(ref _selectedCategory, value))
|
||||||
|
SelectedIndex = _categories.IndexOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ObservableProperty] private int _selectedIndex;
|
||||||
|
|
||||||
|
// True once at least one list has a linked repo (WorkingDir). Drives the repo-hint strip on
|
||||||
|
// Worktrees/Prime Claude/Session Skills/Berichte — nothing is ever hidden, just annotated.
|
||||||
|
[ObservableProperty] private bool _hasLinkedRepo;
|
||||||
|
|
||||||
|
// Wired by WindowDialogService to close this modal and open the existing repo-import flow.
|
||||||
|
public Action? RequestRepoImport { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenRepoImport() => RequestRepoImport?.Invoke();
|
||||||
|
|
||||||
[ObservableProperty] private string _validationError = "";
|
[ObservableProperty] private string _validationError = "";
|
||||||
[ObservableProperty] private bool _isBusy;
|
[ObservableProperty] private bool _isBusy;
|
||||||
[ObservableProperty] private string _statusMessage = "";
|
[ObservableProperty] private string _statusMessage = "";
|
||||||
@@ -31,9 +72,11 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime,
|
public SettingsModalViewModel(IWorkerClient worker, PrimeClaudeTabViewModel prime,
|
||||||
IOnlineLoginService onlineLoginService,
|
IOnlineLoginService onlineLoginService,
|
||||||
ILocalizer localizer, AppSettings appSettings)
|
ILocalizer localizer, AppSettings appSettings,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||||
{
|
{
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
General = new GeneralSettingsTabViewModel(localizer, code =>
|
General = new GeneralSettingsTabViewModel(localizer, code =>
|
||||||
{
|
{
|
||||||
appSettings.Language = code;
|
appSettings.Language = code;
|
||||||
@@ -49,6 +92,21 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
Prime = prime;
|
Prime = prime;
|
||||||
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
|
||||||
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
|
||||||
|
|
||||||
|
_categories = new List<SettingsCategory>
|
||||||
|
{
|
||||||
|
new("General", new LocalizedString(localizer, "settings.sidebar.categoryGeneral"), "Basis", true, false),
|
||||||
|
new("Execution", new LocalizedString(localizer, "settings.sidebar.categoryExecution"), "Basis", true, false),
|
||||||
|
new("Prime", new LocalizedString(localizer, "settings.sidebar.categoryPrime"), "Basis", true, true),
|
||||||
|
new("Worktrees", new LocalizedString(localizer, "settings.sidebar.categoryWorktrees"), "Erweitert", true, true),
|
||||||
|
new("Files", new LocalizedString(localizer, "settings.sidebar.categoryFiles"), "Erweitert", true, false),
|
||||||
|
new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true),
|
||||||
|
new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true),
|
||||||
|
new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false),
|
||||||
|
};
|
||||||
|
BasisCategories = _categories.Where(c => c.Group == "Basis").ToList();
|
||||||
|
ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList();
|
||||||
|
_selectedCategory = _categories[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttle stages are edited by dragging the usage-monitor gauges, not on any Settings tab —
|
// Throttle stages are edited by dragging the usage-monitor gauges, not on any Settings tab —
|
||||||
@@ -89,9 +147,11 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
await Prime.LoadAsync();
|
await Prime.LoadAsync();
|
||||||
await OnlineInbox.LoadAsync();
|
await OnlineInbox.LoadAsync();
|
||||||
await SessionSkills.LoadAsync();
|
await SessionSkills.LoadAsync(dto?.SessionSkills);
|
||||||
await General.LoadSessionSkillsAsync(_worker, dto?.SessionSkills);
|
|
||||||
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
|
||||||
|
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
HasLinkedRepo = await ctx.Lists.AnyAsync(l => l.WorkingDir != null && l.WorkingDir != "");
|
||||||
}
|
}
|
||||||
finally { IsBusy = false; }
|
finally { IsBusy = false; }
|
||||||
}
|
}
|
||||||
@@ -121,7 +181,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
.Split('\n').Select(l => l.Trim().TrimEnd('\r')).Where(l => l.Length > 0).ToList()),
|
||||||
General.StandupWeekday,
|
General.StandupWeekday,
|
||||||
Prime.DailyPrepMaxTasks,
|
Prime.DailyPrepMaxTasks,
|
||||||
General.SelectedSessionSkillNames(),
|
SessionSkills.SelectedSkillNames(),
|
||||||
General.ModelPresetDtos(),
|
General.ModelPresetDtos(),
|
||||||
General.UsageGateFiveHourPct,
|
General.UsageGateFiveHourPct,
|
||||||
General.UsageGateSevenDayPct,
|
General.UsageGateSevenDayPct,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
||||||
x:DataType="vm:SettingsModalViewModel"
|
x:DataType="vm:SettingsModalViewModel"
|
||||||
Title="{loc:Tr settings.title}"
|
Title="{loc:Tr settings.title}"
|
||||||
Width="580" Height="760" MinWidth="480" MinHeight="520"
|
Width="700" Height="760" MinWidth="600" MinHeight="520"
|
||||||
CanResize="True"
|
CanResize="True"
|
||||||
WindowDecorations="BorderOnly"
|
WindowDecorations="BorderOnly"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ctl:ModalShell.Footer>
|
</ctl:ModalShell.Footer>
|
||||||
|
|
||||||
<!-- Body: tabs + bottom validation/status strip -->
|
<!-- Body: sidebar + tab-content host + bottom validation/status strip -->
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2">
|
<StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2">
|
||||||
<TextBlock Classes="meta" Text="{Binding ValidationError}"
|
<TextBlock Classes="meta" Text="{Binding ValidationError}"
|
||||||
@@ -45,9 +45,58 @@
|
|||||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TabControl Padding="20,16" TabStripPlacement="Top">
|
<Grid ColumnDefinitions="170,1,*">
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.tabGeneral}">
|
<!-- Sidebar: two independently-scrolled groups sharing one SelectedCategory. -->
|
||||||
|
<StackPanel Grid.Column="0" Spacing="16" Margin="12,16,8,8">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Classes="eyebrow" Opacity="0.6" Margin="10,0,0,4"
|
||||||
|
Text="{loc:Tr settings.sidebar.groupBasis}"/>
|
||||||
|
<ListBox ItemsSource="{Binding BasisCategories}"
|
||||||
|
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
|
||||||
|
Background="Transparent">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SettingsCategory">
|
||||||
|
<TextBlock Text="{Binding Label.Value}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Classes="eyebrow" Opacity="0.6" Margin="10,0,0,4"
|
||||||
|
Text="{loc:Tr settings.sidebar.groupAdvanced}"/>
|
||||||
|
<ListBox ItemsSource="{Binding ErweitertCategories}"
|
||||||
|
SelectedItem="{Binding SelectedCategory, Mode=TwoWay}"
|
||||||
|
Background="Transparent">
|
||||||
|
<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem" x:DataType="vm:SettingsCategory">
|
||||||
|
<Setter Property="IsVisible" Value="{Binding IsVisible}"/>
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SettingsCategory">
|
||||||
|
<TextBlock Text="{Binding Label.Value}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Background="{DynamicResource LineBrush}" Width="1" Margin="0,8"/>
|
||||||
|
|
||||||
|
<!-- TabControl stays the content host (SelectedIndex driven by the sidebar); its own
|
||||||
|
header strip is re-templated away so only PART_SelectedContentHost renders. -->
|
||||||
|
<TabControl Grid.Column="2" Padding="20,16" SelectedIndex="{Binding SelectedIndex, Mode=OneWay}">
|
||||||
|
<TabControl.Template>
|
||||||
|
<ControlTemplate TargetType="TabControl">
|
||||||
|
<ContentPresenter Name="PART_SelectedContentHost"
|
||||||
|
Content="{TemplateBinding SelectedContent}"
|
||||||
|
ContentTemplate="{TemplateBinding SelectedContentTemplate}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</TabControl.Template>
|
||||||
|
|
||||||
|
<!-- Allgemein: language, accent, default instructions. -->
|
||||||
|
<TabItem Header="{loc:Tr settings.sidebar.categoryGeneral}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
@@ -101,6 +150,14 @@
|
|||||||
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
|
PlaceholderText="{loc:Tr settings.general.defaultInstructionsPlaceholder}"
|
||||||
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
|
Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Ausführung: model/permission, per-model presets, parallelism, usage gate. -->
|
||||||
|
<TabItem Header="{loc:Tr settings.sidebar.categoryExecution}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<Grid ColumnDefinitions="*,12,*">
|
<Grid ColumnDefinitions="*,12,*">
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.model}"/>
|
||||||
@@ -181,42 +238,83 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Spacing="4">
|
</StackPanel>
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
</ScrollViewer>
|
||||||
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
</TabItem>
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Spacing="4">
|
<TabItem Header="{loc:Tr settings.sidebar.categoryPrime}">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.standupWeekday}"/>
|
<ScrollViewer>
|
||||||
<ComboBox SelectedIndex="{Binding General.StandupWeekday, Mode=TwoWay}" HorizontalAlignment="Left">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySunday}"/>
|
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdayMonday}"/>
|
Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdayTuesday}"/>
|
BorderThickness="1" CornerRadius="6" Padding="10,8">
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdayWednesday}"/>
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdayThursday}"/>
|
<TextBlock Classes="meta" Text="{loc:Tr settings.repoHint.text}" VerticalAlignment="Center"/>
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdayFriday}"/>
|
<Button Classes="btn" Content="{loc:Tr settings.repoHint.link}" Command="{Binding OpenRepoImportCommand}"/>
|
||||||
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySaturday}"/>
|
</StackPanel>
|
||||||
</ComboBox>
|
</Border>
|
||||||
</StackPanel>
|
<TextBlock Classes="meta" TextWrapping="Wrap"
|
||||||
<StackPanel Spacing="4">
|
Text="{loc:Tr settings.prime.description}"/>
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.sessionSkills}"/>
|
<ItemsControl ItemsSource="{Binding Prime.Rows}">
|
||||||
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
<ItemsControl.ItemTemplate>
|
||||||
Text="{loc:Tr settings.general.sessionSkillsHint}"/>
|
<DataTemplate x:DataType="settings:PrimeScheduleRowViewModel">
|
||||||
<ItemsControl ItemsSource="{Binding General.SessionSkills}">
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
||||||
<ItemsControl.ItemTemplate>
|
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
||||||
<DataTemplate x:DataType="agent:SelectableSkillViewModel">
|
Background="{DynamicResource DeepBrush}">
|
||||||
<CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" Content="{Binding Name}"
|
<StackPanel Spacing="6">
|
||||||
ToolTip.Tip="{Binding Description}"/>
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
||||||
</DataTemplate>
|
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||||
</ItemsControl.ItemTemplate>
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||||
</ItemsControl>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTu}" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayWe}" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTh}" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayFr}" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
||||||
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox Grid.Column="2"
|
||||||
|
Text="{Binding TimeText, Mode=TwoWay}"
|
||||||
|
PlaceholderText="HH:mm" MaxLength="5"
|
||||||
|
Width="68" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||||
|
MinWidth="80"/>
|
||||||
|
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
||||||
|
ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.promptOverrideLabel}"/>
|
||||||
|
<TextBox Text="{Binding PromptOverride, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{loc:Tr settings.prime.promptOverridePlaceholder}"
|
||||||
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
MinHeight="48" MaxHeight="120"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<Button Classes="btn" Content="{loc:Tr settings.prime.addSchedule}" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.dailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
||||||
|
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100" FormatString="0"
|
||||||
|
Value="{Binding Prime.DailyPrepMaxTasks, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.tabWorktrees}">
|
<TabItem Header="{loc:Tr settings.sidebar.categoryWorktrees}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
|
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||||
|
Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="10,8">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr settings.repoHint.text}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Content="{loc:Tr settings.repoHint.link}" Command="{Binding OpenRepoImportCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
<Grid ColumnDefinitions="*,12,2*">
|
<Grid ColumnDefinitions="*,12,2*">
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.worktrees.strategy}"/>
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.worktrees.strategy}"/>
|
||||||
@@ -258,7 +356,7 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.tabFiles}">
|
<TabItem Header="{loc:Tr settings.sidebar.categoryFiles}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
@@ -332,55 +430,104 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.tabPrime}">
|
<!-- Session Skills: merged install/manage list (was the "Skills" tab) plus the global
|
||||||
|
active-selection checkbox that used to be a separate list on the General tab. -->
|
||||||
|
<TabItem Header="{loc:Tr settings.sidebar.categorySkills}">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
<TextBlock Classes="meta" TextWrapping="Wrap"
|
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||||
Text="{loc:Tr settings.prime.description}"/>
|
Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||||
<ItemsControl ItemsSource="{Binding Prime.Rows}">
|
BorderThickness="1" CornerRadius="6" Padding="10,8">
|
||||||
<ItemsControl.ItemTemplate>
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<DataTemplate x:DataType="settings:PrimeScheduleRowViewModel">
|
<TextBlock Classes="meta" Text="{loc:Tr settings.repoHint.text}" VerticalAlignment="Center"/>
|
||||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
<Button Classes="btn" Content="{loc:Tr settings.repoHint.link}" Command="{Binding OpenRepoImportCommand}"/>
|
||||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
</StackPanel>
|
||||||
Background="{DynamicResource DeepBrush}">
|
</Border>
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
<StackPanel Spacing="6">
|
||||||
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installedSection}"/>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
Text="{loc:Tr settings.general.sessionSkillsHint}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTu}" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
<ctl:OperationIndicator Status="{Binding SessionSkills.UpdateOp}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayWe}" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
<ItemsControl ItemsSource="{Binding SessionSkills.Skills}">
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTh}" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
<ItemsControl.ItemTemplate>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayFr}" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
<DataTemplate x:DataType="settings:SessionSkillRowViewModel">
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
||||||
</StackPanel>
|
Background="{DynamicResource DeepBrush}">
|
||||||
<TextBox Grid.Column="2"
|
<StackPanel Spacing="4">
|
||||||
Text="{Binding TimeText, Mode=TwoWay}"
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
PlaceholderText="HH:mm" MaxLength="5"
|
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||||
Width="68" VerticalAlignment="Center"/>
|
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
<TextBlock Grid.Column="1" Classes="title" Text="{Binding Name}" VerticalAlignment="Center"/>
|
||||||
MinWidth="80"/>
|
<Button Grid.Column="2" Classes="btn" Content="{loc:Tr settings.skills.updateButton}"
|
||||||
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
Margin="0,0,6,0"
|
||||||
ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}"
|
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.UpdateCommand}"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
CommandParameter="{Binding SourceUrl}"/>
|
||||||
CommandParameter="{Binding}"/>
|
<Button Grid.Column="3" Classes="btn danger" Content="{loc:Tr settings.skills.removeButton}"
|
||||||
</Grid>
|
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.RemoveCommand}"
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.promptOverrideLabel}"/>
|
CommandParameter="{Binding SourceUrl}"/>
|
||||||
<TextBox Text="{Binding PromptOverride, Mode=TwoWay}"
|
</Grid>
|
||||||
PlaceholderText="{loc:Tr settings.prime.promptOverridePlaceholder}"
|
<TextBlock Classes="meta" Text="{Binding Description}" TextWrapping="Wrap"
|
||||||
AcceptsReturn="True" TextWrapping="Wrap"
|
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
MinHeight="48" MaxHeight="120"/>
|
<TextBlock Classes="meta" Opacity="0.6" Text="{Binding PinnedRef}"/>
|
||||||
</StackPanel>
|
<TextBlock Classes="path-mono" Text="{Binding SourceUrl}" TextTrimming="PrefixCharacterEllipsis"/>
|
||||||
</Border>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</Border>
|
||||||
</ItemsControl.ItemTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl.ItemTemplate>
|
||||||
<Button Classes="btn" Content="{loc:Tr settings.prime.addSchedule}" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
|
</ItemsControl>
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
||||||
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.dailyPrepMaxTasks}" VerticalAlignment="Center"/>
|
Text="{loc:Tr settings.skills.emptyState}"
|
||||||
<NumericUpDown Minimum="1" Maximum="50" Increment="1" Width="100" FormatString="0"
|
IsVisible="{Binding SessionSkills.IsEmpty}"/>
|
||||||
Value="{Binding Prime.DailyPrepMaxTasks, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"/>
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installSection}"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding SessionSkills.InstallUrl, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{loc:Tr settings.skills.installUrlPlaceholder}"/>
|
||||||
|
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr settings.skills.installButton}"
|
||||||
|
Command="{Binding SessionSkills.InstallCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
<ctl:OperationIndicator Status="{Binding SessionSkills.InstallOp}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Classes="meta" Text="{Binding SessionSkills.StatusMessage}"
|
||||||
|
IsVisible="{Binding SessionSkills.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Berichte: standup weekday + weekly-report excluded paths (split out of General). -->
|
||||||
|
<TabItem Header="{loc:Tr settings.sidebar.categoryReports}">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Spacing="12" Margin="0,8,0,0">
|
||||||
|
<Border IsVisible="{Binding !HasLinkedRepo}"
|
||||||
|
Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="10,8">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr settings.repoHint.text}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="btn" Content="{loc:Tr settings.repoHint.link}" Command="{Binding OpenRepoImportCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.standupWeekday}"/>
|
||||||
|
<ComboBox SelectedIndex="{Binding General.StandupWeekday, Mode=TwoWay}" HorizontalAlignment="Left">
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySunday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdayMonday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdayTuesday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdayWednesday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdayThursday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdayFriday}"/>
|
||||||
|
<ComboBoxItem Content="{loc:Tr settings.general.weekdaySaturday}"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.reportExcludedPaths}"/>
|
||||||
|
<TextBox AcceptsReturn="True" MinHeight="60" Text="{Binding General.ReportExcludedPaths, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
@@ -480,63 +627,8 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|
||||||
<TabItem Header="{loc:Tr settings.tabSkills}">
|
</TabControl>
|
||||||
<ScrollViewer>
|
</Grid>
|
||||||
<StackPanel Spacing="12" Margin="0,8,0,0">
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installSection}"/>
|
|
||||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
|
||||||
<TextBox Grid.Column="0" Text="{Binding SessionSkills.InstallUrl, Mode=TwoWay}"
|
|
||||||
PlaceholderText="{loc:Tr settings.skills.installUrlPlaceholder}"/>
|
|
||||||
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr settings.skills.installButton}"
|
|
||||||
Command="{Binding SessionSkills.InstallCommand}"/>
|
|
||||||
</Grid>
|
|
||||||
<ctl:OperationIndicator Status="{Binding SessionSkills.InstallOp}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
|
|
||||||
|
|
||||||
<StackPanel Spacing="6">
|
|
||||||
<TextBlock Classes="section-label" Text="{loc:Tr settings.skills.installedSection}"/>
|
|
||||||
<ctl:OperationIndicator Status="{Binding SessionSkills.UpdateOp}"/>
|
|
||||||
<ItemsControl ItemsSource="{Binding SessionSkills.Skills}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="services:SessionSkillDto">
|
|
||||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
|
||||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
|
||||||
Background="{DynamicResource DeepBrush}">
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Classes="title" Text="{Binding Name}"/>
|
|
||||||
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr settings.skills.updateButton}"
|
|
||||||
Margin="0,0,6,0"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.UpdateCommand}"
|
|
||||||
CommandParameter="{Binding SourceUrl}"/>
|
|
||||||
<Button Grid.Column="2" Classes="btn danger" Content="{loc:Tr settings.skills.removeButton}"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).SessionSkills.RemoveCommand}"
|
|
||||||
CommandParameter="{Binding SourceUrl}"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock Classes="meta" Text="{Binding Description}" TextWrapping="Wrap"
|
|
||||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<TextBlock Classes="meta" Opacity="0.6" Text="{Binding PinnedRef}"/>
|
|
||||||
<TextBlock Classes="path-mono" Text="{Binding SourceUrl}" TextTrimming="PrefixCharacterEllipsis"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
|
|
||||||
Text="{loc:Tr settings.skills.emptyState}"
|
|
||||||
IsVisible="{Binding SessionSkills.IsEmpty}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Classes="meta" Text="{Binding SessionSkills.StatusMessage}"
|
|
||||||
IsVisible="{Binding SessionSkills.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</TabItem>
|
|
||||||
|
|
||||||
</TabControl>
|
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
</ctl:ModalShell>
|
</ctl:ModalShell>
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ public sealed class WindowDialogService : IDialogService
|
|||||||
{
|
{
|
||||||
vm.Worktrees.ConfirmAsync = ConfirmAsync;
|
vm.Worktrees.ConfirmAsync = ConfirmAsync;
|
||||||
var dlg = new SettingsModalView { DataContext = vm };
|
var dlg = new SettingsModalView { DataContext = vm };
|
||||||
|
vm.RequestRepoImport = () =>
|
||||||
|
{
|
||||||
|
dlg.Close();
|
||||||
|
if (Shell is not null) _ = Shell.OpenRepoImportCommand.ExecuteAsync(null);
|
||||||
|
};
|
||||||
await dlg.ShowDialog(ActiveOwner());
|
await dlg.ShowDialog(ActiveOwner());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using ClaudeDo.Ui.Services;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -6,46 +5,6 @@ namespace ClaudeDo.Ui.Tests.ViewModels;
|
|||||||
|
|
||||||
public class GeneralSettingsTabViewModelTests
|
public class GeneralSettingsTabViewModelTests
|
||||||
{
|
{
|
||||||
private sealed class FakeWorker : StubWorkerClient
|
|
||||||
{
|
|
||||||
public List<SessionSkillDto> Installed = new();
|
|
||||||
public override Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(Installed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task LoadSessionSkillsAsync_reflects_current_selection()
|
|
||||||
{
|
|
||||||
var w = new FakeWorker
|
|
||||||
{
|
|
||||||
Installed = new() { new SessionSkillDto("a", "", "url-a", "ref", DateTimeOffset.UtcNow),
|
|
||||||
new SessionSkillDto("b", "", "url-b", "ref", DateTimeOffset.UtcNow) },
|
|
||||||
};
|
|
||||||
var vm = new GeneralSettingsTabViewModel();
|
|
||||||
|
|
||||||
await vm.LoadSessionSkillsAsync(w, new List<string> { "b" });
|
|
||||||
|
|
||||||
Assert.Equal(2, vm.SessionSkills.Count);
|
|
||||||
Assert.False(vm.SessionSkills.Single(s => s.Name == "a").IsSelected);
|
|
||||||
Assert.True(vm.SessionSkills.Single(s => s.Name == "b").IsSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SelectedSessionSkillNames_round_trips_through_toggle()
|
|
||||||
{
|
|
||||||
var w = new FakeWorker
|
|
||||||
{
|
|
||||||
Installed = new() { new SessionSkillDto("a", "", "url-a", "ref", DateTimeOffset.UtcNow) },
|
|
||||||
};
|
|
||||||
var vm = new GeneralSettingsTabViewModel();
|
|
||||||
await vm.LoadSessionSkillsAsync(w, null);
|
|
||||||
|
|
||||||
Assert.Null(vm.SelectedSessionSkillNames());
|
|
||||||
|
|
||||||
vm.SessionSkills.Single().IsSelected = true;
|
|
||||||
|
|
||||||
Assert.Equal(new List<string> { "a" }, vm.SelectedSessionSkillNames());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(-1, false)]
|
[InlineData(-1, false)]
|
||||||
[InlineData(0, true)]
|
[InlineData(0, true)]
|
||||||
|
|||||||
@@ -180,4 +180,53 @@ public class SessionSkillsSettingsTabViewModelTests
|
|||||||
Assert.Equal("url-a", w.UpdatedSourceUrl);
|
Assert.Equal("url-a", w.UpdatedSourceUrl);
|
||||||
Assert.NotEmpty(vm.StatusMessage);
|
Assert.NotEmpty(vm.StatusMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadAsync_marks_rows_selected_per_given_names()
|
||||||
|
{
|
||||||
|
var w = new FakeWorker
|
||||||
|
{
|
||||||
|
Installed = new()
|
||||||
|
{
|
||||||
|
new SessionSkillDto("a", "", "url-a", "ref", DateTimeOffset.UtcNow),
|
||||||
|
new SessionSkillDto("b", "", "url-b", "ref", DateTimeOffset.UtcNow),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var vm = new SessionSkillsSettingsTabViewModel(w);
|
||||||
|
|
||||||
|
await vm.LoadAsync(new List<string> { "b" });
|
||||||
|
|
||||||
|
Assert.False(vm.Skills.Single(s => s.Name == "a").IsSelected);
|
||||||
|
Assert.True(vm.Skills.Single(s => s.Name == "b").IsSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SelectedSkillNames_round_trips_through_toggle()
|
||||||
|
{
|
||||||
|
var w = new FakeWorker { Installed = new() { new SessionSkillDto("a", "", "url-a", "ref", DateTimeOffset.UtcNow) } };
|
||||||
|
var vm = new SessionSkillsSettingsTabViewModel(w);
|
||||||
|
await vm.LoadAsync(null);
|
||||||
|
|
||||||
|
Assert.Null(vm.SelectedSkillNames());
|
||||||
|
|
||||||
|
vm.Skills.Single().IsSelected = true;
|
||||||
|
|
||||||
|
Assert.Equal(new List<string> { "a" }, vm.SelectedSkillNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RefreshAfterInstall_preserves_existing_selection()
|
||||||
|
{
|
||||||
|
// Install/Update/Remove call LoadAsync() with no explicit selection — that must not
|
||||||
|
// reset the checkbox state of the skills that were already selected.
|
||||||
|
var w = new FakeWorker { Installed = new() { new SessionSkillDto("a", "", "url-a", "ref", DateTimeOffset.UtcNow) } };
|
||||||
|
var vm = new SessionSkillsSettingsTabViewModel(w) { InstallUrl = "https://example.com/repo.git" };
|
||||||
|
await vm.LoadAsync(new List<string> { "a" });
|
||||||
|
Assert.True(vm.Skills.Single(s => s.Name == "a").IsSelected);
|
||||||
|
|
||||||
|
await vm.InstallCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.True(vm.Skills.Single(s => s.Name == "a").IsSelected);
|
||||||
|
Assert.False(vm.Skills.Single(s => s.Name == "new-skill").IsSelected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,56 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Localization;
|
using ClaudeDo.Localization;
|
||||||
using ClaudeDo.Ui;
|
using ClaudeDo.Ui;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Modals;
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||||
|
|
||||||
public class SettingsModalViewModelTests
|
public class SettingsModalViewModelTests : IDisposable
|
||||||
{
|
{
|
||||||
|
private readonly string _dbPath =
|
||||||
|
Path.Combine(Path.GetTempPath(), $"claudedo_settingsmodal_test_{Guid.NewGuid():N}.db");
|
||||||
|
|
||||||
|
public SettingsModalViewModelTests()
|
||||||
|
{
|
||||||
|
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 sealed class FakeWorker : StubWorkerClient
|
private sealed class FakeWorker : StubWorkerClient
|
||||||
{
|
{
|
||||||
public AppSettingsDto? AppToReturn;
|
public AppSettingsDto? AppToReturn;
|
||||||
public AppSettingsDto? Saved;
|
public AppSettingsDto? Saved;
|
||||||
|
public List<SessionSkillDto> InstalledSkills = new();
|
||||||
public override Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult(AppToReturn);
|
public override Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult(AppToReturn);
|
||||||
public override Task UpdateAppSettingsAsync(AppSettingsDto dto) { Saved = dto; return Task.CompletedTask; }
|
public override Task UpdateAppSettingsAsync(AppSettingsDto dto) { Saved = dto; return Task.CompletedTask; }
|
||||||
|
public override Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(InstalledSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class FakePrimeApi : IPrimeScheduleApi
|
private sealed class FakePrimeApi : IPrimeScheduleApi
|
||||||
@@ -70,9 +106,9 @@ public class SettingsModalViewModelTests
|
|||||||
Assert.Equal(88, worker.Saved.UsageThrottleSevenDayHardPct);
|
Assert.Equal(88, worker.Saved.UsageThrottleSevenDayHardPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SettingsModalViewModel MakeVm(FakeWorker worker) =>
|
private SettingsModalViewModel MakeVm(FakeWorker worker) =>
|
||||||
new(worker, new PrimeClaudeTabViewModel(new FakePrimeApi()), new OnlineLoginService(),
|
new(worker, new PrimeClaudeTabViewModel(new FakePrimeApi()), new OnlineLoginService(),
|
||||||
MakeLocalizer(), new AppSettings());
|
MakeLocalizer(), new AppSettings(), new TestDbFactory(NewContext));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoadAsync_fills_usage_gate_fields_from_dto()
|
public async Task LoadAsync_fills_usage_gate_fields_from_dto()
|
||||||
@@ -100,4 +136,95 @@ public class SettingsModalViewModelTests
|
|||||||
Assert.Equal(42, worker.Saved!.UsageGateFiveHourPct);
|
Assert.Equal(42, worker.Saved!.UsageGateFiveHourPct);
|
||||||
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HasLinkedRepo_false_when_no_list_has_a_working_dir()
|
||||||
|
{
|
||||||
|
using (var ctx = NewContext())
|
||||||
|
{
|
||||||
|
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "No repo", CreatedAt = DateTime.UtcNow, WorkingDir = null });
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
var vm = MakeVm(new FakeWorker());
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.False(vm.HasLinkedRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HasLinkedRepo_true_when_a_list_has_a_working_dir()
|
||||||
|
{
|
||||||
|
using (var ctx = NewContext())
|
||||||
|
{
|
||||||
|
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "Repo", CreatedAt = DateTime.UtcNow, WorkingDir = @"C:\repo" });
|
||||||
|
ctx.SaveChanges();
|
||||||
|
}
|
||||||
|
var vm = MakeVm(new FakeWorker());
|
||||||
|
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.True(vm.HasLinkedRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedCategory_updates_SelectedIndex_and_ignores_null()
|
||||||
|
{
|
||||||
|
var vm = MakeVm(new FakeWorker());
|
||||||
|
var worktrees = vm.Categories.Single(c => c.Key == "Worktrees");
|
||||||
|
|
||||||
|
vm.SelectedCategory = worktrees;
|
||||||
|
Assert.Equal(vm.Categories.ToList().IndexOf(worktrees), vm.SelectedIndex);
|
||||||
|
|
||||||
|
// Simulated deselection push from the sibling sidebar ListBox — must not clobber the pick.
|
||||||
|
vm.SelectedCategory = null;
|
||||||
|
Assert.Equal(worktrees, vm.SelectedCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Categories_default_to_first_entry_and_mark_the_repo_gated_ones()
|
||||||
|
{
|
||||||
|
var vm = MakeVm(new FakeWorker());
|
||||||
|
|
||||||
|
Assert.Equal(vm.Categories[0], vm.SelectedCategory);
|
||||||
|
Assert.True(vm.Categories.Single(c => c.Key == "Worktrees").RequiresRepo);
|
||||||
|
Assert.True(vm.Categories.Single(c => c.Key == "Prime").RequiresRepo);
|
||||||
|
Assert.True(vm.Categories.Single(c => c.Key == "SessionSkills").RequiresRepo);
|
||||||
|
Assert.True(vm.Categories.Single(c => c.Key == "Reports").RequiresRepo);
|
||||||
|
Assert.False(vm.Categories.Single(c => c.Key == "General").RequiresRepo);
|
||||||
|
Assert.False(vm.Categories.Single(c => c.Key == "Files").RequiresRepo);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SessionSkillSelection_survives_a_save_and_reload_round_trip()
|
||||||
|
{
|
||||||
|
// The only real regression risk of the Settings-sidebar rework: Session Skills used to be
|
||||||
|
// two separate lists (a General-tab checkbox list + the Skills-tab management list) and is
|
||||||
|
// now one merged list. Setting the selection, saving, and reloading into a *fresh* VM must
|
||||||
|
// come back with the identical selection.
|
||||||
|
var installed = new List<SessionSkillDto>
|
||||||
|
{
|
||||||
|
new("alpha", "", "url-alpha", "ref", DateTimeOffset.UtcNow),
|
||||||
|
new("beta", "", "url-beta", "ref", DateTimeOffset.UtcNow),
|
||||||
|
};
|
||||||
|
var worker = new FakeWorker { AppToReturn = DtoWith(80, 90), InstalledSkills = installed };
|
||||||
|
var vm = MakeVm(worker);
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
vm.SessionSkills.Skills.Single(s => s.Name == "alpha").IsSelected = true;
|
||||||
|
vm.SessionSkills.Skills.Single(s => s.Name == "beta").IsSelected = false;
|
||||||
|
|
||||||
|
await vm.SaveCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Equal(new List<string> { "alpha" }, worker.Saved!.SessionSkills);
|
||||||
|
|
||||||
|
// Reload into a brand-new VM instance, as if the modal were closed and reopened.
|
||||||
|
var worker2 = new FakeWorker { AppToReturn = worker.Saved, InstalledSkills = installed };
|
||||||
|
var vm2 = MakeVm(worker2);
|
||||||
|
await vm2.LoadAsync();
|
||||||
|
|
||||||
|
Assert.True(vm2.SessionSkills.Skills.Single(s => s.Name == "alpha").IsSelected);
|
||||||
|
Assert.False(vm2.SessionSkills.Skills.Single(s => s.Name == "beta").IsSelected);
|
||||||
|
Assert.Equal(new List<string> { "alpha" }, vm2.SessionSkills.SelectedSkillNames());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user