mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
refactor(editor): Reintroduce item
and items
to CodeNodeEditor
(#4553)
* ⚡ Alias legacy refs to new syntax * 📘 Adjust types * 👕 Switch `item` lint error to warning * ⚡ Add completions for legacy vars * ✏️ Add descriptions to completions * ⚡ Add lintings * 📘 Skip `any` for now * ⚡ Expand regex
This commit is contained in:
parent
953457ad86
commit
9582a0f1c0
|
@ -43,6 +43,7 @@ export const completerExtension = mixins(
|
||||||
localCompletionSource,
|
localCompletionSource,
|
||||||
|
|
||||||
// core
|
// core
|
||||||
|
this.itemCompletions,
|
||||||
this.baseCompletions,
|
this.baseCompletions,
|
||||||
this.requireCompletions,
|
this.requireCompletions,
|
||||||
this.nodeSelectorCompletions,
|
this.nodeSelectorCompletions,
|
||||||
|
|
|
@ -15,11 +15,34 @@ function getAutocompletableNodeNames(nodes: INodeUi[]) {
|
||||||
|
|
||||||
export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({
|
export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(
|
...mapStores(useWorkflowsStore),
|
||||||
useWorkflowsStore,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
itemCompletions(context: CompletionContext): CompletionResult | null {
|
||||||
|
const preCursor = context.matchBefore(/i\w*/);
|
||||||
|
|
||||||
|
if (!preCursor || (preCursor.from === preCursor.to && !context.explicit)) return null;
|
||||||
|
|
||||||
|
const options: Completion[] = [];
|
||||||
|
|
||||||
|
if (this.mode === 'runOnceForEachItem') {
|
||||||
|
options.push({
|
||||||
|
label: 'item',
|
||||||
|
info: this.$locale.baseText('codeNodeEditor.completer.$input.item'),
|
||||||
|
});
|
||||||
|
} else if (this.mode === 'runOnceForAllItems') {
|
||||||
|
options.push({
|
||||||
|
label: 'items',
|
||||||
|
info: this.$locale.baseText('codeNodeEditor.completer.$input.all'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: preCursor.from,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - Complete `$` to `$execution $input $prevNode $runIndex $workflow $now $today
|
* - Complete `$` to `$execution $input $prevNode $runIndex $workflow $now $today
|
||||||
* $jmespath $('nodeName')` in both modes.
|
* $jmespath $('nodeName')` in both modes.
|
||||||
|
|
|
@ -126,19 +126,19 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
|
||||||
/**
|
/**
|
||||||
* Lint for `.item` unavailable in `runOnceForAllItems` mode
|
* Lint for `.item` unavailable in `runOnceForAllItems` mode
|
||||||
*
|
*
|
||||||
* $input.all().item -> <removed>
|
* $input.item -> <removed>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (this.mode === 'runOnceForAllItems') {
|
if (this.mode === 'runOnceForAllItems') {
|
||||||
type TargetNode = RangeNode & { property: RangeNode };
|
type TargetNode = RangeNode & { property: RangeNode };
|
||||||
|
|
||||||
const isUnavailablePropertyinAllItems = (node: Node) =>
|
const isUnavailableItemAccess = (node: Node) =>
|
||||||
node.type === 'MemberExpression' &&
|
node.type === 'MemberExpression' &&
|
||||||
node.computed === false &&
|
node.computed === false &&
|
||||||
node.property.type === 'Identifier' &&
|
node.property.type === 'Identifier' &&
|
||||||
node.property.name === 'item';
|
node.property.name === 'item';
|
||||||
|
|
||||||
walk<TargetNode>(ast, isUnavailablePropertyinAllItems).forEach((node) => {
|
walk<TargetNode>(ast, isUnavailableItemAccess).forEach((node) => {
|
||||||
const [start, end] = this.getRange(node.property);
|
const [start, end] = this.getRange(node.property);
|
||||||
|
|
||||||
lintings.push({
|
lintings.push({
|
||||||
|
@ -159,39 +159,74 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lint for `item` (legacy var from Function Item node) being accessed
|
* Lint for `item` (legacy var from Function Item node) unavailable
|
||||||
* in `runOnceForEachItem` mode, unless user-defined `item`.
|
* in `runOnceForAllItems` mode, unless user-defined `item`.
|
||||||
*
|
*
|
||||||
* item. -> $input.item.json.
|
* item -> $input.all()
|
||||||
*/
|
*/
|
||||||
if (this.mode === 'runOnceForEachItem' && !/(let|const|var) item =/.test(script)) {
|
if (this.mode === 'runOnceForAllItems' && !/(let|const|var) item (=|of)/.test(script)) {
|
||||||
type TargetNode = RangeNode & { object: RangeNode & { name: string } };
|
type TargetNode = RangeNode & { object: RangeNode & { name: string } };
|
||||||
|
|
||||||
const isItemAccess = (node: Node) =>
|
const isUnavailableLegacyItems = (node: Node) =>
|
||||||
node.type === 'MemberExpression' &&
|
node.type === 'Identifier' && node.name === 'item';
|
||||||
node.computed === false &&
|
|
||||||
node.object.type === 'Identifier' &&
|
|
||||||
node.object.name === 'item';
|
|
||||||
|
|
||||||
walk<TargetNode>(ast, isItemAccess).forEach((node) => {
|
walk<TargetNode>(ast, isUnavailableLegacyItems).forEach((node) => {
|
||||||
const [start, end] = this.getRange(node.object);
|
const [start, end] = this.getRange(node);
|
||||||
|
|
||||||
lintings.push({
|
lintings.push({
|
||||||
from: start,
|
from: start,
|
||||||
to: end,
|
to: end,
|
||||||
severity: DEFAULT_LINTER_SEVERITY,
|
severity: DEFAULT_LINTER_SEVERITY,
|
||||||
message: this.$locale.baseText('codeNodeEditor.linter.eachItem.legacyItemAccess'),
|
message: this.$locale.baseText('codeNodeEditor.linter.allItems.unavailableItem'),
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: 'Fix',
|
name: 'Fix',
|
||||||
apply(view, from, to) {
|
apply(view, from, to) {
|
||||||
// prevent second insertion of unknown origin
|
// prevent second insertion of unknown origin
|
||||||
if (view.state.doc.toString().slice(from, to).includes('$input.item.json')) {
|
if (view.state.doc.toString().slice(from, to).includes('$input.all()')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
view.dispatch({ changes: { from: start, to: end } });
|
view.dispatch({ changes: { from: start, to: end } });
|
||||||
view.dispatch({ changes: { from, insert: '$input.item.json' } });
|
view.dispatch({ changes: { from, insert: '$input.all()' } });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lint for `items` (legacy var from Function node) unavailable
|
||||||
|
* in `runOnceForEachItem` mode, unless user-defined `items`.
|
||||||
|
*
|
||||||
|
* items -> $input.item
|
||||||
|
*/
|
||||||
|
if (this.mode === 'runOnceForEachItem' && !/(let|const|var) items =/.test(script)) {
|
||||||
|
type TargetNode = RangeNode & { object: RangeNode & { name: string } };
|
||||||
|
|
||||||
|
const isUnavailableLegacyItems = (node: Node) =>
|
||||||
|
node.type === 'Identifier' && node.name === 'items';
|
||||||
|
|
||||||
|
walk<TargetNode>(ast, isUnavailableLegacyItems).forEach((node) => {
|
||||||
|
const [start, end] = this.getRange(node);
|
||||||
|
|
||||||
|
lintings.push({
|
||||||
|
from: start,
|
||||||
|
to: end,
|
||||||
|
severity: DEFAULT_LINTER_SEVERITY,
|
||||||
|
message: this.$locale.baseText('codeNodeEditor.linter.eachItem.unavailableItems'),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'Fix',
|
||||||
|
apply(view, from, to) {
|
||||||
|
// prevent second insertion of unknown origin
|
||||||
|
if (view.state.doc.toString().slice(from, to).includes('$input.item')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dispatch({ changes: { from: start, to: end } });
|
||||||
|
view.dispatch({ changes: { from, insert: '$input.item' } });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -285,8 +320,8 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
|
||||||
node.callee.object.type === 'Identifier' &&
|
node.callee.object.type === 'Identifier' &&
|
||||||
node.callee.object.name === '$input' &&
|
node.callee.object.name === '$input' &&
|
||||||
node.callee.property.type === 'Identifier' &&
|
node.callee.property.type === 'Identifier' &&
|
||||||
['first', 'last'].includes(node.callee.property.name)
|
['first', 'last'].includes(node.callee.property.name) &&
|
||||||
&& node.arguments.length !== 0;
|
node.arguments.length !== 0;
|
||||||
|
|
||||||
walk<TargetNode>(ast, inputFirstOrLastCalledWithArg).forEach((node) => {
|
walk<TargetNode>(ast, inputFirstOrLastCalledWithArg).forEach((node) => {
|
||||||
const [start, end] = this.getRange(node.callee.property);
|
const [start, end] = this.getRange(node.callee.property);
|
||||||
|
|
|
@ -229,6 +229,7 @@
|
||||||
"codeNodeEditor.linter.allItems.emptyReturn": "Code doesn't return items properly. Please return an array of objects, one for each item you would like to output.",
|
"codeNodeEditor.linter.allItems.emptyReturn": "Code doesn't return items properly. Please return an array of objects, one for each item you would like to output.",
|
||||||
"codeNodeEditor.linter.allItems.itemCall": "`item` is a property to access, not a method to call. Did you mean `.item` without brackets?",
|
"codeNodeEditor.linter.allItems.itemCall": "`item` is a property to access, not a method to call. Did you mean `.item` without brackets?",
|
||||||
"codeNodeEditor.linter.allItems.itemMatchingNoArg": "`.itemMatching()` expects an item index to be passed in as its argument.",
|
"codeNodeEditor.linter.allItems.itemMatchingNoArg": "`.itemMatching()` expects an item index to be passed in as its argument.",
|
||||||
|
"codeNodeEditor.linter.allItems.unavailableItem": "Legacy `item` is only available in the 'Run Once for Each Item' mode.",
|
||||||
"codeNodeEditor.linter.allItems.unavailableProperty": "`.item` is only available in the 'Run Once for Each Item' mode.",
|
"codeNodeEditor.linter.allItems.unavailableProperty": "`.item` is only available in the 'Run Once for Each Item' mode.",
|
||||||
"codeNodeEditor.linter.allItems.unavailableVar": "is only available in the 'Run Once for Each Item' mode.",
|
"codeNodeEditor.linter.allItems.unavailableVar": "is only available in the 'Run Once for Each Item' mode.",
|
||||||
"codeNodeEditor.linter.bothModes.directAccess.firstOrLastCall": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
"codeNodeEditor.linter.bothModes.directAccess.firstOrLastCall": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
||||||
|
@ -236,8 +237,9 @@
|
||||||
"codeNodeEditor.linter.bothModes.varDeclaration.itemProperty": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
"codeNodeEditor.linter.bothModes.varDeclaration.itemProperty": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
||||||
"codeNodeEditor.linter.bothModes.varDeclaration.itemSubproperty": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
"codeNodeEditor.linter.bothModes.varDeclaration.itemSubproperty": "@:_reusableBaseText.codeNodeEditor.linter.useJson",
|
||||||
"codeNodeEditor.linter.eachItem.emptyReturn": "Code doesn't return an object. Please return an object representing the output item",
|
"codeNodeEditor.linter.eachItem.emptyReturn": "Code doesn't return an object. Please return an object representing the output item",
|
||||||
"codeNodeEditor.linter.eachItem.legacyItemAccess": "`item` is not defined. Did you mean `$input.item.json`?",
|
"codeNodeEditor.linter.eachItem.legacyItemAccess": "`item` is a legacy var. Consider using `$input.item`",
|
||||||
"codeNodeEditor.linter.eachItem.returnArray": "Code doesn't return an object. Array found instead. Please return an object representing the output item",
|
"codeNodeEditor.linter.eachItem.returnArray": "Code doesn't return an object. Array found instead. Please return an object representing the output item",
|
||||||
|
"codeNodeEditor.linter.eachItem.unavailableItems": "Legacy `items` is only available in the 'Run Once for All Items' mode.",
|
||||||
"codeNodeEditor.linter.eachItem.unavailableMethod": "Method `$input.{method}()` is only available in the 'Run Once for All Items' mode.",
|
"codeNodeEditor.linter.eachItem.unavailableMethod": "Method `$input.{method}()` is only available in the 'Run Once for All Items' mode.",
|
||||||
"codeNodeEditor.linter.bothModes.syntaxError": "Syntax error",
|
"codeNodeEditor.linter.bothModes.syntaxError": "Syntax error",
|
||||||
"collectionParameter.choose": "Choose...",
|
"collectionParameter.choose": "Choose...",
|
||||||
|
|
|
@ -79,6 +79,7 @@ export class Code implements INodeType {
|
||||||
const jsCodeAllItems = this.getNodeParameter('jsCode', 0) as string;
|
const jsCodeAllItems = this.getNodeParameter('jsCode', 0) as string;
|
||||||
|
|
||||||
const context = getSandboxContext.call(this);
|
const context = getSandboxContext.call(this);
|
||||||
|
context.items = context.$input.all();
|
||||||
const sandbox = new Sandbox(context, workflowMode, nodeMode);
|
const sandbox = new Sandbox(context, workflowMode, nodeMode);
|
||||||
|
|
||||||
if (workflowMode === 'manual') {
|
if (workflowMode === 'manual') {
|
||||||
|
@ -111,6 +112,7 @@ export class Code implements INodeType {
|
||||||
const jsCodeEachItem = this.getNodeParameter('jsCode', index) as string;
|
const jsCodeEachItem = this.getNodeParameter('jsCode', index) as string;
|
||||||
|
|
||||||
const context = getSandboxContext.call(this, index);
|
const context = getSandboxContext.call(this, index);
|
||||||
|
context.item = context.$input.item;
|
||||||
const sandbox = new Sandbox(context, workflowMode, nodeMode);
|
const sandbox = new Sandbox(context, workflowMode, nodeMode);
|
||||||
|
|
||||||
if (workflowMode === 'manual') {
|
if (workflowMode === 'manual') {
|
||||||
|
|
|
@ -250,16 +250,19 @@ export class Sandbox extends NodeVM {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSandboxContext(this: IExecuteFunctions, index?: number) {
|
export function getSandboxContext(this: IExecuteFunctions, index?: number) {
|
||||||
const sandboxContext: Record<string, unknown> & { $item: (i: number) => IWorkflowDataProxyData } =
|
const sandboxContext: Record<string, unknown> & {
|
||||||
{
|
$item: (i: number) => IWorkflowDataProxyData;
|
||||||
// from NodeExecuteFunctions
|
$input: any; // tslint:disable-line: no-any
|
||||||
$getNodeParameter: this.getNodeParameter,
|
} = {
|
||||||
$getWorkflowStaticData: this.getWorkflowStaticData,
|
// from NodeExecuteFunctions
|
||||||
helpers: this.helpers,
|
$getNodeParameter: this.getNodeParameter,
|
||||||
|
$getWorkflowStaticData: this.getWorkflowStaticData,
|
||||||
|
helpers: this.helpers,
|
||||||
|
|
||||||
// to bring in all $-prefixed vars and methods from WorkflowDataProxy
|
// to bring in all $-prefixed vars and methods from WorkflowDataProxy
|
||||||
$item: this.getWorkflowDataProxy,
|
$item: this.getWorkflowDataProxy,
|
||||||
};
|
$input: null,
|
||||||
|
};
|
||||||
|
|
||||||
// $node, $items(), $parameter, $json, $env, etc.
|
// $node, $items(), $parameter, $json, $env, etc.
|
||||||
Object.assign(sandboxContext, sandboxContext.$item(index ?? 0));
|
Object.assign(sandboxContext, sandboxContext.$item(index ?? 0));
|
||||||
|
|
Loading…
Reference in a new issue