diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor index ebaabc9..0fec963 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor @@ -125,14 +125,15 @@ else private List _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? _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( diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor b/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor new file mode 100644 index 0000000..5e37b0b --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/CmsAllReleasesGrid.razor @@ -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. *@ + + +@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 _releases = Array.Empty(); + 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(); + var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + Snackbar.Add($"Failed to load releases: {error}", Severity.Error); + } + + _loading = false; + StateHasChanged(); + } +} diff --git a/DeepDrftManager/Components/Pages/Tracks/TrackList.razor b/DeepDrftManager/Components/Pages/Tracks/TrackList.razor index 5fd0acb..0bf6eb0 100644 --- a/DeepDrftManager/Components/Pages/Tracks/TrackList.razor +++ b/DeepDrftManager/Components/Pages/Tracks/TrackList.razor @@ -54,9 +54,11 @@ } else if (VM.Mode == BrowseMode.Albums) { - + @* 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. *@ + } 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(); diff --git a/DeepDrftManager/Services/CmsTrackBrowserViewModel.cs b/DeepDrftManager/Services/CmsTrackBrowserViewModel.cs index 89c2d5f..d457982 100644 --- a/DeepDrftManager/Services/CmsTrackBrowserViewModel.cs +++ b/DeepDrftManager/Services/CmsTrackBrowserViewModel.cs @@ -28,34 +28,23 @@ public class CmsTrackBrowserViewModel public BrowseMode Mode { get; private set; } = BrowseMode.Tracks; - // Album mode. - public IReadOnlyList Albums { get; private set; } = Array.Empty(); - public bool AlbumsLoading { get; private set; } - // Genre mode. public IReadOnlyList Genres { get; private set; } = Array.Empty(); public bool GenresLoading { get; private set; } public string? ExpandedGenre { get; private set; } /// - /// 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 CmsAllReleasesGrid) — so no fetch happens for + /// either here. /// 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(); - 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 } /// - /// Drop the cached album and genre datasets so the next 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 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. /// public void Invalidate() { - Albums = Array.Empty(); Genres = Array.Empty(); } }