mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Propagate targetNodeParameterContext throughout expression resolution logic (no-changelog) (#16476)
This commit is contained in:
@@ -1058,6 +1058,11 @@ export interface NDVState {
|
||||
highlightDraggables: boolean;
|
||||
}
|
||||
|
||||
export type TargetNodeParameterContext = {
|
||||
nodeName: string;
|
||||
parameterPath: string;
|
||||
};
|
||||
|
||||
export interface NotificationOptions extends Partial<ElementNotificationOptions> {
|
||||
message: string | ElementNotificationOptions['message'];
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ import { removeExpressionPrefix } from '@/utils/expressions';
|
||||
import { mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
|
||||
import { editorKeymap } from '@/plugins/codemirror/keymap';
|
||||
import { expressionCloseBrackets } from '@/plugins/codemirror/expressionCloseBrackets';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
|
||||
type Props = {
|
||||
modelValue: string;
|
||||
path: string;
|
||||
targetNodeParameterContext?: TargetNodeParameterContext;
|
||||
isReadOnly?: boolean;
|
||||
};
|
||||
|
||||
@@ -52,7 +54,11 @@ const { segments, readEditorValue, editor, hasFocus, focus } = useExpressionEdit
|
||||
editorValue,
|
||||
extensions,
|
||||
isReadOnly: computed(() => props.isReadOnly),
|
||||
autocompleteTelemetry: { enabled: true, parameterPath: props.path },
|
||||
autocompleteTelemetry: {
|
||||
enabled: true,
|
||||
parameterPath: props.path,
|
||||
},
|
||||
targetNodeParameterContext: props.targetNodeParameterContext,
|
||||
});
|
||||
|
||||
watch(
|
||||
|
||||
@@ -53,6 +53,7 @@ import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
|
||||
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ignoreUpdateAnnotation } from '../utils/forceParse';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
|
||||
export type CodeEditorLanguageParamsMap = {
|
||||
json: {};
|
||||
@@ -67,6 +68,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||
language,
|
||||
languageParams,
|
||||
placeholder,
|
||||
targetNodeParameterContext = undefined,
|
||||
extensions = [],
|
||||
isReadOnly = false,
|
||||
theme = {},
|
||||
@@ -77,6 +79,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||
language: MaybeRefOrGetter<L>;
|
||||
editorValue?: MaybeRefOrGetter<string>;
|
||||
placeholder?: MaybeRefOrGetter<string>;
|
||||
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext>;
|
||||
extensions?: MaybeRefOrGetter<Extension[]>;
|
||||
isReadOnly?: MaybeRefOrGetter<boolean>;
|
||||
theme?: MaybeRefOrGetter<{
|
||||
@@ -106,7 +109,12 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||
const params = toValue(languageParams);
|
||||
return params && 'mode' in params ? params.mode : 'runOnceForAllItems';
|
||||
});
|
||||
const { createWorker: createTsWorker } = useTypescript(editor, mode, id);
|
||||
const { createWorker: createTsWorker } = useTypescript(
|
||||
editor,
|
||||
mode,
|
||||
id,
|
||||
targetNodeParameterContext,
|
||||
);
|
||||
|
||||
function getInitialLanguageExtensions(lang: CodeEditorLanguage): Extension[] {
|
||||
switch (lang) {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Expression, ExpressionExtensions } from 'n8n-workflow';
|
||||
import { EXPRESSION_EDITOR_PARSER_TIMEOUT } from '@/constants';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
import type { TargetItem } from '@/Interface';
|
||||
import type { TargetItem, TargetNodeParameterContext } from '@/Interface';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { closeCursorInfoBox } from '@/plugins/codemirror/tooltips/InfoBoxTooltip';
|
||||
@@ -43,6 +43,7 @@ import { ignoreUpdateAnnotation } from '../utils/forceParse';
|
||||
export const useExpressionEditor = ({
|
||||
editorRef,
|
||||
editorValue,
|
||||
targetNodeParameterContext,
|
||||
extensions = [],
|
||||
additionalData = {},
|
||||
skipSegments = [],
|
||||
@@ -52,6 +53,7 @@ export const useExpressionEditor = ({
|
||||
}: {
|
||||
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
|
||||
editorValue?: MaybeRefOrGetter<string>;
|
||||
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext>;
|
||||
extensions?: MaybeRefOrGetter<Extension[]>;
|
||||
additionalData?: MaybeRefOrGetter<IDataObject>;
|
||||
skipSegments?: MaybeRefOrGetter<string[]>;
|
||||
@@ -305,12 +307,18 @@ export const useExpressionEditor = ({
|
||||
};
|
||||
|
||||
try {
|
||||
if (!ndvStore.activeNode) {
|
||||
if (!ndvStore.activeNode && toValue(targetNodeParameterContext) === undefined) {
|
||||
// e.g. credential modal
|
||||
result.resolved = Expression.resolveWithoutWorkflow(resolvable, toValue(additionalData));
|
||||
} else {
|
||||
let opts: Record<string, unknown> = { additionalKeys: toValue(additionalData) };
|
||||
if (ndvStore.isInputParentOfActiveNode) {
|
||||
let opts: Record<string, unknown> = {
|
||||
additionalKeys: toValue(additionalData),
|
||||
targetNodeParameterContext,
|
||||
};
|
||||
if (
|
||||
toValue(targetNodeParameterContext) === undefined &&
|
||||
ndvStore.isInputParentOfActiveNode
|
||||
) {
|
||||
opts = {
|
||||
targetItem: target ?? undefined,
|
||||
inputNodeName: ndvStore.ndvInputNodeName,
|
||||
|
||||
@@ -12,11 +12,13 @@ export function useResolvedExpression({
|
||||
additionalData,
|
||||
isForCredential,
|
||||
stringifyObject,
|
||||
contextNodeName,
|
||||
}: {
|
||||
expression: MaybeRefOrGetter<unknown>;
|
||||
additionalData?: MaybeRefOrGetter<IDataObject>;
|
||||
isForCredential?: MaybeRefOrGetter<boolean>;
|
||||
stringifyObject?: MaybeRefOrGetter<boolean>;
|
||||
contextNodeName?: MaybeRefOrGetter<string>;
|
||||
}) {
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
@@ -45,9 +47,10 @@ export function useResolvedExpression({
|
||||
let options: ResolveParameterOptions = {
|
||||
isForCredential: toValue(isForCredential),
|
||||
additionalKeys: toValue(additionalData),
|
||||
contextNodeName: toValue(contextNodeName),
|
||||
};
|
||||
|
||||
if (ndvStore.isInputParentOfActiveNode) {
|
||||
if (contextNodeName === undefined && ndvStore.isInputParentOfActiveNode) {
|
||||
options = {
|
||||
...options,
|
||||
targetItem: targetItem.value ?? undefined,
|
||||
|
||||
@@ -18,7 +18,7 @@ export function blankCompletions(context: CompletionContext): CompletionResult |
|
||||
|
||||
return {
|
||||
from: word.to,
|
||||
options: dollarOptions().map(stripExcessParens(context)),
|
||||
options: dollarOptions(context).map(stripExcessParens(context)),
|
||||
filter: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { setActivePinia } from 'pinia';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
||||
import { dollarOptions } from '@/plugins/codemirror/completions/dollar.completions';
|
||||
import * as utils from '@/plugins/codemirror/completions/utils';
|
||||
import {
|
||||
extensions,
|
||||
@@ -62,7 +61,7 @@ describe('No completions', () => {
|
||||
describe('Top-level completions', () => {
|
||||
test('should return dollar completions for blank position: {{ | }}', () => {
|
||||
const result = completions('{{ | }}');
|
||||
expect(result).toHaveLength(dollarOptions().length);
|
||||
expect(result).toHaveLength(18);
|
||||
|
||||
expect(result?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
@@ -109,7 +108,7 @@ describe('Top-level completions', () => {
|
||||
});
|
||||
|
||||
test('should return dollar completions for: {{ $| }}', () => {
|
||||
expect(completions('{{ $| }}')).toHaveLength(dollarOptions().length);
|
||||
expect(completions('{{ $| }}')).toHaveLength(18);
|
||||
});
|
||||
|
||||
test('should return node selector completions for: {{ $(| }}', () => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { Completion, CompletionSection } from '@codemirror/autocomplete';
|
||||
import { i18n } from '@n8n/i18n';
|
||||
import { withSectionHeader } from './utils';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
import { Facet } from '@codemirror/state';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
|
||||
export const FIELDS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.fields'),
|
||||
@@ -436,3 +438,10 @@ export const STRING_SECTIONS: Record<string, CompletionSection> = {
|
||||
rank: 5,
|
||||
}),
|
||||
};
|
||||
|
||||
export const TARGET_NODE_PARAMETER_FACET = Facet.define<
|
||||
TargetNodeParameterContext | undefined,
|
||||
TargetNodeParameterContext | undefined
|
||||
>({
|
||||
combine: (values) => values[0],
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
PREVIOUS_NODES_SECTION,
|
||||
RECOMMENDED_SECTION,
|
||||
ROOT_DOLLAR_COMPLETIONS,
|
||||
TARGET_NODE_PARAMETER_FACET,
|
||||
} from './constants';
|
||||
import { createInfoBoxRenderer } from './infoBoxRenderer';
|
||||
|
||||
@@ -31,7 +32,7 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
|
||||
|
||||
if (word.from === word.to && !context.explicit) return null;
|
||||
|
||||
let options = dollarOptions().map(stripExcessParens(context));
|
||||
let options = dollarOptions(context).map(stripExcessParens(context));
|
||||
|
||||
const userInput = word.text;
|
||||
|
||||
@@ -53,7 +54,7 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
|
||||
};
|
||||
}
|
||||
|
||||
export function dollarOptions(): Completion[] {
|
||||
export function dollarOptions(context: CompletionContext): Completion[] {
|
||||
const SKIP = new Set();
|
||||
let recommendedCompletions: Completion[] = [];
|
||||
|
||||
@@ -117,11 +118,13 @@ export function dollarOptions(): Completion[] {
|
||||
: [];
|
||||
}
|
||||
|
||||
if (!hasActiveNode()) {
|
||||
const targetNodeParameterContext = context.state.facet(TARGET_NODE_PARAMETER_FACET);
|
||||
|
||||
if (!hasActiveNode(targetNodeParameterContext)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (receivesNoBinaryData()) SKIP.add('$binary');
|
||||
if (receivesNoBinaryData(targetNodeParameterContext?.nodeName)) SKIP.add('$binary');
|
||||
|
||||
const previousNodesCompletions = autocompletableNodeNames().map((nodeName) => {
|
||||
const label = `$('${escapeMappingString(nodeName)}')`;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { EditorSelection, type TransactionSpec } from '@codemirror/state';
|
||||
import type { SyntaxNode, Tree } from '@lezer/common';
|
||||
import type { DocMetadata } from 'n8n-workflow';
|
||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
|
||||
/**
|
||||
* Split user input into base (to resolve) and tail (to filter).
|
||||
@@ -144,19 +145,19 @@ export const isAllowedInDotNotation = (str: string) => {
|
||||
// resolution-based utils
|
||||
// ----------------------------------
|
||||
|
||||
export function receivesNoBinaryData() {
|
||||
export function receivesNoBinaryData(contextNodeName?: string) {
|
||||
try {
|
||||
return resolveAutocompleteExpression('={{ $binary }}')?.data === undefined;
|
||||
return resolveAutocompleteExpression('={{ $binary }}', contextNodeName)?.data === undefined;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasNoParams(toResolve: string) {
|
||||
export function hasNoParams(toResolve: string, contextNodeName?: string) {
|
||||
let params;
|
||||
|
||||
try {
|
||||
params = resolveAutocompleteExpression(`={{ ${toResolve}.params }}`);
|
||||
params = resolveAutocompleteExpression(`={{ ${toResolve}.params }}`, contextNodeName);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
@@ -168,19 +169,21 @@ export function hasNoParams(toResolve: string) {
|
||||
return paramKeys.length === 1 && isPseudoParam(paramKeys[0]);
|
||||
}
|
||||
|
||||
export function resolveAutocompleteExpression(expression: string) {
|
||||
export function resolveAutocompleteExpression(expression: string, contextNodeName?: string) {
|
||||
const ndvStore = useNDVStore();
|
||||
return resolveParameter(
|
||||
expression,
|
||||
ndvStore.isInputParentOfActiveNode
|
||||
const inputData =
|
||||
contextNodeName === undefined && ndvStore.isInputParentOfActiveNode
|
||||
? {
|
||||
targetItem: ndvStore.expressionTargetItem ?? undefined,
|
||||
inputNodeName: ndvStore.ndvInputNodeName,
|
||||
inputRunIndex: ndvStore.ndvInputRunIndex,
|
||||
inputBranchIndex: ndvStore.ndvInputBranchIndex,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
: {};
|
||||
return resolveParameter(expression, {
|
||||
...inputData,
|
||||
contextNodeName,
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
@@ -189,21 +192,34 @@ export function resolveAutocompleteExpression(expression: string) {
|
||||
|
||||
export const isCredentialsModalOpen = () => useUIStore().modalsById[CREDENTIAL_EDIT_MODAL_KEY].open;
|
||||
|
||||
export const isInHttpNodePagination = () => {
|
||||
const ndvStore = useNDVStore();
|
||||
return (
|
||||
ndvStore.activeNode?.type === HTTP_REQUEST_NODE_TYPE &&
|
||||
ndvStore.focusedInputPath.startsWith('parameters.options.pagination')
|
||||
);
|
||||
export const isInHttpNodePagination = (targetNodeParameterContext?: TargetNodeParameterContext) => {
|
||||
let nodeType: string | undefined;
|
||||
let path: string;
|
||||
if (targetNodeParameterContext) {
|
||||
nodeType = targetNodeParameterContext.nodeName;
|
||||
path = targetNodeParameterContext.parameterPath;
|
||||
} else {
|
||||
const ndvStore = useNDVStore();
|
||||
nodeType = ndvStore.activeNode?.type;
|
||||
path = ndvStore.focusedInputPath;
|
||||
}
|
||||
|
||||
return nodeType === HTTP_REQUEST_NODE_TYPE && path.startsWith('parameters.options.pagination');
|
||||
};
|
||||
|
||||
export const hasActiveNode = () => useNDVStore().activeNode?.name !== undefined;
|
||||
export const hasActiveNode = (targetNodeParameterContext?: TargetNodeParameterContext) =>
|
||||
(targetNodeParameterContext !== undefined &&
|
||||
useWorkflowsStore().getNodeByName(targetNodeParameterContext.nodeName) !== null) ||
|
||||
useNDVStore().activeNode?.name !== undefined;
|
||||
|
||||
export const isSplitInBatchesAbsent = () =>
|
||||
!useWorkflowsStore().workflow.nodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
||||
|
||||
export function autocompletableNodeNames() {
|
||||
const activeNode = useNDVStore().activeNode;
|
||||
export function autocompletableNodeNames(contextNodeName?: string) {
|
||||
const activeNode =
|
||||
contextNodeName === undefined
|
||||
? useNDVStore().activeNode
|
||||
: useWorkflowsStore().getNodeByName(contextNodeName);
|
||||
|
||||
if (!activeNode) return [];
|
||||
|
||||
|
||||
@@ -21,17 +21,20 @@ import { typescriptWorkerFacet } from './facet';
|
||||
import { typescriptHoverTooltips } from './hoverTooltip';
|
||||
import { linter } from '@codemirror/lint';
|
||||
import { typescriptLintSource } from './linter';
|
||||
import type { TargetNodeParameterContext } from '@/Interface';
|
||||
import { TARGET_NODE_PARAMETER_FACET } from '../../completions/constants';
|
||||
|
||||
export function useTypescript(
|
||||
view: MaybeRefOrGetter<EditorView | undefined>,
|
||||
mode: MaybeRefOrGetter<CodeExecutionMode>,
|
||||
id: MaybeRefOrGetter<string>,
|
||||
targetNodeParameterContext?: MaybeRefOrGetter<TargetNodeParameterContext>,
|
||||
) {
|
||||
const { getInputDataWithPinned, getSchemaForExecutionData } = useDataSchema();
|
||||
const ndvStore = useNDVStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const { debounce } = useDebounce();
|
||||
const activeNodeName = ndvStore.activeNodeName;
|
||||
const activeNodeName = toValue(targetNodeParameterContext)?.nodeName ?? ndvStore.activeNodeName;
|
||||
const worker = ref<Comlink.Remote<LanguageServiceWorker>>();
|
||||
const webWorker = ref<Worker>();
|
||||
|
||||
@@ -64,7 +67,9 @@ export function useTypescript(
|
||||
.getBinaryData(
|
||||
execution?.data?.resultData?.runData ?? null,
|
||||
node.name,
|
||||
ndvStore.ndvInputRunIndex ?? 0,
|
||||
toValue(targetNodeParameterContext) === undefined
|
||||
? (ndvStore.ndvInputRunIndex ?? 0)
|
||||
: 0,
|
||||
0,
|
||||
)
|
||||
.filter((data) => Boolean(data && Object.keys(data).length));
|
||||
@@ -88,6 +93,7 @@ export function useTypescript(
|
||||
|
||||
return [
|
||||
typescriptWorkerFacet.of({ worker: worker.value }),
|
||||
TARGET_NODE_PARAMETER_FACET.of(toValue(targetNodeParameterContext)),
|
||||
new LanguageSupport(javascriptLanguage, [
|
||||
javascriptLanguage.data.of({ autocomplete: typescriptCompletionSource }),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user