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

feat(editor): Completions for extensions in expression editor #5130

Merged
Merged
Show file tree
Hide file tree
Changes from 153 commits
Commits
Show all changes
156 commits
Select commit Hold shift + click to select a range
22bd573
:fire: Remove test extensions
ivov Jan 11, 2023
53f2226
:construction: Add test description
ivov Jan 11, 2023
6a8d709
:blue_book: Expand types
ivov Jan 11, 2023
0edac3b
:zap: Export extensions
ivov Jan 11, 2023
8bb5526
:zap: Export collection
ivov Jan 11, 2023
a925ece
:zap: Mark all proxies
ivov Jan 11, 2023
fe5acdc
:pencil2: Rename for clarity
ivov Jan 11, 2023
ce112cd
:zap: Export from barrel
ivov Jan 11, 2023
fccc9a3
:sparkles: Create datatype completions
ivov Jan 11, 2023
05cce74
:zap: Mount datatype completions
ivov Jan 11, 2023
9d9f610
:test_tube: Adjust tests
ivov Jan 12, 2023
7e4ab66
:twisted_rightwards_arrows: Merge master
ivov Jan 12, 2023
11dd7e2
:zap: Add `path` prop
ivov Jan 12, 2023
9146360
:fire: Remove `()` from completion labels
ivov Jan 12, 2023
09229b5
:zap: Filter out completions for pseudo-proxies
ivov Jan 12, 2023
bc4070c
:bug: Fix method error
ivov Jan 12, 2023
2d9280a
:zap: Add metrics
ivov Jan 12, 2023
269e601
:pencil2: Improve naming
ivov Jan 12, 2023
7f57c8c
:sparkles: Start completion on empty resolvable
ivov Jan 12, 2023
2ee6750
:sparkles: Implement completion previews
ivov Jan 13, 2023
4d329ec
:zap: Break out completion manager
ivov Jan 13, 2023
15e54b6
:zap: Implement in expression editor modal
ivov Jan 13, 2023
9a230bd
:pencil2: Improve naming
ivov Jan 13, 2023
01e064c
:zap: Filter out irrelevant completions
ivov Jan 13, 2023
99e8498
:sparkles: Add preview hint
ivov Jan 14, 2023
3387291
:pencil2: Improve comments
ivov Jan 14, 2023
c827cfe
:art: Style preview hint
ivov Jan 14, 2023
ef48638
:zap: Expand `hasNoParams`
ivov Jan 14, 2023
3e738db
:zap: Add spacing for readability
ivov Jan 14, 2023
8727919
:zap: Add error codes
ivov Jan 14, 2023
d5bd769
:pencil2: Add comment
ivov Jan 14, 2023
73305f9
:bug: Fix Esc behavior
ivov Jan 14, 2023
892d62e
:zap: Parse Unicode
ivov Jan 14, 2023
33f968c
:zap: Throw on invalid `DateTime`
ivov Jan 14, 2023
53331f5
:zap: Fix second root completion detection
ivov Jan 14, 2023
97abc86
:zap: Switch message at completable prefix position
ivov Jan 17, 2023
fbbd0bc
:bug: Fix function names for non-dev build
ivov Jan 18, 2023
0f8f726
:bug: Fix `json` handling
ivov Jan 18, 2023
cba7f56
:fire: Comment out previews
ivov Jan 18, 2023
1abc05f
:recycle: Apply feedback
ivov Jan 18, 2023
d9075d8
:fire: Remove extensions
ivov Jan 19, 2023
6e2f54d
:truck: Rename extensions
ivov Jan 19, 2023
b9b966c
:zap: Adjust some implementations
ivov Jan 19, 2023
e4d17f5
:fire: Remove dummy extensions
ivov Jan 19, 2023
bfa34e6
:bug: Fix object regex
ivov Jan 19, 2023
ad30a1d
:recycle: Apply feedback
ivov Jan 19, 2023
f9a382e
:pencil2: Fix typos
ivov Jan 19, 2023
19d3887
:pencil2: Add `fn is not a function` message
ivov Jan 19, 2023
4ddf260
:fire: Remove check
ivov Jan 20, 2023
8264fcb
:sparkles: Add `isNotEmpty` for objects
ivov Jan 20, 2023
ebe2ef8
:truck: Rename `global` to `alpha`
ivov Jan 20, 2023
e4652e1
:fire: Remove `encrypt`
ivov Jan 20, 2023
ad9f228
:zap: Restore `is not a function` error
ivov Jan 20, 2023
54db360
:zap: Support `week` on `extract()`
ivov Jan 20, 2023
b0b5f18
:test_tube: Fix tests
ivov Jan 20, 2023
4f2823a
:zap: Add validation to some string extensions
ivov Jan 20, 2023
2c9a58f
:zap: Validate number arrays in some extensions
ivov Jan 20, 2023
4899775
:test_tube: Fix tests
ivov Jan 20, 2023
3f03b59
:twisted_rightwards_arrows: Merge master
ivov Jan 20, 2023
f1519b3
:pencil2: Improve error message
ivov Jan 20, 2023
18b185c
:rewind: Revert extensions framework changes
ivov Jan 20, 2023
5b4c56a
:twisted_rightwards_arrows: Merge master
ivov Jan 20, 2023
6fb6e35
:broom: Previews cleanup
ivov Jan 20, 2023
67f57a8
:zap: Condense blank completions
ivov Jan 20, 2023
45b6478
:zap: Refactor dollar completions
ivov Jan 20, 2023
8292b5f
:zap: Refactor non-dollar completions
ivov Jan 20, 2023
213735d
:zap: Refactor Luxon completions
ivov Jan 20, 2023
1100f74
:zap: Refactor datatype completions
ivov Jan 21, 2023
7bfb00f
:twisted_rightwards_arrows: Merge master
ivov Jan 23, 2023
472a77d
:zap: Use `DATETIMEUNIT_MAP`
ivov Jan 23, 2023
a3693d1
:pencil2: Update test description
ivov Jan 23, 2023
623961c
:rewind: Revert "Use `DATETIMEUNIT_MAP`"
ivov Jan 23, 2023
ff670c5
:test_tube: Add tests
ivov Jan 23, 2023
1a6d3bc
:recycle: Restore generic extensions
ivov Jan 24, 2023
d9fffaa
:twisted_rightwards_arrows: Merge master
ivov Jan 24, 2023
fcc5758
:fire: Remove logs
ivov Jan 24, 2023
0183438
:test_tube: Expand tests
ivov Jan 24, 2023
206f15a
:sparkles: Add `Math` completions
ivov Jan 24, 2023
34bc278
:pencil2: List breaking change
ivov Jan 24, 2023
55c33c4
:twisted_rightwards_arrows: Merge `expression-extensions-shortlist`
ivov Jan 24, 2023
999e132
:zap: Add doc tooltips
ivov Jan 24, 2023
d301f58
:bug: Fix node selector regex
ivov Jan 24, 2023
3c79a50
:bug: Fix `context` resolution
ivov Jan 24, 2023
f42e391
:bug: Allow dollar completions in args
ivov Jan 24, 2023
891e371
:zap: Make numeric array methods context-dependent
ivov Jan 24, 2023
152f0d7
:pencil: Adjust docs
ivov Jan 24, 2023
b1d3569
:bug: Fix selector ref
ivov Jan 25, 2023
0e67e5d
:zap: Surface error for valid URL
ivov Jan 25, 2023
b3742d1
:bug: Disallow whitespace in `isEmail` check
ivov Jan 25, 2023
a5c2a9b
:test_tube: Fix test for `isUrl`
ivov Jan 25, 2023
2113474
:zap: Add comma validator in `toFloat`
ivov Jan 25, 2023
b23fe61
:zap: Add validation to `$jmespath()`
ivov Jan 25, 2023
36adc8c
:rewind: Revert valid URL error
ivov Jan 25, 2023
525391d
:zap: Adjust `$jmespath()` validation
ivov Jan 25, 2023
62bf096
:test_tube: Adjust `isUrl` test
ivov Jan 25, 2023
b4147bb
:zap: Remove `{}` and `[]` from compact
ivov Jan 25, 2023
483bc68
:pencil2: Update docs
ivov Jan 25, 2023
59e5638
:truck: Rename `stripTags` to `removeTags`
ivov Jan 25, 2023
2634f02
:zap: Do not inject whitespace inside resolvable
ivov Jan 26, 2023
065f79d
:zap: Make completions aware of `()`
ivov Jan 26, 2023
cf2266b
:pencil2: Add note
ivov Jan 26, 2023
0826772
:zap: Update sorting
ivov Jan 26, 2023
21fe300
:zap: Hide active node name from node selector
ivov Jan 26, 2023
3670cc2
:fire: Remove `length()` and its aliases
ivov Jan 26, 2023
c26c83e
:zap: Validate non-zero for `chunk`
ivov Jan 26, 2023
3827b03
:pencil2: Reword all error messages
ivov Jan 26, 2023
00816a4
:bug: Fix `$now` and `$today`
ivov Jan 26, 2023
b8e7203
:zap: Simplify with `stripExcessParens`
ivov Jan 27, 2023
486d656
:zap: Fold luxon into datatype
ivov Jan 27, 2023
cffd642
:test_tube: Clean up tests
ivov Jan 30, 2023
7437de6
:twisted_rightwards_arrows: Merge master
ivov Jan 30, 2023
65f8680
:fire: Remove tests for removed methods
ivov Jan 30, 2023
1d2c947
:shirt: Fix type
ivov Jan 30, 2023
7c30811
:arrow_up: Upgrade lang pack
ivov Jan 30, 2023
19b480f
:rewind: Undo change to `vitest` command
ivov Jan 30, 2023
42be2f5
:fire: Remove unused method
ivov Jan 30, 2023
ed4a74b
:zap: Separate `return` line
ivov Jan 30, 2023
5f0d8a2
:pencil2: Improve description
ivov Jan 30, 2023
797c1b4
:test_tube: Expand tests for initial-only completions
ivov Jan 30, 2023
034d5e2
:test_tube: Add bracket-aware completions
ivov Jan 30, 2023
21eb8dc
:zap: Make check for `all()` stricter
ivov Jan 30, 2023
1e5b585
:pencil2: Adjust explanatory comments
ivov Jan 30, 2023
82f8317
:fire: Remove unneded copy
ivov Jan 30, 2023
a6797f7
:fire: Remove outdated comment
ivov Jan 30, 2023
ad8cf66
:zap: Make naming consistent
ivov Jan 30, 2023
19d9945
:pencil2: Update comments
ivov Jan 30, 2023
a49f6cc
:zap: Improve URL scheme check
ivov Jan 30, 2023
fc241d1
:pencil2: Add comment
ivov Jan 30, 2023
4f1fd0b
:truck: Move extension
ivov Jan 30, 2023
f0bdef2
:pencil2: Update `BREAKING-CHANGES.md`
ivov Jan 30, 2023
ce7c9eb
:pencil2: Update upcoming version
ivov Jan 30, 2023
14b6143
:pencil2: Fix grammar
ivov Jan 30, 2023
0c986f8
:pencil2: Shorten message
ivov Jan 30, 2023
e3e1dc2
:bug: Fix `Esc` behavior
ivov Jan 31, 2023
cdd8911
:bug: Fix `isNumeric`
ivov Jan 31, 2023
432d526
:sparkles: Support native methods
ivov Jan 31, 2023
f376afc
:twisted_rightwards_arrows: Merge master
ivov Jan 31, 2023
73283ae
:test_tube: Skip Pinia tests
ivov Jan 31, 2023
75dcfc1
:pencil2: Shorten description
ivov Feb 1, 2023
cffcff2
:fire: Remove outdated comment
ivov Feb 1, 2023
54e428b
:test_tube: Unskip Pinia tests
ivov Feb 1, 2023
6f8efb7
:pencil2: Add comments
ivov Feb 1, 2023
a44d614
:test_tube: Expand tests to natives
ivov Feb 1, 2023
8a30f39
:pencil2: Add clarifying comments
ivov Feb 1, 2023
8607d7d
:zap: Use `setTimeout` to make telemetry non-blocking
ivov Feb 1, 2023
f690f10
:bug: Account for no active node in cred modal
ivov Feb 1, 2023
214e7c3
:sparkles: Resolve without workflow
ivov Feb 1, 2023
8b7e550
:fire: Remove `Esc` handling on NDV
ivov Feb 1, 2023
1218a43
:zap: Use `isDateTime`
ivov Feb 1, 2023
5d30ddd
:truck: Move `unique` to next phase
ivov Feb 1, 2023
e9d33c9
:zap: Merge export
ivov Feb 1, 2023
d3efb62
:test_tube: Fix tests
ivov Feb 1, 2023
e7d4f23
:twisted_rightwards_arrows: Merge master
ivov Feb 1, 2023
9f2eeb7
:rewind: Restore check
ivov Feb 2, 2023
a22d1be
:pencil2: Make breaking change description more accurate
ivov Feb 2, 2023
f8a6c99
:test_tube: Fix e2e tests
ivov Feb 2, 2023
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
10 changes: 10 additions & 0 deletions packages/cli/BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

