mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(editor): Move optionSelected logic to a composable (#16968)
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user