mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
✨ Change the UI of the Nodes Panel (#1855)
* 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
* ⚡ 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"
38b7d7ead1
* 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)
* ⚡ Get and add codex categories
* ⚡ Add parens to evaluation + destructuring
* 🔥 Remove non-existing class reference
* ⚡ Add alias to codex property
* move constants
* 🔨 Rename CodexCategories to CodexData
* ✏️ 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
* ⚡ Minor changes
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
6e68d71f4d
commit
0470740737
|
@ -3,6 +3,7 @@ import {
|
||||||
UserSettings,
|
UserSettings,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
|
CodexData,
|
||||||
ICredentialType,
|
ICredentialType,
|
||||||
ILogger,
|
ILogger,
|
||||||
INodeType,
|
INodeType,
|
||||||
|
@ -25,6 +26,8 @@ import {
|
||||||
import * as glob from 'glob-promise';
|
import * as glob from 'glob-promise';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
|
||||||
|
|
||||||
class LoadNodesAndCredentialsClass {
|
class LoadNodesAndCredentialsClass {
|
||||||
nodeTypes: INodeTypeData = {};
|
nodeTypes: INodeTypeData = {};
|
||||||
|
@ -133,7 +136,6 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @param {string} credentialName The name of the credentials
|
* @param {string} credentialName The name of the credentials
|
||||||
* @param {string} filePath The file to read credentials from
|
* @param {string} filePath The file to read credentials from
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof N8nPackagesInformationClass
|
|
||||||
*/
|
*/
|
||||||
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
|
||||||
const tempModule = require(filePath);
|
const tempModule = require(filePath);
|
||||||
|
@ -160,7 +162,6 @@ class LoadNodesAndCredentialsClass {
|
||||||
* @param {string} nodeName Tha name of the node
|
* @param {string} nodeName Tha name of the node
|
||||||
* @param {string} filePath The file to read node from
|
* @param {string} filePath The file to read node from
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof N8nPackagesInformationClass
|
|
||||||
*/
|
*/
|
||||||
async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise<void> {
|
async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise<void> {
|
||||||
let tempNode: INodeType;
|
let tempNode: INodeType;
|
||||||
|
@ -169,6 +170,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
const tempModule = require(filePath);
|
const tempModule = require(filePath);
|
||||||
try {
|
try {
|
||||||
tempNode = new tempModule[nodeName]() as INodeType;
|
tempNode = new tempModule[nodeName]() as INodeType;
|
||||||
|
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
|
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
|
||||||
throw error;
|
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
|
* 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} setPackageName The package name to set for the found nodes
|
||||||
* @param {string} directory The directory to look in
|
* @param {string} directory The directory to look in
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof N8nPackagesInformationClass
|
|
||||||
*/
|
*/
|
||||||
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
|
||||||
const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js'));
|
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
|
* @param {string} packageName The name to read data from
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @memberof N8nPackagesInformationClass
|
|
||||||
*/
|
*/
|
||||||
async loadDataFromPackage(packageName: string): Promise<void> {
|
async loadDataFromPackage(packageName: string): Promise<void> {
|
||||||
// Get the absolute path of the package
|
// Get the absolute path of the package
|
||||||
|
|
|
@ -947,6 +947,9 @@ class App {
|
||||||
|
|
||||||
const filepath = nodeType.description.icon.substr(5);
|
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);
|
res.sendFile(filepath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -489,6 +489,39 @@ export interface ILinkMenuItemProperties {
|
||||||
newWindow?: boolean;
|
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 {
|
export interface ITag {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :style="nodeIconStyle"/>
|
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :style="nodeIconStyle" :shrink="true"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="node-description">
|
<div class="node-description">
|
||||||
<div class="node-name" :title="data.name">
|
<div class="node-name" :title="data.name">
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="node-item clickable" :class="{active: active}" :data-node-name="nodeName" @click="nodeTypeSelected(nodeType)">
|
|
||||||
<NodeIcon class="node-icon" :nodeType="nodeType" :style="nodeIconStyle" />
|
|
||||||
<div class="name">
|
|
||||||
{{nodeType.displayName}}
|
|
||||||
</div>
|
|
||||||
<div class="description">
|
|
||||||
{{nodeType.description}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'NodeCreateItem',
|
|
||||||
components: {
|
|
||||||
NodeIcon,
|
|
||||||
},
|
|
||||||
props: [
|
|
||||||
'active',
|
|
||||||
'filter',
|
|
||||||
'nodeType',
|
|
||||||
],
|
|
||||||
computed: {
|
|
||||||
nodeIconStyle (): object {
|
|
||||||
return {
|
|
||||||
color: this.nodeType.defaults.color,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
nodeName (): string {
|
|
||||||
return this.nodeType.name;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
nodeTypeSelected (nodeType: INodeTypeDescription) {
|
|
||||||
this.$emit('nodeTypeSelected', nodeType.name);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
.node-item {
|
|
||||||
position: relative;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 6px;
|
|
||||||
border-left: 3px solid #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-left: 3px solid #ccc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
border-left: 3px solid $--color-primary;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-icon {
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
top: calc(50% - 15px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin-top: 3px;
|
|
||||||
line-height: 1.7em;
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,172 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<el-input placeholder="Type to filter..." v-model="nodeFilter" ref="inputField" size="small" type="text" prefix-icon="el-icon-search" @keydown.native="nodeFilterKeyDown" clearable ></el-input>
|
|
||||||
</div>
|
|
||||||
<div class="type-selector">
|
|
||||||
<el-tabs v-model="selectedType" stretch>
|
|
||||||
<el-tab-pane label="Regular" name="Regular"></el-tab-pane>
|
|
||||||
<el-tab-pane label="Trigger" name="Trigger"></el-tab-pane>
|
|
||||||
<el-tab-pane label="All" name="All"></el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
<div class="node-create-list-wrapper">
|
|
||||||
<div class="node-create-list">
|
|
||||||
<div v-if="filteredNodeTypes.length === 0" class="no-results">
|
|
||||||
🙃 no nodes matching your search criteria
|
|
||||||
</div>
|
|
||||||
<node-create-item :active="index === activeNodeTypeIndex" :nodeType="nodeType" v-for="(nodeType, index) in filteredNodeTypes" v-bind:key="nodeType.name" @nodeTypeSelected="nodeTypeSelected"></node-create-item>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
import { externalHooks } from "@/components/mixins/externalHooks";
|
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
import NodeCreateItem from '@/components/NodeCreateItem.vue';
|
|
||||||
|
|
||||||
import mixins from "vue-typed-mixins";
|
|
||||||
|
|
||||||
export default mixins(externalHooks).extend({
|
|
||||||
name: 'NodeCreateList',
|
|
||||||
components: {
|
|
||||||
NodeCreateItem,
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
activeNodeTypeIndex: 0,
|
|
||||||
nodeFilter: '',
|
|
||||||
selectedType: 'Regular',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
nodeTypes (): INodeTypeDescription[] {
|
|
||||||
return this.$store.getters.allNodeTypes;
|
|
||||||
},
|
|
||||||
filteredNodeTypes () {
|
|
||||||
const filter = this.nodeFilter.toLowerCase();
|
|
||||||
const nodeTypes: INodeTypeDescription[] = this.$store.getters.allNodeTypes;
|
|
||||||
|
|
||||||
// Apply the filters
|
|
||||||
const returnData = nodeTypes.filter((nodeType) => {
|
|
||||||
if (filter && nodeType.displayName.toLowerCase().indexOf(filter) === -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.selectedType !== 'All') {
|
|
||||||
if (this.selectedType === 'Trigger' && !nodeType.group.includes('trigger')) {
|
|
||||||
return false;
|
|
||||||
} else if (this.selectedType === 'Regular' && nodeType.group.includes('trigger')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort the node types
|
|
||||||
let textA, textB;
|
|
||||||
returnData.sort((a, b) => {
|
|
||||||
textA = a.displayName.toLowerCase();
|
|
||||||
textB = b.displayName.toLowerCase();
|
|
||||||
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', { nodeFilter: this.nodeFilter, result: returnData, selectedType: this.selectedType });
|
|
||||||
return returnData;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
nodeFilter (newValue, oldValue) {
|
|
||||||
// Reset the index whenver the filter-value changes
|
|
||||||
this.activeNodeTypeIndex = 0;
|
|
||||||
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', { oldValue, newValue, selectedType: this.selectedType, filteredNodes: this.filteredNodeTypes });
|
|
||||||
},
|
|
||||||
selectedType (newValue, oldValue) {
|
|
||||||
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', { oldValue, newValue });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
nodeFilterKeyDown (e: KeyboardEvent) {
|
|
||||||
const activeNodeType = this.filteredNodeTypes[this.activeNodeTypeIndex];
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
this.activeNodeTypeIndex++;
|
|
||||||
// Make sure that we stop at the last nodeType
|
|
||||||
this.activeNodeTypeIndex = Math.min(this.activeNodeTypeIndex, this.filteredNodeTypes.length - 1);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
this.activeNodeTypeIndex--;
|
|
||||||
// Make sure that we do not get before the first nodeType
|
|
||||||
this.activeNodeTypeIndex = Math.max(this.activeNodeTypeIndex, 0);
|
|
||||||
} else if (e.key === 'Enter' && activeNodeType) {
|
|
||||||
this.nodeTypeSelected(activeNodeType.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!['Escape', 'Tab'].includes(e.key)) {
|
|
||||||
// We only want to propagate "Escape" as it closes the node-creator and
|
|
||||||
// "Tab" which toggles it
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
nodeTypeSelected (nodeTypeName: string) {
|
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted() {
|
|
||||||
this.$externalHooks().run('nodeCreateList.mounted');
|
|
||||||
},
|
|
||||||
async destroyed() {
|
|
||||||
this.$externalHooks().run('nodeCreateList.destroyed');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
.node-create-list-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 160px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-create-list {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-name {
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 15px 0 5px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper >>> .el-input__inner,
|
|
||||||
.input-wrapper >>> .el-input__inner:hover {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
.input-wrapper {
|
|
||||||
margin: 10px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-selector {
|
|
||||||
height: 50px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-selector >>> .el-tabs__nav {
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-results {
|
|
||||||
margin: 20px 10px 0 10px;
|
|
||||||
line-height: 1.5em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,104 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="node-creator-wrapper">
|
|
||||||
<transition name="el-zoom-in-top">
|
|
||||||
<div class="node-creator" v-show="active">
|
|
||||||
<div class="close-button clickable close-on-click" @click="closeCreator" title="Close">
|
|
||||||
<i class="el-icon-close close-on-click"></i>
|
|
||||||
</div>
|
|
||||||
<div class="header">
|
|
||||||
Create Node
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<node-create-list v-if="active" ref="list" @nodeTypeSelected="nodeTypeSelected"></node-create-list>
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
import NodeCreateList from '@/components/NodeCreateList.vue';
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
name: 'NodeCreator',
|
|
||||||
components: {
|
|
||||||
NodeCreateList,
|
|
||||||
},
|
|
||||||
props: [
|
|
||||||
'active',
|
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
active (newValue, oldValue) {
|
|
||||||
if (newValue === true) {
|
|
||||||
// Try to set focus directly on the filter-input-field
|
|
||||||
setTimeout(() => {
|
|
||||||
// @ts-ignore
|
|
||||||
if (this.$refs.list && this.$refs.list.$refs.inputField) {
|
|
||||||
// @ts-ignore
|
|
||||||
this.$refs.list.$refs.inputField.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
closeCreator () {
|
|
||||||
this.$emit('closeNodeCreator');
|
|
||||||
},
|
|
||||||
nodeTypeSelected (nodeTypeName: string) {
|
|
||||||
this.$emit('nodeTypeSelected', nodeTypeName);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -50px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: $--custom-header-background;
|
|
||||||
border-radius: 18px 0 0 18px;
|
|
||||||
z-index: 110;
|
|
||||||
font-size: 1.7em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 50px;
|
|
||||||
height: 50px;
|
|
||||||
width: 50px;
|
|
||||||
|
|
||||||
.close-on-click {
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-creator {
|
|
||||||
position: fixed;
|
|
||||||
top: 65px;
|
|
||||||
right: 0;
|
|
||||||
width: 350px;
|
|
||||||
height: calc(100% - 65px);
|
|
||||||
background-color: #fff4f1;
|
|
||||||
z-index: 200;
|
|
||||||
color: #555;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
font-size: 1.2em;
|
|
||||||
margin: 20px 15px;
|
|
||||||
height: 25px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template functional>
|
||||||
|
<div :class="$style.category">
|
||||||
|
<span :class="$style.name">{{ props.item.category }}</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
:class="$style.arrow"
|
||||||
|
icon="chevron-down"
|
||||||
|
v-if="props.item.properties.expanded"
|
||||||
|
/>
|
||||||
|
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: ['item'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.category {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 11px;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0 12px;
|
||||||
|
border-bottom: 1px solid $--node-creator-border-color;
|
||||||
|
display: flex;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
width: 12px;
|
||||||
|
color: $--node-creator-arrow-color;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template functional>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
container: true,
|
||||||
|
clickable: props.clickable,
|
||||||
|
active: props.active,
|
||||||
|
}"
|
||||||
|
@click="listeners['click']"
|
||||||
|
>
|
||||||
|
<CategoryItem
|
||||||
|
v-if="props.item.type === 'category'"
|
||||||
|
:item="props.item"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SubcategoryItem
|
||||||
|
v-else-if="props.item.type === 'subcategory'"
|
||||||
|
:item="props.item"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NodeItem
|
||||||
|
v-else-if="props.item.type === 'node'"
|
||||||
|
:nodeType="props.item.properties.nodeType"
|
||||||
|
:bordered="!props.lastNode"
|
||||||
|
></NodeItem>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import NodeItem from './NodeItem.vue';
|
||||||
|
import CategoryItem from './CategoryItem.vue';
|
||||||
|
import SubcategoryItem from './SubcategoryItem.vue';
|
||||||
|
|
||||||
|
Vue.component('CategoryItem', CategoryItem);
|
||||||
|
Vue.component('SubcategoryItem', SubcategoryItem);
|
||||||
|
Vue.component('NodeItem', NodeItem);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['item', 'active', 'clickable', 'lastNode'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $--node-creator-item-hover-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: $--color-primary !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
:is="transitionsEnabled ? 'transition-group' : 'div'"
|
||||||
|
name="accordion"
|
||||||
|
@before-enter="beforeEnter"
|
||||||
|
@enter="enter"
|
||||||
|
@before-leave="beforeLeave"
|
||||||
|
@leave="leave"
|
||||||
|
>
|
||||||
|
<div v-for="(item, index) in elements" :key="item.key" :class="item.type">
|
||||||
|
<CreatorItem
|
||||||
|
:item="item"
|
||||||
|
:active="activeIndex === index && !disabled"
|
||||||
|
:clickable="!disabled"
|
||||||
|
:lastNode="
|
||||||
|
index === elements.length - 1 || elements[index + 1].type !== 'node'
|
||||||
|
"
|
||||||
|
@click="() => selected(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { INodeCreateElement } from '@/Interface';
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
import CreatorItem from './CreatorItem.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ItemIterator',
|
||||||
|
components: {
|
||||||
|
CreatorItem,
|
||||||
|
},
|
||||||
|
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
|
||||||
|
methods: {
|
||||||
|
selected(element: INodeCreateElement) {
|
||||||
|
if (this.$props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('selected', element);
|
||||||
|
},
|
||||||
|
beforeEnter(el: HTMLElement) {
|
||||||
|
el.style.height = '0';
|
||||||
|
},
|
||||||
|
enter(el: HTMLElement) {
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
},
|
||||||
|
beforeLeave(el: HTMLElement) {
|
||||||
|
el.style.height = `${el.scrollHeight}px`;
|
||||||
|
},
|
||||||
|
leave(el: HTMLElement) {
|
||||||
|
el.style.height = '0';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.accordion-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-leave-active {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-leave-active {
|
||||||
|
transition: all 0.25s ease, opacity 0.1s ease;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-enter-active {
|
||||||
|
transition: all 0.25s ease, opacity 0.25s ease;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategory + .category,
|
||||||
|
.node + .category {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
332
packages/editor-ui/src/components/NodeCreator/MainPanel.vue
Normal file
332
packages/editor-ui/src/components/NodeCreator/MainPanel.vue
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
<template>
|
||||||
|
<div @click="onClickInside" class="container">
|
||||||
|
<SlideTransition>
|
||||||
|
<SubcategoryPanel v-if="activeSubcategory" :elements="subcategorizedNodes" :title="activeSubcategory.properties.subcategory" :activeIndex="activeSubcategoryIndex" @close="onSubcategoryClose" @selected="selected" />
|
||||||
|
</SlideTransition>
|
||||||
|
<div class="main-panel">
|
||||||
|
<SearchBar
|
||||||
|
v-model="nodeFilter"
|
||||||
|
:eventBus="searchEventBus"
|
||||||
|
@keydown.native="nodeFilterKeyDown"
|
||||||
|
/>
|
||||||
|
<div class="type-selector">
|
||||||
|
<el-tabs v-model="selectedType" stretch>
|
||||||
|
<el-tab-pane label="All" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||||
|
<el-tab-pane label="Regular" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||||
|
<el-tab-pane label="Trigger" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<div v-if="searchFilter.length === 0" class="scrollable">
|
||||||
|
<ItemIterator
|
||||||
|
:elements="categorized"
|
||||||
|
:disabled="!!activeSubcategory"
|
||||||
|
:activeIndex="activeIndex"
|
||||||
|
:transitionsEnabled="true"
|
||||||
|
@selected="selected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="scrollable"
|
||||||
|
v-else-if="filteredNodeTypes.length > 0"
|
||||||
|
>
|
||||||
|
<ItemIterator
|
||||||
|
:elements="filteredNodeTypes"
|
||||||
|
:activeIndex="activeIndex"
|
||||||
|
@selected="selected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<NoResults v-else @nodeTypeSelected="nodeTypeSelected" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import ItemIterator from './ItemIterator.vue';
|
||||||
|
import NoResults from './NoResults.vue';
|
||||||
|
import SearchBar from './SearchBar.vue';
|
||||||
|
import SubcategoryPanel from './SubcategoryPanel.vue';
|
||||||
|
import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps } from '@/Interface';
|
||||||
|
import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
|
||||||
|
import SlideTransition from '../transitions/SlideTransition.vue';
|
||||||
|
import { matchesNodeType, matchesSelectType } from './helpers';
|
||||||
|
|
||||||
|
|
||||||
|
export default mixins(externalHooks).extend({
|
||||||
|
name: 'NodeCreateList',
|
||||||
|
components: {
|
||||||
|
ItemIterator,
|
||||||
|
NoResults,
|
||||||
|
SubcategoryPanel,
|
||||||
|
SlideTransition,
|
||||||
|
SearchBar,
|
||||||
|
},
|
||||||
|
props: ['categorizedItems', 'categoriesWithNodes', 'searchItems'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeCategory: [] as string[],
|
||||||
|
activeSubcategory: null as INodeCreateElement | null,
|
||||||
|
activeIndex: 1,
|
||||||
|
activeSubcategoryIndex: 0,
|
||||||
|
nodeFilter: '',
|
||||||
|
selectedType: ALL_NODE_FILTER,
|
||||||
|
searchEventBus: new Vue(),
|
||||||
|
REGULAR_NODE_FILTER,
|
||||||
|
TRIGGER_NODE_FILTER,
|
||||||
|
ALL_NODE_FILTER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
searchFilter(): string {
|
||||||
|
return this.nodeFilter.toLowerCase().trim();
|
||||||
|
},
|
||||||
|
filteredNodeTypes(): INodeCreateElement[] {
|
||||||
|
const nodeTypes: INodeCreateElement[] = this.searchItems;
|
||||||
|
const filter = this.searchFilter;
|
||||||
|
|
||||||
|
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
|
||||||
|
const nodeType = (el.properties as INodeItemProps).nodeType;
|
||||||
|
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
|
||||||
|
nodeFilter: this.nodeFilter,
|
||||||
|
result: returnData,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
},
|
||||||
|
|
||||||
|
categorized() {
|
||||||
|
return this.categorizedItems && this.categorizedItems
|
||||||
|
.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
|
||||||
|
if (
|
||||||
|
el.type !== 'category' &&
|
||||||
|
!this.activeCategory.includes(el.category)
|
||||||
|
) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchesSelectType(el, this.selectedType)) {
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.type === 'category') {
|
||||||
|
accu.push({
|
||||||
|
...el,
|
||||||
|
properties: {
|
||||||
|
expanded: this.activeCategory.includes(el.category),
|
||||||
|
},
|
||||||
|
} as INodeCreateElement);
|
||||||
|
return accu;
|
||||||
|
}
|
||||||
|
|
||||||
|
accu.push(el);
|
||||||
|
return accu;
|
||||||
|
}, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
subcategorizedNodes() {
|
||||||
|
const activeSubcategory = this.activeSubcategory as INodeCreateElement;
|
||||||
|
const category = activeSubcategory.category;
|
||||||
|
const subcategory = (activeSubcategory.properties as ISubcategoryItemProps).subcategory;
|
||||||
|
|
||||||
|
return activeSubcategory && this.categoriesWithNodes[category][subcategory]
|
||||||
|
.nodes.filter((el: INodeCreateElement) => matchesSelectType(el, this.selectedType));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
nodeFilter(newValue, oldValue) {
|
||||||
|
// Reset the index whenver the filter-value changes
|
||||||
|
this.activeIndex = 0;
|
||||||
|
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
filteredNodes: this.filteredNodeTypes,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectedType(newValue, oldValue) {
|
||||||
|
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
nodeFilterKeyDown(e: KeyboardEvent) {
|
||||||
|
if (!['Escape', 'Tab'].includes(e.key)) {
|
||||||
|
// We only want to propagate 'Escape' as it closes the node-creator and
|
||||||
|
// 'Tab' which toggles it
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeSubcategory) {
|
||||||
|
const activeList = this.subcategorizedNodes;
|
||||||
|
const activeNodeType = activeList[this.activeSubcategoryIndex];
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown' && this.activeSubcategory) {
|
||||||
|
this.activeSubcategoryIndex++;
|
||||||
|
this.activeSubcategoryIndex = Math.min(
|
||||||
|
this.activeSubcategoryIndex,
|
||||||
|
activeList.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (e.key === 'ArrowUp' && this.activeSubcategory) {
|
||||||
|
this.activeSubcategoryIndex--;
|
||||||
|
this.activeSubcategoryIndex = Math.max(this.activeSubcategoryIndex, 0);
|
||||||
|
}
|
||||||
|
else if (e.key === 'Enter') {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
}
|
||||||
|
else if (e.key === 'ArrowLeft') {
|
||||||
|
this.onSubcategoryClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeList;
|
||||||
|
if (this.searchFilter.length > 0) {
|
||||||
|
activeList = this.filteredNodeTypes;
|
||||||
|
} else {
|
||||||
|
activeList = this.categorized;
|
||||||
|
}
|
||||||
|
const activeNodeType = activeList[this.activeIndex];
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
this.activeIndex++;
|
||||||
|
// Make sure that we stop at the last nodeType
|
||||||
|
this.activeIndex = Math.min(
|
||||||
|
this.activeIndex,
|
||||||
|
activeList.length - 1,
|
||||||
|
);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
this.activeIndex--;
|
||||||
|
// Make sure that we do not get before the first nodeType
|
||||||
|
this.activeIndex = Math.max(this.activeIndex, 0);
|
||||||
|
} else if (e.key === 'Enter' && activeNodeType) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType.type === 'subcategory') {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowRight' && activeNodeType.type === 'category' && !activeNodeType.properties.expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
} else if (e.key === 'ArrowLeft' && activeNodeType.type === 'category' && activeNodeType.properties.expanded) {
|
||||||
|
this.selected(activeNodeType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected(element: INodeCreateElement) {
|
||||||
|
if (element.type === 'node') {
|
||||||
|
const properties = element.properties as INodeItemProps;
|
||||||
|
|
||||||
|
this.nodeTypeSelected(properties.nodeType.name);
|
||||||
|
} else if (element.type === 'category') {
|
||||||
|
this.onCategorySelected(element.category);
|
||||||
|
} else if (element.type === 'subcategory') {
|
||||||
|
this.onSubcategorySelected(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nodeTypeSelected(nodeTypeName: string) {
|
||||||
|
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||||
|
},
|
||||||
|
onCategorySelected(category: string) {
|
||||||
|
if (this.activeCategory.includes(category)) {
|
||||||
|
this.activeCategory = this.activeCategory.filter(
|
||||||
|
(active: string) => active !== category,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.activeCategory = [...this.activeCategory, category];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeIndex = this.categorized.findIndex(
|
||||||
|
(el: INodeCreateElement) => el.category === category,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSubcategorySelected(selected: INodeCreateElement) {
|
||||||
|
this.activeSubcategoryIndex = 0;
|
||||||
|
this.activeSubcategory = selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubcategoryClose() {
|
||||||
|
this.activeSubcategory = null;
|
||||||
|
this.activeSubcategoryIndex = 0;
|
||||||
|
this.nodeFilter = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
onClickInside() {
|
||||||
|
this.searchEventBus.$emit('focus');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// initial opening effect
|
||||||
|
this.activeCategory = [CORE_NODES_CATEGORY];
|
||||||
|
});
|
||||||
|
this.$externalHooks().run('nodeCreateList.mounted');
|
||||||
|
},
|
||||||
|
async destroyed() {
|
||||||
|
this.$externalHooks().run('nodeCreateList.destroyed');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/deep/ .el-tabs__item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .el-tabs__active-bar {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/deep/ .el-tabs__nav-wrap::after {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-panel .scrollable {
|
||||||
|
height: calc(100% - 160px);
|
||||||
|
padding-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-selector {
|
||||||
|
text-align: center;
|
||||||
|
background-color: $--node-creator-select-background-color;
|
||||||
|
|
||||||
|
/deep/ .el-tabs > div {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.el-tabs__nav {
|
||||||
|
height: 43px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
126
packages/editor-ui/src/components/NodeCreator/NoResults.vue
Normal file
126
packages/editor-ui/src/components/NodeCreator/NoResults.vue
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div class="no-results">
|
||||||
|
<div class="icon">
|
||||||
|
<NoResultsIcon />
|
||||||
|
</div>
|
||||||
|
<div class="title">
|
||||||
|
<div>We didn't make that... yet</div>
|
||||||
|
<div class="action">
|
||||||
|
Don’t worry, you can probably do it with the
|
||||||
|
<a @click="selectHttpRequest">HTTP Request</a> or
|
||||||
|
<a @click="selectWebhook">Webhook</a> node
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="request">
|
||||||
|
<div>Want us to make it faster?</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
:href="REQUEST_NODE_FORM_URL"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span>Request the node</span>
|
||||||
|
<span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="external"
|
||||||
|
icon="external-link-alt"
|
||||||
|
title="Request the node"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { HTTP_REQUEST_NODE_NAME, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_NAME } from '@/constants';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import NoResultsIcon from './NoResultsIcon.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'NoResults',
|
||||||
|
components: {
|
||||||
|
NoResultsIcon,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
REQUEST_NODE_FORM_URL,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
basePath(): string {
|
||||||
|
return this.$store.getters.getBaseUrl;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectWebhook() {
|
||||||
|
this.$emit('nodeTypeSelected', WEBHOOK_NODE_NAME);
|
||||||
|
},
|
||||||
|
|
||||||
|
selectHttpRequest() {
|
||||||
|
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_NAME);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.no-results {
|
||||||
|
background-color: $--node-creator-no-results-background-color;
|
||||||
|
text-align: center;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 1px solid $--node-creator-border-color;
|
||||||
|
flex-direction: column;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
padding: 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
margin-top: 50px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action, .request {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-height: 550px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $--color-primary;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-top: 100px;
|
||||||
|
min-height: 67px;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<svg width="75px" height="75px" viewBox="0 0 75 75" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>no-nodes-keyart</title>
|
||||||
|
<g id="Nodes-panel-prototype-V2.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="nodes-panel-(component)" transform="translate(-2085.000000, -352.000000)">
|
||||||
|
<g id="nodes_panel" transform="translate(1880.000000, 151.000000)">
|
||||||
|
<g id="Panel" transform="translate(50.000000, 0.000000)">
|
||||||
|
<g id="Group-3" transform="translate(105.000000, 171.000000)">
|
||||||
|
<g id="no-nodes-keyart" transform="translate(50.000000, 30.000000)">
|
||||||
|
<rect id="Rectangle" x="0" y="0" width="75" height="75"></rect>
|
||||||
|
<g id="Group" transform="translate(6.562500, 8.164062)" fill="#C4C8D1" fill-rule="nonzero">
|
||||||
|
<polygon id="Rectangle" transform="translate(49.192016, 45.302553) rotate(-45.000000) translate(-49.192016, -45.302553) " points="44.5045606 32.0526802 53.8794707 32.0526802 53.8794707 58.5524261 44.5045606 58.5524261"></polygon>
|
||||||
|
<path d="M48.125,23.0859375 C54.15625,23.0859375 59.0625,18.1796875 59.0625,12.1484375 C59.0625,10.3359375 58.5625,8.6484375 57.78125,7.1484375 L49.34375,15.5859375 L44.6875,10.9296875 L53.125,2.4921875 C51.625,1.7109375 49.9375,1.2109375 48.125,1.2109375 C42.09375,1.2109375 37.1875,6.1171875 37.1875,12.1484375 C37.1875,13.4296875 37.4375,14.6484375 37.84375,15.7734375 L32.0625,21.5546875 L26.5,15.9921875 L28.71875,13.7734375 L24.3125,9.3671875 L30.9375,2.7421875 C27.28125,-0.9140625 21.34375,-0.9140625 17.6875,2.7421875 L6.625,13.8046875 L11.03125,18.2109375 L2.21875,18.2109375 L1.38777878e-15,20.4296875 L11.0625,31.4921875 L13.28125,29.2734375 L13.28125,20.4296875 L17.6875,24.8359375 L19.90625,22.6171875 L25.46875,28.1796875 L2.3125,51.3359375 L8.9375,57.9609375 L44.5,22.4296875 C45.625,22.8359375 46.84375,23.0859375 48.125,23.0859375 Z" id="Path"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
101
packages/editor-ui/src/components/NodeCreator/NodeCreator.vue
Normal file
101
packages/editor-ui/src/components/NodeCreator/NodeCreator.vue
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<SlideTransition>
|
||||||
|
<div class="node-creator" v-if="active" v-click-outside="closeCreator">
|
||||||
|
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
|
||||||
|
</div>
|
||||||
|
</SlideTransition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import { ICategoriesWithNodes, INodeCreateElement } from '@/Interface';
|
||||||
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import SlideTransition from '../transitions/SlideTransition.vue';
|
||||||
|
import { HIDDEN_NODES } from '@/constants';
|
||||||
|
|
||||||
|
import MainPanel from './MainPanel.vue';
|
||||||
|
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'NodeCreator',
|
||||||
|
components: {
|
||||||
|
MainPanel,
|
||||||
|
SlideTransition,
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'active',
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
visibleNodeTypes(): INodeTypeDescription[] {
|
||||||
|
return this.$store.getters.allNodeTypes
|
||||||
|
.filter((nodeType: INodeTypeDescription) => {
|
||||||
|
return !HIDDEN_NODES.includes(nodeType.name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
categoriesWithNodes(): ICategoriesWithNodes {
|
||||||
|
return getCategoriesWithNodes(this.visibleNodeTypes);
|
||||||
|
},
|
||||||
|
categorizedItems(): INodeCreateElement[] {
|
||||||
|
return getCategorizedList(this.categoriesWithNodes);
|
||||||
|
},
|
||||||
|
searchItems(): INodeCreateElement[] {
|
||||||
|
const sorted = [...this.visibleNodeTypes];
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const textA = a.displayName.toLowerCase();
|
||||||
|
const textB = b.displayName.toLowerCase();
|
||||||
|
return textA < textB ? -1 : textA > textB ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted.map((nodeType) => ({
|
||||||
|
type: 'node',
|
||||||
|
category: '',
|
||||||
|
key: `${nodeType.name}`,
|
||||||
|
properties: {
|
||||||
|
nodeType,
|
||||||
|
subcategory: '',
|
||||||
|
},
|
||||||
|
includedByTrigger: nodeType.group.includes('trigger'),
|
||||||
|
includedByRegular: !nodeType.group.includes('trigger'),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeCreator () {
|
||||||
|
this.$emit('closeNodeCreator');
|
||||||
|
},
|
||||||
|
nodeTypeSelected (nodeTypeName: string) {
|
||||||
|
this.$emit('nodeTypeSelected', nodeTypeName);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
/deep/ *, *:before, *:after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-creator {
|
||||||
|
position: fixed;
|
||||||
|
top: $--header-height;
|
||||||
|
right: 0;
|
||||||
|
width: $--node-creator-width;
|
||||||
|
height: 100%;
|
||||||
|
background-color: $--node-creator-background-color;
|
||||||
|
z-index: 200;
|
||||||
|
color: $--node-creator-text-color;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid $--node-creator-border-color;
|
||||||
|
width: 1px;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
86
packages/editor-ui/src/components/NodeCreator/NodeItem.vue
Normal file
86
packages/editor-ui/src/components/NodeCreator/NodeItem.vue
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<template functional>
|
||||||
|
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
|
||||||
|
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" :style="{color: props.nodeType.defaults.color}" />
|
||||||
|
<div>
|
||||||
|
<div :class="$style.details">
|
||||||
|
<span :class="$style.name">{{props.nodeType.displayName}}</span>
|
||||||
|
<span :class="$style['trigger-icon']">
|
||||||
|
<TriggerIcon v-if="$options.isTrigger(props.nodeType)" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.description">
|
||||||
|
{{props.nodeType.description}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import NodeIcon from '../NodeIcon.vue';
|
||||||
|
import TriggerIcon from '../TriggerIcon.vue';
|
||||||
|
|
||||||
|
Vue.component('NodeIcon', NodeIcon);
|
||||||
|
Vue.component('TriggerIcon', TriggerIcon);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
'active',
|
||||||
|
'filter',
|
||||||
|
'nodeType',
|
||||||
|
'bordered',
|
||||||
|
],
|
||||||
|
isTrigger (nodeType: INodeTypeDescription): boolean {
|
||||||
|
return nodeType.group.includes('trigger');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.node-item {
|
||||||
|
padding: 11px 8px 11px 0;
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.bordered {
|
||||||
|
border-bottom: 1px solid $--node-creator-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
min-width: 26px;
|
||||||
|
max-width: 26px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $--node-creator-description-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-icon {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
124
packages/editor-ui/src/components/NodeCreator/SearchBar.vue
Normal file
124
packages/editor-ui/src/components/NodeCreator/SearchBar.vue
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<div class="search-container">
|
||||||
|
<div :class="{ prefix: true, active: value.length > 0 }">
|
||||||
|
<font-awesome-icon icon="search" />
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<input
|
||||||
|
placeholder="Search nodes..."
|
||||||
|
ref="input"
|
||||||
|
:value="value"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="suffix" v-if="value.length > 0" @click="clear">
|
||||||
|
<span class="clear el-icon-close clickable"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from "vue";
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: "SearchBar",
|
||||||
|
props: ["value", "eventBus"],
|
||||||
|
mounted() {
|
||||||
|
if (this.$props.eventBus) {
|
||||||
|
this.$props.eventBus.$on("focus", () => {
|
||||||
|
this.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focus() {
|
||||||
|
const input = this.$refs.input as HTMLInputElement;
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInput(event: InputEvent) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
this.$emit("input", input.value);
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
this.$emit("input", "");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-container {
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 20px;
|
||||||
|
border-top: 1px solid $--node-creator-border-color;
|
||||||
|
border-bottom: 1px solid $--node-creator-border-color;
|
||||||
|
background-color: $--node-creator-search-background-color;
|
||||||
|
color: $--node-creator-search-placeholder-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefix {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 14px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: $--color-primary !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
border: none !important;
|
||||||
|
outline: none;
|
||||||
|
font-size: 18px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
|
&::placeholder,
|
||||||
|
&::-webkit-input-placeholder {
|
||||||
|
color: $--node-creator-search-placeholder-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suffix {
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear {
|
||||||
|
background-color: $--node-creator-search-clear-background-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: $--node-creator-search-background-color;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $--node-creator-search-clear-background-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
line-height: 16px;
|
||||||
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template functional>
|
||||||
|
<div :class="$style.subcategory">
|
||||||
|
<div :class="$style.details">
|
||||||
|
<div :class="$style.title">{{ props.item.properties.subcategory }}</div>
|
||||||
|
<div v-if="props.item.properties.description" :class="$style.description">
|
||||||
|
{{ props.item.properties.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.action">
|
||||||
|
<font-awesome-icon :class="$style.arrow" icon="arrow-right" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: ['item'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.subcategory {
|
||||||
|
display: flex;
|
||||||
|
padding: 11px 16px 11px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 16px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $--node-creator-description-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
width: 12px;
|
||||||
|
color: $--node-creator-arrow-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<div class="subcategory-panel">
|
||||||
|
<div class="subcategory-header">
|
||||||
|
<div class="clickable" @click="onBackArrowClick">
|
||||||
|
<font-awesome-icon class="back-arrow" icon="arrow-left" />
|
||||||
|
</div>
|
||||||
|
<span>{{ title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scrollable">
|
||||||
|
<ItemIterator
|
||||||
|
:elements="elements"
|
||||||
|
:activeIndex="activeIndex"
|
||||||
|
@selected="selected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { INodeCreateElement } from '@/Interface';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import ItemIterator from './ItemIterator.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SubcategoryPanel',
|
||||||
|
components: {
|
||||||
|
ItemIterator,
|
||||||
|
},
|
||||||
|
props: ['title', 'elements', 'activeIndex'],
|
||||||
|
methods: {
|
||||||
|
selected(element: INodeCreateElement) {
|
||||||
|
this.$emit('selected', element);
|
||||||
|
},
|
||||||
|
onBackArrowClick() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.subcategory-panel {
|
||||||
|
position: absolute;
|
||||||
|
background: $--node-creator-search-background-color;
|
||||||
|
z-index: 100;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: ' ';
|
||||||
|
border-left: 1px solid $--node-creator-border-color;
|
||||||
|
width: 1px;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subcategory-header {
|
||||||
|
border: $--node-creator-border-color solid 1px;
|
||||||
|
height: 50px;
|
||||||
|
background-color: $--node-creator-subcategory-panel-header-bacground-color;
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 11px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
color: $--node-creator-arrow-color;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
height: calc(100% - 100px);
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
176
packages/editor-ui/src/components/NodeCreator/helpers.ts
Normal file
176
packages/editor-ui/src/components/NodeCreator/helpers.ts
Normal file
|
@ -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);
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="node-icon-wrapper" :style="iconStyleData" :class="{full: isSvgIcon}">
|
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
|
||||||
<div v-if="nodeIconData !== null" class="icon">
|
<div v-if="nodeIconData !== null" class="icon">
|
||||||
<img :src="nodeIconData.path" style="width: 100%; height: 100%;" v-if="nodeIconData.type === 'file'"/>
|
<img :src="nodeIconData.path" style="max-width: 100%; max-height: 100%;" v-if="nodeIconData.type === 'file'"/>
|
||||||
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
|
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="node-icon-placeholder">
|
<div v-else class="node-icon-placeholder">
|
||||||
|
@ -25,6 +25,7 @@ export default Vue.extend({
|
||||||
props: [
|
props: [
|
||||||
'nodeType',
|
'nodeType',
|
||||||
'size',
|
'size',
|
||||||
|
'shrink',
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
iconStyleData (): object {
|
iconStyleData (): object {
|
||||||
|
@ -79,19 +80,23 @@ export default Vue.extend({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
.node-icon-wrapper {
|
.node-icon-wrapper {
|
||||||
width: 30px;
|
width: 26px;
|
||||||
height: 30px;
|
height: 26px;
|
||||||
border-radius: 15px;
|
border-radius: 4px;
|
||||||
color: #444;
|
color: #444;
|
||||||
line-height: 30px;
|
line-height: 26px;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #fff;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
&.full .icon {
|
&.full .icon {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.shrink .icon {
|
||||||
margin: 0.24em;
|
margin: 0.24em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
42
packages/editor-ui/src/components/TriggerIcon.vue
Normal file
42
packages/editor-ui/src/components/TriggerIcon.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template functional>
|
||||||
|
<span :class="$style.trigger">
|
||||||
|
<svg width="36px" height="36px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>Trigger node</title>
|
||||||
|
<g id="/integrations-(V1-feature)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Individual-node-view" transform="translate(-304.000000, -137.000000)" fill-rule="nonzero">
|
||||||
|
<g id="left-column" transform="translate(120.000000, 131.000000)">
|
||||||
|
<g id="trigger-badge" transform="translate(178.000000, 0.000000)">
|
||||||
|
<g id="trigger-icon" transform="translate(6.857143, 6.857143)">
|
||||||
|
<g id="Icon" transform="translate(8.571429, 0.000000)" fill="#FF6150">
|
||||||
|
<polygon id="Icon-Path" points="7.14285714 21.4285714 0 21.4285714 10 1.42857143 10 12.8571429 17.1428571 12.8571429 7.14285714 32.8571429"></polygon>
|
||||||
|
</g>
|
||||||
|
<rect id="ViewBox" x="0" y="0" width="34.2857143" height="34.2857143"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'TriggerIcon',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.trigger {
|
||||||
|
background-color: $--trigger-icon-background-color;
|
||||||
|
border: 1px solid $--trigger-icon-border-color;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<transition name="slide">
|
||||||
|
<slot></slot>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SlideTransition',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.slide-leave-active,
|
||||||
|
.slide-enter-active {
|
||||||
|
transition: 0.3s ease;
|
||||||
|
}
|
||||||
|
.slide-leave-to,
|
||||||
|
.slide-enter {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -15,8 +15,31 @@ export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||||
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
||||||
|
|
||||||
|
// breakpoints
|
||||||
export const BREAKPOINT_SM = 768;
|
export const BREAKPOINT_SM = 768;
|
||||||
export const BREAKPOINT_MD = 992;
|
export const BREAKPOINT_MD = 992;
|
||||||
export const BREAKPOINT_LG = 1200;
|
export const BREAKPOINT_LG = 1200;
|
||||||
export const BREAKPOINT_XL = 1920;
|
export const BREAKPOINT_XL = 1920;
|
||||||
|
|
||||||
|
// Node creator
|
||||||
|
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||||
|
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||||
|
export const SUBCATEGORY_DESCRIPTIONS: {
|
||||||
|
[category: string]: { [subcategory: string]: string };
|
||||||
|
} = {
|
||||||
|
'Core Nodes': {
|
||||||
|
Flow: 'Branches, core triggers, merge data',
|
||||||
|
Files: 'Work with CSV, XML, text, images etc.',
|
||||||
|
'Data Transformation': 'Manipulate data fields, run code',
|
||||||
|
Helpers: 'HTTP Requests (API calls), date and time, scrape HTML',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const REGULAR_NODE_FILTER = 'Regular';
|
||||||
|
export const TRIGGER_NODE_FILTER = 'Trigger';
|
||||||
|
export const ALL_NODE_FILTER = 'All';
|
||||||
|
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
||||||
|
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
||||||
|
export const HIDDEN_NODES = ['n8n-nodes-base.start'];
|
||||||
|
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
|
||||||
|
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
|
||||||
|
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||||
|
|
|
@ -28,12 +28,15 @@ import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faAngleRight,
|
faAngleRight,
|
||||||
faAngleUp,
|
faAngleUp,
|
||||||
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faAt,
|
faAt,
|
||||||
faBook,
|
faBook,
|
||||||
faBug,
|
faBug,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faCheck,
|
faCheck,
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
faCode,
|
faCode,
|
||||||
faCodeBranch,
|
faCodeBranch,
|
||||||
faCog,
|
faCog,
|
||||||
|
@ -79,6 +82,7 @@ import {
|
||||||
faRedo,
|
faRedo,
|
||||||
faRss,
|
faRss,
|
||||||
faSave,
|
faSave,
|
||||||
|
faSearch,
|
||||||
faSearchMinus,
|
faSearchMinus,
|
||||||
faSearchPlus,
|
faSearchPlus,
|
||||||
faServer,
|
faServer,
|
||||||
|
@ -112,12 +116,15 @@ library.add(faAngleDoubleLeft);
|
||||||
library.add(faAngleDown);
|
library.add(faAngleDown);
|
||||||
library.add(faAngleRight);
|
library.add(faAngleRight);
|
||||||
library.add(faAngleUp);
|
library.add(faAngleUp);
|
||||||
|
library.add(faArrowLeft);
|
||||||
library.add(faArrowRight);
|
library.add(faArrowRight);
|
||||||
library.add(faAt);
|
library.add(faAt);
|
||||||
library.add(faBook);
|
library.add(faBook);
|
||||||
library.add(faBug);
|
library.add(faBug);
|
||||||
library.add(faCalendar);
|
library.add(faCalendar);
|
||||||
library.add(faCheck);
|
library.add(faCheck);
|
||||||
|
library.add(faChevronDown);
|
||||||
|
library.add(faChevronUp);
|
||||||
library.add(faCode);
|
library.add(faCode);
|
||||||
library.add(faCodeBranch);
|
library.add(faCodeBranch);
|
||||||
library.add(faCog);
|
library.add(faCog);
|
||||||
|
@ -163,6 +170,7 @@ library.add(faQuestionCircle);
|
||||||
library.add(faRedo);
|
library.add(faRedo);
|
||||||
library.add(faRss);
|
library.add(faRss);
|
||||||
library.add(faSave);
|
library.add(faSave);
|
||||||
|
library.add(faSearch);
|
||||||
library.add(faSearchMinus);
|
library.add(faSearchMinus);
|
||||||
library.add(faSearchPlus);
|
library.add(faSearchPlus);
|
||||||
library.add(faServer);
|
library.add(faServer);
|
||||||
|
|
|
@ -63,3 +63,23 @@ $--tag-text-color: #3d3f46;
|
||||||
$--tag-close-background-color: #717782;
|
$--tag-close-background-color: #717782;
|
||||||
$--tag-close-background-hover-color: #3d3f46;
|
$--tag-close-background-hover-color: #3d3f46;
|
||||||
|
|
||||||
|
// Node creator
|
||||||
|
$--node-creator-width: 385px;
|
||||||
|
$--node-creator-text-color: #555;
|
||||||
|
$--node-creator-select-background-color: #f2f4f8;
|
||||||
|
$--node-creator-background-color: #fff;
|
||||||
|
$--node-creator-search-background-color: #fff;
|
||||||
|
$--node-creator-border-color: #dbdfe7;
|
||||||
|
$--node-creator-item-hover-border-color: #8d939c;
|
||||||
|
$--node-creator-arrow-color: #8d939c;
|
||||||
|
$--node-creator-no-results-background-color: #f8f9fb;
|
||||||
|
$--node-creator-close-button-color: #fff;
|
||||||
|
$--node-creator-search-clear-background-color: #8d939c;
|
||||||
|
$--node-creator-search-clear-background-color-hover: #3d3f46;
|
||||||
|
$--node-creator-search-placeholder-color: #909399;
|
||||||
|
$--node-creator-subcategory-panel-header-bacground-color: #f2f4f8;
|
||||||
|
$--node-creator-description-color: #7d7d87;
|
||||||
|
|
||||||
|
// trigger icon
|
||||||
|
$--trigger-icon-border-color: #dcdfe6;
|
||||||
|
$--trigger-icon-background-color: #fff;
|
||||||
|
|
|
@ -129,7 +129,7 @@ import { workflowRun } from '@/components/mixins/workflowRun';
|
||||||
import DataDisplay from '@/components/DataDisplay.vue';
|
import DataDisplay from '@/components/DataDisplay.vue';
|
||||||
import Modals from '@/components/Modals.vue';
|
import Modals from '@/components/Modals.vue';
|
||||||
import Node from '@/components/Node.vue';
|
import Node from '@/components/Node.vue';
|
||||||
import NodeCreator from '@/components/NodeCreator.vue';
|
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import RunData from '@/components/RunData.vue';
|
import RunData from '@/components/RunData.vue';
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,12 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
copyfiles([join(process.cwd(), './*.png'), outputDirectory], { up: true }, () => resolve(outputDirectory));
|
['*.png', '*.node.json'].forEach(filenamePattern => {
|
||||||
|
copyfiles(
|
||||||
|
[join(process.cwd(), `./${filenamePattern}`), outputDirectory],
|
||||||
|
{ up: true },
|
||||||
|
() => resolve(outputDirectory));
|
||||||
|
});
|
||||||
buildProcess.on('exit', code => {
|
buildProcess.on('exit', code => {
|
||||||
// Remove the tmp tsconfig file
|
// Remove the tmp tsconfig file
|
||||||
tsconfigData.cleanup();
|
tsconfigData.cleanup();
|
||||||
|
|
|
@ -567,6 +567,7 @@ export interface INodeTypeDescription {
|
||||||
deactivate?: INodeHookDescription[];
|
deactivate?: INodeHookDescription[];
|
||||||
};
|
};
|
||||||
webhooks?: IWebhookDescription[];
|
webhooks?: IWebhookDescription[];
|
||||||
|
codex?: CodexData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeHookDescription {
|
export interface INodeHookDescription {
|
||||||
|
@ -781,6 +782,12 @@ export interface IStatusCodeMessages {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodexData = {
|
||||||
|
categories?: string[];
|
||||||
|
subcategories?: {[category: string]: string[]};
|
||||||
|
alias?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
export type JsonValue = string | number | boolean | null | JsonObject | JsonValue[];
|
||||||
|
|
||||||
export type JsonObject = { [key: string]: JsonValue };
|
export type JsonObject = { [key: string]: JsonValue };
|
||||||
|
|
Loading…
Reference in a new issue