feat(spa): wire playout delete/reset/erase + scheduling context + templates preview calendar (#210)
Adds the SPA slice for the newly-merged playout endpoints: - api/playouts: deletePlayout, resetChannelPlayout, erasePlayoutItems, erasePlayoutItemsAndHistory, getPlayoutItemSchedulingContext (+ types). - PlayoutsScreen: enable per-playout Reset (channel playout reset), Delete (danger, confirm), and erase-by-kind buttons (Block = items + items&history; Classic/Sequential/Scripted = items&history; none for ExternalJson); replaces the stale "no per-playout reset endpoint" block. Disable Alternate schedules for OnDemand Classic playouts (Blazor parity). - Per-item scheduling-context dialog (info button when hasSchedulingContext && id != null), rendering the decoded JSON in a scrollable <pre>. - Playout-templates editor: "Preview calendar" toggle with a Monday-first month grid; winning template per day computed client-side via a colocated appliesToDate/firstMatchingIndex helper that ports AlternateScheduleSelector.GetScheduleForDate / PlayoutTemplateEditViewModel .AppliesToDate exactly (inclusive range, year-wrap reverse, start-day rollover, end-day clamp, explicit-year override). - Button gains a title prop (parity with IconButton) for the disabled-button hint. Tests: api client URL/method assertions, PlayoutsScreen action flows (delete/reset/erase per kind, OnDemand gating, scheduling-context dialog + error), calendar-helper unit tests, and a preview-calendar render test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
createPlayout,
|
||||
deletePlayout,
|
||||
erasePlayoutItems,
|
||||
erasePlayoutItemsAndHistory,
|
||||
getAlternateSchedules,
|
||||
getPlayoutItemSchedulingContext,
|
||||
getPlayoutTemplates,
|
||||
replaceAlternateSchedules,
|
||||
replacePlayoutTemplates,
|
||||
resetChannelPlayout,
|
||||
updatePlayoutDefaultDeco,
|
||||
updatePlayoutDetails,
|
||||
type CreatePlayoutRequest
|
||||
@@ -17,6 +22,10 @@ function jsonResponse(body: unknown, status = 200): Response {
|
||||
});
|
||||
}
|
||||
|
||||
function noContentResponse(): Response {
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
describe('playouts api client', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
@@ -96,6 +105,65 @@ describe('playouts api client', () => {
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ decoId: 7 });
|
||||
});
|
||||
|
||||
it('deletePlayout DELETEs the id route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContentResponse());
|
||||
|
||||
await expect(deletePlayout(4)).resolves.toBeUndefined();
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/4');
|
||||
expect(init).toMatchObject({ method: 'DELETE' });
|
||||
});
|
||||
|
||||
it('resetChannelPlayout POSTs to the channel playout reset route without a mode', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({}));
|
||||
|
||||
await resetChannelPlayout('12.3');
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/12.3/playout/reset');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
it('resetChannelPlayout url-encodes the channel number', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({}));
|
||||
|
||||
await resetChannelPlayout('a b');
|
||||
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/a%20b/playout/reset');
|
||||
});
|
||||
|
||||
it('erasePlayoutItems POSTs to the erase-items route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContentResponse());
|
||||
|
||||
await erasePlayoutItems(6);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/6/erase-items');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
it('erasePlayoutItemsAndHistory POSTs to the erase-items-and-history route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContentResponse());
|
||||
|
||||
await erasePlayoutItemsAndHistory(6);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/6/erase-items-and-history');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
it('getPlayoutItemSchedulingContext GETs the item scheduling-context route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ context: '{ "x": 1 }' }));
|
||||
|
||||
await expect(getPlayoutItemSchedulingContext(42)).resolves.toMatchObject({ context: '{ "x": 1 }' });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/items/42/scheduling-context');
|
||||
expect(init?.method ?? 'GET').toBe('GET');
|
||||
});
|
||||
|
||||
it('getAlternateSchedules GETs the alternate-schedules route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ export type PlayoutsPage = components['schemas']['PagedPlayoutsResponseModel'];
|
||||
export type PlayoutItemsPage = components['schemas']['PagedPlayoutItemsResponseModel'];
|
||||
export type PlayoutChannelState = components['schemas']['ChannelStateResponseModel'];
|
||||
export type PlayoutScheduleKind = components['schemas']['PlayoutScheduleKind'];
|
||||
export type ChannelPlayoutMode = components['schemas']['ChannelPlayoutMode'];
|
||||
export type PlayoutItemSchedulingContext = components['schemas']['PlayoutItemSchedulingContextResponseModel'];
|
||||
export type CreatePlayoutRequest = components['schemas']['CreatePlayoutRequest'];
|
||||
export type UpdatePlayoutDetailsRequest = components['schemas']['UpdatePlayoutDetailsRequest'];
|
||||
|
||||
@@ -110,6 +112,28 @@ export function resetAllPlayouts(): Promise<void> {
|
||||
return request<void>('/api/playouts/reset-all', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function deletePlayout(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Resets the given channel's playout. The server picks the correct default mode per schedule kind
|
||||
// (Classic → Refresh, others → Reset), matching the Blazor "Reset Playout" action — so no mode is sent.
|
||||
export function resetChannelPlayout(channelNumber: string): Promise<void> {
|
||||
return request<void>(`/api/channels/${encodeURIComponent(channelNumber)}/playout/reset`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function erasePlayoutItems(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}/erase-items`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function erasePlayoutItemsAndHistory(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}/erase-items-and-history`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function getPlayoutItemSchedulingContext(itemId: number): Promise<PlayoutItemSchedulingContext> {
|
||||
return request<PlayoutItemSchedulingContext>(`/api/playouts/items/${itemId}/scheduling-context`);
|
||||
}
|
||||
|
||||
export function createPlayout(body: CreatePlayoutRequest): Promise<PlayoutDetail> {
|
||||
return request<PlayoutDetail>('/api/playouts', {
|
||||
body,
|
||||
|
||||
Reference in New Issue
Block a user