feat(widgets): consolidate integration metadata + shared response caching (#6691)
This commit is contained in:
1 parent
7d1d9062ed
commit
8d8dec5749
206 files changed
+3893
-1946
No files matched your search
@@ -145,10 +145,14 @@ const ScopedTRPCReactProvider = ({
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
for (const { queryKey, intervalSeconds } of widgetQueryRefetchIntervals) {
|
||||
client.setQueryDefaults(queryKey, {
|
||||
refetchInterval: intervalSeconds === null ? false : intervalSeconds * 1000,
|
||||
});
|
||||
for (const queryDefaults of widgetQueryRefetchIntervals) {
|
||||
const policy: { refetchInterval?: number | false; staleTime?: number } = {};
|
||||
if (queryDefaults.intervalSeconds === null) policy.refetchInterval = false;
|
||||
if (typeof queryDefaults.intervalSeconds === "number") {
|
||||
policy.refetchInterval = queryDefaults.intervalSeconds * 1000;
|
||||
}
|
||||
if ("staleTimeSeconds" in queryDefaults) policy.staleTime = queryDefaults.staleTimeSeconds * 1000;
|
||||
client.setQueryDefaults(queryDefaults.queryKey, policy);
|
||||
}
|
||||
for (const { queryKey, ...policy } of dashboardSupportingQueryPolicies) {
|
||||
client.setQueryDefaults(queryKey, policy);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getRepresentativeLayoutWidth, projectBoardLayout } from "@homarr/boards
|
||||
import { getBoardLaneColumnCount, getRootSectionLane, getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
|
||||
import type { Board } from "../../_types";
|
||||
import classes from "./_layout-preview.module.css";
|
||||
|
||||
@@ -114,12 +114,15 @@ export default async function Layout(props: {
|
||||
})
|
||||
: null,
|
||||
);
|
||||
const assistantAvailabilityPromise: Promise<AssistantAvailability> = sessionPromise.then((session) => {
|
||||
const assistantAvailabilityPromise = sessionPromise.then(async (session): Promise<AssistantAvailability> => {
|
||||
if (!session) return "unauthenticated";
|
||||
return api.assistant
|
||||
.getAvailability()
|
||||
.then((availability) => (availability.enabled ? "enabled" : "unconfigured"))
|
||||
.catch(() => "error");
|
||||
|
||||
try {
|
||||
const availability = await api.assistant.getAvailability();
|
||||
return availability.enabled ? "enabled" : "unconfigured";
|
||||
} catch {
|
||||
return "error";
|
||||
}
|
||||
});
|
||||
const [session, user, serverSettings, colorScheme, assistantAvailability] = await Promise.all([
|
||||
sessionPromise,
|
||||
|
||||
+2
-1
@@ -50,7 +50,8 @@ function CustomWidgetManifestEditor({ form }: { form: CustomWidgetWorkbenchForm
|
||||
onChange={(next) => {
|
||||
setDraft(next);
|
||||
try {
|
||||
const parsed = customWidgetDefinitionSchema.safeParse(JSON.parse(next) as unknown);
|
||||
const value: unknown = JSON.parse(next);
|
||||
const parsed = customWidgetDefinitionSchema.safeParse(value);
|
||||
if (!parsed.success) {
|
||||
setError(parsed.error.issues[0]?.message ?? t("invalidWidget"));
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,8 @@ export function parseSources(value: string): Array<CustomWidgetSource & { id: st
|
||||
|
||||
export function parseJson(value: string): unknown {
|
||||
try {
|
||||
return JSON.parse(value) as unknown;
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,9 @@ const CustomWidgetFormView = memo(function CustomWidgetFormView(props: CustomWid
|
||||
className={classes.paneSwitcher}
|
||||
fullWidth
|
||||
value={props.mobilePane}
|
||||
onChange={(value) => props.setMobilePane(value as MobilePane)}
|
||||
onChange={(value) => {
|
||||
if (value === "configure" || value === "preview") props.setMobilePane(value);
|
||||
}}
|
||||
data={[
|
||||
{ value: "configure", label: w("configure") },
|
||||
{ value: "preview", label: w("section.preview") },
|
||||
|
||||
+3
-1
@@ -173,7 +173,9 @@ export function RequestBodyEditor({
|
||||
onChange={(next) => {
|
||||
setDraft(next);
|
||||
try {
|
||||
onChange(next.trim() ? (JSON.parse(next) as unknown) : undefined);
|
||||
let parsed: unknown;
|
||||
if (next.trim()) parsed = JSON.parse(next);
|
||||
onChange(parsed);
|
||||
setError(undefined);
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : t("invalidJson"));
|
||||
|
||||
@@ -2,7 +2,6 @@ import { headers } from "next/headers";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Stack } from "@mantine/core";
|
||||
|
||||
import { mcpRouter } from "@homarr/api/mcp";
|
||||
import { openApiDocument } from "@homarr/api/open-api";
|
||||
import { api } from "@homarr/api/server";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
@@ -16,40 +15,24 @@ import { ScalarApiReference } from "./components/scalar-api-reference";
|
||||
|
||||
import type { McpToolGroup } from "./components/api-page-tabs";
|
||||
|
||||
let cachedToolGroups: McpToolGroup[] | null = null;
|
||||
|
||||
function getMcpToolGroups(): McpToolGroup[] {
|
||||
if (cachedToolGroups) return cachedToolGroups;
|
||||
|
||||
const tools = extractMcpTools();
|
||||
|
||||
const procedures = (mcpRouter as any)._def.procedures as Record<string, { _def?: { type?: string } }>;
|
||||
const procedureTypeMap = new Map<string, "query" | "mutation">();
|
||||
for (const [key, proc] of Object.entries(procedures)) {
|
||||
const type = proc?._def?.type;
|
||||
if (type === "query" || type === "mutation") {
|
||||
procedureTypeMap.set(key, type);
|
||||
}
|
||||
}
|
||||
|
||||
const groups = new Map<string, McpToolGroup["tools"]>();
|
||||
for (const tool of tools) {
|
||||
const namespace = tool.pathInRouter[0] ?? "other";
|
||||
if (!groups.has(namespace)) {
|
||||
groups.set(namespace, []);
|
||||
}
|
||||
const procedureKey = tool.pathInRouter.join(".");
|
||||
groups.get(namespace)?.push({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
type: procedureTypeMap.get(procedureKey) ?? "query",
|
||||
type: tool.type,
|
||||
});
|
||||
}
|
||||
cachedToolGroups = Array.from(groups.entries()).map(([namespace, items]) => ({
|
||||
return Array.from(groups.entries()).map(([namespace, items]) => ({
|
||||
namespace,
|
||||
tools: items,
|
||||
}));
|
||||
return cachedToolGroups;
|
||||
}
|
||||
|
||||
export async function generateMetadata() {
|
||||
@@ -70,12 +53,14 @@ export default async function ApiPage() {
|
||||
if (!session?.user.permissions.includes("admin")) {
|
||||
notFound();
|
||||
}
|
||||
const requestHeaders = await headers();
|
||||
const [requestHeaders, apiKeys, t, toolGroups] = await Promise.all([
|
||||
headers(),
|
||||
api.apiKeys.getAll(),
|
||||
getI18n("management.page.tool.api.tab"),
|
||||
getMcpToolGroups(),
|
||||
]);
|
||||
const baseUrl = extractBaseUrlFromHeaders(requestHeaders);
|
||||
const document = openApiDocument(baseUrl);
|
||||
const apiKeys = await api.apiKeys.getAll();
|
||||
const t = await getI18n("management.page.tool.api.tab");
|
||||
const toolGroups = getMcpToolGroups();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
|
||||
import { widgetKinds } from "@homarr/definitions";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
|
||||
import { MainNavigation } from "~/components/layout/navigation";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
|
||||
@@ -327,20 +327,6 @@ const sumCompleteStepMetric = (
|
||||
return (values as number[]).reduce((sum, value) => sum + value, 0);
|
||||
};
|
||||
|
||||
const getProcedureTypeMap = () => {
|
||||
const procedures = (
|
||||
mcpRouter as unknown as {
|
||||
["_def"]: { procedures: Record<string, { ["_def"]?: { type?: string } }> };
|
||||
}
|
||||
)["_def"].procedures;
|
||||
return new Map(
|
||||
Object.entries(procedures).flatMap(([path, procedure]) => {
|
||||
const type = procedure["_def"]?.type;
|
||||
return type === "query" || type === "mutation" ? [[path, type] as const] : [];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const waitForDemoChunkAsync = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const createDemoAssistantResponse = (request: z.infer<typeof requestSchema>) => {
|
||||
@@ -385,7 +371,8 @@ export async function POST(request: Request) {
|
||||
const parsed = requestSchema.safeParse(
|
||||
(() => {
|
||||
try {
|
||||
return JSON.parse(requestBody) as unknown;
|
||||
const value: unknown = JSON.parse(requestBody);
|
||||
return value;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -516,14 +503,13 @@ export async function POST(request: Request) {
|
||||
const canAuthorCustomWidgets = session.user.permissions.includes("admin");
|
||||
const customWidgetAuthoringContext = getCustomWidgetAuthoringContext(incomingMessages, canAuthorCustomWidgets);
|
||||
const caller = mcpRouter.createCaller(context);
|
||||
const procedureTypes = getProcedureTypeMap();
|
||||
const omittedCustomWidgetTools = new Set<string>(customWidgetAuthoringContext.omittedToolNames);
|
||||
const mcpTools = extractMcpTools().filter((mcpTool) => !omittedCustomWidgetTools.has(mcpTool.name));
|
||||
const customWidgetComponentDocumentBudget = createCustomWidgetComponentDocumentBudget();
|
||||
|
||||
const homarrTools = Object.fromEntries(
|
||||
mcpTools.map((mcpTool) => {
|
||||
const requiresApproval = procedureTypes.get(mcpTool.pathInRouter.join(".")) === "mutation";
|
||||
const requiresApproval = mcpTool.type === "mutation";
|
||||
return [
|
||||
mcpTool.name,
|
||||
tool({
|
||||
@@ -571,11 +557,9 @@ export async function POST(request: Request) {
|
||||
}),
|
||||
) satisfies ToolSet;
|
||||
const toolApproval = Object.fromEntries(
|
||||
mcpTools.flatMap((mcpTool) =>
|
||||
procedureTypes.get(mcpTool.pathInRouter.join(".")) === "mutation"
|
||||
? [[mcpTool.name, "user-approval" as const]]
|
||||
: [],
|
||||
),
|
||||
mcpTools
|
||||
.filter((mcpTool) => mcpTool.type === "mutation")
|
||||
.map((mcpTool) => [mcpTool.name, "user-approval" as const]),
|
||||
);
|
||||
|
||||
const frontendTools = Object.fromEntries(
|
||||
|
||||
@@ -46,18 +46,21 @@ vi.mock("../_extract-tools", () => ({
|
||||
{
|
||||
name: "board_getAllBoards",
|
||||
description: "List boards",
|
||||
type: "query",
|
||||
pathInRouter: ["board", "getAllBoards"],
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "customWidget_list",
|
||||
description: "List Custom Widgets",
|
||||
type: "query",
|
||||
pathInRouter: ["customWidget", "list"],
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
{
|
||||
name: "customWidget_workshopSearch",
|
||||
description: "Search Workshop",
|
||||
type: "query",
|
||||
pathInRouter: ["customWidget", "workshopSearch"],
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
},
|
||||
|
||||
@@ -80,18 +80,10 @@ function clearAuthFailures(ip: string | null) {
|
||||
authFailures.delete(ip ?? "unknown");
|
||||
}
|
||||
|
||||
let toolsCache: McpTool[] | null = null;
|
||||
|
||||
function getTools() {
|
||||
if (!toolsCache) {
|
||||
toolsCache = extractMcpTools();
|
||||
logger.info(`Extracted ${toolsCache.length} MCP tools`);
|
||||
}
|
||||
return toolsCache;
|
||||
}
|
||||
|
||||
const getVisibleTools = (isAdmin: boolean) =>
|
||||
isAdmin ? getTools() : getTools().filter((tool) => !tool.name.startsWith("customWidget_"));
|
||||
const getVisibleTools = (tools: McpTool[], isAdmin: boolean) => {
|
||||
if (isAdmin) return tools;
|
||||
return tools.filter((tool) => !tool.name.startsWith("customWidget_"));
|
||||
};
|
||||
|
||||
function registerCustomWidgetAuthoring(server: McpServer) {
|
||||
server.registerPrompt(
|
||||
@@ -336,7 +328,7 @@ export const handleMcpRequest = async (req: NextRequest) => {
|
||||
const isAdmin = session.user.permissions.includes("admin");
|
||||
const protocolHandler = createMcpProtocolHandler({
|
||||
caller,
|
||||
tools: getVisibleTools(isAdmin),
|
||||
tools: getVisibleTools(extractMcpTools(), isAdmin),
|
||||
version: getPackageVersion(),
|
||||
instructions: SERVER_INSTRUCTIONS,
|
||||
configureServer: isAdmin ? registerCustomWidgetAuthoring : undefined,
|
||||
|
||||
@@ -50,6 +50,7 @@ const echoValidator = z.object({ message: z.string() });
|
||||
const echoTool: McpTool = {
|
||||
name: "echo",
|
||||
description: "Echo a message",
|
||||
type: "query",
|
||||
pathInRouter: ["echo"],
|
||||
inputValidator: echoValidator,
|
||||
inputSchema: z.toJSONSchema(echoValidator),
|
||||
|
||||
@@ -2,25 +2,32 @@ import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { widgetIntegrationSupport } from "@homarr/definitions";
|
||||
import { widgetImports } from "@homarr/widgets";
|
||||
import { loadAllWidgetDefinitions } from "@homarr/widgets/manifest";
|
||||
|
||||
import { getAssistantWidgetConfiguration } from "./assistant-widget-tool";
|
||||
|
||||
const settings = { enableStatusByDefault: true, forceDisableStatus: false };
|
||||
const widgetDefinitions = await loadAllWidgetDefinitions();
|
||||
|
||||
const getWidgetDefinition = (kind: WidgetKind) => {
|
||||
const definition = widgetDefinitions.get(kind);
|
||||
if (!definition) throw new Error(`No definition is registered for ${kind}`);
|
||||
return definition;
|
||||
};
|
||||
|
||||
describe("getAssistantWidgetConfiguration", () => {
|
||||
test("keeps server-side integration support synchronized with widget definitions", () => {
|
||||
for (const [kind, widgetImport] of Object.entries(widgetImports)) {
|
||||
const definition = widgetImport.definition;
|
||||
for (const [kind, definition] of widgetDefinitions) {
|
||||
const expected = "supportedIntegrations" in definition ? definition.supportedIntegrations : undefined;
|
||||
const supportedIntegrations = widgetIntegrationSupport[kind];
|
||||
|
||||
expect(widgetIntegrationSupport[kind as WidgetKind], kind).toEqual(expected);
|
||||
expect(supportedIntegrations, kind).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves generated notebook content and fills its hidden defaults", () => {
|
||||
const result = getAssistantWidgetConfiguration(
|
||||
widgetImports.notebook.definition,
|
||||
getWidgetDefinition("notebook"),
|
||||
{
|
||||
boardId: "board-1",
|
||||
boardName: "Home",
|
||||
@@ -42,7 +49,7 @@ describe("getAssistantWidgetConfiguration", () => {
|
||||
|
||||
test("keeps only accessible, compatible integrations and respects the widget maximum", () => {
|
||||
const result = getAssistantWidgetConfiguration(
|
||||
widgetImports.audioStats.definition,
|
||||
getWidgetDefinition("audioStats"),
|
||||
{
|
||||
boardId: "board-1",
|
||||
boardName: "Home",
|
||||
@@ -90,13 +97,13 @@ describe("getAssistantWidgetConfiguration", () => {
|
||||
|
||||
test("treats integration-backed widgets as required unless explicitly optional", () => {
|
||||
const mediaServer = getAssistantWidgetConfiguration(
|
||||
widgetImports.mediaServer.definition,
|
||||
getWidgetDefinition("mediaServer"),
|
||||
{ boardId: "board-1", boardName: "Home", kind: "mediaServer", summary: "Show Plex streams" },
|
||||
settings,
|
||||
[],
|
||||
);
|
||||
const calendar = getAssistantWidgetConfiguration(
|
||||
widgetImports.calendar.definition,
|
||||
getWidgetDefinition("calendar"),
|
||||
{ boardId: "board-1", boardName: "Home", kind: "calendar", summary: "Show releases" },
|
||||
settings,
|
||||
[],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { IconLayoutGrid } from "@tabler/icons-react";
|
||||
import type { BoardPreviewData } from "@homarr/boards/layout-preview";
|
||||
import { projectBoardLayout } from "@homarr/boards/layout-preview";
|
||||
import { boardLanes, getBoardLaneColumnCount, getRootSectionLane } from "@homarr/definitions";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
|
||||
import classes from "./board-layout-thumbnail.module.css";
|
||||
|
||||
|
||||
@@ -63,14 +63,14 @@ describe("item actions create-item", () => {
|
||||
{
|
||||
layoutId,
|
||||
height: 1,
|
||||
width: 1,
|
||||
width: 2,
|
||||
...emptyPosition,
|
||||
sectionId: firstSectionId,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(emptyPositionSpy).toHaveBeenCalledWith([], layout.columnCount, undefined, { height: 1, width: 1 });
|
||||
expect(emptyPositionSpy).toHaveBeenCalledWith([], layout.columnCount, undefined, { height: 1, width: 2 });
|
||||
});
|
||||
test("should correctly pass containers and items to getFirstEmptyPosition", () => {
|
||||
// Arrange
|
||||
@@ -118,14 +118,14 @@ describe("item actions create-item", () => {
|
||||
expect(createdItem).toEqual(
|
||||
expect.objectContaining({
|
||||
kind: itemKind,
|
||||
layouts: [{ ...emptyPosition, height: 1, width: 1, sectionId: firstSectionId, layoutId }],
|
||||
layouts: [{ ...emptyPosition, height: 1, width: 2, sectionId: firstSectionId, layoutId }],
|
||||
}),
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
[expect.objectContaining(itemAndSectionPosition), expect.objectContaining(itemAndSectionPosition)],
|
||||
layout.columnCount,
|
||||
undefined,
|
||||
{ height: 1, width: 1 },
|
||||
{ height: 1, width: 2 },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import { nativeFeatureCapabilities, widgetKinds } from "@homarr/definitions";
|
||||
import { widgetIntegrationSupport, widgetKinds } from "@homarr/definitions";
|
||||
|
||||
export interface BoardRecipeRecommendation {
|
||||
widgetKind: WidgetKind;
|
||||
@@ -36,10 +36,10 @@ export const getBoardRecipeRecommendations = ({
|
||||
return widgetKinds
|
||||
.filter((widgetKind) => !existingKinds.has(widgetKind))
|
||||
.flatMap((widgetKind) => {
|
||||
const capability = nativeFeatureCapabilities[widgetKind as keyof typeof nativeFeatureCapabilities];
|
||||
if (!capability) return [];
|
||||
const supportedIntegrations = widgetIntegrationSupport[widgetKind];
|
||||
if (!supportedIntegrations) return [];
|
||||
|
||||
const matches = capability.integrations.filter((integrationKind) => availableKinds.has(integrationKind));
|
||||
const matches = supportedIntegrations.filter((integrationKind) => availableKinds.has(integrationKind));
|
||||
if (matches.length === 0) return [];
|
||||
|
||||
const preferredMatch = preferredIntegrationKind && matches.includes(preferredIntegrationKind);
|
||||
@@ -51,10 +51,10 @@ export const getBoardRecipeRecommendations = ({
|
||||
widgetKind,
|
||||
integrationKind,
|
||||
isNewlyAvailable: Boolean(preferredMatch),
|
||||
score: (preferredMatch ? 1_000 : 0) + matches.length * 10 - capability.integrations.length,
|
||||
score: (preferredMatch ? 1_000 : 0) + matches.length * 10 - supportedIntegrations.length,
|
||||
},
|
||||
];
|
||||
})
|
||||
.sort((left, right) => right.score - left.score || left.widgetKind.localeCompare(right.widgetKind))
|
||||
.toSorted((left, right) => right.score - left.score || left.widgetKind.localeCompare(right.widgetKind))
|
||||
.slice(0, Math.max(0, limit));
|
||||
};
|
||||
@@ -42,7 +42,7 @@ import { useSettings } from "@homarr/settings";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { SelectableCard } from "@homarr/ui";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
import { loadWidgetDefinition, reduceWidgetOptionsWithDefinition } from "@homarr/widgets/manifest";
|
||||
|
||||
import type { EmptySection } from "~/app/[locale]/boards/_types";
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { NormalizedWidgetQuery, WidgetDefinition, WidgetQueryMatcherScope } from "@homarr/widgets";
|
||||
import { getWidgetQueryKeys, widgetImports } from "@homarr/widgets";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import type { NormalizedWidgetQuery, WidgetQueryMatcherScope } from "@homarr/widgets";
|
||||
import { getWidgetQueryKeys } from "@homarr/widgets";
|
||||
import { loadAllWidgetDefinitions } from "@homarr/widgets/manifest";
|
||||
|
||||
import { matchesWidgetItemQuery } from "./widget-query-scope";
|
||||
|
||||
type WidgetKind = keyof typeof widgetImports;
|
||||
const widgetDefinitions = await loadAllWidgetDefinitions();
|
||||
const allPhotosAlbumId = "all";
|
||||
|
||||
const createScope = (overrides: Partial<WidgetQueryMatcherScope> = {}): WidgetQueryMatcherScope => ({
|
||||
@@ -19,7 +21,9 @@ const createScope = (overrides: Partial<WidgetQueryMatcherScope> = {}): WidgetQu
|
||||
});
|
||||
|
||||
const matches = (kind: WidgetKind, path: readonly string[], input: unknown, scope: WidgetQueryMatcherScope) => {
|
||||
const definition = widgetImports[kind].definition as WidgetDefinition & { kind: string };
|
||||
const definition = widgetDefinitions.get(kind);
|
||||
if (!definition) throw new Error(`No definition is registered for ${kind}`);
|
||||
|
||||
const queryKey: QueryKey = [path, { input, type: "query" }];
|
||||
return matchesWidgetItemQuery(queryKey, getWidgetQueryKeys(definition), scope, definition.queryMatcher);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useEditMode } from "@homarr/boards/edit-mode";
|
||||
import { getBoardLaneColumnCount, getRootSectionLane, getWidgetName } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { widgetCatalogIcons } from "@homarr/widgets/catalog";
|
||||
import { widgetCatalogIcons } from "@homarr/ui/widget-icons";
|
||||
|
||||
import type { EmptySection } from "~/app/[locale]/boards/_types";
|
||||
import { useRegisteredGridEditors } from "~/components/board/sections/grid/grid-editor-registry";
|
||||
|
||||
Reference in new issue
Block a user