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:
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
+3
-17
@@ -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;
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user