refactor(editor): Apply Prettier (no-changelog) (#4920)

*  Adjust `format` script

* 🔥 Remove exemption for `editor-ui`

* 🎨 Prettify

* 👕 Fix lint
This commit is contained in:
Iván Ovejero
2022-12-14 10:04:10 +01:00
committed by GitHub
parent bcde07e032
commit 5ca2148c7e
284 changed files with 19247 additions and 15540 deletions

View File

@@ -10,12 +10,8 @@
:isTrigger="isTriggerAction(action)"
>
<template #dragContent>
<div :class="$style.draggableDataTransfer" ref="draggableDataTransfer"/>
<div
:class="$style.draggable"
:style="draggableStyle"
v-show="dragging"
>
<div :class="$style.draggableDataTransfer" ref="draggableDataTransfer" />
<div :class="$style.draggable" :style="draggableStyle" v-show="dragging">
<node-icon :nodeType="nodeType" @click.capture.stop :size="40" :shrink="false" />
</div>
</template>
@@ -34,14 +30,15 @@ import NodeIcon from '@/components/NodeIcon.vue';
import { useNodeCreatorStore } from '@/stores/nodeCreator';
export interface Props {
nodeType: INodeTypeDescription,
action: INodeActionTypeDescription,
nodeType: INodeTypeDescription;
action: INodeActionTypeDescription;
}
const props = defineProps<Props>();
const instance = getCurrentInstance();
const telemetry = instance?.proxy.$telemetry;
const { getActionData, getNodeTypesWithManualTrigger, setAddedNodeActionParameters } = useNodeCreatorStore();
const { getActionData, getNodeTypesWithManualTrigger, setAddedNodeActionParameters } =
useNodeCreatorStore();
const state = reactive({
dragging: false,
@@ -53,19 +50,20 @@ const state = reactive({
draggableDataTransfer: null as Element | null,
});
const emit = defineEmits<{
(event: 'actionSelected', action: IUpdateInformation): void,
(event: 'dragstart', $e: DragEvent): void,
(event: 'dragend', $e: DragEvent): void,
(event: 'actionSelected', action: IUpdateInformation): void;
(event: 'dragstart', $e: DragEvent): void;
(event: 'dragend', $e: DragEvent): void;
}>();
const draggableStyle = computed<{ top: string; left: string; }>(() => ({
const draggableStyle = computed<{ top: string; left: string }>(() => ({
top: `${state.draggablePosition.y}px`,
left: `${state.draggablePosition.x}px`,
}));
const actionData = computed(() => getActionData(props.action));
const isTriggerAction = (action: INodeActionTypeDescription) => action.name?.toLowerCase().includes('trigger');
const isTriggerAction = (action: INodeActionTypeDescription) =>
action.name?.toLowerCase().includes('trigger');
function onActionClick(actionItem: INodeActionTypeDescription) {
emit('actionSelected', getActionData(actionItem));
}
@@ -76,16 +74,19 @@ function onDragStart(event: DragEvent): void {
* All browsers attach the correct page coordinates to the "dragover" event.
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
*/
document.body.addEventListener("dragover", onDragOver);
document.body.addEventListener('dragover', onDragOver);
const { pageX: x, pageY: y } = event;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.dropEffect = "copy";
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.dropEffect = 'copy';
event.dataTransfer.setDragImage(state.draggableDataTransfer as Element, 0, 0);
event.dataTransfer.setData('nodeTypeName', getNodeTypesWithManualTrigger(actionData.value?.key).join(','));
event.dataTransfer.setData(
'nodeTypeName',
getNodeTypesWithManualTrigger(actionData.value?.key).join(','),
);
state.storeWatcher = setAddedNodeActionParameters(actionData.value, telemetry);
document.body.addEventListener("dragend", onDragEnd);
document.body.addEventListener('dragend', onDragEnd);
}
state.dragging = true;
@@ -94,19 +95,19 @@ function onDragStart(event: DragEvent): void {
}
function onDragOver(event: DragEvent): void {
if (!state.dragging || event.pageX === 0 && event.pageY === 0) {
if (!state.dragging || (event.pageX === 0 && event.pageY === 0)) {
return;
}
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
const [x, y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
state.draggablePosition = { x, y };
}
function onDragEnd(event: DragEvent): void {
if(state.storeWatcher) state.storeWatcher();
document.body.removeEventListener("dragend", onDragEnd);
document.body.removeEventListener("dragover", onDragOver);
if (state.storeWatcher) state.storeWatcher();
document.body.removeEventListener('dragend', onDragEnd);
document.body.removeEventListener('dragover', onDragOver);
emit('dragend', event);
@@ -135,7 +136,7 @@ const { draggableDataTransfer, dragging } = toRefs(state);
color: var(--color-text-base);
padding-top: var(--spacing-s);
line-height: var(--font-line-height-regular);
border-top: 1px solid #DBDFE7;
border-top: 1px solid #dbdfe7;
z-index: 1;
// Prevent double borders when the last category is collapsed
margin-top: -1px;

View File

@@ -1,5 +1,5 @@
<template>
<transition :name="`panel-slide-${state.transitionDirection}`" >
<transition :name="`panel-slide-${state.transitionDirection}`">
<div
ref="mainPanelContainer"
tabindex="0"
@@ -12,12 +12,16 @@
<slot name="header" />
</div>
<div :class="$style.subcategoryHeader" v-if="activeSubcategory" data-test-id="categorized-items-subcategory">
<div
:class="$style.subcategoryHeader"
v-if="activeSubcategory"
data-test-id="categorized-items-subcategory"
>
<button :class="$style.subcategoryBackButton" @click="onSubcategoryClose">
<font-awesome-icon :class="$style.subcategoryBackIcon" icon="arrow-left" size="2x" />
</button>
<node-icon
v-if="(showSubcategoryIcon && activeSubcategory.properties.nodeType)"
v-if="showSubcategoryIcon && activeSubcategory.properties.nodeType"
:class="$style.nodeIcon"
:nodeType="activeSubcategory.properties.nodeType"
:size="16"
@@ -30,16 +34,24 @@
v-if="alwaysShowSearch || isSearchVisible"
:key="nodeCreatorStore.selectedType"
:value="nodeCreatorStore.itemsFilter"
:placeholder="searchPlaceholder ? searchPlaceholder : $locale.baseText('nodeCreator.searchBar.searchNodes')"
:placeholder="
searchPlaceholder
? searchPlaceholder
: $locale.baseText('nodeCreator.searchBar.searchNodes')
"
ref="searchBar"
@input="onNodeFilterChange"
/>
<template v-if="(searchFilter.length > 0 && filteredNodeTypes.length === 0)">
<template v-if="searchFilter.length > 0 && filteredNodeTypes.length === 0">
<no-results
data-test-id="categorized-no-results"
:showRequest="(!$slots.noResultsTitle && !$slots.noResultsAction) && filteredAllNodeTypes.length === 0"
:show-icon="(!$slots.noResultsTitle && !$slots.noResultsAction) && filteredAllNodeTypes.length === 0"
:showRequest="
!$slots.noResultsTitle && !$slots.noResultsAction && filteredAllNodeTypes.length === 0
"
:show-icon="
!$slots.noResultsTitle && !$slots.noResultsAction && filteredAllNodeTypes.length === 0
"
>
<template v-if="$slots.noResultsTitle" #title>
<slot name="noResultsTitle" />
@@ -49,7 +61,7 @@
<p v-html="$locale.baseText('nodeCreator.noResults.clickToSeeResults')" />
</template>
<template v-else #title>
<p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')"/>
<p v-text="$locale.baseText('nodeCreator.noResults.weDidntMakeThatYet')" />
</template>
<template v-if="$slots.noResultsAction" #action>
@@ -57,14 +69,20 @@
</template>
<template v-else-if="filteredAllNodeTypes.length === 0" #action>
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
<n8n-link @click="selectHttpRequest" v-if="[REGULAR_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)">
<n8n-link
@click="selectHttpRequest"
v-if="[REGULAR_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)"
>
{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}
</n8n-link>
<template v-if="nodeCreatorStore.selectedType === ALL_NODE_FILTER">
{{ $locale.baseText('nodeCreator.noResults.or') }}
</template>
<n8n-link @click="selectWebhook" v-if="[TRIGGER_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)">
<n8n-link
@click="selectWebhook"
v-if="[TRIGGER_NODE_FILTER, ALL_NODE_FILTER].includes(nodeCreatorStore.selectedType)"
>
{{ $locale.baseText('nodeCreator.noResults.webhook') }}
</n8n-link>
{{ $locale.baseText('nodeCreator.noResults.node') }}
@@ -81,7 +99,7 @@
</template>
<div :class="$style.scrollable" ref="scrollableContainer" v-else>
<item-iterator
:elements="searchFilter.length === 0 ? renderedItems :filteredNodeTypes"
:elements="searchFilter.length === 0 ? renderedItems : filteredNodeTypes"
:activeIndex="activeSubcategory ? activeSubcategoryIndex : activeIndex"
:with-actions-getter="withActionsGetter"
:lazyRender="lazyRender"
@@ -90,7 +108,6 @@
@actionsOpen="$listeners.actionsOpen"
@nodeTypeSelected="$listeners.nodeTypeSelected"
>
</item-iterator>
<div :class="$style.footer" v-if="$slots.footer">
<slot name="footer" />
@@ -101,7 +118,17 @@
</template>
<script lang="ts" setup>
import { computed, reactive, onMounted, watch, getCurrentInstance, toRefs, ref, onUnmounted, nextTick } from 'vue';
import {
computed,
reactive,
onMounted,
watch,
getCurrentInstance,
toRefs,
ref,
onUnmounted,
nextTick,
} from 'vue';
import camelcase from 'lodash.camelcase';
import { externalHooks } from '@/mixins/externalHooks';
@@ -111,10 +138,24 @@ import ItemIterator from './ItemIterator.vue';
import NoResults from './NoResults.vue';
import SearchBar from './SearchBar.vue';
import NodeIcon from '@/components/NodeIcon.vue';
import { INodeCreateElement, ISubcategoryItemProps, ICategoryItemProps, ICategoriesWithNodes, SubcategoryCreateElement, NodeCreateElement } from '@/Interface';
import { WEBHOOK_NODE_TYPE, HTTP_REQUEST_NODE_TYPE, ALL_NODE_FILTER, TRIGGER_NODE_FILTER, REGULAR_NODE_FILTER, NODE_TYPE_COUNT_MAPPER } from '@/constants';
import {
INodeCreateElement,
ISubcategoryItemProps,
ICategoryItemProps,
ICategoriesWithNodes,
SubcategoryCreateElement,
NodeCreateElement,
} from '@/Interface';
import {
WEBHOOK_NODE_TYPE,
HTTP_REQUEST_NODE_TYPE,
ALL_NODE_FILTER,
TRIGGER_NODE_FILTER,
REGULAR_NODE_FILTER,
NODE_TYPE_COUNT_MAPPER,
} from '@/constants';
import { BaseTextKey } from '@/plugins/i18n';
import { sublimeSearch, matchesNodeType, matchesSelectType } from '@/utils';
import { sublimeSearch, matchesNodeType, matchesSelectType } from '@/utils';
import { useWorkflowsStore } from '@/stores/workflows';
import { useRootStore } from '@/stores/n8nRootStore';
import { useNodeCreatorStore } from '@/stores/nodeCreator';
@@ -149,11 +190,11 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
(event: 'subcategoryClose', value: INodeCreateElement[]): void,
(event: 'onSubcategorySelected', value: INodeCreateElement): void,
(event: 'nodeTypeSelected', value: string[]): void,
(event: 'actionSelected', value: INodeCreateElement): void,
(event: 'actionsOpen', value: INodeTypeDescription): void,
(event: 'subcategoryClose', value: INodeCreateElement[]): void;
(event: 'onSubcategorySelected', value: INodeCreateElement): void;
(event: 'nodeTypeSelected', value: string[]): void;
(event: 'actionSelected', value: INodeCreateElement): void;
(event: 'actionsOpen', value: INodeTypeDescription): void;
}>();
const instance = getCurrentInstance();
@@ -168,7 +209,12 @@ const nodeCreatorStore = useNodeCreatorStore();
const state = reactive({
activeCategory: props.initialActiveCategories,
// Keep track of activated subcategories so we could traverse back more than one level
activeSubcategoryHistory: [] as Array<{scrollPosition: number, subcategory: INodeCreateElement, activeIndex: number, filter: string}>,
activeSubcategoryHistory: [] as Array<{
scrollPosition: number;
subcategory: INodeCreateElement;
activeIndex: number;
filter: string;
}>,
activeIndex: props.initialActiveIndex || 0,
activeSubcategoryIndex: 0,
ALL_NODE_FILTER,
@@ -180,151 +226,159 @@ const state = reactive({
const searchBar = ref<InstanceType<typeof SearchBar>>();
const scrollableContainer = ref<InstanceType<typeof HTMLElement>>();
const activeSubcategory = computed<INodeCreateElement | null> (
() => state.activeSubcategoryHistory[state.activeSubcategoryHistory.length - 1]?.subcategory || null,
const activeSubcategory = computed<INodeCreateElement | null>(
() =>
state.activeSubcategoryHistory[state.activeSubcategoryHistory.length - 1]?.subcategory || null,
);
const activeSubcategoryTitle = computed<string> (() => {
if(!activeSubcategory.value || !activeSubcategory.value.properties) return '';
const activeSubcategoryTitle = computed<string>(() => {
if (!activeSubcategory.value || !activeSubcategory.value.properties) return '';
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
const subcategoryName = camelcase(subcategory);
const subcategoryName = camelcase(subcategory);
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
const titleLocaleKey = `nodeCreator.subcategoryTitles.${subcategoryName}` as BaseTextKey;
const nameLocaleKey = `nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey;
const titleLocale = instance?.proxy?.$locale.baseText(titleLocaleKey) as string;
const nameLocale = instance?.proxy?.$locale.baseText(nameLocaleKey) as string;
// If resolved title locale is same as the locale key it means it doesn't exist
// so we fallback to the subcategoryName
if(titleLocale === titleLocaleKey) return nameLocale === nameLocaleKey ? subcategory : nameLocale;
const titleLocale = instance?.proxy?.$locale.baseText(titleLocaleKey) as string;
const nameLocale = instance?.proxy?.$locale.baseText(nameLocaleKey) as string;
// If resolved title locale is same as the locale key it means it doesn't exist
// so we fallback to the subcategoryName
if (titleLocale === titleLocaleKey)
return nameLocale === nameLocaleKey ? subcategory : nameLocale;
return titleLocale;
return titleLocale;
});
const searchFilter = computed<string> (() => nodeCreatorStore.itemsFilter.toLowerCase().trim());
const searchFilter = computed<string>(() => nodeCreatorStore.itemsFilter.toLowerCase().trim());
const matchedTypeNodes = computed<INodeCreateElement[]> (() => {
if(!props.filterByType) return props.searchItems;
return props.searchItems.filter((el: INodeCreateElement) => matchesSelectType(el, nodeCreatorStore.selectedType));
const matchedTypeNodes = computed<INodeCreateElement[]>(() => {
if (!props.filterByType) return props.searchItems;
return props.searchItems.filter((el: INodeCreateElement) =>
matchesSelectType(el, nodeCreatorStore.selectedType),
);
});
const filteredNodeTypes = computed<INodeCreateElement[]> (() => {
const filter = searchFilter.value;
const filteredNodeTypes = computed<INodeCreateElement[]>(() => {
const filter = searchFilter.value;
let returnItems: INodeCreateElement[] = [];
if (defaultLocale !== 'en') {
returnItems = props.searchItems.filter((el: INodeCreateElement) => {
return filter && matchesSelectType(el, nodeCreatorStore.selectedType) && matchesNodeType(el, filter);
});
}
else {
const matchingNodes = props.filterByType
let returnItems: INodeCreateElement[] = [];
if (defaultLocale !== 'en') {
returnItems = props.searchItems.filter((el: INodeCreateElement) => {
return (
filter &&
matchesSelectType(el, nodeCreatorStore.selectedType) &&
matchesNodeType(el, filter)
);
});
} else {
const matchingNodes = props.filterByType
? props.searchItems.filter((el) => matchesSelectType(el, nodeCreatorStore.selectedType))
: props.searchItems;
const matchedCategorizedNodes = sublimeSearch<INodeCreateElement>(filter, matchingNodes, [
{key: 'properties.nodeType.displayName', weight: 2},
{key: 'properties.nodeType.codex.alias', weight: 1},
const matchedCategorizedNodes = sublimeSearch<INodeCreateElement>(filter, matchingNodes, [
{ key: 'properties.nodeType.displayName', weight: 2 },
{ key: 'properties.nodeType.codex.alias', weight: 1 },
]);
returnItems = matchedCategorizedNodes.map(({item}) => item);
}
returnItems = matchedCategorizedNodes.map(({ item }) => item);
}
return returnItems;
return returnItems;
});
const filteredAllNodeTypes = computed<INodeCreateElement[]> (() => {
if(filteredNodeTypes.value.length > 0) return [];
const filteredAllNodeTypes = computed<INodeCreateElement[]>(() => {
if (filteredNodeTypes.value.length > 0) return [];
const matchedAllNodex = props.allItems.filter((el: INodeCreateElement) => {
return searchFilter.value && el.type === 'node' && matchesNodeType(el, searchFilter.value);
});
const matchedAllNodex = props.allItems.filter((el: INodeCreateElement) => {
return searchFilter.value && el.type === 'node' && matchesNodeType(el, searchFilter.value);
});
return matchedAllNodex;
return matchedAllNodex;
});
const categorized = computed<INodeCreateElement[]> (() => {
return props.categorizedItems
.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
if(
el.type === 'subcategory' &&
(props.excludedSubcategories || []).includes((el.properties as ISubcategoryItemProps).subcategory)
) {
return accu;
}
const categorized = computed<INodeCreateElement[]>(() => {
return props.categorizedItems.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
if (
el.type === 'subcategory' &&
(props.excludedSubcategories || []).includes(
(el.properties as ISubcategoryItemProps).subcategory,
)
) {
return accu;
}
if (
el.type !== 'category' &&
!state.activeCategory.includes(el.category)
) {
return accu;
}
if (el.type !== 'category' && !state.activeCategory.includes(el.category)) {
return accu;
}
if (!matchesSelectType(el, nodeCreatorStore.selectedType)) {
return accu;
}
if (!matchesSelectType(el, nodeCreatorStore.selectedType)) {
return accu;
}
if (el.type === 'category') {
accu.push({
...el,
properties: {
expanded: state.activeCategory.includes(el.category),
},
} as INodeCreateElement);
return accu;
}
if (el.type === 'category') {
accu.push({
...el,
properties: {
expanded: state.activeCategory.includes(el.category),
},
} as INodeCreateElement);
return accu;
}
accu.push(el);
return accu;
}, []);
accu.push(el);
return accu;
}, []);
});
const subcategorizedItems = computed<INodeCreateElement[]> (() => {
if(!activeSubcategory.value) return [];
const subcategorizedItems = computed<INodeCreateElement[]>(() => {
if (!activeSubcategory.value) return [];
const category = activeSubcategory.value.category;
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
const category = activeSubcategory.value.category;
const subcategory = (activeSubcategory.value.properties as ISubcategoryItemProps).subcategory;
// If no category is set, we use all categorized nodes
const nodes = category
? props.categoriesWithNodes[category][subcategory].nodes
: categorized.value;
// If no category is set, we use all categorized nodes
const nodes = category
? props.categoriesWithNodes[category][subcategory].nodes
: categorized.value;
return nodes.filter((el: INodeCreateElement) => matchesSelectType(el, nodeCreatorStore.selectedType));
return nodes.filter((el: INodeCreateElement) =>
matchesSelectType(el, nodeCreatorStore.selectedType),
);
});
const renderedItems = computed<INodeCreateElement[]> (() => {
if(props.firstLevelItems.length > 0 && activeSubcategory.value === null) return props.firstLevelItems;
if(props.flatten) return matchedTypeNodes.value;
if(subcategorizedItems.value.length === 0) return categorized.value;
const renderedItems = computed<INodeCreateElement[]>(() => {
if (props.firstLevelItems.length > 0 && activeSubcategory.value === null)
return props.firstLevelItems;
if (props.flatten) return matchedTypeNodes.value;
if (subcategorizedItems.value.length === 0) return categorized.value;
const isSingleCategory = subcategorizedItems.value.filter((item) => item.type === 'category').length === 1;
return isSingleCategory ? subcategorizedItems.value.slice(1) : subcategorizedItems.value;
const isSingleCategory =
subcategorizedItems.value.filter((item) => item.type === 'category').length === 1;
return isSingleCategory ? subcategorizedItems.value.slice(1) : subcategorizedItems.value;
});
const isSearchVisible = computed<boolean> (() => {
if(subcategorizedItems.value.length === 0) return true;
const isSearchVisible = computed<boolean>(() => {
if (subcategorizedItems.value.length === 0) return true;
let totalItems = 0;
for (const item of subcategorizedItems.value) {
// Category contains many nodes so we need to count all of them
// for the current selectedType
if(item.type === 'category') {
const categoryItems = props.categoriesWithNodes[item.key];
const categoryItemsCount = Object.values(categoryItems)?.[0];
const countKeys = NODE_TYPE_COUNT_MAPPER[nodeCreatorStore.selectedType];
let totalItems = 0;
for (const item of subcategorizedItems.value) {
// Category contains many nodes so we need to count all of them
// for the current selectedType
if (item.type === 'category') {
const categoryItems = props.categoriesWithNodes[item.key];
const categoryItemsCount = Object.values(categoryItems)?.[0];
const countKeys = NODE_TYPE_COUNT_MAPPER[nodeCreatorStore.selectedType];
for (const countKey of countKeys) {
totalItems += categoryItemsCount[(countKey as "triggerCount" | "regularCount")];
}
for (const countKey of countKeys) {
totalItems += categoryItemsCount[countKey as 'triggerCount' | 'regularCount'];
}
continue;
}
// If it's not category, it must be just a node item so we count it as 1
totalItems += 1;
}
return totalItems > 9;
continue;
}
// If it's not category, it must be just a node item so we count it as 1
totalItems += 1;
}
return totalItems > 9;
});
// Methods
@@ -332,107 +386,116 @@ function getScrollTop() {
return scrollableContainer.value?.scrollTop || 0;
}
function setScrollTop(scrollTop: number) {
if(scrollableContainer.value) {
if (scrollableContainer.value) {
scrollableContainer.value.scrollTop = scrollTop;
}
}
function switchToAllTabAndFilter() {
const currentFilter = nodeCreatorStore.itemsFilter;
nodeCreatorStore.setShowTabs(true);
nodeCreatorStore.setSelectedType(ALL_NODE_FILTER);
state.activeSubcategoryHistory = [];
nodeCreatorStore.setShowTabs(true);
nodeCreatorStore.setSelectedType(ALL_NODE_FILTER);
state.activeSubcategoryHistory = [];
nextTick(() => onNodeFilterChange(currentFilter));
nextTick(() => onNodeFilterChange(currentFilter));
}
function onNodeFilterChange(filter: string) {
nodeCreatorStore.setFilter(filter);
nodeCreatorStore.setFilter(filter);
}
function selectWebhook() {
emit('nodeTypeSelected', [WEBHOOK_NODE_TYPE]);
emit('nodeTypeSelected', [WEBHOOK_NODE_TYPE]);
}
function selectHttpRequest() {
emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]);
emit('nodeTypeSelected', [HTTP_REQUEST_NODE_TYPE]);
}
function nodeFilterKeyDown(e: KeyboardEvent) {
// We only want to propagate 'Escape' as it closes the node-creator and
// 'Tab' which toggles it
if (!['Escape', 'Tab'].includes(e.key)) e.stopPropagation();
// We only want to propagate 'Escape' as it closes the node-creator and
// 'Tab' which toggles it
if (!['Escape', 'Tab'].includes(e.key)) e.stopPropagation();
// Prevent cursors position change
if(['ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault();
// Prevent cursors position change
if (['ArrowUp', 'ArrowDown'].includes(e.key)) e.preventDefault();
if (activeSubcategory.value) {
const activeList = searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value;;
if (activeSubcategory.value) {
const activeList =
searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value;
const activeNodeType = activeList[state.activeSubcategoryIndex];
if (e.key === 'ArrowDown' && activeSubcategory.value) {
state.activeSubcategoryIndex++;
state.activeSubcategoryIndex = Math.min(
state.activeSubcategoryIndex,
activeList.length - 1,
);
}
else if (e.key === 'ArrowUp' && activeSubcategory.value) {
state.activeSubcategoryIndex--;
state.activeSubcategoryIndex = Math.max(state.activeSubcategoryIndex, 0);
}
else if (e.key === 'Enter') {
selected(activeNodeType);
} else if (e.key === 'ArrowLeft' && activeNodeType?.type === 'category' && (activeNodeType.properties as ICategoryItemProps).expanded) {
selected(activeNodeType);
} else if (e.key === 'ArrowLeft') {
onSubcategoryClose();
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'category' && !(activeNodeType.properties as ICategoryItemProps).expanded) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && (['node','action'].includes(activeNodeType?.type))) {
if (e.key === 'ArrowDown' && activeSubcategory.value) {
state.activeSubcategoryIndex++;
state.activeSubcategoryIndex = Math.min(state.activeSubcategoryIndex, activeList.length - 1);
} else if (e.key === 'ArrowUp' && activeSubcategory.value) {
state.activeSubcategoryIndex--;
state.activeSubcategoryIndex = Math.max(state.activeSubcategoryIndex, 0);
} else if (e.key === 'Enter') {
selected(activeNodeType);
} else if (
e.key === 'ArrowLeft' &&
activeNodeType?.type === 'category' &&
(activeNodeType.properties as ICategoryItemProps).expanded
) {
selected(activeNodeType);
} else if (e.key === 'ArrowLeft') {
onSubcategoryClose();
} else if (
e.key === 'ArrowRight' &&
activeNodeType?.type === 'category' &&
!(activeNodeType.properties as ICategoryItemProps).expanded
) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) {
selected(activeNodeType);
}
return;
}
return;
}
const activeList = searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value;
const activeNodeType = activeList[state.activeIndex];
const activeList = searchFilter.value.length > 0 ? filteredNodeTypes.value : renderedItems.value;
const activeNodeType = activeList[state.activeIndex];
if (e.key === 'ArrowDown') {
state.activeIndex++;
// Make sure that we stop at the last nodeType
state.activeIndex = Math.min(
state.activeIndex,
activeList.length - 1,
);
} else if (e.key === 'ArrowUp') {
state.activeIndex--;
// Make sure that we do not get before the first nodeType
state.activeIndex = Math.max(state.activeIndex, 0);
} else if (e.key === 'Enter' && activeNodeType) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'category' && !(activeNodeType.properties as ICategoryItemProps).expanded) {
selected(activeNodeType);
} else if (e.key === 'ArrowLeft' && activeNodeType?.type === 'category' && (activeNodeType.properties as ICategoryItemProps).expanded) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && (['node','action'].includes(activeNodeType?.type))) {
if (e.key === 'ArrowDown') {
state.activeIndex++;
// Make sure that we stop at the last nodeType
state.activeIndex = Math.min(state.activeIndex, activeList.length - 1);
} else if (e.key === 'ArrowUp') {
state.activeIndex--;
// Make sure that we do not get before the first nodeType
state.activeIndex = Math.max(state.activeIndex, 0);
} else if (e.key === 'Enter' && activeNodeType) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && activeNodeType?.type === 'subcategory') {
selected(activeNodeType);
} else if (
e.key === 'ArrowRight' &&
activeNodeType?.type === 'category' &&
!(activeNodeType.properties as ICategoryItemProps).expanded
) {
selected(activeNodeType);
} else if (
e.key === 'ArrowLeft' &&
activeNodeType?.type === 'category' &&
(activeNodeType.properties as ICategoryItemProps).expanded
) {
selected(activeNodeType);
} else if (e.key === 'ArrowRight' && ['node', 'action'].includes(activeNodeType?.type)) {
selected(activeNodeType);
}
}
function selected(element: INodeCreateElement) {
const typeHandler = {
category: () => onCategorySelected(element.category),
subcategory: () => onSubcategorySelected(element),
const typeHandler = {
category: () => onCategorySelected(element.category),
subcategory: () => onSubcategorySelected(element),
node: () => onNodeSelected(element as NodeCreateElement),
action: () => onActionSelected(element),
};
};
typeHandler[element.type]();
typeHandler[element.type]();
}
function onNodeSelected(element: NodeCreateElement) {
const hasActions = (element.properties.nodeType?.actions?.length || 0) > 0;
if(props.withActionsGetter && props.withActionsGetter(element) === true && hasActions) {
if (props.withActionsGetter && props.withActionsGetter(element) === true && hasActions) {
emit('actionsOpen', element.properties.nodeType);
return;
}
@@ -440,18 +503,19 @@ function onNodeSelected(element: NodeCreateElement) {
}
function onCategorySelected(category: string) {
if (state.activeCategory.includes(category)) {
state.activeCategory = state.activeCategory.filter(
(active: string) => active !== category,
);
} else {
state.activeCategory = [...state.activeCategory, category];
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { category_name: category, workflow_id: workflowId });
}
if (state.activeCategory.includes(category)) {
state.activeCategory = state.activeCategory.filter((active: string) => active !== category);
} else {
state.activeCategory = [...state.activeCategory, category];
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
category_name: category,
workflow_id: workflowId,
});
}
state.activeIndex = categorized.value.findIndex(
(el: INodeCreateElement) => el.category === category,
);
state.activeIndex = categorized.value.findIndex(
(el: INodeCreateElement) => el.category === category,
);
}
function onActionSelected(element: INodeCreateElement) {
emit('actionSelected', element);
@@ -461,19 +525,22 @@ function onSubcategorySelected(selected: INodeCreateElement, track = true) {
state.transitionDirection = 'in';
// Store the current subcategory UI details in the state
// so we could revert it when the user closes the subcategory
state.activeSubcategoryHistory.push({
state.activeSubcategoryHistory.push({
subcategory: selected,
activeIndex: state.activeSubcategoryIndex,
scrollPosition: getScrollTop(),
filter: nodeCreatorStore.itemsFilter,
});
nodeCreatorStore.setFilter('');
emit('onSubcategorySelected', selected);
nodeCreatorStore.setShowTabs(false);
state.activeSubcategoryIndex = 0;
emit('onSubcategorySelected', selected);
nodeCreatorStore.setShowTabs(false);
state.activeSubcategoryIndex = 0;
if(track) {
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { selected, workflow_id: workflowId });
if (track) {
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', {
selected,
workflow_id: workflowId,
});
}
}
@@ -482,23 +549,32 @@ async function onSubcategoryClose() {
const poppedSubCategory = state.activeSubcategoryHistory.pop();
onNodeFilterChange(poppedSubCategory?.filter || '');
await nextTick();
emit('subcategoryClose', state.activeSubcategoryHistory.map((el) => el.subcategory));
emit(
'subcategoryClose',
state.activeSubcategoryHistory.map((el) => el.subcategory),
);
await nextTick();
setScrollTop(poppedSubCategory?.scrollPosition || 0);
state.activeSubcategoryIndex = poppedSubCategory?.activeIndex || 0;
if(!nodeCreatorStore.showScrim && state.activeSubcategoryHistory.length === 0) {
if (!nodeCreatorStore.showScrim && state.activeSubcategoryHistory.length === 0) {
nodeCreatorStore.setShowTabs(true);
}
}
watch(() => props.expandAllCategories, (expandAll) => {
if (expandAll) state.activeCategory = Object.keys(props.categoriesWithNodes);
});
watch(
() => props.expandAllCategories,
(expandAll) => {
if (expandAll) state.activeCategory = Object.keys(props.categoriesWithNodes);
},
);
watch(() => props.subcategoryOverride, (subcategory) => {
if (subcategory) onSubcategorySelected(subcategory, false);
});
watch(
() => props.subcategoryOverride,
(subcategory) => {
if (subcategory) onSubcategorySelected(subcategory, false);
},
);
onMounted(() => {
registerCustomAction('showAllNodeCreatorNodes', switchToAllTabAndFilter);
@@ -518,30 +594,33 @@ watch(filteredNodeTypes, (returnItems) => {
});
watch(isSearchVisible, (isVisible) => {
if(isVisible === false) {
if (isVisible === false) {
// Focus the root container when search is hidden to make sure
// keyboard navigation still works
nextTick(() => state.mainPanelContainer?.focus());
}
});
watch(() => nodeCreatorStore.itemsFilter, (newValue, oldValue) => {
// Reset the index whenver the filter-value changes
state.activeIndex = 0;
state.activeSubcategoryIndex = 0;
$externalHooks().run('nodeCreateList.nodeFilterChanged', {
oldValue,
newValue,
selectedType: nodeCreatorStore.selectedType,
filteredNodes: filteredNodeTypes.value,
});
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
oldValue,
newValue,
selectedType: nodeCreatorStore.selectedType,
filteredNodes: filteredNodeTypes.value,
workflow_id: workflowId,
});
});
watch(
() => nodeCreatorStore.itemsFilter,
(newValue, oldValue) => {
// Reset the index whenver the filter-value changes
state.activeIndex = 0;
state.activeSubcategoryIndex = 0;
$externalHooks().run('nodeCreateList.nodeFilterChanged', {
oldValue,
newValue,
selectedType: nodeCreatorStore.selectedType,
filteredNodes: filteredNodeTypes.value,
});
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
oldValue,
newValue,
selectedType: nodeCreatorStore.selectedType,
filteredNodes: filteredNodeTypes.value,
workflow_id: workflowId,
});
},
);
const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state);
</script>
@@ -557,7 +636,6 @@ const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state
right: 0;
}
:global(.panel-slide-out-enter),
:global(.panel-slide-in-leave-to) {
transform: translateX(0);
@@ -593,7 +671,7 @@ const { activeSubcategoryIndex, activeIndex, mainPanelContainer } = toRefs(state
margin: 0 var(--spacing-xs) 0;
padding: var(--spacing-4xs) 0;
line-height: var(--font-line-height-regular);
border-top: 1px solid #DBDFE7;
border-top: 1px solid #dbdfe7;
z-index: 1;
margin-top: -1px;
}

View File

@@ -1,13 +1,9 @@
<template>
<div :class="$style.category">
<span :class="$style.name">
{{ renderCategoryName(item.category) }}{{ count !== undefined ? ` (${count})` : ''}}
{{ renderCategoryName(item.category) }}{{ count !== undefined ? ` (${count})` : '' }}
</span>
<font-awesome-icon
v-if="isExpanded"
icon="chevron-down"
:class="$style.arrow"
/>
<font-awesome-icon v-if="isExpanded" icon="chevron-down" :class="$style.arrow" />
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
</div>
</template>
@@ -25,15 +21,13 @@ export interface Props {
const props = defineProps<Props>();
const instance = getCurrentInstance();
const isExpanded = computed<boolean>(() =>(props.item.properties as ICategoryItemProps).expanded);
const isExpanded = computed<boolean>(() => (props.item.properties as ICategoryItemProps).expanded);
function renderCategoryName(categoryName: string) {
const camelCasedCategoryName = camelcase(categoryName) as CategoryName;
const key = `nodeCreator.categoryNames.${camelCasedCategoryName}` as const;
return instance?.proxy.$locale.exists(key)
? instance?.proxy.$locale.baseText(key)
: categoryName;
return instance?.proxy.$locale.exists(key) ? instance?.proxy.$locale.baseText(key) : categoryName;
}
</script>

View File

@@ -12,10 +12,10 @@
:key="`${item.key}-${index}`"
data-test-id="item-iterator-item"
:class="{
'clickable': !disabled,
clickable: !disabled,
[$style[item.type]]: true,
[$style.active]: activeIndex === index && !disabled,
[$style.iteratorItem]: true
[$style.iteratorItem]: true,
}"
ref="iteratorItems"
@click="wrappedEmit('selected', item)"
@@ -26,10 +26,7 @@
:count="enableGlobalCategoriesCounter ? getCategoryCount(item) : undefined"
/>
<subcategory-item
v-else-if="item.type === 'subcategory'"
:item="item"
/>
<subcategory-item v-else-if="item.type === 'subcategory'" :item="item" />
<node-item
v-else-if="item.type === 'node'"
@@ -51,7 +48,7 @@
</div>
<aside
v-for="item in elements.length"
v-show="(renderedItems.length < item)"
v-show="renderedItems.length < item"
:key="item"
:class="$style.loadingItem"
>
@@ -85,9 +82,9 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
(event: 'selected', element: INodeCreateElement, $e?: Event): void,
(event: 'dragstart', element: INodeCreateElement, $e: Event): void,
(event: 'dragend', element: INodeCreateElement, $e: Event): void,
(event: 'selected', element: INodeCreateElement, $e?: Event): void;
(event: 'dragstart', element: INodeCreateElement, $e: Event): void;
(event: 'dragend', element: INodeCreateElement, $e: Event): void;
}>();
const state = reactive({
@@ -96,10 +93,14 @@ const state = reactive({
});
const iteratorItems = ref<HTMLElement[]>([]);
function wrappedEmit(event: 'selected' | 'dragstart' | 'dragend', element: INodeCreateElement, $e?: Event) {
function wrappedEmit(
event: 'selected' | 'dragstart' | 'dragend',
element: INodeCreateElement,
$e?: Event,
) {
if (props.disabled) return;
emit((event as 'selected' || 'dragstart' || 'dragend'), element, $e);
emit((event as 'selected') || 'dragstart' || 'dragend', element, $e);
}
function getCategoryCount(item: CategoryCreateElement) {
const { categoriesWithNodes } = useNodeTypesStore();
@@ -113,7 +114,7 @@ function getCategoryCount(item: CategoryCreateElement) {
const countKeys = NODE_TYPE_COUNT_MAPPER[useNodeCreatorStore().selectedType];
for (const countKey of countKeys) {
accu += currentCategory[subcategory][(countKey as "triggerCount" | "regularCount")];
accu += currentCategory[subcategory][countKey as 'triggerCount' | 'regularCount'];
}
return accu;
@@ -124,13 +125,15 @@ function getCategoryCount(item: CategoryCreateElement) {
// Lazy render large items lists to prevent the browser from freezing
// when loading many items.
function renderItems() {
if(props.elements.length <= 20 || props.lazyRender === false) {
if (props.elements.length <= 20 || props.lazyRender === false) {
state.renderedItems = props.elements;
return;
};
}
if (state.renderedItems.length < props.elements.length) {
state.renderedItems.push(...props.elements.slice(state.renderedItems.length, state.renderedItems.length + 10));
state.renderedItems.push(
...props.elements.slice(state.renderedItems.length, state.renderedItems.length + 10),
);
state.renderAnimationRequest = window.requestAnimationFrame(renderItems);
}
}
@@ -139,7 +142,6 @@ function beforeEnter(el: HTMLElement) {
el.style.height = '0';
}
function enter(el: HTMLElement) {
el.style.height = `${el.scrollHeight}px`;
}
@@ -163,17 +165,23 @@ onUnmounted(() => {
// Make sure the active item is always visible
// scroll if needed
watch(() => props.activeIndex, async () => {
if(props.activeIndex === undefined) return;
iteratorItems.value[props.activeIndex]?.scrollIntoView({ block: 'nearest' });
});
watch(
() => props.activeIndex,
async () => {
if (props.activeIndex === undefined) return;
iteratorItems.value[props.activeIndex]?.scrollIntoView({ block: 'nearest' });
},
);
// Trigger elements re-render when they change
watch(() => props.elements, async () => {
window.cancelAnimationFrame(state.renderAnimationRequest);
state.renderedItems = [];
renderItems();
});
watch(
() => props.elements,
async () => {
window.cancelAnimationFrame(state.renderAnimationRequest);
state.renderedItems = [];
renderItems();
},
);
const { renderedItems } = toRefs(state);
</script>
@@ -188,7 +196,7 @@ const { renderedItems } = toRefs(state);
margin-left: 1px;
position: relative;
&::before {
content: "";
content: '';
position: absolute;
left: -1px;
top: 0;
@@ -199,14 +207,13 @@ const { renderedItems } = toRefs(state);
border-color: $node-creator-item-hover-border-color;
}
&.active::before {
&.active::before {
border-color: $color-primary !important;
}
&.category.singleCategory {
display: none;
}
}
.itemIterator {
> *:last-child {

View File

@@ -1,15 +1,12 @@
<template>
<div
class="container"
ref="mainPanelContainer"
>
<div class="container" ref="mainPanelContainer">
<div class="main-panel">
<trigger-helper-panel
v-if="nodeCreatorStore.selectedType === TRIGGER_NODE_FILTER"
@nodeTypeSelected="$listeners.nodeTypeSelected"
>
<template #header>
<type-selector/>
<type-selector />
</template>
</trigger-helper-panel>
<categorized-items
@@ -36,7 +33,12 @@
import { watch, getCurrentInstance, onMounted, onUnmounted } from 'vue';
import { externalHooks } from '@/mixins/externalHooks';
import TriggerHelperPanel from './TriggerHelperPanel.vue';
import { ALL_NODE_FILTER, TRIGGER_NODE_FILTER, OTHER_TRIGGER_NODES_SUBCATEGORY, CORE_NODES_CATEGORY } from '@/constants';
import {
ALL_NODE_FILTER,
TRIGGER_NODE_FILTER,
OTHER_TRIGGER_NODES_SUBCATEGORY,
CORE_NODES_CATEGORY,
} from '@/constants';
import CategorizedItems from './CategorizedItems.vue';
import TypeSelector from './TypeSelector.vue';
import { INodeCreateElement } from '@/Interface';
@@ -58,17 +60,20 @@ const { workflowId } = useWorkflowsStore();
const nodeCreatorStore = useNodeCreatorStore();
const { categorizedItems, categoriesWithNodes } = useNodeTypesStore();
watch(() => nodeCreatorStore.selectedType, (newValue, oldValue) => {
$externalHooks().run('nodeCreateList.selectedTypeChanged', {
oldValue,
newValue,
});
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.selectedTypeChanged', {
old_filter: oldValue,
new_filter: newValue,
workflow_id: workflowId,
});
});
watch(
() => nodeCreatorStore.selectedType,
(newValue, oldValue) => {
$externalHooks().run('nodeCreateList.selectedTypeChanged', {
oldValue,
newValue,
});
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.selectedTypeChanged', {
old_filter: oldValue,
new_filter: newValue,
workflow_id: workflowId,
});
},
);
onMounted(() => {
$externalHooks().run('nodeCreateList.mounted');
@@ -79,9 +84,10 @@ onMounted(() => {
onUnmounted(() => {
nodeCreatorStore.setSelectedType(ALL_NODE_FILTER);
$externalHooks().run('nodeCreateList.destroyed');
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.destroyed', { workflow_id: workflowId });
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.destroyed', {
workflow_id: workflowId,
});
});
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{[$style.noResults]: true, [$style.iconless]: !showIcon}">
<div :class="{ [$style.noResults]: true, [$style.iconless]: !showIcon }">
<div :class="$style.icon" v-if="showIcon">
<no-results-icon />
</div>
@@ -14,7 +14,8 @@
<p v-text="$locale.baseText('nodeCreator.noResults.wantUsToMakeItFaster')" />
<div>
<n8n-link :to="REQUEST_NODE_FORM_URL">
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span>&nbsp;
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span
>&nbsp;
<span>
<font-awesome-icon
:class="$style.external"
@@ -28,7 +29,6 @@
</div>
</template>
<script setup lang="ts">
import { REQUEST_NODE_FORM_URL } from '@/constants';
import NoResultsIcon from './NoResultsIcon.vue';
@@ -65,7 +65,8 @@ defineProps<Props>();
}
}
.action p, .request p {
.action p,
.request p {
font-size: var(--font-size-s);
line-height: var(--font-line-height-xloose);
}
@@ -83,11 +84,10 @@ defineProps<Props>();
.icon {
margin-top: var(--spacing-2xl);
min-height: 67px;
opacity: .6;
opacity: 0.6;
}
.external {
font-size: var(--font-size-2xs);
}
</style>

View File

@@ -1,22 +1,47 @@
<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
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>
</template>

View File

@@ -1,30 +1,26 @@
<template>
<div>
<aside :class="{'node-creator-scrim': true, active: nodeCreatorStore.showScrim}" />
<aside :class="{ 'node-creator-scrim': true, active: nodeCreatorStore.showScrim }" />
<slide-transition>
<div
v-if="active"
class="node-creator"
ref="nodeCreator"
v-click-outside="onClickOutside"
@dragover="onDragOver"
@drop="onDrop"
v-click-outside="onClickOutside"
@dragover="onDragOver"
@drop="onDrop"
@mousedown="onMouseDown"
@mouseup="onMouseUp"
data-test-id="node-creator"
>
<main-panel
@nodeTypeSelected="$listeners.nodeTypeSelected"
:searchItems="searchItems"
/>
<main-panel @nodeTypeSelected="$listeners.nodeTypeSelected" :searchItems="searchItems" />
</div>
</slide-transition>
</div>
</template>
<script setup lang="ts">
import { computed, watch, reactive, toRefs } from 'vue';
import { INodeCreateElement } from '@/Interface';
@@ -41,7 +37,7 @@ export interface Props {
const props = defineProps<Props>();
const emit = defineEmits<{
(event: 'closeNodeCreator'): void,
(event: 'closeNodeCreator'): void;
}>();
const nodeCreatorStore = useNodeCreatorStore();
@@ -72,13 +68,13 @@ const searchItems = computed<INodeCreateElement[]>(() => {
}));
});
function onClickOutside (event: Event) {
function onClickOutside(event: Event) {
// We need to prevent cases where user would click inside the node creator
// and try to drag undraggable element. In that case the click event would
// be fired and the node creator would be closed. So we stop that if we detect
// that the click event originated from inside the node creator. And fire click even on the
// original target.
if(state.mousedownInsideEvent) {
if (state.mousedownInsideEvent) {
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
@@ -86,7 +82,7 @@ function onClickOutside (event: Event) {
state.mousedownInsideEvent.target?.dispatchEvent(clickEvent);
state.mousedownInsideEvent = null;
return;
};
}
if (event.type === 'click') {
emit('closeNodeCreator');
@@ -110,20 +106,29 @@ function onDrop(event: DragEvent) {
const nodeCreatorBoundingRect = (state.nodeCreator as Element).getBoundingClientRect();
// Abort drag end event propagation if dropped inside nodes panel
if (nodeTypeName && event.pageX >= nodeCreatorBoundingRect.x && event.pageY >= nodeCreatorBoundingRect.y) {
if (
nodeTypeName &&
event.pageX >= nodeCreatorBoundingRect.x &&
event.pageY >= nodeCreatorBoundingRect.y
) {
event.stopPropagation();
}
}
watch(() => props.active, (isActive) => {
if(isActive === false) nodeCreatorStore.setShowScrim(false);
});
watch(
() => props.active,
(isActive) => {
if (isActive === false) nodeCreatorStore.setShowScrim(false);
},
);
const { nodeCreator } = toRefs(state);
</script>
<style scoped lang="scss">
::v-deep *, *:before, *:after {
::v-deep *,
*:before,
*:after {
box-sizing: border-box;
}

View File

@@ -18,17 +18,20 @@
<template #tooltip v-if="isCommunityNode">
<p
:class="$style.communityNodeIcon"
v-html="$locale.baseText('generic.communityNode.tooltip', { interpolate: { packageName: nodeType.name.split('.')[0], docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL } })"
v-html="
$locale.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
},
})
"
@click="onCommunityNodeTooltipClick"
/>
</template>
<template #dragContent>
<div :class="$style.draggableDataTransfer" ref="draggableDataTransfer"/>
<div
:class="$style.draggable"
:style="draggableStyle"
v-show="dragging"
>
<div :class="$style.draggableDataTransfer" ref="draggableDataTransfer" />
<div :class="$style.draggable" :style="draggableStyle" v-show="dragging">
<node-icon :nodeType="nodeType" @click.capture.stop :size="40" :shrink="false" />
</div>
</template>
@@ -58,10 +61,10 @@ const props = withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
(event: 'dragstart', $e: DragEvent): void,
(event: 'dragend', $e: DragEvent): void,
(event: 'nodeTypeSelected', value: string[]): void,
(event: 'actionsOpen', value: INodeTypeDescription): void,
(event: 'dragstart', $e: DragEvent): void;
(event: 'dragend', $e: DragEvent): void;
(event: 'nodeTypeSelected', value: string[]): void;
(event: 'actionsOpen', value: INodeTypeDescription): void;
}>();
const instance = getCurrentInstance();
@@ -83,9 +86,11 @@ const showActionArrow = computed(() => props.allowActions && hasActions.value);
const hasActions = computed<boolean>(() => (props.nodeType.actions?.length || 0) > 0);
const shortNodeType = computed<string>(() => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '');
const shortNodeType = computed<string>(
() => instance?.proxy.$locale.shortNodeType(props.nodeType.name) || '',
);
const draggableStyle = computed<{ top: string; left: string; }>(() => ({
const draggableStyle = computed<{ top: string; left: string }>(() => ({
top: `${state.draggablePosition.y}px`,
left: `${state.draggablePosition.x}px`,
}));
@@ -101,10 +106,12 @@ const displayName = computed<any>(() => {
});
});
const isTriggerNode = computed<boolean>(() => props.nodeType.displayName.toLowerCase().includes('trigger'));
const isTriggerNode = computed<boolean>(() =>
props.nodeType.displayName.toLowerCase().includes('trigger'),
);
function onClick() {
if(hasActions.value && props.allowActions) emit('actionsOpen', props.nodeType);
if (hasActions.value && props.allowActions) emit('actionsOpen', props.nodeType);
else emit('nodeTypeSelected', [props.nodeType.name]);
}
function onDragStart(event: DragEvent): void {
@@ -113,13 +120,13 @@ function onDragStart(event: DragEvent): void {
* All browsers attach the correct page coordinates to the "dragover" event.
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
*/
document.body.addEventListener("dragover", onDragOver);
document.body.addEventListener('dragover', onDragOver);
const { pageX: x, pageY: y } = event;
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.dropEffect = "copy";
event.dataTransfer.effectAllowed = 'copy';
event.dataTransfer.dropEffect = 'copy';
event.dataTransfer.setDragImage(state.draggableDataTransfer as Element, 0, 0);
event.dataTransfer.setData(
'nodeTypeName',
@@ -133,17 +140,17 @@ function onDragStart(event: DragEvent): void {
}
function onDragOver(event: DragEvent): void {
if (!state.dragging || event.pageX === 0 && event.pageY === 0) {
if (!state.dragging || (event.pageX === 0 && event.pageY === 0)) {
return;
}
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
const [x, y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
state.draggablePosition = { x, y };
}
function onDragEnd(event: DragEvent): void {
document.body.removeEventListener("dragover", onDragOver);
document.body.removeEventListener('dragover', onDragOver);
emit('dragend', event);
@@ -159,7 +166,6 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
}
}
defineExpose({
onClick,
});

View File

@@ -24,7 +24,7 @@
</template>
<script setup lang="ts">
import Vue, { onMounted, reactive, toRefs, onBeforeUnmount } from 'vue';
import Vue, { onMounted, reactive, toRefs, onBeforeUnmount } from 'vue';
import { externalHooks } from '@/mixins/externalHooks';
export interface Props {
@@ -39,7 +39,7 @@ withDefaults(defineProps<Props>(), {
});
const emit = defineEmits<{
(event: 'input', value: string): void,
(event: 'input', value: string): void;
}>();
const { $externalHooks } = new externalHooks();
@@ -54,11 +54,11 @@ function focus() {
function onInput(event: Event) {
const input = event.target as HTMLInputElement;
emit("input", input.value);
emit('input', input.value);
}
function clear() {
emit("input", "");
emit('input', '');
}
onMounted(() => {
@@ -91,7 +91,7 @@ defineExpose({
border-radius: 4px;
&:focus-within {
border-color: var(--color-secondary)
border-color: var(--color-secondary);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{[$style.subcategory]: true, [$style.subcategoryWithIcon]: hasIcon}">
<div :class="{ [$style.subcategory]: true, [$style.subcategoryWithIcon]: hasIcon }">
<node-icon v-if="hasIcon" :class="$style.subcategoryIcon" :nodeType="itemProperties" />
<div :class="$style.details">
<div :class="$style.title">
@@ -32,7 +32,7 @@ export default Vue.extend({
},
},
computed: {
itemProperties() : ISubcategoryItemProps {
itemProperties(): ISubcategoryItemProps {
return this.item.properties as ISubcategoryItemProps;
},
subcategoryName(): string {
@@ -45,7 +45,6 @@ export default Vue.extend({
});
</script>
<style lang="scss" module>
.subcategoryIcon {
min-width: 26px;
@@ -95,5 +94,4 @@ export default Vue.extend({
width: 12px;
color: $node-creator-arrow-color;
}
</style>

View File

@@ -23,13 +23,17 @@
@actionsOpen="setActiveActionsNodeType"
@actionSelected="onActionSelected"
>
<template #noResultsTitle v-if="isActionsActive">
<i />
</template>
<template #noResultsAction v-if="isActionsActive">
<p v-if="containsAPIAction" v-html="getCustomAPICallHintLocale('apiCallNoResult')" class="clickable" @click.stop="addHttpNode" />
<p v-else v-text="$locale.baseText('nodeCreator.noResults.noMatchingActions')"/>
<template #noResultsAction v-if="isActionsActive">
<p
v-if="containsAPIAction"
v-html="getCustomAPICallHintLocale('apiCallNoResult')"
class="clickable"
@click.stop="addHttpNode"
/>
<p v-else v-text="$locale.baseText('nodeCreator.noResults.noMatchingActions')" />
</template>
<template #header>
@@ -40,9 +44,13 @@
:class="$style.title"
/>
</template>
<template #footer v-if="(activeNodeActions && containsAPIAction)">
<template #footer v-if="activeNodeActions && containsAPIAction">
<slot name="footer" />
<span v-html="getCustomAPICallHintLocale('apiCall')" class="clickable" @click.stop="addHttpNode" />
<span
v-html="getCustomAPICallHintLocale('apiCall')"
class="clickable"
@click.stop="addHttpNode"
/>
</template>
</categorized-items>
</div>
@@ -50,116 +58,156 @@
<script setup lang="ts">
import { reactive, toRefs, getCurrentInstance, computed, onMounted, ref } from 'vue';
import { INodeTypeDescription, INodeActionTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import { INodeCreateElement, IActionItemProps, SubcategoryCreateElement, IUpdateInformation } from '@/Interface';
import { CORE_NODES_CATEGORY, WEBHOOK_NODE_TYPE, OTHER_TRIGGER_NODES_SUBCATEGORY, EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE, SCHEDULE_TRIGGER_NODE_TYPE, EMAIL_IMAP_NODE_TYPE, CUSTOM_API_CALL_NAME, HTTP_REQUEST_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import {
INodeTypeDescription,
INodeActionTypeDescription,
INodeTypeNameVersion,
} from 'n8n-workflow';
import {
INodeCreateElement,
IActionItemProps,
SubcategoryCreateElement,
IUpdateInformation,
} from '@/Interface';
import {
CORE_NODES_CATEGORY,
WEBHOOK_NODE_TYPE,
OTHER_TRIGGER_NODES_SUBCATEGORY,
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
MANUAL_TRIGGER_NODE_TYPE,
SCHEDULE_TRIGGER_NODE_TYPE,
EMAIL_IMAP_NODE_TYPE,
CUSTOM_API_CALL_NAME,
HTTP_REQUEST_NODE_TYPE,
STICKY_NODE_TYPE,
} from '@/constants';
import CategorizedItems from './CategorizedItems.vue';
import { useNodeCreatorStore } from '@/stores/nodeCreator';
import { getCategoriesWithNodes, getCategorizedList } from "@/utils";
import { getCategoriesWithNodes, getCategorizedList } from '@/utils';
import { externalHooks } from '@/mixins/externalHooks';
import { useNodeTypesStore } from '@/stores/nodeTypes';
import { BaseTextKey } from '@/plugins/i18n';
const instance = getCurrentInstance();
const items: INodeCreateElement[] = [{
key: "*",
type: "subcategory",
const items: INodeCreateElement[] = [
{
key: '*',
type: 'subcategory',
title: instance?.proxy.$locale.baseText('nodeCreator.subcategoryNames.appTriggerNodes'),
properties: {
subcategory: "App Trigger Nodes",
description: instance?.proxy.$locale.baseText('nodeCreator.subcategoryDescriptions.appTriggerNodes'),
icon: "fa:satellite-dish",
subcategory: 'App Trigger Nodes',
description: instance?.proxy.$locale.baseText(
'nodeCreator.subcategoryDescriptions.appTriggerNodes',
),
icon: 'fa:satellite-dish',
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
{
key: SCHEDULE_TRIGGER_NODE_TYPE,
type: "node",
type: 'node',
properties: {
nodeType: {
group: [],
name: SCHEDULE_TRIGGER_NODE_TYPE,
displayName: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName'),
description: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.scheduleTriggerDescription'),
icon: "fa:clock",
displayName: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
),
description: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
),
icon: 'fa:clock',
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
},
{
key: WEBHOOK_NODE_TYPE,
type: "node",
type: 'node',
properties: {
nodeType: {
group: [],
name: WEBHOOK_NODE_TYPE,
displayName: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
description: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
displayName: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.webhookTriggerDisplayName',
),
description: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.webhookTriggerDescription',
),
iconData: {
type: "file",
icon: "webhook",
fileBuffer: "/static/webhook-icon.svg",
type: 'file',
icon: 'webhook',
fileBuffer: '/static/webhook-icon.svg',
},
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
},
{
key: MANUAL_TRIGGER_NODE_TYPE,
type: "node",
type: 'node',
properties: {
nodeType: {
group: [],
name: MANUAL_TRIGGER_NODE_TYPE,
displayName: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
description: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
icon: "fa:mouse-pointer",
displayName: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.manualTriggerDisplayName',
),
description: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.manualTriggerDescription',
),
icon: 'fa:mouse-pointer',
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
},
{
key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
type: "node",
type: 'node',
properties: {
nodeType: {
group: [],
name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
displayName: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDisplayName'),
description: instance?.proxy.$locale.baseText('nodeCreator.triggerHelperPanel.workflowTriggerDescription'),
icon: "fa:sign-out-alt",
displayName: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
),
description: instance?.proxy.$locale.baseText(
'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
),
icon: 'fa:sign-out-alt',
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
},
{
type: "subcategory",
type: 'subcategory',
key: OTHER_TRIGGER_NODES_SUBCATEGORY,
category: CORE_NODES_CATEGORY,
properties: {
subcategory: OTHER_TRIGGER_NODES_SUBCATEGORY,
description: instance?.proxy.$locale.baseText('nodeCreator.subcategoryDescriptions.otherTriggerNodes'),
icon: "fa:folder-open",
description: instance?.proxy.$locale.baseText(
'nodeCreator.subcategoryDescriptions.otherTriggerNodes',
),
icon: 'fa:folder-open',
defaults: {
color: "#7D838F",
color: '#7D838F',
},
},
},
];
const emit = defineEmits({
"nodeTypeSelected": (nodeTypes: string[]) => true,
nodeTypeSelected: (nodeTypes: string[]) => true,
});
const state = reactive({
@@ -181,12 +229,19 @@ const {
const telemetry = instance?.proxy.$telemetry;
const { categorizedItems: allNodes, isTriggerNode } = useNodeTypesStore();
const containsAPIAction = computed(() => state.latestNodeData?.properties.some((p) => p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME)) === true);
const containsAPIAction = computed(
() =>
state.latestNodeData?.properties.some((p) =>
p.options?.find((o) => o.name === CUSTOM_API_CALL_NAME),
) === true,
);
const computedCategorizedItems = computed(() => getCategorizedList(computedCategoriesWithNodes.value, true));
const computedCategorizedItems = computed(() =>
getCategorizedList(computedCategoriesWithNodes.value, true),
);
const nodeAppSubcategory = computed<(SubcategoryCreateElement | undefined)>(() => {
if(!state.activeNodeActions) return undefined;
const nodeAppSubcategory = computed<SubcategoryCreateElement | undefined>(() => {
if (!state.activeNodeActions) return undefined;
return {
type: 'subcategory',
@@ -205,45 +260,49 @@ const searchPlaceholder = computed(() => {
const nodeNameTitle = state.activeNodeActions?.displayName?.trim() as string;
const actionsSearchPlaceholder = instance?.proxy.$locale.baseText(
'nodeCreator.actionsCategory.searchActions',
{ interpolate: { nodeNameTitle }},
{ interpolate: { nodeNameTitle } },
);
return isActionsActive.value ? actionsSearchPlaceholder : undefined;
});
const filteredMergedAppNodes = computed(() => {
const WHITELISTED_APP_CORE_NODES = [
EMAIL_IMAP_NODE_TYPE,
WEBHOOK_NODE_TYPE,
];
const WHITELISTED_APP_CORE_NODES = [EMAIL_IMAP_NODE_TYPE, WEBHOOK_NODE_TYPE];
if(isAppEventSubcategory.value) return mergedAppNodes.filter(node => {
const isRegularNode = !isTriggerNode(node.name);
const isStickyNode = node.name === STICKY_NODE_TYPE;
const isCoreNode = node.codex?.categories?.includes(CORE_NODES_CATEGORY) && !WHITELISTED_APP_CORE_NODES.includes(node.name);
const hasActions = (node.actions || []).length > 0;
if (isAppEventSubcategory.value)
return mergedAppNodes.filter((node) => {
const isRegularNode = !isTriggerNode(node.name);
const isStickyNode = node.name === STICKY_NODE_TYPE;
const isCoreNode =
node.codex?.categories?.includes(CORE_NODES_CATEGORY) &&
!WHITELISTED_APP_CORE_NODES.includes(node.name);
const hasActions = (node.actions || []).length > 0;
if(isRegularNode && !hasActions) return false;
return !isCoreNode && !isStickyNode;
});
if (isRegularNode && !hasActions) return false;
return !isCoreNode && !isStickyNode;
});
return mergedAppNodes;
});
const computedCategoriesWithNodes = computed(() => {
if(!state.activeNodeActions) return getCategoriesWithNodes(filteredMergedAppNodes.value, []);
if (!state.activeNodeActions) return getCategoriesWithNodes(filteredMergedAppNodes.value, []);
return getCategoriesWithNodes(selectedNodeActions.value, [], state.activeNodeActions.displayName) ;
return getCategoriesWithNodes(selectedNodeActions.value, [], state.activeNodeActions.displayName);
});
const selectedNodeActions = computed<INodeActionTypeDescription[]>(() => state.activeNodeActions?.actions ?? []);
const isAppEventSubcategory = computed(() => state.selectedSubcategory === "*");
const selectedNodeActions = computed<INodeActionTypeDescription[]>(
() => state.activeNodeActions?.actions ?? [],
);
const isAppEventSubcategory = computed(() => state.selectedSubcategory === '*');
const isActionsActive = computed(() => state.activeNodeActions !== null);
const firstLevelItems = computed(() => isRoot.value ? items : []);
const firstLevelItems = computed(() => (isRoot.value ? items : []));
const isSearchActive = computed(() => useNodeCreatorStore().itemsFilter !== '');
const searchItems = computed<INodeCreateElement[]>(() => {
const sorted = state.activeNodeActions ? [...selectedNodeActions.value] : [...filteredMergedAppNodes.value];
const sorted = state.activeNodeActions
? [...selectedNodeActions.value]
: [...filteredMergedAppNodes.value];
sorted.sort((a, b) => {
const textA = a.displayName.toLowerCase();
const textB = b.displayName.toLowerCase();
@@ -264,21 +323,23 @@ const searchItems = computed<INodeCreateElement[]>(() => {
});
function onNodeTypeSelected(nodeTypes: string[]) {
emit("nodeTypeSelected", nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes);
emit(
'nodeTypeSelected',
nodeTypes.length === 1 ? getNodeTypesWithManualTrigger(nodeTypes[0]) : nodeTypes,
);
}
function getCustomAPICallHintLocale(key: string) {
if(!state.activeNodeActions) return '';
if (!state.activeNodeActions) return '';
const nodeNameTitle = state.activeNodeActions.displayName;
return instance?.proxy.$locale.baseText(
`nodeCreator.actionsList.${key}` as BaseTextKey,
{ interpolate: { nodeNameTitle }},
);
const nodeNameTitle = state.activeNodeActions.displayName;
return instance?.proxy.$locale.baseText(`nodeCreator.actionsList.${key}` as BaseTextKey, {
interpolate: { nodeNameTitle },
});
}
// The nodes.json doesn't contain API CALL option so we need to fetch the node detail
// to determine if need to render the API CALL hint
async function fetchNodeDetails() {
if(!state.activeNodeActions) return;
if (!state.activeNodeActions) return;
const { getNodesInformation } = useNodeTypesStore();
const { version, name } = state.activeNodeActions;
@@ -297,7 +358,7 @@ function setActiveActionsNodeType(nodeType: INodeTypeDescription | null) {
setShowTabs(false);
fetchNodeDetails();
if(nodeType) trackActionsView();
if (nodeType) trackActionsView();
}
function onActionSelected(actionCreateElement: INodeCreateElement) {
@@ -311,7 +372,7 @@ function addHttpNode() {
name: '',
key: HTTP_REQUEST_NODE_TYPE,
value: {
authentication: "predefinedCredentialType",
authentication: 'predefinedCredentialType',
},
} as IUpdateInformation;
@@ -328,28 +389,30 @@ function onSubcategorySelected(subcategory: INodeCreateElement) {
state.selectedSubcategory = subcategory.key;
}
function onSubcategoryClose(activeSubcategories: INodeCreateElement[]) {
if(isActionsActive.value === true) setActiveActionsNodeType(null);
if (isActionsActive.value === true) setActiveActionsNodeType(null);
state.isRoot = activeSubcategories.length === 0;
state.selectedSubcategory = activeSubcategories[activeSubcategories.length - 1]?.key ?? '';
}
function shouldShowNodeActions(node: INodeCreateElement) {
if(isAppEventSubcategory.value) return true;
if(state.isRoot && !isSearchActive.value) return false;
if (isAppEventSubcategory.value) return true;
if (state.isRoot && !isSearchActive.value) return false;
// Do not show actions for core category when searching
if(node.type === 'node') return !node.properties.nodeType.codex?.categories?.includes(CORE_NODES_CATEGORY);
if (node.type === 'node')
return !node.properties.nodeType.codex?.categories?.includes(CORE_NODES_CATEGORY);
return false;
}
function trackActionsView() {
const trigger_action_count = selectedNodeActions.value
.filter((action) => action.name.toLowerCase().includes('trigger')).length;
const trigger_action_count = selectedNodeActions.value.filter((action) =>
action.name.toLowerCase().includes('trigger'),
).length;
const trackingPayload = {
app_identifier: state.activeNodeActions?.name,
actions: selectedNodeActions.value.map(action => action.displayName),
actions: selectedNodeActions.value.map((action) => action.displayName),
regular_action_count: selectedNodeActions.value.length - trigger_action_count,
trigger_action_count,
};

View File

@@ -1,9 +1,26 @@
<template>
<div class="type-selector" v-if="nodeCreatorStore.showTabs" data-test-id="node-creator-type-selector">
<el-tabs stretch :value="nodeCreatorStore.selectedType" @input="nodeCreatorStore.setSelectedType">
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
<div
class="type-selector"
v-if="nodeCreatorStore.showTabs"
data-test-id="node-creator-type-selector"
>
<el-tabs
stretch
:value="nodeCreatorStore.selectedType"
@input="nodeCreatorStore.setSelectedType"
>
<el-tab-pane
:label="$locale.baseText('nodeCreator.mainPanel.all')"
:name="ALL_NODE_FILTER"
></el-tab-pane>
<el-tab-pane
:label="$locale.baseText('nodeCreator.mainPanel.regular')"
:name="REGULAR_NODE_FILTER"
></el-tab-pane>
<el-tab-pane
:label="$locale.baseText('nodeCreator.mainPanel.trigger')"
:name="TRIGGER_NODE_FILTER"
></el-tab-pane>
</el-tabs>
</div>
</template>