Merge remote-tracking branch 'origin/release/v2' into feat/widget-integration-dx
# Conflicts: # packages/api/src/router/board.ts
This commit is contained in:
commit
c81088d876
61 files changed
+1576
-874
No files matched your search
@@ -856,6 +856,7 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
: t("busy.description"),
|
||||
unavailable: !canSend,
|
||||
alwaysVisible: true,
|
||||
placement: "fallback" as const,
|
||||
interaction: (query: string) =>
|
||||
createAssistantPromptInteraction({
|
||||
sendPrompt,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const useRegisterAssistantSpotlightPlaceholder = (description: string) =>
|
||||
description,
|
||||
unavailable: true,
|
||||
alwaysVisible: true,
|
||||
placement: "fallback" as const,
|
||||
interaction: (_query: string) => ({ type: "none" as const }),
|
||||
}),
|
||||
[description, t],
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { getSafeAppHref } from "@homarr/common";
|
||||
import type { ContextSpecificItem } from "@homarr/spotlight";
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { hasPendingSpotlightOpen, spotlightOpenEvent } from "@homarr/spotlight/open";
|
||||
|
||||
export const BoardAppsSpotlightRegistrar = () => {
|
||||
const board = useRequiredBoard();
|
||||
const [isSpotlightActivated, setIsSpotlightActivated] = useState(() => hasPendingSpotlightOpen());
|
||||
|
||||
useEffect(() => {
|
||||
if (isSpotlightActivated) return;
|
||||
|
||||
const activateSpotlightQueries = () => setIsSpotlightActivated(true);
|
||||
window.addEventListener(spotlightOpenEvent, activateSpotlightQueries);
|
||||
|
||||
if (hasPendingSpotlightOpen()) activateSpotlightQueries();
|
||||
|
||||
return () => window.removeEventListener(spotlightOpenEvent, activateSpotlightQueries);
|
||||
}, [isSpotlightActivated]);
|
||||
|
||||
const openInNewTabByAppId = useMemo(() => {
|
||||
const result = new Map<string, boolean>();
|
||||
|
||||
for (const item of board.items) {
|
||||
if (item.kind !== "app" || typeof item.options.appId !== "string" || item.options.appId.length === 0) continue;
|
||||
|
||||
const existingOpenInNewTab = result.get(item.options.appId) ?? false;
|
||||
result.set(item.options.appId, existingOpenInNewTab || item.options.openInNewTab === true);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [board.items]);
|
||||
const appIds = useMemo(() => Array.from(openInNewTabByAppId.keys()), [openInNewTabByAppId]);
|
||||
const activeAppIds = isSpotlightActivated ? appIds : [];
|
||||
const appQueries = clientApi.useQueries((trpc) => activeAppIds.map((id) => trpc.app.byId({ id })));
|
||||
const apps = appQueries.flatMap((query) => query.data ?? []);
|
||||
const spotlightItems = useMemo<ContextSpecificItem[]>(() => {
|
||||
return apps.flatMap((app) => {
|
||||
const href = getSafeAppHref(app.href);
|
||||
if (!href) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
id: app.id,
|
||||
dedupeKey: `app:${app.id}`,
|
||||
name: app.name,
|
||||
icon: app.iconUrl,
|
||||
interaction() {
|
||||
return {
|
||||
type: "link" as const,
|
||||
href,
|
||||
// Opening a new tab wins if any widget instance requests it.
|
||||
newTab: openInNewTabByAppId.get(app.id) ?? false,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
}, [apps, openInNewTabByAppId]);
|
||||
|
||||
useRegisterSpotlightContextResults("board-apps", spotlightItems, [spotlightItems]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import { IconDeviceMobile, IconHomeFilled, IconLayoutBoard, IconSearch } from "@
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useOptionalBoard } from "@homarr/boards/context";
|
||||
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { Link, UserAvatar } from "@homarr/ui";
|
||||
|
||||
@@ -41,6 +42,7 @@ interface BoardSwitcherProps {
|
||||
|
||||
export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
|
||||
const t = useI18n("board.action.switcher");
|
||||
const tBoard = useI18n("board");
|
||||
const manageBoardsT = useI18n("management.page.board");
|
||||
const currentBoard = useOptionalBoard();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -85,6 +87,20 @@ export const BoardSwitcher = ({ children }: BoardSwitcherProps) => {
|
||||
}, []);
|
||||
const preloadBoards = () => void utils.board.getManageOverview.prefetch({ fullPreview: true });
|
||||
|
||||
const spotlightAction = useMemo(
|
||||
() => ({
|
||||
id: "open-board-switcher",
|
||||
name: tBoard("action.switch"),
|
||||
icon: IconLayoutBoard,
|
||||
interaction: () => ({
|
||||
type: "javaScript" as const,
|
||||
onSelect: openSwitcher,
|
||||
}),
|
||||
}),
|
||||
[openSwitcher, tBoard],
|
||||
);
|
||||
useRegisterSpotlightContextActions("board-switcher", [spotlightAction], [spotlightAction]);
|
||||
|
||||
useHotkeys([[boardSwitcherHotkey, openSwitcher, { preventDefault: true }]]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { HotkeyItem } from "@mantine/hooks";
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
|
||||
import { hotkeys } from "@homarr/definitions";
|
||||
@@ -13,23 +14,18 @@ const Spotlight = dynamic(() => loadSpotlight().then(({ Spotlight: Component })
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
const spotlightHotkeys: HotkeyItem[] = [
|
||||
[hotkeys.openSpotlight, () => openSpotlight(), { preventDefault: true }],
|
||||
[hotkeys.openSpotlightApps, () => openSpotlight({ mode: "apps" }), { preventDefault: true }],
|
||||
[hotkeys.openSpotlightAssistant, () => openSpotlight({ mode: "assistant" }), { preventDefault: true }],
|
||||
[hotkeys.openSpotlightPreferences, () => openSpotlight({ mode: "preferences" }), { preventDefault: true }],
|
||||
];
|
||||
|
||||
export const LazySpotlight = () => {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const [isPreloaded, setIsPreloaded] = useState(false);
|
||||
|
||||
useHotkeys(
|
||||
isMounted
|
||||
? []
|
||||
: [
|
||||
[
|
||||
hotkeys.openSpotlight,
|
||||
() => {
|
||||
setIsMounted(true);
|
||||
openSpotlight();
|
||||
},
|
||||
],
|
||||
],
|
||||
);
|
||||
useHotkeys(spotlightHotkeys, [], true);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
@@ -61,12 +57,8 @@ export const LazySpotlight = () => {
|
||||
return isMounted || isPreloaded ? (
|
||||
<>
|
||||
{isPreloaded && <span hidden data-homarr-dev-benchmark-spotlight-preloaded />}
|
||||
{isMounted && (
|
||||
<>
|
||||
<span hidden data-homarr-dev-benchmark-spotlight-mounted />
|
||||
<Spotlight />
|
||||
</>
|
||||
)}
|
||||
{isMounted && <span hidden data-homarr-dev-benchmark-spotlight-mounted />}
|
||||
<Spotlight />
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export const DesktopSearchInput = () => {
|
||||
w={400}
|
||||
size="sm"
|
||||
leftSection={<IconSearch size={20} stroke={1.5} />}
|
||||
onClick={openSpotlight}
|
||||
onClick={() => openSpotlight()}
|
||||
radius="xl"
|
||||
>
|
||||
{`${t("search.placeholder")}...`}
|
||||
@@ -32,7 +32,7 @@ export const MobileSearchButton = () => {
|
||||
return (
|
||||
<HeaderButton
|
||||
data-homarr-dev-benchmark-interaction="search"
|
||||
onClick={openSpotlight}
|
||||
onClick={() => openSpotlight()}
|
||||
className={classes.mobileSearch}
|
||||
>
|
||||
<IconSearch size={20} stroke={1.5} />
|
||||
|
||||
Reference in new issue
Block a user