+12








fcd7c979df
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>
218 lines
7.7 KiB
TypeScript
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>
|
|
);
|
|
}
|