From c468583afde681347b8faf13a51dafb7e17ba54d Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 23 Dec 2025 11:10:19 +0100 Subject: [PATCH] Add user layout management to grids with toolbar actions - Introduced separate auto-save and user-save layout storage keys - Added IMgGridBase methods for saving, loading, resetting, and checking user layouts - Updated grid toolbar with "Layout" menu (load, save, reset) and new icons - Improved layout persistence logic and default layout restore - Enabled forced grid re-render on layout reset - Adjusted grid pager and page size defaults - Updated related components to use new storage keys - Fixed minor bugs and set RELEASE log level to Debug --- .../Components/Grids/MgGridBase.cs | 136 +++++++++++++++++- .../Grids/MgGridToolbarTemplate.razor | 29 +++- .../Grids/MgGridWithInfoPanel.razor | 2 +- 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index f9efa1a..14176c9 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -61,12 +61,36 @@ public interface IMgGridBase : IGrid /// Whether the grid/wrapper is currently in fullscreen mode /// bool IsFullscreen { get; } - string LayoutStorageKey { get; } + + /// + /// Storage key for automatic layout persistence + /// + string AutomaticLayoutStorageKey { get; } /// /// Toggles fullscreen mode for the grid (or wrapper if available) /// void ToggleFullscreen(); + + /// + /// Saves the current layout to user storage (manual save) + /// + Task SaveUserLayoutAsync(); + + /// + /// Loads layout from user storage (manual load) + /// + Task LoadUserLayoutAsync(); + + /// + /// Resets the layout by clearing auto-saved layout and reloading the page + /// + Task ResetLayoutAsync(); + + /// + /// Checks if a user-saved layout exists without loading it + /// + Task HasUserLayoutAsync(); } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -81,6 +105,7 @@ public abstract class MgGridBase? _dataSourceParam = []; @@ -107,7 +132,12 @@ public abstract class MgGridBase + /// Gets the user layout storage key (replaces AutoSave with UserSave) + /// + private string UserLayoutStorageKey => AutomaticLayoutStorageKey.Replace("_AutoSave_", "_UserSave_"); + + public string AutomaticLayoutStorageKey { get { @@ -184,6 +214,7 @@ public abstract class MgGridBase protected virtual int GetLayoutUserId() => 0; + /// + /// Stores the default layout (before any saved layout is loaded) for reset functionality + /// + private string? _defaultLayoutJson = null; + + /// + /// Checks if a layout exists in localStorage without loading its content + /// + protected virtual async Task GetStorageItem(string localStorageKey) + { + try + { + return await JSRuntime.InvokeAsync("localStorage.getItem", localStorageKey); + } + catch + { + // Mute exceptions for the server prerender stage + } + + return null; + } + private async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e) { - e.Layout = await LoadLayoutFromLocalStorageAsync(LayoutStorageKey); + // Save the default layout before loading any saved layout + _defaultLayoutJson ??= JsonSerializer.Serialize(SaveLayout()); + + e.Layout = await LoadLayoutFromLocalStorageAsync(AutomaticLayoutStorageKey); } private async Task Grid_LayoutAutoSaving(GridPersistentLayoutEventArgs e) { - await SaveLayoutToLocalStorageAsync(e.Layout, LayoutStorageKey); + await SaveLayoutToLocalStorageAsync(e.Layout, AutomaticLayoutStorageKey); } protected virtual async Task LoadLayoutFromLocalStorageAsync(string localStorageKey) { try { - var json = await JSRuntime.InvokeAsync("localStorage.getItem", localStorageKey); + var json = await GetStorageItem(localStorageKey); if (!string.IsNullOrWhiteSpace(json)) return JsonSerializer.Deserialize(json); @@ -749,10 +810,75 @@ public abstract class MgGridBase + public async Task SaveUserLayoutAsync() + { + var layout = SaveLayout(); + + await SaveLayoutToLocalStorageAsync(layout, UserLayoutStorageKey); + await SaveLayoutToLocalStorageAsync(layout, AutomaticLayoutStorageKey); + } + + /// + public async Task LoadUserLayoutAsync() + { + var layout = await LoadLayoutFromLocalStorageAsync(UserLayoutStorageKey); + if (layout != null) + { + LoadLayout(layout); + } + } + + /// + public async Task ResetLayoutAsync() + { + await RemoveLayoutFromLocalStorageAsync(AutomaticLayoutStorageKey); + + // Restore the default layout if available + if (!string.IsNullOrWhiteSpace(_defaultLayoutJson)) + { + var defaultLayout = JsonSerializer.Deserialize(_defaultLayoutJson); + if (defaultLayout != null) + LoadLayout(defaultLayout); + } + } + + + /// + public async Task HasUserLayoutAsync() + { + return !(await GetStorageItem(UserLayoutStorageKey)).IsNullOrWhiteSpace(); + } + #endregion //public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Add); + + /// + /// Force grid re-initialization + /// + /// + public async Task ForceRenderAsync() + { + // Force grid re-initialization by changing the render key + _gridRenderKey = Guid.NewGuid(); + + await InvokeAsync(StateHasChanged); + } + public Task UpdateDataItem(TDataItem dataItem) => _dataSource.Update(dataItem, true); public Task UpdateDataItemAsync(TDataItem dataItem) diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor b/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor index f358824..53abc06 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridToolbarTemplate.razor @@ -14,6 +14,13 @@ @if (!OnlyGridEditTools) { + + + + + + + @@ -28,7 +35,7 @@ } -@code { + @code { [Parameter] public bool OnlyGridEditTools { get; set; } = false; [Parameter] public IMgGridBase Grid { get; set; } = null!; [Parameter] public RenderFragment? ToolbarItemsExtended { get; set; } @@ -38,6 +45,7 @@ public MgGridToolbarBase GridToolbar { get; set; } = null!; const string ExportFileName = "ExportResult"; + private bool _hasUserLayout; private bool _isReloadInProgress; /// @@ -70,8 +78,9 @@ /// private string FullscreenIconCssClass => IsFullscreenMode ? "grid-fullscreen-exit" : "grid-fullscreen"; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { + _hasUserLayout = await Grid.HasUserLayoutAsync(); } async Task ReloadData_Click(ToolbarItemClickEventArgs e) @@ -151,4 +160,20 @@ { await Grid.ExportToPdfAsync(ExportFileName); } + + async Task LoadLayout_Click() + { + await Grid.LoadUserLayoutAsync(); + } + + async Task SaveLayout_Click() + { + await Grid.SaveUserLayoutAsync(); + _hasUserLayout = true; + } + + async Task ResetLayout_Click() + { + await Grid.ResetLayoutAsync(); + } } \ No newline at end of file diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor index d4bff51..90441e9 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor @@ -116,7 +116,7 @@ } private string GetStorageKey() => _currentGrid != null - ? $"Splitter_{_currentGrid.LayoutStorageKey}" + ? $"Splitter_{_currentGrid.AutomaticLayoutStorageKey}" : null!; private async Task LoadSavedSizeAsync()