PR U-9: FluentSearch filter on grid pages

Accounts, BookingRules, BookingTexts, BankTransactions each get an
<FluentSearch> above the grid, backed by a computed Filtered
IQueryable over an in-memory source list. FluentSearch is Immediate,
so filtering happens on each keystroke; data sizes are small enough
for per-render LINQ.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-07-04 10:10:43 +02:00
parent 766295a119
commit 0c48539ce6
8 changed files with 69 additions and 24 deletions

View file

@ -3,10 +3,13 @@
<PageTitle>Konten</PageTitle> <PageTitle>Konten</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neues Konto hinzufügen</FluentButton> <FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neues Konto hinzufügen</FluentButton>
<FluentSearch @bind-Value="filter" Immediate="true" Placeholder="Filter…" Style="width: 300px;"/>
</FluentStack>
<FluentPaginator State="@pagination" SummaryTemplate="@template" /> <FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@accountQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium"> <FluentDataGrid Items="@Filtered" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.Class)" Sortable="true" Width="100px" /> <PropertyColumn Property="@(p => p.Class)" Sortable="true" Width="100px" />
<PropertyColumn Property="@(p => p.SubClass)" Sortable="true" Width="140px" /> <PropertyColumn Property="@(p => p.SubClass)" Sortable="true" Width="140px" />
<PropertyColumn Property="@(p => p.Number)" Sortable="true" IsDefaultSortColumn="true" Width="140px" /> <PropertyColumn Property="@(p => p.Number)" Sortable="true" IsDefaultSortColumn="true" Width="140px" />

View file

@ -17,11 +17,20 @@ public partial class Accounts : ComponentBase
[Inject] [Inject]
private IToastService toastService { get; set; } = null!; private IToastService toastService { get; set; } = null!;
private IQueryable<Account>? accountQueryable; private IReadOnlyList<Account> source = [];
private string filter = "";
private IQueryable<Account> Filtered =>
string.IsNullOrWhiteSpace(filter)
? source.AsQueryable()
: source.AsQueryable().Where(a =>
a.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
a.Number.ToString().Contains(filter) ||
(a.BankAccountNumber != null && a.BankAccountNumber.Contains(filter, StringComparison.OrdinalIgnoreCase)));
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); source = await accountRepository.GetAccountListAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -39,7 +48,7 @@ public partial class Accounts : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); source = await accountRepository.GetAccountListAsync();
toastService.ShowToast(ToastIntent.Success, "Konto gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Konto gespeichert", 3000);
} }
} }
@ -58,7 +67,7 @@ public partial class Accounts : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); source = await accountRepository.GetAccountListAsync();
toastService.ShowToast(ToastIntent.Success, "Konto gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Konto gespeichert", 3000);
} }
} }
@ -71,7 +80,7 @@ public partial class Accounts : ComponentBase
if (!result.Cancelled) if (!result.Cancelled)
{ {
await accountRepository.DeleteAccountAsync(id); await accountRepository.DeleteAccountAsync(id);
accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); source = await accountRepository.GetAccountListAsync();
toastService.ShowToast(ToastIntent.Success, "Konto gelöscht", 3000); toastService.ShowToast(ToastIntent.Success, "Konto gelöscht", 3000);
} }
} }

View file

