mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-29 18:35:19 +08:00
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:
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -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/*',
|
||||
];
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-16
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
+2
-4
@@ -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",
|
||||
|
||||
-33
@@ -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",
|
||||
|
||||
+2
-8
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+13
-34
@@ -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
-2
@@ -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(
|
||||
|
||||
+10
-1
@@ -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} />
|
||||
|
||||
+4
-6
@@ -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) {
|
||||
|
||||
+28
-8
@@ -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>
|
||||
|
||||
+13
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
+18
-2
@@ -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>
|
||||
);
|
||||
|
||||
+18
-6
@@ -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>
|
||||
|
||||
+6
-10
@@ -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);
|
||||
|
||||
+16
-4
@@ -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
Reference in New Issue
Block a user