mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Refactor expression editors and mixins to composition API (#8894)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
@@ -1,31 +1,35 @@
|
||||
<template>
|
||||
<div v-on-click-outside="onBlur" :class="$style.sqlEditor">
|
||||
<div :class="$style.codemirror" ref="sqlEditor" data-test-id="sql-editor-container"></div>
|
||||
<div :class="$style.sqlEditor">
|
||||
<div ref="sqlEditor" :class="$style.codemirror" data-test-id="sql-editor-container"></div>
|
||||
<slot name="suffix" />
|
||||
<InlineExpressionEditorOutput
|
||||
v-if="!fillParent"
|
||||
v-if="!fullscreen"
|
||||
:segments="segments"
|
||||
:is-read-only="isReadOnly"
|
||||
:visible="isFocused"
|
||||
:visible="hasFocus"
|
||||
:hovering-item-number="hoveringItemNumber"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
|
||||
import { EXPRESSIONS_DOCS_URL } from '@/constants';
|
||||
import { codeNodeEditorEventBus } from '@/event-bus';
|
||||
import { expressionManager } from '@/mixins/expressionManager';
|
||||
import { useExpressionEditor } from '@/composables/useExpressionEditor';
|
||||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
||||
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
|
||||
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
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';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
import { Prec, type Line } from '@codemirror/state';
|
||||
import {
|
||||
EditorView,
|
||||
dropCursor,
|
||||
@@ -34,7 +38,6 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
} from '@codemirror/view';
|
||||
import type { SQLDialect as SQLDialectType } from '@n8n/codemirror-lang-sql';
|
||||
import {
|
||||
Cassandra,
|
||||
MSSQL,
|
||||
@@ -46,15 +49,8 @@ import {
|
||||
StandardSQL,
|
||||
keywordCompletionSource,
|
||||
} from '@n8n/codemirror-lang-sql';
|
||||
import { defineComponent } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
|
||||
import {
|
||||
autocompleteKeyMap,
|
||||
enterKeyMap,
|
||||
historyKeyMap,
|
||||
tabKeyMap,
|
||||
} from '@/plugins/codemirror/keymap';
|
||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||
|
||||
const SQL_DIALECTS = {
|
||||
StandardSQL,
|
||||
@@ -67,170 +63,142 @@ const SQL_DIALECTS = {
|
||||
PLSQL,
|
||||
} as const;
|
||||
|
||||
type SQLEditorData = {
|
||||
editor: EditorView | null;
|
||||
editorState: EditorState | null;
|
||||
isFocused: boolean;
|
||||
skipSegments: string[];
|
||||
expressionsDocsUrl: string;
|
||||
type Props = {
|
||||
modelValue: string;
|
||||
dialect?: keyof typeof SQL_DIALECTS;
|
||||
rows?: number;
|
||||
isReadOnly?: boolean;
|
||||
fullscreen?: boolean;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SqlEditor',
|
||||
components: {
|
||||
InlineExpressionEditorOutput,
|
||||
},
|
||||
mixins: [expressionManager],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dialect: {
|
||||
type: String,
|
||||
default: 'StandardSQL',
|
||||
validator: (value: string) => {
|
||||
return Object.keys(SQL_DIALECTS).includes(value);
|
||||
},
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fillParent: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
},
|
||||
data(): SQLEditorData {
|
||||
return {
|
||||
editor: null,
|
||||
editorState: null,
|
||||
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
|
||||
isFocused: false,
|
||||
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
doc(): string {
|
||||
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,
|
||||
maxHeight: this.fillParent ? '100%' : '40vh',
|
||||
minHeight: '10vh',
|
||||
rows: this.rows,
|
||||
}),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
this.isFocused = true;
|
||||
},
|
||||
}),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.editable.of(!this.isReadOnly),
|
||||
];
|
||||
|
||||
if (!this.isReadOnly) {
|
||||
extensions.push(
|
||||
history(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap(),
|
||||
...enterKeyMap,
|
||||
...historyKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
]),
|
||||
),
|
||||
n8nAutocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (!this.editor || !viewUpdate.docChanged) return;
|
||||
|
||||
// Force segments value update by keeping track of editor state
|
||||
this.editorState = this.editor.state;
|
||||
highlighter.removeColor(this.editor, this.plaintextSegments);
|
||||
highlighter.addColor(this.editor, this.resolvableSegments);
|
||||
|
||||
this.$emit('update:modelValue', this.editor?.state.doc.toString());
|
||||
}),
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||
|
||||
const state = EditorState.create({ doc: this.modelValue, extensions: this.extensions });
|
||||
|
||||
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
|
||||
this.editorState = this.editor.state;
|
||||
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);
|
||||
},
|
||||
methods: {
|
||||
onBlur() {
|
||||
this.isFocused = false;
|
||||
},
|
||||
line(lineNumber: number): Line | null {
|
||||
try {
|
||||
return this.editor?.state.doc.line(lineNumber) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
highlightLine(lineNumber: number | 'final') {
|
||||
if (!this.editor) return;
|
||||
|
||||
if (lineNumber === 'final') {
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.modelValue.length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const line = this.line(lineNumber);
|
||||
|
||||
if (!line) return;
|
||||
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: line.from },
|
||||
});
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
dialect: 'StandardSQL',
|
||||
rows: 4,
|
||||
isReadOnly: false,
|
||||
fullscreen: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:model-value', value: string): void;
|
||||
}>();
|
||||
|
||||
const sqlEditor = ref<HTMLElement>();
|
||||
const extensions = computed(() => {
|
||||
const dialect = SQL_DIALECTS[props.dialect] ?? SQL_DIALECTS.StandardSQL;
|
||||
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 baseExtensions = [
|
||||
sqlWithN8nLanguageSupport(),
|
||||
expressionInputHandler(),
|
||||
codeNodeEditorTheme({
|
||||
isReadOnly: props.isReadOnly,
|
||||
maxHeight: props.fullscreen ? '100%' : '40vh',
|
||||
minHeight: '10vh',
|
||||
rows: props.rows,
|
||||
}),
|
||||
lineNumbers(),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
|
||||
if (!props.isReadOnly) {
|
||||
return baseExtensions.concat([
|
||||
history(),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
...tabKeyMap(),
|
||||
...enterKeyMap,
|
||||
...historyKeyMap,
|
||||
...autocompleteKeyMap,
|
||||
{ key: 'Mod-/', run: toggleComment },
|
||||
]),
|
||||
),
|
||||
n8nAutocompletion(),
|
||||
indentOnInput(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
foldGutter(),
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
]);
|
||||
}
|
||||
return baseExtensions;
|
||||
});
|
||||
const editorValue = ref(props.modelValue);
|
||||
const {
|
||||
editor,
|
||||
segments: { all: segments },
|
||||
readEditorValue,
|
||||
hasFocus,
|
||||
} = useExpressionEditor({
|
||||
editorRef: sqlEditor,
|
||||
editorValue,
|
||||
extensions,
|
||||
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens'],
|
||||
isReadOnly: props.isReadOnly,
|
||||
});
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const hoveringItemNumber = computed(() => {
|
||||
return ndvStore.hoveringItemNumber;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
editorValue.value = newValue;
|
||||
},
|
||||
);
|
||||
|
||||
watch(segments, () => {
|
||||
emit('update:model-value', readEditorValue());
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
codeNodeEditorEventBus.on('error-line-number', highlightLine);
|
||||
|
||||
if (props.fullscreen) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
codeNodeEditorEventBus.off('error-line-number', highlightLine);
|
||||
emit('update:model-value', readEditorValue());
|
||||
});
|
||||
|
||||
function line(lineNumber: number): Line | null {
|
||||
try {
|
||||
return editor.value?.state.doc.line(lineNumber) ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function highlightLine(lineNumber: number | 'final') {
|
||||
if (!editor.value) return;
|
||||
|
||||
if (lineNumber === 'final') {
|
||||
editor.value.dispatch({
|
||||
selection: { anchor: editor.value.state.doc.length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lineToHighlight = line(lineNumber);
|
||||
|
||||
if (!lineToHighlight) return;
|
||||
|
||||
editor.value.dispatch({
|
||||
selection: { anchor: lineToHighlight.from },
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user