mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-26 10:45:57 +08:00
fix(core): add null checks for timeout refs and event listeners for React 19 compatibility (#9116)
## Description - Add null checks before clearTimeout calls in colorful-fallback.tsx, edgeless.dialog.tsx, and local.dialog.tsx - Fix event listener cleanup in unfolding.tsx - Update tsconfig.jsx to use react-jsx transform ## Testing - [x] Verified type safety improvements for React 19 compatibility - [x] Ensured proper cleanup of event listeners and timeouts - [x] Confirmed no unintended side effects from the changes Link to Devin run: https://app.devin.ai/sessions/2e790f3ea0d84402837ec6c3c6f83e4c
This commit is contained in:
@@ -65,8 +65,8 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"next-themes": "^0.4.0",
|
||||
"query-string": "^9.1.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-transition-state": "^2.1.1",
|
||||
@@ -81,7 +81,9 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@swc/core": "^1.9.3",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@types/animejs": "^3.1.12",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/image-blob-reduce": "^4.1.4",
|
||||
|
||||
@@ -51,7 +51,7 @@ export const AIOnboardingEdgeless = () => {
|
||||
const generalAIOnboardingOpened = useLiveData(showAIOnboardingGeneral$);
|
||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const mode = useLiveData(editorService.editor.mode$);
|
||||
|
||||
@@ -67,7 +67,9 @@ export const AIOnboardingEdgeless = () => {
|
||||
if (generalAIOnboardingOpened) return;
|
||||
if (notifyId) return;
|
||||
if (mode !== 'edgeless') return;
|
||||
clearTimeout(timeoutRef.current);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
// try to close local onboarding
|
||||
notify.dismiss(localNotifyId$.value);
|
||||
|
||||
@@ -67,7 +67,7 @@ export const AIOnboardingLocal = () => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const notifyId = useLiveData(localNotifyId$);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const notSignedIn = loginStatus !== 'authenticated';
|
||||
@@ -75,7 +75,9 @@ export const AIOnboardingLocal = () => {
|
||||
useEffect(() => {
|
||||
if (!notSignedIn) return;
|
||||
if (notifyId) return;
|
||||
clearTimeout(timeoutRef.current);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
// try to close edgeless onboarding
|
||||
notify.dismiss(edgelessNotifyId$.value);
|
||||
|
||||
@@ -5,12 +5,12 @@ import { OAuthProviderType } from '@affine/graphql';
|
||||
import track from '@affine/track';
|
||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
import { type ReactElement, type SVGAttributes, useCallback } from 'react';
|
||||
|
||||
const OAuthProviderMap: Record<
|
||||
OAuthProviderType,
|
||||
{
|
||||
icon: ReactElement;
|
||||
icon: ReactElement<SVGAttributes<SVGElement>>;
|
||||
}
|
||||
> = {
|
||||
[OAuthProviderType.Google]: {
|
||||
|
||||
@@ -43,7 +43,9 @@ export const EdgelessSwitch = ({
|
||||
const prevStateRef = useRef<EdgelessSwitchState | null>(
|
||||
article.initState ?? null
|
||||
);
|
||||
const enableScrollTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const enableScrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
);
|
||||
const turnOffScalingRef = useRef<() => void>(() => {});
|
||||
|
||||
const [scrollable, setScrollable] = useState(false);
|
||||
|
||||
@@ -33,8 +33,8 @@ export const Unfolding = ({
|
||||
const handler = () => {
|
||||
onChanged?.(fold);
|
||||
};
|
||||
ref.current.addEventListener('transitionend', handler, { once: true });
|
||||
return () => paper?.removeEventListener('transitionend', handler);
|
||||
paper.addEventListener('transitionend', handler, { once: true });
|
||||
return () => paper.removeEventListener('transitionend', handler);
|
||||
}
|
||||
|
||||
return () => null;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { InternalLottie } from '@affine/component/internal-lottie';
|
||||
import {
|
||||
type CustomLottieProps,
|
||||
InternalLottie,
|
||||
} from '@affine/component/internal-lottie';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type React from 'react';
|
||||
import { cloneElement, useState } from 'react';
|
||||
@@ -10,7 +13,7 @@ type HoverAnimateControllerProps = {
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
trash?: boolean;
|
||||
children: React.ReactElement;
|
||||
children: React.ReactElement<CustomLottieProps>;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const HoverAnimateController = ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Collection, Tag } from '@affine/env/filter';
|
||||
import type { DocCollection, DocMeta } from '@blocksuite/affine/store';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import type { JSX, PropsWithChildren, ReactNode } from 'react';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
export type ListItem = DocMeta | CollectionMeta | TagMeta;
|
||||
|
||||
@@ -27,7 +27,12 @@ import {
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { type ReactElement, useCallback, useState } from 'react';
|
||||
import {
|
||||
type ReactElement,
|
||||
type SVGAttributes,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import * as style from './styles.css';
|
||||
|
||||
@@ -222,8 +227,8 @@ const ImportOptionItem = ({
|
||||
onImport,
|
||||
}: {
|
||||
label: string;
|
||||
prefixIcon: ReactElement;
|
||||
suffixIcon?: ReactElement;
|
||||
prefixIcon: ReactElement<SVGAttributes<SVGElement>>;
|
||||
suffixIcon?: ReactElement<SVGAttributes<SVGElement>>;
|
||||
suffixTooltip?: string;
|
||||
type: ImportType;
|
||||
onImport: (type: ImportType) => void;
|
||||
|
||||
@@ -4,7 +4,14 @@ import { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { AfFiNeIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { CloudPlanLayout } from './layout';
|
||||
import { LifetimePlan } from './lifetime/lifetime-plan';
|
||||
@@ -236,7 +243,7 @@ export const CloudPlans = () => {
|
||||
// auto scroll to current plan card
|
||||
useEffect(() => {
|
||||
if (!scrollWrapper.current) return;
|
||||
const currentPlanCard = scrollWrapper.current?.querySelector(
|
||||
const currentPlanCard = scrollWrapper.current.querySelector(
|
||||
'[data-current="true"]'
|
||||
);
|
||||
const wrapperComputedStyle = getComputedStyle(scrollWrapper.current);
|
||||
@@ -247,11 +254,12 @@ export const CloudPlans = () => {
|
||||
: 0;
|
||||
const appeared = scrollWrapper.current.dataset.appeared === 'true';
|
||||
const animationFrameId = requestAnimationFrame(() => {
|
||||
scrollWrapper.current?.scrollTo({
|
||||
if (!scrollWrapper.current) return;
|
||||
scrollWrapper.current.scrollTo({
|
||||
behavior: appeared ? 'smooth' : 'instant',
|
||||
left,
|
||||
});
|
||||
scrollWrapper.current?.setAttribute('data-appeared', 'true');
|
||||
scrollWrapper.current.dataset.appeared = 'true';
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
@@ -343,7 +351,7 @@ export const CloudPlans = () => {
|
||||
select={cloudSelect}
|
||||
toggle={cloudToggle}
|
||||
scroll={cloudScroll}
|
||||
scrollRef={scrollWrapper}
|
||||
scrollRef={scrollWrapper as RefObject<HTMLDivElement>}
|
||||
lifetime={isOnetimePro ? null : <LifetimePlan />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { SafeArea } from '@affine/component';
|
||||
import clsx from 'clsx';
|
||||
import type { HtmlHTMLAttributes, ReactElement, ReactNode } from 'react';
|
||||
import type {
|
||||
HtmlHTMLAttributes,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
SVGAttributes,
|
||||
} from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { NavigationBackButton } from '../navigation-back';
|
||||
@@ -12,7 +17,7 @@ export interface PageHeaderProps
|
||||
* whether to show back button
|
||||
*/
|
||||
back?: boolean;
|
||||
backIcon?: ReactElement;
|
||||
backIcon?: ReactElement<SVGAttributes<SVGElement>>;
|
||||
/**
|
||||
* Override back button action
|
||||
*/
|
||||
|
||||
@@ -139,7 +139,7 @@ const close = (
|
||||
};
|
||||
|
||||
const SwipeDialogContext = createContext<{
|
||||
stack: Array<RefObject<HTMLElement>>;
|
||||
stack: Array<RefObject<HTMLElement | null>>;
|
||||
}>({
|
||||
stack: [],
|
||||
});
|
||||
@@ -155,7 +155,7 @@ export const SwipeDialog = ({
|
||||
const { onOpen: globalOnOpen } = useContext(ModalConfigContext);
|
||||
const swiperTriggerRef = useRef<HTMLDivElement>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const dialogRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { stack } = useContext(SwipeDialogContext);
|
||||
const prev = stack[stack.length - 1]?.current;
|
||||
@@ -187,9 +187,11 @@ export const SwipeDialog = ({
|
||||
onSwipeStart: () => {},
|
||||
onSwipe({ deltaX }) {
|
||||
const prevOrAppRoot = prev ?? document.querySelector('#app');
|
||||
if (!overlay || !dialog) return;
|
||||
tick(overlay, dialog, prevOrAppRoot, deltaX, overlay.clientWidth);
|
||||
},
|
||||
onSwipeEnd: ({ deltaX }) => {
|
||||
if (!overlay || !dialog) return;
|
||||
const shouldClose = deltaX > overlay.clientWidth * 0.2;
|
||||
if (shouldClose) {
|
||||
close(overlay, dialog, prev, deltaX, handleClose);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DualLinkIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { ReactElement, SVGAttributes } from 'react';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
import { MenuLinkItem } from './index';
|
||||
@@ -28,7 +28,7 @@ export const ExternalMenuLinkItem = ({
|
||||
label,
|
||||
}: {
|
||||
href: string;
|
||||
icon: ReactElement;
|
||||
icon: ReactElement<SVGAttributes<SVGElement>>;
|
||||
label: string;
|
||||
}) => {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, { type SVGAttributes } from 'react';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
icon?: React.ReactElement;
|
||||
icon?: React.ReactElement<SVGAttributes<SVGElement>>;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
// true, false, undefined. undefined means no collapse
|
||||
|
||||
@@ -4,15 +4,19 @@ import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type JSX,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
type SVGAttributes,
|
||||
type SVGProps,
|
||||
} from 'react';
|
||||
|
||||
import * as styles from './empty-section.css';
|
||||
|
||||
interface ExplorerEmptySectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||
icon: ((props: SVGProps<SVGSVGElement>) => JSX.Element) | ReactElement;
|
||||
icon:
|
||||
| ((props: SVGProps<SVGSVGElement>) => JSX.Element)
|
||||
| ReactElement<SVGAttributes<SVGElement>>;
|
||||
message: string;
|
||||
messageTestId?: string;
|
||||
actionText?: string;
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface UseZoomControlsProps {
|
||||
zoomRef: RefObject<HTMLDivElement>;
|
||||
imageRef: RefObject<HTMLImageElement>;
|
||||
zoomRef: RefObject<HTMLDivElement | null>;
|
||||
imageRef: RefObject<HTMLImageElement | null>;
|
||||
}
|
||||
|
||||
export const useZoomControls = ({
|
||||
|
||||
@@ -153,19 +153,17 @@ const ImagePreviewModalImpl = ({
|
||||
},
|
||||
[blocksuiteDoc, blocks, onBlockIdChange, resetZoom, onClose]
|
||||
);
|
||||
|
||||
const downloadHandler = useAsyncCallback(async () => {
|
||||
const url = imageRef.current?.src;
|
||||
if (url) {
|
||||
await downloadResourceWithUrl(url, caption || blockModel?.id || 'image');
|
||||
}
|
||||
const image = imageRef.current;
|
||||
if (!image?.src) return;
|
||||
const filename = caption || blockModel?.id || 'image';
|
||||
await downloadResourceWithUrl(image.src, filename);
|
||||
}, [caption, blockModel?.id]);
|
||||
|
||||
const copyHandler = useAsyncCallback(async () => {
|
||||
const url = imageRef.current?.src;
|
||||
if (url) {
|
||||
await copyImageToClipboard(url);
|
||||
}
|
||||
const image = imageRef.current;
|
||||
if (!image?.src) return;
|
||||
await copyImageToClipboard(image.src);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type HTMLAttributes,
|
||||
type MouseEventHandler,
|
||||
type ReactElement,
|
||||
type SVGAttributes,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
@@ -26,7 +27,7 @@ import * as styles from './peek-view-controls.css';
|
||||
|
||||
type ControlButtonProps = {
|
||||
nameKey: string;
|
||||
icon: ReactElement;
|
||||
icon: ReactElement<SVGAttributes<SVGElement>>;
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user