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();
|
private List<AlbumRow> _rows = new();
|
||||||
|
|
||||||
// Tracks the Releases reference last projected into _rows. Guards against OnParametersSet
|
// 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
|
// resurrecting a row we removed locally on delete: while the parent holds the same Releases
|
||||||
// not re-fetched after a delete, so a blind rebuild every render would bring the deleted album
|
// instance (e.g. a mid-operation re-render under IsDeleting, before any refresh hands us a new
|
||||||
// back. We only re-project when the parent hands us a genuinely new list.
|
// 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;
|
private IReadOnlyList<ReleaseDto>? _projectedReleases;
|
||||||
|
|
||||||
// Re-project rows only when the parent supplies a genuinely new release list (reference change).
|
// 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
|
// 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()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(_projectedReleases, Releases))
|
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 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
|
// 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)
|
private async Task ConfirmAndDeleteEmptyReleaseAsync(AlbumRow row)
|
||||||
{
|
{
|
||||||
var confirmed = await DialogService.ShowMessageBox(
|
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)
|
else if (VM.Mode == BrowseMode.Albums)
|
||||||
{
|
{
|
||||||
<CmsAlbumBrowser Releases="VM.Albums"
|
@* The all-releases grid is now a self-contained component (Phase 9 §8.B): it owns its own load
|
||||||
IsLoading="VM.AlbumsLoading"
|
and refresh, so the host renders it with no parameters. The 8.A tab strip hosts this same
|
||||||
OnReleasesChanged="OnAlbumsChanged" />
|
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)
|
else if (VM.Mode == BrowseMode.Archive)
|
||||||
{
|
{
|
||||||
@@ -76,8 +78,8 @@
|
|||||||
@code {
|
@code {
|
||||||
private CmsTrackGrid? _grid;
|
private CmsTrackGrid? _grid;
|
||||||
|
|
||||||
// The album browser owns its own row state and removes a deleted release locally. Invalidate the
|
// The all-releases grid refreshes its own list after a delete; this notification lets us invalidate
|
||||||
// VM cache so genres and album counts reflect the deletion on next mode switch.
|
// the VM's genre cache so genre counts reflect the deletion on the next switch into Genre mode.
|
||||||
private void OnAlbumsChanged()
|
private void OnAlbumsChanged()
|
||||||
{
|
{
|
||||||
VM.Invalidate();
|
VM.Invalidate();
|
||||||
|
|||||||
@@ -28,34 +28,23 @@ public class CmsTrackBrowserViewModel
|
|||||||
|
|
||||||
public BrowseMode Mode { get; private set; } = BrowseMode.Tracks;
|
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.
|
// Genre mode.
|
||||||
public IReadOnlyList<GenreSummaryDto> Genres { get; private set; } = Array.Empty<GenreSummaryDto>();
|
public IReadOnlyList<GenreSummaryDto> Genres { get; private set; } = Array.Empty<GenreSummaryDto>();
|
||||||
public bool GenresLoading { get; private set; }
|
public bool GenresLoading { get; private set; }
|
||||||
public string? ExpandedGenre { get; private set; }
|
public string? ExpandedGenre { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Switch the active mode, lazily loading the album or genre dataset on first entry. Collapses
|
/// Switch the active mode, lazily loading the genre dataset on first entry into Genre mode and
|
||||||
/// any expanded genre row. The grid in Track mode owns its own data, so no fetch happens there.
|
/// 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>
|
/// </summary>
|
||||||
public async Task SwitchModeAsync(BrowseMode mode)
|
public async Task SwitchModeAsync(BrowseMode mode)
|
||||||
{
|
{
|
||||||
Mode = mode;
|
Mode = mode;
|
||||||
ExpandedGenre = null; // collapse on mode switch
|
ExpandedGenre = null; // collapse on mode switch
|
||||||
|
|
||||||
if (mode == BrowseMode.Albums && Albums.Count == 0 && !AlbumsLoading)
|
if (mode == BrowseMode.Genres && Genres.Count == 0 && !GenresLoading)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
GenresLoading = true;
|
GenresLoading = true;
|
||||||
var result = await _trackService.GetGenreSummariesAsync();
|
var result = await _trackService.GetGenreSummariesAsync();
|
||||||
@@ -73,13 +62,13 @@ public class CmsTrackBrowserViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Drop the cached album and genre datasets so the next <see cref="SwitchModeAsync"/> into
|
/// Drop the cached genre dataset so the next <see cref="SwitchModeAsync"/> into Genre mode
|
||||||
/// either mode re-fetches from the API. Call after a track or release mutation (edit, delete)
|
/// re-fetches from the API. Call after a track or release mutation (edit, delete) since the genre
|
||||||
/// since both datasets are derived from the catalogue and go stale on any such change.
|
/// 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>
|
/// </summary>
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
{
|
{
|
||||||
Albums = Array.Empty<ReleaseDto>();
|
|
||||||
Genres = Array.Empty<GenreSummaryDto>();
|
Genres = Array.Empty<GenreSummaryDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user