feat(editor): Add lint for $('Node').item in runOnceForAllItems mode (#10743)

This commit is contained in:
Elias Meire 2024-09-10 17:26:17 +02:00 committed by GitHub
parent 6ecf84ffe8
commit 1b04be1240
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 64 additions and 7 deletions

View file

@ -38,6 +38,51 @@ describe('Code node', () => {
successToast().contains('Node executed successfully');
});
it('should show lint errors in `runOnceForAllItems` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
getEditor().type('{selectall}').paste(`$input.itemMatching()
$input.item
$('When clicking Test workflow').item
$input.first(1)
for (const item of $input.all()) {
item.foo
}
return
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 6);
getParameter().contains('itemMatching').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
'`.itemMatching()` expects an item index to be passed in as its argument.',
);
});
it('should show lint errors in `runOnceForEachItem` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
getEditor().type('{selectall}').paste(`$input.itemMatching()
$input.all()
$input.first()
$input.item()
return []
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 5);
getParameter().contains('all').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
"Method `$input.all()` is only available in the 'Run Once for All Items' mode.",
);
});
});
describe('Ask AI', () => {

View file

@ -2,7 +2,7 @@ import type { Diagnostic } from '@codemirror/lint';
import { linter } from '@codemirror/lint';
import type { EditorView } from '@codemirror/view';
import * as esprima from 'esprima-next';
import type { Node } from 'estree';
import type { Node, MemberExpression } from 'estree';
import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow';
import { toValue, type MaybeRefOrGetter } from 'vue';
@ -153,13 +153,20 @@ export const useLinter = (
if (toValue(mode) === 'runOnceForAllItems') {
type TargetNode = RangeNode & { property: RangeNode };
const isInputIdentifier = (node: Node) =>
node.type === 'Identifier' && node.name === '$input';
const isPreviousNodeCall = (node: Node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$';
const isDirectMemberExpression = (node: Node): node is MemberExpression =>
node.type === 'MemberExpression' && !node.computed;
const isItemIdentifier = (node: Node) => node.type === 'Identifier' && node.name === 'item';
const isUnavailableInputItemAccess = (node: Node) =>
node.type === 'MemberExpression' &&
!node.computed &&
node.object.type === 'Identifier' &&
node.object.name === '$input' &&
node.property.type === 'Identifier' &&
node.property.name === 'item';
isDirectMemberExpression(node) &&
(isInputIdentifier(node.object) || isPreviousNodeCall(node.object)) &&
isItemIdentifier(node.property);
walk<TargetNode>(ast, isUnavailableInputItemAccess).forEach((node) => {
const [start, end] = getRange(node.property);

View file

@ -102,6 +102,11 @@
padding: var(--spacing-xs);
}
&:has(.cm-tooltip-lint) {
padding: 0;
overflow: hidden;
}
.autocomplete-info-container {
display: flex;
flex-direction: column;