Add fuzzy matching to the tasks filter (#1960)

* Add a custom react hook to enable fuzzy searching in item lists

* Use fuzzy filtering in the tasks view list

* Use fuzzy filtering in the test tasks list

* Remove the old tasks filtering react hook
This commit is contained in:
Saadi Myftija
2025-04-22 08:34:56 -07:00
committed by GitHub
parent b62c17bec1
commit 26aad58abd
4 changed files with 69 additions and 57 deletions
-38
View File
@@ -1,38 +0,0 @@
import { useTextFilter } from "./useTextFilter";
type Task = {
id: string;
friendlyId: string;
taskIdentifier: string;
filePath: string;
triggerSource: string;
};
export function useFilterTasks<T extends Task>({ tasks }: { tasks: T[] }) {
return useTextFilter<T>({
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;
},
});
}
+61
View File
@@ -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<T extends Object>({
items,
keys,
}: {
items: T[];
keys: Extract<keyof T, string>[];
}) {
const [filterText, setFilterText] = useState("");
const filteredItems = useMemo<T[]>(() => {
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,
};
}
@@ -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<typeof loader>();
const { filterText, setFilterText, filteredItems } = useTextFilter<TaskListItem>({
const { filterText, setFilterText, filteredItems } = useFuzzyFilter<TaskListItem>({
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;
@@ -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<TaskListItem>({ tasks });
const { filterText, setFilterText, filteredItems } = useFuzzyFilter<TaskListItem>({
items: tasks,
keys: ["taskIdentifier", "friendlyId", "id", "filePath", "triggerSource"],
});
const hasTaskInEnvironment = activeTaskIdentifier
? tasks.some((t) => t.taskIdentifier === activeTaskIdentifier)
: undefined;