feat(cms): extract all-releases grid as embeddable ALL-tab component (9.8.B)
CmsAllReleasesGrid self-loads the cross-medium release list so 8.A can host it as the ALL tab with no VM plumbing; TrackList's Albums mode renders it now. Preserves sort/delete/expand/edit and the 8.D Type chip.
This commit is contained in:
@@ -125,14 +125,15 @@ else
|
||||
private List<AlbumRow> _rows = new();
|
||||
|
||||
// Tracks the Releases reference last projected into _rows. Guards against OnParametersSet
|
||||
// resurrecting a row we removed locally on delete: VM.Albums is cached for the circuit and is
|
||||
// not re-fetched after a delete, so a blind rebuild every render would bring the deleted album
|
||||
// back. We only re-project when the parent hands us a genuinely new list.
|
||||
// resurrecting a row we removed locally on delete: while the parent holds the same Releases
|
||||
// instance (e.g. a mid-operation re-render under IsDeleting, before any refresh hands us a new
|
||||
// list), a blind rebuild every render would bring the deleted row back. We only re-project when
|
||||
// the parent hands us a genuinely new list.
|
||||
private IReadOnlyList<ReleaseDto>? _projectedReleases;
|
||||
|
||||
// Re-project rows only when the parent supplies a genuinely new release list (reference change).
|
||||
// Local edits to _rows (a removed row after delete) must survive re-renders triggered by the
|
||||
// same cached VM.Albums instance.
|
||||
// same Releases instance.
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!ReferenceEquals(_projectedReleases, Releases))
|
||||
@@ -245,7 +246,7 @@ else
|
||||
|
||||
// Delete an orphaned release (0 live tracks) via the release endpoint. Mirrors the track-cascade
|
||||
// delete path's row lifecycle: confirm, guard with IsDeleting, then remove the row and notify the
|
||||
// parent so the cached VM.Albums stays in sync with what is shown.
|
||||
// parent so its release list stays in sync with what is shown.
|
||||
private async Task ConfirmAndDeleteEmptyReleaseAsync(AlbumRow row)
|
||||
{
|
||||
var confirmed = await DialogService.ShowMessageBox(
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
@using DeepDrftManager.Services
|
||||
@using DeepDrftModels.DTOs
|
||||
@inject ICmsTrackService CmsTrackService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@* The ALL-tab content (Phase 9 §8.B): the cross-medium all-releases grid (CUTS, SESSIONS, MIXES
|
||||
together) with per-row edit, delete, expand-tracks, and the 8.D Type chip. Self-contained — owns its
|
||||
own data load so a host (TrackList today, the 8.A tab strip later) renders it with no parameters and
|
||||
no VM plumbing. Re-loads on first render and re-fetches after a row mutation so the list stays in
|
||||
sync with the catalogue. *@
|
||||
<CmsAlbumBrowser Releases="_releases"
|
||||
IsLoading="_loading"
|
||||
OnReleasesChanged="OnGridReleasesChanged" />
|
||||
|
||||
@code {
|
||||
// Fires after a row mutation (delete) so a host can invalidate sibling caches derived from the same
|
||||
// catalogue — e.g. TrackList's genre cache. The grid refreshes its own list regardless; this is a
|
||||
// notification, not the data source. Optional: an embed that has no sibling state leaves it unset.
|
||||
[Parameter] public EventCallback OnReleasesChanged { get; set; }
|
||||
|
||||
private IReadOnlyList<ReleaseDto> _releases = Array.Empty<ReleaseDto>();
|
||||
private bool _loading = true;
|
||||
|
||||
protected override Task OnInitializedAsync() => ReloadAsync();
|
||||
|
||||
private async Task OnGridReleasesChanged()
|
||||
{
|
||||
await ReloadAsync();
|
||||
await OnReleasesChanged.InvokeAsync();
|
||||
}
|
||||
|
||||
// Single load path: the initial fetch and the post-mutation refresh both run through here. After a
|
||||
// delete CmsAlbumBrowser has already dropped the row from its own projection, so this re-fetch
|
||||
// reconciles the authoritative list (track counts, orphaned-release cleanup) without a stale cache.
|
||||
private async Task ReloadAsync()
|
||||
{
|
||||
_loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var result = await CmsTrackService.GetReleasesAsync();
|
||||
if (result.Success && result.Value is not null)
|
||||
{
|
||||
_releases = result.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_releases = Array.Empty<ReleaseDto>();
|
||||
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
||||
Snackbar.Add($"Failed to load releases: {error}", Severity.Error);
|
||||
}
|
||||
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -54,9 +54,11 @@
|
||||
}
|
||||
else if (VM.Mode == BrowseMode.Albums)
|
||||
{
|
||||
<CmsAlbumBrowser Releases="VM.Albums"
|
||||
IsLoading="VM.AlbumsLoading"
|
||||
OnReleasesChanged="OnAlbumsChanged" />
|
||||
@* The all-releases grid is now a self-contained component (Phase 9 §8.B): it owns its own load
|
||||
and refresh, so the host renders it with no parameters. The 8.A tab strip hosts this same
|
||||
component as its ALL tab. Genre mode still uses the VM cache below; only album loading moved
|
||||
into the component, so VM.Albums / VM.AlbumsLoading are no longer read here. *@
|
||||
<CmsAllReleasesGrid OnReleasesChanged="OnAlbumsChanged" />
|
||||
}
|
||||
else if (VM.Mode == BrowseMode.Archive)
|
||||
{
|
||||
@@ -76,8 +78,8 @@
|
||||
@code {
|
||||
private CmsTrackGrid? _grid;
|
||||
|
||||
// The album browser owns its own row state and removes a deleted release locally. Invalidate the
|
||||
// VM cache so genres and album counts reflect the deletion on next mode switch.
|
||||
// The all-releases grid refreshes its own list after a delete; this notification lets us invalidate
|
||||
// the VM's genre cache so genre counts reflect the deletion on the next switch into Genre mode.
|
||||
private void OnAlbumsChanged()
|
||||
{
|
||||
VM.Invalidate();
|
||||
|
||||
@@ -28,34 +28,23 @@ public class CmsTrackBrowserViewModel
|
||||
|
||||
public BrowseMode Mode { get; private set; } = BrowseMode.Tracks;
|
||||
|
||||
// Album mode.
|
||||
public IReadOnlyList<ReleaseDto> Albums { get; private set; } = Array.Empty<ReleaseDto>();
|
||||
public bool AlbumsLoading { get; private set; }
|
||||
|
||||
// Genre mode.
|
||||
public IReadOnlyList<GenreSummaryDto> Genres { get; private set; } = Array.Empty<GenreSummaryDto>();
|
||||
public bool GenresLoading { get; private set; }
|
||||
public string? ExpandedGenre { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Switch the active mode, lazily loading the album or genre dataset on first entry. Collapses
|
||||
/// any expanded genre row. The grid in Track mode owns its own data, so no fetch happens there.
|
||||
/// Switch the active mode, lazily loading the genre dataset on first entry into Genre mode and
|
||||
/// collapsing any expanded genre row. Track mode and the all-releases grid (Albums mode) each own
|
||||
/// their own data — the grid loads itself (see <c>CmsAllReleasesGrid</c>) — so no fetch happens for
|
||||
/// either here.
|
||||
/// </summary>
|
||||
public async Task SwitchModeAsync(BrowseMode mode)
|
||||
{
|
||||
Mode = mode;
|
||||
ExpandedGenre = null; // collapse on mode switch
|
||||
|
||||
if (mode == BrowseMode.Albums && Albums.Count == 0 && !AlbumsLoading)
|
||||
{
|
||||
AlbumsLoading = true;
|
||||
var result = await _trackService.GetReleasesAsync();
|
||||
Albums = result.Success && result.Value is not null
|
||||
? result.Value
|
||||
: Array.Empty<ReleaseDto>();
|
||||
AlbumsLoading = false;
|
||||
}
|
||||
else if (mode == BrowseMode.Genres && Genres.Count == 0 && !GenresLoading)
|
||||
if (mode == BrowseMode.Genres && Genres.Count == 0 && !GenresLoading)
|
||||
{
|
||||
GenresLoading = true;
|
||||
var result = await _trackService.GetGenreSummariesAsync();
|
||||
@@ -73,13 +62,13 @@ public class CmsTrackBrowserViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop the cached album and genre datasets so the next <see cref="SwitchModeAsync"/> into
|
||||
/// either mode re-fetches from the API. Call after a track or release mutation (edit, delete)
|
||||
/// since both datasets are derived from the catalogue and go stale on any such change.
|
||||
/// Drop the cached genre dataset so the next <see cref="SwitchModeAsync"/> into Genre mode
|
||||
/// re-fetches from the API. Call after a track or release mutation (edit, delete) since the genre
|
||||
/// summaries are derived from the catalogue and go stale on any such change. The all-releases grid
|
||||
/// owns and refreshes its own data, so it needs no invalidation here.
|
||||
/// </summary>
|
||||
public void Invalidate()
|
||||
{
|
||||
Albums = Array.Empty<ReleaseDto>();
|
||||
Genres = Array.Empty<GenreSummaryDto>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user