diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 93c21c9..4f95af2 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -26,7 +26,7 @@ public interface IMgGridBase : IGrid bool IsSyncing { get; } string Caption { get; set; } - + /// /// Current edit state of the grid (None, New, Edit) /// @@ -51,7 +51,7 @@ public interface IMgGridBase : IGrid /// Navigates to the next row in the grid /// void StepNextRow(); - + /// /// InfoPanel instance for displaying row details (from wrapper) /// @@ -73,22 +73,24 @@ public interface IMgGridBase : IGrid void ToggleFullscreen(); /// - /// Saves the current layout to user storage (manual save) + /// Saves the current layout to user storage (manual save); roams it to the server when server layouts are enabled /// Task SaveUserLayoutAsync(); /// - /// Loads layout from user storage (manual load) + /// Loads layout from user storage (manual load); prefers the server copy when server layouts are enabled /// Task LoadUserLayoutAsync(); /// - /// Resets the layout by clearing auto-saved layout and reloading the page + /// Resets the layout by clearing the auto-saved layout and restoring the default layout. + /// Saved copies (local UserSave and server) are not touched — Load brings them back. /// Task ResetLayoutAsync(); /// - /// Checks if a user-saved layout exists without loading it + /// Checks if a user-saved layout exists locally without loading it; + /// returns true optimistically when server layouts are enabled (no server round-trip) /// Task HasUserLayoutAsync(); } @@ -153,7 +155,7 @@ public abstract class MgGridBase /// InfoPanel instance for displaying row details. /// First checks own wrapper, then gets InfoPanel from root grid. @@ -165,16 +167,16 @@ public abstract class MgGridBase public bool IsFullscreen => GridWrapper?.IsFullscreen ?? _isStandaloneFullscreen; @@ -195,7 +197,7 @@ public abstract class MgGridBase + /// When true, the UserSave layout tier also roams via the SignalR server endpoint + /// (AcSignalRTags.Get/Save/DeleteGridLayoutTag): SaveUserLayoutAsync pushes to the server after the + /// local save, LoadUserLayoutAsync prefers the server copy. The server must implement the tags. + /// Server communication happens exclusively on user clicks (Save/Load). + /// + [Parameter] public bool EnableServerLayouts { get; set; } = true; + [Parameter] public TLoggerClient Logger { get; set; } [Parameter] public string GridName { get; set; } [Parameter] public IId? ParentDataItem { get; set; } @@ -272,7 +282,7 @@ public abstract class MgGridBase /// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}" /// @@ -360,7 +370,7 @@ public abstract class MgGridBase? referenceList) { _dataSource?.SetWorkingReferenceList(referenceList); - + SetGridData(referenceList); } @@ -391,21 +401,21 @@ public abstract class MgGridBase SetGridData(_dataSource!.GetReferenceInnerList())); @@ -498,7 +508,7 @@ public abstract class MgGridBase typeof(TDataItem).GetProperty(propertyName); - + protected virtual async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) { var editModel = (e.EditModel as TDataItem)!; @@ -546,11 +556,11 @@ public abstract class MgGridBase + /// Reads the UserSave layout JSON from the server endpoint (roaming). Best-effort: returns null when + /// the server has no copy or is unreachable — callers fall back to localStorage. + /// + private async Task GetServerLayoutJsonAsync() + { + try + { + return await SignalRClient.GetByIdAsync(AcSignalRTags.GetGridLayoutTag, [UserLayoutStorageKey, GetLayoutUserId()]); + } + catch (Exception ex) + { + // Server roaming is best-effort — fall back to the local copy + Logger.Warning($"[{GetType().Name}] Server layout GET failed — falling back to the local copy; key: {UserLayoutStorageKey}; error: {ex.Message}"); + } + + return null; + } + private async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e) { // Save the default layout before loading any saved layout @@ -786,7 +815,7 @@ public abstract class MgGridBase(json); } catch @@ -829,13 +858,54 @@ public abstract class MgGridBase(AcSignalRTags.SaveGridLayoutTag, [UserLayoutStorageKey, json, GetLayoutUserId()]); + } + catch (Exception ex) + { + // Server roaming is best-effort — the local copy is already persisted + Logger.Warning($"[{GetType().Name}] Server layout SAVE failed — the local copy is persisted; key: {UserLayoutStorageKey}; error: {ex.Message}"); + } + } } /// public async Task LoadUserLayoutAsync() { - var layout = await LoadLayoutFromLocalStorageAsync(UserLayoutStorageKey); + GridPersistentLayout? layout = null; + + if (EnableServerLayouts) + { + var json = await GetServerLayoutJsonAsync(); + + if (!json.IsNullOrWhiteSpace()) + { + try + { + layout = JsonSerializer.Deserialize(json!); + + if (layout != null) + { + await SaveLayoutToLocalStorageAsync(layout, UserLayoutStorageKey); + await SaveLayoutToLocalStorageAsync(layout, AutomaticLayoutStorageKey); + } + } + catch (Exception ex) + { + // Corrupt server copy — fall back to the local one + Logger.Warning($"[{GetType().Name}] Server layout copy could not be applied — falling back to the local copy; key: {UserLayoutStorageKey}; error: {ex.Message}"); + } + } + } + + // Fallback: local UserSave copy (offline, or no server copy yet) + layout ??= await LoadLayoutFromLocalStorageAsync(UserLayoutStorageKey); + if (layout != null) { LoadLayout(layout); @@ -860,7 +930,13 @@ public abstract class MgGridBase public async Task HasUserLayoutAsync() { - return !(await GetStorageItem(UserLayoutStorageKey)).IsNullOrWhiteSpace(); + // Local check only — server communication happens exclusively on user clicks (Save/Load) + if (!(await GetStorageItem(UserLayoutStorageKey)).IsNullOrWhiteSpace()) + return true; + + // With server layouts enabled we can't know without a round-trip — report true optimistically; + // LoadUserLayoutAsync no-ops when neither the server nor localStorage has a copy + return EnableServerLayouts; } #endregion @@ -876,7 +952,7 @@ public abstract class MgGridBase` | Check if manual save exists | +| `HasUserLayoutAsync()` | `Task` | Check if manual save exists (optimistic `true` when server layouts are enabled) | ## Event Args Classes