From 6e2e0e18583f60c947a4e8d896801ce93b1b646d Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 9 Jul 2026 07:52:14 +0200 Subject: [PATCH] feat(api): optional refresh param on GET /api/graphics-elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review SHOULD-FIX (#145): the SPA graphics picker could go stale because Blazor ran RefreshGraphicsElements (disk->DB sync) before listing, while the API endpoint never refreshed — a newly added .yml would not appear. GET /api/graphics-elements?refresh=true now sends RefreshGraphicsElements before the list query; default false leaves existing callers untouched. The playback troubleshooting screen passes refresh=true. Controller tests cover refresh-iff-true ordering; regenerated OpenAPI v1.json (endpoint index and generated TS schemas unchanged - query params are not part of either). Co-Authored-By: Claude Fable 5 --- .../GraphicsElementControllerTests.cs | 30 +++++++++++++++++-- .../Api/GraphicsElementController.cs | 16 ++++++++-- ErsatzTV/wwwroot/openapi/v1.json | 10 +++++++ web/src/api/pickers.test.ts | 29 ++++++++++++++++++ web/src/api/pickers.ts | 7 +++-- 5 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 web/src/api/pickers.test.ts diff --git a/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs b/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs index 8a366e9d4..55bdff023 100644 --- a/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs +++ b/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs @@ -45,7 +45,7 @@ public class GraphicsElementControllerTests _mediator.Send(Arg.Any(), Arg.Any()) .Returns(models); - List result = await _controller.GetAll(CancellationToken.None); + List result = await _controller.GetAll(refresh: false, CancellationToken.None); result.ShouldBe(models); } @@ -56,8 +56,34 @@ public class GraphicsElementControllerTests _mediator.Send(Arg.Any(), Arg.Any()) .Returns([]); - List result = await _controller.GetAll(CancellationToken.None); + List result = await _controller.GetAll(refresh: false, CancellationToken.None); result.ShouldBeEmpty(); } + + [Test] + public async Task GetAll_Should_Not_Refresh_When_Refresh_Is_False() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([]); + + await _controller.GetAll(refresh: false, CancellationToken.None); + + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task GetAll_Should_Refresh_Before_Listing_When_Refresh_Is_True() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([]); + + await _controller.GetAll(refresh: true, CancellationToken.None); + + Received.InOrder(() => + { + _mediator.Send(Arg.Any(), Arg.Any()); + _mediator.Send(Arg.Any(), Arg.Any()); + }); + } } diff --git a/ErsatzTV/Controllers/Api/GraphicsElementController.cs b/ErsatzTV/Controllers/Api/GraphicsElementController.cs index 7196f1f14..3496d7d96 100644 --- a/ErsatzTV/Controllers/Api/GraphicsElementController.cs +++ b/ErsatzTV/Controllers/Api/GraphicsElementController.cs @@ -12,8 +12,20 @@ public class GraphicsElementController(IMediator mediator) : ControllerBase [HttpGet("/api/graphics-elements", Name = "GetGraphicsElements")] [Tags("Graphics Elements")] [EndpointSummary("Get all graphics elements")] + [EndpointDescription( + "Returns all graphics elements. Pass refresh=true to first re-sync the on-disk graphics element " + + "definitions into the database (matching the legacy Blazor behavior) so newly added files appear.")] [EndpointGroupName("general")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - public async Task> GetAll(CancellationToken cancellationToken) => - await mediator.Send(new GetAllGraphicsElementsForApi(), cancellationToken); + public async Task> GetAll( + [FromQuery] bool refresh, + CancellationToken cancellationToken) + { + if (refresh) + { + await mediator.Send(new RefreshGraphicsElements(), cancellationToken); + } + + return await mediator.Send(new GetAllGraphicsElementsForApi(), cancellationToken); + } } diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index 5008c67ab..669f39879 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -4874,7 +4874,17 @@ "Graphics Elements" ], "summary": "Get all graphics elements", + "description": "Returns all graphics elements. Pass refresh=true to first re-sync the on-disk graphics element definitions into the database (matching the legacy Blazor behavior) so newly added files appear.", "operationId": "GetGraphicsElements", + "parameters": [ + { + "name": "refresh", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { "description": "OK", diff --git a/web/src/api/pickers.test.ts b/web/src/api/pickers.test.ts new file mode 100644 index 000000000..64bffda11 --- /dev/null +++ b/web/src/api/pickers.test.ts @@ -0,0 +1,29 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getGraphicsElements } from './pickers'; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { headers: { 'Content-Type': 'application/json' }, status }); +} + +describe('getGraphicsElements', () => { + beforeEach(() => { + window.localStorage.clear(); + vi.restoreAllMocks(); + }); + + it('GETs /api/graphics-elements without refresh by default', async () => { + const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([])); + + await getGraphicsElements(); + + expect(fetchSpy.mock.calls[0][0]).toBe('/api/graphics-elements'); + }); + + it('passes refresh=true to re-sync on-disk definitions first', async () => { + const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([])); + + await getGraphicsElements(true); + + expect(fetchSpy.mock.calls[0][0]).toBe('/api/graphics-elements?refresh=true'); + }); +}); diff --git a/web/src/api/pickers.ts b/web/src/api/pickers.ts index 1fbf3a6e9..ba35fc3f8 100644 --- a/web/src/api/pickers.ts +++ b/web/src/api/pickers.ts @@ -14,8 +14,11 @@ export function getWatermarks(): Promise { return request('/api/watermarks').then(sortByName); } -export function getGraphicsElements(): Promise { - return request('/api/graphics-elements').then(sortByName); +// Pass refresh=true to first re-sync the on-disk graphics element definitions into the database +// (matching legacy Blazor's RefreshGraphicsElements-before-list) so newly added files appear. +export function getGraphicsElements(refresh = false): Promise { + const url = refresh ? '/api/graphics-elements?refresh=true' : '/api/graphics-elements'; + return request(url).then(sortByName); } export function getFFmpegProfiles(): Promise {