mirror of
https://github.com/n8n-io/n8n.git
synced 2024-09-19 22:37:31 -07:00
feat(editor): Add sections to autocomplete dropdown (#8720)
Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
parent
ed6dc86d60
commit
9b4618dd5e
|
@ -195,5 +195,7 @@ function setExpressionOnStringValueInSet(expression: string) {
|
|||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.clear()
|
||||
.type(expression, { parseSpecialCharSequences: false });
|
||||
.type(expression, { parseSpecialCharSequences: false })
|
||||
// hide autocomplete
|
||||
.type('{esc}');
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
--color-sticky-background-7: var(--prim-gray-740);
|
||||
--color-sticky-border-7: var(--prim-gray-670);
|
||||
|
||||
// Expressions
|
||||
// Expressions and autocomplete
|
||||
--color-valid-resolvable-foreground: var(--prim-color-alt-a-tint-300);
|
||||
--color-valid-resolvable-background: var(--prim-color-alt-a-alpha-025);
|
||||
--color-invalid-resolvable-foreground: var(--prim-color-alt-c-tint-250);
|
||||
|
@ -78,6 +78,8 @@
|
|||
--color-pending-resolvable-background: var(--prim-gray-70-alpha-01);
|
||||
--color-expression-editor-background: var(--prim-gray-800);
|
||||
--color-expression-syntax-example: var(--prim-gray-670);
|
||||
--color-autocomplete-item-selected: var(--prim-color-secondary-tint-200);
|
||||
--color-autocomplete-section-header-border: var(--prim-gray-540);
|
||||
|
||||
// Code
|
||||
--color-code-tags-string: var(--prim-color-alt-f-tint-150);
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
--color-sticky-background-7: var(--prim-gray-10);
|
||||
--color-sticky-border-7: var(--prim-gray-120);
|
||||
|
||||
// Expressions
|
||||
// Expressions and autocomplete
|
||||
--color-valid-resolvable-foreground: var(--prim-color-alt-a);
|
||||
--color-valid-resolvable-background: var(--prim-color-alt-a-tint-500);
|
||||
--color-invalid-resolvable-foreground: var(--prim-color-alt-c);
|
||||
|
@ -111,6 +111,8 @@
|
|||
--color-pending-resolvable-background: var(--prim-gray-40);
|
||||
--color-expression-editor-background: var(--prim-gray-0);
|
||||
--color-expression-syntax-example: var(--prim-gray-40);
|
||||
--color-autocomplete-item-selected: var(--color-secondary);
|
||||
--color-autocomplete-section-header-border: var(--color-foreground-light);
|
||||
|
||||
// Code
|
||||
--color-code-tags-string: var(--prim-color-alt-f);
|
||||
|
@ -138,8 +140,6 @@
|
|||
--color-code-gutterBackground: var(--prim-gray-0);
|
||||
--color-code-gutterForeground: var(--prim-gray-320);
|
||||
--color-code-tags-comment: var(--prim-gray-420);
|
||||
--color-autocomplete-selected-background: var(--prim-color-alt-e);
|
||||
--color-autocomplete-selected-font: var(--prim-gray-0);
|
||||
|
||||
// Variables
|
||||
--color-variables-usage-font: var(--color-success);
|
||||
|
|
|
@ -107,6 +107,10 @@ declare global {
|
|||
};
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Cypress: unknown;
|
||||
|
||||
Sentry?: {
|
||||
captureException: (error: Error, metadata?: unknown) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,24 +6,19 @@ import {
|
|||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
type KeyBinding,
|
||||
} from '@codemirror/view';
|
||||
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { acceptCompletion, selectedCompletion } from '@codemirror/autocomplete';
|
||||
import {
|
||||
history,
|
||||
indentLess,
|
||||
indentMore,
|
||||
insertNewlineAndIndent,
|
||||
toggleComment,
|
||||
redo,
|
||||
deleteCharBackward,
|
||||
undo,
|
||||
} from '@codemirror/commands';
|
||||
import { history, toggleComment, deleteCharBackward } from '@codemirror/commands';
|
||||
import { lintGutter } from '@codemirror/lint';
|
||||
import { type Extension, Prec } from '@codemirror/state';
|
||||
|
||||
import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
|
||||
export const readOnlyEditorExtensions: readonly Extension[] = [
|
||||
lineNumbers(),
|
||||
|
@ -31,42 +26,6 @@ export const readOnlyEditorExtensions: readonly Extension[] = [
|
|||
highlightSpecialChars(),
|
||||
];
|
||||
|
||||
export const tabKeyMap: KeyBinding[] = [
|
||||
{
|
||||
any(editor, event) {
|
||||
if (event.key === 'Tab' || (event.key === 'Escape' && selectedCompletion(editor.state))) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Tab',
|
||||
run: (editor) => {
|
||||
if (selectedCompletion(editor.state)) {
|
||||
return acceptCompletion(editor);
|
||||
}
|
||||
|
||||
return indentMore(editor);
|
||||
},
|
||||
},
|
||||
{ key: 'Shift-Tab', run: indentLess },
|
||||
];
|
||||
|
||||
export const enterKeyMap: KeyBinding[] = [
|
||||
{
|
||||
key: 'Enter',
|
||||
run: (editor) => {
|
||||
if (selectedCompletion(editor.state)) {
|
||||
return acceptCompletion(editor);
|
||||
}
|
||||
|
||||
return insertNewlineAndIndent(editor);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const writableEditorExtensions: readonly Extension[] = [
|
||||
history(),
|
||||
lintGutter(),
|
||||
|
@ -79,11 +38,11 @@ export const writableEditorExtensions: readonly Extension[] = [
|
|||
highlightActiveLineGutter(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap,
|
||||
...tabKeyMap(),
|
||||
...enterKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
...historyKeyMap,
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
{ key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward },
|
||||
]),
|
||||
),
|
||||
|
|
|
@ -37,6 +37,7 @@ export const completerExtension = defineComponent({
|
|||
}
|
||||
|
||||
return autocompletion({
|
||||
icons: false,
|
||||
compareCompletions: (a: Completion, b: Completion) => {
|
||||
if (/\.json$|id$|id['"]\]$/.test(a.label)) return 0;
|
||||
|
||||
|
|
|
@ -6,18 +6,24 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { EditorState, Prec } from '@codemirror/state';
|
||||
import { history, redo, undo } from '@codemirror/commands';
|
||||
import { history } from '@codemirror/commands';
|
||||
|
||||
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 { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { inputTheme } from './theme';
|
||||
import { forceParse } from '@/utils/forceParse';
|
||||
import { acceptCompletion, autocompletion } from '@codemirror/autocomplete';
|
||||
import { completionStatus } from '@codemirror/autocomplete';
|
||||
|
||||
import type { IVariableItemSelected } from '@/Interface';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ExpressionEditorModalInput',
|
||||
|
@ -44,13 +50,15 @@ export default defineComponent({
|
|||
mounted() {
|
||||
const extensions = [
|
||||
inputTheme(),
|
||||
autocompletion(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
...tabKeyMap(),
|
||||
...historyKeyMap,
|
||||
...enterKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
{
|
||||
any: (_: EditorView, event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
any: (view, event) => {
|
||||
if (event.key === 'Escape' && completionStatus(view.state) === null) {
|
||||
event.stopPropagation();
|
||||
this.$emit('close');
|
||||
}
|
||||
|
@ -58,11 +66,10 @@ export default defineComponent({
|
|||
return false;
|
||||
},
|
||||
},
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
]),
|
||||
),
|
||||
n8nLang(),
|
||||
n8nAutocompletion(),
|
||||
history(),
|
||||
expressionInputHandler(),
|
||||
EditorView.lineWrapping,
|
||||
|
@ -71,7 +78,11 @@ export default defineComponent({
|
|||
EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly
|
||||
EditorView.domEventHandlers({ scroll: forceParse }),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!this.editor || !viewUpdate.docChanged) return;
|
||||
if (!this.editor) return;
|
||||
|
||||
this.completionStatus = completionStatus(viewUpdate.view.state);
|
||||
|
||||
if (!viewUpdate.docChanged) return;
|
||||
|
||||
this.editorState = this.editor.state;
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { history, redo, undo } from '@codemirror/commands';
|
||||
import { history } from '@codemirror/commands';
|
||||
import {
|
||||
LanguageSupport,
|
||||
bracketMatching,
|
||||
|
@ -39,11 +38,18 @@ import { expressionManager } from '@/mixins/expressionManager';
|
|||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import type { Range, Section } from './types';
|
||||
import { nonTakenRanges } from './utils';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
import { completionStatus } from '@codemirror/autocomplete';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HtmlEditor',
|
||||
|
@ -105,17 +111,12 @@ export default defineComponent({
|
|||
|
||||
return [
|
||||
bracketMatching(),
|
||||
autocompletion(),
|
||||
n8nAutocompletion(),
|
||||
this.disableExpressionCompletions ? html() : htmlWithCompletions(),
|
||||
autoCloseTags,
|
||||
expressionInputHandler(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap,
|
||||
...enterKeyMap,
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
]),
|
||||
keymap.of([...tabKeyMap(), ...enterKeyMap, ...historyKeyMap, ...autocompleteKeyMap]),
|
||||
),
|
||||
indentOnInput(),
|
||||
codeNodeEditorTheme({
|
||||
|
@ -135,7 +136,11 @@ export default defineComponent({
|
|||
EditorView.editable.of(!this.isReadOnly),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (!this.editor || !viewUpdate.docChanged) return;
|
||||
if (!this.editor) return;
|
||||
|
||||
this.completionStatus = completionStatus(viewUpdate.view.state);
|
||||
|
||||
if (!viewUpdate.docChanged) return;
|
||||
|
||||
// Force segments value update by keeping track of editor state
|
||||
this.editorState = this.editor.state;
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { acceptCompletion, autocompletion, completionStatus } from '@codemirror/autocomplete';
|
||||
import { history, redo, undo } from '@codemirror/commands';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import type { PropType } from 'vue';
|
||||
|
@ -13,11 +12,18 @@ import { defineComponent } from 'vue';
|
|||
import { completionManager } from '@/mixins/completionManager';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { n8nAutocompletion, n8nLang } from '@/plugins/codemirror/n8nLang';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { inputTheme } from './theme';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { completionStatus } from '@codemirror/autocomplete';
|
||||
|
||||
const editableConf = new Compartment();
|
||||
|
||||
|
@ -81,25 +87,12 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
const extensions = [
|
||||
n8nLang(),
|
||||
inputTheme({ rows: this.rows }),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
{
|
||||
any(view: EditorView, event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && completionStatus(view.state) !== null) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
]),
|
||||
keymap.of([...tabKeyMap(true), ...enterKeyMap, ...autocompleteKeyMap, ...historyKeyMap]),
|
||||
),
|
||||
autocompletion(),
|
||||
n8nLang(),
|
||||
n8nAutocompletion(),
|
||||
inputTheme({ rows: this.rows }),
|
||||
history(),
|
||||
expressionInputHandler(),
|
||||
EditorView.lineWrapping,
|
||||
|
@ -111,7 +104,11 @@ export default defineComponent({
|
|||
},
|
||||
}),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!this.editor || !viewUpdate.docChanged) return;
|
||||
if (!this.editor) return;
|
||||
|
||||
this.completionStatus = completionStatus(viewUpdate.view.state);
|
||||
|
||||
if (!viewUpdate.docChanged) return;
|
||||
|
||||
// Force segments value update by keeping track of editor state
|
||||
this.editorState = this.editor.state;
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { history, redo, toggleComment, undo } from '@codemirror/commands';
|
||||
import { history, toggleComment } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { lintGutter } from '@codemirror/lint';
|
||||
|
@ -24,8 +23,14 @@ import {
|
|||
} from '@codemirror/view';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JsEditor',
|
||||
|
@ -77,15 +82,15 @@ export default defineComponent({
|
|||
history(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap,
|
||||
...tabKeyMap(),
|
||||
...enterKeyMap,
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
...historyKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
]),
|
||||
),
|
||||
lintGutter(),
|
||||
autocompletion(),
|
||||
n8nAutocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { history, redo, undo } from '@codemirror/commands';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { json, jsonParseLinter } from '@codemirror/lang-json';
|
||||
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { linter as createLinter, lintGutter } from '@codemirror/lint';
|
||||
|
@ -24,8 +23,14 @@ import {
|
|||
} from '@codemirror/view';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'JsonEditor',
|
||||
|
@ -76,16 +81,11 @@ export default defineComponent({
|
|||
extensions.push(
|
||||
history(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap,
|
||||
...enterKeyMap,
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
]),
|
||||
keymap.of([...tabKeyMap(), ...enterKeyMap, ...historyKeyMap, ...autocompleteKeyMap]),
|
||||
),
|
||||
createLinter(jsonParseLinter()),
|
||||
lintGutter(),
|
||||
autocompletion(),
|
||||
n8nAutocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
|
|
|
@ -20,8 +20,8 @@ import { expressionManager } from '@/mixins/expressionManager';
|
|||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { autocompletion, ifNotIn } from '@codemirror/autocomplete';
|
||||
import { history, redo, toggleComment, undo } from '@codemirror/commands';
|
||||
import { ifNotIn } from '@codemirror/autocomplete';
|
||||
import { history, toggleComment } from '@codemirror/commands';
|
||||
import { LanguageSupport, bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { type Extension, type Line, Prec } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
@ -47,9 +47,15 @@ import {
|
|||
keywordCompletionSource,
|
||||
} from '@n8n/codemirror-lang-sql';
|
||||
import { defineComponent } from 'vue';
|
||||
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
|
||||
const SQL_DIALECTS = {
|
||||
StandardSQL,
|
||||
|
@ -157,14 +163,14 @@ export default defineComponent({
|
|||
history(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap,
|
||||
...tabKeyMap(),
|
||||
...enterKeyMap,
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
...historyKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
]),
|
||||
),
|
||||
autocompletion(),
|
||||
n8nAutocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
|
|
|
@ -3,6 +3,9 @@ import { ExpressionExtensions } from 'n8n-workflow';
|
|||
import type { EditorView, ViewUpdate } from '@codemirror/view';
|
||||
|
||||
import { expressionManager } from './expressionManager';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
|
||||
export const completionManager = defineComponent({
|
||||
mixins: [expressionManager],
|
||||
|
@ -12,6 +15,7 @@ export const completionManager = defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore, useRootStore),
|
||||
expressionExtensionsCategories() {
|
||||
return ExpressionExtensions.reduce<Record<string, string | undefined>>((acc, cur) => {
|
||||
for (const fnName of Object.keys(cur.functions)) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/e
|
|||
import type { EditorView } from '@codemirror/view';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { getExpressionErrorMessage, getResolvableState } from '@/utils/expressions';
|
||||
import type { EditorState } from '@codemirror/state';
|
||||
|
||||
export const expressionManager = defineComponent({
|
||||
props: {
|
||||
|
@ -27,10 +28,16 @@ export const expressionManager = defineComponent({
|
|||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
data(): {
|
||||
editor: EditorView;
|
||||
skipSegments: string[];
|
||||
editorState: EditorState | undefined;
|
||||
completionStatus: 'active' | 'pending' | null;
|
||||
} {
|
||||
return {
|
||||
editor: {} as EditorView,
|
||||
skipSegments: [] as string[],
|
||||
skipSegments: [],
|
||||
completionStatus: null,
|
||||
editorState: undefined,
|
||||
};
|
||||
},
|
||||
|
@ -70,15 +77,12 @@ export const expressionManager = defineComponent({
|
|||
},
|
||||
|
||||
segments(): Segment[] {
|
||||
if (!this.editorState || !this.editorState) return [];
|
||||
const state = this.editorState as EditorState;
|
||||
if (!state) return [];
|
||||
|
||||
const rawSegments: RawSegment[] = [];
|
||||
|
||||
const fullTree = ensureSyntaxTree(
|
||||
this.editorState,
|
||||
this.editorState.doc.length,
|
||||
EXPRESSION_EDITOR_PARSER_TIMEOUT,
|
||||
);
|
||||
const fullTree = ensureSyntaxTree(state, state.doc.length, EXPRESSION_EDITOR_PARSER_TIMEOUT);
|
||||
|
||||
if (fullTree === null) {
|
||||
throw new Error(`Failed to parse expression: ${this.editorValue}`);
|
||||
|
@ -87,7 +91,7 @@ export const expressionManager = defineComponent({
|
|||
const skipSegments = ['Program', 'Script', 'Document', ...this.skipSegments];
|
||||
|
||||
fullTree.cursor().iterate((node) => {
|
||||
const text = this.editorState.sliceDoc(node.from, node.to);
|
||||
const text = state.sliceDoc(node.from, node.to);
|
||||
|
||||
if (skipSegments.includes(node.type.name)) return;
|
||||
|
||||
|
@ -108,8 +112,7 @@ export const expressionManager = defineComponent({
|
|||
const { from, to, text, token } = segment;
|
||||
|
||||
if (token === 'Resolvable') {
|
||||
const { resolved, fullError } = this.resolve(text, this.hoveringItem);
|
||||
|
||||
const { resolved, error, fullError } = this.resolve(text, this.hoveringItem);
|
||||
acc.push({
|
||||
kind: 'resolvable',
|
||||
from,
|
||||
|
@ -119,7 +122,7 @@ export const expressionManager = defineComponent({
|
|||
// For some reason, expressions that resolve to a number 0 are breaking preview in the SQL editor
|
||||
// This fixes that but as as TODO we should figure out why this is happening
|
||||
resolved: String(resolved),
|
||||
state: getResolvableState(fullError),
|
||||
state: getResolvableState(fullError ?? error, this.completionStatus !== null),
|
||||
error: fullError,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
|
||||
|
@ -21,20 +21,22 @@ import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
|||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY, EnterpriseEditionFeature } from '@/constants';
|
||||
import { setupServer } from '@/__tests__/server';
|
||||
import {
|
||||
ARRAY_NUMBER_ONLY_METHODS,
|
||||
LUXON_RECOMMENDED_OPTIONS,
|
||||
METADATA_SECTION,
|
||||
METHODS_SECTION,
|
||||
RECOMMENDED_SECTION,
|
||||
STRING_RECOMMENDED_OPTIONS,
|
||||
} from '../constants';
|
||||
import { set, uniqBy } from 'lodash-es';
|
||||
|
||||
let externalSecretsStore: ReturnType<typeof useExternalSecretsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
setActivePinia(createPinia());
|
||||
setActivePinia(createTestingPinia());
|
||||
|
||||
externalSecretsStore = useExternalSecretsStore();
|
||||
uiStore = useUIStore();
|
||||
|
@ -43,12 +45,6 @@ beforeEach(async () => {
|
|||
vi.spyOn(utils, 'receivesNoBinaryData').mockReturnValue(true); // hide $binary
|
||||
vi.spyOn(utils, 'isSplitInBatchesAbsent').mockReturnValue(false); // show context
|
||||
vi.spyOn(utils, 'hasActiveNode').mockReturnValue(true);
|
||||
|
||||
await settingsStore.getSettings();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.shutdown();
|
||||
});
|
||||
|
||||
describe('No completions', () => {
|
||||
|
@ -63,7 +59,18 @@ describe('No completions', () => {
|
|||
|
||||
describe('Top-level completions', () => {
|
||||
test('should return dollar completions for blank position: {{ | }}', () => {
|
||||
expect(completions('{{ | }}')).toHaveLength(dollarOptions().length);
|
||||
const result = completions('{{ | }}');
|
||||
expect(result).toHaveLength(dollarOptions().length);
|
||||
|
||||
expect(result?.[0]).toEqual(
|
||||
expect.objectContaining({ label: '$json', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(result?.[4]).toEqual(
|
||||
expect.objectContaining({ label: '$execution', section: METADATA_SECTION }),
|
||||
);
|
||||
expect(result?.[14]).toEqual(
|
||||
expect.objectContaining({ label: '$max()', section: METHODS_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should return DateTime completion for: {{ D| }}', () => {
|
||||
|
@ -98,77 +105,73 @@ describe('Top-level completions', () => {
|
|||
});
|
||||
|
||||
test('should return node selector completions for: {{ $(| }}', () => {
|
||||
const initialState = { workflows: { workflow: { nodes: mockNodes } } };
|
||||
|
||||
setActivePinia(createTestingPinia({ initialState }));
|
||||
vi.spyOn(utils, 'autocompletableNodeNames').mockReturnValue(mockNodes.map((node) => node.name));
|
||||
|
||||
expect(completions('{{ $(| }}')).toHaveLength(mockNodes.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Luxon method completions', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
|
||||
test('should return class completions for: {{ DateTime.| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce(DateTime);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime);
|
||||
|
||||
expect(completions('{{ DateTime.| }}')).toHaveLength(luxonStaticOptions().length);
|
||||
});
|
||||
|
||||
test('should return instance completions for: {{ $now.| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce(DateTime.now());
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
|
||||
expect(completions('{{ $now.| }}')).toHaveLength(
|
||||
luxonInstanceOptions().length + extensions('date').length,
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return instance completions for: {{ $today.| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce(DateTime.now());
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
|
||||
expect(completions('{{ $today.| }}')).toHaveLength(
|
||||
luxonInstanceOptions().length + extensions('date').length,
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resolution-based completions', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
|
||||
describe('literals', () => {
|
||||
test('should return completions for string literal: {{ "abc".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce('abc');
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('abc');
|
||||
|
||||
expect(completions('{{ "abc".| }}')).toHaveLength(
|
||||
natives('string').length + extensions('string').length,
|
||||
natives('string').length + extensions('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should properly handle string that contain dollar signs', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce('"You \'owe\' me 200$"');
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce("You 'owe' me 200$ ");
|
||||
|
||||
expect(completions('{{ "You \'owe\' me 200$".| }}')).toHaveLength(
|
||||
natives('string').length + extensions('string').length,
|
||||
);
|
||||
const result = completions('{{ "You \'owe\' me 200$".| }}');
|
||||
|
||||
expect(result).toHaveLength(natives('string').length + extensions('string').length + 1);
|
||||
});
|
||||
|
||||
test('should return completions for number literal: {{ (123).| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce(123);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(123);
|
||||
|
||||
expect(completions('{{ (123).| }}')).toHaveLength(
|
||||
natives('number').length + extensions('number').length,
|
||||
natives('number').length + extensions('number').length + ['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for array literal: {{ [1, 2, 3].| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce([1, 2, 3]);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce([1, 2, 3]);
|
||||
|
||||
expect(completions('{{ [1, 2, 3].| }}')).toHaveLength(
|
||||
natives('array').length + extensions('array').length,
|
||||
|
@ -177,7 +180,7 @@ describe('Resolution-based completions', () => {
|
|||
|
||||
test('should return completions for Object methods: {{ Object.values({ abc: 123 }).| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce([123]);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce([123]);
|
||||
|
||||
const found = completions('{{ Object.values({ abc: 123 }).| }}');
|
||||
|
||||
|
@ -189,10 +192,10 @@ describe('Resolution-based completions', () => {
|
|||
test('should return completions for object literal', () => {
|
||||
const object = { a: 1 };
|
||||
|
||||
resolveParameterSpy.mockReturnValueOnce(object);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(object);
|
||||
|
||||
expect(completions('{{ ({ a: 1 }).| }}')).toHaveLength(
|
||||
Object.keys(object).length + natives('object').length + extensions('object').length,
|
||||
Object.keys(object).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -200,23 +203,24 @@ describe('Resolution-based completions', () => {
|
|||
describe('indexed access completions', () => {
|
||||
test('should return string completions for indexed access that resolves to string literal: {{ "abc"[0].| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
resolveParameterSpy.mockReturnValueOnce('a');
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('a');
|
||||
|
||||
expect(completions('{{ "abc"[0].| }}')).toHaveLength(
|
||||
natives('string').length + extensions('string').length,
|
||||
natives('string').length + extensions('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('complex expression completions', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input } = mockProxy;
|
||||
|
||||
test('should return completions when $input is used as a function parameter', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.num);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.num);
|
||||
const found = completions('{{ Math.abs($input.item.json.num1).| }}');
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
expect(found).toHaveLength(extensions('number').length + natives('number').length);
|
||||
expect(found).toHaveLength(
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions when node reference is used as a function parameter', () => {
|
||||
|
@ -228,69 +232,74 @@ describe('Resolution-based completions', () => {
|
|||
});
|
||||
|
||||
test('should return completions for complex expression: {{ $now.diff($now.diff($now.|)) }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(DateTime.now());
|
||||
expect(completions('{{ $now.diff($now.diff($now.|)) }}')).toHaveLength(
|
||||
natives('date').length + extensions('object').length,
|
||||
uniqBy(luxonInstanceOptions().concat(extensions('date')), (option) => option.label).length +
|
||||
LUXON_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for complex expression: {{ $execution.resumeUrl.includes($json.) }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce($input.item.json);
|
||||
const { $json } = mockProxy;
|
||||
const found = completions('{{ $execution.resumeUrl.includes($json.|) }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
});
|
||||
|
||||
test('should return completions for operation expression: {{ $now.day + $json. }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce($input.item.json);
|
||||
const { $json } = mockProxy;
|
||||
const found = completions('{{ $now.day + $json.| }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
});
|
||||
|
||||
test('should return completions for operation expression: {{ Math.abs($now.day) >= 10 ? $now : Math.abs($json.). }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json);
|
||||
const { $json } = mockProxy;
|
||||
const found = completions('{{ Math.abs($now.day) >= 10 ? $now : Math.abs($json.|) }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(Object.keys($json).length + natives('object').length);
|
||||
expect(found).toHaveLength(Object.keys($json).length + extensions('object').length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bracket-aware completions', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input } = mockProxy;
|
||||
|
||||
test('should return bracket-aware completions for: {{ $input.item.json.str.|() }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.str);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.str);
|
||||
|
||||
const found = completions('{{ $input.item.json.str.|() }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(extensions('string').length + natives('string').length);
|
||||
expect(found).toHaveLength(
|
||||
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
expect(found.map((c) => c.label).every((l) => !l.endsWith('()')));
|
||||
});
|
||||
|
||||
test('should return bracket-aware completions for: {{ $input.item.json.num.|() }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.num);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.num);
|
||||
|
||||
const found = completions('{{ $input.item.json.num.|() }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completions');
|
||||
|
||||
expect(found).toHaveLength(extensions('number').length + natives('number').length);
|
||||
expect(found).toHaveLength(
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
expect(found.map((c) => c.label).every((l) => !l.endsWith('()')));
|
||||
});
|
||||
|
||||
test('should return bracket-aware completions for: {{ $input.item.json.arr.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.arr);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.arr);
|
||||
|
||||
const found = completions('{{ $input.item.json.arr.|() }}');
|
||||
|
||||
|
@ -302,17 +311,18 @@ describe('Resolution-based completions', () => {
|
|||
});
|
||||
|
||||
describe('secrets', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input, $ } = mockProxy;
|
||||
const { $input } = mockProxy;
|
||||
|
||||
beforeEach(() => {});
|
||||
|
||||
test('should return completions for: {{ $secrets.| }}', () => {
|
||||
const provider = 'infisical';
|
||||
const secrets = ['SECRET'];
|
||||
|
||||
resolveParameterSpy.mockReturnValue($input);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
|
||||
|
||||
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].open = true;
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.ExternalSecrets] = true;
|
||||
set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true);
|
||||
externalSecretsStore.state.secrets = {
|
||||
[provider]: secrets,
|
||||
};
|
||||
|
@ -324,6 +334,7 @@ describe('Resolution-based completions', () => {
|
|||
info: expect.any(Function),
|
||||
label: provider,
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -332,10 +343,10 @@ describe('Resolution-based completions', () => {
|
|||
const provider = 'infisical';
|
||||
const secrets = ['SECRET1', 'SECRET2'];
|
||||
|
||||
resolveParameterSpy.mockReturnValue($input);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
|
||||
|
||||
uiStore.modals[CREDENTIAL_EDIT_MODAL_KEY].open = true;
|
||||
settingsStore.settings.enterprise[EnterpriseEditionFeature.ExternalSecrets] = true;
|
||||
set(settingsStore.settings, ['enterprise', EnterpriseEditionFeature.ExternalSecrets], true);
|
||||
externalSecretsStore.state.secrets = {
|
||||
[provider]: secrets,
|
||||
};
|
||||
|
@ -347,138 +358,141 @@ describe('Resolution-based completions', () => {
|
|||
info: expect.any(Function),
|
||||
label: secrets[0],
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
{
|
||||
info: expect.any(Function),
|
||||
label: secrets[1],
|
||||
type: 'keyword',
|
||||
apply: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('references', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input, $ } = mockProxy;
|
||||
|
||||
test('should return completions for: {{ $input.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
|
||||
|
||||
expect(completions('{{ $input.| }}')).toHaveLength(
|
||||
Reflect.ownKeys($input).length + natives('object').length,
|
||||
);
|
||||
expect(completions('{{ $input.| }}')).toHaveLength(Reflect.ownKeys($input).length);
|
||||
});
|
||||
|
||||
test('should return completions for: {{ "hello"+input.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input);
|
||||
|
||||
expect(completions('{{ "hello"+$input.| }}')).toHaveLength(
|
||||
Reflect.ownKeys($input).length + natives('object').length,
|
||||
);
|
||||
expect(completions('{{ "hello"+$input.| }}')).toHaveLength(Reflect.ownKeys($input).length);
|
||||
});
|
||||
|
||||
test("should return completions for: {{ $('nodeName').| }}", () => {
|
||||
resolveParameterSpy.mockReturnValue($('Rename'));
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($('Rename'));
|
||||
|
||||
expect(completions('{{ $("Rename").| }}')).toHaveLength(
|
||||
Reflect.ownKeys($('Rename')).length + natives('object').length - ['pairedItem'].length,
|
||||
Reflect.ownKeys($('Rename')).length - ['pairedItem'].length,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return completions for: {{ $('(Complex) \"No\\'de\" name').| }}", () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($('Rename'));
|
||||
|
||||
expect(completions("{{ $('(Complex) \"No\\'de\" name').| }}")).toHaveLength(
|
||||
Reflect.ownKeys($('Rename')).length - ['pairedItem'].length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.item.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item);
|
||||
|
||||
const found = completions('{{ $input.item.| }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completion');
|
||||
|
||||
expect(found).toHaveLength(3);
|
||||
expect(found).toHaveLength(1);
|
||||
expect(found[0].label).toBe('json');
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.first().| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.first());
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.first());
|
||||
|
||||
const found = completions('{{ $input.first().| }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completion');
|
||||
|
||||
expect(found).toHaveLength(3);
|
||||
expect(found).toHaveLength(1);
|
||||
expect(found[0].label).toBe('json');
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.last().| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.last());
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.last());
|
||||
|
||||
const found = completions('{{ $input.last().| }}');
|
||||
|
||||
if (!found) throw new Error('Expected to find completion');
|
||||
|
||||
expect(found).toHaveLength(3);
|
||||
expect(found).toHaveLength(1);
|
||||
expect(found[0].label).toBe('json');
|
||||
});
|
||||
|
||||
test('should return no completions for: {{ $input.all().| }}', () => {
|
||||
test('should return completions for: {{ $input.all().| }}', () => {
|
||||
// @ts-expect-error
|
||||
resolveParameterSpy.mockReturnValue([$input.item]);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue([$input.item]);
|
||||
|
||||
expect(completions('{{ $input.all().| }}')).toBeNull();
|
||||
expect(completions('{{ $input.all().| }}')).toHaveLength(
|
||||
extensions('array').length + natives('array').length - ARRAY_NUMBER_ONLY_METHODS.length,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return completions for: '{{ $input.item.| }}'", () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json);
|
||||
|
||||
expect(completions('{{ $input.item.| }}')).toHaveLength(
|
||||
Object.keys($input.item.json).length +
|
||||
(extensions('object').length + natives('object').length),
|
||||
Object.keys($input.item.json).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return completions for: '{{ $input.first().| }}'", () => {
|
||||
resolveParameterSpy.mockReturnValue($input.first().json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.first().json);
|
||||
|
||||
expect(completions('{{ $input.first().| }}')).toHaveLength(
|
||||
Object.keys($input.first().json).length +
|
||||
(extensions('object').length + natives('object').length),
|
||||
Object.keys($input.first().json).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return completions for: '{{ $input.last().| }}'", () => {
|
||||
resolveParameterSpy.mockReturnValue($input.last().json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.last().json);
|
||||
|
||||
expect(completions('{{ $input.last().| }}')).toHaveLength(
|
||||
Object.keys($input.last().json).length +
|
||||
(extensions('object').length + natives('object').length),
|
||||
Object.keys($input.last().json).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return completions for: '{{ $input.all()[0].| }}'", () => {
|
||||
resolveParameterSpy.mockReturnValue($input.all()[0].json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.all()[0].json);
|
||||
|
||||
expect(completions('{{ $input.all()[0].| }}')).toHaveLength(
|
||||
Object.keys($input.all()[0].json).length +
|
||||
(extensions('object').length + natives('object').length),
|
||||
Object.keys($input.all()[0].json).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.item.json.str.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.str);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.str);
|
||||
|
||||
expect(completions('{{ $input.item.json.str.| }}')).toHaveLength(
|
||||
extensions('string').length + natives('string').length,
|
||||
extensions('string').length + natives('string').length + STRING_RECOMMENDED_OPTIONS.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.item.json.num.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.num);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.num);
|
||||
|
||||
expect(completions('{{ $input.item.json.num.| }}')).toHaveLength(
|
||||
extensions('number').length + natives('number').length,
|
||||
extensions('number').length + natives('number').length + ['isEven()', 'isOdd()'].length,
|
||||
);
|
||||
});
|
||||
|
||||
test('should return completions for: {{ $input.item.json.arr.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.arr);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.arr);
|
||||
|
||||
expect(completions('{{ $input.item.json.arr.| }}')).toHaveLength(
|
||||
extensions('array').length + natives('array').length,
|
||||
|
@ -486,22 +500,20 @@ describe('Resolution-based completions', () => {
|
|||
});
|
||||
|
||||
test('should return completions for: {{ $input.item.json.obj.| }}', () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.obj);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.obj);
|
||||
|
||||
expect(completions('{{ $input.item.json.obj.| }}')).toHaveLength(
|
||||
Object.keys($input.item.json.obj).length +
|
||||
(extensions('object').length + natives('object').length),
|
||||
Object.keys($input.item.json.obj).length + extensions('object').length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bracket access', () => {
|
||||
const resolveParameterSpy = vi.spyOn(workflowHelpers, 'resolveParameter');
|
||||
const { $input } = mockProxy;
|
||||
|
||||
['{{ $input.item.json[| }}', '{{ $json[| }}'].forEach((expression) => {
|
||||
test(`should return completions for: ${expression}`, () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json);
|
||||
|
||||
const found = completions(expression);
|
||||
|
||||
|
@ -514,7 +526,7 @@ describe('Resolution-based completions', () => {
|
|||
|
||||
["{{ $input.item.json['obj'][| }}", "{{ $json['obj'][| }}"].forEach((expression) => {
|
||||
test(`should return completions for: ${expression}`, () => {
|
||||
resolveParameterSpy.mockReturnValue($input.item.json.obj);
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValue($input.item.json.obj);
|
||||
|
||||
const found = completions(expression);
|
||||
|
||||
|
@ -525,6 +537,68 @@ describe('Resolution-based completions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('recommended completions', () => {
|
||||
test('should recommended toDate() for {{ "1-Feb-2024".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('1-Feb-2024');
|
||||
|
||||
expect(completions('{{ "1-Feb-2024".| }}')?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toDate()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended toInt(),toFloat() for: {{ "5.3".| }}', () => {
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce('5.3');
|
||||
const options = completions('{{ "5.3".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'toInt()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[1]).toEqual(
|
||||
expect.objectContaining({ label: 'toFloat()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended extractEmail() for: {{ "string with test@n8n.io in it".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'string with test@n8n.io in it',
|
||||
);
|
||||
const options = completions('{{ "string with test@n8n.io in it".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'extractEmail()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended extractDomain() for: {{ "test@n8n.io".| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
'test@n8n.io',
|
||||
);
|
||||
const options = completions('{{ "test@n8n.io".| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'extractDomain()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
|
||||
test('should recommended round(),floor(),ceil() for: {{ (5.46).| }}', () => {
|
||||
vi.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(
|
||||
// @ts-expect-error Spied function is mistyped
|
||||
5.46,
|
||||
);
|
||||
const options = completions('{{ (5.46).| }}');
|
||||
expect(options?.[0]).toEqual(
|
||||
expect.objectContaining({ label: 'round()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[1]).toEqual(
|
||||
expect.objectContaining({ label: 'floor()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
expect(options?.[2]).toEqual(
|
||||
expect.objectContaining({ label: 'ceil()', section: RECOMMENDED_SECTION }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function completions(docWithCursor: string) {
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
import type { Completion, CompletionSection } from '@codemirror/autocomplete';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { withSectionHeader } from './utils';
|
||||
|
||||
export const FIELDS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.fields'),
|
||||
rank: -1,
|
||||
});
|
||||
|
||||
export const RECOMMENDED_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.recommended'),
|
||||
rank: 0,
|
||||
});
|
||||
|
||||
export const RECOMMENDED_METHODS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.recommendedMethods'),
|
||||
rank: 0,
|
||||
});
|
||||
|
||||
export const PREVIOUS_NODES_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.prevNodes'),
|
||||
rank: 1,
|
||||
});
|
||||
|
||||
export const PROPERTIES_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.properties'),
|
||||
rank: 2,
|
||||
});
|
||||
|
||||
export const METHODS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.methods'),
|
||||
rank: 3,
|
||||
});
|
||||
|
||||
export const METADATA_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.metadata'),
|
||||
rank: 4,
|
||||
});
|
||||
|
||||
export const OTHER_METHODS_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.otherMethods'),
|
||||
rank: 100,
|
||||
});
|
||||
|
||||
export const OTHER_SECTION: CompletionSection = withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.other'),
|
||||
rank: 101,
|
||||
});
|
||||
|
||||
export const ROOT_DOLLAR_COMPLETIONS: Completion[] = [
|
||||
{
|
||||
label: '$json',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$json,
|
||||
},
|
||||
{
|
||||
label: '$binary',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$binary,
|
||||
},
|
||||
{
|
||||
label: '$now',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$now,
|
||||
},
|
||||
{
|
||||
label: '$if()',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$if,
|
||||
},
|
||||
{
|
||||
label: '$ifEmpty()',
|
||||
section: RECOMMENDED_SECTION,
|
||||
info: i18n.rootVars.$ifEmpty,
|
||||
},
|
||||
{
|
||||
label: '$execution',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$execution,
|
||||
},
|
||||
{
|
||||
label: '$itemIndex',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$itemIndex,
|
||||
},
|
||||
{
|
||||
label: '$input',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$input,
|
||||
},
|
||||
{
|
||||
label: '$parameter',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$parameter,
|
||||
},
|
||||
{
|
||||
label: '$prevNode',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$prevNode,
|
||||
},
|
||||
{
|
||||
label: '$runIndex',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$runIndex,
|
||||
},
|
||||
{
|
||||
label: '$today',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$today,
|
||||
},
|
||||
{
|
||||
label: '$vars',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$vars,
|
||||
},
|
||||
{
|
||||
label: '$workflow',
|
||||
section: METADATA_SECTION,
|
||||
info: i18n.rootVars.$workflow,
|
||||
},
|
||||
{
|
||||
label: '$jmespath()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$jmespath,
|
||||
},
|
||||
{
|
||||
label: '$max()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$max,
|
||||
},
|
||||
{
|
||||
label: '$min()',
|
||||
section: METHODS_SECTION,
|
||||
info: i18n.rootVars.$min,
|
||||
},
|
||||
];
|
||||
|
||||
export const STRING_RECOMMENDED_OPTIONS = [
|
||||
'includes()',
|
||||
'split()',
|
||||
'startsWith()',
|
||||
'replaceAll()',
|
||||
'length',
|
||||
];
|
||||
|
||||
export const DATE_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'extract()'];
|
||||
export const LUXON_RECOMMENDED_OPTIONS = ['format()', 'minus()', 'plus()', 'diff()', 'extract()'];
|
||||
export const OBJECT_RECOMMENDED_OPTIONS = ['keys()', 'values()', 'isEmpty()'];
|
||||
export const ARRAY_RECOMMENDED_OPTIONS = ['length', 'last()', 'includes()', 'map()', 'filter()'];
|
||||
export const ARRAY_NUMBER_ONLY_METHODS = ['max()', 'min()', 'sum()', 'average()'];
|
||||
|
||||
export const LUXON_SECTIONS: Record<string, CompletionSection> = {
|
||||
edit: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.edit'),
|
||||
rank: 1,
|
||||
}),
|
||||
compare: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.compare'),
|
||||
rank: 2,
|
||||
}),
|
||||
format: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.format'),
|
||||
rank: 3,
|
||||
}),
|
||||
query: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.component'),
|
||||
rank: 4,
|
||||
}),
|
||||
};
|
||||
|
||||
export const STRING_SECTIONS: Record<string, CompletionSection> = {
|
||||
edit: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.edit'),
|
||||
rank: 1,
|
||||
}),
|
||||
query: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.query'),
|
||||
rank: 2,
|
||||
}),
|
||||
validation: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.validation'),
|
||||
rank: 3,
|
||||
}),
|
||||
case: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.case'),
|
||||
rank: 4,
|
||||
}),
|
||||
cast: withSectionHeader({
|
||||
name: i18n.baseText('codeNodeEditor.completer.section.cast'),
|
||||
rank: 5,
|
||||
}),
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import type { IDataObject, DocMetadata, NativeDoc } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions, NativeMethods } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions, NativeMethods, validateFieldType } from 'n8n-workflow';
|
||||
import { DateTime } from 'luxon';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { resolveParameter } from '@/composables/useWorkflowHelpers';
|
||||
|
@ -14,15 +14,48 @@ import {
|
|||
isPseudoParam,
|
||||
stripExcessParens,
|
||||
isCredentialsModalOpen,
|
||||
applyCompletion,
|
||||
sortCompletionsAlpha,
|
||||
hasRequiredArgs,
|
||||
} from './utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { AutocompleteOptionType, ExtensionTypeName, FnToDoc, Resolved } from './types';
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
CompletionSection,
|
||||
} from '@codemirror/autocomplete';
|
||||
import type {
|
||||
AutocompleteInput,
|
||||
AutocompleteOptionType,
|
||||
ExtensionTypeName,
|
||||
FnToDoc,
|
||||
Resolved,
|
||||
} from './types';
|
||||
import { sanitizeHtml } from '@/utils/htmlUtils';
|
||||
import { isFunctionOption } from './typeGuards';
|
||||
import { luxonInstanceDocs } from './nativesAutocompleteDocs/luxon.instance.docs';
|
||||
import { luxonStaticDocs } from './nativesAutocompleteDocs/luxon.static.docs';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import {
|
||||
ARRAY_NUMBER_ONLY_METHODS,
|
||||
ARRAY_RECOMMENDED_OPTIONS,
|
||||
DATE_RECOMMENDED_OPTIONS,
|
||||
FIELDS_SECTION,
|
||||
LUXON_RECOMMENDED_OPTIONS,
|
||||
LUXON_SECTIONS,
|
||||
METHODS_SECTION,
|
||||
OBJECT_RECOMMENDED_OPTIONS,
|
||||
OTHER_METHODS_SECTION,
|
||||
OTHER_SECTION,
|
||||
PROPERTIES_SECTION,
|
||||
RECOMMENDED_METHODS_SECTION,
|
||||
RECOMMENDED_SECTION,
|
||||
STRING_RECOMMENDED_OPTIONS,
|
||||
STRING_SECTIONS,
|
||||
} from './constants';
|
||||
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||
import { uniqBy } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* Resolution-based completions offered according to datatype.
|
||||
|
@ -63,7 +96,7 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
|||
if (resolved === null) return null;
|
||||
|
||||
try {
|
||||
options = datatypeOptions(resolved, base).map(stripExcessParens(context));
|
||||
options = datatypeOptions({ resolved, base, tail }).map(stripExcessParens(context));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
@ -87,40 +120,33 @@ export function datatypeCompletions(context: CompletionContext): CompletionResul
|
|||
};
|
||||
}
|
||||
|
||||
function datatypeOptions(resolved: Resolved, toResolve: string) {
|
||||
function datatypeOptions(input: AutocompleteInput): Completion[] {
|
||||
const { resolved } = input;
|
||||
|
||||
if (resolved === null) return [];
|
||||
|
||||
if (typeof resolved === 'number') {
|
||||
return [...natives('number'), ...extensions('number')];
|
||||
return numberOptions(input as AutocompleteInput<number>);
|
||||
}
|
||||
|
||||
if (typeof resolved === 'string') {
|
||||
return [...natives('string'), ...extensions('string')];
|
||||
return stringOptions(input as AutocompleteInput<string>);
|
||||
}
|
||||
|
||||
if (['$now', '$today'].includes(toResolve)) {
|
||||
return [...luxonInstanceOptions(), ...extensions('date')];
|
||||
if (resolved instanceof DateTime) {
|
||||
return luxonOptions();
|
||||
}
|
||||
|
||||
if (resolved instanceof Date) {
|
||||
return [...natives('date'), ...extensions('date')];
|
||||
return dateOptions();
|
||||
}
|
||||
|
||||
if (Array.isArray(resolved)) {
|
||||
if (/all\(.*?\)/.test(toResolve)) return [];
|
||||
|
||||
const arrayMethods = [...natives('array'), ...extensions('array')];
|
||||
|
||||
if (resolved.length > 0 && resolved.some((i) => typeof i !== 'number')) {
|
||||
const NUMBER_ONLY_ARRAY_EXTENSIONS = new Set(['max()', 'min()', 'sum()', 'average()']);
|
||||
|
||||
return arrayMethods.filter((m) => !NUMBER_ONLY_ARRAY_EXTENSIONS.has(m.label));
|
||||
}
|
||||
return arrayMethods;
|
||||
return arrayOptions(input as AutocompleteInput<unknown[]>);
|
||||
}
|
||||
|
||||
if (typeof resolved === 'object') {
|
||||
return objectOptions(toResolve, resolved);
|
||||
return objectOptions(input as AutocompleteInput<IDataObject>);
|
||||
}
|
||||
|
||||
return [];
|
||||
|
@ -137,7 +163,7 @@ export const natives = (typeName: ExtensionTypeName): Completion[] => {
|
|||
return [...nativeProps, ...nativeMethods];
|
||||
};
|
||||
|
||||
export const extensions = (typeName: ExtensionTypeName) => {
|
||||
export const extensions = (typeName: ExtensionTypeName, includeHidden = false) => {
|
||||
const extensions = ExpressionExtensions.find((ee) => ee.typeName.toLowerCase() === typeName);
|
||||
|
||||
if (!extensions) return [];
|
||||
|
@ -146,18 +172,20 @@ export const extensions = (typeName: ExtensionTypeName) => {
|
|||
return { ...acc, [fnName]: { doc: fn.doc } };
|
||||
}, {});
|
||||
|
||||
return toOptions(fnToDoc, typeName, 'extension-function');
|
||||
return toOptions(fnToDoc, typeName, 'extension-function', includeHidden);
|
||||
};
|
||||
|
||||
export const toOptions = (
|
||||
fnToDoc: FnToDoc,
|
||||
typeName: ExtensionTypeName,
|
||||
optionType: AutocompleteOptionType = 'native-function',
|
||||
includeHidden = false,
|
||||
) => {
|
||||
return Object.entries(fnToDoc)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([fnName, fn]) => {
|
||||
return createCompletionOption(typeName, fnName, optionType, fn);
|
||||
.filter(([, docInfo]) => !docInfo.doc?.hidden || includeHidden)
|
||||
.map(([fnName, docInfo]) => {
|
||||
return createCompletionOption(typeName, fnName, optionType, docInfo);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -167,9 +195,13 @@ const createCompletionOption = (
|
|||
optionType: AutocompleteOptionType,
|
||||
docInfo: { doc?: DocMetadata | undefined },
|
||||
): Completion => {
|
||||
const isFunction = isFunctionOption(optionType);
|
||||
const label = isFunction ? name + '()' : name;
|
||||
const option: Completion = {
|
||||
label: isFunctionOption(optionType) ? name + '()' : name,
|
||||
label,
|
||||
type: optionType,
|
||||
section: docInfo.doc?.section,
|
||||
apply: applyCompletion(hasRequiredArgs(docInfo?.doc)),
|
||||
};
|
||||
|
||||
option.info = () => {
|
||||
|
@ -271,15 +303,16 @@ const createPropHeader = (typeName: string, property: { doc?: DocMetadata | unde
|
|||
return header;
|
||||
};
|
||||
|
||||
const objectOptions = (toResolve: string, resolved: IDataObject) => {
|
||||
const objectOptions = (input: AutocompleteInput<IDataObject>): Completion[] => {
|
||||
const { base, resolved } = input;
|
||||
const rank = setRank(['item', 'all', 'first', 'last']);
|
||||
const SKIP = new Set(['__ob__', 'pairedItem']);
|
||||
|
||||
if (isSplitInBatchesAbsent()) SKIP.add('context');
|
||||
|
||||
const name = toResolve.startsWith('$(') ? '$()' : toResolve;
|
||||
const name = /^\$\(.*\)$/.test(base) ? '$()' : base;
|
||||
|
||||
if (['$input', '$()'].includes(name) && hasNoParams(toResolve)) SKIP.add('params');
|
||||
if (['$input', '$()'].includes(name) && hasNoParams(base)) SKIP.add('params');
|
||||
|
||||
let rawKeys = Object.keys(resolved);
|
||||
|
||||
|
@ -287,7 +320,7 @@ const objectOptions = (toResolve: string, resolved: IDataObject) => {
|
|||
rawKeys = Reflect.ownKeys(resolved) as string[];
|
||||
}
|
||||
|
||||
if (toResolve === 'Math') {
|
||||
if (base === 'Math') {
|
||||
const descriptors = Object.getOwnPropertyDescriptors(Math);
|
||||
rawKeys = Object.keys(descriptors).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
@ -296,27 +329,26 @@ const objectOptions = (toResolve: string, resolved: IDataObject) => {
|
|||
.filter((key) => !SKIP.has(key) && isAllowedInDotNotation(key) && !isPseudoParam(key))
|
||||
.map((key) => {
|
||||
ensureKeyCanBeResolved(resolved, key);
|
||||
const resolvedProp = resolved[key];
|
||||
|
||||
const isFunction = typeof resolved[key] === 'function';
|
||||
const isFunction = typeof resolvedProp === 'function';
|
||||
const hasArgs = isFunction && resolvedProp.length > 0 && name !== '$()';
|
||||
|
||||
const option: Completion = {
|
||||
label: isFunction ? key + '()' : key,
|
||||
type: isFunction ? 'function' : 'keyword',
|
||||
section: getObjectPropertySection({ name, key, isFunction }),
|
||||
apply: applyCompletion(hasArgs),
|
||||
};
|
||||
|
||||
const infoKey = [name, key].join('.');
|
||||
option.info = createCompletionOption(
|
||||
'Object',
|
||||
key,
|
||||
isFunction ? 'native-function' : 'keyword',
|
||||
{
|
||||
doc: {
|
||||
name: key,
|
||||
returnType: typeof resolved[key],
|
||||
description: i18n.proxyVars[infoKey],
|
||||
},
|
||||
option.info = createCompletionOption('', key, isFunction ? 'native-function' : 'keyword', {
|
||||
doc: {
|
||||
name: key,
|
||||
returnType: typeof resolvedProp,
|
||||
description: i18n.proxyVars[infoKey],
|
||||
},
|
||||
).info;
|
||||
}).info;
|
||||
|
||||
return option;
|
||||
});
|
||||
|
@ -324,14 +356,192 @@ const objectOptions = (toResolve: string, resolved: IDataObject) => {
|
|||
const skipObjectExtensions =
|
||||
resolved.isProxy ||
|
||||
resolved.json ||
|
||||
/json('])?$/.test(toResolve) ||
|
||||
toResolve === '$execution' ||
|
||||
toResolve.endsWith('params') ||
|
||||
toResolve === 'Math';
|
||||
/json('])$/.test(base) ||
|
||||
base === '$execution' ||
|
||||
base.endsWith('params') ||
|
||||
base === 'Math';
|
||||
|
||||
if (skipObjectExtensions) return [...localKeys, ...natives('object')];
|
||||
if (skipObjectExtensions) {
|
||||
return sortCompletionsAlpha([...localKeys, ...natives('object')]);
|
||||
}
|
||||
|
||||
return [...localKeys, ...natives('object'), ...extensions('object')];
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([...localKeys, ...natives('object'), ...extensions('object')]),
|
||||
recommended: OBJECT_RECOMMENDED_OPTIONS,
|
||||
recommendedSection: RECOMMENDED_METHODS_SECTION,
|
||||
methodsSection: OTHER_METHODS_SECTION,
|
||||
propSection: FIELDS_SECTION,
|
||||
excludeRecommended: true,
|
||||
});
|
||||
};
|
||||
|
||||
const getObjectPropertySection = ({
|
||||
name,
|
||||
key,
|
||||
isFunction,
|
||||
}: {
|
||||
name: string;
|
||||
key: string;
|
||||
isFunction: boolean;
|
||||
}): CompletionSection => {
|
||||
if (name === '$input' || name === '$()') {
|
||||
if (key === 'item') return RECOMMENDED_SECTION;
|
||||
return OTHER_SECTION;
|
||||
}
|
||||
|
||||
return isFunction ? METHODS_SECTION : FIELDS_SECTION;
|
||||
};
|
||||
|
||||
const applySections = ({
|
||||
options,
|
||||
sections,
|
||||
recommended = [],
|
||||
excludeRecommended = false,
|
||||
methodsSection = METHODS_SECTION,
|
||||
propSection = PROPERTIES_SECTION,
|
||||
recommendedSection = RECOMMENDED_SECTION,
|
||||
}: {
|
||||
options: Completion[];
|
||||
recommended?: string[];
|
||||
recommendedSection?: CompletionSection;
|
||||
methodsSection?: CompletionSection;
|
||||
propSection?: CompletionSection;
|
||||
sections?: Record<string, CompletionSection>;
|
||||
excludeRecommended?: boolean;
|
||||
}) => {
|
||||
const recommendedSet = new Set(recommended);
|
||||
const optionByLabel = options.reduce(
|
||||
(acc, option) => {
|
||||
acc[option.label] = option;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Completion>,
|
||||
);
|
||||
return recommended
|
||||
.map(
|
||||
(reco): Completion => ({
|
||||
...optionByLabel[reco],
|
||||
section: recommendedSection,
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
options
|
||||
.filter((option) => !excludeRecommended || !recommendedSet.has(option.label))
|
||||
.map((option) => {
|
||||
if (sections) {
|
||||
option.section = sections[option.section as string] ?? OTHER_SECTION;
|
||||
} else {
|
||||
option.section = option.label.endsWith('()') ? methodsSection : propSection;
|
||||
}
|
||||
return option;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const isUrl = (url: string): boolean => {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const stringOptions = (input: AutocompleteInput<string>): Completion[] => {
|
||||
const { resolved, tail } = input;
|
||||
const options = sortCompletionsAlpha([...natives('string'), ...extensions('string')]);
|
||||
|
||||
if (validateFieldType('string', resolved, 'number').valid) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toInt()', 'toFloat()'],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (validateFieldType('string', resolved, 'dateTime').valid) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['toDate()'],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (VALID_EMAIL_REGEX.test(resolved) || isUrl(resolved)) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['extractDomain()', 'isEmail()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
if (resolved.split(/\s/).find((token) => VALID_EMAIL_REGEX.test(token))) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ['extractEmail()', ...STRING_RECOMMENDED_OPTIONS],
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
}
|
||||
|
||||
return applySections({
|
||||
options,
|
||||
recommended: STRING_RECOMMENDED_OPTIONS,
|
||||
sections: STRING_SECTIONS,
|
||||
});
|
||||
};
|
||||
|
||||
const numberOptions = (input: AutocompleteInput<number>): Completion[] => {
|
||||
const { resolved } = input;
|
||||
const options = sortCompletionsAlpha([...natives('number'), ...extensions('number')]);
|
||||
const ONLY_INTEGER = ['isEven()', 'isOdd()'];
|
||||
|
||||
if (Number.isInteger(resolved)) {
|
||||
return applySections({
|
||||
options,
|
||||
recommended: ONLY_INTEGER,
|
||||
});
|
||||
} else {
|
||||
const exclude = new Set(ONLY_INTEGER);
|
||||
return applySections({
|
||||
options: options.filter((option) => !exclude.has(option.label)),
|
||||
recommended: ['round()', 'floor()', 'ceil()', 'toFixed()'],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const dateOptions = (): Completion[] => {
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha([...natives('date'), ...extensions('date', true)]),
|
||||
recommended: DATE_RECOMMENDED_OPTIONS,
|
||||
});
|
||||
};
|
||||
|
||||
const luxonOptions = (): Completion[] => {
|
||||
return applySections({
|
||||
options: sortCompletionsAlpha(
|
||||
uniqBy([...extensions('date'), ...luxonInstanceOptions()], (option) => option.label),
|
||||
),
|
||||
recommended: LUXON_RECOMMENDED_OPTIONS,
|
||||
sections: LUXON_SECTIONS,
|
||||
});
|
||||
};
|
||||
|
||||
const arrayOptions = (input: AutocompleteInput<unknown[]>): Completion[] => {
|
||||
const { resolved } = input;
|
||||
const options = applySections({
|
||||
options: sortCompletionsAlpha([...natives('array'), ...extensions('array')]),
|
||||
recommended: ARRAY_RECOMMENDED_OPTIONS,
|
||||
methodsSection: OTHER_SECTION,
|
||||
propSection: OTHER_SECTION,
|
||||
excludeRecommended: true,
|
||||
});
|
||||
|
||||
if (resolved.length > 0 && resolved.some((i) => typeof i !== 'number')) {
|
||||
const NUMBER_ONLY_ARRAY_EXTENSIONS = new Set(ARRAY_NUMBER_ONLY_METHODS);
|
||||
|
||||
return options.filter((m) => !NUMBER_ONLY_ARRAY_EXTENSIONS.has(m.label));
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
function ensureKeyCanBeResolved(obj: IDataObject, key: string) {
|
||||
|
@ -410,7 +620,7 @@ export const secretProvidersOptions = () => {
|
|||
/**
|
||||
* Methods and fields defined on a Luxon `DateTime` class instance.
|
||||
*/
|
||||
export const luxonInstanceOptions = () => {
|
||||
export const luxonInstanceOptions = (includeHidden = false) => {
|
||||
const SKIP = new Set(['constructor', 'get', 'invalidExplanation', 'invalidReason']);
|
||||
|
||||
return Object.entries(Object.getOwnPropertyDescriptors(DateTime.prototype))
|
||||
|
@ -419,8 +629,15 @@ export const luxonInstanceOptions = () => {
|
|||
.map(([key, descriptor]) => {
|
||||
const isFunction = typeof descriptor.value === 'function';
|
||||
const optionType = isFunction ? 'native-function' : 'keyword';
|
||||
return createLuxonAutocompleteOption(key, optionType, luxonInstanceDocs, i18n.luxonInstance);
|
||||
});
|
||||
return createLuxonAutocompleteOption(
|
||||
key,
|
||||
optionType,
|
||||
luxonInstanceDocs,
|
||||
i18n.luxonInstance,
|
||||
includeHidden,
|
||||
) as Completion;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -429,17 +646,19 @@ export const luxonInstanceOptions = () => {
|
|||
export const luxonStaticOptions = () => {
|
||||
const SKIP = new Set(['prototype', 'name', 'length', 'invalid']);
|
||||
|
||||
return Object.keys(Object.getOwnPropertyDescriptors(DateTime))
|
||||
.filter((key) => !SKIP.has(key) && !key.includes('_'))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((key) => {
|
||||
return createLuxonAutocompleteOption(
|
||||
key,
|
||||
'native-function',
|
||||
luxonStaticDocs,
|
||||
i18n.luxonStatic,
|
||||
);
|
||||
});
|
||||
return sortCompletionsAlpha(
|
||||
Object.keys(Object.getOwnPropertyDescriptors(DateTime))
|
||||
.filter((key) => !SKIP.has(key) && !key.includes('_'))
|
||||
.map((key) => {
|
||||
return createLuxonAutocompleteOption(
|
||||
key,
|
||||
'native-function',
|
||||
luxonStaticDocs,
|
||||
i18n.luxonStatic,
|
||||
) as Completion;
|
||||
})
|
||||
.filter(Boolean),
|
||||
);
|
||||
};
|
||||
|
||||
const createLuxonAutocompleteOption = (
|
||||
|
@ -447,11 +666,10 @@ const createLuxonAutocompleteOption = (
|
|||
type: AutocompleteOptionType,
|
||||
docDefinition: NativeDoc,
|
||||
translations: Record<string, string | undefined>,
|
||||
): Completion => {
|
||||
const option: Completion = {
|
||||
label: isFunctionOption(type) ? name + '()' : name,
|
||||
type,
|
||||
};
|
||||
includeHidden = false,
|
||||
): Completion | null => {
|
||||
const isFunction = isFunctionOption(type);
|
||||
const label = isFunction ? name + '()' : name;
|
||||
|
||||
let doc: DocMetadata | undefined;
|
||||
if (docDefinition.properties && docDefinition.properties.hasOwnProperty(name)) {
|
||||
|
@ -469,6 +687,17 @@ const createLuxonAutocompleteOption = (
|
|||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetime',
|
||||
};
|
||||
}
|
||||
|
||||
if (doc?.hidden && !includeHidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const option: Completion = {
|
||||
label,
|
||||
type,
|
||||
section: doc?.section,
|
||||
apply: applyCompletion(hasRequiredArgs(doc)),
|
||||
};
|
||||
option.info = createCompletionOption('DateTime', name, type, {
|
||||
// Add translated description
|
||||
doc: { ...doc, description: translations[name] } as DocMetadata,
|
||||
|
@ -495,20 +724,20 @@ export const objectGlobalOptions = () => {
|
|||
};
|
||||
|
||||
const regexes = {
|
||||
generalRef: /\$[^$'"]+\.([^{\s])*/, // $input. or $json. or similar ones
|
||||
selectorRef: /\$\(['"][\S\s]+['"]\)\.([^{\s])*/, // $('nodeName').
|
||||
generalRef: /\$[^$'"]+\.(.*)/, // $input. or $json. or similar ones
|
||||
selectorRef: /\$\(['"][\S\s]+['"]\)\.(.*)/, // $('nodeName').
|
||||
|
||||
numberLiteral: /\((\d+)\.?(\d*)\)\.([^{\s])*/, // (123). or (123.4).
|
||||
numberLiteral: /\((\d+)\.?(\d*)\)\.(.*)/, // (123). or (123.4).
|
||||
singleQuoteStringLiteral: /('.+')\.([^'{\s])*/, // 'abc'.
|
||||
doubleQuoteStringLiteral: /(".+")\.([^"{\s])*/, // "abc".
|
||||
dateLiteral: /\(?new Date\(\(?.*?\)\)?\.([^{\s])*/, // new Date(). or (new Date()).
|
||||
arrayLiteral: /(\[.+\])\.([^{\s])*/, // [1, 2, 3].
|
||||
indexedAccess: /([^{\s]+\[.+\])\.([^{\s])*/, // 'abc'[0]. or 'abc'.split('')[0] or similar ones
|
||||
objectLiteral: /\(\{.*\}\)\.([^{\s])*/, // ({}).
|
||||
dateLiteral: /\(?new Date\(\(?.*?\)\)?\.(.*)/, // new Date(). or (new Date()).
|
||||
arrayLiteral: /(\[.*\])\.(.*)/, // [1, 2, 3].
|
||||
indexedAccess: /([^"{\s]+\[.+\])\.(.*)/, // 'abc'[0]. or 'abc'.split('')[0] or similar ones
|
||||
objectLiteral: /\(\{.*\}\)\.(.*)/, // ({}).
|
||||
|
||||
mathGlobal: /Math\.([^{\s])*/, // Math.
|
||||
datetimeGlobal: /DateTime\.[^.}]*/, // DateTime.
|
||||
objectGlobal: /Object\.(\w+\(.*\)\.[^{\s]*)?/, // Object. or Object.method(arg).
|
||||
mathGlobal: /Math\.(.*)/, // Math.
|
||||
datetimeGlobal: /DateTime\.(.*)/, // DateTime.
|
||||
objectGlobal: /Object\.(.*)/, // Object. or Object.method(arg).
|
||||
};
|
||||
|
||||
const DATATYPE_REGEX = new RegExp(
|
||||
|
|
|
@ -3,15 +3,16 @@ import {
|
|||
autocompletableNodeNames,
|
||||
receivesNoBinaryData,
|
||||
longestCommonPrefix,
|
||||
setRank,
|
||||
prefixMatch,
|
||||
stripExcessParens,
|
||||
hasActiveNode,
|
||||
isCredentialsModalOpen,
|
||||
applyCompletion,
|
||||
} from './utils';
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { escapeMappingString } from '@/utils/mappingUtils';
|
||||
import { PREVIOUS_NODES_SECTION, ROOT_DOLLAR_COMPLETIONS } from './constants';
|
||||
|
||||
/**
|
||||
* Completions offered at the dollar position: `$|`
|
||||
|
@ -45,10 +46,8 @@ export function dollarCompletions(context: CompletionContext): CompletionResult
|
|||
};
|
||||
}
|
||||
|
||||
export function dollarOptions() {
|
||||
const rank = setRank(['$json', '$input']);
|
||||
export function dollarOptions(): Completion[] {
|
||||
const SKIP = new Set();
|
||||
const DOLLAR_FUNCTIONS = ['$jmespath', '$ifEmpty'];
|
||||
|
||||
if (isCredentialsModalOpen()) {
|
||||
return useExternalSecretsStore().isEnterpriseExternalSecretsEnabled
|
||||
|
@ -71,29 +70,14 @@ export function dollarOptions() {
|
|||
|
||||
if (receivesNoBinaryData()) SKIP.add('$binary');
|
||||
|
||||
const keys = Object.keys(i18n.rootVars).sort((a, b) => a.localeCompare(b));
|
||||
const previousNodesCompletions = autocompletableNodeNames().map((nodeName) => ({
|
||||
label: `$('${escapeMappingString(nodeName)}')`,
|
||||
type: 'keyword',
|
||||
info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
section: PREVIOUS_NODES_SECTION,
|
||||
}));
|
||||
|
||||
return rank(keys)
|
||||
.filter((key) => !SKIP.has(key))
|
||||
.map((key) => {
|
||||
const isFunction = DOLLAR_FUNCTIONS.includes(key);
|
||||
|
||||
const option: Completion = {
|
||||
label: isFunction ? key + '()' : key,
|
||||
type: isFunction ? 'function' : 'keyword',
|
||||
};
|
||||
|
||||
const info = i18n.rootVars[key];
|
||||
|
||||
if (info) option.info = info;
|
||||
|
||||
return option;
|
||||
})
|
||||
.concat(
|
||||
autocompletableNodeNames().map((nodeName) => ({
|
||||
label: `$('${escapeMappingString(nodeName)}')`,
|
||||
type: 'keyword',
|
||||
info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
|
||||
})),
|
||||
);
|
||||
return ROOT_DOLLAR_COMPLETIONS.filter(({ label }) => !SKIP.has(label))
|
||||
.concat(previousNodesCompletions)
|
||||
.map((completion) => ({ ...completion, apply: applyCompletion() }));
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
day: {
|
||||
doc: {
|
||||
name: 'day',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeday',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -15,6 +16,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
daysInMonth: {
|
||||
doc: {
|
||||
name: 'daysInMonth',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimedaysinmonth',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -22,6 +25,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
daysInYear: {
|
||||
doc: {
|
||||
name: 'daysInYear',
|
||||
hidden: true,
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimedaysinyear',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -29,6 +34,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
hour: {
|
||||
doc: {
|
||||
name: 'hour',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimehour',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -36,6 +42,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
locale: {
|
||||
doc: {
|
||||
name: 'locale',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimelocale',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -43,6 +50,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
millisecond: {
|
||||
doc: {
|
||||
name: 'millisecond',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimemillisecond',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -50,6 +58,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
minute: {
|
||||
doc: {
|
||||
name: 'minute',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeminute',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -57,6 +66,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
month: {
|
||||
doc: {
|
||||
name: 'month',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimemonth',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -64,6 +74,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
monthLong: {
|
||||
doc: {
|
||||
name: 'monthLong',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimemonthlong',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -71,6 +82,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
monthShort: {
|
||||
doc: {
|
||||
name: 'monthShort',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimemonthshort',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -78,6 +90,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
numberingSystem: {
|
||||
doc: {
|
||||
name: 'numberingSystem',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimenumberingsystem',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -85,6 +99,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
offset: {
|
||||
doc: {
|
||||
name: 'offset',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeoffset',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -92,6 +108,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
offsetNameLong: {
|
||||
doc: {
|
||||
name: 'offsetNameLong',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeoffsetnamelong',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -99,6 +117,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
offsetNameShort: {
|
||||
doc: {
|
||||
name: 'offsetNameShort',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeoffsetnameshort',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -106,6 +126,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
ordinal: {
|
||||
doc: {
|
||||
name: 'ordinal',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeordinal',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -113,6 +135,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
outputCalendar: {
|
||||
doc: {
|
||||
name: 'outputCalendar',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeoutputcalendar',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -120,6 +144,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
quarter: {
|
||||
doc: {
|
||||
name: 'quarter',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimequarter',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -127,6 +152,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
second: {
|
||||
doc: {
|
||||
name: 'second',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimesecond',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -134,6 +160,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weekday: {
|
||||
doc: {
|
||||
name: 'weekday',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweekday',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -141,6 +168,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weekdayLong: {
|
||||
doc: {
|
||||
name: 'weekdayLong',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweekdaylong',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -148,6 +176,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weekdayShort: {
|
||||
doc: {
|
||||
name: 'weekdayShort',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweekdayshort',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -155,6 +184,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weekNumber: {
|
||||
doc: {
|
||||
name: 'weekNumber',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweeknumber',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -162,6 +192,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weeksInWeekYear: {
|
||||
doc: {
|
||||
name: 'weeksInWeekYear',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweeksinweekyear',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -169,6 +201,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
weekYear: {
|
||||
doc: {
|
||||
name: 'weekYear',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeweekyear',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -176,6 +209,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
year: {
|
||||
doc: {
|
||||
name: 'year',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeyear',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -183,6 +217,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
zone: {
|
||||
doc: {
|
||||
name: 'zone',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimezone',
|
||||
returnType: 'Zone',
|
||||
},
|
||||
|
@ -190,6 +225,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
zoneName: {
|
||||
doc: {
|
||||
name: 'zoneName',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimezonename',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -197,6 +233,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
isInDST: {
|
||||
doc: {
|
||||
name: 'isInDST',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeisindst',
|
||||
returnType: 'boolean',
|
||||
},
|
||||
|
@ -204,6 +241,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
isInLeapYear: {
|
||||
doc: {
|
||||
name: 'isInLeapYear',
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeisinleapyear',
|
||||
returnType: 'boolean',
|
||||
},
|
||||
|
@ -211,6 +249,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
isOffsetFixed: {
|
||||
doc: {
|
||||
name: 'isOffsetFixed',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeisoffsetfixed',
|
||||
returnType: 'boolean',
|
||||
},
|
||||
|
@ -218,6 +258,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
isValid: {
|
||||
doc: {
|
||||
name: 'isValid',
|
||||
hidden: true,
|
||||
section: 'query',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeisvalid',
|
||||
returnType: 'boolean',
|
||||
},
|
||||
|
@ -227,6 +269,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
diff: {
|
||||
doc: {
|
||||
name: 'diff',
|
||||
section: 'compare',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimediff',
|
||||
returnType: 'Duration',
|
||||
args: [
|
||||
|
@ -239,6 +282,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
diffNow: {
|
||||
doc: {
|
||||
name: 'diffNow',
|
||||
section: 'compare',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimediffnow',
|
||||
returnType: 'Duration',
|
||||
args: [
|
||||
|
@ -250,6 +294,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
endOf: {
|
||||
doc: {
|
||||
name: 'endOf',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeendof',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'unit', type: 'string' }],
|
||||
|
@ -258,6 +303,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
equals: {
|
||||
doc: {
|
||||
name: 'equals',
|
||||
section: 'compare',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeequals',
|
||||
returnType: 'boolean',
|
||||
args: [{ name: 'other', type: 'DateTime' }],
|
||||
|
@ -266,6 +312,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
hasSame: {
|
||||
doc: {
|
||||
name: 'hasSame',
|
||||
section: 'compare',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimehassame',
|
||||
returnType: 'boolean',
|
||||
args: [
|
||||
|
@ -277,6 +324,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
minus: {
|
||||
doc: {
|
||||
name: 'minus',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeminus',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'duration', type: 'Duration|object|number' }],
|
||||
|
@ -285,6 +333,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
plus: {
|
||||
doc: {
|
||||
name: 'plus',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeplus',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'duration', type: 'Duration|object|number' }],
|
||||
|
@ -293,6 +342,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
reconfigure: {
|
||||
doc: {
|
||||
name: 'reconfigure',
|
||||
section: 'other',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimereconfigure',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'properties', type: 'object' }],
|
||||
|
@ -301,6 +352,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
resolvedLocaleOptions: {
|
||||
doc: {
|
||||
name: 'resolvedLocaleOptions',
|
||||
section: 'other',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeresolvedlocaleoptions',
|
||||
returnType: 'object',
|
||||
args: [{ name: 'opts', type: 'object' }],
|
||||
|
@ -309,6 +362,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
set: {
|
||||
doc: {
|
||||
name: 'set',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeset',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'values', type: 'object' }],
|
||||
|
@ -317,6 +371,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
setLocale: {
|
||||
doc: {
|
||||
name: 'setLocale',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimesetlocale',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'locale', type: 'any' }],
|
||||
|
@ -325,6 +380,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
setZone: {
|
||||
doc: {
|
||||
name: 'setZone',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimesetzone',
|
||||
returnType: 'DateTime',
|
||||
args: [
|
||||
|
@ -336,6 +392,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
startOf: {
|
||||
doc: {
|
||||
name: 'startOf',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimestartof',
|
||||
returnType: 'DateTime',
|
||||
args: [{ name: 'unit', type: 'string' }],
|
||||
|
@ -344,6 +401,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toBSON: {
|
||||
doc: {
|
||||
name: 'toBSON',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetobson',
|
||||
returnType: 'Date',
|
||||
},
|
||||
|
@ -351,6 +410,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toFormat: {
|
||||
doc: {
|
||||
name: 'toFormat',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetime',
|
||||
returnType: 'string',
|
||||
args: [
|
||||
|
@ -362,6 +423,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toHTTP: {
|
||||
doc: {
|
||||
name: 'toHTTP',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetohttp',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -369,6 +432,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toISO: {
|
||||
doc: {
|
||||
name: 'toISO',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoiso',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'opts', type: 'object' }],
|
||||
|
@ -377,6 +441,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toISODate: {
|
||||
doc: {
|
||||
name: 'toISODate',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoisodate',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'opts', type: 'object' }],
|
||||
|
@ -385,6 +451,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toISOTime: {
|
||||
doc: {
|
||||
name: 'toISOTime',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoisotime',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'opts', type: 'object' }],
|
||||
|
@ -393,6 +461,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toISOWeekDate: {
|
||||
doc: {
|
||||
name: 'toISOWeekDate',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoisoweekdate',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -400,6 +470,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toJSDate: {
|
||||
doc: {
|
||||
name: 'toJSDate',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetojsdate',
|
||||
returnType: 'Date',
|
||||
},
|
||||
|
@ -407,6 +479,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toJSON: {
|
||||
doc: {
|
||||
name: 'toJSON',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetojson',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -414,6 +488,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toLocal: {
|
||||
doc: {
|
||||
name: 'toLocal',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetolocal',
|
||||
returnType: 'DateTime',
|
||||
},
|
||||
|
@ -421,6 +496,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toLocaleParts: {
|
||||
doc: {
|
||||
name: 'toLocaleParts',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetolocaleparts',
|
||||
returnType: 'string',
|
||||
args: [
|
||||
|
@ -432,6 +509,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toLocaleString: {
|
||||
doc: {
|
||||
name: 'toLocaleString',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetolocalestring',
|
||||
returnType: 'string',
|
||||
args: [
|
||||
|
@ -443,6 +521,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toMillis: {
|
||||
doc: {
|
||||
name: 'toMillis',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetomillis',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -450,6 +529,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toObject: {
|
||||
doc: {
|
||||
name: 'toObject',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoobject',
|
||||
returnType: 'object',
|
||||
args: [{ name: 'opts', type: 'any' }],
|
||||
|
@ -458,6 +539,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toRelative: {
|
||||
doc: {
|
||||
name: 'toRelative',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetorelative',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'options', type: 'object' }],
|
||||
|
@ -466,6 +548,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toRelativeCalendar: {
|
||||
doc: {
|
||||
name: 'toRelativeCalendar',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetorelativecalendar',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'options', type: 'object' }],
|
||||
|
@ -474,6 +558,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toRFC2822: {
|
||||
doc: {
|
||||
name: 'toRFC2822',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetorfc2822',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -481,6 +567,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toSeconds: {
|
||||
doc: {
|
||||
name: 'toSeconds',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoseconds',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -488,14 +575,18 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toSQL: {
|
||||
doc: {
|
||||
name: 'toSQL',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetosql',
|
||||
returnType: 'string',
|
||||
hidden: true,
|
||||
args: [{ name: 'options', type: 'object' }],
|
||||
},
|
||||
},
|
||||
toSQLDate: {
|
||||
doc: {
|
||||
name: 'toSQLDate',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetosqldate',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -503,6 +594,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toSQLTime: {
|
||||
doc: {
|
||||
name: 'toSQLTime',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetosqltime',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -510,6 +603,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toString: {
|
||||
doc: {
|
||||
name: 'toString',
|
||||
section: 'format',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetostring',
|
||||
returnType: 'string',
|
||||
},
|
||||
|
@ -517,6 +611,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toUnixInteger: {
|
||||
doc: {
|
||||
name: 'toUnixInteger',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetounixinteger',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
@ -524,6 +620,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
toUTC: {
|
||||
doc: {
|
||||
name: 'toUTC',
|
||||
section: 'edit',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimetoutc',
|
||||
returnType: 'DateTime',
|
||||
args: [
|
||||
|
@ -535,6 +632,7 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
until: {
|
||||
doc: {
|
||||
name: 'until',
|
||||
section: 'compare',
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimeuntil',
|
||||
returnType: 'Interval',
|
||||
args: [{ name: 'other', type: 'DateTime' }],
|
||||
|
@ -543,6 +641,8 @@ export const luxonInstanceDocs: Required<NativeDoc> = {
|
|||
valueOf: {
|
||||
doc: {
|
||||
name: 'valueOf',
|
||||
section: 'format',
|
||||
hidden: true,
|
||||
docURL: 'https://moment.github.io/luxon/api-docs/index.html#datetimevalueof',
|
||||
returnType: 'number',
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import type { resolveParameter } from '@/composables/useWorkflowHelpers';
|
||||
import type { DocMetadata } from 'n8n-workflow';
|
||||
|
||||
export type Resolved = ReturnType<typeof resolveParameter>;
|
||||
export type Resolved = unknown;
|
||||
|
||||
export type ExtensionTypeName = 'number' | 'string' | 'date' | 'array' | 'object';
|
||||
|
||||
|
@ -10,3 +9,8 @@ export type FnToDoc = { [fnName: string]: { doc?: DocMetadata } };
|
|||
export type FunctionOptionType = 'native-function' | 'extension-function';
|
||||
export type KeywordOptionType = 'keyword';
|
||||
export type AutocompleteOptionType = FunctionOptionType | KeywordOptionType;
|
||||
export type AutocompleteInput<R = Resolved> = {
|
||||
resolved: R;
|
||||
base: string;
|
||||
tail: string;
|
||||
};
|
||||
|
|
|
@ -1,24 +1,40 @@
|
|||
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 '@/composables/useWorkflowHelpers';
|
||||
import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||
import {
|
||||
insertCompletionText,
|
||||
type Completion,
|
||||
type CompletionContext,
|
||||
pickedCompletion,
|
||||
type CompletionSection,
|
||||
} from '@codemirror/autocomplete';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { TransactionSpec } from '@codemirror/state';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type { DocMetadata } from 'n8n-workflow';
|
||||
|
||||
// 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() ?? '';
|
||||
const read = (node: SyntaxNode | null) => (node ? userInput.slice(node.from, node.to) : '');
|
||||
const lastNode = javascriptLanguage.parser.parse(userInput).resolveInner(userInput.length, -1);
|
||||
|
||||
return [parts.join('.'), tail];
|
||||
switch (lastNode.type.name) {
|
||||
case '.':
|
||||
return [read(lastNode.parent).slice(0, -1), ''];
|
||||
case 'MemberExpression':
|
||||
return [read(lastNode.parent), read(lastNode)];
|
||||
case 'PropertyName':
|
||||
const tail = read(lastNode);
|
||||
return [read(lastNode.parent).slice(0, -(tail.length + 1)), tail];
|
||||
default:
|
||||
return ['', ''];
|
||||
}
|
||||
}
|
||||
|
||||
export function longestCommonPrefix(...strings: string[]) {
|
||||
|
@ -37,29 +53,6 @@ export function longestCommonPrefix(...strings: string[]) {
|
|||
}, '');
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
|
@ -133,15 +126,15 @@ 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;
|
||||
const activeNodeName = useNDVStore().activeNode?.name;
|
||||
|
||||
return (
|
||||
!NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION.includes(node.type) && node.name !== activeNodeName
|
||||
);
|
||||
})
|
||||
.map((node) => node.name);
|
||||
if (!activeNodeName) return [];
|
||||
|
||||
return useWorkflowHelpers({ router: useRouter() })
|
||||
.getCurrentWorkflow()
|
||||
.getParentNodesByDepth(activeNodeName)
|
||||
.map((node) => node.name)
|
||||
.filter((name) => name !== activeNodeName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -157,3 +150,50 @@ export const stripExcessParens = (context: CompletionContext) => (option: Comple
|
|||
|
||||
return option;
|
||||
};
|
||||
|
||||
/**
|
||||
* When a function completion is selected, set the cursor correctly
|
||||
* @example `.includes()` -> `.includes(<cursor>)`
|
||||
* @example `$max()` -> `$max()<cursor>`
|
||||
*/
|
||||
export const applyCompletion =
|
||||
(hasArgs = true) =>
|
||||
(view: EditorView, completion: Completion, from: number, to: number): void => {
|
||||
const tx: TransactionSpec = {
|
||||
...insertCompletionText(view.state, completion.label, from, to),
|
||||
annotations: pickedCompletion.of(completion),
|
||||
};
|
||||
|
||||
if (completion.label.endsWith('()') && hasArgs) {
|
||||
const cursorPosition = from + completion.label.length - 1;
|
||||
tx.selection = { anchor: cursorPosition, head: cursorPosition };
|
||||
}
|
||||
|
||||
view.dispatch(tx);
|
||||
};
|
||||
|
||||
export const hasRequiredArgs = (doc?: DocMetadata): boolean => {
|
||||
if (!doc) return false;
|
||||
const requiredArgs = doc?.args?.filter((arg) => !arg.name.endsWith('?')) ?? [];
|
||||
return requiredArgs.length > 0;
|
||||
};
|
||||
|
||||
export const sortCompletionsAlpha = (completions: Completion[]): Completion[] => {
|
||||
return completions.sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
|
||||
export const renderSectionHeader = (section: CompletionSection): HTMLElement => {
|
||||
const container = document.createElement('li');
|
||||
container.classList.add('cm-section-header');
|
||||
const inner = document.createElement('div');
|
||||
inner.classList.add('cm-section-title');
|
||||
inner.textContent = section.name;
|
||||
container.appendChild(inner);
|
||||
|
||||
return container;
|
||||
};
|
||||
|
||||
export const withSectionHeader = (section: CompletionSection): CompletionSection => {
|
||||
section.header = renderSectionHeader;
|
||||
return section;
|
||||
};
|
||||
|
|
76
packages/editor-ui/src/plugins/codemirror/keymap.ts
vendored
Normal file
76
packages/editor-ui/src/plugins/codemirror/keymap.ts
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
acceptCompletion,
|
||||
completionStatus,
|
||||
moveCompletionSelection,
|
||||
selectedCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { indentLess, indentMore, insertNewlineAndIndent, redo, undo } from '@codemirror/commands';
|
||||
import type { EditorView, KeyBinding } from '@codemirror/view';
|
||||
|
||||
export const tabKeyMap = (singleLine = false): KeyBinding[] => [
|
||||
{
|
||||
any(view, event) {
|
||||
if (
|
||||
event.key === 'Tab' ||
|
||||
(event.key === 'Escape' && completionStatus(view.state) !== null)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Tab',
|
||||
run: (view) => {
|
||||
if (selectedCompletion(view.state)) {
|
||||
return acceptCompletion(view);
|
||||
}
|
||||
|
||||
if (!singleLine) return indentMore(view);
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{ key: 'Shift-Tab', run: indentLess },
|
||||
];
|
||||
|
||||
export const enterKeyMap: KeyBinding[] = [
|
||||
{
|
||||
key: 'Enter',
|
||||
run: (view) => {
|
||||
if (selectedCompletion(view.state)) {
|
||||
return acceptCompletion(view);
|
||||
}
|
||||
|
||||
return insertNewlineAndIndent(view);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const SELECTED_AUTOCOMPLETE_OPTION_SELECTOR = '.cm-tooltip-autocomplete li[aria-selected]';
|
||||
const onAutocompleteNavigate = (dir: 'up' | 'down') => (view: EditorView) => {
|
||||
if (completionStatus(view.state) !== null) {
|
||||
moveCompletionSelection(dir === 'down')(view);
|
||||
document
|
||||
.querySelector(SELECTED_AUTOCOMPLETE_OPTION_SELECTOR)
|
||||
?.scrollIntoView({ block: 'nearest' });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const autocompleteKeyMap: KeyBinding[] = [
|
||||
{
|
||||
key: 'ArrowDown',
|
||||
run: onAutocompleteNavigate('down'),
|
||||
},
|
||||
{
|
||||
key: 'ArrowUp',
|
||||
run: onAutocompleteNavigate('up'),
|
||||
},
|
||||
];
|
||||
|
||||
export const historyKeyMap: KeyBinding[] = [
|
||||
{ key: 'Mod-z', run: undo },
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
];
|
|
@ -4,6 +4,7 @@ import { parseMixed } from '@lezer/common';
|
|||
import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
|
||||
import { n8nCompletionSources } from './completions/addCompletions';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
|
||||
const n8nParserWithNestedJsParser = n8nParser.configure({
|
||||
wrap: parseMixed((node) => {
|
||||
|
@ -23,3 +24,5 @@ export function n8nLang() {
|
|||
...n8nCompletionSources().map((source) => n8nLanguage.data.of(source)),
|
||||
]);
|
||||
}
|
||||
|
||||
export const n8nAutocompletion = () => autocompletion({ icons: false });
|
||||
|
|
|
@ -63,28 +63,32 @@ const coloringStateField = StateField.define<DecorationSet>({
|
|||
return Decoration.none;
|
||||
},
|
||||
update(colorings, transaction) {
|
||||
colorings = colorings.map(transaction.changes); // recalculate positions for new doc
|
||||
try {
|
||||
colorings = colorings.map(transaction.changes); // recalculate positions for new doc
|
||||
|
||||
for (const txEffect of transaction.effects) {
|
||||
if (txEffect.is(coloringStateEffects.removeColorEffect)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
}
|
||||
|
||||
if (txEffect.is(coloringStateEffects.addColorEffect)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
|
||||
const decoration = resolvableStateToDecoration[txEffect.value.state ?? 'pending'];
|
||||
|
||||
if (txEffect.value.from === 0 && txEffect.value.to === 0) continue;
|
||||
|
||||
colorings = colorings.update({
|
||||
add: [decoration.range(txEffect.value.from, txEffect.value.to)],
|
||||
});
|
||||
for (const txEffect of transaction.effects) {
|
||||
if (txEffect.is(coloringStateEffects.removeColorEffect)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
}
|
||||
|
||||
if (txEffect.is(coloringStateEffects.addColorEffect)) {
|
||||
colorings = colorings.update({
|
||||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
|
||||
const decoration = resolvableStateToDecoration[txEffect.value.state ?? 'pending'];
|
||||
|
||||
if (txEffect.value.from === 0 && txEffect.value.to === 0) continue;
|
||||
|
||||
colorings = colorings.update({
|
||||
add: [decoration.range(txEffect.value.from, txEffect.value.to)],
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window?.Sentry?.captureException(error);
|
||||
}
|
||||
|
||||
return colorings;
|
||||
|
|
|
@ -357,7 +357,7 @@ export class I18nClass {
|
|||
});
|
||||
}
|
||||
|
||||
rootVars: Record<string, string | undefined> = {
|
||||
rootVars = {
|
||||
$binary: this.baseText('codeNodeEditor.completer.binary'),
|
||||
$execution: this.baseText('codeNodeEditor.completer.$execution'),
|
||||
$ifEmpty: this.baseText('codeNodeEditor.completer.$ifEmpty'),
|
||||
|
@ -375,7 +375,8 @@ export class I18nClass {
|
|||
$today: this.baseText('codeNodeEditor.completer.$today'),
|
||||
$vars: this.baseText('codeNodeEditor.completer.$vars'),
|
||||
$workflow: this.baseText('codeNodeEditor.completer.$workflow'),
|
||||
};
|
||||
DateTime: this.baseText('codeNodeEditor.completer.dateTime'),
|
||||
} as const satisfies Record<string, string | undefined>;
|
||||
|
||||
proxyVars: Record<string, string | undefined> = {
|
||||
'$input.all': this.baseText('codeNodeEditor.completer.$input.all'),
|
||||
|
@ -390,6 +391,7 @@ export class I18nClass {
|
|||
'$().itemMatching': this.baseText('codeNodeEditor.completer.selector.itemMatching'),
|
||||
'$().last': this.baseText('codeNodeEditor.completer.selector.last'),
|
||||
'$().params': this.baseText('codeNodeEditor.completer.selector.params'),
|
||||
'$().isExecuted': this.baseText('codeNodeEditor.completer.selector.isExecuted'),
|
||||
|
||||
'$prevNode.name': this.baseText('codeNodeEditor.completer.$prevNode.name'),
|
||||
'$prevNode.outputIndex': this.baseText('codeNodeEditor.completer.$prevNode.outputIndex'),
|
||||
|
|
|
@ -205,6 +205,7 @@
|
|||
"codeNodeEditor.completer.$workflow.active": "Whether the workflow is active or not (boolean)",
|
||||
"codeNodeEditor.completer.$workflow.id": "The ID of the workflow",
|
||||
"codeNodeEditor.completer.$workflow.name": "The name of the workflow",
|
||||
"codeNodeEditor.completer.dateTime": "Luxon DateTime. Use this object to parse, format and manipulate dates and times",
|
||||
"codeNodeEditor.completer.binary": "The item's binary (file) data",
|
||||
"codeNodeEditor.completer.globalObject.assign": "Copy of the object containing all enumerable own properties",
|
||||
"codeNodeEditor.completer.globalObject.entries": "The object's keys and values",
|
||||
|
@ -314,6 +315,25 @@
|
|||
"codeNodeEditor.completer.selector.itemMatching": "@:_reusableBaseText.codeNodeEditor.completer.itemMatching",
|
||||
"codeNodeEditor.completer.selector.last": "@:_reusableBaseText.codeNodeEditor.completer.last",
|
||||
"codeNodeEditor.completer.selector.params": "The parameters of the node",
|
||||
"codeNodeEditor.completer.selector.isExecuted": "Whether the node has executed",
|
||||
"codeNodeEditor.completer.section.input": "Input",
|
||||
"codeNodeEditor.completer.section.prevNodes": "Earlier nodes",
|
||||
"codeNodeEditor.completer.section.metadata": "Metadata",
|
||||
"codeNodeEditor.completer.section.fields": "Fields",
|
||||
"codeNodeEditor.completer.section.properties": "Properties",
|
||||
"codeNodeEditor.completer.section.methods": "Methods",
|
||||
"codeNodeEditor.completer.section.otherMethods": "Other methods",
|
||||
"codeNodeEditor.completer.section.recommended": "Suggested",
|
||||
"codeNodeEditor.completer.section.recommendedMethods": "Suggested methods",
|
||||
"codeNodeEditor.completer.section.other": "Other",
|
||||
"codeNodeEditor.completer.section.edit": "Edit",
|
||||
"codeNodeEditor.completer.section.query": "Query",
|
||||
"codeNodeEditor.completer.section.format": "Format",
|
||||
"codeNodeEditor.completer.section.component": "Component",
|
||||
"codeNodeEditor.completer.section.case": "Case",
|
||||
"codeNodeEditor.completer.section.cast": "Cast",
|
||||
"codeNodeEditor.completer.section.compare": "Compare",
|
||||
"codeNodeEditor.completer.section.validation": "Validate",
|
||||
"codeNodeEditor.linter.allItems.firstOrLastCalledWithArg": "expects no argument.",
|
||||
"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?",
|
||||
|
|
|
@ -8,55 +8,90 @@
|
|||
content: 'n8n supports all JavaScript functions, including those not listed.';
|
||||
}
|
||||
|
||||
// Custom autocomplete item type icons
|
||||
// 1. Native and n8n extension functions:
|
||||
.cm-completionIcon-extension-function,
|
||||
.cm-completionIcon-native-function {
|
||||
&::after {
|
||||
content: 'ƒ';
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
.ͼ2 .cm-tooltip-autocomplete {
|
||||
background-color: var(--color-background-xlight) !important;
|
||||
box-shadow: var(--box-shadow-light);
|
||||
border: none;
|
||||
|
||||
.cm-tooltip {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
li .cm-completionLabel {
|
||||
color: var(--color-success);
|
||||
> ul[role='listbox'] {
|
||||
font-family: var(--font-family-monospace);
|
||||
max-height: min(220px, 50vh);
|
||||
width: min(260px, 50vw);
|
||||
min-width: 100%;
|
||||
max-width: none;
|
||||
border: var(--border-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
|
||||
li[role='option'] {
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
font-size: var(--font-size-2xs);
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-5xs) var(--spacing-2xs);
|
||||
scroll-padding: 40px;
|
||||
scroll-margin: 40px;
|
||||
}
|
||||
|
||||
li .cm-completionLabel {
|
||||
line-height: var(--font-line-height-xloose);
|
||||
}
|
||||
|
||||
li[aria-selected] {
|
||||
background-color: var(--color-background-base);
|
||||
color: var(--color-autocomplete-item-selected);
|
||||
}
|
||||
|
||||
> .cm-section-header {
|
||||
padding: var(--spacing-4xs) var(--spacing-3xs);
|
||||
border-bottom: 1px solid var(--color-autocomplete-section-header-border);
|
||||
background-color: var(--color-background-xlight);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
> .cm-section-header:not(:first-child) {
|
||||
margin-top: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.cm-section-title {
|
||||
color: var(--color-text-dark);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-3xs);
|
||||
text-transform: uppercase;
|
||||
padding: var(--spacing-5xs) var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ͼ2 .cm-tooltip-autocomplete ul li[aria-selected] {
|
||||
background-color: var(--color-autocomplete-selected-background);
|
||||
}
|
||||
|
||||
.ͼ2 .cm-tooltip-autocomplete ul li[aria-selected] .cm-completionLabel {
|
||||
color: var(--color-autocomplete-selected-font) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.autocomplete-info-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-4xs) 0;
|
||||
}
|
||||
|
||||
.cm-completionInfo {
|
||||
background-color: var(--color-background-xlight) !important;
|
||||
.ͼ2 .cm-completionInfo {
|
||||
background-color: var(--color-background-xlight);
|
||||
border: var(--border-base);
|
||||
margin-left: var(--spacing-5xs);
|
||||
border-radius: var(--border-radius-base);
|
||||
line-height: var(--font-line-height-loose);
|
||||
|
||||
.autocomplete-info-header {
|
||||
color: var(--color-text-dark);
|
||||
color: var(--color-text-base);
|
||||
font-size: var(--font-size-2xs);
|
||||
font-family: var(--font-family-monospace);
|
||||
line-height: var(--font-line-height-compact);
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.autocomplete-info-name {
|
||||
color: var(--color-success);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-autocomplete-item-selected);
|
||||
}
|
||||
|
||||
.autocomplete-info-description {
|
||||
|
|
|
@ -51,13 +51,14 @@ export const isAnyPairedItemError = (error: unknown): error is ExpressionError =
|
|||
return error instanceof ExpressionError && error.context.functionality === 'pairedItem';
|
||||
};
|
||||
|
||||
export const getResolvableState = (error: unknown): ResolvableState => {
|
||||
export const getResolvableState = (error: unknown, ignoreError = false): ResolvableState => {
|
||||
if (!error) return 'valid';
|
||||
|
||||
if (
|
||||
isNoExecDataExpressionError(error) ||
|
||||
isNoNodeExecDataExpressionError(error) ||
|
||||
isPairedItemIntermediateNodesError(error)
|
||||
isPairedItemIntermediateNodesError(error) ||
|
||||
ignoreError
|
||||
) {
|
||||
return 'pending';
|
||||
}
|
||||
|
|
|
@ -219,27 +219,35 @@ function plus(
|
|||
endOfMonth.doc = {
|
||||
name: 'endOfMonth',
|
||||
returnType: 'Date',
|
||||
hidden: true,
|
||||
description: 'Transforms a date to the last possible moment that lies within the month.',
|
||||
section: 'edit',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-endOfMonth',
|
||||
};
|
||||
|
||||
isDst.doc = {
|
||||
name: 'isDst',
|
||||
returnType: 'boolean',
|
||||
hidden: true,
|
||||
description: 'Checks if a Date is within Daylight Savings Time.',
|
||||
section: 'query',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isDst',
|
||||
};
|
||||
|
||||
isWeekend.doc = {
|
||||
name: 'isWeekend',
|
||||
returnType: 'boolean',
|
||||
hidden: true,
|
||||
description: 'Checks if the Date falls on a Saturday or Sunday.',
|
||||
section: 'query',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-isWeekend',
|
||||
};
|
||||
|
||||
beginningOf.doc = {
|
||||
name: 'beginningOf',
|
||||
description: 'Transform a Date to the start of the given time period. Default unit is `week`.',
|
||||
section: 'edit',
|
||||
hidden: true,
|
||||
returnType: 'Date',
|
||||
args: [{ name: 'unit?', type: 'DurationUnit' }],
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-beginningOf',
|
||||
|
@ -248,6 +256,7 @@ beginningOf.doc = {
|
|||
extract.doc = {
|
||||
name: 'extract',
|
||||
description: 'Extracts the part defined in `datePart` from a Date. Default unit is `week`.',
|
||||
section: 'query',
|
||||
returnType: 'number',
|
||||
args: [{ name: 'datePart?', type: 'DurationUnit' }],
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-extract',
|
||||
|
@ -257,6 +266,7 @@ format.doc = {
|
|||
name: 'format',
|
||||
description: 'Formats a Date in the given structure.',
|
||||
returnType: 'string',
|
||||
section: 'format',
|
||||
args: [{ name: 'fmt', type: 'TimeFormat' }],
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/dates/#date-format',
|
||||
};
|
||||
|
@ -264,6 +274,7 @@ format.doc = {
|
|||
isBetween.doc = {
|
||||
name: 'isBetween',
|
||||
description: 'Checks if a Date is between two given dates.',
|
||||
section: 'query',
|
||||
returnType: 'boolean',
|
||||
args: [
|
||||
{ name: 'date1', type: 'Date|string' },
|
||||
|
@ -275,6 +286,7 @@ isBetween.doc = {
|
|||
isInLast.doc = {
|
||||
name: 'isInLast',
|
||||
description: 'Checks if a Date is within a given time period. Default unit is `minute`.',
|
||||
section: 'query',
|
||||
returnType: 'boolean',
|
||||
args: [
|
||||
{ name: 'n', type: 'number' },
|
||||
|
@ -286,6 +298,7 @@ isInLast.doc = {
|
|||
minus.doc = {
|
||||
name: 'minus',
|
||||
description: 'Subtracts a given time period from a Date. Default unit is `milliseconds`.',
|
||||
section: 'edit',
|
||||
returnType: 'Date',
|
||||
args: [
|
||||
{ name: 'n', type: 'number' },
|
||||
|
@ -297,6 +310,7 @@ minus.doc = {
|
|||
plus.doc = {
|
||||
name: 'plus',
|
||||
description: 'Adds a given time period to a Date. Default unit is `milliseconds`.',
|
||||
section: 'edit',
|
||||
returnType: 'Date',
|
||||
args: [
|
||||
{ name: 'n', type: 'number' },
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
export interface ExtensionMap {
|
||||
typeName: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
functions: Record<string, Function & { doc?: DocMetadata }>;
|
||||
functions: Record<string, Extension>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type Extension = Function & { doc?: DocMetadata };
|
||||
|
||||
export type NativeDoc = {
|
||||
typeName: string;
|
||||
properties?: Record<string, { doc?: DocMetadata }>;
|
||||
|
@ -14,6 +16,8 @@ export type DocMetadata = {
|
|||
name: string;
|
||||
returnType: string;
|
||||
description?: string;
|
||||
section?: string;
|
||||
hidden?: boolean;
|
||||
aliases?: string[];
|
||||
args?: Array<{ name: string; type?: string }>;
|
||||
docURL?: string;
|
||||
|
|
|
@ -9,6 +9,14 @@ function isNotEmpty(value: object): boolean {
|
|||
return !isEmpty(value);
|
||||
}
|
||||
|
||||
function keys(value: object): string[] {
|
||||
return Object.keys(value);
|
||||
}
|
||||
|
||||
function values(value: object): unknown[] {
|
||||
return Object.values(value);
|
||||
}
|
||||
|
||||
function hasField(value: object, extraArgs: string[]): boolean {
|
||||
const [name] = extraArgs;
|
||||
return name in value;
|
||||
|
@ -146,6 +154,20 @@ keepFieldsContaining.doc = {
|
|||
'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-keepFieldsContaining',
|
||||
};
|
||||
|
||||
keys.doc = {
|
||||
name: 'keys',
|
||||
description: "Returns an array of a given object's own enumerable string-keyed property names.",
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-keys',
|
||||
returnType: 'Array',
|
||||
};
|
||||
|
||||
values.doc = {
|
||||
name: 'values',
|
||||
description: "Returns an array of a given object's own enumerable string-keyed property values.",
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/objects/#object-values',
|
||||
returnType: 'Array',
|
||||
};
|
||||
|
||||
export const objectExtensions: ExtensionMap = {
|
||||
typeName: 'Object',
|
||||
functions: {
|
||||
|
@ -157,5 +179,7 @@ export const objectExtensions: ExtensionMap = {
|
|||
keepFieldsContaining,
|
||||
compact,
|
||||
urlEncode,
|
||||
keys,
|
||||
values,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import SHA from 'jssha';
|
|||
import MD5 from 'md5';
|
||||
import { encode } from 'js-base64';
|
||||
import { titleCase } from 'title-case';
|
||||
import type { ExtensionMap } from './Extensions';
|
||||
import type { Extension, ExtensionMap } from './Extensions';
|
||||
import { transliterate } from 'transliteration';
|
||||
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||
|
||||
|
@ -362,6 +362,7 @@ function extractUrl(value: string) {
|
|||
removeMarkdown.doc = {
|
||||
name: 'removeMarkdown',
|
||||
description: 'Removes Markdown formatting from a string.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-removeMarkdown',
|
||||
|
@ -370,6 +371,7 @@ removeMarkdown.doc = {
|
|||
removeTags.doc = {
|
||||
name: 'removeTags',
|
||||
description: 'Removes tags, such as HTML or XML, from a string.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-removeTags',
|
||||
|
@ -378,6 +380,7 @@ removeTags.doc = {
|
|||
toDate.doc = {
|
||||
name: 'toDate',
|
||||
description: 'Converts a string to a date.',
|
||||
section: 'cast',
|
||||
returnType: 'Date',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toDate',
|
||||
};
|
||||
|
@ -385,6 +388,7 @@ toDate.doc = {
|
|||
toFloat.doc = {
|
||||
name: 'toFloat',
|
||||
description: 'Converts a string to a decimal number.',
|
||||
section: 'cast',
|
||||
returnType: 'number',
|
||||
aliases: ['toDecimalNumber'],
|
||||
docURL:
|
||||
|
@ -394,6 +398,7 @@ toFloat.doc = {
|
|||
toInt.doc = {
|
||||
name: 'toInt',
|
||||
description: 'Converts a string to an integer.',
|
||||
section: 'cast',
|
||||
returnType: 'number',
|
||||
args: [{ name: 'radix?', type: 'number' }],
|
||||
aliases: ['toWholeNumber'],
|
||||
|
@ -403,6 +408,7 @@ toInt.doc = {
|
|||
toSentenceCase.doc = {
|
||||
name: 'toSentenceCase',
|
||||
description: 'Formats a string to sentence case. Example: "This is a sentence".',
|
||||
section: 'case',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toSentenceCase',
|
||||
|
@ -411,6 +417,7 @@ toSentenceCase.doc = {
|
|||
toSnakeCase.doc = {
|
||||
name: 'toSnakeCase',
|
||||
description: 'Formats a string to snake case. Example: "this_is_snake_case".',
|
||||
section: 'case',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toSnakeCase',
|
||||
|
@ -420,6 +427,7 @@ toTitleCase.doc = {
|
|||
name: 'toTitleCase',
|
||||
description:
|
||||
'Formats a string to title case. Example: "This Is a Title". Will not change already uppercase letters to prevent losing information from acronyms and trademarks such as iPhone or FAANG.',
|
||||
section: 'case',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-toTitleCase',
|
||||
|
@ -428,6 +436,7 @@ toTitleCase.doc = {
|
|||
urlEncode.doc = {
|
||||
name: 'urlEncode',
|
||||
description: 'Encodes a string to be used/included in a URL.',
|
||||
section: 'edit',
|
||||
args: [{ name: 'entireString?', type: 'boolean' }],
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
|
@ -438,6 +447,7 @@ urlDecode.doc = {
|
|||
name: 'urlDecode',
|
||||
description:
|
||||
'Decodes a URL-encoded string. It decodes any percent-encoded characters in the input string, and replaces them with their original characters.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-urlDecode',
|
||||
|
@ -446,6 +456,7 @@ urlDecode.doc = {
|
|||
replaceSpecialChars.doc = {
|
||||
name: 'replaceSpecialChars',
|
||||
description: 'Replaces non-ASCII characters in a string with an ASCII representation.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-replaceSpecialChars',
|
||||
|
@ -453,6 +464,8 @@ replaceSpecialChars.doc = {
|
|||
|
||||
length.doc = {
|
||||
name: 'length',
|
||||
section: 'query',
|
||||
hidden: true,
|
||||
description: 'Returns the character count of a string.',
|
||||
returnType: 'number',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings',
|
||||
|
@ -461,6 +474,7 @@ length.doc = {
|
|||
isDomain.doc = {
|
||||
name: 'isDomain',
|
||||
description: 'Checks if a string is a domain.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isDomain',
|
||||
};
|
||||
|
@ -468,13 +482,15 @@ isDomain.doc = {
|
|||
isEmail.doc = {
|
||||
name: 'isEmail',
|
||||
description: 'Checks if a string is an email.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isEmail',
|
||||
};
|
||||
|
||||
isNumeric.doc = {
|
||||
name: 'isEmail',
|
||||
name: 'isNumeric',
|
||||
description: 'Checks if a string only contains digits.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isNumeric',
|
||||
|
@ -483,6 +499,7 @@ isNumeric.doc = {
|
|||
isUrl.doc = {
|
||||
name: 'isUrl',
|
||||
description: 'Checks if a string is a valid URL.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isUrl',
|
||||
};
|
||||
|
@ -490,6 +507,7 @@ isUrl.doc = {
|
|||
isEmpty.doc = {
|
||||
name: 'isEmpty',
|
||||
description: 'Checks if a string is empty.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isEmpty',
|
||||
};
|
||||
|
@ -497,6 +515,7 @@ isEmpty.doc = {
|
|||
isNotEmpty.doc = {
|
||||
name: 'isNotEmpty',
|
||||
description: 'Checks if a string has content.',
|
||||
section: 'validation',
|
||||
returnType: 'boolean',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-isNotEmpty',
|
||||
|
@ -505,6 +524,7 @@ isNotEmpty.doc = {
|
|||
extractEmail.doc = {
|
||||
name: 'extractEmail',
|
||||
description: 'Extracts an email from a string. Returns undefined if none is found.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractEmail',
|
||||
|
@ -514,6 +534,7 @@ extractDomain.doc = {
|
|||
name: 'extractDomain',
|
||||
description:
|
||||
'Extracts a domain from a string containing a valid URL. Returns undefined if none is found.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractDomain',
|
||||
|
@ -522,6 +543,7 @@ extractDomain.doc = {
|
|||
extractUrl.doc = {
|
||||
name: 'extractUrl',
|
||||
description: 'Extracts a URL from a string. Returns undefined if none is found.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
docURL:
|
||||
'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-extractUrl',
|
||||
|
@ -530,6 +552,7 @@ extractUrl.doc = {
|
|||
hash.doc = {
|
||||
name: 'hash',
|
||||
description: 'Returns a string hashed with the given algorithm. Default algorithm is `md5`.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'algo?', type: 'Algorithm' }],
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-hash',
|
||||
|
@ -538,11 +561,17 @@ hash.doc = {
|
|||
quote.doc = {
|
||||
name: 'quote',
|
||||
description: 'Returns a string wrapped in the quotation marks. Default quotation is `"`.',
|
||||
section: 'edit',
|
||||
returnType: 'string',
|
||||
args: [{ name: 'mark?', type: 'string' }],
|
||||
docURL: 'https://docs.n8n.io/code/builtin/data-transformation-functions/strings/#string-quote',
|
||||
};
|
||||
|
||||
const toDecimalNumber: Extension = toFloat.bind({});
|
||||
toDecimalNumber.doc = { ...toFloat.doc, hidden: true };
|
||||
const toWholeNumber: Extension = toInt.bind({});
|
||||
toWholeNumber.doc = { ...toInt.doc, hidden: true };
|
||||
|
||||
export const stringExtensions: ExtensionMap = {
|
||||
typeName: 'String',
|
||||
functions: {
|
||||
|
@ -550,10 +579,10 @@ export const stringExtensions: ExtensionMap = {
|
|||
removeMarkdown,
|
||||
removeTags,
|
||||
toDate,
|
||||
toDecimalNumber: toFloat,
|
||||
toDecimalNumber,
|
||||
toFloat,
|
||||
toInt,
|
||||
toWholeNumber: toInt,
|
||||
toWholeNumber,
|
||||
toSentenceCase,
|
||||
toSnakeCase,
|
||||
toTitleCase,
|
||||
|
|
|
@ -2,26 +2,5 @@ import type { NativeDoc } from '@/Extensions/Extensions';
|
|||
|
||||
export const objectMethods: NativeDoc = {
|
||||
typeName: 'Object',
|
||||
functions: {
|
||||
keys: {
|
||||
doc: {
|
||||
name: 'keys',
|
||||
description:
|
||||
"Returns an array of a given object's own enumerable string-keyed property names.",
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys',
|
||||
returnType: 'Array',
|
||||
},
|
||||
},
|
||||
values: {
|
||||
doc: {
|
||||
name: 'values',
|
||||
description:
|
||||
"Returns an array of a given object's own enumerable string-keyed property values.",
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values',
|
||||
returnType: 'Array',
|
||||
},
|
||||
},
|
||||
},
|
||||
functions: {},
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'length',
|
||||
description: 'Returns the number of characters in the string.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length',
|
||||
returnType: 'number',
|
||||
|
@ -18,6 +19,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'concat',
|
||||
description: 'Concatenates the string arguments to the calling string.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat',
|
||||
returnType: 'string',
|
||||
|
@ -27,6 +29,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'endsWith',
|
||||
description: 'Checks if a string ends with `searchString`.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith',
|
||||
returnType: 'boolean',
|
||||
|
@ -37,6 +40,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'indexOf',
|
||||
description: 'Returns the index of the first occurrence of `searchString`.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf',
|
||||
returnType: 'number',
|
||||
|
@ -50,6 +54,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'lastIndexOf',
|
||||
description: 'Returns the index of the last occurrence of `searchString`.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf',
|
||||
returnType: 'number',
|
||||
|
@ -63,6 +68,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'match',
|
||||
description: 'Retrieves the result of matching a string against a regular expression.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match',
|
||||
returnType: 'Array',
|
||||
|
@ -73,6 +79,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'includes',
|
||||
description: 'Checks if `searchString` may be found within the calling string.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes',
|
||||
returnType: 'boolean',
|
||||
|
@ -87,6 +94,7 @@ export const stringMethods: NativeDoc = {
|
|||
name: 'replace',
|
||||
description:
|
||||
'Returns a string with matches of a `pattern` replaced by a `replacement`. If `pattern` is a string, only the first occurrence will be replaced.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace',
|
||||
returnType: 'string',
|
||||
|
@ -100,6 +108,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'replaceAll',
|
||||
description: 'Returns a string with matches of a `pattern` replaced by a `replacement`.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll',
|
||||
returnType: 'string',
|
||||
|
@ -113,6 +122,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'search',
|
||||
description: 'Returns a string that matches `pattern` within the given string.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search',
|
||||
returnType: 'string',
|
||||
|
@ -124,6 +134,7 @@ export const stringMethods: NativeDoc = {
|
|||
name: 'slice',
|
||||
description:
|
||||
'Returns a section of a string. `indexEnd` defaults to the length of the string if not given.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice',
|
||||
returnType: 'string',
|
||||
|
@ -138,6 +149,7 @@ export const stringMethods: NativeDoc = {
|
|||
name: 'split',
|
||||
description:
|
||||
'Returns the substrings that result from dividing the given string with `separator`.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split',
|
||||
returnType: 'Array',
|
||||
|
@ -151,6 +163,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'startsWith',
|
||||
description: 'Checks if the string begins with `searchString`.',
|
||||
section: 'query',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith',
|
||||
returnType: 'boolean',
|
||||
|
@ -165,6 +178,7 @@ export const stringMethods: NativeDoc = {
|
|||
name: 'substring',
|
||||
description:
|
||||
'Returns the part of the string from the start index up to and excluding the end index, or to the end of the string if no end index is supplied.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring',
|
||||
returnType: 'string',
|
||||
|
@ -178,6 +192,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'toLowerCase',
|
||||
description: 'Formats a string to lowercase. Example: "this is lowercase”.',
|
||||
section: 'case',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase',
|
||||
returnType: 'string',
|
||||
|
@ -187,6 +202,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'toUpperCase',
|
||||
description: 'Formats a string to lowercase. Example: "THIS IS UPPERCASE”.',
|
||||
section: 'case',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase',
|
||||
returnType: 'string',
|
||||
|
@ -196,6 +212,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'trim',
|
||||
description: 'Removes whitespace from both ends of a string and returns a new string.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim',
|
||||
returnType: 'string',
|
||||
|
@ -205,6 +222,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'trimEnd',
|
||||
description: 'Removes whitespace from the end of a string and returns a new string.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd',
|
||||
returnType: 'string',
|
||||
|
@ -214,6 +232,7 @@ export const stringMethods: NativeDoc = {
|
|||
doc: {
|
||||
name: 'trimStart',
|
||||
description: 'Removes whitespace from the beginning of a string and returns a new string.',
|
||||
section: 'edit',
|
||||
docURL:
|
||||
'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart',
|
||||
returnType: 'string',
|
||||
|
|
|
@ -81,5 +81,13 @@ describe('Data Transformation Functions', () => {
|
|||
test('.urlEncode should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1, test2: "2" }).urlEncode() }}')).toEqual('test1=1&test2=2');
|
||||
});
|
||||
|
||||
test('.keys should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1, test2: "2" }).keys() }}')).toEqual(['test1', 'test2']);
|
||||
});
|
||||
|
||||
test('.values should work on an object', () => {
|
||||
expect(evaluate('={{ ({ test1: 1, test2: "2" }).values() }}')).toEqual([1, '2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue