Files
manage/frontend/src/api/widgets.ts
T
Developer 1e636fdbe2 Follow-ups: reference reorder, detach service_id, named-dashboard widgets
Three reusable-widget follow-up fixes:

1. Reference sort_order independently reorderable. Reordering a
   referenced widget now updates the widget_references.sort_order (per-
   dashboard), not the shared widget instance sort_order. New backend
   update_widget_reference method + PUT /api/widgets/references/{id}
   endpoint. Frontend moveInstance checks _ref_id to choose the right
   mutation (updateRef for references, saveWidget for owned).

2. Detach preserves service_id. detach_widget_reference now copies the
   original widget's service_id into the clone, so service-bound widgets
   (Grafana chart, Jellyfin activity) continue to render after detach.

3. Named dashboards support widget references. NamedDashboardPage
   fetches useWidgetReferences('named:<slug>') and renders them via
   WidgetInstanceCard alongside pinned links. 'Edit widgets' button
   opens WidgetConfigDialog with dashboardScope='named:<slug>'.

Also: removed useMemo on combinedWidgets in WidgetConfigDialog to fix
a react-hooks/preserve-manual-memoization lint error (the React Compiler
ESLint plugin couldn't verify the spread+sort memoization).

283 backend tests pass (+1 update_reference test); 128 frontend tests
pass; ruff clean; 0 lint errors.
2026-07-06 12:11:47 +00:00

100 lines
2.6 KiB
TypeScript

import { del, get, post, put } from "./shared";
import type {
BuiltinWidgetKindInfo,
WidgetDataResponse,
WidgetInstance,
WidgetInstanceInput,
} from "../types";
export interface WidgetReference {
id: string;
dashboard_scope: string;
widget_id: string;
sort_order: number;
created_at: number;
widget: WidgetInstance;
}
export interface WidgetReferenceInput {
dashboard_scope: string;
widget_id: string;
sort_order?: number;
}
export async function fetchBuiltinWidgetKinds(): Promise<
BuiltinWidgetKindInfo[]
> {
return get<BuiltinWidgetKindInfo[]>("/api/widgets/builtin");
}
export async function fetchWidgetInstances(
serviceId?: string,
scope?: "dashboard" | "service",
): Promise<WidgetInstance[]> {
const params: Record<string, string> = {};
if (serviceId) params.service_id = serviceId;
if (scope) params.scope = scope;
return get<WidgetInstance[]>("/api/widgets/instances", params);
}
export async function createWidgetInstance(
input: WidgetInstanceInput,
): Promise<WidgetInstance> {
return post<WidgetInstance>("/api/widgets/instances", input);
}
export async function updateWidgetInstance(
input: WidgetInstanceInput,
): Promise<WidgetInstance> {
if (!input.id) throw new Error("Widget ID is required for update");
return put<WidgetInstance>(`/api/widgets/instances/${input.id}`, input);
}
export async function deleteWidgetInstance(
widgetId: string,
): Promise<{ status: string }> {
return del<{ status: string }>(`/api/widgets/instances/${widgetId}`);
}
export async function fetchWidgetData(
widgetId: string,
): Promise<WidgetDataResponse> {
return get<WidgetDataResponse>(`/api/widgets/instances/${widgetId}/data`);
}
export async function fetchWidgetReferences(
dashboardScope: string,
): Promise<WidgetReference[]> {
return get<WidgetReference[]>("/api/widgets/references", {
dashboard_scope: dashboardScope,
});
}
export async function createWidgetReference(
input: WidgetReferenceInput,
): Promise<WidgetReference> {
return post<WidgetReference>("/api/widgets/references", input);
}
export async function deleteWidgetReference(
referenceId: string,
): Promise<{ status: string }> {
return del<{ status: string }>(`/api/widgets/references/${referenceId}`);
}
export async function detachWidgetReference(
referenceId: string,
): Promise<WidgetInstance> {
return post<WidgetInstance>(`/api/widgets/references/${referenceId}/detach`);
}
export async function updateWidgetReference(
referenceId: string,
sortOrder: number,
): Promise<WidgetReference> {
return put<WidgetReference>(
`/api/widgets/references/${referenceId}?sort_order=${sortOrder}`,
{},
);
}