mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): SQL editor overhaul (#6282)
* Draft setup * ⚡ Implemented expression evaluation in Postgres node, minor SQL editor UI improvements, minor refacring * ⚡ Added initial version of expression preview for SQL editor * ⚡ Linking npm package for codemirror sql grammar instead of a local file * ⚡ Moving expression editor wrapper elements to the component * ⚡ Using expression preview in SQL editor * Use SQL parser skipping whitespace * ✨ Added support for custom skipped segments specification * ✨ Fixing highlight problems with dots and expressions that resolve to zero * 👕 Fixing linting error * ✨ Added current item support * ⚡ Added expression support to more nodes with sql editor * ✨ Added expression support for other nodes * ✨ Implemented different SQL dialect support * 🐛 Fixing hard-coded parameter names for editors * ✨ Fixing preview for nested queries, updating query when input data changes, adding keyboard shortcut to toggle comments * ✨ Adding a custom automcomplete notice for different editors * ⚡ Updating SQL autocomplete notice * ✅ Added unit tests for SQL editor * ⚡ Using latest grammar * 🐛 Fixing code node editor rendering * 💄 SQL preview dropdown matches editor width. Removing unnecessary css * ⚡ Addressing PR review feedback * 👌 Addressing PR review feedback pt2 * 👌 Added path alias for utils in nodes-base package * 👌 Addressing more PR review feedback * ✅ Adding tests for `getResolvables` utility function * ⚡Fixing lodash imports * 👌 Better focus handling, adding more plugins to the editor, other minor imrovements * ⚡ Not showing SQL autocomplete suggestions inside expressions * ⚡ Using npm package for sql grammar * ⚡ Removing autocomplete notice, adding line highlight on syntax error * 👌 Addressing code review feedback --------- Co-authored-by: Milorad Filipovic <milorad@n8n.io>
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
<template>
|
||||
<div ref="sqlEditor" class="ph-no-capture"></div>
|
||||
<div :class="$style.sqlEditor" v-click-outside="onBlur">
|
||||
<div ref="sqlEditor" data-test-id="sql-editor-container" class="ph-no-capture"></div>
|
||||
<InlineExpressionEditorOutput
|
||||
:segments="segments"
|
||||
:value="query"
|
||||
:isReadOnly="isReadOnly"
|
||||
:visible="isFocused"
|
||||
:hoveringItemNumber="hoveringItemNumber"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { indentWithTab, history, redo } from '@codemirror/commands';
|
||||
import { foldGutter, indentOnInput } from '@codemirror/language';
|
||||
import { lintGutter } from '@codemirror/lint';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { acceptCompletion, autocompletion, ifNotIn } from '@codemirror/autocomplete';
|
||||
import { indentWithTab, history, redo, toggleComment } from '@codemirror/commands';
|
||||
import { bracketMatching, foldGutter, indentOnInput, LanguageSupport } from '@codemirror/language';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import {
|
||||
dropCursor,
|
||||
EditorView,
|
||||
@@ -20,76 +26,189 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import { MSSQL, MySQL, PostgreSQL, sql, StandardSQL } from '@codemirror/lang-sql';
|
||||
import type { SQLDialect } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
MSSQL,
|
||||
MySQL,
|
||||
PostgreSQL,
|
||||
StandardSQL,
|
||||
MariaSQL,
|
||||
SQLite,
|
||||
Cassandra,
|
||||
PLSQL,
|
||||
keywordCompletionSource,
|
||||
} from '@n8n/codemirror-lang-sql';
|
||||
import type { SQLDialect as SQLDialectType } from '@n8n/codemirror-lang-sql';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
import { codeNodeEditorEventBus } from '@/event-bus';
|
||||
|
||||
const SQL_DIALECTS = {
|
||||
standard: StandardSQL,
|
||||
mssql: MSSQL,
|
||||
mysql: MySQL,
|
||||
postgres: PostgreSQL,
|
||||
StandardSQL,
|
||||
PostgreSQL,
|
||||
MySQL,
|
||||
MariaSQL,
|
||||
MSSQL,
|
||||
SQLite,
|
||||
Cassandra,
|
||||
PLSQL,
|
||||
} as const;
|
||||
|
||||
type SQLEditorData = {
|
||||
editor: EditorView | null;
|
||||
isFocused: boolean;
|
||||
skipSegments: string[];
|
||||
expressionsDocsUrl: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'sql-editor',
|
||||
components: {
|
||||
InlineExpressionEditorOutput,
|
||||
},
|
||||
mixins: [expressionManager],
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dialect: {
|
||||
type: String as PropType<SQLDialect>,
|
||||
default: 'standard',
|
||||
type: String,
|
||||
default: 'StandardSQL',
|
||||
validator: (value: string) => {
|
||||
return Object.keys(SQL_DIALECTS).includes(value);
|
||||
},
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
data(): SQLEditorData {
|
||||
return {
|
||||
editor: {} as EditorView,
|
||||
editor: null,
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
isFocused: false,
|
||||
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens'],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'ndvStore.ndvInputData'() {
|
||||
this.editor?.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this.editor.state.doc.length,
|
||||
insert: this.query,
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.editor?.contentDOM.blur();
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
doc(): string {
|
||||
return this.editor.state.doc.toString();
|
||||
return this.editor?.state.doc.toString() ?? '';
|
||||
},
|
||||
hoveringItemNumber(): number {
|
||||
return this.ndvStore.hoveringItemNumber;
|
||||
},
|
||||
sqlDialect(): SQLDialectType {
|
||||
return SQL_DIALECTS[this.dialect as keyof typeof SQL_DIALECTS] ?? SQL_DIALECTS.StandardSQL;
|
||||
},
|
||||
extensions(): Extension[] {
|
||||
const dialect = this.sqlDialect;
|
||||
|
||||
function sqlWithN8nLanguageSupport() {
|
||||
return new LanguageSupport(dialect.language, [
|
||||
dialect.language.data.of({
|
||||
autocomplete: ifNotIn(['Resolvable'], keywordCompletionSource(dialect, true)),
|
||||
}),
|
||||
n8nCompletionSources().map((source) => dialect.language.data.of(source)),
|
||||
]);
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
sqlWithN8nLanguageSupport(),
|
||||
expressionInputHandler(),
|
||||
codeNodeEditorTheme({ isReadOnly: this.isReadOnly, customMaxHeight: '350px' }),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
this.isFocused = true;
|
||||
},
|
||||
}),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.editable.of(!this.isReadOnly),
|
||||
];
|
||||
|
||||
if (!this.isReadOnly) {
|
||||
extensions.push(
|
||||
history(),
|
||||
keymap.of([
|
||||
{ key: 'Mod-Shift-z', run: redo },
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
indentWithTab,
|
||||
]),
|
||||
autocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!viewUpdate.docChanged || !this.editor) return;
|
||||
|
||||
highlighter.removeColor(this.editor as EditorView, this.plaintextSegments);
|
||||
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);
|
||||
|
||||
this.$emit('valueChanged', this.doc);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const dialect = SQL_DIALECTS[this.dialect as SQLDialect] ?? SQL_DIALECTS.standard;
|
||||
const extensions: Extension[] = [
|
||||
sql({ dialect, upperCaseKeywords: true }),
|
||||
codeNodeEditorTheme({ maxHeight: false }),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
lintGutter(),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
];
|
||||
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||
|
||||
if (this.isReadOnly) {
|
||||
extensions.push(EditorView.editable.of(this.isReadOnly));
|
||||
} else {
|
||||
extensions.push(
|
||||
history(),
|
||||
keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo }]),
|
||||
autocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter(),
|
||||
dropCursor(),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (!viewUpdate.docChanged) return;
|
||||
this.$emit('valueChanged', this.doc);
|
||||
}),
|
||||
);
|
||||
}
|
||||
const state = EditorState.create({ doc: this.query, extensions });
|
||||
const state = EditorState.create({ doc: this.query, extensions: this.extensions });
|
||||
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
|
||||
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);
|
||||
},
|
||||
methods: {
|
||||
onBlur() {
|
||||
this.isFocused = false;
|
||||
},
|
||||
highlightLine(line: number | 'final') {
|
||||
if (!this.editor) return;
|
||||
|
||||
if (line === 'final') {
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.query.length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.editor.state.doc.line(line).from },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.sqlEditor {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user