@ -1,10 +1,11 @@
@page "/banktransactions" @page "/banktransactions"
<PageTitle>BankTransactions</PageTitle> <PageTitle>BankTransactions</PageTitle>
<FluentSearch @bind-Value="filter" Immediate="true" Placeholder="Filter…" Style="width: 300px;"/>
<FluentPaginator State="@pagination" /> <FluentPaginator State="@pagination" />
<div id="datagrid-container"> <div id="datagrid-container">
<FluentDataGrid Items="@TransactionList" Pagination="@pagination" RowSize="DataGridRowSize.Small" AutoItemsPerPage="true" Style="overflow-y:hidden;"> <FluentDataGrid Items="@Filtered" Pagination="@pagination" RowSize="DataGridRowSize.Small" AutoItemsPerPage="true" Style="overflow-y:hidden;">
<PropertyColumn Title="Buchungsdatum" Width="170px" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" /> <PropertyColumn Title="Buchungsdatum" Width="170px" Property="@(p => p.BookingDate)" Format="dd.MM.yyyy" Sortable="true" IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" />
<PropertyColumn Title="Valutadatum" Width="170px" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" /> <PropertyColumn Title="Valutadatum" Width="170px" Property="@(p => p.ValueDate)" Format="dd.MM.yyyy" Sortable="true" />
<PropertyColumn Title="Text" Property="@(p => p.Text)" /> <PropertyColumn Title="Text" Property="@(p => p.Text)" />

View file

@ -8,11 +8,22 @@ public partial class BankTransactions : ComponentBase
{ {
[Inject] [Inject]
private IBankTransactionRepository bankTransactionRepository { get; set; } = null!; private IBankTransactionRepository bankTransactionRepository { get; set; } = null!;
private IQueryable<BankTransaction>? TransactionList;
private IReadOnlyList<BankTransaction> source = [];
private string filter = "";
private IQueryable<BankTransaction> Filtered =>
string.IsNullOrWhiteSpace(filter)
? source.AsQueryable()
: source.AsQueryable().Where(t =>
(t.Text != null && t.Text.Contains(filter, StringComparison.OrdinalIgnoreCase)) ||
(t.Debtor != null && t.Debtor.Contains(filter, StringComparison.OrdinalIgnoreCase)) ||
(t.Creditor != null && t.Creditor.Contains(filter, StringComparison.OrdinalIgnoreCase)) ||
t.Value.ToString().Contains(filter));
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
TransactionList = (await bankTransactionRepository.GetBankTransactionListAsync()).AsQueryable(); source = await bankTransactionRepository.GetBankTransactionListAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
} }

View file

@ -3,10 +3,13 @@
<PageTitle>Buchungsregeln</PageTitle> <PageTitle>Buchungsregeln</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Regel hinzufügen</FluentButton> <FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neue Regel hinzufügen</FluentButton>
<FluentSearch @bind-Value="filter" Immediate="true" Placeholder="Filter…" Style="width: 300px;"/>
</FluentStack>
<FluentPaginator State="@pagination" SummaryTemplate="@template" /> <FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@bookingRulesQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium"> <FluentDataGrid Items="@Filtered" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.LookupText)" Sortable="true" IsDefaultSortColumn="true" /> <PropertyColumn Property="@(p => p.LookupText)" Sortable="true" IsDefaultSortColumn="true" />
<PropertyColumn Property="@(p => p.LookupValue)" Format="C2" Align="Align.End"/> <PropertyColumn Property="@(p => p.LookupValue)" Format="C2" Align="Align.End"/>
<PropertyColumn Property="@(p => p.BookingText)" /> <PropertyColumn Property="@(p => p.BookingText)" />

View file

@ -21,11 +21,20 @@ public partial class BookingRules : ComponentBase
[Inject] [Inject]
private IToastService toastService { get; set; } = null!; private IToastService toastService { get; set; } = null!;
private IQueryable<BookingRuleDataset>? bookingRulesQueryable; private IReadOnlyList<BookingRuleDataset> source = [];
private string filter = "";
private IQueryable<BookingRuleDataset> Filtered =>
string.IsNullOrWhiteSpace(filter)
? source.AsQueryable()
: source.AsQueryable().Where(r =>
r.LookupText.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
(r.BookingText != null && r.BookingText.Contains(filter, StringComparison.OrdinalIgnoreCase)) ||
(r.Account != null && r.Account.Contains(filter, StringComparison.OrdinalIgnoreCase)));
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); source = await viewService.GetBookingRuleViewListAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -43,7 +52,7 @@ public partial class BookingRules : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); source = await viewService.GetBookingRuleViewListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungsregel gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungsregel gespeichert", 3000);
} }
} }
@ -62,7 +71,7 @@ public partial class BookingRules : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); source = await viewService.GetBookingRuleViewListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungsregel gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungsregel gespeichert", 3000);
} }
} }
@ -75,7 +84,7 @@ public partial class BookingRules : ComponentBase
if (!result.Cancelled) if (!result.Cancelled)
{ {
await bookingRuleRepository.DeleteBookingRuleAsync(id); await bookingRuleRepository.DeleteBookingRuleAsync(id);
bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); source = await viewService.GetBookingRuleViewListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungsregel gelöscht", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungsregel gelöscht", 3000);
} }
} }

