W3 T4: PUT api/cms/track/{id} + /cms/tracks/{id} edit page (metadata-only, Admin-gated)

This commit is contained in:
Daniel Harvey
2026-05-18 15:13:48 -04:00
parent f46c2557c8
commit 531115b655
3 changed files with 289 additions and 0 deletions
+1
View File
@@ -21,6 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
<ProjectReference Include="..\DeepDrftWeb.Services\DeepDrftWeb.Services.csproj" />
</ItemGroup>
</Project>
+230
View File
@@ -0,0 +1,230 @@
@page "/cms/tracks/{Id:int}"
@rendermode InteractiveServer
@using AuthBlocksWeb.HierarchicalAuthorize
@using AuthBlocksWeb.Services
@using DeepDrftWeb.Services
@using System.Net.Http.Headers
@using System.Net.Http.Json
@attribute [HierarchicalRoleAuthorize("Admin")]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
@inject ITokenService TokenService
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager Nav
<PageTitle>Edit Track — DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="mt-8">
<MudButton Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.ArrowBack"
Href="/cms/tracks"
Class="mb-4">
Back to tracks
</MudButton>
@if (_loading)
{
<MudProgressCircular Indeterminate="true" />
}
else if (_track is null)
{
<MudAlert Severity="Severity.Warning">
Track not found.
</MudAlert>
}
else
{
<MudText Typo="Typo.h4" GutterBottom="true">Edit Track</MudText>
<MudPaper Class="pa-6" Elevation="2">
<MudStack Spacing="4">
<MudField Label="Entry Key" Variant="Variant.Outlined" InnerPadding="false">
<MudText Typo="Typo.body1" Style="font-family: monospace;">@_track.EntryKey</MudText>
<MudText Typo="Typo.caption" Color="Color.Default">
Vault reference — not editable.
</MudText>
</MudField>
<MudTextField @bind-Value="_form.TrackName"
Label="Track Name"
Required="true"
RequiredError="Track name is required"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Artist"
Label="Artist"
Required="true"
RequiredError="Artist is required"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Album"
Label="Album"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="_form.Genre"
Label="Genre"
Variant="Variant.Outlined" />
<MudDatePicker @bind-Date="_form.ReleaseDate"
Label="Release Date"
DateFormat="yyyy-MM-dd"
Variant="Variant.Outlined" />
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween">
<MudButton Variant="Variant.Filled"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Delete"
Disabled="_busy"
OnClick="ConfirmDelete">
Delete
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Save"
Disabled="_busy || !CanSave"
OnClick="SaveAsync">
Save Changes
</MudButton>
</MudStack>
</MudStack>
</MudPaper>
}
</MudContainer>
@code {
[Parameter] public int Id { get; set; }
private TrackEntity? _track;
private TrackEditForm _form = new();
private bool _loading = true;
private bool _busy;
private bool CanSave =>
!string.IsNullOrWhiteSpace(_form.TrackName)
&& !string.IsNullOrWhiteSpace(_form.Artist);
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
var result = await TrackService.GetById(Id);
_track = result.Success ? result.Value : null;
if (_track is not null)
{
_form = TrackEditForm.From(_track);
}
_loading = false;
}
private async Task SaveAsync()
{
if (_track is null || !CanSave) return;
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var payload = new
{
TrackName = _form.TrackName,
Artist = _form.Artist,
Album = string.IsNullOrWhiteSpace(_form.Album) ? null : _form.Album,
Genre = string.IsNullOrWhiteSpace(_form.Genre) ? null : _form.Genre,
ReleaseDate = _form.ReleaseDate is { } d ? DateOnly.FromDateTime(d) : (DateOnly?)null
};
var response = await http.PutAsJsonAsync($"api/cms/track/{Id}", payload);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Track updated.", Severity.Success);
await LoadAsync();
}
else
{
Snackbar.Add($"Save failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Save failed: {ex.Message}", Severity.Error);
}
finally
{
_busy = false;
}
}
private async Task ConfirmDelete()
{
if (_track is null) return;
var confirmed = await DialogService.ShowMessageBox(
"Delete track",
$"Permanently delete \"{_track.TrackName}\" by {_track.Artist}? This cannot be undone.",
yesText: "Delete",
cancelText: "Cancel");
if (confirmed != true) return;
_busy = true;
try
{
var http = HttpClientFactory.CreateClient("DeepDrft.API");
await AttachBearerAsync(http);
var response = await http.DeleteAsync($"api/cms/track/{Id}");
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Track deleted.", Severity.Success);
Nav.NavigateTo("/cms/tracks");
}
else
{
Snackbar.Add($"Delete failed: {(int)response.StatusCode} {response.ReasonPhrase}", Severity.Error);
_busy = false;
}
}
catch (Exception ex)
{
Snackbar.Add($"Delete failed: {ex.Message}", Severity.Error);
_busy = false;
}
}
private async Task AttachBearerAsync(HttpClient http)
{
var token = await TokenService.GetAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
private sealed class TrackEditForm
{
public string TrackName { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public string? Album { get; set; }
public string? Genre { get; set; }
public DateTime? ReleaseDate { get; set; }
public static TrackEditForm From(TrackEntity track) => new()
{
TrackName = track.TrackName,
Artist = track.Artist,
Album = track.Album,
Genre = track.Genre,
ReleaseDate = track.ReleaseDate is { } d
? d.ToDateTime(TimeOnly.MinValue)
: null
};
}
}
@@ -0,0 +1,58 @@
using DeepDrftModels.Entities;
using DeepDrftWeb.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetBlocks.Models;
namespace DeepDrftWeb.Controllers;
[ApiController]
[Authorize(Roles = "Admin")]
[Route("api/cms/track")]
public class CmsEditController : ControllerBase
{
private readonly ITrackService _trackService;
public CmsEditController(ITrackService trackService)
{
_trackService = trackService;
}
// Metadata-only update. EntryKey is immutable in Wave 1 — audio replacement
// is a separate Wave 2 operation that touches the vault.
[HttpPut("{id:int}")]
public async Task<ActionResult<ApiResultDto<TrackEntity>>> Update(int id, [FromBody] CmsTrackUpdateRequest request)
{
var existing = await _trackService.GetById(id);
if (!existing.Success)
{
var failure = ApiResult<TrackEntity>.CreateFailResult(existing.GetMessage());
return StatusCode(500, new ApiResultDto<TrackEntity>(failure));
}
if (existing.Value is null)
{
return NotFound();
}
var track = existing.Value;
track.TrackName = request.TrackName;
track.Artist = request.Artist;
track.Album = request.Album;
track.Genre = request.Genre;
track.ReleaseDate = request.ReleaseDate;
var updated = await _trackService.Update(track);
var apiResult = ApiResult<TrackEntity>.From(updated);
var dto = new ApiResultDto<TrackEntity>(apiResult);
return updated.Success ? Ok(dto) : StatusCode(500, dto);
}
}
public record CmsTrackUpdateRequest(
string TrackName,
string Artist,
string? Album,
string? Genre,
DateOnly? ReleaseDate);