@@ -8,37 +8,18 @@ import {
88 RepoIcon ,
99 SearchIcon ,
1010} from '@primer/octicons-react' ;
11- import { Box , Stack , Text , TextInputWithTokens } from '@primer/react' ;
11+ import { Box , Stack , Text } from '@primer/react' ;
1212
1313import { AppContext } from '../../context/App' ;
1414import { IconColor , type SearchToken , Size } from '../../types' ;
15- import {
16- hasExcludeSearchFilters ,
17- hasIncludeSearchFilters ,
18- SEARCH_PREFIXES ,
19- } from '../../utils/notifications/filters/search' ;
15+ import { hasExcludeSearchFilters , hasIncludeSearchFilters } from '../../utils/notifications/filters/search' ;
2016import { Title } from '../primitives/Title' ;
2117import { RequiresDetailedNotificationWarning } from './RequiresDetailedNotificationsWarning' ;
18+ import { TokenSearchInput } from './TokenSearchInput' ;
19+ import { cn } from '../../utils/cn' ;
2220
2321type InputToken = { id : number ; text : string } ;
2422
25- import { SearchFilterSuggestions } from './SearchFilterSuggestions' ;
26-
27- const tokenEvents = [ 'Enter' , 'Tab' , ' ' , ',' ] ;
28-
29- function parseRawValue ( raw : string ) : string | null {
30- const value = raw . trim ( ) ;
31- if ( ! value ) return null ;
32- // Find a matching prefix (prefixes already include the colon)
33- const matched = SEARCH_PREFIXES . find ( ( p ) =>
34- value . toLowerCase ( ) . startsWith ( p ) ,
35- ) ;
36- if ( ! matched ) return null ;
37- const rest = value . substring ( matched . length ) ;
38- if ( rest . length === 0 ) return null ;
39- return `${ matched } ${ rest } ` ; // matched already has ':'
40- }
41-
4223export const SearchFilter : FC = ( ) => {
4324 const { updateFilter, settings } = useContext ( AppContext ) ;
4425
@@ -56,89 +37,39 @@ export const SearchFilter: FC = () => {
5637 const mapValuesToTokens = ( values : string [ ] ) : InputToken [ ] =>
5738 values . map ( ( value , index ) => ( { id : index , text : value } ) ) ;
5839
59- const [ includeSearchTokens , setIncludeSearchTokens ] = useState < InputToken [ ] > (
60- mapValuesToTokens ( settings . filterIncludeSearchTokens ) ,
61- ) ;
62-
63- const addIncludeSearchToken = (
64- event :
65- | React . KeyboardEvent < HTMLInputElement >
66- | React . FocusEvent < HTMLInputElement > ,
67- ) => {
68- const raw = ( event . target as HTMLInputElement ) . value ;
69- const value = parseRawValue ( raw ) ;
40+ const [ includeSearchTokens , setIncludeSearchTokens ] = useState < InputToken [ ] > ( mapValuesToTokens ( settings . filterIncludeSearchTokens ) ) ;
7041
71- if ( value && ! includeSearchTokens . some ( ( v ) => v . text === value ) ) {
72- setIncludeSearchTokens ( [
73- ...includeSearchTokens ,
74- { id : includeSearchTokens . length , text : value } ,
75- ] ) ;
76- updateFilter ( 'filterIncludeSearchTokens' , value as SearchToken , true ) ;
77- ( event . target as HTMLInputElement ) . value = '' ;
78- }
42+ const addIncludeSearchToken = ( value : string ) => {
43+ if ( ! value || includeSearchTokens . some ( ( v ) => v . text === value ) ) return ;
44+ const nextId = includeSearchTokens . reduce ( ( m , t ) => Math . max ( m , t . id ) , - 1 ) + 1 ;
45+ setIncludeSearchTokens ( [ ...includeSearchTokens , { id : nextId , text : value } ] ) ;
46+ updateFilter ( 'filterIncludeSearchTokens' , value as SearchToken , true ) ;
7947 } ;
8048
8149 const removeIncludeSearchToken = ( tokenId : string | number ) => {
8250 const value = includeSearchTokens . find ( ( v ) => v . id === tokenId ) ?. text || '' ;
83- updateFilter ( 'filterIncludeSearchTokens' , value as SearchToken , false ) ;
51+ if ( value ) updateFilter ( 'filterIncludeSearchTokens' , value as SearchToken , false ) ;
8452 setIncludeSearchTokens ( includeSearchTokens . filter ( ( v ) => v . id !== tokenId ) ) ;
8553 } ;
8654
87- const [ includeInputValue , setIncludeInputValue ] = useState ( '' ) ;
88- const [ showIncludeSuggestions , setShowIncludeSuggestions ] = useState ( false ) ;
89-
90- const includeSearchTokensKeyDown = (
91- event : React . KeyboardEvent < HTMLInputElement > ,
92- ) => {
93- if ( tokenEvents . includes ( event . key ) ) {
94- addIncludeSearchToken ( event ) ;
95- setShowIncludeSuggestions ( false ) ;
96- } else if ( event . key === 'ArrowDown' ) {
97- setShowIncludeSuggestions ( true ) ;
98- }
99- } ;
100-
101- const [ excludeSearchTokens , setExcludeSearchTokens ] = useState < InputToken [ ] > (
102- mapValuesToTokens ( settings . filterExcludeSearchTokens ) ,
103- ) ;
55+ // now handled inside TokenSearchInput
10456
105- const addExcludeSearchToken = (
106- event :
107- | React . KeyboardEvent < HTMLInputElement >
108- | React . FocusEvent < HTMLInputElement > ,
109- ) => {
110- const raw = ( event . target as HTMLInputElement ) . value ;
111- const value = parseRawValue ( raw ) ;
57+ const [ excludeSearchTokens , setExcludeSearchTokens ] = useState < InputToken [ ] > ( mapValuesToTokens ( settings . filterExcludeSearchTokens ) ) ;
11258
113- if ( value && ! excludeSearchTokens . some ( ( v ) => v . text === value ) ) {
114- setExcludeSearchTokens ( [
115- ...excludeSearchTokens ,
116- { id : excludeSearchTokens . length , text : value } ,
117- ] ) ;
118- updateFilter ( 'filterExcludeSearchTokens' , value as SearchToken , true ) ;
119- ( event . target as HTMLInputElement ) . value = '' ;
120- }
59+ const addExcludeSearchToken = ( value : string ) => {
60+ if ( ! value || excludeSearchTokens . some ( ( v ) => v . text === value ) ) return ;
61+ const nextId = excludeSearchTokens . reduce ( ( m , t ) => Math . max ( m , t . id ) , - 1 ) + 1 ;
62+ setExcludeSearchTokens ( [ ...excludeSearchTokens , { id : nextId , text : value } ] ) ;
63+ updateFilter ( 'filterExcludeSearchTokens' , value as SearchToken , true ) ;
12164 } ;
12265
12366 const removeExcludeSearchToken = ( tokenId : string | number ) => {
12467 const value = excludeSearchTokens . find ( ( v ) => v . id === tokenId ) ?. text || '' ;
125- updateFilter ( 'filterExcludeSearchTokens' , value as SearchToken , false ) ;
68+ if ( value ) updateFilter ( 'filterExcludeSearchTokens' , value as SearchToken , false ) ;
12669 setExcludeSearchTokens ( excludeSearchTokens . filter ( ( v ) => v . id !== tokenId ) ) ;
12770 } ;
12871
129- const [ excludeInputValue , setExcludeInputValue ] = useState ( '' ) ;
130- const [ showExcludeSuggestions , setShowExcludeSuggestions ] = useState ( false ) ;
131-
132- const excludeSearchTokensKeyDown = (
133- event : React . KeyboardEvent < HTMLInputElement > ,
134- ) => {
135- if ( tokenEvents . includes ( event . key ) ) {
136- addExcludeSearchToken ( event ) ;
137- setShowExcludeSuggestions ( false ) ;
138- } else if ( event . key === 'ArrowDown' ) {
139- setShowExcludeSuggestions ( true ) ;
140- }
141- } ;
72+ // handled by TokenSearchInput
14273
14374 // Basic suggestions for prefixes
14475 const fieldsetId = useId ( ) ;
@@ -154,14 +85,15 @@ export const SearchFilter: FC = () => {
15485 < Stack direction = "vertical" gap = "condensed" >
15586 < Stack direction = "horizontal" gap = "condensed" >
15687 < PersonIcon size = { Size . SMALL } />
157- Author (author:handle)
88+ < Text className = { cn ( "text-gitify-caution" , ! settings . detailedNotifications && "line-through" ) } > Author (author:handle)</ Text >
15889 </ Stack >
15990 < Stack direction = "horizontal" gap = "condensed" >
16091 < OrganizationIcon size = { Size . SMALL } />
161- Organization (org:name)
92+ < Text > Organization (org:name)</ Text >
16293 </ Stack >
16394 < Stack direction = "horizontal" gap = "condensed" >
164- < RepoIcon size = { Size . SMALL } /> Repository (repo:fullname)
95+ < RepoIcon size = { Size . SMALL } />
96+ < Text > Repository (repo:fullname)</ Text >
16597 </ Stack >
16698 </ Stack >
16799 </ Box >
@@ -173,110 +105,28 @@ export const SearchFilter: FC = () => {
173105 </ Title >
174106
175107 < Stack direction = "vertical" gap = "condensed" >
176- < Stack
177- align = "center"
178- className = "text-sm"
179- direction = "horizontal"
180- gap = "condensed"
181- >
182- < Box className = "font-medium text-gitify-font w-20" >
183- < Stack align = "center" direction = "horizontal" gap = "condensed" >
184- < CheckCircleFillIcon className = { IconColor . GREEN } />
185- < Text > Include:</ Text >
186- </ Stack >
187- </ Box >
188- < Box flexGrow = { 1 } position = "relative" >
189- < TextInputWithTokens
190- block
191- disabled = { ! settings . detailedNotifications }
192- onBlur = { ( e ) => {
193- addIncludeSearchToken ( e ) ;
194- setShowIncludeSuggestions ( false ) ;
195- } }
196- onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => {
197- setIncludeInputValue ( e . target . value ) ;
198- // Show suggestions once user starts typing or clears until prefix chosen
199- const val = e . target . value . trim ( ) ;
200- if ( ! val . includes ( ':' ) ) {
201- setShowIncludeSuggestions ( true ) ;
202- } else {
203- setShowIncludeSuggestions ( false ) ;
204- }
205- } }
206- onFocus = { ( e ) => {
207- if (
208- ! hasExcludeSearchFilters ( settings ) &&
209- ! ! settings . detailedNotifications &&
210- ( e . target as HTMLInputElement ) . value . trim ( ) === ''
211- ) {
212- setShowIncludeSuggestions ( true ) ;
213- }
214- } }
215- onKeyDown = { includeSearchTokensKeyDown }
216- onTokenRemove = { removeIncludeSearchToken }
217- size = "small"
218- title = "Include searches"
219- tokens = { includeSearchTokens }
220- />
221- < SearchFilterSuggestions
222- inputValue = { includeInputValue }
223- onClose = { ( ) => setShowIncludeSuggestions ( false ) }
224- open = { showIncludeSuggestions }
225- />
226- </ Box >
227- </ Stack >
228-
229- < Stack
230- align = "center"
231- className = "text-sm"
232- direction = "horizontal"
233- gap = "condensed"
234- >
235- < Box className = "font-medium text-gitify-font w-20" >
236- < Stack align = "center" direction = "horizontal" gap = "condensed" >
237- < NoEntryFillIcon className = { IconColor . RED } />
238- < Text > Exclude:</ Text >
239- </ Stack >
240- </ Box >
241- < Box flexGrow = { 1 } position = "relative" >
242- < TextInputWithTokens
243- block
244- disabled = { ! settings . detailedNotifications }
245- onBlur = { ( e ) => {
246- addExcludeSearchToken ( e ) ;
247- setShowExcludeSuggestions ( false ) ;
248- } }
249- onChange = { ( e : React . ChangeEvent < HTMLInputElement > ) => {
250- setExcludeInputValue ( e . target . value ) ;
251- const val = e . target . value . trim ( ) ;
252- if ( ! val . includes ( ':' ) ) {
253- setShowExcludeSuggestions ( true ) ;
254- } else {
255- setShowExcludeSuggestions ( false ) ;
256- }
257- } }
258- onFocus = { ( e ) => {
259- if (
260- ! hasIncludeSearchFilters ( settings ) &&
261- ! ! settings . detailedNotifications &&
262- ( e . target as HTMLInputElement ) . value . trim ( ) === ''
263- ) {
264- setShowExcludeSuggestions ( true ) ;
265- }
266- } }
267- onKeyDown = { excludeSearchTokensKeyDown }
268- onTokenRemove = { removeExcludeSearchToken }
269- size = "small"
270- title = "Exclude searches"
271- tokens = { excludeSearchTokens }
272- />
273- < SearchFilterSuggestions
274- inputValue = { excludeInputValue }
275- onClose = { ( ) => setShowExcludeSuggestions ( false ) }
276- open = { showExcludeSuggestions }
277- />
278- </ Box >
279- </ Stack >
108+ < TokenSearchInput
109+ icon = { CheckCircleFillIcon }
110+ iconColorClass = { IconColor . GREEN }
111+ label = "Include"
112+ onAdd = { addIncludeSearchToken }
113+ onRemove = { removeIncludeSearchToken }
114+ showSuggestionsOnFocusIfEmpty = {
115+ ! hasExcludeSearchFilters ( settings ) && ! ! settings . detailedNotifications
116+ }
117+ tokens = { includeSearchTokens }
118+ />
119+ < TokenSearchInput
120+ icon = { NoEntryFillIcon }
121+ iconColorClass = { IconColor . RED }
122+ label = "Exclude"
123+ onAdd = { addExcludeSearchToken }
124+ onRemove = { removeExcludeSearchToken }
125+ showSuggestionsOnFocusIfEmpty = {
126+ ! hasIncludeSearchFilters ( settings ) && ! ! settings . detailedNotifications
127+ }
128+ tokens = { excludeSearchTokens }
129+ />
280130 </ Stack >
281131 </ fieldset >
282132 ) ;
0 commit comments