Merge remote-tracking branch 'origin/main' into feat/253-pr3-diff-scalar
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m12s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / Build & push image (amd64) (push) Has been cancelled
Build ErsatzTV Image / Build & test (.NET) (push) Has been cancelled
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Has been cancelled
# Conflicts: # docs/decisions.md
This commit was merged in pull request #270.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ApiError, request } from './client';
|
||||
import { ApiError, request, requestWithMeta, type ResponseWithMeta } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
export type DecoTemplateGroup = components['schemas']['DecoTemplateGroupResponseModel'];
|
||||
@@ -46,8 +46,25 @@ export function getDecoTemplateItems(id: number): Promise<DecoTemplateItem[]> {
|
||||
return request<DecoTemplateItem[]>(`/api/deco-templates/${id}/items`);
|
||||
}
|
||||
|
||||
export function replaceDecoTemplate(id: number, body: ReplaceDecoTemplateRequest): Promise<DecoTemplateWithItems> {
|
||||
return request<DecoTemplateWithItems>(`/api/deco-templates/${id}`, { body, method: 'PUT' });
|
||||
/** Load deco template items together with the deco template's concurrency ETag (issue #253). */
|
||||
export function getDecoTemplateItemsWithMeta(id: number): Promise<ResponseWithMeta<DecoTemplateItem[]>> {
|
||||
return requestWithMeta<DecoTemplateItem[]>(`/api/deco-templates/${id}/items`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a deco template. Pass the last-seen ETag as `If-Match` to reject a stale overwrite with
|
||||
* 412; the resolved value carries the new ETag for a subsequent save (issue #253).
|
||||
*/
|
||||
export function replaceDecoTemplate(
|
||||
id: number,
|
||||
body: ReplaceDecoTemplateRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<DecoTemplateWithItems>> {
|
||||
return requestWithMeta<DecoTemplateWithItems>(`/api/deco-templates/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export function messageFromDecoTemplateError(error: unknown, fallback = 'Unable to load deco templates'): string {
|
||||
|
||||
Vendored
+7
@@ -1094,6 +1094,7 @@ export interface components {
|
||||
"buildStatus": null | components["schemas"]["PlayoutBuildStatusResponseModel"];
|
||||
"decoId": null | number;
|
||||
"decoName": null | string;
|
||||
"isLocked": boolean;
|
||||
};
|
||||
"PlayoutScheduleKind": "None" | "Classic" | "Block" | "Sequential" | "Scripted" | "ExternalJson";
|
||||
"PlayoutSettingsResponseModel": {
|
||||
@@ -1154,6 +1155,7 @@ export interface components {
|
||||
"shuffleScheduleItems": boolean;
|
||||
"randomStartPoint": boolean;
|
||||
"fixedStartTimeBehavior": components["schemas"]["FixedStartTimeBehavior"];
|
||||
"version": number;
|
||||
};
|
||||
"RemoteConnectionResponseModel": {
|
||||
"address": null | string;
|
||||
@@ -1244,6 +1246,11 @@ export interface components {
|
||||
"selectedName": null | string;
|
||||
"firstRunPlaybackOrder": components["schemas"]["PlaybackOrder"];
|
||||
"rerunPlaybackOrder": components["schemas"]["PlaybackOrder"];
|
||||
};
|
||||
"ResetAllPlayoutsResponseModel": {
|
||||
"queuedPlayoutIds": Array<number>;
|
||||
"skippedLocked": Array<number>;
|
||||
"skippedUnsupported": Array<number>;
|
||||
};
|
||||
"ResolutionResponseModel": {
|
||||
"id": number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { scanLibrary, scanShow } from './libraries';
|
||||
import { scanCollections, scanLibrary, scanShow } from './libraries';
|
||||
|
||||
function noContent(): Response {
|
||||
return new Response(null, { status: 200 });
|
||||
@@ -22,6 +22,25 @@ describe('libraries api client', () => {
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/libraries/4/scan', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanLibrary appends ?deep=true for a deep scan', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanLibrary(4, true);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/libraries/4/scan?deep=true', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanCollections POSTs to the media-source scan-collections endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanCollections('jellyfin', 7);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/media-sources/jellyfin/7/scan-collections', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanCollections appends ?deep=true for a deep scan', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanCollections('plex', 3, true);
|
||||
const { url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/media-sources/plex/3/scan-collections?deep=true');
|
||||
});
|
||||
|
||||
it('scanShow POSTs the show id and deepScan flag', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanShow(4, { deepScan: true, showId: 42 });
|
||||
|
||||
+129
-9
@@ -16,7 +16,7 @@ export type LibrariesScreenQueryState =
|
||||
data: LibrariesScreenData;
|
||||
error: string | null;
|
||||
refresh: () => void;
|
||||
scanLibrary: (libraryId: number) => Promise<void>;
|
||||
scanLibrary: (libraryId: number, deep?: boolean) => Promise<void>;
|
||||
scanningLibraryIds: Set<number>;
|
||||
status: 'success';
|
||||
}
|
||||
@@ -42,8 +42,24 @@ export function getLibraryScanStatus(): Promise<LibraryScanStatus[]> {
|
||||
);
|
||||
}
|
||||
|
||||
export function scanLibrary(libraryId: number): Promise<void> {
|
||||
return request<void>(`/api/libraries/${libraryId}/scan`, { method: 'POST' });
|
||||
// Queues a library scan. `deep` requests a deep (force-metadata) scan — the API equivalent of the
|
||||
// legacy Blazor "Deep Scan Library" button (#235 F9); omit or pass false for a quick scan. Returns
|
||||
// 202 when queued, 404 (missing), 409 (already scanning), 422 (sync disabled) — see §3b.
|
||||
export function scanLibrary(libraryId: number, deep = false): Promise<void> {
|
||||
const query = deep ? '?deep=true' : '';
|
||||
return request<void>(`/api/libraries/${libraryId}/scan${query}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// Media-source families that support an external-collections scan (#235 F9). Matches the three
|
||||
// media-source controllers #202 introduced.
|
||||
export type CollectionsScanSource = 'emby' | 'jellyfin' | 'plex';
|
||||
|
||||
// Queues an external-collections scan for a Plex/Jellyfin/Emby media source — the API equivalent of
|
||||
// the legacy Blazor "Scan Collections" button (#235 F9). `deep` requests a deep scan. Returns 202
|
||||
// when queued, 404 (source missing), 409 (a collections scan is already running for that source).
|
||||
export function scanCollections(source: CollectionsScanSource, sourceId: number, deep = false): Promise<void> {
|
||||
const query = deep ? '?deep=true' : '';
|
||||
return request<void>(`/api/media-sources/${source}/${sourceId}/scan-collections${query}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export interface ScanShowParams {
|
||||
@@ -51,9 +67,11 @@ export interface ScanShowParams {
|
||||
deepScan?: boolean;
|
||||
}
|
||||
|
||||
// Queues a scan of a single show (by id) within a library. Returns 200 on success, 404 when the
|
||||
// show id doesn't exist in the library, 400 when the library doesn't support single-show
|
||||
// scanning. Body keys are `showId` and `deepScan` (see LibrariesController.ScanShowRequest).
|
||||
// Queues a scan of a single show (by id) within a library. Returns 202 when queued, 404 when the
|
||||
// show id doesn't exist in the library, 409 when a scan is already running, and 422 when the
|
||||
// library doesn't support single-show scanning / sync is disabled / the scan failed to start (all
|
||||
// error bodies are ProblemDetails; #235 normalized the old conflated 400). Body keys are `showId`
|
||||
// and `deepScan` (see LibrariesController.ScanShowRequest).
|
||||
export function scanShow(libraryId: number, params: ScanShowParams): Promise<void> {
|
||||
return request<void>(`/api/libraries/${libraryId}/scan-show`, {
|
||||
body: { deepScan: params.deepScan ?? false, showId: params.showId },
|
||||
@@ -297,9 +315,10 @@ export function useLibrariesScreenQuery(pollMs = 10000): LibrariesScreenQuerySta
|
||||
});
|
||||
}, []);
|
||||
|
||||
const triggerScan = useCallback((libraryId: number): Promise<void> => {
|
||||
const triggerScan = useCallback((libraryId: number, deep = false): Promise<void> => {
|
||||
if (pendingIdsRef.current.has(libraryId) || activeIdsRef.current.has(libraryId)) {
|
||||
// Already pending or active - ignore the duplicate submission.
|
||||
// Already pending or active - ignore the duplicate submission. Quick and deep scans share the
|
||||
// same per-library lock, so both affordances gate on (and reconcile through) this one id set.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -320,7 +339,7 @@ export function useLibrariesScreenQuery(pollMs = 10000): LibrariesScreenQuerySta
|
||||
};
|
||||
});
|
||||
|
||||
return scanLibrary(libraryId)
|
||||
return scanLibrary(libraryId, deep)
|
||||
.then(() => {
|
||||
// 202 Accepted - a scan is genuinely queued. Poll scan-status; the pending flag is
|
||||
// promoted to "active" once the scan appears there.
|
||||
@@ -381,6 +400,107 @@ export function useLibrariesScreenQuery(pollMs = 10000): LibrariesScreenQuerySta
|
||||
return { data: null, error: null, refresh, status: 'loading' };
|
||||
}
|
||||
|
||||
// --- External collections scan (Plex/Jellyfin/Emby) ---
|
||||
|
||||
// A pending collections scan re-enables its button after this bound. Unlike library scans, an
|
||||
// external-collections scan has NO authoritative "in progress" REST surface: /api/libraries/scan-status
|
||||
// is library-keyed, and Blazor only ever observed collections locks through in-process IEntityLocker
|
||||
// events (Are{X}CollectionsLocked) that have no HTTP mirror. So we can't reconcile "pending" against a
|
||||
// live active set the way library scans do - we optimistically disable, then give up after this bound
|
||||
// so a button can't wedge disabled forever. (A collections scan-status endpoint would let us reconcile
|
||||
// properly - see the #91b follow-up note.)
|
||||
const COLLECTIONS_PENDING_TIMEOUT_MS = 30000;
|
||||
|
||||
export interface CollectionsScanState {
|
||||
error: string | null;
|
||||
scan: (source: CollectionsScanSource, sourceId: number, deep: boolean) => Promise<void>;
|
||||
scanningKeys: Set<string>;
|
||||
}
|
||||
|
||||
// Stable key for a per-(family, source) collections scan, used by both the hook and the screen so the
|
||||
// "is this row scanning?" lookup has exactly one definition.
|
||||
export function collectionsScanKey(source: CollectionsScanSource, sourceId: number): string {
|
||||
return `${source}:${sourceId}`;
|
||||
}
|
||||
|
||||
export function useCollectionsScan(): CollectionsScanState {
|
||||
const [scanningKeys, setScanningKeys] = useState<Set<string>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Mirror kept in sync synchronously so scan() can guard double-submits without waiting for a render.
|
||||
const scanningKeysRef = useRef<Set<string>>(new Set());
|
||||
const timeoutsRef = useRef<Map<string, number>>(new Map());
|
||||
const activeRef = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
activeRef.current = true;
|
||||
// Identity is stable across the hook's life (we only .set/.delete entries, never reassign the
|
||||
// Map), so capturing it here is the same instance the cleanup clears at unmount.
|
||||
const timeouts = timeoutsRef.current;
|
||||
|
||||
return () => {
|
||||
activeRef.current = false;
|
||||
timeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
||||
timeouts.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearKey = useCallback((key: string) => {
|
||||
const timeoutId = timeoutsRef.current.get(key);
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId);
|
||||
timeoutsRef.current.delete(key);
|
||||
}
|
||||
|
||||
const next = new Set(scanningKeysRef.current);
|
||||
next.delete(key);
|
||||
scanningKeysRef.current = next;
|
||||
|
||||
if (activeRef.current) {
|
||||
setScanningKeys(next);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scan = useCallback(
|
||||
(source: CollectionsScanSource, sourceId: number, deep: boolean): Promise<void> => {
|
||||
const key = collectionsScanKey(source, sourceId);
|
||||
if (scanningKeysRef.current.has(key)) {
|
||||
// Already pending - ignore the duplicate submission (quick and deep share the source lock).
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const pending = new Set(scanningKeysRef.current).add(key);
|
||||
scanningKeysRef.current = pending;
|
||||
setScanningKeys(pending);
|
||||
setError(null);
|
||||
|
||||
const timeoutId = window.setTimeout(() => clearKey(key), COLLECTIONS_PENDING_TIMEOUT_MS);
|
||||
timeoutsRef.current.set(key, timeoutId);
|
||||
|
||||
return scanCollections(source, sourceId, deep)
|
||||
.then(() => {
|
||||
// 202 Accepted - a scan is genuinely queued. Keep the button disabled until the bounded
|
||||
// timeout expires (there is no completion signal to reconcile against).
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof ApiError && err.status === 409) {
|
||||
// 409 Conflict - a collections scan is already running for this source. Benign: keep the
|
||||
// optimistic pending flag (button stays disabled) with no error; it clears on the timeout.
|
||||
return;
|
||||
}
|
||||
|
||||
// 404 (source missing) / network error - nothing was queued, so re-enable and surface it.
|
||||
clearKey(key);
|
||||
if (activeRef.current) {
|
||||
setError(messageFromLibrariesError(err, 'Unable to scan collections'));
|
||||
}
|
||||
});
|
||||
},
|
||||
[clearKey]
|
||||
);
|
||||
|
||||
return { error, scan, scanningKeys };
|
||||
}
|
||||
|
||||
function messageFromLibrariesError(error: unknown, fallback = 'Unable to load libraries'): string {
|
||||
if (error instanceof ApiError) {
|
||||
return error.detail ?? error.message;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiError, request } from './client';
|
||||
import { ApiError, request, requestWithMeta, type ResponseWithMeta } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
export type AddItemsToPlaylistRequest = components['schemas']['AddItemsToPlaylistRequest'];
|
||||
@@ -41,13 +41,28 @@ export function getPlaylistItems(id: number): Promise<PlaylistItem[]> {
|
||||
return request<PlaylistItem[]>(`/api/playlists/${id}/items`);
|
||||
}
|
||||
|
||||
/** Load playlist items together with the playlist's concurrency ETag (issue #253). */
|
||||
export function getPlaylistItemsWithMeta(id: number): Promise<ResponseWithMeta<PlaylistItem[]>> {
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/playlists/${id}/items`);
|
||||
}
|
||||
|
||||
export function createPlaylist(body: CreatePlaylistRequest): Promise<Playlist> {
|
||||
return request<Playlist>('/api/playlists', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// PUT = rename + replace the full item list; returns the persisted (re-indexed) items.
|
||||
export function updatePlaylist(id: number, body: ReplacePlaylistRequest): Promise<PlaylistItem[]> {
|
||||
return request<PlaylistItem[]>(`/api/playlists/${id}`, { body, method: 'PUT' });
|
||||
// PUT = rename + replace the full item list; returns the persisted (re-indexed) items. Pass the
|
||||
// last-seen ETag as `If-Match` to reject a stale overwrite with 412; the resolved value carries
|
||||
// the new ETag for a subsequent save (issue #253).
|
||||
export function updatePlaylist(
|
||||
id: number,
|
||||
body: ReplacePlaylistRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<PlaylistItem[]>> {
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/playlists/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export function deletePlaylist(id: number): Promise<void> {
|
||||
|
||||
@@ -108,8 +108,13 @@ export function getPlayoutChannelStates(): Promise<PlayoutChannelState[]> {
|
||||
return request<PlayoutChannelState[]>('/api/channels/state');
|
||||
}
|
||||
|
||||
export function resetAllPlayouts(): Promise<void> {
|
||||
return request<void>('/api/playouts/reset-all', { method: 'POST' });
|
||||
export type ResetAllPlayoutsResult = components['schemas']['ResetAllPlayoutsResponseModel'];
|
||||
|
||||
// Queues a reset of every eligible playout. Returns 202 with a body reporting which playouts were
|
||||
// queued vs skipped (locked, or an unsupported ExternalJson/None schedule kind) — #235 replaced the
|
||||
// old silent skip. The caller may surface `skipped*` to explain why some playouts didn't reset.
|
||||
export function resetAllPlayouts(): Promise<ResetAllPlayoutsResult> {
|
||||
return request<ResetAllPlayoutsResult>('/api/playouts/reset-all', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function deletePlayout(playoutId: number): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiError, request } from './client';
|
||||
import { ApiError, request, requestWithMeta, type ResponseWithMeta } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
// FillerPreset is exported by ./pickers — import (don't re-export) to avoid a duplicate `export *`
|
||||
// name in api/index.ts. LanguageCode/getLanguages moved to ./languages (also re-exported via
|
||||
@@ -46,17 +46,31 @@ export function getScheduleItems(scheduleId: number): Promise<ScheduleItemsRespo
|
||||
return request<ScheduleItemsResponse>(`/api/schedules/${scheduleId}/items`);
|
||||
}
|
||||
|
||||
/** Load schedule items together with the schedule's concurrency ETag (issue #253). */
|
||||
export function getScheduleItemsWithMeta(
|
||||
scheduleId: number
|
||||
): Promise<ResponseWithMeta<ScheduleItemsResponse>> {
|
||||
return requestWithMeta<ScheduleItemsResponse>(`/api/schedules/${scheduleId}/items`);
|
||||
}
|
||||
|
||||
export function addScheduleItem(scheduleId: number, body: ScheduleItemRequest): Promise<ScheduleItem> {
|
||||
return request<ScheduleItem>(`/api/schedules/${scheduleId}/items`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// Destructive replace: the server deletes+recreates every item row (new ids) and triggers playout
|
||||
// rebuilds. The editor batches all local draft edits into this single call. See docs/decisions.md.
|
||||
// Positional in-place reconcile: the server reuses same-typed item rows (keeping fill-group state) and
|
||||
// triggers playout rebuilds. The editor batches all local draft edits into this single call. Pass the
|
||||
// last-seen ETag as `If-Match` to reject a stale overwrite with 412; the resolved value carries the new
|
||||
// ETag for a subsequent save (issue #253). See docs/decisions.md.
|
||||
export function replaceScheduleItems(
|
||||
scheduleId: number,
|
||||
body: ReplaceScheduleItemsRequest
|
||||
): Promise<ScheduleItem[]> {
|
||||
return request<ScheduleItem[]>(`/api/schedules/${scheduleId}/items`, { body, method: 'PUT' });
|
||||
body: ReplaceScheduleItemsRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<ScheduleItem[]>> {
|
||||
return requestWithMeta<ScheduleItem[]>(`/api/schedules/${scheduleId}/items`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteScheduleItem(scheduleId: number, itemId: number): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiError, request } from './client';
|
||||
import { ApiError, request, requestWithMeta, type ResponseWithMeta } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
export type TemplateGroup = components['schemas']['TemplateGroupResponseModel'];
|
||||
@@ -47,8 +47,25 @@ export function getTemplateItems(id: number): Promise<TemplateItem[]> {
|
||||
return request<TemplateItem[]>(`/api/templates/${id}/items`);
|
||||
}
|
||||
|
||||
export function replaceTemplate(id: number, body: ReplaceTemplateRequest): Promise<TemplateWithItems> {
|
||||
return request<TemplateWithItems>(`/api/templates/${id}`, { body, method: 'PUT' });
|
||||
/** Load template items together with the template's concurrency ETag (issue #253). */
|
||||
export function getTemplateItemsWithMeta(id: number): Promise<ResponseWithMeta<TemplateItem[]>> {
|
||||
return requestWithMeta<TemplateItem[]>(`/api/templates/${id}/items`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a template. Pass the last-seen ETag as `If-Match` to reject a stale overwrite with 412;
|
||||
* the resolved value carries the new ETag for a subsequent save (issue #253).
|
||||
*/
|
||||
export function replaceTemplate(
|
||||
id: number,
|
||||
body: ReplaceTemplateRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<TemplateWithItems>> {
|
||||
return requestWithMeta<TemplateWithItems>(`/api/templates/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
});
|
||||
}
|
||||
|
||||
export function copyTemplate(id: number, body: CopyTemplateRequest): Promise<Template> {
|
||||
|
||||
Reference in New Issue
Block a user