Files
homarr/packages/widgets/src/custom-api/widget-definition-context.tsx
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

218 lines
7.7 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { fetchApi, clientApi } from "@homarr/api/client";
import type {
CustomWidgetPublishedQueryState,
CustomJsxRequestCapability,
CustomWidgetRuntimeMessages,
CustomWidgetRuntimePort,
} from "@homarr/custom-widgets/runtime";
import { CustomWidgetRuntimeProvider } from "@homarr/custom-widgets/runtime";
import { useConfirmModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { useQueryClient } from "@tanstack/react-query";
interface WidgetDefinitionProviderProps {
itemId?: string;
definitionId?: string;
queryCacheKey?: string;
previewSessionId?: string;
previewLiveActions?: boolean;
queriesDisabled?: boolean;
isEditMode?: boolean;
requestCapabilities?: readonly CustomJsxRequestCapability[];
setQueryState?(requestId: string, value: CustomWidgetPublishedQueryState | null): void;
children: ReactNode;
}
const INACTIVE_PORT: CustomWidgetRuntimePort = {
query: async () => ({ ok: false, status: 0, data: null, error: "Custom widget unavailable" }),
executeAction: async () => ({ ok: false, status: 0, data: null, error: "Custom widget unavailable" }),
invalidate: async () => undefined,
confirm: async () => false,
notify: () => undefined,
};
const useRuntimeMessages = (): CustomWidgetRuntimeMessages => {
const t = useI18n("widget.customApi.customJsx");
const actionT = useI18n("common.action");
return {
requestIdRequired: t("requestIdRequired"),
unsavedPreview: t("unsavedPreview"),
invalidParams: t("invalidParams"),
loadRequest: t("loadRequest"),
requestFailed: t("requestFailed"),
loading: t("loading"),
retry: t("retry"),
widgetItemUnavailable: t("widgetItemUnavailable"),
actionsDisabledEditMode: t("actionsDisabledEditMode"),
actionSimulated: t("actionSimulated"),
actionCompleted: t("actionCompleted"),
confirmDelete: t("confirmDelete"),
toggle: t("toggle"),
refresh: actionT("refresh"),
};
};
export function InactiveWidgetDefinitionProvider({
definitionId,
isEditMode,
children,
}: Pick<WidgetDefinitionProviderProps, "definitionId" | "isEditMode" | "children">) {
const messages = useRuntimeMessages();
return (
<CustomWidgetRuntimeProvider
definitionId={definitionId}
isEditMode={isEditMode ?? false}
requestCapabilities={[]}
port={INACTIVE_PORT}
messages={messages}
>
{children}
</CustomWidgetRuntimeProvider>
);
}
export function WidgetDefinitionProvider(props: WidgetDefinitionProviderProps) {
const utils = clientApi.useUtils();
const queryClient = useQueryClient();
const { openConfirmModal } = useConfirmModal();
const messages = useRuntimeMessages();
const port: CustomWidgetRuntimePort = {
query: async (input, signal) => {
if (input.itemId) {
return fetchApi.widget.customApi.queryRequest.query(
{ itemId: input.itemId, requestId: input.requestId, params: input.params },
{ signal },
);
}
return fetchApi.customWidget.previewQuery.query(
{ sessionId: input.previewSessionId ?? "", requestId: input.requestId, params: input.params },
{ signal },
);
},
executeAction: (input) =>
input.itemId
? fetchApi.widget.customApi.executeAction.mutate({
itemId: input.itemId,
requestId: input.requestId,
params: input.params,
confirmed: input.confirmed,
})
: fetchApi.customWidget.previewAction.mutate({
sessionId: input.previewSessionId ?? "",
requestId: input.requestId,
params: input.params,
confirmed: input.confirmed,
}),
invalidate: async ({ itemId, previewSessionId, targets }) => {
if (targets.length === 0) return;
const invalidateAll = targets.includes("*");
if (previewSessionId) {
if (props.queriesDisabled) return;
const requests = (props.requestCapabilities ?? []).filter(
(request) => request.kind === "query" && (invalidateAll || targets.includes(request.id)),
);
await fetchApi.customWidget.previewRefresh.mutate({
sessionId: previewSessionId,
requestIds: requests.map((request) => request.id),
});
// Preview load data starts as a snapshot, not an active React Query subscription.
// Publish refreshed load results so both editor and standalone previews update.
await Promise.all(
requests
.filter((request) => request.trigger === "load")
.map(async (request) => {
props.setQueryState?.(request.id, { data: null, status: { loading: true } });
try {
const result = await port.query({ previewSessionId, requestId: request.id, params: {} });
props.setQueryState?.(request.id, {
data: result.data,
status: {
loading: false,
ok: result.ok,
status: result.status,
statusText: result.statusText,
error: result.error,
},
});
} catch {
props.setQueryState?.(request.id, {
data: null,
status: { loading: false, ok: false, error: messages.requestFailed },
});
}
}),
);
await queryClient.invalidateQueries({
predicate: (query) => {
const key = query.queryKey;
return (
key[0] === "custom-widget" &&
key[1] === "preview" &&
key[2] === previewSessionId &&
(invalidateAll || targets.includes(String(key[3])))
);
},
});
return;
}
if (!itemId) return;
if (invalidateAll) await fetchApi.widget.customApi.refresh.mutate({ itemId });
const tasks: Promise<unknown>[] = [
queryClient.invalidateQueries({
predicate: (query) => {
const key = query.queryKey;
return (
key[0] === "custom-widget" &&
key[1] === "item" &&
key[2] === itemId &&
(invalidateAll || targets.includes(String(key[3])))
);
},
}),
];
if (
invalidateAll ||
(props.requestCapabilities ?? []).some((request) => request.trigger === "load" && targets.includes(request.id))
) {
tasks.push(utils.widget.customApi.getData.invalidate({ itemId }));
}
await Promise.all(tasks);
},
confirm: ({ title, message, confirmLabel, destructive }) =>
new Promise((resolve) => {
openConfirmModal({
title,
children: message,
labels: { confirm: confirmLabel },
confirmProps: { color: destructive ? "red.9" : "blue" },
onConfirm: () => resolve(true),
onCancel: () => resolve(false),
});
}),
notify: ({ kind, ...notification }) =>
kind === "success" ? showSuccessNotification(notification) : showErrorNotification(notification),
};
return (
<CustomWidgetRuntimeProvider
itemId={props.itemId}
definitionId={props.definitionId}
queryCacheKey={props.queryCacheKey}
previewSessionId={props.previewSessionId}
previewLiveActions={props.previewLiveActions}
queriesDisabled={props.queriesDisabled}
isEditMode={props.isEditMode ?? false}
requestCapabilities={props.requestCapabilities ?? []}
port={port}
messages={messages}
setQueryState={props.setQueryState}
>
{props.children}
</CustomWidgetRuntimeProvider>
);
}