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
This commit is contained in:
Loretta 2025-12-23 11:10:19 +01:00
parent 22821a4b27
commit 3a14b570ef
7 changed files with 109 additions and 60 deletions

View File

@ -107,7 +107,7 @@ public static class FruitBankConstClient
#if RELEASE
public static string SystemEmailAddress = "test@touriam.com";
public static LogLevel DefaultLogLevelClient = LogLevel.Error;
public static LogLevel DefaultLogLevelClient = LogLevel.Debug;
#else
public static string SystemEmailAddress = "test@touriam.com";
public static LogLevel DefaultLogLevelClient = LogLevel.Detail;

View File

@ -20,7 +20,7 @@
<MgGridWithInfoPanel ShowInfoPanel="@IsMasterGrid">
<GridContent>
<GridProductDto @ref="Grid" DataSource="ProductDtos" CssClass="@GridCss" AutoSaveLayoutName="GridProductDtoTemplate"
Logger="_logger" SignalRClient="FruitBankSignalRClient">
Logger="_logger" SignalRClient="FruitBankSignalRClient" Caption="Termék(ek)">
<Columns>
<MgGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending"
UrlLink=@(FruitBankConstClient.BaseUrl + "/Admin/Product/Edit/{Id}") />

View File

@ -73,8 +73,8 @@ public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservab
AutoCollapseDetailRow = true;
AutoExpandAllGroupRows = false;
PagerVisible = IsMasterGrid;
PageSize = IsMasterGrid ? (SizeMode == DevExpress.Blazor.SizeMode.Small ? 23 : 15) : 50;
PagerVisible = true;//IsMasterGrid;
PageSize = IsMasterGrid ? (SizeMode == DevExpress.Blazor.SizeMode.Small ? 20 : 15) : 10;
AllowColumnReorder = true;
AllowGroup = IsMasterGrid;

View File

@ -22,7 +22,6 @@
Logger="_logger"
CssClass="@GridCss"
ValidationEnabled="false"
ShowInfoPanel="true"
OnGridFocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />

View File

