Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions packages/node_modules/@node-red/editor-client/src/js/ui/palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ RED.palette = (function() {

let filterRefreshTimeout

// Catalog search variables
let catalogues = [];
let loadedCatalogs = [];
let catalogLoadedList = [];
let catalogLoadedIndex = {};
let catalogSearchTimeout;
let availableNodesContainer;

function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
Expand Down Expand Up @@ -603,6 +611,175 @@ RED.palette = (function() {
}
}
}

// Trigger catalog search with debouncing
clearTimeout(catalogSearchTimeout);
if (val && val.trim() !== "") {
catalogSearchTimeout = setTimeout(function() {
searchCatalog(val);
}, 300);
} else {
hideAvailableNodes();
}
}

// Load node catalogs for search
function loadNodeCatalogs(done) {
catalogLoadedList = [];
catalogLoadedIndex = {};
loadedCatalogs.length = 0;
let handled = 0;
const catalogueCount = catalogues.length;

for (let index = 0; index < catalogues.length; index++) {
const url = catalogues[index];
$.getJSON(url, {_: new Date().getTime()}, function(v) {
loadedCatalogs.push({
index: index,
url: url,
name: v.name,
updated_at: v.updated_at,
modules_count: (v.modules || []).length
});
handleCatalogResponse({ url: url, name: v.name }, index, v);
}).fail(function(_jqxhr, _textStatus, error) {
console.warn("Error loading catalog", url, ":", error);
}).always(function() {
handled++;
if (handled === catalogueCount) {
loadedCatalogs.sort((a, b) => a.index - b.index);
if (done) {
done();
}
}
});
}
}

function handleCatalogResponse(catalog, index, v) {
if (v.modules) {
v.modules.forEach(function(m) {
if (RED.utils.checkModuleAllowed(m.id, m.version, null, null)) {
catalogLoadedIndex[m.id] = m;
m.index = [m.id];
if (m.keywords) {
m.index = m.index.concat(m.keywords);
}
if (m.types) {
m.index = m.index.concat(m.types);
}
if (m.updated_at) {
m.timestamp = new Date(m.updated_at).getTime();
} else {
m.timestamp = 0;
}
m.index = m.index.join(",").toLowerCase();
m.catalog = catalog;
m.catalogIndex = index;
catalogLoadedList.push(m);
}
});
}
}

// Search available nodes from catalog
function searchCatalog(searchTerm) {
if (!searchTerm || searchTerm.trim() === "") {
hideAvailableNodes();
return;
}

var re = new RegExp(searchTerm.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'i');
var results = [];

catalogLoadedList.forEach(function(m) {
if (re.test(m.index) || re.test(m.id)) {
// Check if not already installed
if (!RED.nodes.registry.getModule(m.id)) {
results.push(m);
}
}
});

if (results.length > 0) {
showAvailableNodes(results.slice(0, 10)); // Limit to first 10 results
} else {
hideAvailableNodes();
}
}

function showAvailableNodes(modules) {
if (!availableNodesContainer) {
createAvailableNodesContainer();
}

availableNodesContainer.empty();
availableNodesContainer.show();

modules.forEach(function(m) {
var nodeDiv = $('<div class="red-ui-palette-node red-ui-palette-node-available"></div>')
.appendTo(availableNodesContainer);

var labelDiv = $('<div class="red-ui-palette-label"></div>')
.text(m.id.replace('node-red-contrib-', '').replace('node-red-node-', ''))
.appendTo(nodeDiv);

var installBtn = $('<button class="red-ui-palette-node-install-btn">Install</button>')
.appendTo(nodeDiv)
.on('click', function(e) {
e.stopPropagation();
e.preventDefault();
installNodeModule(m.id);
});

if (m.description) {
nodeDiv.attr('title', m.description);
}
});
}

function hideAvailableNodes() {
if (availableNodesContainer) {
availableNodesContainer.hide();
}
}

function createAvailableNodesContainer() {
availableNodesContainer = $('<div id="red-ui-palette-available-nodes" class="red-ui-palette-category">' +
'<div class="red-ui-palette-header">' +
'<i class="expanded fa fa-angle-down"></i>' +
'<span>Available Nodes</span>' +
'</div>' +
'<div class="red-ui-palette-content" style="display: block;"></div>' +
'</div>')
.appendTo("#red-ui-palette-container");

availableNodesContainer = availableNodesContainer.find('.red-ui-palette-content');
}

function installNodeModule(moduleId) {
// Show installation started notification
var notification = RED.notify(RED._("palette.editor.installingModule", {module: moduleId}), {
type: 'info',
fixed: true,
spinner: true
});

// Use palette editor's install function
if (RED.palette.editor && RED.palette.editor.install) {
RED.palette.editor.install(moduleId);
// Close the notification after a delay (install process manages its own notifications)
setTimeout(function() {
notification.close();
// Refresh the filter to update the list
refreshFilter();
}, 2000);
} else {
notification.close();
RED.notify(RED._("palette.editor.errors.installFailed", {module: moduleId}), {
type: 'error'
});
}
}

function init() {
Expand Down Expand Up @@ -744,6 +921,11 @@ RED.palette = (function() {
} catch (error) {
console.error("Unexpected error loading palette state from localStorage: ", error);
}

// Initialize catalog loading
catalogues = RED.settings.theme('palette.catalogues') || ['https://catalogue.nodered.org/catalogue.json'];
loadNodeCatalogs();

setTimeout(() => {
// Lazily tidy up any categories that haven't been reloaded
paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,70 @@
margin-left: 4px;
color: var(--red-ui-secondary-text-color);
}

// Available nodes from catalog
#red-ui-palette-available-nodes {
border-top: 2px solid var(--red-ui-primary-border-color);
margin-top: 10px;

.red-ui-palette-header {
background: var(--red-ui-palette-header-background);
color: var(--red-ui-palette-header-color);
font-style: italic;
}

.red-ui-palette-content {
background: var(--red-ui-palette-content-background);
}
}

.red-ui-palette-node-available {
opacity: 0.7;
border-style: dashed;
position: relative;
padding-right: 35px;
min-height: 30px;
display: flex;
align-items: center;

&:hover {
opacity: 1;
border-style: solid;
}

.red-ui-palette-label {
margin-right: 45px;
margin-left: 5px;
font-size: 11px;
word-break: break-word;
overflow-wrap: break-word;
white-space: normal;
line-height: 1.3;
flex: 1;
text-align: left;
}
}

.red-ui-palette-node-install-btn {
position: absolute;
right: 3px;
top: 50%;
transform: translateY(-50%);
padding: 2px 6px;
font-size: 10px;
background: var(--red-ui-button-background);
color: var(--red-ui-button-color);
border: 1px solid var(--red-ui-button-border-color);
border-radius: 3px;
cursor: pointer;
white-space: nowrap;

&:hover {
background: var(--red-ui-button-background-hover);
color: var(--red-ui-button-color-hover);
}

&:active {
background: var(--red-ui-button-background-active);
}
}
Loading