diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx index fe54e39f33..623537e6f2 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode.tsx @@ -34,6 +34,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat control, disabled, selected, + indeterminateSelected, loading, showInFilter, expanded, @@ -70,6 +71,7 @@ export const NavigationNode: NavigationNodeComponent = observer(function Navigat loading={loading} disabled={disabled} selected={selected} + indeterminateSelected={indeterminateSelected} expanded={expanded} showInFilter={showInFilter} externalExpanded={expandedExternal} diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts index 9a92caf4e7..7a6c9f023c 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/NavigationTreeNode/useNavigationNode.ts @@ -26,6 +26,7 @@ interface INavigationNode { group: boolean; showInFilter: boolean; selected: boolean; + indeterminateSelected: boolean; loading: boolean; expanded: boolean; leaf: boolean; @@ -54,6 +55,9 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod const control = getComputed(() => contextRef.context?.control); const disabled = getComputed(() => contextRef.context?.tree.disabled || false); const selected = getComputed(() => contextRef.context?.tree.isNodeSelected(node.id) || false); + const indeterminateSelected = getComputed( + () => contextRef.context?.tree.isNodeIndeterminateSelected(node.id) || false + ); const handleClick = async (leaf: boolean) => await contextRef.context?.onClick?.(node, path, leaf); const handleOpen = async (leaf: boolean) => await contextRef.context?.onOpen?.(node, path, leaf); @@ -94,6 +98,7 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod control, disabled, selected, + indeterminateSelected, loading, expanded, leaf, diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/ElementsTree/useElementsTree.ts index e8492967ff..ea0b0958d2 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -89,6 +89,7 @@ export interface IElementsTree { getNodeState: (nodeId: string) => ITreeNodeState; isNodeExpanded: (nodeId: string, ignoreFilter?: boolean) => boolean; isNodeSelected: (nodeId: string) => boolean; + isNodeIndeterminateSelected: (nodeId: string) => boolean; getNodeChildren: (nodeId: string) => string[]; isGroup?: (node: NavNode) => boolean; setFilter: (value: string) => Promise; @@ -196,7 +197,6 @@ export function useElementsTree(options: IOptions): IElementsTree { } } }, - async setSelection(nodeId: string, selected: boolean): Promise { const node = navNodeInfoResource.get(nodeId); @@ -303,6 +303,25 @@ export function useElementsTree(options: IOptions): IElementsTree { return this.getNodeState(nodeId).selected; }, + isNodeIndeterminateSelected(nodeId: string): boolean { + if (this.isNodeSelected(nodeId)) { + return false; + } + + const node = navNodeInfoResource.get(nodeId); + + if (node && elementsTree.isGroup?.(node)) { + const children = options.getChildren(nodeId) || []; + + if (children.length > 0) { + return children.some(child => this.isNodeSelected(child) || this.isNodeIndeterminateSelected(child)); + } + + return false; + } + + return false; + }, getNodeChildren(nodeId: string): string[] { const node = navNodeInfoResource.get(nodeId); @@ -330,6 +349,7 @@ export function useElementsTree(options: IOptions): IElementsTree { collapse() { for (const state of this.state.values()) { state.expanded = false; + state.showInFilter = false; } }, async show(nodeId: string, path: string[]): Promise { diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx index 7d72346b37..6381818471 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/CheckboxMarkup.tsx @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { useLayoutEffect, useRef } from 'react'; import styled, { css } from 'reshadow'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; @@ -96,6 +97,14 @@ interface ICheckboxMarkupProps extends Omit = function CheckboxMarkup({ id, label, className, title, mod = ['primary'], ripple = true, style, ...rest }) { + const checkboxRef = useRef(null); + + useLayoutEffect(() => { + if (checkboxRef.current) { + checkboxRef.current.indeterminate = rest.indeterminate || false; + } + }); + return styled( useStyles( checkboxStyles, @@ -107,7 +116,7 @@ export const CheckboxMarkup: React.FC = function CheckboxM )( - + diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts index bf7c498d3e..f64d4496b9 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/ITreeNodeState.ts @@ -11,6 +11,7 @@ export interface ITreeNodeState { disabled?: boolean; loading?: boolean; selected?: boolean; + indeterminateSelected?: boolean; externalExpanded?: boolean; expanded?: boolean; showInFilter?: boolean; diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx index 47f4a6379f..5605480a2e 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx @@ -34,6 +34,7 @@ export const TreeNode = observer(function TreeNode group = false, loading = false, selected = false, + indeterminateSelected = false, disabled = false, expanded = false, showInFilter = false, @@ -87,14 +88,17 @@ export const TreeNode = observer(function TreeNode inProgress: observable.ref, loading: observable.ref, selected: observable.ref, + indeterminateSelected: observable.ref, expanded: observable.ref, externalExpanded: observable.ref, + showInFilter: observable.ref, leaf: observable.ref, }, { group, disabled, loading, selected, + indeterminateSelected, expanded, showInFilter, externalExpanded: externalExpanded || false, diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index 85ba68127f..288d8d92a9 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -16,6 +16,7 @@ export interface ITreeNodeContext { showInFilter: boolean; loading: boolean; selected: boolean; + indeterminateSelected: boolean; leaf: boolean; group: boolean; readonly select: (multiple?: boolean, nested?: boolean) => Promise; diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx index 7a3862bdc9..da637f6da0 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx @@ -28,6 +28,7 @@ interface Props { group?: boolean; onSelect?: () => Promise | boolean | void; selected?: boolean; + indeterminate?: boolean; disabled?: boolean; loadIndicator?: boolean; className?: string; @@ -37,6 +38,7 @@ export const TreeNodeSelect = observer(function TreeNodeSelect({ onSelect, group, selected, + indeterminate, disabled, loadIndicator, className, @@ -51,6 +53,7 @@ export const TreeNodeSelect = observer(function TreeNodeSelect({ group = group ?? context.group; const loading = loadIndicator && context.loading; selected = selected ?? context.selected; + indeterminate = indeterminate ?? context.indeterminateSelected; async function handleSelect() { await onSelect?.(); @@ -72,6 +75,7 @@ export const TreeNodeSelect = observer(function TreeNodeSelect({ : (