Skip to content

Commit 684a588

Browse files
Add button to select all trees and all segments that match a search (#8123)
* add button to select all trees that match a search * add function for segments and improve icon * remove console log * add ts-expect-error tag again * focus first search result and only allow select all matches for leaves * fix select segment group as search result * expand parent groups and fix mixed tree and tree group selection * changelog * lint * address review * add placeholder and disable field if all matches all selected * fix case where group is selected --------- Co-authored-by: MichaelBuessemeyer <[email protected]>
1 parent 9b5a12e commit 684a588

File tree

5 files changed

+112
-16
lines changed

5 files changed

+112
-16
lines changed

CHANGELOG.unreleased.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
1616
- Most sliders have been improved: Wheeling above a slider now changes its value and double-clicking its knob resets it to its default value. [#8095](https://github.com/scalableminds/webknossos/pull/8095)
1717
- It is now possible to search for unnamed segments with the full default name instead of only their id. [#8133](https://github.com/scalableminds/webknossos/pull/8133)
1818
- Increased loading speed for precomputed meshes. [#8110](https://github.com/scalableminds/webknossos/pull/8110)
19+
- Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123)
1920
- Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111)
2021
- Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122)
2122

frontend/javascripts/oxalis/view/right-border-tabs/advanced_search_popover.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { Input, Tooltip, Popover, Space, type InputRef } from "antd";
2-
import { DownOutlined, UpOutlined } from "@ant-design/icons";
2+
import { CheckSquareOutlined, DownOutlined, UpOutlined } from "@ant-design/icons";
33
import * as React from "react";
44
import memoizeOne from "memoize-one";
55
import ButtonComponent from "oxalis/view/components/button_component";
66
import Shortcut from "libs/shortcut_component";
77
import DomVisibilityObserver from "oxalis/view/components/dom_visibility_observer";
88
import { mod } from "libs/utils";
99

10+
const PRIMARY_COLOR = "var(--ant-color-primary)";
11+
1012
type Props<S> = {
1113
data: S[];
1214
searchKey: keyof S | ((item: S) => string);
1315
onSelect: (arg0: S) => void;
16+
onSelectAllMatches?: (arg0: S[]) => void;
1417
children: React.ReactNode;
1518
provideShortcut?: boolean;
1619
targetId: string;
@@ -20,6 +23,7 @@ type State = {
2023
isVisible: boolean;
2124
searchQuery: string;
2225
currentPosition: number | null | undefined;
26+
areAllMatchesSelected: boolean;
2327
};
2428

2529
export default class AdvancedSearchPopover<
@@ -29,6 +33,7 @@ export default class AdvancedSearchPopover<
2933
isVisible: false,
3034
searchQuery: "",
3135
currentPosition: null,
36+
areAllMatchesSelected: false,
3237
};
3338

3439
getAvailableOptions = memoizeOne(
@@ -69,6 +74,7 @@ export default class AdvancedSearchPopover<
6974
currentPosition = mod(currentPosition + offset, numberOfAvailableOptions);
7075
this.setState({
7176
currentPosition,
77+
areAllMatchesSelected: false,
7278
});
7379
this.props.onSelect(availableOptions[currentPosition]);
7480
};
@@ -101,21 +107,25 @@ export default class AdvancedSearchPopover<
101107

102108
render() {
103109
const { data, searchKey, provideShortcut, children, targetId } = this.props;
104-
const { searchQuery, isVisible } = this.state;
110+
const { searchQuery, isVisible, areAllMatchesSelected } = this.state;
105111
let { currentPosition } = this.state;
106112
const availableOptions = this.getAvailableOptions(data, searchQuery, searchKey);
107113
const numberOfAvailableOptions = availableOptions.length;
108114
// Ensure that currentPosition to not higher than numberOfAvailableOptions.
109115
currentPosition =
110116
currentPosition == null ? -1 : Math.min(currentPosition, numberOfAvailableOptions - 1);
111117
const hasNoResults = numberOfAvailableOptions === 0;
112-
const hasMultipleResults = numberOfAvailableOptions > 1;
118+
const availableOptionsToSelectAllMatches = availableOptions.filter(
119+
(result) => result.type === "Tree" || result.type === "segment",
120+
);
121+
const isSelectAllMatchesDisabled = availableOptionsToSelectAllMatches.length < 2;
113122
const additionalInputStyle =
114123
hasNoResults && searchQuery !== ""
115124
? {
116125
color: "red",
117126
}
118127
: {};
128+
const selectAllMatchesButtonColor = areAllMatchesSelected ? PRIMARY_COLOR : undefined;
119129
return (
120130
<React.Fragment>
121131
{provideShortcut ? (
@@ -171,9 +181,23 @@ export default class AdvancedSearchPopover<
171181
this.setState({
172182
searchQuery: evt.target.value,
173183
currentPosition: null,
184+
areAllMatchesSelected: false,
174185
})
175186
}
176-
addonAfter={`${currentPosition + 1}/${numberOfAvailableOptions}`}
187+
addonAfter={
188+
<div
189+
style={{
190+
minWidth: 25,
191+
color: areAllMatchesSelected
192+
? "var(--ant-color-text-disabled)"
193+
: undefined,
194+
}}
195+
>
196+
{areAllMatchesSelected
197+
? "all"
198+
: `${currentPosition + 1}/${numberOfAvailableOptions}`}
199+
</div>
200+
}
177201
ref={this.autoFocus}
178202
autoFocus
179203
/>
@@ -183,7 +207,7 @@ export default class AdvancedSearchPopover<
183207
width: 40,
184208
}}
185209
onClick={this.selectPreviousOption}
186-
disabled={!hasMultipleResults}
210+
disabled={hasNoResults}
187211
>
188212
<UpOutlined />
189213
</ButtonComponent>
@@ -194,11 +218,32 @@ export default class AdvancedSearchPopover<
194218
width: 40,
195219
}}
196220
onClick={this.selectNextOption}
197-
disabled={!hasMultipleResults}
221+
disabled={hasNoResults}
198222
>
199223
<DownOutlined />
200224
</ButtonComponent>
201225
</Tooltip>
226+
<Tooltip title="Select all matches (except groups)">
227+
<ButtonComponent
228+
style={{
229+
width: 40,
230+
color: selectAllMatchesButtonColor,
231+
borderColor: selectAllMatchesButtonColor,
232+
}}
233+
onClick={
234+
this.props.onSelectAllMatches != null
235+
? () => {
236+
this.props.onSelectAllMatches!(availableOptionsToSelectAllMatches);
237+
if (!areAllMatchesSelected)
238+
this.setState({ areAllMatchesSelected: true });
239+
}
240+
: undefined
241+
}
242+
disabled={isSelectAllMatchesDisabled}
243+
>
244+
<CheckSquareOutlined />
245+
</ButtonComponent>
246+
</Tooltip>
202247
</Space.Compact>
203248
</React.Fragment>
204249
)

