Skip to content

Upgrade to React 19 #1653

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

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
"@docusaurus/plugin-svgr": "^3.7.0",
"@docusaurus/theme-classic": "^3.7.0",
"@docusaurus/theme-search-algolia": "^3.7.0",
"@mdx-js/react": "^3.0.0",
"@mdx-js/react": "^3.1.0",
"@waveterm/docusaurus-og": "https://github.com/wavetermdev/docusaurus-og.git",
"clsx": "^2.1.1",
"docusaurus-plugin-sass": "^0.2.6",
"prism-react-renderer": "^2.4.1",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.0",
"remark-typescript-code-import": "^1.0.1",
Expand Down
9 changes: 5 additions & 4 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ import clsx from "clsx";
import * as jotai from "jotai";
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
import * as React from "react";
import { JSX } from "react";
import { CopyButton } from "../element/copybutton";
import { BlockFrameProps } from "./blocktypes";

const NumActiveConnColors = 8;

function handleHeaderContextMenu(
e: React.MouseEvent<HTMLDivElement>,
e: React.PointerEvent<HTMLDivElement>,
blockData: Block,
viewModel: ViewModel,
magnified: boolean,
Expand Down Expand Up @@ -106,7 +107,7 @@ const OptMagnifyButton = React.memo(
function computeEndIcons(
viewModel: ViewModel,
nodeModel: NodeModel,
onContextMenu: (e: React.MouseEvent<HTMLDivElement>) => void
onContextMenu: (e: React.PointerEvent<HTMLDivElement>) => void
): JSX.Element[] {
const endIconsElem: JSX.Element[] = [];
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
Expand Down Expand Up @@ -197,7 +198,7 @@ const BlockFrame_Header = ({
}

const onContextMenu = React.useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
(e: React.PointerEvent<HTMLDivElement>) => {
handleHeaderContextMenu(e, blockData, viewModel, magnified, nodeModel.toggleMagnify, nodeModel.onClose);
},
[magnified]
Expand Down Expand Up @@ -536,7 +537,7 @@ const BlockFrame_Default_Component = (props: BlockFrameProps) => {
const magnifiedBlockBlur = jotai.useAtomValue(magnifiedBlockBlurAtom);
const [magnifiedBlockOpacityAtom] = React.useState(() => getSettingsKeyAtom("window:magnifiedblockopacity"));
const magnifiedBlockOpacity = jotai.useAtomValue(magnifiedBlockOpacityAtom);
const connBtnRef = React.useRef<HTMLDivElement>();
const connBtnRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!manageConnection) {
return;
Expand Down
155 changes: 76 additions & 79 deletions frontend/app/block/blockutil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.R
interface ConnectionButtonProps {
connection: string;
changeConnModalAtom: jotai.PrimitiveAtom<boolean>;
ref?: React.RefObject<HTMLDivElement>;
}

export function computeConnColorNum(connStatus: ConnStatus): number {
Expand All @@ -156,88 +157,84 @@ export function computeConnColorNum(connStatus: ConnStatus): number {
return connColorNum;
}

export const ConnectionButton = React.memo(
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
({ connection, changeConnModalAtom }: ConnectionButtonProps, ref) => {
const [connModalOpen, setConnModalOpen] = jotai.useAtom(changeConnModalAtom);
const isLocal = util.isBlank(connection);
const connStatusAtom = getConnStatusAtom(connection);
const connStatus = jotai.useAtomValue(connStatusAtom);
let showDisconnectedSlash = false;
let connIconElem: React.ReactNode = null;
const connColorNum = computeConnColorNum(connStatus);
let color = `var(--conn-icon-color-${connColorNum})`;
const clickHandler = function () {
setConnModalOpen(true);
};
let titleText = null;
let shouldSpin = false;
if (isLocal) {
color = "var(--grey-text-color)";
titleText = "Connected to Local Machine";
connIconElem = (
<i
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
} else {
titleText = "Connected to " + connection;
let iconName = "arrow-right-arrow-left";
let iconSvg = null;
if (connStatus?.status == "connecting") {
color = "var(--warning-color)";
titleText = "Connecting to " + connection;
shouldSpin = false;
iconSvg = (
<div className="connecting-svg">
<DotsSvg />
</div>
);
} else if (connStatus?.status == "error") {
color = "var(--error-color)";
titleText = "Error connecting to " + connection;
if (connStatus?.error != null) {
titleText += " (" + connStatus.error + ")";
}
showDisconnectedSlash = true;
} else if (!connStatus?.connected) {
color = "var(--grey-text-color)";
titleText = "Disconnected from " + connection;
showDisconnectedSlash = true;
}
if (iconSvg != null) {
connIconElem = iconSvg;
} else {
connIconElem = (
<i
className={clsx(util.makeIconClass(iconName, false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
}
}

return (
<div ref={ref} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
<span className={clsx("fa-stack connection-icon-box", shouldSpin ? "fa-spin" : null)}>
{connIconElem}
<i
className="fa-slash fa-solid fa-stack-1x"
style={{
color: color,
marginRight: "2px",
textShadow: "0 1px black, 0 1.5px black",
opacity: showDisconnectedSlash ? 1 : 0,
}}
/>
</span>
{isLocal ? null : <div className="connection-name">{connection}</div>}
export const ConnectionButton = React.memo(({ connection, changeConnModalAtom, ref }: ConnectionButtonProps) => {
const setConnModalOpen = jotai.useSetAtom(changeConnModalAtom);
const isLocal = util.isBlank(connection);
const connStatusAtom = getConnStatusAtom(connection);
const connStatus = jotai.useAtomValue(connStatusAtom);
let showDisconnectedSlash = false;
let connIconElem: React.ReactNode = null;
const connColorNum = computeConnColorNum(connStatus);
let color = `var(--conn-icon-color-${connColorNum})`;
const clickHandler = function () {
setConnModalOpen(true);
};
let titleText = null;
let shouldSpin = false;
if (isLocal) {
color = "var(--grey-text-color)";
titleText = "Connected to Local Machine";
connIconElem = (
<i
className={clsx(util.makeIconClass("laptop", false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
} else {
titleText = "Connected to " + connection;
let iconName = "arrow-right-arrow-left";
let iconSvg = null;
if (connStatus?.status == "connecting") {
color = "var(--warning-color)";
titleText = "Connecting to " + connection;
shouldSpin = false;
iconSvg = (
<div className="connecting-svg">
<DotsSvg />
</div>
);
} else if (connStatus?.status == "error") {
color = "var(--error-color)";
titleText = "Error connecting to " + connection;
if (connStatus?.error != null) {
titleText += " (" + connStatus.error + ")";
}
showDisconnectedSlash = true;
} else if (!connStatus?.connected) {
color = "var(--grey-text-color)";
titleText = "Disconnected from " + connection;
showDisconnectedSlash = true;
}
if (iconSvg != null) {
connIconElem = iconSvg;
} else {
connIconElem = (
<i
className={clsx(util.makeIconClass(iconName, false), "fa-stack-1x")}
style={{ color: color, marginRight: 2 }}
/>
);
}
)
);
}

return (
<div ref={ref} className={clsx("connection-button")} onClick={clickHandler} title={titleText}>
<span className={clsx("fa-stack connection-icon-box", shouldSpin ? "fa-spin" : null)}>
{connIconElem}
<i
className="fa-slash fa-solid fa-stack-1x"
style={{
color: color,
marginRight: "2px",
textShadow: "0 1px black, 0 1.5px black",
opacity: showDisconnectedSlash ? 1 : 0,
}}
/>
</span>
{isLocal ? null : <div className="connection-name">{connection}</div>}
</div>
);
});

export const Input = React.memo(
({ decl, className, preview }: { decl: HeaderInput; className: string; preview: boolean }) => {
Expand Down
59 changes: 28 additions & 31 deletions frontend/app/element/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,43 @@
// SPDX-License-Identifier: Apache-2.0

import clsx from "clsx";
import { forwardRef, memo, ReactNode, useImperativeHandle, useRef } from "react";
import { JSX, memo, ReactNode, useImperativeHandle, useRef } from "react";

import "./button.scss";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
children?: ReactNode;
as?: keyof JSX.IntrinsicElements | React.ComponentType<any>;
ref?: React.RefObject<HTMLButtonElement>;
}

const Button = memo(
forwardRef<HTMLButtonElement, ButtonProps>(
({ children, disabled, className = "", as: Component = "button", ...props }: ButtonProps, ref) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef.current as HTMLButtonElement);

// Check if the className contains any of the categories: solid, outlined, or ghost
const containsButtonCategory = /(solid|outline|ghost)/.test(className);
// If no category is present, default to 'solid'
const categoryClassName = containsButtonCategory ? className : `solid ${className}`;

// Check if the className contains any of the color options: green, grey, red, or yellow
const containsColor = /(green|grey|red|yellow)/.test(categoryClassName);
// If no color is present, default to 'green'
const finalClassName = containsColor ? categoryClassName : `green ${categoryClassName}`;

return (
<Component
ref={btnRef}
tabIndex={disabled ? -1 : 0}
className={clsx("wave-button", finalClassName)}
disabled={disabled}
{...props}
>
{children}
</Component>
);
}
)
);
const Button = memo(({ children, disabled, className = "", as: Component = "button", ref, ...props }: ButtonProps) => {
const btnRef = useRef<HTMLButtonElement>(null);
useImperativeHandle(ref, () => btnRef.current as HTMLButtonElement);

// Check if the className contains any of the categories: solid, outlined, or ghost
const containsButtonCategory = /(solid|outline|ghost)/.test(className);
// If no category is present, default to 'solid'
const categoryClassName = containsButtonCategory ? className : `solid ${className}`;

// Check if the className contains any of the color options: green, grey, red, or yellow
const containsColor = /(green|grey|red|yellow)/.test(categoryClassName);
// If no color is present, default to 'green'
const finalClassName = containsColor ? categoryClassName : `green ${categoryClassName}`;

return (
<Component
ref={btnRef}
tabIndex={disabled ? -1 : 0}
className={clsx("wave-button", finalClassName)}
disabled={disabled}
{...props}
>
{children}
</Component>
);
});

Button.displayName = "Button";

Expand Down
4 changes: 2 additions & 2 deletions frontend/app/element/copybutton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { IconButton } from "./iconbutton";
type CopyButtonProps = {
title: string;
className?: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
onClick: (e: React.PointerEvent<HTMLButtonElement>) => void;
};

const CopyButton = ({ title, className, onClick }: CopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const handleOnClick = (e: React.PointerEvent<HTMLButtonElement>) => {
if (isCopied) {
return;
}
Expand Down
7 changes: 4 additions & 3 deletions frontend/app/element/expandablemenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { clsx } from "clsx";
import { atom, useAtom } from "jotai";
import { Children, ReactElement, ReactNode, cloneElement, isValidElement, useRef } from "react";
import { Children, ReactNode, cloneElement, isValidElement, useRef } from "react";

import "./expandablemenu.scss";

Expand Down Expand Up @@ -109,7 +109,7 @@ const ExpandableMenuItemGroup = ({
const [openGroups, setOpenGroups] = useAtom(openGroupsAtom);

// Generate a unique ID for this group using useRef
const idRef = useRef<string>();
const idRef = useRef<string>(null);

if (!idRef.current) {
// Generate a unique ID when the component is first rendered
Expand Down Expand Up @@ -144,7 +144,8 @@ const ExpandableMenuItemGroup = ({
}
};

const renderChildren = Children.map(children, (child: ReactElement) => {
// TODO: As of React 19, this is bad practice and considered unsound. See https://react.dev/blog/2024/04/25/react-19-upgrade-guide#changes-to-the-reactelement-typescript-type
const renderChildren = Children.map(children, (child: any) => {
if (child && child.type === ExpandableMenuItemGroupTitle) {
return cloneElement(child, {
...child.props,
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/element/flyoutmenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { FloatingPortal, type Placement, useDismiss, useFloating, useInteractions } from "@floating-ui/react";
import clsx from "clsx";
import { createRef, Fragment, memo, ReactNode, useRef, useState } from "react";
import { createRef, Fragment, JSX, memo, ReactNode, useRef, useState } from "react";
import ReactDOM from "react-dom";

import "./flyoutmenu.scss";
Expand Down
Loading