This list shows all the versions which include breaking changes and how to upgrade.

## 0.214.0

### What changed?

In expressions, `DateTime.fromHTTP()`, `DateTime.fromISO()` and `DateTime.fromJSDate()` require an argument. Before, they all resolved `null` when called without an argument; now, they throw an error when called without an argument.

### When is action necessary?

If you are relying on the above behavior, review your workflow to ensure you are passing in the required argument.

## 0.202.0

### What changed?
Expand Down
2 changes: 1 addition & 1 deletion packages/editor-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@jsplumb/util": "^5.13.2",
"axios": "^0.21.1",
"codemirror-lang-html-n8n": "^1.0.0",
"codemirror-lang-n8n-expression": "^0.1.0",
"codemirror-lang-n8n-expression": "^0.2.0",
"dateformat": "^3.0.3",
"esprima-next": "5.8.4",
"fast-json-stable-stringify": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions packages/editor-ui/src/components/ExpressionEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<ExpressionEditorModalInput
:value="value"
:isReadOnly="isReadOnly"
:path="path"
@change="valueChanged"
@close="closeDialog"
ref="inputFieldExpression"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
<template>
<div ref="root" class="ph-no-capture" @keydown.stop @keydown.esc="onClose"></div>
<div ref="root" class="ph-no-capture" @keydown.stop></div>
</template>

