@@ -10,21 +10,19 @@ import { safeParseFloat } from 'utils/index';
1010import { RouteLocationRaw } from 'vue-router' ;
1111import { fuzzyMatch } from '.' ;
1212import { getFormRoute , routeTo } from './ui' ;
13+ import { searchGroups } from '../../utils/types' ;
14+ import type { SearchGroup , SearchItem } from '../../utils/types' ;
1315
14- export const searchGroups = [
15- 'Docs' ,
16- 'List' ,
17- 'Create' ,
18- 'Report' ,
19- 'Page' ,
20- ] as const ;
16+ export { searchGroups } ;
17+ export type { SearchGroup , SearchItem } ;
2118
22- export type SearchGroup = typeof searchGroups [ number ] ;
23- interface SearchItem {
19+ interface StoredRecentItem {
2420 label : string ;
25- group : Exclude < SearchGroup , 'Docs' > ;
21+ group : string ;
2622 route ?: string ;
27- action ?: ( ) => void | Promise < void > ;
23+ schemaName ?: string ;
24+ reportName ?: string ;
25+ timestamp : number ;
2826}
2927
3028interface DocSearchItem extends Omit < SearchItem , 'group' > {
@@ -33,7 +31,11 @@ interface DocSearchItem extends Omit<SearchItem, 'group'> {
3331 more : string [ ] ;
3432}
3533
36- export type SearchItems = ( DocSearchItem | SearchItem ) [ ] ;
34+ interface RecentSearchItem extends Omit < SearchItem , 'group' > {
35+ group : 'Recent' ;
36+ }
37+
38+ export type SearchItems = ( DocSearchItem | SearchItem | RecentSearchItem ) [ ] ;
3739
3840interface Searchable {
3941 needsUpdate : boolean ;
@@ -69,6 +71,7 @@ export function getGroupLabelMap() {
6971 Report : t `Report` ,
7072 Docs : t `Docs` ,
7173 Page : t `Page` ,
74+ Recent : t `Recent` ,
7275 } ;
7376}
7477
@@ -195,7 +198,7 @@ function getListViewList(fyo: Fyo): SearchItem[] {
195198 ModelNameEnum . PrintTemplate ,
196199 ] ;
197200
198- if ( fyo . doc . singles . AccountingSecuttings ?. enableInventory ) {
201+ if ( fyo . doc . singles . AccountingSettings ?. enableInventory ) {
199202 schemaNames . push (
200203 ModelNameEnum . StockMovement ,
201204 ModelNameEnum . Shipment ,
@@ -350,6 +353,7 @@ export class Search {
350353
351354 _obsSet = false ;
352355 numSearches = 0 ;
356+ recentKey = 'searchRecents' ;
353357 searchables : Record < string , Searchable > ;
354358 keywords : Record < string , Keyword [ ] > ;
355359 priorityMap : Record < string , number > = {
@@ -371,6 +375,7 @@ export class Search {
371375 Create : true ,
372376 Page : true ,
373377 Docs : true ,
378+ Recent : true ,
374379 } ,
375380 schemaFilters : { } ,
376381 skipTables : false ,
@@ -384,6 +389,9 @@ export class Search {
384389 _nonDocSearchList : SearchItem [ ] ;
385390 _groupLabelMap ?: Record < SearchGroup , string > ;
386391
392+ maxRecentItems = 10 ;
393+ recentExpiryDays = 30 ;
394+
387395 constructor ( fyo : Fyo ) {
388396 this . fyo = fyo ;
389397 this . keywords = { } ;
@@ -396,6 +404,89 @@ export class Search {
396404 * `skipT*` filters and the `schemaFilters`.
397405 */
398406
407+ private _loadAndCleanRecentItems ( ) : StoredRecentItem [ ] {
408+ try {
409+ const raw = localStorage . getItem ( this . recentKey ) ;
410+ return raw ? ( JSON . parse ( raw ) as StoredRecentItem [ ] ) : [ ] ;
411+ } catch ( error ) {
412+ return [ ] ;
413+ }
414+ }
415+
416+ private _saveRecentItems ( items : StoredRecentItem [ ] ) {
417+ localStorage . setItem ( this . recentKey , JSON . stringify ( items ) ) ;
418+ }
419+
420+ addToRecent ( item : SearchItems [ number ] ) {
421+ const recents = this . _loadAndCleanRecentItems ( ) ;
422+
423+ const recentItem : StoredRecentItem = {
424+ label : item . label ,
425+ group : item . group ,
426+ timestamp : Date . now ( ) ,
427+ } ;
428+
429+ if ( 'route' in item && item . route ) {
430+ recentItem . route = item . route ;
431+ } else if ( item . group === 'Docs' ) {
432+ recentItem . schemaName = item . schemaLabel ;
433+ }
434+
435+ const updatedRecents = [
436+ recentItem ,
437+ ...recents . filter ( ( r ) => r . label !== recentItem . label ) ,
438+ ] . slice ( 0 , this . maxRecentItems ) ;
439+
440+ this . _saveRecentItems ( updatedRecents ) ;
441+ }
442+
443+ getRecentItems ( searchTerm ?: string ) : RecentSearchItem [ ] {
444+ try {
445+ const recents = this . _loadAndCleanRecentItems ( ) ;
446+
447+ let filtered = recents ;
448+ if ( searchTerm ) {
449+ const lower = searchTerm . toLowerCase ( ) ;
450+ filtered = recents . filter (
451+ ( item ) =>
452+ item . label . toLowerCase ( ) . includes ( lower ) ||
453+ item . group . toLowerCase ( ) . includes ( lower )
454+ ) ;
455+ }
456+
457+ const result = filtered . map ( ( item ) => ( {
458+ label : item . label ,
459+ group : 'Recent' as const ,
460+ action : ( ) => this . _executeRecentAction ( item ) ,
461+ route : item . route ,
462+ } ) ) ;
463+
464+ return result ;
465+ } catch ( error ) {
466+ return [ ] ;
467+ }
468+ }
469+
470+ private _executeRecentAction ( item : StoredRecentItem ) {
471+ if ( item . route ) {
472+ void routeTo ( item . route ) ;
473+ } else if ( item . schemaName ) {
474+ this . _openDocList ( item . schemaName ) ;
475+ } else if ( item . reportName ) {
476+ this . _openReport ( item . reportName ) ;
477+ }
478+ }
479+
480+ private _openDocList ( schemaName : string ) {
481+ const route = `/list/${ schemaName } ` ;
482+ void routeTo ( route ) ;
483+ }
484+
485+ private _openReport ( reportName : string ) {
486+ const route = `/report/${ reportName } ` ;
487+ void routeTo ( route ) ;
488+ }
489+
399490 get skipTables ( ) {
400491 let value = true ;
401492 for ( const val of Object . values ( this . searchables ) ) {
@@ -500,7 +591,7 @@ export class Search {
500591 }
501592
502593 _searchSuggestions ( input : string ) : SearchItems {
503- const matches : { si : SearchItem | DocSearchItem ; distance : number } [ ] = [ ] ;
594+ const matches : { si : SearchItems [ number ] ; distance : number } [ ] = [ ] ;
504595
505596 for ( const si of this . _intermediate . suggestions ) {
506597 const label = si . label ;
@@ -571,9 +662,21 @@ export class Search {
571662
572663 keys . sort ( ( a , b ) => safeParseFloat ( b ) - safeParseFloat ( a ) ) ;
573664 const array : SearchItems = [ ] ;
665+
666+ const showRecent =
667+ ! input ||
668+ input . startsWith ( '#' ) ||
669+ input . toLowerCase ( ) . startsWith ( 'recent' ) ;
670+ if ( showRecent && this . filters . groupFilters . Recent ) {
671+ const recentSearchTerm = input ?. replace ( / ^ # | r e c e n t / gi, '' ) . trim ( ) ;
672+ const recentItems = this . getRecentItems ( recentSearchTerm ) ;
673+ if ( recentItems . length > 0 ) {
674+ array . push ( ...recentItems ) ;
675+ }
676+ }
677+
574678 for ( const key of keys ) {
575679 const keywords = groupedKeywords [ key ] ?? [ ] ;
576-
577680 this . _pushDocSearchItems ( keywords , array , input ) ;
578681 if ( key === '0' ) {
579682 this . _pushNonDocSearchItems ( array , input ) ;
@@ -609,8 +712,7 @@ export class Search {
609712 items : ( SearchItem | Keyword ) [ ] ,
610713 input ?: string
611714 ) : SearchItems {
612- const subArray : { item : SearchItem | DocSearchItem ; distance : number } [ ] =
613- [ ] ;
715+ const subArray : { item : SearchItems [ number ] ; distance : number } [ ] = [ ] ;
614716
615717 for ( const item of items ) {
616718 const subArrayItem = this . _getSubArrayItem ( item , input ) ;
@@ -625,7 +727,10 @@ export class Search {
625727 return subArray . map ( ( { item } ) => item ) ;
626728 }
627729
628- _getSubArrayItem ( item : SearchItem | Keyword , input ?: string ) {
730+ _getSubArrayItem (
731+ item : SearchItem | Keyword ,
732+ input ?: string
733+ ) : { item : SearchItems [ number ] ; distance : number } | null {
629734 if ( isSearchItem ( item ) ) {
630735 return this . _getSubArrayItemFromSearchItem ( item , input ) ;
631736 }
0 commit comments