mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Feat CB-3030 conflict resolving (#1814)
* CB-3030 feat: conflict resolving * CB-3030 fix: display os specific key instead of mod * CB-3030 fix: sql editor status * CB-3030 fix: styles and data validation * CB-3030 fix: sql editor styles --------- Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ export const BASE_CONTAINERS_STYLES = css`
|
||||
flex-direction: row;
|
||||
align-content: baseline;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&[hideEmpty]:empty {
|
||||
display: none;
|
||||
|
||||
@@ -43,15 +43,15 @@ export const styles = css`
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props extends Omit<ButtonProps, 'style'> {
|
||||
name?: string;
|
||||
interface Props extends Omit<ButtonProps, 'style' | 'icon'> {
|
||||
icon?: string;
|
||||
viewBox?: string;
|
||||
style?: ComponentStyle;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const MenuBarSmallItem: React.FC<React.PropsWithChildren<Props>> = function MenuBarSmallItem({
|
||||
name,
|
||||
icon,
|
||||
viewBox,
|
||||
children,
|
||||
className,
|
||||
@@ -63,7 +63,7 @@ export const MenuBarSmallItem: React.FC<React.PropsWithChildren<Props>> = functi
|
||||
// TODO: use button for icon-box (maybe)
|
||||
return styled(useStyles(styles, style))(
|
||||
<icon-box className={className} tabIndex={0} onClick={onClick}>
|
||||
{name && <IconButton name={name} viewBox={viewBox} {...rest} />}
|
||||
{icon && <IconButton name={icon} viewBox={viewBox} {...rest} />}
|
||||
{children && <icon-label>{children}</icon-label>}
|
||||
</icon-box>,
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ export default [
|
||||
['ui_custom', 'Custom'],
|
||||
['ui_configure', 'Configure'],
|
||||
['ui_apply', 'Apply'],
|
||||
['ui_ignore', 'Ignore'],
|
||||
['ui_unexpected_error', 'Unexpected error'],
|
||||
['ui_error', 'Error'],
|
||||
['ui_error_message', 'Error:'],
|
||||
|
||||
@@ -25,6 +25,7 @@ export default [
|
||||
['ui_custom', 'Custom'],
|
||||
['ui_configure', 'Configure'],
|
||||
['ui_apply', 'Applica'],
|
||||
['ui_ignore', 'Ignore'],
|
||||
['ui_error', 'Errore'],
|
||||
['ui_error_message', 'Errore:'],
|
||||
['ui_error_close', 'Chiudi'],
|
||||
|
||||
@@ -24,6 +24,7 @@ export default [
|
||||
['ui_custom', 'Настроить'],
|
||||
['ui_configure', 'Настроить'],
|
||||
['ui_apply', 'Применить'],
|
||||
['ui_ignore', 'Пропустить'],
|
||||
['ui_error', 'Ошибка'],
|
||||
['ui_error_message', 'Ошибка:'],
|
||||
['ui_error_close', 'Закрыть'],
|
||||
|
||||
@@ -25,6 +25,7 @@ export default [
|
||||
['ui_custom', 'Custom'],
|
||||
['ui_configure', '配置'],
|
||||
['ui_apply', '应用'],
|
||||
['ui_ignore', 'Ignore'],
|
||||
['ui_error', '错误'],
|
||||
['ui_error_message', '错误:'],
|
||||
['ui_error_close', '关闭'],
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@cloudbeaver/core-utils": "~0.1.0",
|
||||
"@cloudbeaver/core-extensions": "~0.1.0",
|
||||
"@cloudbeaver/core-theming": "~0.1.0",
|
||||
"react-hotkeys-hook": "~3.4.7"
|
||||
"react-hotkeys-hook": "~4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mobx": "~6.x.x",
|
||||
@@ -30,4 +30,4 @@
|
||||
"react": "~18.x.x",
|
||||
"reshadow": "~0.x.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { createKeyBinding } from '../createKeyBinding';
|
||||
|
||||
export const KEY_BINDING_REDO = createKeyBinding({
|
||||
id: 'redo',
|
||||
keys: ['ctrl+y', 'ctrl+shift+z'],
|
||||
keysMac: ['shift+cmd+z', 'cmd+y'],
|
||||
keys: ['mod+y', 'mod+shift+z'],
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { createKeyBinding } from '../createKeyBinding';
|
||||
|
||||
export const KEY_BINDING_SAVE = createKeyBinding({
|
||||
id: 'save',
|
||||
keysWin: ['ctrl+s'],
|
||||
keysMac: ['cmd+s'],
|
||||
keys: ['mod+s'],
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import { createKeyBinding } from '../createKeyBinding';
|
||||
|
||||
export const KEY_BINDING_UNDO = createKeyBinding({
|
||||
id: 'undo',
|
||||
keys: 'ctrl+z',
|
||||
keysMac: 'cmd+z',
|
||||
keys: 'mod+z',
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
@@ -12,12 +12,14 @@ import styled, { css } from 'reshadow';
|
||||
|
||||
import { useFocus } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { isObjectsEqual } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { ActionService } from '../Action/ActionService';
|
||||
import type { IActionItem } from '../Action/IActionItem';
|
||||
import { getCommonAndOSSpecificKeys } from '../Action/KeyBinding/getCommonAndOSSpecificKeys';
|
||||
import { CaptureViewContext } from './CaptureViewContext';
|
||||
import type { IView } from './IView';
|
||||
import { parseHotkey } from './parseHotkey';
|
||||
import { useActiveView } from './useActiveView';
|
||||
import { useViewContext } from './useViewContext';
|
||||
|
||||
@@ -44,14 +46,7 @@ export const CaptureView = observer<React.PropsWithChildren<Props>>(function Cap
|
||||
.filter(action => action?.binding && !action.isDisabled())
|
||||
.filter(Boolean) as IActionItem[];
|
||||
|
||||
let keys = actionItems
|
||||
.map(item => getCommonAndOSSpecificKeys(item.binding?.binding))
|
||||
.flat()
|
||||
.join(', ');
|
||||
|
||||
if (keys === '') {
|
||||
keys = '*';
|
||||
}
|
||||
const keys = actionItems.map(item => getCommonAndOSSpecificKeys(item.binding?.binding)).flat();
|
||||
|
||||
useHotkeys(
|
||||
keys,
|
||||
@@ -62,18 +57,31 @@ export const CaptureView = observer<React.PropsWithChildren<Props>>(function Cap
|
||||
|
||||
const action = actionItems.find(action => {
|
||||
const commonAndSpecificKeys = getCommonAndOSSpecificKeys(action.binding?.binding);
|
||||
return commonAndSpecificKeys.includes(handler.key);
|
||||
});
|
||||
return commonAndSpecificKeys.some(key => {
|
||||
const hotkey = parseHotkey(key);
|
||||
|
||||
if (action?.binding?.binding.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return isObjectsEqual(hotkey, handler);
|
||||
});
|
||||
});
|
||||
|
||||
action?.activate(true);
|
||||
},
|
||||
{
|
||||
enabled: keys !== '*',
|
||||
enableOnTags: ['INPUT', 'SELECT', 'TEXTAREA'],
|
||||
enabled: keys.length > 0,
|
||||
enableOnFormTags: ['INPUT', 'SELECT', 'TEXTAREA'],
|
||||
preventDefault(event, handler) {
|
||||
const action = actionItems.find(action => {
|
||||
const commonAndSpecificKeys = getCommonAndOSSpecificKeys(action.binding?.binding);
|
||||
return commonAndSpecificKeys.some(key => {
|
||||
const hotkey = parseHotkey(key);
|
||||
|
||||
return isObjectsEqual(hotkey, handler);
|
||||
});
|
||||
});
|
||||
|
||||
return action?.binding?.binding.preventDefault === true;
|
||||
},
|
||||
enableOnContentEditable: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { Hotkey, KeyboardModifiers } from 'react-hotkeys-hook/dist/types';
|
||||
|
||||
const reservedModifierKeywords = ['shift', 'alt', 'meta', 'mod', 'ctrl'];
|
||||
|
||||
const mappedKeys: Record<string, string> = {
|
||||
esc: 'escape',
|
||||
return: 'enter',
|
||||
'.': 'period',
|
||||
',': 'comma',
|
||||
'-': 'slash',
|
||||
' ': 'space',
|
||||
'`': 'backquote',
|
||||
'#': 'backslash',
|
||||
'+': 'bracketright',
|
||||
ShiftLeft: 'shift',
|
||||
ShiftRight: 'shift',
|
||||
AltLeft: 'alt',
|
||||
AltRight: 'alt',
|
||||
MetaLeft: 'meta',
|
||||
MetaRight: 'meta',
|
||||
OSLeft: 'meta',
|
||||
OSRight: 'meta',
|
||||
ControlLeft: 'ctrl',
|
||||
ControlRight: 'ctrl',
|
||||
};
|
||||
|
||||
export function mapKey(key: string): string {
|
||||
return (mappedKeys[key] || key)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/key|digit|numpad|arrow/, '');
|
||||
}
|
||||
|
||||
export function isHotkeyModifier(key: string) {
|
||||
return reservedModifierKeywords.includes(key);
|
||||
}
|
||||
|
||||
export function parseKeysHookInput(keys: string, splitKey = ','): string[] {
|
||||
return keys.split(splitKey);
|
||||
}
|
||||
|
||||
export function parseHotkey(hotkey: string, combinationKey = '+', description?: string): Hotkey {
|
||||
const keys = hotkey
|
||||
.toLocaleLowerCase()
|
||||
.split(combinationKey)
|
||||
.map(k => mapKey(k));
|
||||
|
||||
const modifiers: KeyboardModifiers = {
|
||||
alt: keys.includes('alt'),
|
||||
ctrl: keys.includes('ctrl') || keys.includes('control'),
|
||||
shift: keys.includes('shift'),
|
||||
meta: keys.includes('meta'),
|
||||
mod: keys.includes('mod'),
|
||||
};
|
||||
|
||||
const singleCharKeys = keys.filter(k => !reservedModifierKeywords.includes(k));
|
||||
|
||||
return {
|
||||
...modifiers,
|
||||
keys: singleCharKeys,
|
||||
description,
|
||||
};
|
||||
}
|
||||
@@ -17,12 +17,25 @@
|
||||
"dependencies": {
|
||||
"@cloudbeaver/core-blocks": "~0.1.0",
|
||||
"@cloudbeaver/core-utils": "~0.1.0",
|
||||
"@codemirror/lang-html": "^6.4.4",
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-sql": "^6.5.0",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"codemirror": "^6.0.1"
|
||||
"@codemirror/lang-html": "6.4.5",
|
||||
"@codemirror/lang-javascript": "6.1.9",
|
||||
"@codemirror/lang-json": "6.0.1",
|
||||
"@codemirror/lang-sql": "6.5.2",
|
||||
"@codemirror/lang-xml": "6.0.2",
|
||||
"@codemirror/merge": "6.1.1",
|
||||
"@codemirror/commands": "6.2.4",
|
||||
"@codemirror/autocomplete": "6.8.1",
|
||||
"@codemirror/search": "6.5.0",
|
||||
"@codemirror/language": "6.8.0",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.14.1"
|
||||
},
|
||||
"peerDependencies": {}
|
||||
"peerDependencies": {
|
||||
"react": "~18.x.x",
|
||||
"react-dom": "~18.x.x",
|
||||
"mobx": "~6.x.x",
|
||||
"mobx-react-lite": "~3.x.x",
|
||||
"reshadow": "~0.x.x",
|
||||
"@testing-library/jest-dom": "~5.x.x"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,22 @@ import styled from 'reshadow';
|
||||
|
||||
import { clsx } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { IDefaultExtensions } from './getDefaultExtensions';
|
||||
import type { IEditorProps } from './IEditorProps';
|
||||
import type { IEditorRef } from './IEditorRef';
|
||||
import { ReactCodemirror } from './ReactCodemirror';
|
||||
import { EDITOR_BASE_STYLES } from './theme';
|
||||
import { useEditorDefaultExtensions } from './useDefaultExtensions';
|
||||
import { useCodemirrorExtensions } from './useCodemirrorExtensions';
|
||||
import { type IDefaultExtensions, useEditorDefaultExtensions } from './useEditorDefaultExtensions';
|
||||
|
||||
export const Editor = observer<IEditorProps & IDefaultExtensions, IEditorRef>(
|
||||
forwardRef(function Editor({ lineNumbers, extensions = [], ...rest }, ref) {
|
||||
forwardRef(function Editor({ lineNumbers, extensions, ...rest }, ref) {
|
||||
extensions = useCodemirrorExtensions(extensions);
|
||||
const defaultExtensions = useEditorDefaultExtensions({ lineNumbers });
|
||||
const combinedExtensions = [...defaultExtensions];
|
||||
combinedExtensions.push(...extensions);
|
||||
extensions.set(...defaultExtensions);
|
||||
|
||||
return styled(EDITOR_BASE_STYLES)(
|
||||
<wrapper className={clsx('editor', rest.className)}>
|
||||
<ReactCodemirror {...rest} ref={ref} extensions={combinedExtensions} />
|
||||
<ReactCodemirror {...rest} ref={ref} extensions={extensions} />
|
||||
</wrapper>,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -9,9 +9,9 @@ import { forwardRef } from 'react';
|
||||
|
||||
import { ComplexLoader, createComplexLoader } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { IDefaultExtensions } from './getDefaultExtensions';
|
||||
import type { IEditorProps } from './IEditorProps';
|
||||
import type { IEditorRef } from './IEditorRef';
|
||||
import type { IDefaultExtensions } from './useEditorDefaultExtensions';
|
||||
|
||||
const loader = createComplexLoader(async function loader() {
|
||||
const { Editor } = await import('./Editor');
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import type { Compartment, Extension } from '@codemirror/state';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
|
||||
export interface IReactCodeMirrorProps {
|
||||
export interface IReactCodeMirrorProps extends React.PropsWithChildren {
|
||||
/** in case of using editor in editing mode its better for performance to use getValue instead */
|
||||
value?: string;
|
||||
incomingValue?: string;
|
||||
getValue?: () => string;
|
||||
extensions?: Extension[];
|
||||
extensions?: Map<Compartment, Extension>;
|
||||
readonly?: boolean;
|
||||
autoFocus?: boolean;
|
||||
onChange?: (value: string, update: ViewUpdate) => void;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { Annotation, EditorState, StateEffect } from '@codemirror/state';
|
||||
import { MergeView } from '@codemirror/merge';
|
||||
import { Annotation, Compartment, Extension, StateEffect } from '@codemirror/state';
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -14,81 +15,124 @@ import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { IEditorRef } from './IEditorRef';
|
||||
import type { IReactCodeMirrorProps } from './IReactCodemirrorProps';
|
||||
import { type IReactCodemirrorContext, ReactCodemirrorContext } from './ReactCodemirrorContext';
|
||||
import { useCodemirrorExtensions } from './useCodemirrorExtensions';
|
||||
|
||||
const External = Annotation.define<boolean>();
|
||||
|
||||
const defaultTheme = EditorView.theme({
|
||||
'&': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
|
||||
forwardRef(function ReactCodemirror({ getValue, value, extensions, readonly, autoFocus, onChange, onUpdate }, ref) {
|
||||
forwardRef(function ReactCodemirror(
|
||||
{ children, getValue, value, incomingValue, extensions = new Map<Compartment, Extension>(), readonly, autoFocus, onChange, onUpdate },
|
||||
ref,
|
||||
) {
|
||||
value = value ?? getValue?.();
|
||||
const currentExtensions = useRef<Map<Compartment, Extension>>(new Map());
|
||||
const readOnlyFacet = useMemo(() => EditorView.editable.of(!readonly), [readonly]);
|
||||
extensions = useCodemirrorExtensions(extensions, readOnlyFacet);
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
const [view, setView] = useState<EditorView | null>(null);
|
||||
const lastValueUpdate = useRef<string | undefined>(undefined);
|
||||
|
||||
const ext = [defaultTheme];
|
||||
const [incomingView, setIncomingView] = useState<EditorView | null>(null);
|
||||
const callbackRef = useObjectRef({ onChange, onUpdate });
|
||||
|
||||
if (readonly) {
|
||||
// can lead to missing state updates
|
||||
ext.push(EditorState.readOnly.of(true));
|
||||
// ext.push(EditorView.editable.of(false));
|
||||
}
|
||||
|
||||
if (extensions) {
|
||||
ext.push(extensions);
|
||||
}
|
||||
|
||||
const updateListener = useMemo(
|
||||
() =>
|
||||
EditorView.updateListener.of((update: ViewUpdate) => {
|
||||
useLayoutEffect(() => {
|
||||
if (container) {
|
||||
const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
|
||||
const remote = update.transactions.some(tr => tr.annotation(External));
|
||||
|
||||
if (update.docChanged && !remote) {
|
||||
const doc = update.state.doc;
|
||||
const value = doc.toString();
|
||||
|
||||
lastValueUpdate.current = value;
|
||||
callbackRef.onChange?.(value, update);
|
||||
}
|
||||
|
||||
callbackRef.onUpdate?.(update);
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
ext.push(updateListener);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (container) {
|
||||
const ev = new EditorView({
|
||||
parent: container,
|
||||
});
|
||||
|
||||
setView(ev);
|
||||
let editorView: EditorView;
|
||||
let incomingView: EditorView | undefined;
|
||||
let merge: MergeView | undefined;
|
||||
|
||||
ev.dom.addEventListener('keydown', event => {
|
||||
const effects: Extension[] = [];
|
||||
for (const [compartment, extension] of extensions) {
|
||||
effects.push(compartment.of(extension));
|
||||
}
|
||||
|
||||
if (incomingValue !== undefined) {
|
||||
merge = new MergeView({
|
||||
a: {
|
||||
extensions: [updateListener, ...effects],
|
||||
},
|
||||
b: {
|
||||
extensions: [EditorView.editable.of(false), ...effects],
|
||||
},
|
||||
parent: container,
|
||||
});
|
||||
|
||||
editorView = merge.a;
|
||||
incomingView = merge.b;
|
||||
} else {
|
||||
editorView = new EditorView({
|
||||
parent: container,
|
||||
extensions: [updateListener, ...effects],
|
||||
});
|
||||
}
|
||||
|
||||
if (incomingView) {
|
||||
setIncomingView(incomingView);
|
||||
}
|
||||
setView(editorView);
|
||||
currentExtensions.current = extensions;
|
||||
|
||||
editorView.dom.addEventListener('keydown', event => {
|
||||
const newEvent = new KeyboardEvent('keydown', event);
|
||||
document.dispatchEvent(newEvent);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ev.destroy();
|
||||
editorView.destroy();
|
||||
merge?.destroy();
|
||||
setView(null);
|
||||
setIncomingView(null);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [container]);
|
||||
}, [container, incomingValue !== undefined]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (view && value !== lastValueUpdate.current) {
|
||||
lastValueUpdate.current = value;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const effects: StateEffect<any>[] = [];
|
||||
for (const [compartment, extension] of extensions) {
|
||||
if (currentExtensions.current.get(compartment) !== extension) {
|
||||
if (compartment.get(view.state) === undefined) {
|
||||
effects.push(StateEffect.appendConfig.of(compartment.of(extension)));
|
||||
} else {
|
||||
effects.push(compartment.reconfigure(extension));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const compartment of currentExtensions.current.keys()) {
|
||||
if (!extensions.has(compartment)) {
|
||||
effects.push(compartment.reconfigure([]));
|
||||
}
|
||||
}
|
||||
|
||||
view.dispatch({ effects });
|
||||
|
||||
if (incomingView) {
|
||||
incomingView.dispatch({ effects });
|
||||
}
|
||||
|
||||
currentExtensions.current = extensions;
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (value !== undefined && view && value !== view.state.doc.toString()) {
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
annotations: [External.of(true)],
|
||||
@@ -97,10 +141,12 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
|
||||
}, [value, view]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (view) {
|
||||
view.dispatch({ effects: StateEffect.reconfigure.of(ext) });
|
||||
if (incomingValue !== undefined && incomingView && incomingValue !== incomingView.state.doc.toString()) {
|
||||
incomingView.dispatch({
|
||||
changes: { from: 0, to: incomingView.state.doc.length, insert: incomingValue },
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [incomingValue, incomingView]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!readonly && autoFocus && view) {
|
||||
@@ -117,6 +163,20 @@ export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(
|
||||
[container, view],
|
||||
);
|
||||
|
||||
return <div ref={setContainer} className="ReactCodemirror" />;
|
||||
const context = useMemo<IReactCodemirrorContext>(
|
||||
() => ({
|
||||
view,
|
||||
incomingView,
|
||||
}),
|
||||
[view, incomingView],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactCodemirrorContext.Provider value={context}>
|
||||
<div ref={setContainer} className="ReactCodemirror">
|
||||
{children}
|
||||
</div>
|
||||
</ReactCodemirrorContext.Provider>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface IReactCodemirrorContext {
|
||||
view: EditorView | null;
|
||||
incomingView: EditorView | null;
|
||||
}
|
||||
|
||||
export const ReactCodemirrorContext = createContext<IReactCodemirrorContext | null>(null);
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { Compartment, StateEffect } from '@codemirror/state';
|
||||
import { showPanel } from '@codemirror/view';
|
||||
import { useContext, useLayoutEffect, useMemo } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ReactCodemirrorContext } from './ReactCodemirrorContext';
|
||||
|
||||
interface Props extends React.PropsWithChildren {
|
||||
className?: string;
|
||||
incomingView?: boolean;
|
||||
top?: boolean;
|
||||
}
|
||||
|
||||
export const ReactCodemirrorPanel: React.FC<Props> = function ReactCodemirrorPanel({ className, children, incomingView, top }) {
|
||||
const dom = useMemo(() => document.createElement('div'), []);
|
||||
const compartment = useMemo(() => new Compartment(), []);
|
||||
const context = useContext(ReactCodemirrorContext);
|
||||
|
||||
const view = incomingView ? context?.incomingView : context?.view;
|
||||
useLayoutEffect(() => {
|
||||
if (view) {
|
||||
view.dispatch({
|
||||
effects: [StateEffect.appendConfig.of(compartment.of(showPanel.of(() => ({ top, dom }))))],
|
||||
});
|
||||
|
||||
return () => {
|
||||
view.dispatch({
|
||||
effects: compartment.reconfigure([]),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [view, top]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (className) {
|
||||
const classes = className.split(' ');
|
||||
dom.classList.add(...classes);
|
||||
|
||||
return () => {
|
||||
dom.classList.remove(...classes);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}, [className]);
|
||||
|
||||
return createPortal(children, dom);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { bracketMatching, foldGutter, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
||||
import { highlightSelectionMatches } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import {
|
||||
crosshairCursor,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
tooltips,
|
||||
} from '@codemirror/view';
|
||||
import { classHighlighter } from '@lezer/highlight';
|
||||
|
||||
import { clsx, GlobalConstants } from '@cloudbeaver/core-utils';
|
||||
|
||||
// @TODO allow to configure bindings outside of the component
|
||||
const DEFAULT_KEY_MAP = defaultKeymap.filter(binding => binding.mac !== 'Ctrl-f' && binding.key !== 'Mod-Enter');
|
||||
|
||||
DEFAULT_KEY_MAP.push(indentWithTab);
|
||||
|
||||
export interface IDefaultExtensions {
|
||||
lineNumbers?: boolean;
|
||||
}
|
||||
|
||||
/** Provides the necessary extensions to establish a basic editor */
|
||||
export function getDefaultExtensions(options?: IDefaultExtensions): Record<string, Extension> {
|
||||
let extensions: Record<string, Extension> = {
|
||||
tooltips: tooltips({
|
||||
parent: document.body,
|
||||
}),
|
||||
highlightSpecialChars: highlightSpecialChars(),
|
||||
highlightSelectionMatches: highlightSelectionMatches(),
|
||||
syntaxHighlighting: syntaxHighlighting(classHighlighter),
|
||||
bracketMatching: bracketMatching(),
|
||||
dropCursor: dropCursor(),
|
||||
crosshairCursor: crosshairCursor(),
|
||||
foldGutter: foldGutter({
|
||||
markerDOM: (open: boolean) => {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttributeNS(null, 'viewBox', '0 0 15 8');
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.maxHeight = '100%';
|
||||
|
||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', GlobalConstants.absoluteUrl('/icons/icons.svg#angle'));
|
||||
svg.appendChild(use);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.appendChild(svg);
|
||||
element.className = clsx('cm-gutterElement-icon', open ? 'cm-foldGutter-open' : 'cm-foldGutter-folded');
|
||||
|
||||
return element;
|
||||
},
|
||||
}),
|
||||
highlightActiveLineGutter: highlightActiveLineGutter(),
|
||||
highlightActiveLine: highlightActiveLine(),
|
||||
indentOnInput: indentOnInput(),
|
||||
rectangularSelection: rectangularSelection(),
|
||||
keymap: keymap.of(DEFAULT_KEY_MAP),
|
||||
};
|
||||
|
||||
if (options?.lineNumbers) {
|
||||
extensions = {
|
||||
lineNumbers: lineNumbers(),
|
||||
...extensions,
|
||||
};
|
||||
}
|
||||
|
||||
return extensions;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createComplexLoader } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export * from './ReactCodemirrorPanel';
|
||||
export * from './EditorLoader';
|
||||
export * from './IEditorProps';
|
||||
export * from './IEditorRef';
|
||||
export type { IDefaultExtensions } from './getDefaultExtensions';
|
||||
export * from './useEditorDefaultExtensions';
|
||||
export * from './useEditorAutocompletion';
|
||||
export * from './useCodemirrorExtensions';
|
||||
|
||||
export * from '@codemirror/view';
|
||||
export * from '@codemirror/state';
|
||||
|
||||
@@ -7,12 +7,27 @@
|
||||
*/
|
||||
|
||||
@mixin base-code-editor() {
|
||||
.editor,
|
||||
.cm-editor,
|
||||
.cm-mergeView,
|
||||
.cm-mergeViewEditors,
|
||||
.cm-scroller,
|
||||
.ReactCodemirror {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.editor,
|
||||
.ReactCodemirror {
|
||||
.ReactCodemirror,
|
||||
.cm-mergeView,
|
||||
.cm-mergeViewEditors,
|
||||
.cm-editor {
|
||||
@include mdc-typography('body2');
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cm-mergeViewEditor:first-child {
|
||||
border-right: 1px solid;
|
||||
@include mdc-theme-prop(border-color, background, false);
|
||||
}
|
||||
|
||||
.editor {
|
||||
@@ -37,22 +52,26 @@
|
||||
border-left: 1.2px solid $cursor;
|
||||
}
|
||||
|
||||
.editor .cm-gutters {
|
||||
.editor .cm-gutters,
|
||||
.cm-panel {
|
||||
@include mdc-theme-prop(background-color, surface, false);
|
||||
@include mdc-theme-prop(color, text-hint-on-light, false);
|
||||
@include mdc-theme-prop(border-color, background, false);
|
||||
}
|
||||
|
||||
.editor .cm-activeLine {
|
||||
background: none;
|
||||
box-shadow: 0 0 0 2px $mdc-theme-sub-secondary;
|
||||
}
|
||||
|
||||
.editor .cm-activeLineGutter {
|
||||
background: none;
|
||||
@include mdc-theme-prop(color, on-surface, false);
|
||||
}
|
||||
|
||||
.ͼ2 .cm-activeLine:not(.cm-changedLine, .cm-deletedLine),
|
||||
.ͼ2 .cm-activeLineGutter:not(.cm-changedLineGutter, .cm-deletedLineGutter) {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.editor .active-query {
|
||||
/* disable because look too ugly */
|
||||
/*
|
||||
@@ -62,21 +81,10 @@
|
||||
}
|
||||
|
||||
.editor .cm-content ::selection {
|
||||
@include mdc-theme-prop(background, primary, false);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.editor .cm-line::selection,
|
||||
.editor .cm-line> :global(span)::selection,
|
||||
.editor .cm-line> :global(span)> :global(span)::selection {
|
||||
@include mdc-theme-prop(background, primary, false);
|
||||
}
|
||||
|
||||
.editor .cm-line::-moz-selection,
|
||||
.editor .cm-line> :global(span)::-moz-selection,
|
||||
.editor .cm-line> :global(span)> :global(span)::-moz-selection {
|
||||
@include mdc-theme-prop(background, primary, false);
|
||||
}
|
||||
|
||||
.editor .cm-lineNumbers .cm-gutterElement {
|
||||
min-width: 3ch;
|
||||
box-sizing: content-box;
|
||||
@@ -197,4 +205,4 @@
|
||||
.editor .tok-type {
|
||||
color: $variable-3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { Compartment, Extension } from '@codemirror/state';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useCodemirrorExtensions(extensions?: Map<Compartment, Extension>, staticExtensions?: Extension): Map<Compartment, Extension> {
|
||||
const [compartment] = useState(() => new Compartment());
|
||||
const map = new Map(extensions?.entries());
|
||||
map.set(compartment, staticExtensions || []);
|
||||
return map;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { createComplexLoader, useComplexLoader } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { IDefaultExtensions } from './getDefaultExtensions';
|
||||
|
||||
// chunk splitting
|
||||
const getDefaultExtensionsLoader = createComplexLoader(() => import('./getDefaultExtensions').then(module => module.getDefaultExtensions));
|
||||
|
||||
export function useEditorDefaultExtensions(options?: IDefaultExtensions) {
|
||||
const getDefaultExtensions = useComplexLoader(getDefaultExtensionsLoader);
|
||||
const [extensions, setExtensions] = useState<Record<string, Extension>>({});
|
||||
|
||||
const newExtensions = getDefaultExtensions(options);
|
||||
const diff: Record<string, 'add' | 'remove'> = {};
|
||||
|
||||
for (const key in extensions) {
|
||||
if (!(key in newExtensions)) {
|
||||
diff[key] = 'remove';
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in newExtensions) {
|
||||
if (!(key in extensions)) {
|
||||
diff[key] = 'add';
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(diff).length > 0) {
|
||||
const nextExtensions = { ...extensions };
|
||||
|
||||
for (const key in diff) {
|
||||
if (diff[key] === 'add') {
|
||||
nextExtensions[key] = newExtensions[key];
|
||||
} else {
|
||||
delete nextExtensions[key];
|
||||
}
|
||||
}
|
||||
|
||||
setExtensions(nextExtensions);
|
||||
}
|
||||
|
||||
return Object.values(extensions);
|
||||
}
|
||||
@@ -6,28 +6,30 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { Compartment, type Extension } from '@codemirror/state';
|
||||
import { keymap } from '@codemirror/view';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export type CompletionConfig = Parameters<typeof autocompletion>[0];
|
||||
|
||||
export function useEditorAutocompletion(config?: CompletionConfig): Extension[] {
|
||||
const [autocompleteKeyMap] = useState(() =>
|
||||
keymap.of([
|
||||
{ key: 'Alt-Space', run: startCompletion, preventDefault: true },
|
||||
{ key: 'Shift-Ctrl-Space', run: startCompletion, preventDefault: true },
|
||||
]),
|
||||
);
|
||||
const EDITOR_AUTOCOMPLETION_COMPARTMENT = new Compartment();
|
||||
|
||||
const EDITOR_AUTOCOMPLETION_KEYMAP = keymap.of([
|
||||
{ key: 'Alt-Space', run: startCompletion, preventDefault: true },
|
||||
{ key: 'Shift-Ctrl-Space', run: startCompletion, preventDefault: true },
|
||||
]);
|
||||
|
||||
export function useEditorAutocompletion(config?: CompletionConfig): [Compartment, Extension] {
|
||||
const autocompletionExtension = useMemo(
|
||||
() =>
|
||||
() => [
|
||||
EDITOR_AUTOCOMPLETION_KEYMAP,
|
||||
autocompletion({
|
||||
...config,
|
||||
closeOnBlur: false,
|
||||
}),
|
||||
],
|
||||
[config],
|
||||
);
|
||||
|
||||
return [autocompletionExtension, autocompleteKeyMap];
|
||||
return [EDITOR_AUTOCOMPLETION_COMPARTMENT, autocompletionExtension];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { bracketMatching, foldGutter, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
||||
import { highlightSelectionMatches } from '@codemirror/search';
|
||||
import { Compartment, Extension } from '@codemirror/state';
|
||||
import {
|
||||
crosshairCursor,
|
||||
dropCursor,
|
||||
highlightActiveLine,
|
||||
highlightActiveLineGutter,
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
tooltips,
|
||||
} from '@codemirror/view';
|
||||
import { classHighlighter } from '@lezer/highlight';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { clsx, GlobalConstants } from '@cloudbeaver/core-utils';
|
||||
|
||||
// @TODO allow to configure bindings outside of the component
|
||||
const DEFAULT_KEY_MAP = defaultKeymap.filter(binding => binding.mac !== 'Ctrl-f' && binding.key !== 'Mod-Enter');
|
||||
|
||||
DEFAULT_KEY_MAP.push(indentWithTab);
|
||||
DEFAULT_KEY_MAP.push({
|
||||
key: 'Mod-s',
|
||||
run: () => true,
|
||||
});
|
||||
|
||||
export interface IDefaultExtensions {
|
||||
lineNumbers?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_EXTENSIONS_COMPARTMENT = new Compartment();
|
||||
|
||||
/** Provides the necessary extensions to establish a basic editor */
|
||||
export function useEditorDefaultExtensions(options?: IDefaultExtensions): [Compartment, Extension] {
|
||||
return useMemo(() => {
|
||||
const extensions = [];
|
||||
if (options?.lineNumbers) {
|
||||
extensions.push(lineNumbers());
|
||||
}
|
||||
|
||||
extensions.push(
|
||||
tooltips({
|
||||
parent: document.body,
|
||||
}),
|
||||
highlightSpecialChars(),
|
||||
highlightSelectionMatches(),
|
||||
syntaxHighlighting(classHighlighter),
|
||||
bracketMatching(),
|
||||
dropCursor(),
|
||||
crosshairCursor(),
|
||||
foldGutter({
|
||||
markerDOM: (open: boolean) => {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttributeNS(null, 'viewBox', '0 0 15 8');
|
||||
svg.style.maxWidth = '100%';
|
||||
svg.style.maxHeight = '100%';
|
||||
|
||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', GlobalConstants.absoluteUrl('/icons/icons.svg#angle'));
|
||||
svg.appendChild(use);
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.appendChild(svg);
|
||||
element.className = clsx('cm-gutterElement-icon', open ? 'cm-foldGutter-open' : 'cm-foldGutter-folded');
|
||||
|
||||
return element;
|
||||
},
|
||||
}),
|
||||
highlightActiveLineGutter(),
|
||||
highlightActiveLine(),
|
||||
indentOnInput(),
|
||||
rectangularSelection(),
|
||||
keymap.of(DEFAULT_KEY_MAP),
|
||||
);
|
||||
|
||||
return [DEFAULT_EXTENSIONS_COMPARTMENT, extensions];
|
||||
}, [options?.lineNumbers]);
|
||||
}
|
||||
+3
-8
@@ -16,7 +16,7 @@ import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { QuotasService } from '@cloudbeaver/core-root';
|
||||
import { BASE_TAB_STYLES, TabContainerPanelComponent, TabList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-ui';
|
||||
import { bytesToSize } from '@cloudbeaver/core-utils';
|
||||
import { EditorLoader } from '@cloudbeaver/plugin-codemirror6';
|
||||
import { EditorLoader, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
|
||||
import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey';
|
||||
import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
|
||||
@@ -176,6 +176,7 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
const autoFormat = !!firstSelectedCell && !editor.isElementEdited(firstSelectedCell);
|
||||
const canSave = !!firstSelectedCell && content.isDownloadable(firstSelectedCell);
|
||||
const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]);
|
||||
const extensions = useCodemirrorExtensions(undefined, typeExtension);
|
||||
|
||||
const value = autoFormat ? formatter.format(state.currentContentType, stringValue) : stringValue;
|
||||
|
||||
@@ -191,13 +192,7 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
|
||||
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} />
|
||||
</TabsState>
|
||||
</actions>
|
||||
<EditorLoader
|
||||
key={readonly ? '1' : '0'}
|
||||
value={value}
|
||||
readonly={readonly}
|
||||
extensions={[typeExtension]}
|
||||
onChange={value => handleChange(value)}
|
||||
/>
|
||||
<EditorLoader key={readonly ? '1' : '0'} value={value} readonly={readonly} extensions={extensions} onChange={value => handleChange(value)} />
|
||||
{valueTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
|
||||
{canSave && (
|
||||
<tools-container>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { MenuBar } from '@cloudbeaver/core-ui';
|
||||
import { useMenu } from '@cloudbeaver/core-view';
|
||||
import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree';
|
||||
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
|
||||
|
||||
@@ -36,13 +37,15 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio
|
||||
const connectionParam = connection ? createConnectionParam(connection) : null;
|
||||
const connectionDialectResource = useResource(DDLViewerTabPanel, ConnectionDialectResource, connectionParam);
|
||||
const sqlDialect = useSqlDialectExtension(connectionDialectResource.data);
|
||||
const extensions = useCodemirrorExtensions();
|
||||
extensions.set(...sqlDialect);
|
||||
|
||||
menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId);
|
||||
menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlResource.data);
|
||||
|
||||
return (
|
||||
<div className={s(styles, { wrapper: true })}>
|
||||
<SQLCodeEditorLoader className={s(styles, { sqlCodeEditorLoader: true })} value={ddlResource.data ?? ''} extensions={[sqlDialect]} readonly />
|
||||
<SQLCodeEditorLoader className={s(styles, { sqlCodeEditorLoader: true })} value={ddlResource.data ?? ''} extensions={extensions} readonly />
|
||||
<MenuBar className={s(styles, { menuBar: true })} menu={menu} />
|
||||
</div>
|
||||
);
|
||||
|
||||
+4
-1
@@ -16,6 +16,7 @@ import {
|
||||
} from '@cloudbeaver/core-connections';
|
||||
import { MenuBar } from '@cloudbeaver/core-ui';
|
||||
import { useMenu } from '@cloudbeaver/core-view';
|
||||
import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree';
|
||||
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
|
||||
|
||||
@@ -36,6 +37,8 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer
|
||||
const connectionParam = connection ? createConnectionParam(connection) : null;
|
||||
const connectionDialectResource = useResource(ExtendedDDLViewerTabPanel, ConnectionDialectResource, connectionParam);
|
||||
const sqlDialect = useSqlDialectExtension(connectionDialectResource.data);
|
||||
const extensions = useCodemirrorExtensions();
|
||||
extensions.set(...sqlDialect);
|
||||
|
||||
menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId);
|
||||
menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDLResource.data);
|
||||
@@ -45,7 +48,7 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer
|
||||
<SQLCodeEditorLoader
|
||||
className={s(styles, { sqlCodeEditorLoader: true })}
|
||||
value={extendedDDLResource.data ?? ''}
|
||||
extensions={[sqlDialect]}
|
||||
extensions={extensions}
|
||||
readonly
|
||||
/>
|
||||
<MenuBar className={s(styles, { menuBar: true })} menu={menu} />
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { getOS, OperatingSystem } from '@cloudbeaver/core-utils';
|
||||
import { getCommonAndOSSpecificKeys, IKeyBinding, KEY_BINDING_OPEN_IN_TAB, KEY_BINDING_REDO, KEY_BINDING_UNDO } from '@cloudbeaver/core-view';
|
||||
import { KEY_BINDING_COLLAPSE_ALL, KEY_BINDING_ENABLE_FILTER, KEY_BINDING_LINK_OBJECT } from '@cloudbeaver/plugin-navigation-tree';
|
||||
import {
|
||||
@@ -101,5 +102,17 @@ export const NAVIGATION_TREE_SHORTCUTS: IShortcut[] = [
|
||||
function transformKeys(keyBinding: IKeyBinding): string[] {
|
||||
const keys = getCommonAndOSSpecificKeys(keyBinding);
|
||||
|
||||
return keys.map(key => key.toLocaleUpperCase().replace(/\+/gi, ' + '));
|
||||
return keys.map(key => transformModToDisplayKey(key.toLocaleUpperCase().replace(/\+/gi, ' + ')));
|
||||
}
|
||||
|
||||
function transformModToDisplayKey(key: string): string {
|
||||
const OS = getOS();
|
||||
if (OS === OperatingSystem.windowsOS) {
|
||||
return key.replace('MOD', 'CTRL');
|
||||
}
|
||||
|
||||
if (OS === OperatingSystem.macOS) {
|
||||
return key.replace('MOD', 'CMD');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
+9
-2
@@ -47,8 +47,12 @@ export const DataSynchronizationNotification = observer<NotificationComponentPro
|
||||
notification.close(false);
|
||||
}
|
||||
|
||||
function ignoreChanges() {
|
||||
notification.close(false);
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<SnackbarWrapper onClose={() => notification.close(false)}>
|
||||
<SnackbarWrapper onClose={ignoreChanges}>
|
||||
<SnackbarStatus status={ENotificationType.Info} />
|
||||
<SnackbarContent>
|
||||
<SnackbarBody title={translate(notification.title)}>
|
||||
@@ -62,7 +66,10 @@ export const DataSynchronizationNotification = observer<NotificationComponentPro
|
||||
</message>
|
||||
</SnackbarBody>
|
||||
<SnackbarFooter timestamp={notification.timestamp}>
|
||||
<Button type="button" mod={['outlined']} onClick={applyChanges}>
|
||||
<Button type="button" mod={['outlined']} onClick={ignoreChanges}>
|
||||
{translate('ui_ignore')}
|
||||
</Button>
|
||||
<Button type="button" mod={['unelevated']} onClick={applyChanges}>
|
||||
{translate('ui_apply')}
|
||||
</Button>
|
||||
</SnackbarFooter>
|
||||
|
||||
+5
@@ -6,8 +6,13 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
|
||||
import type { ISqlDataSourceHistoryState } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
export interface IResourceSqlDataSourceState {
|
||||
resourceKey?: string;
|
||||
script: string;
|
||||
baseScript: string;
|
||||
executionContext?: IConnectionExecutionContextInfo;
|
||||
baseExecutionContext?: IConnectionExecutionContextInfo;
|
||||
history: ISqlDataSourceHistoryState;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { CachedMapAllKey } from '@cloudbeaver/core-sdk';
|
||||
import { createPath, getPathName } from '@cloudbeaver/core-utils';
|
||||
import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, KEY_BINDING_SAVE, KeyBindingService, MenuService } from '@cloudbeaver/core-view';
|
||||
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
|
||||
import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
|
||||
import { getResourceKeyFromNodeId } from '@cloudbeaver/plugin-navigation-tree-rm';
|
||||
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
|
||||
import { ResourceManagerScriptsService, SaveScriptDialog, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
|
||||
import {
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
MemorySqlDataSource,
|
||||
SQL_EDITOR_TOOLS_MENU,
|
||||
SqlDataSourceService,
|
||||
SqlEditorService,
|
||||
SqlEditorSettingsService,
|
||||
} from '@cloudbeaver/plugin-sql-editor';
|
||||
import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
|
||||
@@ -41,7 +40,6 @@ import { SqlEditorTabResourceService } from './SqlEditorTabResourceService';
|
||||
export class PluginBootstrap extends Bootstrap {
|
||||
constructor(
|
||||
private readonly navNodeManagerService: NavNodeManagerService,
|
||||
private readonly navResourceNodeService: NavResourceNodeService,
|
||||
private readonly navNodeInfoResource: NavNodeInfoResource,
|
||||
private readonly navigationTabsService: NavigationTabsService,
|
||||
private readonly notificationService: NotificationService,
|
||||
@@ -55,7 +53,6 @@ export class PluginBootstrap extends Bootstrap {
|
||||
private readonly menuService: MenuService,
|
||||
private readonly sqlDataSourceService: SqlDataSourceService,
|
||||
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
|
||||
private readonly sqlEditorService: SqlEditorService,
|
||||
private readonly resourceManagerResource: ResourceManagerResource,
|
||||
private readonly resourceManagerScriptsService: ResourceManagerScriptsService,
|
||||
private readonly keyBindingService: KeyBindingService,
|
||||
@@ -70,33 +67,24 @@ export class PluginBootstrap extends Bootstrap {
|
||||
this.actionService.addHandler({
|
||||
id: 'scripts-base-handler',
|
||||
isActionApplicable: (context, action): boolean => {
|
||||
if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) {
|
||||
const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE);
|
||||
const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE);
|
||||
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.projectsService.activeProjects.some(project => project.canEditResources)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.projectsService.activeProjects.some(project => project.canEditResources)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataSource = this.sqlDataSourceService.get(state.editorId);
|
||||
const dataSource = this.sqlDataSourceService.get(state.editorId);
|
||||
|
||||
if (action === ACTION_SAVE_AS_SCRIPT) {
|
||||
return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource;
|
||||
}
|
||||
|
||||
if (action === ACTION_SAVE) {
|
||||
return dataSource instanceof ResourceSqlDataSource;
|
||||
}
|
||||
if (action === ACTION_SAVE_AS_SCRIPT) {
|
||||
return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
isHidden: (context, action) => {
|
||||
if (action === ACTION_SAVE) {
|
||||
return this.sqlEditorService.autoSave;
|
||||
return dataSource?.isAutoSaveEnabled === false;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -196,7 +184,7 @@ export class PluginBootstrap extends Bootstrap {
|
||||
return true;
|
||||
}
|
||||
|
||||
return source.isLoading() || source.isSaved();
|
||||
return source.isLoading() || source.isSaved;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
+205
-156
@@ -17,7 +17,7 @@ import { TaskScheduler } from '@cloudbeaver/core-executor';
|
||||
import type { ProjectInfoResource } from '@cloudbeaver/core-projects';
|
||||
import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-resource-manager';
|
||||
import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { createPath, debounce, getPathName, getPathParent, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
import { debounce, getPathName, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
|
||||
import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
@@ -28,7 +28,7 @@ interface IResourceInfo {
|
||||
}
|
||||
|
||||
interface IResourceActions {
|
||||
rename(dataSource: ResourceSqlDataSource, key: string, newKey: string): Promise<string>;
|
||||
rename(dataSource: ResourceSqlDataSource, key: string, name: string): Promise<string>;
|
||||
read(dataSource: ResourceSqlDataSource, key: string): Promise<string>;
|
||||
write(dataSource: ResourceSqlDataSource, key: string, value: string): Promise<void>;
|
||||
getProperties(dataSource: ResourceSqlDataSource, key: string): Promise<IConnectionExecutionContextInfo | undefined>;
|
||||
@@ -55,7 +55,15 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
}
|
||||
|
||||
get script(): string {
|
||||
return this._script;
|
||||
return this.state.script;
|
||||
}
|
||||
|
||||
get baseScript(): string {
|
||||
return this.state.baseScript;
|
||||
}
|
||||
|
||||
get baseExecutionContext(): IConnectionExecutionContextInfo | undefined {
|
||||
return this.state.baseExecutionContext;
|
||||
}
|
||||
|
||||
get projectId(): string | null {
|
||||
@@ -92,11 +100,14 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName];
|
||||
}
|
||||
|
||||
private _script: string;
|
||||
get isAutoSaveEnabled(): boolean {
|
||||
return this.sqlEditorService.autoSave;
|
||||
}
|
||||
|
||||
private actions?: IResourceActions;
|
||||
private info?: IResourceInfo;
|
||||
private lastAction: (() => Promise<void>) | undefined;
|
||||
private readonly state: IResourceSqlDataSourceState;
|
||||
private state!: IResourceSqlDataSourceState;
|
||||
|
||||
private loaded: boolean;
|
||||
private readonly scheduler: TaskScheduler;
|
||||
@@ -110,8 +121,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
state: IResourceSqlDataSourceState,
|
||||
) {
|
||||
super();
|
||||
this.state = state;
|
||||
this._script = '';
|
||||
this.bindState(state);
|
||||
this.lastAction = undefined;
|
||||
this.loaded = false;
|
||||
this.resourceUseKeyId = null;
|
||||
@@ -122,18 +132,15 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
|
||||
resourceManagerResource.onDataOutdated.addHandler(this.syncResource);
|
||||
|
||||
makeObservable<this, '_script' | 'lastAction' | 'loaded'>(this, {
|
||||
makeObservable<this, 'lastAction' | 'loaded'>(this, {
|
||||
script: computed,
|
||||
executionContext: computed,
|
||||
resourceKey: computed,
|
||||
features: computed<ESqlDataSourceFeatures[]>({
|
||||
equals: isArraysEqual,
|
||||
}),
|
||||
_script: observable,
|
||||
lastAction: observable.ref,
|
||||
loaded: observable,
|
||||
setExecutionContext: action,
|
||||
setScript: action,
|
||||
setResourceKey: action,
|
||||
});
|
||||
}
|
||||
@@ -154,6 +161,20 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
return this.scheduler.executing;
|
||||
}
|
||||
|
||||
canRename(name: string | null): boolean {
|
||||
if (this.isReadonly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
|
||||
return !!this.actions && !!this.resourceKey && name.length > 0;
|
||||
}
|
||||
|
||||
setResourceKey(resourceKey: string | undefined): void {
|
||||
if (this.state.resourceKey && this.resourceUseKeyId) {
|
||||
this.resourceManagerResource.free(toJS(this.state.resourceKey), this.resourceUseKeyId);
|
||||
@@ -161,7 +182,8 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
}
|
||||
|
||||
this.state.resourceKey = toJS(resourceKey);
|
||||
this.reloadResource();
|
||||
this.loaded = false;
|
||||
this.markOutdated();
|
||||
this.onUpdate.execute();
|
||||
}
|
||||
|
||||
@@ -174,78 +196,45 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
}
|
||||
|
||||
setName(name: string | null): void {
|
||||
this.rename(name);
|
||||
name = name?.trim() ?? null;
|
||||
if (!name || name === this.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousName = this.name;
|
||||
super.setName(name);
|
||||
|
||||
this.rename(name).catch(() => {
|
||||
super.setName(previousName);
|
||||
});
|
||||
}
|
||||
|
||||
setProject(projectId: string | null): void {
|
||||
super.setProject(projectId);
|
||||
}
|
||||
|
||||
canRename(name: string | null): boolean {
|
||||
if (this.isReadonly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.trim();
|
||||
|
||||
return !!this.actions && !!this.resourceKey && this.saved && name.length > 0;
|
||||
}
|
||||
|
||||
setScript(script: string): void {
|
||||
const previous = this._script;
|
||||
const previous = this.state.script;
|
||||
if (previous === script) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._script = script;
|
||||
this.state.script = script;
|
||||
super.setScript(script);
|
||||
|
||||
if (this.sqlEditorService.autoSave && previous !== script) {
|
||||
if (this.isAutoSaveEnabled) {
|
||||
this.debouncedWrite();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.resourceManagerResource.onItemUpdate.removeHandler(this.syncResource);
|
||||
if (this.state.resourceKey && this.resourceUseKeyId) {
|
||||
this.resourceManagerResource.free(this.state.resourceKey, this.resourceUseKeyId);
|
||||
this.resourceUseKeyId = null;
|
||||
}
|
||||
}
|
||||
setExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
|
||||
executionContext = JSON.parse(JSON.stringify(toJS(executionContext) ?? {}));
|
||||
|
||||
syncResource(key: ResourceKey<string>) {
|
||||
if (isResourceAlias(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceKey = this.resourceKey;
|
||||
if (resourceKey && ResourceKeyUtils.some(key, key => resourceKey.startsWith(key))) {
|
||||
this.reloadResource();
|
||||
}
|
||||
}
|
||||
|
||||
reloadResource(): void {
|
||||
this.markOutdated();
|
||||
this.saved = true;
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
if (this.state.resourceKey && !this.resourceUseKeyId) {
|
||||
this.resourceUseKeyId = this.resourceManagerResource.use(this.state.resourceKey);
|
||||
}
|
||||
await this.read();
|
||||
}
|
||||
|
||||
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void {
|
||||
if (this.resourceKey && executionContext?.projectId && getRmResourceKey(this.resourceKey).projectId !== executionContext.projectId) {
|
||||
throw new Error('Resource SQL Data Source and Execution context projects don\t match');
|
||||
}
|
||||
|
||||
if (!isObjectsEqual(toJS(this.state.executionContext), toJS(executionContext))) {
|
||||
if (!isObjectsEqual(toJS(this.state.executionContext), executionContext)) {
|
||||
const initNew =
|
||||
!isValuesEqual(executionContext?.connectionId, this.executionContext?.connectionId, undefined) ||
|
||||
!isValuesEqual(executionContext?.defaultCatalog, this.executionContext?.defaultCatalog, undefined) ||
|
||||
@@ -263,130 +252,182 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
this.state.executionContext = toJS(executionContext);
|
||||
this.state.executionContext = executionContext;
|
||||
super.setExecutionContext(executionContext);
|
||||
|
||||
if (this.sqlEditorService.autoSave) {
|
||||
if (this.isAutoSaveEnabled) {
|
||||
this.debouncedSaveProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rename(name: string | null) {
|
||||
await this.write();
|
||||
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || !this.projectId || !this.saved || !name?.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
name = this.projectInfoResource.getNameWithExtension(this.projectId, SCRIPTS_TYPE_ID, name);
|
||||
|
||||
this.lastAction = this.rename.bind(this, name);
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_renaming';
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
this.setResourceKey(await this.actions.rename(this, this.resourceKey, createPath(getPathParent(this.resourceKey), name)));
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async read() {
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || !this.isOutdated() || !this.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.read.bind(this);
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_reading';
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
this._script = await this.actions.read(this, this.resourceKey);
|
||||
|
||||
const executionContext = await this.actions.getProperties(this, this.resourceKey);
|
||||
|
||||
runInAction(() => {
|
||||
if (executionContext) {
|
||||
this.setExecutionContext(executionContext);
|
||||
} else {
|
||||
this.setExecutionContext();
|
||||
}
|
||||
});
|
||||
|
||||
this.markUpdated();
|
||||
this.loaded = true;
|
||||
super.setScript(this.script);
|
||||
this.saved = true;
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async write() {
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || this.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.write.bind(this);
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_saving';
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
await this.actions.write(this, this.resourceKey, this.script);
|
||||
this.saved = true;
|
||||
this.markUpdated();
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
async load(): Promise<void> {
|
||||
if (this.state.resourceKey && !this.resourceUseKeyId) {
|
||||
this.resourceUseKeyId = this.resourceManagerResource.use(this.state.resourceKey);
|
||||
}
|
||||
await this.read();
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await this.write();
|
||||
await this.saveProperties();
|
||||
try {
|
||||
await this.write();
|
||||
await this.saveProperties();
|
||||
super.save();
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
}
|
||||
}
|
||||
|
||||
private async saveProperties() {
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.resourceManagerResource.onItemUpdate.removeHandler(this.syncResource);
|
||||
if (this.state.resourceKey && this.resourceUseKeyId) {
|
||||
this.resourceManagerResource.free(this.state.resourceKey, this.resourceUseKeyId);
|
||||
this.resourceUseKeyId = null;
|
||||
}
|
||||
}
|
||||
|
||||
bindState(state: IResourceSqlDataSourceState): void {
|
||||
this.state = state;
|
||||
this.outdated = true;
|
||||
this.history.restore(state.history);
|
||||
}
|
||||
|
||||
private syncResource(key: ResourceKey<string>) {
|
||||
if (isResourceAlias(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceKey = this.resourceKey;
|
||||
if (resourceKey && ResourceKeyUtils.some(key, key => resourceKey.startsWith(key))) {
|
||||
this.markOutdated();
|
||||
}
|
||||
}
|
||||
|
||||
private async rename(name: string) {
|
||||
await this.save();
|
||||
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.rename.bind(this, name);
|
||||
|
||||
try {
|
||||
if (!this.isSaved) {
|
||||
throw new Error('Please save changes before renaming');
|
||||
}
|
||||
this.exception = null;
|
||||
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_renaming';
|
||||
this.setResourceKey(await this.actions.rename(this, this.resourceKey, name));
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
throw exception;
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async read() {
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || !this.isOutdated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.read.bind(this);
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
await this.readData();
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async saveProperties() {
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || this.isExecutionContextSaved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.saveProperties.bind(this);
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_updating';
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
|
||||
if (!this.isReadonly()) {
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_updating';
|
||||
const executionContext = await this.actions.setProperties(this, this.resourceKey, this.executionContext);
|
||||
|
||||
if (executionContext) {
|
||||
this.setExecutionContext(executionContext);
|
||||
} else {
|
||||
this.setExecutionContext();
|
||||
}
|
||||
this.setExecutionContext(executionContext);
|
||||
this.setBaseExecutionContext(this.executionContext);
|
||||
}
|
||||
} catch (exception: any) {
|
||||
this.exception = exception;
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async write() {
|
||||
await this.scheduler.schedule(undefined, async () => {
|
||||
if (!this.actions || !this.resourceKey || this.isScriptSaved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastAction = this.write.bind(this);
|
||||
|
||||
try {
|
||||
this.exception = null;
|
||||
await this.readData();
|
||||
|
||||
if (!this.isIncomingChanges) {
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_saving';
|
||||
await this.actions.write(this, this.resourceKey, this.script);
|
||||
this.setBaseScript(this.script);
|
||||
}
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async readData() {
|
||||
try {
|
||||
if (!this.actions || !this.resourceKey) {
|
||||
return;
|
||||
}
|
||||
this.message = 'plugin_sql_editor_navigation_tab_script_state_reading';
|
||||
const script = await this.actions.read(this, this.resourceKey);
|
||||
const executionContext = await this.actions.getProperties(this, this.resourceKey);
|
||||
|
||||
runInAction(() => {
|
||||
if (!this.loaded) {
|
||||
if (this.baseScript !== script) {
|
||||
this.setScript(script);
|
||||
}
|
||||
if (!isObjectsEqual(toJS(this.baseExecutionContext), executionContext)) {
|
||||
this.setExecutionContext(executionContext);
|
||||
}
|
||||
|
||||
this.setBaseScript(script);
|
||||
this.setBaseExecutionContext(executionContext);
|
||||
} else {
|
||||
this.setIncomingExecutionContext(executionContext);
|
||||
this.setIncomingScript(script);
|
||||
}
|
||||
|
||||
this.markUpdated();
|
||||
this.loaded = true;
|
||||
});
|
||||
} finally {
|
||||
this.message = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private debouncedWrite() {
|
||||
this.write();
|
||||
}
|
||||
@@ -394,4 +435,12 @@ export class ResourceSqlDataSource extends BaseSqlDataSource {
|
||||
private debouncedSaveProperties() {
|
||||
this.saveProperties();
|
||||
}
|
||||
|
||||
protected setBaseScript(script: string): void {
|
||||
this.state.baseScript = script;
|
||||
}
|
||||
|
||||
protected setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
|
||||
this.state.baseExecutionContext = toJS(executionContext);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-24
@@ -16,12 +16,12 @@ import { getRmResourceKey, IResourceManagerMoveData, ResourceManagerResource } f
|
||||
import { NetworkStateService, WindowEventsService } from '@cloudbeaver/core-root';
|
||||
import { CachedMapAllKey, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
|
||||
import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
|
||||
import { throttle } from '@cloudbeaver/core-utils';
|
||||
import { createPath, getPathParent, throttle } from '@cloudbeaver/core-utils';
|
||||
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
|
||||
import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm';
|
||||
import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
|
||||
import { ResourceManagerScriptsService } from '@cloudbeaver/plugin-resource-manager-scripts';
|
||||
import { getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
|
||||
import { ResourceManagerScriptsService, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts';
|
||||
import { createSqlDataSourceHistoryInitialState, getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor';
|
||||
import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
|
||||
|
||||
import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState';
|
||||
@@ -70,17 +70,18 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
for (const [key, value] of Array.from(map.entries())) {
|
||||
if (
|
||||
!['string', 'undefined'].includes(typeof value.resourceKey) ||
|
||||
!['undefined', 'object'].includes(typeof value.executionContext) ||
|
||||
!['string', 'undefined'].includes(typeof value.executionContext?.connectionId) ||
|
||||
!['string', 'undefined'].includes(typeof value.executionContext?.id) ||
|
||||
!['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultCatalog) ||
|
||||
!['string', 'undefined', 'object'].includes(typeof value.executionContext?.defaultSchema)
|
||||
!['string'].includes(typeof value.script) ||
|
||||
!['string', 'undefined'].includes(typeof value.baseScript) ||
|
||||
isExecutionContextInfoValid(value.executionContext) ||
|
||||
isExecutionContextInfoValid(value.baseExecutionContext)
|
||||
) {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
this.bindState.bind(this),
|
||||
'indexed',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,20 +98,16 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
this.connectionInfoResource,
|
||||
this.resourceManagerResource,
|
||||
this.sqlEditorService,
|
||||
this.createState(editorId),
|
||||
this.createState(editorId, options?.script),
|
||||
);
|
||||
|
||||
if (options?.executionContext) {
|
||||
dataSource.setExecutionContext(options.executionContext);
|
||||
}
|
||||
|
||||
if (options?.script) {
|
||||
dataSource.setScript(options.script);
|
||||
}
|
||||
|
||||
if (options?.name) {
|
||||
dataSource.setName(options.name);
|
||||
}
|
||||
// if (options?.name) {
|
||||
// dataSource.setName(options.name);
|
||||
// }
|
||||
|
||||
dataSource.setActions({
|
||||
rename: this.rename.bind(this),
|
||||
@@ -138,15 +135,11 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
},
|
||||
onDestroy: (_, editorId) => this.deleteState(editorId),
|
||||
onUnload: async dataSource => {
|
||||
if (dataSource instanceof ResourceSqlDataSource) {
|
||||
await dataSource.write();
|
||||
}
|
||||
await dataSource.save();
|
||||
},
|
||||
canDestroy: async (dataSource, editorId) => {
|
||||
try {
|
||||
if (dataSource instanceof ResourceSqlDataSource) {
|
||||
await dataSource.write();
|
||||
}
|
||||
await dataSource.save();
|
||||
} catch {
|
||||
const tab = this.sqlEditorTabService.sqlEditorTabs.find(tab => tab.handlerState.editorId === editorId);
|
||||
let name: string | undefined = undefined;
|
||||
@@ -173,12 +166,15 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
|
||||
load(): void | Promise<void> {}
|
||||
|
||||
private createState(editorId: string, resourceKey?: string): IResourceSqlDataSourceState {
|
||||
private createState(editorId: string, script?: string, resourceKey?: string): IResourceSqlDataSourceState {
|
||||
let state = this.dataSourceStateState.get(editorId);
|
||||
|
||||
if (!state) {
|
||||
state = observable<IResourceSqlDataSourceState>({
|
||||
script: script ?? '',
|
||||
baseScript: script ?? '',
|
||||
resourceKey,
|
||||
history: createSqlDataSourceHistoryInitialState(script),
|
||||
});
|
||||
|
||||
this.dataSourceStateState.set(editorId, state);
|
||||
@@ -250,12 +246,18 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
this.navigationTabsService.closeTabSilent(resourceKeyList(tabs), true);
|
||||
}
|
||||
|
||||
private async rename(dataSource: ResourceSqlDataSource, resourceKey: string, newResourceKey: string): Promise<string> {
|
||||
private async rename(dataSource: ResourceSqlDataSource, resourceKey: string, name: string): Promise<string> {
|
||||
if (!dataSource.projectId) {
|
||||
throw new Error('Project ID is not defined');
|
||||
}
|
||||
if (!this.resourceManagerService.enabled) {
|
||||
throw new Error('Resource Manager disabled');
|
||||
}
|
||||
|
||||
try {
|
||||
name = this.projectInfoResource.getNameWithExtension(dataSource.projectId, SCRIPTS_TYPE_ID, name);
|
||||
const newResourceKey = createPath(getPathParent(resourceKey), name);
|
||||
|
||||
await this.navResourceNodeService.move(resourceKey, newResourceKey);
|
||||
return newResourceKey;
|
||||
} catch (exception) {
|
||||
@@ -308,4 +310,22 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private bindState() {
|
||||
for (const [editorId, datasource] of this.sqlDataSourceService.dataSources) {
|
||||
if (datasource instanceof ResourceSqlDataSource) {
|
||||
datasource.bindState(this.createState(editorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isExecutionContextInfoValid(executionContext: IConnectionExecutionContextInfo | undefined) {
|
||||
return (
|
||||
!['undefined', 'object'].includes(typeof executionContext) ||
|
||||
!['string', 'undefined'].includes(typeof executionContext?.connectionId) ||
|
||||
!['string', 'undefined'].includes(typeof executionContext?.id) ||
|
||||
!['string', 'undefined', 'object'].includes(typeof executionContext?.defaultCatalog) ||
|
||||
!['string', 'undefined', 'object'].includes(typeof executionContext?.defaultSchema)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.unsavedMark {
|
||||
background-color: var(--theme-primary);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
margin-right: 4px;
|
||||
margin-left: -4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { s, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui';
|
||||
@@ -18,6 +18,7 @@ import type { TabHandlerTabComponent } from '@cloudbeaver/plugin-navigation-tabs
|
||||
import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, ISqlEditorTabState, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB';
|
||||
import sqlEditorTabStyles from './SqlEditorTab.m.css';
|
||||
|
||||
export const SqlEditorTab: TabHandlerTabComponent<ISqlEditorTabState> = observer(function SqlEditorTab({ tab, onSelect, onClose, style }) {
|
||||
const viewContext = useContext(CaptureViewContext);
|
||||
@@ -41,6 +42,7 @@ export const SqlEditorTab: TabHandlerTabComponent<ISqlEditorTabState> = observer
|
||||
|
||||
const name = getSqlEditorName(tab.handlerState, dataSource, connection);
|
||||
const icon = dataSource?.icon ?? '/icons/sql_script_m.svg';
|
||||
const saved = dataSource?.isSaved !== false;
|
||||
|
||||
const handleSelect = ({ tabId }: ITabData<any>) => onSelect(tabId);
|
||||
const handleClose = onClose ? ({ tabId }: ITabData<any>) => onClose(tabId) : undefined;
|
||||
@@ -49,6 +51,7 @@ export const SqlEditorTab: TabHandlerTabComponent<ISqlEditorTabState> = observer
|
||||
<Tab tabId={tab.id} style={style} title={name} menuContext={tabMenuContext} onOpen={handleSelect} onClose={handleClose}>
|
||||
<TabIcon icon={icon} />
|
||||
<TabTitle>{name}</TabTitle>
|
||||
{!saved && <unsaved-mark className={s(sqlEditorTabStyles, { unsavedMark: true })} />}
|
||||
</Tab>,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -457,7 +457,7 @@ export class SqlEditorTabService extends Bootstrap {
|
||||
|
||||
const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId);
|
||||
|
||||
if (dataSource?.isSaved() === false) {
|
||||
if (dataSource?.isSaved === false) {
|
||||
const result = await this.commonDialogService.open(ConfirmationDialog, {
|
||||
title: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_title',
|
||||
subTitle: dataSource.name ?? undefined,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2023 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
|
||||
@injectable()
|
||||
export class LocaleService extends Bootstrap {
|
||||
constructor(private readonly localizationService: LocalizationService) {
|
||||
super();
|
||||
}
|
||||
|
||||
register(): void | Promise<void> {
|
||||
this.localizationService.addProvider(this.provider.bind(this));
|
||||
}
|
||||
|
||||
load(): void | Promise<void> {}
|
||||
|
||||
private async provider(locale: string) {
|
||||
switch (locale) {
|
||||
case 'ru':
|
||||
return (await import('./locales/ru')).default;
|
||||
case 'it':
|
||||
return (await import('./locales/it')).default;
|
||||
case 'zh':
|
||||
return (await import('./locales/zh')).default;
|
||||
default:
|
||||
return (await import('./locales/en')).default;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.reactCodemirrorPanel {
|
||||
composes: theme-typography--caption from global;
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
}
|
||||
.box {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
+38
-16
@@ -7,14 +7,13 @@
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useState } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { useCombinedRef, useExecutor, useStyles } from '@cloudbeaver/core-blocks';
|
||||
import { MenuBarSmallItem, useExecutor, useS, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { DATA_CONTEXT_NAV_NODE, getNodesFromContext, NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
|
||||
import { TabContainerPanelComponent, useDNDBox } from '@cloudbeaver/core-ui';
|
||||
import { closeCompletion, IEditorRef } from '@cloudbeaver/plugin-codemirror6';
|
||||
import { closeCompletion, IEditorRef, ReactCodemirrorPanel, useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { ISqlEditorModeProps } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
import { ACTIVE_QUERY_EXTENSION } from '../ACTIVE_QUERY_EXTENSION';
|
||||
@@ -23,27 +22,26 @@ import { SQLCodeEditorLoader } from '../SQLCodeEditor/SQLCodeEditorLoader';
|
||||
import { useSQLCodeEditor } from '../SQLCodeEditor/useSQLCodeEditor';
|
||||
import { useSqlDialectAutocompletion } from '../useSqlDialectAutocompletion';
|
||||
import { useSqlDialectExtension } from '../useSqlDialectExtension';
|
||||
import style from './SQLCodeEditorPanel.m.css';
|
||||
import { useSQLCodeEditorPanel } from './useSQLCodeEditorPanel';
|
||||
|
||||
const styles = css`
|
||||
box {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps> = observer(function SQLCodeEditorPanel({ data }) {
|
||||
const notificationService = useService(NotificationService);
|
||||
const navNodeManagerService = useService(NavNodeManagerService);
|
||||
const translate = useTranslate();
|
||||
|
||||
const styles = useS(style);
|
||||
const [editorRef, setEditorRef] = useState<IEditorRef | null>(null);
|
||||
|
||||
const editor = useSQLCodeEditor(editorRef);
|
||||
const panel = useSQLCodeEditorPanel(data, editor);
|
||||
const extensions = useCodemirrorExtensions(undefined, [ACTIVE_QUERY_EXTENSION, QUERY_STATUS_GUTTER_EXTENSION]);
|
||||
const autocompletion = useSqlDialectAutocompletion(data);
|
||||
const sqlDialect = useSqlDialectExtension(data.dialect);
|
||||
|
||||
extensions.set(...autocompletion);
|
||||
extensions.set(...sqlDialect);
|
||||
|
||||
const dndBox = useDNDBox({
|
||||
canDrop: context => context.has(DATA_CONTEXT_NAV_NODE),
|
||||
onDrop: async (context, mouse) => {
|
||||
@@ -92,18 +90,42 @@ export const SQLCodeEditorPanel: TabContainerPanelComponent<ISqlEditorModeProps>
|
||||
],
|
||||
});
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<box ref={dndBox.setRef}>
|
||||
function applyIncoming() {
|
||||
data.dataSource?.applyIncoming();
|
||||
}
|
||||
|
||||
function keepCurrent() {
|
||||
data.dataSource?.keepCurrent();
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={dndBox.setRef} className={styles.box}>
|
||||
<SQLCodeEditorLoader
|
||||
ref={setEditorRef}
|
||||
getValue={() => data.value}
|
||||
extensions={[ACTIVE_QUERY_EXTENSION, QUERY_STATUS_GUTTER_EXTENSION, autocompletion, sqlDialect]}
|
||||
incomingValue={data.incomingValue}
|
||||
extensions={extensions}
|
||||
readonly={data.readonly}
|
||||
autoFocus
|
||||
lineNumbers
|
||||
onChange={panel.onQueryChange}
|
||||
onUpdate={panel.onUpdate}
|
||||
/>
|
||||
</box>,
|
||||
>
|
||||
{data.isIncomingChanges && (
|
||||
<>
|
||||
<ReactCodemirrorPanel className={styles.reactCodemirrorPanel} top>
|
||||
<MenuBarSmallItem title={translate('plugin_sql_editor_new_merge_conflict_keep_current_tooltip')} onClick={keepCurrent}>
|
||||
{translate('plugin_sql_editor_new_merge_conflict_keep_current_label')}
|
||||
</MenuBarSmallItem>
|
||||
</ReactCodemirrorPanel>
|
||||
<ReactCodemirrorPanel className={styles.reactCodemirrorPanel} top incomingView>
|
||||
<MenuBarSmallItem title={translate('plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip')} onClick={applyIncoming}>
|
||||
{translate('plugin_sql_editor_new_merge_conflict_accept_incoming_label')}
|
||||
</MenuBarSmallItem>
|
||||
</ReactCodemirrorPanel>
|
||||
</>
|
||||
)}
|
||||
</SQLCodeEditorLoader>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { createComplexLoader, useComplexLoader, useObjectRef } from '@cloudbeave
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { LocalizationService } from '@cloudbeaver/core-localization';
|
||||
import { GlobalConstants } from '@cloudbeaver/core-utils';
|
||||
import type { Completion, CompletionConfig, CompletionContext, CompletionResult, Extension } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { Compartment, Completion, CompletionConfig, CompletionContext, CompletionResult, Extension } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { ISQLEditorData, SQLProposal } from '@cloudbeaver/plugin-sql-editor';
|
||||
|
||||
const codemirrorComplexLoader = createComplexLoader(() => import('@cloudbeaver/plugin-codemirror6'));
|
||||
@@ -23,7 +23,7 @@ type SqlCompletion = Completion & {
|
||||
const CLOSE_CHARACTERS = /[\s()[\]{};:>,=\\*]/;
|
||||
const COMPLETION_WORD = /[\w*]*/;
|
||||
|
||||
export function useSqlDialectAutocompletion(data: ISQLEditorData): Extension[] {
|
||||
export function useSqlDialectAutocompletion(data: ISQLEditorData): [Compartment, Extension] {
|
||||
const { closeCompletion, useEditorAutocompletion } = useComplexLoader(codemirrorComplexLoader);
|
||||
const localizationService = useService(LocalizationService);
|
||||
const optionsRef = useObjectRef({ data });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createComplexLoader, type IComplexLoaderData, useComplexLoader } from '
|
||||
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
|
||||
import {
|
||||
CassandraLoader,
|
||||
Compartment,
|
||||
type Extension,
|
||||
MariaSQLLoader,
|
||||
MSSQLLoader,
|
||||
@@ -24,7 +25,9 @@ import {
|
||||
|
||||
const codemirrorComplexLoader = createComplexLoader(() => import('@cloudbeaver/plugin-codemirror6'));
|
||||
|
||||
export function useSqlDialectExtension(dialectInfo: SqlDialectInfo | undefined): Extension {
|
||||
const SQL_EDITOR_COMPARTMENT = new Compartment();
|
||||
|
||||
export function useSqlDialectExtension(dialectInfo: SqlDialectInfo | undefined): [Compartment, Extension] {
|
||||
const { SQLDialect, SQL_EDITOR } = useComplexLoader(codemirrorComplexLoader);
|
||||
const loader = getDialectLoader(dialectInfo?.name);
|
||||
const dialect = useComplexLoader(loader);
|
||||
@@ -45,9 +48,12 @@ export function useSqlDialectExtension(dialectInfo: SqlDialectInfo | undefined):
|
||||
});
|
||||
}
|
||||
|
||||
return SQL_EDITOR({
|
||||
dialect: dialectInner,
|
||||
});
|
||||
return [
|
||||
SQL_EDITOR_COMPARTMENT,
|
||||
SQL_EDITOR({
|
||||
dialect: dialectInner,
|
||||
}),
|
||||
];
|
||||
}, [dialect, dialectInfo]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default [
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Keep Current'],
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Keep Current changes'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Accept Incoming'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Accept Incoming changes'],
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export default [
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Keep Current'],
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Keep Current changes'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Accept Incoming'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Accept Incoming changes'],
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export default [
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Оставить текущие'],
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Оставить текущие изменения'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Принять входящие'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Принять входящие изменения'],
|
||||
];
|
||||
@@ -0,0 +1,6 @@
|
||||
export default [
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_label', 'Keep Current'],
|
||||
['plugin_sql_editor_new_merge_conflict_keep_current_tooltip', 'Keep Current changes'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_label', 'Accept Incoming'],
|
||||
['plugin_sql_editor_new_merge_conflict_accept_incoming_tooltip', 'Accept Incoming changes'],
|
||||
];
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import type { PluginManifest } from '@cloudbeaver/core-di';
|
||||
|
||||
import { LocaleService } from './LocaleService';
|
||||
import { PluginBootstrap } from './PluginBootstrap';
|
||||
import { SQLCodeEditorPanelService } from './SQLEditor/SQLCodeEditorPanel/SQLCodeEditorPanelService';
|
||||
|
||||
@@ -15,5 +16,5 @@ export const sqlEditorNewPlugin: PluginManifest = {
|
||||
name: 'Sql Editor New Plugin',
|
||||
},
|
||||
|
||||
providers: [PluginBootstrap, SQLCodeEditorPanelService],
|
||||
providers: [PluginBootstrap, SQLCodeEditorPanelService, LocaleService],
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { makeObservable, observable } from 'mobx';
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
|
||||
import type { IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
|
||||
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import { isContainsException, staticImplements } from '@cloudbeaver/core-utils';
|
||||
import { isContainsException, isObjectsEqual, isValuesEqual, staticImplements } from '@cloudbeaver/core-utils';
|
||||
import type { IDatabaseDataModel, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import type { IDataQueryOptions } from '../QueryDataSource';
|
||||
@@ -25,10 +25,38 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
static key = 'base';
|
||||
abstract get name(): string | null;
|
||||
abstract get script(): string;
|
||||
abstract get baseScript(): string;
|
||||
abstract get baseExecutionContext(): IConnectionExecutionContextInfo | undefined;
|
||||
abstract get executionContext(): IConnectionExecutionContextInfo | undefined;
|
||||
databaseModels: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>[];
|
||||
exception?: Error | Error[] | null | undefined;
|
||||
message?: string;
|
||||
incomingScript: string | undefined;
|
||||
incomingExecutionContext: IConnectionExecutionContextInfo | undefined | null;
|
||||
|
||||
get isIncomingChanges(): boolean {
|
||||
return this.incomingScript !== undefined || this.incomingExecutionContext !== null;
|
||||
}
|
||||
|
||||
get isAutoSaveEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
get isScriptSaved(): boolean {
|
||||
return this.script === this.baseScript;
|
||||
}
|
||||
|
||||
get isExecutionContextSaved(): boolean {
|
||||
return (
|
||||
isValuesEqual(this.baseExecutionContext?.connectionId, this.executionContext?.connectionId, undefined) &&
|
||||
isValuesEqual(this.baseExecutionContext?.defaultCatalog, this.executionContext?.defaultCatalog, undefined) &&
|
||||
isValuesEqual(this.baseExecutionContext?.defaultSchema, this.executionContext?.defaultSchema, undefined)
|
||||
);
|
||||
}
|
||||
|
||||
get isSaved(): boolean {
|
||||
return this.isScriptSaved && this.isExecutionContextSaved;
|
||||
}
|
||||
|
||||
get sourceKey(): string {
|
||||
return Object.getPrototypeOf(this).constructor.key;
|
||||
@@ -48,18 +76,18 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
readonly onSetScript: ISyncExecutor<ISetScriptData>;
|
||||
readonly onDatabaseModelUpdate: ISyncExecutor<IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>[]>;
|
||||
|
||||
protected saved: boolean;
|
||||
protected outdated: boolean;
|
||||
protected editing: boolean;
|
||||
|
||||
constructor(icon = '/icons/sql_script_m.svg') {
|
||||
this.icon = icon;
|
||||
this.databaseModels = [];
|
||||
this.incomingScript = undefined;
|
||||
this.incomingExecutionContext = null;
|
||||
this.exception = undefined;
|
||||
this.message = undefined;
|
||||
this.outdated = true;
|
||||
this.editing = true;
|
||||
this.saved = true;
|
||||
this.history = new SqlDataSourceHistory();
|
||||
this.onUpdate = new SyncExecutor();
|
||||
this.onSetScript = new SyncExecutor();
|
||||
@@ -76,21 +104,51 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
|
||||
this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY));
|
||||
|
||||
makeObservable<this, 'outdated' | 'editing' | 'saved'>(this, {
|
||||
makeObservable<this, 'outdated' | 'editing'>(this, {
|
||||
isSaved: computed,
|
||||
isIncomingChanges: computed,
|
||||
isAutoSaveEnabled: computed,
|
||||
isScriptSaved: computed,
|
||||
isExecutionContextSaved: computed,
|
||||
setScript: action,
|
||||
setIncomingScript: action,
|
||||
setName: action,
|
||||
setExecutionContext: action,
|
||||
setIncomingExecutionContext: action,
|
||||
markUpdated: action,
|
||||
markOutdated: action,
|
||||
setEditing: action,
|
||||
setProject: action,
|
||||
applyIncoming: action,
|
||||
keepCurrent: action,
|
||||
reset: action,
|
||||
databaseModels: observable.ref,
|
||||
exception: observable.ref,
|
||||
outdated: observable.ref,
|
||||
message: observable.ref,
|
||||
editing: observable.ref,
|
||||
saved: observable.ref,
|
||||
incomingScript: observable.ref,
|
||||
incomingExecutionContext: observable.ref,
|
||||
});
|
||||
}
|
||||
|
||||
setScript(script: string, source?: string): void {
|
||||
this.saved = false;
|
||||
this.onSetScript.execute({ script, source });
|
||||
}
|
||||
|
||||
setIncomingScript(script: string): void {
|
||||
if (script !== this.baseScript) {
|
||||
if (this.script === this.baseScript) {
|
||||
this.setBaseScript(script);
|
||||
this.setScript(script);
|
||||
} else {
|
||||
this.incomingScript = script;
|
||||
}
|
||||
} else {
|
||||
this.incomingScript = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
abstract canRename(name: string | null): boolean;
|
||||
|
||||
setName(name: string | null): void {
|
||||
@@ -98,12 +156,26 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
}
|
||||
|
||||
setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void {
|
||||
this.saved = false;
|
||||
this.onUpdate.execute();
|
||||
}
|
||||
|
||||
isSaved(): boolean {
|
||||
return this.saved;
|
||||
setIncomingExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void {
|
||||
executionContext = toJS(executionContext);
|
||||
|
||||
this.setBaseExecutionContext(executionContext);
|
||||
this.setExecutionContext(executionContext);
|
||||
|
||||
// TODO: we need to display execution context changes
|
||||
// if (!isObjectsEqual(executionContext, toJS(this.baseExecutionContext))) {
|
||||
// if (isObjectsEqual(this.executionContext, toJS(this.baseExecutionContext))) {
|
||||
// this.setBaseExecutionContext(executionContext);
|
||||
// this.setExecutionContext(executionContext);
|
||||
// } else {
|
||||
// this.incomingExecutionContext = executionContext;
|
||||
// }
|
||||
// } else {
|
||||
// this.incomingExecutionContext = null;
|
||||
// }
|
||||
}
|
||||
|
||||
isError(): boolean {
|
||||
@@ -154,10 +226,42 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
|
||||
this.onUpdate.execute();
|
||||
}
|
||||
|
||||
save(): Promise<void> | void {}
|
||||
load(): Promise<void> | void {}
|
||||
reset(): Promise<void> | void {
|
||||
this.saved = true;
|
||||
keepCurrent(): void {
|
||||
if (this.incomingScript !== undefined) {
|
||||
this.setBaseScript(this.incomingScript);
|
||||
this.incomingScript = undefined;
|
||||
}
|
||||
if (this.incomingExecutionContext !== null) {
|
||||
this.setBaseExecutionContext(this.incomingExecutionContext);
|
||||
this.incomingExecutionContext = null;
|
||||
}
|
||||
}
|
||||
|
||||
applyIncoming(): void {
|
||||
if (this.incomingScript !== undefined) {
|
||||
this.setBaseScript(this.incomingScript);
|
||||
}
|
||||
if (this.incomingExecutionContext !== null) {
|
||||
this.setBaseExecutionContext(this.incomingExecutionContext);
|
||||
}
|
||||
this.reset();
|
||||
}
|
||||
|
||||
save(): Promise<void> | void {
|
||||
this.markUpdated();
|
||||
}
|
||||
|
||||
load(): Promise<void> | void {}
|
||||
|
||||
reset(): Promise<void> | void {
|
||||
this.setScript(this.baseScript);
|
||||
this.setExecutionContext(this.baseExecutionContext);
|
||||
this.incomingScript = undefined;
|
||||
this.incomingExecutionContext = null;
|
||||
}
|
||||
|
||||
dispose(): void | Promise<void> {}
|
||||
|
||||
protected abstract setBaseScript(script: string): void;
|
||||
protected abstract setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface ISqlDataSource extends ILoadableState {
|
||||
readonly icon?: string;
|
||||
readonly emptyPlaceholder?: string;
|
||||
readonly script: string;
|
||||
readonly incomingScript?: string;
|
||||
readonly projectId: string | null;
|
||||
readonly databaseModels: IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>[];
|
||||
readonly executionContext?: IConnectionExecutionContextInfo;
|
||||
@@ -38,8 +39,12 @@ export interface ISqlDataSource extends ILoadableState {
|
||||
readonly onDatabaseModelUpdate: ISyncExecutor<IDatabaseDataModel<IDataQueryOptions, IDatabaseResultSet>[]>;
|
||||
readonly features: ESqlDataSourceFeatures[];
|
||||
readonly history: ISqlDataSourceHistory;
|
||||
readonly isAutoSaveEnabled: boolean;
|
||||
readonly isIncomingChanges: boolean;
|
||||
readonly isSaved: boolean;
|
||||
readonly isScriptSaved: boolean;
|
||||
readonly isExecutionContextSaved: boolean;
|
||||
|
||||
isSaved(): boolean;
|
||||
isReadonly(): boolean;
|
||||
isEditing(): boolean;
|
||||
isOutdated(): boolean;
|
||||
@@ -52,6 +57,10 @@ export interface ISqlDataSource extends ILoadableState {
|
||||
setScript(script: string, source?: string): void;
|
||||
setEditing(state: boolean): void;
|
||||
setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
|
||||
setIncomingExecutionContext(executionContext?: IConnectionExecutionContextInfo): void;
|
||||
setIncomingScript(script?: string): void;
|
||||
applyIncoming(): void;
|
||||
keepCurrent(): void;
|
||||
save(): Promise<void> | void;
|
||||
load(): Promise<void> | void;
|
||||
reset(): Promise<void> | void;
|
||||
|
||||
+20
-4
@@ -14,6 +14,14 @@ import { ESqlDataSourceFeatures } from '../ESqlDataSourceFeatures';
|
||||
import type { ILocalStorageSqlDataSourceState } from './ILocalStorageSqlDataSourceState';
|
||||
|
||||
export class LocalStorageSqlDataSource extends BaseSqlDataSource {
|
||||
get baseScript(): string {
|
||||
return this.state.script;
|
||||
}
|
||||
|
||||
get baseExecutionContext(): IConnectionExecutionContextInfo | undefined {
|
||||
return this.state.executionContext;
|
||||
}
|
||||
|
||||
static key = 'local-storage';
|
||||
|
||||
get name(): string | null {
|
||||
@@ -32,6 +40,10 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
|
||||
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName];
|
||||
}
|
||||
|
||||
get isSaved(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private state!: ILocalStorageSqlDataSourceState;
|
||||
|
||||
constructor(state: ILocalStorageSqlDataSourceState) {
|
||||
@@ -45,10 +57,6 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
isSaved(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -77,4 +85,12 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource {
|
||||
this.outdated = false;
|
||||
this.history.restore(state.history);
|
||||
}
|
||||
|
||||
protected setBaseScript(script: string): void {
|
||||
this.state.script = script;
|
||||
}
|
||||
|
||||
protected setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
|
||||
this.state.executionContext = executionContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ import { BaseSqlDataSource } from './BaseSqlDataSource';
|
||||
import { ESqlDataSourceFeatures } from './ESqlDataSourceFeatures';
|
||||
|
||||
export class MemorySqlDataSource extends BaseSqlDataSource {
|
||||
get baseScript(): string {
|
||||
return this._script;
|
||||
}
|
||||
get baseExecutionContext(): IConnectionExecutionContextInfo | undefined {
|
||||
return this._executionContext;
|
||||
}
|
||||
static key = 'memory';
|
||||
|
||||
get name(): string | null {
|
||||
@@ -31,6 +37,10 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
|
||||
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable, ESqlDataSourceFeatures.setName];
|
||||
}
|
||||
|
||||
get isSaved(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private _name: string | null;
|
||||
private _script: string;
|
||||
private _executionContext?: IConnectionExecutionContextInfo;
|
||||
@@ -50,10 +60,6 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
isSaved(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
isReadonly(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -76,4 +82,12 @@ export class MemorySqlDataSource extends BaseSqlDataSource {
|
||||
canRename(name: string | null): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected setBaseScript(script: string): void {
|
||||
this._script = script;
|
||||
}
|
||||
|
||||
protected setBaseExecutionContext(executionContext: IConnectionExecutionContextInfo | undefined): void {
|
||||
this._executionContext = executionContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ export interface ISQLEditorData {
|
||||
readonly isLineScriptEmpty: boolean;
|
||||
readonly isScriptEmpty: boolean;
|
||||
readonly isDisabled: boolean;
|
||||
readonly isIncomingChanges: boolean;
|
||||
readonly value: string;
|
||||
readonly incomingValue?: string;
|
||||
readonly dataSource: ISqlDataSource | undefined;
|
||||
readonly onExecute: ISyncExecutor<boolean>;
|
||||
readonly onSegmentExecute: ISyncExecutor<ISegmentExecutionData>;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.sqlActions .menuBarItem {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.sqlActions .menuBarItemLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { SyncExecutor } from '@cloudbeaver/core-executor';
|
||||
import type { SqlCompletionProposal, SqlDialectInfo, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk';
|
||||
import { createLastPromiseGetter, isObjectsEqual, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils';
|
||||
import { createLastPromiseGetter, LastPromiseGetter, throttleAsync } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { ISqlEditorTabState } from '../ISqlEditorTabState';
|
||||
import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures';
|
||||
@@ -122,10 +122,18 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
|
||||
return context?.executing || false;
|
||||
},
|
||||
|
||||
get isIncomingChanges(): boolean {
|
||||
return this.dataSource?.isIncomingChanges ?? false;
|
||||
},
|
||||
|
||||
get value(): string {
|
||||
return this.dataSource?.script ?? '';
|
||||
},
|
||||
|
||||
get incomingValue(): string | undefined {
|
||||
return this.dataSource?.incomingScript;
|
||||
},
|
||||
|
||||
onMode: new SyncExecutor(),
|
||||
onExecute: new SyncExecutor(),
|
||||
onSegmentExecute: new SyncExecutor(),
|
||||
|
||||
@@ -31,5 +31,5 @@ interface Props {
|
||||
}
|
||||
|
||||
export const SqlEditorStatusBar = observer<Props>(function SqlEditorStatusBar({ dataSource }) {
|
||||
return styled(viewerStyles)(<Loader state={dataSource} message={dataSource?.message} small inline inlineException />);
|
||||
return styled(viewerStyles)(<Loader state={dataSource} message={dataSource?.message} inline inlineException />);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ export * from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN';
|
||||
export * from './SqlDataSource/LocalStorage/ILocalStorageSqlDataSourceState';
|
||||
export * from './SqlDataSource/LocalStorage/LocalStorageSqlDataSource';
|
||||
export * from './SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap';
|
||||
export * from './SqlDataSource/SqlDataSourceHistory/ISqlDataSourceHistoryState';
|
||||
export * from './SqlDataSource/SqlDataSourceHistory/createSqlDataSourceHistoryInitialState';
|
||||
export * from './SqlDataSource/BaseSqlDataSource';
|
||||
export * from './SqlDataSource/ESqlDataSourceFeatures';
|
||||
export * from './SqlDataSource/ISqlDataSource';
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Button, useClipboard, useResource, useTranslate } from '@cloudbeaver/co
|
||||
import { ConnectionDialectResource, ConnectionExecutionContextService, createConnectionParam } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs';
|
||||
import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer';
|
||||
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
|
||||
|
||||
@@ -53,6 +54,8 @@ export const ScriptPreviewDialog = observer<DialogComponentProps<Payload>>(funct
|
||||
contextInfo ? createConnectionParam(contextInfo.projectId, contextInfo.connectionId) : null,
|
||||
);
|
||||
const sqlDialect = useSqlDialectExtension(dialect.data);
|
||||
const extensions = useCodemirrorExtensions();
|
||||
extensions.set(...sqlDialect);
|
||||
|
||||
const apply = async () => {
|
||||
await payload.model.save();
|
||||
@@ -64,7 +67,7 @@ export const ScriptPreviewDialog = observer<DialogComponentProps<Payload>>(funct
|
||||
<CommonDialogHeader title="data_viewer_script_preview_dialog_title" icon="sql-script" onReject={rejectDialog} />
|
||||
<CommonDialogBody noBodyPadding noOverflow>
|
||||
<wrapper>
|
||||
<SQLCodeEditorLoader value={payload.script} extensions={[sqlDialect]} lineNumbers readonly />
|
||||
<SQLCodeEditorLoader value={payload.script} extensions={extensions} lineNumbers readonly />
|
||||
</wrapper>
|
||||
</CommonDialogBody>
|
||||
<CommonDialogFooter>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs';
|
||||
import { GQLErrorCatcher, SqlDialectInfo } from '@cloudbeaver/core-sdk';
|
||||
import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6';
|
||||
import { SqlDialectInfoService } from '@cloudbeaver/plugin-sql-editor';
|
||||
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
|
||||
|
||||
@@ -99,6 +100,8 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
|
||||
);
|
||||
|
||||
const sqlDialect = useSqlDialectExtension(state.dialect);
|
||||
const extensions = useCodemirrorExtensions();
|
||||
extensions.set(...sqlDialect);
|
||||
const error = useErrorDetails(state.error.exception);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,7 +125,7 @@ export const GeneratedSqlDialog = observer<DialogComponentProps<Payload>>(functi
|
||||
<CommonDialogBody noOverflow noBodyPadding>
|
||||
<wrapper>
|
||||
<Loader loading={state.loading}>
|
||||
{() => styled(styles)(<SQLCodeEditorLoader value={state.query} extensions={[sqlDialect]} readonly />)}
|
||||
{() => styled(styles)(<SQLCodeEditorLoader value={state.query} extensions={extensions} readonly />)}
|
||||
</Loader>
|
||||
</wrapper>
|
||||
</CommonDialogBody>
|
||||
|
||||
+2415
-2887
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user