@ -23,7 +23,9 @@
</BeforeColumnsTemplate> *@
<AfterColumnsTemplate Context="ctx">
@if (ctx is { IsEditMode: false, DataItem: ShippingDocument doc })
@if (ctx.DataItem is not ShippingDocument && ctx.DataItem is not ShippingItem) return;
@if (ctx is { DataItem: ShippingDocument doc, IsEditMode: false })
{
<table class="table table-sm table-bordered table-striped" style="margin-top: 35px;">
<colgroup>
@ -68,16 +70,16 @@
</tr>
</tfoot>
</table>
<MgLazyLoadContent @ref="_lazyContentRef"
MinHeight="800px"
RootMargin="50px"
OnContentVisible="OnPdfContainerVisibleAsync"
ContainerStyle="margin-top: 30px;">
<div id="pdfContainer" style="width: 100%; height: 800px; overflow-y: auto;">
</div>
</MgLazyLoadContent>
}
<MgLazyLoadContent @ref="_lazyContentRef"
MinHeight="800px"
RootMargin="50px"
OnContentVisible="OnPdfContainerVisibleAsync"
ContainerStyle="margin-top: 30px;">
<div id="pdfContainer" style="width: 100%; height: 800px; overflow-y: auto;">
</div>
</MgLazyLoadContent>
</AfterColumnsTemplate>
@* <FooterTemplate Context="ctx">
@ -108,10 +110,12 @@
private async Task OnDataItemChangedAsync(object? dataItem)
{
@if (dataItem is not ShippingDocument && dataItem is not ShippingItem) return;
// Store the PDF to render
_randomPdf = _pdfFiles[Random.Shared.Next(_pdfFiles.Length)];
_currentPdfToRender = $"_content/FruitBankHybrid.Shared/uploads/{_randomPdf}";
// If MgLazyLoadContent is already visible, render the PDF immediately
if (_lazyContentRef is { IsVisible: true })
{

View File

@ -47,7 +47,9 @@ public class MgGridBase : DxGrid, IMgGridBase
/// <inheritdoc />
public bool IsFullscreen => false;
public string LayoutStorageKey { get; }
public string AutomaticLayoutStorageKey => $"{AutoSaveLayoutName}_Master_AutoSave_{LoggedInModel.CustomerDto?.Id ?? 0}";
private string UserLayoutStorageKey => AutomaticLayoutStorageKey.Replace("_AutoSave_", "_UserSave_");
/// <inheritdoc />
public void ToggleFullscreen()
@ -141,8 +143,8 @@ public class MgGridBase : DxGrid, IMgGridBase
AutoCollapseDetailRow = true;
AutoExpandAllGroupRows = false;
PagerVisible = IsMasterGrid;
PageSize = IsMasterGrid ? (SizeMode == DevExpress.Blazor.SizeMode.Small ? 23 : 15) : 50;
PagerVisible = true; //IsMasterGrid;
PageSize = IsMasterGrid ? (SizeMode == DevExpress.Blazor.SizeMode.Small ? 20 : 15) : 10;
AllowColumnReorder = true;
AllowGroup = IsMasterGrid;
@ -204,12 +206,12 @@ public class MgGridBase : DxGrid, IMgGridBase
async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e)
{
e.Layout = await LoadLayoutFromLocalStorageAsync($"{AutoSaveLayoutName}_AutoSave_{LoggedInModel.CustomerDto?.Id ?? 0}");
e.Layout = await LoadLayoutFromLocalStorageAsync(AutomaticLayoutStorageKey);
}
private async Task Grid_LayoutAutoSaving(GridPersistentLayoutEventArgs e)
{
await SaveLayoutToLocalStorageAsync(e.Layout, $"{AutoSaveLayoutName}_AutoSave_{LoggedInModel.CustomerDto?.Id ?? 0}");
await SaveLayoutToLocalStorageAsync(e.Layout, AutomaticLayoutStorageKey);
}
async Task<GridPersistentLayout?> LoadLayoutFromLocalStorageAsync(string localStorageKey)
@ -227,6 +229,7 @@ public class MgGridBase : DxGrid, IMgGridBase
return null;
}
async Task SaveLayoutToLocalStorageAsync(GridPersistentLayout layout, string localStorageKey)
{
try
@ -243,54 +246,49 @@ public class MgGridBase : DxGrid, IMgGridBase
{
try
{
await JSRuntime.InvokeVoidAsync("localStorage.removeItem", AutoSaveLayoutName);
await JSRuntime.InvokeVoidAsync("localStorage.removeItem", AutomaticLayoutStorageKey);
}
catch
{
// Mute exceptions for the server prerender stage
}
}
async Task ReloadPageButton_ClickAsync()
/// <inheritdoc />
public async Task SaveUserLayoutAsync()
{
await JSRuntime.InvokeVoidAsync("location.reload");
var layout = SaveLayout();
await SaveLayoutToLocalStorageAsync(layout, UserLayoutStorageKey);
}
async Task ResetLayoutButton_ClickAsync()
/// <inheritdoc />
public async Task LoadUserLayoutAsync()
{
var layout = await LoadLayoutFromLocalStorageAsync(UserLayoutStorageKey);
if (layout != null)
{
LoadLayout(layout);
}
}
/// <inheritdoc />
public async Task ResetLayoutAsync()
{
await RemoveLayoutFromLocalStorageAsync();
await JSRuntime.InvokeVoidAsync("location.reload");
}
//public RenderFragment AddCommandColumn()
//{
// RenderFragment columns = b =>
// {
// if (!IsMasterGrid)
// {
// b.OpenComponent(0, typeof(DxGridCommandColumn));
// b.CloseComponent();
// }
// };
// this.Columns.ApplyChain(x) = AddCommandColumn();
// return columns;
//}
//private RenderFragment BuildColumnsGrid()
//{
// PropertyInfo[] props = DataSource.FirstOrDefault().GetType().GetProperties();
// RenderFragment columns = b =>
// {
// foreach (var prop in props)
// {
// if (prop.PropertyType == typeof(string))
// {
// b.OpenComponent(0, typeof(DxGridDataColumn));
// b.AddAttribute(0, "FieldName", prop.Name);
// b.CloseComponent();
// }
// }
// };
// return columns;
//}
/// <inheritdoc />
public async Task<bool> HasUserLayoutAsync()
{
try
{
var value = await JSRuntime.InvokeAsync<string>("localStorage.getItem", UserLayoutStorageKey);
return !string.IsNullOrWhiteSpace(value);
}
catch
{
return false;
}
}
}

View File

