@page "/tracks/sessions" @inherits CmsMediumBrowserBase @using DeepDrftModels.DTOs @using DeepDrftModels.Enums @using Microsoft.AspNetCore.Components.Forms @attribute [Authorize] @inject ILogger Logger @* Embedded as the SESSIONS tab content of the Release Archive (Phase 9 §8.A), and still routable at /tracks/sessions for direct-URL access. The grid is the rich CmsAlbumBrowser filtered to Sessions (§8.C parity: expand-tracks, delete, Type chip, per-row edit), with the Session hero upload supplied as its medium-specific special-action column so that affordance survives the move off the thin table. When embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive" button) is suppressed; the standalone route keeps it. The hero affordance (9.5.E) is preserved in both contexts. *@ @if (Embedded) { @GridContent } else { Sessions — DeepDrft CMS Back to Releases Sessions @GridContent } @code { /// /// True when rendered as tab content inside the Release Archive; suppresses the standalone page /// chrome (title, container, back button). False (default) renders the full routable page. /// [Parameter] public bool Embedded { get; set; } /// /// Forwarded from the inner : fires after any per-row waveform /// generate succeeds so the parent page can refresh its catalogue-wide missing-count badges. /// [Parameter] public EventCallback OnWaveformGenerated { get; set; } private CmsAlbumBrowser? _albumBrowser; protected override ReleaseMedium Medium => ReleaseMedium.Session; protected override string MediumNoun => "sessions"; /// /// Clears the inner grid's cached per-track waveform status so the next row expand re-fetches. /// Called by the parent page after a catalogue-wide bulk run. /// public Task InvalidateWaveformStatusAsync() => _albumBrowser?.InvalidateWaveformStatusAsync() ?? Task.CompletedTask; protected override SessionRow ToRow(ReleaseDto release) => new() { Release = release, HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey }; protected override ReleaseDto ReleaseOf(SessionRow row) => row.Release; // The grid itself — identical in the embedded and standalone contexts. Defined once as a fragment so // both branches above render the same markup without duplication. The Session declares one dedicated // "Hero" special-action column; the grid renders it between Tracks and Actions, handing the cell each // release, and RowFor recovers the matching SessionRow's upload state. private RenderFragment GridContent => @; // Allocated once per component instance in OnInitialized (field initializers cannot reference // instance members, so initialization is deferred to the first lifecycle hook). private IReadOnlyList _specialColumns = Array.Empty(); protected override void OnInitialized() { _specialColumns = new[] { new SpecialActionColumn("Hero", HeroThumbCell), new SpecialActionColumn("", HeroButtonCell), }; base.OnInitialized(); } // Per-row cell for the "Hero" thumbnail column: just the image preview div. private RenderFragment HeroThumbCell => release =>@ @{ var row = RowFor(release); } @if (row is not null) { @if (row.HeroImageEntryKey is { Length: > 0 } heroKey) {
} else {
} }
; // Per-row cell for the "Hero Image" upload button column: set/replace upload button with progress. private RenderFragment HeroButtonCell => release =>@ @{ var row = RowFor(release); } @if (row is not null) { @if (row.IsUploading) { Uploading… } else { @(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero") } } ; private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file) { if (file is null) return; row.IsUploading = true; StateHasChanged(); try { await using var stream = file.OpenReadStream(maxAllowedSize: 50_000_000); var result = await CmsReleaseService.UploadSessionHeroImageAsync( row.Release.Id, stream, file.Name, file.ContentType); if (result.Success) { // The endpoint returns no payload; the entry key is server-generated. Re-fetch the // release so the hero thumbnail reflects the new key without guessing it. var refreshed = await CmsReleaseService.GetByIdAsync(row.Release.Id); if (refreshed.Success && refreshed.Value is { } release) { row.Release = release; row.HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey; } Snackbar.Add($"Hero image set for '{row.Release.Title}'.", Severity.Success); } else { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Hero image upload failed: {error}", Severity.Error); } } catch (Exception ex) { Logger.LogError(ex, "Hero image upload failed for release {ReleaseId}", row.Release.Id); Snackbar.Add("Hero image upload failed — please try again.", Severity.Error); } finally { row.IsUploading = false; StateHasChanged(); } } public sealed class SessionRow { public required ReleaseDto Release { get; set; } public string? HeroImageEntryKey { get; set; } public bool IsUploading { get; set; } } }