refactor: streamline widget and integration architecture
This commit is contained in:
1 parent
ec295fe26e
commit
425cb653a1
243 files changed
+5722
-2255
No files matched your search
@@ -74,7 +74,6 @@ const unavailableMessageKeys = {
|
||||
|
||||
export const AssistantGate = ({ availability, children }: AssistantGateProps) => {
|
||||
const t = useI18n("assistant");
|
||||
|
||||
if (availability === "enabled") {
|
||||
return <EnabledAssistantRoot>{children}</EnabledAssistantRoot>;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
/**
|
||||
* Keeps the assistant discoverable in the spotlight when it is unavailable, without loading the
|
||||
* assistant runtime. Lives apart from `assistant-provider` so the disabled path stays cheap.
|
||||
* assistant runtime. Lives apart from `assistant-provider` so the unavailable path stays cheap.
|
||||
*/
|
||||
export const useRegisterAssistantSpotlightPlaceholder = (description: string) => {
|
||||
const t = useI18n("assistant");
|
||||
|
||||
@@ -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,
|
||||
[],
|
||||
|
||||
@@ -55,6 +55,6 @@ export const getBoardRecipeRecommendations = ({
|
||||
},
|
||||
];
|
||||
})
|
||||
.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));
|
||||
};
|
||||
@@ -91,6 +91,7 @@ const ItemSelectModalContent = ({
|
||||
initialSearch = "",
|
||||
}: ItemSelectModalContentProps) => {
|
||||
const [search, setSearch] = useState(initialSearch);
|
||||
const canBrowseCustomWidgets = isAdmin;
|
||||
const t = useI18n();
|
||||
const board = useRequiredBoard();
|
||||
const currentLayoutId = useCurrentLayout();
|
||||
@@ -479,7 +480,7 @@ const ItemSelectModalContent = ({
|
||||
/>
|
||||
))}
|
||||
|
||||
{isAdmin && (
|
||||
{canBrowseCustomWidgets && (
|
||||
<>
|
||||
<Divider
|
||||
label={
|
||||
@@ -534,7 +535,7 @@ const ItemSelectModalContent = ({
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
{filteredItems.length === 0 && (!isAdmin || filteredCustomWidgets.length === 0) && (
|
||||
{filteredItems.length === 0 && (!canBrowseCustomWidgets || filteredCustomWidgets.length === 0) && (
|
||||
<Center p="xl">
|
||||
<Text c="dimmed">{t("common.noResults")}</Text>
|
||||
</Center>
|
||||
@@ -653,12 +654,13 @@ const ItemSelectModalFrame = ({
|
||||
const utils = clientApi.useUtils();
|
||||
const { data: session } = useSession();
|
||||
const isAdmin = session?.user.permissions.includes("admin") ?? false;
|
||||
const canBrowseCustomWidgets = isAdmin;
|
||||
const canCreateIntegration = session?.user.permissions.includes("integration-create") ?? false;
|
||||
const t = useI18n();
|
||||
const { data: integrationData } = clientApi.integration.all.useQuery();
|
||||
const { data: customWidgetDefs } = clientApi.customWidget.available.useQuery(
|
||||
{ boardId: innerProps.boardId },
|
||||
{ enabled: isAdmin },
|
||||
{ enabled: canBrowseCustomWidgets },
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user