View file

@ -3,10 +3,13 @@
<PageTitle>Buchungstexte</PageTitle> <PageTitle>Buchungstexte</PageTitle>
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neuer Text hinzufügen</FluentButton> <FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
<FluentButton IconStart="@(new Icons.Regular.Size16.Add())" Appearance="Appearance.Neutral" OnClick="AddAsync">Neuer Text hinzufügen</FluentButton>
<FluentSearch @bind-Value="filter" Immediate="true" Placeholder="Filter…" Style="width: 300px;"/>
</FluentStack>
<FluentPaginator State="@pagination" SummaryTemplate="@template" /> <FluentPaginator State="@pagination" SummaryTemplate="@template" />
<FluentDataGrid Items="@bookingTextQueryable" Pagination="@pagination" RowSize="DataGridRowSize.Medium"> <FluentDataGrid Items="@Filtered" Pagination="@pagination" RowSize="DataGridRowSize.Medium">
<PropertyColumn Property="@(p => p.Text)" Sortable="true" IsDefaultSortColumn="true" /> <PropertyColumn Property="@(p => p.Text)" Sortable="true" IsDefaultSortColumn="true" />
<TemplateColumn Align="Align.Center" Width="50px"> <TemplateColumn Align="Align.Center" Width="50px">
<FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" /> <FluentButton IconEnd="@(new Icons.Regular.Size16.Delete())" Appearance="Appearance.Neutral" OnClick="@(() => DeleteAsync(context.Id))" />

View file

@ -17,11 +17,17 @@ public partial class BookingTexts : ComponentBase
[Inject] [Inject]
private IToastService toastService { get; set; } = null!; private IToastService toastService { get; set; } = null!;
private IQueryable<BookingText>? bookingTextQueryable; private IReadOnlyList<BookingText> source = [];
private string filter = "";
private IQueryable<BookingText> Filtered =>
string.IsNullOrWhiteSpace(filter)
? source.AsQueryable()
: source.AsQueryable().Where(t => t.Text.Contains(filter, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); source = await bookingTextRepository.GetBookingTextListAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -39,7 +45,7 @@ public partial class BookingTexts : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); source = await bookingTextRepository.GetBookingTextListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungstext gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungstext gespeichert", 3000);
} }
} }
@ -58,7 +64,7 @@ public partial class BookingTexts : ComponentBase
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Cancelled && result.Data != null) if (!result.Cancelled && result.Data != null)
{ {
bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); source = await bookingTextRepository.GetBookingTextListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungstext gespeichert", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungstext gespeichert", 3000);
} }
} }
@ -71,7 +77,7 @@ public partial class BookingTexts : ComponentBase
if (!result.Cancelled) if (!result.Cancelled)
{ {
await bookingTextRepository.DeleteBookingTextAsync(id); await bookingTextRepository.DeleteBookingTextAsync(id);
bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); source = await bookingTextRepository.GetBookingTextListAsync();
toastService.ShowToast(ToastIntent.Success, "Buchungstext gelöscht", 3000); toastService.ShowToast(ToastIntent.Success, "Buchungstext gelöscht", 3000);
} }
} }