feat: add context specific search and actions (#1570)
This commit is contained in:
@@ -13,6 +13,7 @@ import { env } from "@homarr/auth/env.mjs";
|
|||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { ModalProvider } from "@homarr/modals";
|
import { ModalProvider } from "@homarr/modals";
|
||||||
import { Notifications } from "@homarr/notifications";
|
import { Notifications } from "@homarr/notifications";
|
||||||
|
import { SpotlightProvider } from "@homarr/spotlight";
|
||||||
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
|
||||||
import { getI18nMessages } from "@homarr/translation/server";
|
import { getI18nMessages } from "@homarr/translation/server";
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export default async function Layout(props: { children: React.ReactNode; params:
|
|||||||
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
(innerProps) => <NextIntlClientProvider {...innerProps} messages={i18nMessages} />,
|
||||||
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
(innerProps) => <CustomMantineProvider {...innerProps} defaultColorScheme={colorScheme} />,
|
||||||
(innerProps) => <ModalProvider {...innerProps} />,
|
(innerProps) => <ModalProvider {...innerProps} />,
|
||||||
|
(innerProps) => <SpotlightProvider {...innerProps} />,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { parseAppHrefWithVariables } from "./base";
|
import { parseAppHrefWithVariables } from "./base";
|
||||||
|
|
||||||
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
export const parseAppHrefWithVariablesClient = <TInput extends string | null>(url: TInput): TInput => {
|
||||||
|
if (typeof window === "undefined") return url;
|
||||||
return parseAppHrefWithVariables(url, window.location.href);
|
return parseAppHrefWithVariables(url, window.location.href);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export const SpotlightGroupActions = <TOption extends Record<string, unknown>>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Array.isArray(options)) {
|
if (Array.isArray(options)) {
|
||||||
|
if (options.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const filteredOptions = options
|
const filteredOptions = options
|
||||||
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
.filter((option) => ("filter" in group ? group.filter(query, option) : false))
|
||||||
.sort((optionA, optionB) => {
|
.sort((optionA, optionB) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
import { ActionIcon, Center, Group, Kbd } from "@mantine/core";
|
||||||
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
import { Spotlight as MantineSpotlight } from "@mantine/spotlight";
|
||||||
import { IconSearch, IconX } from "@tabler/icons-react";
|
import { IconSearch, IconX } from "@tabler/icons-react";
|
||||||
@@ -12,6 +12,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
import type { inferSearchInteractionOptions } from "../lib/interaction";
|
||||||
import type { SearchMode } from "../lib/mode";
|
import type { SearchMode } from "../lib/mode";
|
||||||
import { searchModes } from "../modes";
|
import { searchModes } from "../modes";
|
||||||
|
import { useSpotlightContextResults } from "../modes/home/context";
|
||||||
import { selectAction, spotlightStore } from "../spotlight-store";
|
import { selectAction, spotlightStore } from "../spotlight-store";
|
||||||
import { SpotlightChildrenActions } from "./actions/children-actions";
|
import { SpotlightChildrenActions } from "./actions/children-actions";
|
||||||
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
import { SpotlightActionGroups } from "./actions/groups/action-group";
|
||||||
@@ -19,24 +20,45 @@ import { SpotlightActionGroups } from "./actions/groups/action-group";
|
|||||||
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
type SearchModeKey = keyof TranslationObject["search"]["mode"];
|
||||||
|
|
||||||
export const Spotlight = () => {
|
export const Spotlight = () => {
|
||||||
const searchModeState = useState<SearchModeKey>("help");
|
const items = useSpotlightContextResults();
|
||||||
|
// We fallback to help if no context results are available
|
||||||
|
const defaultMode = items.length >= 1 ? "home" : "help";
|
||||||
|
const searchModeState = useState<SearchModeKey>(defaultMode);
|
||||||
const mode = searchModeState[0];
|
const mode = searchModeState[0];
|
||||||
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
const activeMode = useMemo(() => searchModes.find((searchMode) => searchMode.modeKey === mode), [mode]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The below logic is used to switch to home page if any context results are registered
|
||||||
|
* or to help page if context results are unregistered
|
||||||
|
*/
|
||||||
|
const previousLengthRef = useRef(items.length);
|
||||||
|
useEffect(() => {
|
||||||
|
if (items.length >= 1 && previousLengthRef.current === 0) {
|
||||||
|
searchModeState[1]("home");
|
||||||
|
} else if (items.length === 0 && previousLengthRef.current >= 1) {
|
||||||
|
searchModeState[1]("help");
|
||||||
|
}
|
||||||
|
|
||||||
|
previousLengthRef.current = items.length;
|
||||||
|
}, [items.length, searchModeState]);
|
||||||
|
|
||||||
if (!activeMode) {
|
if (!activeMode) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
// We use the "key" below to prevent the 'Different amounts of hooks' error
|
||||||
return <SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} />;
|
return (
|
||||||
|
<SpotlightWithActiveMode key={mode} modeState={searchModeState} activeMode={activeMode} defaultMode={defaultMode} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SpotlightWithActiveModeProps {
|
interface SpotlightWithActiveModeProps {
|
||||||
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
modeState: [SearchModeKey, Dispatch<SetStateAction<SearchModeKey>>];
|
||||||
activeMode: SearchMode;
|
activeMode: SearchMode;
|
||||||
|
defaultMode: SearchModeKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveModeProps) => {
|
const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: SpotlightWithActiveModeProps) => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [mode, setMode] = modeState;
|
const [mode, setMode] = modeState;
|
||||||
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
const [childrenOptions, setChildrenOptions] = useState<inferSearchInteractionOptions<"children"> | null>(null);
|
||||||
@@ -50,12 +72,12 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
<MantineSpotlight.Root
|
<MantineSpotlight.Root
|
||||||
yOffset={8}
|
yOffset={8}
|
||||||
onSpotlightClose={() => {
|
onSpotlightClose={() => {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}}
|
}}
|
||||||
query={query}
|
query={query}
|
||||||
onQueryChange={(query) => {
|
onQueryChange={(query) => {
|
||||||
if (mode !== "help" || query.length !== 1) {
|
if ((mode !== "help" && mode !== "home") || query.length !== 1) {
|
||||||
setQuery(query);
|
setQuery(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +95,13 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
<MantineSpotlight.Search
|
<MantineSpotlight.Search
|
||||||
placeholder={`${t("search.placeholder")}...`}
|
placeholder={`${t("search.placeholder")}...`}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
leftSectionWidth={activeMode.modeKey !== "help" ? 80 : 48}
|
leftSectionWidth={activeMode.modeKey !== defaultMode ? 80 : 48}
|
||||||
leftSection={
|
leftSection={
|
||||||
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
<Group align="center" wrap="nowrap" gap="xs" w="100%" h="100%">
|
||||||
<Center w={48} h="100%">
|
<Center w={48} h="100%">
|
||||||
<IconSearch stroke={1.5} />
|
<IconSearch stroke={1.5} />
|
||||||
</Center>
|
</Center>
|
||||||
{activeMode.modeKey !== "help" ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
{activeMode.modeKey !== defaultMode ? <Kbd size="sm">{activeMode.character}</Kbd> : null}
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -88,10 +110,10 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
rightSection={
|
rightSection={
|
||||||
mode === "help" ? undefined : (
|
mode === defaultMode ? undefined : (
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
@@ -103,8 +125,8 @@ const SpotlightWithActiveMode = ({ modeState, activeMode }: SpotlightWithActiveM
|
|||||||
}
|
}
|
||||||
value={query}
|
value={query}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (query.length === 0 && mode !== "help" && event.key === "Backspace") {
|
if (query.length === 0 && mode !== defaultMode && event.key === "Backspace") {
|
||||||
setMode("help");
|
setMode(defaultMode);
|
||||||
setChildrenOptions(null);
|
setChildrenOptions(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,5 +4,10 @@ import { spotlightActions } from "./spotlight-store";
|
|||||||
|
|
||||||
export { Spotlight } from "./components/spotlight";
|
export { Spotlight } from "./components/spotlight";
|
||||||
export { openSpotlight };
|
export { openSpotlight };
|
||||||
|
export {
|
||||||
|
SpotlightProvider,
|
||||||
|
useRegisterSpotlightContextResults,
|
||||||
|
useRegisterSpotlightContextActions,
|
||||||
|
} from "./modes/home/context";
|
||||||
|
|
||||||
const openSpotlight = spotlightActions.open;
|
const openSpotlight = spotlightActions.open;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { SearchGroup } from "./group";
|
|||||||
|
|
||||||
export type SearchMode = {
|
export type SearchMode = {
|
||||||
modeKey: keyof TranslationObject["search"]["mode"];
|
modeKey: keyof TranslationObject["search"]["mode"];
|
||||||
character: string;
|
character: string | undefined;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
groups: SearchGroup[];
|
groups: SearchGroup[];
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { ContextSpecificItem } from "../home/context";
|
||||||
|
import { useSpotlightContextActions } from "../home/context";
|
||||||
|
|
||||||
|
export const contextSpecificActionsSearchGroups = createGroup<ContextSpecificItem>({
|
||||||
|
title: (t) => t("search.mode.command.group.localCommand.title"),
|
||||||
|
keyPath: "id",
|
||||||
|
Component(option) {
|
||||||
|
const icon =
|
||||||
|
typeof option.icon !== "string" ? (
|
||||||
|
<option.icon size={24} />
|
||||||
|
) : (
|
||||||
|
<img width={24} height={24} src={option.icon} alt={option.name} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||||
|
{icon}
|
||||||
|
<Text>{option.name}</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
useInteraction(option) {
|
||||||
|
return option.interaction();
|
||||||
|
},
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
return useSpotlightContextActions();
|
||||||
|
},
|
||||||
|
});
|
||||||
166
packages/spotlight/src/modes/command/global-group.tsx
Normal file
166
packages/spotlight/src/modes/command/global-group.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||||
|
import type { TablerIcon } from "@tabler/icons-react";
|
||||||
|
import {
|
||||||
|
IconBox,
|
||||||
|
IconCategoryPlus,
|
||||||
|
IconFileImport,
|
||||||
|
IconLanguage,
|
||||||
|
IconMailForward,
|
||||||
|
IconMoon,
|
||||||
|
IconPlug,
|
||||||
|
IconSun,
|
||||||
|
IconUserPlus,
|
||||||
|
IconUsersGroup,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { useSession } from "@homarr/auth/client";
|
||||||
|
import { useModalAction } from "@homarr/modals";
|
||||||
|
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
||||||
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
import { interaction } from "../../lib/interaction";
|
||||||
|
import { languageChildrenOptions } from "./children/language";
|
||||||
|
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
||||||
|
|
||||||
|
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
||||||
|
commandKey: string;
|
||||||
|
icon: TablerIcon;
|
||||||
|
name: string;
|
||||||
|
useInteraction: (
|
||||||
|
_c: Command<TSearchInteraction>,
|
||||||
|
query: string,
|
||||||
|
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const globalCommandGroup = createGroup<Command>({
|
||||||
|
keyPath: "commandKey",
|
||||||
|
title: "Global commands",
|
||||||
|
useInteraction: (option, query) => option.useInteraction(option, query),
|
||||||
|
Component: ({ icon: Icon, name }) => (
|
||||||
|
<Group px="md" py="sm">
|
||||||
|
<Icon stroke={1.5} />
|
||||||
|
<Text>{name}</Text>
|
||||||
|
</Group>
|
||||||
|
),
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
const commands: (Command & { hidden?: boolean })[] = [
|
||||||
|
{
|
||||||
|
commandKey: "colorScheme",
|
||||||
|
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
||||||
|
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
||||||
|
useInteraction: () => {
|
||||||
|
const { toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect: toggleColorScheme,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "language",
|
||||||
|
icon: IconLanguage,
|
||||||
|
name: tOption("language.label"),
|
||||||
|
useInteraction: interaction.children(languageChildrenOptions),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newBoard",
|
||||||
|
icon: IconCategoryPlus,
|
||||||
|
name: tOption("newBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "importBoard",
|
||||||
|
icon: IconFileImport,
|
||||||
|
name: tOption("importBoard.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(ImportBoardModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("board-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newApp",
|
||||||
|
icon: IconBox,
|
||||||
|
name: tOption("newApp.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("app-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newIntegration",
|
||||||
|
icon: IconPlug,
|
||||||
|
name: tOption("newIntegration.label"),
|
||||||
|
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
||||||
|
hidden: !session?.user.permissions.includes("integration-create"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newUser",
|
||||||
|
icon: IconUserPlus,
|
||||||
|
name: tOption("newUser.label"),
|
||||||
|
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newInvite",
|
||||||
|
icon: IconMailForward,
|
||||||
|
name: tOption("newInvite.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(InviteCreateModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
commandKey: "newGroup",
|
||||||
|
icon: IconUsersGroup,
|
||||||
|
name: tOption("newGroup.label"),
|
||||||
|
useInteraction() {
|
||||||
|
const { openModal } = useModalAction(AddGroupModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
openModal(undefined);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
hidden: !session?.user.permissions.includes("admin"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return commands.filter((command) => !command.hidden);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,173 +1,9 @@
|
|||||||
import { Group, Text, useMantineColorScheme } from "@mantine/core";
|
|
||||||
import {
|
|
||||||
IconBox,
|
|
||||||
IconCategoryPlus,
|
|
||||||
IconFileImport,
|
|
||||||
IconLanguage,
|
|
||||||
IconMailForward,
|
|
||||||
IconMoon,
|
|
||||||
IconPlug,
|
|
||||||
IconSun,
|
|
||||||
IconUserPlus,
|
|
||||||
IconUsersGroup,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import { useSession } from "@homarr/auth/client";
|
|
||||||
import { useModalAction } from "@homarr/modals";
|
|
||||||
import { AddBoardModal, AddGroupModal, ImportBoardModal, InviteCreateModal } from "@homarr/modals-collection";
|
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
|
||||||
import type { TablerIcon } from "@homarr/ui";
|
|
||||||
|
|
||||||
import { createGroup } from "../../lib/group";
|
|
||||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
|
||||||
import { interaction } from "../../lib/interaction";
|
|
||||||
import type { SearchMode } from "../../lib/mode";
|
import type { SearchMode } from "../../lib/mode";
|
||||||
import { languageChildrenOptions } from "./children/language";
|
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
|
||||||
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
import { globalCommandGroup } from "./global-group";
|
||||||
|
|
||||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
type Command<TSearchInteraction extends SearchInteraction = SearchInteraction> = {
|
|
||||||
commandKey: string;
|
|
||||||
icon: TablerIcon;
|
|
||||||
name: string;
|
|
||||||
useInteraction: (
|
|
||||||
_c: Command<TSearchInteraction>,
|
|
||||||
query: string,
|
|
||||||
) => inferSearchInteractionDefinition<TSearchInteraction>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const commandMode = {
|
export const commandMode = {
|
||||||
modeKey: "command",
|
modeKey: "command",
|
||||||
character: ">",
|
character: ">",
|
||||||
groups: [
|
groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
|
||||||
createGroup<Command>({
|
|
||||||
keyPath: "commandKey",
|
|
||||||
title: "Global commands",
|
|
||||||
useInteraction: (option, query) => option.useInteraction(option, query),
|
|
||||||
Component: ({ icon: Icon, name }) => (
|
|
||||||
<Group px="md" py="sm">
|
|
||||||
<Icon stroke={1.5} />
|
|
||||||
<Text>{name}</Text>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
filter(query, option) {
|
|
||||||
return option.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
},
|
|
||||||
useOptions() {
|
|
||||||
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const commands: (Command & { hidden?: boolean })[] = [
|
|
||||||
{
|
|
||||||
commandKey: "colorScheme",
|
|
||||||
icon: colorScheme === "dark" ? IconSun : IconMoon,
|
|
||||||
name: tOption(`colorScheme.${colorScheme === "dark" ? "light" : "dark"}`),
|
|
||||||
useInteraction: () => {
|
|
||||||
const { toggleColorScheme } = useMantineColorScheme();
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect: toggleColorScheme,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "language",
|
|
||||||
icon: IconLanguage,
|
|
||||||
name: tOption("language.label"),
|
|
||||||
useInteraction: interaction.children(languageChildrenOptions),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newBoard",
|
|
||||||
icon: IconCategoryPlus,
|
|
||||||
name: tOption("newBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "importBoard",
|
|
||||||
icon: IconFileImport,
|
|
||||||
name: tOption("importBoard.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(ImportBoardModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("board-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newApp",
|
|
||||||
icon: IconBox,
|
|
||||||
name: tOption("newApp.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("app-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newIntegration",
|
|
||||||
icon: IconPlug,
|
|
||||||
name: tOption("newIntegration.label"),
|
|
||||||
useInteraction: interaction.children(newIntegrationChildrenOptions),
|
|
||||||
hidden: !session?.user.permissions.includes("integration-create"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newUser",
|
|
||||||
icon: IconUserPlus,
|
|
||||||
name: tOption("newUser.label"),
|
|
||||||
useInteraction: interaction.link(() => ({ href: "/manage/users/new" })),
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newInvite",
|
|
||||||
icon: IconMailForward,
|
|
||||||
name: tOption("newInvite.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(InviteCreateModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commandKey: "newGroup",
|
|
||||||
icon: IconUsersGroup,
|
|
||||||
name: tOption("newGroup.label"),
|
|
||||||
useInteraction() {
|
|
||||||
const { openModal } = useModalAction(AddGroupModal);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "javaScript",
|
|
||||||
onSelect() {
|
|
||||||
openModal(undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
hidden: !session?.user.permissions.includes("admin"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return commands.filter((command) => !command.hidden);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
|
|||||||
34
packages/spotlight/src/modes/home/context-specific-group.tsx
Normal file
34
packages/spotlight/src/modes/home/context-specific-group.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Group, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
import { createGroup } from "../../lib/group";
|
||||||
|
import type { ContextSpecificItem } from "./context";
|
||||||
|
import { useSpotlightContextResults } from "./context";
|
||||||
|
|
||||||
|
export const contextSpecificSearchGroups = createGroup<ContextSpecificItem>({
|
||||||
|
title: (t) => t("search.mode.home.group.local.title"),
|
||||||
|
keyPath: "id",
|
||||||
|
Component(option) {
|
||||||
|
const icon =
|
||||||
|
typeof option.icon !== "string" ? (
|
||||||
|
<option.icon size={24} />
|
||||||
|
) : (
|
||||||
|
<img width={24} height={24} src={option.icon} alt={option.name} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||||
|
{icon}
|
||||||
|
<Text>{option.name}</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
useInteraction(option) {
|
||||||
|
return option.interaction();
|
||||||
|
},
|
||||||
|
filter(query, option) {
|
||||||
|
return option.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
},
|
||||||
|
useOptions() {
|
||||||
|
return useSpotlightContextResults();
|
||||||
|
},
|
||||||
|
});
|
||||||
122
packages/spotlight/src/modes/home/context.tsx
Normal file
122
packages/spotlight/src/modes/home/context.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import type { DependencyList, PropsWithChildren } from "react";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
|
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
export type ContextSpecificItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
icon: TablerIcon | string;
|
||||||
|
interaction: () => inferSearchInteractionDefinition<SearchInteraction>;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SpotlightContextProps {
|
||||||
|
items: ContextSpecificItem[];
|
||||||
|
registerItems: (key: string, results: ContextSpecificItem[]) => void;
|
||||||
|
unregisterItems: (key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSpotlightContext = (displayName: string) => {
|
||||||
|
const SpotlightContext = createContext<SpotlightContextProps | null>(null);
|
||||||
|
SpotlightContext.displayName = displayName;
|
||||||
|
|
||||||
|
const Provider = ({ children }: PropsWithChildren) => {
|
||||||
|
const [itemsMap, setItemsMap] = useState<Map<string, { items: ContextSpecificItem[]; count: number }>>(new Map());
|
||||||
|
|
||||||
|
const registerItems = useCallback((key: string, newItems: ContextSpecificItem[]) => {
|
||||||
|
setItemsMap((prevItems) => {
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.set(key, { items: newItems, count: (newItemsMap.get(key)?.count ?? 0) + 1 });
|
||||||
|
return newItemsMap;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unregisterItems = useCallback((key: string) => {
|
||||||
|
setItemsMap((prevItems) => {
|
||||||
|
const registrationCount = prevItems.get(key)?.count ?? 0;
|
||||||
|
|
||||||
|
if (registrationCount <= 1) {
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.delete(key);
|
||||||
|
return newItemsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItemsMap = new Map(prevItems);
|
||||||
|
newItemsMap.set(key, { items: newItemsMap.get(key)?.items ?? [], count: registrationCount - 1 });
|
||||||
|
|
||||||
|
return prevItems;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const items = useMemo(() => Array.from(itemsMap.values()).flatMap(({ items }) => items), [itemsMap]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpotlightContext.Provider value={{ items, registerItems, unregisterItems }}>
|
||||||
|
{children}
|
||||||
|
</SpotlightContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSpotlightContextItems = () => {
|
||||||
|
const context = useContext(SpotlightContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(`useSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useRegisterSpotlightContextItems = (
|
||||||
|
key: string,
|
||||||
|
items: ContextSpecificItem[],
|
||||||
|
dependencyArray: DependencyList,
|
||||||
|
) => {
|
||||||
|
const context = useContext(SpotlightContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
`useRegisterSpotlightContextItems must be used within SpotlightContext[displayName=${displayName}]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
context.registerItems(
|
||||||
|
key,
|
||||||
|
items.filter((item) => !item.disabled),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
context.unregisterItems(key);
|
||||||
|
};
|
||||||
|
// We ignore the results
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [...dependencyArray, key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [SpotlightContext, Provider, useSpotlightContextItems, useRegisterSpotlightContextItems] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [_ResultContext, ResultProvider, useSpotlightContextResults, useRegisterSpotlightContextResults] =
|
||||||
|
createSpotlightContext("SpotlightContextSpecificResults");
|
||||||
|
const [_ActionContext, ActionProvider, useSpotlightContextActions, useRegisterSpotlightContextActions] =
|
||||||
|
createSpotlightContext("SpotlightContextSpecificActions");
|
||||||
|
|
||||||
|
export {
|
||||||
|
useRegisterSpotlightContextActions,
|
||||||
|
useRegisterSpotlightContextResults,
|
||||||
|
useSpotlightContextActions,
|
||||||
|
useSpotlightContextResults,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SpotlightProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<ResultProvider>
|
||||||
|
<ActionProvider>{children}</ActionProvider>
|
||||||
|
</ResultProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
8
packages/spotlight/src/modes/home/index.tsx
Normal file
8
packages/spotlight/src/modes/home/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { SearchMode } from "../../lib/mode";
|
||||||
|
import { contextSpecificSearchGroups } from "./context-specific-group";
|
||||||
|
|
||||||
|
export const homeMode = {
|
||||||
|
character: undefined,
|
||||||
|
modeKey: "home",
|
||||||
|
groups: [contextSpecificSearchGroups],
|
||||||
|
} satisfies SearchMode;
|
||||||
@@ -11,10 +11,11 @@ import type { SearchMode } from "../lib/mode";
|
|||||||
import { appIntegrationBoardMode } from "./app-integration-board";
|
import { appIntegrationBoardMode } from "./app-integration-board";
|
||||||
import { commandMode } from "./command";
|
import { commandMode } from "./command";
|
||||||
import { externalMode } from "./external";
|
import { externalMode } from "./external";
|
||||||
|
import { homeMode } from "./home";
|
||||||
import { pageMode } from "./page";
|
import { pageMode } from "./page";
|
||||||
import { userGroupMode } from "./user-group";
|
import { userGroupMode } from "./user-group";
|
||||||
|
|
||||||
const searchModesWithoutHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
const searchModesForHelp = [userGroupMode, appIntegrationBoardMode, externalMode, commandMode, pageMode] as const;
|
||||||
|
|
||||||
const helpMode = {
|
const helpMode = {
|
||||||
modeKey: "help",
|
modeKey: "help",
|
||||||
@@ -82,4 +83,4 @@ const helpMode = {
|
|||||||
},
|
},
|
||||||
} satisfies SearchMode;
|
} satisfies SearchMode;
|
||||||
|
|
||||||
export const searchModes = [...searchModesWithoutHelp, helpMode] as const;
|
export const searchModes = [...searchModesForHelp, helpMode, homeMode] as const;
|
||||||
|
|||||||
@@ -1159,6 +1159,9 @@
|
|||||||
"automationId": {
|
"automationId": {
|
||||||
"label": "Automation ID"
|
"label": "Automation ID"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"spotlightAction": {
|
||||||
|
"run": "Run {name}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
@@ -2450,6 +2453,9 @@
|
|||||||
"command": {
|
"command": {
|
||||||
"help": "Activate command mode",
|
"help": "Activate command mode",
|
||||||
"group": {
|
"group": {
|
||||||
|
"localCommand": {
|
||||||
|
"title": "Local commands"
|
||||||
|
},
|
||||||
"globalCommand": {
|
"globalCommand": {
|
||||||
"title": "Global commands",
|
"title": "Global commands",
|
||||||
"option": {
|
"option": {
|
||||||
@@ -2559,6 +2565,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"home": {
|
||||||
|
"group": {
|
||||||
|
"local": {
|
||||||
|
"title": "Local results"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"help": "Search for pages",
|
"help": "Search for pages",
|
||||||
"group": {
|
"group": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import combineClasses from "clsx";
|
|||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
||||||
|
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
@@ -28,6 +29,24 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
|||||||
retry: false,
|
retry: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
useRegisterSpotlightContextResults(
|
||||||
|
`app-${app.id}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: app.id,
|
||||||
|
name: app.name,
|
||||||
|
icon: app.iconUrl,
|
||||||
|
interaction() {
|
||||||
|
return {
|
||||||
|
type: "link",
|
||||||
|
href: parseAppHrefWithVariablesClient(app.href ?? ""),
|
||||||
|
newTab: options.openInNewTab,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[app, options.openInNewTab],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLink
|
<AppLink
|
||||||
|
|||||||
@@ -4,17 +4,36 @@ import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledBu
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { parseAppHrefWithVariablesClient } from "@homarr/common/client";
|
||||||
|
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import classes from "./bookmark.module.css";
|
import classes from "./bookmark.module.css";
|
||||||
|
|
||||||
export default function BookmarksWidget({ options, width, height }: WidgetComponentProps<"bookmarks">) {
|
export default function BookmarksWidget({ options, width, height, itemId }: WidgetComponentProps<"bookmarks">) {
|
||||||
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
||||||
select(data) {
|
select(data) {
|
||||||
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useRegisterSpotlightContextResults(
|
||||||
|
`bookmark-${itemId}`,
|
||||||
|
data.map((app) => ({
|
||||||
|
id: app.id,
|
||||||
|
name: app.name,
|
||||||
|
icon: app.iconUrl,
|
||||||
|
interaction() {
|
||||||
|
return {
|
||||||
|
type: "link",
|
||||||
|
href: parseAppHrefWithVariablesClient(app.href ?? ""),
|
||||||
|
newTab: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h="100%" gap="sm" p="sm">
|
<Stack h="100%" gap="sm" p="sm">
|
||||||
<Title order={4} px="0.25rem">
|
<Title order={4} px="0.25rem">
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
|
import { Center, Stack, Text, UnstyledButton } from "@mantine/core";
|
||||||
|
import { IconBinaryTree } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
import { NoIntegrationSelectedError } from "../../errors";
|
import { NoIntegrationSelectedError } from "../../errors";
|
||||||
@@ -60,6 +62,27 @@ const InnerComponent = ({ options, integrationId, isEditMode }: InnerComponentPr
|
|||||||
});
|
});
|
||||||
}, [integrationId, isEditMode, mutate, options.clickable, options.entityId]);
|
}, [integrationId, isEditMode, mutate, options.clickable, options.entityId]);
|
||||||
|
|
||||||
|
useRegisterSpotlightContextActions(
|
||||||
|
`smartHome-entityState-${options.entityId}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: options.entityId,
|
||||||
|
name: options.displayName,
|
||||||
|
icon: IconBinaryTree,
|
||||||
|
interaction() {
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
onSelect() {
|
||||||
|
handleClick();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
disabled: !options.clickable,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[handleClick, options.clickable, options.displayName, options.entityId],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useDisclosure, useTimeout } from "@mantine/hooks";
|
|||||||
import { IconAutomation, IconCheck } from "@tabler/icons-react";
|
import { IconAutomation, IconCheck } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import { useRegisterSpotlightContextActions } from "@homarr/spotlight";
|
||||||
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
|
|
||||||
@@ -34,6 +36,29 @@ export default function SmartHomeTriggerAutomationWidget({
|
|||||||
integrationId: integrationIds[0] ?? "",
|
integrationId: integrationIds[0] ?? "",
|
||||||
});
|
});
|
||||||
}, [integrationIds, isEditMode, mutateAsync, options.automationId]);
|
}, [integrationIds, isEditMode, mutateAsync, options.automationId]);
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
useRegisterSpotlightContextActions(
|
||||||
|
`smartHome-automation-${options.automationId}`,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: options.automationId,
|
||||||
|
name: t("widget.smartHome-executeAutomation.spotlightAction.run", { name: options.displayName }),
|
||||||
|
icon: IconAutomation,
|
||||||
|
interaction() {
|
||||||
|
return {
|
||||||
|
type: "javaScript",
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
async onSelect() {
|
||||||
|
await handleClick();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[handleClick, options.automationId, options.displayName],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton onClick={handleClick} style={{ cursor: !isEditMode ? "pointer" : "initial" }} w="100%" h="100%">
|
<UnstyledButton onClick={handleClick} style={{ cursor: !isEditMode ? "pointer" : "initial" }} w="100%" h="100%">
|
||||||
{isShowSuccess && (
|
{isShowSuccess && (
|
||||||
|
|||||||
Reference in New Issue
Block a user