diff --git a/apps/webapp/app/hooks/useFilterTasks.ts b/apps/webapp/app/hooks/useFilterTasks.ts deleted file mode 100644 index 7b95cf8128..0000000000 --- a/apps/webapp/app/hooks/useFilterTasks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useTextFilter } from "./useTextFilter"; - -type Task = { - id: string; - friendlyId: string; - taskIdentifier: string; - filePath: string; - triggerSource: string; -}; - -export function useFilterTasks({ tasks }: { tasks: T[] }) { - return useTextFilter({ - items: tasks, - filter: (task, text) => { - if (task.taskIdentifier.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.filePath.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.id.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.friendlyId.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) { - return true; - } - - return false; - }, - }); -} diff --git a/apps/webapp/app/hooks/useFuzzyFilter.ts b/apps/webapp/app/hooks/useFuzzyFilter.ts new file mode 100644 index 0000000000..1c0f604826 --- /dev/null +++ b/apps/webapp/app/hooks/useFuzzyFilter.ts @@ -0,0 +1,61 @@ +import { useMemo, useState } from "react"; +import { matchSorter } from "match-sorter"; + +/** + * A hook that provides fuzzy filtering functionality for a list of objects. + * Uses match-sorter to perform the filtering across multiple object properties and + * consistently order the results by score. + * + * @param params - The parameters object + * @param params.items - Array of objects to filter + * @param params.keys - Array of object keys to perform the fuzzy search on + * @returns An object containing: + * - filterText: The current filter text + * - setFilterText: Function to update the filter text + * - filteredItems: The filtered array of items based on the current filter text + * + * @example + * ```tsx + * const users = [{ name: "John", email: "john@example.com" }]; + * const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ + * items: users, + * keys: ["name", "email"] + * }); + * ``` + */ +export function useFuzzyFilter({ + items, + keys, +}: { + items: T[]; + keys: Extract[]; +}) { + const [filterText, setFilterText] = useState(""); + + const filteredItems = useMemo(() => { + const filterTerms = filterText + .trim() + .split(" ") + .map((term) => term.trim()) + .filter((term) => term !== ""); + + if (filterTerms.length === 0) { + return items; + } + + // sort by the score of the first term + return filterTerms.reduceRight( + (results, term) => + matchSorter(results, term, { + keys, + }), + items + ); + }, [items, filterText]); + + return { + filterText, + setFilterText, + filteredItems, + }; +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index e9204a7d31..73c0dbbddd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -67,9 +67,9 @@ import { } from "~/components/runs/v3/TaskTriggerSource"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; +import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useTextFilter } from "~/hooks/useTextFilter"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { @@ -169,23 +169,9 @@ export default function Page() { const environment = useEnvironment(); const { tasks, activity, runningStats, durations, usefulLinksPreference } = useTypedLoaderData(); - const { filterText, setFilterText, filteredItems } = useTextFilter({ + const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ items: tasks, - filter: (task, text) => { - if (task.slug.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.filePath.toLowerCase().includes(text.toLowerCase())) { - return true; - } - - if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) { - return true; - } - - return false; - }, + keys: ["slug", "filePath", "triggerSource"], }); const hasTasks = tasks.length > 0; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index ce90133253..4d33289493 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -26,7 +26,7 @@ import { } from "~/components/primitives/Table"; import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource"; import { useEnvironment } from "~/hooks/useEnvironment"; -import { useFilterTasks } from "~/hooks/useFilterTasks"; +import { useFuzzyFilter } from "~/hooks/useFuzzyFilter"; import { useLinkStatus } from "~/hooks/useLinkStatus"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; @@ -120,7 +120,10 @@ function TaskSelector({ tasks: TaskListItem[]; activeTaskIdentifier?: string; }) { - const { filterText, setFilterText, filteredItems } = useFilterTasks({ tasks }); + const { filterText, setFilterText, filteredItems } = useFuzzyFilter({ + items: tasks, + keys: ["taskIdentifier", "friendlyId", "id", "filePath", "triggerSource"], + }); const hasTaskInEnvironment = activeTaskIdentifier ? tasks.some((t) => t.taskIdentifier === activeTaskIdentifier) : undefined;