From e0fd7b6caadb86f18dbd2939834ad10b0c32b39a Mon Sep 17 00:00:00 2001 From: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:30:33 +0100 Subject: [PATCH] Wrap antd tree to enable scrolling while dragging (#8162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Wrap antd tree to enable scrolling while dragging * add explanation to biome ignore comment * add changelog entry * fix typing * extract magic numbers into constants * apply feedback - send datasource id in correct format to backend - fix dataset renaming in dataset settings - fix typo in filename --------- Co-authored-by: Michael Büßemeyer --- CHANGELOG.unreleased.md | 1 + .../view/layouting/flex_layout_wrapper.tsx | 2 +- .../view/layouting/tracing_layout_view.tsx | 2 +- .../scrollable_virtualized_tree.tsx | 64 +++++++++++++++++++ .../segments_tab/segments_view.tsx | 4 +- .../segments_tab/segments_view_helper.tsx | 14 ++-- .../hide_tree_edges_icon.tsx} | 0 .../{ => trees_tab}/skeleton_tab_view.tsx | 9 ++- .../tree_hierarchy_renderers.tsx | 4 +- .../{ => trees_tab}/tree_hierarchy_view.tsx | 17 +++-- 10 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx rename frontend/javascripts/oxalis/view/right-border-tabs/{hide_tree_eges_icon.tsx => trees_tab/hide_tree_edges_icon.tsx} (100%) rename frontend/javascripts/oxalis/view/right-border-tabs/{ => trees_tab}/skeleton_tab_view.tsx (99%) rename frontend/javascripts/oxalis/view/right-border-tabs/{ => trees_tab}/tree_hierarchy_renderers.tsx (99%) rename frontend/javascripts/oxalis/view/right-border-tabs/{ => trees_tab}/tree_hierarchy_view.tsx (95%) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 1b6ddae2176..a0384b3cbc1 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -32,6 +32,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Fixed a bug during dataset upload in case the configured `datastore.baseFolder` is an absolute path. [#8098](https://github.com/scalableminds/webknossos/pull/8098) [#8103](https://github.com/scalableminds/webknossos/pull/8103) - Fixed bbox export menu item [#8152](https://github.com/scalableminds/webknossos/pull/8152) - When trying to save an annotation opened via a link including a sharing token, the token is automatically discarded in case it is insufficient for update actions but the users token is. [#8139](https://github.com/scalableminds/webknossos/pull/8139) +- Fix that scrolling in the trees and segments tab did not work while dragging. [#8162](https://github.com/scalableminds/webknossos/pull/8162) - Fixed that uploading a dataset which needs a conversion failed when the angstrom unit was configured for the conversion. [#8173](https://github.com/scalableminds/webknossos/pull/8173) - Fixed that the skeleton search did not automatically expand groups that contained the selected tree [#8129](https://github.com/scalableminds/webknossos/pull/8129) - Fixed a bug that zarr streaming version 3 returned the shape of mag (1, 1, 1) / the finest mag for all mags. [#8116](https://github.com/scalableminds/webknossos/pull/8116) diff --git a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx index a754e7d1bfd..6c79170cbcf 100644 --- a/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx +++ b/frontend/javascripts/oxalis/view/layouting/flex_layout_wrapper.tsx @@ -28,7 +28,7 @@ import InputCatcher from "oxalis/view/input_catcher"; import LayerSettingsTab from "oxalis/view/left-border-tabs/layer_settings_tab"; import RecordingSwitch from "oxalis/view/recording_switch"; import SegmentsView from "oxalis/view/right-border-tabs/segments_tab/segments_view"; -import SkeletonTabView from "oxalis/view/right-border-tabs/skeleton_tab_view"; +import SkeletonTabView from "oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view"; import Statusbar from "oxalis/view/statusbar"; import type { OxalisState, BusyBlockingInfo, BorderOpenStatus } from "oxalis/store"; import Store from "oxalis/store"; diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx index 7362b366cd8..a1671264ae5 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.tsx @@ -32,7 +32,7 @@ import { RenderToPortal } from "oxalis/view/layouting/portal_utils"; import NmlUploadZoneContainer from "oxalis/view/nml_upload_zone_container"; import PresentModernControls from "oxalis/view/novel_user_experiences/01-present-modern-controls"; import WelcomeToast from "oxalis/view/novel_user_experiences/welcome_toast"; -import { importTracingFiles } from "oxalis/view/right-border-tabs/skeleton_tab_view"; +import { importTracingFiles } from "oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view"; import TracingView from "oxalis/view/tracing_view"; import VersionView from "oxalis/view/version_view"; import * as React from "react"; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx new file mode 100644 index 00000000000..f2a8a8439fe --- /dev/null +++ b/frontend/javascripts/oxalis/view/right-border-tabs/scrollable_virtualized_tree.tsx @@ -0,0 +1,64 @@ +import { Tree as AntdTree, type TreeProps } from "antd"; +import type { BasicDataNode } from "antd/es/tree"; +import { throttle } from "lodash"; +import { useCallback, useRef } from "react"; +import type RcTree from "rc-tree"; + +const MIN_SCROLL_SPEED = 30; +const MAX_SCROLL_SPEED = 200; +const MIN_SCROLL_AREA_HEIGHT = 60; +const SCROLL_AREA_RATIO = 10; // 1/10th of the container height +const THROTTLE_TIME = 25; + +function ScrollableVirtualizedTree( + props: TreeProps & { ref: React.RefObject }, +) { + const wrapperRef = useRef(null); + // biome-ignore lint/correctness/useExhaustiveDependencies: biome is not smart enough to notice that the function needs to be re-created when wrapperRef changes. + const onDragOver = useCallback( + throttle((info: { event: React.DragEvent }) => { + const target = info.event.target as HTMLElement; + if (!target || !wrapperRef.current) { + return; + } + const { bottom: currentBottom, top: currentTop } = target.getBoundingClientRect(); + const { bottom: boxBottom, top: boxTop } = wrapperRef.current.getBoundingClientRect(); + const scrollableList = wrapperRef.current.getElementsByClassName("ant-tree-list-holder")[0]; + if (!scrollableList) { + return; + } + const scrollAreaHeight = Math.max( + MIN_SCROLL_AREA_HEIGHT, + Math.round((boxBottom - boxTop) / SCROLL_AREA_RATIO), + ); + + if (currentTop > boxBottom - scrollAreaHeight && scrollableList) { + const ratioWithinScrollingArea = + (currentTop - (boxBottom - scrollAreaHeight)) / scrollAreaHeight; + const scrollingValue = Math.max( + Math.round(ratioWithinScrollingArea * MAX_SCROLL_SPEED), + MIN_SCROLL_SPEED, + ); + scrollableList.scrollTop += scrollingValue; + } + if (boxTop + scrollAreaHeight > currentBottom && scrollableList) { + const ratioWithinScrollingArea = + (boxTop + scrollAreaHeight - currentBottom) / scrollAreaHeight; + const scrollingValue = Math.max( + Math.round(ratioWithinScrollingArea * MAX_SCROLL_SPEED), + MIN_SCROLL_SPEED, + ); + scrollableList.scrollTop -= scrollingValue; + } + }, THROTTLE_TIME), + [wrapperRef], + ); + + return ( +
+ +
+ ); +} + +export default ScrollableVirtualizedTree; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx index e795a0a581e..f37f3860e08 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx @@ -29,7 +29,6 @@ import { Modal, Popover, Select, - Tree, type MenuProps, } from "antd"; import type { DataNode } from "antd/lib/tree"; @@ -136,6 +135,7 @@ import { MetadataEntryTableRows } from "../metadata_table"; import { SegmentStatisticsModal } from "./segment_statistics_modal"; import type { ItemType } from "antd/lib/menu/interface"; import { InputWithUpdateOnBlur } from "oxalis/view/components/input_with_update_on_blur"; +import ScrollableVirtualizedTree from "../scrollable_virtualized_tree"; const SCROLL_DELAY_MS = 50; @@ -1904,7 +1904,7 @@ class SegmentsView extends React.Component { overflow: "hidden", }} > - allowDrop={this.allowDrop} onDrop={this.onDrop} onSelect={this.onSelectTreeItem} diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx index 0440554ef95..c0eadefa017 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view_helper.tsx @@ -12,10 +12,11 @@ import { } from "oxalis/model/accessors/volumetracing_accessor"; import type { MenuClickEventHandler } from "rc-menu/lib/interface"; import { hasSegmentIndexInDataStore } from "admin/admin_rest_api"; +import type { BasicDataNode } from "antd/es/tree"; const { confirm } = Modal; -export type SegmentHierarchyGroup = { +export type SegmentHierarchyGroup = BasicDataNode & { title: string; type: "group"; name: string | null | undefined; @@ -25,11 +26,12 @@ export type SegmentHierarchyGroup = { children: Array; }; -export type SegmentHierarchyLeaf = Segment & { - type: "segment"; - key: string; - title: string; -}; +export type SegmentHierarchyLeaf = BasicDataNode & + Segment & { + type: "segment"; + key: string; + title: string; + }; export type SegmentHierarchyNode = SegmentHierarchyLeaf | SegmentHierarchyGroup; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/hide_tree_eges_icon.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/hide_tree_edges_icon.tsx similarity index 100% rename from frontend/javascripts/oxalis/view/right-border-tabs/hide_tree_eges_icon.tsx rename to frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/hide_tree_edges_icon.tsx diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx similarity index 99% rename from frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx rename to frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx index f209d676d4e..2085d9f101d 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx @@ -86,12 +86,12 @@ import type { } from "oxalis/store"; import Store from "oxalis/store"; import Toast from "libs/toast"; -import TreeHierarchyView from "oxalis/view/right-border-tabs/tree_hierarchy_view"; +import TreeHierarchyView from "oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_view"; import * as Utils from "libs/utils"; import { api } from "oxalis/singletons"; import messages from "messages"; -import AdvancedSearchPopover from "./advanced_search_popover"; -import DeleteGroupModalView from "./delete_group_modal_view"; +import AdvancedSearchPopover from "../advanced_search_popover"; +import DeleteGroupModalView from "../delete_group_modal_view"; import { isAnnotationOwner } from "oxalis/model/accessors/annotation_accessor"; import { LongUnitToShortUnitMap } from "oxalis/constants"; @@ -365,7 +365,7 @@ class SkeletonTabView extends React.PureComponent { _groups: Array, _groupToTreesMap: Record>, _sortBy: string, - ): Generator { + ): Generator { for (const group of _groups) { yield makeGroup(group); @@ -380,7 +380,6 @@ class SkeletonTabView extends React.PureComponent { // Trees are sorted by the sortBy property const sortedTrees = _.orderBy(_groupToTreesMap[group.groupId], [_sortBy], ["asc"]); - // @ts-expect-error ts-migrate(2766) FIXME: Cannot delegate iteration to value because the 'ne... Remove this comment to see the full error message yield* sortedTrees.map(makeTree); } } diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_renderers.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx similarity index 99% rename from frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_renderers.tsx rename to frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx index cc57d2de57e..08039bc4897 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_renderers.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx @@ -51,8 +51,8 @@ import { MISSING_GROUP_ID, type TreeNode, } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers"; -import { HideTreeEdgesIcon } from "./hide_tree_eges_icon"; -import { ColoredDotIcon } from "./segments_tab/segment_list_item"; +import { HideTreeEdgesIcon } from "./hide_tree_edges_icon"; +import { ColoredDotIcon } from "../segments_tab/segment_list_item"; export type Props = { activeTreeId: number | null | undefined; diff --git a/frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_view.tsx b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_view.tsx similarity index 95% rename from frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_view.tsx rename to frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_view.tsx index 29312811bf1..e25c28cc865 100644 --- a/frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_view.tsx +++ b/frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_view.tsx @@ -1,5 +1,5 @@ import { DownOutlined } from "@ant-design/icons"; -import { Tree as AntdTree, type GetRef, type MenuProps, Modal, type TreeProps } from "antd"; +import { type Tree as AntdTree, type GetRef, type MenuProps, Modal, type TreeProps } from "antd"; import React, { memo, useCallback, useEffect, useRef, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; import { mapGroups } from "oxalis/model/accessors/skeletontracing_accessor"; @@ -25,8 +25,8 @@ import { moveGroupsHelper, type TreeNode, } from "oxalis/view/right-border-tabs/tree_hierarchy_view_helpers"; -import { getContextMenuPositionFromEvent } from "../context_menu"; -import { ContextMenuContainer } from "./sidebar_context_menu"; +import { getContextMenuPositionFromEvent } from "../../context_menu"; +import { ContextMenuContainer } from "../sidebar_context_menu"; import { onBatchActions, type Props, @@ -36,10 +36,11 @@ import { setExpandedGroups, setUpdateTreeGroups, } from "./tree_hierarchy_renderers"; -import { ResizableSplitPane } from "./resizable_split_pane"; -import { MetadataEntryTableRows } from "./metadata_table"; +import { ResizableSplitPane } from "../resizable_split_pane"; +import { MetadataEntryTableRows } from "../metadata_table"; import type { MetadataEntryProto } from "types/api_flow_types"; -import { InputWithUpdateOnBlur } from "../components/input_with_update_on_blur"; +import { InputWithUpdateOnBlur } from "../../components/input_with_update_on_blur"; +import ScrollableVirtualizedTree from "../scrollable_virtualized_tree"; const onCheck: TreeProps["onCheck"] = (_checkedKeysValue, info) => { const { id, type } = info.node; @@ -61,6 +62,7 @@ function TreeHierarchyView(props: Props) { const [menu, setMenu] = useState(null); const treeRef = useRef>(null); + const wrapperRef = useRef(null); useEffect(() => { // equivalent of LifeCycle hook "getDerivedStateFromProps" @@ -275,12 +277,13 @@ function TreeHierarchyView(props: Props) { {({ height, width }) => (
-