mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: Run once for each item tooltip (#9486)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
631f077c18
commit
b91e50fc92
|
@ -114,6 +114,7 @@
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
:block-u-i="blockUi && showTriggerPanel"
|
:block-u-i="blockUi && showTriggerPanel"
|
||||||
:executable="!readOnly"
|
:executable="!readOnly"
|
||||||
|
:input-size="inputSize"
|
||||||
@value-changed="valueChanged"
|
@value-changed="valueChanged"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
@stop-execution="onStopExecution"
|
@stop-execution="onStopExecution"
|
||||||
|
@ -313,6 +314,8 @@ export default defineComponent({
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inputSize = computed(() => ndvStore.ndvInputDataWithPinnedData.length);
|
||||||
|
|
||||||
const isTriggerNode = computed(
|
const isTriggerNode = computed(
|
||||||
() =>
|
() =>
|
||||||
!!activeNodeType.value &&
|
!!activeNodeType.value &&
|
||||||
|
@ -848,6 +851,7 @@ export default defineComponent({
|
||||||
inputRun,
|
inputRun,
|
||||||
linked,
|
linked,
|
||||||
inputNodeName,
|
inputNodeName,
|
||||||
|
inputSize,
|
||||||
hasForeignCredential,
|
hasForeignCredential,
|
||||||
outputRun,
|
outputRun,
|
||||||
isOutputPaneActive,
|
isOutputPaneActive,
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n8n-tooltip placement="bottom" :disabled="!disabledHint">
|
<n8n-tooltip placement="right" :disabled="!tooltipText">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>{{ disabledHint }}</div>
|
<div>{{ tooltipText }}</div>
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:loading="nodeRunning && !isListeningForEvents && !isListeningForWorkflowEvents"
|
:loading
|
||||||
:disabled="disabled || !!disabledHint"
|
:disabled="disabled || !!disabledHint"
|
||||||
:label="buttonLabel"
|
:label="buttonLabel"
|
||||||
:type="type"
|
:type="type"
|
||||||
:size="size"
|
:size="size"
|
||||||
:icon="!isListeningForEvents && !hideIcon ? 'flask' : undefined"
|
:icon="!isListeningForEvents && !hideIcon ? 'flask' : undefined"
|
||||||
:transparent-background="transparent"
|
:transparent-background="transparent"
|
||||||
:title="!isTriggerNode ? $locale.baseText('ndv.execute.testNode.description') : ''"
|
:title="
|
||||||
|
!isTriggerNode && !tooltipText
|
||||||
|
? $locale.baseText('ndv.execute.testNode.description')
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@mouseover="onMouseOver"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,6 +51,10 @@ import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const NODE_TEST_STEP_POPUP_COUNT_KEY = 'N8N_NODE_TEST_STEP_POPUP_COUNT';
|
||||||
|
const MAX_POPUP_COUNT = 10;
|
||||||
|
const POPUP_UPDATE_DELAY = 3000;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
|
@ -76,6 +85,9 @@ export default defineComponent({
|
||||||
hideIcon: {
|
hideIcon: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
tooltip: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['stopExecution', 'execute'],
|
emits: ['stopExecution', 'execute'],
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
@ -91,6 +103,7 @@ export default defineComponent({
|
||||||
pinnedData,
|
pinnedData,
|
||||||
runWorkflow,
|
runWorkflow,
|
||||||
stopCurrentExecution,
|
stopCurrentExecution,
|
||||||
|
lastPopupCountUpdate: 0,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
...useMessage(),
|
...useMessage(),
|
||||||
};
|
};
|
||||||
|
@ -193,6 +206,12 @@ export default defineComponent({
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
tooltipText(): string {
|
||||||
|
if (this.disabledHint) return this.disabledHint;
|
||||||
|
if (this.tooltip && !this.loading && this.testStepButtonPopupCount() < MAX_POPUP_COUNT)
|
||||||
|
return this.tooltip;
|
||||||
|
return '';
|
||||||
|
},
|
||||||
buttonLabel(): string {
|
buttonLabel(): string {
|
||||||
if (this.isListeningForEvents || this.isListeningForWorkflowEvents) {
|
if (this.isListeningForEvents || this.isListeningForWorkflowEvents) {
|
||||||
return this.$locale.baseText('ndv.execute.stopListening');
|
return this.$locale.baseText('ndv.execute.stopListening');
|
||||||
|
@ -220,6 +239,10 @@ export default defineComponent({
|
||||||
|
|
||||||
return this.$locale.baseText('ndv.execute.testNode');
|
return this.$locale.baseText('ndv.execute.testNode');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loading(): boolean {
|
||||||
|
return this.nodeRunning && !this.isListeningForEvents && !this.isListeningForWorkflowEvents;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async stopWaitingForWebhook() {
|
async stopWaitingForWebhook() {
|
||||||
|
@ -231,6 +254,22 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
testStepButtonPopupCount() {
|
||||||
|
return Number(localStorage.getItem(NODE_TEST_STEP_POPUP_COUNT_KEY));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMouseOver() {
|
||||||
|
const count = this.testStepButtonPopupCount();
|
||||||
|
|
||||||
|
if (count < MAX_POPUP_COUNT && !this.disabledHint && this.tooltipText) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!this.lastPopupCountUpdate || now - this.lastPopupCountUpdate >= POPUP_UPDATE_DELAY) {
|
||||||
|
localStorage.setItem(NODE_TEST_STEP_POPUP_COUNT_KEY, `${count + 1}`);
|
||||||
|
this.lastPopupCountUpdate = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async onClick() {
|
async onClick() {
|
||||||
// Show chat if it's a chat node or a child of a chat node with no input data
|
// Show chat if it's a chat node or a child of a chat node with no input data
|
||||||
if (this.isChatNode || (this.isChatChild && this.ndvStore.isNDVDataEmpty('input'))) {
|
if (this.isChatNode || (this.isChatChild && this.ndvStore.isNDVDataEmpty('input'))) {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
data-test-id="node-execute-button"
|
data-test-id="node-execute-button"
|
||||||
:node-name="node.name"
|
:node-name="node.name"
|
||||||
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
:disabled="outputPanelEditMode.enabled && !isTriggerNode"
|
||||||
|
:tooltip="executeButtonTooltip"
|
||||||
size="small"
|
size="small"
|
||||||
telemetry-source="parameters"
|
telemetry-source="parameters"
|
||||||
@execute="onNodeExecute"
|
@execute="onNodeExecute"
|
||||||
|
@ -307,6 +308,19 @@ export default defineComponent({
|
||||||
isLatestNodeVersion(): boolean {
|
isLatestNodeVersion(): boolean {
|
||||||
return !this.node?.typeVersion || this.latestVersion === this.node.typeVersion;
|
return !this.node?.typeVersion || this.latestVersion === this.node.typeVersion;
|
||||||
},
|
},
|
||||||
|
executeButtonTooltip(): string {
|
||||||
|
if (
|
||||||
|
this.node &&
|
||||||
|
this.isLatestNodeVersion &&
|
||||||
|
this.inputSize > 1 &&
|
||||||
|
!NodeHelpers.isSingleExecution(this.node.type, this.node.parameters)
|
||||||
|
) {
|
||||||
|
return this.$locale.baseText('nodeSettings.executeButtonTooltip.times', {
|
||||||
|
interpolate: { inputSize: this.inputSize },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
nodeVersionTag(): string {
|
nodeVersionTag(): string {
|
||||||
if (!this.nodeType || this.nodeType.hidden) {
|
if (!this.nodeType || this.nodeType.hidden) {
|
||||||
return this.$locale.baseText('nodeSettings.deprecated');
|
return this.$locale.baseText('nodeSettings.deprecated');
|
||||||
|
@ -422,6 +436,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
inputSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1149,6 +1149,7 @@
|
||||||
"nodeSettings.onError.options.stopWorkflow.description": "Halt execution and fail workflow",
|
"nodeSettings.onError.options.stopWorkflow.description": "Halt execution and fail workflow",
|
||||||
"nodeSettings.onError.options.stopWorkflow.displayName": "Stop Workflow",
|
"nodeSettings.onError.options.stopWorkflow.displayName": "Stop Workflow",
|
||||||
"nodeSettings.docs": "Docs",
|
"nodeSettings.docs": "Docs",
|
||||||
|
"nodeSettings.executeButtonTooltip.times": "Will execute {inputSize} times, once for each input item",
|
||||||
"nodeSettings.executeOnce.description": "If active, the node executes only once, with data from the first item it receives",
|
"nodeSettings.executeOnce.description": "If active, the node executes only once, with data from the first item it receives",
|
||||||
"nodeSettings.executeOnce.displayName": "Execute Once",
|
"nodeSettings.executeOnce.displayName": "Execute Once",
|
||||||
"nodeSettings.maxTries.description": "Number of times to attempt to execute the node before failing the execution",
|
"nodeSettings.maxTries.description": "Number of times to attempt to execute the node before failing the execution",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type { NodeParameterValue } from './Interfaces';
|
||||||
|
|
||||||
export const BINARY_ENCODING = 'base64';
|
export const BINARY_ENCODING = 'base64';
|
||||||
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
||||||
|
|
||||||
|
@ -62,3 +64,32 @@ export const LANGCHAIN_CUSTOM_TOOLS = [
|
||||||
CODE_TOOL_LANGCHAIN_NODE_TYPE,
|
CODE_TOOL_LANGCHAIN_NODE_TYPE,
|
||||||
WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
|
WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
//nodes that would execute only once with such parameters
|
||||||
|
//add 'undefined' to parameters values if it is parameter's default value
|
||||||
|
export const SINGLE_EXECUTION_NODES: { [key: string]: { [key: string]: NodeParameterValue[] } } = {
|
||||||
|
'n8n-nodes-base.code': {
|
||||||
|
mode: [undefined, 'runOnceForAllItems'],
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.executeWorkflow': {
|
||||||
|
mode: [undefined, 'once'],
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.crateDb': {
|
||||||
|
operation: [undefined, 'update'], // default insert
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.timescaleDb': {
|
||||||
|
operation: [undefined, 'update'], // default insert
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.microsoftSql': {
|
||||||
|
operation: [undefined, 'update', 'delete'], // default insert
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.questDb': {
|
||||||
|
operation: [undefined], // default insert
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.mongoDb': {
|
||||||
|
operation: ['insert', 'update'],
|
||||||
|
},
|
||||||
|
'n8n-nodes-base.redis': {
|
||||||
|
operation: [undefined], // default info
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -56,6 +56,7 @@ import type { Workflow } from './Workflow';
|
||||||
import { validateFilterParameter } from './NodeParameters/FilterParameter';
|
import { validateFilterParameter } from './NodeParameters/FilterParameter';
|
||||||
import { validateFieldType } from './TypeValidation';
|
import { validateFieldType } from './TypeValidation';
|
||||||
import { ApplicationError } from './errors/application.error';
|
import { ApplicationError } from './errors/application.error';
|
||||||
|
import { SINGLE_EXECUTION_NODES } from './Constants';
|
||||||
|
|
||||||
export const cronNodeOptions: INodePropertyCollection[] = [
|
export const cronNodeOptions: INodePropertyCollection[] = [
|
||||||
{
|
{
|
||||||
|
@ -1748,3 +1749,19 @@ export function getCredentialsForNode(
|
||||||
|
|
||||||
return object.description.credentials ?? [];
|
return object.description.credentials ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSingleExecution(type: string, parameters: INodeParameters): boolean {
|
||||||
|
const singleExecutionCase = SINGLE_EXECUTION_NODES[type];
|
||||||
|
|
||||||
|
if (singleExecutionCase) {
|
||||||
|
for (const parameter of Object.keys(singleExecutionCase)) {
|
||||||
|
if (!singleExecutionCase[parameter].includes(parameters[parameter] as NodeParameterValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { INode, INodeParameters, INodeProperties, INodeTypeDescription } from '@/Interfaces';
|
import type { INode, INodeParameters, INodeProperties, INodeTypeDescription } from '@/Interfaces';
|
||||||
import type { Workflow } from '../src';
|
import type { Workflow } from '../src';
|
||||||
|
|
||||||
import { getNodeParameters, getNodeHints } from '@/NodeHelpers';
|
import { getNodeParameters, getNodeHints, isSingleExecution } from '@/NodeHelpers';
|
||||||
|
|
||||||
describe('NodeHelpers', () => {
|
describe('NodeHelpers', () => {
|
||||||
describe('getNodeParameters', () => {
|
describe('getNodeParameters', () => {
|
||||||
|
@ -3528,4 +3528,31 @@ describe('NodeHelpers', () => {
|
||||||
expect(hints).toHaveLength(1);
|
expect(hints).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('isSingleExecution', () => {
|
||||||
|
test('should determine based on node parameters if it would be executed once', () => {
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.code', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.code', { mode: 'runOnceForEachItem' })).toEqual(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.executeWorkflow', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.executeWorkflow', { mode: 'each' })).toEqual(false);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.crateDb', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.crateDb', { operation: 'update' })).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.timescaleDb', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.timescaleDb', { operation: 'update' })).toEqual(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.microsoftSql', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.microsoftSql', { operation: 'update' })).toEqual(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.microsoftSql', { operation: 'delete' })).toEqual(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.questDb', {})).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.mongoDb', { operation: 'insert' })).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.mongoDb', { operation: 'update' })).toEqual(true);
|
||||||
|
expect(isSingleExecution('n8n-nodes-base.redis', {})).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue