kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(web): implement board search functionality and integrate with task filters

Andrej aa5d86ac 615d5dd2

+84 -20
-16
apps/web/src/components/common/project-layout.tsx
··· 5 5 import ProjectCrumbSelect from "@/components/common/header/project-crumb-select"; 6 6 import WorkspaceCrumbSelect from "@/components/common/header/workspace-crumb-select"; 7 7 import Layout from "@/components/common/layout"; 8 - import NotificationDropdown from "@/components/notification/notification-dropdown"; 9 8 import CreateProjectModal from "@/components/shared/modals/create-project-modal"; 10 9 import { Button } from "@/components/ui/button"; 11 10 import { KbdSequence } from "@/components/ui/kbd"; ··· 18 17 } from "@/components/ui/tooltip"; 19 18 import { shortcuts } from "@/constants/shortcuts"; 20 19 import useGetProject from "@/hooks/queries/project/use-get-project"; 21 - import useActiveWorkspace from "@/hooks/queries/workspace/use-active-workspace"; 22 - import { authClient } from "@/lib/auth-client"; 23 20 import { cn } from "@/lib/cn"; 24 21 25 22 type ProjectLayoutProps = { ··· 41 38 }: ProjectLayoutProps) { 42 39 const navigate = useNavigate(); 43 40 const location = useLocation(); 44 - const { data: workspace } = useActiveWorkspace(); 45 41 const { data: project } = useGetProject({ id: projectId, workspaceId }); 46 42 const [isCreateProjectModalOpen, setIsCreateProjectModalOpen] = 47 43 useState(false); ··· 61 57 navigate({ 62 58 to: "/dashboard/workspace/$workspaceId/project/$projectId/board", 63 59 params: { workspaceId, projectId }, 64 - }); 65 - }; 66 - 67 - const handleWorkspaceSwitch = async (nextWorkspaceId: string) => { 68 - await authClient.organization.setActive({ 69 - organizationId: nextWorkspaceId, 70 - }); 71 - 72 - navigate({ 73 - to: "/dashboard/workspace/$workspaceId", 74 - params: { workspaceId: nextWorkspaceId }, 75 60 }); 76 61 }; 77 62 ··· 170 155 </div> 171 156 172 157 <div className="flex shrink-0 items-center gap-1.5"> 173 - <NotificationDropdown /> 174 158 {headerActions} 175 159 </div> 176 160 </div>
-2
apps/web/src/components/common/workspace-layout.tsx
··· 1 1 import type { ReactNode } from "react"; 2 2 import Layout from "@/components/common/layout"; 3 - import NotificationDropdown from "@/components/notification/notification-dropdown"; 4 3 import { 5 4 Breadcrumb, 6 5 BreadcrumbItem, ··· 79 78 </Breadcrumb> 80 79 </div> 81 80 <div className={`${cn("flex items-center gap-1.5", className)}`}> 82 - <NotificationDropdown /> 83 81 {headerActions} 84 82 </div> 85 83 </div>
+16 -1
apps/web/src/hooks/use-task-filters-with-labels-support.ts
··· 43 43 export function useTaskFiltersWithLabelsSupport( 44 44 project: ProjectWithTasks | null | undefined, 45 45 projectId?: string, 46 + textQuery?: string, 46 47 ) { 47 48 const queryClient = useQueryClient(); 48 49 const storageKey = projectId ? `kaneo:board-filters:${projectId}` : null; ··· 84 85 85 86 const filterTasks = useCallback( 86 87 (tasks: Task[]): Task[] => { 88 + const normalizedTextQuery = textQuery?.trim().toLowerCase(); 89 + 87 90 return tasks.filter((task) => { 91 + if (normalizedTextQuery) { 92 + const title = task.title?.toLowerCase() ?? ""; 93 + const description = task.description?.toLowerCase() ?? ""; 94 + const matchesText = 95 + title.includes(normalizedTextQuery) || 96 + description.includes(normalizedTextQuery); 97 + 98 + if (!matchesText) { 99 + return false; 100 + } 101 + } 102 + 88 103 if ( 89 104 filters.status && 90 105 filters.status.length > 0 && ··· 167 182 return true; 168 183 }); 169 184 }, 170 - [filters, getTaskLabels], 185 + [filters, getTaskLabels, textQuery], 171 186 ); 172 187 173 188 const filteredProject = useMemo(() => {
+68 -1
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board.tsx
··· 1 1 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 2 + import { Search } from "lucide-react"; 2 3 import { useCallback, useEffect, useState } from "react"; 3 4 import BoardToolbar from "@/components/board/board-toolbar"; 4 5 import ProjectLayout from "@/components/common/project-layout"; ··· 7 8 import PageTitle from "@/components/page-title"; 8 9 import CreateTaskModal from "@/components/shared/modals/create-task-modal"; 9 10 import TaskDetailsSheet from "@/components/task/task-details-sheet"; 11 + import { Input } from "@/components/ui/input"; 10 12 import { shortcuts } from "@/constants/shortcuts"; 11 13 import useGetLabelsByWorkspace from "@/hooks/queries/label/use-get-labels-by-workspace"; 12 14 import { useGetTasks } from "@/hooks/queries/task/use-get-tasks"; ··· 37 39 const { project, setProject } = useProjectStore(); 38 40 const { viewMode, setViewMode } = useUserPreferencesStore(); 39 41 const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); 42 + const [boardSearchQuery, setBoardSearchQuery] = useState(""); 43 + const [isBoardSearchMounted, setIsBoardSearchMounted] = useState(false); 44 + const [isBoardSearchVisible, setIsBoardSearchVisible] = useState(false); 45 + const [boardSearchInput, setBoardSearchInput] = 46 + useState<HTMLInputElement | null>(null); 40 47 41 48 const { data: users } = useGetActiveWorkspaceUsers(workspaceId); 42 49 const { data: workspaceLabels = [] } = useGetLabelsByWorkspace(workspaceId); ··· 69 76 } 70 77 }, [data, setProject]); 71 78 79 + const openBoardSearch = useCallback(() => { 80 + setIsBoardSearchMounted(true); 81 + window.requestAnimationFrame(() => setIsBoardSearchVisible(true)); 82 + }, []); 83 + 84 + const closeBoardSearch = useCallback(() => { 85 + setIsBoardSearchVisible(false); 86 + window.setTimeout(() => setIsBoardSearchMounted(false), 180); 87 + }, []); 88 + 89 + useEffect(() => { 90 + const onKeyDown = (event: KeyboardEvent) => { 91 + const isFindShortcut = 92 + (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f"; 93 + 94 + if (!isFindShortcut) return; 95 + 96 + event.preventDefault(); 97 + openBoardSearch(); 98 + }; 99 + 100 + window.addEventListener("keydown", onKeyDown); 101 + return () => window.removeEventListener("keydown", onKeyDown); 102 + }, [openBoardSearch]); 103 + 104 + useEffect(() => { 105 + if (!isBoardSearchMounted) return; 106 + window.requestAnimationFrame(() => boardSearchInput?.focus()); 107 + }, [isBoardSearchMounted, boardSearchInput]); 108 + 72 109 const { 73 110 filters, 74 111 updateFilter, ··· 76 113 filteredProject, 77 114 hasActiveFilters, 78 115 clearFilters, 79 - } = useTaskFiltersWithLabelsSupport(project, projectId); 116 + } = useTaskFiltersWithLabelsSupport(project, projectId, boardSearchQuery); 117 + 118 + const boardHeaderSearch = isBoardSearchMounted ? ( 119 + <div 120 + className={`relative w-[240px] origin-top transition-all duration-180 ease-out ${ 121 + isBoardSearchVisible 122 + ? "translate-y-0 scale-y-100 opacity-100" 123 + : "pointer-events-none -translate-y-1 scale-y-95 opacity-0" 124 + }`} 125 + > 126 + <Search className="-translate-y-1/2 pointer-events-none absolute top-1/2 left-2.5 h-3.5 w-3.5 text-muted-foreground" /> 127 + <Input 128 + ref={setBoardSearchInput} 129 + value={boardSearchQuery} 130 + onChange={(event) => setBoardSearchQuery(event.target.value)} 131 + onKeyDown={(event) => { 132 + if (event.key === "Escape" && !boardSearchQuery.trim()) { 133 + closeBoardSearch(); 134 + } 135 + }} 136 + onBlur={() => { 137 + if (!boardSearchQuery.trim()) { 138 + closeBoardSearch(); 139 + } 140 + }} 141 + placeholder="Search tickets..." 142 + className="h-7.5 [&_[data-slot=input]]:h-7 [&_[data-slot=input]]:leading-7 [&_[data-slot=input]]:pl-8 [&_[data-slot=input]]:text-xs [&_[data-slot=input]]:placeholder:text-xs [&_[data-slot=input]]:placeholder:leading-7" 143 + /> 144 + </div> 145 + ) : null; 80 146 81 147 return ( 82 148 <ProjectLayout 83 149 projectId={projectId} 84 150 workspaceId={workspaceId} 85 151 activeView="board" 152 + headerActions={boardHeaderSearch} 86 153 > 87 154 <PageTitle 88 155 title={`${project?.name} — ${viewMode === "board" ? "Board" : "List"}`}