feat(cms): bring per-medium tab grids to ALL-tab parity (§8.C)

Render the rich CmsAlbumBrowser filtered per medium in the CUTS/SESSIONS/MIXES
tabs via an optional RowActions slot; retire the thin CmsMediumTable. Session
hero and Mix waveform actions preserved; ALL tab and TrackList unchanged.
This commit is contained in:
daniel-c-harvey
2026-06-13 22:33:31 -04:00
parent 4b9e6531fd
commit 3ef98aa3ff
8 changed files with 151 additions and 191 deletions
@@ -7,14 +7,18 @@ using MudBlazor;
namespace DeepDrftManager.Components.Pages.Tracks;
/// <summary>
/// Shared fetch + state logic for the single-track medium browsers (Sessions, Mixes). Analogous to the
/// public-site <c>MediumBrowseBase</c>: subclasses supply the <see cref="Medium"/>, the noun used in
/// error text, and a per-row projection from <see cref="ReleaseDto"/> to their own row model; this base
/// owns the loading flag, the row list, the initial load, and the cover-thumbnail URL helper. The shared
/// table structure lives in <c>CmsMediumTable</c>; subclasses render it and fill only the action column.
/// Shared fetch + state logic for the per-medium browsers (Cuts, Sessions, Mixes). Each subclass feeds
/// the rich <c>CmsAlbumBrowser</c> grid a medium-filtered release list, so the per-medium tabs gain the
/// same expand-tracks / delete / Type-chip / edit behaviour as the ALL tab without re-implementing any of
/// it (§8.C parity — reuse, don't fork). This base owns the loading flag, the medium-filtered load, the
/// per-release row projection, and a cover-thumbnail helper; subclasses supply the <see cref="Medium"/>,
/// an error noun, and their bespoke per-row action (Session hero upload, Mix waveform generate) via the
/// rich grid's <c>RowActions</c> slot, looking their action-state row up with <see cref="RowFor"/>.
/// </summary>
/// <typeparam name="TRow">The subclass's row model wrapping a <see cref="ReleaseDto"/>.</typeparam>
public abstract class CmsMediumBrowserBase<TRow> : ComponentBase
/// <typeparam name="TRow">The subclass's row model wrapping a <see cref="ReleaseDto"/> plus its
/// medium-specific action state (upload/generate flags). The rich grid renders from the bare
/// <see cref="Releases"/> projection; <typeparamref name="TRow"/> only carries the action state.</typeparam>
public abstract class CmsMediumBrowserBase<TRow> : ComponentBase where TRow : class
{
[Inject] public required ICmsReleaseService CmsReleaseService { get; set; }
[Inject] public required ISnackbar Snackbar { get; set; }
@@ -28,11 +32,38 @@ public abstract class CmsMediumBrowserBase<TRow> : ComponentBase
/// <summary>Projects a fetched release into the subclass's row model.</summary>
protected abstract TRow ToRow(ReleaseDto release);
/// <summary>The release carried by a subclass row, for keying the action-state lookup.</summary>
protected abstract ReleaseDto ReleaseOf(TRow row);
protected List<TRow> Rows { get; private set; } = new();
protected bool Loading { get; private set; } = true;
// Bare release projection handed to the rich grid. The grid does the expand/delete/edit/Type-chip;
// it never sees TRow. Rebuilt on every (re)load so the grid re-projects against a fresh reference.
protected IReadOnlyList<ReleaseDto> Releases { get; private set; } = Array.Empty<ReleaseDto>();
// release.Id → action-state row, so a RowActions fragment (which the grid hands a ReleaseDto) can
// recover its TRow. Rebuilt alongside Rows so a refresh never leaves a stale row behind.
private Dictionary<long, TRow> _rowsById = new();
protected override async Task OnInitializedAsync() => await LoadAsync();
/// <summary>Recovers the action-state row for a release the rich grid is rendering. Null if the
/// release is not in the current page (e.g. just deleted), in which case the action is skipped.</summary>
protected TRow? RowFor(ReleaseDto release) =>
_rowsById.TryGetValue(release.Id, out var row) ? row : null;
/// <summary>
/// Reloads the medium-filtered release list. Wired to the rich grid's <c>OnReleasesChanged</c> so a
/// delete re-fetches the authoritative list (track counts, orphan cleanup) — the same single-load
/// posture <c>CmsAllReleasesGrid</c> uses for the ALL tab.
/// </summary>
protected async Task ReloadAsync()
{
await LoadAsync();
StateHasChanged();
}
private async Task LoadAsync()
{
Loading = true;
@@ -53,6 +84,8 @@ public abstract class CmsMediumBrowserBase<TRow> : ComponentBase
Rows = new List<TRow>();
}
Releases = Rows.Select(ReleaseOf).ToList();
_rowsById = Rows.ToDictionary(r => ReleaseOf(r).Id);
Loading = false;
}