diff --git a/webapp/packages/core-app/public/favicon.png b/webapp/packages/core-app/public/favicon.png index a5db6fe383..4a7b780549 100644 Binary files a/webapp/packages/core-app/public/favicon.png and b/webapp/packages/core-app/public/favicon.png differ diff --git a/webapp/packages/core-blocks/src/ComplexLoader.tsx b/webapp/packages/core-blocks/src/ComplexLoader.tsx index 1318bc9eb3..13f83b0ec5 100644 --- a/webapp/packages/core-blocks/src/ComplexLoader.tsx +++ b/webapp/packages/core-blocks/src/ComplexLoader.tsx @@ -8,7 +8,7 @@ import { LoadingError } from '@cloudbeaver/core-utils'; -interface IComplexLoaderData { +export interface IComplexLoaderData { promise: Promise | undefined; data: T | undefined; error: Error | undefined; @@ -21,6 +21,16 @@ export interface ComplexLoaderProps { children: (content: T) => JSX.Element; } +export function useComplexLoader(loader: IComplexLoaderData): T { + if (loader.error) { + throw loader.error; + } + if (loader.data) { + return loader.data; + } + throw loader.loader(); +} + export const ComplexLoader: React.FC> = function ComplexLoader(props) { if (props.loader.error) { throw props.loader.error; diff --git a/webapp/packages/core-blocks/src/FormControls/FormContext.ts b/webapp/packages/core-blocks/src/FormControls/FormContext.ts index 03be5053c1..a93a43d856 100644 --- a/webapp/packages/core-blocks/src/FormControls/FormContext.ts +++ b/webapp/packages/core-blocks/src/FormControls/FormContext.ts @@ -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) => void; export interface IChangeData { value: FormChangeValues; @@ -21,6 +22,7 @@ export interface IChangeData { export interface IFormContext { changeExecutor: IExecutor; change: FormChangeHandler; + keyDown: KeyHandler; } export const FormContext = createContext(null); diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx index 87ca8567f1..b7e05d785d 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -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; diff --git a/webapp/packages/core-blocks/src/FormControls/RadioGroupContext.ts b/webapp/packages/core-blocks/src/FormControls/RadioGroupContext.ts index 78699db8e3..5e0979f4a2 100644 --- a/webapp/packages/core-blocks/src/FormControls/RadioGroupContext.ts +++ b/webapp/packages/core-blocks/src/FormControls/RadioGroupContext.ts @@ -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(undefined); diff --git a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx index 1abb819524..226927faea 100644 --- a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx +++ b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx @@ -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, HTMLFormElement>, 'onChange' | 'onSubmit'> & { disabled?: boolean; disableEnterSubmit?: boolean; - onSubmit?: (event: React.FormEvent) => Promise | void; + onSubmit?: (event?: React.FormEvent) => Promise | void; onChange?: FormChangeHandler; }; @@ -38,6 +39,8 @@ export const SubmittingForm = forwardRef(fun }, ref ) { + const [formRef, setFormInnerRef] = useState(null); + const setFormRef = useCombinedRef(setFormInnerRef, ref); let [disabled, setDisabled] = useState(false); disabled = disabled || disabledProp || false; @@ -56,6 +59,8 @@ export const SubmittingForm = forwardRef(fun } }, }), { + formRef, + disableEnterSubmit, parentContext: useContext(FormContext), onChange, onSubmit, @@ -68,10 +73,25 @@ export const SubmittingForm = forwardRef(fun props.parentContext?.change(value, name); this.changeExecutor.execute({ value, name }); }, - }), false, ['change']); + keyDown(event: React.KeyboardEvent) { + if (event.key === 'Enter') { + const form = event.currentTarget.closest('form'); + if (form) { + event.preventDefault(); + const submitButton = form.querySelector( + 'button[type=submit]' + ); + if (submitButton) { + submitButton.click(); + } + } + } + props.parentContext?.keyDown(event); + }, + }), false, ['change', 'keyDown']); return ( -
props.handleSubmit(e)}> + props.handleSubmit(e)}>
{children} diff --git a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs index e26348c25b..8ee396a45b 100644 --- a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs +++ b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs @@ -1,6 +1,8 @@ + + diff --git a/webapp/packages/core-blocks/src/Loader/loaderStyles.ts b/webapp/packages/core-blocks/src/Loader/loaderStyles.ts index b7aebf14f4..1e4d53bce0 100644 --- a/webapp/packages/core-blocks/src/Loader/loaderStyles.ts +++ b/webapp/packages/core-blocks/src/Loader/loaderStyles.ts @@ -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); diff --git a/webapp/packages/core-blocks/src/Menu/Menu.tsx b/webapp/packages/core-blocks/src/Menu/Menu.tsx index b1badb4a31..d4c1ed915a 100644 --- a/webapp/packages/core-blocks/src/Menu/Menu.tsx +++ b/webapp/packages/core-blocks/src/Menu/Menu.tsx @@ -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, 'style'> { + mouseContextMenu?: IMouseContextMenu; label: string; items: React.ReactNode | (() => React.ReactNode); menuRef?: React.RefObject; @@ -38,6 +41,7 @@ interface IMenuProps extends Omit, 'style'> { } export const Menu = observer(forwardRef(function Menu({ + mouseContextMenu, label, items, menuRef, @@ -55,8 +59,18 @@ export const Menu = observer(forwardRef(function rtl, ...props }, ref) { + const innerMenuButtonRef = useRef(null); + const combinedRef = useCombinedRef(ref, innerMenuButtonRef); + const [relativePosition, setRelativePosition] = useState<{ x: number; y: number } | null>(null); + const menuButtonLinkRef = useRef(null); + const menuPanelRef = useRef(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(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)( - + {disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })} (forwardRef(function > {items} + {relativePosition && ( + + )} ); @@ -102,10 +168,17 @@ export const Menu = observer(forwardRef(function return styled(styles)( - + {children} (forwardRef(function > {items} + {relativePosition && ( + + )} ); diff --git a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx index d52c0d4b00..c18401e0cc 100644 --- a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx +++ b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx @@ -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(function MenuPanel({ +export const MenuPanel = observer(forwardRef(function MenuPanel({ label, menu, panelAvailable = true, @@ -43,7 +43,7 @@ export const MenuPanel = observer(function MenuPanel({ children, style, className, -}) { +}, ref) { const styles = useStyles(menuPanelStyles, style); const visible = menu.visible; @@ -63,7 +63,7 @@ export const MenuPanel = observer(function MenuPanel({ return styled(styles)( - + {Children.count(renderedChildren) === 0 && ( @@ -73,4 +73,4 @@ export const MenuPanel = observer(function MenuPanel({ ); -}); \ No newline at end of file +})); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts index dd3e907838..e5750318b2 100644 --- a/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts +++ b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts @@ -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; diff --git a/webapp/packages/core-blocks/src/Menu/useMouseContextMenu.ts b/webapp/packages/core-blocks/src/Menu/useMouseContextMenu.ts new file mode 100644 index 0000000000..191874a965 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/useMouseContextMenu.ts @@ -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) => void; +} + +export function useMouseContextMenu(): IMouseContextMenu { + return useObservableRef(() => ({ + position: null, + handleContextMenuOpen(event: React.MouseEvent) { + 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']); +} \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index b0bcae6775..7dd94b2491 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -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'; diff --git a/webapp/packages/core-blocks/src/TimerIcon.tsx b/webapp/packages/core-blocks/src/TimerIcon.tsx index 05abb68cff..8e8c7b97a7 100644 --- a/webapp/packages/core-blocks/src/TimerIcon.tsx +++ b/webapp/packages/core-blocks/src/TimerIcon.tsx @@ -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 { diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts index 7de4b93ee6..9214ba3501 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts @@ -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; diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 2312bfd78d..fcc0a35386 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -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'; diff --git a/webapp/packages/core-blocks/src/useCombinedRef.ts b/webapp/packages/core-blocks/src/useCombinedRef.ts new file mode 100644 index 0000000000..c17ea083d9 --- /dev/null +++ b/webapp/packages/core-blocks/src/useCombinedRef.ts @@ -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(...refs: React.ForwardedRef[]) { + return useCallback((instance: T) => { + for (const ref of refs) { + if (typeof ref === 'function') { + ref(instance); + } else if (ref) { + (ref as React.MutableRefObject).current = instance; + } + } + }, refs); +} \ No newline at end of file diff --git a/webapp/packages/core-browser/package.json b/webapp/packages/core-browser/package.json index d24bfabf52..4f31720110 100644 --- a/webapp/packages/core-browser/package.json +++ b/webapp/packages/core-browser/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/webapp/packages/core-view/src/Menu/IMenuActionsSet.ts b/webapp/packages/core-browser/src/IndexedDB/IndexedDB.ts similarity index 68% rename from webapp/packages/core-view/src/Menu/IMenuActionsSet.ts rename to webapp/packages/core-browser/src/IndexedDB/IndexedDB.ts index e31fd01e3d..33092542a8 100644 --- a/webapp/packages/core-view/src/Menu/IMenuActionsSet.ts +++ b/webapp/packages/core-browser/src/IndexedDB/IndexedDB.ts @@ -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 }; \ No newline at end of file diff --git a/webapp/packages/core-browser/src/IndexedDB/IndexedDBService.ts b/webapp/packages/core-browser/src/IndexedDB/IndexedDBService.ts new file mode 100644 index 0000000000..8621604aa0 --- /dev/null +++ b/webapp/packages/core-browser/src/IndexedDB/IndexedDBService.ts @@ -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 + } +} \ No newline at end of file diff --git a/webapp/packages/core-browser/src/index.ts b/webapp/packages/core-browser/src/index.ts index f394350ce0..0e941cae55 100644 --- a/webapp/packages/core-browser/src/index.ts +++ b/webapp/packages/core-browser/src/index.ts @@ -1,3 +1,5 @@ +export * from './IndexedDB/IndexedDBService'; +export * from './IndexedDB/IndexedDB'; export * from './CookiesService'; export * from './manifest'; export * from './ServiceWorkerService'; \ No newline at end of file diff --git a/webapp/packages/core-browser/src/manifest.ts b/webapp/packages/core-browser/src/manifest.ts index 00c3b6b897..ff9d15d8b7 100644 --- a/webapp/packages/core-browser/src/manifest.ts +++ b/webapp/packages/core-browser/src/manifest.ts @@ -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, ], }; diff --git a/webapp/packages/core-cli/bin/dependency-graph.js b/webapp/packages/core-cli/bin/dependency-graph.js new file mode 100644 index 0000000000..ad611b10b4 --- /dev/null +++ b/webapp/packages/core-cli/bin/dependency-graph.js @@ -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); + }); \ No newline at end of file diff --git a/webapp/packages/core-cli/configs/excludedFromVendor.js b/webapp/packages/core-cli/configs/excludedFromVendor.js index 5ba6589fcc..91866498ce 100644 --- a/webapp/packages/core-cli/configs/excludedFromVendor.js +++ b/webapp/packages/core-cli/configs/excludedFromVendor.js @@ -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/*', ]; \ No newline at end of file diff --git a/webapp/packages/core-cli/package.json b/webapp/packages/core-cli/package.json index c8c8099614..00ad03f17a 100644 --- a/webapp/packages/core-cli/package.json +++ b/webapp/packages/core-cli/package.json @@ -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" } } diff --git a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx index ff2d9dd04b..8e2ae9f551 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialog/CommonDialogWrapper.tsx @@ -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(function CommonDialogWrapper({ +export const CommonDialogWrapper = observer(forwardRef(function CommonDialogWrapper({ size = 'medium', fixedSize, fixedWidth, @@ -36,10 +36,9 @@ export const CommonDialogWrapper = observer(function C className, children, style, -}) { +}, ref) { const context = useContext(DialogContext); const dialogState = useDialogState({ visible: true }); - const focusedElementRef = useRef(null); useEffect(() => { if (!dialogState.visible && !context.dialog.options?.persistent) { @@ -47,18 +46,15 @@ export const CommonDialogWrapper = observer(function C } }); - useEffect(() => { - if (document.activeElement instanceof HTMLElement) { - focusedElementRef.current = document.activeElement; - } - - return () => { - focusedElementRef.current?.focus(); - }; - }, []); - return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, dialogStyles, style))( - + {children} @@ -66,4 +62,4 @@ export const CommonDialogWrapper = observer(function C ); -}); +})); diff --git a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx index 579ff6e820..4c71635bed 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx @@ -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({ focusFirstChild: true }); const { icon, title, subTitle, bigIcon, viewBox, message, confirmActionText, cancelActionText } = payload; return styled(useStyles(style))( - + (null); let activeDialog: DialogInternal | undefined; @@ -42,6 +43,11 @@ export const DialogsPortal = observer(function DialogsPortal() { } }, backdropClick(e: React.MouseEvent) { + 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)( diff --git a/webapp/packages/core-settings/package.json b/webapp/packages/core-settings/package.json index 6000ae0d6e..0a06543a09 100644 --- a/webapp/packages/core-settings/package.json +++ b/webapp/packages/core-settings/package.json @@ -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" diff --git a/webapp/packages/core-settings/src/LocalStorageSaveService.ts b/webapp/packages/core-settings/src/LocalStorageSaveService.ts index e7429a82a3..9d29885428 100644 --- a/webapp/packages/core-settings/src/LocalStorageSaveService.ts +++ b/webapp/packages/core-settings/src/LocalStorageSaveService.ts @@ -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 | Map> { 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>; 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 | Record | string; +} + +class LocalStorageIndexedDB extends IndexedDB { + values!: IndexedDBTable; + + constructor() { + super('local-storage'); + this.version(1).stores({ + values: 'key', + }); + } + + count(): Promise { + return this.values.count(); + } + + async clear(): Promise { + await this.values.clear(); + } + + async getItem(key: string): Promise { + const record = await this.values.get(key); + + return record?.data ?? null; + } + + async removeItem(key: string): Promise { + await this.values.delete(key); + } + + async setItem(key: string, data: any): Promise { + await this.values.put({ key, data }); + } +} + class DataStorage | Map> implements ILocalStorageElement { 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 | Map> 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(this.store, key, value); - } - }); + for (const [key, value] of entries(parsed)) { + set(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 { + 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 | Map> 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 | Map> 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): string { return JSON.stringify(Array.from(map.entries())); } - private parseMap(data: string): Map { - return new Map(JSON.parse(data)); + private parseMap(data: string | Map): Map { + if (typeof data === 'string') { + return new Map(JSON.parse(data)); + } + + return data; } } diff --git a/webapp/packages/core-theming/src/styles/main/base.pure.css b/webapp/packages/core-theming/src/styles/main/base.pure.css index 378f0dac97..5b85507fa5 100644 --- a/webapp/packages/core-theming/src/styles/main/base.pure.css +++ b/webapp/packages/core-theming/src/styles/main/base.pure.css @@ -16,4 +16,4 @@ label { label[title] { text-decoration: none; -} \ No newline at end of file +} diff --git a/webapp/packages/core-theming/src/styles/main/normalize.pure.css b/webapp/packages/core-theming/src/styles/main/normalize.pure.css index 8e4b4da433..9b76547f4b 100644 --- a/webapp/packages/core-theming/src/styles/main/normalize.pure.css +++ b/webapp/packages/core-theming/src/styles/main/normalize.pure.css @@ -12,7 +12,8 @@ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; - /* 2 */ + overflow: hidden; + overscroll-behavior: none; } /* Sections diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx index 5d89d59c51..077c2d5dab 100644 --- a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx @@ -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, 'style' | 'children'> { + mouseContextMenu?: IMouseContextMenu; menu: IMenuData; style?: ComponentStyle; disclosure?: boolean; @@ -37,6 +38,7 @@ interface IContextMenuProps extends Omit, 'style' | 'c } export const ContextMenu = observer(forwardRef(function ContextMenu({ + mouseContextMenu, menu: menuData, disclosure, children, @@ -114,6 +116,7 @@ export const ContextMenu = observer(forwar menuRef={menu} modal={modal} visible={visible} + mouseContextMenu={mouseContextMenu} placement={placement} disabled={disabled} disclosure={disclosure} diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES.ts b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES.ts index 07fa10d899..e109d16dd1 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES.ts +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MENU_BAR_SMALL_STYLES.ts @@ -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; diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx index d018ecc2dc..97a8def6a1 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx @@ -200,9 +200,10 @@ const SubMenuItem = observer(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(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)( (function MenuIt + , 'onChange' | 'onKeyDown' | 'autoFocus'> { +export interface InlineEditorProps extends Omit, 'onChange' | 'onKeyDown' | 'autoFocus' | 'style'> { value: string; controlsPosition?: InlineEditorControls; simple?: boolean; @@ -33,6 +33,7 @@ export interface InlineEditorProps extends Omit void; onSave?: () => void; onReject?: () => void; @@ -54,6 +55,7 @@ export const InlineEditor = observer(forwar active, loading, disabled, + style, onChange, onSave, onUndo, @@ -110,8 +112,8 @@ export const InlineEditor = observer(forwar useImperativeHandle(ref, () => inputRef.current!); - return styled(useStyles(InlineEditorStyles))( - + return styled(useStyles(InlineEditorStyles, style))( + { 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)*/ diff --git a/webapp/packages/core-view/src/Menu/useMenu.ts b/webapp/packages/core-view/src/Menu/useMenu.ts index b7436f4b04..8f25d84714 100644 --- a/webapp/packages/core-view/src/Menu/useMenu.ts +++ b/webapp/packages/core-view/src/Menu/useMenu.ts @@ -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); diff --git a/webapp/packages/eslint-config/eslint-config.json b/webapp/packages/eslint-config/eslint-config.json index c3eb1bb7a6..ca261ae188 100644 --- a/webapp/packages/eslint-config/eslint-config.json +++ b/webapp/packages/eslint-config/eslint-config.json @@ -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", diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsDrawerItem.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsDrawerItem.tsx deleted file mode 100644 index 107f61df76..0000000000 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/Teams/TeamsDrawerItem.tsx +++ /dev/null @@ -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 = function TeamsDrawerItem({ - item, - onSelect, - style, - disabled, -}) { - return styled(useStyles(style))( - onSelect(item.name)} - > - - - - ); -}; diff --git a/webapp/packages/plugin-codemirror/package.json b/webapp/packages/plugin-codemirror/package.json deleted file mode 100644 index f2a970ebdf..0000000000 --- a/webapp/packages/plugin-codemirror/package.json +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx b/webapp/packages/plugin-codemirror/src/CodeEditor.tsx deleted file mode 100644 index abdee3802e..0000000000 --- a/webapp/packages/plugin-codemirror/src/CodeEditor.tsx +++ /dev/null @@ -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(function CodeEditor(props) { - const { autoFormat, className, editorDidMount } = props; - - const formatter = useAutoFormat(props.options?.mode); - const wrapperRef = useRef(null); - const editorRef = useRef(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))( - - - - ); -}); diff --git a/webapp/packages/plugin-codemirror/src/CodeEditorLoader.tsx b/webapp/packages/plugin-codemirror/src/CodeEditorLoader.tsx deleted file mode 100644 index 74ef34e5e4..0000000000 --- a/webapp/packages/plugin-codemirror/src/CodeEditorLoader.tsx +++ /dev/null @@ -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 = function CodeEditorLoader(props) { - return ( - - {({ CodeEditor }) => } - - ); -}; diff --git a/webapp/packages/plugin-codemirror/src/ICodeEditorProps.ts b/webapp/packages/plugin-codemirror/src/ICodeEditorProps.ts deleted file mode 100644 index 0072a3f368..0000000000 --- a/webapp/packages/plugin-codemirror/src/ICodeEditorProps.ts +++ /dev/null @@ -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; -} diff --git a/webapp/packages/plugin-codemirror/src/codemirror.hint.d.ts b/webapp/packages/plugin-codemirror/src/codemirror.hint.d.ts deleted file mode 100644 index d92518f184..0000000000 --- a/webapp/packages/plugin-codemirror/src/codemirror.hint.d.ts +++ /dev/null @@ -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; -} diff --git a/webapp/packages/plugin-codemirror/src/codemirror.meta.d.ts b/webapp/packages/plugin-codemirror/src/codemirror.meta.d.ts deleted file mode 100644 index 3a02b5533b..0000000000 --- a/webapp/packages/plugin-codemirror/src/codemirror.meta.d.ts +++ /dev/null @@ -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; -} diff --git a/webapp/packages/plugin-codemirror/src/formatValue.ts b/webapp/packages/plugin-codemirror/src/formatValue.ts deleted file mode 100644 index 395647450e..0000000000 --- a/webapp/packages/plugin-codemirror/src/formatValue.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/webapp/packages/plugin-codemirror/src/index.ts b/webapp/packages/plugin-codemirror/src/index.ts deleted file mode 100644 index 1d5ecdefed..0000000000 --- a/webapp/packages/plugin-codemirror/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './CodeEditorLoader'; -export * from './useAutoFormat'; diff --git a/webapp/packages/plugin-codemirror/src/theme/_base-code-editor.scss b/webapp/packages/plugin-codemirror/src/theme/_base-code-editor.scss deleted file mode 100644 index 5788cce211..0000000000 --- a/webapp/packages/plugin-codemirror/src/theme/_base-code-editor.scss +++ /dev/null @@ -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); - } - } -} diff --git a/webapp/packages/plugin-codemirror/src/theme/dark.module.scss b/webapp/packages/plugin-codemirror/src/theme/dark.module.scss deleted file mode 100644 index a9d280c25f..0000000000 --- a/webapp/packages/plugin-codemirror/src/theme/dark.module.scss +++ /dev/null @@ -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; -} diff --git a/webapp/packages/plugin-codemirror/src/theme/index.ts b/webapp/packages/plugin-codemirror/src/theme/index.ts deleted file mode 100644 index 15c73a4500..0000000000 --- a/webapp/packages/plugin-codemirror/src/theme/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './styles'; diff --git a/webapp/packages/plugin-codemirror/src/theme/light.module.scss b/webapp/packages/plugin-codemirror/src/theme/light.module.scss deleted file mode 100644 index f1f2cc4b51..0000000000 --- a/webapp/packages/plugin-codemirror/src/theme/light.module.scss +++ /dev/null @@ -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; -} diff --git a/webapp/packages/plugin-codemirror/src/theme/styles.ts b/webapp/packages/plugin-codemirror/src/theme/styles.ts deleted file mode 100644 index 5808478ec5..0000000000 --- a/webapp/packages/plugin-codemirror/src/theme/styles.ts +++ /dev/null @@ -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; -}; diff --git a/webapp/packages/plugin-codemirror/src/useAutoFormat.ts b/webapp/packages/plugin-codemirror/src/useAutoFormat.ts deleted file mode 100644 index 8fa3cc8c9c..0000000000 --- a/webapp/packages/plugin-codemirror/src/useAutoFormat.ts +++ /dev/null @@ -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 | 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 }); -} diff --git a/webapp/packages/plugin-codemirror6/package.json b/webapp/packages/plugin-codemirror6/package.json index 92e86eea8c..8b8c40334d 100644 --- a/webapp/packages/plugin-codemirror6/package.json +++ b/webapp/packages/plugin-codemirror6/package.json @@ -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": {} } diff --git a/webapp/packages/plugin-codemirror6/src/Editor.tsx b/webapp/packages/plugin-codemirror6/src/Editor.tsx index 8dec6754ae..9ab7da1a2f 100644 --- a/webapp/packages/plugin-codemirror6/src/Editor.tsx +++ b/webapp/packages/plugin-codemirror6/src/Editor.tsx @@ -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(function Editor(props, ref) { - const extensions = []; - - if (!props.extensions) { - extensions.push(getDefaultExtensions()); - } else { - extensions.push(props.extensions); - } +export const Editor = observer(forwardRef(function Editor({ + lineNumbers, + extensions = [], + ...rest +}, ref) { + const defaultExtensions = useEditorDefaultExtensions({ lineNumbers }); + const combinedExtensions = [...defaultExtensions]; + combinedExtensions.push(...extensions); return styled(EDITOR_BASE_STYLES)( - - + + ); })); \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx b/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx index ed1995cea4..487709b884 100644 --- a/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx +++ b/webapp/packages/plugin-codemirror6/src/EditorLoader.tsx @@ -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(function EditorLoader(props, ref) { +export const EditorLoader = forwardRef(function EditorLoader(props, ref) { return ( {({ Editor }) => } diff --git a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts index b7ee574cfe..a22ba1b5cc 100644 --- a/webapp/packages/plugin-codemirror6/src/IEditorRef.ts +++ b/webapp/packages/plugin-codemirror6/src/IEditorRef.ts @@ -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; } \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts index 06c39a4898..6c32b5f0f0 100644 --- a/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts +++ b/webapp/packages/plugin-codemirror6/src/IReactCodemirrorProps.ts @@ -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; diff --git a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx index 5a045993e0..ea814fb33a 100644 --- a/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx +++ b/webapp/packages/plugin-codemirror6/src/ReactCodemirror.tsx @@ -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(function ReactCodemirror({ +export const ReactCodemirror = observer(forwardRef(function ReactCodemirror({ + getValue, value, extensions, readonly, @@ -32,42 +34,44 @@ export const ReactCodemirror = forwardRef(fun onChange, onUpdate, }, ref) { + value = value ?? getValue?.(); const [container, setContainer] = useState(null); const [view, setView] = useState(null); - const [state] = useState(() => EditorState.create()); + const lastValueUpdate = useRef(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(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(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 (
); -}); \ No newline at end of file +})); \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/getDefaultExtensions.ts b/webapp/packages/plugin-codemirror6/src/getDefaultExtensions.ts index 743da96eb5..b57ea9b7f2 100644 --- a/webapp/packages/plugin-codemirror6/src/getDefaultExtensions.ts +++ b/webapp/packages/plugin-codemirror6/src/getDefaultExtensions.ts @@ -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 { + let extensions: Record = { + 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; } \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/index.ts b/webapp/packages/plugin-codemirror6/src/index.ts index aac5ec11d9..ed835db176 100644 --- a/webapp/packages/plugin-codemirror6/src/index.ts +++ b/webapp/packages/plugin-codemirror6/src/index.ts @@ -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'; \ No newline at end of file +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); diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss new file mode 100644 index 0000000000..5a84128762 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-autocompletion.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss new file mode 100644 index 0000000000..6d8429de78 --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor-tooltip.scss @@ -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); + } + } +} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss index f585fd9c2e..222da0cf4f 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/_base-code-editor.scss @@ -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; } -} +} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss b/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss index 4d0b76b639..921d84044e 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/dark.module.scss @@ -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; } diff --git a/webapp/packages/plugin-codemirror6/src/theme/light.module.scss b/webapp/packages/plugin-codemirror6/src/theme/light.module.scss index 19fbf75427..4d28833c25 100644 --- a/webapp/packages/plugin-codemirror6/src/theme/light.module.scss +++ b/webapp/packages/plugin-codemirror6/src/theme/light.module.scss @@ -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; } diff --git a/webapp/packages/plugin-codemirror6/src/useDefaultExtensions.ts b/webapp/packages/plugin-codemirror6/src/useDefaultExtensions.ts new file mode 100644 index 0000000000..7892fef28a --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/useDefaultExtensions.ts @@ -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>({}); + + const newExtensions = getDefaultExtensions(options); + const diff: Record = {}; + + 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); +} \ No newline at end of file diff --git a/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts b/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts new file mode 100644 index 0000000000..28c3f4e85d --- /dev/null +++ b/webapp/packages/plugin-codemirror6/src/useEditorAutocompletion.ts @@ -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, ComponentStyle]; + +export function useEditorAutocompletion(config?: Parameters[0]): UseEditorAutocompletionResult { + const [editor, setEditor] = useState(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('.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, + ]; +} diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 54e3f9bf3e..11f453b543 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -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'; diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index 5ef1b0aeb5..d36b8280a1 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -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", diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx index 8cc1a90330..54cb3205f1 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenu.tsx @@ -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(function TableFooterMenu({ {mainMenuService.constructMenuWithContext(model, resultIndex, simple).map((topItem, i) => ( ))} - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx index e6f5cd9753..7144b5c1e9 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuItem.tsx @@ -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; diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx index 2ca60cae77..062b6e580a 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableViewer.tsx @@ -58,7 +58,7 @@ const viewerStyles = css` flex-direction: column; overflow: hidden; } - Split { + Split:not([disable]) { gap: 8px; } Pane { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index 109f968a4a..2e09893b1c 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -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; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx index bc877d3315..a575307e72 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentation.tsx @@ -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> = observer(function TextValuePresentation({ model, resultIndex, @@ -180,12 +172,11 @@ export const TextValuePresentation: TabContainerPanelComponent getTypeExtension(state.currentContentType) ?? [], [state.currentContentType]); - return styled(style, textAreaStyles)( + return styled(style)( - {useCodeEditor ? ( - handleChange(value)} - /> - ) : ( -