From be0a9a60c00b9914110af876c987f05d054fd491 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 10 Oct 2026 17:11:10 -0500 Subject: [PATCH] feat(wizard): offer the default token when adding or editing an account Step 2 of the account wizard becomes a choice when a default token is saved in Settings: "Use the default token from Settings" (preselected for a new account, or when the Quick Sync connection being saved runs on it) or "Use a different token for this account". The default token is fetched only to validate and is never loaded into the box, which is disabled while the default is selected. Editing switches either way: choosing the default says the account's own token will be removed on save, choosing an own token on a default-token account asks for one, and a default token removed while the wizard is open gives a Settings-specific message instead of a generic failure. The pane tags default-token accounts ("Default token", also in the item's automation name), and Sync All skips such an account with no default token saved, naming Settings, before it would hit a generic auth error. Closes #105 Part of #102 Co-Authored-By: Claude Fable 5.1 Signed-off-by: James Maes --- .../AccountWizardViewModelTests.cs | 200 ++++++++++++++++++ gclo.Engine.Tests/SyncAllCoordinatorTests.cs | 63 ++++++ gclo.Engine.Tests/WorkspaceViewModelTests.cs | 1 + gclo.ViewModels/AccountWizardViewModel.cs | 119 ++++++++++- gclo.ViewModels/SyncAllCoordinator.cs | 10 + gclo.ViewModels/WorkspaceViewModel.cs | 7 + gclo/AccountWizardDialog.xaml | 19 ++ gclo/AccountWizardDialog.xaml.cs | 2 +- gclo/MainWindow.xaml.cs | 38 +++- 9 files changed, 443 insertions(+), 16 deletions(-) diff --git a/gclo.Engine.Tests/AccountWizardViewModelTests.cs b/gclo.Engine.Tests/AccountWizardViewModelTests.cs index 9ee962d..4071dbd 100644 --- a/gclo.Engine.Tests/AccountWizardViewModelTests.cs +++ b/gclo.Engine.Tests/AccountWizardViewModelTests.cs @@ -390,6 +390,206 @@ public sealed class AccountWizardViewModelTests : IDisposable + // ---------------------------------------------------------------- default token (#102) + + private AccountWizardViewModel NewWizardWithDefault(string? defaultToken, Account? existing = null, string? existingToken = null) + => new(_store, _orgs, _defaults, existing, () => existingToken, defaultToken: () => defaultToken); + + + + [Fact] + public void NewWizard_WithoutADefaultToken_OffersNoChoice_AndUsesAnOwnToken() + { + AccountWizardViewModel wizard = NewWizardWithDefault(defaultToken: null); + + Assert.False(wizard.HasDefaultToken); + Assert.False(wizard.UseDefaultToken); + Assert.True(wizard.IsOwnTokenSelected); + Assert.Equal(1, wizard.TokenChoiceIndex); + } + + + + [Fact] + public void NewWizard_WithADefaultToken_PreselectsIt() + { + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default"); + + Assert.True(wizard.HasDefaultToken); + Assert.True(wizard.UseDefaultToken); + Assert.False(wizard.IsOwnTokenSelected); + Assert.Equal(0, wizard.TokenChoiceIndex); + Assert.Equal("", wizard.Token); // the default token is never loaded into the box + Assert.StartsWith("Used by Quick Sync.", wizard.DefaultTokenCaption, StringComparison.Ordinal); + } + + + + [Fact] + public void TokenChoiceIndex_RoundTripsToUseDefaultToken_AndNotifies() + { + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default"); + var changed = new List(); + wizard.PropertyChanged += (_, e) => changed.Add(e.PropertyName); + + wizard.TokenChoiceIndex = 1; + + Assert.False(wizard.UseDefaultToken); + Assert.Contains(changed, n => string.Equals(n, nameof(AccountWizardViewModel.IsOwnTokenSelected), StringComparison.Ordinal)); + Assert.Contains(changed, n => string.Equals(n, nameof(AccountWizardViewModel.TokenChoiceIndex), StringComparison.Ordinal)); + Assert.Contains(changed, n => string.Equals(n, nameof(AccountWizardViewModel.DefaultTokenCaption), StringComparison.Ordinal)); + + wizard.TokenChoiceIndex = 0; + Assert.True(wizard.UseDefaultToken); + } + + + + [Fact] + public async Task NewWizard_UsingTheDefaultToken_ValidatesWithIt_AndSavesWithoutAnOwnEntry() + { + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default"); + wizard.Name = "Shared"; + string? validated = null; + _orgs.Handler = (token, _) => + { + validated = token; + return Task.FromResult>(["acme"]); + }; + + await AdvanceToStepAsync(wizard, 3); + Assert.Equal("ghp_default", validated); + Assert.Equal("", wizard.TokenError); + wizard.Organization = "acme"; + wizard.TargetRoot = @"C:\repos"; + await AdvanceToStepAsync(wizard, 4); + await wizard.SaveAsync(); + + Account saved = Assert.Single(_store.GetAll()); + Assert.True(saved.UsesDefaultToken); + Assert.Null(_vault.TryRetrieve(saved.Id)); + Assert.Equal(1, _store.CountUsingDefaultToken()); + } + + + + [Fact] + public async Task NewWizard_UsingTheDefaultToken_WhenItWasRemovedMeanwhile_StaysOnStepTwo() + { + string? liveDefault = "ghp_default"; + var wizard = new AccountWizardViewModel(_store, _orgs, _defaults, defaultToken: () => liveDefault); + wizard.Name = "Shared"; + await AdvanceToStepAsync(wizard, 2); + liveDefault = null; // removed in Settings while the wizard is open + + Assert.False(await wizard.TryAdvanceAsync()); + + Assert.Equal(2, wizard.Step); + Assert.Contains("default token is no longer saved", wizard.TokenError); + Assert.Contains("Settings", wizard.TokenError); + } + + + + [Fact] + public async Task NewWizard_ChoosingAnOwnToken_IgnoresTheDefault_AndStoresTheTypedOne() + { + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default"); + wizard.Name = "Mine"; + wizard.UseDefaultToken = false; + wizard.Token = "ghp_mine"; + string? validated = null; + _orgs.Handler = (token, _) => + { + validated = token; + return Task.FromResult>(["acme"]); + }; + + await AdvanceToStepAsync(wizard, 3); + wizard.Organization = "acme"; + wizard.TargetRoot = @"C:\repos"; + await AdvanceToStepAsync(wizard, 4); + await wizard.SaveAsync(); + + Assert.Equal("ghp_mine", validated); + Account saved = Assert.Single(_store.GetAll()); + Assert.False(saved.UsesDefaultToken); + Assert.Equal("ghp_mine", _vault.TryRetrieve(saved.Id)); + } + + + + [Fact] + public async Task EditWizard_DefaultTokenAccount_StartsOnTheDefault_AndCanSwitchToAnOwnToken() + { + Account existing = MakeAccount("Shared") with { TokenSource = TokenSource.Default }; + _store.Save(existing, null); + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default", existing); + Assert.True(wizard.UseDefaultToken); + Assert.StartsWith("Used by Quick Sync.", wizard.DefaultTokenCaption, StringComparison.Ordinal); // itself excluded + + wizard.UseDefaultToken = false; + await AdvanceToStepAsync(wizard, 2); + Assert.False(await wizard.TryAdvanceAsync()); // no own token typed yet + Assert.Contains("instead of the default one", wizard.TokenError); + + wizard.Token = "ghp_mine"; + await AdvanceToStepAsync(wizard, 4); + await wizard.SaveAsync(); + + Account saved = Assert.Single(_store.GetAll()); + Assert.False(saved.UsesDefaultToken); + Assert.Equal("ghp_mine", _vault.TryRetrieve(saved.Id)); + } + + + + [Fact] + public async Task EditWizard_OwnTokenAccount_SwitchingToTheDefault_RemovesItsOwnEntry_AndSaysSo() + { + Account existing = MakeAccount("Work"); + _store.Save(existing, "ghp_own"); + _store.Save(MakeAccount("Other") with { TokenSource = TokenSource.Default }, null); + AccountWizardViewModel wizard = NewWizardWithDefault("ghp_default", existing, "ghp_own"); + Assert.False(wizard.UseDefaultToken); + + wizard.UseDefaultToken = true; + + Assert.Contains("1 other account", wizard.DefaultTokenCaption); + Assert.Contains("own token will be removed", wizard.DefaultTokenCaption); + await AdvanceToStepAsync(wizard, 4); + await wizard.SaveAsync(); + Account saved = _store.FindByName("Work")!; + Assert.True(saved.UsesDefaultToken); + Assert.Null(_vault.TryRetrieve(saved.Id)); + } + + + + [Fact] + public void DefaultTokenCaption_CountsOtherAccounts() + { + _store.Save(MakeAccount("A") with { TokenSource = TokenSource.Default }, null); + _store.Save(MakeAccount("B") with { TokenSource = TokenSource.Default }, null); + + Assert.Contains("2 other accounts", NewWizardWithDefault("ghp_default").DefaultTokenCaption); + } + + + + [Fact] + public void SeededWizard_PreselectsTheDefault_OnlyWhenTheSeedTokenIsTheDefaultToken() + { + var onDefault = new AccountWizardSeed("ghp_default", "acme", @"C:\src", CreateOrgSubfolder: false, MaxConcurrency: 8); + var other = new AccountWizardSeed("ghp_other", "acme", @"C:\src", CreateOrgSubfolder: false, MaxConcurrency: 8); + + Assert.True(new AccountWizardViewModel(_store, _orgs, onDefault, defaultToken: () => "ghp_default").UseDefaultToken); + Assert.False(new AccountWizardViewModel(_store, _orgs, other, defaultToken: () => "ghp_default").UseDefaultToken); + Assert.False(new AccountWizardViewModel(_store, _orgs, onDefault).UseDefaultToken); + } + + + [Fact] public void SeededWizard_NullSeed_Throws() { diff --git a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs index a8903a5..57c6dd3 100644 --- a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs +++ b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs @@ -132,6 +132,69 @@ public sealed class SyncAllCoordinatorTests : IDisposable + // ---------------------------------------------------------------- default token (#102) + + /// A default-token account workspace; the default token is seeded only when asked. + private WorkspaceViewModel CreateDefaultTokenWorkspace(string name, bool withDefaultToken) + { + var account = new Account + { + Id = Guid.NewGuid(), + Name = name, + Organization = "org-" + name, + TargetRoot = Path.Combine(_root, name), + TokenSource = TokenSource.Default, + }; + if(withDefaultToken) + { + _vault.Store(AppSettings.DefaultTokenVaultId, "tok-default"); + } + var workspace = new WorkspaceViewModel( + _lister, + new FakeGitClient(), + _orgs, + handler => new SyncProgress(handler), + TimeSpan.FromMilliseconds(1), + new NullActivityLog(), + account, + _vault); + _workspaces.Add(workspace); + return workspace; + } + + + + [Fact] + public async Task RunAsync_DefaultTokenAccount_WithoutADefaultToken_IsSkipped_NotRun() + { + WorkspaceViewModel shared = CreateDefaultTokenWorkspace("shared", withDefaultToken: false); + Assert.True(shared.UsesDefaultToken); + Assert.Equal("", shared.Token); + var states = new List(); + _coordinator.AccountStateChanged = (_, state) => states.Add(state); + + SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None); + + Assert.Equal(0, result.Ran); + Assert.Equal(1, result.Skipped); + Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Skipped], states); + Assert.Empty(_lister.Organizations); // never asked GitHub: skipped before loading + } + + + + [Fact] + public async Task RunAsync_DefaultTokenAccount_WithADefaultToken_Runs() + { + WorkspaceViewModel shared = CreateDefaultTokenWorkspace("shared", withDefaultToken: true); + Assert.Equal("tok-default", shared.Token); + + SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None); + + Assert.Equal(1, result.Ran); + Assert.Equal(0, result.Skipped); + } + // ---------------------------------------------------------------- ordering [Fact] diff --git a/gclo.Engine.Tests/WorkspaceViewModelTests.cs b/gclo.Engine.Tests/WorkspaceViewModelTests.cs index 297cf11..9c6852d 100644 --- a/gclo.Engine.Tests/WorkspaceViewModelTests.cs +++ b/gclo.Engine.Tests/WorkspaceViewModelTests.cs @@ -1816,6 +1816,7 @@ public sealed class WorkspaceViewModelTests : IDisposable WorkspaceViewModel vm = CreateViewModel(account: account, vault: vault); Assert.Equal("default-token-1234567890", vm.Token); + Assert.True(vm.UsesDefaultToken); await WaitUntilAsync(() => lookedUpToken is not null, "org lookup to fire"); Assert.Equal("default-token-1234567890", lookedUpToken); } diff --git a/gclo.ViewModels/AccountWizardViewModel.cs b/gclo.ViewModels/AccountWizardViewModel.cs index e214647..69d71b0 100644 --- a/gclo.ViewModels/AccountWizardViewModel.cs +++ b/gclo.ViewModels/AccountWizardViewModel.cs @@ -36,13 +36,22 @@ public sealed partial class AccountWizardViewModel : ObservableObject /// private readonly Func? _storedToken; + /// + /// Fetches the default token from Settings on demand (null, or returning null, + /// when none is saved). Read when step 2 validates or the account is saved with + /// "Use the default token"; never copied into (#102). + /// + private readonly Func? _defaultToken; + /// /// A wizard for a new account seeded from , or — when /// is given — an edit wizard seeded from that account. /// fetches the vault's current token on demand for - /// an edit (null, or returning null, when the vault has no entry). + /// an edit (null, or returning null, when the vault has no entry); + /// fetches the default token from Settings the + /// same way and, when it returns one, offers "Use the default token" on step 2. /// public AccountWizardViewModel( AccountsStore store, @@ -50,8 +59,9 @@ public sealed partial class AccountWizardViewModel : ObservableObject AppSettings defaults, Account? existing = null, Func? storedToken = null, - IActivityLog? log = null) - : this(store, orgLister, existing, storedToken, log) + IActivityLog? log = null, + Func? defaultToken = null) + : this(store, orgLister, existing, storedToken, defaultToken, log) { ArgumentNullException.ThrowIfNull(defaults); @@ -85,13 +95,19 @@ public sealed partial class AccountWizardViewModel : ObservableObject /// always written to the vault on save. /// public AccountWizardViewModel( - AccountsStore store, IOrganizationLister orgLister, AccountWizardSeed seed, IActivityLog? log = null) - : this(store, orgLister, existing: null, storedToken: null, log) + AccountsStore store, IOrganizationLister orgLister, AccountWizardSeed seed, IActivityLog? log = null, + Func? defaultToken = null) + : this(store, orgLister, existing: null, storedToken: null, defaultToken, log) { ArgumentNullException.ThrowIfNull(seed); Name = ""; Description = ""; Token = seed.Token; + // A Quick Sync connection that runs on the default token becomes a + // default-token account; any other token is the account's own. Compared in + // memory only; neither value is logged. + UseDefaultToken = HasDefaultToken + && string.Equals(seed.Token.Trim(), _defaultToken?.Invoke()?.Trim(), StringComparison.Ordinal); Organization = seed.Organization; TargetRoot = seed.TargetRoot; CreateOrgSubfolder = seed.CreateOrgSubfolder; @@ -101,7 +117,8 @@ public sealed partial class AccountWizardViewModel : ObservableObject private AccountWizardViewModel( - AccountsStore store, IOrganizationLister orgLister, Account? existing, Func? storedToken, IActivityLog? log) + AccountsStore store, IOrganizationLister orgLister, Account? existing, Func? storedToken, + Func? defaultToken, IActivityLog? log) { ArgumentNullException.ThrowIfNull(store); ArgumentNullException.ThrowIfNull(orgLister); @@ -110,6 +127,11 @@ public sealed partial class AccountWizardViewModel : ObservableObject _log = log ?? new NullActivityLog(); _existing = existing; _storedToken = existing is null ? null : storedToken; + _defaultToken = defaultToken; + // Decided once: the choice is offered only when a default token exists at + // open time, and the value itself is fetched again when a step needs it. + HasDefaultToken = !string.IsNullOrWhiteSpace(defaultToken?.Invoke()); + UseDefaultToken = existing is null ? HasDefaultToken : existing.UsesDefaultToken; Step = 1; NameError = ""; @@ -167,6 +189,59 @@ public sealed partial class AccountWizardViewModel : ObservableObject [ObservableProperty] public partial bool IsValidatingToken { get; set; } + /// + /// True when a default token was saved in Settings when the wizard opened, which + /// is when step 2 offers the choice between it and a token of the account's own. + /// + public bool HasDefaultToken { get; } + + /// + /// Step 2's choice: true means the account resolves to the default token from + /// Settings () and the token box is disabled; + /// false means a token of its own. Preselected for a new account when a default + /// token exists, and from the account's current source for an edit. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsOwnTokenSelected))] + [NotifyPropertyChangedFor(nameof(TokenChoiceIndex))] + [NotifyPropertyChangedFor(nameof(DefaultTokenCaption))] + public partial bool UseDefaultToken { get; set; } + + /// The token box is enabled only for a token of the account's own. + public bool IsOwnTokenSelected => !UseDefaultToken; + + /// + /// as the selected index of a two-item radio group: + /// 0 = the default token, 1 = a token of this account's own. + /// + public int TokenChoiceIndex + { + get => UseDefaultToken ? 0 : 1; + set => UseDefaultToken = value == 0; + } + + /// + /// What choosing the default token means for this account, with how many other + /// accounts share it; shown under the radio button. + /// + public string DefaultTokenCaption + { + get + { + int others = _store.CountUsingDefaultToken() - (_existing?.UsesDefaultToken == true ? 1 : 0); + string shared = others switch + { + 0 => "Used by Quick Sync.", + 1 => "Used by Quick Sync and 1 other account.", + _ => $"Used by Quick Sync and {others} other accounts.", + }; + string switching = IsEditing && _existing?.UsesDefaultToken == false && UseDefaultToken + ? " This account's own token will be removed from Windows Credential Manager on save." + : ""; + return shared + " Replacing it in Settings updates this account too." + switching; + } + } + /// Step 1's validation message; empty when the name is acceptable. [ObservableProperty] public partial string NameError { get; set; } @@ -254,16 +329,34 @@ public sealed partial class AccountWizardViewModel : ObservableObject // token and returns the organizations the dropdown offers otherwise. // An edit with the box left empty validates the STORED token, fetched // here and used for this one call only. - string candidate = Token.Trim(); - if(candidate.Length == 0 && IsEditing) + string candidate; + if(UseDefaultToken) { - candidate = _storedToken?.Invoke()?.Trim() ?? ""; + // The default token is fetched for this one call only; the box + // stays untouched (and disabled) so it is never shown or retained. + candidate = _defaultToken?.Invoke()?.Trim() ?? ""; if(candidate.Length == 0) { - TokenError = "This account has no stored token. Enter one to continue."; + TokenError = "The default token is no longer saved. Save one in Settings, " + + "or use a different token for this account."; return false; } } + else + { + candidate = Token.Trim(); + if(candidate.Length == 0 && IsEditing) + { + candidate = _storedToken?.Invoke()?.Trim() ?? ""; + if(candidate.Length == 0) + { + TokenError = _existing?.UsesDefaultToken == true + ? "Enter the token this account should use instead of the default one." + : "This account has no stored token. Enter one to continue."; + return false; + } + } + } IsValidatingToken = true; _log.Info("Account wizard: validating the token."); @@ -333,7 +426,10 @@ public sealed partial class AccountWizardViewModel : ObservableObject /// public Task SaveAsync() { - _store.Save(BuildAccount(), TokenChanged ? Token.Trim() : null); + // A default-token account stores no token of its own (the store removes any + // leftover entry when an account switches); otherwise new accounts always + // persist their token and edits only when one was typed. + _store.Save(BuildAccount(), !UseDefaultToken && TokenChanged ? Token.Trim() : null); return Task.CompletedTask; } @@ -353,6 +449,7 @@ public sealed partial class AccountWizardViewModel : ObservableObject TargetRoot = TargetRoot.Trim(), CreateOrgSubfolder = CreateOrgSubfolder, MaxConcurrency = Math.Clamp(MaxConcurrency, AppSettings.MinConcurrency, AppSettings.MaxConcurrency), + TokenSource = UseDefaultToken ? TokenSource.Default : TokenSource.Own, LastSyncUtc = _existing?.LastSyncUtc, LastSyncSummary = _existing?.LastSyncSummary, }; diff --git a/gclo.ViewModels/SyncAllCoordinator.cs b/gclo.ViewModels/SyncAllCoordinator.cs index 6c47481..6808152 100644 --- a/gclo.ViewModels/SyncAllCoordinator.cs +++ b/gclo.ViewModels/SyncAllCoordinator.cs @@ -78,6 +78,16 @@ public sealed class SyncAllCoordinator(IActivityLog log) continue; } + if(workspace.UsesDefaultToken && string.IsNullOrEmpty(workspace.Token)) + { + // The account resolves to the default token and none is saved: a + // generic auth failure would send the user to the wrong place. + skipped++; + Announce(workspace, SyncAllAccountState.Skipped); + _log.Info($"Sync all: skipped '{workspace.DisplayName}' — no default token is saved (Settings)."); + continue; + } + Announce(workspace, SyncAllAccountState.Running); if(!workspace.HasLoadedRepos) diff --git a/gclo.ViewModels/WorkspaceViewModel.cs b/gclo.ViewModels/WorkspaceViewModel.cs index 091fe13..994a40a 100644 --- a/gclo.ViewModels/WorkspaceViewModel.cs +++ b/gclo.ViewModels/WorkspaceViewModel.cs @@ -157,6 +157,13 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable /// Name shown for this workspace in navigation: the account's name, or "Quick Sync". public string DisplayName => _account?.Name ?? "Quick Sync"; + /// + /// True for an account workspace whose token is the default token from Settings + /// (): the pane tags it, and Sync All names + /// Settings when that token is missing. + /// + public bool UsesDefaultToken => _account?.UsesDefaultToken == true; + public ObservableCollection Repos { get; } = new(); diff --git a/gclo/AccountWizardDialog.xaml b/gclo/AccountWizardDialog.xaml index 451bf34..b4e1015 100644 --- a/gclo/AccountWizardDialog.xaml +++ b/gclo/AccountWizardDialog.xaml @@ -57,12 +57,31 @@ Style="{ThemeResource CaptionTextBlockStyle}" Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Step 2 of 4: Access token" /> + + + + + + + /// The pane item's content: the name, plus a "Default token" tag for an account + /// that resolves to the default token from Settings, so the dependency on + /// Settings is visible where the account is used (#102). + /// + private static object AccountItemContent(Account account) + { + if(!account.UsesDefaultToken) + { + return account.Name; + } + var panel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 }; + panel.Children.Add(new TextBlock { Text = account.Name, VerticalAlignment = VerticalAlignment.Center }); + panel.Children.Add(new TextBlock + { + Text = "Default token", + VerticalAlignment = VerticalAlignment.Center, + Style = (Style)Application.Current.Resources["CaptionTextBlockStyle"], + Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"], + }); + return panel; + } + + + /// The Edit/Delete context menu for an account's pane item. private MenuFlyout CreateAccountContextFlyout(Guid id) { @@ -688,7 +714,8 @@ public sealed partial class MainWindow : Window // validation call; the wizard never displays or retains it. Func? storedToken = existing is null ? null : () => _tokenVault.TryRetrieve(existing.Id); var viewModel = new AccountWizardViewModel( - _accountsStore, new GitHubOrganizationLister(), _settings, existing, storedToken, _log); + _accountsStore, new GitHubOrganizationLister(), _settings, existing, storedToken, _log, + defaultToken: () => _tokenVault.TryRetrieve(AppSettings.DefaultTokenVaultId)); var dialog = new AccountWizardDialog( viewModel, () => WinRT.Interop.WindowNative.GetWindowHandle(this)) { @@ -737,7 +764,9 @@ public sealed partial class MainWindow : Window /// private async Task ShowSeededAccountWizardAsync(AccountWizardSeed seed) { - var viewModel = new AccountWizardViewModel(_accountsStore, new GitHubOrganizationLister(), seed, _log); + var viewModel = new AccountWizardViewModel( + _accountsStore, new GitHubOrganizationLister(), seed, _log, + defaultToken: () => _tokenVault.TryRetrieve(AppSettings.DefaultTokenVaultId)); var dialog = new AccountWizardDialog( viewModel, () => WinRT.Interop.WindowNative.GetWindowHandle(this)) { @@ -798,7 +827,8 @@ public sealed partial class MainWindow : Window if(_navItems.TryGetValue(id, out NavigationViewItem? item)) { - item.Content = account.Name; + item.Content = AccountItemContent(account); + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(item, account.UsesDefaultToken ? $"{account.Name}, uses the default token" : account.Name); ToolTipService.SetToolTip(item, account.LastSyncSummary ?? "Never synced"); } -- 2.54.0