perf: optimize Tasker boot and kanban load
This commit is contained in:
@@ -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 (
|
||||
<Suspense fallback={null}>
|
||||
{shouldLoadWorkspaceSettings && <WorkspaceSettingsModal />}
|
||||
{shouldLoadProjectSettings && <ProjectSettingsModal />}
|
||||
{shouldLoadWorkspaceNotifications && <WorkspaceNotificationsModal />}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -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 <ActiveLoader layout={layout} />;
|
||||
}
|
||||
|
||||
const issueCount = issues.getGroupIssueCount(undefined, undefined, false);
|
||||
|
||||
if (issues?.getIssueLoader() === "init-loader" || issueCount === undefined) {
|
||||
if (issueLoader === "init-loader" || issueCount === undefined) {
|
||||
return <ActiveLoader layout={layout} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -341,6 +341,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||
horizontalOffset={100}
|
||||
verticalOffset={200}
|
||||
defaultValue={shouldRenderByDefault}
|
||||
useIdletime
|
||||
>
|
||||
<KanbanIssueDetailsBlock
|
||||
cardRef={cardRef}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
|
||||
issueId={issueId}
|
||||
groupId={groupId}
|
||||
subGroupId={sub_group_id}
|
||||
shouldRenderByDefault={index <= 10}
|
||||
shouldRenderByDefault={index < 3}
|
||||
issuesMap={issuesMap}
|
||||
displayProperties={displayProperties}
|
||||
updateIssue={updateIssue}
|
||||
|
||||
@@ -210,7 +210,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
|
||||
shouldAnimate={false}
|
||||
/>
|
||||
}
|
||||
defaultValue={groupIndex < 5 && subGroupIndex < 2}
|
||||
defaultValue={groupIndex < 3 && subGroupIndex < 2}
|
||||
useIdletime
|
||||
>
|
||||
<KanbanGroup
|
||||
|
||||
+33
-11
@@ -4,7 +4,7 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
@@ -19,15 +19,26 @@ import { TransferIssuesModal } from "@/components/cycles/transfer-issues-modal";
|
||||
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
|
||||
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
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 { CycleCalendarLayout } from "../calendar/roots/cycle-root";
|
||||
import { BaseGanttRoot } from "../gantt";
|
||||
import { CycleKanBanLayout } from "../kanban/roots/cycle-root";
|
||||
import { CycleListLayout } from "../list/roots/cycle-root";
|
||||
import { CycleSpreadsheetLayout } from "../spreadsheet/roots/cycle-root";
|
||||
|
||||
const IssuePeekOverview = lazy(() =>
|
||||
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() {
|
||||
/>
|
||||
)}
|
||||
<div className="h-full w-full overflow-auto">
|
||||
<CycleIssueLayout activeLayout={activeLayout} cycleId={cycleId} isCompletedCycle={isCompletedCycle} />
|
||||
<Suspense fallback={null}>
|
||||
<CycleIssueLayout
|
||||
activeLayout={activeLayout}
|
||||
cycleId={cycleId}
|
||||
isCompletedCycle={isCompletedCycle}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{shouldRenderPeekOverview && (
|
||||
<Suspense fallback={null}>
|
||||
<IssuePeekOverview />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
+29
-11
@@ -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() {
|
||||
/>
|
||||
)}
|
||||
<Row variant={ERowVariant.HUGGING} className="h-full w-full overflow-auto">
|
||||
<ModuleIssueLayout activeLayout={activeLayout} moduleId={moduleId} />
|
||||
<Suspense fallback={null}>
|
||||
<ModuleIssueLayout activeLayout={activeLayout} moduleId={moduleId} />
|
||||
</Suspense>
|
||||
</Row>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{shouldRenderPeekOverview && (
|
||||
<Suspense fallback={null}>
|
||||
<IssuePeekOverview />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
|
||||
+27
-10
@@ -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() {
|
||||
<Spinner className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
<ProjectIssueLayout activeLayout={activeLayout} />
|
||||
<Suspense fallback={null}>
|
||||
<ProjectIssueLayout activeLayout={activeLayout} />
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{shouldRenderPeekOverview && (
|
||||
<Suspense fallback={null}>
|
||||
<IssuePeekOverview />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
|
||||
+29
-11
@@ -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() {
|
||||
/>
|
||||
)}
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<ProjectViewIssueLayout activeLayout={activeLayout} viewId={viewId.toString()} />
|
||||
<Suspense fallback={null}>
|
||||
<ProjectViewIssueLayout activeLayout={activeLayout} viewId={viewId.toString()} />
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* peek overview */}
|
||||
<IssuePeekOverview />
|
||||
{shouldRenderPeekOverview && (
|
||||
<Suspense fallback={null}>
|
||||
<IssuePeekOverview />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</ProjectLevelWorkItemFiltersHOC>
|
||||
|
||||
@@ -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" && (
|
||||
<div className="size-full overflow-hidden">
|
||||
<ModulesListGanttChartView />
|
||||
<Suspense fallback={<GanttLayoutLoader />}>
|
||||
<ModulesListGanttChartView />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-shrink-0">
|
||||
|
||||
@@ -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 (
|
||||
<Suspense fallback={<TopNavPowerKFallback variant={variant} onOpen={handleOpen} label={searchLabel} />}>
|
||||
<TopNavPowerK
|
||||
autoOpen={shouldAutoOpen}
|
||||
variant={variant}
|
||||
onAutoOpenComplete={() => setShouldAutoOpen(false)}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return <TopNavPowerKFallback variant={variant} onOpen={handleOpen} label={searchLabel} />;
|
||||
};
|
||||
|
||||
const TopNavPowerKFallback = ({
|
||||
label,
|
||||
onOpen,
|
||||
variant,
|
||||
}: {
|
||||
label: string;
|
||||
onOpen: () => void;
|
||||
variant: "top-navigation" | "sidebar" | "expanded-toolbar";
|
||||
}) => {
|
||||
if (variant === "expanded-toolbar") {
|
||||
return (
|
||||
<div className="nodedc-expanded-search-control" data-open={false}>
|
||||
<button
|
||||
type="button"
|
||||
className="nodedc-expanded-tool-button nodedc-expanded-search-trigger"
|
||||
aria-label={label}
|
||||
aria-pressed={false}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "sidebar") {
|
||||
return (
|
||||
<div className="relative z-30 h-8 w-8">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-0 top-0 z-[161] flex size-8 items-center justify-center rounded-full border-0 bg-white/[0.04] text-placeholder backdrop-blur-[18px] outline-none transition-all hover:bg-white/[0.07]"
|
||||
aria-label={label}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<SearchIcon className="size-3.5 shrink-0 text-placeholder" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative z-30 flex w-[364px] items-center transition-all duration-300 ease-in-out">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex h-7 w-full items-center rounded-lg border border-subtle-1 bg-layer-2 p-2 text-left transition-colors duration-200"
|
||||
)}
|
||||
aria-label={label}
|
||||
onClick={onOpen}
|
||||
>
|
||||
<SearchIcon className="mr-2 size-3.5 shrink-0 text-placeholder" />
|
||||
<span className="min-w-0 flex-1 text-13 text-placeholder">{label}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<PublishProjectModal isOpen={publishModalOpen} projectId={projectId} onClose={() => handlePublishModal(false)} />
|
||||
<LeaveProjectModal
|
||||
project={project}
|
||||
isOpen={leaveProjectModalOpen}
|
||||
onClose={() => handleLeaveProjectModal(false)}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
{publishModalOpen && (
|
||||
<PublishProjectModal isOpen={publishModalOpen} projectId={projectId} onClose={() => handlePublishModal(false)} />
|
||||
)}
|
||||
{leaveProjectModalOpen && (
|
||||
<LeaveProjectModal
|
||||
project={project}
|
||||
isOpen={leaveProjectModalOpen}
|
||||
onClose={() => handleLeaveProjectModal(false)}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
{/* container for the tab navigation */}
|
||||
<div className="flex size-full items-center gap-3 overflow-hidden">
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const sidebarSearchButtonRef = useRef<HTMLButtonElement>(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) => {
|
||||
>
|
||||
<Command.Input value={searchTerm} hidden />
|
||||
<Command.List className="vertical-scrollbar scrollbar-sm max-h-[60vh] overflow-y-auto px-2 pb-4 outline-none">
|
||||
<ProjectsAppPowerKCommandsList
|
||||
activePage={activePage}
|
||||
context={context}
|
||||
handleCommandSelect={handleCommandSelect}
|
||||
handlePageDataSelection={handlePageDataSelection}
|
||||
isWorkspaceLevel={isWorkspaceLevel}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
handleSearchMenuClose={() => closePanel()}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<ProjectsAppPowerKCommandsList
|
||||
activePage={activePage}
|
||||
context={context}
|
||||
handleCommandSelect={handleCommandSelect}
|
||||
handlePageDataSelection={handlePageDataSelection}
|
||||
isWorkspaceLevel={isWorkspaceLevel}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
handleSearchMenuClose={() => closePanel()}
|
||||
/>
|
||||
</Suspense>
|
||||
</Command.List>
|
||||
<PowerKModalFooter
|
||||
isWorkspaceLevel={isWorkspaceLevel}
|
||||
projectId={context.params.projectId?.toString()}
|
||||
onWorkspaceLevelChange={setIsWorkspaceLevel}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
<PowerKModalFooter
|
||||
isWorkspaceLevel={isWorkspaceLevel}
|
||||
projectId={context.params.projectId?.toString()}
|
||||
onWorkspaceLevelChange={setIsWorkspaceLevel}
|
||||
/>
|
||||
</Suspense>
|
||||
</Command>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,24 +4,53 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { lazy, Suspense, useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
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";
|
||||
// plane web imports
|
||||
import { ProjectLevelModals } from "@/plane-web/components/command-palette/modals/project-level";
|
||||
import { WorkItemLevelModals } from "@/plane-web/components/command-palette/modals/work-item-level";
|
||||
import { WorkspaceLevelModals } from "@/plane-web/components/command-palette/modals/workspace-level";
|
||||
// local imports
|
||||
import { useProjectsAppPowerKCommands } from "./config/commands";
|
||||
import type { TPowerKCommandConfig, TPowerKContext } from "./core/types";
|
||||
import { GlobalShortcutsProvider } from "./global-shortcuts";
|
||||
import { ProjectsAppPowerKCommandsList } from "./ui/modal/commands-list";
|
||||
import { ProjectsAppPowerKModalWrapper } from "./ui/modal/wrapper";
|
||||
|
||||
const WorkspaceLevelModals = lazy(() =>
|
||||
import("@/plane-web/components/command-palette/modals/workspace-level").then((module) => ({
|
||||
default: module.WorkspaceLevelModals,
|
||||
}))
|
||||
);
|
||||
|
||||
const ProjectLevelModals = lazy(() =>
|
||||
import("@/plane-web/components/command-palette/modals/project-level").then((module) => ({
|
||||
default: module.ProjectLevelModals,
|
||||
}))
|
||||
);
|
||||
|
||||
const WorkItemLevelModals = lazy(() =>
|
||||
import("@/plane-web/components/command-palette/modals/work-item-level").then((module) => ({
|
||||
default: module.WorkItemLevelModals,
|
||||
}))
|
||||
);
|
||||
|
||||
const ProjectsAppPowerKModal = lazy(async () => {
|
||||
const [wrapperModule, commandsListModule] = await Promise.all([
|
||||
import("./ui/modal/wrapper"),
|
||||
import("./ui/modal/commands-list"),
|
||||
]);
|
||||
|
||||
return {
|
||||
default: (props: Omit<Parameters<typeof wrapperModule.ProjectsAppPowerKModalWrapper>[0], "commandsListComponent">) => (
|
||||
<wrapperModule.ProjectsAppPowerKModalWrapper
|
||||
{...props}
|
||||
commandsListComponent={commandsListModule.ProjectsAppPowerKCommandsList}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Projects App PowerK provider
|
||||
@@ -36,6 +65,16 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
|
||||
const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true);
|
||||
// store hooks
|
||||
const { activeContext, isPowerKModalOpen, togglePowerKModal, setActivePage } = usePowerK();
|
||||
const {
|
||||
createPageModal,
|
||||
isBulkDeleteIssueModalOpen,
|
||||
isCreateCycleModalOpen,
|
||||
isCreateIssueModalOpen,
|
||||
isCreateModuleModalOpen,
|
||||
isCreateProjectModalOpen,
|
||||
isCreateViewModalOpen,
|
||||
isDeleteIssueModalOpen,
|
||||
} = useCommandPalette();
|
||||
const { data: currentUser } = useUser();
|
||||
// derived values
|
||||
const {
|
||||
@@ -46,6 +85,15 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
|
||||
const workItemDetails = workItemId ? getIssueById(workItemId) : undefined;
|
||||
const projectId: string | string[] | undefined | null = routerProjectId ?? workItemDetails?.project_id;
|
||||
const commands = useProjectsAppPowerKCommands();
|
||||
const shouldLoadWorkspaceLevelModals = Boolean(workspaceSlug && isCreateProjectModalOpen);
|
||||
const shouldLoadProjectLevelModals = Boolean(
|
||||
workspaceSlug &&
|
||||
projectId &&
|
||||
(isCreateCycleModalOpen || isCreateModuleModalOpen || isCreateViewModalOpen || createPageModal.isOpen)
|
||||
);
|
||||
const shouldLoadWorkItemLevelModals = Boolean(
|
||||
isCreateIssueModalOpen || isDeleteIssueModalOpen || isBulkDeleteIssueModalOpen
|
||||
);
|
||||
// Build command context from props and store
|
||||
const context: TPowerKContext = useMemo(
|
||||
() => ({
|
||||
@@ -79,17 +127,16 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
|
||||
return (
|
||||
<>
|
||||
<GlobalShortcutsProvider context={context} commands={commands} />
|
||||
{workspaceSlug && <WorkspaceLevelModals workspaceSlug={workspaceSlug.toString()} />}
|
||||
{workspaceSlug && projectId && (
|
||||
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} />
|
||||
)}
|
||||
<WorkItemLevelModals workItemIdentifier={workItemIdentifier?.toString()} />
|
||||
<ProjectsAppPowerKModalWrapper
|
||||
commandsListComponent={ProjectsAppPowerKCommandsList}
|
||||
context={context}
|
||||
isOpen={isPowerKModalOpen}
|
||||
onClose={() => togglePowerKModal(false)}
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
{shouldLoadWorkspaceLevelModals && <WorkspaceLevelModals workspaceSlug={workspaceSlug!.toString()} />}
|
||||
{shouldLoadProjectLevelModals && (
|
||||
<ProjectLevelModals workspaceSlug={workspaceSlug!.toString()} projectId={projectId!.toString()} />
|
||||
)}
|
||||
{shouldLoadWorkItemLevelModals && <WorkItemLevelModals workItemIdentifier={workItemIdentifier?.toString()} />}
|
||||
{isPowerKModalOpen && (
|
||||
<ProjectsAppPowerKModal context={context} isOpen={isPowerKModalOpen} onClose={() => togglePowerKModal(false)} />
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "@plane/i18n";
|
||||
import { InboxIcon, PlusIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import useSWR from "swr";
|
||||
import { TopNavPowerK } from "@/components/navigation";
|
||||
import { DeferredTopNavPowerK } from "@/components/navigation/deferred-top-nav-power-k";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
@@ -61,7 +61,7 @@ export const SidebarUtilityRail = observer(function SidebarUtilityRail() {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<TopNavPowerK variant="sidebar" />
|
||||
<DeferredTopNavPowerK variant="sidebar" />
|
||||
<Tooltip tooltipContent="Уведомления" position="right">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user