Merge cms-w3-t4-edit: PUT api/cms/track/{id} + /cms/tracks/{id} edit page
This commit is contained in:
@@ -0,0 +1,235 @@
|
|||||||
|
@page "/cms/tracks/{Id:int}"
|
||||||
|
@* InteractiveServer: page injects ITrackService in-process; ITokenService reads localStorage via JS interop over the circuit. *@
|
||||||
|
@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
|
||||||
|
@inject ILogger<TrackEdit> Logger
|
||||||
|
|
||||||
|
<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)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Save failed for track {TrackId}", Id);
|
||||||
|
Snackbar.Add("Save failed — please try again.", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_busy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/cms/track/{Id} is handled by CmsDeleteController (T3 branch).
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Delete failed for track {TrackId}", Id);
|
||||||
|
Snackbar.Add("Delete failed — please try again.", 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,59 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
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(
|
||||||
|
[Required, MaxLength(200)] string TrackName,
|
||||||
|
[Required, MaxLength(200)] string Artist,
|
||||||
|
[MaxLength(200)] string? Album,
|
||||||
|
[MaxLength(100)] string? Genre,
|
||||||
|
DateOnly? ReleaseDate);
|
||||||
Reference in New Issue
Block a user