From 0470740737c5ee199447a68b7277c43be2042976 Mon Sep 17 00:00:00 2001 From: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com> Date: Fri, 18 Jun 2021 07:58:26 +0200 Subject: [PATCH] :sparkles: Change the UI of the Nodes Panel (#1855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add codex search properties to node types * implement basic styles * update header designs * update node list designs * add trigger icon * refactor node creator list * implement categories and subcategories * fix up spacing * add arrows * implement navigatable list * implement more of feature * implement navigation * add transitions * fix lint issues * fix overlay * :zap: Get and add codex categories * fix up design * update borders * implement no-matches view * fix preview bug * add color to search * clean up borders * add comma * Revert "Merge branch 'add-codex-data' of github.com:n8n-io/n8n into PROD-819-nodes-panel-redesign" 38b7d7ead19ab069f3f00a1ae6b6267eee55122a * use new impl * remove empty categories * update scrolling, hide start node * make scrollable * remove text while subcategory panel is open * fix up spacing * fix lint issues * update descriptions * update path * update images * fix tags manager * give min height to image * gst * update clear color * update font size * fix firefox spacing * close on click outside * add external link icon * update iterator key * add client side caching for images * update caching header * ⚡️ Add properties to codex for nodes panel (#1854) * :zap: Get and add codex categories * :zap: Add parens to evaluation + destructuring * :fire: Remove non-existing class reference * :zap: Add alias to codex property * move constants * :hammer: Rename CodexCategories to CodexData * :pencil2: Update getCodex documentation * refactor and move * refactor no results view * more refactoring * refactor subcategory panel * more refactoring * update text * update no results view * add miscellaneous to end of list * address design feedback * reimplement node search * fix up clear * update placeholder color * impl transition * focus on tab * update spacing * fix transition bug on start * fix up x * fix position * build * safari fix * remove input changes * css bleed issue with image * update css value * clean up * simplify impl * rename again * rename again * rename all * fix hover bug * remove keep alive * delete icon * update interface type * refactor components * update scss to module * clean up impl * clean up colors as vars * fix indentation * clean up scss * clean up scss * clean up scss * clean up scss * Clean up files * update logic to be more efficient * fix search bug * update type * remove unused * clean up js * update scrollable, border impl, transition * fix simicolon * build * update search * address max's comments * change icon border radius * change margin * update icon size * update icon size * update slide transition out * add comma * remove full * update trigger icon size * fix image size * address design feedback * update external link icons * address codacy issues * support custom nodes without codex file * address jan's feedback * address Ben's comments * add subcategory index * open/close categories with arrow keys * add lint comment * Address latest comments * :zap: Minor changes Co-authored-by: Iván Ovejero Co-authored-by: Mutasem Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Jan Oberhauser --- packages/cli/src/LoadNodesAndCredentials.ts | 59 +++- packages/cli/src/Server.ts | 3 + packages/editor-ui/src/Interface.ts | 33 ++ packages/editor-ui/src/components/Node.vue | 2 +- .../src/components/NodeCreateItem.vue | 85 ----- .../src/components/NodeCreateList.vue | 172 --------- .../editor-ui/src/components/NodeCreator.vue | 104 ------ .../components/NodeCreator/CategoryItem.vue | 42 +++ .../components/NodeCreator/CreatorItem.vue | 57 +++ .../components/NodeCreator/ItemIterator.vue | 94 +++++ .../src/components/NodeCreator/MainPanel.vue | 332 ++++++++++++++++++ .../src/components/NodeCreator/NoResults.vue | 126 +++++++ .../components/NodeCreator/NoResultsIcon.vue | 22 ++ .../components/NodeCreator/NodeCreator.vue | 101 ++++++ .../src/components/NodeCreator/NodeItem.vue | 86 +++++ .../src/components/NodeCreator/SearchBar.vue | 124 +++++++ .../NodeCreator/SubcategoryItem.vue | 58 +++ .../NodeCreator/SubcategoryPanel.vue | 96 +++++ .../src/components/NodeCreator/helpers.ts | 176 ++++++++++ .../editor-ui/src/components/NodeIcon.vue | 19 +- .../editor-ui/src/components/TriggerIcon.vue | 42 +++ .../transitions/SlideTransition.vue | 24 ++ packages/editor-ui/src/constants.ts | 23 ++ packages/editor-ui/src/main.ts | 8 + .../editor-ui/src/n8n-theme-variables.scss | 20 ++ packages/editor-ui/src/views/NodeView.vue | 2 +- packages/node-dev/src/Build.ts | 7 +- packages/workflow/src/Interfaces.ts | 7 + 28 files changed, 1549 insertions(+), 375 deletions(-) delete mode 100644 packages/editor-ui/src/components/NodeCreateItem.vue delete mode 100644 packages/editor-ui/src/components/NodeCreateList.vue delete mode 100644 packages/editor-ui/src/components/NodeCreator.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/CategoryItem.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/CreatorItem.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/ItemIterator.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/MainPanel.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/NoResults.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/NoResultsIcon.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/NodeCreator.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/NodeItem.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/SearchBar.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/SubcategoryItem.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/SubcategoryPanel.vue create mode 100644 packages/editor-ui/src/components/NodeCreator/helpers.ts create mode 100644 packages/editor-ui/src/components/TriggerIcon.vue create mode 100644 packages/editor-ui/src/components/transitions/SlideTransition.vue diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index 2b1263e6d..777fa0bb0 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -3,6 +3,7 @@ import { UserSettings, } from 'n8n-core'; import { + CodexData, ICredentialType, ILogger, INodeType, @@ -25,6 +26,8 @@ import { import * as glob from 'glob-promise'; import * as path from 'path'; +const CUSTOM_NODES_CATEGORY = 'Custom Nodes'; + class LoadNodesAndCredentialsClass { nodeTypes: INodeTypeData = {}; @@ -133,7 +136,6 @@ class LoadNodesAndCredentialsClass { * @param {string} credentialName The name of the credentials * @param {string} filePath The file to read credentials from * @returns {Promise} - * @memberof N8nPackagesInformationClass */ async loadCredentialsFromFile(credentialName: string, filePath: string): Promise { const tempModule = require(filePath); @@ -160,7 +162,6 @@ class LoadNodesAndCredentialsClass { * @param {string} nodeName Tha name of the node * @param {string} filePath The file to read node from * @returns {Promise} - * @memberof N8nPackagesInformationClass */ async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise { let tempNode: INodeType; @@ -169,6 +170,7 @@ class LoadNodesAndCredentialsClass { const tempModule = require(filePath); try { tempNode = new tempModule[nodeName]() as INodeType; + this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' }); } catch (error) { console.error(`Error loading node "${nodeName}" from: "${filePath}"`); throw error; @@ -202,6 +204,57 @@ class LoadNodesAndCredentialsClass { }; } + /** + * Retrieves `categories`, `subcategories` and alias (if defined) + * from the codex data for the node at the given file path. + * + * @param {string} filePath The file path to a `*.node.js` file + * @returns {CodexData} + */ + getCodex(filePath: string): CodexData { + const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json + return { + ...(categories && { categories }), + ...(subcategories && { subcategories }), + ...(alias && { alias }), + }; + } + + /** + * Adds a node codex `categories` and `subcategories` (if defined) + * to a node description `codex` property. + * + * @param {object} obj + * @param obj.node Node to add categories to + * @param obj.filePath Path to the built node + * @param obj.isCustom Whether the node is custom + * @returns {void} + */ + addCodex({ node, filePath, isCustom }: { + node: INodeType; + filePath: string; + isCustom: boolean; + }) { + try { + const codex = this.getCodex(filePath); + + if (isCustom) { + codex.categories = codex.categories + ? codex.categories.concat(CUSTOM_NODES_CATEGORY) + : [CUSTOM_NODES_CATEGORY]; + } + + node.description.codex = codex; + } catch (_) { + this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`); + + if (isCustom) { + node.description.codex = { + categories: [CUSTOM_NODES_CATEGORY], + }; + } + } + } /** * Loads nodes and credentials from the given directory @@ -209,7 +262,6 @@ class LoadNodesAndCredentialsClass { * @param {string} setPackageName The package name to set for the found nodes * @param {string} directory The directory to look in * @returns {Promise} - * @memberof N8nPackagesInformationClass */ async loadDataFromDirectory(setPackageName: string, directory: string): Promise { const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js')); @@ -237,7 +289,6 @@ class LoadNodesAndCredentialsClass { * * @param {string} packageName The name to read data from * @returns {Promise} - * @memberof N8nPackagesInformationClass */ async loadDataFromPackage(packageName: string): Promise { // Get the absolute path of the package diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index f19c88289..0a3114499 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -947,6 +947,9 @@ class App { const filepath = nodeType.description.icon.substr(5); + const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days + res.setHeader('Cache-control', `private max-age=${maxAge}`); + res.sendFile(filepath); }); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 3dab3901e..e01b747fb 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -489,6 +489,39 @@ export interface ILinkMenuItemProperties { newWindow?: boolean; } +export interface ISubcategoryItemProps { + subcategory: string; + description: string; +} + +export interface INodeItemProps { + subcategory: string; + nodeType: INodeTypeDescription; +} + +export interface ICategoryItemProps { + expanded: boolean; +} + +export interface INodeCreateElement { + type: 'node' | 'category' | 'subcategory'; + category: string; + key: string; + includedByTrigger?: boolean; + includedByRegular?: boolean; + properties: ISubcategoryItemProps | INodeItemProps | ICategoryItemProps; +} + +export interface ICategoriesWithNodes { + [category: string]: { + [subcategory: string]: { + regularCount: number; + triggerCount: number; + nodes: INodeCreateElement[]; + }; + }; +} + export interface ITag { id: string; name: string; diff --git a/packages/editor-ui/src/components/Node.vue b/packages/editor-ui/src/components/Node.vue index 335c19e6f..4ce5b36ce 100644 --- a/packages/editor-ui/src/components/Node.vue +++ b/packages/editor-ui/src/components/Node.vue @@ -30,7 +30,7 @@ - +
diff --git a/packages/editor-ui/src/components/NodeCreateItem.vue b/packages/editor-ui/src/components/NodeCreateItem.vue deleted file mode 100644 index a1ebeca06..000000000 --- a/packages/editor-ui/src/components/NodeCreateItem.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/NodeCreateList.vue b/packages/editor-ui/src/components/NodeCreateList.vue deleted file mode 100644 index a00b3ff2d..000000000 --- a/packages/editor-ui/src/components/NodeCreateList.vue +++ /dev/null @@ -1,172 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/NodeCreator.vue b/packages/editor-ui/src/components/NodeCreator.vue deleted file mode 100644 index 7090d5749..000000000 --- a/packages/editor-ui/src/components/NodeCreator.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/NodeCreator/CategoryItem.vue b/packages/editor-ui/src/components/NodeCreator/CategoryItem.vue new file mode 100644 index 000000000..3f4dff39b --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/CategoryItem.vue @@ -0,0 +1,42 @@ + + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/CreatorItem.vue b/packages/editor-ui/src/components/NodeCreator/CreatorItem.vue new file mode 100644 index 000000000..a024c2ec4 --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/CreatorItem.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/ItemIterator.vue b/packages/editor-ui/src/components/NodeCreator/ItemIterator.vue new file mode 100644 index 000000000..7cf00057e --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/ItemIterator.vue @@ -0,0 +1,94 @@ + + + + + + diff --git a/packages/editor-ui/src/components/NodeCreator/MainPanel.vue b/packages/editor-ui/src/components/NodeCreator/MainPanel.vue new file mode 100644 index 000000000..bd7f665db --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/MainPanel.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/packages/editor-ui/src/components/NodeCreator/NoResults.vue b/packages/editor-ui/src/components/NodeCreator/NoResults.vue new file mode 100644 index 000000000..c03823bfb --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/NoResults.vue @@ -0,0 +1,126 @@ + + + + + + diff --git a/packages/editor-ui/src/components/NodeCreator/NoResultsIcon.vue b/packages/editor-ui/src/components/NodeCreator/NoResultsIcon.vue new file mode 100644 index 000000000..697b1a42c --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/NoResultsIcon.vue @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/NodeCreator.vue b/packages/editor-ui/src/components/NodeCreator/NodeCreator.vue new file mode 100644 index 000000000..4c9eccf5e --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/NodeCreator.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/packages/editor-ui/src/components/NodeCreator/NodeItem.vue b/packages/editor-ui/src/components/NodeCreator/NodeItem.vue new file mode 100644 index 000000000..06f4d4f4b --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/NodeItem.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/packages/editor-ui/src/components/NodeCreator/SearchBar.vue b/packages/editor-ui/src/components/NodeCreator/SearchBar.vue new file mode 100644 index 000000000..482650f31 --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/SearchBar.vue @@ -0,0 +1,124 @@ + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/SubcategoryItem.vue b/packages/editor-ui/src/components/NodeCreator/SubcategoryItem.vue new file mode 100644 index 000000000..2ca06e3a2 --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/SubcategoryItem.vue @@ -0,0 +1,58 @@ + + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/SubcategoryPanel.vue b/packages/editor-ui/src/components/NodeCreator/SubcategoryPanel.vue new file mode 100644 index 000000000..01af81b4a --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/SubcategoryPanel.vue @@ -0,0 +1,96 @@ + + + + + \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeCreator/helpers.ts b/packages/editor-ui/src/components/NodeCreator/helpers.ts new file mode 100644 index 000000000..ae194443b --- /dev/null +++ b/packages/editor-ui/src/components/NodeCreator/helpers.ts @@ -0,0 +1,176 @@ +import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER } from '@/constants'; +import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface'; +import { INodeTypeDescription } from 'n8n-workflow'; + + +export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICategoriesWithNodes => { + return nodeTypes.reduce( + (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => { + if (!nodeType.codex || !nodeType.codex.categories) { + accu[UNCATEGORIZED_CATEGORY][UNCATEGORIZED_SUBCATEGORY].nodes.push({ + type: 'node', + category: UNCATEGORIZED_CATEGORY, + key: `${UNCATEGORIZED_CATEGORY}_${nodeType.name}`, + properties: { + subcategory: UNCATEGORIZED_SUBCATEGORY, + nodeType, + }, + includedByTrigger: nodeType.group.includes('trigger'), + includedByRegular: !nodeType.group.includes('trigger'), + }); + return accu; + } + nodeType.codex.categories.forEach((_category: string) => { + const category = _category.trim(); + const subcategory = + nodeType.codex && + nodeType.codex.subcategories && + nodeType.codex.subcategories[category] + ? nodeType.codex.subcategories[category][0] + : UNCATEGORIZED_SUBCATEGORY; + if (!accu[category]) { + accu[category] = {}; + } + if (!accu[category][subcategory]) { + accu[category][subcategory] = { + triggerCount: 0, + regularCount: 0, + nodes: [], + }; + } + const isTrigger = nodeType.group.includes('trigger'); + if (isTrigger) { + accu[category][subcategory].triggerCount++; + } + if (!isTrigger) { + accu[category][subcategory].regularCount++; + } + accu[category][subcategory].nodes.push({ + type: 'node', + key: `${category}_${nodeType.name}`, + category, + properties: { + nodeType, + subcategory, + }, + includedByTrigger: isTrigger, + includedByRegular: !isTrigger, + }); + }); + return accu; + }, + { + [UNCATEGORIZED_CATEGORY]: { + [UNCATEGORIZED_SUBCATEGORY]: { + triggerCount: 0, + regularCount: 0, + nodes: [], + }, + }, + }, + ); +}; + +const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => { + const categories = Object.keys(categoriesWithNodes); + const sorted = categories.filter( + (category: string) => + category !== CORE_NODES_CATEGORY && category !== CUSTOM_NODES_CATEGORY && category !== UNCATEGORIZED_CATEGORY, + ); + sorted.sort(); + + return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY]; +}; + +export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => { + const categories = getCategories(categoriesWithNodes); + + return categories.reduce( + (accu: INodeCreateElement[], category: string) => { + if (!categoriesWithNodes[category]) { + return accu; + } + + const categoryEl: INodeCreateElement = { + type: 'category', + key: category, + category, + properties: { + expanded: false, + }, + }; + + const subcategories = Object.keys(categoriesWithNodes[category]); + if (subcategories.length === 1) { + const subcategory = categoriesWithNodes[category][ + subcategories[0] + ]; + if (subcategory.triggerCount > 0) { + categoryEl.includedByTrigger = subcategory.triggerCount > 0; + } + if (subcategory.regularCount > 0) { + categoryEl.includedByRegular = subcategory.regularCount > 0; + } + return [...accu, categoryEl, ...subcategory.nodes]; + } + + subcategories.sort(); + const subcategorized = subcategories.reduce( + (accu: INodeCreateElement[], subcategory: string) => { + const subcategoryEl: INodeCreateElement = { + type: 'subcategory', + key: `${category}_${subcategory}`, + category, + properties: { + subcategory, + description: SUBCATEGORY_DESCRIPTIONS[category][subcategory], + }, + includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0, + includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0, + }; + + if (subcategoryEl.includedByTrigger) { + categoryEl.includedByTrigger = true; + } + if (subcategoryEl.includedByRegular) { + categoryEl.includedByRegular = true; + } + + accu.push(subcategoryEl); + return accu; + }, + [], + ); + + return [...accu, categoryEl, ...subcategorized]; + }, + [], + ); +}; + +export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => { + if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) { + return true; + } + if (selectedType === TRIGGER_NODE_FILTER && el.includedByTrigger) { + return true; + } + + return selectedType === ALL_NODE_FILTER; +}; + +const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => { + if (!nodeType.codex || !nodeType.codex.alias) { + return false; + } + + return nodeType.codex.alias.reduce((accu: boolean, alias: string) => { + return accu || alias.toLowerCase().indexOf(filter) > -1; + }, false); +}; + +export const matchesNodeType = (el: INodeCreateElement, filter: string) => { + const nodeType = (el.properties as INodeItemProps).nodeType; + + return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter); +}; \ No newline at end of file diff --git a/packages/editor-ui/src/components/NodeIcon.vue b/packages/editor-ui/src/components/NodeIcon.vue index d32a1a40e..bf13e2ef7 100644 --- a/packages/editor-ui/src/components/NodeIcon.vue +++ b/packages/editor-ui/src/components/NodeIcon.vue @@ -1,7 +1,7 @@