refactor(editor): Move optionSelected logic to a composable (#16968)

This commit is contained in:
Daria
2025-07-07 15:20:06 +03:00
committed by GitHub
parent 32bb33bd65
commit 534f34d4c1
10 changed files with 764 additions and 433 deletions

View File

@@ -18,7 +18,6 @@ import type {
INodeParameterResourceLocator,
INodeParameters,
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
IParameterLabel,
NodeParameterValueType,
@@ -37,6 +36,13 @@ import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import TextEdit from '@/components/TextEdit.vue';
import {
formatAsExpression,
getParameterTypeOption,
isResourceLocatorParameterType,
isValidParameterOption,
parseFromExpression,
} from '@/utils/nodeSettingsUtils';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
import {
@@ -54,23 +60,23 @@ import { useI18n } from '@n8n/i18n';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useTelemetry } from '@/composables/useTelemetry';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
import { htmlEditorEventBus } from '@/event-bus';
import { useCredentialsStore } from '@/stores/credentials.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSettingsStore } from '@/stores/settings.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 type { EventBus } from '@n8n/utils/event-bus';
import { createEventBus } from '@n8n/utils/event-bus';
import { useElementSize } from '@vueuse/core';
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 { isPresent } from '@/utils/typesUtils';
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 };
@@ -125,6 +131,7 @@ const i18n = useI18n();
const nodeHelpers = useNodeHelpers();
const { debounce } = useDebounce();
const workflowHelpers = useWorkflowHelpers();
const nodeSettingsParameters = useNodeSettingsParameters();
const telemetry = useTelemetry();
const credentialsStore = useCredentialsStore();
@@ -133,7 +140,6 @@ const workflowsStore = useWorkflowsStore();
const settingsStore = useSettingsStore();
const nodeTypesStore = useNodeTypesStore();
const uiStore = useUIStore();
const focusPanelStore = useFocusPanelStore();
// ESLint: false positive
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
@@ -181,6 +187,77 @@ const dateTimePickerOptions = ref({
});
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(() => {
if (remoteParameterOptionsLoadingIssues.value) {
if (!nodeType.value || nodeType.value?.codex?.categories?.includes(CORE_NODES_CATEGORY)) {
@@ -234,7 +311,7 @@ const displayValue = computed(() => {
if (
Array.isArray(returnValue) &&
props.parameter.type === 'color' &&
getArgument('showAlpha') === true &&
getTypeOption('showAlpha') === true &&
(returnValue as unknown as string).charAt(0) === '#'
) {
// Convert the value to rgba that el-color-picker can display it correctly
@@ -273,10 +350,8 @@ const expressionDisplayValue = computed(() => {
return `${displayValue.value ?? ''}`;
});
const isModelValueExpression = computed(() => isValueExpression(props.parameter, props.modelValue));
const dependentParametersValues = computed<string | null>(() => {
const loadOptionsDependsOn = getArgument<string[] | undefined>('loadOptionsDependsOn');
const loadOptionsDependsOn = getTypeOption<string[] | undefined>('loadOptionsDependsOn');
if (loadOptionsDependsOn === undefined) {
return null;
@@ -293,32 +368,13 @@ const dependentParametersValues = computed<string | null>(() => {
}
return returnValues.join('|');
} catch (error) {
} catch {
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(() => {
if (getArgument('password') === true) {
if (getTypeOption('password') === true) {
return 'password';
}
@@ -370,7 +426,7 @@ const getIssues = computed<string[]>(() => {
let checkValues: string[] = [];
if (!skipCheck(displayValue.value)) {
if (!nodeSettingsParameters.shouldSkipParamValidation(displayValue.value)) {
if (Array.isArray(displayValue.value)) {
checkValues = checkValues.concat(displayValue.value);
} else {
@@ -380,9 +436,7 @@ const getIssues = computed<string[]>(() => {
for (const checkValue of checkValues) {
if (checkValue === null || !validOptions.includes(checkValue)) {
if (issues.parameters === undefined) {
issues.parameters = {};
}
issues.parameters = issues.parameters ?? {};
const issue = i18n.baseText('parameterInput.theValueIsNotSupported', {
interpolate: { checkValue },
@@ -392,9 +446,7 @@ const getIssues = computed<string[]>(() => {
}
}
} else if (remoteParameterOptionsLoadingIssues.value !== null && !isModelValueExpression.value) {
if (issues.parameters === undefined) {
issues.parameters = {};
}
issues.parameters = issues.parameters ?? {};
issues.parameters[props.parameter.name] = [
`There was a problem loading the parameter options from server: "${remoteParameterOptionsLoadingIssues.value}"`,
];
@@ -406,9 +458,7 @@ const getIssues = computed<string[]>(() => {
);
if (isSelectedArchived) {
if (issues.parameters === undefined) {
issues.parameters = {};
}
issues.parameters = issues.parameters ?? {};
const issue = i18n.baseText('parameterInput.selectedWorkflowIsArchived');
issues.parameters[props.parameter.name] = [issue];
}
@@ -422,6 +472,20 @@ const getIssues = computed<string[]>(() => {
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(
() =>
props.parameter.type !== 'credentialsSelect' &&
@@ -429,26 +493,6 @@ const displayIssues = computed(
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(
() => props.parameter.type === 'boolean' && !isModelValueExpression.value,
);
@@ -500,28 +544,10 @@ const parameterInputWrapperStyle = computed(() => {
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(() => {
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[]>(() => {
return (remoteParameterOptions.value || []).map((o) => o.name);
});
@@ -530,28 +556,6 @@ const shouldRedactValue = computed<boolean>(() => {
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(
() => !!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 isDropDisabled = computed(
() =>
props.parameter.noDataExpression ||
props.parameter.noDataExpression === true ||
props.isReadOnly ||
isResourceLocatorParameter.value ||
isModelValueExpression.value,
@@ -581,18 +585,7 @@ const showDragnDropTip = computed(
!props.isForCredential,
);
const shouldCaptureForPosthog = computed(() => {
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);
}
const shouldCaptureForPosthog = computed(() => node.value?.type === AI_TRANSFORM_NODE_TYPE);
function isRemoteParameterOption(option: INodePropertyOptions) {
return remoteParameterOptionsKeys.value.includes(option.name);
@@ -612,13 +605,6 @@ function credentialSelected(updateInformation: INodeUpdatePropertiesInformation)
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 {
return props.isForCredential
? i18n.credText(uiStore.activeCredentialType).placeholder(props.parameter)
@@ -662,8 +648,8 @@ async function loadRemoteParameterOptions() {
props.parameter,
currentNodeParameters,
) as INodeParameters;
const loadOptionsMethod = getArgument<string | undefined>('loadOptionsMethod');
const loadOptions = getArgument<ILoadOptions | undefined>('loadOptions');
const loadOptionsMethod = getTypeOption<string | undefined>('loadOptionsMethod');
const loadOptions = getTypeOption<ILoadOptions | undefined>('loadOptions');
const options = await nodeTypesStore.getNodeParameterOptions({
nodeTypeAndVersion: {
@@ -678,8 +664,12 @@ async function loadRemoteParameterOptions() {
});
remoteParameterOptions.value = remoteParameterOptions.value.concat(options);
} catch (error) {
remoteParameterOptionsLoadingIssues.value = error.message;
} catch (error: unknown) {
if (error instanceof Error) {
remoteParameterOptionsLoadingIssues.value = error.message;
} else {
remoteParameterOptionsLoadingIssues.value = String(error);
}
}
remoteParameterOptionsLoading.value = false;
@@ -717,12 +707,14 @@ function trackExpressionEditOpen() {
}
}
async function closeTextEditDialog() {
function closeTextEditDialog() {
textEditDialogVisible.value = false;
editDialogClosing.value = true;
void nextTick().then(() => {
inputField.value?.blur?.();
if (isBlurrableEl(inputField.value)) {
inputField.value.blur();
}
editDialogClosing.value = false;
});
}
@@ -739,10 +731,179 @@ function displayEditDialog() {
}
}
function getArgument<T = string | number | boolean | undefined>(argumentName: string): T {
return props.parameter.typeOptions?.[argumentName];
function openExpressionEditorModal() {
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) {
const val: NodeParameterValueType = isResourceLocatorParameter.value
? { __rl: true, value, mode: modelValueResourceLocator.value.mode }
@@ -750,13 +911,6 @@ function expressionUpdated(value: string) {
valueChanged(val);
}
function openExpressionEditorModal() {
if (!isModelValueExpression.value) return;
expressionEditDialogVisible.value = true;
trackExpressionEditOpen();
}
function onBlur() {
emit('blur');
isFocused.value = false;
@@ -800,236 +954,50 @@ function onResourceLocatorDrop(data: string) {
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) {
valueChanged(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) {
const prevValue = props.modelValue;
if (command === 'resetValue') {
valueChanged(props.parameter.default);
} else if (command === 'addExpression') {
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: '' });
switch (command) {
case 'resetValue':
return valueChanged(props.parameter.default);
case 'addExpression':
valueChanged(formatAsExpression(props.modelValue, props.parameter.type));
await setFocus();
break;
case 'removeExpression':
isFocused.value = false;
valueChanged(
parseFromExpression(
props.modelValue,
props.expressionEvaluated,
props.parameter.type,
props.parameter.default,
parameterOptions.value,
),
);
break;
case 'refreshOptions':
if (isResourceLocatorParameter.value) {
props.eventBus.emit('refreshList');
}
} 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}`);
}
void loadRemoteParameterOptions();
return;
await setFocus();
} else if (command === 'removeExpression') {
let value = props.expressionEvaluated;
case 'formatHtml':
htmlEditorEventBus.emit('format-html');
return;
isFocused.value = false;
if (props.parameter.type === 'multiOptions' && typeof value === 'string') {
value = (value || '')
.split(',')
.filter((valueItem) =>
(parameterOptions.value ?? []).find((option) => option.value === valueItem),
);
}
if (isResourceLocatorParameter.value && isResourceLocatorValue(props.modelValue)) {
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) {
props.eventBus.emit('refreshList');
}
void loadRemoteParameterOptions();
} else if (command === 'formatHtml') {
htmlEditorEventBus.emit('format-html');
case 'focus':
nodeSettingsParameters.handleFocus(node.value, props.path, props.parameter);
return;
}
if (node.value && (command === 'addExpression' || command === 'removeExpression')) {
@@ -1045,22 +1013,6 @@ async function optionSelected(command: string) {
telemetry.track('User switched parameter mode', 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(() => {
@@ -1078,7 +1030,7 @@ onMounted(() => {
if (
props.parameter.type === 'color' &&
getArgument('showAlpha') === true &&
getTypeOption('showAlpha') === true &&
displayValue.value !== null &&
displayValue.value.toString().charAt(0) !== '#'
) {
@@ -1090,6 +1042,7 @@ onMounted(() => {
void externalHooks.run('parameterInput.mount', {
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>,
});
});
@@ -1148,8 +1101,8 @@ watch(dependentParametersValues, async () => {
watch(
() => 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
return;
}
@@ -1344,7 +1297,7 @@ onUpdated(async () => {
<SqlEditor
v-else-if="editorType === 'sqlEditor' && codeEditDialogVisible"
:model-value="modelValueString"
:dialect="getArgument('sqlDialect')"
:dialect="getTypeOption('sqlDialect')"
:is-read-only="isReadOnly"
:rows="editorRows"
fullscreen
@@ -1459,7 +1412,7 @@ onUpdated(async () => {
<SqlEditor
v-else-if="editorType === 'sqlEditor'"
:model-value="modelValueString"
:dialect="getArgument('sqlDialect')"
:dialect="getTypeOption('sqlDialect')"
:is-read-only="isReadOnly"
:rows="editorRows"
@update:model-value="valueChangedDebounced"
@@ -1588,7 +1541,7 @@ onUpdated(async () => {
:model-value="displayValue"
:disabled="isReadOnly"
:title="displayTitle"
:show-alpha="getArgument('showAlpha')"
:show-alpha="getTypeOption('showAlpha')"
@focus="setFocus"
@blur="onBlur"
@update:model-value="valueChanged"
@@ -1634,9 +1587,9 @@ onUpdated(async () => {
:size="inputSize"
:model-value="displayValue"
:controls="false"
:max="getArgument('maxValue')"
:min="getArgument('minValue')"
:precision="getArgument('numberPrecision')"
:max="getTypeOption('maxValue')"
:min="getTypeOption('minValue')"
:precision="getTypeOption('numberPrecision')"
:disabled="isReadOnly"
:class="{ 'ph-no-capture': shouldRedactValue }"
:title="displayTitle"