diff --git a/DeepDrftCms/CmsStartup.cs b/DeepDrftCms/CmsStartup.cs index 44861e1..1b80271 100644 --- a/DeepDrftCms/CmsStartup.cs +++ b/DeepDrftCms/CmsStartup.cs @@ -7,6 +7,11 @@ public static class CmsStartup public static IServiceCollection AddCmsServices(this IServiceCollection services) { // CMS-specific services registered here as implementation waves land. + // + // Note: ITrackService (and its dependencies: TrackRepository, DeepDrftContext) is registered + // by DeepDrftWeb.Startup.ConfigureDomainServices, not here. The CMS RCL runs inside the + // DeepDrftWeb host, which wires those up before this method is called. A standalone CMS + // host would need to register ITrackService separately. return services; } } diff --git a/DeepDrftCms/DeepDrftCms.csproj b/DeepDrftCms/DeepDrftCms.csproj index 65b0747..c484fdc 100644 --- a/DeepDrftCms/DeepDrftCms.csproj +++ b/DeepDrftCms/DeepDrftCms.csproj @@ -21,6 +21,7 @@ + diff --git a/DeepDrftCms/Pages/Tracks/TrackList.razor b/DeepDrftCms/Pages/Tracks/TrackList.razor new file mode 100644 index 0000000..a7f66d4 --- /dev/null +++ b/DeepDrftCms/Pages/Tracks/TrackList.razor @@ -0,0 +1,141 @@ +@page "/cms/tracks" +@rendermode InteractiveServer +@using System.Net +@using System.Net.Http.Headers +@using AuthBlocksWeb.HierarchicalAuthorize +@using DeepDrftModels.Models +@attribute [HierarchicalRoleAuthorize("Admin")] +@inject ITrackService TrackService +@inject IHttpClientFactory HttpClientFactory +@inject AuthBlocksWeb.Services.ITokenService TokenService +@inject IDialogService DialogService +@inject ISnackbar Snackbar +@inject ILogger Logger + +Tracks — DeepDrft CMS + + + + Tracks + + Add Track + + + + + + No tracks found. + + + Loading tracks… + + + Track Name + Artist + Album + Genre + Release Date + Entry Key + Actions + + + @context.TrackName + @context.Artist + @(context.Album ?? "—") + @(context.Genre ?? "—") + @(context.ReleaseDate?.ToString("yyyy-MM-dd") ?? "—") + @context.EntryKey + + + + + + + + + + + + + + + +@code { + private MudTable? _table; + + private async Task> LoadServerData(TableState state, CancellationToken cancellationToken) + { + var pageNumber = state.Page + 1; // MudTable is 0-based, service is 1-based. + var sortColumn = string.IsNullOrEmpty(state.SortLabel) ? "TrackName" : state.SortLabel; + var sortDescending = state.SortDirection == SortDirection.Descending; + + var result = await TrackService.GetPaged(pageNumber, state.PageSize, sortColumn, sortDescending, cancellationToken); + + if (!result.Success || result.Value is null) + { + var errorText = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; + Snackbar.Add($"Failed to load tracks: {errorText}", Severity.Error); + return new TableData { Items = Array.Empty(), TotalItems = 0 }; + } + + var page = result.Value; + return new TableData + { + Items = page.Items, + TotalItems = page.TotalCount + }; + } + + private async Task ConfirmAndDelete(TrackEntity track) + { + var confirmed = await DialogService.ShowMessageBox( + title: "Delete track", + markupMessage: new MarkupString($"Delete {WebUtility.HtmlEncode(track.TrackName)} by {WebUtility.HtmlEncode(track.Artist)}? This removes both the metadata row and the underlying audio entry."), + yesText: "Delete", + cancelText: "Cancel"); + + if (confirmed != true) return; + + try + { + var client = HttpClientFactory.CreateClient("DeepDrft.API"); + var token = await TokenService.GetAccessTokenAsync(); + if (!string.IsNullOrEmpty(token)) + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await client.DeleteAsync($"api/cms/track/{track.Id}"); + + if (response.IsSuccessStatusCode) + { + Snackbar.Add($"Deleted '{track.TrackName}'.", Severity.Success); + if (_table is not null) await _table.ReloadServerData(); + } + else + { + Snackbar.Add($"Delete failed ({(int)response.StatusCode} {response.ReasonPhrase}).", Severity.Error); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Delete failed for track {TrackId}", track.Id); + Snackbar.Add("Delete failed — please try again.", Severity.Error); + } + } +} diff --git a/DeepDrftCms/_Imports.razor b/DeepDrftCms/_Imports.razor index cf3d836..edc98a0 100644 --- a/DeepDrftCms/_Imports.razor +++ b/DeepDrftCms/_Imports.razor @@ -5,7 +5,9 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.Extensions.Logging @using Microsoft.JSInterop @using DeepDrftCms @using DeepDrftModels.Entities +@using DeepDrftWeb.Services @using MudBlazor diff --git a/DeepDrftWeb.Services/ITrackService.cs b/DeepDrftWeb.Services/ITrackService.cs index 93184a5..6d387ef 100644 --- a/DeepDrftWeb.Services/ITrackService.cs +++ b/DeepDrftWeb.Services/ITrackService.cs @@ -8,7 +8,7 @@ public interface ITrackService { Task> GetById(long id); Task>> GetAll(); - Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending); + Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default); Task> Create(TrackEntity newTrack); Task> Update(TrackEntity track); Task Delete(long id); diff --git a/DeepDrftWeb.Services/Repositories/TrackRepository.cs b/DeepDrftWeb.Services/Repositories/TrackRepository.cs index d4cb123..8399d4e 100644 --- a/DeepDrftWeb.Services/Repositories/TrackRepository.cs +++ b/DeepDrftWeb.Services/Repositories/TrackRepository.cs @@ -24,12 +24,12 @@ public class TrackRepository return await _db.Tracks.ToListAsync(); } - public async Task> GetPage(PagingParameters pageParameters) + public async Task> GetPage(PagingParameters pageParameters, CancellationToken cancellationToken = default) { // Two separate queries with no transaction: count and page can be momentarily inconsistent // under concurrent writes. Acceptable — write volume is low and the UI is read-only. // If filtering is added, the count query must be updated to apply the same filter. - var count = await _db.Tracks.CountAsync(); + var count = await _db.Tracks.CountAsync(cancellationToken); var orderBy = pageParameters.OrderBy ?? (t => t.Id); var ordered = pageParameters.IsDescending @@ -39,7 +39,7 @@ public class TrackRepository var page = await ordered .Skip(pageParameters.Skip) .Take(pageParameters.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new PagedResult(page, count, pageParameters.Page, pageParameters.PageSize); } diff --git a/DeepDrftWeb.Services/TrackService.cs b/DeepDrftWeb.Services/TrackService.cs index fe6015d..e1cd6ad 100644 --- a/DeepDrftWeb.Services/TrackService.cs +++ b/DeepDrftWeb.Services/TrackService.cs @@ -41,7 +41,7 @@ public class TrackService : ITrackService } } - public async Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending) + public async Task>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default) { try { @@ -75,7 +75,7 @@ public class TrackService : ITrackService } } - var page = await _repository.GetPage(parameters); + var page = await _repository.GetPage(parameters, cancellationToken); return ResultContainer>.CreatePassResult(page); } catch (Exception e)