✨ 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" 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) * ⚡ 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:
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>
|
||||
Reference in New Issue
Block a user