Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap antd tree to enable scrolling while dragging #8162

Merged
merged 9 commits into from
Nov 11, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 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)
- Fixed sorting of mags in outbound zarr streaming. [#8125](https://github.com/scalableminds/webknossos/pull/8125)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T extends BasicDataNode>(
props: TreeProps<T> & { ref: React.RefObject<RcTree> },
) {
const wrapperRef = useRef<HTMLDivElement>(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<HTMLDivElement> }) => {
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 (
<div ref={wrapperRef}>
<AntdTree {...props} onDragOver={onDragOver} />
</div>
);
MichaelBuessemeyer marked this conversation as resolved.
Show resolved Hide resolved
}

export default ScrollableVirtualizedTree;
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
Modal,
Popover,
Select,
Tree,
type MenuProps,
} from "antd";
import type { DataNode } from "antd/lib/tree";
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -1904,7 +1904,7 @@ class SegmentsView extends React.Component<Props, State> {
overflow: "hidden",
}}
>
<Tree
<ScrollableVirtualizedTree<SegmentHierarchyNode>
allowDrop={this.allowDrop}
Comment on lines +1907 to 1908
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider performance optimizations for the tree rendering

The current implementation could benefit from the following performance optimizations:

  1. Use React.memo to prevent unnecessary re-renders of tree items
  2. Implement shouldComponentUpdate or convert to a functional component with React.memo
  3. Consider using useCallback for event handlers passed to the tree component
  4. Move the titleRender function outside the render method to prevent recreation on each render

Example optimization for tree items:

const TreeItem = React.memo(({ node, ...props }) => {
  // Tree item rendering logic
});

const titleRender = React.useCallback((node: SegmentHierarchyNode) => {
  return <TreeItem node={node} {...props} />;
}, [props]);

onDrop={this.onDrop}
onSelect={this.onSelectTreeItem}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,11 +26,12 @@ export type SegmentHierarchyGroup = {
children: Array<SegmentHierarchyNode>;
};

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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -365,7 +365,7 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
_groups: Array<TreeGroup>,
_groupToTreesMap: Record<number, Array<Tree>>,
_sortBy: string,
): Generator<TreeOrTreeGroup, void, void> {
): Generator<TreeOrTreeGroup, void, undefined> {
for (const group of _groups) {
yield makeGroup(group);

Expand All @@ -380,7 +380,6 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
// 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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<TreeNode>["onCheck"] = (_checkedKeysValue, info) => {
const { id, type } = info.node;
Expand All @@ -61,6 +62,7 @@ function TreeHierarchyView(props: Props) {
const [menu, setMenu] = useState<MenuProps | null>(null);

const treeRef = useRef<GetRef<typeof AntdTree>>(null);
const wrapperRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// equivalent of LifeCycle hook "getDerivedStateFromProps"
Expand Down Expand Up @@ -275,12 +277,13 @@ function TreeHierarchyView(props: Props) {
<AutoSizer>
{({ height, width }) => (
<div
ref={wrapperRef}
style={{
height,
width,
}}
>
<AntdTree
<ScrollableVirtualizedTree
treeData={UITreeData}
height={height}
ref={treeRef}
Expand Down