diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx index a6b7006121a..f8380cc7fab 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx @@ -1,7 +1,7 @@ import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; -import { useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { HiOutlineMagnifyingGlass, HiXMark } from "react-icons/hi2"; import { LuPlay } from "react-icons/lu"; import { useAppHotkey } from "renderer/stores/hotkeys"; @@ -54,6 +54,23 @@ export function TasksTopBar({ onClearSelection, }: TasksTopBarProps) { const searchInputRef = useRef(null); + const [localSearch, setLocalSearch] = useState(searchQuery); + const debounceRef = useRef>(undefined); + + // Sync local state when parent clears the search + useEffect(() => { + if (searchQuery === "") setLocalSearch(""); + }, [searchQuery]); + + const handleSearchInput = (value: string) => { + setLocalSearch(value); + clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => onSearchChange(value), 150); + }; + + useEffect(() => { + return () => clearTimeout(debounceRef.current); + }, []); useAppHotkey( "FOCUS_TASK_SEARCH", @@ -129,10 +146,12 @@ export function TasksTopBar({ ref={searchInputRef} type="text" placeholder="Search tasks..." - value={searchQuery} - onChange={(e) => onSearchChange(e.target.value)} + value={localSearch} + onChange={(e) => handleSearchInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") { + clearTimeout(debounceRef.current); + setLocalSearch(""); onSearchChange(""); searchInputRef.current?.blur(); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts index ec33397b0d5..0170b2c8c63 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch/useHybridSearch.ts @@ -1,5 +1,5 @@ import Fuse from "fuse.js"; -import { useCallback, useMemo } from "react"; +import { useCallback, useRef } from "react"; interface SearchableTask { id: string; @@ -16,52 +16,56 @@ interface SearchResult { } export function useHybridSearch(tasks: T[]) { - const exactFuse = useMemo( - () => - new Fuse(tasks, { - keys: [ - { name: "slug", weight: 2 }, - { name: "labels", weight: 1 }, - ], - threshold: 0, - includeScore: true, - ignoreLocation: true, - useExtendedSearch: false, - }), - [tasks], - ); + const tasksRef = useRef(tasks); + tasksRef.current = tasks; - const fuzzyFuse = useMemo( - () => - new Fuse(tasks, { - keys: [ - { name: "title", weight: 2 }, - { name: "description", weight: 1 }, - ], - threshold: 0.3, - includeScore: true, - ignoreLocation: true, - useExtendedSearch: false, - }), - [tasks], - ); + const exactFuseRef = useRef | null>(null); + const fuzzyFuseRef = useRef | null>(null); + const indexedTasksRef = useRef(null); + + const ensureIndex = useCallback(() => { + if (indexedTasksRef.current === tasksRef.current) return; + exactFuseRef.current = new Fuse(tasksRef.current, { + keys: [ + { name: "slug", weight: 2 }, + { name: "labels", weight: 1 }, + ], + threshold: 0, + includeScore: true, + ignoreLocation: true, + useExtendedSearch: false, + }); + fuzzyFuseRef.current = new Fuse(tasksRef.current, { + keys: [ + { name: "title", weight: 2 }, + { name: "description", weight: 1 }, + ], + threshold: 0.3, + includeScore: true, + ignoreLocation: true, + useExtendedSearch: false, + }); + indexedTasksRef.current = tasksRef.current; + }, []); const search = useCallback( (query: string): SearchResult[] => { if (!query.trim()) { - return tasks.map((item) => ({ + return tasksRef.current.map((item) => ({ item, score: 1, matchType: "exact" as const, })); } - const exactMatches = exactFuse.search(query); + ensureIndex(); + + const exactMatches = exactFuseRef.current?.search(query) ?? []; const exactIds = new Set(exactMatches.map((m) => m.item.id)); - const fuzzyMatches = fuzzyFuse - .search(query) - .filter((m) => !exactIds.has(m.item.id)); + const fuzzyMatches = (fuzzyFuseRef.current?.search(query) ?? []).filter( + (m) => !exactIds.has(m.item.id), + ); return [ ...exactMatches.map((m) => ({ @@ -76,7 +80,7 @@ export function useHybridSearch(tasks: T[]) { })), ]; }, - [exactFuse, fuzzyFuse, tasks], + [ensureIndex], ); return { search }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx index f555a8f396e..3797b0bf0c9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/components/AssigneeCell/AssigneeCell.tsx @@ -1,3 +1,4 @@ +import type { SelectUser } from "@superset/db/schema"; import { Avatar } from "@superset/ui/atoms/Avatar"; import { DropdownMenu, @@ -5,31 +6,24 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@superset/ui/dropdown-menu"; -import { useLiveQuery } from "@tanstack/react-db"; import type { CellContext } from "@tanstack/react-table"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { HiOutlineUserCircle } from "react-icons/hi2"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { TaskWithStatus } from "../../useTasksTable"; interface AssigneeCellProps { info: CellContext; + users: SelectUser[]; } -export function AssigneeCell({ info }: AssigneeCellProps) { +export function AssigneeCell({ info, users }: AssigneeCellProps) { const collections = useCollections(); const [open, setOpen] = useState(false); const task = info.row.original; const assigneeId = info.getValue(); - const { data: allUsers } = useLiveQuery( - (q) => q.from({ users: collections.users }), - [collections], - ); - - const users = useMemo(() => allUsers || [], [allUsers]); - const handleSelectUser = (userId: string | null) => { if (userId === assigneeId) { setOpen(false); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/useTasksTable.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/useTasksTable.tsx index e56458cef29..4ae5d0d8148 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/useTasksTable.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useTasksTable/useTasksTable.tsx @@ -86,6 +86,13 @@ export function useTasksTable({ const rowSelection = useRowSelectionStore((s) => s.rowSelection); const setRowSelection = useRowSelectionStore((s) => s.setRowSelection); + const { data: allUsers } = useLiveQuery( + (q) => q.from({ users: collections.users }), + [collections], + ); + + const users = useMemo(() => allUsers ?? [], [allUsers]); + const { data: allData, isLoading } = useLiveQuery( (q) => q @@ -299,7 +306,7 @@ export function useTasksTable({ }, cell: (info) => { if (info.cell.getIsPlaceholder()) return null; - return ; + return ; }, }), @@ -317,7 +324,7 @@ export function useTasksTable({ }, }), ], - [], + [users], ); const table = useReactTable({