mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Show the right editor in focus panel (#17062)
Co-authored-by: Charlie Kolb <charlie@n8n.io>
This commit is contained in:
@@ -1497,6 +1497,7 @@
|
|||||||
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
"nodeView.deletesTheCurrentExecutionData": "Deletes the current execution data",
|
||||||
"nodeView.focusPanel.title": "Focus",
|
"nodeView.focusPanel.title": "Focus",
|
||||||
"nodeView.focusPanel.noParameters": "No parameters focused. Focus a parameter by clicking on the action dropdown in the node detail view.",
|
"nodeView.focusPanel.noParameters": "No parameters focused. Focus a parameter by clicking on the action dropdown in the node detail view.",
|
||||||
|
"nodeView.focusPanel.missingParameter": "This parameter is no longer visible on the node. A related parameter was likely changed, removing this one.",
|
||||||
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
"nodeView.itLooksLikeYouHaveBeenEditingSomething": "It looks like you made some edits. If you leave before saving, your changes will be lost.",
|
||||||
"nodeView.loadingTemplate": "Loading template",
|
"nodeView.loadingTemplate": "Loading template",
|
||||||
"nodeView.moreInfo": "More info",
|
"nodeView.moreInfo": "More info",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { CODE_PLACEHOLDERS } from './constants';
|
|||||||
import { useLinter } from './linter';
|
import { useLinter } from './linter';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop';
|
import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop';
|
||||||
|
import type { TargetNodeParameterContext } from '@/Interface';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mode: CodeExecutionMode;
|
mode: CodeExecutionMode;
|
||||||
@@ -29,6 +30,8 @@ type Props = {
|
|||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
rows?: number;
|
rows?: number;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
targetNodeParameterContext?: TargetNodeParameterContext;
|
||||||
|
disableAskAi?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -38,6 +41,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
rows: 4,
|
rows: 4,
|
||||||
id: () => crypto.randomUUID(),
|
id: () => crypto.randomUUID(),
|
||||||
|
targetNodeParameterContext: undefined,
|
||||||
|
disableAskAi: false,
|
||||||
});
|
});
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:modelValue': [value: string];
|
'update:modelValue': [value: string];
|
||||||
@@ -81,6 +86,7 @@ const { highlightLine, readEditorValue, editor } = useCodeEditor({
|
|||||||
rows: props.rows,
|
rows: props.rows,
|
||||||
},
|
},
|
||||||
onChange: onEditorUpdate,
|
onChange: onEditorUpdate,
|
||||||
|
targetNodeParameterContext: () => props.targetNodeParameterContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -98,7 +104,9 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const askAiEnabled = computed(() => {
|
const askAiEnabled = computed(() => {
|
||||||
return settingsStore.isAskAiEnabled && props.language === 'javaScript';
|
return (
|
||||||
|
props.disableAskAi !== true && settingsStore.isAskAiEnabled && props.language === 'javaScript'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch([() => props.language, () => props.mode], (_, [prevLanguage, prevMode]) => {
|
watch([() => props.language, () => props.mode], (_, [prevLanguage, prevMode]) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Props = {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
targetNodeParameterContext: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -85,7 +86,7 @@ onMounted(() => {
|
|||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({ editor });
|
defineExpose({ editor, focus });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -2,23 +2,53 @@
|
|||||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { N8nText, N8nInput } from '@n8n/design-system';
|
import { N8nText, N8nInput } from '@n8n/design-system';
|
||||||
import { computed } from 'vue';
|
import { computed, nextTick, ref } from 'vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import {
|
||||||
|
formatAsExpression,
|
||||||
|
getParameterTypeOption,
|
||||||
|
isValidParameterOption,
|
||||||
|
parseFromExpression,
|
||||||
|
} from '@/utils/nodeSettingsUtils';
|
||||||
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
import { isValueExpression } from '@/utils/nodeTypesUtils';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
|
import { useResolvedExpression } from '@/composables/useResolvedExpression';
|
||||||
|
import {
|
||||||
|
AI_TRANSFORM_NODE_TYPE,
|
||||||
|
type CodeExecutionMode,
|
||||||
|
type CodeNodeEditorLanguage,
|
||||||
|
type EditorType,
|
||||||
|
HTML_NODE_TYPE,
|
||||||
|
isResourceLocatorValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||||
|
import { useDebounce } from '@/composables/useDebounce';
|
||||||
|
import { htmlEditorEventBus } from '@/event-bus';
|
||||||
|
import { hasFocusOnInput, isFocusableEl } from '@/utils/typesUtils';
|
||||||
|
import type { TargetNodeParameterContext } from '@/Interface';
|
||||||
|
|
||||||
defineOptions({ name: 'FocusPanel' });
|
defineOptions({ name: 'FocusPanel' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
executable: boolean;
|
isCanvasReadOnly: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
focus: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// ESLint: false positive
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||||
|
const inputField = ref<InstanceType<typeof N8nInput> | HTMLElement>();
|
||||||
|
|
||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const focusPanelStore = useFocusPanelStore();
|
const focusPanelStore = useFocusPanelStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const nodeSettingsParameters = useNodeSettingsParameters();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
|
const environmentsStore = useEnvironmentsStore();
|
||||||
|
const { debounce } = useDebounce();
|
||||||
|
|
||||||
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
const focusedNodeParameter = computed(() => focusPanelStore.focusedNodeParameters[0]);
|
||||||
const resolvedParameter = computed(() =>
|
const resolvedParameter = computed(() =>
|
||||||
@@ -29,32 +59,119 @@ const resolvedParameter = computed(() =>
|
|||||||
|
|
||||||
const focusPanelActive = computed(() => focusPanelStore.focusPanelActive);
|
const focusPanelActive = computed(() => focusPanelStore.focusPanelActive);
|
||||||
|
|
||||||
|
const isDisabled = computed(() => {
|
||||||
|
if (!resolvedParameter.value) return false;
|
||||||
|
|
||||||
|
// shouldDisplayNodeParameter returns true if disabledOptions exists and matches, OR if disabledOptions doesn't exist
|
||||||
|
return (
|
||||||
|
!!resolvedParameter.value.parameter.disabledOptions &&
|
||||||
|
nodeSettingsParameters.shouldDisplayNodeParameter(
|
||||||
|
resolvedParameter.value.node.parameters,
|
||||||
|
resolvedParameter.value.node,
|
||||||
|
resolvedParameter.value.parameter,
|
||||||
|
'',
|
||||||
|
'disabledOptions',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDisplayed = computed(() => {
|
||||||
|
if (!resolvedParameter.value) return true;
|
||||||
|
|
||||||
|
return nodeSettingsParameters.shouldDisplayNodeParameter(
|
||||||
|
resolvedParameter.value.node.parameters,
|
||||||
|
resolvedParameter.value.node,
|
||||||
|
resolvedParameter.value.parameter,
|
||||||
|
'',
|
||||||
|
'displayOptions',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const isExecutable = computed(() => {
|
const isExecutable = computed(() => {
|
||||||
if (!resolvedParameter.value) return false;
|
if (!resolvedParameter.value) return false;
|
||||||
|
|
||||||
|
if (!isDisplayed.value) return false;
|
||||||
|
|
||||||
const foreignCredentials = nodeHelpers.getForeignCredentialsIfSharingEnabled(
|
const foreignCredentials = nodeHelpers.getForeignCredentialsIfSharingEnabled(
|
||||||
resolvedParameter.value.node.credentials,
|
resolvedParameter.value.node.credentials,
|
||||||
);
|
);
|
||||||
return nodeHelpers.isNodeExecutable(
|
return nodeHelpers.isNodeExecutable(
|
||||||
resolvedParameter.value.node,
|
resolvedParameter.value.node,
|
||||||
props.executable,
|
!props.isCanvasReadOnly,
|
||||||
foreignCredentials,
|
foreignCredentials,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getTypeOption<T>(optionName: string): T | undefined {
|
||||||
|
return resolvedParameter.value
|
||||||
|
? getParameterTypeOption<T>(resolvedParameter.value.parameter, optionName)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeEditorMode = computed<CodeExecutionMode>(() => {
|
||||||
|
return resolvedParameter.value?.node.parameters.mode as CodeExecutionMode;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorType = computed<EditorType | 'json' | 'code' | 'cssEditor' | undefined>(() => {
|
||||||
|
return getTypeOption('editor') ?? undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorLanguage = computed<CodeNodeEditorLanguage>(() => {
|
||||||
|
if (editorType.value === 'json' || resolvedParameter.value?.parameter.type === 'json')
|
||||||
|
return 'json' as CodeNodeEditorLanguage;
|
||||||
|
|
||||||
|
return getTypeOption('editorLanguage') ?? 'javaScript';
|
||||||
|
});
|
||||||
|
|
||||||
|
const editorRows = computed(() => getTypeOption<number>('rows'));
|
||||||
|
|
||||||
const isToolNode = computed(() =>
|
const isToolNode = computed(() =>
|
||||||
resolvedParameter.value ? nodeTypesStore.isToolNode(resolvedParameter.value?.node.type) : false,
|
resolvedParameter.value ? nodeTypesStore.isToolNode(resolvedParameter.value?.node.type) : false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isHtmlNode = computed(
|
||||||
|
() => !!resolvedParameter.value && resolvedParameter.value.node.type === HTML_NODE_TYPE,
|
||||||
|
);
|
||||||
|
|
||||||
const expressionModeEnabled = computed(
|
const expressionModeEnabled = computed(
|
||||||
() =>
|
() =>
|
||||||
resolvedParameter.value &&
|
resolvedParameter.value &&
|
||||||
isValueExpression(resolvedParameter.value.parameter, resolvedParameter.value.value),
|
isValueExpression(resolvedParameter.value.parameter, resolvedParameter.value.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
function optionSelected() {
|
const expression = computed(() => {
|
||||||
// TODO: Handle the option selected (command: string) from the dropdown
|
if (!expressionModeEnabled.value) return '';
|
||||||
}
|
return isResourceLocatorValue(resolvedParameter.value)
|
||||||
|
? resolvedParameter.value.value
|
||||||
|
: resolvedParameter.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldCaptureForPosthog = computed(
|
||||||
|
() => resolvedParameter.value?.node.type === AI_TRANSFORM_NODE_TYPE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isReadOnly = computed(() => props.isCanvasReadOnly || isDisabled.value);
|
||||||
|
|
||||||
|
const resolvedAdditionalExpressionData = computed(() => {
|
||||||
|
return {
|
||||||
|
$vars: environmentsStore.variablesAsObject,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetNodeParameterContext = computed<TargetNodeParameterContext | undefined>(() => {
|
||||||
|
if (!resolvedParameter.value) return undefined;
|
||||||
|
return {
|
||||||
|
nodeName: resolvedParameter.value.node.name,
|
||||||
|
parameterPath: resolvedParameter.value.parameterPath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resolvedExpression } = useResolvedExpression({
|
||||||
|
expression,
|
||||||
|
additionalData: resolvedAdditionalExpressionData,
|
||||||
|
stringifyObject:
|
||||||
|
resolvedParameter.value && resolvedParameter.value.parameter.type !== 'multiOptions',
|
||||||
|
});
|
||||||
|
|
||||||
function valueChanged(value: string) {
|
function valueChanged(value: string) {
|
||||||
if (resolvedParameter.value === undefined) {
|
if (resolvedParameter.value === undefined) {
|
||||||
@@ -68,6 +185,65 @@ function valueChanged(value: string) {
|
|||||||
isToolNode.value,
|
isToolNode.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setFocus() {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (inputField.value) {
|
||||||
|
if (hasFocusOnInput(inputField.value)) {
|
||||||
|
inputField.value.focusOnInput();
|
||||||
|
} else if (isFocusableEl(inputField.value)) {
|
||||||
|
inputField.value.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('focus');
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionSelected(command: string) {
|
||||||
|
if (!resolvedParameter.value) return;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'resetValue':
|
||||||
|
return (
|
||||||
|
typeof resolvedParameter.value.parameter.default === 'string' &&
|
||||||
|
valueChanged(resolvedParameter.value.parameter.default)
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'addExpression': {
|
||||||
|
const newValue = formatAsExpression(
|
||||||
|
resolvedParameter.value.value,
|
||||||
|
resolvedParameter.value.parameter.type,
|
||||||
|
);
|
||||||
|
valueChanged(typeof newValue === 'string' ? newValue : newValue.value);
|
||||||
|
void setFocus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'removeExpression': {
|
||||||
|
const newValue = parseFromExpression(
|
||||||
|
resolvedParameter.value.value,
|
||||||
|
resolvedExpression.value,
|
||||||
|
resolvedParameter.value.parameter.type,
|
||||||
|
resolvedParameter.value.parameter.default,
|
||||||
|
(resolvedParameter.value.parameter.options ?? []).filter(isValidParameterOption),
|
||||||
|
);
|
||||||
|
if (typeof newValue === 'string') {
|
||||||
|
valueChanged(newValue);
|
||||||
|
} else if (newValue && typeof (newValue as { value?: unknown }).value === 'string') {
|
||||||
|
valueChanged((newValue as { value: string }).value);
|
||||||
|
}
|
||||||
|
void setFocus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'formatHtml':
|
||||||
|
htmlEditorEventBus.emit('format-html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueChangedDebounced = debounce(valueChanged, { debounceTime: 0 });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -104,34 +280,96 @@ function valueChanged(value: string) {
|
|||||||
<div :class="$style.parameterOptionsWrapper">
|
<div :class="$style.parameterOptionsWrapper">
|
||||||
<div></div>
|
<div></div>
|
||||||
<ParameterOptions
|
<ParameterOptions
|
||||||
|
v-if="isDisplayed"
|
||||||
:parameter="resolvedParameter.parameter"
|
:parameter="resolvedParameter.parameter"
|
||||||
:value="resolvedParameter.value"
|
:value="resolvedParameter.value"
|
||||||
:is-read-only="false"
|
:is-read-only="isReadOnly"
|
||||||
@update:model-value="optionSelected"
|
@update:model-value="optionSelected"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="typeof resolvedParameter.value === 'string'" :class="$style.editorContainer">
|
<div v-if="typeof resolvedParameter.value === 'string'" :class="$style.editorContainer">
|
||||||
|
<div v-if="!isDisplayed" :class="[$style.content, $style.emptyContent]">
|
||||||
|
<div :class="$style.emptyText">
|
||||||
|
<N8nText color="text-base">
|
||||||
|
{{ locale.baseText('nodeView.focusPanel.missingParameter') }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ExpressionEditorModalInput
|
<ExpressionEditorModalInput
|
||||||
v-if="expressionModeEnabled"
|
v-else-if="expressionModeEnabled"
|
||||||
|
ref="inputField"
|
||||||
:model-value="resolvedParameter.value"
|
:model-value="resolvedParameter.value"
|
||||||
:class="$style.editor"
|
:class="$style.editor"
|
||||||
:is-read-only="false"
|
:is-read-only="isReadOnly"
|
||||||
:path="resolvedParameter.parameterPath"
|
:path="resolvedParameter.parameterPath"
|
||||||
data-test-id="expression-modal-input"
|
data-test-id="expression-modal-input"
|
||||||
:target-node-parameter-context="{
|
:target-node-parameter-context="targetNodeParameterContext"
|
||||||
nodeName: resolvedParameter.node.name,
|
@change="valueChangedDebounced($event.value)"
|
||||||
parameterPath: resolvedParameter.parameterPath,
|
|
||||||
}"
|
|
||||||
@change="valueChanged($event.value)"
|
|
||||||
/>
|
/>
|
||||||
<N8nInput
|
<template v-else-if="['json', 'string'].includes(resolvedParameter.parameter.type)">
|
||||||
v-else
|
<CodeNodeEditor
|
||||||
:model-value="resolvedParameter.value"
|
v-if="editorType === 'codeNodeEditor'"
|
||||||
:class="$style.editor"
|
:id="resolvedParameter.parameterPath"
|
||||||
type="textarea"
|
:mode="codeEditorMode"
|
||||||
resize="none"
|
:model-value="resolvedParameter.value"
|
||||||
@update:model-value="valueChanged($event)"
|
:default-value="resolvedParameter.parameter.default"
|
||||||
></N8nInput>
|
:language="editorLanguage"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:target-node-parameter-context="targetNodeParameterContext"
|
||||||
|
fill-parent
|
||||||
|
:disable-ask-ai="true"
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<HtmlEditor
|
||||||
|
v-else-if="editorType === 'htmlEditor'"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:rows="editorRows"
|
||||||
|
:disable-expression-coloring="!isHtmlNode"
|
||||||
|
:disable-expression-completions="!isHtmlNode"
|
||||||
|
fullscreen
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<CssEditor
|
||||||
|
v-else-if="editorType === 'cssEditor'"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:rows="editorRows"
|
||||||
|
fullscreen
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<SqlEditor
|
||||||
|
v-else-if="editorType === 'sqlEditor'"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:dialect="getTypeOption('sqlDialect')"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:rows="editorRows"
|
||||||
|
fullscreen
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<JsEditor
|
||||||
|
v-else-if="editorType === 'jsEditor'"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:rows="editorRows"
|
||||||
|
:posthog-capture="shouldCaptureForPosthog"
|
||||||
|
fill-parent
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<JsonEditor
|
||||||
|
v-else-if="resolvedParameter.parameter.type === 'json'"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:rows="editorRows"
|
||||||
|
fullscreen
|
||||||
|
fill-parent
|
||||||
|
@update:model-value="valueChangedDebounced" />
|
||||||
|
<N8nInput
|
||||||
|
v-else
|
||||||
|
ref="inputField"
|
||||||
|
:model-value="resolvedParameter.value"
|
||||||
|
:class="$style.editor"
|
||||||
|
:readonly="isReadOnly"
|
||||||
|
type="textarea"
|
||||||
|
resize="none"
|
||||||
|
@update:model-value="valueChangedDebounced"
|
||||||
|
></N8nInput
|
||||||
|
></template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +389,7 @@ function valueChanged(value: string) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 528px;
|
width: 528px;
|
||||||
border-left: 1px solid var(--color-foreground-base);
|
border-left: 1px solid var(--color-foreground-base);
|
||||||
background: var(--color-background-base);
|
background: var(--color-foreground-light);
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +402,7 @@ function valueChanged(value: string) {
|
|||||||
padding: var(--spacing-2xs);
|
padding: var(--spacing-2xs);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--color-foreground-base);
|
border-bottom: 1px solid var(--color-foreground-base);
|
||||||
background: var(--color-background-xlight);
|
background: var(--color-foreground-xlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -193,7 +431,7 @@ function valueChanged(value: string) {
|
|||||||
.tabHeaderText {
|
.tabHeaderText {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-4xs);
|
gap: var(--spacing-4xs);
|
||||||
align-items: center;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
@@ -216,7 +454,6 @@ function valueChanged(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editorContainer {
|
.editorContainer {
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
@@ -224,7 +461,7 @@ function valueChanged(value: string) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-2xs);
|
||||||
|
|
||||||
:global(.cm-editor) {
|
:global(.cm-editor) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import type {
|
|||||||
CalloutActionType,
|
CalloutActionType,
|
||||||
INodeParameters,
|
INodeParameters,
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
NodeParameterValue,
|
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { ADD_FORM_NOTICE, deepCopy, getParameterValueByPath, NodeHelpers } from 'n8n-workflow';
|
import { ADD_FORM_NOTICE, getParameterValueByPath, NodeHelpers } from 'n8n-workflow';
|
||||||
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';
|
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch, type WatchSource } from 'vue';
|
||||||
|
|
||||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
@@ -19,7 +18,7 @@ import MultipleParameter from '@/components/MultipleParameter.vue';
|
|||||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeSettingsParameters } from '@/composables/useNodeSettingsParameters';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import {
|
import {
|
||||||
@@ -32,15 +31,9 @@ import {
|
|||||||
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 {
|
|
||||||
getMainAuthField,
|
|
||||||
getNodeAuthFields,
|
|
||||||
isAuthRelatedParameter,
|
|
||||||
} from '@/utils/nodeTypesUtils';
|
|
||||||
import { captureException } from '@sentry/vue';
|
import { captureException } from '@sentry/vue';
|
||||||
import { computedWithControl } from '@vueuse/core';
|
import { computedWithControl } from '@vueuse/core';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
|
||||||
import {
|
import {
|
||||||
N8nCallout,
|
N8nCallout,
|
||||||
N8nIcon,
|
N8nIcon,
|
||||||
@@ -52,6 +45,7 @@ import {
|
|||||||
} from '@n8n/design-system';
|
} from '@n8n/design-system';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
|
import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
|
||||||
|
import { getParameterTypeOption } from '@/utils/nodeSettingsUtils';
|
||||||
|
|
||||||
const LazyFixedCollectionParameter = defineAsyncComponent(
|
const LazyFixedCollectionParameter = defineAsyncComponent(
|
||||||
async () => await import('./FixedCollectionParameter.vue'),
|
async () => await import('./FixedCollectionParameter.vue'),
|
||||||
@@ -86,7 +80,7 @@ const nodeTypesStore = useNodeTypesStore();
|
|||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeSettingsParameters = useNodeSettingsParameters();
|
||||||
const asyncLoadingError = ref(false);
|
const asyncLoadingError = ref(false);
|
||||||
const workflowHelpers = useWorkflowHelpers();
|
const workflowHelpers = useWorkflowHelpers();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
@@ -114,6 +108,8 @@ onErrorCaptured((e, component) => {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const node = computed(() => props.node ?? ndvStore.activeNode);
|
||||||
|
|
||||||
const nodeType = computed(() => {
|
const nodeType = computed(() => {
|
||||||
if (node.value) {
|
if (node.value) {
|
||||||
return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
|
return nodeTypesStore.getNodeType(node.value.type, node.value.typeVersion);
|
||||||
@@ -121,13 +117,11 @@ const nodeType = computed(() => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const node = computed(() => props.node ?? ndvStore.activeNode);
|
|
||||||
|
|
||||||
const filteredParameters = computedWithControl(
|
const filteredParameters = computedWithControl(
|
||||||
[() => props.parameters, () => props.nodeValues] as WatchSource[],
|
[() => props.parameters, () => props.nodeValues] as WatchSource[],
|
||||||
() => {
|
() => {
|
||||||
const parameters = props.parameters.filter((parameter: INodeProperties) =>
|
const parameters = props.parameters.filter((parameter: INodeProperties) =>
|
||||||
displayNodeParameter(parameter),
|
shouldDisplayNodeParameter(parameter),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (node.value && node.value.type === FORM_TRIGGER_NODE_TYPE) {
|
if (node.value && node.value.type === FORM_TRIGGER_NODE_TYPE) {
|
||||||
@@ -154,10 +148,6 @@ const filteredParameterNames = computed(() => {
|
|||||||
return filteredParameters.value.map((parameter) => parameter.name);
|
return filteredParameters.value.map((parameter) => parameter.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
const nodeAuthFields = computed(() => {
|
|
||||||
return getNodeAuthFields(nodeType.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const credentialsParameterIndex = computed(() => {
|
const credentialsParameterIndex = computed(() => {
|
||||||
return filteredParameters.value.findIndex((parameter) => parameter.type === 'credentials');
|
return filteredParameters.value.findIndex((parameter) => parameter.type === 'credentials');
|
||||||
});
|
});
|
||||||
@@ -182,10 +172,6 @@ const indexToShowSlotAt = computed(() => {
|
|||||||
return Math.min(index, filteredParameters.value.length - 1);
|
return Math.min(index, filteredParameters.value.length - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainNodeAuthField = computed(() => {
|
|
||||||
return getMainAuthField(nodeType.value || null);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(filteredParameterNames, (newValue, oldValue) => {
|
watch(filteredParameterNames, (newValue, oldValue) => {
|
||||||
if (newValue === undefined) {
|
if (newValue === undefined) {
|
||||||
return;
|
return;
|
||||||
@@ -210,8 +196,8 @@ function updateFormTriggerParameters(parameters: INodeProperties[], triggerName:
|
|||||||
const connectedNodes = workflow.getChildNodes(triggerName);
|
const connectedNodes = workflow.getChildNodes(triggerName);
|
||||||
|
|
||||||
const hasFormPage = connectedNodes.some((nodeName) => {
|
const hasFormPage = connectedNodes.some((nodeName) => {
|
||||||
const node = workflow.getNode(nodeName);
|
const _node = workflow.getNode(nodeName);
|
||||||
return node && node.type === FORM_NODE_TYPE;
|
return _node && _node.type === FORM_NODE_TYPE;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasFormPage) {
|
if (hasFormPage) {
|
||||||
@@ -255,15 +241,15 @@ function updateWaitParameters(parameters: INodeProperties[], nodeName: string) {
|
|||||||
const parentNodes = workflow.getParentNodes(nodeName);
|
const parentNodes = workflow.getParentNodes(nodeName);
|
||||||
|
|
||||||
const formTriggerName = parentNodes.find(
|
const formTriggerName = parentNodes.find(
|
||||||
(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
|
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
|
||||||
);
|
);
|
||||||
if (!formTriggerName) return parameters;
|
if (!formTriggerName) return parameters;
|
||||||
|
|
||||||
const connectedNodes = workflow.getChildNodes(formTriggerName);
|
const connectedNodes = workflow.getChildNodes(formTriggerName);
|
||||||
|
|
||||||
const hasFormPage = connectedNodes.some((nodeName) => {
|
const hasFormPage = connectedNodes.some((_nodeName) => {
|
||||||
const node = workflow.getNode(nodeName);
|
const _node = workflow.getNode(_nodeName);
|
||||||
return node && node.type === FORM_NODE_TYPE;
|
return _node && _node.type === FORM_NODE_TYPE;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasFormPage) {
|
if (hasFormPage) {
|
||||||
@@ -294,7 +280,7 @@ function updateFormParameters(parameters: INodeProperties[], nodeName: string) {
|
|||||||
const parentNodes = workflow.getParentNodes(nodeName);
|
const parentNodes = workflow.getParentNodes(nodeName);
|
||||||
|
|
||||||
const formTriggerName = parentNodes.find(
|
const formTriggerName = parentNodes.find(
|
||||||
(node) => workflow.nodes[node].type === FORM_TRIGGER_NODE_TYPE,
|
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice');
|
if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice');
|
||||||
@@ -321,22 +307,7 @@ function getCredentialsDependencies() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function multipleValues(parameter: INodeProperties): boolean {
|
function multipleValues(parameter: INodeProperties): boolean {
|
||||||
return getArgument('multipleValues', parameter) === true;
|
return getParameterTypeOption(parameter, 'multipleValues') === true;
|
||||||
}
|
|
||||||
|
|
||||||
function getArgument(
|
|
||||||
argumentName: string,
|
|
||||||
parameter: INodeProperties,
|
|
||||||
): string | string[] | number | boolean | undefined {
|
|
||||||
if (parameter.typeOptions === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter.typeOptions[argumentName] === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parameter.typeOptions[argumentName];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPath(parameterName: string): string {
|
function getPath(parameterName: string): string {
|
||||||
@@ -354,116 +325,15 @@ function deleteOption(optionName: string): void {
|
|||||||
emit('valueChanged', parameterData);
|
emit('valueChanged', parameterData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mustHideDuringCustomApiCall(
|
function shouldDisplayNodeParameter(
|
||||||
parameter: INodeProperties,
|
|
||||||
nodeValues: INodeParameters,
|
|
||||||
): boolean {
|
|
||||||
if (parameter?.displayOptions?.hide) return true;
|
|
||||||
|
|
||||||
const MUST_REMAIN_VISIBLE = [
|
|
||||||
'authentication',
|
|
||||||
'resource',
|
|
||||||
'operation',
|
|
||||||
...Object.keys(nodeValues),
|
|
||||||
];
|
|
||||||
|
|
||||||
return !MUST_REMAIN_VISIBLE.includes(parameter.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayNodeParameter(
|
|
||||||
parameter: INodeProperties,
|
parameter: INodeProperties,
|
||||||
displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
|
displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
|
||||||
): boolean {
|
): boolean {
|
||||||
if (parameter.type === 'hidden') {
|
return nodeSettingsParameters.shouldDisplayNodeParameter(
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
nodeHelpers.isCustomApiCallSelected(props.nodeValues) &&
|
|
||||||
mustHideDuringCustomApiCall(parameter, props.nodeValues)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide authentication related fields since it will now be part of credentials modal
|
|
||||||
if (
|
|
||||||
!KEEP_AUTH_IN_NDV_FOR_NODES.includes(node.value?.type || '') &&
|
|
||||||
mainNodeAuthField.value &&
|
|
||||||
(parameter.name === mainNodeAuthField.value?.name || shouldHideAuthRelatedParameter(parameter))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameter[displayKey] === undefined) {
|
|
||||||
// If it is not defined no need to do a proper check
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeValues: INodeParameters = {};
|
|
||||||
let rawValues = props.nodeValues;
|
|
||||||
if (props.path) {
|
|
||||||
rawValues = get(props.nodeValues, props.path) as INodeParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rawValues) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Resolve expressions
|
|
||||||
const resolveKeys = Object.keys(rawValues);
|
|
||||||
let key: string;
|
|
||||||
let i = 0;
|
|
||||||
let parameterGotResolved = false;
|
|
||||||
do {
|
|
||||||
key = resolveKeys.shift() as string;
|
|
||||||
const value = rawValues[key];
|
|
||||||
if (typeof value === 'string' && value?.charAt(0) === '=') {
|
|
||||||
// Contains an expression that
|
|
||||||
if (
|
|
||||||
value.includes('$parameter') &&
|
|
||||||
resolveKeys.some((parameterName) => value.includes(parameterName))
|
|
||||||
) {
|
|
||||||
// Contains probably an expression of a missing parameter so skip
|
|
||||||
resolveKeys.push(key);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
// Contains probably no expression with a missing parameter so resolve
|
|
||||||
try {
|
|
||||||
nodeValues[key] = workflowHelpers.resolveExpression(
|
|
||||||
value,
|
|
||||||
nodeValues,
|
|
||||||
) as NodeParameterValue;
|
|
||||||
} catch (e) {
|
|
||||||
// If expression is invalid ignore
|
|
||||||
nodeValues[key] = '';
|
|
||||||
}
|
|
||||||
parameterGotResolved = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Does not contain an expression, add directly
|
|
||||||
nodeValues[key] = rawValues[key];
|
|
||||||
}
|
|
||||||
// TODO: Think about how to calculate this best
|
|
||||||
if (i++ > 50) {
|
|
||||||
// Make sure we do not get caught
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (resolveKeys.length !== 0);
|
|
||||||
|
|
||||||
if (parameterGotResolved) {
|
|
||||||
if (props.path) {
|
|
||||||
rawValues = deepCopy(props.nodeValues);
|
|
||||||
set(rawValues, props.path, nodeValues);
|
|
||||||
return nodeHelpers.displayParameter(rawValues, parameter, props.path, node.value, displayKey);
|
|
||||||
} else {
|
|
||||||
return nodeHelpers.displayParameter(nodeValues, parameter, '', node.value, displayKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodeHelpers.displayParameter(
|
|
||||||
props.nodeValues,
|
props.nodeValues,
|
||||||
|
node.value,
|
||||||
parameter,
|
parameter,
|
||||||
props.path,
|
props.path,
|
||||||
node.value,
|
|
||||||
displayKey,
|
displayKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -493,26 +363,15 @@ function getParameterIssues(parameter: INodeProperties): string[] {
|
|||||||
return issues.parameters?.[parameter.name] ?? [];
|
return issues.parameters?.[parameter.name] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles default node button parameter type actions
|
|
||||||
* @param parameter
|
|
||||||
*/
|
|
||||||
|
|
||||||
function shouldHideAuthRelatedParameter(parameter: INodeProperties): boolean {
|
|
||||||
// TODO: For now, hide all fields that are used in authentication fields displayOptions
|
|
||||||
// Ideally, we should check if any non-auth field depends on it before hiding it but
|
|
||||||
// since there is no such case, omitting it to avoid additional computation
|
|
||||||
return isAuthRelatedParameter(nodeAuthFields.value, parameter);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldShowOptions(parameter: INodeProperties): boolean {
|
function shouldShowOptions(parameter: INodeProperties): boolean {
|
||||||
return parameter.type !== 'resourceMapper';
|
return parameter.type !== 'resourceMapper';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDependentParametersValues(parameter: INodeProperties): string | null {
|
function getDependentParametersValues(parameter: INodeProperties): string | null {
|
||||||
const loadOptionsDependsOn = getArgument('loadOptionsDependsOn', parameter) as
|
const loadOptionsDependsOn = getParameterTypeOption<string[] | undefined>(
|
||||||
| string[]
|
parameter,
|
||||||
| undefined;
|
'loadOptionsDependsOn',
|
||||||
|
);
|
||||||
|
|
||||||
if (loadOptionsDependsOn === undefined) {
|
if (loadOptionsDependsOn === undefined) {
|
||||||
return null;
|
return null;
|
||||||
@@ -529,7 +388,7 @@ function getDependentParametersValues(parameter: INodeProperties): string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
return returnValues.join('|');
|
return returnValues.join('|');
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -792,7 +651,7 @@ const onCalloutDismiss = async (parameter: INodeProperties) => {
|
|||||||
:path="getPath(parameter.name)"
|
:path="getPath(parameter.name)"
|
||||||
:is-read-only="
|
:is-read-only="
|
||||||
isReadOnly ||
|
isReadOnly ||
|
||||||
(parameter.disabledOptions && displayNodeParameter(parameter, 'disabledOptions'))
|
(parameter.disabledOptions && shouldDisplayNodeParameter(parameter, 'disabledOptions'))
|
||||||
"
|
"
|
||||||
:hide-label="false"
|
:hide-label="false"
|
||||||
:node-values="nodeValues"
|
:node-values="nodeValues"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
|||||||
language: MaybeRefOrGetter<L>;
|
language: MaybeRefOrGetter<L>;
|
||||||
editorValue?: MaybeRefOrGetter<string>;
|
editorValue?: MaybeRefOrGetter<string>;
|
||||||
placeholder?: MaybeRefOrGetter<string>;
|
placeholder?: MaybeRefOrGetter<string>;
|
||||||
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext>;
|
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext | undefined>;
|
||||||
extensions?: MaybeRefOrGetter<Extension[]>;
|
extensions?: MaybeRefOrGetter<Extension[]>;
|
||||||
isReadOnly?: MaybeRefOrGetter<boolean>;
|
isReadOnly?: MaybeRefOrGetter<boolean>;
|
||||||
theme?: MaybeRefOrGetter<{
|
theme?: MaybeRefOrGetter<{
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { EXPRESSION_EDITOR_PARSER_TIMEOUT } from '@/constants';
|
|||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
|
||||||
import type { TargetItem, TargetNodeParameterContext } from '@/Interface';
|
import type { TargetItem, TargetNodeParameterContext } from '@/Interface';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import { type ResolveParameterOptions, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||||
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
||||||
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
|
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
|
||||||
@@ -311,9 +311,9 @@ export const useExpressionEditor = ({
|
|||||||
// e.g. credential modal
|
// e.g. credential modal
|
||||||
result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData));
|
result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData));
|
||||||
} else {
|
} else {
|
||||||
let opts: Record<string, unknown> = {
|
let opts: ResolveParameterOptions = {
|
||||||
additionalKeys: toValue(additionalData),
|
additionalKeys: toValue(additionalData),
|
||||||
targetNodeParameterContext,
|
contextNodeName: toValue(targetNodeParameterContext)?.nodeName,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
toValue(targetNodeParameterContext) === undefined &&
|
toValue(targetNodeParameterContext) === undefined &&
|
||||||
|
|||||||
@@ -11,22 +11,33 @@ import {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { useTelemetry } from './useTelemetry';
|
import { useTelemetry } from './useTelemetry';
|
||||||
import { useNodeHelpers } from './useNodeHelpers';
|
import { useNodeHelpers } from './useNodeHelpers';
|
||||||
|
import { useWorkflowHelpers } from './useWorkflowHelpers';
|
||||||
import { useCanvasOperations } from './useCanvasOperations';
|
import { useCanvasOperations } from './useCanvasOperations';
|
||||||
import { useExternalHooks } from './useExternalHooks';
|
import { useExternalHooks } from './useExternalHooks';
|
||||||
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
import type { INodeUi, IUpdateInformation } from '@/Interface';
|
||||||
import { updateDynamicConnections, updateParameterByPath } from '@/utils/nodeSettingsUtils';
|
import {
|
||||||
|
mustHideDuringCustomApiCall,
|
||||||
|
updateDynamicConnections,
|
||||||
|
updateParameterByPath,
|
||||||
|
} from '@/utils/nodeSettingsUtils';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
import { useFocusPanelStore } from '@/stores/focusPanel.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
import { CUSTOM_API_CALL_KEY, KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||||
import { omitKey } from '@/utils/objectUtils';
|
import { omitKey } from '@/utils/objectUtils';
|
||||||
|
import {
|
||||||
|
getMainAuthField,
|
||||||
|
getNodeAuthFields,
|
||||||
|
isAuthRelatedParameter,
|
||||||
|
} from '@/utils/nodeTypesUtils';
|
||||||
|
|
||||||
export function useNodeSettingsParameters() {
|
export function useNodeSettingsParameters() {
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
const workflowHelpers = useWorkflowHelpers();
|
||||||
const canvasOperations = useCanvasOperations();
|
const canvasOperations = useCanvasOperations();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
|
|
||||||
@@ -236,6 +247,113 @@ export function useNodeSettingsParameters() {
|
|||||||
focusPanelStore.focusPanelActive = true;
|
focusPanelStore.focusPanelActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldDisplayNodeParameter(
|
||||||
|
nodeParameters: INodeParameters,
|
||||||
|
node: INodeUi | null,
|
||||||
|
parameter: INodeProperties,
|
||||||
|
path: string | undefined = '',
|
||||||
|
displayKey: 'displayOptions' | 'disabledOptions' = 'displayOptions',
|
||||||
|
): boolean {
|
||||||
|
if (parameter.type === 'hidden') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
nodeHelpers.isCustomApiCallSelected(nodeParameters) &&
|
||||||
|
mustHideDuringCustomApiCall(parameter, nodeParameters)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = !node ? null : nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
|
||||||
|
// TODO: For now, hide all fields that are used in authentication fields displayOptions
|
||||||
|
// Ideally, we should check if any non-auth field depends on it before hiding it but
|
||||||
|
// since there is no such case, omitting it to avoid additional computation
|
||||||
|
const shouldHideAuthRelatedParameter = isAuthRelatedParameter(
|
||||||
|
getNodeAuthFields(nodeType),
|
||||||
|
parameter,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mainNodeAuthField = getMainAuthField(nodeType);
|
||||||
|
|
||||||
|
// Hide authentication related fields since it will now be part of credentials modal
|
||||||
|
if (
|
||||||
|
!KEEP_AUTH_IN_NDV_FOR_NODES.includes(node?.type ?? '') &&
|
||||||
|
mainNodeAuthField &&
|
||||||
|
(parameter.name === mainNodeAuthField.name || shouldHideAuthRelatedParameter)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameter[displayKey] === undefined) {
|
||||||
|
// If it is not defined no need to do a proper check
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeParams: INodeParameters = {};
|
||||||
|
let rawValues = nodeParameters;
|
||||||
|
if (path) {
|
||||||
|
rawValues = get(nodeParameters, path) as INodeParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawValues) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Resolve expressions
|
||||||
|
const resolveKeys = Object.keys(rawValues);
|
||||||
|
let key: string;
|
||||||
|
let i = 0;
|
||||||
|
let parameterGotResolved = false;
|
||||||
|
do {
|
||||||
|
key = resolveKeys.shift() as string;
|
||||||
|
const value = rawValues[key];
|
||||||
|
if (typeof value === 'string' && value?.charAt(0) === '=') {
|
||||||
|
// Contains an expression that
|
||||||
|
if (
|
||||||
|
value.includes('$parameter') &&
|
||||||
|
resolveKeys.some((parameterName) => value.includes(parameterName))
|
||||||
|
) {
|
||||||
|
// Contains probably an expression of a missing parameter so skip
|
||||||
|
resolveKeys.push(key);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// Contains probably no expression with a missing parameter so resolve
|
||||||
|
try {
|
||||||
|
nodeParams[key] = workflowHelpers.resolveExpression(
|
||||||
|
value,
|
||||||
|
nodeParams,
|
||||||
|
) as NodeParameterValue;
|
||||||
|
} catch (e) {
|
||||||
|
// If expression is invalid ignore
|
||||||
|
nodeParams[key] = '';
|
||||||
|
}
|
||||||
|
parameterGotResolved = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Does not contain an expression, add directly
|
||||||
|
nodeParams[key] = rawValues[key];
|
||||||
|
}
|
||||||
|
// TODO: Think about how to calculate this best
|
||||||
|
if (i++ > 50) {
|
||||||
|
// Make sure we do not get caught
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (resolveKeys.length !== 0);
|
||||||
|
|
||||||
|
if (parameterGotResolved) {
|
||||||
|
if (path) {
|
||||||
|
rawValues = deepCopy(nodeParameters);
|
||||||
|
set(rawValues, path, nodeParams);
|
||||||
|
return nodeHelpers.displayParameter(rawValues, parameter, path, node, displayKey);
|
||||||
|
} else {
|
||||||
|
return nodeHelpers.displayParameter(nodeParams, parameter, '', node, displayKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeHelpers.displayParameter(nodeParameters, parameter, path, node, displayKey);
|
||||||
|
}
|
||||||
|
|
||||||
function shouldSkipParamValidation(value: string | number | boolean | null) {
|
function shouldSkipParamValidation(value: string | number | boolean | null) {
|
||||||
return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
|
return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
|
||||||
}
|
}
|
||||||
@@ -243,6 +361,7 @@ export function useNodeSettingsParameters() {
|
|||||||
return {
|
return {
|
||||||
nodeValues,
|
nodeValues,
|
||||||
setValue,
|
setValue,
|
||||||
|
shouldDisplayNodeParameter,
|
||||||
updateParameterByPath,
|
updateParameterByPath,
|
||||||
updateNodeParameter,
|
updateNodeParameter,
|
||||||
handleFocus,
|
handleFocus,
|
||||||
|
|||||||
@@ -126,18 +126,20 @@ export function dollarOptions(context: CompletionContext): Completion[] {
|
|||||||
|
|
||||||
if (receivesNoBinaryData(targetNodeParameterContext?.nodeName)) SKIP.add('$binary');
|
if (receivesNoBinaryData(targetNodeParameterContext?.nodeName)) SKIP.add('$binary');
|
||||||
|
|
||||||
const previousNodesCompletions = autocompletableNodeNames().map((nodeName) => {
|
const previousNodesCompletions = autocompletableNodeNames(targetNodeParameterContext).map(
|
||||||
const label = `$('${escapeMappingString(nodeName)}')`;
|
(nodeName) => {
|
||||||
return {
|
const label = `$('${escapeMappingString(nodeName)}')`;
|
||||||
label,
|
return {
|
||||||
info: createInfoBoxRenderer({
|
label,
|
||||||
name: label,
|
info: createInfoBoxRenderer({
|
||||||
returnType: 'Object',
|
name: label,
|
||||||
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
returnType: 'Object',
|
||||||
}),
|
description: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||||
section: PREVIOUS_NODES_SECTION,
|
}),
|
||||||
};
|
section: PREVIOUS_NODES_SECTION,
|
||||||
});
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return recommendedCompletions
|
return recommendedCompletions
|
||||||
.concat(ROOT_DOLLAR_COMPLETIONS)
|
.concat(ROOT_DOLLAR_COMPLETIONS)
|
||||||
|
|||||||
@@ -215,11 +215,11 @@ export const hasActiveNode = (targetNodeParameterContext?: TargetNodeParameterCo
|
|||||||
export const isSplitInBatchesAbsent = () =>
|
export const isSplitInBatchesAbsent = () =>
|
||||||
!useWorkflowsStore().workflow.nodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
!useWorkflowsStore().workflow.nodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
||||||
|
|
||||||
export function autocompletableNodeNames(contextNodeName?: string) {
|
export function autocompletableNodeNames(targetNodeParameterContext?: TargetNodeParameterContext) {
|
||||||
const activeNode =
|
const activeNode =
|
||||||
contextNodeName === undefined
|
targetNodeParameterContext === undefined
|
||||||
? useNDVStore().activeNode
|
? useNDVStore().activeNode
|
||||||
: useWorkflowsStore().getNodeByName(contextNodeName);
|
: useWorkflowsStore().getNodeByName(targetNodeParameterContext.nodeName);
|
||||||
|
|
||||||
if (!activeNode) return [];
|
if (!activeNode) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '../../completions/utils';
|
} from '../../completions/utils';
|
||||||
import { typescriptWorkerFacet } from './facet';
|
import { typescriptWorkerFacet } from './facet';
|
||||||
import { blockCommentSnippet, snippets } from './snippets';
|
import { blockCommentSnippet, snippets } from './snippets';
|
||||||
|
import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants';
|
||||||
|
|
||||||
const START_CHARACTERS = ['"', "'", '(', '.', '@'];
|
const START_CHARACTERS = ['"', "'", '(', '.', '@'];
|
||||||
const START_CHARACTERS_REGEX = /[\.\(\'\"\@]/;
|
const START_CHARACTERS_REGEX = /[\.\(\'\"\@]/;
|
||||||
@@ -30,7 +31,7 @@ export const matchText = (context: CompletionContext) => {
|
|||||||
|
|
||||||
export const typescriptCompletionSource: CompletionSource = async (context) => {
|
export const typescriptCompletionSource: CompletionSource = async (context) => {
|
||||||
const { worker } = context.state.facet(typescriptWorkerFacet);
|
const { worker } = context.state.facet(typescriptWorkerFacet);
|
||||||
|
const targetNodeParameter = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||||
const word = matchText(context);
|
const word = matchText(context);
|
||||||
|
|
||||||
const blockComment = context.matchBefore(/\/\*?\*?/);
|
const blockComment = context.matchBefore(/\/\*?\*?/);
|
||||||
@@ -55,7 +56,7 @@ export const typescriptCompletionSource: CompletionSource = async (context) => {
|
|||||||
if (opt.label === '$()') {
|
if (opt.label === '$()') {
|
||||||
return [
|
return [
|
||||||
opt,
|
opt,
|
||||||
...autocompletableNodeNames().map((name) => ({
|
...autocompletableNodeNames(targetNodeParameter).map((name) => ({
|
||||||
...opt,
|
...opt,
|
||||||
label: `$('${escapeMappingString(name)}')`,
|
label: `$('${escapeMappingString(name)}')`,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function useTypescript(
|
|||||||
view: MaybeRefOrGetter<EditorView | undefined>,
|
view: MaybeRefOrGetter<EditorView | undefined>,
|
||||||
mode: MaybeRefOrGetter<CodeExecutionMode>,
|
mode: MaybeRefOrGetter<CodeExecutionMode>,
|
||||||
id: MaybeRefOrGetter<string>,
|
id: MaybeRefOrGetter<string>,
|
||||||
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext>,
|
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext | undefined>,
|
||||||
) {
|
) {
|
||||||
const { getInputDataWithPinned, getSchemaForExecutionData } = useDataSchema();
|
const { getInputDataWithPinned, getSchemaForExecutionData } = useDataSchema();
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
@@ -47,7 +47,7 @@ export function useTypescript(
|
|||||||
{
|
{
|
||||||
id: toValue(id),
|
id: toValue(id),
|
||||||
content: Comlink.proxy((toValue(view)?.state.doc ?? Text.empty).toJSON()),
|
content: Comlink.proxy((toValue(view)?.state.doc ?? Text.empty).toJSON()),
|
||||||
allNodeNames: autocompletableNodeNames(),
|
allNodeNames: autocompletableNodeNames(toValue(targetNodeParameterContext)),
|
||||||
variables: useEnvironmentsStore().variables.map((v) => v.key),
|
variables: useEnvironmentsStore().variables.map((v) => v.key),
|
||||||
inputNodeNames: activeNodeName
|
inputNodeNames: activeNodeName
|
||||||
? workflowsStore
|
? workflowsStore
|
||||||
|
|||||||
@@ -259,6 +259,22 @@ export function isValidParameterOption(
|
|||||||
return 'value' in option && isPresent(option.value) && isPresent(option.name);
|
return 'value' in option && isPresent(option.value) && isPresent(option.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mustHideDuringCustomApiCall(
|
||||||
|
parameter: INodeProperties,
|
||||||
|
nodeParameters: INodeParameters,
|
||||||
|
): boolean {
|
||||||
|
if (parameter?.displayOptions?.hide) return true;
|
||||||
|
|
||||||
|
const MUST_REMAIN_VISIBLE = [
|
||||||
|
'authentication',
|
||||||
|
'resource',
|
||||||
|
'operation',
|
||||||
|
...Object.keys(nodeParameters),
|
||||||
|
];
|
||||||
|
|
||||||
|
return !MUST_REMAIN_VISIBLE.includes(parameter.name);
|
||||||
|
}
|
||||||
|
|
||||||
export function nameIsParameter(
|
export function nameIsParameter(
|
||||||
parameterData: IUpdateInformation,
|
parameterData: IUpdateInformation,
|
||||||
): parameterData is IUpdateInformation & { name: `parameters.${string}` } {
|
): parameterData is IUpdateInformation & { name: `parameters.${string}` } {
|
||||||
|
|||||||
@@ -2157,7 +2157,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</WorkflowCanvas>
|
</WorkflowCanvas>
|
||||||
<FocusPanel v-if="isFocusPanelFeatureEnabled" :executable="!isCanvasReadOnly" />
|
<FocusPanel v-if="isFocusPanelFeatureEnabled" :is-canvas-read-only="isCanvasReadOnly" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user