feat(editor): Show the right editor in focus panel (#17062)

Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
Daria
2025-07-09 14:40:39 +03:00
committed by GitHub
parent c37397cb2b
commit 3aeb622978
14 changed files with 464 additions and 220 deletions

View File

@@ -3,10 +3,9 @@ import type {
CalloutActionType,
INodeParameters,
INodeProperties,
NodeParameterValue,
NodeParameterValueType,
} from 'n8n-workflow';
import { ADD_FORM_NOTICE, deepCopy, getParameterValueByPath, NodeHelpers } from 'n8n-workflow';
import { ADD_FORM_NOTICE, getParameterValueByPath, NodeHelpers } from 'n8n-workflow';
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';
import type { INodeUi, IUpdateInformation } from '@/Interface';
@@ -19,7 +18,7 @@ import MultipleParameter from '@/components/MultipleParameter.vue';
import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
import { useI18n } from '@n8n/i18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useMessage } from '@/composables/useMessage';
import {
@@ -32,15 +31,9 @@ import {
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import {
getMainAuthField,
getNodeAuthFields,
isAuthRelatedParameter,
} from '@/utils/nodeTypesUtils';
import { captureException } from '@sentry/vue';
import { computedWithControl } from '@vueuse/core';
import get from 'lodash/get';
import set from 'lodash/set';
import {
N8nCallout,
N8nIcon,
@@ -52,6 +45,7 @@ import {
} from '@n8n/design-system';
import { storeToRefs } from 'pinia';
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
import { getParameterTypeOption } from '@/utils/nodeSettingsUtils';
const LazyFixedCollectionParameter = defineAsyncComponent(
async () => await import('./FixedCollectionParameter.vue'),
@@ -86,7 +80,7 @@ const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore();
const message = useMessage();
const nodeHelpers = useNodeHelpers();
const nodeSettingsParameters = useNodeSettingsParameters();
const asyncLoadingError = ref(false);
const workflowHelpers = useWorkflowHelpers();
const i18n = useI18n();
@@ -114,6 +108,8 @@ onErrorCaptured((e, component) => {
return false;
});
const node = computed(() => props.node ?? ndvStore.activeNode);
const nodeType = computed(() => {
if (node.value) {
return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
@@ -121,13 +117,11 @@ const nodeType = computed(() => {
return null;
});
const node = computed(() => props.node ?? ndvStore.activeNode);
const filteredParameters = computedWithControl(
[() => props.parameters, () => props.nodeValues] as WatchSource[],
() => {
const parameters = props.parameters.filter((parameter: INodeProperties) =>
displayNodeParameter(parameter),
shouldDisplayNodeParameter(parameter),
);
if (node.value && node.value.type === FORM_TRIGGER_NODE_TYPE) {
@@ -154,10 +148,6 @@ const filteredParameterNames = computed(() => {
return filteredParameters.value.map((parameter) => parameter.name);
});
const nodeAuthFields = computed(() => {
return getNodeAuthFields(nodeType.value);
});
const credentialsParameterIndex = computed(() => {
return filteredParameters.value.findIndex((parameter) => parameter.type === 'credentials');
});
@@ -182,10 +172,6 @@ const indexToShowSlotAt = computed(() => {
return Math.min(index, filteredParameters.value.length - 1);
});
const mainNodeAuthField = computed(() => {
return getMainAuthField(nodeType.value || null);
});
watch(filteredParameterNames, (newValue, oldValue) => {
if (newValue === undefined) {
return;
@@ -210,8 +196,8 @@ function updateFormTriggerParameters(parameters: INodeProperties[], triggerName:
const connectedNodes = workflow.getChildNodes(triggerName);
const hasFormPage = connectedNodes.some((nodeName) => {
const node = workflow.getNode(nodeName);
return node && node.type === FORM_NODE_TYPE;
const _node = workflow.getNode(nodeName);
return _node && _node.type === FORM_NODE_TYPE;
});
if (hasFormPage) {
@@ -255,15 +241,15 @@ function updateWaitParameters(parameters: INodeProperties[], nodeName: string) {
const parentNodes = workflow.getParentNodes(nodeName);
const formTriggerName = parentNodes.find(
(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
);
if (!formTriggerName) return parameters;
const connectedNodes = workflow.getChildNodes(formTriggerName);
const hasFormPage = connectedNodes.some((nodeName) => {
const node = workflow.getNode(nodeName);
return node && node.type === FORM_NODE_TYPE;
const hasFormPage = connectedNodes.some((_nodeName) => {
const _node = workflow.getNode(_nodeName);
return _node && _node.type === FORM_NODE_TYPE;
});
if (hasFormPage) {
@@ -294,7 +280,7 @@ function updateFormParameters(parameters: INodeProperties[], nodeName: string) {
const parentNodes = workflow.getParentNodes(nodeName);
const formTriggerName = parentNodes.find(
(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
);
if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice');
@@ -321,22 +307,7 @@ function getCredentialsDependencies() {
}
function multipleValues(parameter: INodeProperties): boolean {
return getArgument('multipleValues', parameter) === true;
}
function getArgument(
argumentName: string,
parameter: INodeProperties,
): string | string[] | number | boolean | undefined {
if (parameter.typeOptions === undefined) {
return undefined;
}
if (parameter.typeOptions[argumentName] === undefined) {
return undefined;
}
return parameter.typeOptions[argumentName];
return getParameterTypeOption(parameter, 'multipleValues') === true;
}
function getPath(parameterName: string): string {
@@ -354,116 +325,15 @@ function deleteOption(optionName: string): void {
emit('valueChanged', parameterData);
}
function mustHideDuringCustomApiCall(
parameter: INodeProperties,
nodeValues: INodeParameters,
): boolean {
if (parameter?.displayOptions?.hide) return true;
const MUST_REMAIN_VISIBLE = [
'authentication',
'resource',
'operation',
...Object.keys(nodeValues),
];
return !MUST_REMAIN_VISIBLE.includes(parameter.name);
}
function displayNodeParameter(
function shouldDisplayNodeParameter(
parameter: INodeProperties,
displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
): boolean {
if (parameter.type === 'hidden') {
return false;
}
if (
nodeHelpers.isCustomApiCallSelected(props.nodeValues) &&
mustHideDuringCustomApiCall(parameter, props.nodeValues)
) {
return false;
}
// Hide authentication related fields since it will now be part of credentials modal
if (
!KEEP_AUTH_IN_NDV_FOR_NODES.includes(node.value?.type || '') &&
mainNodeAuthField.value &&
(parameter.name === mainNodeAuthField.value?.name || shouldHideAuthRelatedParameter(parameter))
) {
return false;
}
if (parameter[displayKey] === undefined) {
// If it is not defined no need to do a proper check
return true;
}
const nodeValues: INodeParameters = {};
let rawValues = props.nodeValues;
if (props.path) {
rawValues = get(props.nodeValues, props.path) as INodeParameters;
}
if (!rawValues) {
return false;
}
// Resolve expressions
const resolveKeys = Object.keys(rawValues);
let key: string;
let i = 0;
let parameterGotResolved = false;
do {
key = resolveKeys.shift() as string;
const value = rawValues[key];
if (typeof value === 'string' && value?.charAt(0) === '=') {
// Contains an expression that
if (
value.includes('$parameter') &&
resolveKeys.some((parameterName) => value.includes(parameterName))
) {
// Contains probably an expression of a missing parameter so skip
resolveKeys.push(key);
continue;
} else {
// Contains probably no expression with a missing parameter so resolve
try {
nodeValues[key] = workflowHelpers.resolveExpression(
value,
nodeValues,
) as NodeParameterValue;
} catch (e) {
// If expression is invalid ignore
nodeValues[key] = '';
}
parameterGotResolved = true;
}
} else {
// Does not contain an expression, add directly
nodeValues[key] = rawValues[key];
}
// TODO: Think about how to calculate this best
if (i++ > 50) {
// Make sure we do not get caught
break;
}
} while (resolveKeys.length !== 0);
if (parameterGotResolved) {
if (props.path) {
rawValues = deepCopy(props.nodeValues);
set(rawValues, props.path, nodeValues);
return nodeHelpers.displayParameter(rawValues, parameter, props.path, node.value, displayKey);
} else {
return nodeHelpers.displayParameter(nodeValues, parameter, '', node.value, displayKey);
}
}
return nodeHelpers.displayParameter(
return nodeSettingsParameters.shouldDisplayNodeParameter(
props.nodeValues,
node.value,
parameter,
props.path,
node.value,
displayKey,
);
}
@@ -493,26 +363,15 @@ function getParameterIssues(parameter: INodeProperties): string[] {
return issues.parameters?.[parameter.name] ?? [];
}
/**
* Handles default node button parameter type actions
* @param parameter
*/
function shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean {
// TODO: For now, hide all fields that are used in authentication fields displayOptions
// Ideally, we should check if any non-auth field depends on it before hiding it but
// since there is no such case, omitting it to avoid additional computation
return isAuthRelatedParameter(nodeAuthFields.value, parameter);
}
function shouldShowOptions(parameter: INodeProperties): boolean {
return parameter.type !== 'resourceMapper';
}
function getDependentParametersValues(parameter: INodeProperties): string | null {
const loadOptionsDependsOn = getArgument('loadOptionsDependsOn', parameter) as
| string[]
| undefined;
const loadOptionsDependsOn = getParameterTypeOption<string[] | undefined>(
parameter,
'loadOptionsDependsOn',
);
if (loadOptionsDependsOn === undefined) {
return null;
@@ -529,7 +388,7 @@ function getDependentParametersValues(parameter: INodeProperties): string | null
}
return returnValues.join('|');
} catch (error) {
} catch {
return null;
}
}
@@ -792,7 +651,7 @@ const onCalloutDismiss = async (parameter: INodeProperties) => {
:path="getPath(parameter.name)"
:is-read-only="
isReadOnly ||
(parameter.disabledOptions && displayNodeParameter(parameter, 'disabledOptions'))
(parameter.disabledOptions && shouldDisplayNodeParameter(parameter, 'disabledOptions'))
"
:hide-label="false"
:node-values="nodeValues"