@@ -9,78 +9,86 @@ import Fuse from 'fuse.js';
9
9
import React , { useEffect } from 'react' ;
10
10
import { sandboxesTypes } from 'app/overmind/namespaces/dashboard/types' ;
11
11
12
- const useSearchedSandboxes = ( query : string ) => {
13
- const state = useAppState ( ) ;
14
- const actions = useActions ( ) ;
15
- const [ foundResults , setFoundResults ] = React . useState <
16
- | ( SandboxFragmentDashboardFragment | SidebarCollectionDashboardFragment ) [ ]
17
- | null
18
- > ( null ) ;
19
- const [ searchIndex , setSearchindex ] = React . useState < Fuse <
20
- SandboxFragmentDashboardFragment | SidebarCollectionDashboardFragment ,
21
- unknown
22
- > | null > ( null ) ;
23
-
24
- useEffect ( ( ) => {
25
- actions . dashboard . getPage ( sandboxesTypes . SEARCH ) ;
26
- } , [ actions . dashboard , state . activeTeam ] ) ;
27
-
28
- useEffect (
29
- ( ) => {
30
- setSearchindex ( calculateSearchIndex ( state . dashboard , state . activeTeam ) ) ;
31
- } ,
32
- // eslint-disable-next-line react-hooks/exhaustive-deps
33
- [
34
- state . dashboard . sandboxes . SEARCH ,
35
- state . dashboard . repositoriesByTeamId ,
36
- state . activeTeam ,
37
- ]
38
- ) ;
39
-
40
- useEffect ( ( ) => {
41
- if ( searchIndex ) {
42
- setFoundResults ( searchIndex . search ( query ) ) ;
43
- }
44
- // eslint-disable-next-line react-hooks/exhaustive-deps
45
- } , [ query , searchIndex ] ) ;
46
-
47
- return foundResults ;
48
- } ;
49
-
50
- const calculateSearchIndex = ( dashboard : any , activeTeam : string ) => {
51
- const sandboxes = dashboard . sandboxes . SEARCH || [ ] ;
52
-
53
- const folders : Collection [ ] = ( dashboard . allCollections || [ ] )
54
- . map ( collection => ( {
55
- ...collection ,
56
- title : collection . name ,
12
+ type DashboardItem =
13
+ | SandboxFragmentDashboardFragment
14
+ | SidebarCollectionDashboardFragment ;
15
+
16
+ // define which fields to search, with per-key thresholds & weights
17
+ const SEARCH_KEYS = [
18
+ { name : 'title' , threshold : 0.2 , weight : 0.4 } ,
19
+ { name : 'description' , threshold : 0.3 , weight : 0.2 } ,
20
+ { name : 'alias' , threshold : 0.3 , weight : 0.2 } ,
21
+ { name : 'source.template' , threshold : 0.4 , weight : 0.1 } ,
22
+ { name : 'id' , threshold : 0.0 , weight : 0.1 } , // exact-only
23
+ ] as const ;
24
+
25
+ interface SearchIndex {
26
+ fuses : Record < string , Fuse < DashboardItem > > ;
27
+ weights : Record < string , number > ;
28
+ items : DashboardItem [ ] ;
29
+ }
30
+
31
+ const buildSearchIndex = ( dashboard : any , activeTeam : string ) : SearchIndex => {
32
+ const sandboxes : DashboardItem [ ] = dashboard . sandboxes . SEARCH || [ ] ;
33
+
34
+ const folders : DashboardItem [ ] = ( dashboard . allCollections || [ ] )
35
+ . map ( ( c : Collection ) => ( {
36
+ ...c ,
37
+ title : c . name ,
57
38
} ) )
58
39
. filter ( f => f . title ) ;
59
40
60
- const teamRepos = dashboard . repositoriesByTeamId [ activeTeam ] ?? [ ] ;
61
- const repositories = ( teamRepos || [ ] ) . map ( ( repo : Repository ) => {
62
- return {
63
- title : repo . repository . name ,
64
- /**
65
- * Due to the lack of description we add the owner so we can at least
66
- * include that in the search query.
67
- */
68
- description : repo . repository . owner ,
69
- ...repo ,
70
- } ;
71
- } ) ;
41
+ const repos : DashboardItem [ ] = (
42
+ dashboard . repositoriesByTeamId [ activeTeam ] || [ ]
43
+ ) . map ( ( r : Repository ) => ( {
44
+ title : r . repository . name ,
45
+ description : r . repository . owner ,
46
+ ...r ,
47
+ } ) ) ;
48
+
49
+ const items = [ ...sandboxes , ...folders , ...repos ] ;
50
+
51
+ // build a Fuse instance per key
52
+ const fuses : Record < string , Fuse < DashboardItem > > = { } ;
53
+ const weights : Record < string , number > = { } ;
54
+
55
+ for ( const { name, threshold, weight } of SEARCH_KEYS ) {
56
+ fuses [ name ] = new Fuse ( items , {
57
+ keys : [ name ] ,
58
+ threshold : threshold ,
59
+ distance : 1000 ,
60
+ } ) ;
61
+ weights [ name ] = weight ;
62
+ }
63
+
64
+ return { fuses, weights, items } ;
65
+ } ;
72
66
73
- return new Fuse ( [ ...sandboxes , ...folders , ...repositories ] , {
74
- threshold : 0.1 ,
75
- distance : 1000 ,
76
- keys : [
77
- { name : 'title' , weight : 0.4 } ,
78
- { name : 'description' , weight : 0.2 } ,
79
- { name : 'alias' , weight : 0.2 } ,
80
- { name : 'source.template' , weight : 0.1 } ,
81
- { name : 'id' , weight : 0.1 } ,
82
- ] ,
83
- } ) ;
67
+ // merge+dedupe results from every key
68
+ const mergeSearchResults = (
69
+ index : SearchIndex ,
70
+ query : string
71
+ ) : DashboardItem [ ] => {
72
+ const hits : Array < DashboardItem > = [ ] ;
73
+
74
+ for ( const key of Object . keys ( index . fuses ) ) {
75
+ const fuse = index . fuses [ key ] ;
76
+ for ( const item of fuse . search ( query ) ) {
77
+ hits . push ( item ) ;
78
+ }
79
+ }
80
+
81
+ // dedupe by item.id, keep the best (lowest) weighted score
82
+ const byId : Record < string , DashboardItem > = { } ;
83
+ for ( const item of hits ) {
84
+ const id = ( item as any ) . id as string ;
85
+ if ( ! byId [ id ] ) {
86
+ byId [ id ] = item ;
87
+ }
88
+ }
89
+
90
+ // sort & return
91
+ return Object . values ( byId ) ;
84
92
} ;
85
93
86
94
export const useGetItems = ( {
@@ -91,73 +99,71 @@ export const useGetItems = ({
91
99
query : string ;
92
100
username : string ;
93
101
getFilteredSandboxes : (
94
- sandboxes : (
95
- | SandboxFragmentDashboardFragment
96
- | SidebarCollectionDashboardFragment
97
- ) [ ]
102
+ list : DashboardItem [ ]
98
103
) => SandboxFragmentDashboardFragment [ ] ;
99
104
} ) => {
100
- const foundResults : Array <
101
- SandboxFragmentDashboardFragment | SidebarCollectionDashboardFragment
102
- > = useSearchedSandboxes ( query ) || [ ] ;
105
+ const state = useAppState ( ) ;
106
+ const actions = useActions ( ) ;
103
107
104
- // @ts -ignore
105
- const sandboxesInSearch = foundResults . filter ( s => ! s . path ) ;
106
- // @ts -ignore
107
- const foldersInSearch = foundResults . filter ( s => s . path ) ;
108
+ // load page once
109
+ useEffect ( ( ) => {
110
+ actions . dashboard . getPage ( sandboxesTypes . SEARCH ) ;
111
+ } , [ actions . dashboard , state . activeTeam ] ) ;
108
112
109
- const filteredSandboxes : SandboxFragmentDashboardFragment [ ] = getFilteredSandboxes (
110
- sandboxesInSearch
113
+ // keep a SearchIndex in state
114
+ const [ searchIndex , setSearchIndex ] = React . useState < SearchIndex | null > (
115
+ null
111
116
) ;
117
+ useEffect ( ( ) => {
118
+ if ( ! state . dashboard . sandboxes . SEARCH || ! state . dashboard . allCollections )
119
+ return ;
120
+ const idx = buildSearchIndex ( state . dashboard , state . activeTeam ) ;
121
+ setSearchIndex ( idx ) ;
122
+ } , [
123
+ state . dashboard . sandboxes . SEARCH ,
124
+ state . dashboard . allCollections ,
125
+ state . dashboard . repositoriesByTeamId ,
126
+ state . activeTeam ,
127
+ ] ) ;
128
+
129
+ // run the merged search whenever query or index changes
130
+ const [ foundResults , setFoundResults ] = React . useState < DashboardItem [ ] > ( [ ] ) ;
131
+ useEffect ( ( ) => {
132
+ if ( searchIndex && query ) {
133
+ setFoundResults ( mergeSearchResults ( searchIndex , query ) ) ;
134
+ } else {
135
+ setFoundResults ( [ ] ) ;
136
+ }
137
+ } , [ query , searchIndex ] ) ;
112
138
113
- const orderedSandboxes = [ ...foldersInSearch , ...filteredSandboxes ] . filter (
114
- item => {
115
- // @ts -ignore
116
- if ( item . path || item . repository ) {
117
- return true ;
118
- }
139
+ // then the rest is just your existing filtering / mapping logic:
140
+ const sandboxesInSearch = foundResults . filter ( s => ! ( s as any ) . path ) ;
141
+ const foldersInSearch = foundResults . filter ( s => ( s as any ) . path ) ;
142
+ const filteredSandboxes = getFilteredSandboxes ( sandboxesInSearch ) ;
143
+ const isLoadingQuery = query && ! searchIndex ;
119
144
120
- const sandbox = item as SandboxFragmentDashboardFragment ;
145
+ const ordered = [ ...foldersInSearch , ...filteredSandboxes ] . filter ( item => {
146
+ if ( ( item as any ) . path || ( item as any ) . repository ) return true ;
147
+ const sb = item as SandboxFragmentDashboardFragment ;
148
+ return ! sb . draft || ( sb . draft && sb . author . username === username ) ;
149
+ } ) ;
121
150
122
- // Remove draft sandboxes from other authors
123
- return (
124
- ! sandbox . draft ||
125
- ( sandbox . draft && sandbox . author . username === username )
126
- ) ;
151
+ const items = ordered . map ( found => {
152
+ if ( ( found as any ) . path ) {
153
+ return { type : 'folder' , ...( found as object ) } as any ;
127
154
}
128
- ) ;
155
+ if ( ( found as any ) . repository ) {
156
+ const f = found as any ;
157
+ return {
158
+ type : 'repository' ,
159
+ repository : {
160
+ branchCount : f . branchCount ,
161
+ repository : f . repository ,
162
+ } ,
163
+ } as any ;
164
+ }
165
+ return { type : 'sandbox' , sandbox : found } as any ;
166
+ } ) ;
129
167
130
- // @ts -ignore
131
- const items : DashboardGridItem [ ] =
132
- foundResults != null
133
- ? orderedSandboxes . map ( found => {
134
- // @ts -ignore
135
- if ( found . path ) {
136
- return {
137
- type : 'folder' ,
138
- ...found ,
139
- } ;
140
- }
141
-
142
- // @ts -ignore
143
- if ( found . repository ) {
144
- return {
145
- type : 'repository' ,
146
- repository : {
147
- // @ts -ignore
148
- branchCount : found . branchCount ,
149
- // @ts -ignore
150
- repository : found . repository ,
151
- } ,
152
- } ;
153
- }
154
-
155
- return {
156
- type : 'sandbox' ,
157
- sandbox : found ,
158
- } ;
159
- } )
160
- : [ { type : 'skeleton-row' } ] ;
161
-
162
- return [ items , sandboxesInSearch ] ;
168
+ return [ items , sandboxesInSearch , isLoadingQuery ] as const ;
163
169
} ;
0 commit comments