11'use client'
22
33import React , { useCallback , useEffect , useMemo , useState } from 'react'
4- import { GitMergeIcon , GitPullRequestClosedIcon , GitPullRequestIcon } from '@primer/octicons-react'
4+ import { GitMergeIcon , GitPullRequestClosedIcon , GitPullRequestIcon , XIcon } from '@primer/octicons-react'
55import { formatDistance , fromUnixTime } from 'date-fns'
66import { useAtom } from 'jotai'
77import { useRouter } from 'next/router'
88
99import { LabelItem , SyncOrganizationMember as Member , PostApiClListData } from '@gitmono/types/generated'
10- import { Button , CheckIcon , ChevronDownIcon , OrderedListIcon } from '@gitmono/ui'
10+ import { Button , CheckIcon , ChevronDownIcon , OrderedListIcon , SearchIcon } from '@gitmono/ui'
1111import { cn } from '@gitmono/ui/src/utils'
1212
1313import { MemberHovercard } from '@/components/InlinePost/MemberHovercard'
1414import { IssueIndexTabFilter as CLIndexTabFilter } from '@/components/Issues/IssueIndex'
1515import {
16+ ListItem as CLItem ,
17+ IssueList as ClList ,
1618 Dropdown ,
1719 DropdownItemwithAvatar ,
1820 DropdownItemwithLabel ,
1921 DropdownOrder ,
2022 DropdownReview ,
21- ListBanner ,
22- ListItem as CLItem ,
23- IssueList as ClList
23+ ListBanner
2424} from '@/components/Issues/IssueList'
2525import { useScope } from '@/contexts/scope'
26- import { useGetLabelList } from '@/hooks/useGetLabelList'
2726import { usePostClList } from '@/hooks/CL/usePostClList'
27+ import { useGetLabelList } from '@/hooks/useGetLabelList'
2828import { useSyncedMembers } from '@/hooks/useSyncedMembers'
2929import { apiErrorToast } from '@/utils/apiErrorToast'
3030import { atomWithWebStorage } from '@/utils/atomWithWebStorage'
@@ -34,7 +34,7 @@ import { AdditionType, ItemLabels, ItemRightIcons } from '../Issues/IssuesConten
3434import { Pagination } from '../Issues/Pagenation'
3535import { orderTags , reviewTags } from '../Issues/utils/consts'
3636import { generateAllMenuItems , MenuConfig } from '../Issues/utils/generateAllMenuItems'
37- import { filterAtom , clCloseCurrentPage , clidAtom , clOpenCurrentPage , sortAtom } from '../Issues/utils/store'
37+ import { clCloseCurrentPage , clidAtom , clOpenCurrentPage , filterAtom , sortAtom } from '../Issues/utils/store'
3838import { Heading } from './catalyst/heading'
3939
4040// interface ClInfoItem {
@@ -48,6 +48,10 @@ import { Heading } from './catalyst/heading'
4848
4949type ItemsType = NonNullable < PostApiClListData [ 'data' ] > [ 'items' ]
5050
51+ const escapeRegExp = ( str : string ) : string => {
52+ return str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
53+ }
54+
5155export default function CLView ( ) {
5256 const { scope } = useScope ( )
5357 const [ clList , setClList ] = useState < ItemsType > ( [ ] )
@@ -81,6 +85,8 @@ export default function CLView() {
8185
8286 const [ review , setReview ] = useAtom ( reviewAtom )
8387
88+ const [ searchQuery , setSearchQuery ] = useState ( '' )
89+
8490 const additions = useCallback (
8591 ( labels : number [ ] ) : AdditionType => {
8692 const additional : AdditionType = { status, asc : false }
@@ -179,11 +185,8 @@ export default function CLView() {
179185 < >
180186 opened { formatDistance ( fromUnixTime ( item . open_timestamp ) , new Date ( ) , { addSuffix : true } ) } by{ ' ' }
181187 < MemberHovercard username = { item . author } >
182- < span className = 'cursor-pointer hover:text-blue-600 hover:underline' >
183- { item . author }
184- </ span >
188+ < span className = 'cursor-pointer hover:text-blue-600 hover:underline' > { item . author } </ span >
185189 </ MemberHovercard >
186-
187190 </ >
188191 )
189192 case 'merged' :
@@ -192,13 +195,10 @@ export default function CLView() {
192195 < >
193196 by{ ' ' }
194197 < MemberHovercard username = { item . author } >
195- < span className = 'cursor-pointer hover:text-blue-600 hover:underline' >
196- { item . author }
197- </ span >
198+ < span className = 'cursor-pointer hover:text-blue-600 hover:underline' > { item . author } </ span >
198199 </ MemberHovercard >
199200 { ' was merged ' }
200201 { formatDistance ( fromUnixTime ( item . merge_timestamp ?? 0 ) , new Date ( ) , { addSuffix : true } ) }
201-
202202 </ >
203203 )
204204 } else {
@@ -209,16 +209,10 @@ export default function CLView() {
209209 < >
210210 by{ ' ' }
211211 < MemberHovercard username = { item . author } >
212- < span className = 'cursor-pointer hover:text-blue-600 hover:underline' >
213- { item . author }
214- </ span >
212+ < span className = 'cursor-pointer hover:text-blue-600 hover:underline' > { item . author } </ span >
215213 </ MemberHovercard >
216-
217214 { ' was closed ' }
218-
219215 { formatDistance ( fromUnixTime ( item . updated_at ) , new Date ( ) , { addSuffix : true } ) }
220-
221-
222216 </ >
223217 )
224218 default :
@@ -233,12 +227,18 @@ export default function CLView() {
233227 onSelectFactory : ( item : Member ) => ( e : Event ) => {
234228 e . preventDefault ( )
235229 if ( item . user . username === sort [ 'Author' ] ) {
236- loadClList ( )
230+ const escapedAuthor = escapeRegExp ( sort [ 'Author' ] )
231+ const newQuery = searchQuery . replace ( new RegExp ( `author:${ escapedAuthor } \\s*` , 'g' ) , '' ) . trim ( )
232+
233+ setSearchQuery ( newQuery )
237234 setSort ( {
238235 ...sort ,
239236 Author : ''
240237 } )
241238 } else {
239+ const newQuery = searchQuery ? `${ searchQuery } author:${ item . user . username } ` : `author:${ item . user . username } `
240+
241+ setSearchQuery ( newQuery )
242242 setSort ( {
243243 ...sort ,
244244 Author : item . user . username
@@ -254,12 +254,20 @@ export default function CLView() {
254254 onSelectFactory : ( item : Member ) => ( e : Event ) => {
255255 e . preventDefault ( )
256256 if ( item . user . username === sort [ 'Assignees' ] ) {
257- loadClList ( )
257+ const escapedAssignee = escapeRegExp ( sort [ 'Assignees' ] )
258+ const newQuery = searchQuery . replace ( new RegExp ( `assignee:${ escapedAssignee } \\s*` , 'g' ) , '' ) . trim ( )
259+
260+ setSearchQuery ( newQuery )
258261 setSort ( {
259262 ...sort ,
260263 Assignees : ''
261264 } )
262265 } else {
266+ const newQuery = searchQuery
267+ ? `${ searchQuery } assignee:${ item . user . username } `
268+ : `assignee:${ item . user . username } `
269+
270+ setSearchQuery ( newQuery )
263271 setSort ( {
264272 ...sort ,
265273 Assignees : item . user . username
@@ -279,8 +287,16 @@ export default function CLView() {
279287 onSelectFactory : ( item ) => ( e : Event ) => {
280288 e . preventDefault ( )
281289 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 )
282294 setLabel ( label . filter ( ( i ) => i !== String ( item . id ) ) )
283295 } else {
296+ const escapedLabelName = escapeRegExp ( item . name )
297+ const newQuery = searchQuery ? `${ searchQuery } label:"${ escapedLabelName } "` : `label:"${ escapedLabelName } "`
298+
299+ setSearchQuery ( newQuery )
284300 setLabel ( [ ...label , String ( item . id ) ] )
285301 }
286302 } ,
@@ -297,8 +313,17 @@ export default function CLView() {
297313 onSelectFactory : ( item ) => ( e : Event ) => {
298314 e . preventDefault ( )
299315 if ( item === review ) {
316+ const escapedReview = escapeRegExp ( review )
317+ const newQuery = searchQuery . replace ( new RegExp ( `review:${ escapedReview } \\s*` , 'g' ) , '' ) . trim ( )
318+
319+ setSearchQuery ( newQuery )
300320 setReview ( '' )
301321 } 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 )
302327 setReview ( item )
303328 }
304329 } ,
@@ -445,13 +470,52 @@ export default function CLView() {
445470
446471 const router = useRouter ( )
447472
473+ const clearAllFilters = ( ) => {
474+ setSearchQuery ( '' )
475+ setSort ( { ...sort , Author : '' , Assignees : '' } )
476+ setLabel ( [ ] )
477+ setReview ( '' )
478+ }
479+
448480 return (
449481 < div className = 'relative m-4 flex h-screen flex-col' >
450482 < Heading > Change List</ Heading >
451483 < br />
452484 < IndexPageContainer >
453485 < IndexPageContent id = '/[org]/cl' className = { cn ( '@container' , '3xl:max-w-7xl max-w-7xl' ) } >
454486 < div className = 'flex h-full flex-col' >
487+ < div className = 'mb-4' >
488+ < div className = 'group flex min-h-[42px] items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm transition-all focus-within:border-blue-500 focus-within:shadow-md focus-within:ring-2 focus-within:ring-blue-100 hover:border-gray-400' >
489+ < div className = 'flex items-center text-gray-400' >
490+ < SearchIcon className = 'h-4 w-4' />
491+ </ div >
492+
493+ < input
494+ type = 'text'
495+ className = 'w-full flex-1 border-none bg-transparent py-1 text-sm text-gray-400 outline-none ring-0 focus:outline-none focus:ring-0'
496+ value = { searchQuery }
497+ onChange = { ( e ) => setSearchQuery ( e . target . value ) }
498+ onKeyDown = { ( e ) => {
499+ if ( e . key === 'Enter' ) {
500+ loadClList ( )
501+ }
502+ } }
503+ />
504+
505+ { searchQuery && (
506+ < button
507+ onClick = { ( ) => {
508+ clearAllFilters ( )
509+ } }
510+ className = 'flex items-center justify-center rounded-md p-1 text-gray-400 transition-all hover:bg-gray-100 hover:text-gray-600'
511+ title = 'Clear search'
512+ >
513+ < XIcon className = 'h-4 w-4' />
514+ </ button >
515+ ) }
516+ </ div >
517+ </ div >
518+
455519 < ClList
456520 isLoading = { isLoading }
457521 Issuelists = { clList }
@@ -487,7 +551,7 @@ export default function CLView() {
487551 } }
488552 >
489553 < div className = 'text-xs text-[#59636e]' >
490- < span className = " mr-2" > #{ i . link } </ span >
554+ < span className = ' mr-2' > #{ i . link } </ span >
491555 { getDescription ( i ) }
492556 { ' • ChangeList' }
493557 </ div >
0 commit comments