@@ -39,6 +39,14 @@ RED.palette = (function() {
3939
4040 let filterRefreshTimeout
4141
42+ // Catalog search variables
43+ let catalogues = [ ] ;
44+ let loadedCatalogs = [ ] ;
45+ let catalogLoadedList = [ ] ;
46+ let catalogLoadedIndex = { } ;
47+ let catalogSearchTimeout ;
48+ let availableNodesContainer ;
49+
4250 function createCategory ( originalCategory , rootCategory , category , ns ) {
4351 if ( $ ( "#red-ui-palette-base-category-" + rootCategory ) . length === 0 ) {
4452 createCategoryContainer ( originalCategory , rootCategory , ns + ":palette.label." + rootCategory ) ;
@@ -603,6 +611,175 @@ RED.palette = (function() {
603611 }
604612 }
605613 }
614+
615+ // Trigger catalog search with debouncing
616+ clearTimeout ( catalogSearchTimeout ) ;
617+ if ( val && val . trim ( ) !== "" ) {
618+ catalogSearchTimeout = setTimeout ( function ( ) {
619+ searchCatalog ( val ) ;
620+ } , 300 ) ;
621+ } else {
622+ hideAvailableNodes ( ) ;
623+ }
624+ }
625+
626+ // Load node catalogs for search
627+ function loadNodeCatalogs ( done ) {
628+ catalogLoadedList = [ ] ;
629+ catalogLoadedIndex = { } ;
630+ loadedCatalogs . length = 0 ;
631+ let handled = 0 ;
632+ const catalogueCount = catalogues . length ;
633+
634+ for ( let index = 0 ; index < catalogues . length ; index ++ ) {
635+ const url = catalogues [ index ] ;
636+ $ . getJSON ( url , { _ : new Date ( ) . getTime ( ) } , function ( v ) {
637+ loadedCatalogs . push ( {
638+ index : index ,
639+ url : url ,
640+ name : v . name ,
641+ updated_at : v . updated_at ,
642+ modules_count : ( v . modules || [ ] ) . length
643+ } ) ;
644+ handleCatalogResponse ( { url : url , name : v . name } , index , v ) ;
645+ } ) . fail ( function ( _jqxhr , _textStatus , error ) {
646+ console . warn ( "Error loading catalog" , url , ":" , error ) ;
647+ } ) . always ( function ( ) {
648+ handled ++ ;
649+ if ( handled === catalogueCount ) {
650+ loadedCatalogs . sort ( ( a , b ) => a . index - b . index ) ;
651+ if ( done ) {
652+ done ( ) ;
653+ }
654+ }
655+ } ) ;
656+ }
657+ }
658+
659+ function handleCatalogResponse ( catalog , index , v ) {
660+ if ( v . modules ) {
661+ v . modules . forEach ( function ( m ) {
662+ if ( RED . utils . checkModuleAllowed ( m . id , m . version , null , null ) ) {
663+ catalogLoadedIndex [ m . id ] = m ;
664+ m . index = [ m . id ] ;
665+ if ( m . keywords ) {
666+ m . index = m . index . concat ( m . keywords ) ;
667+ }
668+ if ( m . types ) {
669+ m . index = m . index . concat ( m . types ) ;
670+ }
671+ if ( m . updated_at ) {
672+ m . timestamp = new Date ( m . updated_at ) . getTime ( ) ;
673+ } else {
674+ m . timestamp = 0 ;
675+ }
676+ m . index = m . index . join ( "," ) . toLowerCase ( ) ;
677+ m . catalog = catalog ;
678+ m . catalogIndex = index ;
679+ catalogLoadedList . push ( m ) ;
680+ }
681+ } ) ;
682+ }
683+ }
684+
685+ // Search available nodes from catalog
686+ function searchCatalog ( searchTerm ) {
687+ if ( ! searchTerm || searchTerm . trim ( ) === "" ) {
688+ hideAvailableNodes ( ) ;
689+ return ;
690+ }
691+
692+ var re = new RegExp ( searchTerm . replace ( / [ - \/ \\ ^ $ * + ? . ( ) | [ \] { } ] / g, '\\$&' ) , 'i' ) ;
693+ var results = [ ] ;
694+
695+ catalogLoadedList . forEach ( function ( m ) {
696+ if ( re . test ( m . index ) || re . test ( m . id ) ) {
697+ // Check if not already installed
698+ if ( ! RED . nodes . registry . getModule ( m . id ) ) {
699+ results . push ( m ) ;
700+ }
701+ }
702+ } ) ;
703+
704+ if ( results . length > 0 ) {
705+ showAvailableNodes ( results . slice ( 0 , 10 ) ) ; // Limit to first 10 results
706+ } else {
707+ hideAvailableNodes ( ) ;
708+ }
709+ }
710+
711+ function showAvailableNodes ( modules ) {
712+ if ( ! availableNodesContainer ) {
713+ createAvailableNodesContainer ( ) ;
714+ }
715+
716+ availableNodesContainer . empty ( ) ;
717+ availableNodesContainer . show ( ) ;
718+
719+ modules . forEach ( function ( m ) {
720+ var nodeDiv = $ ( '<div class="red-ui-palette-node red-ui-palette-node-available"></div>' )
721+ . appendTo ( availableNodesContainer ) ;
722+
723+ var labelDiv = $ ( '<div class="red-ui-palette-label"></div>' )
724+ . text ( m . id . replace ( 'node-red-contrib-' , '' ) . replace ( 'node-red-node-' , '' ) )
725+ . appendTo ( nodeDiv ) ;
726+
727+ var installBtn = $ ( '<button class="red-ui-palette-node-install-btn">Install</button>' )
728+ . appendTo ( nodeDiv )
729+ . on ( 'click' , function ( e ) {
730+ e . stopPropagation ( ) ;
731+ e . preventDefault ( ) ;
732+ installNodeModule ( m . id ) ;
733+ } ) ;
734+
735+ if ( m . description ) {
736+ nodeDiv . attr ( 'title' , m . description ) ;
737+ }
738+ } ) ;
739+ }
740+
741+ function hideAvailableNodes ( ) {
742+ if ( availableNodesContainer ) {
743+ availableNodesContainer . hide ( ) ;
744+ }
745+ }
746+
747+ function createAvailableNodesContainer ( ) {
748+ availableNodesContainer = $ ( '<div id="red-ui-palette-available-nodes" class="red-ui-palette-category">' +
749+ '<div class="red-ui-palette-header">' +
750+ '<i class="expanded fa fa-angle-down"></i>' +
751+ '<span>Available Nodes</span>' +
752+ '</div>' +
753+ '<div class="red-ui-palette-content" style="display: block;"></div>' +
754+ '</div>' )
755+ . appendTo ( "#red-ui-palette-container" ) ;
756+
757+ availableNodesContainer = availableNodesContainer . find ( '.red-ui-palette-content' ) ;
758+ }
759+
760+ function installNodeModule ( moduleId ) {
761+ // Show installation started notification
762+ var notification = RED . notify ( RED . _ ( "palette.editor.installingModule" , { module : moduleId } ) , {
763+ type : 'info' ,
764+ fixed : true ,
765+ spinner : true
766+ } ) ;
767+
768+ // Use palette editor's install function
769+ if ( RED . palette . editor && RED . palette . editor . install ) {
770+ RED . palette . editor . install ( moduleId ) ;
771+ // Close the notification after a delay (install process manages its own notifications)
772+ setTimeout ( function ( ) {
773+ notification . close ( ) ;
774+ // Refresh the filter to update the list
775+ refreshFilter ( ) ;
776+ } , 2000 ) ;
777+ } else {
778+ notification . close ( ) ;
779+ RED . notify ( RED . _ ( "palette.editor.errors.installFailed" , { module : moduleId } ) , {
780+ type : 'error'
781+ } ) ;
782+ }
606783 }
607784
608785 function init ( ) {
@@ -744,6 +921,11 @@ RED.palette = (function() {
744921 } catch ( error ) {
745922 console . error ( "Unexpected error loading palette state from localStorage: " , error ) ;
746923 }
924+
925+ // Initialize catalog loading
926+ catalogues = RED . settings . theme ( 'palette.catalogues' ) || [ 'https://catalogue.nodered.org/catalogue.json' ] ;
927+ loadNodeCatalogs ( ) ;
928+
747929 setTimeout ( ( ) => {
748930 // Lazily tidy up any categories that haven't been reloaded
749931 paletteState . collapsed = paletteState . collapsed . filter ( category => ! ! categoryContainers [ category ] )
0 commit comments