mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
import { NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION } from '@/components/CodeNodeEditor/constants';
|
|
import { CREDENTIAL_EDIT_MODAL_KEY, SPLIT_IN_BATCHES_NODE_TYPE } from '@/constants';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { resolveParameter } from '@/mixins/workflowHelpers';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useUIStore } from '@/stores/ui.store';
|
|
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
|
|
|
// String literal expression is everything enclosed in single, double or tick quotes following a dot
|
|
const stringLiteralRegex = /^"[^"]+"|^'[^']+'|^`[^`]+`\./;
|
|
// JavaScript operands
|
|
const operandsRegex = /[+\-*/><<==>**!=?]/;
|
|
/**
|
|
* Split user input into base (to resolve) and tail (to filter).
|
|
*/
|
|
export function splitBaseTail(userInput: string): [string, string] {
|
|
const processedInput = extractSubExpression(userInput);
|
|
const parts = processedInput.split('.');
|
|
const tail = parts.pop() ?? '';
|
|
|
|
return [parts.join('.'), tail];
|
|
}
|
|
|
|
export function longestCommonPrefix(...strings: string[]) {
|
|
if (strings.length < 2) {
|
|
throw new Error('Expected at least two strings');
|
|
}
|
|
|
|
return strings.reduce((acc, next) => {
|
|
let i = 0;
|
|
|
|
while (acc[i] && next[i] && acc[i] === next[i]) {
|
|
i++;
|
|
}
|
|
|
|
return acc.slice(0, i);
|
|
}, '');
|
|
}
|
|
|
|
// Process user input if expressions are used as part of complex expression
|
|
// i.e. as a function parameter or an operation expression
|
|
// this function will extract expression that is currently typed so autocomplete
|
|
// suggestions can be matched based on it.
|
|
function extractSubExpression(userInput: string): string {
|
|
const dollarSignIndex = userInput.indexOf('$');
|
|
if (dollarSignIndex === -1) {
|
|
return userInput;
|
|
} else if (!stringLiteralRegex.test(userInput)) {
|
|
// If there is a dollar sign in the input and input is not a string literal,
|
|
// extract part of following the last $
|
|
const expressionParts = userInput.split('$');
|
|
userInput = `$${expressionParts[expressionParts.length - 1]}`;
|
|
// If input is part of a complex operation expression and extract last operand
|
|
const operationPart = userInput.split(operandsRegex).pop()?.trim() || '';
|
|
const lastOperand = operationPart.split(' ').pop();
|
|
if (lastOperand) {
|
|
userInput = lastOperand;
|
|
}
|
|
}
|
|
return userInput;
|
|
}
|
|
|
|
export const prefixMatch = (first: string, second: string) =>
|
|
first.startsWith(second) && first !== second;
|
|
|
|
/**
|
|
* Make a function to bring selected elements to the start of an array, in order.
|
|
*/
|
|
export const setRank = (selected: string[]) => (full: string[]) => {
|
|
const fullCopy = [...full];
|
|
|
|
[...selected].reverse().forEach((s) => {
|
|
const index = fullCopy.indexOf(s);
|
|
|
|
if (index !== -1) fullCopy.unshift(fullCopy.splice(index, 1)[0]);
|
|
});
|
|
|
|
return fullCopy;
|
|
};
|
|
|
|
export const isPseudoParam = (candidate: string) => {
|
|
const PSEUDO_PARAMS = ['notice']; // user input disallowed
|
|
|
|
return PSEUDO_PARAMS.includes(candidate);
|
|
};
|
|
|
|
/**
|
|
* Whether a string may be used as a key in object dot access notation.
|
|
*/
|
|
export const isAllowedInDotNotation = (str: string) => {
|
|
const DOT_NOTATION_BANNED_CHARS = /^(\d)|[\\ `!@#$%^&*()+\-=[\]{};':"\\|,.<>?~]/g;
|
|
|
|
return !DOT_NOTATION_BANNED_CHARS.test(str);
|
|
};
|
|
|
|
// ----------------------------------
|
|
// resolution-based utils
|
|
// ----------------------------------
|
|
|
|
export function receivesNoBinaryData() {
|
|
try {
|
|
return resolveParameter('={{ $binary }}')?.data === undefined;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function hasNoParams(toResolve: string) {
|
|
let params;
|
|
|
|
try {
|
|
params = resolveParameter(`={{ ${toResolve}.params }}`);
|
|
} catch {
|
|
return true;
|
|
}
|
|
|
|
if (!params) return true;
|
|
|
|
const paramKeys = Object.keys(params);
|
|
|
|
return paramKeys.length === 1 && isPseudoParam(paramKeys[0]);
|
|
}
|
|
|
|
// ----------------------------------
|
|
// state-based utils
|
|
// ----------------------------------
|
|
|
|
export const isCredentialsModalOpen = () => useUIStore().modals[CREDENTIAL_EDIT_MODAL_KEY].open;
|
|
|
|
export const hasActiveNode = () => useNDVStore().activeNode?.name !== undefined;
|
|
|
|
export const isSplitInBatchesAbsent = () =>
|
|
!useWorkflowsStore().workflow.nodes.some((node) => node.type === SPLIT_IN_BATCHES_NODE_TYPE);
|
|
|
|
export function autocompletableNodeNames() {
|
|
return useWorkflowsStore()
|
|
.allNodes.filter((node) => {
|
|
const activeNodeName = useNDVStore().activeNode?.name;
|
|
|
|
return (
|
|
!NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION.includes(node.type) && node.name !== activeNodeName
|
|
);
|
|
})
|
|
.map((node) => node.name);
|
|
}
|
|
|
|
/**
|
|
* Remove excess parens from an option label when the cursor is already
|
|
* followed by parens, e.g. `$json.myStr.|()` -> `isNumeric`
|
|
*/
|
|
export const stripExcessParens = (context: CompletionContext) => (option: Completion) => {
|
|
const followedByParens = context.state.sliceDoc(context.pos, context.pos + 2) === '()';
|
|
|
|
if (option.label.endsWith('()') && followedByParens) {
|
|
option.label = option.label.slice(0, '()'.length * -1);
|
|
}
|
|
|
|
return option;
|
|
};
|