Skip to content

Commit 4fed077

Browse files
authored
feat: implement bidirectional sync between search box and filter state for Issue and CL (#1601)
- Leverage additional object with filter state as single source of truth - Fix null value issue when selecting labels in Issue - Fix bug where labels are cleared after selecting assignee in Issue/CL - Comment out Search component in Issue, replace with read-only filter display - Simplify dropdown configs to directly update state
1 parent 9dc2fe8 commit 4fed077

File tree

4 files changed

+187
-119
lines changed

4 files changed

+187
-119
lines changed

moon/apps/web/components/ClView/index.tsx

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ import { Heading } from './catalyst/heading'
4848

4949
type ItemsType = NonNullable<PostApiClListData['data']>['items']
5050

51-
const escapeRegExp = (str: string): string => {
52-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
53-
}
54-
5551
export default function CLView() {
5652
const { scope } = useScope()
5753
const [clList, setClList] = useState<ItemsType>([])
@@ -85,8 +81,71 @@ export default function CLView() {
8581

8682
const [review, setReview] = useAtom(reviewAtom)
8783

84+
const autoGeneratedQuery = useMemo(() => {
85+
const parts: string[] = []
86+
87+
if (sort.Author) {
88+
parts.push(`author:${sort.Author}`)
89+
}
90+
91+
if (sort.Assignees) {
92+
parts.push(`assignee:${sort.Assignees}`)
93+
}
94+
95+
if (label.length > 0) {
96+
const labelNames = label
97+
.map((id) => labelList.find((l) => String(l.id) === id)?.name)
98+
.filter((name): name is string => Boolean(name))
99+
100+
labelNames.forEach((name: string) => {
101+
parts.push(`label:${name}`)
102+
})
103+
}
104+
105+
if (review) {
106+
parts.push(`review:${review}`)
107+
}
108+
109+
return parts.join(' ')
110+
}, [sort, label, labelList, review])
111+
88112
const [searchQuery, setSearchQuery] = useState('')
89113

114+
useEffect(() => {
115+
setSearchQuery(autoGeneratedQuery)
116+
}, [autoGeneratedQuery])
117+
118+
const parseAndApplyQuery = useCallback(
119+
(query: string) => {
120+
const authorMatch = query.match(/author:(\S+)/)
121+
const newAuthor = authorMatch ? authorMatch[1] : ''
122+
123+
const assigneeMatch = query.match(/assignee:(\S+)/)
124+
const newAssignee = assigneeMatch ? assigneeMatch[1] : ''
125+
126+
const labelRegex = /label:"([^"]+)"|label:(\S+)/g
127+
const labelNames: string[] = []
128+
let labelMatch
129+
130+
while ((labelMatch = labelRegex.exec(query)) !== null) {
131+
const labelName = labelMatch[1] || labelMatch[2]
132+
133+
if (labelName) labelNames.push(labelName)
134+
}
135+
136+
const reviewMatch = query.match(/review:(\S+)/)
137+
const reviewValue = reviewMatch ? reviewMatch[1] : ''
138+
139+
const newLabelIds =
140+
labelNames.length > 0 ? labelList.filter((l) => labelNames.includes(l.name)).map((l) => String(l.id)) : label
141+
142+
setSort({ ...sort, Author: newAuthor, Assignees: newAssignee })
143+
setLabel(newLabelIds)
144+
setReview(reviewValue)
145+
},
146+
[labelList, sort, label, setSort, setLabel, setReview]
147+
)
148+
90149
const additions = useCallback(
91150
(labels: number[]): AdditionType => {
92151
const additional: AdditionType = { status, asc: false }
@@ -112,7 +171,8 @@ export default function CLView() {
112171
const loadClList = useCallback(
113172
(additional?: AdditionType) => {
114173
setIsLoading(true)
115-
const addittion = additional ? additional : additions([])
174+
const labelIds = label.map((i) => Number(i))
175+
const addittion = additional ? additional : additions(labelIds)
116176

117177
fetchClList(
118178
{
@@ -142,7 +202,7 @@ export default function CLView() {
142202
}
143203
)
144204
},
145-
[page, pageSize, fetchClList, additions]
205+
[page, pageSize, fetchClList, additions, label]
146206
)
147207

148208
useEffect(() => {
@@ -227,22 +287,9 @@ export default function CLView() {
227287
onSelectFactory: (item: Member) => (e: Event) => {
228288
e.preventDefault()
229289
if (item.user.username === sort['Author']) {
230-
const escapedAuthor = escapeRegExp(sort['Author'])
231-
const newQuery = searchQuery.replace(new RegExp(`author:${escapedAuthor}\\s*`, 'g'), '').trim()
232-
233-
setSearchQuery(newQuery)
234-
setSort({
235-
...sort,
236-
Author: ''
237-
})
290+
setSort({ ...sort, Author: '' })
238291
} else {
239-
const newQuery = searchQuery ? `${searchQuery} author:${item.user.username}` : `author:${item.user.username}`
240-
241-
setSearchQuery(newQuery)
242-
setSort({
243-
...sort,
244-
Author: item.user.username
245-
})
292+
setSort({ ...sort, Author: item.user.username })
246293
}
247294
},
248295
className: 'overflow-hidden',
@@ -254,24 +301,9 @@ export default function CLView() {
254301
onSelectFactory: (item: Member) => (e: Event) => {
255302
e.preventDefault()
256303
if (item.user.username === sort['Assignees']) {
257-
const escapedAssignee = escapeRegExp(sort['Assignees'])
258-
const newQuery = searchQuery.replace(new RegExp(`assignee:${escapedAssignee}\\s*`, 'g'), '').trim()
259-
260-
setSearchQuery(newQuery)
261-
setSort({
262-
...sort,
263-
Assignees: ''
264-
})
304+
setSort({ ...sort, Assignees: '' })
265305
} else {
266-
const newQuery = searchQuery
267-
? `${searchQuery} assignee:${item.user.username}`
268-
: `assignee:${item.user.username}`
269-
270-
setSearchQuery(newQuery)
271-
setSort({
272-
...sort,
273-
Assignees: item.user.username
274-
})
306+
setSort({ ...sort, Assignees: item.user.username })
275307
}
276308
},
277309
className: 'overflow-hidden',
@@ -283,20 +315,11 @@ export default function CLView() {
283315
{
284316
key: 'Labels',
285317
isChosen: (item) => label?.includes(String(item.id)),
286-
287318
onSelectFactory: (item) => (e: Event) => {
288319
e.preventDefault()
289320
if (label?.includes(String(item.id))) {
290-
const escapedLabelName = escapeRegExp(item.name)
291-
const newQuery = searchQuery.replace(new RegExp(`label:"${escapedLabelName}"\\s*`, 'g'), '').trim()
292-
293-
setSearchQuery(newQuery)
294321
setLabel(label.filter((i) => i !== String(item.id)))
295322
} else {
296-
const escapedLabelName = escapeRegExp(item.name)
297-
const newQuery = searchQuery ? `${searchQuery} label:"${escapedLabelName}"` : `label:"${escapedLabelName}"`
298-
299-
setSearchQuery(newQuery)
300323
setLabel([...label, String(item.id)])
301324
}
302325
},
@@ -309,21 +332,11 @@ export default function CLView() {
309332
{
310333
key: 'Review',
311334
isChosen: () => true,
312-
313335
onSelectFactory: (item) => (e: Event) => {
314336
e.preventDefault()
315337
if (item === review) {
316-
const escapedReview = escapeRegExp(review)
317-
const newQuery = searchQuery.replace(new RegExp(`review:${escapedReview}\\s*`, 'g'), '').trim()
318-
319-
setSearchQuery(newQuery)
320338
setReview('')
321339
} else {
322-
const escapedReview = escapeRegExp(review)
323-
let newQuery = searchQuery.replace(new RegExp(`review:${escapedReview}\\s*`, 'g'), '').trim()
324-
325-
newQuery = newQuery ? `${newQuery} review:${item}` : `review:${item}`
326-
setSearchQuery(newQuery)
327340
setReview(item)
328341
}
329342
},
@@ -471,7 +484,6 @@ export default function CLView() {
471484
const router = useRouter()
472485

473486
const clearAllFilters = () => {
474-
setSearchQuery('')
475487
setSort({ ...sort, Author: '', Assignees: '' })
476488
setLabel([])
477489
setReview('')
@@ -497,7 +509,7 @@ export default function CLView() {
497509
onChange={(e) => setSearchQuery(e.target.value)}
498510
onKeyDown={(e) => {
499511
if (e.key === 'Enter') {
500-
loadClList()
512+
parseAndApplyQuery(searchQuery)
501513
}
502514
}}
503515
/>

moon/apps/web/components/Issues/IssueIndex.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ export const IssueIndex = () => {
2727
// const isSearchLoading = queryDebounced.length > 0 && getNotes.isFetching
2828
// const isSearchLoading = queryDebounced.length > 0
2929

30+
const [filterQuery, setFilterQuery] = useState('')
31+
const [onClearFilters, setOnClearFilters] = useState<(() => void) | undefined>()
32+
33+
const handleClearFilters = () => {
34+
if (onClearFilters) {
35+
onClearFilters()
36+
}
37+
}
38+
3039
return (
3140
<>
3241
<FloatingNewDocButton />
@@ -36,8 +45,12 @@ export const IssueIndex = () => {
3645
<IssueBreadcrumbIcon />
3746
</BreadcrumbTitlebar>
3847
<IndexPageContent id='/[org]/issue' className={cn('@container', '3xl:max-w-7xl max-w-7xl')}>
39-
<IssueSearch />
40-
<IssuesContent searching={isSearching} />
48+
<IssueSearch filterQuery={filterQuery} onClearFilters={handleClearFilters} />
49+
<IssuesContent
50+
searching={isSearching}
51+
setFilterQuery={setFilterQuery}
52+
onRegisterClearFilters={setOnClearFilters}
53+
/>
4154
</IndexPageContent>
4255
</IndexPageContainer>
4356
<SplitViewDetail />

moon/apps/web/components/Issues/IssueSearch.tsx

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
1-
import { useState } from 'react'
2-
import { useDebounce } from 'use-debounce'
1+
// import { useState } from 'react'
2+
// import { useDebounce } from 'use-debounce'
3+
4+
import { XIcon } from '@primer/octicons-react'
35

46
import { Button, Link } from '@gitmono/ui'
57

68
import { BreadcrumbTitlebar } from '@/components/Titlebar/BreadcrumbTitlebar'
79
import { useScope } from '@/contexts/scope'
810

9-
import Search from './Search'
10-
import { fuseOptions, searchList } from './utils/consts'
11+
// import Search from './Search'
12+
// import { fuseOptions, searchList } from './utils/consts'
13+
14+
interface IssueSearchProps {
15+
filterQuery?: string
16+
onClearFilters?: () => void
17+
}
1118

12-
export default function IssueSearch() {
13-
const [query, setQuery] = useState('')
14-
const [queryDebounced] = useDebounce(query, 150)
15-
const [open, setOpen] = useState(false)
16-
const handleQuery = (val: string) => {
17-
setOpen(true)
18-
setQuery(val)
19-
}
19+
export default function IssueSearch({ filterQuery, onClearFilters }: IssueSearchProps) {
20+
// const [query, setQuery] = useState('')
21+
// const [queryDebounced] = useDebounce(query, 150)
22+
// const [open, setOpen] = useState(false)
23+
// const handleQuery = (val: string) => {
24+
// setOpen(true)
25+
// setQuery(val)
26+
// }
2027

2128
// const isSearching = query.length > 0
2229
// const isSearchLoading = queryDebounced.length > 0 && getNotes.isFetching
23-
const isSearchLoading = queryDebounced.length > 0
30+
// const isSearchLoading = queryDebounced.length > 0
2431

2532
return (
2633
<>
2734
<BreadcrumbTitlebar className='z-20 justify-between border-b-0 px-0'>
28-
<Search
35+
{/* <Search
2936
SearchQuery={{ query, setQuery: handleQuery, isSearchLoading }}
3037
SearchListTable={{
3138
open,
@@ -36,7 +43,26 @@ export default function IssueSearch() {
3643
fuseOptions,
3744
searchList
3845
}}
39-
/>
46+
/> */}
47+
48+
<div className='relative flex flex-1 items-center'>
49+
<input
50+
type='text'
51+
value={filterQuery || ''}
52+
readOnly
53+
placeholder='Filter issues by author, assignee, or labels...'
54+
className='flex-1 rounded-md border border-gray-300 bg-gray-50 px-3 py-2 pr-10 text-sm text-gray-700'
55+
/>
56+
{filterQuery && (
57+
<button
58+
onClick={onClearFilters}
59+
className='absolute right-2 flex items-center justify-center rounded-md p-1 text-gray-400 transition-all hover:bg-gray-200 hover:text-gray-600'
60+
title='Clear filters'
61+
>
62+
<XIcon className='h-4 w-4' />
63+
</button>
64+
)}
65+
</div>
4066
{/* <IndexSearchInput query={query} setQuery={setQuery} isSearchLoading={isSearchLoading} /> */}
4167
<LabelsButton />
4268
<NewIssueButton />
@@ -67,4 +93,4 @@ const LabelsButton = () => {
6793
</Button>
6894
</Link>
6995
)
70-
}
96+
}

0 commit comments

Comments
 (0)