Skip to content

Commit 87e922a

Browse files
feat: Make custom nodes searchable from Node Palette
- Integrated catalog search functionality into main palette filter - Shows available (uninstalled) nodes in dedicated section when searching - Added visual distinction with dashed borders and reduced opacity - Implemented install buttons directly in palette search results - Added debounced search to optimize catalog API calls - Limited results to 10 items for better performance Closes #49 Co-authored-by: Dimitrie Hoekstra <[email protected]>
1 parent 5fc1875 commit 87e922a

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

packages/node_modules/@node-red/editor-client/src/js/ui/palette.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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])

packages/node_modules/@node-red/editor-client/src/sass/palette.scss

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,59 @@
344344
margin-left: 4px;
345345
color: var(--red-ui-secondary-text-color);
346346
}
347+
348+
// Available nodes from catalog
349+
#red-ui-palette-available-nodes {
350+
border-top: 2px solid var(--red-ui-primary-border-color);
351+
margin-top: 10px;
352+
353+
.red-ui-palette-header {
354+
background: var(--red-ui-palette-header-background);
355+
color: var(--red-ui-palette-header-color);
356+
font-style: italic;
357+
}
358+
359+
.red-ui-palette-content {
360+
background: var(--red-ui-palette-content-background);
361+
}
362+
}
363+
364+
.red-ui-palette-node-available {
365+
opacity: 0.7;
366+
border-style: dashed;
367+
position: relative;
368+
padding-right: 35px;
369+
370+
&:hover {
371+
opacity: 1;
372+
border-style: solid;
373+
}
374+
375+
.red-ui-palette-label {
376+
margin-right: 35px;
377+
margin-left: 5px;
378+
font-size: 11px;
379+
}
380+
}
381+
382+
.red-ui-palette-node-install-btn {
383+
position: absolute;
384+
right: 3px;
385+
top: 3px;
386+
padding: 2px 6px;
387+
font-size: 10px;
388+
background: var(--red-ui-button-background);
389+
color: var(--red-ui-button-color);
390+
border: 1px solid var(--red-ui-button-border-color);
391+
border-radius: 3px;
392+
cursor: pointer;
393+
394+
&:hover {
395+
background: var(--red-ui-button-background-hover);
396+
color: var(--red-ui-button-color-hover);
397+
}
398+
399+
&:active {
400+
background: var(--red-ui-button-background-active);
401+
}
402+
}

0 commit comments

Comments
 (0)