55 ChevronUpIcon ,
66 ExclamationTriangleIcon ,
77 LightBulbIcon ,
8+ MagnifyingGlassIcon ,
9+ XMarkIcon ,
810 UserPlusIcon ,
911 VideoCameraIcon ,
1012} from "@heroicons/react/20/solid" ;
@@ -14,7 +16,7 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/ser
1416import { DiscordIcon } from "@trigger.dev/companyicons" ;
1517import { formatDurationMilliseconds } from "@trigger.dev/core/v3" ;
1618import type { TaskRunStatus } from "@trigger.dev/database" ;
17- import { Fragment , Suspense , useCallback , useEffect , useMemo , useRef , useState } from "react" ;
19+ import { Fragment , Suspense , useCallback , useEffect , useRef , useState } from "react" ;
1820import type { PanelHandle } from "react-window-splitter" ;
1921import { Bar , BarChart , ResponsiveContainer , Tooltip , type TooltipProps } from "recharts" ;
2022import { TypedAwait , typeddefer , useTypedLoaderData } from "remix-typedjson" ;
@@ -36,7 +38,7 @@ import { Callout } from "~/components/primitives/Callout";
3638import { formatDateTime } from "~/components/primitives/DateTime" ;
3739import { Dialog , DialogContent , DialogHeader , DialogTitle } from "~/components/primitives/Dialog" ;
3840import { Header2 , Header3 } from "~/components/primitives/Headers" ;
39- import { SearchInput } from "~/components/primitives/SearchInput " ;
41+ import { Input } from "~/components/primitives/Input " ;
4042import { NavBar , PageAccessories , PageTitle } from "~/components/primitives/PageHeader" ;
4143import { Paragraph } from "~/components/primitives/Paragraph" ;
4244import { PopoverMenuItem } from "~/components/primitives/Popover" ;
@@ -70,8 +72,7 @@ import {
7072} from "~/components/runs/v3/TaskTriggerSource" ;
7173import { useEnvironment } from "~/hooks/useEnvironment" ;
7274import { useEventSource } from "~/hooks/useEventSource" ;
73- import { useSearchParams } from "~/hooks/useSearchParam" ;
74- import { matchSorter } from "match-sorter" ;
75+ import { useFuzzyFilter } from "~/hooks/useFuzzyFilter" ;
7576import { useOrganization } from "~/hooks/useOrganizations" ;
7677import { useProject } from "~/hooks/useProject" ;
7778import { findProjectBySlug } from "~/models/project.server" ;
@@ -87,6 +88,7 @@ import {
8788 uiPreferencesStorage ,
8889} from "~/services/preferences/uiPreferences.server" ;
8990import { requireUserId } from "~/services/session.server" ;
91+ import { motion } from "framer-motion" ;
9092import { cn } from "~/utils/cn" ;
9193import {
9294 docsPath ,
@@ -174,19 +176,10 @@ export default function Page() {
174176 const environment = useEnvironment ( ) ;
175177 const { tasks, activity, runningStats, durations, usefulLinksPreference } =
176178 useTypedLoaderData < typeof loader > ( ) ;
177- const { value : searchValue } = useSearchParams ( ) ;
178- const search = searchValue ( "search" ) ?? "" ;
179- const filteredItems = useMemo ( ( ) => {
180- const terms = search
181- . trim ( )
182- . split ( " " )
183- . filter ( ( t ) => t !== "" ) ;
184- if ( terms . length === 0 ) return tasks ;
185- return terms . reduceRight (
186- ( results , term ) => matchSorter ( results , term , { keys : [ "slug" , "filePath" , "triggerSource" ] } ) ,
187- tasks
188- ) ;
189- } , [ tasks , search ] ) ;
179+ const { filterText, setFilterText, filteredItems } = useFuzzyFilter < TaskListItem > ( {
180+ items : tasks ,
181+ keys : [ "slug" , "filePath" , "triggerSource" ] ,
182+ } ) ;
190183
191184 const hasTasks = tasks . length > 0 ;
192185
@@ -251,8 +244,13 @@ export default function Page() {
251244 { tasks . length === 0 ? < UserHasNoTasks /> : null }
252245 < div className = "max-h-full overflow-hidden" >
253246 < div className = "flex items-center justify-between gap-1 p-2" >
254- < SearchInput placeholder = "Search tasks…" autoFocus />
255- { ! showUsefulLinks && (
247+ < AnimatedSearchField
248+ value = { filterText }
249+ onChange = { setFilterText }
250+ placeholder = "Search tasks…"
251+ autoFocus
252+ />
253+ { ! showUsefulLinks && (
256254 < Button
257255 variant = "secondary/small"
258256 TrailingIcon = { LightBulbIcon }
@@ -871,3 +869,54 @@ function FailedToLoadStats() {
871869 />
872870 ) ;
873871}
872+
873+ function AnimatedSearchField ( {
874+ value,
875+ onChange,
876+ placeholder,
877+ autoFocus,
878+ } : {
879+ value : string ;
880+ onChange : ( value : string ) => void ;
881+ placeholder ?: string ;
882+ autoFocus ?: boolean ;
883+ } ) {
884+ const [ isFocused , setIsFocused ] = useState ( false ) ;
885+
886+ return (
887+ < motion . div
888+ initial = { { width : "auto" } }
889+ animate = { { width : isFocused && value . length > 0 ? "24rem" : "auto" } }
890+ transition = { { type : "spring" , stiffness : 300 , damping : 30 } }
891+ className = "relative h-6 min-w-52"
892+ >
893+ < Input
894+ type = "text"
895+ variant = "secondary-small"
896+ placeholder = { placeholder }
897+ value = { value }
898+ onChange = { ( e ) => onChange ( e . target . value ) }
899+ fullWidth
900+ autoFocus = { autoFocus }
901+ className = { cn ( isFocused && "placeholder:text-text-dimmed/70" ) }
902+ onFocus = { ( ) => setIsFocused ( true ) }
903+ onBlur = { ( ) => setIsFocused ( false ) }
904+ onKeyDown = { ( e ) => {
905+ if ( e . key === "Escape" ) e . currentTarget . blur ( ) ;
906+ } }
907+ icon = { < MagnifyingGlassIcon className = "size-4" /> }
908+ accessory = {
909+ value . length > 0 ? (
910+ < button
911+ type = "button"
912+ onClick = { ( ) => onChange ( "" ) }
913+ className = "flex size-4.5 items-center justify-center rounded-[2px] border border-text-dimmed/40 text-text-dimmed transition hover:bg-charcoal-600 hover:text-text-bright"
914+ >
915+ < XMarkIcon className = "size-3" />
916+ </ button >
917+ ) : undefined
918+ }
919+ />
920+ </ motion . div >
921+ ) ;
922+ }
0 commit comments