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:
24
packages/spotlight/src/lib/children.ts
Normal file
24
packages/spotlight/src/lib/children.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { inferSearchInteractionDefinition } from "./interaction";
|
||||
|
||||
export interface CreateChildrenOptionsProps<TParentOptions extends Record<string, unknown>> {
|
||||
detailComponent: ({ options }: { options: TParentOptions }) => ReactNode;
|
||||
useActions: (options: TParentOptions, query: string) => ChildrenAction<TParentOptions>[];
|
||||
}
|
||||
|
||||
export interface ChildrenAction<TParentOptions extends Record<string, unknown>> {
|
||||
key: string;
|
||||
component: (option: TParentOptions) => JSX.Element;
|
||||
useInteraction: (option: TParentOptions, query: string) => inferSearchInteractionDefinition<"link" | "javaScript">;
|
||||
hide?: boolean | ((option: TParentOptions) => boolean);
|
||||
}
|
||||
|
||||
export const createChildrenOptions = <TParentOptions extends Record<string, unknown>>(
|
||||
props: CreateChildrenOptionsProps<TParentOptions>,
|
||||
) => {
|
||||
return (option: TParentOptions) => ({
|
||||
option,
|
||||
...props,
|
||||
});
|
||||
};
|
||||
28
packages/spotlight/src/lib/group.ts
Normal file
28
packages/spotlight/src/lib/group.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { UseTRPCQueryResult } from "@trpc/react-query/shared";
|
||||
|
||||
import type { stringOrTranslation } from "@homarr/translation";
|
||||
|
||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "./interaction";
|
||||
|
||||
type CommonSearchGroup<TOption extends Record<string, unknown>, TOptionProps extends Record<string, unknown>> = {
|
||||
// key path is used to define the path to a unique key in the option object
|
||||
keyPath: keyof TOption;
|
||||
title: stringOrTranslation;
|
||||
component: (option: TOption) => JSX.Element;
|
||||
useInteraction: (option: TOption, query: string) => inferSearchInteractionDefinition<SearchInteraction>;
|
||||
} & TOptionProps;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type SearchGroup<TOption extends Record<string, unknown> = any> =
|
||||
| CommonSearchGroup<TOption, { filter: (query: string, option: TOption) => boolean; options: TOption[] }>
|
||||
| CommonSearchGroup<
|
||||
TOption,
|
||||
{
|
||||
filter: (query: string, option: TOption) => boolean;
|
||||
sort?: (query: string, options: [TOption, TOption]) => number;
|
||||
useOptions: () => TOption[];
|
||||
}
|
||||
>
|
||||
| CommonSearchGroup<TOption, { useQueryOptions: (query: string) => UseTRPCQueryResult<TOption[], unknown> }>;
|
||||
|
||||
export const createGroup = <TOption extends Record<string, unknown>>(group: SearchGroup<TOption>) => group;
|
||||
56
packages/spotlight/src/lib/interaction.ts
Normal file
56
packages/spotlight/src/lib/interaction.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { MaybePromise } from "@homarr/common/types";
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
|
||||
import type { CreateChildrenOptionsProps } from "./children";
|
||||
|
||||
const createSearchInteraction = <TType extends string>(type: TType) => ({
|
||||
optionsType: <TOption extends Record<string, unknown>>() => ({ type, _inferOptions: {} as TOption }),
|
||||
});
|
||||
|
||||
// This is used to define search interactions with their options
|
||||
const searchInteractions = [
|
||||
createSearchInteraction("link").optionsType<{ href: string; newTab?: boolean }>(),
|
||||
createSearchInteraction("javaScript").optionsType<{ onSelect: () => MaybePromise<void> }>(),
|
||||
createSearchInteraction("mode").optionsType<{ mode: keyof TranslationObject["search"]["mode"] }>(),
|
||||
createSearchInteraction("children").optionsType<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
useActions: CreateChildrenOptionsProps<any>["useActions"];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
detailComponent: CreateChildrenOptionsProps<any>["detailComponent"];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
option: any;
|
||||
}>(),
|
||||
] as const;
|
||||
|
||||
// Union of all search interactions types
|
||||
export type SearchInteraction = (typeof searchInteractions)[number]["type"];
|
||||
|
||||
// Infer the options for the specified search interaction
|
||||
export type inferSearchInteractionOptions<TInteraction extends SearchInteraction> = Extract<
|
||||
(typeof searchInteractions)[number],
|
||||
{ type: TInteraction }
|
||||
>["_inferOptions"];
|
||||
|
||||
// Infer the search interaction definition (type + options) for the specified search interaction
|
||||
export type inferSearchInteractionDefinition<TInteraction extends SearchInteraction> = {
|
||||
[interactionKey in TInteraction]: { type: interactionKey } & inferSearchInteractionOptions<interactionKey>;
|
||||
}[TInteraction];
|
||||
|
||||
// Type used for helper functions to define basic search interactions
|
||||
type SearchInteractions = {
|
||||
[optionKey in SearchInteraction]: <TOption extends Record<string, unknown>>(
|
||||
callback: (option: TOption, query: string) => inferSearchInteractionOptions<optionKey>,
|
||||
) => (option: TOption, query: string) => inferSearchInteractionDefinition<optionKey>;
|
||||
};
|
||||
|
||||
// Helper functions to define basic search interactions
|
||||
export const interaction = searchInteractions.reduce((acc, interaction) => {
|
||||
return {
|
||||
...acc,
|
||||
[interaction.type]: <TOption extends Record<string, unknown>>(
|
||||
callback: (option: TOption, query: string) => inferSearchInteractionOptions<SearchInteraction>,
|
||||
) => {
|
||||
return (option: TOption, query: string) => ({ type: interaction.type, ...callback(option, query) });
|
||||
},
|
||||
};
|
||||
}, {} as SearchInteractions);
|
||||
9
packages/spotlight/src/lib/mode.ts
Normal file
9
packages/spotlight/src/lib/mode.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
|
||||
import type { SearchGroup } from "./group";
|
||||
|
||||
export interface SearchMode {
|
||||
modeKey: keyof TranslationObject["search"]["mode"];
|
||||
character: string;
|
||||
groups: SearchGroup[];
|
||||
}
|
||||
Reference in New Issue
Block a user