Skip to content

Commit 26aad58

Browse files
authored
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
1 parent b62c17b commit 26aad58

File tree

4 files changed

+69
-57
lines changed

4 files changed

+69
-57
lines changed

apps/webapp/app/hooks/useFilterTasks.ts

-38
This file was deleted.
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useMemo, useState } from "react";
2+
import { matchSorter } from "match-sorter";
3+
4+
/**
5+
* A hook that provides fuzzy filtering functionality for a list of objects.
6+
* Uses match-sorter to perform the filtering across multiple object properties and
7+
* consistently order the results by score.
8+
*
9+
* @param params - The parameters object
10+
* @param params.items - Array of objects to filter
11+
* @param params.keys - Array of object keys to perform the fuzzy search on
12+
* @returns An object containing:
13+
* - filterText: The current filter text
14+
* - setFilterText: Function to update the filter text
15+
* - filteredItems: The filtered array of items based on the current filter text
16+
*
17+
* @example
18+
* ```tsx
19+
* const users = [{ name: "John", email: "john@example.com" }];
20+
* const { filterText, setFilterText, filteredItems } = useFuzzyFilter({
21+
* items: users,
22+
* keys: ["name", "email"]
23+
* });
24+
* ```
25+
*/
26+
export function useFuzzyFilter<T extends Object>({
27+
items,
28+
keys,
29+
}: {
30+
items: T[];
31+
keys: Extract<keyof T, string>[];
32+
}) {
33+
const [filterText, setFilterText] = useState("");
34+
35+
const filteredItems = useMemo<T[]>(() => {
36+
const filterTerms = filterText
37+
.trim()
38+
.split(" ")
39+
.map((term) => term.trim())
40+
.filter((term) => term !== "");
41+
42+
if (filterTerms.length === 0) {
43+
return items;
44+
}
45+
46+
// sort by the score of the first term
47+
return filterTerms.reduceRight(
48+
(results, term) =>
49+
matchSorter(results, term, {
50+
keys,
51+
}),
52+
items
53+
);
54+
}, [items, filterText]);
55+
56+
return {
57+
filterText,
58+
setFilterText,
59+
filteredItems,
60+
};
61+
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx

+3-17
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ import {
6767
} from "~/components/runs/v3/TaskTriggerSource";
6868
import { useEnvironment } from "~/hooks/useEnvironment";
6969
import { useEventSource } from "~/hooks/useEventSource";
70+
import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
7071
import { useOrganization } from "~/hooks/useOrganizations";
7172
import { useProject } from "~/hooks/useProject";
72-
import { useTextFilter } from "~/hooks/useTextFilter";
7373
import { findProjectBySlug } from "~/models/project.server";
7474
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
7575
import {
@@ -169,23 +169,9 @@ export default function Page() {
169169
const environment = useEnvironment();
170170
const { tasks, activity, runningStats, durations, usefulLinksPreference } =
171171
useTypedLoaderData<typeof loader>();
172-
const { filterText, setFilterText, filteredItems } = useTextFilter<TaskListItem>({
172+
const { filterText, setFilterText, filteredItems } = useFuzzyFilter<TaskListItem>({
173173
items: tasks,
174-
filter: (task, text) => {
175-
if (task.slug.toLowerCase().includes(text.toLowerCase())) {
176-
return true;
177-
}
178-
179-
if (task.filePath.toLowerCase().includes(text.toLowerCase())) {
180-
return true;
181-
}
182-
183-
if (task.triggerSource === "SCHEDULED" && "scheduled".includes(text.toLowerCase())) {
184-
return true;
185-
}
186-
187-
return false;
188-
},
174+
keys: ["slug", "filePath", "triggerSource"],
189175
});
190176

191177
const hasTasks = tasks.length > 0;

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from "~/components/primitives/Table";
2727
import { TaskTriggerSourceIcon } from "~/components/runs/v3/TaskTriggerSource";
2828
import { useEnvironment } from "~/hooks/useEnvironment";
29-
import { useFilterTasks } from "~/hooks/useFilterTasks";
29+
import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
3030
import { useLinkStatus } from "~/hooks/useLinkStatus";
3131
import { useOrganization } from "~/hooks/useOrganizations";
3232
import { useProject } from "~/hooks/useProject";
@@ -120,7 +120,10 @@ function TaskSelector({
120120
tasks: TaskListItem[];
121121
activeTaskIdentifier?: string;
122122
}) {
123-
const { filterText, setFilterText, filteredItems } = useFilterTasks<TaskListItem>({ tasks });
123+
const { filterText, setFilterText, filteredItems } = useFuzzyFilter<TaskListItem>({
124+
items: tasks,
125+
keys: ["taskIdentifier", "friendlyId", "id", "filePath", "triggerSource"],
126+
});
124127
const hasTaskInEnvironment = activeTaskIdentifier
125128
? tasks.some((t) => t.taskIdentifier === activeTaskIdentifier)
126129
: undefined;

0 commit comments

Comments
 (0)