mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
refactor(editor): Move optionSelected logic to a composable (#16968)
This commit is contained in:
@@ -31,6 +31,7 @@ import NodeSettingsHeader from '@/components/NodeSettingsHeader.vue';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
import NodeExecuteButton from './NodeExecuteButton.vue';
|
import NodeExecuteButton from './NodeExecuteButton.vue';
|
||||||
|
import { nameIsParameter } from '@/utils/nodeSettingsUtils';
|
||||||
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
@@ -369,7 +370,7 @@ const valueChanged = (parameterData: IUpdateInformation) => {
|
|||||||
nodeHelpers.updateNodeParameterIssuesByName(_node.name);
|
nodeHelpers.updateNodeParameterIssuesByName(_node.name);
|
||||||
nodeHelpers.updateNodeCredentialIssuesByName(_node.name);
|
nodeHelpers.updateNodeCredentialIssuesByName(_node.name);
|
||||||
}
|
}
|
||||||
} else if (nodeSettingsParameters.nameIsParameter(parameterData)) {
|
} else if (nameIsParameter(parameterData)) {
|
||||||
// A node parameter changed
|
// A node parameter changed
|
||||||
nodeSettingsParameters.updateNodeParameter(parameterData, newValue, _node, isToolNode.value);
|
nodeSettingsParameters.updateNodeParameter(parameterData, newValue, _node, isToolNode.value);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type {
|
|||||||
INodeParameterResourceLocator,
|
INodeParameterResourceLocator,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodePropertyCollection,
|
|
||||||
INodePropertyOptions,
|
INodePropertyOptions,
|
||||||
IParameterLabel,
|
IParameterLabel,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
@@ -37,6 +36,13 @@ import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
|
|||||||
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
|
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
|
||||||
import TextEdit from '@/components/TextEdit.vue';
|
import TextEdit from '@/components/TextEdit.vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatAsExpression,
|
||||||
|
getParameterTypeOption,
|
||||||
|
isResourceLocatorParameterType,
|
||||||
|
isValidParameterOption,
|
||||||
|
parseFromExpression,
|
||||||
|
} from '@/utils/nodeSettingsUtils';
|
||||||
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
|
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -54,23 +60,23 @@ import { useI18n } from '@n8n/i18n';
|
|||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { htmlEditorEventBus } from '@/event-bus';
|
import { htmlEditorEventBus } from '@/event-bus';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
|
import { N8nIcon, N8nInput, N8nInputNumber, N8nOption, N8nSelect } from '@n8n/design-system';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
import { captureMessage } from '@sentry/vue';
|
import { captureMessage } from '@sentry/vue';
|
||||||
|
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
||||||
|
import { hasFocusOnInput, isBlurrableEl, isFocusableEl, isSelectableEl } from '@/utils/typesUtils';
|
||||||
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
import { completeExpressionSyntax, shouldConvertToExpression } from '@/utils/expressions';
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
|
||||||
import CssEditor from './CssEditor/CssEditor.vue';
|
import CssEditor from './CssEditor/CssEditor.vue';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
|
||||||
|
|
||||||
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
||||||
|
|
||||||
@@ -125,6 +131,7 @@ const i18n = useI18n();
|
|||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const { debounce } = useDebounce();
|
const { debounce } = useDebounce();
|
||||||
const workflowHelpers = useWorkflowHelpers();
|
const workflowHelpers = useWorkflowHelpers();
|
||||||
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
const credentialsStore = useCredentialsStore();
|
const credentialsStore = useCredentialsStore();
|
||||||
@@ -133,7 +140,6 @@ const workflowsStore = useWorkflowsStore();
|
|||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const focusPanelStore = useFocusPanelStore();
|
|
||||||
|
|
||||||
// ESLint: false positive
|
// ESLint: false positive
|
||||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||||
@@ -181,6 +187,77 @@ const dateTimePickerOptions = ref({
|
|||||||
});
|
});
|
||||||
const isFocused = ref(false);
|
const isFocused = ref(false);
|
||||||
|
|
||||||
|
const node = computed(() => ndvStore.activeNode ?? undefined);
|
||||||
|
const nodeType = computed(
|
||||||
|
() => node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shortPath = computed<string>(() => {
|
||||||
|
const short = props.path.split('.');
|
||||||
|
short.shift();
|
||||||
|
return short.join('.');
|
||||||
|
});
|
||||||
|
|
||||||
|
function getTypeOption<T>(optionName: string): T {
|
||||||
|
return getParameterTypeOption<T>(props.parameter, optionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isModelValueExpression = computed(() => isValueExpression(props.parameter, props.modelValue));
|
||||||
|
|
||||||
|
const isResourceLocatorParameter = computed<boolean>(() => {
|
||||||
|
return isResourceLocatorParameterType(props.parameter.type);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSecretParameter = computed<boolean>(() => {
|
||||||
|
return getTypeOption('password') === true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasRemoteMethod = computed<boolean>(() => {
|
||||||
|
return !!getTypeOption('loadOptionsMethod') || !!getTypeOption('loadOptions');
|
||||||
|
});
|
||||||
|
|
||||||
|
const parameterOptions = computed(() => {
|
||||||
|
const options = hasRemoteMethod.value ? remoteParameterOptions.value : props.parameter.options;
|
||||||
|
const safeOptions = (options ?? []).filter(isValidParameterOption);
|
||||||
|
|
||||||
|
return safeOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValueString = computed<string>(() => {
|
||||||
|
return props.modelValue as string;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValueResourceLocator = computed<INodeParameterResourceLocator>(() => {
|
||||||
|
return props.modelValue as INodeParameterResourceLocator;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValueExpressionEdit = computed<string>(() => {
|
||||||
|
return isResourceLocatorParameter.value && typeof props.modelValue !== 'string'
|
||||||
|
? props.modelValue
|
||||||
|
? ((props.modelValue as INodeParameterResourceLocator).value as string)
|
||||||
|
: ''
|
||||||
|
: (props.modelValue as string);
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorRows = computed(() => getTypeOption<number>('rows'));
|
||||||
|
|
||||||
|
const editorType = computed<EditorType | 'json' | 'code' | 'cssEditor'>(() => {
|
||||||
|
return getTypeOption<EditorType>('editor');
|
||||||
|
});
|
||||||
|
const editorIsReadOnly = computed<boolean>(() => {
|
||||||
|
return getTypeOption<boolean>('editorIsReadOnly') ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorLanguage = computed<CodeNodeEditorLanguage>(() => {
|
||||||
|
if (editorType.value === 'json' || props.parameter.type === 'json')
|
||||||
|
return 'json' as CodeNodeEditorLanguage;
|
||||||
|
return getTypeOption<CodeNodeEditorLanguage>('editorLanguage') ?? 'javaScript';
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeEditorMode = computed<CodeExecutionMode>(() => {
|
||||||
|
return node.value?.parameters.mode as CodeExecutionMode;
|
||||||
|
});
|
||||||
|
|
||||||
const displayValue = computed(() => {
|
const displayValue = computed(() => {
|
||||||
if (remoteParameterOptionsLoadingIssues.value) {
|
if (remoteParameterOptionsLoadingIssues.value) {
|
||||||
if (!nodeType.value || nodeType.value?.codex?.categories?.includes(CORE_NODES_CATEGORY)) {
|
if (!nodeType.value || nodeType.value?.codex?.categories?.includes(CORE_NODES_CATEGORY)) {
|
||||||
@@ -234,7 +311,7 @@ const displayValue = computed(() => {
|
|||||||
if (
|
if (
|
||||||
Array.isArray(returnValue) &&
|
Array.isArray(returnValue) &&
|
||||||
props.parameter.type === 'color' &&
|
props.parameter.type === 'color' &&
|
||||||
getArgument('showAlpha') === true &&
|
getTypeOption('showAlpha') === true &&
|
||||||
(returnValue as unknown as string).charAt(0) === '#'
|
(returnValue as unknown as string).charAt(0) === '#'
|
||||||
) {
|
) {
|
||||||
// Convert the value to rgba that el-color-picker can display it correctly
|
// Convert the value to rgba that el-color-picker can display it correctly
|
||||||
@@ -273,10 +350,8 @@ const expressionDisplayValue = computed(() => {
|
|||||||
return `${displayValue.value ?? ''}`;
|
return `${displayValue.value ?? ''}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isModelValueExpression = computed(() => isValueExpression(props.parameter, props.modelValue));
|
|
||||||
|
|
||||||
const dependentParametersValues = computed<string | null>(() => {
|
const dependentParametersValues = computed<string | null>(() => {
|
||||||
const loadOptionsDependsOn = getArgument<string[] | undefined>('loadOptionsDependsOn');
|
const loadOptionsDependsOn = getTypeOption<string[] | undefined>('loadOptionsDependsOn');
|
||||||
|
|
||||||
if (loadOptionsDependsOn === undefined) {
|
if (loadOptionsDependsOn === undefined) {
|
||||||
return null;
|
return null;
|
||||||
@@ -293,32 +368,13 @@ const dependentParametersValues = computed<string | null>(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return returnValues.join('|');
|
return returnValues.join('|');
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const node = computed(() => ndvStore.activeNode ?? undefined);
|
|
||||||
const nodeType = computed(
|
|
||||||
() => node.value && nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion),
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayTitle = computed<string>(() => {
|
|
||||||
const interpolation = { interpolate: { shortPath: shortPath.value } };
|
|
||||||
|
|
||||||
if (getIssues.value.length && isModelValueExpression.value) {
|
|
||||||
return i18n.baseText('parameterInput.parameterHasIssuesAndExpression', interpolation);
|
|
||||||
} else if (getIssues.value.length && !isModelValueExpression.value) {
|
|
||||||
return i18n.baseText('parameterInput.parameterHasIssues', interpolation);
|
|
||||||
} else if (!getIssues.value.length && isModelValueExpression.value) {
|
|
||||||
return i18n.baseText('parameterInput.parameterHasExpression', interpolation);
|
|
||||||
}
|
|
||||||
|
|
||||||
return i18n.baseText('parameterInput.parameter', interpolation);
|
|
||||||
});
|
|
||||||
|
|
||||||
const getStringInputType = computed(() => {
|
const getStringInputType = computed(() => {
|
||||||
if (getArgument('password') === true) {
|
if (getTypeOption('password') === true) {
|
||||||
return 'password';
|
return 'password';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +426,7 @@ const getIssues = computed<string[]>(() => {
|
|||||||
|
|
||||||
let checkValues: string[] = [];
|
let checkValues: string[] = [];
|
||||||
|
|
||||||
if (!skipCheck(displayValue.value)) {
|
if (!nodeSettingsParameters.shouldSkipParamValidation(displayValue.value)) {
|
||||||
if (Array.isArray(displayValue.value)) {
|
if (Array.isArray(displayValue.value)) {
|
||||||
checkValues = checkValues.concat(displayValue.value);
|
checkValues = checkValues.concat(displayValue.value);
|
||||||
} else {
|
} else {
|
||||||
@@ -380,9 +436,7 @@ const getIssues = computed<string[]>(() => {
|
|||||||
|
|
||||||
for (const checkValue of checkValues) {
|
for (const checkValue of checkValues) {
|
||||||
if (checkValue === null || !validOptions.includes(checkValue)) {
|
if (checkValue === null || !validOptions.includes(checkValue)) {
|
||||||
if (issues.parameters === undefined) {
|
issues.parameters = issues.parameters ?? {};
|
||||||
issues.parameters = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const issue = i18n.baseText('parameterInput.theValueIsNotSupported', {
|
const issue = i18n.baseText('parameterInput.theValueIsNotSupported', {
|
||||||
interpolate: { checkValue },
|
interpolate: { checkValue },
|
||||||
@@ -392,9 +446,7 @@ const getIssues = computed<string[]>(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (remoteParameterOptionsLoadingIssues.value !== null && !isModelValueExpression.value) {
|
} else if (remoteParameterOptionsLoadingIssues.value !== null && !isModelValueExpression.value) {
|
||||||
if (issues.parameters === undefined) {
|
issues.parameters = issues.parameters ?? {};
|
||||||
issues.parameters = {};
|
|
||||||
}
|
|
||||||
issues.parameters[props.parameter.name] = [
|
issues.parameters[props.parameter.name] = [
|
||||||
`There was a problem loading the parameter options from server: "${remoteParameterOptionsLoadingIssues.value}"`,
|
`There was a problem loading the parameter options from server: "${remoteParameterOptionsLoadingIssues.value}"`,
|
||||||
];
|
];
|
||||||
@@ -406,9 +458,7 @@ const getIssues = computed<string[]>(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isSelectedArchived) {
|
if (isSelectedArchived) {
|
||||||
if (issues.parameters === undefined) {
|
issues.parameters = issues.parameters ?? {};
|
||||||
issues.parameters = {};
|
|
||||||
}
|
|
||||||
const issue = i18n.baseText('parameterInput.selectedWorkflowIsArchived');
|
const issue = i18n.baseText('parameterInput.selectedWorkflowIsArchived');
|
||||||
issues.parameters[props.parameter.name] = [issue];
|
issues.parameters[props.parameter.name] = [issue];
|
||||||
}
|
}
|
||||||
@@ -422,6 +472,20 @@ const getIssues = computed<string[]>(() => {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const displayTitle = computed<string>(() => {
|
||||||
|
const interpolation = { interpolate: { shortPath: shortPath.value } };
|
||||||
|
|
||||||
|
if (getIssues.value.length && isModelValueExpression.value) {
|
||||||
|
return i18n.baseText('parameterInput.parameterHasIssuesAndExpression', interpolation);
|
||||||
|
} else if (getIssues.value.length && !isModelValueExpression.value) {
|
||||||
|
return i18n.baseText('parameterInput.parameterHasIssues', interpolation);
|
||||||
|
} else if (!getIssues.value.length && isModelValueExpression.value) {
|
||||||
|
return i18n.baseText('parameterInput.parameterHasExpression', interpolation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n.baseText('parameterInput.parameter', interpolation);
|
||||||
|
});
|
||||||
|
|
||||||
const displayIssues = computed(
|
const displayIssues = computed(
|
||||||
() =>
|
() =>
|
||||||
props.parameter.type !== 'credentialsSelect' &&
|
props.parameter.type !== 'credentialsSelect' &&
|
||||||
@@ -429,26 +493,6 @@ const displayIssues = computed(
|
|||||||
getIssues.value.length > 0,
|
getIssues.value.length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorType = computed<EditorType | 'json' | 'code' | 'cssEditor'>(() => {
|
|
||||||
return getArgument<EditorType>('editor');
|
|
||||||
});
|
|
||||||
const editorIsReadOnly = computed<boolean>(() => {
|
|
||||||
return getArgument<boolean>('editorIsReadOnly') ?? false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorLanguage = computed<CodeNodeEditorLanguage>(() => {
|
|
||||||
if (editorType.value === 'json' || props.parameter.type === 'json')
|
|
||||||
return 'json' as CodeNodeEditorLanguage;
|
|
||||||
return getArgument<CodeNodeEditorLanguage>('editorLanguage') ?? 'javaScript';
|
|
||||||
});
|
|
||||||
|
|
||||||
const parameterOptions = computed(() => {
|
|
||||||
const options = hasRemoteMethod.value ? remoteParameterOptions.value : props.parameter.options;
|
|
||||||
const safeOptions = (options ?? []).filter(isValidParameterOption);
|
|
||||||
|
|
||||||
return safeOptions;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSwitch = computed(
|
const isSwitch = computed(
|
||||||
() => props.parameter.type === 'boolean' && !isModelValueExpression.value,
|
() => props.parameter.type === 'boolean' && !isModelValueExpression.value,
|
||||||
);
|
);
|
||||||
@@ -500,28 +544,10 @@ const parameterInputWrapperStyle = computed(() => {
|
|||||||
return styles;
|
return styles;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasRemoteMethod = computed<boolean>(() => {
|
|
||||||
return !!getArgument('loadOptionsMethod') || !!getArgument('loadOptions');
|
|
||||||
});
|
|
||||||
|
|
||||||
const shortPath = computed<string>(() => {
|
|
||||||
const short = props.path.split('.');
|
|
||||||
short.shift();
|
|
||||||
return short.join('.');
|
|
||||||
});
|
|
||||||
|
|
||||||
const parameterId = computed(() => {
|
const parameterId = computed(() => {
|
||||||
return `${node.value?.id ?? crypto.randomUUID()}${props.path}`;
|
return `${node.value?.id ?? crypto.randomUUID()}${props.path}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isResourceLocatorParameter = computed<boolean>(() => {
|
|
||||||
return props.parameter.type === 'resourceLocator' || props.parameter.type === 'workflowSelector';
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSecretParameter = computed<boolean>(() => {
|
|
||||||
return getArgument('password') === true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const remoteParameterOptionsKeys = computed<string[]>(() => {
|
const remoteParameterOptionsKeys = computed<string[]>(() => {
|
||||||
return (remoteParameterOptions.value || []).map((o) => o.name);
|
return (remoteParameterOptions.value || []).map((o) => o.name);
|
||||||
});
|
});
|
||||||
@@ -530,28 +556,6 @@ const shouldRedactValue = computed<boolean>(() => {
|
|||||||
return getStringInputType.value === 'password' || props.isForCredential;
|
return getStringInputType.value === 'password' || props.isForCredential;
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelValueString = computed<string>(() => {
|
|
||||||
return props.modelValue as string;
|
|
||||||
});
|
|
||||||
|
|
||||||
const modelValueResourceLocator = computed<INodeParameterResourceLocator>(() => {
|
|
||||||
return props.modelValue as INodeParameterResourceLocator;
|
|
||||||
});
|
|
||||||
|
|
||||||
const modelValueExpressionEdit = computed<string>(() => {
|
|
||||||
return isResourceLocatorParameter.value && typeof props.modelValue !== 'string'
|
|
||||||
? props.modelValue
|
|
||||||
? ((props.modelValue as INodeParameterResourceLocator).value as string)
|
|
||||||
: ''
|
|
||||||
: (props.modelValue as string);
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorRows = computed(() => getArgument<number>('rows'));
|
|
||||||
|
|
||||||
const codeEditorMode = computed<CodeExecutionMode>(() => {
|
|
||||||
return node.value?.parameters.mode as CodeExecutionMode;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isCodeNode = computed(
|
const isCodeNode = computed(
|
||||||
() => !!node.value && NODES_USING_CODE_NODE_EDITOR.includes(node.value.type),
|
() => !!node.value && NODES_USING_CODE_NODE_EDITOR.includes(node.value.type),
|
||||||
);
|
);
|
||||||
@@ -564,7 +568,7 @@ const isInputTypeNumber = computed(() => props.parameter.type === 'number');
|
|||||||
const isInputDataEmpty = computed(() => ndvStore.isInputPanelEmpty);
|
const isInputDataEmpty = computed(() => ndvStore.isInputPanelEmpty);
|
||||||
const isDropDisabled = computed(
|
const isDropDisabled = computed(
|
||||||
() =>
|
() =>
|
||||||
props.parameter.noDataExpression ||
|
props.parameter.noDataExpression === true ||
|
||||||
props.isReadOnly ||
|
props.isReadOnly ||
|
||||||
isResourceLocatorParameter.value ||
|
isResourceLocatorParameter.value ||
|
||||||
isModelValueExpression.value,
|
isModelValueExpression.value,
|
||||||
@@ -581,18 +585,7 @@ const showDragnDropTip = computed(
|
|||||||
!props.isForCredential,
|
!props.isForCredential,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldCaptureForPosthog = computed(() => {
|
const shouldCaptureForPosthog = computed(() => node.value?.type === AI_TRANSFORM_NODE_TYPE);
|
||||||
if (node.value?.type) {
|
|
||||||
return [AI_TRANSFORM_NODE_TYPE].includes(node.value?.type);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
function isValidParameterOption(
|
|
||||||
option: INodePropertyOptions | INodeProperties | INodePropertyCollection,
|
|
||||||
): option is INodePropertyOptions {
|
|
||||||
return 'value' in option && isPresent(option.value) && isPresent(option.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRemoteParameterOption(option: INodePropertyOptions) {
|
function isRemoteParameterOption(option: INodePropertyOptions) {
|
||||||
return remoteParameterOptionsKeys.value.includes(option.name);
|
return remoteParameterOptionsKeys.value.includes(option.name);
|
||||||
@@ -612,13 +605,6 @@ function credentialSelected(updateInformation: INodeUpdatePropertiesInformation)
|
|||||||
void externalHooks.run('nodeSettings.credentialSelected', { updateInformation });
|
void externalHooks.run('nodeSettings.credentialSelected', { updateInformation });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a param value must be skipped when collecting node param issues for validation.
|
|
||||||
*/
|
|
||||||
function skipCheck(value: string | number | boolean | null) {
|
|
||||||
return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlaceholder(): string {
|
function getPlaceholder(): string {
|
||||||
return props.isForCredential
|
return props.isForCredential
|
||||||
? i18n.credText(uiStore.activeCredentialType).placeholder(props.parameter)
|
? i18n.credText(uiStore.activeCredentialType).placeholder(props.parameter)
|
||||||
@@ -662,8 +648,8 @@ async function loadRemoteParameterOptions() {
|
|||||||
props.parameter,
|
props.parameter,
|
||||||
currentNodeParameters,
|
currentNodeParameters,
|
||||||
) as INodeParameters;
|
) as INodeParameters;
|
||||||
const loadOptionsMethod = getArgument<string | undefined>('loadOptionsMethod');
|
const loadOptionsMethod = getTypeOption<string | undefined>('loadOptionsMethod');
|
||||||
const loadOptions = getArgument<ILoadOptions | undefined>('loadOptions');
|
const loadOptions = getTypeOption<ILoadOptions | undefined>('loadOptions');
|
||||||
|
|
||||||
const options = await nodeTypesStore.getNodeParameterOptions({
|
const options = await nodeTypesStore.getNodeParameterOptions({
|
||||||
nodeTypeAndVersion: {
|
nodeTypeAndVersion: {
|
||||||
@@ -678,8 +664,12 @@ async function loadRemoteParameterOptions() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
remoteParameterOptions.value = remoteParameterOptions.value.concat(options);
|
remoteParameterOptions.value = remoteParameterOptions.value.concat(options);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
remoteParameterOptionsLoadingIssues.value = error.message;
|
remoteParameterOptionsLoadingIssues.value = error.message;
|
||||||
|
} else {
|
||||||
|
remoteParameterOptionsLoadingIssues.value = String(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteParameterOptionsLoading.value = false;
|
remoteParameterOptionsLoading.value = false;
|
||||||
@@ -717,12 +707,14 @@ function trackExpressionEditOpen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeTextEditDialog() {
|
function closeTextEditDialog() {
|
||||||
textEditDialogVisible.value = false;
|
textEditDialogVisible.value = false;
|
||||||
|
|
||||||
editDialogClosing.value = true;
|
editDialogClosing.value = true;
|
||||||
void nextTick().then(() => {
|
void nextTick().then(() => {
|
||||||
inputField.value?.blur?.();
|
if (isBlurrableEl(inputField.value)) {
|
||||||
|
inputField.value.blur();
|
||||||
|
}
|
||||||
editDialogClosing.value = false;
|
editDialogClosing.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -739,10 +731,179 @@ function displayEditDialog() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArgument<T = string | number | boolean | undefined>(argumentName: string): T {
|
function openExpressionEditorModal() {
|
||||||
return props.parameter.typeOptions?.[argumentName];
|
if (!isModelValueExpression.value) return;
|
||||||
|
|
||||||
|
expressionEditDialogVisible.value = true;
|
||||||
|
trackExpressionEditOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectInput() {
|
||||||
|
if (inputField.value) {
|
||||||
|
if (isSelectableEl(inputField.value)) {
|
||||||
|
inputField.value.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setFocus() {
|
||||||
|
if (['json'].includes(props.parameter.type) && getTypeOption('alwaysOpenEditWindow')) {
|
||||||
|
displayEditDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.value) {
|
||||||
|
// When an event like mouse-click removes the active node while
|
||||||
|
// editing is active it does not know where to save the value to.
|
||||||
|
// For that reason do we save the node-name here. We could probably
|
||||||
|
// also just do that once on load but if Vue decides for some reason to
|
||||||
|
// reuse the input it could have the wrong value so lets set it everytime
|
||||||
|
// just to be sure
|
||||||
|
nodeName.value = node.value.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (inputField.value) {
|
||||||
|
if (hasFocusOnInput(inputField.value)) {
|
||||||
|
inputField.value.focusOnInput();
|
||||||
|
} else if (isFocusableEl(inputField.value)) {
|
||||||
|
inputField.value.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
isFocused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbaToHex(value: string): string | null {
|
||||||
|
// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
||||||
|
const valueMatch = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);
|
||||||
|
if (valueMatch === null) {
|
||||||
|
// TODO: Display something if value is not valid
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const [r, g, b, a] = valueMatch.splice(1, 4).map((v) => Number(v));
|
||||||
|
return (
|
||||||
|
'#' +
|
||||||
|
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) +
|
||||||
|
((1 << 8) + Math.floor((1 - a) * 255)).toString(16).slice(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function onTextInputChange(value: string) {
|
||||||
|
const parameterData = {
|
||||||
|
node: node.value ? node.value.name : nodeName.value,
|
||||||
|
name: props.path,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('textInput', parameterData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackWorkflowInputModeEvent(value: string) {
|
||||||
|
const telemetryValuesMap: Record<string, string> = {
|
||||||
|
workflowInputs: 'fields',
|
||||||
|
jsonExample: 'json',
|
||||||
|
passthrough: 'all',
|
||||||
|
};
|
||||||
|
telemetry.track('User chose input data mode', {
|
||||||
|
option: telemetryValuesMap[value],
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
node_id: node.value?.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueChanged(untypedValue: unknown) {
|
||||||
|
if (remoteParameterOptionsLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldValue = get(node.value, props.path) as unknown;
|
||||||
|
if (oldValue !== undefined && oldValue === untypedValue) {
|
||||||
|
// Skip emit if value hasn't changed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: NodeParameterValueType;
|
||||||
|
|
||||||
|
if (untypedValue instanceof Date) {
|
||||||
|
value = untypedValue.toISOString();
|
||||||
|
} else if (
|
||||||
|
typeof untypedValue === 'string' ||
|
||||||
|
typeof untypedValue === 'number' ||
|
||||||
|
typeof untypedValue === 'boolean' ||
|
||||||
|
untypedValue === null ||
|
||||||
|
Array.isArray(untypedValue)
|
||||||
|
) {
|
||||||
|
value = untypedValue;
|
||||||
|
} else if (typeof untypedValue === 'object' && untypedValue !== null && '__rl' in untypedValue) {
|
||||||
|
// likely INodeParameterResourceLocator
|
||||||
|
value = untypedValue as NodeParameterValueType;
|
||||||
|
} else {
|
||||||
|
// fallback
|
||||||
|
value = untypedValue as NodeParameterValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSpecializedEditor = props.parameter.typeOptions?.editor !== undefined;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!oldValue &&
|
||||||
|
oldValue !== undefined &&
|
||||||
|
shouldConvertToExpression(value, isSpecializedEditor)
|
||||||
|
) {
|
||||||
|
// if empty old value and updated value has an expression, add '=' prefix to switch to expression mode
|
||||||
|
value = '=' + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.parameter.name === 'nodeCredentialType') {
|
||||||
|
activeCredentialType.value = value as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = completeExpressionSyntax(value, isSpecializedEditor);
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.parameter.type === 'color' &&
|
||||||
|
getTypeOption('showAlpha') === true &&
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
(value as string).toString().charAt(0) !== '#'
|
||||||
|
) {
|
||||||
|
const newValue = rgbaToHex(value as string);
|
||||||
|
if (newValue !== null) {
|
||||||
|
tempValue.value = newValue;
|
||||||
|
value = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterData: IUpdateInformation = {
|
||||||
|
node: node.value ? node.value.name : nodeName.value,
|
||||||
|
name: props.path,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('update', parameterData);
|
||||||
|
|
||||||
|
if (props.parameter.name === 'operation' || props.parameter.name === 'mode') {
|
||||||
|
telemetry.track('User set node operation or mode', {
|
||||||
|
workflow_id: workflowsStore.workflowId,
|
||||||
|
node_type: node.value?.type,
|
||||||
|
resource: node.value?.parameters.resource,
|
||||||
|
is_custom: value === CUSTOM_API_CALL_KEY,
|
||||||
|
push_ref: ndvStore.pushRef,
|
||||||
|
parameter: props.parameter.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Track workflow input data mode change
|
||||||
|
const isWorkflowInputParameter =
|
||||||
|
props.parameter.name === 'inputSource' && props.parameter.default === 'workflowInputs';
|
||||||
|
if (isWorkflowInputParameter) {
|
||||||
|
trackWorkflowInputModeEvent(value as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 100 });
|
||||||
|
|
||||||
function expressionUpdated(value: string) {
|
function expressionUpdated(value: string) {
|
||||||
const val: NodeParameterValueType = isResourceLocatorParameter.value
|
const val: NodeParameterValueType = isResourceLocatorParameter.value
|
||||||
? { __rl: true, value, mode: modelValueResourceLocator.value.mode }
|
? { __rl: true, value, mode: modelValueResourceLocator.value.mode }
|
||||||
@@ -750,13 +911,6 @@ function expressionUpdated(value: string) {
|
|||||||
valueChanged(val);
|
valueChanged(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openExpressionEditorModal() {
|
|
||||||
if (!isModelValueExpression.value) return;
|
|
||||||
|
|
||||||
expressionEditDialogVisible.value = true;
|
|
||||||
trackExpressionEditOpen();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBlur() {
|
function onBlur() {
|
||||||
emit('blur');
|
emit('blur');
|
||||||
isFocused.value = false;
|
isFocused.value = false;
|
||||||
@@ -800,236 +954,50 @@ function onResourceLocatorDrop(data: string) {
|
|||||||
emit('drop', data);
|
emit('drop', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectInput() {
|
|
||||||
const inputRef = inputField.value;
|
|
||||||
if (inputRef) {
|
|
||||||
if ('select' in inputRef) {
|
|
||||||
inputRef.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setFocus() {
|
|
||||||
if (['json'].includes(props.parameter.type) && getArgument('alwaysOpenEditWindow')) {
|
|
||||||
displayEditDialog();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.value) {
|
|
||||||
// When an event like mouse-click removes the active node while
|
|
||||||
// editing is active it does not know where to save the value to.
|
|
||||||
// For that reason do we save the node-name here. We could probably
|
|
||||||
// also just do that once on load but if Vue decides for some reason to
|
|
||||||
// reuse the input it could have the wrong value so lets set it everytime
|
|
||||||
// just to be sure
|
|
||||||
nodeName.value = node.value.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const inputRef = inputField.value;
|
|
||||||
if (inputRef) {
|
|
||||||
if ('focusOnInput' in inputRef) {
|
|
||||||
inputRef.focusOnInput();
|
|
||||||
} else if (inputRef.focus) {
|
|
||||||
inputRef.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
isFocused.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('focus');
|
|
||||||
}
|
|
||||||
|
|
||||||
function rgbaToHex(value: string): string | null {
|
|
||||||
// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
|
||||||
const valueMatch = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);
|
|
||||||
if (valueMatch === null) {
|
|
||||||
// TODO: Display something if value is not valid
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const [r, g, b, a] = valueMatch.splice(1, 4).map((v) => Number(v));
|
|
||||||
return (
|
|
||||||
'#' +
|
|
||||||
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) +
|
|
||||||
((1 << 8) + Math.floor((1 - a) * 255)).toString(16).slice(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function onTextInputChange(value: string) {
|
|
||||||
const parameterData = {
|
|
||||||
node: node.value ? node.value.name : nodeName.value,
|
|
||||||
name: props.path,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('textInput', parameterData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 100 });
|
|
||||||
|
|
||||||
function onUpdateTextInput(value: string) {
|
function onUpdateTextInput(value: string) {
|
||||||
valueChanged(value);
|
valueChanged(value);
|
||||||
onTextInputChange(value);
|
onTextInputChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueChanged(value: NodeParameterValueType | {} | Date) {
|
|
||||||
if (remoteParameterOptionsLoading.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldValue = get(node.value, props.path);
|
|
||||||
|
|
||||||
if (oldValue !== undefined && oldValue === value) {
|
|
||||||
// Only update the value if it has changed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSpecializedEditor = props.parameter.typeOptions?.editor !== undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!oldValue &&
|
|
||||||
oldValue !== undefined &&
|
|
||||||
shouldConvertToExpression(value, isSpecializedEditor)
|
|
||||||
) {
|
|
||||||
// if empty old value and updated value has an expression, add '=' prefix to switch to expression mode
|
|
||||||
value = '=' + value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.parameter.name === 'nodeCredentialType') {
|
|
||||||
activeCredentialType.value = value as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = completeExpressionSyntax(value, isSpecializedEditor);
|
|
||||||
|
|
||||||
if (value instanceof Date) {
|
|
||||||
value = value.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.parameter.type === 'color' &&
|
|
||||||
getArgument('showAlpha') === true &&
|
|
||||||
value !== null &&
|
|
||||||
value !== undefined &&
|
|
||||||
(value as string).toString().charAt(0) !== '#'
|
|
||||||
) {
|
|
||||||
const newValue = rgbaToHex(value as string);
|
|
||||||
if (newValue !== null) {
|
|
||||||
tempValue.value = newValue;
|
|
||||||
value = newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parameterData = {
|
|
||||||
node: node.value ? node.value.name : nodeName.value,
|
|
||||||
name: props.path,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('update', parameterData);
|
|
||||||
|
|
||||||
if (props.parameter.name === 'operation' || props.parameter.name === 'mode') {
|
|
||||||
telemetry.track('User set node operation or mode', {
|
|
||||||
workflow_id: workflowsStore.workflowId,
|
|
||||||
node_type: node.value?.type,
|
|
||||||
resource: node.value?.parameters.resource,
|
|
||||||
is_custom: value === CUSTOM_API_CALL_KEY,
|
|
||||||
push_ref: ndvStore.pushRef,
|
|
||||||
parameter: props.parameter.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Track workflow input data mode change
|
|
||||||
const isWorkflowInputParameter =
|
|
||||||
props.parameter.name === 'inputSource' && props.parameter.default === 'workflowInputs';
|
|
||||||
if (isWorkflowInputParameter) {
|
|
||||||
trackWorkflowInputModeEvent(value as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackWorkflowInputModeEvent(value: string) {
|
|
||||||
const telemetryValuesMap: Record<string, string> = {
|
|
||||||
workflowInputs: 'fields',
|
|
||||||
jsonExample: 'json',
|
|
||||||
passthrough: 'all',
|
|
||||||
};
|
|
||||||
telemetry.track('User chose input data mode', {
|
|
||||||
option: telemetryValuesMap[value],
|
|
||||||
workflow_id: workflowsStore.workflowId,
|
|
||||||
node_id: node.value?.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function optionSelected(command: string) {
|
async function optionSelected(command: string) {
|
||||||
const prevValue = props.modelValue;
|
const prevValue = props.modelValue;
|
||||||
|
|
||||||
if (command === 'resetValue') {
|
switch (command) {
|
||||||
valueChanged(props.parameter.default);
|
case 'resetValue':
|
||||||
} else if (command === 'addExpression') {
|
return valueChanged(props.parameter.default);
|
||||||
if (isResourceLocatorParameter.value) {
|
|
||||||
if (isResourceLocatorValue(props.modelValue)) {
|
|
||||||
valueChanged({
|
|
||||||
__rl: true,
|
|
||||||
value: `=${props.modelValue.value}`,
|
|
||||||
mode: props.modelValue.mode,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
valueChanged({ __rl: true, value: `=${props.modelValue}`, mode: '' });
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
props.parameter.type === 'number' &&
|
|
||||||
(!props.modelValue || props.modelValue === '[Object: null]')
|
|
||||||
) {
|
|
||||||
valueChanged('={{ 0 }}');
|
|
||||||
} else if (props.parameter.type === 'multiOptions') {
|
|
||||||
valueChanged(`={{ ${JSON.stringify(props.modelValue)} }}`);
|
|
||||||
} else if (
|
|
||||||
props.parameter.type === 'number' ||
|
|
||||||
props.parameter.type === 'boolean' ||
|
|
||||||
typeof props.modelValue !== 'string'
|
|
||||||
) {
|
|
||||||
valueChanged(`={{ ${props.modelValue} }}`);
|
|
||||||
} else {
|
|
||||||
valueChanged(`=${props.modelValue}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
case 'addExpression':
|
||||||
|
valueChanged(formatAsExpression(props.modelValue, props.parameter.type));
|
||||||
await setFocus();
|
await setFocus();
|
||||||
} else if (command === 'removeExpression') {
|
break;
|
||||||
let value = props.expressionEvaluated;
|
|
||||||
|
|
||||||
|
case 'removeExpression':
|
||||||
isFocused.value = false;
|
isFocused.value = false;
|
||||||
|
valueChanged(
|
||||||
if (props.parameter.type === 'multiOptions' && typeof value === 'string') {
|
parseFromExpression(
|
||||||
value = (value || '')
|
props.modelValue,
|
||||||
.split(',')
|
props.expressionEvaluated,
|
||||||
.filter((valueItem) =>
|
props.parameter.type,
|
||||||
(parameterOptions.value ?? []).find((option) => option.value === valueItem),
|
props.parameter.default,
|
||||||
|
parameterOptions.value,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
|
|
||||||
if (isResourceLocatorParameter.value && isResourceLocatorValue(props.modelValue)) {
|
case 'refreshOptions':
|
||||||
valueChanged({ __rl: true, value, mode: props.modelValue.mode });
|
|
||||||
} else {
|
|
||||||
let newValue: NodeParameterValueType | {} = typeof value !== 'undefined' ? value : null;
|
|
||||||
|
|
||||||
if (props.parameter.type === 'string') {
|
|
||||||
// Strip the '=' from the beginning
|
|
||||||
newValue = modelValueString.value
|
|
||||||
? modelValueString.value.toString().replace(/^=+/, '')
|
|
||||||
: null;
|
|
||||||
} else if (newValue === null) {
|
|
||||||
// Invalid expressions land here
|
|
||||||
if (['number', 'boolean'].includes(props.parameter.type)) {
|
|
||||||
newValue = props.parameter.default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valueChanged(newValue);
|
|
||||||
}
|
|
||||||
} else if (command === 'refreshOptions') {
|
|
||||||
if (isResourceLocatorParameter.value) {
|
if (isResourceLocatorParameter.value) {
|
||||||
props.eventBus.emit('refreshList');
|
props.eventBus.emit('refreshList');
|
||||||
}
|
}
|
||||||
void loadRemoteParameterOptions();
|
void loadRemoteParameterOptions();
|
||||||
} else if (command === 'formatHtml') {
|
return;
|
||||||
|
|
||||||
|
case 'formatHtml':
|
||||||
htmlEditorEventBus.emit('format-html');
|
htmlEditorEventBus.emit('format-html');
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'focus':
|
||||||
|
nodeSettingsParameters.handleFocus(node.value, props.path, props.parameter);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.value && (command === 'addExpression' || command === 'removeExpression')) {
|
if (node.value && (command === 'addExpression' || command === 'removeExpression')) {
|
||||||
@@ -1045,22 +1013,6 @@ async function optionSelected(command: string) {
|
|||||||
telemetry.track('User switched parameter mode', telemetryPayload);
|
telemetry.track('User switched parameter mode', telemetryPayload);
|
||||||
void externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
|
void externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.value && command === 'focus') {
|
|
||||||
focusPanelStore.setFocusedNodeParameter({
|
|
||||||
nodeId: node.value.id,
|
|
||||||
parameterPath: props.path,
|
|
||||||
parameter: props.parameter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ndvStore.activeNode) {
|
|
||||||
ndvStore.activeNodeName = null;
|
|
||||||
// TODO: check what this does - close method on NodeDetailsView
|
|
||||||
ndvStore.resetNDVPushRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
focusPanelStore.focusPanelActive = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -1078,7 +1030,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
props.parameter.type === 'color' &&
|
props.parameter.type === 'color' &&
|
||||||
getArgument('showAlpha') === true &&
|
getTypeOption('showAlpha') === true &&
|
||||||
displayValue.value !== null &&
|
displayValue.value !== null &&
|
||||||
displayValue.value.toString().charAt(0) !== '#'
|
displayValue.value.toString().charAt(0) !== '#'
|
||||||
) {
|
) {
|
||||||
@@ -1090,6 +1042,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
void externalHooks.run('parameterInput.mount', {
|
void externalHooks.run('parameterInput.mount', {
|
||||||
parameter: props.parameter,
|
parameter: props.parameter,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion
|
||||||
inputFieldRef: inputField.value as InstanceType<typeof N8nInput>,
|
inputFieldRef: inputField.value as InstanceType<typeof N8nInput>,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1148,8 +1101,8 @@ watch(dependentParametersValues, async () => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
async () => {
|
() => {
|
||||||
if (props.parameter.type === 'color' && getArgument('showAlpha') === true) {
|
if (props.parameter.type === 'color' && getTypeOption('showAlpha') === true) {
|
||||||
// Do not set for color with alpha else wrong value gets displayed in field
|
// Do not set for color with alpha else wrong value gets displayed in field
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1344,7 +1297,7 @@ onUpdated(async () => {
|
|||||||
<SqlEditor
|
<SqlEditor
|
||||||
v-else-if="editorType === 'sqlEditor' && codeEditDialogVisible"
|
v-else-if="editorType === 'sqlEditor' && codeEditDialogVisible"
|
||||||
:model-value="modelValueString"
|
:model-value="modelValueString"
|
||||||
:dialect="getArgument('sqlDialect')"
|
:dialect="getTypeOption('sqlDialect')"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
fullscreen
|
fullscreen
|
||||||
@@ -1459,7 +1412,7 @@ onUpdated(async () => {
|
|||||||
<SqlEditor
|
<SqlEditor
|
||||||
v-else-if="editorType === 'sqlEditor'"
|
v-else-if="editorType === 'sqlEditor'"
|
||||||
:model-value="modelValueString"
|
:model-value="modelValueString"
|
||||||
:dialect="getArgument('sqlDialect')"
|
:dialect="getTypeOption('sqlDialect')"
|
||||||
:is-read-only="isReadOnly"
|
:is-read-only="isReadOnly"
|
||||||
:rows="editorRows"
|
:rows="editorRows"
|
||||||
@update:model-value="valueChangedDebounced"
|
@update:model-value="valueChangedDebounced"
|
||||||
@@ -1588,7 +1541,7 @@ onUpdated(async () => {
|
|||||||
:model-value="displayValue"
|
:model-value="displayValue"
|
||||||
:disabled="isReadOnly"
|
:disabled="isReadOnly"
|
||||||
:title="displayTitle"
|
:title="displayTitle"
|
||||||
:show-alpha="getArgument('showAlpha')"
|
:show-alpha="getTypeOption('showAlpha')"
|
||||||
@focus="setFocus"
|
@focus="setFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@update:model-value="valueChanged"
|
@update:model-value="valueChanged"
|
||||||
@@ -1634,9 +1587,9 @@ onUpdated(async () => {
|
|||||||
:size="inputSize"
|
:size="inputSize"
|
||||||
:model-value="displayValue"
|
:model-value="displayValue"
|
||||||
:controls="false"
|
:controls="false"
|
||||||
:max="getArgument('maxValue')"
|
:max="getTypeOption('maxValue')"
|
||||||
:min="getArgument('minValue')"
|
:min="getTypeOption('minValue')"
|
||||||
:precision="getArgument('numberPrecision')"
|
:precision="getTypeOption('numberPrecision')"
|
||||||
:disabled="isReadOnly"
|
:disabled="isReadOnly"
|
||||||
:class="{ 'ph-no-capture': shouldRedactValue }"
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
||||||
:title="displayTitle"
|
:title="displayTitle"
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
import { useNodeSettingsParameters } from './useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from './useNodeSettingsParameters';
|
||||||
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
import type { MockedStore } from '@/__tests__/utils';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
|
||||||
describe('useNodeSettingsParameters', () => {
|
describe('useNodeSettingsParameters', () => {
|
||||||
|
describe('setValue', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createTestingPinia());
|
setActivePinia(createTestingPinia());
|
||||||
});
|
});
|
||||||
@@ -11,30 +18,6 @@ describe('useNodeSettingsParameters', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('nameIsParameter', () => {
|
|
||||||
it.each([
|
|
||||||
['', false],
|
|
||||||
['parameters', false],
|
|
||||||
['parameters.', true],
|
|
||||||
['parameters.path.to.some', true],
|
|
||||||
['', false],
|
|
||||||
])('%s should be %s', (input, expected) => {
|
|
||||||
const { nameIsParameter } = useNodeSettingsParameters();
|
|
||||||
const result = nameIsParameter({ name: input } as never);
|
|
||||||
expect(result).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject path on other input', () => {
|
|
||||||
const { nameIsParameter } = useNodeSettingsParameters();
|
|
||||||
const result = nameIsParameter({
|
|
||||||
name: 'aName',
|
|
||||||
value: 'parameters.path.to.parameters',
|
|
||||||
} as never);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setValue', () => {
|
|
||||||
it('mutates nodeValues as expected', () => {
|
it('mutates nodeValues as expected', () => {
|
||||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
|
|
||||||
@@ -66,4 +49,76 @@ describe('useNodeSettingsParameters', () => {
|
|||||||
expect(nodeSettingsParameters.nodeValues.value.newProperty).toBe('newValue');
|
expect(nodeSettingsParameters.nodeValues.value.newProperty).toBe('newValue');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('handleFocus', () => {
|
||||||
|
let ndvStore: MockedStore<typeof useNDVStore>;
|
||||||
|
let focusPanelStore: MockedStore<typeof useFocusPanelStore>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
ndvStore = mockedStore(useNDVStore);
|
||||||
|
focusPanelStore = mockedStore(useFocusPanelStore);
|
||||||
|
|
||||||
|
ndvStore.activeNode = {
|
||||||
|
id: '123',
|
||||||
|
name: 'myParam',
|
||||||
|
parameters: {},
|
||||||
|
position: [0, 0],
|
||||||
|
type: 'test',
|
||||||
|
typeVersion: 1,
|
||||||
|
};
|
||||||
|
ndvStore.activeNodeName = 'Node1';
|
||||||
|
ndvStore.setActiveNodeName = vi.fn();
|
||||||
|
ndvStore.resetNDVPushRef = vi.fn();
|
||||||
|
focusPanelStore.setFocusedNodeParameter = vi.fn();
|
||||||
|
focusPanelStore.focusPanelActive = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets focused node parameter and activates panel', () => {
|
||||||
|
const { handleFocus } = useNodeSettingsParameters();
|
||||||
|
const node: INodeUi = {
|
||||||
|
id: '1',
|
||||||
|
name: 'Node1',
|
||||||
|
position: [0, 0],
|
||||||
|
typeVersion: 1,
|
||||||
|
type: 'test',
|
||||||
|
parameters: {},
|
||||||
|
};
|
||||||
|
const path = 'parameters.foo';
|
||||||
|
const parameter: INodeProperties = {
|
||||||
|
name: 'foo',
|
||||||
|
displayName: 'Foo',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFocus(node, path, parameter);
|
||||||
|
|
||||||
|
expect(focusPanelStore.setFocusedNodeParameter).toHaveBeenCalledWith({
|
||||||
|
nodeId: node.id,
|
||||||
|
parameterPath: path,
|
||||||
|
parameter,
|
||||||
|
});
|
||||||
|
expect(focusPanelStore.focusPanelActive).toBe(true);
|
||||||
|
|
||||||
|
expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(null);
|
||||||
|
expect(ndvStore.resetNDVPushRef).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if node is undefined', () => {
|
||||||
|
const { handleFocus } = useNodeSettingsParameters();
|
||||||
|
|
||||||
|
const parameter: INodeProperties = {
|
||||||
|
name: 'foo',
|
||||||
|
displayName: 'Foo',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFocus(undefined, 'parameters.foo', parameter);
|
||||||
|
|
||||||
|
expect(focusPanelStore.setFocusedNodeParameter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
import type { IUpdateInformation } from '@/Interface';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
|
import { ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
type INode,
|
type INode,
|
||||||
type INodeParameters,
|
type INodeParameters,
|
||||||
|
type INodeProperties,
|
||||||
type NodeParameterValue,
|
type NodeParameterValue,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
deepCopy,
|
deepCopy,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { useTelemetry } from './useTelemetry';
|
import { useTelemetry } from './useTelemetry';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useNodeHelpers } from './useNodeHelpers';
|
import { useNodeHelpers } from './useNodeHelpers';
|
||||||
import { useCanvasOperations } from './useCanvasOperations';
|
import { useCanvasOperations } from './useCanvasOperations';
|
||||||
import { useExternalHooks } from './useExternalHooks';
|
import { useExternalHooks } from './useExternalHooks';
|
||||||
import { ref } from 'vue';
|
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
import { updateDynamicConnections, updateParameterByPath } from '@/utils/nodeSettingsUtils';
|
import { updateDynamicConnections, updateParameterByPath } from '@/utils/nodeSettingsUtils';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||||
|
import { omitKey } from '@/utils/objectUtils';
|
||||||
|
|
||||||
export function useNodeSettingsParameters() {
|
export function useNodeSettingsParameters() {
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -43,7 +48,7 @@ export function useNodeSettingsParameters() {
|
|||||||
let lastNamePart: string | undefined = nameParts.pop();
|
let lastNamePart: string | undefined = nameParts.pop();
|
||||||
|
|
||||||
let isArray = false;
|
let isArray = false;
|
||||||
if (lastNamePart !== undefined && lastNamePart.includes('[')) {
|
if (lastNamePart?.includes('[')) {
|
||||||
// It includes an index so we have to extract it
|
// It includes an index so we have to extract it
|
||||||
const lastNameParts = lastNamePart.match(/(.*)\[(\d+)\]$/);
|
const lastNameParts = lastNamePart.match(/(.*)\[(\d+)\]$/);
|
||||||
if (lastNameParts) {
|
if (lastNameParts) {
|
||||||
@@ -59,8 +64,7 @@ export function useNodeSettingsParameters() {
|
|||||||
if (value === null) {
|
if (value === null) {
|
||||||
// Property should be deleted
|
// Property should be deleted
|
||||||
if (lastNamePart) {
|
if (lastNamePart) {
|
||||||
const { [lastNamePart]: removedNodeValue, ...remainingNodeValues } = nodeValues.value;
|
nodeValues.value = omitKey(nodeValues.value, lastNamePart);
|
||||||
nodeValues.value = remainingNodeValues;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Value should be set
|
// Value should be set
|
||||||
@@ -78,8 +82,7 @@ export function useNodeSettingsParameters() {
|
|||||||
| INodeParameters[];
|
| INodeParameters[];
|
||||||
|
|
||||||
if (lastNamePart && !Array.isArray(tempValue)) {
|
if (lastNamePart && !Array.isArray(tempValue)) {
|
||||||
const { [lastNamePart]: removedNodeValue, ...remainingNodeValues } = tempValue;
|
tempValue = omitKey(tempValue, lastNamePart);
|
||||||
tempValue = remainingNodeValues;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isArray && Array.isArray(tempValue) && tempValue.length === 0) {
|
if (isArray && Array.isArray(tempValue) && tempValue.length === 0) {
|
||||||
@@ -88,9 +91,7 @@ export function useNodeSettingsParameters() {
|
|||||||
lastNamePart = nameParts.pop();
|
lastNamePart = nameParts.pop();
|
||||||
tempValue = get(nodeValues.value, nameParts.join('.')) as INodeParameters;
|
tempValue = get(nodeValues.value, nameParts.join('.')) as INodeParameters;
|
||||||
if (lastNamePart) {
|
if (lastNamePart) {
|
||||||
const { [lastNamePart]: removedArrayNodeValue, ...remainingArrayNodeValues } =
|
tempValue = omitKey(tempValue, lastNamePart);
|
||||||
tempValue;
|
|
||||||
tempValue = remainingArrayNodeValues;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -114,12 +115,6 @@ export function useNodeSettingsParameters() {
|
|||||||
nodeValues.value = { ...nodeValues.value };
|
nodeValues.value = { ...nodeValues.value };
|
||||||
}
|
}
|
||||||
|
|
||||||
function nameIsParameter(
|
|
||||||
parameterData: IUpdateInformation,
|
|
||||||
): parameterData is IUpdateInformation & { name: `parameters.${string}` } {
|
|
||||||
return parameterData.name.startsWith('parameters.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateNodeParameter(
|
function updateNodeParameter(
|
||||||
parameterData: IUpdateInformation & { name: `parameters.${string}` },
|
parameterData: IUpdateInformation & { name: `parameters.${string}` },
|
||||||
newValue: NodeParameterValue,
|
newValue: NodeParameterValue,
|
||||||
@@ -221,11 +216,36 @@ export function useNodeSettingsParameters() {
|
|||||||
telemetry.trackNodeParametersValuesChange(nodeTypeDescription.name, parameterData);
|
telemetry.trackNodeParametersValuesChange(nodeTypeDescription.name, parameterData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFocus(node: INodeUi | undefined, path: string, parameter: INodeProperties) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const focusPanelStore = useFocusPanelStore();
|
||||||
|
|
||||||
|
focusPanelStore.setFocusedNodeParameter({
|
||||||
|
nodeId: node.id,
|
||||||
|
parameterPath: path,
|
||||||
|
parameter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ndvStore.activeNode) {
|
||||||
|
ndvStore.setActiveNodeName(null);
|
||||||
|
ndvStore.resetNDVPushRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusPanelStore.focusPanelActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSkipParamValidation(value: string | number | boolean | null) {
|
||||||
|
return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodeValues,
|
nodeValues,
|
||||||
setValue,
|
setValue,
|
||||||
updateParameterByPath,
|
updateParameterByPath,
|
||||||
updateNodeParameter,
|
updateNodeParameter,
|
||||||
nameIsParameter,
|
handleFocus,
|
||||||
|
shouldSkipParamValidation,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,8 +147,12 @@ export const completeExpressionSyntax = <T>(value: T, isSpecializedEditor = fals
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shouldConvertToExpression = <T>(value: T, isSpecializedEditor = false): boolean => {
|
export const shouldConvertToExpression = (
|
||||||
|
value: unknown,
|
||||||
|
isSpecializedEditor = false,
|
||||||
|
): value is string => {
|
||||||
if (isSpecializedEditor) return false;
|
if (isSpecializedEditor) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
typeof value === 'string' &&
|
typeof value === 'string' &&
|
||||||
!value.startsWith('=') &&
|
!value.startsWith('=') &&
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ import type {
|
|||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
|
INodePropertyOptions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { updateDynamicConnections, updateParameterByPath } from './nodeSettingsUtils';
|
import {
|
||||||
|
updateDynamicConnections,
|
||||||
|
updateParameterByPath,
|
||||||
|
nameIsParameter,
|
||||||
|
formatAsExpression,
|
||||||
|
parseFromExpression,
|
||||||
|
} from './nodeSettingsUtils';
|
||||||
import { SWITCH_NODE_TYPE } from '@/constants';
|
import { SWITCH_NODE_TYPE } from '@/constants';
|
||||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
|
|
||||||
@@ -14,6 +21,7 @@ describe('updateDynamicConnections', () => {
|
|||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove extra outputs when the number of outputs decreases', () => {
|
it('should remove extra outputs when the number of outputs decreases', () => {
|
||||||
const node = mock<INodeUi>({
|
const node = mock<INodeUi>({
|
||||||
name: 'TestNode',
|
name: 'TestNode',
|
||||||
@@ -282,3 +290,109 @@ describe('updateParameterByPath', () => {
|
|||||||
expect(nodeParameters.arrayParam).toEqual(['value1', 'value3']);
|
expect(nodeParameters.arrayParam).toEqual(['value1', 'value3']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('nameIsParameter', () => {
|
||||||
|
it.each([
|
||||||
|
['', false],
|
||||||
|
['parameters', false],
|
||||||
|
['parameters.', true],
|
||||||
|
['parameters.path.to.some', true],
|
||||||
|
['', false],
|
||||||
|
])('%s should be %s', (input, expected) => {
|
||||||
|
const result = nameIsParameter({ name: input } as never);
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject path on other input', () => {
|
||||||
|
const result = nameIsParameter({
|
||||||
|
name: 'aName',
|
||||||
|
value: 'parameters.path.to.parameters',
|
||||||
|
} as never);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatAsExpression', () => {
|
||||||
|
it('wraps string value with "="', () => {
|
||||||
|
expect(formatAsExpression('foo', 'string')).toBe('=foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps number value with "={{ }}"', () => {
|
||||||
|
expect(formatAsExpression(42, 'number')).toBe('={{ 42 }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps boolean value with "={{ }}"', () => {
|
||||||
|
expect(formatAsExpression(true, 'boolean')).toBe('={{ true }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps multiOptions value with "={{ }}" and stringifies', () => {
|
||||||
|
expect(formatAsExpression(['a', 'b'], 'multiOptions')).toBe('={{ ["a","b"] }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "={{ 0 }}" for number with empty value', () => {
|
||||||
|
expect(formatAsExpression('', 'number')).toBe('={{ 0 }}');
|
||||||
|
expect(formatAsExpression('[Object: null]', 'number')).toBe('={{ 0 }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wraps non-string, non-number, non-boolean value with "={{ }}"', () => {
|
||||||
|
expect(formatAsExpression({ foo: 'bar' }, 'string')).toBe('={{ [object Object] }}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles resourceLocator value', () => {
|
||||||
|
const value = { __rl: true, value: 'abc', mode: 'url' };
|
||||||
|
expect(formatAsExpression(value, 'resourceLocator')).toEqual({
|
||||||
|
__rl: true,
|
||||||
|
value: '=abc',
|
||||||
|
mode: 'url',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles resourceLocator value as string', () => {
|
||||||
|
expect(formatAsExpression('abc', 'resourceLocator')).toEqual({
|
||||||
|
__rl: true,
|
||||||
|
value: '=abc',
|
||||||
|
mode: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseFromExpression', () => {
|
||||||
|
it('removes expression from multiOptions string value', () => {
|
||||||
|
const options: INodePropertyOptions[] = [
|
||||||
|
{ name: 'Option A', value: 'a' },
|
||||||
|
{ name: 'Option B', value: 'b' },
|
||||||
|
{ name: 'Option C', value: 'c' },
|
||||||
|
];
|
||||||
|
expect(parseFromExpression('', 'a,b,c', 'multiOptions', [], options)).toEqual(['a', 'b', 'c']);
|
||||||
|
expect(parseFromExpression('', 'a,x', 'multiOptions', [], options)).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes expression from resourceLocator value', () => {
|
||||||
|
const modelValue = { __rl: true, value: '=abc', mode: 'url' };
|
||||||
|
expect(parseFromExpression(modelValue, 'abc', 'resourceLocator', '', [])).toEqual({
|
||||||
|
__rl: true,
|
||||||
|
value: 'abc',
|
||||||
|
mode: 'url',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes leading "=" from string parameter', () => {
|
||||||
|
expect(parseFromExpression('=foo', undefined, 'string', '', [])).toBe('foo');
|
||||||
|
expect(parseFromExpression('==bar', undefined, 'string', '', [])).toBe('bar');
|
||||||
|
expect(parseFromExpression('', undefined, 'string', '', [])).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns value if defined and not string/resourceLocator/multiOptions', () => {
|
||||||
|
expect(parseFromExpression(123, 456, 'number', 0, [])).toBe(456);
|
||||||
|
expect(parseFromExpression(true, false, 'boolean', true, [])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns defaultValue for number/boolean if value is undefined', () => {
|
||||||
|
expect(parseFromExpression(123, undefined, 'number', 0, [])).toBe(0);
|
||||||
|
expect(parseFromExpression(true, undefined, 'boolean', false, [])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for other types if value is undefined', () => {
|
||||||
|
expect(parseFromExpression({}, undefined, 'json', null, [])).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ import {
|
|||||||
type INode,
|
type INode,
|
||||||
type INodeParameters,
|
type INodeParameters,
|
||||||
type NodeParameterValue,
|
type NodeParameterValue,
|
||||||
|
type INodeProperties,
|
||||||
|
type INodePropertyOptions,
|
||||||
|
type INodePropertyCollection,
|
||||||
|
type NodePropertyTypes,
|
||||||
isINodePropertyCollectionList,
|
isINodePropertyCollectionList,
|
||||||
isINodePropertiesList,
|
isINodePropertiesList,
|
||||||
isINodePropertyOptionsList,
|
isINodePropertyOptionsList,
|
||||||
displayParameter,
|
displayParameter,
|
||||||
|
isResourceLocatorValue,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
import { SWITCH_NODE_TYPE } from '@/constants';
|
import { SWITCH_NODE_TYPE } from '@/constants';
|
||||||
@@ -21,6 +26,7 @@ import set from 'lodash/set';
|
|||||||
import unset from 'lodash/unset';
|
import unset from 'lodash/unset';
|
||||||
|
|
||||||
import { captureException } from '@sentry/vue';
|
import { captureException } from '@sentry/vue';
|
||||||
|
import { isPresent } from './typesUtils';
|
||||||
|
|
||||||
export function updateDynamicConnections(
|
export function updateDynamicConnections(
|
||||||
node: INodeUi,
|
node: INodeUi,
|
||||||
@@ -235,3 +241,99 @@ export function updateParameterByPath(
|
|||||||
|
|
||||||
return parameterPath;
|
return parameterPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getParameterTypeOption<T = string | number | boolean | undefined>(
|
||||||
|
parameter: INodeProperties,
|
||||||
|
optionName: string,
|
||||||
|
): T {
|
||||||
|
return parameter.typeOptions?.[optionName] as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isResourceLocatorParameterType(type: NodePropertyTypes) {
|
||||||
|
return type === 'resourceLocator' || type === 'workflowSelector';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidParameterOption(
|
||||||
|
option: INodePropertyOptions | INodeProperties | INodePropertyCollection,
|
||||||
|
): option is INodePropertyOptions {
|
||||||
|
return 'value' in option && isPresent(option.value) && isPresent(option.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nameIsParameter(
|
||||||
|
parameterData: IUpdateInformation,
|
||||||
|
): parameterData is IUpdateInformation & { name: `parameters.${string}` } {
|
||||||
|
return parameterData.name.startsWith('parameters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAsExpression(
|
||||||
|
value: NodeParameterValueType,
|
||||||
|
parameterType: NodePropertyTypes,
|
||||||
|
) {
|
||||||
|
if (isResourceLocatorParameterType(parameterType)) {
|
||||||
|
if (isResourceLocatorValue(value)) {
|
||||||
|
return {
|
||||||
|
__rl: true,
|
||||||
|
value: `=${value.value}`,
|
||||||
|
mode: value.mode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { __rl: true, value: `=${value as string}`, mode: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumber = parameterType === 'number';
|
||||||
|
const isBoolean = parameterType === 'boolean';
|
||||||
|
const isMultiOptions = parameterType === 'multiOptions';
|
||||||
|
|
||||||
|
if (isNumber && (!value || value === '[Object: null]')) {
|
||||||
|
return '={{ 0 }}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultiOptions) {
|
||||||
|
return `={{ ${JSON.stringify(value)} }}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNumber || isBoolean || typeof value !== 'string') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- stringified intentionally
|
||||||
|
return `={{ ${String(value)} }}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `=${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFromExpression(
|
||||||
|
currentParameterValue: NodeParameterValueType,
|
||||||
|
evaluatedExpressionValue: unknown,
|
||||||
|
parameterType: NodePropertyTypes,
|
||||||
|
defaultValue: NodeParameterValueType,
|
||||||
|
parameterOptions: INodePropertyOptions[] = [],
|
||||||
|
) {
|
||||||
|
if (parameterType === 'multiOptions' && typeof evaluatedExpressionValue === 'string') {
|
||||||
|
return evaluatedExpressionValue
|
||||||
|
.split(',')
|
||||||
|
.filter((valueItem) => parameterOptions.find((option) => option.value === valueItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isResourceLocatorParameterType(parameterType) &&
|
||||||
|
isResourceLocatorValue(currentParameterValue)
|
||||||
|
) {
|
||||||
|
return { __rl: true, value: evaluatedExpressionValue, mode: currentParameterValue.mode };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterType === 'string') {
|
||||||
|
return currentParameterValue
|
||||||
|
? (currentParameterValue as string).toString().replace(/^=+/, '')
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof evaluatedExpressionValue !== 'undefined') {
|
||||||
|
return evaluatedExpressionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['number', 'boolean'].includes(parameterType)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { isObjectOrArray, isObject, searchInObject, getObjectSizeInKB } from '@/utils/objectUtils';
|
import {
|
||||||
|
isObjectOrArray,
|
||||||
|
isObject,
|
||||||
|
searchInObject,
|
||||||
|
getObjectSizeInKB,
|
||||||
|
omitKey,
|
||||||
|
} from '@/utils/objectUtils';
|
||||||
|
|
||||||
const testData = [1, '', true, null, undefined, new Date(), () => {}].map((value) => [
|
const testData = [1, '', true, null, undefined, new Date(), () => {}].map((value) => [
|
||||||
value,
|
value,
|
||||||
@@ -154,4 +160,37 @@ describe('objectUtils', () => {
|
|||||||
expect(getObjectSizeInKB(obj)).toBe(0.02);
|
expect(getObjectSizeInKB(obj)).toBe(0.02);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('omitKey', () => {
|
||||||
|
it('should remove a top-level key from a flat object', () => {
|
||||||
|
const input = { a: 1, b: 2, c: 3 };
|
||||||
|
const result = omitKey(input, 'b');
|
||||||
|
expect(result).toEqual({ a: 1, c: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not mutate the original object', () => {
|
||||||
|
const input = { a: 1, b: 2 };
|
||||||
|
const copy = { ...input };
|
||||||
|
omitKey(input, 'b');
|
||||||
|
expect(input).toEqual(copy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same object if the key does not exist', () => {
|
||||||
|
const input = { a: 1, b: 2 };
|
||||||
|
const result = omitKey(input, 'z');
|
||||||
|
expect(result).toEqual(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove a key with an undefined value', () => {
|
||||||
|
const input = { a: 1, b: undefined };
|
||||||
|
const result = omitKey(input, 'b');
|
||||||
|
expect(result).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with nested objects but only remove the top-level key', () => {
|
||||||
|
const input = { a: { nested: true }, b: 2 };
|
||||||
|
const result = omitKey(input, 'a');
|
||||||
|
expect(result).toEqual({ b: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,3 +50,10 @@ export const getObjectSizeInKB = (obj: unknown): number => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function omitKey<T extends Record<string, unknown>, S extends string>(
|
||||||
|
obj: T,
|
||||||
|
key: S,
|
||||||
|
): Omit<T, S> {
|
||||||
|
return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key)) as Omit<T, S>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,3 +170,39 @@ export const tryToParseNumber = (value: string): number | string => {
|
|||||||
export function isPresent<T>(arg: T): arg is Exclude<T, null | undefined> {
|
export function isPresent<T>(arg: T): arg is Exclude<T, null | undefined> {
|
||||||
return arg !== null && arg !== undefined;
|
return arg !== null && arg !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isFocusableEl(el: unknown): el is HTMLElement & { focus: () => void } {
|
||||||
|
return (
|
||||||
|
typeof el === 'object' &&
|
||||||
|
el !== null &&
|
||||||
|
'focus' in el &&
|
||||||
|
typeof (el as { focus?: unknown }).focus === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlurrableEl(el: unknown): el is HTMLElement & { blur: () => void } {
|
||||||
|
return (
|
||||||
|
typeof el === 'object' &&
|
||||||
|
el !== null &&
|
||||||
|
'blur' in el &&
|
||||||
|
typeof (el as { blur?: unknown }).blur === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSelectableEl(el: unknown): el is HTMLInputElement | { select: () => void } {
|
||||||
|
return (
|
||||||
|
typeof el === 'object' &&
|
||||||
|
el !== null &&
|
||||||
|
'select' in el &&
|
||||||
|
typeof (el as { select?: unknown }).select === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasFocusOnInput(el: unknown): el is { focusOnInput: () => void } {
|
||||||
|
return (
|
||||||
|
typeof el === 'object' &&
|
||||||
|
el !== null &&
|
||||||
|
'focusOnInput' in el &&
|
||||||
|
typeof (el as { focusOnInput?: unknown }).focusOnInput === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user