feat(editor): Show execution hint message in FocusPanel (no-changelog) (#17442)

This commit is contained in:
Charlie Kolb
2025-07-21 11:48:46 +02:00
committed by GitHub
parent 5cf74beec1
commit b14ad784cb
4 changed files with 79 additions and 31 deletions

View File

@@ -1502,6 +1502,7 @@
"nodeView.couldntImportWorkflow": "Could not import workflow",
"nodeView.couldntLoadWorkflow.invalidWorkflowObject": "Invalid workflow object",
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
"nodeView.focusPanel.noExecutionData": "Execute previous node for autocomplete",
"nodeView.focusPanel.noParameters.title": "Show a node parameter here, to iterate easily",
"nodeView.focusPanel.noParameters.subtitle": "For example, keep your prompt always visible so you can run the workflow while tweaking it",
"nodeView.focusPanel.missingParameter": "This parameter is no longer visible on the node. A related parameter was likely changed, removing this one.",

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useFocusPanelStore } from '@/stores/focusPanel.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { N8nText, N8nInput, N8nResizeWrapper } from '@n8n/design-system';
import { N8nText, N8nInput, N8nResizeWrapper, N8nInfoTip } from '@n8n/design-system';
import { computed, nextTick, ref, watch, toRef } from 'vue';
import { useI18n } from '@n8n/i18n';
import {
@@ -29,6 +29,8 @@ import { htmlEditorEventBus } from '@/event-bus';
import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils';
import type { ResizeData, TargetNodeParameterContext } from '@/Interface';
import { useThrottleFn } from '@vueuse/core';
import { useExecutionData } from '@/composables/useExecutionData';
import { useWorkflowsStore } from '@/stores/workflows.store';
defineOptions({ name: 'FocusPanel' });
@@ -48,6 +50,7 @@ const inputField = ref<InstanceType<typeof N8nInput> | HTMLElement>();
const locale = useI18n();
const nodeHelpers = useNodeHelpers();
const focusPanelStore = useFocusPanelStore();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const nodeSettingsParameters = useNodeSettingsParameters();
const environmentsStore = useEnvironmentsStore();
@@ -108,6 +111,10 @@ const isExecutable = computed(() => {
);
});
const node = computed(() => resolvedParameter.value?.node);
const { hasNodeRun } = useExecutionData({ node });
function getTypeOption<T>(optionName: string): T | undefined {
return resolvedParameter.value
? getParameterTypeOption<T>(resolvedParameter.value.parameter, optionName)
@@ -172,6 +179,8 @@ const targetNodeParameterContext = computed<TargetNodeParameterContext | undefin
};
});
const isNodeExecuting = computed(() => workflowsStore.isNodeExecuting(node.value?.name ?? ''));
const { resolvedExpression } = useResolvedExpression({
expression,
additionalData: resolvedAdditionalExpressionData,
@@ -345,7 +354,15 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
</div>
<div :class="$style.parameterDetailsWrapper">
<div :class="$style.parameterOptionsWrapper">
<div></div>
<div :class="$style.noExecutionDataTip">
<N8nInfoTip
v-if="!hasNodeRun && !isNodeExecuting"
:class="$style.delayedShow"
:bold="true"
>
{{ locale.baseText('nodeView.focusPanel.noExecutionData') }}
</N8nInfoTip>
</div>
<ParameterOptions
v-if="isDisplayed"
:parameter="resolvedParameter.parameter"
@@ -575,6 +592,10 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
justify-content: space-between;
}
.noExecutionDataTip {
align-content: center;
}
.editorContainer {
height: 100%;
overflow-y: auto;
@@ -594,6 +615,20 @@ const onResizeThrottle = useThrottleFn(onResize, 10);
}
}
// We have this animation here to hide the short time between no longer
// executing the node and having runData available
.delayedShow {
opacity: 0;
transition: opacity 0.1s none;
animation: triggerShow 0.1s normal 0.1s forwards;
}
@keyframes triggerShow {
to {
opacity: 1;
}
}
.closeButton {
cursor: pointer;
}

View File

@@ -1,11 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import {
NodeConnectionTypes,
type IRunData,
type IRunExecutionData,
type Workflow,
} from 'n8n-workflow';
import { NodeConnectionTypes, type IRunData, type Workflow } from 'n8n-workflow';
import RunData from './RunData.vue';
import RunInfo from './RunInfo.vue';
import { storeToRefs } from 'pinia';
@@ -26,6 +21,7 @@ import { NDV_UI_OVERHAUL_EXPERIMENT } from '@/constants';
import { usePostHog } from '@/stores/posthog.store';
import { type IRunDataDisplayMode } from '@/Interface';
import { I18nT } from 'vue-i18n';
import { useExecutionData } from '@/composables/useExecutionData';
// Types
@@ -111,6 +107,7 @@ const collapsingColumnName = ref<string | null>(null);
const node = computed(() => {
return ndvStore.activeNode ?? undefined;
});
const { hasNodeRun, workflowExecution, workflowRunData } = useExecutionData({ node });
const isTriggerNode = computed(() => {
return !!node.value && nodeTypesStore.isTriggerNode(node.value.type);
@@ -149,29 +146,6 @@ const isNodeRunning = computed(() => {
const workflowRunning = computed(() => workflowsStore.isWorkflowRunning);
const workflowExecution = computed(() => {
return workflowsStore.getWorkflowExecution;
});
const workflowRunData = computed(() => {
if (workflowExecution.value === null) {
return null;
}
const executionData: IRunExecutionData | undefined = workflowExecution.value.data;
if (!executionData?.resultData?.runData) {
return null;
}
return executionData.resultData.runData;
});
const hasNodeRun = computed(() => {
if (workflowsStore.subWorkflowExecutionError) return true;
return Boolean(
node.value && workflowRunData.value && workflowRunData.value.hasOwnProperty(node.value.name),
);
});
const runTaskData = computed(() => {
if (!node.value || workflowExecution.value === null) {
return null;

View File

@@ -0,0 +1,38 @@
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { INode, IRunExecutionData } from 'n8n-workflow';
import { computed, type ComputedRef } from 'vue';
export function useExecutionData({ node }: { node: ComputedRef<INode | undefined> }) {
const workflowsStore = useWorkflowsStore();
const workflowExecution = computed(() => {
return workflowsStore.getWorkflowExecution;
});
const workflowRunData = computed(() => {
if (workflowExecution.value === null) {
return null;
}
const executionData: IRunExecutionData | undefined = workflowExecution.value.data;
if (!executionData?.resultData?.runData) {
return null;
}
return executionData.resultData.runData;
});
const hasNodeRun = computed(() => {
if (workflowsStore.subWorkflowExecutionError) return true;
return Boolean(
node.value &&
workflowRunData.value &&
Object.prototype.hasOwnProperty.bind(workflowRunData.value)(node.value.name),
);
});
return {
workflowExecution,
workflowRunData,
hasNodeRun,
};
}