diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js index 89337e7c9e..655178aa5b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette.js @@ -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); @@ -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 = $('
') + .appendTo(availableNodesContainer); + + var labelDiv = $('
') + .text(m.id.replace('node-red-contrib-', '').replace('node-red-node-', '')) + .appendTo(nodeDiv); + + var installBtn = $('') + .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 = $('
' + + '
' + + '' + + 'Available Nodes' + + '
' + + '
' + + '
') + .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() { @@ -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]) diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss index 507869690b..5fca907a41 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/palette.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/palette.scss @@ -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); + } +}