mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-29 22:51:49 +01:00
Fix infinite loop in layoutAtom, improve iconbutton disable code (#306)
Fixes an infinite loop in the layoutModel atom synchronization that would cause the atom to update indefinitely when the root node is deleted. Also adds a dedicated `disabled` flag for the IconButton decl so we can disable the onClick handler when the button is disabled. Also updates the Magnify toggle button to use this new flag, so that when there's only one leaf in a layout, the magnify button is disabed.
This commit is contained in:
parent
0084f8eb97
commit
383a71fc25
@ -134,7 +134,7 @@ func validateEasyORef(oref string) error {
|
||||
}
|
||||
_, err := uuid.Parse(oref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid object reference (must be UUID, or a positive nonzero integer): %v", err)
|
||||
return fmt.Errorf("invalid object reference (must be UUID, or a positive integer): %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -91,12 +91,13 @@ function getViewIconElem(viewIconUnion: string | HeaderIconButton, blockData: Bl
|
||||
}
|
||||
|
||||
const OptMagnifyButton = React.memo(
|
||||
({ magnified, toggleMagnify }: { magnified: boolean; toggleMagnify: () => void }) => {
|
||||
({ magnified, toggleMagnify, disabled }: { magnified: boolean; toggleMagnify: () => void; disabled: boolean }) => {
|
||||
const magnifyDecl: HeaderIconButton = {
|
||||
elemtype: "iconbutton",
|
||||
icon: <MagnifyIcon enabled={magnified} />,
|
||||
title: magnified ? "Minimize" : "Magnify",
|
||||
click: toggleMagnify,
|
||||
disabled,
|
||||
};
|
||||
return <IconButton key="magnify" decl={magnifyDecl} className="block-frame-magnify" />;
|
||||
}
|
||||
@ -104,13 +105,15 @@ const OptMagnifyButton = React.memo(
|
||||
|
||||
function computeEndIcons(
|
||||
viewModel: ViewModel,
|
||||
magnified: boolean,
|
||||
toggleMagnify: () => void,
|
||||
onClose: () => void,
|
||||
nodeModel: NodeModel,
|
||||
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
): JSX.Element[] {
|
||||
const endIconsElem: JSX.Element[] = [];
|
||||
const endIconButtons = util.useAtomValueSafe(viewModel.endIconButtons);
|
||||
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
|
||||
const numLeafs = jotai.useAtomValue(nodeModel.numLeafs);
|
||||
const magnifyDisabled = numLeafs <= 1;
|
||||
|
||||
if (endIconButtons && endIconButtons.length > 0) {
|
||||
endIconsElem.push(...endIconButtons.map((button, idx) => <IconButton key={idx} decl={button} />));
|
||||
}
|
||||
@ -121,12 +124,19 @@ function computeEndIcons(
|
||||
click: onContextMenu,
|
||||
};
|
||||
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
|
||||
endIconsElem.push(<OptMagnifyButton key="unmagnify" magnified={magnified} toggleMagnify={toggleMagnify} />);
|
||||
endIconsElem.push(
|
||||
<OptMagnifyButton
|
||||
key="unmagnify"
|
||||
magnified={magnified}
|
||||
toggleMagnify={nodeModel.toggleMagnify}
|
||||
disabled={magnifyDisabled}
|
||||
/>
|
||||
);
|
||||
const closeDecl: HeaderIconButton = {
|
||||
elemtype: "iconbutton",
|
||||
icon: "xmark-large",
|
||||
title: "Close",
|
||||
click: onClose,
|
||||
click: nodeModel.onClose,
|
||||
};
|
||||
endIconsElem.push(<IconButton key="close" decl={closeDecl} className="block-frame-default-close" />);
|
||||
return endIconsElem;
|
||||
@ -156,13 +166,7 @@ const BlockFrame_Header = ({
|
||||
[magnified]
|
||||
);
|
||||
|
||||
const endIconsElem = computeEndIcons(
|
||||
viewModel,
|
||||
magnified,
|
||||
nodeModel.toggleMagnify,
|
||||
nodeModel.onClose,
|
||||
onContextMenu
|
||||
);
|
||||
const endIconsElem = computeEndIcons(viewModel, nodeModel, onContextMenu);
|
||||
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
|
||||
let preIconButtonElem: JSX.Element = null;
|
||||
if (preIconButton) {
|
||||
|
@ -139,9 +139,13 @@ export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.R
|
||||
|
||||
export const IconButton = React.memo(({ decl, className }: { decl: HeaderIconButton; className?: string }) => {
|
||||
const buttonRef = React.useRef<HTMLDivElement>(null);
|
||||
useLongClick(buttonRef, decl.click, decl.longClick);
|
||||
useLongClick(buttonRef, decl.click, decl.longClick, decl.disabled);
|
||||
return (
|
||||
<div ref={buttonRef} className={clsx("iconbutton", className)} title={decl.title}>
|
||||
<div
|
||||
ref={buttonRef}
|
||||
className={clsx("iconbutton", className, decl.className, { disabled: decl.disabled })}
|
||||
title={decl.title}
|
||||
>
|
||||
{typeof decl.icon === "string" ? <i className={util.makeIconClass(decl.icon, true)} /> : decl.icon}
|
||||
</div>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useLongClick = (ref, onClick, onLongClick, ms = 300) => {
|
||||
export const useLongClick = (ref, onClick, onLongClick, disabled = false, ms = 300) => {
|
||||
const timerRef = useRef(null);
|
||||
const [longClickTriggered, setLongClickTriggered] = useState(false);
|
||||
|
||||
@ -40,7 +40,7 @@ export const useLongClick = (ref, onClick, onLongClick, ms = 300) => {
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
|
||||
if (!element) return;
|
||||
if (!element || disabled) return;
|
||||
|
||||
element.addEventListener("mousedown", startPress);
|
||||
element.addEventListener("mouseup", stopPress);
|
||||
|
@ -63,15 +63,15 @@ export class WebViewModel implements ViewModel {
|
||||
return [
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
className: this.shouldDisabledBackButton() ? "disabled" : "",
|
||||
icon: "chevron-left",
|
||||
click: this.handleBack.bind(this),
|
||||
disabled: this.shouldDisabledBackButton(),
|
||||
},
|
||||
{
|
||||
elemtype: "iconbutton",
|
||||
className: this.shouldDisabledForwardButton() ? "disabled" : "",
|
||||
icon: "chevron-right",
|
||||
click: this.handleForward.bind(this),
|
||||
disabled: this.shouldDisabledForwardButton(),
|
||||
},
|
||||
{
|
||||
elemtype: "div",
|
||||
|
@ -439,7 +439,6 @@ export class LayoutModel {
|
||||
}
|
||||
} else {
|
||||
this.updateTree();
|
||||
this.setTreeStateAtom();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -448,7 +447,7 @@ export class LayoutModel {
|
||||
* Set the upstream tree state atom to the value of the local tree state.
|
||||
* @param bumpGeneration Whether to bump the generation of the tree state before setting the atom.
|
||||
*/
|
||||
setTreeStateAtom(bumpGeneration = true) {
|
||||
setTreeStateAtom(bumpGeneration = false) {
|
||||
if (bumpGeneration) {
|
||||
this.treeState.generation++;
|
||||
}
|
||||
@ -460,7 +459,7 @@ export class LayoutModel {
|
||||
* This is a hack to ensure that when the updateTree first successfully runs, we set the upstream atom state to persist the initial leaf order.
|
||||
* @see updateTree should be the only caller of this method.
|
||||
*/
|
||||
setTreeStateAtomOnce = lazy(() => this.setTreeStateAtom());
|
||||
setTreeStateAtomOnce = lazy(() => this.setTreeStateAtom(true));
|
||||
|
||||
/**
|
||||
* Recursively walks the tree to find leaf nodes, update the resize handles, and compute additional properties for each node.
|
||||
@ -761,6 +760,7 @@ export class LayoutModel {
|
||||
const isFocused = treeState.focusedNodeId === nodeid;
|
||||
return isFocused;
|
||||
}),
|
||||
numLeafs: this.numLeafs,
|
||||
isMagnified: atom((get) => {
|
||||
const treeState = get(this.treeStateAtom);
|
||||
return treeState.magnifiedNodeId === nodeid;
|
||||
|
@ -328,6 +328,7 @@ export interface NodeModel {
|
||||
animationTimeS: number;
|
||||
innerRect: Atom<CSSProperties>;
|
||||
blockNum: Atom<number>;
|
||||
numLeafs: Atom<number>;
|
||||
nodeId: string;
|
||||
blockId: string;
|
||||
isFocused: Atom<boolean>;
|
||||
|
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
@ -154,6 +154,7 @@ declare global {
|
||||
title?: string;
|
||||
click?: (e: React.MouseEvent<any>) => void;
|
||||
longClick?: (e: React.MouseEvent<any>) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type HeaderTextButton = {
|
||||
|
Loading…
Reference in New Issue
Block a user