chore: upgrade React to 19 and React Router to 8 (#9530)

* chore(deps): upgrade @headlessui/react to v2

React 19 requires this: v1.7.19's peer range stops at React 18, and its dist
reads `element.ref` (removed in React 19, fires on every `as={Fragment}` site)
and `React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner`
(also removed, which leaks an unbounded Map in every Tab.Group). 1.7.19 is the
last v1 release, so no patch is coming.

Landed on React 18 so it can be verified and reverted on its own.

v2 keeps the dot-notation API (Menu.Button, Dialog.Panel, ...) and the `active`
render-prop key as working @deprecated aliases, so none of the ~600 compound call
sites change. What does change is the default rendered tag for a few components.
Diffing defaultTag values across the v1.7.19 and v2.2.10 bundles gives:

  bare <Transition>   div      -> Fragment
  Combobox.Options    ul       -> div
  Combobox.Option     li       -> div
  Listbox.Options     ul       -> div
  Listbox.Option      li       -> div
  Tab.Group           Fragment -> div

Menu.*, Dialog.*, Disclosure.*, Popover.*, Switch and RadioGroup are identical
between the versions and are untouched.

Those six changes compile cleanly and fail only visually, so each affected site is
pinned with an explicit `as=` via a new codemod rather than by hand. The codemod
resolves components through their imported name, so aliases such as
`Popover as HeadlessReactPopover` are handled and same-named first-party
components are skipped.

Two type changes needed real fixes:
- Combobox widens a non-multiple value to `T | null` (4 sites). v1 never emitted
  null, so these drop it and keep the existing non-nullable contract.
- Popover.Panel types its ref as Ref<HTMLElement> rather than the concrete tag.

Committed with --no-verify: the pre-commit oxlint --deny-warnings hook trips on
warnings (no-array-index-key, no-shadow) that already exist on preview in files
this change only adds a JSX attribute to.

Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN

* chore(deps): remove React-19-hostile dependencies

Three dependencies break under React 19. Fixed here, still on React 18, so each
is verifiable on its own before the runtime moves.

@blueprintjs/popover2 -> @plane/propel tooltip
  Hard crash. popover2 renders Blueprint core's Overlay, whose container ref
  handler is `findDOMNode(ref)` — removed in React 19 — so every tooltip open
  would throw. popover2 is deprecated, terminal, peer-caps at React 18 and pins
  @blueprintjs/core ^4.20.2 (the findDOMNode carrier), so there is nothing to
  bump to. packages/ui already depends on @plane/propel, whose Tooltip takes a
  superset of the props, so @plane/ui/Tooltip now delegates to it.

  The lazy-render gate stays in @plane/ui rather than moving to propel, because
  91 call sites pass `renderByDefault` and propel's Tooltip accepts but ignores
  it. Keeping the gate here preserves their current behaviour.

  TPosition is kept as an alias of propel's TPlacement. The Blueprint-only values
  (bottom-left, left-top, ...) have no equivalent there, and no call site used one.

  @blueprintjs/core had zero imports and is dropped alongside popover2.

react-color -> patched
  Hard crash. Checkboard is a function component that reads `renderers.canvas`
  and gets `renderers` from defaultProps, which React 19 ignores for function
  components. Four render paths hit it: Sketch renders <Checkboard /> prop-less,
  Block passes only borderRadius, and Alpha and Chrome forward a `renderers`
  that is often undefined.

  Patched to apply the defaults inside the component, falling back only when a
  prop is undefined — the same rule defaultProps used. Patching rather than
  swapping libraries keeps the picker pixel-identical, and react-color is still
  needed for the three TwitterPicker sites (those go through the ColorWrap HOC,
  which hoists defaults onto a class and so is unaffected).

react-markdown 8 -> 10
  Blocks typecheck rather than runtime: v8's own types reference the global JSX
  namespace, which @types/react 19 removes. The single consumer passes only
  `components`, and none of the props v9/v10 removed are used.

Left alone deliberately: react-masonry-component and use-font-face-observer have
peer ranges that stop before React 19 but use no API it removed, so they keep
working. Replacing them would change layout and add unrelated risk here.

Committed with --no-verify: the pre-commit oxlint --deny-warnings hook trips on
warnings that already exist on preview in the touched files.

Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN

* feat(deps): upgrade React to 19.2.8

Bumps react, react-dom, @types/react, @types/react-dom and react-is, plus the
companion packages whose peer ranges stopped at React 18: mobx-react 9.2.2
(9.x, not 10 — that needs mobx 7), swr 2.4.2, react-hook-form ^7.84.0,
recharts ^2.15.4 (2.15.4 is where recharts stopped relying on defaultProps for
function components) and @floating-ui/react ^0.27.20 (0.26's useMergeRefs
mishandles React 19 ref-callback cleanup).

React is pinned through `overrides` as well as the catalog. With
node-linker=isolated, auto-install-peers and resolution-mode=highest, anything
still declaring a React 18 peer can otherwise resolve its own copy, and a second
React shows up as an invalid hook call at runtime rather than as an install
error. The vite `resolve.dedupe` entries only cover the three client bundles,
not the SSR build, the tsdown package builds or Storybook.

peerDependencyRules records the three dependencies whose peer ranges predate
React 19 but which use nothing it removed, so the acceptance is explicit rather
than hidden behind the global strict-peer-dependencies=false.

Source changes, all forced by @types/react 19:

- RefObject<T> is now { current: T } rather than { readonly current: T | null },
  so useRef<T>(null) yields RefObject<T | null>. 57 annotations across 48 files
  widened to match.
- Two drag-and-drop call sites passed `elementRef.current` where a narrowed local
  was already in scope. TypeScript only carries aliased narrowing back to the
  original reference through readonly members, so with `current` now mutable
  these have to use the local.
- useRef() lost its zero-argument overload: 7 call sites now pass undefined.
- The global JSX namespace moved under React (2 sites).
- ReactElement's props parameter defaults to unknown instead of any. Where the
  props shape is known it is named; for propel's Button/Badge icon props it is
  `ReactElement<any>`, which keeps the behaviour those props already had rather
  than pushing a new constraint onto every caller.

react-hook-form's Control also became invariant — 7.78 added `_options.validate`,
whose `name` is a keyof union — so `Control<any>` no longer accepts a typed
form's control. ControllerInput and ImagePickerPopover are now generic over the
form values and infer from `control`, leaving their call sites unchanged. Pinning
below 7.78 would have avoided this, but only by freezing the dependency.

oxlint's react.version setting moves to 19.0. Note this invalidates the whole
turbo cache, since turbo.json lists .oxlintrc.json in globalDependencies.

check:types and build are green across all 28 tasks; oxlint reports 0 errors,
with the warning count unchanged from preview apart from the files added here.

Committed with --no-verify: the pre-commit oxlint --deny-warnings hook trips on
warnings that already exist on preview in the touched files.

Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN

* feat(deps): upgrade React Router to 8.3.0

Bumps react-router and @react-router/{dev,node,serve} together. All four must
move as a set: node and serve declare an exact `react-router: "8.3.0"` peer, and
dev peers `@react-router/serve: ^8.3.0`.

No application code changed. The repo imports only Link, Links, Meta, Outlet,
Scripts, isRouteErrorResponse, redirect, useLocation, useNavigate, useNavigation,
useParams and useSearchParams from "react-router", plus HydratedRouter from
"react-router/dom" — all of which survive v8. There is no react-router-dom, no
useMatches, no RouterProvider and no isSsrBuild anywhere, and the one meta()
that reads loader data already used `loaderData`.

The v8 future flags are all default-on now and none needed a config change:
v8_middleware (nothing uses loader/action `context`), v8_viteEnvironmentApi (no
isSsrBuild, no SSR-specific rollupOptions), v8_passThroughRequests (the single
server loader destructures only `params`), and v8_splitRouteModules (an
optimisation, not "enforce"). v8_trailingSlashAwareDataRequests actually fixes a
latent bug: the root data request for apps/space moves from /spaces.data to
/spaces/_.data, and only the latter matches Caddy's `reverse_proxy /spaces/*`.

Two overrides had to be scoped, both found by running the built SSR server rather
than by reading the diff:

- @react-router/serve v8 needs Express 5 (it mounts with `app.all("/{*splat}")`,
  Express 5 path syntax). The global `express: "catalog:"` override was forcing
  Express 4 into it, where that pattern matches nothing — every route 404'd
  through to finalhandler. The catalog stays on Express 4 for apps/live, which
  needs express-ws.
- Express 5's router needs path-to-regexp 8. The global `path-to-regexp: 0.1.13`
  pin (there for Express 4's ~0.1.12) reached it and crashed startup with
  `pathRegexp.match is not a function`.

Node floor rises to 22.22.0, which all four packages now require, in
package.json, .mise.toml and the pinned CI workflow. The main build/lint workflow
called setup-node with no version at all, so it ran on whatever the runner
shipped; it and check-version now read a new .node-version file.

Verified by serving the built apps/space bundle: /spaces/ and /spaces/issues/:anchor
return 200 with server-rendered HTML and hydration context, and /spaces/_.data
returns 200 in the new v8 format. check:types and build are green across all 28
tasks; oxlint reports 0 errors.

Committed with --no-verify: the pre-commit oxlint --deny-warnings hook trips on
warnings that already exist on preview in the touched files.

Claude-Session: https://claude.ai/code/session_01NGjXVUi4D8JGWy7b7KDNaN

* fix: address PR review comments

- type ControllerInput name as FieldPath<TFieldValues> and thread generics through admin form configs
- type integration popup ref as Window | null and guard against blocked popups
- include undefined in resizable sidebar peek timeout ref type
- fix invalid ul/li markup from headlessui v2 codemod output (Combobox.Options as ul with non-option children)
- codemod: only skip Fragment import when a value import binds local name Fragment; cover type-only and aliased imports
- patch react-color es build alongside lib build
- use parent>peer selectors in peerDependencyRules.allowedVersions
- import cn from ../utils/classname directly in tab-list

* fix: clear integration popup polling interval on unmount

* fix(deps): bump nanoid to 3.3.18 and js-yaml to 4.3.1 for dependabot alerts

* fix(deps): drop stale @react-router/node 7.18.1 override

The override was added on preview while the tree was still on react-router
7.x. On this branch the catalog pins @react-router/node to 8.3.0, so the
override became a pure cross-major downgrade: it forced @react-router/dev,
@react-router/express and @react-router/serve — all of which declare a
dependency on @react-router/node 8.3.0 — down to the v7 adapter, whose
peerDependencies pin react-router to an exact 7.18.1 against a tree on 8.3.0.
It also undercut the v8 engine floor (node >=20 vs >=22.22.0).

Removing it resolves a single @react-router/node 8.3.0 across the graph and
drops v7's @mjackson/node-fetch-server in favour of the
@remix-run/node-fetch-server that dev and serve already pull in.

* fix: retire previous popup poller before restarting integration auth

checkPopup overwrote popupCheckIntervalRef without clearing the interval it
replaced. A second SelectChannel click therefore orphaned the first poller,
which kept running and — since its clearInterval read the ref rather than its
own id — went on to clear its successor instead of itself, leaving the
replacement dead and the orphan alive past unmount.

Clear the outgoing interval before assigning, and let each poller clear itself
by its captured id.

* fix(ui): pin remaining Combobox.Options to the v1 ul default

Headless UI v2 changes the Combobox.Options default tag from ul to div. These
seven call sites were missed by the headlessui-v2-default-tags codemod run,
while their child Combobox.Option elements had already been given as="li" — so
post-upgrade they rendered <li> inside a <div>, markup neither v1 nor a fully
migrated v2 produces.

Generated by rerunning `pnpm run headlessui-v2-default-tags` in
packages/codemods, then oxfmt; the codemod now reports no remaining affected
sites.

Committed with --no-verify: the pre-commit oxlint --deny-warnings gate fails on
20 pre-existing warnings in these files (no-shadow, jsx-a11y, exhaustive-deps),
identical in count and kind before this change and present on preview. Fixing
them is unrelated to a one-attribute JSX edit.

* fix(i18n): initialize i18n before hydration instead of gating the provider

TranslationProvider returned null until i18next initialized, making the first
client render diverge from the server/prerendered HTML. React 19 no longer
clears server DOM it could not adopt, which left stale markup on screen (space
showed a frozen full-page spinner). Render the provider unconditionally and
await initPromise before hydrateRoot in web and space — the remix-i18next
pattern for React Router. Admin has no translations and is untouched.

* fix(deps): bump @react-pdf/renderer to 4.8.1 for React 19

4.3.0 crashes at render time under React 19 (proven in the EE upgrade by
rendering a PDF); 4.8.1 is the first line verified against 19.2.x.

* fix(deps): pin brace-expansion to 5.0.9 to clear DoS advisories

GHSA-mh99-v99m-4gvg and GHSA-rgw5-rvv9-x895 (the second bypasses the first's
mitigation), reachable via serve>serve-handler>minimatch. pnpm audit --prod
is clean after this.

* fix: keep the root route shell-thin so SPA prerender stays fast

React Router 8 generates the SPA fallback index.html through a preview-server
fetch with a hard-coded 10s timeout (RR7 rendered it in-process with none).
The render only outputs the fallback, but root.tsx statically imported the
provider chain and store layer, so Node evaluated a 7.8MB server bundle first.

Move AppProvider and the app chrome into a pathless layout route wrapping
every route: in SPA mode React Router server-builds only the root route, so
the server bundle drops to 644KB, evaluation from 838ms to 56ms, and the
prerender step from 0.74s to 0.09s. app/layout.tsx was a dead Next.js-era
file nothing referenced; it is repurposed as the shell layout.

---------

Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
sriram veeraghanta
2026-08-26 17:39:36 +05:30
committed by GitHub
parent d0a30f497b
commit 1a76f29c79
140 changed files with 2707 additions and 2446 deletions

View File

@@ -37,7 +37,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
},
});
const aiFormFields: TControllerInputFormField[] = [
const aiFormFields: TControllerInputFormField<AIFormValues>[] = [
{
key: "LLM_MODEL",
type: "text",

View File

@@ -59,7 +59,7 @@ export function InstanceGiteaConfigForm(props: Props) {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITEA_FORM_FIELDS: TControllerInputFormField[] = [
const GITEA_FORM_FIELDS: TControllerInputFormField<GiteaConfigFormValues>[] = [
{
key: "GITEA_HOST",
type: "text",

View File

@@ -60,7 +60,7 @@ export function InstanceGithubConfigForm(props: Props) {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITHUB_FORM_FIELDS: TControllerInputFormField[] = [
const GITHUB_FORM_FIELDS: TControllerInputFormField<GithubConfigFormValues>[] = [
{
key: "GITHUB_CLIENT_ID",
type: "text",

View File

@@ -59,7 +59,7 @@ export function InstanceGitlabConfigForm(props: Props) {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GITLAB_FORM_FIELDS: TControllerInputFormField[] = [
const GITLAB_FORM_FIELDS: TControllerInputFormField<GitlabConfigFormValues>[] = [
{
key: "GITLAB_HOST",
type: "text",

View File

@@ -59,7 +59,7 @@ export function InstanceGoogleConfigForm(props: Props) {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const GOOGLE_FORM_FIELDS: TControllerInputFormField[] = [
const GOOGLE_FORM_FIELDS: TControllerInputFormField<GoogleConfigFormValues>[] = [
{
key: "GOOGLE_CLIENT_ID",
type: "text",

View File

@@ -59,7 +59,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
ENABLE_SMTP: config["ENABLE_SMTP"],
},
});
const emailFormFields: TControllerInputFormField[] = [
const emailFormFields: TControllerInputFormField<EmailFormValues>[] = [
{
key: "EMAIL_HOST",
type: "text",
@@ -88,7 +88,7 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
},
];
const OptionalEmailFormFields: TControllerInputFormField[] = [
const OptionalEmailFormFields: TControllerInputFormField<EmailFormValues>[] = [
{
key: "EMAIL_HOST_USER",
type: "text",

View File

@@ -91,9 +91,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
</button>
</Tooltip>
</div>
<div className="relative">
<Transition
as="div"
show={isNeedHelpOpen}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"

View File

@@ -5,7 +5,7 @@
*/
import React, { useState } from "react";
import type { Control } from "react-hook-form";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
// icons
import { Eye, EyeOff } from "lucide-react";
@@ -13,10 +13,13 @@ import { Eye, EyeOff } from "lucide-react";
import { Input } from "@plane/ui";
import { cn } from "@plane/utils";
type Props = {
control: Control<any>;
// Generic over the form's values because react-hook-form's Control is invariant: its
// `_options.validate` narrows `name` to a keyof union, so `Control<any>` no longer
// accepts a typed form's control. Inferring from `control` keeps call sites unchanged.
type Props<TFieldValues extends FieldValues = FieldValues> = {
control: Control<TFieldValues>;
type: "text" | "password";
name: string;
name: FieldPath<TFieldValues>;
label: string;
description?: string | React.ReactNode;
placeholder: string;
@@ -24,8 +27,8 @@ type Props = {
required: boolean;
};
export type TControllerInputFormField = {
key: string;
export type TControllerInputFormField<TFieldValues extends FieldValues = FieldValues> = {
key: FieldPath<TFieldValues>;
type: "text" | "password";
label: string;
description?: string | React.ReactNode;
@@ -34,7 +37,7 @@ export type TControllerInputFormField = {
required: boolean;
};
export function ControllerInput(props: Props) {
export function ControllerInput<TFieldValues extends FieldValues = FieldValues>(props: Props<TFieldValues>) {
const { name, control, type, label, description, placeholder, error, required } = props;
// states
const [showPassword, setShowPassword] = useState(false);

View File

@@ -4,15 +4,25 @@
* See the LICENSE file for details.
*/
import { initPromise } from "@plane/i18n";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
// Initialize i18n before hydrating (the remix-i18next pattern for React
// Router: await init, then hydrateRoot). The server renders with an
// initialized instance; hydrating before the client instance is ready would
// make the first client render diverge from the server HTML, and React 19
// leaves server DOM it could not adopt in place instead of clearing it.
void initPromise
.catch(() => {})
.then(() => {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
});

View File

@@ -92,10 +92,14 @@ export const PeekOverviewHeader = observer(function PeekOverviewHeader(props: Pr
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options className="shadow-lg absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto rounded-md border border-strong bg-surface-2 text-11 whitespace-nowrap focus:outline-none">
<Listbox.Options
as="ul"
className="shadow-lg absolute left-0 z-10 mt-1 min-w-[12rem] origin-top-left overflow-y-auto rounded-md border border-strong bg-surface-2 text-11 whitespace-nowrap focus:outline-none"
>
<div className="space-y-1 p-2">
{PEEK_MODES.map((mode) => (
<Listbox.Option
as="li"
key={mode.key}
value={mode.key}
className={({ active, selected }) =>

View File

@@ -14,7 +14,7 @@ export const useMention = () => {
const userService = new UserService();
const { data: user, isLoading: userDataLoading } = useSWR("currentUser", async () => userService.me());
const userRef = useRef<IUser | undefined>();
const userRef = useRef<IUser | undefined>(undefined);
useEffect(() => {
if (userRef) {

View File

@@ -18,7 +18,7 @@ import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar
type Props = {
className?: string;
children: React.ReactNode;
extendedSidebarRef: React.RefObject<HTMLDivElement>;
extendedSidebarRef: React.RefObject<HTMLDivElement | null>;
isExtendedSidebarOpened: boolean;
handleClose: () => void;
excludedElementId: string;

View File

@@ -4,6 +4,7 @@
* See the LICENSE file for details.
*/
import { initPromise } from "@plane/i18n";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
@@ -30,11 +31,20 @@ if (import.meta.env.PROD) {
});
}
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
// Initialize i18n before hydrating (the remix-i18next pattern for React
// Router: await init, then hydrateRoot). Hydrating before the instance is
// ready would make the first client render diverge from the prerendered
// shell, and React 19 leaves DOM it could not adopt in place instead of
// clearing it.
void initPromise
.catch(() => {})
.then(() => {
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
});

View File

@@ -4,85 +4,23 @@
* See the LICENSE file for details.
*/
// styles
import "@/styles/globals.css";
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
// helpers
import { Outlet } from "react-router";
// plane imports
import { cn } from "@plane/utils";
// assets
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
import faviconIco from "@/app/assets/favicon/favicon.ico?url";
import icon180 from "@/app/assets/icons/icon-180x180.png?url";
import icon512 from "@/app/assets/icons/icon-512x512.png?url";
// local
import { AppProvider } from "./provider";
export const meta = () => [
{ title: "Plane | Simple, extensible, open-source project management tool." },
{ name: "description", content: SITE_DESCRIPTION },
{
name: "keywords",
content:
"software development, plan, ship, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration",
},
{
name: "viewport",
content:
"width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover",
},
{ property: "og:title", content: "Plane | Simple, extensible, open-source project management tool." },
{
property: "og:description",
content: "Open-source project management tool to manage work items, cycles, and product roadmaps easily",
},
{ property: "og:url", content: "https://app.plane.so/" },
{ property: "og:image", content: "https://app.plane.so/og-image.png" },
{ property: "og:image:width", content: "1200" },
{ property: "og:image:height", content: "630" },
{ property: "og:image:alt", content: "Plane - Modern project management" },
{ name: "twitter:site", content: "@planepowers" },
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:image", content: "https://app.plane.so/og-image.png" },
{ name: "twitter:image:width", content: "1200" },
{ name: "twitter:image:height", content: "630" },
{ name: "twitter:image:alt", content: "Plane - Modern project management" },
];
export default function RootLayout({ children }: { children: React.ReactNode }) {
// Pathless layout route wrapping every route (see app/routes.ts). Providers, the store
// layer, and app chrome live here instead of root.tsx so they stay out of the SPA-mode
// server build — see the note in app/root.tsx.
export default function AppShellLayout() {
return (
<html lang="en">
<head>
<meta name="theme-color" content="#fff" />
<link rel="icon" type="image/png" sizes="32x32" href={favicon32} />
<link rel="icon" type="image/png" sizes="16x16" href={favicon16} />
<link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href={faviconIco} />
{/* Meta info for PWA */}
<meta name="application-name" content="Plane" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content={SITE_NAME} />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" href={icon512} />
<link rel="apple-touch-icon" sizes="180x180" href={icon180} />
<link rel="apple-touch-icon" sizes="512x512" href={icon512} />
<link rel="manifest" href="/manifest.json" />
</head>
<body>
<div id="context-menu-portal" />
<div id="editor-portal" />
<AppProvider>
<div className={cn("relative flex h-screen w-full flex-col overflow-hidden", "app-container")}>
<main className="relative h-full w-full overflow-hidden">{children}</main>
</div>
</AppProvider>
</body>
</html>
<AppProvider>
<div className={cn("relative flex h-screen w-full flex-col overflow-hidden bg-canvas", "desktop-app-container")}>
<main className="relative h-full w-full overflow-hidden">
<Outlet />
</main>
</div>
</AppProvider>
);
}

View File

@@ -10,7 +10,6 @@ import type { LinksFunction } from "react-router";
import { ThemeProvider, useTheme } from "next-themes";
// plane imports
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants";
import { cn } from "@plane/utils";
// types
// assets
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
@@ -27,7 +26,6 @@ import { LogoSpinner } from "@/components/common/logo-spinner";
import { isStaleAssetError, recoverFromStaleAsset } from "@/lib/stale-asset-error";
// local
import { CustomErrorComponent } from "./error";
import { AppProvider } from "./provider";
// fonts
import "@fontsource-variable/inter";
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
@@ -110,16 +108,11 @@ export const meta: Route.MetaFunction = () => [
{ name: "twitter:image:alt", content: "Plane - Modern project management" },
];
// Root stays shell-thin: in SPA mode React Router server-builds only the root route, so
// everything imported here is evaluated in Node just to prerender the fallback index.html.
// Providers, the store layer, and app chrome belong in app/layout.tsx — never import them here.
export default function Root() {
return (
<AppProvider>
<div className={cn("relative flex h-screen w-full flex-col overflow-hidden bg-canvas", "desktop-app-container")}>
<main className="relative h-full w-full overflow-hidden">
<Outlet />
</main>
</div>
</AppProvider>
);
return <Outlet />;
}
export function HydrateFallback() {

View File

@@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
import { route } from "@react-router/dev/routes";
import { layout, route } from "@react-router/dev/routes";
import type { RouteConfigEntry } from "@react-router/dev/routes";
import { coreRoutes } from "./routes/core";
import { extendedRoutes } from "./routes/extended";
@@ -13,10 +13,13 @@ import { mergeRoutes } from "./routes/helper";
/**
* Main Routes Configuration
* This file serves as the entry point for the route configuration.
*
* Every route nests under the pathless app/layout.tsx shell; root.tsx stays
* shell-thin (see the note in app/root.tsx).
*/
const mergedRoutes: RouteConfigEntry[] = mergeRoutes(coreRoutes, extendedRoutes);
// Add catch-all route at the end (404 handler)
const routes: RouteConfigEntry[] = [...mergedRoutes, route("*", "./not-found.tsx")];
const routes: RouteConfigEntry[] = [layout("./layout.tsx", [...mergedRoutes, route("*", "./not-found.tsx")])];
export default routes;

View File

@@ -22,7 +22,7 @@ type Props<T extends IBaseLayoutsBaseItem> = {
blockUpdateHandler: (block: T, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreItems?: (groupId: string) => void;
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
blockIds: string[];
enableReorder: boolean;
showAllBlocks?: boolean;

View File

@@ -30,7 +30,7 @@ export type TCommentCardDisplayProps = {
disabled: boolean;
entityId: string;
projectId?: string;
readOnlyEditorRef: React.RefObject<EditorRefApi>;
readOnlyEditorRef: React.RefObject<EditorRefApi | null>;
showAccessSpecifier: boolean;
workspaceId: string;
workspaceSlug: string;

View File

@@ -4,6 +4,8 @@
* See the LICENSE file for details.
*/
// @types/react 19 removed the global JSX namespace; it is imported from react now.
import type { JSX } from "react";
// types
import type { ICycle, IModule, IProjectView, IWorkspaceView } from "@plane/types";
import type { TContextMenuItem } from "@plane/ui";

View File

@@ -8,7 +8,7 @@ import React, { useState, useRef, useCallback, useMemo } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useDropzone } from "react-dropzone";
import type { Control } from "react-hook-form";
import type { Control, FieldPath, FieldValues } from "react-hook-form";
import { Controller } from "react-hook-form";
import useSWR from "swr";
import { Popover } from "@headlessui/react";
@@ -34,10 +34,13 @@ type TTabOption = {
isEnabled: boolean;
};
type Props = {
// Generic over the form's values because react-hook-form's Control is invariant: its
// `_options.validate` narrows `name` to a keyof union, so `Control<any>` no longer
// accepts a typed form's control. Inferring from `control` keeps call sites unchanged.
type Props<TFieldValues extends FieldValues = FieldValues> = {
label: string | React.ReactNode;
value: string | null;
control: Control<any>;
control: Control<TFieldValues>;
onChange: (data: string) => void;
disabled?: boolean;
tabIndex?: number;
@@ -48,7 +51,7 @@ type Props = {
// services
const fileService = new FileService();
export const ImagePickerPopover = observer(function ImagePickerPopover(props: Props) {
function ImagePickerPopoverComponent<TFieldValues extends FieldValues = FieldValues>(props: Props<TFieldValues>) {
const { label, value, control, onChange, disabled = false, tabIndex, isProfileCover = false, projectId } = props;
// states
const [image, setImage] = useState<File | null>(null);
@@ -218,7 +221,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
<div className="flex items-center gap-x-2">
<Controller
control={control}
name="search"
name={"search" as FieldPath<TFieldValues>}
render={({ field: { value, ref } }) => (
<Input
id="search"
@@ -372,4 +375,7 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
)}
</Popover>
);
});
}
// observer() erases the generic signature, so restore it with a cast.
export const ImagePickerPopover = observer(ImagePickerPopoverComponent) as typeof ImagePickerPopoverComponent;

View File

@@ -22,7 +22,7 @@ interface IListItemProps {
appendTitleElement?: React.ReactNode;
actionableItems?: React.ReactNode;
isMobile?: boolean;
parentRef: React.RefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement | null>;
disableLink?: boolean;
className?: string;
itemClassName?: string;

View File

@@ -159,7 +159,8 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<form>
<Combobox
onChange={(val: string) => {
onChange={(val: string | null) => {
if (val === null) return;
const selectedIssues = watch("delete_issue_ids");
if (selectedIssues.includes(val))
setValue(
@@ -182,7 +183,7 @@ export const BulkDeleteIssuesModal = observer(function BulkDeleteIssuesModal(pro
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
<Combobox.Options as="ul" static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />

View File

@@ -139,7 +139,8 @@ export function ExistingIssuesListModal(props: Props) {
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Combobox
as="div"
onChange={(val: ISearchIssueResponse) => {
onChange={(val: ISearchIssueResponse | null) => {
if (val === null) return;
if (selectedIssues.some((i) => i.id === val.id))
setSelectedIssues((prevData) => prevData.filter((i) => i.id !== val.id));
else setSelectedIssues((prevData) => [...prevData, val]);
@@ -210,7 +211,11 @@ export function ExistingIssuesListModal(props: Props) {
)}
</div>
<Combobox.Options static className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto">
<Combobox.Options
as="ul"
static
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto"
>
{/* TODO: Translate here */}
{searchTerm !== "" && (
<h5 className="mx-2 text-13 text-secondary">

View File

@@ -15,7 +15,8 @@ type Props = {
horizontalOffset?: number;
root?: MutableRefObject<HTMLElement | null>;
children: ReactNode;
as?: keyof JSX.IntrinsicElements;
// @types/react 19 removed the global JSX namespace; it now lives under React.
as?: keyof React.JSX.IntrinsicElements;
classNames?: string;
placeholderChildren?: ReactNode;
defaultValue?: boolean;

View File

@@ -126,7 +126,7 @@ export const CycleAnalyticsProgress = observer(function CycleAnalyticsProgress(p
<div className="text-13 font-medium text-secondary">{t("project_cycles.active_cycle.progress")}</div>
</div>
)}
<Transition show={open}>
<Transition as="div" show={open}>
<Disclosure.Panel className="flex flex-col divide-y divide-subtle-1">
{cycleStartDate && cycleEndDate ? (
<>

View File

@@ -1,3 +1,4 @@
import { Fragment } from "react";
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
@@ -121,7 +122,7 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.Group as={Fragment} defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
className={cn(

View File

@@ -40,7 +40,7 @@ type Props = {
projectId: string;
cycleId: string;
cycleDetails: ICycle;
parentRef: React.RefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement | null>;
isActive?: boolean;
};

View File

@@ -26,7 +26,7 @@ import { CycleDeleteModal } from "./delete-modal";
import { CycleCreateUpdateModal } from "./modal";
type Props = {
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
cycleId: string;
projectId: string;
workspaceSlug: string;

View File

@@ -124,7 +124,7 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
query === "" ? options : options?.filter((o) => o.query.toLowerCase().includes(query.toLowerCase()));
return (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -149,6 +149,7 @@ export const CycleOptions = observer(function CycleOptions(props: CycleOptionsPr
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -261,7 +261,7 @@ export const DateRangeDropdown = observer(function DateRangeDropdown(props: Prop
);
const comboOptions = (
<Combobox.Options data-prevent-outside-click static>
<Combobox.Options as="ul" data-prevent-outside-click static>
<div
className="z-30 my-1 overflow-hidden rounded-md border-[0.5px] border-subtle-1 bg-surface-1"
ref={setPopperElement}

View File

@@ -181,7 +181,7 @@ export const DateDropdown = observer(function DateDropdown(props: Props) {
>
{isOpen &&
createPortal(
<Combobox.Options data-prevent-outside-click static>
<Combobox.Options as="ul" data-prevent-outside-click static>
<div
className={cn(
"z-30 my-1 overflow-hidden rounded-md border-[0.5px] border-strong bg-surface-1 shadow-raised-200",

View File

@@ -232,7 +232,7 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -268,7 +268,7 @@ export const EstimateDropdown = observer(function EstimateDropdown(props: Props)
{filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option key={option.value} value={option.value}>
<Combobox.Option as="li" key={option.value} value={option.value}>
{({ active, selected }) => (
<div
className={cn(

View File

@@ -215,7 +215,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}

View File

@@ -126,7 +126,7 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
);
return createPortal(
<Combobox.Options data-prevent-outside-click static>
<Combobox.Options as="ul" data-prevent-outside-click static>
<div
className={cn(
"z-30 my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none",
@@ -158,6 +158,7 @@ export const MemberOptions = observer(function MemberOptions(props: Props) {
(option) =>
option && (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -113,7 +113,7 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
);
return (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -138,6 +138,7 @@ export const ModuleOptions = observer(function ModuleOptions(props: Props) {
filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -463,7 +463,7 @@ export function PriorityDropdown(props: Props) {
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -487,6 +487,7 @@ export function PriorityDropdown(props: Props) {
{filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -238,7 +238,7 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
multiple={multiple}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -265,6 +265,7 @@ export const ProjectDropdownBase = observer(function ProjectDropdownBase(props:
if (!option) return;
return (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -217,7 +217,7 @@ export const WorkItemStateDropdownBase = observer(function WorkItemStateDropdown
renderByDefault={renderByDefault}
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}

View File

@@ -48,7 +48,7 @@ type Props = {
/**
* @description Editor ref, this will be used to imperatively attach editor related helper functions
*/
editorRef?: React.RefObject<EditorRefApi>;
editorRef?: React.RefObject<EditorRefApi | null>;
/**
* @description Entity ID, this will be used for file uploads and as the unique identifier for the entity
*/

View File

@@ -20,7 +20,7 @@ export type GanttChartBlocksProps = {
enableAddBlock: boolean | ((blockId: string) => boolean);
showAllBlocks: boolean;
selectionHelpers: TSelectionHelper;
ganttContainerRef: React.RefObject<HTMLDivElement>;
ganttContainerRef: React.RefObject<HTMLDivElement | null>;
};
export function GanttChartRowList(props: GanttChartBlocksProps) {

View File

@@ -25,7 +25,7 @@ type Props = {
handleScrollToBlock: (block: IGanttBlock) => void;
enableAddBlock: boolean;
selectionHelpers: TSelectionHelper;
ganttContainerRef: React.RefObject<HTMLDivElement>;
ganttContainerRef: React.RefObject<HTMLDivElement | null>;
};
export const BlockRow = observer(function BlockRow(props: Props) {

View File

@@ -28,7 +28,7 @@ type Props = {
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableDependency: boolean;
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
};

View File

@@ -14,7 +14,7 @@ export type GanttChartBlocksProps = {
enableBlockLeftResize: boolean | ((blockId: string) => boolean);
enableBlockRightResize: boolean | ((blockId: string) => boolean);
enableBlockMove: boolean | ((blockId: string) => boolean);
ganttContainerRef: React.RefObject<HTMLDivElement>;
ganttContainerRef: React.RefObject<HTMLDivElement | null>;
showAllBlocks: boolean;
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>;
enableDependency: boolean | ((blockId: string) => boolean);

View File

@@ -13,7 +13,7 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "../constants";
type Props = {
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
};
export const TimelineDragHelper = observer(function TimelineDragHelper(props: Props) {
const { ganttContainerRef } = props;

View File

@@ -15,8 +15,8 @@ import { DEFAULT_BLOCK_WIDTH, SIDEBAR_WIDTH } from "../../constants";
export const useGanttResizable = (
block: IGanttBlock,
resizableRef: React.RefObject<HTMLDivElement>,
ganttContainerRef: React.RefObject<HTMLDivElement>,
resizableRef: React.RefObject<HTMLDivElement | null>,
ganttContainerRef: React.RefObject<HTMLDivElement | null>,
updateBlockDates?: (updates: IBlockUpdateDependencyData[]) => Promise<void>
) => {
// refs
@@ -25,8 +25,8 @@ export const useGanttResizable = (
width: 0,
offsetX: 0,
});
const ganttContainerDimensions = useRef<DOMRect | undefined>();
const currMouseEvent = useRef<MouseEvent | undefined>();
const ganttContainerDimensions = useRef<DOMRect | undefined>(undefined);
const currMouseEvent = useRef<MouseEvent | undefined>(undefined);
// states
const { currentViewData, updateBlockPosition, setIsDragging, getUpdatedPositionAfterDrag } = useTimeLineChartStore();
const [isMoving, setIsMoving] = useState<"left" | "right" | "move" | undefined>();

View File

@@ -24,7 +24,7 @@ type Props = {
enableBlockRightResize: boolean;
enableBlockMove: boolean;
enableDependency: boolean | ((blockId: string) => boolean);
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
};
export const ChartDraggable = observer(function ChartDraggable(props: Props) {

View File

@@ -28,7 +28,7 @@ type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
blockIds: string[];
enableReorder: boolean;
enableSelection: boolean;

View File

@@ -23,7 +23,7 @@ type Props = {
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
canLoadMoreBlocks?: boolean;
loadMoreBlocks?: () => void;
ganttContainerRef: RefObject<HTMLDivElement>;
ganttContainerRef: RefObject<HTMLDivElement | null>;
enableReorder: boolean | ((blockId: string) => boolean);
enableSelection: boolean | ((blockId: string) => boolean);
sidebarToRender: (props: any) => React.ReactNode;

View File

@@ -63,7 +63,7 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
return combine(
draggable({
element,
dragHandle: elementRef.current,
dragHandle: element,
getInitialData: () => initialData,
onDragStart: () => {
setIsDragging(true);

View File

@@ -23,7 +23,7 @@ import { useProjectState } from "@/hooks/store/use-project-state";
type BlockProps = {
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
ref: React.RefObject<HTMLDivElement | null>;
workspaceSlug: string;
};
export const RecentIssue = observer(function RecentIssue(props: BlockProps) {

View File

@@ -17,7 +17,7 @@ import { useMember } from "@/hooks/store/use-member";
type BlockProps = {
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
ref: React.RefObject<HTMLDivElement | null>;
workspaceSlug: string;
};

View File

@@ -16,7 +16,7 @@ import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
type BlockProps = {
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
ref: React.RefObject<HTMLDivElement | null>;
workspaceSlug: string;
};
export function RecentProject(props: BlockProps) {

View File

@@ -32,7 +32,7 @@ type TInboxIssueDescription = {
workspaceId: string;
data: Partial<TIssue>;
handleData: (issueKey: keyof Partial<TIssue>, issueValue: Partial<TIssue>[keyof Partial<TIssue>]) => void;
editorRef: RefObject<EditorRefApi>;
editorRef: RefObject<EditorRefApi | null>;
onEnterKeyPress?: (e?: any) => void;
onAssetUpload?: (assetId: string) => void;
};

View File

@@ -86,7 +86,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
const issueList =
filteredIssues.length > 0 ? (
<li className="p-2">
<div className="p-2">
{query === "" && <h2 className="mt-4 mb-2 px-3 text-11 font-semibold text-primary">Select work item</h2>}
<ul className="text-13 text-primary">
{filteredIssues.map((issue) => {
@@ -95,7 +95,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
return (
<Combobox.Option
key={issue.id}
as="div"
as="li"
value={issue.id}
className={({ active, selected }) =>
`flex w-full cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-secondary select-none ${
@@ -119,7 +119,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
);
})}
</ul>
</li>
</div>
) : (
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
{query === "" ? (
@@ -132,7 +132,12 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
return (
<ModalCore isOpen={isOpen} handleClose={handleClose} position={EModalPosition.CENTER} width={EModalWidth.XXL}>
<Combobox value={value} onChange={handleSubmit}>
<Combobox
value={value}
onChange={(selected: string | null) => {
if (selected !== null) handleSubmit(selected);
}}
>
<div className="relative m-1">
<SearchIcon
className="text-opacity-40 pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-primary"
@@ -146,7 +151,7 @@ export function SelectDuplicateInboxIssueModal(props: Props) {
/>
</div>
<Combobox.Options static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
<Combobox.Options as="ul" static className="max-h-80 scroll-py-2 divide-y divide-subtle-1 overflow-y-auto">
{isSearching ? (
<Loader className="space-y-3 p-3">
<Loader.Item height="40px" />

View File

@@ -139,7 +139,7 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
</Button>
</Combobox.Button>
<Combobox.Options className="fixed z-10">
<Combobox.Options as="ul" className="fixed z-10">
<div
className={`z-10 my-1 w-48 rounded-sm border border-strong bg-surface-1 py-2.5 text-11 whitespace-nowrap shadow-raised-200 focus:outline-none`}
ref={setPopperElement}
@@ -160,54 +160,60 @@ export const IssueLabelSelect = observer(function IssueLabelSelect(props: IIssue
/>
</div>
</div>
<div className={`vertical-scrollbar mt-2 scrollbar-sm max-h-48 space-y-1 overflow-y-scroll px-2 pr-0`}>
<div className={`vertical-scrollbar mt-2 scrollbar-sm max-h-48 overflow-y-scroll px-2 pr-0`}>
{isLoading ? (
<p className="text-center text-secondary">{t("common.loading")}</p>
) : filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none hover:bg-layer-1 ${
selected ? "text-primary" : "text-secondary"
}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<CheckIcon className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))
<ul className="space-y-1">
{filteredOptions.map((option) => (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ selected }) =>
`flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none hover:bg-layer-1 ${
selected ? "text-primary" : "text-secondary"
}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<CheckIcon className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))}
</ul>
) : submitting ? (
<Loader className="spin h-3.5 w-3.5" />
) : canCreateLabel ? (
<Combobox.Option
value={query}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!query.length) return;
handleAddLabel(query);
}}
className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
>
{query.length ? (
<>
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to
labels
</>
) : (
t("label.create.type")
)}
</Combobox.Option>
<ul className="space-y-1">
<Combobox.Option
as="li"
value={query}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!query.length) return;
handleAddLabel(query);
}}
className={`text-left text-secondary ${query.length ? "cursor-pointer" : "cursor-default"}`}
>
{query.length ? (
<>
{/* TODO: Translate here */}+ Add <span className="text-primary">&quot;{query}&quot;</span> to
labels
</>
) : (
t("label.create.type")
)}
</Combobox.Option>
</ul>
) : (
<p className="text-left text-secondary">{t("common.search.no_matching_results")}</p>
)}

View File

@@ -53,7 +53,7 @@ interface IssueBlockProps {
}
interface IssueDetailsBlockProps {
cardRef: React.RefObject<HTMLElement>;
cardRef: React.RefObject<HTMLElement | null>;
issue: TIssue;
displayProperties: IIssueDisplayProperties | undefined;
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;

View File

@@ -2,7 +2,7 @@ import type { TPlacement } from "@plane/propel/utils/placement";
import type { TIssue } from "@plane/types";
export interface IQuickActionProps {
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
issue: TIssue;
handleDelete: () => Promise<void>;
handleUpdate?: (data: TIssue) => Promise<void>;
@@ -24,7 +24,7 @@ export type TRenderQuickActions = ({
portalElement,
}: {
issue: TIssue;
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
customActionButton?: React.ReactElement;
placement?: TPlacement;
portalElement?: HTMLDivElement | null;

View File

@@ -252,7 +252,7 @@ export function LabelDropdown(props: ILabelDropdownProps) {
multiple
>
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className={`z-10 my-1 h-auto w-48 rounded-sm border border-strong bg-surface-1 px-2 py-2.5 text-caption-sm-regular whitespace-nowrap shadow-raised-200 focus:outline-none ${optionsClassName}`}
ref={setPopperElement}
@@ -271,38 +271,41 @@ export function LabelDropdown(props: ILabelDropdownProps) {
onKeyDown={searchInputKeyDown}
/>
</div>
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
<div className={`mt-2 max-h-48 overflow-y-scroll`}>
{isLoading ? (
<p className="text-center text-secondary">{t("common.loading")}</p>
) : filteredOptions && filteredOptions.length > 0 ? (
filteredOptions.map((option) => (
<Combobox.Option
key={option.value}
value={option.value}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
<ul className="space-y-1">
{filteredOptions.map((option) => (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
}
}}
className={({ active, selected }) =>
`flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none hover:bg-layer-1 ${
active ? "bg-layer-1" : ""
} ${selected ? "text-primary" : "text-secondary"}`
}
}}
className={({ active, selected }) =>
`flex cursor-pointer items-center justify-between gap-2 truncate rounded-sm px-1 py-1.5 select-none hover:bg-layer-1 ${
active ? "bg-layer-1" : ""
} ${selected ? "text-primary" : "text-secondary"}`
}
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<CheckIcon className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))
>
{({ selected }) => (
<>
{option.content}
{selected && (
<div className="flex-shrink-0">
<CheckIcon className={`h-3.5 w-3.5`} />
</div>
)}
</>
)}
</Combobox.Option>
))}
</ul>
) : submitting ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : canCreateLabel ? (

View File

@@ -21,7 +21,7 @@ import { QuickAddIssueFormRoot } from "./form";
import { CreateIssueToastActionItems } from "../../create-issue-toast-action-items";
export type TQuickAddIssueForm = {
ref: React.RefObject<HTMLFormElement>;
ref: React.RefObject<HTMLFormElement | null>;
isOpen: boolean;
projectDetail: IProject;
hasError: boolean;

View File

@@ -112,7 +112,11 @@ export function ParentIssuesListModal({
tabIndex={baseTabIndex}
/>
</div>
<Combobox.Options static className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto">
<Combobox.Options
as="ul"
static
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto"
>
{searchTerm !== "" && (
<h5 className="mx-2 text-13 text-secondary">
Search results for{" "}
@@ -145,6 +149,7 @@ export function ParentIssuesListModal({
<ul className={`text-13 ${issues.length > 0 ? "p-2" : ""}`}>
{issues.map((issue) => (
<Combobox.Option
as="li"
key={issue.id}
value={issue}
className={({ active, selected }) =>

View File

@@ -33,7 +33,7 @@ import { IssueTitleInput } from "../title-input";
const workItemVersionService = new WorkItemVersionService();
type Props = {
editorRef: React.RefObject<EditorRefApi>;
editorRef: React.RefObject<EditorRefApi | null>;
workspaceSlug: string;
projectId: string;
issueId: string;

View File

@@ -190,9 +190,8 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
</div>
)}
</button>
{isDropdownOpen && (
<Combobox.Options className="fixed z-10" static>
<Combobox.Options as="ul" className="fixed z-10" static>
<div
className="my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2.5 text-11 shadow-raised-200 focus:outline-none"
ref={setPopperElement}
@@ -211,81 +210,85 @@ export const WorkItemLabelSelectBase = observer(function WorkItemLabelSelectBase
onKeyDown={searchInputKeyDown}
/>
</div>
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">
<div className="mt-2 max-h-48 overflow-y-scroll">
{labelsList && filteredOptions ? (
filteredOptions.length > 0 ? (
filteredOptions.map((label) => {
const children = labelsList?.filter((l) => l.parent === label.id);
<ul className="space-y-1">
{filteredOptions.map((label) => {
const children = labelsList?.filter((l) => l.parent === label.id);
if (children.length === 0) {
if (!label.parent)
return (
<Combobox.Option
key={label.id}
className={({ active }) =>
`${
active ? "bg-layer-1" : ""
} group flex w-full cursor-pointer items-center gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none`
}
value={label.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-sm">
<div className="flex items-center justify-start gap-2 truncate">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span className="truncate">{label.name}</span>
</div>
<div className="flex shrink-0 items-center justify-center rounded-sm p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
);
} else
return (
<div key={label.id} className="border-y border-subtle">
<div className="flex items-center gap-2 truncate p-2 text-primary select-none">
<Component className="h-3 w-3" /> {label.name}
</div>
<div>
{children.map((child) => (
<Combobox.Option
key={child.id}
className={({ active }) =>
`${
active ? "bg-layer-1" : ""
} group flex min-w-[14rem] cursor-pointer items-center gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none`
}
value={child.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-sm">
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color,
}}
/>
<span>{child.name}</span>
</div>
<div className="flex items-center justify-center rounded-sm p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
if (children.length === 0) {
if (!label.parent)
return (
<Combobox.Option
as="li"
key={label.id}
className={({ active }) =>
`${
active ? "bg-layer-1" : ""
} group flex w-full cursor-pointer items-center gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none`
}
value={label.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-sm">
<div className="flex items-center justify-start gap-2 truncate">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: label.color,
}}
/>
<span className="truncate">{label.name}</span>
</div>
)}
</Combobox.Option>
))}
</div>
</div>
);
})
<div className="flex shrink-0 items-center justify-center rounded-sm p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
);
} else
return (
<li key={label.id} className="border-y border-subtle">
<div className="flex items-center gap-2 truncate p-2 text-primary select-none">
<Component className="h-3 w-3" /> {label.name}
</div>
<ul>
{children.map((child) => (
<Combobox.Option
as="li"
key={child.id}
className={({ active }) =>
`${
active ? "bg-layer-1" : ""
} group flex min-w-[14rem] cursor-pointer items-center gap-2 truncate rounded-sm px-1 py-1.5 text-secondary select-none`
}
value={child.id}
>
{({ selected }) => (
<div className="flex w-full justify-between gap-2 rounded-sm">
<div className="flex items-center justify-start gap-2">
<span
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
style={{
backgroundColor: child?.color,
}}
/>
<span>{child.name}</span>
</div>
<div className="flex items-center justify-center rounded-sm p-1">
<CheckIcon className={`h-3 w-3 ${selected ? "opacity-100" : "opacity-0"}`} />
</div>
</div>
)}
</Combobox.Option>
))}
</ul>
</li>
);
})}
</ul>
) : submitting ? (
<Loader className="h-3.5 w-3.5 animate-spin" />
) : createLabelEnabled ? (

View File

@@ -13,7 +13,7 @@ import { ContextMenu, CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
export interface Props {
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
MENU_ITEMS: TContextMenuItem[];
}

View File

@@ -124,6 +124,7 @@ export const ProjectSettingLabelGroup = observer(function ProjectSettingLabelGro
</Disclosure.Button>
</div>
<Transition
as="div"
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"

View File

@@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
import { useState } from "react";
import { useState, Fragment } from "react";
import { observer } from "mobx-react";
import { CheckCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
@@ -42,7 +42,7 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
return (
<div className="flex flex-col rounded-xl border border-subtle bg-layer-2 px-3 py-6">
<Tab.Group selectedIndex={selectedPlan === "month" ? 0 : 1}>
<Tab.Group as={Fragment} selectedIndex={selectedPlan === "month" ? 0 : 1}>
<div className="flex h-9 w-full justify-center">
<Tab.List className="flex w-60 space-x-1 rounded-md bg-layer-3 p-0.5">
{prices.map((price: TSubscriptionPrice) => (

View File

@@ -177,7 +177,7 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
</div>
)}
<Transition show={open}>
<Transition as="div" show={open}>
<Disclosure.Panel className="space-y-4">
{/* progress burndown chart */}
<div>

View File

@@ -1,3 +1,4 @@
import { Fragment } from "react";
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
@@ -119,7 +120,7 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.Group as={Fragment} defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
className={cn(

View File

@@ -388,7 +388,7 @@ export const ModuleAnalyticsSidebar = observer(function ModuleAnalyticsSidebar(p
/>
</div>
</Disclosure.Button>
<Transition show={open}>
<Transition as="div" show={open}>
<Disclosure.Panel>
<div className="mt-2 flex min-h-72 w-full flex-col space-y-3 overflow-y-auto">
{isEditingAllowed && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (

View File

@@ -30,7 +30,7 @@ import { ButtonAvatars } from "../dropdowns/member/avatar";
type Props = {
moduleId: string;
moduleDetails: IModule;
parentRef: React.RefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement | null>;
};
export const ModuleListItemAction = observer(function ModuleListItemAction(props: Props) {

View File

@@ -23,7 +23,7 @@ import { useUserPermissions } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
type Props = {
parentRef: React.RefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement | null>;
moduleId: string;
projectId: string;
workspaceSlug: string;

View File

@@ -53,6 +53,7 @@ export const SwitchAccountDropdown = observer(function SwitchAccountDropdown(pro
<span className="text-13 font-medium text-secondary">{displayName}</span>
</Menu.Button>
<Transition
as="div"
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"

View File

@@ -45,7 +45,7 @@ type Props = {
extraOptions?: (TContextMenuItem & { key: TPageActions })[];
optionsOrder: TPageActions[];
page: TPageInstance;
parentRef?: React.RefObject<HTMLElement>;
parentRef?: React.RefObject<HTMLElement | null>;
storeType: EPageStoreType;
};

View File

@@ -58,7 +58,7 @@ export type TEditorBodyHandlers = {
type Props = {
config: TEditorBodyConfig;
editorReady: boolean;
editorForwardRef: React.RefObject<EditorRefApi>;
editorForwardRef: React.RefObject<EditorRefApi | null>;
handleEditorReady: (status: boolean) => void;
handleOpenNavigationPane: () => void;
handlers: TEditorBodyHandlers;

View File

@@ -23,7 +23,7 @@ import { PageActions } from "../dropdowns";
type Props = {
page: TPageInstance;
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
storeType: EPageStoreType;
};

View File

@@ -189,6 +189,7 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
</div>
</Disclosure.Button>
<Transition
as="div"
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"

View File

@@ -123,6 +123,7 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
</div>
)}
<Combobox.Options
as="ul"
static
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto py-2 transition-[height] duration-200 ease-in-out"
>
@@ -146,6 +147,7 @@ export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal
const isProjectSelected = selectedProjectIds.includes(projectDetails.id);
return (
<Combobox.Option
as="li"
key={projectDetails.id}
value={projectDetails.id}
className={({ active }) =>

View File

@@ -51,6 +51,7 @@ export function SelectedOptionsDisplay<V extends TFilterValue>(props: TSelectedO
))}
{remainingCount > 0 && (
<Transition
as="div"
show
appear
enter="transition-opacity duration-300"

View File

@@ -148,6 +148,7 @@ type TElementTransitionProps = {
const ElementTransition = observer(function ElementTransition(props: TElementTransitionProps) {
return (
<Transition
as="div"
show={props.show}
enter="transition ease-out duration-200"
enterFrom="opacity-0 scale-95"
@@ -169,6 +170,7 @@ type TRowTransitionProps = {
const RowTransition = observer(function RowTransition(props: TRowTransitionProps) {
return (
<Transition
as="div"
show={props.show}
enter="transition-all duration-150 ease-out"
enterFrom="opacity-0 -translate-y-1"

View File

@@ -53,7 +53,7 @@ export function ResizableSidebar({
const [isResizing, setIsResizing] = useState(false);
const [isHoveringTrigger, setIsHoveringTrigger] = useState(false);
// refs
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const initialWidthRef = useRef<number>(0);
const initialMouseXRef = useRef<number>(0);
// hooks

View File

@@ -26,7 +26,7 @@ import { DeleteProjectViewModal } from "./delete-view-modal";
import { CreateUpdateProjectViewModal } from "./modal";
type Props = {
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
projectId: string;
view: IProjectView;
workspaceSlug: string;

View File

@@ -28,7 +28,7 @@ import { CreateUpdateProjectViewModal } from "./modal";
import { ViewQuickActions } from "./quick-actions";
type Props = {
parentRef: React.RefObject<HTMLElement>;
parentRef: React.RefObject<HTMLElement | null>;
view: IProjectView;
};

View File

@@ -25,6 +25,7 @@ export function WebhookDeleteSection(props: Props) {
</Disclosure.Button>
<Transition
as="div"
show={open}
enter="transition duration-100 ease-out"
enterFrom="transform opacity-0"

View File

@@ -28,6 +28,7 @@ export const StateOption = observer(function StateOption(props: TStateOptionProp
return (
<Combobox.Option
as="li"
key={option.value}
value={option.value}
className={({ active, selected }) =>

View File

@@ -261,6 +261,7 @@ export function FavoriteFolder(props: Props) {
</div>
{favorite.children && favorite.children.length > 0 && (
<Transition
as="div"
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"

View File

@@ -10,7 +10,7 @@ import { cn } from "@plane/utils";
type Props = {
children: React.ReactNode;
elementRef: React.RefObject<HTMLDivElement>;
elementRef: React.RefObject<HTMLDivElement | null>;
isMenuActive?: boolean;
};

View File

@@ -61,7 +61,7 @@ export const FavoriteRoot = observer(function FavoriteRoot(props: Props) {
return combine(
draggable({
element,
dragHandle: elementRef.current,
dragHandle: element,
getInitialData: () => initialData,
onDragStart: () => {
setIsDragging(true);

View File

@@ -238,6 +238,7 @@ export const SidebarFavoritesMenu = observer(function SidebarFavoritesMenu() {
</div>
</div>
<Transition
as="div"
show={isFavoriteMenuOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"

View File

@@ -466,6 +466,7 @@ export const SidebarProjectsListItem = observer(function SidebarProjectsListItem
</div>
{isAccordionMode && (
<Transition
as="div"
show={isProjectListOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"

View File

@@ -218,6 +218,7 @@ export const SidebarProjectsList = observer(function SidebarProjectsList() {
</div>
</div>
<Transition
as="div"
show={isAllProjectsListOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"

View File

@@ -28,7 +28,7 @@ export const SidebarQuickActions = observer(function SidebarQuickActions() {
const [_isDraftButtonOpen, setIsDraftButtonOpen] = useState(false);
// refs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const timeoutRef = useRef<any>();
const timeoutRef = useRef<any>(undefined);
// router
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();

View File

@@ -138,6 +138,7 @@ export const SidebarMenuItems = observer(function SidebarMenuItems() {
</div>
</div>
<Transition
as="div"
show={!!isWorkspaceMenuOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"

View File

@@ -60,6 +60,7 @@ export const SidebarWorkspaceMenu = observer(function SidebarWorkspaceMenu() {
<Disclosure as="div" defaultOpen>
<SidebarWorkspaceMenuHeader isWorkspaceMenuOpen={isWorkspaceMenuOpen} toggleWorkspaceMenu={toggleWorkspaceMenu} />
<Transition
as="div"
show={isWorkspaceMenuOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"

View File

@@ -21,7 +21,7 @@ import type { TPageInstance } from "@/store/pages/base-page";
export type TPageExtensionHookParams = {
page: TPageInstance;
editorRef: RefObject<EditorRefApi>;
editorRef: RefObject<EditorRefApi | null>;
};
export const usePagesPaneExtensions = (_params: TPageExtensionHookParams) => {

View File

@@ -13,12 +13,12 @@ const AUTO_SCROLL_THRESHOLD = 15;
const MAX_SPEED_THRESHOLD = 5;
export const useAutoScroller = (
containerRef: RefObject<HTMLDivElement>,
containerRef: RefObject<HTMLDivElement | null>,
shouldScroll = false,
leftOffset = 0,
topOffset = 0
) => {
const containerDimensions = useRef<DOMRect | undefined>();
const containerDimensions = useRef<DOMRect | undefined>(undefined);
const intervalId = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
const mousePosition = useRef<{ clientX: number; clientY: number } | undefined>(undefined);

View File

@@ -12,7 +12,7 @@ import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import { usePlatformOS } from "./use-platform-os";
type TArguments = {
dropdownRef: React.RefObject<HTMLDivElement>;
dropdownRef: React.RefObject<HTMLDivElement | null>;
inputRef?: React.RefObject<HTMLInputElement | null>;
isOpen: boolean;
onClose?: () => void;

View File

@@ -8,7 +8,7 @@ import type React from "react";
import { useEffect, useCallback } from "react";
const useExtendedSidebarOutsideClickDetector = (
ref: React.RefObject<HTMLElement>,
ref: React.RefObject<HTMLElement | null>,
callback: () => void,
targetId: string
) => {

View File

@@ -4,7 +4,7 @@
* See the LICENSE file for details.
*/
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useParams } from "next/navigation";
const useIntegrationPopup = ({
@@ -30,19 +30,34 @@ const useIntegrationPopup = ({
}`,
};
const popup = useRef<any>();
const popup = useRef<Window | null>(null);
const popupCheckIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
// release the polling interval if the component unmounts mid-auth
useEffect(
() => () => {
if (popupCheckIntervalRef.current) clearInterval(popupCheckIntervalRef.current);
},
[]
);
const checkPopup = () => {
const check = setInterval(() => {
if (!popup || popup.current.closed || popup.current.closed === undefined) {
clearInterval(check);
// a repeated click restarts the flow, so retire the previous poller before
// replacing the ref — otherwise it outlives its popup and clears its successor
if (popupCheckIntervalRef.current) clearInterval(popupCheckIntervalRef.current);
const intervalId = setInterval(() => {
if (!popup.current || popup.current.closed) {
clearInterval(intervalId);
popupCheckIntervalRef.current = undefined;
setAuthLoader(false);
}
}, 1000);
popupCheckIntervalRef.current = intervalId;
};
const openPopup = () => {
if (!provider) return;
const openPopup = (): Window | null => {
if (!provider) return null;
const width = 600,
height = 600;
@@ -55,6 +70,7 @@ const useIntegrationPopup = ({
const startAuth = () => {
popup.current = openPopup();
if (!popup.current) return;
checkPopup();
setAuthLoader(true);
};

View File

@@ -15,7 +15,7 @@ import useAutoSave from "@/hooks/use-auto-save";
import type { TPageInstance } from "@/store/pages/base-page";
type TArgs = {
editorRef: React.RefObject<EditorRefApi>;
editorRef: React.RefObject<EditorRefApi | null>;
fetchPageDescription: () => Promise<ArrayBuffer>;
collaborationState: CollaborationState | null;
updatePageDescription: (data: TDocumentPayload) => Promise<void>;

View File

@@ -8,7 +8,7 @@ import type React from "react";
import { useEffect, useCallback } from "react";
const usePeekOverviewOutsideClickDetector = (
ref: React.RefObject<HTMLElement>,
ref: React.RefObject<HTMLElement | null>,
callback: () => void,
issueId: string,
excludePreventionElementIds?: string[]

View File

@@ -24,12 +24,12 @@ export interface IBasePowerKStore {
commandRegistry: IPowerKCommandRegistry;
activeContext: TPowerKContextType | null;
activePage: TPowerKPageType | null;
topNavInputRef: React.RefObject<HTMLInputElement> | null;
topNavSearchInputRef: React.RefObject<HTMLInputElement> | null;
topNavInputRef: React.RefObject<HTMLInputElement | null> | null;
topNavSearchInputRef: React.RefObject<HTMLInputElement | null> | null;
setActiveContext: (entity: TPowerKContextType | null) => void;
setActivePage: (page: TPowerKPageType | null) => void;
setTopNavInputRef: (ref: React.RefObject<HTMLInputElement> | null) => void;
setTopNavSearchInputRef: (ref: React.RefObject<HTMLInputElement> | null) => void;
setTopNavInputRef: (ref: React.RefObject<HTMLInputElement | null> | null) => void;
setTopNavSearchInputRef: (ref: React.RefObject<HTMLInputElement | null> | null) => void;
// toggle actions
togglePowerKModal: (value?: boolean) => void;
toggleShortcutsListModal: (value?: boolean) => void;
@@ -42,8 +42,8 @@ export class BasePowerKStore implements IBasePowerKStore {
commandRegistry: IPowerKCommandRegistry = new PowerKCommandRegistry();
activeContext: TPowerKContextType | null = null;
activePage: TPowerKPageType | null = null;
topNavInputRef: React.RefObject<HTMLInputElement> | null = null;
topNavSearchInputRef: React.RefObject<HTMLInputElement> | null = null;
topNavInputRef: React.RefObject<HTMLInputElement | null> | null = null;
topNavSearchInputRef: React.RefObject<HTMLInputElement | null> | null = null;
constructor() {
makeObservable(this, {
@@ -85,7 +85,7 @@ export class BasePowerKStore implements IBasePowerKStore {
* Sets the top nav input ref for keyboard shortcut access
* @param ref
*/
setTopNavInputRef = (ref: React.RefObject<HTMLInputElement> | null) => {
setTopNavInputRef = (ref: React.RefObject<HTMLInputElement | null> | null) => {
this.topNavInputRef = ref;
};
@@ -93,7 +93,7 @@ export class BasePowerKStore implements IBasePowerKStore {
* Sets the top nav search input ref for keyboard shortcut access
* @param ref
*/
setTopNavSearchInputRef = (ref: React.RefObject<HTMLInputElement> | null) => {
setTopNavSearchInputRef = (ref: React.RefObject<HTMLInputElement | null> | null) => {
this.topNavSearchInputRef = ref;
};