<script lang="ts">
import mixins from 'vue-typed-mixins';
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState, Prec } from '@codemirror/state';
import { history } from '@codemirror/commands';

import { workflowHelpers } from '@/mixins/workflowHelpers';
import { expressionManager } from '@/mixins/expressionManager';
import { completionManager } from '@/mixins/completionManager';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { n8nLang } from '@/plugins/codemirror/n8nLang';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { inputTheme } from './theme';

import type { IVariableItemSelected } from '@/Interface';
import { forceParse } from '@/utils/forceParse';
import { autocompletion } from '@codemirror/autocomplete';

export default mixins(expressionManager, workflowHelpers).extend({
import type { IVariableItemSelected } from '@/Interface';

export default mixins(expressionManager, completionManager, workflowHelpers).extend({
name: 'ExpressionEditorModalInput',
props: {
value: {
type: String,
},
path: {
type: String,
},
isReadOnly: {
type: Boolean,
},
Expand All @@ -38,6 +42,20 @@ export default mixins(expressionManager, workflowHelpers).extend({
const extensions = [
inputTheme(),
autocompletion(),
Prec.highest(
keymap.of([
{
any: (_: EditorView, event: KeyboardEvent) => {
if (event.key === 'Escape') {
event.stopPropagation();
this.$emit('close');
}

return false;
},
},
]),
),
n8nLang(),
history(),
expressionInputHandler(),
Expand All @@ -50,14 +68,17 @@ export default mixins(expressionManager, workflowHelpers).extend({
highlighter.removeColor(this.editor, this.plaintextSegments);
highlighter.addColor(this.editor, this.resolvableSegments);

setTimeout(() => this.editor?.focus()); // prevent blur on paste

setTimeout(() => {
this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
}, this.evaluationDelay);
this.editor?.focus(); // prevent blur on paste
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
});

this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
}),
];

Expand Down Expand Up @@ -86,9 +107,6 @@ export default mixins(expressionManager, workflowHelpers).extend({
this.editor?.destroy();
},
methods: {
onClose() {
this.$emit('close');
},
itemSelected({ variable }: IVariableItemSelected) {
if (!this.editor || this.isReadOnly) return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
:isReadOnly="isReadOnly"
:targetItem="hoveringItem"
:isSingleLine="isForRecordLocator"
:path="path"
@focus="onFocus"
@blur="onBlur"
@change="onChange"
Expand Down Expand Up @@ -93,6 +94,9 @@ export default Vue.extend({
};
},
props: {
path: {
type: String,
},
value: {
type: String,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@
<script lang="ts">
import mixins from 'vue-typed-mixins';
import { mapStores } from 'pinia';
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { EditorView, keymap } from '@codemirror/view';
import { EditorState, Prec } from '@codemirror/state';
import { history } from '@codemirror/commands';
import { autocompletion, completionStatus } from '@codemirror/autocomplete';

import { useNDVStore } from '@/stores/ndv';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { expressionManager } from '@/mixins/expressionManager';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { inputTheme } from './theme';
import { autocompletion, ifIn } from '@codemirror/autocomplete';
import { n8nLang } from '@/plugins/codemirror/n8nLang';
import { completionManager } from '@/mixins/completionManager';

export default mixins(expressionManager, workflowHelpers).extend({
export default mixins(completionManager, expressionManager, workflowHelpers).extend({
name: 'InlineExpressionEditorInput',
props: {
value: {
Expand All @@ -32,11 +33,9 @@ export default mixins(expressionManager, workflowHelpers).extend({
type: Boolean,
default: false,
},
},
data() {
return {
cursorPosition: 0,
};
path: {
type: String,
},
},
watch: {
value(newValue) {
Expand Down Expand Up @@ -77,6 +76,19 @@ export default mixins(expressionManager, workflowHelpers).extend({
mounted() {
const extensions = [
inputTheme({ isSingleLine: this.isSingleLine }),
Prec.highest(
keymap.of([
{
any(view: EditorView, event: KeyboardEvent) {
if (event.key === 'Escape' && completionStatus(view.state) !== null) {
event.stopPropagation();
}

return false;
},
},
]),
),
autocompletion(),
n8nLang(),
history(),
Expand All @@ -94,14 +106,16 @@ export default mixins(expressionManager, workflowHelpers).extend({
highlighter.removeColor(this.editor, this.plaintextSegments);
highlighter.addColor(this.editor, this.resolvableSegments);

this.cursorPosition = viewUpdate.view.state.selection.ranges[0].from;

setTimeout(() => {
this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
}, this.evaluationDelay);
try {
this.trackCompletion(viewUpdate, this.path);
} catch {}
});

this.$emit('change', {
value: this.unresolvedExpression,
segments: this.displayableSegments,
});
}),
];

Expand Down
1 change: 1 addition & 0 deletions packages/editor-ui/src/components/ParameterInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
:value="expressionDisplayValue"
:title="displayTitle"
:isReadOnly="isReadOnly"
:path="path"
@valueChanged="expressionUpdated"
@modalOpenerClick="openExpressionEditorModal"
@focus="setFocus"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<ExpressionParameterInput
v-if="isValueExpression || forceShowExpression"
:value="expressionDisplayValue"
:path="path"
isForRecordLocator
@valueChanged="onInputChange"
@modalOpenerClick="$emit('modalOpenerClick')"
Expand Down
76 changes: 76 additions & 0 deletions packages/editor-ui/src/mixins/completionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import mixins from 'vue-typed-mixins';
import { ExpressionExtensions } from 'n8n-workflow';
import { EditorView, ViewUpdate } from '@codemirror/view';

import { expressionManager } from './expressionManager';

export const completionManager = mixins(expressionManager).extend({
data() {
return {
editor: {} as EditorView,
};
},
computed: {
expressionExtensionsCategories() {
return ExpressionExtensions.reduce<Record<string, string | undefined>>((acc, cur) => {
for (const fnName of Object.keys(cur.functions)) {
acc[fnName] = cur.typeName;
}

return acc;
}, {});
},
},
methods: {
trackCompletion(viewUpdate: ViewUpdate, parameterPath: string) {
ivov marked this conversation as resolved.
Show resolved Hide resolved
const completionTx = viewUpdate.transactions.find((tx) => tx.isUserEvent('input.complete'));

if (!completionTx) return;

let completion = '';
let completionBase = '';

viewUpdate.changes.iterChanges((_: number, __: number, fromB: number, toB: number) => {
completion = this.editor.state.doc.slice(fromB, toB).toString();

const index = this.findCompletionBaseStartIndex(fromB);

completionBase = this.editor.state.doc
.slice(index, fromB - 1)
.toString()
.trim();
});

const category = this.expressionExtensionsCategories[completion];

const payload = {
instance_id: this.rootStore.instanceId,
node_type: this.ndvStore.activeNode?.type,
field_name: parameterPath,
field_type: 'expression',
context: completionBase,
inserted_text: completion,
category: category ?? 'n/a', // only applicable if expression extension completion
};

this.$telemetry.track('User autocompleted code', payload);
},

findCompletionBaseStartIndex(fromIndex: number) {
const INDICATORS = [
' $', // proxy
'{ ', // primitive
];

const doc = this.editor.state.doc.toString();

for (let index = fromIndex; index > 0; index--) {
if (INDICATORS.some((indicator) => indicator === doc[index] + doc[index + 1])) {
return index + 1;
}
}

return -1;
},
},
});
Loading