From a07e27b735ea23b93f47e129f4688cbaac4dfa47 Mon Sep 17 00:00:00 2001
From: Andrey Khasanov <7570386+Aliamondo@users.noreply.github.com>
Date: Fri, 24 Jul 2026 11:23:25 +0200
Subject: [PATCH 1/2] Add search
---
src/components/AppBar.tsx | 6 ++
src/components/PullRequestFilterBar.tsx | 50 +++++++-----
src/components/SearchBar.tsx | 103 ++++++++++++++++++++++++
src/helpers/prFilters.ts | 18 +++++
src/hooks/useDebounce.ts | 12 +++
src/views/DailyHelper/DailyHelper.tsx | 12 ++-
6 files changed, 177 insertions(+), 24 deletions(-)
create mode 100644 src/components/SearchBar.tsx
create mode 100644 src/hooks/useDebounce.ts
diff --git a/src/components/AppBar.tsx b/src/components/AppBar.tsx
index a72e403..7e1c5c1 100644
--- a/src/components/AppBar.tsx
+++ b/src/components/AppBar.tsx
@@ -15,6 +15,7 @@ import IconButton from '@mui/material/IconButton'
import LinearProgress from '@mui/material/LinearProgress'
import MenuIcon from '@mui/icons-material/Menu'
import ReloadIcon from '@mui/icons-material/Replay'
+import SearchBar from './SearchBar'
import Settings from './settings/Settings'
import SettingsIcon from '@mui/icons-material/Settings'
import Tab from '@mui/material/Tab'
@@ -35,6 +36,8 @@ type AppBarElementProps = {
drawbarName: string
isDrawbarOpen: boolean
setIsDrawbarOpen: (newState: boolean) => void
+ onSearch?: (query: string) => void
+ showSearch?: boolean
}
export default function AppBarElement({
handleReload: initialHandleReload,
@@ -45,6 +48,8 @@ export default function AppBarElement({
drawbarName,
isDrawbarOpen,
setIsDrawbarOpen,
+ onSearch,
+ showSearch = true,
}: AppBarElementProps) {
const relativeTime = useRelativeTime(lastUpdated)
const [teamTabValue, setTeamTabValue] = useState(0)
@@ -118,6 +123,7 @@ export default function AppBarElement({
+ {onSearch && showSearch && }
{showDrawbar && (
diff --git a/src/components/PullRequestFilterBar.tsx b/src/components/PullRequestFilterBar.tsx
index 4a57633..a883d88 100644
--- a/src/components/PullRequestFilterBar.tsx
+++ b/src/components/PullRequestFilterBar.tsx
@@ -24,28 +24,34 @@ function FilterButton({
onClick,
}: FilterButtonProps) {
return (
-
-
-
- {label}
-
-
+
+
+
+
+ {label}
+
+
+
)
}
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx
new file mode 100644
index 0000000..0d041fa
--- /dev/null
+++ b/src/components/SearchBar.tsx
@@ -0,0 +1,103 @@
+import { useEffect, useState } from 'react'
+
+import CloseIcon from '@mui/icons-material/Close'
+import IconButton from '@mui/material/IconButton'
+import InputAdornment from '@mui/material/InputAdornment'
+import SearchIcon from '@mui/icons-material/Search'
+import TextField from '@mui/material/TextField'
+import Tooltip from '@mui/material/Tooltip'
+import { useDebounce } from '../hooks/useDebounce'
+
+const DEBOUNCE_MS = 120
+
+type SearchBarProps = {
+ onSearch: (query: string) => void
+}
+
+export default function SearchBar({ onSearch }: SearchBarProps) {
+ const [expanded, setExpanded] = useState(false)
+ const [value, setValue] = useState('')
+ const debouncedValue = useDebounce(value, DEBOUNCE_MS)
+
+ useEffect(() => {
+ onSearch(debouncedValue)
+ }, [debouncedValue, onSearch])
+
+ const collapseIfEmpty = () => {
+ if (!value) setExpanded(false)
+ }
+
+ const clear = () => {
+ setValue('')
+ setExpanded(false)
+ }
+
+ if (!expanded) {
+ return (
+
+ setExpanded(true)}
+ >
+
+
+
+ )
+ }
+
+ return (
+ setValue(e.target.value)}
+ onBlur={collapseIfEmpty}
+ onKeyDown={e => {
+ if (e.key === 'Escape') clear()
+ }}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ endAdornment: (
+
+ e.preventDefault()}
+ onClick={clear}
+ >
+
+
+
+ ),
+ }}
+ sx={{
+ width: { xs: 150, sm: 240 },
+ '& .MuiOutlinedInput-root': {
+ color: 'inherit',
+ bgcolor: 'rgba(255, 255, 255, 0.12)',
+ '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
+ '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
+ '&.Mui-focused fieldset': {
+ borderColor: 'rgba(255, 255, 255, 0.7)',
+ },
+ },
+ '& .MuiInputBase-input::placeholder': {
+ color: 'inherit',
+ opacity: 0.7,
+ },
+ }}
+ />
+ )
+}
diff --git a/src/helpers/prFilters.ts b/src/helpers/prFilters.ts
index 600507b..2a83f5a 100644
--- a/src/helpers/prFilters.ts
+++ b/src/helpers/prFilters.ts
@@ -17,6 +17,24 @@ function isTitleWhitelisted(title: string, filters: Settings_Filters): boolean {
)
}
+export function matchesSearch(pr: PullRequest, rawQuery: string): boolean {
+ const query = rawQuery.trim().toLowerCase()
+ if (!query) return true
+
+ const userMatches = (user: User) =>
+ getDisplayName(user).toLowerCase().includes(query) ||
+ user.login.toLowerCase().includes(query)
+
+ return (
+ pr.title.toLowerCase().includes(query) ||
+ `#${pr.number}`.includes(query) ||
+ pr.repositoryName.toLowerCase().includes(query) ||
+ userMatches(pr.author) ||
+ pr.assignees.some(userMatches) ||
+ pr.labels.some(label => label.name.toLowerCase().includes(query))
+ )
+}
+
export function applyReviewRequiredFilter(
prs: PullRequest[],
viewerLogin: string,
diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts
new file mode 100644
index 0000000..8523ae1
--- /dev/null
+++ b/src/hooks/useDebounce.ts
@@ -0,0 +1,12 @@
+import { useEffect, useState } from 'react'
+
+export function useDebounce(value: T, delayMs: number): T {
+ const [debounced, setDebounced] = useState(value)
+
+ useEffect(() => {
+ const id = setTimeout(() => setDebounced(value), delayMs)
+ return () => clearTimeout(id)
+ }, [value, delayMs])
+
+ return debounced
+}
diff --git a/src/views/DailyHelper/DailyHelper.tsx b/src/views/DailyHelper/DailyHelper.tsx
index 963f38b..5d5f79b 100644
--- a/src/views/DailyHelper/DailyHelper.tsx
+++ b/src/views/DailyHelper/DailyHelper.tsx
@@ -21,7 +21,10 @@ import Stack from '@mui/material/Stack'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import VisibleIcon from '@mui/icons-material/Visibility'
-import { applyReviewRequiredFilter } from '../../helpers/prFilters'
+import {
+ applyReviewRequiredFilter,
+ matchesSearch,
+} from '../../helpers/prFilters'
import { getDisplayName } from '../../helpers/getDisplayName'
import { getStateRank } from '../../helpers/getStateRank'
import { dataFetcher } from '../../helpers/dataFetcher'
@@ -68,6 +71,7 @@ export default function DailyHelper() {
const isMyPrsFilterActive = activeFilter === 'myPrs'
const isMyWorkFilterActive = activeFilter === 'myWork'
const [viewerLogin, setViewerLogin] = useState(null)
+ const [searchQuery, setSearchQuery] = useState('')
const [activeView, setActiveView] = useState<'list' | 'kanban' | 'notes'>(
settingsHandler.loadView(),
)
@@ -191,7 +195,8 @@ export default function DailyHelper() {
(!reviewRequiredFilteredIds ||
reviewRequiredFilteredIds.has(pr.id)) &&
(!isMyPrsFilterActive || pr.author.login === viewerLogin) &&
- (!isMyWorkFilterActive || isMyWork(pr)),
+ (!isMyWorkFilterActive || isMyWork(pr)) &&
+ matchesSearch(pr, searchQuery),
),
[
sortedPullRequests,
@@ -202,6 +207,7 @@ export default function DailyHelper() {
viewerLogin,
hiddenLabels,
isPullRequestsWithoutLabelsHidden,
+ searchQuery,
],
)
@@ -275,6 +281,8 @@ export default function DailyHelper() {
drawbarName="Search filters"
isDrawbarOpen={isDrawbarOpen}
setIsDrawbarOpen={setIsDrawbarOpen}
+ onSearch={setSearchQuery}
+ showSearch={activeView !== 'notes'}
/>
Date: Fri, 24 Jul 2026 11:26:15 +0200
Subject: [PATCH 2/2] Bump package.json
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 60f55fb..9ffa59b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "daily-helper",
- "version": "2.3.0",
+ "version": "2.4.0",
"private": true,
"scripts": {
"start": "vite",