Add Edit action to medium browsers; extract CmsMediumBrowserBase + CmsMediumTable

Session/Mix browsers share base (load/state/thumb) and a shared table shell carrying the per-row Edit link to BatchEdit; subclasses supply only their medium action.
This commit is contained in:
daniel-c-harvey
2026-06-13 11:08:43 -04:00
parent ea018beb3e
commit a7e2335c20
7 changed files with 245 additions and 226 deletions
@@ -1,13 +1,10 @@
@page "/tracks/sessions"
@using DeepDrftManager.Services
@inherits CmsMediumBrowserBase<CmsSessionBrowser.SessionRow>
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@using Microsoft.AspNetCore.Components.Forms
@attribute [Authorize]
@inject ICmsReleaseService CmsReleaseService
@inject ISnackbar Snackbar
@inject ILogger<CmsSessionBrowser> Logger
@inject NavigationManager Navigation
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
@@ -21,112 +18,56 @@
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
@if (_loading)
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
else if (_rows.Count == 0)
{
<MudText Typo="Typo.body1" Class="mt-4">No sessions found.</MudText>
}
else
{
<MudTable T="SessionRow" Items="_rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
<HeaderContent>
<MudTh Style="width: 1%;">Cover</MudTh>
<MudTh Style="width: 1%;">Hero</MudTh>
<MudTh>Session</MudTh>
<MudTh>Artist</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Cover">
@if (!string.IsNullOrEmpty(context.Release.ImagePath))
{
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(context.Release.ImagePath)');"></div>
}
else
{
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
}
</MudTd>
<MudTd DataLabel="Hero">
@if (context.HeroImageEntryKey is { Length: > 0 } heroKey)
{
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
}
else
{
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
}
</MudTd>
<MudTd DataLabel="Session">@context.Release.Title</MudTd>
<MudTd DataLabel="Artist">@context.Release.Artist</MudTd>
<MudTd DataLabel="Actions">
<MudFileUpload T="IBrowserFile"
Accept="image/*"
FilesChanged="@(file => UploadHeroAsync(context, file))"
Disabled="@context.IsUploading">
<ActivatorContent>
<MudButton Variant="Variant.Outlined"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Image"
Disabled="@context.IsUploading">
@if (context.IsUploading)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Uploading…</span>
}
else
{
<span>@(context.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
}
</MudButton>
</ActivatorContent>
</MudFileUpload>
</MudTd>
</RowTemplate>
</MudTable>
}
<CmsMediumTable TRow="SessionRow"
Rows="Rows"
Loading="Loading"
ReleaseAccessor="@(row => row.Release)"
ThumbUrl="@(key => ThumbUrl(key))"
TitleHeader="Session"
EmptyMessage="No sessions found.">
<ActionContent Context="row">
@if (row.HeroImageEntryKey is { Length: > 0 } heroKey)
{
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
}
else
{
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
}
<MudFileUpload T="IBrowserFile"
Accept="image/*"
FilesChanged="@(file => UploadHeroAsync(row, file))"
Disabled="@row.IsUploading">
<ActivatorContent>
<MudButton Variant="Variant.Outlined"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Image"
Disabled="@row.IsUploading">
@if (row.IsUploading)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Uploading…</span>
}
else
{
<span>@(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
}
</MudButton>
</ActivatorContent>
</MudFileUpload>
</ActionContent>
</CmsMediumTable>
</MudContainer>
@code {
private List<SessionRow> _rows = new();
private bool _loading = true;
protected override ReleaseMedium Medium => ReleaseMedium.Session;
protected override string MediumNoun => "sessions";
protected override async Task OnInitializedAsync() => await LoadAsync();
private async Task LoadAsync()
protected override SessionRow ToRow(ReleaseDto release) => new()
{
_loading = true;
// Sessions are single-track releases; a single generous page covers the CMS catalogue (same
// small-catalogue assumption the album browser makes).
var result = await CmsReleaseService.GetPagedAsync(
ReleaseMedium.Session, page: 1, pageSize: 100,
sortColumn: "Title", sortDescending: false);
if (result.Success && result.Value is not null)
{
_rows = result.Value.Items
.Select(r => new SessionRow
{
Release = r,
HeroImageEntryKey = r.SessionMetadata?.HeroImageEntryKey
})
.ToList();
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Failed to load sessions: {error}", Severity.Error);
_rows = new List<SessionRow>();
}
_loading = false;
}
// Relative path — resolves against the Manager's own origin, proxied by ImageProxyController.
private static string ThumbUrl(string entryKey) =>
$"/api/image/{Uri.EscapeDataString(entryKey)}";
Release = release,
HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey
};
private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file)
{
@@ -169,7 +110,7 @@
}
}
private sealed class SessionRow
public sealed class SessionRow
{
public required ReleaseDto Release { get; set; }
public string? HeroImageEntryKey { get; set; }