feat(wizard): offer the default token when adding or editing an account #111

Merged
KofTwentyTwo merged 1 commits from feat/default-token-wizard-2 into main 2026-10-10 23:24:45 +00:00
9 changed files with 443 additions and 16 deletions

No files matched your search

@@ -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<string?>();
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<IReadOnlyList<string>>(["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<IReadOnlyList<string>>(["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()
{
@@ -132,6 +132,69 @@ public sealed class SyncAllCoordinatorTests : IDisposable
// ---------------------------------------------------------------- default token (#102)
/// <summary>A default-token account workspace; the default token is seeded only when asked.</summary>
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<SyncAllAccountState>();
_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]
@@ -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);
}
+108 -11
View File
@@ -36,13 +36,22 @@ public sealed partial class AccountWizardViewModel : ObservableObject
/// </summary>
private readonly Func<string?>? _storedToken;
/// <summary>
/// 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 <see cref="Token"/> (#102).
/// </summary>
private readonly Func<string?>? _defaultToken;
/// <summary>
/// A wizard for a new account seeded from <paramref name="defaults"/>, or — when
/// <paramref name="existing"/> is given — an edit wizard seeded from that account.
/// <paramref name="storedToken"/> 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);
/// <paramref name="defaultToken"/> fetches the default token from Settings the
/// same way and, when it returns one, offers "Use the default token" on step 2.
/// </summary>
public AccountWizardViewModel(
AccountsStore store,
@@ -50,8 +59,9 @@ public sealed partial class AccountWizardViewModel : ObservableObject
AppSettings defaults,
Account? existing = null,
Func<string?>? storedToken = null,
IActivityLog? log = null)
: this(store, orgLister, existing, storedToken, log)
IActivityLog? log = null,
Func<string?>? 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.
/// </summary>
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<string?>? 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<string?>? storedToken, IActivityLog? log)
AccountsStore store, IOrganizationLister orgLister, Account? existing, Func<string?>? storedToken,
Func<string?>? 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; }
/// <summary>
/// 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.
/// </summary>
public bool HasDefaultToken { get; }
/// <summary>
/// Step 2's choice: true means the account resolves to the default token from
/// Settings (<see cref="TokenSource.Default"/>) 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.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsOwnTokenSelected))]
[NotifyPropertyChangedFor(nameof(TokenChoiceIndex))]
[NotifyPropertyChangedFor(nameof(DefaultTokenCaption))]
public partial bool UseDefaultToken { get; set; }
/// <summary>The token box is enabled only for a token of the account's own.</summary>
public bool IsOwnTokenSelected => !UseDefaultToken;
/// <summary>
/// <see cref="UseDefaultToken"/> as the selected index of a two-item radio group:
/// 0 = the default token, 1 = a token of this account's own.
/// </summary>
public int TokenChoiceIndex
{
get => UseDefaultToken ? 0 : 1;
set => UseDefaultToken = value == 0;
}
/// <summary>
/// What choosing the default token means for this account, with how many other
/// accounts share it; shown under the radio button.
/// </summary>
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;
}
}
/// <summary>Step 1's validation message; empty when the name is acceptable.</summary>
[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
/// </summary>
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,
};
+10
View File
@@ -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)
+7
View File
@@ -157,6 +157,13 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable
/// <summary>Name shown for this workspace in navigation: the account's name, or "Quick Sync".</summary>
public string DisplayName => _account?.Name ?? "Quick Sync";
/// <summary>
/// True for an account workspace whose token is the default token from Settings
/// (<see cref="TokenSource.Default"/>): the pane tags it, and Sync All names
/// Settings when that token is missing.
/// </summary>
public bool UsesDefaultToken => _account?.UsesDefaultToken == true;
public ObservableCollection<RepoItemViewModel> Repos { get; } = new();
+19
View File
@@ -57,12 +57,31 @@
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Step 2 of 4: Access token" />
<!-- Offered only when a default token is saved in Settings (#102). The
account then references that token instead of holding a copy, so
replacing it in Settings updates every such account. -->
<RadioButtons
x:Name="TokenChoice"
AutomationProperties.AutomationId="WizardTokenChoice"
Header="Which token should this account use?"
SelectedIndex="{x:Bind ViewModel.TokenChoiceIndex, Mode=TwoWay}"
Visibility="{x:Bind local:StatusFormat.VisibleIf(ViewModel.HasDefaultToken), Mode=OneWay}">
<RadioButton AutomationProperties.AutomationId="WizardUseDefaultToken" Content="Use the default token from Settings" />
<RadioButton AutomationProperties.AutomationId="WizardUseOwnToken" Content="Use a different token for this account" />
</RadioButtons>
<TextBlock
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="{x:Bind ViewModel.DefaultTokenCaption, Mode=OneWay}"
Visibility="{x:Bind local:StatusFormat.VisibleIf(ViewModel.UseDefaultToken), Mode=OneWay}" />
<PasswordBox
x:Name="TokenBox"
AutomationProperties.AutomationId="WizardTokenBox"
Header="Personal access token"
ToolTipService.ToolTip="Stored in Windows Credential Manager, never in a file; needs repository read access"
PlaceholderText="ghp_…"
IsEnabled="{x:Bind ViewModel.IsOwnTokenSelected, Mode=OneWay}"
PasswordChanged="TokenBox_PasswordChanged" />
<HyperlinkButton
Padding="0"
+1 -1
View File
@@ -57,7 +57,7 @@ public sealed partial class AccountWizardDialog : ContentDialog
// by hand. The stored token of an existing account is deliberately NOT
// loaded into the box: an empty box means "keep it", and the secret never
// sits in an editable control with a reveal button (#32).
TokenBox.PlaceholderText = ViewModel.IsEditing
TokenBox.PlaceholderText = ViewModel.IsEditing && !ViewModel.UseDefaultToken
? "Leave empty to keep the stored token"
: "ghp_…";
+34 -4
View File
@@ -615,11 +615,12 @@ public sealed partial class MainWindow : Window
{
var item = new NavigationViewItem
{
Content = account.Name,
Content = AccountItemContent(account),
Tag = account.Id,
Icon = new FontIcon { Glyph = "" },
ContextFlyout = CreateAccountContextFlyout(account.Id),
};
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(item, account.UsesDefaultToken ? $"{account.Name}, uses the default token" : account.Name);
ToolTipService.SetToolTip(item, account.LastSyncSummary ?? "Never synced");
_navItems[account.Id] = item;
return item;
@@ -627,6 +628,31 @@ public sealed partial class MainWindow : Window
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>The Edit/Delete context menu for an account's pane item.</summary>
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<string?>? 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
/// </summary>
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");
}