Add some local storage to save settings between
page reloads.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
@inject IConfiguration Configuration
|
@inject IConfiguration Configuration
|
||||||
@inject AppDbContext Context
|
@inject AppDbContext Context
|
||||||
@inject ClipboardService ClipboardService
|
@inject ClipboardService ClipboardService
|
||||||
|
@inject LocalStorageService LocalStorage
|
||||||
|
|
||||||
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
|
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
<MudPaper Class="pa-2 ma-2" Elevation="3">
|
<MudPaper Class="pa-2 ma-2" Elevation="3">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="6" sm="3" lg="2">
|
<MudItem xs="6" sm="3" lg="2">
|
||||||
<MudNumericField @bind-Value="_parameters.TimeSlots"
|
<MudNumericField Value="_parameters.TimeSlots"
|
||||||
|
ValueChanged="async (int val) => await OnTimeSlotCountChanged(val)"
|
||||||
Label="Time Slots" Min="1" Max="4">
|
Label="Time Slots" Min="1" Max="4">
|
||||||
</MudNumericField>
|
</MudNumericField>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@@ -83,13 +85,15 @@
|
|||||||
<MudItem xs="5" sm="4" lg="3">
|
<MudItem xs="5" sm="4" lg="3">
|
||||||
<MudStack>
|
<MudStack>
|
||||||
<StudentTextBoxSelector Students="@_students"
|
<StudentTextBoxSelector Students="@_students"
|
||||||
@bind-SelectedStudents="_absentStudents"
|
SelectedStudents="_absentStudents"
|
||||||
|
SelectedStudentsChanged="OnAbsentStudentsChanged"
|
||||||
Title="Absent Students"
|
Title="Absent Students"
|
||||||
Label="Search for absent students"
|
Label="Search for absent students"
|
||||||
ShowFullName="true"/>
|
ShowFullName="true"/>
|
||||||
<MudDivider Class="my-4"/>
|
<MudDivider Class="my-4"/>
|
||||||
<TeamToggleSelector Teams="@_teams"
|
<TeamToggleSelector Teams="@_teams"
|
||||||
@bind-SelectedTeams="_scheduledTeams"
|
SelectedTeams="_scheduledTeams"
|
||||||
|
SelectedTeamsChanged="OnScheduledTeamsChanged"
|
||||||
Title="Scheduled Teams"
|
Title="Scheduled Teams"
|
||||||
ShowEventAttributes="true" />
|
ShowEventAttributes="true" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -109,43 +113,67 @@
|
|||||||
private IEnumerable<Student> _absentStudents = [];
|
private IEnumerable<Student> _absentStudents = [];
|
||||||
private IEnumerable<Team> _possibleAdditions = [];
|
private IEnumerable<Team> _possibleAdditions = [];
|
||||||
|
|
||||||
private void AddRegionals()
|
private async Task OnScheduledTeamsChanged(IEnumerable<Team> teams)
|
||||||
|
{
|
||||||
|
_scheduledTeams = teams;
|
||||||
|
await SaveScheduledTeams();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnAbsentStudentsChanged(IEnumerable<Student> students)
|
||||||
|
{
|
||||||
|
_absentStudents = students;
|
||||||
|
await SaveAbsentStudents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnTimeSlotCountChanged(int timeSlots)
|
||||||
|
{
|
||||||
|
_parameters.TimeSlots = timeSlots;
|
||||||
|
await SaveTimeSlotCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AddRegionals()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams
|
||||||
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
|
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddHighLevelOfEffort()
|
private async void AddHighLevelOfEffort()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams
|
||||||
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
|
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveIndividual()
|
private async void RemoveIndividual()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams
|
||||||
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
|
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveLowLevelOfEffort()
|
private async void RemoveLowLevelOfEffort()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams
|
||||||
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
|
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Invert()
|
private async void Invert()
|
||||||
{
|
{
|
||||||
var rt = _scheduledTeams.ToArray();
|
var rt = _scheduledTeams.ToArray();
|
||||||
_scheduledTeams
|
_scheduledTeams
|
||||||
= _teams.Where(t => !rt.Contains(t));
|
= _teams.Where(t => !rt.Contains(t));
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Reset()
|
private async void Reset()
|
||||||
{
|
{
|
||||||
_scheduledTeams = [];
|
_scheduledTeams = [];
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleRequiredTeam(Team unassignedTeam)
|
private async void ToggleRequiredTeam(Team unassignedTeam)
|
||||||
{
|
{
|
||||||
if (_scheduledTeams.Contains(unassignedTeam))
|
if (_scheduledTeams.Contains(unassignedTeam))
|
||||||
_scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam);
|
_scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam);
|
||||||
@@ -153,6 +181,7 @@
|
|||||||
{
|
{
|
||||||
_scheduledTeams = _scheduledTeams.Concat(new[] { unassignedTeam });
|
_scheduledTeams = _scheduledTeams.Concat(new[] { unassignedTeam });
|
||||||
}
|
}
|
||||||
|
await SaveScheduledTeams();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -201,6 +230,55 @@
|
|||||||
.Include(e => e.EventRankings)
|
.Include(e => e.EventRankings)
|
||||||
.ThenInclude(e => e.EventDefinition)
|
.ThenInclude(e => e.EventDefinition)
|
||||||
.OrderBy(e => e.FirstName).ToArrayAsync();
|
.OrderBy(e => e.FirstName).ToArrayAsync();
|
||||||
|
|
||||||
|
// Load saved selections from localStorage
|
||||||
|
await LoadScheduledTeams();
|
||||||
|
await LoadAbsentStudents();
|
||||||
|
await LoadTimeSlotCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveScheduledTeams()
|
||||||
|
{
|
||||||
|
var teamIds = _scheduledTeams.Select(t => t.Id).ToArray();
|
||||||
|
await LocalStorage.SetIntArrayAsync("MeetingSchedule_ScheduledTeams", teamIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadScheduledTeams()
|
||||||
|
{
|
||||||
|
var teamIds = await LocalStorage.GetIntArrayAsync("MeetingSchedule_ScheduledTeams");
|
||||||
|
if (teamIds.Length > 0)
|
||||||
|
{
|
||||||
|
_scheduledTeams = _teams.Where(t => teamIds.Contains(t.Id)).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveAbsentStudents()
|
||||||
|
{
|
||||||
|
var studentIds = _absentStudents.Select(s => s.Id).ToArray();
|
||||||
|
await LocalStorage.SetIntArrayAsync("MeetingSchedule_AbsentStudents", studentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAbsentStudents()
|
||||||
|
{
|
||||||
|
var studentIds = await LocalStorage.GetIntArrayAsync("MeetingSchedule_AbsentStudents");
|
||||||
|
if (studentIds.Length > 0)
|
||||||
|
{
|
||||||
|
_absentStudents = _students.Where(s => studentIds.Contains(s.Id)).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveTimeSlotCount()
|
||||||
|
{
|
||||||
|
await LocalStorage.SetIntAsync("MeetingSchedule_TimeSlotCount", _parameters.TimeSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadTimeSlotCount()
|
||||||
|
{
|
||||||
|
var timeSlots = await LocalStorage.GetIntAsync("MeetingSchedule_TimeSlotCount", defaultValue: 2);
|
||||||
|
if (timeSlots > 0)
|
||||||
|
{
|
||||||
|
_parameters.TimeSlots = timeSlots;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ else
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="@($"/students/edit?id={student.Id}")" Variant="Variant.Filled" Color="Color.Primary">Edit</MudButton>
|
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="@($"/students/edit?id={student.Id}&returnUrl={ReturnUrl ?? "/students"}")" Variant="Variant.Filled" Color="Color.Primary">Edit</MudButton>
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="/students" Variant="Variant.Text">Back to List</MudButton>
|
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="@(ReturnUrl ?? "/students")" Variant="Variant.Text">Back to List</MudButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,9 @@ else
|
|||||||
[SupplyParameterFromQuery]
|
[SupplyParameterFromQuery]
|
||||||
private int Id { get; set; }
|
private int Id { get; set; }
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery]
|
||||||
|
private string? ReturnUrl { get; set; }
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
student = await context.Students.FirstOrDefaultAsync(m => m.Id == Id);
|
student = await context.Students.FirstOrDefaultAsync(m => m.Id == Id);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ else
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="students">Back</MudButton>
|
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="@(ReturnUrl ?? "/students")">Back</MudButton>
|
||||||
<MudButton StartIcon="@Icons.Material.Filled.Save" OnClick="UpdateStudent">Save</MudButton>
|
<MudButton StartIcon="@Icons.Material.Filled.Save" OnClick="UpdateStudent">Save</MudButton>
|
||||||
</EditForm>
|
</EditForm>
|
||||||
}
|
}
|
||||||
@@ -54,6 +54,9 @@ else
|
|||||||
[SupplyParameterFromQuery]
|
[SupplyParameterFromQuery]
|
||||||
private int Id { get; set; }
|
private int Id { get; set; }
|
||||||
|
|
||||||
|
[SupplyParameterFromQuery]
|
||||||
|
private string? ReturnUrl { get; set; }
|
||||||
|
|
||||||
[SupplyParameterFromForm]
|
[SupplyParameterFromForm]
|
||||||
private Student? Student { get; set; }
|
private Student? Student { get; set; }
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigationManager.NavigateTo("/students");
|
NavigationManager.NavigateTo(ReturnUrl ?? "/students");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool StudentExists(int id)
|
private bool StudentExists(int id)
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
</PropertyColumn>
|
</PropertyColumn>
|
||||||
<TemplateColumn>
|
<TemplateColumn>
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
<CrudActions DetailsHref="@($"/students/details?id={context.Item!.Id}")"
|
<CrudActions DetailsHref="@($"/students/details?id={context.Item!.Id}&returnUrl=/students")"
|
||||||
EditHref="@($"/students/edit?id={context.Item!.Id}")"
|
EditHref="@($"/students/edit?id={context.Item!.Id}&returnUrl=/students")"
|
||||||
DeleteOnClick="() => DeleteStudent(context.Item!)">
|
DeleteOnClick="() => DeleteStudent(context.Item!)">
|
||||||
</CrudActions>
|
</CrudActions>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using WebApp.Models
|
@using WebApp.Models
|
||||||
@inject AppDbContext Context
|
@inject AppDbContext Context
|
||||||
|
@inject WebApp.LocalStorageService LocalStorage
|
||||||
|
|
||||||
<PageTitle>Registration - TSA Chapter Organizer</PageTitle>
|
<PageTitle>Registration - TSA Chapter Organizer</PageTitle>
|
||||||
|
|
||||||
@@ -23,10 +24,10 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.body2" Style="margin-bottom: 8px;">
|
<MudText Typo="Typo.body2" Style="margin-bottom: 8px;">
|
||||||
<strong>Show Columns:</strong>
|
<strong>Show Columns:</strong>
|
||||||
<MudCheckBox Value="_showGrade" ValueChanged="(bool val) => { _showGrade = val; _gridKey++; StateHasChanged(); }" Label="Grade" Dense="true" Size="Size.Small" />
|
<MudCheckBox Value="_showGrade" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showGrade), val, v => _showGrade = v)" Label="Grade" Dense="true" Size="Size.Small" />
|
||||||
<MudCheckBox Value="_showRegionalId" ValueChanged="(bool val) => { _showRegionalId = val; _gridKey++; StateHasChanged(); }" Label="Regional ID" Dense="true" Size="Size.Small" />
|
<MudCheckBox Value="_showRegionalId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showRegionalId), val, v => _showRegionalId = v)" Label="Regional ID" Dense="true" Size="Size.Small" />
|
||||||
<MudCheckBox Value="_showStateId" ValueChanged="(bool val) => { _showStateId = val; _gridKey++; StateHasChanged(); }" Label="State ID" Dense="true" Size="Size.Small" />
|
<MudCheckBox Value="_showStateId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showStateId), val, v => _showStateId = v)" Label="State ID" Dense="true" Size="Size.Small" />
|
||||||
<MudCheckBox Value="_showNationalId" ValueChanged="(bool val) => { _showNationalId = val; _gridKey++; StateHasChanged(); }" Label="National ID" Dense="true" Size="Size.Small" />
|
<MudCheckBox Value="_showNationalId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showNationalId), val, v => _showNationalId = v)" Label="National ID" Dense="true" Size="Size.Small" />
|
||||||
</MudText>
|
</MudText>
|
||||||
|
|
||||||
<MudDataGrid T="StudentTeamInfo" ServerData="ServerReload" @ref="_dataGrid" @key="_gridKey" Filterable="true" RowsPerPage="35">
|
<MudDataGrid T="StudentTeamInfo" ServerData="ServerReload" @ref="_dataGrid" @key="_gridKey" Filterable="true" RowsPerPage="35">
|
||||||
@@ -34,6 +35,12 @@
|
|||||||
<PropertyColumn Property="@(e => e.Student.LastName)" Title="Student" Sortable="true">
|
<PropertyColumn Property="@(e => e.Student.LastName)" Title="Student" Sortable="true">
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
@context.Item.Student.LastNameFirstName
|
@context.Item.Student.LastNameFirstName
|
||||||
|
<MudTooltip Text="Edit">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||||
|
Size="Size.Small"
|
||||||
|
Href="@($"/students/edit?id={context.Item.Student.Id}&returnUrl=/students/teams")"
|
||||||
|
Style="margin-left: 4px;" />
|
||||||
|
</MudTooltip>
|
||||||
</CellTemplate>
|
</CellTemplate>
|
||||||
</PropertyColumn>
|
</PropertyColumn>
|
||||||
@if (_showGrade)
|
@if (_showGrade)
|
||||||
@@ -94,6 +101,7 @@
|
|||||||
private bool _showRegionalId;
|
private bool _showRegionalId;
|
||||||
private bool _showStateId;
|
private bool _showStateId;
|
||||||
private bool _showNationalId;
|
private bool _showNationalId;
|
||||||
|
private bool _preferencesLoaded = false;
|
||||||
|
|
||||||
// TODO: Remove this workaround once MudBlazor fixes dynamic column ordering
|
// TODO: Remove this workaround once MudBlazor fixes dynamic column ordering
|
||||||
// https://dev.to/the_real_slim_janey/get-in-line-customizing-column-order-in-mudblazor-3ail
|
// https://dev.to/the_real_slim_janey/get-in-line-customizing-column-order-in-mudblazor-3ail
|
||||||
@@ -103,6 +111,39 @@
|
|||||||
// 3. Remove "@key="_gridKey"" attribute from MudDataGrid (line 32)
|
// 3. Remove "@key="_gridKey"" attribute from MudDataGrid (line 32)
|
||||||
private int _gridKey = 0;
|
private int _gridKey = 0;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!_preferencesLoaded)
|
||||||
|
{
|
||||||
|
await LoadColumnPreferences();
|
||||||
|
_preferencesLoaded = true;
|
||||||
|
_gridKey++; // Force grid recreation with loaded preferences
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
// Reset flag when component is initialized/re-initialized
|
||||||
|
_preferencesLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadColumnPreferences()
|
||||||
|
{
|
||||||
|
_showGrade = await LocalStorage.GetBoolAsync("Registration_ShowGrade", false);
|
||||||
|
_showRegionalId = await LocalStorage.GetBoolAsync("Registration_ShowRegionalId", false);
|
||||||
|
_showStateId = await LocalStorage.GetBoolAsync("Registration_ShowStateId", false);
|
||||||
|
_showNationalId = await LocalStorage.GetBoolAsync("Registration_ShowNationalId", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnColumnToggle(string columnName, bool value, Action<bool> setter)
|
||||||
|
{
|
||||||
|
setter(value);
|
||||||
|
_gridKey++;
|
||||||
|
await LocalStorage.SetBoolAsync($"Registration_{columnName}", value);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ToggleRegionalFilter()
|
private async Task ToggleRegionalFilter()
|
||||||
{
|
{
|
||||||
_showRegionalOnly = !_showRegionalOnly;
|
_showRegionalOnly = !_showRegionalOnly;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@student.FirstName
|
@student.FirstName
|
||||||
@if (captain)
|
@if (captain)
|
||||||
{
|
{
|
||||||
<span> (Cpt)</span>
|
<MudIcon Icon="@AppIcons.Captain" Size="Size.Small" Style="margin-left: 4px;" />
|
||||||
}
|
}
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace WebApp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for managing browser localStorage with type-safe methods.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LocalStorageService
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
|
||||||
|
public LocalStorageService(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a boolean value from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="defaultValue">Default value if key doesn't exist or parsing fails.</param>
|
||||||
|
/// <returns>The stored boolean value or default value.</returns>
|
||||||
|
public async Task<bool> GetBoolAsync(string key, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
|
||||||
|
return value != null && bool.TryParse(value, out var result) ? result : defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets a boolean value in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="value">The boolean value to store.</param>
|
||||||
|
public async Task SetBoolAsync(string key, bool value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to save boolean to localStorage [{key}]: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an integer value from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="defaultValue">Default value if key doesn't exist or parsing fails.</param>
|
||||||
|
/// <returns>The stored integer value or default value.</returns>
|
||||||
|
public async Task<int> GetIntAsync(string key, int defaultValue = 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
|
||||||
|
return value != null && int.TryParse(value, out var result) ? result : defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets an integer value in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="value">The integer value to store.</param>
|
||||||
|
public async Task SetIntAsync(string key, int value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to save integer to localStorage [{key}]: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an array of integers from localStorage (stored as JSON).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <returns>Array of integers or empty array if not found.</returns>
|
||||||
|
public async Task<int[]> GetIntArrayAsync(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
|
||||||
|
if (!string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
var array = JsonSerializer.Deserialize<int[]>(json);
|
||||||
|
return array ?? Array.Empty<int>();
|
||||||
|
}
|
||||||
|
return Array.Empty<int>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to load integer array from localStorage [{key}]: {ex.Message}");
|
||||||
|
return Array.Empty<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets an array of integers in localStorage (stored as JSON).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key.</param>
|
||||||
|
/// <param name="values">The integer array to store.</param>
|
||||||
|
public async Task SetIntArrayAsync(string key, int[] values)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(values);
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to save integer array to localStorage [{key}]: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an item from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The storage key to remove.</param>
|
||||||
|
public async Task RemoveAsync(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to remove from localStorage [{key}]: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all items from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
public async Task ClearAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync("localStorage.clear");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to clear localStorage: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Parsing;
|
||||||
|
|
||||||
|
namespace WebApp.Logging
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Custom Serilog sink that downgrades antiforgery token deserialization errors to Information level
|
||||||
|
/// </summary>
|
||||||
|
public class AntiforgeryLogEventSink : ILogEventSink
|
||||||
|
{
|
||||||
|
private readonly ILogEventSink _wrappedSink;
|
||||||
|
private readonly MessageTemplateParser _parser;
|
||||||
|
|
||||||
|
public AntiforgeryLogEventSink(ILogEventSink wrappedSink)
|
||||||
|
{
|
||||||
|
_wrappedSink = wrappedSink;
|
||||||
|
_parser = new MessageTemplateParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Emit(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
// Check if this is an antiforgery token deserialization error (Event ID 7)
|
||||||
|
if (IsTokenDeserializeException(logEvent))
|
||||||
|
{
|
||||||
|
// Rewrite the log event at Information level with explanatory message
|
||||||
|
var messageTemplate = _parser.Parse(
|
||||||
|
"Antiforgery token validation failed - expected after container restart. " +
|
||||||
|
"User will receive a fresh token on page refresh. Original message: {OriginalMessage}");
|
||||||
|
|
||||||
|
var rewrittenEvent = new LogEvent(
|
||||||
|
logEvent.Timestamp,
|
||||||
|
LogEventLevel.Information,
|
||||||
|
null, // No exception at Info level
|
||||||
|
messageTemplate,
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new LogEventProperty("OriginalMessage",
|
||||||
|
new ScalarValue(logEvent.MessageTemplate.Render(logEvent.Properties))),
|
||||||
|
new LogEventProperty("SourceContext",
|
||||||
|
logEvent.Properties.GetValueOrDefault("SourceContext") ?? new ScalarValue("Unknown")),
|
||||||
|
new LogEventProperty("RequestPath",
|
||||||
|
logEvent.Properties.GetValueOrDefault("RequestPath") ?? new ScalarValue("Unknown"))
|
||||||
|
});
|
||||||
|
|
||||||
|
_wrappedSink.Emit(rewrittenEvent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Pass through all other log events unchanged
|
||||||
|
_wrappedSink.Emit(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsTokenDeserializeException(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
if (logEvent.Level != LogEventLevel.Error)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (logEvent.Properties.TryGetValue("EventId", out var eventId))
|
||||||
|
{
|
||||||
|
var eventIdString = eventId.ToString();
|
||||||
|
return eventIdString.Contains("\"Id\":7") &&
|
||||||
|
eventIdString.Contains("\"Name\":\"TokenDeserializeException\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,6 +167,7 @@ builder.Services.AddQuickGridEntityFrameworkAdapter();
|
|||||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||||
|
|
||||||
builder.Services.AddScoped<ClipboardService>();
|
builder.Services.AddScoped<ClipboardService>();
|
||||||
|
builder.Services.AddScoped<WebApp.LocalStorageService>();
|
||||||
|
|
||||||
// State container for maintaining state per user connection (Blazor Server)
|
// State container for maintaining state per user connection (Blazor Server)
|
||||||
builder.Services.AddScoped<StateContainer>();
|
builder.Services.AddScoped<StateContainer>();
|
||||||
|
|||||||
Reference in New Issue
Block a user