refactor: simplify widget and integration architecture

This commit is contained in:
ajnart committed 2026-08-24 00:57:36 +02:00
1 parent c988c35b29
commit 8f9efd0074
179 files changed
+1509 -4187

No files matched your search

@@ -74,6 +74,7 @@ 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 unavailable path stays cheap.
* assistant runtime. Lives apart from `assistant-provider` so the disabled path stays cheap.
*/
export const useRegisterAssistantSpotlightPlaceholder = (description: string) => {
const t = useI18n("assistant");
@@ -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 { widgetIntegrationConfigs, 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];
const capability = widgetIntegrationConfigs[widgetKind as keyof typeof widgetIntegrationConfigs];
if (!capability) return [];
const matches = capability.integrations.filter((integrationKind) => availableKinds.has(integrationKind));
const matches = capability.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 - capability.supportedIntegrations.length,
},
];
})
.toSorted((left, right) => right.score - left.score || left.widgetKind.localeCompare(right.widgetKind))
.sort((left, right) => right.score - left.score || left.widgetKind.localeCompare(right.widgetKind))
.slice(0, Math.max(0, limit));
};
@@ -91,7 +91,6 @@ const ItemSelectModalContent = ({
initialSearch = "",
}: ItemSelectModalContentProps) => {
const [search, setSearch] = useState(initialSearch);
const canBrowseCustomWidgets = isAdmin;
const t = useI18n();
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
@@ -480,7 +479,7 @@ const ItemSelectModalContent = ({
/>
))}
{canBrowseCustomWidgets && (
{isAdmin && (
<>
<Divider
label={
@@ -535,7 +534,7 @@ const ItemSelectModalContent = ({
)}
</SimpleGrid>
{filteredItems.length === 0 && (!canBrowseCustomWidgets || filteredCustomWidgets.length === 0) && (
{filteredItems.length === 0 && (!isAdmin || filteredCustomWidgets.length === 0) && (
<Center p="xl">
<Text c="dimmed">{t("common.noResults")}</Text>
</Center>
@@ -654,13 +653,12 @@ 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: canBrowseCustomWidgets },
{ enabled: isAdmin },
);
return (