Compare commits
2 Commits
7322af703b
...
076162f6ee
| Author | SHA1 | Date |
|---|---|---|
|
|
076162f6ee | |
|
|
bd65d70d60 |
|
|
@ -73,22 +73,24 @@ public interface IMgGridBase : IGrid
|
|||
void ToggleFullscreen();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
Task SaveUserLayoutAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Loads layout from user storage (manual load)
|
||||
/// Loads layout from user storage (manual load); prefers the server copy when server layouts are enabled
|
||||
/// </summary>
|
||||
Task LoadUserLayoutAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Task ResetLayoutAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
Task<bool> HasUserLayoutAsync();
|
||||
}
|
||||
|
|
@ -265,6 +267,14 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
|
||||
[Inject] protected IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[Parameter] public bool EnableServerLayouts { get; set; } = true;
|
||||
|
||||
[Parameter] public TLoggerClient Logger { get; set; }
|
||||
[Parameter] public string GridName { get; set; }
|
||||
[Parameter] public IId<TId>? ParentDataItem { get; set; }
|
||||
|
|
@ -767,6 +777,25 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private async Task<string?> GetServerLayoutJsonAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await SignalRClient.GetByIdAsync<string>(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
|
||||
|
|
@ -829,13 +858,54 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
var layout = SaveLayout();
|
||||
|
||||
await SaveLayoutToLocalStorageAsync(layout, UserLayoutStorageKey);
|
||||
await SaveLayoutToLocalStorageAsync(layout, AutomaticLayoutStorageKey);
|
||||
|
||||
if (EnableServerLayouts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(layout);
|
||||
await SignalRClient.PostAsync<bool>(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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<GridPersistentLayout>(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<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
/// <inheritdoc />
|
||||
public async Task<bool> 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
|
||||
|
|
|
|||
|
|
@ -32,10 +32,30 @@ Splitter_GridOrder_Master_AutoSave_42 ← splitter pane size
|
|||
|
||||
| Method | Behavior |
|
||||
|---|---|
|
||||
| `SaveUserLayoutAsync()` | Saves current layout to both UserSave AND AutoSave keys |
|
||||
| `LoadUserLayoutAsync()` | Loads from UserSave key (if exists) |
|
||||
| `ResetLayoutAsync()` | Removes AutoSave key, restores in-memory `_defaultLayoutJson` |
|
||||
| `HasUserLayoutAsync()` | Checks if UserSave key exists in localStorage |
|
||||
| `SaveUserLayoutAsync()` | Saves to the local UserSave key first (the AutoSave key is already current — `LayoutAutoSaving` fires on every user-made layout change), then (when `EnableServerLayouts`) pushes the layout JSON to the server |
|
||||
| `LoadUserLayoutAsync()` | Prefers the server copy (when `EnableServerLayouts`) — writing it back to the local UserSave + AutoSave keys so it survives a page refresh — and falls back to the local UserSave copy; then applies the layout |
|
||||
| `ResetLayoutAsync()` | Removes AutoSave key, restores in-memory `_defaultLayoutJson` (never touches the server copy) |
|
||||
| `HasUserLayoutAsync()` | Local check; when `EnableServerLayouts` and no local copy exists, returns `true` optimistically — no server round-trip |
|
||||
|
||||
## Server Roaming (UserSave tier)
|
||||
|
||||
`EnableServerLayouts` (default `true`) roams the **UserSave** tier through the grid's own `SignalRClient`,
|
||||
using the framework tags defined in `AcSignalRTags` (AyCode.Services):
|
||||
|
||||
| Tag | Value | Params (in order) | Returns |
|
||||
|---|---|---|---|
|
||||
| `GetGridLayoutTag` | 90010 | `key` (string), `userId` (int) | layout JSON, or null |
|
||||
| `SaveGridLayoutTag` | 90011 | `key`, `layoutJson`, `userId` | bool |
|
||||
| `DeleteGridLayoutTag` | 90012 | `key`, `userId` | bool |
|
||||
|
||||
- The `key` is the full composite UserSave storage key (it already contains the user id); the server treats
|
||||
it as an opaque string. `userId` comes from `GetLayoutUserId()`.
|
||||
- **Server communication happens exclusively on user clicks** (Save/Load layout toolbar buttons) — never on
|
||||
grid init, toolbar init, detail-row expand, or prerender.
|
||||
- All server calls are **best-effort** (`try/catch`): offline, or when the server does not implement the
|
||||
tags, behavior degrades to the local-only flow without errors.
|
||||
- The server must implement the three tags; the storage schema is the consumer's choice.
|
||||
- The `AutoSave` and `Splitter` tiers never touch the server.
|
||||
|
||||
## Persisted State
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ These are bundled into a `SignalRCrudTags` during `OnInitializedAsync`. See `AyC
|
|||
| `Caption` | `string` | `typeof(TDataItem).Name` | Grid title (shown in fullscreen header) |
|
||||
| `GridName` | `string` | `"{TDataItem.Name}Grid"` | Name used in log messages |
|
||||
| `AutoSaveLayoutName` | `string?` | `"Grid{TDataItem.Name}"` | Base name for layout storage keys |
|
||||
| `EnableServerLayouts` | `bool` | `true` | Roams the UserSave layout tier via the SignalR server endpoint (see `MGGRID_LAYOUT.md` → Server Roaming). Server calls happen only on the Save/Load layout buttons. |
|
||||
|
||||
## Event Callbacks
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ The public contract exposed to companion components (toolbar, InfoPanel, wrapper
|
|||
| `SaveUserLayoutAsync()` | `Task` | Save layout manually |
|
||||
| `LoadUserLayoutAsync()` | `Task` | Load manually saved layout |
|
||||
| `ResetLayoutAsync()` | `Task` | Reset to default layout |
|
||||
| `HasUserLayoutAsync()` | `Task<bool>` | Check if manual save exists |
|
||||
| `HasUserLayoutAsync()` | `Task<bool>` | Check if manual save exists (optimistic `true` when server layouts are enabled) |
|
||||
|
||||
## Event Args Classes
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# AyCode.Blazor — Consumer Workspace Map
|
||||
|
||||
> **Bounded consumer-awareness exception** to the Framework-First Design Principle (LLMP-DEC-62): this single file lists which higher-layer repos consume `AyCode.Blazor` (Layer 1 UI framework). For change-impact assessment when modifying ACBLAZOR — NOT a coupling vector.
|
||||
>
|
||||
> **Strict rule**: AyCode.Blazor code, types, namespaces, topic IDs, and any other `.md` files MUST NOT name any specific consumer listed here. **This is the SINGLE place** in ACBLAZOR that may name consumer repos.
|
||||
|
||||
## Workspace consumer map (as of 2026-07-08)
|
||||
|
||||
| Prefix | Repo | Layer | Type | Path (workspace-relative) | Brief usage |
|
||||
|------------|--------------------|------:|-----------------|-------------------------------|---------------------------------------------------------------------------|
|
||||
| `FBANKAPP` | FruitBankHybridApp | 2 | product (hybrid)| `Mango/Source/FruitBankHybridApp` | MAUI/Blazor hybrid client — MgGrid + SignalR datasource stack |
|
||||
| `GIAAPP` | GiaApp | 2 | product (web) | `Mango/Source/GiaApp` | Blazor Server app — MgGrid with server-side data (no SignalR datasource) |
|
||||
|
||||
(See `AyCode.Core/.github/REPO_PREFIXES.md` for the prefix scheme; each consumer's own `.github/copilot-instructions.md` `@repo` block declares its `prefix` value.)
|
||||
|
||||
## When modifying AyCode.Blazor — change-impact lens
|
||||
|
||||
- A breaking change to `MgGridBase` / `MgGridWithInfoPanel` affects both consumers above.
|
||||
- A change to `MgGridSignalRDataSource` affects only SignalR-datasource consumers (FBANKAPP; GIAAPP binds server-side data directly).
|
||||
- DevExpress version bumps cascade to every consumer (license + API compatibility check per consumer).
|
||||
|
||||
**Do NOT** use this list to write framework code that conditionalizes on specific consumers. Generic abstractions only. The list is a **HEURISTIC for impact**, not an **INPUT to logic**.
|
||||
|
||||
## Maintenance
|
||||
|
||||
When a repo joins or leaves the workspace consuming AyCode.Blazor, update the table above. The list ITSELF is workspace-meta-tooling — not framework code domain.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- **Decision Log entries**: LLMP-DEC-62 (CONSUMERS.md pattern introduction); LLMP-DEC-69 (GiaApp join).
|
||||
- **AyCode.Core's consumer map**: `AyCode.Core/CONSUMERS.md`.
|
||||
Loading…
Reference in New Issue