diff --git a/packages/frontend/editor-ui/src/components/NodeDetailsView.vue b/packages/frontend/editor-ui/src/components/NodeDetailsView.vue index 21b065b984..cb3d8b6848 100644 --- a/packages/frontend/editor-ui/src/components/NodeDetailsView.vue +++ b/packages/frontend/editor-ui/src/components/NodeDetailsView.vue @@ -49,7 +49,6 @@ const emit = defineEmits<{ connectionType: NodeConnectionType, connectionIndex?: number, ]; - redrawNode: [nodeName: string]; stopExecution: []; }>(); @@ -81,7 +80,6 @@ const message = useMessage(); const { APP_Z_INDEXES } = useStyles(); const settingsEventBus = createEventBus(); -const redrawRequired = ref(false); const runInputIndex = ref(-1); const runOutputIndex = computed(() => ndvStore.output.run ?? -1); const selectedInput = ref(); @@ -498,18 +496,6 @@ const close = async () => { return; } - if ( - activeNode.value && - (typeof activeNodeType.value?.outputs === 'string' || - typeof activeNodeType.value?.inputs === 'string' || - redrawRequired.value) - ) { - const nodeName = activeNode.value.name; - setTimeout(() => { - emit('redrawNode', nodeName); - }, 1); - } - if (outputPanelEditMode.value.enabled && activeNode.value) { const shouldPinDataBeforeClosing = await message.confirm( '', @@ -842,7 +828,6 @@ onBeforeUnmount(() => { @value-changed="valueChanged" @execute="onNodeExecute" @stop-execution="onStopExecution" - @redraw-required="redrawRequired = true" @activate="onWorkflowActivate" @switch-selected-node="onSwitchSelectedNode" @open-connection-node-creator="onOpenConnectionNodeCreator" diff --git a/packages/frontend/editor-ui/src/components/NodeDetailsViewV2.vue b/packages/frontend/editor-ui/src/components/NodeDetailsViewV2.vue index 94c86df9c1..e7082cefca 100644 --- a/packages/frontend/editor-ui/src/components/NodeDetailsViewV2.vue +++ b/packages/frontend/editor-ui/src/components/NodeDetailsViewV2.vue @@ -85,7 +85,6 @@ const message = useMessage(); const { APP_Z_INDEXES } = useStyles(); const settingsEventBus = createEventBus(); -const redrawRequired = ref(false); const runInputIndex = ref(-1); const runOutputIndex = ref(-1); const isLinkingEnabled = ref(true); @@ -821,7 +820,6 @@ onBeforeUnmount(() => { :class="$style.settings" @execute="onNodeExecute" @stop-execution="onStopExecution" - @redraw-required="redrawRequired = true" @activate="onWorkflowActivate" @switch-selected-node="onSwitchSelectedNode" @open-connection-node-creator="onOpenConnectionNodeCreator" diff --git a/packages/frontend/editor-ui/src/components/NodeSettings.vue b/packages/frontend/editor-ui/src/components/NodeSettings.vue index 13ec3a9a2f..a4d6a6cc16 100644 --- a/packages/frontend/editor-ui/src/components/NodeSettings.vue +++ b/packages/frontend/editor-ui/src/components/NodeSettings.vue @@ -2,7 +2,6 @@ import { useTemplateRef, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import type { INodeParameters, - INodeProperties, NodeConnectionType, NodeParameterValue, INodeCredentialDescription, @@ -16,12 +15,7 @@ import type { IUpdateInformation, } from '@/Interface'; -import { - BASE_NODE_SURVEY_URL, - COMMUNITY_NODES_INSTALLATION_DOCS_URL, - CUSTOM_NODES_DOCS_URL, - NDV_UI_OVERHAUL_EXPERIMENT, -} from '@/constants'; +import { BASE_NODE_SURVEY_URL, NDV_UI_OVERHAUL_EXPERIMENT } from '@/constants'; import ParameterInputList from '@/components/ParameterInputList.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; @@ -32,7 +26,12 @@ import NodeSettingsHeader from '@/components/NodeSettingsHeader.vue'; import get from 'lodash/get'; import NodeExecuteButton from './NodeExecuteButton.vue'; -import { nameIsParameter } from '@/utils/nodeSettingsUtils'; +import { + collectSettings, + createCommonNodeSettings, + nameIsParameter, + getNodeSettingsInitialValues, +} from '@/utils/nodeSettingsUtils'; import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNDVStore } from '@/stores/ndv.store'; @@ -54,9 +53,9 @@ import { usePostHog } from '@/stores/posthog.store'; import { shouldShowParameter } from './canvas/experimental/experimentalNdv.utils'; import { useResizeObserver } from '@vueuse/core'; import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters'; -import { I18nT } from 'vue-i18n'; -import { N8nBlockUi, N8nIcon, N8nLink, N8nNotice, N8nText } from '@n8n/design-system'; +import { N8nBlockUi, N8nIcon, N8nNotice, N8nText } from '@n8n/design-system'; import ExperimentalEmbeddedNdvHeader from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvHeader.vue'; +import NodeSettingsInvalidNodeWarning from '@/components/NodeSettingsInvalidNodeWarning.vue'; const props = withDefaults( defineProps<{ @@ -86,7 +85,6 @@ const props = withDefaults( const emit = defineEmits<{ stopExecution: []; - redrawRequired: []; valueChanged: [value: IUpdateInformation]; switchSelectedNode: [nodeName: string]; openConnectionNodeCreator: [ @@ -101,18 +99,7 @@ const emit = defineEmits<{ const slots = defineSlots<{ actions?: {} }>(); -const nodeValues = ref({ - color: '#ff0000', - alwaysOutputData: false, - executeOnce: false, - notesInFlow: false, - onError: 'stopWorkflow', - retryOnFail: false, - maxTries: 3, - waitBetweenTries: 1000, - notes: '', - parameters: {}, -}); +const nodeValues = ref(getNodeSettingsInitialValues()); const nodeTypesStore = useNodeTypesStore(); const ndvStore = useNDVStore(); @@ -145,7 +132,6 @@ const openPanel = ref('params'); const nodeValuesInitialized = ref(false); const hiddenIssuesInputs = ref([]); -const nodeSettings = ref([]); const subConnections = ref | null>(null); const installedPackage = ref(undefined); @@ -306,11 +292,6 @@ const valueChanged = (parameterData: IUpdateInformation) => { return; } - if (parameterData.name === 'onError') { - // If that parameter changes, we need to redraw the connections, as the error output may need to be added or removed - emit('redrawRequired'); - } - if (parameterData.name === 'name') { // Name of node changed so we have to set also the new node name as active @@ -456,132 +437,9 @@ const populateHiddenIssuesSet = () => { workflowsStore.setNodePristine(node.value.name, false); }; -const populateSettings = () => { - if (isExecutable.value && !isTriggerNode.value) { - nodeSettings.value.push( - ...([ - { - displayName: i18n.baseText('nodeSettings.alwaysOutputData.displayName'), - name: 'alwaysOutputData', - type: 'boolean', - default: false, - noDataExpression: true, - description: i18n.baseText('nodeSettings.alwaysOutputData.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.executeOnce.displayName'), - name: 'executeOnce', - type: 'boolean', - default: false, - noDataExpression: true, - description: i18n.baseText('nodeSettings.executeOnce.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.retryOnFail.displayName'), - name: 'retryOnFail', - type: 'boolean', - default: false, - noDataExpression: true, - description: i18n.baseText('nodeSettings.retryOnFail.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.maxTries.displayName'), - name: 'maxTries', - type: 'number', - typeOptions: { - minValue: 2, - maxValue: 5, - }, - default: 3, - displayOptions: { - show: { - retryOnFail: [true], - }, - }, - noDataExpression: true, - description: i18n.baseText('nodeSettings.maxTries.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.waitBetweenTries.displayName'), - name: 'waitBetweenTries', - type: 'number', - typeOptions: { - minValue: 0, - maxValue: 5000, - }, - default: 1000, - displayOptions: { - show: { - retryOnFail: [true], - }, - }, - noDataExpression: true, - description: i18n.baseText('nodeSettings.waitBetweenTries.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.onError.displayName'), - name: 'onError', - type: 'options', - options: [ - { - name: i18n.baseText('nodeSettings.onError.options.stopWorkflow.displayName'), - value: 'stopWorkflow', - description: i18n.baseText('nodeSettings.onError.options.stopWorkflow.description'), - }, - { - name: i18n.baseText('nodeSettings.onError.options.continueRegularOutput.displayName'), - value: 'continueRegularOutput', - description: i18n.baseText( - 'nodeSettings.onError.options.continueRegularOutput.description', - ), - }, - { - name: i18n.baseText('nodeSettings.onError.options.continueErrorOutput.displayName'), - value: 'continueErrorOutput', - description: i18n.baseText( - 'nodeSettings.onError.options.continueErrorOutput.description', - ), - }, - ], - default: 'stopWorkflow', - description: i18n.baseText('nodeSettings.onError.description'), - noDataExpression: true, - isNodeSetting: true, - }, - ] as INodeProperties[]), - ); - } - nodeSettings.value.push( - ...([ - { - displayName: i18n.baseText('nodeSettings.notes.displayName'), - name: 'notes', - type: 'string', - typeOptions: { - rows: 5, - }, - default: '', - noDataExpression: true, - description: i18n.baseText('nodeSettings.notes.description'), - isNodeSetting: true, - }, - { - displayName: i18n.baseText('nodeSettings.notesInFlow.displayName'), - name: 'notesInFlow', - type: 'boolean', - default: false, - noDataExpression: true, - description: i18n.baseText('nodeSettings.notesInFlow.description'), - isNodeSetting: true, - }, - ] as INodeProperties[]), - ); -}; +const nodeSettings = computed(() => + createCommonNodeSettings(isExecutable.value, isToolNode.value, i18n.baseText.bind(i18n)), +); const onParameterBlur = (parameterName: string) => { hiddenIssuesInputs.value = hiddenIssuesInputs.value.filter((name) => name !== parameterName); @@ -631,103 +489,7 @@ const setNodeValues = () => { if (nodeType.value !== null) { nodeValid.value = true; - - const foundNodeSettings = []; - if (node.value.color) { - foundNodeSettings.push('color'); - nodeValues.value = { - ...nodeValues.value, - color: node.value.color, - }; - } - - if (node.value.notes) { - foundNodeSettings.push('notes'); - nodeValues.value = { - ...nodeValues.value, - notes: node.value.notes, - }; - } - - if (node.value.alwaysOutputData) { - foundNodeSettings.push('alwaysOutputData'); - nodeValues.value = { - ...nodeValues.value, - alwaysOutputData: node.value.alwaysOutputData, - }; - } - - if (node.value.executeOnce) { - foundNodeSettings.push('executeOnce'); - nodeValues.value = { - ...nodeValues.value, - executeOnce: node.value.executeOnce, - }; - } - - if (node.value.continueOnFail) { - foundNodeSettings.push('onError'); - nodeValues.value = { - ...nodeValues.value, - onError: 'continueRegularOutput', - }; - } - - if (node.value.onError) { - foundNodeSettings.push('onError'); - nodeValues.value = { - ...nodeValues.value, - onError: node.value.onError, - }; - } - - if (node.value.notesInFlow) { - foundNodeSettings.push('notesInFlow'); - nodeValues.value = { - ...nodeValues.value, - notesInFlow: node.value.notesInFlow, - }; - } - - if (node.value.retryOnFail) { - foundNodeSettings.push('retryOnFail'); - nodeValues.value = { - ...nodeValues.value, - retryOnFail: node.value.retryOnFail, - }; - } - - if (node.value.maxTries) { - foundNodeSettings.push('maxTries'); - nodeValues.value = { - ...nodeValues.value, - maxTries: node.value.maxTries, - }; - } - - if (node.value.waitBetweenTries) { - foundNodeSettings.push('waitBetweenTries'); - nodeValues.value = { - ...nodeValues.value, - waitBetweenTries: node.value.waitBetweenTries, - }; - } - - // Set default node settings - for (const nodeSetting of nodeSettings.value) { - if (!foundNodeSettings.includes(nodeSetting.name)) { - // Set default value - nodeValues.value = { - ...nodeValues.value, - [nodeSetting.name]: nodeSetting.default, - }; - } - } - - nodeValues.value = { - ...nodeValues.value, - parameters: deepCopy(node.value.parameters), - }; + nodeValues.value = collectSettings(node.value, nodeSettings.value); } else { nodeValid.value = false; } @@ -735,22 +497,6 @@ const setNodeValues = () => { nodeValuesInitialized.value = true; }; -const onMissingNodeTextClick = (event: MouseEvent) => { - if ((event.target as Element).localName === 'a') { - telemetry.track('user clicked cnr browse button', { - source: 'cnr missing node modal', - }); - } -}; - -const onMissingNodeLearnMoreLinkClick = () => { - telemetry.track('user clicked cnr docs link', { - source: 'missing node modal source', - package_name: node.value?.type.split('.')[0], - node_type: node.value?.type, - }); -}; - const onStopExecution = () => { emit('stopExecution'); }; @@ -782,7 +528,6 @@ watch(node, () => { onMounted(async () => { populateHiddenIssuesSet(); - populateSettings(); setNodeValues(); props.eventBus?.on('openSettings', openSettings); if (node.value !== null) { @@ -885,49 +630,9 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio @value-changed="valueChanged" @tab-changed="onTabSelect" /> -
-

- -

-
- - {{ i18n.baseText('nodeSettings.communityNodeUnknown.title') }} - -
-
-
- - - -
- - {{ i18n.baseText('nodeSettings.communityNodeUnknown.installLink.text') }} - -
- - - -
+ + +
+import { useTelemetry } from '@/composables/useTelemetry'; +import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, CUSTOM_NODES_DOCS_URL } from '@/constants'; +import type { INodeUi } from '@/Interface'; +import { isCommunityPackageName } from '@/utils/nodeTypesUtils'; +import { N8nIcon, N8nLink, N8nText } from '@n8n/design-system'; +import { useI18n } from '@n8n/i18n'; +import { computed } from 'vue'; +import { I18nT } from 'vue-i18n'; + +const { node } = defineProps<{ node: INodeUi }>(); + +const i18n = useI18n(); +const telemetry = useTelemetry(); + +const isCommunityNode = computed(() => isCommunityPackageName(node.type)); +const npmPackage = computed(() => node.type.split('.')[0]); + +function onMissingNodeTextClick(event: MouseEvent) { + if (event.target instanceof Element && event.target.localName === 'a') { + telemetry.track('user clicked cnr browse button', { + source: 'cnr missing node modal', + }); + } +} + +function onMissingNodeLearnMoreLinkClick() { + telemetry.track('user clicked cnr docs link', { + source: 'missing node modal source', + package_name: node.type.split('.')[0], + node_type: node.type, + }); +} + + + + + diff --git a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts index 5dbac008b9..0963d1b387 100644 --- a/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts +++ b/packages/frontend/editor-ui/src/utils/nodeSettingsUtils.ts @@ -30,6 +30,22 @@ import { captureException } from '@sentry/vue'; import { isPresent } from './typesUtils'; import type { Ref } from 'vue'; import { omitKey } from './objectUtils'; +import type { BaseTextKey } from '@n8n/i18n'; + +export function getNodeSettingsInitialValues(): INodeParameters { + return { + color: '#ff0000', + alwaysOutputData: false, + executeOnce: false, + notesInFlow: false, + onError: 'stopWorkflow', + retryOnFail: false, + maxTries: 3, + waitBetweenTries: 1000, + notes: '', + parameters: {}, + }; +} export function setValue( nodeValues: Ref, @@ -448,3 +464,235 @@ export function shouldSkipParamValidation( Boolean(parameter.allowArbitraryValues)) ); } + +export function createCommonNodeSettings( + isExecutable: boolean, + isTriggerNode: boolean, + t: (key: BaseTextKey) => string, +) { + const ret: INodeProperties[] = []; + + if (isExecutable && !isTriggerNode) { + ret.push( + { + displayName: t('nodeSettings.alwaysOutputData.displayName'), + name: 'alwaysOutputData', + type: 'boolean', + default: false, + noDataExpression: true, + description: t('nodeSettings.alwaysOutputData.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.executeOnce.displayName'), + name: 'executeOnce', + type: 'boolean', + default: false, + noDataExpression: true, + description: t('nodeSettings.executeOnce.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.retryOnFail.displayName'), + name: 'retryOnFail', + type: 'boolean', + default: false, + noDataExpression: true, + description: t('nodeSettings.retryOnFail.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.maxTries.displayName'), + name: 'maxTries', + type: 'number', + typeOptions: { + minValue: 2, + maxValue: 5, + }, + default: 3, + displayOptions: { + show: { + retryOnFail: [true], + }, + }, + noDataExpression: true, + description: t('nodeSettings.maxTries.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.waitBetweenTries.displayName'), + name: 'waitBetweenTries', + type: 'number', + typeOptions: { + minValue: 0, + maxValue: 5000, + }, + default: 1000, + displayOptions: { + show: { + retryOnFail: [true], + }, + }, + noDataExpression: true, + description: t('nodeSettings.waitBetweenTries.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.onError.displayName'), + name: 'onError', + type: 'options', + options: [ + { + name: t('nodeSettings.onError.options.stopWorkflow.displayName'), + value: 'stopWorkflow', + description: t('nodeSettings.onError.options.stopWorkflow.description'), + }, + { + name: t('nodeSettings.onError.options.continueRegularOutput.displayName'), + value: 'continueRegularOutput', + description: t('nodeSettings.onError.options.continueRegularOutput.description'), + }, + { + name: t('nodeSettings.onError.options.continueErrorOutput.displayName'), + value: 'continueErrorOutput', + description: t('nodeSettings.onError.options.continueErrorOutput.description'), + }, + ], + default: 'stopWorkflow', + description: t('nodeSettings.onError.description'), + noDataExpression: true, + isNodeSetting: true, + }, + ); + } + + ret.push( + { + displayName: t('nodeSettings.notes.displayName'), + name: 'notes', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + noDataExpression: true, + description: t('nodeSettings.notes.description'), + isNodeSetting: true, + }, + { + displayName: t('nodeSettings.notesInFlow.displayName'), + name: 'notesInFlow', + type: 'boolean', + default: false, + noDataExpression: true, + description: t('nodeSettings.notesInFlow.description'), + isNodeSetting: true, + }, + ); + + return ret; +} + +export function collectSettings(node: INodeUi, nodeSettings: INodeProperties[]): INodeParameters { + let ret = getNodeSettingsInitialValues(); + + const foundNodeSettings = []; + + if (node.color) { + foundNodeSettings.push('color'); + ret = { + ...ret, + color: node.color, + }; + } + + if (node.notes) { + foundNodeSettings.push('notes'); + ret = { + ...ret, + notes: node.notes, + }; + } + + if (node.alwaysOutputData) { + foundNodeSettings.push('alwaysOutputData'); + ret = { + ...ret, + alwaysOutputData: node.alwaysOutputData, + }; + } + + if (node.executeOnce) { + foundNodeSettings.push('executeOnce'); + ret = { + ...ret, + executeOnce: node.executeOnce, + }; + } + + if (node.continueOnFail) { + foundNodeSettings.push('onError'); + ret = { + ...ret, + onError: 'continueRegularOutput', + }; + } + + if (node.onError) { + foundNodeSettings.push('onError'); + ret = { + ...ret, + onError: node.onError, + }; + } + + if (node.notesInFlow) { + foundNodeSettings.push('notesInFlow'); + ret = { + ...ret, + notesInFlow: node.notesInFlow, + }; + } + + if (node.retryOnFail) { + foundNodeSettings.push('retryOnFail'); + ret = { + ...ret, + retryOnFail: node.retryOnFail, + }; + } + + if (node.maxTries) { + foundNodeSettings.push('maxTries'); + ret = { + ...ret, + maxTries: node.maxTries, + }; + } + + if (node.waitBetweenTries) { + foundNodeSettings.push('waitBetweenTries'); + ret = { + ...ret, + waitBetweenTries: node.waitBetweenTries, + }; + } + + // Set default node settings + for (const nodeSetting of nodeSettings) { + if (!foundNodeSettings.includes(nodeSetting.name)) { + // Set default value + ret = { + ...ret, + [nodeSetting.name]: nodeSetting.default, + }; + } + } + + ret = { + ...ret, + parameters: deepCopy(node.parameters), + }; + + return ret; +} diff --git a/packages/frontend/editor-ui/src/utils/parameterUtils.ts b/packages/frontend/editor-ui/src/utils/parameterUtils.ts deleted file mode 100644 index e69de29bb2..0000000000