frontend/javascripts/oxalis/view/right-border-tabs/segments_tab/segments_view.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ import { SegmentStatisticsModal } from "./segment_statistics_modal";
137137
import type { ItemType } from "antd/lib/menu/interface";
138138
import { InputWithUpdateOnBlur } from "oxalis/view/components/input_with_update_on_blur";
139139

140+
const SCROLL_DELAY_MS = 50;
141+
140142
const { confirm } = Modal;
141143
const { Option } = Select;
142144
// Interval in ms to check for running mesh file computation jobs for this dataset
@@ -1590,7 +1592,7 @@ class SegmentsView extends React.Component<Props, State> {
15901592
this.setState(({ renamingCounter }) => ({ renamingCounter: renamingCounter - 1 }));
15911593
};
15921594

1593-
handleSearchSelect = (selectedElement: SegmentHierarchyNode) => {
1595+
maybeExpandParentGroup = (selectedElement: SegmentHierarchyNode) => {
15941596
if (this.tree?.current == null) {
15951597
return;
15961598
}
@@ -1606,16 +1608,47 @@ class SegmentsView extends React.Component<Props, State> {
16061608
if (expandedGroups) {
16071609
this.setExpandedGroupsFromSet(expandedGroups);
16081610
}
1611+
};
1612+
1613+
handleSearchSelect = (selectedElement: SegmentHierarchyNode) => {
1614+
this.maybeExpandParentGroup(selectedElement);
16091615
// As parent groups might still need to expand, we need to wait for this to finish.
16101616
setTimeout(() => {
16111617
if (this.tree.current) this.tree.current.scrollTo({ key: selectedElement.key });
1612-
}, 50);
1618+
}, SCROLL_DELAY_MS);
16131619
const isASegment = "color" in selectedElement;
16141620
if (isASegment) {
16151621
this.onSelectSegment(selectedElement);
1622+
} else {
1623+
if (this.props.visibleSegmentationLayer == null) return;
1624+
Store.dispatch(
1625+
setSelectedSegmentsOrGroupAction(
1626+
[],
1627+
selectedElement.id,
1628+
this.props.visibleSegmentationLayer?.name,
1629+
),
1630+
);
16161631
}
16171632
};
16181633

1634+
handleSelectAllMatchingSegments = (allMatches: SegmentHierarchyNode[]) => {
1635+
if (this.props.visibleSegmentationLayer == null) return;
1636+
const allMatchingSegmentIds = allMatches.map((match) => {
1637+
this.maybeExpandParentGroup(match);
1638+
return match.id;
1639+
});
1640+
Store.dispatch(
1641+
setSelectedSegmentsOrGroupAction(
1642+
allMatchingSegmentIds,
1643+
null,
1644+
this.props.visibleSegmentationLayer.name,
1645+
),
1646+
);
1647+
setTimeout(() => {
1648+
this.tree.current?.scrollTo({ key: allMatches[0].key });
1649+
}, SCROLL_DELAY_MS);
1650+
};
1651+
16191652
getSegmentStatisticsModal = (groupId: number) => {
16201653
const visibleSegmentationLayer = this.props.visibleSegmentationLayer;
16211654
if (visibleSegmentationLayer == null) {
@@ -1833,6 +1866,7 @@ class SegmentsView extends React.Component<Props, State> {
18331866
searchKey={(item) => getSegmentName(item)}
18341867
provideShortcut
18351868
targetId={segmentsTabId}
1869+
onSelectAllMatches={this.handleSelectAllMatchingSegments}
18361870
>
18371871
<ButtonComponent
18381872
size="small"

frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,7 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
668668
});
669669
};
670670

671-
handleSearchSelect = (selectedElement: TreeOrTreeGroup) => {
671+
maybeExpandParentGroups = (selectedElement: TreeOrTreeGroup) => {
672672
const { skeletonTracing } = this.props;
673673
if (!skeletonTracing) {
674674
return;
@@ -682,13 +682,26 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
682682
if (expandedGroups) {
683683
this.props.onSetExpandedGroups(expandedGroups);
684684
}
685+
};
686+
687+
handleSearchSelect = (selectedElement: TreeOrTreeGroup) => {
688+
this.maybeExpandParentGroups(selectedElement);
685689
if (selectedElement.type === GroupTypeEnum.TREE) {
686690
this.props.onSetActiveTree(selectedElement.id);
687691
} else {
688692
this.props.onSetActiveTreeGroup(selectedElement.id);
689693
}
690694
};
691695

696+
handleSelectAllMatchingTrees = (matchingTrees: TreeOrTreeGroup[]) => {
697+
this.props.onDeselectActiveGroup();
698+
const treeIds = matchingTrees.map((tree) => {
699+
this.maybeExpandParentGroups(tree);
700+
return tree.id;
701+
});
702+
this.setState({ selectedTreeIds: treeIds });
703+
};
704+
692705
getTreesComponents(sortBy: string) {
693706
if (!this.props.skeletonTracing) {
694707
return null;
@@ -864,6 +877,7 @@ class SkeletonTabView extends React.PureComponent<Props, State> {
864877
searchKey="name"
865878
provideShortcut
866879
targetId={treeTabId}
880+
onSelectAllMatches={this.handleSelectAllMatchingTrees}
867881
>
868882
<ButtonComponent title="Open the search via CTRL + Shift + F">
869883
<SearchOutlined />

frontend/javascripts/oxalis/view/right-border-tabs/tree_hierarchy_view.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,7 @@ function TreeHierarchyView(props: Props) {
188188
}
189189
}
190190

191-
function onSelectGroupNode(node: TreeNode) {
192-
const groupId = node.id;
191+
function onSelectGroupNode(groupId: number) {
193192
const numberOfSelectedTrees = props.selectedTreeIds.length;
194193

195194
if (numberOfSelectedTrees > 1) {
@@ -254,11 +253,14 @@ function TreeHierarchyView(props: Props) {
254253
const checkedKeys = deepFlatFilter(UITreeData, (node) => node.isChecked).map((node) => node.key);
255254

256255
// selectedKeys is mainly used for highlighting, i.e. blueish background color
257-
const selectedKeys = props.selectedTreeIds.map((treeId) =>
258-
getNodeKey(GroupTypeEnum.TREE, treeId),
259-
);
256+
const selectedKeys = props.activeGroupId
257+
? [getNodeKey(GroupTypeEnum.GROUP, props.activeGroupId)]
258+
: props.selectedTreeIds.map((treeId) => getNodeKey(GroupTypeEnum.TREE, treeId));
260259

261-
if (props.activeGroupId) selectedKeys.push(getNodeKey(GroupTypeEnum.GROUP, props.activeGroupId));
260+
useEffect(
261+
() => treeRef.current?.scrollTo({ key: selectedKeys[0], align: "auto" }),
262+
[selectedKeys[0]],
263+
);
262264

263265
return (
264266
<>
@@ -297,7 +299,7 @@ function TreeHierarchyView(props: Props) {
297299
onSelect={(_selectedKeys, info: { node: TreeNode; nativeEvent: MouseEvent }) =>
298300
info.node.type === GroupTypeEnum.TREE
299301
? onSelectTreeNode(info.node, info.nativeEvent)
300-
: onSelectGroupNode(info.node)
302+
: onSelectGroupNode(info.node.id)
301303
}
302304
onDrop={onDrop}
303305
onCheck={onCheck}

0 commit comments

Comments
 (0)