feat: add improved search (#1051)
* feat: add improved search * wip: add support for sorting, rename use-options to use-query-options, add use-options for local usage, add pages search group * feat: add help links from manage layout to help search mode * feat: add additional search engines * feat: add group search details * refactor: improve users search group type * feat: add apps search group, add disabled search interaction * feat: add integrations and boards for search * wip: hook issue with react * fix: hook issue regarding actions and interactions * chore: address pull request feedback * fix: format issues * feat: add additional global actions to search * chore: remove unused code * fix: search engine short key * fix: typecheck issues * fix: deepsource issues * fix: eslint issue * fix: lint issues * fix: unordered dependencies * chore: address pull request feedback
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { Avatar, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconExternalLink, IconEye } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createChildrenOptions } from "../../lib/children";
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type App = { id: string; name: string; iconUrl: string; href: string | null };
|
||||
|
||||
const appChildrenOptions = createChildrenOptions<App>({
|
||||
useActions: () => [
|
||||
{
|
||||
key: "open",
|
||||
component: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconExternalLink stroke={1.5} />
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.app.children.action.open.label")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
useInteraction: interaction.link((option) => ({ href: option.href! })),
|
||||
hide(option) {
|
||||
return !option.href;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
component: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconEye stroke={1.5} />
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.app.children.action.edit.label")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.link(({ id }) => ({ href: `/manage/apps/edit/${id}` })),
|
||||
},
|
||||
],
|
||||
detailComponent: ({ options }) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm">
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.app.children.detail.title")}</Text>
|
||||
|
||||
<Group>
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={options.iconUrl}
|
||||
radius={0}
|
||||
styles={{
|
||||
image: {
|
||||
objectFit: "contain",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text>{options.name}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const appsSearchGroup = createGroup<App>({
|
||||
keyPath: "id",
|
||||
title: (t) => t("search.mode.appIntegrationBoard.group.app.title"),
|
||||
component: (app) => (
|
||||
<Group px="md" py="sm">
|
||||
<Avatar
|
||||
size="sm"
|
||||
src={app.iconUrl}
|
||||
radius={0}
|
||||
styles={{
|
||||
image: {
|
||||
objectFit: "contain",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text>{app.name}</Text>
|
||||
</Group>
|
||||
),
|
||||
useInteraction: interaction.children(appChildrenOptions),
|
||||
useQueryOptions(query) {
|
||||
return clientApi.app.search.useQuery({ query, limit: 5 });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconHome, IconLayoutDashboard, IconLink, IconSettings } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { ChildrenAction } from "../../lib/children";
|
||||
import { createChildrenOptions } from "../../lib/children";
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type Board = {
|
||||
id: string;
|
||||
name: string;
|
||||
logoImageUrl: string | null;
|
||||
permissions: { hasFullAccess: boolean; hasChangeAccess: boolean; hasViewAccess: boolean };
|
||||
};
|
||||
|
||||
const boardChildrenOptions = createChildrenOptions<Board>({
|
||||
useActions: (options) => {
|
||||
const actions: (ChildrenAction<Board> & { hidden?: boolean })[] = [
|
||||
{
|
||||
key: "open",
|
||||
component: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconLink stroke={1.5} />
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.board.children.action.open.label")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.link(({ name }) => ({ href: `/boards/${name}` })),
|
||||
},
|
||||
{
|
||||
key: "homeBoard",
|
||||
component: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconHome stroke={1.5} />
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.board.children.action.homeBoard.label")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction(option) {
|
||||
const { mutateAsync } = clientApi.board.setHomeBoard.useMutation();
|
||||
|
||||
return {
|
||||
type: "javaScript",
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
async onSelect() {
|
||||
await mutateAsync({ id: option.id });
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "settings",
|
||||
component: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconSettings stroke={1.5} />
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.board.children.action.settings.label")}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.link(({ name }) => ({ href: `/boards/${name}/settings` })),
|
||||
hidden: !options.permissions.hasChangeAccess,
|
||||
},
|
||||
];
|
||||
|
||||
return actions;
|
||||
},
|
||||
detailComponent: ({ options: board }) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm">
|
||||
<Text>{t("search.mode.appIntegrationBoard.group.board.children.detail.title")}</Text>
|
||||
|
||||
<Group>
|
||||
{board.logoImageUrl ? (
|
||||
<img src={board.logoImageUrl} alt={board.name} width={24} height={24} />
|
||||
) : (
|
||||
<IconLayoutDashboard size={24} />
|
||||
)}
|
||||
|
||||
<Text>{board.name}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const boardsSearchGroup = createGroup<Board>({
|
||||
keyPath: "id",
|
||||
title: "Boards",
|
||||
component: (board) => (
|
||||
<Group px="md" py="sm">
|
||||
{board.logoImageUrl ? (
|
||||
<img src={board.logoImageUrl} alt={board.name} width={24} height={24} />
|
||||
) : (
|
||||
<IconLayoutDashboard size={24} />
|
||||
)}
|
||||
|
||||
<Text>{board.name}</Text>
|
||||
</Group>
|
||||
),
|
||||
useInteraction: interaction.children(boardChildrenOptions),
|
||||
useQueryOptions(query) {
|
||||
return clientApi.board.search.useQuery({ query, limit: 5 });
|
||||
},
|
||||
});
|
||||
10
packages/spotlight/src/modes/app-integration-board/index.tsx
Normal file
10
packages/spotlight/src/modes/app-integration-board/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { appsSearchGroup } from "./apps-search-group";
|
||||
import { boardsSearchGroup } from "./boards-search-group";
|
||||
import { integrationsSearchGroup } from "./integrations-search-group";
|
||||
|
||||
export const appIntegrationBoardMode = {
|
||||
modeKey: "appIntegrationBoard",
|
||||
character: "#",
|
||||
groups: [appsSearchGroup, integrationsSearchGroup, boardsSearchGroup],
|
||||
} satisfies SearchMode;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Group, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { IntegrationAvatar } from "@homarr/ui";
|
||||
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
|
||||
export const integrationsSearchGroup = createGroup<{ id: string; kind: IntegrationKind; name: string }>({
|
||||
keyPath: "id",
|
||||
title: (t) => t("search.mode.appIntegrationBoard.group.integration.title"),
|
||||
component: (integration) => (
|
||||
<Group px="md" py="sm">
|
||||
<IntegrationAvatar size="sm" kind={integration.kind} />
|
||||
|
||||
<Text>{integration.name}</Text>
|
||||
</Group>
|
||||
),
|
||||
useInteraction: interaction.link(({ id }) => ({ href: `/manage/integrations/edit/${id}` })),
|
||||
useQueryOptions(query) {
|
||||
return clientApi.integration.search.useQuery({ query, limit: 5 });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user