Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(editor): Reintroduce item and items to CodeNodeEditor #4553

Merged
merged 9 commits into from
Nov 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const completerExtension = mixins(
localCompletionSource,

// core
this.itemCompletions,
this.baseCompletions,
this.requireCompletions,
this.nodeSelectorCompletions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,34 @@ function getAutocompletableNodeNames(nodes: INodeUi[]) {

export const baseCompletions = (Vue as CodeNodeEditorMixin).extend({
computed: {
...mapStores(
useWorkflowsStore,
),
...mapStores(useWorkflowsStore),
},
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
* $jmespath $('nodeName')` in both modes.
Expand Down
73 changes: 54 additions & 19 deletions packages/editor-ui/src/components/CodeNodeEditor/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,19 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
/**
* Lint for `.item` unavailable in `runOnceForAllItems` mode
*
* $input.all().item -> <removed>
* $input.item -> <removed>
*/

if (this.mode === 'runOnceForAllItems') {
type TargetNode = RangeNode & { property: RangeNode };

const isUnavailablePropertyinAllItems = (node: Node) =>
const isUnavailableItemAccess = (node: Node) =>
node.type === 'MemberExpression' &&
node.computed === false &&
node.property.type === 'Identifier' &&
node.property.name === 'item';

walk<TargetNode>(ast, isUnavailablePropertyinAllItems).forEach((node) => {
walk<TargetNode>(ast, isUnavailableItemAccess).forEach((node) => {
const [start, end] = this.getRange(node.property);

lintings.push({
Expand All @@ -159,39 +159,74 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
}

/**
* Lint for `item` (legacy var from Function Item node) being accessed
* in `runOnceForEachItem` mode, unless user-defined `item`.
* Lint for `item` (legacy var from Function Item node) unavailable
* 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 } };

const isItemAccess = (node: Node) =>
node.type === 'MemberExpression' &&
node.computed === false &&
node.object.type === 'Identifier' &&
node.object.name === 'item';
const isUnavailableLegacyItems = (node: Node) =>
node.type === 'Identifier' && node.name === 'item';

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.allItems.unavailableItem'),
actions: [
{
name: 'Fix',
apply(view, from, to) {
// prevent second insertion of unknown origin
if (view.state.doc.toString().slice(from, to).includes('$input.all()')) {
return;
}

view.dispatch({ changes: { from: start, to: end } });
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 } };

walk<TargetNode>(ast, isItemAccess).forEach((node) => {
const [start, end] = this.getRange(node.object);
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.legacyItemAccess'),
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.json')) {
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.json' } });
view.dispatch({ changes: { from, insert: '$input.item' } });
},
},
],
Expand Down Expand Up @@ -285,8 +320,8 @@ export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
node.callee.object.type === 'Identifier' &&
node.callee.object.name === '$input' &&
node.callee.property.type === 'Identifier' &&
['first', 'last'].includes(node.callee.property.name)
&& node.arguments.length !== 0;
['first', 'last'].includes(node.callee.property.name) &&
node.arguments.length !== 0;

walk<TargetNode>(ast, inputFirstOrLastCalledWithArg).forEach((node) => {
const [start, end] = this.getRange(node.callee.property);
Expand Down
4 changes: 3 additions & 1 deletion packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,17 @@
"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.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.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.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.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.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.bothModes.syntaxError": "Syntax error",
"collectionParameter.choose": "Choose...",
Expand Down
2 changes: 2 additions & 0 deletions packages/nodes-base/nodes/Code/Code.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class Code implements INodeType {
const jsCodeAllItems = this.getNodeParameter('jsCode', 0) as string;

const context = getSandboxContext.call(this);
context.items = context.$input.all();
const sandbox = new Sandbox(context, workflowMode, nodeMode);

if (workflowMode === 'manual') {
Expand Down Expand Up @@ -111,6 +112,7 @@ export class Code implements INodeType {
const jsCodeEachItem = this.getNodeParameter('jsCode', index) as string;

const context = getSandboxContext.call(this, index);
context.item = context.$input.item;
const sandbox = new Sandbox(context, workflowMode, nodeMode);

if (workflowMode === 'manual') {
Expand Down
23 changes: 13 additions & 10 deletions packages/nodes-base/nodes/Code/Sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,16 +250,19 @@ export class Sandbox extends NodeVM {
}

export function getSandboxContext(this: IExecuteFunctions, index?: number) {
const sandboxContext: Record<string, unknown> & { $item: (i: number) => IWorkflowDataProxyData } =
{
// from NodeExecuteFunctions
$getNodeParameter: this.getNodeParameter,
$getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,

// to bring in all $-prefixed vars and methods from WorkflowDataProxy
$item: this.getWorkflowDataProxy,
};
const sandboxContext: Record<string, unknown> & {
$item: (i: number) => IWorkflowDataProxyData;
$input: any; // tslint:disable-line: no-any
} = {
// from NodeExecuteFunctions
$getNodeParameter: this.getNodeParameter,
$getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,

// to bring in all $-prefixed vars and methods from WorkflowDataProxy
$item: this.getWorkflowDataProxy,
$input: null,
};

// $node, $items(), $parameter, $json, $env, etc.
Object.assign(sandboxContext, sandboxContext.$item(index ?? 0));
Expand Down