diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index b0efafba55..0c2ab0ff49 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -75,6 +75,16 @@ type WebServiceConfig { bundleVersion: String! } +type ProductInfo { + id: ID! + version: String! + name: String! + description: String + buildTime: String! + releaseTime: String! + licenseInfo: String +} + type ServerConfig { name: String! version: String! @@ -100,6 +110,7 @@ type ServerConfig { supportedLanguages: [ ServerLanguage! ]! services: [ WebServiceConfig ] productConfiguration: Object! + productInfo: ProductInfo! defaultNavigatorSettings: NavigatorSettings! } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java new file mode 100644 index 0000000000..1121b7c81b --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java @@ -0,0 +1,76 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import io.cloudbeaver.server.CBApplication; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; + +import java.text.DateFormat; +import java.util.Date; + +/** + * Web server configuration + */ +public class WebProductInfo { + + public WebProductInfo() { + + } + + @Property + public String getId() { + return CommonUtils.notEmpty(Platform.getProduct().getId()); + } + + @Property + public String getVersion() { + return GeneralUtils.getProductVersion().toString(); + } + + @Property + public String getName() { + return CommonUtils.notEmpty(Platform.getProduct().getName()); + } + + @Property + public String getDescription() { + return CommonUtils.notEmpty(Platform.getProduct().getDescription()); + } + + @Property + public String getBuildTime() { + Date buildTime = GeneralUtils.getProductBuildTime(); + if (buildTime == null) { + buildTime = new Date(); + } + return DateFormat.getDateInstance(DateFormat.LONG).format(buildTime); + } + + @Property + public String getReleaseTime() { + return DateFormat.getDateInstance(DateFormat.LONG).format(GeneralUtils.getProductReleaseDate()); + } + + @Property + public String getLicenseInfo() { + return CBApplication.getInstance().getInfoDetails(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 0976101877..2e8d055377 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -146,4 +146,9 @@ public class WebServerConfig { return application.getAppConfiguration().getDefaultNavigatorSettings(); } + @Property + public WebProductInfo getProductInfo() { + return new WebProductInfo(); + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 71d9ead53c..8e910da128 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -465,7 +465,7 @@ public class CBApplication extends BaseApplicationImpl { @Override public String getInfoDetails() { - return serverName; + return ""; } @Override diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index 92d2cb6cea..8591e26aa2 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -6,15 +6,12 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { Loader } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CoreSettingsService } from '../CoreSettingsService'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; import { useChildren } from '../shared/useChildren'; import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement'; @@ -42,7 +39,7 @@ interface Props { emptyPlaceholder: React.FC; className?: string; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => boolean; + onSelect?: (node: NavNode, multiple: boolean) => void; isSelected?: (node: NavNode) => boolean; } @@ -55,10 +52,8 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ onSelect, isSelected, }) { - const config = useService(CoreSettingsService); const nodeChildren = useChildren(root); const Placeholder = emptyPlaceholder; - const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]); const context = useMemo( () => ({ control, onOpen, onSelect, isSelected }), @@ -80,7 +75,7 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ return styled(styles)( - {nodeChildren.children.slice(0, limit.get()).map(id => ( + {nodeChildren.children.map(id => ( ))} diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index 6e82e7f876..1357a6ecd3 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -29,11 +29,11 @@ export class NavigationTreeContextMenuService { private connectionInfoResource: ConnectionInfoResource ) { } - getMenuToken() { + getMenuToken(): string { return NavigationTreeContextMenuService.menuToken; } - getNodeViewMenuItemToken() { + getNodeViewMenuItemToken(): string { return NavigationTreeContextMenuService.nodeViewMenuItemToken; } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx index 3b469ca521..24ae84d5e9 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx @@ -6,16 +6,13 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; import styled from 'reshadow'; import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; -import { CoreSettingsService } from '../../../CoreSettingsService'; import { useChildren } from '../../../shared/useChildren'; interface Props { @@ -29,11 +26,9 @@ export const NavigationNodeNested: React.FC = observer(function Navigatio nodeId, component, }) { - const config = useService(CoreSettingsService); const styles = useStyles(TREE_NODE_STYLES); const context = useContext(TreeNodeContext); const children = useChildren(nodeId); - const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]); if (!children.children || !context?.expanded) { return null; @@ -43,7 +38,7 @@ export const NavigationNodeNested: React.FC = observer(function Navigatio return styled(styles)( - {children.children.slice(0, limit.get()).map(child => )} + {children.children.map(child => )} ); }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 80d5e4bf48..c3f442d4d4 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -27,18 +27,18 @@ interface INavigationNode { leaf: boolean; handleExpand: () => void; handleOpen: () => void; - handleSelect: (isMultiple?: boolean) => boolean; + handleSelect: (isMultiple?: boolean) => void; } export function useNavigationNode(node: NavNode): INavigationNode { const context = useContext(TreeContext); const navigationTreeService = useService(NavigationTreeService); const [processing, setProcessing] = useState(false); - const [isExpanded, switchExpand] = useState(false); const { isLoading, isOutdated } = useNode(node.id); const children = useChildren(node.id); const loading = isLoading() || children.isLoading() || processing; + const isExpanded = navigationTreeService.isNodeExpanded(node.id); let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated()); let expanded = isExpanded && !leaf; @@ -60,11 +60,11 @@ export function useNavigationNode(node: NavNode): INavigationNode { clearTimeout(timeout); setProcessing(false); if (!state) { - switchExpand(false); + navigationTreeService.expandNode(node.id, false); return; } } - switchExpand(!expanded); + navigationTreeService.expandNode(node.id, !expanded); }; const handleOpen = async () => { @@ -76,7 +76,9 @@ export function useNavigationNode(node: NavNode): INavigationNode { } }; - const handleSelect = (multiple = false) => context?.onSelect?.(node, multiple) || false; + const handleSelect = (multiple = false) => { + context?.onSelect?.(node, multiple); + }; // TODO: probably should be refactored useEffect(() => { @@ -87,7 +89,7 @@ export function useNavigationNode(node: NavNode): INavigationNode { .then(state => { setProcessing(false); if (!state) { - switchExpand(false); + navigationTreeService.expandNode(node.id, false); } }); } @@ -97,7 +99,12 @@ export function useNavigationNode(node: NavNode): INavigationNode { if (node && context?.isSelected?.(node)) { context.onSelect?.(node, true); } - }, [context, node]); + }, [context, node.id]); + + useEffect(() => () => { + // TODO: seems like selection & expand should be specific for separate tree definitions + navigationTreeService.expandNode(node.id, false); + }, [node.id]); return { control: context?.control, diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 3eeee5654f..1299db75af 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -6,43 +6,91 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; -import { Subject, Observable } from 'rxjs'; +import { action, computed, makeObservable } from 'mobx'; import { ConnectionAuthService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; import type { IActiveView } from '@cloudbeaver/core-view'; +import { CoreSettingsService } from '../CoreSettingsService'; import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService'; -import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerService'; +import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils'; +export interface INavigationNodeMetadata { + selected: boolean; + expanded: boolean; +} + +export interface INavigationNodeSelectionData { + id: string; + selected: boolean; +} + @injectable() export class NavigationTreeService { - readonly selectedNodes = observable.array([]); - readonly onNodeSelect: Observable<[string, boolean]>; + readonly navigationTreeMetadata: MetadataMap; + readonly nodeSelectionTask: IExecutor; - private nodeSelectSubject: Subject<[string, boolean]>; + get childrenLimit(): number { + return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); + } constructor( private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private connectionAuthService: ConnectionAuthService, - private navNodeExtensionsService: NavNodeExtensionsService + private navNodeExtensionsService: NavNodeExtensionsService, + private navTreeResource: NavTreeResource, + private coreSettingsService: CoreSettingsService, + private navNodeInfoResource: NavNodeInfoResource ) { - this.nodeSelectSubject = new Subject(); - this.onNodeSelect = this.nodeSelectSubject.asObservable(); + makeObservable(this, { + childrenLimit: computed, + unselectAll: action, + }); + + this.nodeSelectionTask = new Executor(); + this.navigationTreeMetadata = new MetadataMap(() => ({ + selected: false, + expanded: false, + })); this.getView = this.getView.bind(this); + + this.navNodeInfoResource.onItemDelete.addHandler(key => { + ResourceKeyUtils.forEach(key, key => { + this.navigationTreeMetadata.delete(key); + }); + }); + this.navTreeResource.onItemDelete.addHandler(key => { + ResourceKeyUtils.forEach(key, key => { + this.expandNode(key, false); + }); + }); } - async navToNode(id: string, parentId: string) { + getChildren(id: string): string[] | undefined { + const children = this.navTreeResource.get(id); + + if (children) { + return children.slice(0, this.childrenLimit); + } + + return children; + } + + async navToNode(id: string, parentId: string): Promise { await this.navNodeManagerService.navToNode(id, parentId); } - async loadNestedNodes(id = ROOT_NODE_PATH) { + async loadNestedNodes(id = ROOT_NODE_PATH): Promise { try { if (this.isConnectionNode(id) && !await this.tryInitConnection(id)) { return false; @@ -55,39 +103,56 @@ export class NavigationTreeService { return false; } - selectNode(id: string, multiple?: boolean): boolean { + async selectNode(id: string, multiple?: boolean): Promise { if (!multiple) { - for (const id of this.selectedNodes) { - this.nodeSelectSubject.next([id, false]); - } - this.selectedNodes.clear(); + await this.unselectAll(); } - if (!this.isNodeSelected(id)) { - this.selectedNodes.push(id); - this.nodeSelectSubject.next([id, true]); - return true; - } - this.selectedNodes.remove(id); - this.nodeSelectSubject.next([id, false]); - return false; + + const metadata = this.navigationTreeMetadata.get(id); + metadata.selected = !metadata.selected; + + await this.nodeSelectionTask.execute({ + id, + selected: metadata.selected, + }); } - isNodeSelected(navNodeId: string) { - return this.selectedNodes.includes(navNodeId); + isNodeExpanded(navNodeId: string): boolean { + return this.navigationTreeMetadata.get(navNodeId).expanded; + } + + isNodeSelected(navNodeId: string): boolean { + return this.navigationTreeMetadata.get(navNodeId).selected; + } + + expandNode(navNodeId: string, state: boolean): void { + const metadata = this.navigationTreeMetadata.get(navNodeId); + metadata.expanded = state; } getView(): IActiveView | null { - if (!this.selectedNodes.length) { + const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected); + + if (!element) { return null; } - const context = this.selectedNodes[0]; return { - context, + context: element[0], extensions: this.navNodeExtensionsService.extensions, }; } + private async unselectAll() { + for (const [id, metadata] of this.navigationTreeMetadata) { + metadata.selected = false; + await this.nodeSelectionTask.execute({ + id, + selected: false, + }); + } + } + private isConnectionNode(navNodeId: string) { const node = this.navNodeManagerService.getNode(navNodeId); return node?.objectFeatures.includes(EObjectFeature.dataSource); diff --git a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts index 97e7e217c1..0abbdec834 100644 --- a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts +++ b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts @@ -15,7 +15,7 @@ export interface ITreeContext { node: NavNode; }>; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => boolean; + onSelect?: (node: NavNode, multiple: boolean) => void; isSelected?: (node: NavNode) => boolean; } diff --git a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts index 14d08e89e7..8883ec0a03 100644 --- a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts @@ -16,7 +16,7 @@ import { NavigationTreeService } from './NavigationTreeService'; interface INavigationTree { isSelected: (node: NavNode) => boolean; handleOpen: (node: NavNode) => Promise; - handleSelect: (node: NavNode, isMultiple: boolean) => boolean; + handleSelect: (node: NavNode, isMultiple: boolean) => void; } export function useNavigationTree(): INavigationTree { diff --git a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx index 2953adc0f4..bbf7af7906 100644 --- a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx +++ b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx @@ -13,9 +13,9 @@ import { import { useImperativeHandle } from 'react'; import styled, { use } from 'reshadow'; -import { Icon } from '@cloudbeaver/core-blocks'; +import { Icon, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { useStyles } from '@cloudbeaver/core-theming'; import { EditorDialog } from './EditorDialog'; @@ -64,33 +64,39 @@ export const InlineEditor = observer onReject, className, }, ref) { + const props = useObjectRef({ + onChange, + onReject, + onSave, + value, + }); + const commonDialogService = useService(CommonDialogService); - const handleChange = useCallback((event: ChangeEvent| string) => { - const newValue = typeof event === 'string' ? event : event.target.value; - onChange(newValue); - }, [onChange]); + const handleChange = useCallback((event: ChangeEvent) => { + props.onChange(event.target.value); + }, []); const handlePopup = useCallback(async () => { - const newValue = await commonDialogService.open(EditorDialog, value); - if (typeof newValue === 'string') { - handleChange(newValue); - onSave(); - } else if (onReject) { - onReject(); + const newValue = await commonDialogService.open(EditorDialog, props.value); + if (newValue === DialogueStateResult.Rejected || newValue === DialogueStateResult.Resolved) { + props.onReject?.(); + } else { + props.onChange(newValue); + props.onSave(); } - }, [value, commonDialogService, onSave, onReject, handleChange]); + }, []); const handleKeyDown = useCallback((event: React.KeyboardEvent) => { switch (event.key) { case 'Enter': - onSave(); + props.onSave(); break; case 'Escape': - onReject?.(); + props.onReject?.(); break; } - }, [onSave, onReject]); + }, []); const inputRef = useRef(null); diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 082eb22bc3..452bd334d6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -332,20 +332,23 @@ export class NavNodeManagerService extends Bootstrap { }; async updateRoot(): Promise { - if (await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (enabled) { await this.navTree.refresh(ROOT_NODE_PATH); } } async refreshRoot(): Promise { this.navTree.delete(ROOT_NODE_PATH); - if (await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (enabled) { await this.navTree.refresh(ROOT_NODE_PATH); } } private async connectionCreateHandler(connection: Connection) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -360,7 +363,8 @@ export class NavNodeManagerService extends Bootstrap { } private async connectionRefreshHandler(connections: Connection[]) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -368,7 +372,8 @@ export class NavNodeManagerService extends Bootstrap { } private async connectionUpdateHandler(key: ResourceKey) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -430,7 +435,8 @@ export class NavNodeManagerService extends Bootstrap { } private async isNavTreeEnabled() { - if (!await this.permissionsService.hasAsync(EPermission.public)) { + const active = await this.permissionsService.hasAsync(EPermission.public); + if (!active) { return false; } diff --git a/webapp/packages/core-app/src/shared/useChildren.ts b/webapp/packages/core-app/src/shared/useChildren.ts index 1fef9d8b33..0cf5d40acf 100644 --- a/webapp/packages/core-app/src/shared/useChildren.ts +++ b/webapp/packages/core-app/src/shared/useChildren.ts @@ -10,6 +10,7 @@ import { useCallback } from 'react'; import { useService } from '@cloudbeaver/core-di'; +import { NavigationTreeService } from '../NavigationTree/NavigationTreeService'; import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource'; import { NavTreeResource } from './NodesManager/NavTreeResource'; @@ -21,8 +22,9 @@ interface Hook { } export function useChildren(navNodeId = ROOT_NODE_PATH): Hook { + const navTreeService = useService(NavigationTreeService); const navTreeResource = useService(NavTreeResource); - const children = navTreeResource.get(navNodeId); + const children = navTreeService.getChildren(navNodeId); const deps = [navNodeId]; diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index e2f1ac650e..72930be907 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -7,9 +7,12 @@ */ import styled, { css } from 'reshadow'; +import { use } from 'reshadow'; import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { Loader } from './Loader/Loader'; + const buttonStyles = composes( css` Button { @@ -22,7 +25,31 @@ const buttonStyles = composes( css` Button { display: flex; + + & Loader, & button-label { + transition: opacity cubic-bezier(0.4, 0.0, 0.2, 1) 0.3s; + } + + & Loader { + position: absolute; + opacity: 0; + } + + & button-label { + opacity: 1; + } + + &[|loading] { + & Loader { + opacity: 1; + } + + & button-label { + opacity: 0; + } + } } + ` ); @@ -61,6 +88,7 @@ type ButtonProps = ( React.ButtonHTMLAttributes & React.LinkHTMLAttributes ) & { + loading?: boolean; mod?: Array; tag?: 'button' | 'a'; href?: string; @@ -72,10 +100,20 @@ export const Button: React.FC = function Button({ children, mod, tag = 'button', + disabled = false, + loading, ...rest }) { + if (loading) { + disabled = true; + } + const Button = tag; return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))( - + ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index 5000087835..486d8b5ac3 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -7,8 +7,8 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext } from 'react'; -import styled, { use } from 'reshadow'; +import { useCallback, useContext, useState } from 'react'; +import { use } from 'reshadow'; import type { IGridItemsLayoutProps } from '../../Containers/ILayoutContainerProps'; import { FormContext } from '../FormContext'; @@ -62,6 +62,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ onChange, ...rest }: CheckboxControlledProps | CheckboxObjectProps) { + const [count, refresh] = useState(0); const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { if (state) { @@ -84,7 +85,8 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ if (context) { context.onChange(event.target.checked, name); } - }, [state, name, value, onChange, context]); + refresh(count + 1); + }, [state, name, value, onChange, context, count]); if (autoHide && !isControlPresented(name, state)) { return null; @@ -104,7 +106,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ checked = checked.includes(value); } - return styled()( + return ( , HTMLFormElement>, 'onChange'> & { + disabled?: boolean; onChange?: (value: string | number | boolean | null | undefined, name: string | undefined) => void; }; export const SubmittingForm = forwardRef(function SubmittingForm( { + disabled, children, onSubmit, onChange = () => {}, @@ -34,9 +36,11 @@ export const SubmittingForm = forwardRef(fun return (
- - {children} - +
+ + {children} + +