@ -161,10 +161,16 @@ h1:focus {
.grid-chevron-up,
.grid-chevron-down,
.grid-column-chooser,
.grid-layout,
.grid-layout-load,
.grid-layout-save,
.grid-layout-reset,
.grid-export,
.grid-export-xlsx,
.grid-export-pdf,
.grid-refresh {
.grid-refresh,
.grid-fullscreen,
.grid-fullscreen-exit {
display: inline-flex;
align-items: center;
justify-content: center;
@ -178,10 +184,16 @@ h1:focus {
.grid-chevron-up::before,
.grid-chevron-down::before,
.grid-column-chooser::before,
.grid-layout::before,
.grid-layout-load::before,
.grid-layout-save::before,
.grid-layout-reset::before,
.grid-export::before,
.grid-export-xlsx::before,
.grid-export-pdf::before,
.grid-refresh::before {
.grid-refresh::before,
.grid-fullscreen::before,
.grid-fullscreen-exit::before {
content: "";
display: inline-block;
width: 1.25rem;
@ -268,6 +280,42 @@ h1:focus {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='23 4 23 10 17 10'/%3E%3Cpolyline points='1 20 1 14 7 14'/%3E%3Cpath d='M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15'/%3E%3C/svg%3E");
}
/* Layout icon (sliders/settings) */
.grid-layout::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cline x1='4' y1='21' x2='4' y2='14'/%3E%3Cline x1='4' y1='10' x2='4' y2='3'/%3E%3Cline x1='12' y1='21' x2='12' y2='12'/%3E%3Cline x1='12' y1='8' x2='12' y2='3'/%3E%3Cline x1='20' y1='21' x2='20' y2='16'/%3E%3Cline x1='20' y1='12' x2='20' y2='3'/%3E%3Cline x1='1' y1='14' x2='7' y2='14'/%3E%3Cline x1='9' y1='8' x2='15' y2='8'/%3E%3Cline x1='17' y1='16' x2='23' y2='16'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cline x1='4' y1='21' x2='4' y2='14'/%3E%3Cline x1='4' y1='10' x2='4' y2='3'/%3E%3Cline x1='12' y1='21' x2='12' y2='12'/%3E%3Cline x1='12' y1='8' x2='12' y2='3'/%3E%3Cline x1='20' y1='21' x2='20' y2='16'/%3E%3Cline x1='20' y1='12' x2='20' y2='3'/%3E%3Cline x1='1' y1='14' x2='7' y2='14'/%3E%3Cline x1='9' y1='8' x2='15' y2='8'/%3E%3Cline x1='17' y1='16' x2='23' y2='16'/%3E%3C/svg%3E");
}
/* Layout Load icon (folder open) */
.grid-layout-load::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z'/%3E%3C/svg%3E");
}
/* Layout Save icon (save/floppy) */
.grid-layout-save::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z'/%3E%3Cpolyline points='17 21 17 13 7 13 7 21'/%3E%3Cpolyline points='7 3 7 8 15 8'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z'/%3E%3Cpolyline points='17 21 17 13 7 13 7 21'/%3E%3Cpolyline points='7 3 7 8 15 8'/%3E%3C/svg%3E");
}
/* Layout Reset icon (rotate-ccw) */
.grid-layout-reset::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='1 4 1 10 7 10'/%3E%3Cpath d='M3.51 15a9 9 0 1 0 2.13-9.36L1 10'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='1 4 1 10 7 10'/%3E%3Cpath d='M3.51 15a9 9 0 1 0 2.13-9.36L1 10'/%3E%3C/svg%3E");
}
/* Fullscreen icon (maximize) */
.grid-fullscreen::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cpolyline points='9 21 3 21 3 15'/%3E%3Cline x1='21' y1='3' x2='14' y2='10'/%3E%3Cline x1='3' y1='21' x2='10' y2='14'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='15 3 21 3 21 9'/%3E%3Cpolyline points='9 21 3 21 3 15'/%3E%3Cline x1='21' y1='3' x2='14' y2='10'/%3E%3Cline x1='3' y1='21' x2='10' y2='14'/%3E%3C/svg%3E");
}
/* Fullscreen Exit icon (minimize) */
.grid-fullscreen-exit::before {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='4 14 10 14 10 20'/%3E%3Cpolyline points='20 10 14 10 14 4'/%3E%3Cline x1='14' y1='10' x2='21' y2='3'/%3E%3Cline x1='3' y1='21' x2='10' y2='14'/%3E%3C/svg%3E");
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpolyline points='4 14 10 14 10 20'/%3E%3Cpolyline points='20 10 14 10 14 4'/%3E%3Cline x1='14' y1='10' x2='21' y2='3'/%3E%3Cline x1='3' y1='21' x2='10' y2='14'/%3E%3C/svg%3E");
}
/* ========================================
MgGrid Splitter with InfoPanel
InfoPanel sticky via JavaScript