Repo table: styled header row + per-column filtering (name/status/branch/archived)

The header now reads as a header: subtle fill, bottom divider, muted
semibold labels via a shared ColumnHeaderButtonStyle instead of four
copies of inline styling. Each sortable column gains a funnel button
opening a filter flyout — substring boxes for Name and Branch, radio
choices for Status (kept in lockstep with the toolbar chips both ways)
and Archived (all/only/not). Active filters tint their funnel accent and
carry the state in the automation name.

Filtering logic lives in WorkspaceViewModel (NameFilter/BranchFilter/
ArchivedFilter compose by AND with the status filter; selection is
untouched, so hidden rows still sync) with five new headless tests.
Verified live against a real 269-repo load: filtering to a non-match
empties the table and shows the empty-filter hint.

Name filtering also addresses the review's P1 "no repository name
search" finding at its first level (#30).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James MaesandClaude Fable 5 committed 2026-07-05 07:26:59 -05:00
1 parent fb153f35ba
commit 53a230f40a
4 files changed
+435 -100

No files matched your search

@@ -910,6 +910,75 @@ public sealed class WorkspaceViewModelTests
Assert.NotEmpty(vm.Repos); // the empty-filter hint case: rows exist, none match
}
[Fact]
public async Task NameFilter_CaseInsensitiveSubstring_ComposesWithStatusFilter()
{
var vm = await CreateLoadedViewModelAsync(
Repo("platform-api"), Repo("platform-web"), Repo("tools"));
vm.Repos.First(r => r.Name == "platform-api").Status = SyncStatus.Failed;
vm.NameFilter = "PLATFORM";
Assert.Equal(["platform-api", "platform-web"], vm.FilteredRepos.Select(r => r.Name));
vm.Filter = RepoFilter.Failed; // AND-composes with the name filter
Assert.Equal(["platform-api"], vm.FilteredRepos.Select(r => r.Name));
vm.NameFilter = "";
vm.Filter = RepoFilter.All;
Assert.Equal(3, vm.FilteredRepos.Count);
}
[Fact]
public async Task NameFilter_TrimsBeforeMatching()
{
var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo"));
vm.NameFilter = " alpha ";
Assert.Equal(["alpha"], vm.FilteredRepos.Select(r => r.Name));
}
[Fact]
public async Task BranchFilter_MatchesTheDisplayedBranchText()
{
var vm = await CreateLoadedViewModelAsync(
Repo("alpha", branch: "main"),
Repo("bravo", branch: "develop"),
Repo("empty", branch: null)); // empty repository: BranchText is ""
vm.BranchFilter = "dev";
Assert.Equal(["bravo"], vm.FilteredRepos.Select(r => r.Name));
}
[Fact]
public async Task ArchivedFilter_TriState_AllArchivedUnarchived()
{
var vm = await CreateLoadedViewModelAsync(
Repo("alpha", archived: true), Repo("bravo"));
vm.ArchivedFilter = true;
Assert.Equal(["alpha"], vm.FilteredRepos.Select(r => r.Name));
vm.ArchivedFilter = false;
Assert.Equal(["bravo"], vm.FilteredRepos.Select(r => r.Name));
vm.ArchivedFilter = null;
Assert.Equal(2, vm.FilteredRepos.Count);
}
[Fact]
public async Task ColumnFilters_NeverTouchSelection_HiddenRowsStillSync()
{
var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo"));
Assert.True(vm.Repos.All(r => r.IsSelected)); // load selects everything
vm.NameFilter = "alpha"; // bravo is hidden but stays selected
Assert.Equal(2, vm.SelectedCount);
Assert.Contains("2", vm.SyncButtonLabel);
}
[Fact]
public async Task FilteredRepos_FollowsTerminalTransitions_AcrossARun()
{
+34 -1
View File
@@ -74,6 +74,8 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable
Organization = "";
Token = "";
TargetFolder = "";
NameFilter = "";
BranchFilter = "";
MaxConcurrency = AppSettings.DefaultConcurrency;
StatusText = "";
ResultMessage = "";
@@ -196,6 +198,23 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable
[ObservableProperty]
public partial RepoFilter Filter { get; set; }
/// <summary>
/// Column filter: case-insensitive substring the repository name must contain.
/// Empty (the default) matches everything. Composes with <see cref="Filter"/>
/// and the other column filters; selection state is unaffected — hidden rows
/// stay selected and still sync.
/// </summary>
[ObservableProperty]
public partial string NameFilter { get; set; }
/// <summary>Column filter: case-insensitive substring the branch name must contain.</summary>
[ObservableProperty]
public partial string BranchFilter { get; set; }
/// <summary>Column filter: null shows all, true only archived, false only unarchived.</summary>
[ObservableProperty]
public partial bool? ArchivedFilter { get; set; }
/// <summary>Number of rows currently selected for the next sync.</summary>
[ObservableProperty]
public partial int SelectedCount { get; set; }
@@ -231,6 +250,12 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable
partial void OnFilterChanged(RepoFilter value) => RebuildFilteredRepos();
partial void OnNameFilterChanged(string value) => RebuildFilteredRepos();
partial void OnBranchFilterChanged(string value) => RebuildFilteredRepos();
partial void OnArchivedFilterChanged(bool? value) => RebuildFilteredRepos();
partial void OnSelectedCountChanged(int value) => OnPropertyChanged(nameof(SyncButtonLabel));
partial void OnIsRunningChanged(bool value) => UpdateCanEditInputs();
@@ -861,7 +886,15 @@ public sealed partial class WorkspaceViewModel : ObservableObject, IDisposable
}
}
private bool MatchesFilter(RepoItemViewModel repo) => Filter switch
private bool MatchesFilter(RepoItemViewModel repo)
=> MatchesStatusFilter(repo)
&& (NameFilter.Length == 0
|| repo.Name.Contains(NameFilter.Trim(), StringComparison.OrdinalIgnoreCase))
&& (BranchFilter.Length == 0
|| repo.BranchText.Contains(BranchFilter.Trim(), StringComparison.OrdinalIgnoreCase))
&& (ArchivedFilter is not { } archived || repo.IsArchived == archived);
private bool MatchesStatusFilter(RepoItemViewModel repo) => Filter switch
{
RepoFilter.Active => repo.Status is SyncStatus.Cloning or SyncStatus.Pulling,
RepoFilter.Failed => repo.Status == SyncStatus.Failed,
+239 -99
View File
@@ -9,6 +9,26 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<UserControl.Resources>
<!-- Sortable column header: quiet button, muted semibold label; left padding
stays 0 so header text aligns exactly with the row cells below. -->
<Style x:Key="ColumnHeaderButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0,4,4,4" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<!-- The per-column funnel that opens that column's filter flyout. -->
<Style x:Key="ColumnFilterButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="4" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Resources>
<Grid Padding="20" RowSpacing="12">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
@@ -508,105 +528,225 @@
</InfoBar>
<!-- Table header: fixed column widths mirrored by the row template below.
Sort arrows are visual-only; each button carries a composed
automation name ("Name, sorted ascending"). -->
<Grid Grid.Row="3" Margin="0,8,0,0" Padding="12,0,12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="160" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<CheckBox
Grid.Column="0"
MinWidth="0"
Padding="0"
VerticalAlignment="Center"
ToolTipService.ToolTip="Select all"
AutomationProperties.Name="Select all repositories"
IsChecked="{x:Bind ViewModel.AllSelected, Mode=TwoWay}" />
<Button
Grid.Column="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Background="Transparent"
BorderThickness="0"
Padding="0,4,0,4"
AutomationProperties.Name="{x:Bind SortAutomationName('Name', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Name">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Name" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Name', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Name', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Background="Transparent"
BorderThickness="0"
Padding="0,4,0,4"
AutomationProperties.Name="{x:Bind SortAutomationName('Status', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Status">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Status" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Status', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Status', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="3"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Background="Transparent"
BorderThickness="0"
Padding="0,4,0,4"
AutomationProperties.Name="{x:Bind SortAutomationName('Branch', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Branch">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Branch" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Branch', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Branch', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="4"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Background="Transparent"
BorderThickness="0"
Padding="0,4,0,4"
AutomationProperties.Name="{x:Bind SortAutomationName('Archived', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Archived">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Archived" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Archived', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Archived', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
</Grid>
Every column sorts (button, composed automation name like
"Name, sorted ascending") and filters (funnel flyout; the funnel
turns accent-colored while its filter is active). The header row
itself gets subtle chrome to read as a header, not a data row. -->
<Border
Grid.Row="3"
Margin="0,8,0,0"
Padding="12,2,12,2"
Background="{ThemeResource SubtleFillColorSecondaryBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
CornerRadius="4,4,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="160" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<CheckBox
Grid.Column="0"
MinWidth="0"
Padding="0"
VerticalAlignment="Center"
ToolTipService.ToolTip="Select all"
AutomationProperties.Name="Select all repositories"
IsChecked="{x:Bind ViewModel.AllSelected, Mode=TwoWay}" />
<Grid Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Style="{StaticResource ColumnHeaderButtonStyle}"
AutomationProperties.Name="{x:Bind SortAutomationName('Name', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Name">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Name" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Name', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Name', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="1"
Style="{StaticResource ColumnFilterButtonStyle}"
AutomationProperties.AutomationId="NameFilterButton"
AutomationProperties.Name="{x:Bind TextFilterAutomationName('name', ViewModel.NameFilter), Mode=OneWay}"
ToolTipService.ToolTip="Filter by name">
<FontIcon
FontSize="12"
Glyph="&#xE71C;"
Foreground="{x:Bind TextFilterIndicatorBrush(ViewModel.NameFilter), Mode=OneWay}" />
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel Width="220" Spacing="8">
<TextBox
Header="Name contains"
AutomationProperties.AutomationId="NameFilterBox"
Text="{x:Bind ViewModel.NameFilter, Mode=OneWay}"
TextChanged="NameFilterBox_TextChanged" />
<Button
HorizontalAlignment="Right"
Content="Clear"
Click="ClearNameFilter_Click" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Style="{StaticResource ColumnHeaderButtonStyle}"
AutomationProperties.Name="{x:Bind SortAutomationName('Status', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Status">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Status" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Status', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Status', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="1"
Style="{StaticResource ColumnFilterButtonStyle}"
AutomationProperties.AutomationId="StatusFilterButton"
AutomationProperties.Name="{x:Bind StatusFilterAutomationName(ViewModel.Filter), Mode=OneWay}"
ToolTipService.ToolTip="Filter by status">
<FontIcon
FontSize="12"
Glyph="&#xE71C;"
Foreground="{x:Bind StatusFilterIndicatorBrush(ViewModel.Filter), Mode=OneWay}" />
<Button.Flyout>
<Flyout Placement="Bottom">
<RadioButtons
Header="Show"
AutomationProperties.AutomationId="StatusFilterChoices"
SelectedIndex="{x:Bind StatusFilterIndex(ViewModel.Filter), Mode=OneWay}"
SelectionChanged="StatusFilterChoices_SelectionChanged">
<x:String>All</x:String>
<x:String>Active</x:String>
<x:String>Failed</x:String>
<x:String>Pending</x:String>
</RadioButtons>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Grid Grid.Column="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Style="{StaticResource ColumnHeaderButtonStyle}"
AutomationProperties.Name="{x:Bind SortAutomationName('Branch', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Branch">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Branch" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Branch', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Branch', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="1"
Style="{StaticResource ColumnFilterButtonStyle}"
AutomationProperties.AutomationId="BranchFilterButton"
AutomationProperties.Name="{x:Bind TextFilterAutomationName('branch', ViewModel.BranchFilter), Mode=OneWay}"
ToolTipService.ToolTip="Filter by branch">
<FontIcon
FontSize="12"
Glyph="&#xE71C;"
Foreground="{x:Bind TextFilterIndicatorBrush(ViewModel.BranchFilter), Mode=OneWay}" />
<Button.Flyout>
<Flyout Placement="Bottom">
<StackPanel Width="220" Spacing="8">
<TextBox
Header="Branch contains"
AutomationProperties.AutomationId="BranchFilterBox"
Text="{x:Bind ViewModel.BranchFilter, Mode=OneWay}"
TextChanged="BranchFilterBox_TextChanged" />
<Button
HorizontalAlignment="Right"
Content="Clear"
Click="ClearBranchFilter_Click" />
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<Grid Grid.Column="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
Style="{StaticResource ColumnHeaderButtonStyle}"
AutomationProperties.Name="{x:Bind SortAutomationName('Archived', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Command="{x:Bind ViewModel.SortCommand, Mode=OneTime}"
CommandParameter="Archived">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Archived" />
<FontIcon
FontSize="10"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Glyph="{x:Bind SortGlyph('Archived', ViewModel.SortColumn, ViewModel.SortDescending), Mode=OneWay}"
Visibility="{x:Bind SortGlyphVisibility('Archived', ViewModel.SortColumn), Mode=OneWay}" />
</StackPanel>
</Button>
<Button
Grid.Column="1"
Style="{StaticResource ColumnFilterButtonStyle}"
AutomationProperties.AutomationId="ArchivedFilterButton"
AutomationProperties.Name="{x:Bind ArchivedFilterAutomationName(ViewModel.ArchivedFilter), Mode=OneWay}"
ToolTipService.ToolTip="Filter by archived">
<FontIcon
FontSize="12"
Glyph="&#xE71C;"
Foreground="{x:Bind ArchivedFilterIndicatorBrush(ViewModel.ArchivedFilter), Mode=OneWay}" />
<Button.Flyout>
<Flyout Placement="Bottom">
<RadioButtons
Header="Show"
AutomationProperties.AutomationId="ArchivedFilterChoices"
SelectedIndex="{x:Bind ArchivedFilterIndex(ViewModel.ArchivedFilter), Mode=OneWay}"
SelectionChanged="ArchivedFilterChoices_SelectionChanged">
<x:String>All</x:String>
<x:String>Archived only</x:String>
<x:String>Not archived</x:String>
</RadioButtons>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
</Grid>
</Border>
<!-- Per-repo table body. The ListView carries the view model as DataContext so
the row template's Resolve link can reach WorkspaceViewModel.ResolvePathsCommand
+93
View File
@@ -123,6 +123,20 @@ namespace gclo
{
UpdateDerivedTexts();
}
else if (e.PropertyName == nameof(WorkspaceViewModel.Filter))
{
// The status filter can now change from the header flyout too; keep
// the toolbar chips in agreement. Selecting the already-selected item
// (or the echo from the chips' own handler) is a no-op.
foreach (SelectorBarItem item in FilterSelectorBar.Items)
{
if (item.Tag is string tag && tag == ViewModel.Filter.ToString())
{
FilterSelectorBar.SelectedItem = item;
break;
}
}
}
}
/// <summary>
@@ -149,6 +163,85 @@ namespace gclo
}
}
// -------------------------------------------------------- column filters
//
// The flyout inputs bind OneWay from the view model and write back through
// these handlers; the view model setters no-op on equal values, so the echo
// from a programmatic Text/SelectedIndex update never loops.
private void NameFilterBox_TextChanged(object sender, TextChangedEventArgs e)
=> ViewModel.NameFilter = ((TextBox)sender).Text;
private void ClearNameFilter_Click(object sender, RoutedEventArgs e)
=> ViewModel.NameFilter = "";
private void BranchFilterBox_TextChanged(object sender, TextChangedEventArgs e)
=> ViewModel.BranchFilter = ((TextBox)sender).Text;
private void ClearBranchFilter_Click(object sender, RoutedEventArgs e)
=> ViewModel.BranchFilter = "";
private void StatusFilterChoices_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (((RadioButtons)sender).SelectedIndex is >= 0 and int index)
{
ViewModel.Filter = (RepoFilter)index; // list order mirrors the enum
}
}
private void ArchivedFilterChoices_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ViewModel.ArchivedFilter = ((RadioButtons)sender).SelectedIndex switch
{
1 => true,
2 => false,
_ => null,
};
}
/// <summary>Flyout radio index for the status filter; list order mirrors the enum.</summary>
public int StatusFilterIndex(RepoFilter filter) => (int)filter;
/// <summary>Flyout radio index for the archived filter: All / Archived only / Not archived.</summary>
public int ArchivedFilterIndex(bool? filter) => filter switch
{
true => 1,
false => 2,
null => 0,
};
/// <summary>Funnel tint: accent while that column's filter is active.</summary>
public Microsoft.UI.Xaml.Media.Brush TextFilterIndicatorBrush(string filter)
=> FilterIndicatorBrush(filter.Length > 0);
public Microsoft.UI.Xaml.Media.Brush StatusFilterIndicatorBrush(RepoFilter filter)
=> FilterIndicatorBrush(filter != RepoFilter.All);
public Microsoft.UI.Xaml.Media.Brush ArchivedFilterIndicatorBrush(bool? filter)
=> FilterIndicatorBrush(filter is not null);
private static Microsoft.UI.Xaml.Media.Brush FilterIndicatorBrush(bool active)
=> (Microsoft.UI.Xaml.Media.Brush)Application.Current.Resources[
active ? "AccentTextFillColorPrimaryBrush" : "TextFillColorSecondaryBrush"];
/// <summary>Composed automation name for a text-filter funnel, carrying its state.</summary>
public string TextFilterAutomationName(string column, string filter)
=> filter.Length == 0
? $"Filter by {column}"
: $"Filter by {column}, filtering on '{filter}'";
public string StatusFilterAutomationName(RepoFilter filter)
=> filter == RepoFilter.All
? "Filter by status"
: $"Filter by status, showing {filter}";
public string ArchivedFilterAutomationName(bool? filter) => filter switch
{
true => "Filter by archived, showing archived only",
false => "Filter by archived, showing not archived",
null => "Filter by archived",
};
private void Toolbar_SizeChanged(object sender, SizeChangedEventArgs e)
=> UpdateToolbarLayout();