feat: 重构数据空间与 SQL 工作台
- 提供连接管理、逻辑表编排和轻量 SQL 工作台 - 增加 SQL 补全、执行分析、结果分栏与导出交互 - 统一数据空间导航、图标和编辑器体验
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.0",
|
||||
"@codemirror/commands": "^6.10.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
|
||||
import { completionStatus, startCompletion } from '@codemirror/autocomplete';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { beforeEach, expect, it, vi } from 'vitest';
|
||||
|
||||
import CodeEditor from './CodeEditor.vue';
|
||||
@@ -83,3 +85,65 @@ it('shows a recoverable state when a language extension fails', async () => {
|
||||
expect(wrapper.findAll('.cm-editor')).toHaveLength(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('supports a workbench editor without the outer focus ring', () => {
|
||||
const wrapper = mount(CodeEditor, {
|
||||
props: { focusRing: false, modelValue: 'SELECT 1;' },
|
||||
});
|
||||
|
||||
expect(wrapper.classes()).toContain('is-focus-ring-hidden');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('supports dynamically enabling an asynchronous completion source', async () => {
|
||||
const completionSource = vi.fn(() => null);
|
||||
const wrapper = mount(CodeEditor, {
|
||||
props: { modelValue: 'SELECT ' },
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.setProps({ completionSource });
|
||||
|
||||
expect(wrapper.find('.cm-editor').exists()).toBe(true);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it.each(['Tab', 'Enter'])(
|
||||
'accepts the selected completion with %s',
|
||||
async (key) => {
|
||||
const completionSource = vi.fn(() => ({
|
||||
from: 0,
|
||||
options: [{ label: 'SELECT' }],
|
||||
}));
|
||||
const wrapper = mount(CodeEditor, {
|
||||
attachTo: document.body,
|
||||
props: { completionSource, modelValue: 'SEL' },
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
const editorElement = wrapper.get('.cm-editor').element as HTMLElement;
|
||||
const view = EditorView.findFromDOM(editorElement);
|
||||
expect(view).not.toBeNull();
|
||||
if (!view) return;
|
||||
|
||||
view.dispatch({ selection: { anchor: view.state.doc.length } });
|
||||
view.focus();
|
||||
startCompletion(view);
|
||||
await vi.waitFor(() => {
|
||||
expect(completionStatus(view.state)).toBe('active');
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
view.contentDOM.dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
code: key,
|
||||
key,
|
||||
}),
|
||||
);
|
||||
await flushPromises();
|
||||
|
||||
expect(view.state.doc.toString()).toBe('SELECT');
|
||||
wrapper.unmount();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { CompletionSource } from '@codemirror/autocomplete';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import type { EditorLanguage } from './types';
|
||||
|
||||
import { onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
|
||||
import { acceptCompletion, autocompletion } from '@codemirror/autocomplete';
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
@@ -42,7 +44,9 @@ import {
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
autofocus?: boolean;
|
||||
completionSource?: CompletionSource;
|
||||
disabled?: boolean;
|
||||
focusRing?: boolean;
|
||||
language?: EditorLanguage;
|
||||
lineWrapping?: boolean;
|
||||
loading?: boolean;
|
||||
@@ -52,7 +56,9 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
autofocus: false,
|
||||
completionSource: undefined,
|
||||
disabled: false,
|
||||
focusRing: true,
|
||||
language: 'text',
|
||||
lineWrapping: true,
|
||||
loading: false,
|
||||
@@ -74,6 +80,7 @@ const languageLoading = ref(false);
|
||||
const languageSlot = new Compartment();
|
||||
const readonlySlot = new Compartment();
|
||||
const wrappingSlot = new Compartment();
|
||||
const completionSlot = new Compartment();
|
||||
let languageRequest = 0;
|
||||
let syncingFromOutside = false;
|
||||
|
||||
@@ -140,6 +147,17 @@ function wrappingExtension(enabled: boolean): Extension {
|
||||
return enabled ? EditorView.lineWrapping : [];
|
||||
}
|
||||
|
||||
function completionExtension(source?: CompletionSource): Extension {
|
||||
return source
|
||||
? autocompletion({
|
||||
activateOnTyping: true,
|
||||
activateOnTypingDelay: 160,
|
||||
maxRenderedOptions: 36,
|
||||
override: [source],
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
function createState(doc: string, language: Extension) {
|
||||
return EditorState.create({
|
||||
doc,
|
||||
@@ -176,6 +194,7 @@ function createState(doc: string, language: Extension) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
indentWithTab,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
@@ -185,6 +204,7 @@ function createState(doc: string, language: Extension) {
|
||||
languageSlot.of(language),
|
||||
readonlySlot.of(readonlyExtensions(isReadonly())),
|
||||
wrappingSlot.of(wrappingExtension(props.lineWrapping)),
|
||||
completionSlot.of(completionExtension(props.completionSource)),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged && !syncingFromOutside) {
|
||||
emit('update:modelValue', update.state.doc.toString());
|
||||
@@ -232,6 +252,40 @@ function createState(doc: string, language: Extension) {
|
||||
color: 'hsl(var(--foreground))',
|
||||
padding: '8px 12px',
|
||||
},
|
||||
'.cm-tooltip-autocomplete': {
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'hsl(var(--surface-elevated))',
|
||||
border: '1px solid hsl(var(--line-subtle))',
|
||||
borderRadius: 'var(--radius-control)',
|
||||
boxShadow: 'var(--shadow-popover)',
|
||||
},
|
||||
'.cm-tooltip-autocomplete > ul': {
|
||||
maxHeight: '248px',
|
||||
padding: '4px',
|
||||
fontFamily: 'var(--font-family-sans)',
|
||||
},
|
||||
'.cm-tooltip-autocomplete > ul > li': {
|
||||
minHeight: '30px',
|
||||
padding: '5px 8px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
},
|
||||
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
||||
color: 'hsl(var(--foreground))',
|
||||
backgroundColor: 'hsl(var(--nav-item-hover))',
|
||||
},
|
||||
'.cm-completionLabel': {
|
||||
color: 'hsl(var(--foreground))',
|
||||
},
|
||||
'.cm-completionMatchedText': {
|
||||
color: 'hsl(var(--primary))',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
'.cm-completionDetail': {
|
||||
color: 'hsl(var(--text-muted))',
|
||||
fontSize: '11px',
|
||||
fontStyle: 'normal',
|
||||
marginLeft: '16px',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -337,6 +391,15 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.completionSource,
|
||||
(source) => {
|
||||
viewRef.value?.dispatch({
|
||||
effects: completionSlot.reconfigure(completionExtension(source)),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.lineWrapping,
|
||||
(value) => {
|
||||
@@ -361,6 +424,7 @@ defineExpose({ focus, openSearch, scrollToLine });
|
||||
class="easyflow-code-editor"
|
||||
:class="{
|
||||
'is-disabled': disabled,
|
||||
'is-focus-ring-hidden': !focusRing,
|
||||
'is-loading': loading || languageLoading,
|
||||
}"
|
||||
:aria-busy="loading || languageLoading"
|
||||
@@ -405,6 +469,11 @@ defineExpose({ focus, openSearch, scrollToLine });
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 12%);
|
||||
}
|
||||
|
||||
.easyflow-code-editor.is-focus-ring-hidden:focus-within {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.easyflow-code-editor.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.62;
|
||||
|
||||
@@ -4,3 +4,9 @@ export * from './markdown-security';
|
||||
export { default as MarkdownLiveEditor } from './MarkdownLiveEditor.vue';
|
||||
export { default as MarkdownSourceEditor } from './MarkdownSourceEditor.vue';
|
||||
export * from './types';
|
||||
export type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from '@codemirror/autocomplete';
|
||||
|
||||
Reference in New Issue
Block a user