From 83ea51596225c96f2985644c924955c76263d41a Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Fri, 15 May 2026 19:18:25 +0300 Subject: [PATCH] perf: optimize Tasker boot and kanban load --- .../issues/(list)/mobile-header.tsx | 23 ++-- .../(projects)/top-toolbar/compact-layout.tsx | 4 +- .../top-toolbar/expanded-layout.tsx | 4 +- .../web/app/(all)/[workspaceSlug]/layout.tsx | 8 +- .../web/ce/components/common/modal/global.tsx | 5 + .../navigations/top-navigation-root.tsx | 4 +- plane-src/apps/web/ce/store/root.store.ts | 17 ++- .../common/modal/lazy-workspace-modals.tsx | 94 ++++++++++++++++ .../issues/issue-layouts/issue-layout-HOC.tsx | 10 +- .../issues/issue-layouts/kanban/block.tsx | 1 + .../issue-layouts/kanban/blocks-list.tsx | 2 +- .../issues/issue-layouts/kanban/default.tsx | 2 +- .../issue-layouts/roots/cycle-layout-root.tsx | 44 ++++++-- .../roots/module-layout-root.tsx | 40 +++++-- .../roots/project-layout-root.tsx | 37 ++++-- .../roots/project-view-layout-root.tsx | 40 +++++-- .../components/modules/modules-list-view.tsx | 13 ++- .../navigation/deferred-top-nav-power-k.tsx | 105 ++++++++++++++++++ .../navigation/tab-navigation-root.tsx | 36 ++++-- .../components/navigation/top-nav-power-k.tsx | 64 +++++++---- .../power-k/projects-app-provider.tsx | 83 +++++++++++--- .../sidebar/sidebar-utility-rail.tsx | 4 +- .../core/hooks/use-issue-realtime-events.ts | 12 +- .../apps/web/core/hooks/use-timeline-chart.ts | 2 + .../layouts/auth-layout/project-wrapper.tsx | 11 +- .../apps/web/core/store/editor/asset.store.ts | 23 ++-- .../issue/issue-details/attachment.store.ts | 26 +++-- .../core/store/workspace/api-token.store.ts | 81 ++++++++------ plane-src/apps/web/nginx/nginx.conf | 22 ++++ 29 files changed, 631 insertions(+), 186 deletions(-) create mode 100644 plane-src/apps/web/core/components/common/modal/lazy-workspace-modals.tsx create mode 100644 plane-src/apps/web/core/components/navigation/deferred-top-nav-power-k.tsx diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index e43b38e..04e2a1e 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import { useCallback, useState } from "react"; +import { lazy, Suspense, useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports @@ -14,7 +14,6 @@ import { ChevronDownIcon } from "@plane/propel/icons"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; // components -import { WorkItemsModal } from "@/components/analytics/work-items/modal"; import { DisplayFiltersSelection, FiltersDropdown, @@ -24,6 +23,12 @@ import { import { useIssues } from "@/hooks/store/use-issues"; import { useProject } from "@/hooks/store/use-project"; +const WorkItemsModal = lazy(() => + import("@/components/analytics/work-items/modal").then((module) => ({ + default: module.WorkItemsModal, + })) +); + export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHeader() { // i18n const { t } = useTranslation(); @@ -63,11 +68,15 @@ export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHe return ( <> - setAnalyticsModal(false)} - projectDetails={currentProjectDetails ?? undefined} - /> + {analyticsModal && ( + + setAnalyticsModal(false)} + projectDetails={currentProjectDetails ?? undefined} + /> + + )}
- + {!isWorkspaceHome && (
- +
)} diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/layout.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/layout.tsx index 25fed7a..6d97337 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/layout.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/layout.tsx @@ -10,9 +10,7 @@ import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/conten import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail"; import { GlobalModals } from "@/plane-web/components/common/modal/global"; import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper"; -import { ProjectSettingsModal } from "@/components/project/settings/project-settings-modal"; -import { WorkspaceSettingsModal } from "@/components/workspace/settings/workspace-settings-modal"; -import { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal"; +import { LazyWorkspaceModals } from "@/components/common/modal/lazy-workspace-modals"; import type { Route } from "./+types/layout"; export default function WorkspaceLayout(props: Route.ComponentProps) { @@ -24,9 +22,7 @@ export default function WorkspaceLayout(props: Route.ComponentProps) { - - - + diff --git a/plane-src/apps/web/ce/components/common/modal/global.tsx b/plane-src/apps/web/ce/components/common/modal/global.tsx index 98292c6..b71bb3e 100644 --- a/plane-src/apps/web/ce/components/common/modal/global.tsx +++ b/plane-src/apps/web/ce/components/common/modal/global.tsx @@ -6,6 +6,7 @@ import { lazy, Suspense } from "react"; import { observer } from "mobx-react"; +import { useCommandPalette } from "@/hooks/store/use-command-palette"; const ProfileSettingsModal = lazy(() => import("@/components/settings/profile/modal").then((module) => ({ @@ -24,6 +25,10 @@ type TGlobalModalsProps = { * - Profile settings modal */ export const GlobalModals = observer(function GlobalModals(_props: TGlobalModalsProps) { + const { profileSettingsModal } = useCommandPalette(); + + if (!profileSettingsModal.isOpen) return null; + return ( diff --git a/plane-src/apps/web/ce/components/navigations/top-navigation-root.tsx b/plane-src/apps/web/ce/components/navigations/top-navigation-root.tsx index 2322cb7..1e5ebae 100644 --- a/plane-src/apps/web/ce/components/navigations/top-navigation-root.tsx +++ b/plane-src/apps/web/ce/components/navigations/top-navigation-root.tsx @@ -8,7 +8,7 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { cn } from "@plane/utils"; -import { TopNavPowerK } from "@/components/navigation"; +import { DeferredTopNavPowerK } from "@/components/navigation/deferred-top-nav-power-k"; import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; import { useAppRailPreferences } from "@/hooks/use-navigation-preferences"; @@ -57,7 +57,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
{/* Power K Search */}
- +
{/* Additional Actions */}
diff --git a/plane-src/apps/web/ce/store/root.store.ts b/plane-src/apps/web/ce/store/root.store.ts index 9eadadf..35e9fca 100644 --- a/plane-src/apps/web/ce/store/root.store.ts +++ b/plane-src/apps/web/ce/store/root.store.ts @@ -7,14 +7,21 @@ // store import { CoreRootStore } from "@/store/root.store"; import type { ITimelineStore } from "./timeline"; -import { TimeLineStore } from "./timeline"; export class RootStore extends CoreRootStore { - timelineStore: ITimelineStore; + timelineStore: ITimelineStore | undefined; + private timelineStorePromise: Promise | undefined; - constructor() { - super(); + loadTimelineStore = async (): Promise => { + if (this.timelineStore) return this.timelineStore; - this.timelineStore = new TimeLineStore(this); + if (!this.timelineStorePromise) { + this.timelineStorePromise = import("./timeline").then(({ TimeLineStore }) => { + this.timelineStore = new TimeLineStore(this); + return this.timelineStore; + }); + } + + return this.timelineStorePromise; } } diff --git a/plane-src/apps/web/core/components/common/modal/lazy-workspace-modals.tsx b/plane-src/apps/web/core/components/common/modal/lazy-workspace-modals.tsx new file mode 100644 index 0000000..b3343df --- /dev/null +++ b/plane-src/apps/web/core/components/common/modal/lazy-workspace-modals.tsx @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { lazy, Suspense, useEffect, useState } from "react"; +import { + getProjectSettingsModalProjectIdFromSearch, + getProjectSettingsModalTabFromSearch, + PROJECT_SETTINGS_MODAL_EVENT, +} from "@/components/project/settings/project-settings-modal.utils"; +import { + getWorkspaceSettingsModalTabFromSearch, + WORKSPACE_SETTINGS_MODAL_EVENT, +} from "@/components/workspace/settings/workspace-settings-modal.utils"; +import { + getWorkspaceNotificationsModalOpenFromSearch, + WORKSPACE_NOTIFICATIONS_MODAL_EVENT, +} from "@/components/workspace-notifications/notifications-modal.utils"; + +const WorkspaceSettingsModal = lazy(() => + import("@/components/workspace/settings/workspace-settings-modal").then((module) => ({ + default: module.WorkspaceSettingsModal, + })) +); + +const ProjectSettingsModal = lazy(() => + import("@/components/project/settings/project-settings-modal").then((module) => ({ + default: module.ProjectSettingsModal, + })) +); + +const WorkspaceNotificationsModal = lazy(() => + import("@/components/workspace-notifications/notifications-modal").then((module) => ({ + default: module.WorkspaceNotificationsModal, + })) +); + +const hasWorkspaceSettingsSearch = () => + typeof window !== "undefined" && Boolean(getWorkspaceSettingsModalTabFromSearch(window.location.search)); + +const hasProjectSettingsSearch = () => + typeof window !== "undefined" && + Boolean(getProjectSettingsModalTabFromSearch(window.location.search) && getProjectSettingsModalProjectIdFromSearch(window.location.search)); + +const hasWorkspaceNotificationsSearch = () => + typeof window !== "undefined" && getWorkspaceNotificationsModalOpenFromSearch(window.location.search); + +export function LazyWorkspaceModals() { + const [shouldLoadWorkspaceSettings, setShouldLoadWorkspaceSettings] = useState(hasWorkspaceSettingsSearch); + const [shouldLoadProjectSettings, setShouldLoadProjectSettings] = useState(hasProjectSettingsSearch); + const [shouldLoadWorkspaceNotifications, setShouldLoadWorkspaceNotifications] = useState(hasWorkspaceNotificationsSearch); + + useEffect(() => { + const syncFromLocation = () => { + if (hasWorkspaceSettingsSearch()) setShouldLoadWorkspaceSettings(true); + if (hasProjectSettingsSearch()) setShouldLoadProjectSettings(true); + if (hasWorkspaceNotificationsSearch()) setShouldLoadWorkspaceNotifications(true); + }; + + const handleWorkspaceSettingsEvent = (event: Event) => { + if ((event as CustomEvent<{ isOpen: boolean }>).detail?.isOpen) setShouldLoadWorkspaceSettings(true); + }; + + const handleProjectSettingsEvent = (event: Event) => { + if ((event as CustomEvent<{ isOpen: boolean }>).detail?.isOpen) setShouldLoadProjectSettings(true); + }; + + const handleWorkspaceNotificationsEvent = (event: Event) => { + if ((event as CustomEvent<{ isOpen: boolean }>).detail?.isOpen) setShouldLoadWorkspaceNotifications(true); + }; + + window.addEventListener("popstate", syncFromLocation); + window.addEventListener(WORKSPACE_SETTINGS_MODAL_EVENT, handleWorkspaceSettingsEvent); + window.addEventListener(PROJECT_SETTINGS_MODAL_EVENT, handleProjectSettingsEvent); + window.addEventListener(WORKSPACE_NOTIFICATIONS_MODAL_EVENT, handleWorkspaceNotificationsEvent); + + return () => { + window.removeEventListener("popstate", syncFromLocation); + window.removeEventListener(WORKSPACE_SETTINGS_MODAL_EVENT, handleWorkspaceSettingsEvent); + window.removeEventListener(PROJECT_SETTINGS_MODAL_EVENT, handleProjectSettingsEvent); + window.removeEventListener(WORKSPACE_NOTIFICATIONS_MODAL_EVENT, handleWorkspaceNotificationsEvent); + }; + }, []); + + return ( + + {shouldLoadWorkspaceSettings && } + {shouldLoadProjectSettings && } + {shouldLoadWorkspaceNotifications && } + + ); +} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx index 0a895ca..5784e0d 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx @@ -50,15 +50,17 @@ export const IssueLayoutHOC = observer(function IssueLayoutHOC(props: Props) { const storeType = useIssueStoreType(); const { issues } = useIssues(storeType); - useIssueRealtimeEvents(storeType, workspaceSlug?.toString(), projectId?.toString()); + const issueLoader = issues?.getIssueLoader(); + const issueCount = issues?.getGroupIssueCount(undefined, undefined, false); + const isInitialLoadComplete = !!issues && issueLoader !== "init-loader" && issueCount !== undefined; + + useIssueRealtimeEvents(storeType, workspaceSlug?.toString(), projectId?.toString(), isInitialLoadComplete); if (!issues) { return ; } - const issueCount = issues.getGroupIssueCount(undefined, undefined, false); - - if (issues?.getIssueLoader() === "init-loader" || issueCount === undefined) { + if (issueLoader === "init-loader" || issueCount === undefined) { return ; } diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx index 8e111b9..6c9c096 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -341,6 +341,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB horizontalOffset={100} verticalOffset={200} defaultValue={shouldRenderByDefault} + useIdletime > } - defaultValue={groupIndex < 5 && subGroupIndex < 2} + defaultValue={groupIndex < 3 && subGroupIndex < 2} useIdletime > + import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview })) +); +const CycleCalendarLayout = lazy(() => + import("../calendar/roots/cycle-root").then((module) => ({ default: module.CycleCalendarLayout })) +); +const BaseGanttRoot = lazy(() => import("../gantt").then((module) => ({ default: module.BaseGanttRoot }))); +const CycleKanBanLayout = lazy(() => + import("../kanban/roots/cycle-root").then((module) => ({ default: module.CycleKanBanLayout })) +); +const CycleListLayout = lazy(() => + import("../list/roots/cycle-root").then((module) => ({ default: module.CycleListLayout })) +); +const CycleSpreadsheetLayout = lazy(() => + import("../spreadsheet/roots/cycle-root").then((module) => ({ default: module.CycleSpreadsheetLayout })) +); function CycleIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined; @@ -58,6 +69,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() { // store hooks const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { getCycleById } = useCycle(); + const { peekIssue } = useIssueDetail(); // state const [transferIssuesModal, setTransferIssuesModal] = useState(false); // derived values @@ -82,6 +94,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() { ? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues : 0; const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0; + const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId; if (!workspaceSlug || !projectId || !cycleId || !workItemFilters) return <>; return ( @@ -120,10 +133,19 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() { /> )}
- + + +
- {/* peek overview */} - + {shouldRenderPeekOverview && ( + + + + )}
)} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/roots/module-layout-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/roots/module-layout-root.tsx index 811cb02..e307082 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import React from "react"; +import { lazy, Suspense } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -15,15 +15,26 @@ import { Row, ERowVariant } from "@plane/ui"; // hooks import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useIssues } from "@/hooks/store/use-issues"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -// local imports -import { IssuePeekOverview } from "../../peek-overview"; -import { ModuleCalendarLayout } from "../calendar/roots/module-root"; -import { BaseGanttRoot } from "../gantt"; -import { ModuleKanBanLayout } from "../kanban/roots/module-root"; -import { ModuleListLayout } from "../list/roots/module-root"; -import { ModuleSpreadsheetLayout } from "../spreadsheet/roots/module-root"; + +const IssuePeekOverview = lazy(() => + import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview })) +); +const ModuleCalendarLayout = lazy(() => + import("../calendar/roots/module-root").then((module) => ({ default: module.ModuleCalendarLayout })) +); +const BaseGanttRoot = lazy(() => import("../gantt").then((module) => ({ default: module.BaseGanttRoot }))); +const ModuleKanBanLayout = lazy(() => + import("../kanban/roots/module-root").then((module) => ({ default: module.ModuleKanBanLayout })) +); +const ModuleListLayout = lazy(() => + import("../list/roots/module-root").then((module) => ({ default: module.ModuleListLayout })) +); +const ModuleSpreadsheetLayout = lazy(() => + import("../spreadsheet/roots/module-root").then((module) => ({ default: module.ModuleSpreadsheetLayout })) +); function ModuleIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined; moduleId: string }) { switch (props.activeLayout) { @@ -50,9 +61,11 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() { const moduleId = routerModuleId ? routerModuleId.toString() : undefined; // hooks const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); + const { peekIssue } = useIssueDetail(); // derived values const workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined; const activeLayout = workItemFilters?.displayFilters?.layout || undefined; + const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId; useSWR( workspaceSlug && projectId && moduleId @@ -90,10 +103,15 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() { /> )} - + + + - {/* peek overview */} - + {shouldRenderPeekOverview && ( + + + + )}
)} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-layout-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-layout-root.tsx index 79696b8..3c15c69 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -4,6 +4,7 @@ * See the LICENSE file for details. */ +import { lazy, Suspense } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -15,15 +16,24 @@ import { Spinner } from "@plane/ui"; import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; // hooks +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useIssues } from "@/hooks/store/use-issues"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -// local imports -import { IssuePeekOverview } from "../../peek-overview"; -import { CalendarLayout } from "../calendar/roots/project-root"; -import { BaseGanttRoot } from "../gantt"; -import { KanBanLayout } from "../kanban/roots/project-root"; -import { ListLayout } from "../list/roots/project-root"; -import { ProjectSpreadsheetLayout } from "../spreadsheet/roots/project-root"; + +const CalendarLayout = lazy(() => + import("../calendar/roots/project-root").then((module) => ({ default: module.CalendarLayout })) +); +const BaseGanttRoot = lazy(() => import("../gantt").then((module) => ({ default: module.BaseGanttRoot }))); +const IssuePeekOverview = lazy(() => + import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview })) +); +const KanBanLayout = lazy(() => + import("../kanban/roots/project-root").then((module) => ({ default: module.KanBanLayout })) +); +const ListLayout = lazy(() => import("../list/roots/project-root").then((module) => ({ default: module.ListLayout }))); +const ProjectSpreadsheetLayout = lazy(() => + import("../spreadsheet/roots/project-root").then((module) => ({ default: module.ProjectSpreadsheetLayout })) +); function ProjectIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined }) { switch (props.activeLayout) { @@ -49,9 +59,11 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() { const projectId = routerProjectId ? routerProjectId.toString() : undefined; // hooks const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); + const { peekIssue } = useIssueDetail(); // derived values const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined; const activeLayout = workItemFilters?.displayFilters?.layout; + const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId; useSWR( workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, @@ -93,10 +105,15 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
)} - + + + - {/* peek overview */} - + {shouldRenderPeekOverview && ( + + + + )} )} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-view-layout-root.tsx index 3d01bd8..0045896 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import React, { useEffect } from "react"; +import { lazy, Suspense, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; @@ -14,16 +14,27 @@ import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; // hooks import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; +import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useIssues } from "@/hooks/store/use-issues"; import { useProjectView } from "@/hooks/store/use-project-view"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -// local imports -import { IssuePeekOverview } from "../../peek-overview"; -import { ProjectViewCalendarLayout } from "../calendar/roots/project-view-root"; -import { BaseGanttRoot } from "../gantt"; -import { ProjectViewKanBanLayout } from "../kanban/roots/project-view-root"; -import { ProjectViewListLayout } from "../list/roots/project-view-root"; -import { ProjectViewSpreadsheetLayout } from "../spreadsheet/roots/project-view-root"; + +const IssuePeekOverview = lazy(() => + import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview })) +); +const ProjectViewCalendarLayout = lazy(() => + import("../calendar/roots/project-view-root").then((module) => ({ default: module.ProjectViewCalendarLayout })) +); +const BaseGanttRoot = lazy(() => import("../gantt").then((module) => ({ default: module.BaseGanttRoot }))); +const ProjectViewKanBanLayout = lazy(() => + import("../kanban/roots/project-view-root").then((module) => ({ default: module.ProjectViewKanBanLayout })) +); +const ProjectViewListLayout = lazy(() => + import("../list/roots/project-view-root").then((module) => ({ default: module.ProjectViewListLayout })) +); +const ProjectViewSpreadsheetLayout = lazy(() => + import("../spreadsheet/roots/project-view-root").then((module) => ({ default: module.ProjectViewSpreadsheetLayout })) +); function ProjectViewIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined; viewId: string }) { switch (props.activeLayout) { @@ -51,6 +62,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() { // hooks const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { getViewById } = useProjectView(); + const { peekIssue } = useIssueDetail(); // derived values const projectView = viewId ? getViewById(viewId) : undefined; const workItemFilters = viewId ? issuesFilter?.getIssueFilters(viewId) : undefined; @@ -63,6 +75,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() { richFilters: projectView.rich_filters, } : undefined; + const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId; useSWR( workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null, @@ -110,10 +123,15 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() { /> )}
- + + +
- {/* peek overview */} - + {shouldRenderPeekOverview && ( + + + + )} )} diff --git a/plane-src/apps/web/core/components/modules/modules-list-view.tsx b/plane-src/apps/web/core/components/modules/modules-list-view.tsx index da07843..45ae150 100644 --- a/plane-src/apps/web/core/components/modules/modules-list-view.tsx +++ b/plane-src/apps/web/core/components/modules/modules-list-view.tsx @@ -4,6 +4,7 @@ * See the LICENSE file for details. */ +import { lazy, Suspense } from "react"; import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; // components @@ -14,7 +15,7 @@ import { EUserProjectRoles } from "@plane/types"; import { ContentWrapper, Row, ERowVariant } from "@plane/ui"; // components import { ListLayout } from "@/components/core/list"; -import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules"; +import { ModuleCardItem, ModuleListItem, ModulePeekOverview } from "@/components/modules"; import { CycleModuleBoardLayoutLoader } from "@/components/ui/loader/cycle-module-board-loader"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; import { GanttLayoutLoader } from "@/components/ui/loader/layouts/gantt-layout-loader"; @@ -24,6 +25,12 @@ import { useModule } from "@/hooks/store/use-module"; import { useModuleFilter } from "@/hooks/store/use-module-filter"; import { useUserPermissions } from "@/hooks/store/user"; +const ModulesListGanttChartView = lazy(() => + import("@/components/modules/gantt-chart/modules-list-layout").then((module) => ({ + default: module.ModulesListGanttChartView, + })) +); + export const ModulesListView = observer(function ModulesListView() { // router const { workspaceSlug, projectId } = useParams(); @@ -105,7 +112,9 @@ export const ModulesListView = observer(function ModulesListView() { )} {displayFilters?.layout === "gantt" && (
- + }> + +
)}
diff --git a/plane-src/apps/web/core/components/navigation/deferred-top-nav-power-k.tsx b/plane-src/apps/web/core/components/navigation/deferred-top-nav-power-k.tsx new file mode 100644 index 0000000..2096cfe --- /dev/null +++ b/plane-src/apps/web/core/components/navigation/deferred-top-nav-power-k.tsx @@ -0,0 +1,105 @@ +"use client"; + +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { lazy, Suspense, useState } from "react"; +import { useTranslation } from "@plane/i18n"; +import { SearchIcon } from "@plane/propel/icons"; +import { cn } from "@plane/utils"; + +const TopNavPowerK = lazy(() => + import("./top-nav-power-k").then((module) => ({ + default: module.TopNavPowerK, + })) +); + +type TDeferredTopNavPowerKProps = { + variant?: "top-navigation" | "sidebar" | "expanded-toolbar"; +}; + +export const DeferredTopNavPowerK = ({ variant = "top-navigation" }: TDeferredTopNavPowerKProps) => { + const { t } = useTranslation(); + const [shouldLoad, setShouldLoad] = useState(false); + const [shouldAutoOpen, setShouldAutoOpen] = useState(false); + const searchLabel = t("power_k.search_menu.quick_command_placeholder"); + + const handleOpen = () => { + setShouldAutoOpen(true); + setShouldLoad(true); + }; + + if (shouldLoad) { + return ( + }> + setShouldAutoOpen(false)} + /> + + ); + } + + return ; +}; + +const TopNavPowerKFallback = ({ + label, + onOpen, + variant, +}: { + label: string; + onOpen: () => void; + variant: "top-navigation" | "sidebar" | "expanded-toolbar"; +}) => { + if (variant === "expanded-toolbar") { + return ( +
+ +
+ ); + } + + if (variant === "sidebar") { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ ); +}; diff --git a/plane-src/apps/web/core/components/navigation/tab-navigation-root.tsx b/plane-src/apps/web/core/components/navigation/tab-navigation-root.tsx index f3bd16c..0ca57d8 100644 --- a/plane-src/apps/web/core/components/navigation/tab-navigation-root.tsx +++ b/plane-src/apps/web/core/components/navigation/tab-navigation-root.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import React, { useEffect } from "react"; +import React, { lazy, Suspense, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams, useLocation, Link, useNavigate } from "react-router"; import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; @@ -18,8 +18,6 @@ import { useUserPermissions } from "@/hooks/store/user"; // plane web imports import { useNavigationItems } from "@/plane-web/components/navigations"; // local imports -import { LeaveProjectModal } from "../project/leave-project-modal"; -import { PublishProjectModal } from "../project/publish-project/modal"; import { ProjectActionsMenu } from "./project-actions-menu"; import { ProjectHeader } from "./project-header"; import { TabNavigationOverflowMenu } from "./tab-navigation-overflow-menu"; @@ -30,6 +28,18 @@ import { useProjectActions } from "./use-project-actions"; import { useResponsiveTabLayout } from "./use-responsive-tab-layout"; import { useTabPreferences } from "./use-tab-preferences"; +const LeaveProjectModal = lazy(() => + import("../project/leave-project-modal").then((module) => ({ + default: module.LeaveProjectModal, + })) +); + +const PublishProjectModal = lazy(() => + import("../project/publish-project/modal").then((module) => ({ + default: module.PublishProjectModal, + })) +); + // Local type definition for navigation items with app-specific fields export type TNavigationItem = { name: string; @@ -109,7 +119,7 @@ export const TabNavigationRoot = observer(function TabNavigationRoot(props: TTab // Filter and sort navigation items const allNavigationItems = navigationItems .filter((item) => item.shouldRender) - .sort((a, b) => a.sortOrder - b.sortOrder); + .toSorted((a, b) => a.sortOrder - b.sortOrder); // Split items into two categories: // 1. visibleNavigationItems: Items NOT user-hidden (may still overflow due to space) @@ -162,12 +172,18 @@ export const TabNavigationRoot = observer(function TabNavigationRoot(props: TTab return ( <> - handlePublishModal(false)} /> - handleLeaveProjectModal(false)} - /> + + {publishModalOpen && ( + handlePublishModal(false)} /> + )} + {leaveProjectModalOpen && ( + handleLeaveProjectModal(false)} + /> + )} + {/* container for the tab navigation */}
diff --git a/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx b/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx index cffa355..52a8af5 100644 --- a/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx +++ b/plane-src/apps/web/core/components/navigation/top-nav-power-k.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import { useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from "react"; +import { lazy, Suspense, useState, useMemo, useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; @@ -15,20 +15,32 @@ import { CloseIcon, SearchIcon } from "@plane/propel/icons"; import { cn } from "@plane/utils"; // power-k import type { TPowerKCommandConfig, TPowerKContext } from "@/components/power-k/core/types"; -import { ProjectsAppPowerKCommandsList } from "@/components/power-k/ui/modal/commands-list"; -import { PowerKModalFooter } from "@/components/power-k/ui/modal/footer"; import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { usePowerK } from "@/hooks/store/use-power-k"; import { useUser } from "@/hooks/store/user"; import { useAppRouter } from "@/hooks/use-app-router"; import { useExpandableSearch } from "@/hooks/use-expandable-search"; +const ProjectsAppPowerKCommandsList = lazy(() => + import("@/components/power-k/ui/modal/commands-list").then((module) => ({ + default: module.ProjectsAppPowerKCommandsList, + })) +); + +const PowerKModalFooter = lazy(() => + import("@/components/power-k/ui/modal/footer").then((module) => ({ + default: module.PowerKModalFooter, + })) +); + type TTopNavPowerKProps = { + autoOpen?: boolean; + onAutoOpenComplete?: () => void; variant?: "top-navigation" | "sidebar" | "expanded-toolbar"; }; export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { - const { variant = "top-navigation" } = props; + const { autoOpen = false, onAutoOpenComplete, variant = "top-navigation" } = props; const { t } = useTranslation(); const isWideSearch = variant === "top-navigation" || variant === "expanded-toolbar"; const isExpandedToolbar = variant === "expanded-toolbar"; @@ -51,6 +63,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { const sidebarSearchPortalRef = useRef(null); const sidebarSearchButtonRef = useRef(null); + const autoOpenHandledRef = useRef(false); // store hooks const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK(); @@ -76,6 +89,15 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { additionalRefs: [sidebarSearchPortalRef], }); + useEffect(() => { + if (!autoOpen || autoOpenHandledRef.current) return; + + autoOpenHandledRef.current = true; + openPanel(); + requestAnimationFrame(() => inputRef.current?.focus()); + onAutoOpenComplete?.(); + }, [autoOpen, inputRef, onAutoOpenComplete, openPanel]); + // derived values const { issue: { getIssueById, getIssueIdByIdentifier }, @@ -277,22 +299,26 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { >