Skip to content

Commit d93f70e

Browse files
authored
Refactor action bar components to React functional components (#8754)
This PR converts several class components in the action bar to React functional components. The following components were refactored: - DatasetPositionView - SaveButton - TracingActionsView - ViewModesView I split some of these rather long files into more subcomponents. This has several advantages IMHO: - each sub component only listens to a small number of store changes --> less re-renders - better readability of the parent components - shorter files per component. (I am starting to think Hirschfeld et al were right about line limits) ### URL of deployed dev instance (used for testing): - https://___.webknossos.xyz ### Steps to test: - Check that action bar components still do their job - Check that performance is on par with current master ### Issues: - Contributes to #8747 ------ (Please delete unneeded items, merge only when none are left open) - [ ] Added changelog entry (create a `$PR_NUMBER.md` file in `unreleased_changes` or use `./tools/create-changelog-entry.py`) - [ ] Added migration guide entry if applicable (edit the same file as for the changelog) - [ ] Updated [documentation](../blob/master/docs) if applicable - [ ] Adapted [wk-libs python client](https://github.com/scalableminds/webknossos-libs/tree/master/webknossos/webknossos/client) if relevant API parts change - [ ] Removed dev-only changes like prints and application.conf edits - [ ] Considered [common edge cases](../blob/master/.github/common_edge_cases.md) - [ ] Needs datastore update after deployment
1 parent 40c6938 commit d93f70e

13 files changed

+1172
-1064
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { DownOutlined } from "@ant-design/icons";
2+
import { Dropdown } from "antd";
3+
import type { SubMenuType } from "antd/es/menu/interface";
4+
import { useWkSelector } from "libs/react_hooks";
5+
import { memo } from "react";
6+
import ButtonComponent from "viewer/view/components/button_component";
7+
import { useTracingViewMenuItems } from "./use_tracing_view_menu_items";
8+
9+
type Props = {
10+
layoutMenu: SubMenuType | null;
11+
};
12+
13+
function ActionsMenu({ layoutMenu }: Props) {
14+
// Explicitly use very "precise" selectors to avoid unnecessary re-renders
15+
const annotationType = useWkSelector((state) => state.annotation.annotationType);
16+
const annotationId = useWkSelector((state) => state.annotation.annotationId);
17+
const restrictions = useWkSelector((state) => state.annotation.restrictions);
18+
const annotationOwner = useWkSelector((state) => state.annotation.owner);
19+
20+
const task = useWkSelector((state) => state.task);
21+
const activeUser = useWkSelector((state) => state.activeUser);
22+
const isAnnotationLockedByUser = useWkSelector((state) => state.annotation.isLockedByOwner);
23+
24+
const menuItems = useTracingViewMenuItems(
25+
{
26+
restrictions,
27+
task,
28+
annotationType,
29+
annotationId,
30+
activeUser,
31+
isAnnotationLockedByUser,
32+
annotationOwner,
33+
},
34+
layoutMenu,
35+
);
36+
37+
return (
38+
<div>
39+
<Dropdown menu={{ items: menuItems }} trigger={["click"]}>
40+
<ButtonComponent className="narrow">
41+
Menu
42+
<DownOutlined />
43+
</ButtonComponent>
44+
</Dropdown>
45+
</div>
46+
);
47+
}
48+
49+
export default memo(ActionsMenu);
Lines changed: 19 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -1,194 +1,22 @@
1-
import { PushpinOutlined, ReloadOutlined } from "@ant-design/icons";
2-
import { Space } from "antd";
3-
import FastTooltip from "components/fast_tooltip";
4-
import { V3 } from "libs/mjs";
5-
import Toast from "libs/toast";
6-
import { Vector3Input } from "libs/vector_input";
7-
import message from "messages";
8-
import type React from "react";
9-
import { PureComponent } from "react";
10-
import { connect } from "react-redux";
11-
import type { APIDataset } from "types/api_types";
12-
import type { Vector3, ViewMode } from "viewer/constants";
1+
import { useWkSelector } from "libs/react_hooks";
132
import constants from "viewer/constants";
14-
import { getDatasetExtentInVoxel } from "viewer/model/accessors/dataset_accessor";
15-
import { getPosition, getRotation } from "viewer/model/accessors/flycam_accessor";
16-
import { setPositionAction, setRotationAction } from "viewer/model/actions/flycam_actions";
17-
import type { Flycam, Task, WebknossosState } from "viewer/store";
18-
import Store from "viewer/store";
19-
import { ShareButton } from "viewer/view/action-bar/share_modal_view";
20-
import ButtonComponent from "viewer/view/components/button_component";
21-
22-
type Props = {
23-
flycam: Flycam;
24-
viewMode: ViewMode;
25-
dataset: APIDataset;
26-
task: Task | null | undefined;
27-
};
28-
const positionIconStyle: React.CSSProperties = {
29-
transform: "rotate(-45deg)",
30-
marginRight: 0,
31-
};
32-
const warningColors: React.CSSProperties = {
33-
color: "rgb(255, 155, 85)",
34-
borderColor: "rgb(241, 122, 39)",
35-
};
36-
const iconErrorStyle: React.CSSProperties = { ...warningColors };
37-
const positionInputDefaultStyle: React.CSSProperties = {
38-
textAlign: "center",
39-
};
40-
const positionInputErrorStyle: React.CSSProperties = {
41-
...positionInputDefaultStyle,
42-
...warningColors,
43-
};
44-
45-
class DatasetPositionView extends PureComponent<Props> {
46-
copyPositionToClipboard = async () => {
47-
const position = V3.floor(getPosition(this.props.flycam)).join(", ");
48-
await navigator.clipboard.writeText(position);
49-
Toast.success("Position copied to clipboard");
50-
};
51-
52-
copyRotationToClipboard = async () => {
53-
const rotation = V3.round(getRotation(this.props.flycam)).join(", ");
54-
await navigator.clipboard.writeText(rotation);
55-
Toast.success("Rotation copied to clipboard");
56-
};
57-
58-
handleChangePosition = (position: Vector3) => {
59-
Store.dispatch(setPositionAction(position));
60-
};
61-
62-
handleChangeRotation = (rotation: Vector3) => {
63-
Store.dispatch(setRotationAction(rotation));
64-
};
65-
66-
isPositionOutOfBounds = (position: Vector3) => {
67-
const { dataset, task } = this.props;
68-
const { min: datasetMin, max: datasetMax } = getDatasetExtentInVoxel(dataset);
69-
70-
const isPositionOutOfBounds = (min: Vector3, max: Vector3) =>
71-
position[0] < min[0] ||
72-
position[1] < min[1] ||
73-
position[2] < min[2] ||
74-
position[0] >= max[0] ||
75-
position[1] >= max[1] ||
76-
position[2] >= max[2];
77-
78-
const isOutOfDatasetBounds = isPositionOutOfBounds(datasetMin, datasetMax);
79-
let isOutOfTaskBounds = false;
80-
81-
if (task?.boundingBox) {
82-
const bbox = task.boundingBox;
83-
const bboxMax = [
84-
bbox.topLeft[0] + bbox.width,
85-
bbox.topLeft[1] + bbox.height,
86-
bbox.topLeft[2] + bbox.depth,
87-
];
88-
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number[]' is not assignable to p... Remove this comment to see the full error message
89-
isOutOfTaskBounds = isPositionOutOfBounds(bbox.topLeft, bboxMax);
90-
}
91-
92-
return {
93-
isOutOfDatasetBounds,
94-
isOutOfTaskBounds,
95-
};
96-
};
97-
98-
render() {
99-
const position = V3.floor(getPosition(this.props.flycam));
100-
const { isOutOfDatasetBounds, isOutOfTaskBounds } = this.isPositionOutOfBounds(position);
101-
const iconColoringStyle = isOutOfDatasetBounds || isOutOfTaskBounds ? iconErrorStyle : {};
102-
const positionInputStyle =
103-
isOutOfDatasetBounds || isOutOfTaskBounds
104-
? positionInputErrorStyle
105-
: positionInputDefaultStyle;
106-
let maybeErrorMessage = null;
107-
108-
if (isOutOfDatasetBounds) {
109-
maybeErrorMessage = message["tracing.out_of_dataset_bounds"];
110-
} else if (!maybeErrorMessage && isOutOfTaskBounds) {
111-
maybeErrorMessage = message["tracing.out_of_task_bounds"];
112-
}
113-
114-
const rotation = V3.round(getRotation(this.props.flycam));
115-
const isArbitraryMode = constants.MODES_ARBITRARY.includes(this.props.viewMode);
116-
const positionView = (
117-
<div
118-
style={{
119-
display: "flex",
120-
}}
121-
>
122-
<Space.Compact
123-
style={{
124-
whiteSpace: "nowrap",
125-
}}
126-
>
127-
<FastTooltip title={message["tracing.copy_position"]} placement="bottom-start">
128-
<ButtonComponent
129-
onClick={this.copyPositionToClipboard}
130-
style={{ padding: "0 10px", ...iconColoringStyle }}
131-
className="hide-on-small-screen"
132-
>
133-
<PushpinOutlined style={positionIconStyle} />
134-
</ButtonComponent>
135-
</FastTooltip>
136-
<Vector3Input
137-
value={position}
138-
onChange={this.handleChangePosition}
139-
autoSize
140-
style={positionInputStyle}
141-
allowDecimals
142-
/>
143-
<ShareButton dataset={this.props.dataset} style={iconColoringStyle} />
144-
</Space.Compact>
145-
{isArbitraryMode ? (
146-
<Space.Compact
147-
style={{
148-
whiteSpace: "nowrap",
149-
marginLeft: 10,
150-
}}
151-
>
152-
<FastTooltip title={message["tracing.copy_rotation"]} placement="bottom-start">
153-
<ButtonComponent
154-
onClick={this.copyRotationToClipboard}
155-
style={{
156-
padding: "0 10px",
157-
}}
158-
className="hide-on-small-screen"
159-
>
160-
<ReloadOutlined />
161-
</ButtonComponent>
162-
</FastTooltip>
163-
<Vector3Input
164-
value={rotation}
165-
onChange={this.handleChangeRotation}
166-
style={{
167-
textAlign: "center",
168-
width: 120,
169-
}}
170-
allowDecimals
171-
/>
172-
</Space.Compact>
173-
) : null}
174-
</div>
175-
);
176-
return (
177-
<FastTooltip title={maybeErrorMessage || null} wrapper="div">
178-
{positionView}
179-
</FastTooltip>
180-
);
181-
}
182-
}
183-
184-
function mapStateToProps(state: WebknossosState): Props {
185-
return {
186-
flycam: state.flycam,
187-
viewMode: state.temporaryConfiguration.viewMode,
188-
dataset: state.dataset,
189-
task: state.task,
190-
};
3+
import PositionView from "./position_view";
4+
import RotationView from "./rotation_view";
5+
6+
function DatasetPositionView() {
7+
const viewMode = useWkSelector((state) => state.temporaryConfiguration.viewMode);
8+
const isArbitraryMode = constants.MODES_ARBITRARY.includes(viewMode);
9+
10+
return (
11+
<div
12+
style={{
13+
display: "flex",
14+
}}
15+
>
16+
<PositionView />
17+
{isArbitraryMode ? <RotationView /> : null}
18+
</div>
19+
);
19120
}
19221

193-
const connector = connect(mapStateToProps);
194-
export default connector(DatasetPositionView);
22+
export default DatasetPositionView;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { PushpinOutlined } from "@ant-design/icons";
2+
import { Space } from "antd";
3+
import FastTooltip from "components/fast_tooltip";
4+
import { V3 } from "libs/mjs";
5+
import { useWkSelector } from "libs/react_hooks";
6+
import Toast from "libs/toast";
7+
import { Vector3Input } from "libs/vector_input";
8+
import message from "messages";
9+
import type React from "react";
10+
import type { Vector3 } from "viewer/constants";
11+
import { getDatasetExtentInVoxel } from "viewer/model/accessors/dataset_accessor";
12+
import { getPosition } from "viewer/model/accessors/flycam_accessor";
13+
import { setPositionAction } from "viewer/model/actions/flycam_actions";
14+
import Store from "viewer/store";
15+
import { ShareButton } from "viewer/view/action-bar/share_modal_view";
16+
import ButtonComponent from "viewer/view/components/button_component";
17+
18+
const positionIconStyle: React.CSSProperties = {
19+
transform: "rotate(-45deg)",
20+
marginRight: 0,
21+
};
22+
const warningColors: React.CSSProperties = {
23+
color: "rgb(255, 155, 85)",
24+
borderColor: "rgb(241, 122, 39)",
25+
};
26+
const iconErrorStyle: React.CSSProperties = { ...warningColors };
27+
const positionInputDefaultStyle: React.CSSProperties = {
28+
textAlign: "center",
29+
};
30+
const positionInputErrorStyle: React.CSSProperties = {
31+
...positionInputDefaultStyle,
32+
...warningColors,
33+
};
34+
35+
function PositionView() {
36+
const flycam = useWkSelector((state) => state.flycam);
37+
const dataset = useWkSelector((state) => state.dataset);
38+
const task = useWkSelector((state) => state.task);
39+
40+
const copyPositionToClipboard = async () => {
41+
const position = V3.floor(getPosition(flycam)).join(", ");
42+
await navigator.clipboard.writeText(position);
43+
Toast.success("Position copied to clipboard");
44+
};
45+
46+
const handleChangePosition = (position: Vector3) => {
47+
Store.dispatch(setPositionAction(position));
48+
};
49+
50+
const isPositionOutOfBounds = (position: Vector3) => {
51+
const { min: datasetMin, max: datasetMax } = getDatasetExtentInVoxel(dataset);
52+
53+
const isPositionOutOfBounds = (min: Vector3, max: Vector3) =>
54+
position[0] < min[0] ||
55+
position[1] < min[1] ||
56+
position[2] < min[2] ||
57+
position[0] >= max[0] ||
58+
position[1] >= max[1] ||
59+
position[2] >= max[2];
60+
61+
const isOutOfDatasetBounds = isPositionOutOfBounds(datasetMin, datasetMax);
62+
let isOutOfTaskBounds = false;
63+
64+
if (task?.boundingBox) {
65+
const bbox = task.boundingBox;
66+
const bboxMax: Vector3 = [
67+
bbox.topLeft[0] + bbox.width,
68+
bbox.topLeft[1] + bbox.height,
69+
bbox.topLeft[2] + bbox.depth,
70+
];
71+
isOutOfTaskBounds = isPositionOutOfBounds(bbox.topLeft, bboxMax);
72+
}
73+
74+
return {
75+
isOutOfDatasetBounds,
76+
isOutOfTaskBounds,
77+
};
78+
};
79+
80+
const position = V3.floor(getPosition(flycam));
81+
const { isOutOfDatasetBounds, isOutOfTaskBounds } = isPositionOutOfBounds(position);
82+
const iconColoringStyle = isOutOfDatasetBounds || isOutOfTaskBounds ? iconErrorStyle : {};
83+
const positionInputStyle =
84+
isOutOfDatasetBounds || isOutOfTaskBounds ? positionInputErrorStyle : positionInputDefaultStyle;
85+
let maybeErrorMessage = null;
86+
87+
if (isOutOfDatasetBounds) {
88+
maybeErrorMessage = message["tracing.out_of_dataset_bounds"];
89+
} else if (!maybeErrorMessage && isOutOfTaskBounds) {
90+
maybeErrorMessage = message["tracing.out_of_task_bounds"];
91+
}
92+
93+
return (
94+
<FastTooltip title={maybeErrorMessage || null} wrapper="div">
95+
<Space.Compact
96+
style={{
97+
whiteSpace: "nowrap",
98+
}}
99+
>
100+
<FastTooltip title={message["tracing.copy_position"]} placement="bottom-start">
101+
<ButtonComponent
102+
onClick={copyPositionToClipboard}
103+
style={{ padding: "0 10px", ...iconColoringStyle }}
104+
className="hide-on-small-screen"
105+
>
106+
<PushpinOutlined style={positionIconStyle} />
107+
</ButtonComponent>
108+
</FastTooltip>
109+
<Vector3Input
110+
value={position}
111+
onChange={handleChangePosition}
112+
autoSize
113+
style={positionInputStyle}
114+
allowDecimals
115+
/>
116+
<ShareButton dataset={dataset} style={iconColoringStyle} />
117+
</Space.Compact>
118+
</FastTooltip>
119+
);
120+
}
121+
122+
export default PositionView;

0 commit comments

Comments
 (0)