refactor(editor): Fix types issues in src/components/Node/* (no-changelog) (#9444)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
oleg 2024-05-17 14:46:11 +02:00 committed by GitHub
parent aac19d3285
commit 69bb745cac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 183 additions and 111 deletions

View file

@ -16,7 +16,11 @@ export const retry = async (
try {
resolve(assertion());
} catch (err) {
Date.now() - startTime > timeout ? reject(err) : tryAgain();
if (Date.now() - startTime > timeout) {
reject(err);
} else {
tryAgain();
}
}
}, interval);
};
@ -62,7 +66,10 @@ export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
binaryDataMode: 'default',
initialized: false,
mfa: {
enabled: false,
},
};
export const getDropdownItems = async (dropdownTriggerParent: HTMLElement) => {

View file

@ -1,5 +1,6 @@
<template>
<div
v-if="data"
:id="nodeId"
:ref="data.name"
:class="nodeWrapperClass"
@ -262,6 +263,16 @@ export default defineComponent({
callDebounced,
};
},
data() {
return {
isTouchActive: false,
nodeSubtitle: '',
showTriggerNodeTooltip: false,
pinDataDiscoveryTooltipVisible: false,
dragging: false,
unwatchWorkflowDataItems: () => {},
};
},
computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
showPinnedDataInfo(): boolean {
@ -325,6 +336,7 @@ export default defineComponent({
return !!this.nodeType?.polling;
},
isExecuting(): boolean {
if (!this.data) return false;
return this.workflowsStore.isNodeExecuting(this.data.name);
},
isSingleActiveTriggerNode(): boolean {
@ -336,12 +348,15 @@ export default defineComponent({
return nodes.length === 1;
},
isManualTypeNode(): boolean {
return this.data.type === MANUAL_TRIGGER_NODE_TYPE;
return this.data?.type === MANUAL_TRIGGER_NODE_TYPE;
},
isConfigNode(): boolean {
return this.nodeTypesStore.isConfigNode(this.workflow, this.data, this.data?.type ?? '');
if (!this.data) return false;
return this.nodeTypesStore.isConfigNode(this.workflow, this.data, this.data.type ?? '');
},
isConfigurableNode(): boolean {
if (!this.data) return false;
return this.nodeTypesStore.isConfigurableNode(
this.workflow,
this.data,
@ -365,10 +380,10 @@ export default defineComponent({
return this.workflowsStore.nodesByName[this.name] as INodeUi | undefined;
},
sameTypeNodes(): INodeUi[] {
return this.workflowsStore.allNodes.filter((node: INodeUi) => node.type === this.data.type);
return this.workflowsStore.allNodes.filter((node: INodeUi) => node.type === this.data?.type);
},
nodeWrapperClass(): object {
const classes = {
nodeWrapperClass() {
const classes: Record<string, boolean> = {
'node-wrapper': true,
'node-wrapper--trigger': this.isTriggerNode,
'node-wrapper--configurable': this.isConfigurableNode,
@ -389,7 +404,7 @@ export default defineComponent({
return classes;
},
nodeWrapperStyles(): object {
nodeWrapperStyles() {
const styles: {
[key: string]: string | number;
} = {
@ -435,7 +450,7 @@ export default defineComponent({
nodeClass(): object {
return {
'node-box': true,
disabled: this.data.disabled,
disabled: this.data?.disabled,
executing: this.isExecuting,
};
},
@ -466,7 +481,7 @@ export default defineComponent({
return issues;
},
nodeDisabledTitle(): string {
return this.data.disabled
return this.data?.disabled
? this.$locale.baseText('node.enable')
: this.$locale.baseText('node.disable');
},
@ -476,21 +491,21 @@ export default defineComponent({
showDisabledLinethrough(): boolean {
return (
!this.isConfigurableNode &&
!!(this.data.disabled && this.inputs.length === 1 && this.outputs.length === 1)
!!(this.data?.disabled && this.inputs.length === 1 && this.outputs.length === 1)
);
},
shortNodeType(): string {
return this.$locale.shortNodeType(this.data.type);
return this.$locale.shortNodeType(this.data?.type ?? '');
},
nodeTitle(): string {
if (this.data.name === 'Start') {
if (this.data?.name === 'Start') {
return this.$locale.headerText({
key: 'headers.start.displayName',
fallback: 'Start',
});
}
return this.data.name;
return this.data?.name ?? '';
},
waiting(): string | undefined {
const workflowExecution = this.workflowsStore.getWorkflowExecution as ExecutionSummary;
@ -518,7 +533,7 @@ export default defineComponent({
workflowRunning(): boolean {
return this.uiStore.isActionActive('workflowRunning');
},
nodeStyle(): object {
nodeStyle() {
const returnStyles: {
[key: string]: string;
} = {};
@ -529,7 +544,7 @@ export default defineComponent({
borderColor = '--color-foreground-dark';
}
if (this.data.disabled) {
if (this.data?.disabled) {
borderColor = '--color-foreground-base';
} else if (!this.isExecuting) {
if (this.hasIssues && !this.hideNodeIssues) {
@ -559,7 +574,7 @@ export default defineComponent({
},
isSelected(): boolean {
return (
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name) !==
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data?.name) !==
undefined
);
},
@ -668,23 +683,13 @@ export default defineComponent({
}, 0);
}
},
data() {
return {
isTouchActive: false,
nodeSubtitle: '',
showTriggerNodeTooltip: false,
pinDataDiscoveryTooltipVisible: false,
dragging: false,
unwatchWorkflowDataItems: () => {},
};
},
methods: {
showPinDataDiscoveryTooltip(dataItemsCount: number): void {
if (
!this.isTriggerNode ||
this.isManualTypeNode ||
this.isScheduledGroup ||
this.uiStore.isModalActive ||
this.uiStore.isAnyModalOpen ||
dataItemsCount === 0
)
return;
@ -695,13 +700,14 @@ export default defineComponent({
this.unwatchWorkflowDataItems();
},
setSubtitle() {
if (!this.data || !this.nodeType) return;
// why is this not a computed property? because it's a very expensive operation
// it requires expressions to resolve each subtitle...
// and ends up bogging down the UI with big workflows, for example when pasting a workflow or even opening a node...
// so we only update it when necessary (when node is mounted and when it's opened and closed (isActive))
try {
const nodeSubtitle =
this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.workflow) || '';
this.nodeHelpers.getNodeSubtitle(this.data, this.nodeType, this.workflow) ?? '';
this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY) ? '' : nodeSubtitle;
} catch (e) {
@ -727,9 +733,9 @@ export default defineComponent({
},
executeNode() {
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
this.$emit('runWorkflow', this.data?.name, 'Node.executeNode');
this.$telemetry.track('User clicked node hover button', {
node_type: this.data.type,
node_type: this.data?.type,
button_name: 'execute',
workflow_id: this.workflowsStore.workflowId,
});
@ -737,18 +743,18 @@ export default defineComponent({
deleteNode() {
this.$telemetry.track('User clicked node hover button', {
node_type: this.data.type,
node_type: this.data?.type,
button_name: 'delete',
workflow_id: this.workflowsStore.workflowId,
});
this.$emit('removeNode', this.data.name);
this.$emit('removeNode', this.data?.name);
},
toggleDisableNode(event: MouseEvent) {
(event.currentTarget as HTMLButtonElement).blur();
this.$telemetry.track('User clicked node hover button', {
node_type: this.data.type,
node_type: this.data?.type,
button_name: 'disable',
workflow_id: this.workflowsStore.workflowId,
});
@ -759,7 +765,8 @@ export default defineComponent({
void this.callDebounced(this.onClickDebounced, { debounceTime: 50, trailing: true }, event);
},
onClickDebounced(event: MouseEvent) {
onClickDebounced(...args: unknown[]) {
const event = args[0] as MouseEvent;
const isDoubleClick = event.detail >= 2;
if (isDoubleClick) {
this.setNodeActive();

View file

@ -21,7 +21,7 @@
</template>
<script setup lang="ts">
import { reactive, computed, toRefs, getCurrentInstance } from 'vue';
import { reactive, computed, toRefs } from 'vue';
import type { ActionTypeDescription, SimplifiedNodeType } from '@/Interface';
import { WEBHOOK_NODE_TYPE, DRAG_EVENT_DATA_KEY } from '@/constants';
@ -30,6 +30,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import { useViewStacks } from '../composables/useViewStacks';
import { useActions } from '../composables/useActions';
import { useTelemetry } from '@/composables/useTelemetry';
export interface Props {
nodeType: SimplifiedNodeType;
@ -37,9 +38,7 @@ export interface Props {
}
const props = defineProps<Props>();
const instance = getCurrentInstance();
const telemetry = instance?.proxy.$telemetry;
const telemetry = useTelemetry();
const { getActionData, getAddedNodesAndConnections, setAddedNodeActionParameters } = useActions();
const { activeViewStack } = useViewStacks();
@ -104,7 +103,7 @@ function onDragOver(event: DragEvent): void {
state.draggablePosition = { x, y };
}
function onDragEnd(event: DragEvent): void {
function onDragEnd(): void {
if (state.storeWatcher) state.storeWatcher();
document.body.removeEventListener('dragend', onDragEnd);
document.body.removeEventListener('dragover', onDragOver);

View file

@ -67,6 +67,7 @@ export interface Props {
const props = withDefaults(defineProps<Props>(), {
active: false,
subcategory: undefined,
});
const i18n = useI18n();
@ -105,8 +106,7 @@ const hasActions = computed(() => {
});
const nodeActions = computed(() => {
const nodeActions = actions[props.nodeType.name] || [];
return nodeActions;
return actions[props.nodeType.name] || [];
});
const shortNodeType = computed<string>(() => i18n.shortNodeType(props.nodeType.name) || '');
@ -119,11 +119,11 @@ const draggableStyle = computed<{ top: string; left: string }>(() => ({
const isCommunityNode = computed<boolean>(() => isCommunityPackageName(props.nodeType.name));
const displayName = computed<string>(() => {
const displayName = props.nodeType.displayName.trimEnd();
const trimmedDisplayName = props.nodeType.displayName.trimEnd();
return i18n.headerText({
key: `headers.${shortNodeType.value}.displayName`,
fallback: hasActions.value ? displayName.replace('Trigger', '') : displayName,
fallback: hasActions.value ? trimmedDisplayName.replace('Trigger', '') : trimmedDisplayName,
});
});
@ -165,7 +165,7 @@ function onDragOver(event: DragEvent): void {
draggablePosition.value = { x, y };
}
function onDragEnd(event: DragEvent): void {
function onDragEnd(): void {
document.body.removeEventListener('dragover', onDragOver);
dragging.value = false;

View file

@ -1,9 +1,11 @@
<template>
<n8n-node-creator-node
:class="$style.subCategory"
:title="i18n.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`)"
:title="i18n.baseText(`nodeCreator.subcategoryNames.${subcategoryName}` as BaseTextKey)"
:is-trigger="false"
:description="i18n.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}`)"
:description="
i18n.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryName}` as BaseTextKey)
"
:show-action-arrow="true"
>
<template #icon>
@ -23,6 +25,7 @@ import type { SubcategoryItemProps } from '@/Interface';
import { camelCase } from 'lodash-es';
import { computed } from 'vue';
import { useI18n } from '@/composables/useI18n';
import type { BaseTextKey } from '@/plugins/i18n';
export interface Props {
item: SubcategoryItemProps;

View file

@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed, getCurrentInstance, onMounted, defineComponent, h } from 'vue';
import { computed, onMounted, defineComponent, h } from 'vue';
import type { PropType } from 'vue';
import type {
INodeCreateElement,
ActionTypeDescription,
NodeFilterType,
IUpdateInformation,
ActionCreateElement,
NodeCreateElement,
} from '@/Interface';
import {
HTTP_REQUEST_NODE_TYPE,
@ -27,18 +27,17 @@ import { useViewStacks } from '../composables/useViewStacks';
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue';
import type { IDataObject } from 'n8n-workflow';
import { useTelemetry } from '@/composables/useTelemetry';
const emit = defineEmits({
nodeTypeSelected: (nodeTypes: string[]) => true,
nodeTypeSelected: (_nodeTypes: string[]) => true,
});
const instance = getCurrentInstance();
const telemetry = instance?.proxy.$telemetry;
const telemetry = useTelemetry();
const { userActivated } = useUsersStore();
const { popViewStack, updateCurrentViewStack } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation();
const {
getNodeTypesWithManualTrigger,
setAddedNodeActionParameters,
getActionData,
getPlaceholderTriggerActions,
@ -133,13 +132,15 @@ function arrowLeft() {
function onKeySelect(activeItemId: string) {
const mergedActions = [...actions.value, ...placeholderTriggerActions];
const activeAction = mergedActions.find((a) => a.uuid === activeItemId);
const activeAction = mergedActions.find((a): a is NodeCreateElement => a.uuid === activeItemId);
if (activeAction) onSelected(activeAction);
}
function onSelected(actionCreateElement: INodeCreateElement) {
const actionData = getActionData(actionCreateElement.properties as ActionTypeDescription);
if (actionCreateElement.type !== 'action') return;
const actionData = getActionData(actionCreateElement.properties);
const isPlaceholderTriggerAction = placeholderTriggerActions.some(
(p) => p.key === actionCreateElement.key,
);
@ -211,6 +212,7 @@ const OrderSwitcher = defineComponent({
props: {
rootView: {
type: String as PropType<NodeFilterType>,
required: true,
},
},
setup(props, { slots }) {
@ -232,7 +234,7 @@ onMounted(() => {
<template>
<div :class="$style.container">
<OrderSwitcher :root-view="rootView">
<OrderSwitcher v-if="rootView" :root-view="rootView">
<template v-if="isTriggerRootView || parsedTriggerActionsBaseline.length !== 0" #triggers>
<!-- Triggers Category -->
<CategorizedItemsRenderer
@ -255,7 +257,7 @@ onMounted(() => {
<span
v-html="
$locale.baseText('nodeCreator.actionsCallout.noTriggerItems', {
interpolate: { nodeName: subcategory },
interpolate: { nodeName: subcategory ?? '' },
})
"
/>
@ -295,7 +297,7 @@ onMounted(() => {
<span
v-html="
$locale.baseText('nodeCreator.actionsCallout.noActionItems', {
interpolate: { nodeName: subcategory },
interpolate: { nodeName: subcategory ?? '' },
})
"
/>
@ -316,7 +318,7 @@ onMounted(() => {
@click.prevent="addHttpNode"
v-html="
$locale.baseText('nodeCreator.actionsList.apiCall', {
interpolate: { node: subcategory },
interpolate: { node: subcategory ?? '' },
})
"
/>

View file

@ -30,7 +30,7 @@ export interface Props {
}
const emit = defineEmits({
nodeTypeSelected: (nodeTypes: string[]) => true,
nodeTypeSelected: (_nodeTypes: string[]) => true,
});
const i18n = useI18n();
@ -98,7 +98,7 @@ function onSelected(item: INodeCreateElement) {
subcategory: item.properties.displayName,
title: item.properties.displayName,
nodeIcon: {
color: item.properties.defaults?.color || '',
color: item.properties.defaults?.color?.toString(),
icon,
iconType: item.properties.iconUrl ? 'file' : 'icon',
},

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, watch, ref, getCurrentInstance } from 'vue';
import { computed, watch, ref } from 'vue';
import type { INodeCreateElement } from '@/Interface';
import { useWorkflowsStore } from '@/stores/workflows.store';
@ -8,6 +8,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation';
import { useViewStacks } from '../composables/useViewStacks';
import ItemsRenderer from './ItemsRenderer.vue';
import CategoryItem from '../ItemTypes/CategoryItem.vue';
import { useTelemetry } from '@/composables/useTelemetry';
export interface Props {
elements: INodeCreateElement[];
@ -23,8 +24,7 @@ const props = withDefaults(defineProps<Props>(), {
elements: () => [],
});
const instance = getCurrentInstance();
const telemetry = useTelemetry();
const { popViewStack } = useViewStacks();
const { registerKeyHook } = useKeyboardNavigation();
const { workflowId } = useWorkflowsStore();
@ -41,7 +41,7 @@ function setExpanded(isExpanded: boolean) {
expanded.value = isExpanded;
if (expanded.value) {
instance?.proxy.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', {
category_name: props.category,
workflow_id: workflowId,
});

View file

@ -25,9 +25,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;
selected: [element: INodeCreateElement, $e?: Event];
dragstart: [element: INodeCreateElement, $e: Event];
dragend: [element: INodeCreateElement, $e: Event];
}>();
const renderedItems = ref<INodeCreateElement[]>([]);
@ -61,7 +61,23 @@ function wrappedEmit(
) {
if (props.disabled) return;
emit(event, element, $e);
switch (event) {
case 'dragstart':
if ($e) {
emit('dragstart', element, $e);
break;
}
case 'dragend':
if ($e) {
emit('dragend', element, $e);
break;
}
case 'selected':
emit('selected', element, $e);
break;
default:
emit(event, element, $e);
}
}
function beforeEnter(el: HTMLElement) {

View file

@ -9,7 +9,7 @@ import {
mockViewCreateElement,
mockSectionCreateElement,
} from './utils';
import ItemsRenderer from '../Renderers/ItemsRenderer.vue';
import ItemsRenderer from '@/components/Node/NodeCreator/Renderers/ItemsRenderer.vue';
import { createComponentRenderer } from '@/__tests__/render';
const renderComponent = createComponentRenderer(ItemsRenderer);
@ -88,8 +88,21 @@ describe('ItemsRenderer', () => {
for (const [index, itemType] of Object.keys(itemTypes).entries()) {
const itemElement = itemTypes[itemType as keyof typeof itemTypes];
await fireEvent.click(itemElement!);
expect(emitted().selected[index][0].type).toBe(itemType);
if (itemElement) {
await fireEvent.click(itemElement);
const emittedEvent = emitted().selected[index];
// Use a type guard to check if emittedEvent is an array and if its first element has a 'type' property
if (
Array.isArray(emittedEvent) &&
emittedEvent.length > 0 &&
typeof emittedEvent[0] === 'object' &&
'type' in emittedEvent[0]
) {
expect(emittedEvent[0].type).toBe(itemType);
} else {
fail('Emitted event is not an array or does not have a type property');
}
}
}
});
});

View file

@ -74,7 +74,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({
@ -117,7 +117,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({
@ -156,7 +156,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [],
});
@ -189,7 +189,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({
@ -251,7 +251,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({
@ -326,7 +326,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({
@ -371,7 +371,7 @@ describe('useActionsGenerator', () => {
],
};
const { actions } = generateMergedNodesAndActions([node]);
const { actions } = generateMergedNodesAndActions([node], []);
expect(actions).toEqual({
[NODE_NAME]: [
expect.objectContaining({

View file

@ -33,6 +33,7 @@ export const mockSimplifiedNodeType = (
defaults: {
color: '#ffffff',
},
outputs: [],
...overrides,
});
@ -123,7 +124,7 @@ export const mockLabelCreateElement = (
uuid: uuidv4(),
key: uuidv4(),
type: 'label',
subcategory: subcategory || 'sampleSubcategory',
subcategory: subcategory ?? 'sampleSubcategory',
properties: mockLabelItemProps(overrides),
});
@ -134,6 +135,6 @@ export const mockActionCreateElement = (
uuid: uuidv4(),
key: uuidv4(),
type: 'action',
subcategory: subcategory || 'sampleSubcategory',
subcategory: subcategory ?? 'sampleSubcategory',
properties: mockActionTypeDescription(overrides),
});

View file

@ -1,4 +1,4 @@
import { getCurrentInstance, computed } from 'vue';
import { computed } from 'vue';
import type { IDataObject, INodeParameters } from 'n8n-workflow';
import type {
ActionTypeDescription,
@ -27,7 +27,6 @@ import {
TRIGGER_NODE_CREATOR_VIEW,
WEBHOOK_NODE_TYPE,
} from '@/constants';
import { i18n } from '@/plugins/i18n';
import type { BaseTextKey } from '@/plugins/i18n';
import type { Telemetry } from '@/plugins/telemetry';
@ -37,10 +36,11 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { sortNodeCreateElements, transformNodeType } from '../utils';
import { useI18n } from '@/composables/useI18n';
export const useActions = () => {
const nodeCreatorStore = useNodeCreatorStore();
const instance = getCurrentInstance();
const i18n = useI18n();
const singleNodeOpenSources = [
NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
@ -50,8 +50,8 @@ export const useActions = () => {
const actionsCategoryLocales = computed(() => {
return {
actions: instance?.proxy.$locale.baseText('nodeCreator.actionsCategory.actions') ?? '',
triggers: instance?.proxy.$locale.baseText('nodeCreator.actionsCategory.triggers') ?? '',
actions: i18n.baseText('nodeCreator.actionsCategory.actions') ?? '',
triggers: i18n.baseText('nodeCreator.actionsCategory.triggers') ?? '',
};
});
@ -66,7 +66,7 @@ export const useActions = () => {
if (transformed.type === 'action') {
const nameBase = node.name.replace('n8n-nodes-base.', '');
const localeKey = `nodeCreator.actionsPlaceholderNode.${nameBase}` as BaseTextKey;
const overwriteLocale = instance?.proxy.$locale.baseText(localeKey) as string;
const overwriteLocale = i18n.baseText(localeKey);
// If the locale key is not the same as the node name, it means it contain a translation
// and we should use it
@ -127,9 +127,11 @@ export const useActions = () => {
},
};
const insertIndex = firstIndexMap.get(label)! + insertedLabels;
extendedActions.splice(insertIndex, 0, newLabel);
insertedLabels++;
const insertIndex = firstIndexMap.get(label);
if (insertIndex !== undefined) {
extendedActions.splice(insertIndex + insertedLabels, 0, newLabel);
insertedLabels++;
}
}
return extendedActions;
@ -148,7 +150,7 @@ export const useActions = () => {
function getActionData(actionItem: ActionTypeDescription): IUpdateInformation {
const displayOptions = actionItem.displayOptions;
const displayConditions = Object.keys(displayOptions?.show || {}).reduce(
const displayConditions = Object.keys(displayOptions?.show ?? {}).reduce(
(acc: IDataObject, showCondition: string) => {
acc[showCondition] = displayOptions?.show?.[showCondition]?.[0];
return acc;

View file

@ -39,7 +39,7 @@ const customNodeActionsParsers: {
displayName: cachedBaseText('nodeCreator.actionsCategory.onEvent', {
interpolate: { event: startCase(categoryItem.name) },
}),
description: categoryItem.description || '',
description: categoryItem.description ?? '',
displayOptions: matchedProperty.displayOptions,
values: { eventsUi: { eventValues: [{ name: categoryItem.value }] } },
}),
@ -56,7 +56,7 @@ function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, label?: stri
name: nodeTypeDescription.name,
group: nodeTypeDescription.group,
codex: {
label: label || '',
label: label ?? '',
categories: [category],
},
iconUrl: nodeTypeDescription.iconUrl,
@ -139,7 +139,7 @@ function triggersCategory(nodeTypeDescription: INodeTypeDescription): ActionType
cachedBaseText('nodeCreator.actionsCategory.onEvent', {
interpolate: { event: startCase(categoryItem.name) },
}),
description: categoryItem.description || '',
description: categoryItem.description ?? '',
displayOptions: matchedProperty.displayOptions,
values: {
[matchedProperty.name]:
@ -159,14 +159,14 @@ function resourceCategories(nodeTypeDescription: INodeTypeDescription): ActionTy
matchedProperties.forEach((property) => {
((property.options as INodePropertyOptions[]) || [])
.filter((option) => option.value !== CUSTOM_API_CALL_KEY)
.forEach((resourceOption, i, options) => {
.forEach((resourceOption, _i, options) => {
const isSingleResource = options.length === 1;
// Match operations for the resource by checking if displayOptions matches or contains the resource name
const operations = nodeTypeDescription.properties.find((operation) => {
const isOperation = operation.name === 'operation';
const isMatchingResource =
operation.displayOptions?.show?.resource?.includes(resourceOption.value) ||
operation.displayOptions?.show?.resource?.includes(resourceOption.value) ??
isSingleResource;
// If the operation doesn't have a version defined, it should be
@ -178,7 +178,9 @@ function resourceCategories(nodeTypeDescription: INodeTypeDescription): ActionTy
: [nodeTypeDescription.version];
const isMatchingVersion = operationVersions
? operationVersions.some((version) => nodeTypeVersions.includes(version))
? operationVersions.some(
(version) => typeof version === 'number' && nodeTypeVersions.includes(version),
)
: true;
return isOperation && isMatchingResource && isMatchingVersion;
@ -286,10 +288,17 @@ export function useActionsGenerator() {
actions[app.name] = appActions;
if (app.name === HTTP_REQUEST_NODE_TYPE) {
const credentialOnlyNodes = httpOnlyCredentials.map((credentialType) =>
getSimplifiedNodeType(getCredentialOnlyNodeType(app, credentialType)),
const credentialOnlyNodes = httpOnlyCredentials.map((credentialType) => {
const credsOnlyNode = getCredentialOnlyNodeType(app, credentialType);
if (credsOnlyNode) return getSimplifiedNodeType(credsOnlyNode);
return null;
});
const filteredNodes = credentialOnlyNodes.filter(
(node): node is SimplifiedNodeType => node !== null,
);
mergedNodes.push(...credentialOnlyNodes);
mergedNodes.push(...filteredNodes);
}
mergedNodes.push(getSimplifiedNodeType(app));

View file

@ -54,7 +54,7 @@ interface ViewStack {
baseFilter?: (item: INodeCreateElement) => boolean;
itemsMapper?: (item: INodeCreateElement) => INodeCreateElement;
panelClass?: string;
sections?: NodeViewItemSection[];
sections?: string[] | NodeViewItemSection[];
}
export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
@ -128,7 +128,7 @@ export const useViewStacks = defineStore('nodeCreatorViewStacks', () => {
const i18n = useI18n();
let nodesByConnectionType: { [key: string]: string[] };
let relatedAIView: NodeViewItem | { properties: { title: string; icon: string } } | undefined;
let relatedAIView: { properties: NodeViewItem['properties'] } | undefined;
if (isOutput === true) {
nodesByConnectionType = useNodeTypesStore().visibleNodeTypesByInputConnectionTypeNames;

View file

@ -86,16 +86,20 @@ export function flattenCreateElements(items: INodeCreateElement[]): INodeCreateE
export function groupItemsInSections(
items: INodeCreateElement[],
sections: NodeViewItemSection[],
sections: string[] | NodeViewItemSection[],
): INodeCreateElement[] {
const filteredSections = sections.filter(
(section): section is NodeViewItemSection => typeof section === 'object',
);
const itemsBySection = items.reduce((acc: Record<string, INodeCreateElement[]>, item) => {
const section = sections.find((s) => s.items.includes(item.key));
const section = filteredSections.find((s) => s.items.includes(item.key));
const key = section?.key ?? 'other';
acc[key] = [...(acc[key] ?? []), item];
return acc;
}, {});
const result: SectionCreateElement[] = sections
const result: SectionCreateElement[] = filteredSections
.map(
(section): SectionCreateElement => ({
type: 'section',

View file

@ -71,8 +71,8 @@ export interface NodeViewItem {
type: string;
properties: {
name?: string;
title: string;
icon: string;
title?: string;
icon?: string;
iconProps?: {
color?: string;
};
@ -81,8 +81,14 @@ export interface NodeViewItem {
group?: string[];
sections?: NodeViewItemSection[];
description?: string;
displayName?: string;
tag?: string;
forceIncludeNodes?: string[];
iconData?: {
type: string;
icon?: string;
fileBuffer?: string;
};
};
category?: string | string[];
}
@ -264,7 +270,7 @@ export function AINodesView(_nodes: SimplifiedNodeType[]): NodeView {
};
}
export function TriggerView(nodes: SimplifiedNodeType[]) {
export function TriggerView() {
const i18n = useI18n();
const view: NodeView = {

View file

@ -11,12 +11,12 @@
:show-tooltip="showTooltip"
:tooltip-position="tooltipPosition"
:badge="badge"
@click="(e) => $emit('click')"
@click="() => $emit('click')"
></n8n-node-icon>
</template>
<script lang="ts">
import type { IVersionNode } from '@/Interface';
import type { ActionTypeDescription, IVersionNode, SimplifiedNodeType } from '@/Interface';
import { useRootStore } from '@/stores/n8nRoot.store';
import type { INodeTypeDescription } from 'n8n-workflow';
import { mapStores } from 'pinia';
@ -32,7 +32,10 @@ export default defineComponent({
name: 'NodeIcon',
props: {
nodeType: {
type: Object as PropType<INodeTypeDescription | IVersionNode | null>,
type: Object as PropType<
INodeTypeDescription | IVersionNode | SimplifiedNodeType | ActionTypeDescription | null
>,
required: true,
},
size: {
type: Number,