feat(editor): Add missing extension methods for expressions (#8845)
This commit is contained in:
@@ -1235,6 +1235,7 @@ export interface NDVState {
|
||||
};
|
||||
};
|
||||
focusedMappableInput: string;
|
||||
focusedInputPath: string;
|
||||
mappingTelemetry: { [key: string]: string | number | boolean };
|
||||
hoveringItem: null | TargetItem;
|
||||
draggable: {
|
||||
|
||||
@@ -230,6 +230,7 @@ export default defineComponent({
|
||||
if (!this.parameter.noDataExpression) {
|
||||
this.ndvStore.setMappableNDVInputFocus(this.parameter.displayName);
|
||||
}
|
||||
this.ndvStore.setFocusedInputPath(this.path ?? '');
|
||||
},
|
||||
onBlur() {
|
||||
this.focused = false;
|
||||
@@ -239,6 +240,7 @@ export default defineComponent({
|
||||
) {
|
||||
this.ndvStore.setMappableNDVInputFocus('');
|
||||
}
|
||||
this.ndvStore.setFocusedInputPath('');
|
||||
this.$emit('blur');
|
||||
},
|
||||
onMenuExpanded(expanded: boolean) {
|
||||
|
||||
@@ -176,13 +176,17 @@ export function resolveParameter(
|
||||
};
|
||||
|
||||
if (activeNode?.type === HTTP_REQUEST_NODE_TYPE) {
|
||||
// Add $response for HTTP Request-Nodes as it is used
|
||||
const EMPTY_RESPONSE = { statusCode: 200, headers: {}, body: {} };
|
||||
const EMPTY_REQUEST = { headers: {}, body: {}, qs: {} };
|
||||
// Add $request,$response,$pageCount for HTTP Request-Nodes as it is used
|
||||
// in pagination expressions
|
||||
additionalKeys.$pageCount = 0;
|
||||
additionalKeys.$response = get(
|
||||
executionData,
|
||||
['data', 'executionData', 'contextData', `node:${activeNode.name}`, 'response'],
|
||||
{},
|
||||
EMPTY_RESPONSE,
|
||||
);
|
||||
additionalKeys.$request = EMPTY_REQUEST;
|
||||
}
|
||||
|
||||
let runIndexCurrent = opts?.targetItem?.runIndex ?? 0;
|
||||
|
||||
@@ -545,16 +545,16 @@ describe('Resolution-based completions', () => {
|
||||
});
|
||||
|
||||
describe('recommended completions', () => {
|
||||
test('should recommended toDate() for {{ "1-Feb-2024".| }}', () => {
|
||||
test('should recommend toDateTime() for {{ "1-Feb-2024".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('1-Feb-2024');
|
||||
|
||||
expect(completions('{{ "1-Feb-2024".| }}')?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDate()', section: RECOMMENDED_SECTION }),
|
||||
expect.objectContaining({ label: 'toDateTime()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
||||
test('should recommend toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('5.3');
|
||||
const options = completions('{{ "5.3".| }}');
|
||||
@@ -566,7 +566,7 @@ describe('Resolution-based completions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended extractEmail() for: {{ "string with test@n8n.io in it".| }}', () => {
|
||||
test('should recommend extractEmail() for: {{ "string with test@n8n.io in it".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'string with test@n8n.io in it',
|
||||
@@ -577,7 +577,7 @@ describe('Resolution-based completions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended extractDomain() for: {{ "test@n8n.io".| }}', () => {
|
||||
test('should recommend extractDomain(), isEmail() for: {{ "test@n8n.io".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'test@n8n.io',
|
||||
@@ -586,9 +586,26 @@ describe('Resolution-based completions', () => {
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[1]).toEqual(
|
||||
expect.objectContaining({ label: 'isEmail()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended round(),floor(),ceil() for: {{ (5.46).| }}', () => {
|
||||
test('should recommend extractDomain(), extractUrlPath() for: {{ "https://n8n.io/pricing".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'https://n8n.io/pricing',
|
||||
);
|
||||
const options = completions('{{ "https://n8n.io/pricing".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[1]).toEqual(
|
||||
expect.objectContaining({ label: 'extractUrlPath()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend round(),floor(),ceil() for: {{ (5.46).| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
5.46,
|
||||
@@ -604,6 +621,50 @@ describe('Resolution-based completions', () => {
|
||||
expect.objectContaining({ label: 'ceil()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toDateTime("s") for: {{ (1900062210).| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
1900062210,
|
||||
);
|
||||
const options = completions('{{ (1900062210).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDateTime("s")', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toDateTime("ms") for: {{ (1900062210000).| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
1900062210000,
|
||||
);
|
||||
const options = completions('{{ (1900062210000).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDateTime("ms")', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toBoolean() for: {{ (0).| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
0,
|
||||
);
|
||||
const options = completions('{{ (0).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toBoolean()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommend toBoolean() for: {{ "true".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'true',
|
||||
);
|
||||
const options = completions('{{ "true".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toBoolean()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('explicit completions (opened by Ctrl+Space or programatically)', () => {
|
||||
|
||||
@@ -145,7 +145,7 @@ export const STRING_RECOMMENDED_OPTIONS = [
|
||||
|
||||
export const DATE_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'extract()'];
|
||||
export const LUXON_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'diff()', 'extract()'];
|
||||
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()'];
|
||||
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()', 'hasField()'];
|
||||
export const ARRAY_RECOMMENDED_OPTIONS = ['length', 'last()', 'includes()', 'map()', 'filter()'];
|
||||
export const ARRAY_NUMBER_ONLY_METHODS = ['max()', 'min()', 'sum()', 'average()'];
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
applyCompletion,
|
||||
sortCompletionsAlpha,
|
||||
hasRequiredArgs,
|
||||
getDefaultArgs,
|
||||
insertDefaultArgs,
|
||||
} from './utils';
|
||||
import type {
|
||||
Completion,
|
||||
@@ -155,6 +157,10 @@ function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||
return stringOptions(input as AutocompleteInput<string>);
|
||||
}
|
||||
|
||||
if (typeof resolved === 'boolean') {
|
||||
return booleanOptions();
|
||||
}
|
||||
|
||||
if (resolved instanceof DateTime) {
|
||||
return luxonOptions(input as AutocompleteInput<DateTime>);
|
||||
}
|
||||
@@ -239,7 +245,7 @@ export const toOptions = (
|
||||
) => {
|
||||
return Object.entries(fnToDoc)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.filter(([, docInfo]) => !docInfo.doc?.hidden || includeHidden)
|
||||
.filter(([, docInfo]) => (docInfo.doc && !docInfo.doc?.hidden) || includeHidden)
|
||||
.map(([fnName, docInfo]) => {
|
||||
return createCompletionOption(typeName, fnName, optionType, docInfo, transformLabel);
|
||||
});
|
||||
@@ -258,7 +264,11 @@ const createCompletionOption = (
|
||||
label,
|
||||
type: optionType,
|
||||
section: docInfo.doc?.section,
|
||||
apply: applyCompletion(hasRequiredArgs(docInfo?.doc), transformLabel),
|
||||
apply: applyCompletion({
|
||||
hasArgs: hasRequiredArgs(docInfo?.doc),
|
||||
defaultArgs: getDefaultArgs(docInfo?.doc),
|
||||
transformLabel,
|
||||
}),
|
||||
};
|
||||
|
||||
option.info = () => {
|
||||
@@ -395,8 +405,8 @@ const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||
label: isFunction ? key + '()' : key,
|
||||
type: isFunction ? 'function' : 'keyword',
|
||||
section: getObjectPropertySection({ name, key, isFunction }),
|
||||
apply: applyCompletion({ hasArgs, transformLabel }),
|
||||
detail: getDetail(name, resolvedProp),
|
||||
apply: applyCompletion(hasArgs, transformLabel),
|
||||
};
|
||||
|
||||
const infoKey = [name, key].join('.');
|
||||
@@ -466,7 +476,7 @@ const applySections = ({
|
||||
recommendedSection = RECOMMENDED_SECTION,
|
||||
}: {
|
||||
options: Completion[];
|
||||
recommended?: string[];
|
||||
recommended?: Array<string | { label: string; args: unknown[] }>;
|
||||
recommendedSection?: CompletionSection;
|
||||
methodsSection?: CompletionSection;
|
||||
propSection?: CompletionSection;
|
||||
@@ -482,12 +492,12 @@ const applySections = ({
|
||||
{} as Record<string, Completion>,
|
||||
);
|
||||
return recommended
|
||||
.map(
|
||||
(reco): Completion => ({
|
||||
...optionByLabel[reco],
|
||||
section: recommendedSection,
|
||||
}),
|
||||
)
|
||||
.map((reco): Completion => {
|
||||
const option = optionByLabel[typeof reco === 'string' ? reco : reco.label];
|
||||
const label =
|
||||
typeof reco === 'string' ? option.label : insertDefaultArgs(reco.label, reco.args);
|
||||
return { ...option, label, section: recommendedSection };
|
||||
})
|
||||
.concat(
|
||||
options
|
||||
.filter((option) => !excludeRecommended || !recommendedSet.has(option.label))
|
||||
@@ -529,12 +539,12 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
if (validateFieldType('string', resolved, 'dateTime').valid) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toDate()'],
|
||||
recommended: ['toDateTime()'],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (VALID_EMAIL_REGEX.test(resolved) || isUrl(resolved)) {
|
||||
if (VALID_EMAIL_REGEX.test(resolved)) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['extractDomain()', 'isEmail()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
@@ -542,6 +552,14 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isUrl(resolved)) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['extractDomain()', 'extractUrlPath()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved.split(/\s/).find((token) => VALID_EMAIL_REGEX.test(token))) {
|
||||
return applySections({
|
||||
options,
|
||||
@@ -550,6 +568,26 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
});
|
||||
}
|
||||
|
||||
const trimmed = resolved.trim();
|
||||
if (
|
||||
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
||||
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
||||
) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['parseJson()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (['true', 'false'].includes(resolved.toLocaleLowerCase())) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toBoolean()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
return applySections({
|
||||
options,
|
||||
recommended: STRING_RECOMMENDED_OPTIONS,
|
||||
@@ -557,6 +595,12 @@ const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
});
|
||||
};
|
||||
|
||||
const booleanOptions = (): Completion[] => {
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([...natives('boolean'), ...extensions('boolean')]),
|
||||
});
|
||||
};
|
||||
|
||||
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
const { resolved, transformLabel } = input;
|
||||
const options = sortCompletionsAlpha([
|
||||
@@ -566,6 +610,36 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
||||
|
||||
if (Number.isInteger(resolved)) {
|
||||
const nowMillis = Date.now();
|
||||
const marginMillis = 946_707_779_000; // 30y
|
||||
const isPlausableMillisDateTime =
|
||||
resolved > nowMillis - marginMillis && resolved < nowMillis + marginMillis;
|
||||
|
||||
if (isPlausableMillisDateTime) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: [{ label: 'toDateTime()', args: ['ms'] }],
|
||||
});
|
||||
}
|
||||
|
||||
const nowSeconds = nowMillis / 1000;
|
||||
const marginSeconds = marginMillis / 1000;
|
||||
const isPlausableSecondsDateTime =
|
||||
resolved > nowSeconds - marginSeconds && resolved < nowSeconds + marginSeconds;
|
||||
if (isPlausableSecondsDateTime) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: [{ label: 'toDateTime()', args: ['s'] }],
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved === 0 || resolved === 1) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toBoolean()'],
|
||||
});
|
||||
}
|
||||
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ONLY_INTEGER,
|
||||
@@ -574,7 +648,7 @@ const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
const exclude = new Set(ONLY_INTEGER);
|
||||
return applySections({
|
||||
options: options.filter((option) => !exclude.has(option.label)),
|
||||
recommended: ['round()', 'floor()', 'ceil()', 'toFixed()'],
|
||||
recommended: ['round()', 'floor()', 'ceil()'],
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -775,7 +849,7 @@ const createLuxonAutocompleteOption = (
|
||||
};
|
||||
}
|
||||
|
||||
if (doc?.hidden && !includeHidden) {
|
||||
if (!doc || (doc?.hidden && !includeHidden)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -783,7 +857,11 @@ const createLuxonAutocompleteOption = (
|
||||
label,
|
||||
type,
|
||||
section: doc?.section,
|
||||
apply: applyCompletion(hasRequiredArgs(doc), transformLabel),
|
||||
apply: applyCompletion({
|
||||
hasArgs: hasRequiredArgs(doc),
|
||||
defaultArgs: getDefaultArgs(doc),
|
||||
transformLabel,
|
||||
}),
|
||||
};
|
||||
option.info = createCompletionOption(
|
||||
'DateTime',
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
hasActiveNode,
|
||||
isCredentialsModalOpen,
|
||||
applyCompletion,
|
||||
isInHttpNodePagination,
|
||||
} from './utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||
import { PREVIOUS_NODES_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
||||
import { PREVIOUS_NODES_SECTION, RECOMMENDED_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
||||
|
||||
/**
|
||||
* Completions offered at the dollar position: `$|`
|
||||
@@ -48,6 +49,15 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
|
||||
|
||||
export function dollarOptions(): Completion[] {
|
||||
const SKIP = new Set();
|
||||
let recommendedCompletions: Completion[] = [];
|
||||
|
||||
if (isInHttpNodePagination()) {
|
||||
recommendedCompletions = [
|
||||
{ label: '$pageCount', section: RECOMMENDED_SECTION, info: i18n.rootVars.$pageCount },
|
||||
{ label: '$response', section: RECOMMENDED_SECTION, info: i18n.rootVars.$response },
|
||||
{ label: '$request', section: RECOMMENDED_SECTION, info: i18n.rootVars.$request },
|
||||
];
|
||||
}
|
||||
|
||||
if (isCredentialsModalOpen()) {
|
||||
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled
|
||||
@@ -77,7 +87,9 @@ export function dollarOptions(): Completion[] {
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
}));
|
||||
|
||||
return ROOT_DOLLAR_COMPLETIONS.filter(({ label }) => !SKIP.has(label))
|
||||
return recommendedCompletions
|
||||
.concat(ROOT_DOLLAR_COMPLETIONS)
|
||||
.filter(({ label }) => !SKIP.has(label))
|
||||
.concat(previousNodesCompletions)
|
||||
.map((completion) => ({ ...completion, apply: applyCompletion() }));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { DocMetadata } from 'n8n-workflow';
|
||||
|
||||
export type Resolved = unknown;
|
||||
|
||||
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object';
|
||||
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object' | 'boolean';
|
||||
|
||||
export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } };
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY, SPLIT_IN_BATCHES_NODE_TYPE } from '@/constants';
|
||||
import {
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
SPLIT_IN_BATCHES_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
@@ -120,6 +124,14 @@ export function hasNoParams(toResolve: string) {
|
||||
|
||||
export const isCredentialsModalOpen = () => useUIStore().modals[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 hasActiveNode = () => useNDVStore().activeNode?.name !== undefined;
|
||||
|
||||
export const isSplitInBatchesAbsent = () =>
|
||||
@@ -151,23 +163,50 @@ export const stripExcessParens = (context: CompletionContext) => (option: Comple
|
||||
return option;
|
||||
};
|
||||
|
||||
export const getDefaultArgs = (doc?: DocMetadata): unknown[] => {
|
||||
return doc?.args?.map((arg) => arg.default).filter(Boolean) ?? [];
|
||||
};
|
||||
|
||||
export const insertDefaultArgs = (label: string, args: unknown[]): string => {
|
||||
if (!label.endsWith('()')) return label;
|
||||
const argList = args.map((arg) => JSON.stringify(arg)).join(', ');
|
||||
const fnName = label.replace('()', '');
|
||||
|
||||
return `${fnName}(${argList})`;
|
||||
};
|
||||
|
||||
/**
|
||||
* When a function completion is selected, set the cursor correctly
|
||||
* @example `.includes()` -> `.includes(<cursor>)`
|
||||
*
|
||||
* @example `.includes()` -> `.includes(<cursor>)`
|
||||
* @example `$max()` -> `$max()<cursor>`
|
||||
*/
|
||||
export const applyCompletion =
|
||||
(hasArgs = true, transform: (label: string) => string = (label) => label) =>
|
||||
({
|
||||
hasArgs = true,
|
||||
defaultArgs = [],
|
||||
transformLabel = (label) => label,
|
||||
}: {
|
||||
hasArgs?: boolean;
|
||||
defaultArgs?: unknown[];
|
||||
transformLabel?: (label: string) => string;
|
||||
} = {}) =>
|
||||
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
||||
const label = transform(completion.label);
|
||||
const isFunction = completion.label.endsWith('()');
|
||||
const label = insertDefaultArgs(transformLabel(completion.label), defaultArgs);
|
||||
|
||||
const tx: TransactionSpec = {
|
||||
...insertCompletionText(view.state, label, from, to),
|
||||
annotations: pickedCompletion.of(completion),
|
||||
};
|
||||
|
||||
if (label.endsWith('()') && hasArgs) {
|
||||
const cursorPosition = from + label.length - 1;
|
||||
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
||||
if (isFunction) {
|
||||
if (defaultArgs.length > 0) {
|
||||
tx.selection = { anchor: from + label.indexOf('(') + 1, head: from + label.length - 1 };
|
||||
} else if (hasArgs) {
|
||||
const cursorPosition = from + label.length - 1;
|
||||
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
||||
}
|
||||
}
|
||||
|
||||
view.dispatch(tx);
|
||||
|
||||
@@ -376,6 +376,9 @@ export class I18nClass {
|
||||
$vars: this.baseText('codeNodeEditor.completer.$vars'),
|
||||
$workflow: this.baseText('codeNodeEditor.completer.$workflow'),
|
||||
DateTime: this.baseText('codeNodeEditor.completer.dateTime'),
|
||||
$request: this.baseText('codeNodeEditor.completer.$request'),
|
||||
$response: this.baseText('codeNodeEditor.completer.$response'),
|
||||
$pageCount: this.baseText('codeNodeEditor.completer.$pageCount'),
|
||||
} as const satisfies Record<string, string | undefined>;
|
||||
|
||||
proxyVars: Record<string, string | undefined> = {
|
||||
|
||||
@@ -206,6 +206,9 @@
|
||||
"codeNodeEditor.completer.$workflow.active": "Whether the workflow is active or not (boolean)",
|
||||
"codeNodeEditor.completer.$workflow.id": "The ID of the workflow",
|
||||
"codeNodeEditor.completer.$workflow.name": "The name of the workflow",
|
||||
"codeNodeEditor.completer.$response": "The response object received by the HTTP node.",
|
||||
"codeNodeEditor.completer.$request": "The request object sent by the HTTP node.",
|
||||
"codeNodeEditor.completer.$pageCount": "Tracks how many pages the HTTP node has fetched.",
|
||||
"codeNodeEditor.completer.dateTime": "Luxon DateTime. Use this object to parse, format and manipulate dates and times",
|
||||
"codeNodeEditor.completer.binary": "The item's binary (file) data",
|
||||
"codeNodeEditor.completer.globalObject.assign": "Copy of the object containing all enumerable own properties",
|
||||
|
||||
@@ -44,6 +44,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||
},
|
||||
},
|
||||
focusedMappableInput: '',
|
||||
focusedInputPath: '',
|
||||
mappingTelemetry: {},
|
||||
hoveringItem: null,
|
||||
draggable: {
|
||||
@@ -268,5 +269,8 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||
});
|
||||
}
|
||||
},
|
||||
setFocusedInputPath(path: string) {
|
||||
this.focusedInputPath = path;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user