@@ -9,78 +9,86 @@ import Fuse from 'fuse.js';
99import React , { useEffect } from 'react' ;
1010import { sandboxesTypes } from 'app/overmind/namespaces/dashboard/types' ;
1111
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 ,
5738 } ) )
5839 . filter ( f => f . title ) ;
5940
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+ } ;
7266
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 ) ;
8492} ;
8593
8694export const useGetItems = ( {
@@ -91,73 +99,71 @@ export const useGetItems = ({
9199 query : string ;
92100 username : string ;
93101 getFilteredSandboxes : (
94- sandboxes : (
95- | SandboxFragmentDashboardFragment
96- | SidebarCollectionDashboardFragment
97- ) [ ]
102+ list : DashboardItem [ ]
98103 ) => SandboxFragmentDashboardFragment [ ] ;
99104} ) => {
100- const foundResults : Array <
101- SandboxFragmentDashboardFragment | SidebarCollectionDashboardFragment
102- > = useSearchedSandboxes ( query ) || [ ] ;
105+ const state = useAppState ( ) ;
106+ const actions = useActions ( ) ;
103107
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 ] ) ;
108112
109- const filteredSandboxes : SandboxFragmentDashboardFragment [ ] = getFilteredSandboxes (
110- sandboxesInSearch
113+ // keep a SearchIndex in state
114+ const [ searchIndex , setSearchIndex ] = React . useState < SearchIndex | null > (
115+ null
111116 ) ;
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 ] ) ;
112138
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 ;
119144
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+ } ) ;
121150
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 ;
127154 }
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+ } ) ;
129167
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 ;
163169} ;
0 commit comments