CB-3372 feat: codemirror6 autocomplete (#1646)

* CB-3372 feat: codemirror6 autocomplete

* CB-3372 feat: autocomplete icons

* CB-3371 feat: sql editor syntax highlight

* CB-3371 fix: syntax highlight blinking

* CB-3371 fix: editor extensions state

* CB-3372 chore: code cleanup

* CB-3372 fix: remove useSqlDIalectAutocompletion

* CB-3372 fix: add useSqlDIalectAutocompletion

* CB-3372 feat: experimental context menu

* CB-3372 feat: use new sql editor

* CB-3372 chore: remove sql script parser

* CB-3372 fix: rounded menu

* CB-3372 fix: event preventDefault on dialog

* CB-3372 fix: context menu styles

* CB-3372 feat: remove old codemirror

* CB-3372 fix: numbers completion & extensions caching

* CB-3372 fix: table display

* CB-3372 fix: decrease sql editor font size

* CB-3372 fix: copy in readonly mode & autocompletion in safari on MacOS

* CB-3372 fix: value panel styles

* CB-3372 fix: menu bar styles

* CB-3372 fix: menu bar and inline editor styles

* CB-3372 fix: menu and inline editor styles

* CB-3372 chore: update bundle

---------

Co-authored-by: dariamarutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Alexey
2023-05-17 17:32:09 +03:00
committed by GitHub
co-authored by dariamarutkina
parent a219593e1d
commit 64040bc3a3
151 changed files with 2224 additions and 2303 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -8,7 +8,7 @@
import { LoadingError } from '@cloudbeaver/core-utils';
interface IComplexLoaderData<T> {
export interface IComplexLoaderData<T> {
promise: Promise<T> | undefined;
data: T | undefined;
error: Error | undefined;
@@ -21,6 +21,16 @@ export interface ComplexLoaderProps<T> {
children: (content: T) => JSX.Element;
}
export function useComplexLoader<T>(loader: IComplexLoaderData<T>): T {
if (loader.error) {
throw loader.error;
}
if (loader.data) {
return loader.data;
}
throw loader.loader();
}
export const ComplexLoader: React.FC<ComplexLoaderProps<any>> = function ComplexLoader(props) {
if (props.loader.error) {
throw props.loader.error;
@@ -12,6 +12,7 @@ import type { IExecutor } from '@cloudbeaver/core-executor';
export type FormChangeValues = string | number | boolean | FileList | null | undefined;
export type FormChangeHandler = (value: FormChangeValues, name: string | undefined) => void;
type KeyHandler = (event: React.KeyboardEvent<HTMLInputElement>) => void;
export interface IChangeData {
value: FormChangeValues;
@@ -21,6 +22,7 @@ export interface IChangeData {
export interface IFormContext {
changeExecutor: IExecutor<IChangeData>;
change: FormChangeHandler;
keyDown: KeyHandler;
}
export const FormContext = createContext<IFormContext | null>(null);
@@ -161,7 +161,7 @@ export const InputField: InputFieldType = observer(forwardRef(function InputFiel
}, [state, name, context, onChange]);
const handleBlur = useCombinedHandler(rest.onBlur, capsLock.handleBlur);
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown);
const handleKeyDown = useCombinedHandler(rest.onKeyDown, capsLock.handleKeyDown, context?.keyDown);
if (autoHide && !isControlPresented(name, state, defaultValue)) {
return null;
@@ -11,7 +11,7 @@ import { createContext } from 'react';
export interface IRadioGroupContext {
value: string | number | undefined;
name: string;
onChange: (value: string| number) => any;
onChange: (value: string | number) => any;
}
export const RadioGroupContext = createContext<IRadioGroupContext | undefined>(undefined);
@@ -10,6 +10,7 @@ import React, { forwardRef, useContext, useState } from 'react';
import { Executor } from '@cloudbeaver/core-executor';
import { useCombinedRef } from '../useCombinedRef';
import { useObjectRef } from '../useObjectRef';
import { FormChangeHandler, FormContext, IChangeData, IFormContext } from './FormContext';
@@ -23,7 +24,7 @@ export interface IFormStateInfo {
type FormDetailedProps = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onChange' | 'onSubmit'> & {
disabled?: boolean;
disableEnterSubmit?: boolean;
onSubmit?: (event: React.FormEvent<HTMLFormElement>) => Promise<void> | void;
onSubmit?: (event?: React.FormEvent<HTMLFormElement>) => Promise<void> | void;
onChange?: FormChangeHandler;
};
@@ -38,6 +39,8 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
},
ref
) {
const [formRef, setFormInnerRef] = useState<HTMLFormElement | null>(null);
const setFormRef = useCombinedRef<HTMLFormElement>(setFormInnerRef, ref);
let [disabled, setDisabled] = useState(false);
disabled = disabled || disabledProp || false;
@@ -56,6 +59,8 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
}
},
}), {
formRef,
disableEnterSubmit,
parentContext: useContext(FormContext),
onChange,
onSubmit,
@@ -68,10 +73,25 @@ export const SubmittingForm = forwardRef<HTMLFormElement, FormDetailedProps>(fun
props.parentContext?.change(value, name);
this.changeExecutor.execute({ value, name });
},
}), false, ['change']);
keyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
const form = event.currentTarget.closest('form');
if (form) {
event.preventDefault();
const submitButton = form.querySelector<HTMLButtonElement>(
'button[type=submit]'
);
if (submitButton) {
submitButton.click();
}
}
}
props.parentContext?.keyDown(event);
},
}), false, ['change', 'keyDown']);
return (
<form {...rest} ref={ref} onSubmit={e => props.handleSubmit(e)}>
<form {...rest} ref={setFormRef} onSubmit={e => props.handleSubmit(e)}>
<fieldset disabled={disabled} className={rest.className}>
<FormContext.Provider value={context}>
{children}
@@ -1,6 +1,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta content="text/html; charset=utf-8" />
<link rel="manifest" href="{ROOT_URI}manifest.webmanifest">
<link rel="alternate icon" type="image/png" href="{ROOT_URI}favicon.png" sizes="16x16">
<link rel="icon" type="image/png" sizes="32x32" href="{ROOT_URI}favicon-32x32.png">
@@ -23,6 +23,7 @@ export const loaderStyles = css`
}
& StaticImage {
height: 100%;
width: 100%;
display: none;
}
@@ -78,23 +79,6 @@ export const loaderStyles = css`
padding-top: 42px;
}
loader[|small] {
& icon {
width: 16px;
height: 16px;
}
& message {
display: none;
}
}
loader[|fullSize] {
& icon {
width: 100%;
height: 100%;
}
}
loader[|inline] {
height: 38px;
flex-direction: row;
@@ -114,6 +98,25 @@ export const loaderStyles = css`
}
}
loader[|small] {
& icon {
width: 16px;
height: 16px;
}
& message {
display: none;
}
}
loader[|fullSize] {
height: 100%;
& icon {
width: 100%;
height: 100%;
}
}
@keyframes rotation {
from {
transform: rotate(0deg);
+88 -8
View File
@@ -7,20 +7,23 @@
*/
import { observer } from 'mobx-react-lite';
import React, { forwardRef, useEffect } from 'react';
import React, { forwardRef, useLayoutEffect, useRef, useState } from 'react';
import { MenuButton, MenuInitialState, useMenuState } from 'reakit/Menu';
import styled from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { ErrorBoundary } from '../ErrorBoundary';
import { useCombinedRef } from '../useCombinedRef';
import { useObjectRef } from '../useObjectRef';
import { useStyles } from '../useStyles';
import { MenuPanel } from './MenuPanel';
import { menuPanelStyles } from './menuPanelStyles';
import { IMenuState, MenuStateContext } from './MenuStateContext';
import type { IMouseContextMenu } from './useMouseContextMenu';
interface IMenuProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
mouseContextMenu?: IMouseContextMenu;
label: string;
items: React.ReactNode | (() => React.ReactNode);
menuRef?: React.RefObject<IMenuState | undefined>;
@@ -38,6 +41,7 @@ interface IMenuProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
}
export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function Menu({
mouseContextMenu,
label,
items,
menuRef,
@@ -55,8 +59,18 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
rtl,
...props
}, ref) {
const innerMenuButtonRef = useRef<HTMLButtonElement>(null);
const combinedRef = useCombinedRef(ref, innerMenuButtonRef);
const [relativePosition, setRelativePosition] = useState<{ x: number; y: number } | null>(null);
const menuButtonLinkRef = useRef<HTMLButtonElement>(null);
const menuPanelRef = useRef<HTMLDivElement>(null);
const propsRef = useObjectRef({ onVisibleSwitch, visible });
const menu = useMenuState({ modal, placement, visible, rtl });
const menu = useMenuState({
modal,
placement,
visible,
rtl,
});
const styles = useStyles(menuPanelStyles, style);
if (menuRef) {
@@ -64,25 +78,70 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
menuRef.current = menu;
}
useEffect(() => {
propsRef.onVisibleSwitch?.(menu.visible);
}, [menu.visible]);
let menuVisible = menu.visible;
if (panelAvailable === false) {
menuVisible = false;
}
useLayoutEffect(() => {
if (!menuVisible) {
setRelativePosition(null);
}
propsRef.onVisibleSwitch?.(menuVisible);
}, [menuVisible]);
useLayoutEffect(() => {
if (!mouseContextMenu?.position) {
return;
}
if (menuVisible) {
menu.hide();
return;
}
if (innerMenuButtonRef.current) {
menu.show();
const boxSize = innerMenuButtonRef.current.getBoundingClientRect();
setRelativePosition({
x: mouseContextMenu.position.x - boxSize.x,
y: mouseContextMenu.position.y - boxSize.y,
});
mouseContextMenu.position = null;
}
}, [mouseContextMenu?.position, menuVisible]);
useLayoutEffect(() => {
if (relativePosition) {
if (menuButtonLinkRef.current) {
menuButtonLinkRef.current.style.left = `${relativePosition.x}px`;
menuButtonLinkRef.current.style.top = `${relativePosition.y}px`;
}
}
});
const MenuButtonLink = MenuButton;
if (React.isValidElement(children) && disclosure) {
return styled(styles)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props} {...children.props}>
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
{...menu}
visible={menuVisible}
{...props}
{...children.props}
>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
</MenuButton>
<MenuPanel
ref={menuPanelRef}
label={label}
menu={menu}
style={style}
@@ -94,6 +153,13 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
>
{items}
</MenuPanel>
{relativePosition && (
<MenuButtonLink
ref={menuButtonLinkRef}
{...menu}
visible={menuVisible}
/>
)}
</MenuStateContext.Provider>
</ErrorBoundary>
);
@@ -102,10 +168,17 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
return styled(styles)(
<ErrorBoundary>
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} visible={menuVisible} {...props}>
<MenuButton
key={relativePosition ? 'link' : 'main'}
ref={combinedRef}
{...menu}
visible={menuVisible}
{...props}
>
<box>{children}</box>
</MenuButton>
<MenuPanel
ref={menuPanelRef}
label={label}
menu={menu}
style={style}
@@ -117,6 +190,13 @@ export const Menu = observer<IMenuProps, HTMLButtonElement>(forwardRef(function
>
{items}
</MenuPanel>
{relativePosition && (
<MenuButtonLink
ref={menuButtonLinkRef}
{...menu}
visible={menuVisible}
/>
)}
</MenuStateContext.Provider>
</ErrorBoundary>
);
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { Children } from 'react';
import { Children, forwardRef } from 'react';
import { Menu, MenuStateReturn } from 'reakit/Menu';
import styled, { use } from 'reshadow';
@@ -33,7 +33,7 @@ export interface IMenuPanelProps {
className?: string;
}
export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
export const MenuPanel = observer<IMenuPanelProps, HTMLDivElement>(forwardRef(function MenuPanel({
label,
menu,
panelAvailable = true,
@@ -43,7 +43,7 @@ export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
children,
style,
className,
}) {
}, ref) {
const styles = useStyles(menuPanelStyles, style);
const visible = menu.visible;
@@ -63,7 +63,7 @@ export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
return styled(styles)(
<ErrorBoundary>
<Menu {...menu} aria-label={label} className={className} visible={panelAvailable}>
<Menu ref={ref} {...menu} aria-label={label} className={className} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(renderedChildren) === 0 && (
<MenuEmptyItem style={style} />
@@ -73,4 +73,4 @@ export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
</Menu>
</ErrorBoundary>
);
});
}));
@@ -35,20 +35,31 @@ export const menuPanelStyles = css`
height: inherit;
}
}
MenuButtonLink {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
box-sizing: border-box;
padding: 0;
margin: 0;
border: none;
background: transparent;
}
Menu {
outline: none;
z-index: 999;
}
MenuPanel[submenu] {
margin-top: -12px;
margin-top: -6px;
}
menu-box {
composes: theme-typography--body2 theme-elevation-z5 from global;
composes: theme-group-element-radius theme-typography--body2 theme-elevation-z5 from global;
min-width: 140px;
padding: 12px 0;
padding: 6px 0;
& menu-box {
margin-top: -12px;
margin-top: -6px;
}
}
@@ -98,7 +109,7 @@ export const menuPanelStyles = css`
flex: 1;
width: 100%;
display: flex;
margin: 4px 0;
margin: 6px 0;
border: none !important;
border-bottom: 1px solid !important;
@@ -0,0 +1,39 @@
/*
* 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 { observable } from 'mobx';
import { useObservableRef } from '../useObservableRef';
export interface IContextMenuPosition {
x: number;
y: number;
}
export interface IMouseContextMenu {
position: IContextMenuPosition | null;
handleContextMenuOpen: (event: React.MouseEvent<HTMLDivElement>) => void;
}
export function useMouseContextMenu(): IMouseContextMenu {
return useObservableRef<IMouseContextMenu>(() => ({
position: null,
handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
if (!event.currentTarget.contains(event.target as Node)) {
return;
}
event.preventDefault();
event.stopPropagation();
this.position = {
x: event.clientX,
y: event.clientY,
};
},
}), { position: observable.ref }, false, ['handleContextMenuOpen']);
}
@@ -10,8 +10,7 @@ import { action, computed, observable, reaction, toJS, untracked, comparer } fro
import { useEffect, useContext, useState } from 'react';
import { IServiceConstructor, useService } from '@cloudbeaver/core-di';
import { CachedMapResource, CachedMapResourceGetter, CachedMapResourceValue, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, CachedResourceData, CachedDataResourceGetter, CachedResource, CachedDataResource, CachedResourceKey, CachedResourceContext, ResourceKey } from '@cloudbeaver/core-sdk';
import type { ResourceKeyListAlias } from '@cloudbeaver/core-sdk/src/Resource/ResourceKeyListAlias';
import { CachedMapResource, CachedMapResourceGetter, CachedMapResourceValue, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter, CachedResourceData, CachedDataResourceGetter, CachedResource, CachedDataResource, CachedResourceKey, CachedResourceContext, ResourceKey, ResourceKeyListAlias } from '@cloudbeaver/core-sdk';
import { ILoadableState, isArraysEqual, isContainsException, LoadingError } from '@cloudbeaver/core-utils';
import { ErrorContext } from '../ErrorContext';
@@ -17,12 +17,13 @@ import { useStyles } from './useStyles';
const style = css`
timer {
position: relative;
width: 24px;
height: 24px;
width: 16px;
height: 16px;
overflow: hidden;
& > Icon {
width: 24px;
height: 24px;
width: 16px;
height: 16px;
}
& state {
@@ -39,8 +40,10 @@ const style = css`
right: 0;
top: 0;
display: flex;
width: 12px;
height: 12px;
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
}
timer:hover {
& state {
@@ -32,7 +32,7 @@ export const TREE_NODE_STYLES = css`
& TreeNodeControl {
box-sizing: border-box;
height: 24px;
height: 22px;
display: flex;
align-items: center;
padding: 2px 0 2px 0;
+2
View File
@@ -33,6 +33,7 @@ export * from './Menu/MenuItemElement';
export * from './Menu/MenuPanel';
export * from './Menu/menuPanelStyles';
export * from './Menu/MenuStateContext';
export * from './Menu/useMouseContextMenu';
export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm';
export * from './ObjectPropertyInfo/useObjectPropertyCategories';
@@ -165,6 +166,7 @@ export * from './useClickEvents';
export * from './useControlledScroll';
export * from './useClipboard';
export * from './useCombinedHandler';
export * from './useCombinedRef';
export * from './useExecutor';
export * from './useFn';
export * from './useFocus';
@@ -0,0 +1,21 @@
/*
* 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 { useCallback } from 'react';
export function useCombinedRef<T>(...refs: React.ForwardedRef<T>[]) {
return useCallback((instance: T) => {
for (const ref of refs) {
if (typeof ref === 'function') {
ref(instance);
} else if (ref) {
(ref as React.MutableRefObject<T>).current = instance;
}
}
}, refs);
}
+9 -8
View File
@@ -18,18 +18,19 @@
},
"dependencies": {
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-executor": "~0.1.0",
"@cloudbeaver/core-plugin": "~0.1.0",
"@cloudbeaver/core-authentication": "~0.1.0",
"@cloudbeaver/core-root": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"dexie": "^3.2.3",
"workbox-core": "~6.x.x",
"workbox-window": "~6.x.x",
"workbox-precaching": "~6.x.x"
"workbox-precaching": "~6.x.x",
"workbox-window": "~6.x.x"
},
"peerDependencies": {
"react": "~18.x.x",
"react-dom": "~18.x.x",
"@testing-library/jest-dom": "~5.x.x",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"@testing-library/jest-dom": "~5.x.x"
"react": "~18.x.x",
"react-dom": "~18.x.x"
}
}
}
@@ -6,7 +6,6 @@
* you may not use this file except in compliance with the License.
*/
export interface IMenuActionsSet {
id: string;
list: string[];
}
import IndexedDB, { Table as IndexedDBTable } from 'dexie';
export { IndexedDB, type IndexedDBTable };
@@ -0,0 +1,19 @@
/*
* 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 { injectable } from '@cloudbeaver/core-di';
import type { IndexedDB } from './IndexedDB';
@injectable()
export class IndexedDBService {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
register(db: IndexedDB): void {
// TODO: implement
}
}
@@ -1,3 +1,5 @@
export * from './IndexedDB/IndexedDBService';
export * from './IndexedDB/IndexedDB';
export * from './CookiesService';
export * from './manifest';
export * from './ServiceWorkerService';
@@ -10,6 +10,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di';
import { BrowserSettingsService } from './BrowserSettingsService';
import { CookiesService } from './CookiesService';
import { IndexedDBService } from './IndexedDB/IndexedDBService';
import { ServiceWorkerBootstrap } from './ServiceWorkerBootstrap';
import { ServiceWorkerService } from './ServiceWorkerService';
@@ -23,5 +24,6 @@ export const coreBrowserManifest: PluginManifest = {
CookiesService,
ServiceWorkerBootstrap,
ServiceWorkerService,
IndexedDBService,
],
};
@@ -0,0 +1,27 @@
#!/usr/bin/env node
'use strict';
process.title = 'dependency-graph';
const madge = require('madge');
const { resolve } = require('path');
madge('packages', {
includeNpm: true,
fileExtensions: ['ts', 'tsx'],
detectiveOptions: {
// noTypeDefinitions: true,
es6: {
mixedImports: true,
// skipTypeImports: true,
},
ts: {
mixedImports: true,
// skipTypeImports: true,
},
},
})
.then(res => res.image('image.svg'))
.then(writtenImagePath => {
console.log('Image written to ' + writtenImagePath);
});
@@ -13,12 +13,19 @@ module.exports = [
'@emotion',
'stackblur-canvas',
'graphlib',
'@emotion',
'svg-tag-names',
'html-tags',
'codemirror',
'codemirror6',
'react-codemirror2',
'd3',
'dagre',
'codemirror',
'@codemirror/*',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@codemirror/autocomplete',
'@codemirror/lang-*',
'@lezer',
'@lezer/*',
];
+3
View File
@@ -13,6 +13,7 @@
"/tests"
],
"bin": {
"core-cli-dependency-graph": "./bin/dependency-graph.js",
"core-cli-build": "./bin/build.js",
"core-cli-test": "./bin/test.js",
"core-cli-analyzer": "./bin/analyzer.js",
@@ -75,6 +76,7 @@
"jest-environment-jsdom": "~29.x.x"
},
"devDependencies": {
"@types/madge": "^5.0.0",
"allure-commandline": "^2.20.1",
"crypto": "^1.0.1",
"http-proxy": "^1.18.1",
@@ -82,6 +84,7 @@
"jest-allure": "^0.1.3",
"jest-environment-jsdom": "~29.5.0",
"jest-jasmine2": "^29.3.1",
"madge": "^6.0.0",
"workbox-webpack-plugin": "^6.5.4"
}
}
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef } from 'react';
import { forwardRef, useContext, useEffect } from 'react';
import { Dialog, useDialogState } from 'reakit/Dialog';
import styled, { use } from 'reshadow';
@@ -28,7 +28,7 @@ export interface CommonDialogWrapperProps {
style?: ComponentStyle;
}
export const CommonDialogWrapper = observer<CommonDialogWrapperProps>(function CommonDialogWrapper({
export const CommonDialogWrapper = observer<CommonDialogWrapperProps, HTMLDivElement>(forwardRef(function CommonDialogWrapper({
size = 'medium',
fixedSize,
fixedWidth,
@@ -36,10 +36,9 @@ export const CommonDialogWrapper = observer<CommonDialogWrapperProps>(function C
className,
children,
style,
}) {
}, ref) {
const context = useContext(DialogContext);
const dialogState = useDialogState({ visible: true });
const focusedElementRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!dialogState.visible && !context.dialog.options?.persistent) {
@@ -47,18 +46,15 @@ export const CommonDialogWrapper = observer<CommonDialogWrapperProps>(function C
}
});
useEffect(() => {
if (document.activeElement instanceof HTMLElement) {
focusedElementRef.current = document.activeElement;
}
return () => {
focusedElementRef.current?.focus();
};
}, []);
return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, dialogStyles, style))(
<Dialog {...dialogState} aria-label={ariaLabel} visible={context.visible} hideOnClickOutside={false} modal={false}>
<Dialog
{...dialogState}
ref={ref}
aria-label={ariaLabel}
visible={context.visible}
hideOnClickOutside={false}
modal={false}
>
<dialog className={className} {...use({ size, fixedSize, fixedWidth })}>
<Loader suspense>
{children}
@@ -66,4 +62,4 @@ export const CommonDialogWrapper = observer<CommonDialogWrapperProps>(function C
</dialog>
</Dialog>
);
});
}));
@@ -8,7 +8,7 @@
import styled, { css } from 'reshadow';
import { Button, Translate, useStyles } from '@cloudbeaver/core-blocks';
import { Button, Translate, useFocus, useStyles } from '@cloudbeaver/core-blocks';
import type { TLocalizationToken } from '@cloudbeaver/core-localization';
import { CommonDialogBody } from './CommonDialog/CommonDialogBody';
@@ -47,10 +47,11 @@ export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload, Dial
rejectDialog,
className,
}) {
const [focusedRef] = useFocus<HTMLDivElement>({ focusFirstChild: true });
const { icon, title, subTitle, bigIcon, viewBox, message, confirmActionText, cancelActionText } = payload;
return styled(useStyles(style))(
<CommonDialogWrapper size='small' className={className} fixedWidth>
<CommonDialogWrapper ref={focusedRef} size='small' className={className} fixedWidth>
<CommonDialogHeader
title={title}
subTitle={subTitle}
@@ -7,7 +7,7 @@
*/
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import { useLayoutEffect, useMemo, useRef } from 'react';
import { DialogBackdrop } from 'reakit/Dialog';
import styled from 'reshadow';
@@ -23,6 +23,7 @@ import { dialogStyles } from './styles';
export const DialogsPortal = observer(function DialogsPortal() {
const styles = useStyles(dialogStyles);
const commonDialogService = useService(CommonDialogService);
const focusedElementRef = useRef<HTMLElement | null>(null);
let activeDialog: DialogInternal<any> | undefined;
@@ -42,6 +43,11 @@ export const DialogsPortal = observer(function DialogsPortal() {
}
},
backdropClick(e: React.MouseEvent<HTMLDivElement>) {
if (e.target !== e.currentTarget) {
return;
}
e.preventDefault(); // prevent focus loss
if (!this.dialog?.options?.persistent && e.currentTarget.isEqualNode(e.target as HTMLElement)) {
this.reject();
}
@@ -50,6 +56,29 @@ export const DialogsPortal = observer(function DialogsPortal() {
dialog: activeDialog,
}, ['reject', 'resolve', 'backdropClick']);
useMemo(() => {
if (!activeDialog) {
return;
}
// capture focused element before dialog open
if (document.activeElement instanceof HTMLElement) {
focusedElementRef.current = document.activeElement;
}
}, [activeDialog]);
useLayoutEffect(() => {
if (!activeDialog) {
return;
}
return () => {
// restore focus after dialog close
focusedElementRef.current?.focus();
focusedElementRef.current = null;
};
}, [activeDialog]);
return styled(styles)(
<Loader suspense overlay>
<DialogBackdrop visible={!!activeDialog} onMouseDown={state.backdropClick}>
+2 -1
View File
@@ -16,7 +16,8 @@
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
"@cloudbeaver/core-di": "~0.1.0"
"@cloudbeaver/core-di": "~0.1.0",
"@cloudbeaver/core-browser": "~0.1.0"
},
"peerDependencies": {
"mobx": "~6.x.x"
@@ -8,6 +8,7 @@
import { entries, IReactionDisposer, keys, observable, ObservableMap, reaction, remove, runInAction, set, toJS } from 'mobx';
import { IndexedDB, IndexedDBService, IndexedDBTable } from '@cloudbeaver/core-browser';
import { injectable } from '@cloudbeaver/core-di';
import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
@@ -17,15 +18,10 @@ interface ILocalStorageElement<T extends Record<any, any> | Map<any, any>> {
defaultValue: () => T;
remap?: (savedStore: T) => T;
onUpdate?: () => void;
readState: (reset: boolean) => void;
saveState: () => void;
setStorage(storage: Storage): void;
subscribe(): void;
unsubscribe(): void;
setStorage(storage: Storage | LocalStorageIndexedDB): void;
}
export type LocalStorageType = 'local' | 'session';
export type LocalStorageType = 'local' | 'session' | 'indexed';
interface IMessage {
type: 'init' | 'respond';
@@ -42,12 +38,17 @@ export class LocalStorageSaveService {
private readonly broadcastChannel: BroadcastChannel;
private readonly storages: Map<string, ILocalStorageElement<any>>;
private storageType: LocalStorageType;
private readonly indexedDBStore: LocalStorageIndexedDB;
constructor() {
constructor(
private readonly indexedDBService: IndexedDBService,
) {
this.storageType = 'local';
this.broadcastChannel = new BroadcastChannel('local-storage');
this.storages = new Map();
this.onStorageChange = new SyncExecutor();
this.indexedDBStore = new LocalStorageIndexedDB();
this.indexedDBService.register(this.indexedDBStore);
// this.broadcastChannel.addEventListener('message', event => {
// const message: IMessage = event.data;
@@ -91,14 +92,15 @@ export class LocalStorageSaveService {
store: T,
defaultValue: () => T extends any ? T : never,
remap?: (savedStore: T) => T,
onUpdate?: () => void
onUpdate?: () => void,
storageType?: LocalStorageType
): void {
if (this.storages.has(storeId)) {
return;
}
this.storages.set(storeId, new DataStorage(
this.getStorage(),
this.getStorage(storageType),
storeId,
store,
defaultValue,
@@ -127,11 +129,17 @@ export class LocalStorageSaveService {
this.onStorageChange.execute(this.storageType);
}
private getStorage(): Storage {
if (this.storageType === 'local') {
return localStorage;
private getStorage(storageType?: LocalStorageType): Storage | LocalStorageIndexedDB {
const type = storageType ?? this.storageType;
switch (type) {
case 'local':
return localStorage;
case 'indexed':
return this.indexedDBStore;
default:
return sessionStorage;
}
return sessionStorage;
}
private sendMessage(message: IMessage) {
@@ -139,12 +147,50 @@ export class LocalStorageSaveService {
}
}
interface ILocalStorageIndexedRecord {
key: string;
data: Map<any, any> | Record<any, any> | string;
}
class LocalStorageIndexedDB extends IndexedDB {
values!: IndexedDBTable<ILocalStorageIndexedRecord>;
constructor() {
super('local-storage');
this.version(1).stores({
values: 'key',
});
}
count(): Promise<number> {
return this.values.count();
}
async clear(): Promise<void> {
await this.values.clear();
}
async getItem(key: string): Promise<unknown | null> {
const record = await this.values.get(key);
return record?.data ?? null;
}
async removeItem(key: string): Promise<void> {
await this.values.delete(key);
}
async setItem(key: string, data: any): Promise<void> {
await this.values.put({ key, data });
}
}
class DataStorage<T extends Record<any, any> | Map<any, any>> implements ILocalStorageElement<T> {
private firstRun: boolean;
private mobxSub: IReactionDisposer | undefined;
constructor(
private storage: Storage,
private storage: Storage | LocalStorageIndexedDB,
readonly storeId: string,
readonly store: T,
readonly defaultValue: () => T,
@@ -153,69 +199,83 @@ class DataStorage<T extends Record<any, any> | Map<any, any>> implements ILocalS
) {
this.firstRun = true;
this.mobxSub = undefined;
this.subscribe();
}
setStorage(storage: Storage): void {
setStorage(storage: Storage | LocalStorageIndexedDB): void {
this.storage = storage;
this.firstRun = true;
this.saveState();
this.saveState(toJS(this.store), this.firstRun);
}
readState(reset: boolean) {
private async readState(reset: boolean) {
try {
this.unsubscribe();
const state = this.storage.getItem(this.storeId);
const state = await this.storage.getItem(this.storeId);
let nextState: T;
if (state && !reset) {
nextState = this.parseData(this.store, state, this.remap);
nextState = this.deserializeData(this.store, state, this.remap);
} else {
nextState = toJS(this.defaultValue());
}
const parsed = observable(nextState);
const parsed = observable(nextState, undefined, { deep: true });
const oldKeys = keys(this.store);
const newKeys = keys(parsed);
runInAction(() => {
for (const oldKey of oldKeys) {
if (!newKeys.includes(oldKey)) {
remove(this.store, oldKey as any);
this.unsubscribe();
try {
runInAction(() => {
for (const oldKey of oldKeys) {
if (!newKeys.includes(oldKey)) {
remove(this.store, oldKey as any);
}
}
}
for (const [key, value] of entries(parsed)) {
set<T>(this.store, key, value);
}
});
for (const [key, value] of entries(parsed)) {
set<T>(this.store, key, value);
}
});
} finally {
this.subscribe();
}
this.onUpdate?.();
} catch (e: any) {
console.log('Error when parsing local storage value', e);
} finally {
this.subscribe();
console.error('Error when parsing local storage value', e);
}
}
saveState(): void {
if (this.firstRun) {
this.readState(false);
private async saveState(store: T, firstRun: boolean): Promise<void> {
if (firstRun) {
await this.readState(false);
this.firstRun = false;
return;
}
try {
if (this.storage instanceof LocalStorageIndexedDB) {
await this.storage.setItem(
this.storeId,
this.serializeData(store)
);
} else {
this.storage.setItem(
this.storeId,
this.serializeData(store)
);
}
} catch (e: any) {
console.error('Error when saving local storage value', e);
}
this.storage.setItem(
this.storeId,
this.stringifyData(this.store)
);
}
subscribe() {
private subscribe() {
this.unsubscribe();
this.mobxSub = reaction(
() => toJS(this.store),
() => this.saveState(),
() => [toJS(this.store), this.firstRun] as const,
([store, firstRun]) => this.saveState(store, firstRun),
{
fireImmediately: true,
delay: 500,
@@ -223,15 +283,15 @@ class DataStorage<T extends Record<any, any> | Map<any, any>> implements ILocalS
);
}
unsubscribe() {
private unsubscribe() {
this.mobxSub?.();
this.mobxSub = undefined;
}
private parseData(store: any, data: any, remap?: (savedStore: any) => any): any {
private deserializeData(store: any, data: any, remap?: (savedStore: any) => any): any {
if (store instanceof ObservableMap) {
data = this.parseMap(data);
} else {
} else if (typeof data === 'string') {
data = JSON.parse(data);
}
@@ -252,19 +312,27 @@ class DataStorage<T extends Record<any, any> | Map<any, any>> implements ILocalS
return data;
}
private stringifyData(store: any): string {
if (store instanceof ObservableMap) {
return this.stringifyMap(toJS(store));
private serializeData(store: any): any {
if (this.storage instanceof LocalStorageIndexedDB) {
return store;
}
return JSON.stringify(toJS(store));
if (store instanceof Map) {
return this.stringifyMap(store);
}
return JSON.stringify(store);
}
private stringifyMap(map: Map<any, any>): string {
return JSON.stringify(Array.from(map.entries()));
}
private parseMap(data: string): Map<any, any> {
return new Map(JSON.parse(data));
private parseMap(data: string | Map<any, any>): Map<any, any> {
if (typeof data === 'string') {
return new Map(JSON.parse(data));
}
return data;
}
}
@@ -16,4 +16,4 @@ label {
label[title] {
text-decoration: none;
}
}
@@ -12,7 +12,8 @@ html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
overflow: hidden;
overscroll-behavior: none;
}
/* Sections
@@ -11,7 +11,7 @@ import { ButtonHTMLAttributes, forwardRef, useRef, useState } from 'react';
import type { MenuInitialState } from 'reakit/Menu';
import styled from 'reshadow';
import { getComputed, IMenuState, Menu, menuPanelStyles, useObjectRef, useTranslate, useStyles, useAutoLoad } from '@cloudbeaver/core-blocks';
import { getComputed, IMenuState, Menu, menuPanelStyles, useObjectRef, useTranslate, useStyles, useAutoLoad, IMouseContextMenu } from '@cloudbeaver/core-blocks';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { IMenuData, MenuActionItem } from '@cloudbeaver/core-view';
@@ -25,6 +25,7 @@ interface IMenuProps extends React.PropsWithChildren {
type ContextMenuRenderingChildren = (props: IMenuProps) => React.ReactNode | React.ReactElement;
interface IContextMenuProps extends Omit<ButtonHTMLAttributes<any>, 'style' | 'children'> {
mouseContextMenu?: IMouseContextMenu;
menu: IMenuData;
style?: ComponentStyle;
disclosure?: boolean;
@@ -37,6 +38,7 @@ interface IContextMenuProps extends Omit<ButtonHTMLAttributes<any>, 'style' | 'c
}
export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(forwardRef(function ContextMenu({
mouseContextMenu,
menu: menuData,
disclosure,
children,
@@ -114,6 +116,7 @@ export const ContextMenu = observer<IContextMenuProps, HTMLButtonElement>(forwar
menuRef={menu}
modal={modal}
visible={visible}
mouseContextMenu={mouseContextMenu}
placement={placement}
disabled={disabled}
disclosure={disclosure}
@@ -11,6 +11,19 @@ import { css } from 'reshadow';
export const MENU_BAR_SMALL_STYLES = css`
menu-bar-item {
padding: 0 12px;
& menu-bar-item-icon {
display: flex;
width: 16px;
}
& menu-bar-item-label {
font-size: 12px;
}
& Loader, & IconOrImage {
width: 16px;
}
}
menu-bar {
height: 32px;
@@ -200,9 +200,10 @@ const SubMenuItem = observer<ISubMenuItemProps>(function SubmenuItem({
subMenuData.context.set(DATA_CONTEXT_SUBMENU_ITEM, item);
const handler = subMenuData.handler;
const hideIfEmpty = handler?.hideIfEmpty?.(subMenuData.context) ?? true;
const hidden = getComputed(() => subMenuData.items.every(item => item.hidden));
if (hidden) {
if (hideIfEmpty && hidden) {
return null;
}
@@ -210,12 +211,14 @@ const SubMenuItem = observer<ISubMenuItemProps>(function SubmenuItem({
const extraProps = handler?.getExtraProps?.() ?? item.getExtraProps?.() as any;
/** @deprecated must be refactored (#1)*/
const displayLabel = getComputed(() => handler?.isLabelVisible?.(subMenuData.context, subMenuData.menu) ?? true);
const loaded = getComputed(() => !subMenuData.loaders.some(loader => !loader.isLoaded()));
// TODO: seems like we don't need this, it's was used in panelAvailable to display > arrow in menu bar
// when menu isn't loaded yet
// const loaded = getComputed(() => !subMenuData.loaders.some(loader => !loader.isLoaded()));
const info = handler?.getInfo?.(subMenuData.context, subMenuData.menu);
const label = info?.label ?? item.label ?? item.menu.label;
const icon = info?.icon ?? item.icon ?? item.menu.icon;
const tooltip = info?.tooltip ?? item.tooltip ?? item.menu.tooltip;
const panelAvailable = subMenuData.available || !loaded;
const panelAvailable = subMenuData.itemCreators.length > 0;
return styled(styles)(
<ContextMenu
@@ -131,7 +131,7 @@ export const MenuItemRenderer = observer<IMenuItemRendererProps>(function MenuIt
<MenuItemElement
label={item.label}
icon={IconComponent ? styled(styles)(
<Loader suspense small inline fullSize>
<Loader suspense inline fullSize>
<IconComponent
item={item}
style={joinStyles(menuPanelStyles, style)}
@@ -15,14 +15,14 @@ import styled, { use } from 'reshadow';
import { Icon, IconOrImage, Loader, useObjectRef, useStyles } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { EditorDialog } from './EditorDialog';
import { InlineEditorStyles } from './styles';
export type InlineEditorControls = 'right' | 'top' | 'bottom' | 'inside';
export interface InlineEditorProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyDown' | 'autoFocus'> {
export interface InlineEditorProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'onKeyDown' | 'autoFocus' | 'style'> {
value: string;
controlsPosition?: InlineEditorControls;
simple?: boolean;
@@ -33,6 +33,7 @@ export interface InlineEditorProps extends Omit<React.InputHTMLAttributes<HTMLIn
autofocus?: boolean;
active?: boolean;
loading?: boolean;
style?: ComponentStyle;
onChange: (value: string) => void;
onSave?: () => void;
onReject?: () => void;
@@ -54,6 +55,7 @@ export const InlineEditor = observer<InlineEditorProps, HTMLInputElement>(forwar
active,
loading,
disabled,
style,
onChange,
onSave,
onUndo,
@@ -110,8 +112,8 @@ export const InlineEditor = observer<InlineEditorProps, HTMLInputElement>(forwar
useImperativeHandle(ref, () => inputRef.current!);
return styled(useStyles(InlineEditorStyles))(
<editor as='div' className={className} {...use({ active })} onClick={onClick} onDoubleClick={onDoubleClick}>
return styled(useStyles(InlineEditorStyles, style))(
<editor className={className} {...use({ active })} onClick={onClick} onDoubleClick={onDoubleClick}>
<editor-container>
<input
ref={inputRef}
@@ -10,7 +10,8 @@ import { css } from 'reshadow';
export const InlineEditorStyles = css`
input {
composes: theme-text-text-primary-on-light from global;
composes: theme-typography--body2 theme-text-text-primary-on-light from global;
font: inherit;
}
editor {
@@ -32,6 +33,7 @@ export const InlineEditorStyles = css`
height: 100%;
flex: 1;
display: flex;
box-sizing: border-box;
& input, & input[disabled], & input[readonly] {
border: none;
@@ -52,6 +54,7 @@ export const InlineEditorStyles = css`
display: flex;
flex-direction: row;
border: solid 1px;
box-sizing: border-box;
&:empty {
display: none;
@@ -69,9 +72,11 @@ export const InlineEditorStyles = css`
editor-actions[|position=inside] {
position: relative;
flex: 0 0 auto;
bottom: auto;
top: 0;
left: auto;
border-right: none !important;
border-top: none;
border-bottom: none;
}
editor-actions[|position=bottom],
@@ -48,7 +48,10 @@ tab-inner {
tab-inner[|selected] portal,
tab-inner portal[|menuOpened],
tab-inner:hover portal,
tab-inner:focus-within portal {
tab-inner:focus-within portal,
tab-inner[|selected] tab-action,
tab-inner:hover tab-action,
tab-inner:focus-within tab-action {
visibility: visible;
}
@@ -130,6 +133,7 @@ export const BASE_TAB_ACTION_STYLES = css`
flex-shrink: 0;
padding: 4px;
padding-left: 0;
visibility: hidden;
&:hover {
opacity: 0.8;
+1
View File
@@ -28,6 +28,7 @@ export * from './errorOf';
export * from './isArraysEqual';
export * from './isPrimitive';
export * from './isPropertiesEqual';
export * from './isSafari';
export * from './isValuesEqual';
export * from './md5';
export * from './MetadataMap';
@@ -6,8 +6,6 @@
* you may not use this file except in compliance with the License.
*/
import type { ILoadableState } from '@cloudbeaver/core-utils';
const ua = navigator.userAgent.toLowerCase();
export type LoaderState = ILoadableState | {
loading: boolean;
};
export const isSafari = ua.includes('safari') && !ua.includes('chrome');
@@ -23,6 +23,7 @@ export interface IMenuHandler<TExtraProps = unknown> {
isLoading?: (context: IDataContextProvider) => boolean;
isDisabled?: (context: IDataContextProvider) => boolean;
isHidden?: (context: IDataContextProvider) => boolean;
hideIfEmpty?: (context: IDataContextProvider) => boolean;
handler?: (context: IDataContextProvider) => void;
/** @deprecated must be refactored (#1)*/
@@ -56,7 +56,7 @@ export function useMenu(options: IMenuOptions): IMenuData {
return menuService.getMenuItemCreators(this.context);
},
get available() {
return this.itemCreators.length > 0;
return this.handler?.hideIfEmpty?.(this.context) === false || this.itemCreators.length > 0;
},
get items() {
return menuService.getMenu(this.context, this.itemCreators);
@@ -233,6 +233,14 @@
}
}
],
"no-restricted-imports": [
"error",
{
"patterns": [
"@cloudbeaver/*/src/*"
]
}
],
"react/destructuring-assignment": "off",
"react/require-render-return": "off",
"react/no-string-refs": "off",
@@ -1,33 +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 styled from 'reshadow';
import type { AdministrationItemDrawerProps } from '@cloudbeaver/core-administration';
import { Tab, TabTitle, TabIcon } from '@cloudbeaver/core-ui';
export const TeamsDrawerItem: React.FC<AdministrationItemDrawerProps> = function TeamsDrawerItem({
item,
onSelect,
style,
disabled,
}) {
return styled(useStyles(style))(
<Tab
tabId={item.name}
disabled={disabled}
title='administration_teams_tab_title'
onOpen={() => onSelect(item.name)}
>
<TabIcon icon='/icons/tab_team.svg' />
<TabTitle><Translate token='administration_teams_tab_title' /></TabTitle>
</Tab>
);
};
@@ -1,33 +0,0 @@
{
"name": "@cloudbeaver/plugin-codemirror",
"sideEffects": false,
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "src/index.ts",
"scripts": {
"prebuild": "rimraf --glob lib/*",
"build": "webpack --config ../../configs/webpack.plugin.config.js",
"postbuild": "tsc",
"lint": "eslint ./src/ --ext .ts,.tsx",
"lint-fix": "eslint ./src/ --ext .ts,.tsx --fix",
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"codemirror": "5.65.7",
"react-codemirror2": "~7.2.1"
},
"devDependencies": {
"@types/codemirror": "~5.60.5"
},
"peerDependencies": {
"react": "~18.x.x",
"mobx": "~6.x.x",
"mobx-react-lite": "~3.x.x",
"reshadow": "~0.x.x"
}
}
@@ -1,85 +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 { observer } from 'mobx-react-lite';
import { useCallback, useLayoutEffect, useRef } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import styled, { use } from 'reshadow';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/xml/xml';
import 'codemirror/mode/htmlembedded/htmlembedded';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/meta';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/selection/mark-selection';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/hint/show-hint.css';
import { useStyles } from '@cloudbeaver/core-blocks';
import type { ICodeEditorProps } from './ICodeEditorProps';
import { SqlEditorStyles } from './theme';
import { useAutoFormat } from './useAutoFormat';
/** We use hardcoded value because codemirror treats undefined and lack of prop diffrently */
export const DEFAULT_CURSOR_BLINK_RATE = 530;
export const CodeEditor = observer<ICodeEditorProps>(function CodeEditor(props) {
const { autoFormat, className, editorDidMount } = props;
const formatter = useAutoFormat(props.options?.mode);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<CodeMirror.Editor | null>(null);
let value = props.value;
if (autoFormat) {
value = formatter.format(value);
}
useLayoutEffect(() => {
const observable = wrapperRef.current;
if (!observable) {
return;
}
const observer = new ResizeObserver(() => {
editorRef.current?.refresh();
});
observer.observe(observable);
return () => observer.unobserve(observable);
}, []);
const handleMount = useCallback((editor: CodeMirror.Editor, value: string, cb: () => void) => {
editorRef.current = editor;
if (editorDidMount) {
editorDidMount(editor, value, cb);
}
}, [editorDidMount]);
const readonly = props.options?.readOnly ?? false;
return styled(useStyles(SqlEditorStyles))(
<code-editor ref={wrapperRef} {...use({ readonly })} className={className}>
<CodeMirror
{...props}
value={value}
editorDidMount={handleMount}
options={{
styleSelectedText: true,
...props.options,
cursorBlinkRate: props.options?.cursorBlinkRate ?? DEFAULT_CURSOR_BLINK_RATE,
}}
/>
</code-editor>
);
});
@@ -1,24 +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 { ComplexLoader, createComplexLoader, Loader } from '@cloudbeaver/core-blocks';
import type { ICodeEditorProps } from './ICodeEditorProps';
const loader = createComplexLoader(async function loader() {
const { CodeEditor } = await import('./CodeEditor');
return { CodeEditor };
});
export const CodeEditorLoader: React.FC<ICodeEditorProps> = function CodeEditorLoader(props) {
return (
<ComplexLoader loader={loader}>
{({ CodeEditor }) => <CodeEditor {...props} />}
</ComplexLoader>
);
};
@@ -1,13 +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 { IControlledCodeMirror } from 'react-codemirror2';
export interface ICodeEditorProps extends IControlledCodeMirror {
autoFormat?: boolean;
}
@@ -1,13 +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 * as CodeMirror from 'codemirror';
declare module 'codemirror' {
const hint: Record<string, HintFunction | AsyncHintFunction>;
}
@@ -1,20 +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 * as CodeMirror from 'codemirror';
declare module 'codemirror' {
interface ModeInfo {
name: string;
mime: string;
mode: string;
ext?: string[];
alias?: string[];
}
function findModeByName(name: string): ModeInfo | undefined;
}
@@ -1,23 +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.
*/
export async function formatValue(value: string, mode: string | undefined): Promise<string> {
try {
switch (mode) {
case 'application/json':
return JSON.stringify(JSON.parse(value), null, 2);
case 'text/html':
case 'text/xml':
return value;
default:
return value;
}
} catch {
return value;
}
}
@@ -1,2 +0,0 @@
export * from './CodeEditorLoader';
export * from './useAutoFormat';
@@ -1,232 +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.
*/
@mixin base-code-editor() {
.material.CodeMirror-hints {
border: none;
border-radius: 0;
padding: 0;
@include mdc-theme-prop(background-color, surface, false);
@include mdc-theme-prop(color, on-surface, false);
}
.material .CodeMirror-hint {
position: relative;
border: none;
border-radius: 0;
padding: 0 8px;
@include mdc-theme-prop(color, on-surface, false);
&:hover:before {
content: "";
position: absolute;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.14;
@include mdc-theme-prop(background-color, primary, false);
}
}
.material .CodeMirror-hint-active {
@include mdc-theme-prop(background-color, primary, false);
@include mdc-theme-prop(color, on-primary, false);
&:hover:before {
display: none;
}
}
.react-codemirror2 {
height: 100%;
flex: 1 auto;
[use|readonly] .CodeMirror-cursors {
display: none;
}
.cm-s-material.CodeMirror {
height: 100%;
@include mdc-theme-prop(background-color, surface, false);
@include mdc-theme-prop(color, on-surface, false);
// color: #EEFFFF;
}
.cm-s-material .CodeMirror-gutters {
@include mdc-theme-prop(background, secondary, false);
@include mdc-theme-prop(color, on-secondary, false);
@include mdc-theme-prop(border-color, background, false);
// color: #546E7A;
// border: none;
}
.cm-s-material .CodeMirror-guttermarker,
.cm-s-material .CodeMirror-guttermarker-subtle,
.cm-s-material .CodeMirror-linenumber {
@include mdc-theme-prop(color, on-secondary, false);
// color: #546E7A;
}
.cm-s-material .CodeMirror-cursor {
border-left: 1px solid $cursor;
}
.cm-s-material .CodeMirror-scrollbar-filler {
@include mdc-theme-prop(background, secondary, false);
}
.cm-s-material :global(div).CodeMirror-selected {
@include mdc-theme-prop(background, primary, false);
}
.cm-s-material.CodeMirror-focused :global(div).CodeMirror-selected {
@include mdc-theme-prop(background, primary, false);
}
.CodeMirror :global(pre).CodeMirror-line,
.CodeMirror :global(pre).CodeMirror-line-like {
padding: 0 16px;
}
.cm-s-material .CodeMirror-line::selection,
.cm-s-material .CodeMirror-line > :global(span)::selection,
.cm-s-material .CodeMirror-line > :global(span) > :global(span)::selection {
@include mdc-theme-prop(background, primary, false);
}
.cm-s-material .CodeMirror-line::-moz-selection,
.cm-s-material .CodeMirror-line > :global(span)::-moz-selection,
.cm-s-material .CodeMirror-line > :global(span) > :global(span)::-moz-selection {
@include mdc-theme-prop(background, primary, false);
}
.cm-s-material .CodeMirror-activeline-background {
background: rgba(0, 0, 0, 0.5);
}
.cm-s-material .cm-keyword {
color: $keyword;
font-weight: bold;
}
.cm-s-material .cm-operator {
color: $operator;
}
.cm-s-material .cm-variable-2 {
color: $variable-2;
}
.cm-s-material .cm-variable-3,
.cm-s-material .cm-type {
color: $variable-3;
}
.cm-s-material .cm-builtin {
color: $builtin;
}
.cm-s-material .cm-atom {
color: $atom;
}
.cm-s-material .cm-number {
color: $number;
}
.cm-s-material .cm-def {
color: $def;
}
.cm-s-material .cm-string {
color: $string;
}
.cm-s-material .cm-string-2 {
color: $string-2;
}
.cm-s-material .cm-comment {
color: $comment;
}
.cm-s-material .cm-variable {
color: $variable;
}
.cm-s-material .cm-tag {
color: $tag;
}
.cm-s-material .cm-meta {
color: $meta;
}
.cm-s-material .cm-attribute {
color: $attribute;
}
.cm-s-material .cm-property {
color: $property;
}
.cm-s-material .cm-qualifier {
color: $qualifier;
}
.cm-s-material .cm-variable-3,
.cm-s-material .cm-type {
color: $variable-3;
}
.cm-s-material .cm-error {
color: rgba(255, 255, 255, 1);
background-color: $error;
}
.cm-s-material .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}
.cm-s-material :not(.CodeMirror-selectedtext).active-query {
@include mdc-theme-prop(background, secondary, false);
}
.cm-s-material .CodeMirror-selectedtext {
color: white;
}
.cm-s-material .running-query:before {
content: "";
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
height: 0;
width: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid;
@include mdc-theme-prop(border-left-color, primary, false);
}
.cm-s-material .running-query-error:before {
content: "";
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
height: 0;
width: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid;
@include mdc-theme-prop(border-left-color, negative, false);
}
}
}
@@ -1,43 +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 "@cloudbeaver/core-theming/src/styles/theme-dark";
@import "base-code-editor";
$meta: #ff1717;
$keyword: rgb(115, 158, 202);
$atom: #604aff;
$number: rgb(192, 192, 192);
$def: #00f;
$variable: rgb(126, 186, 211);
$variable-2: rgb(193, 170, 108);
$variable-3: rgb(193, 170, 108);
$property: rgb(126, 186, 211);
$operator: rgb(126, 186, 211);
$comment: rgb(102, 151, 104);
$string: rgb(202, 197, 128);
$string-2: #f50;
$qualifier: #555;
$builtin:#ab87ff;
$bracket: #cc7;
$tag: #170;
$attribute: #00c;
$link: #219;
$error: #f00;
$delimiter: rgb(238, 204, 100);
$cursor: #fff;
$active-query: $color-positive;
:global .CodeMirror-hints {
@include mdc-elevation(3);
@include mdc-typography(caption);
}
:global .#{$theme-class} {
@include base-code-editor;
}
@@ -1 +0,0 @@
export * from './styles';
@@ -1,43 +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 "@cloudbeaver/core-theming/src/styles/theme-light";
@import "base-code-editor";
$meta: #ff1717;
$keyword: #07a;
$atom: #905;
$number: #905;
$def: #00f;
$variable: black;
$variable-2: #a8bd00;
$variable-3: #a8bd00;
$property: black;
$operator: #a67f59;
$comment: #3f7f5f;
$string: #690;
$string-2: #f50;
$qualifier: #555;
$builtin: #07a;
$bracket: #cc7;
$tag: #170;
$attribute: #00c;
$link: #905;
$error: #f00;
$delimiter: rgb(238, 204, 100);
$cursor: #000;
$active-query: $color-positive;
:global .CodeMirror-hints {
@include mdc-elevation(3);
@include mdc-typography(caption);
}
:global .#{$theme-class} {
@include base-code-editor;
}
@@ -1,24 +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 { ThemeSelector } from '@cloudbeaver/core-theming';
export const SqlEditorStyles: ThemeSelector = async theme => {
let styles: any;
switch (theme) {
case 'dark':
styles = await import('./dark.module.scss');
break;
default:
styles = await import('./light.module.scss');
break;
}
return styles.default;
};
@@ -1,39 +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 { ModeSpec } from 'codemirror';
import { useObjectRef } from '@cloudbeaver/core-blocks';
export function useAutoFormat(mode: string | ModeSpec<any> | undefined) {
let modeName = 'sql';
if (typeof mode === 'string') {
modeName = mode;
} else if (typeof mode === 'object') {
modeName = mode.name;
}
return useObjectRef(() => ({
format(value: string) {
try {
switch (this.mode) {
case 'application/json':
return JSON.stringify(JSON.parse(value), null, 2);
case 'text/xml':
case 'text/html':
return value;
default:
return value;
}
} catch {
return value;
}
},
}), { mode: modeName });
}
@@ -17,16 +17,12 @@
"dependencies": {
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-utils": "~0.1.0",
"@codemirror/commands": "^6.1.1",
"@codemirror/lang-html": "^6.4.3",
"@codemirror/lang-javascript": "^6.1.5",
"@codemirror/lang-javascript": "^6.1.8",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.4.0",
"@codemirror/lang-sql": "^6.5.0",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/state": "^6.1.1",
"@codemirror/view": "^6.1.1",
"@lezer/highlight": "^1.1.4",
"codemirror6": "npm:codemirror@6.0.0"
"codemirror": "^6.0.1"
},
"peerDependencies": {}
}
@@ -12,24 +12,25 @@ import styled from 'reshadow';
import { clsx } from '@cloudbeaver/core-utils';
import { getDefaultExtensions } from './getDefaultExtensions';
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';
export const Editor = observer(forwardRef<IEditorRef, IEditorProps>(function Editor(props, ref) {
const extensions = [];
if (!props.extensions) {
extensions.push(getDefaultExtensions());
} else {
extensions.push(props.extensions);
}
export const Editor = observer<IEditorProps & IDefaultExtensions, IEditorRef>(forwardRef(function Editor({
lineNumbers,
extensions = [],
...rest
}, ref) {
const defaultExtensions = useEditorDefaultExtensions({ lineNumbers });
const combinedExtensions = [...defaultExtensions];
combinedExtensions.push(...extensions);
return styled(EDITOR_BASE_STYLES)(
<wrapper className={clsx('editor', props.className)}>
<ReactCodemirror {...props} ref={ref} extensions={extensions} />
<wrapper className={clsx('editor', rest.className)}>
<ReactCodemirror {...rest} ref={ref} extensions={combinedExtensions} />
</wrapper>
);
}));
@@ -10,6 +10,7 @@ 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';
@@ -18,7 +19,7 @@ const loader = createComplexLoader(async function loader() {
return { Editor };
});
export const EditorLoader = forwardRef<IEditorRef, IEditorProps>(function EditorLoader(props, ref) {
export const EditorLoader = forwardRef<IEditorRef, IEditorProps & IDefaultExtensions>(function EditorLoader(props, ref) {
return (
<ComplexLoader loader={loader}>
{({ Editor }) => <Editor {...props} ref={ref} />}
@@ -6,11 +6,9 @@
* you may not use this file except in compliance with the License.
*/
import type { EditorState } from '@codemirror/state';
import type { EditorView } from '@codemirror/view';
export interface IEditorRef {
container: HTMLDivElement | null;
state: EditorState | null;
view: EditorView | null;
}
@@ -10,7 +10,9 @@ import type { Extension } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
export interface IReactCodeMirrorProps {
value: string;
/** in case of using editor in editing mode its better for performance to use getValue instead */
value?: string;
getValue?: () => string;
extensions?: Extension[];
readonly?: boolean;
autoFocus?: boolean;
@@ -6,11 +6,12 @@
* you may not use this file except in compliance with the License.
*/
import { EditorView } from 'codemirror6';
import { forwardRef, useImperativeHandle, useLayoutEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { forwardRef, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { EditorState, Annotation, StateEffect } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import { useObjectRef } from '@cloudbeaver/core-blocks';
import { Annotation, EditorState, StateEffect } from '@codemirror/state';
import { ViewUpdate, EditorView } from '@codemirror/view';
import type { IEditorRef } from './IEditorRef';
import type { IReactCodeMirrorProps } from './IReactCodemirrorProps';
@@ -24,7 +25,8 @@ const defaultTheme = EditorView.theme({
},
});
export const ReactCodemirror = forwardRef<IEditorRef, IReactCodeMirrorProps>(function ReactCodemirror({
export const ReactCodemirror = observer<IReactCodeMirrorProps, IEditorRef>(forwardRef(function ReactCodemirror({
getValue,
value,
extensions,
readonly,
@@ -32,42 +34,44 @@ export const ReactCodemirror = forwardRef<IEditorRef, IReactCodeMirrorProps>(fun
onChange,
onUpdate,
}, ref) {
value = value ?? getValue?.();
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [view, setView] = useState<EditorView | null>(null);
const [state] = useState<EditorState>(() => EditorState.create());
const lastValueUpdate = useRef<string | undefined>(undefined);
const ext = [defaultTheme];
const callbackRef = useObjectRef({ onChange, onUpdate });
if (readonly) {
ext.push(EditorView.editable.of(false));
// can lead to missing state updates
ext.push(EditorState.readOnly.of(true));
// ext.push(EditorView.editable.of(false));
}
if (extensions) {
ext.push(extensions);
}
if (onChange || onUpdate) {
const updateListener = EditorView.updateListener.of((update: ViewUpdate) => {
const remote = update.transactions.some(tr => tr.annotation(External));
const updateListener = useMemo(() => 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();
if (update.docChanged && !remote) {
const doc = update.state.doc;
const value = doc.toString();
onChange?.(value, update);
}
lastValueUpdate.current = value;
callbackRef.onChange?.(value, update);
}
onUpdate?.(update);
});
callbackRef.onUpdate?.(update);
}), []);
ext.push(updateListener);
}
ext.push(updateListener);
useLayoutEffect(() => {
if (container) {
const ev = new EditorView({
parent: container,
state,
});
setView(ev);
@@ -87,11 +91,10 @@ export const ReactCodemirror = forwardRef<IEditorRef, IReactCodeMirrorProps>(fun
}, [container]);
useLayoutEffect(() => {
const currentValue = view?.state.doc.toString() ?? '';
if (view && value !== currentValue) {
if (view && value !== lastValueUpdate.current) {
lastValueUpdate.current = value;
view.dispatch({
changes: { from: 0, to: currentValue.length, insert: value },
changes: { from: 0, to: view.state.doc.length, insert: value },
annotations: [External.of(true)],
});
}
@@ -104,18 +107,17 @@ export const ReactCodemirror = forwardRef<IEditorRef, IReactCodeMirrorProps>(fun
});
useLayoutEffect(() => {
if (autoFocus && view) {
if (!readonly && autoFocus && view) {
view.focus();
}
}, [autoFocus, view]);
}, [autoFocus, view, readonly]);
useImperativeHandle(ref, () => ({
container,
view,
state,
}), [container, view, state]);
}), [container, view]);
return (
<div ref={setContainer} className='ReactCodemirror' />
);
});
}));
@@ -6,33 +6,67 @@
* you may not use this file except in compliance with the License.
*/
import { GlobalConstants, clsx } from '@cloudbeaver/core-utils';
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
import { foldGutter, indentOnInput, syntaxHighlighting, bracketMatching } from '@codemirror/language';
import { highlightSelectionMatches } from '@codemirror/search';
import type { Extension } from '@codemirror/state';
import { lineNumbers, highlightSpecialChars, dropCursor, rectangularSelection, crosshairCursor, keymap } from '@codemirror/view';
import { lineNumbers, highlightSpecialChars, dropCursor, rectangularSelection, crosshairCursor, keymap, highlightActiveLineGutter, highlightActiveLine } from '@codemirror/view';
import { classHighlighter } from '@lezer/highlight';
// @TODO allow to configure bindings outside of the component
const DEFAULT_KEY_MAP = defaultKeymap.filter(binding => binding.mac !== 'Ctrl-f');
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(): Extension[] {
const extensions: Extension[] = [
lineNumbers(),
highlightSpecialChars(),
highlightSelectionMatches(),
syntaxHighlighting(classHighlighter),
bracketMatching(),
dropCursor(),
crosshairCursor(),
foldGutter(),
indentOnInput(),
rectangularSelection(),
keymap.of(DEFAULT_KEY_MAP),
];
export function getDefaultExtensions(options?: IDefaultExtensions): Record<string, Extension> {
let extensions: Record<string, Extension> = {
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,13 +1,26 @@
import { createComplexLoader } from '@cloudbeaver/core-blocks';
export * from './EditorLoader';
export * from './IEditorProps';
export * from './IEditorRef';
export * from './getDefaultExtensions';
export type { IDefaultExtensions } from './getDefaultExtensions';
export * from './useEditorAutocompletion';
export * from '@codemirror/view';
export * from '@codemirror/state';
export * from '@codemirror/autocomplete';
export { html as HTML_EDITOR } from '@codemirror/lang-html';
export { javascript as JAVASCRIPT_EDITOR } from '@codemirror/lang-javascript';
export { json as JSON_EDITOR } from '@codemirror/lang-json';
export { sql as SQL_EDITOR } from '@codemirror/lang-sql';
export { xml as XML_EDITOR } from '@codemirror/lang-xml';
export { sql as SQL_EDITOR, SQLDialect } from '@codemirror/lang-sql';
export { xml as XML_EDITOR } from '@codemirror/lang-xml';
export const StandardSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).StandardSQL);
export const PostgreSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).PostgreSQL);
export const MySQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).MySQL);
export const MariaSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).MariaSQL);
export const MSSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).MSSQL);
export const SQLiteLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).SQLite);
export const CassandraLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).Cassandra);
export const PLSQLLoader = createComplexLoader(async () => (await import('@codemirror/lang-sql')).PLSQL);
@@ -0,0 +1,48 @@
/*
* 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.
*/
@mixin base-code-editor-autocompletion() {
.editor {
.cm-tooltip.cm-tooltip-autocomplete {
&>:global(ul) {
border-radius: var(--theme-form-element-radius);
}
& :global(li) {
display: flex;
align-items: center;
}
& .cm-completionIcon {
opacity: 1;
width: 16px;
height: 16px;
padding: 0;
margin-right: .6em;
&:not(:global([src])) {
opacity: 0;
}
}
& :global(li):hover {
@include mdc-theme-prop(background, secondary, false);
@include mdc-theme-prop(color, on-secondary, false);
}
& :global(li):global([aria-selected=true]) {
@include mdc-theme-prop(background, background, false);
@include mdc-theme-prop(color, text-primary-on-light, false);
}
}
.cm-tooltip.cm-completionInfo {
margin-left: 8px;
}
}
}
@@ -0,0 +1,20 @@
/*
* 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.
*/
@mixin base-code-editor-tooltip() {
.editor {
.cm-tooltip {
@include mdc-theme-prop(background-color, surface, false);
@include mdc-theme-prop(color, on-surface, false);
@include mdc-elevation(3);
@include mdc-typography(body2);
border: none;
border-radius: var(--theme-form-element-radius);
}
}
}
@@ -7,7 +7,10 @@
*/
@mixin base-code-editor() {
.editor, .ReactCodemirror {
.editor,
.ReactCodemirror {
@include mdc-typography('body2');
width: 100%;
height: 100%;
}
@@ -17,12 +20,9 @@
@include mdc-theme-prop(color, on-surface, false);
}
.editor .cm-lineNumbers {
@include mdc-theme-prop(color, on-secondary, false);
}
.editor .cm-scroller {
outline: none;
line-height: unset;
}
.editor .cm-content {
@@ -38,21 +38,27 @@
}
.editor .cm-gutters {
@include mdc-theme-prop(background, secondary, false);
@include mdc-theme-prop(color, on-secondary, false);
@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 {
@include mdc-theme-prop(background, background, false);
background: none;
box-shadow: 0 0 0 2px $mdc-theme-secondary;
}
.editor .cm-activeLineGutter {
@include mdc-theme-prop(background, background, false);
background: none;
@include mdc-theme-prop(color, on-surface, false);
}
.editor .active-query {
/* disable because look too ugly */
/*
display: inline-block;
@include mdc-theme-prop(background, secondary, false);
*/
}
.editor .cm-content ::selection {
@@ -60,44 +66,61 @@
}
.editor .cm-line::selection,
.editor .cm-line > :global(span)::selection,
.editor .cm-line > :global(span) > :global(span)::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 {
.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-gutters .cm-gutterElement {
.editor .cm-lineNumbers .cm-gutterElement {
min-width: 3ch;
box-sizing: content-box;
}
.editor .cm-gutters .cm-foldGutter .cm-gutterElement {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.editor .cm-gutters .query-status .cm-gutterElement .running-query-line {
.editor .cm-gutters .cm-foldGutter .cm-gutterElement-icon {
padding: 0 3px 0 5px;
width: 12px;
height: 12px;
display: flex;
height: 0;
width: 0;
margin-right: 4px;
margin-left: 4px;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid;
@include mdc-theme-prop(border-left-color, primary, false);
&.cm-foldGutter-folded :global(svg) {
transform: rotate(-90deg);
}
}
.editor .cm-gutters .query-status .cm-gutterElement .running-query-error-line {
.editor .cm-gutters .query-status .cm-gutterElement {
display: flex;
height: 0;
width: 0;
margin-right: 4px;
margin-left: 4px;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid;
@include mdc-theme-prop(border-left-color, negative, false);
align-items: center;
justify-content: center;
width: 14px;
.running-query-line {
display: flex;
height: 8px;
width: 8px;
border-radius: 100%;
@include mdc-theme-prop(background, primary, false);
}
.running-query-error-line {
display: flex;
height: 8px;
width: 8px;
border-radius: 100%;
@include mdc-theme-prop(background, negative, false);
}
}
.editor .tok-keyword {
@@ -174,4 +197,4 @@
.editor .tok-type {
color: $variable-3;
}
}
}
@@ -8,6 +8,8 @@
@import "@cloudbeaver/core-theming/src/styles/theme-dark";
@import "base-code-editor";
@import "base-code-editor-autocompletion";
@import "base-code-editor-tooltip";
$meta: #ff1717;
$keyword: rgb(115, 158, 202);
@@ -35,4 +37,6 @@ $active-query: $color-positive;
:global .#{$theme-class} {
@include base-code-editor;
@include base-code-editor-autocompletion;
@include base-code-editor-tooltip;
}
@@ -8,6 +8,8 @@
@import "@cloudbeaver/core-theming/src/styles/theme-light";
@import "base-code-editor";
@import "base-code-editor-autocompletion";
@import "base-code-editor-tooltip";
$meta: #ff1717;
$keyword: #07a;
@@ -35,4 +37,6 @@ $active-query: $color-positive;
:global .#{$theme-class} {
@include base-code-editor;
@include base-code-editor-autocompletion;
@include base-code-editor-tooltip;
}
@@ -0,0 +1,53 @@
/*
* 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 { useState } from 'react';
import { createComplexLoader, useComplexLoader } from '@cloudbeaver/core-blocks';
import type { Extension } from '@codemirror/state';
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);
}
@@ -0,0 +1,91 @@
import type React from 'react';
import { useLayoutEffect, useState } from 'react';
import { css } from 'reshadow';
import type { ComponentStyle } from '@cloudbeaver/core-theming';
import { clsx } from '@cloudbeaver/core-utils';
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
import type { Extension, EditorState } from '@codemirror/state';
import { keymap } from '@codemirror/view';
import type { IEditorRef } from './IEditorRef';
const styles = css`
:global(.cm-fix-position) {
margin-left: var(--cm-fix-position-left);
margin-top: var(--cm-fix-position-top);
z-index: 100;
}
:global(.cm-fixed-point) {
position: fixed;
top: 0;
left: 0;
width: 0;
height: 0;
}
`;
export type UseEditorAutocompletionResult = [Extension, React.ForwardedRef<IEditorRef>, ComponentStyle];
export function useEditorAutocompletion(config?: Parameters<typeof autocompletion>[0]): UseEditorAutocompletionResult {
const [editor, setEditor] = useState<IEditorRef | null>(null);
useLayoutEffect(() => {
const container = editor?.container;
if (!container) {
return;
}
// we need this element to calculate the position of the autocompletion tooltip
// because codemirror fixed position is incorrect in case of translated container
let element = container.querySelector<HTMLDivElement>('.cm-fixed-point');
if (!element) {
element = document.createElement('div');
element.className = 'cm-fixed-point';
container.append(element);
}
const resizeObserver = new ResizeObserver(() => {
if (!element) {
return;
}
let top = 0;
let left = 0;
// if (!isSafari) {
// seems like it's not working on iPad OS
const viewportOffset = element.getBoundingClientRect();
top = Math.floor(viewportOffset.top);
left = Math.floor(viewportOffset.left);
// }
container.style.setProperty('--cm-fix-position-top', `${-top}px`);
container.style.setProperty('--cm-fix-position-left', `${-left}px`);
});
resizeObserver.observe(container);
return () => {
resizeObserver.disconnect();
};
});
return [
[
autocompletion({
...config,
tooltipClass: (state: EditorState) => clsx(config?.tooltipClass?.(state), 'cm-fix-position'),
closeOnBlur: false,
}),
keymap.of([
{ key: 'Alt-Space', run: startCompletion, preventDefault: true },
{ key: 'Shift-Ctrl-Space', run: startCompletion, preventDefault: true },
]),
],
setEditor,
styles,
];
}
@@ -10,8 +10,7 @@
import { untracked } from 'mobx';
import { UserInfoResource } from '@cloudbeaver/core-authentication';
import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, getConnectionFolderIdFromNodeId, IConnectionFolderParam, IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { getConnectionFolderId } from '@cloudbeaver/core-connections/src/NavTree/getConnectionFolderId';
import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, ConnectionsManagerService, CONNECTION_FOLDER_NAME_VALIDATION, createConnectionFolderParam, createConnectionParam, getConnectionFolderIdFromNodeId, IConnectionFolderParam, IConnectionInfoParams, getConnectionFolderId } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -17,7 +17,6 @@
"validate-dependencies": "core-cli-validate-dependencies"
},
"dependencies": {
"@cloudbeaver/plugin-codemirror": "~0.1.0",
"@cloudbeaver/plugin-datasource-context-switch": "~0.1.0",
"@cloudbeaver/plugin-navigation-tabs": "~0.1.0",
"@cloudbeaver/plugin-navigation-tree": "~0.1.0",
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { useService } from '@cloudbeaver/core-di';
import { MENU_BAR_DEFAULT_STYLES, MenuBar } from '@cloudbeaver/core-ui';
import { MENU_BAR_DEFAULT_STYLES, MENU_BAR_SMALL_STYLES, MenuBar } from '@cloudbeaver/core-ui';
import { IDataContext, useMenu } from '@cloudbeaver/core-view';
import { DATA_CONTEXT_DV_DDM } from '../../../DatabaseDataModel/DataContext/DATA_CONTEXT_DV_DDM';
@@ -33,14 +33,8 @@ const menuStyles = css`
height: 100%;
}
menu-bar-item {
padding: 0 8px;
& menu-bar-item-icon {
width: 16px;
}
& menu-bar-item-label {
font-weight: 700;
font-size: 12px;
}
}
`;
@@ -72,7 +66,7 @@ export const TableFooterMenu = observer<Props>(function TableFooterMenu({
{mainMenuService.constructMenuWithContext(model, resultIndex, simple).map((topItem, i) => (
<TableFooterMenuItem key={i} menuItem={topItem} />
))}
<MenuBar menu={menu} style={[MENU_BAR_DEFAULT_STYLES, menuStyles]} />
<MenuBar menu={menu} style={[MENU_BAR_DEFAULT_STYLES, MENU_BAR_SMALL_STYLES, menuStyles]} />
</menu-wrapper>
);
});
@@ -24,7 +24,7 @@ export const tableFooterMenuStyles = css`
MenuTrigger {
composes: theme-ripple from global;
height: 100%;
padding: 0 16px;
padding: 0 12px;
display: flex;
align-items: center;
cursor: pointer;
@@ -37,7 +37,7 @@ export const tableFooterMenuStyles = css`
}
menu-trigger-icon IconOrImage {
display: block;
width: 24px;
width: 16px;
}
menu-trigger-title {
display: block;
@@ -58,7 +58,7 @@ const viewerStyles = css`
flex-direction: column;
overflow: hidden;
}
Split {
Split:not([disable]) {
gap: 8px;
}
Pane {
@@ -47,7 +47,7 @@ const styles = css`
display: none;
}
TabPanel {
padding: 16px;
padding-top: 8px;
}
TabList, TabPanel {
composes: theme-background-secondary theme-text-on-secondary from global;
@@ -8,15 +8,16 @@
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Textarea, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { BASE_CONTAINERS_STYLES, Button, useObservableRef, useStyles, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
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, getDefaultExtensions } from '@cloudbeaver/plugin-codemirror6';
import { EditorLoader } from '@cloudbeaver/plugin-codemirror6';
import type { IResultSetElementKey } from '../../DatabaseDataModel/Actions/ResultSet/IResultSetDataKey';
import { isResultSetContentValue } from '../../DatabaseDataModel/Actions/ResultSet/isResultSetContentValue';
@@ -39,7 +40,7 @@ const styles = css`
}
container {
display: flex;
gap: 16px;
gap: 8px;
flex-direction: column;
overflow: auto;
flex: 1;
@@ -52,9 +53,6 @@ const styles = css`
EditorLoader {
border-radius: var(--theme-group-element-radius);
}
Textarea {
flex: 1;
}
EditorLoader {
flex: 1;
overflow: auto;
@@ -74,12 +72,6 @@ const styles = css`
}
`;
const textAreaStyles = css`
container > Textarea > textarea {
border-radius: var(--theme-form-element-radius) !important;
}
`;
export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelProps<any, IDatabaseResultSet>> = observer(function TextValuePresentation({
model,
resultIndex,
@@ -180,12 +172,11 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
}
}
const useCodeEditor = state.currentContentType !== 'text/plain';
const autoFormat = !!firstSelectedCell && !editor.isElementEdited(firstSelectedCell);
const canSave = !!firstSelectedCell && content.isDownloadable(firstSelectedCell);
const typeExtension = getTypeExtension(state.currentContentType);
const typeExtension = useMemo(() => getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]);
return styled(style, textAreaStyles)(
return styled(style)(
<container>
<actions>
<TabsState
@@ -197,25 +188,13 @@ export const TextValuePresentation: TabContainerPanelComponent<IDataValuePanelPr
<TabList style={[BASE_TAB_STYLES, styles, UNDERLINE_TAB_STYLES]} />
</TabsState>
</actions>
{useCodeEditor ? (
<EditorLoader
key={readonly ? '1' : '0'}
value={autoFormat ? formatter.format(stringValue) : stringValue}
readonly={readonly}
extensions={typeExtension ? [getDefaultExtensions(), typeExtension] : undefined}
onChange={value => handleChange(value)}
/>
) : (
<Textarea
name="value"
rows={3}
value={stringValue}
readOnly={readonly}
style={textAreaStyles}
embedded
onChange={handleChange}
/>
)}
<EditorLoader
key={readonly ? '1' : '0'}
value={autoFormat ? formatter.format(stringValue) : stringValue}
readonly={readonly}
extensions={[typeExtension]}
onChange={value => handleChange(value)}
/>
{valueTruncated && <QuotaPlaceholder limit={limit} size={valueSize} />}
{canSave && (
<tools-container>
@@ -6,15 +6,19 @@
* you may not use this file except in compliance with the License.
*/
import React from 'react';
import React, { lazy } from 'react';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DataValuePanelService } from '../../TableViewer/ValuePanel/DataValuePanelService';
import { TextValuePresentation } from './TextValuePresentation';
import { TextValuePresentationService } from './TextValuePresentationService';
const TextValuePresentation = lazy(async () => {
const { TextValuePresentation } = await import('./TextValuePresentation');
return { default: TextValuePresentation };
});
@injectable()
export class TextValuePresentationBootstrap extends Bootstrap {
constructor(
@@ -71,6 +71,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
};
},
iconComponent: () => ConnectionIcon,
hideIfEmpty: () => false,
getExtraProps: () => ({ connectionKey: this.connectionSchemaManagerService.currentConnectionKey, small: true }),
getLoader: (context, menu) => {
if (this.isHidden()) {
@@ -99,7 +100,10 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
this.menuService.addCreator({
menus: [MENU_CONNECTION_SELECTOR],
isApplicable: () => this.connectionsManagerService.hasAnyConnection(),
isApplicable: () => (
this.connectionsManagerService.hasAnyConnection()
&& this.connectionSchemaManagerService.isConnectionChangeable
),
getItems: (context, items) => {
items = [...items];
@@ -200,6 +204,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
() => this.appAuthService.loaders
);
},
hideIfEmpty: () => false,
getInfo: (context, menu) => {
const connectionSchemaManagerService = this.connectionSchemaManagerService;
@@ -241,6 +246,10 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap {
this.menuService.addCreator({
menus: [MENU_CONNECTION_DATA_CONTAINER_SELECTOR],
isApplicable: () => (
this.connectionSchemaManagerService.isObjectCatalogChangeable
&& !!this.connectionSchemaManagerService.objectContainerList
),
getItems: (context, items) => {
items = [...items];
@@ -11,6 +11,7 @@ import { injectable } from '@cloudbeaver/core-di';
import { NavNodeManagerService } from '@cloudbeaver/core-navigation-tree';
import { download, generateFileName } from '@cloudbeaver/core-utils';
import { ActionService, ACTION_SAVE, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view';
import { LocalStorageSqlDataSource } from '@cloudbeaver/plugin-sql-editor';
import { ACTION_SQL_EDITOR_OPEN, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE';
@@ -82,6 +83,7 @@ export class DDLViewerFooterService {
await this.sqlEditorNavigatorService.openNewEditor({
name,
dataSourceKey: LocalStorageSqlDataSource.key,
connectionKey: connection && createConnectionParam(connection),
catalogId: container.catalogId,
schemaId: container.schemaId,
@@ -14,7 +14,7 @@ import { ConnectionDialectResource, ConnectionInfoActiveProjectKey, ConnectionIn
import { MenuBar, MENU_BAR_DEFAULT_STYLES } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree';
import { SQLCodeEditorLoader } from '@cloudbeaver/plugin-sql-editor-new';
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
import { TAB_PANEL_STYLES } from '../TAB_PANEL_STYLES';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from './DATA_CONTEXT_DDL_VIEWER_NODE';
@@ -32,6 +32,7 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio
const connection = connectionInfoResource.resource.getConnectionForNode(nodeId);
const connectionParam = connection ? createConnectionParam(connection) : null;
const connectionDialectResource = useResource(DDLViewerTabPanel, ConnectionDialectResource, connectionParam);
const sqlDialect = useSqlDialectExtension(connectionDialectResource.data);
menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId);
menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, ddlResource.data);
@@ -40,6 +41,7 @@ export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(functio
<wrapper>
<SQLCodeEditorLoader
value={ddlResource.data ?? ''}
extensions={[sqlDialect]}
readonly
/>
<MenuBar menu={menu} style={MENU_BAR_DEFAULT_STYLES} />
@@ -14,7 +14,7 @@ import { ConnectionDialectResource, ConnectionInfoActiveProjectKey, ConnectionIn
import { MenuBar, MENU_BAR_DEFAULT_STYLES } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import type { NavNodeTransformViewComponent } from '@cloudbeaver/plugin-navigation-tree';
import { SQLCodeEditorLoader } from '@cloudbeaver/plugin-sql-editor';
import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new';
import { DATA_CONTEXT_DDL_VIEWER_NODE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_NODE';
import { DATA_CONTEXT_DDL_VIEWER_VALUE } from '../DdlViewer/DATA_CONTEXT_DDL_VIEWER_VALUE';
@@ -38,6 +38,7 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer
const connection = connectionInfoResource.resource.getConnectionForNode(nodeId);
const connectionParam = connection ? createConnectionParam(connection) : null;
const connectionDialectResource = useResource(ExtendedDDLViewerTabPanel, ConnectionDialectResource, connectionParam);
const sqlDialect = useSqlDialectExtension(connectionDialectResource.data);
menu.context.set(DATA_CONTEXT_DDL_VIEWER_NODE, nodeId);
menu.context.set(DATA_CONTEXT_DDL_VIEWER_VALUE, extendedDDLResource.data);
@@ -45,11 +46,8 @@ export const ExtendedDDLViewerTabPanel: NavNodeTransformViewComponent = observer
return styled(style)(
<wrapper>
<SQLCodeEditorLoader
bindings={{
autoCursor: false,
}}
value={extendedDDLResource.data}
dialect={connectionDialectResource.data}
value={extendedDDLResource.data ?? ''}
extensions={[sqlDialect]}
readonly
/>
<MenuBar menu={menu} style={MENU_BAR_DEFAULT_STYLES} />
@@ -15,7 +15,6 @@ import { LocalStorageSaveService } from '@cloudbeaver/core-settings';
interface IDevToolsSettings {
enabled: boolean;
distributed: boolean;
codemirror: boolean;
}
const DEVTOOLS = 'devtools';
@@ -30,10 +29,6 @@ export class DevToolsService {
return this.settings.distributed;
}
get isCodemirror(): boolean {
return this.settings.codemirror;
}
private readonly settings: IDevToolsSettings;
constructor(
@@ -59,10 +54,6 @@ export class DevToolsService {
this.syncDistributedMode();
}
setCodemirror(value: boolean) {
this.settings.codemirror = value;
}
private syncDistributedMode() {
if (this.isEnabled) {
if (this.serverConfigResource.data) {
@@ -76,6 +67,5 @@ function getDefaultDevToolsSettings(): IDevToolsSettings {
return {
enabled: false,
distributed: false,
codemirror: false,
};
}
@@ -15,7 +15,6 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile';
import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS';
import { ACTION_DEVTOOLS_CODEMIRROR } from './actions/ACTION_DEVTOOLS_CODEMIRROR';
import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED';
import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH';
import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem';
@@ -112,7 +111,6 @@ export class PluginBootstrap extends Bootstrap {
return [
new SearchResourceMenuItem(),
ACTION_DEVTOOLS_MODE_DISTRIBUTED,
ACTION_DEVTOOLS_CODEMIRROR,
MENU_PLUGINS,
...items,
];
@@ -128,15 +126,6 @@ export class PluginBootstrap extends Bootstrap {
},
});
this.actionService.addHandler({
id: 'devtools-codemirror',
isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_CODEMIRROR,
isChecked: () => this.devToolsService.isCodemirror,
handler: () => {
this.devToolsService.setCodemirror(!this.devToolsService.isCodemirror);
},
});
this.menuService.addCreator({
isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_PLUGINS,
getItems: (context, items) => [
@@ -1,15 +0,0 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view';
export const ACTION_DEVTOOLS_CODEMIRROR = createAction('devtools-codemirror', {
type: 'checkbox',
label: 'New Code mirror',
tooltip: 'Enable New Code mirror',
});
@@ -41,6 +41,8 @@ const styles = css`
map {
flex: 1 1 auto;
border-radius: var(--theme-group-element-radius);
overflow: hidden;
}
toolbar {
@@ -6,21 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { createComplexLoader, ComplexLoader } from '@cloudbeaver/core-blocks';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import type { IDataValuePanelProps, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer';
const loader = createComplexLoader(async function loader() {
const { GISValuePresentation } = await import('./GISValuePresentation');
return { GISValuePresentation };
});
import { GISValuePresentation } from './GISValuePresentation';
export const GISViewer: TabContainerPanelComponent<IDataValuePanelProps<any, IDatabaseResultSet>> = function GISViewer({ model, resultIndex }) {
return (
<ComplexLoader loader={loader}>
{({ GISValuePresentation }) => (
<GISValuePresentation model={model} resultIndex={resultIndex} />
)}
</ComplexLoader>
<GISValuePresentation model={model} resultIndex={resultIndex} />
);
};
@@ -6,13 +6,20 @@
* you may not use this file except in compliance with the License.
*/
import { lazy } from 'react';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DataValuePanelService, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer';
import { GISViewer } from './GISViewer';
import { ResultSetGISAction } from './ResultSetGISAction';
const GISViewer = lazy(async () => {
const { GISViewer } = await import('./GISViewer');
return { default: GISViewer };
});
@injectable()
export class GISViewerBootstrap extends Bootstrap {
constructor(private readonly dataValuePanelService: DataValuePanelService) {
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useDeferredValue, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { ConnectionImageWithMask, getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { ConnectionImageWithMask, getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES, useObjectRef, useMouseContextMenu, Loader } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, NavTreeResource, EObjectFeature, type INodeActions } from '@cloudbeaver/core-navigation-tree';
@@ -41,6 +41,12 @@ const styles = css`
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
&[|editing] {
padding: 0;
overflow: visible;
margin-left: 2px;
}
}
portal {
position: relative;
@@ -60,6 +66,7 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
dndElement,
dndPlaceholder,
}, ref) {
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const treeContext = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
@@ -95,6 +102,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
function handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
mouseContextMenu.handleContextMenuOpen(event);
treeNodeContext.select();
}
const expandable = useDeferredValue(getComputed(() => treeContext?.tree.isNodeExpandable(node.id) ?? true));
const filterActive = useDeferredValue(getComputed(() => treeContext?.tree.filtering));
@@ -105,22 +117,30 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
ref={ref}
{...attributes}
onClick={onClickHandler}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, editing, dragging: dndElement })}
>
{expandable && <TreeNodeExpand filterActive={filterActive} />}
<TreeNodeIcon {...use({ connected })}>
<ConnectionImageWithMask icon={icon} connected={connected} maskId="tree-node-icon" />
</TreeNodeIcon>
<TreeNodeName title={node.name}>
{editing ? (
<NavigationNodeEditorLoader node={node} onClose={() => setEditing(false)} />
) : (
<name-box>{node.name}</name-box>
)}
<TreeNodeName title={node.name} {...use({ editing })}>
<Loader suspense inline fullSize>
{editing ? (
<NavigationNodeEditorLoader node={node} onClose={() => setEditing(false)} />
) : (
<name-box>{node.name}</name-box>
)}
</Loader>
</TreeNodeName>
{!editing && !dndPlaceholder && (
<portal onClick={handlePortalClick}>
<TreeNodeMenuLoader node={node} actions={nodeActions} selected={selected} />
<TreeNodeMenuLoader
mouseContextMenu={mouseContextMenu}
node={node}
actions={nodeActions}
selected={selected}
/>
</portal>
)}
</TreeNodeControl>
@@ -8,12 +8,22 @@
import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import styled, { css } from 'reshadow';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag, NotificationService } from '@cloudbeaver/core-events';
import { type NavNode, NavTreeResource } from '@cloudbeaver/core-navigation-tree';
import { InlineEditor } from '@cloudbeaver/core-ui';
const styles = css`
InlineEditor {
height: 22px;
& input {
padding: 1px;
}
}
`;
interface Props {
node: NavNode;
@@ -49,19 +59,20 @@ export const NavigationNodeEditor = observer<Props>(function NavigationNodeEdito
EventContext.set(event, EventStopPropagationFlag);
}
return (
return styled(styles)(
<InlineEditor
value={name}
disabled={loading}
controlsPosition='inside'
style={styles}
simple
autofocus
onChange={setName}
onSave={save}
onDoubleClick={stopPropagation}
onReject={onClose}
onBlur={onClose}
onClick={stopPropagation}
onDoubleClick={stopPropagation}
/>
);
});
@@ -9,7 +9,7 @@
import { observer } from 'mobx-react-lite';
import styled, { use } from 'reshadow';
import { getComputed, Icon } from '@cloudbeaver/core-blocks';
import { getComputed, Icon, IMouseContextMenu } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { type NavNode, type INodeActions, DATA_CONTEXT_NAV_NODE } from '@cloudbeaver/core-navigation-tree';
@@ -25,12 +25,16 @@ interface Props {
node: NavNode;
actions?: INodeActions;
selected?: boolean;
mouseContextMenu?: IMouseContextMenu;
onClose?: () => void;
}
export const TreeNodeMenu = observer<Props>(function TreeNodeMenu({
node,
actions,
selected,
mouseContextMenu,
onClose,
}) {
const connectionsInfoResource = useService(ConnectionInfoResource);
const menu = useMenu({ menu: MENU_NAV_TREE });
@@ -43,8 +47,20 @@ export const TreeNodeMenu = observer<Props>(function TreeNodeMenu({
menu.context.set(DATA_CONTEXT_CONNECTION, connection);
}
function handleVisibleSwitch(visible: boolean) {
if (!visible) {
onClose?.();
}
}
return styled(treeNodeMenuStyles)(
<ContextMenu menu={menu} {...use({ selected })} modal>
<ContextMenu
menu={menu}
mouseContextMenu={mouseContextMenu}
onVisibleSwitch={handleVisibleSwitch}
{...use({ selected })}
modal
>
<Icon name="snack" viewBox="0 0 16 10" />
</ContextMenu>
);
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef, useMouseContextMenu } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
@@ -61,6 +61,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
dndPlaceholder,
className,
}, ref) {
const mouseContextMenu = useMouseContextMenu();
const treeNodeContext = useContext(TreeNodeContext);
const elementsTreeContext = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
@@ -88,11 +89,16 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
treeNodeContext.select();
}
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
function handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
mouseContextMenu.handleContextMenuOpen(event);
treeNodeContext.select();
}
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
function onDbClickHandler(event: React.MouseEvent<HTMLDivElement>) {
function handleDbClick(event: React.MouseEvent<HTMLDivElement>) {
elementsTreeContext?.tree.open(node, navNodeInfoResource.getParents(node.id), false);
}
@@ -103,8 +109,9 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl
ref={ref}
onClick={onClickHandler}
onDoubleClick={onDbClickHandler}
onClick={handleClick}
onDoubleClick={handleDbClick}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, editing, dragging: dndElement })}
className={className}
>
@@ -117,7 +124,12 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
</TreeNodeName>
{!editing && !dndPlaceholder && (
<portal onClick={handlePortalClick}>
<TreeNodeMenuLoader node={node} actions={nodeActions} selected={selected} />
<TreeNodeMenuLoader
mouseContextMenu={mouseContextMenu}
node={node}
actions={nodeActions}
selected={selected}
/>
</portal>
)}
</TreeNodeControl>
@@ -111,23 +111,19 @@ export class NavNodeContextMenuService extends Bootstrap {
return node.features.includes(ENodeFeature.canDelete);
}
return [
ACTION_OPEN,
ACTION_REFRESH,
].includes(action);
},
isHidden: (context, action) => {
const node = context.get(DATA_CONTEXT_NAV_NODE);
if (action === ACTION_OPEN) {
// TODO: replace with loader, this call can lead to concurrent calls limit (Executor)
untracked(() => {
this.navNodeManagerService.canOpen(node.id, node.parentId); // trigger info
});
return !this.navNodeManagerService.getNavNodeCache(node.id).canOpen;
return this.navNodeManagerService.getNavNodeCache(node.id).canOpen;
}
return false;
return [
ACTION_OPEN,
ACTION_REFRESH,
].includes(action);
},
handler: async (context, action) => {
const node = context.get(DATA_CONTEXT_NAV_NODE);
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef, useMouseContextMenu } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
@@ -64,6 +64,7 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
dndPlaceholder,
className,
}, ref) {
const mouseContextMenu = useMouseContextMenu();
const viewContext = useContext(CaptureViewContext);
const elementsTreeContext = useContext(ElementsTreeContext);
const treeNodeContext = useContext(TreeNodeContext);
@@ -98,7 +99,12 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
treeNodeContext.select();
}
function onClickHandler(event: React.MouseEvent<HTMLDivElement>) {
function handleContextMenuOpen(event: React.MouseEvent<HTMLDivElement>) {
mouseContextMenu.handleContextMenuOpen(event);
treeNodeContext.select();
}
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
@@ -129,7 +135,8 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl
ref={ref}
onClick={onClickHandler}
onClick={handleClick}
onContextMenu={handleContextMenuOpen}
{...use({ outdated, editing, dragging: dndElement })}
className={className}
>
@@ -142,7 +149,12 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
</TreeNodeName>
{!editing && !dndPlaceholder && (
<portal onClick={handlePortalClick}>
<TreeNodeMenuLoader node={node} actions={nodeActions} selected={selected} />
<TreeNodeMenuLoader
mouseContextMenu={mouseContextMenu}
node={node}
actions={nodeActions}
selected={selected}
/>
</portal>
)}
</TreeNodeControl>
@@ -17,7 +17,6 @@
},
"dependencies": {
"@cloudbeaver/plugin-codemirror6": "~0.1.0",
"@cloudbeaver/plugin-devtools": "~0.1.0",
"@cloudbeaver/plugin-sql-editor": "~0.1.0",
"@cloudbeaver/core-blocks": "~0.1.0",
"@cloudbeaver/core-di": "~0.1.0"
@@ -6,8 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { StateField, StateEffect } from '@codemirror/state';
import { EditorView, Decoration } from '@codemirror/view';
import { StateEffect, Decoration, StateField, EditorView, DecorationSet } from '@cloudbeaver/plugin-codemirror6';
const ACTIVE_QUERY_EFFECT_ADD = StateEffect.define<{ from: number; to: number | undefined }>({
map: (val, mapping) => ({
@@ -22,7 +22,7 @@ const ACTIVE_QUERY_DECORATION = Decoration.mark({
class: 'active-query',
});
export const ACTIVE_QUERY_EXTENSION = StateField.define({
export const ACTIVE_QUERY_EXTENSION: StateField<DecorationSet> = StateField.define({
create() {
return Decoration.none;
},

Some files were not shown because too many files have changed in this diff Show More