perf: optimize Tasker boot and kanban load

This commit is contained in:
DCCONSTRUCTIONS 2026-05-15 19:18:25 +03:00
parent 44d2c5dd27
commit 83ea515962
29 changed files with 631 additions and 186 deletions

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useCallback, useState } from "react"; import { lazy, Suspense, useCallback, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// plane imports // plane imports
@ -14,7 +14,6 @@ import { ChevronDownIcon } from "@plane/propel/icons";
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types"; import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
// components // components
import { WorkItemsModal } from "@/components/analytics/work-items/modal";
import { import {
DisplayFiltersSelection, DisplayFiltersSelection,
FiltersDropdown, FiltersDropdown,
@ -24,6 +23,12 @@ import {
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
import { useProject } from "@/hooks/store/use-project"; 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() { export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHeader() {
// i18n // i18n
const { t } = useTranslation(); const { t } = useTranslation();
@ -63,11 +68,15 @@ export const ProjectIssuesMobileHeader = observer(function ProjectIssuesMobileHe
return ( return (
<> <>
<WorkItemsModal {analyticsModal && (
isOpen={analyticsModal} <Suspense fallback={null}>
onClose={() => setAnalyticsModal(false)} <WorkItemsModal
projectDetails={currentProjectDetails ?? undefined} isOpen={analyticsModal}
/> onClose={() => setAnalyticsModal(false)}
projectDetails={currentProjectDetails ?? undefined}
/>
</Suspense>
)}
<div className="z-[13] flex justify-evenly border-b border-subtle bg-surface-1 py-2 md:hidden"> <div className="z-[13] flex justify-evenly border-b border-subtle bg-surface-1 py-2 md:hidden">
<LayoutSelection <LayoutSelection
layouts={[ layouts={[

View File

@ -10,7 +10,7 @@ import { useTranslation } from "@plane/i18n";
import { PlusIcon } from "@plane/propel/icons"; import { PlusIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// components // components
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 { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
import { ProjectsToolbarMenu } from "./projects-toolbar-menu"; import { ProjectsToolbarMenu } from "./projects-toolbar-menu";
@ -40,7 +40,7 @@ export const CompactProjectShellToolbarLayout = ({
<div className="flex min-w-0 items-center gap-3"> <div className="flex min-w-0 items-center gap-3">
<div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible"> <div className="nodedc-toolbar-group relative flex items-center gap-1 overflow-visible">
<WorkspaceMenuRoot variant="toolbar" /> <WorkspaceMenuRoot variant="toolbar" />
<TopNavPowerK variant="sidebar" /> <DeferredTopNavPowerK variant="sidebar" />
<UserMenuRoot variant="toolbar" /> <UserMenuRoot variant="toolbar" />
<ToolbarNotificationsButton <ToolbarNotificationsButton
label={t("notification.label")} label={t("notification.label")}

View File

@ -10,7 +10,7 @@ import { useTranslation } from "@plane/i18n";
import { Shapes } from "lucide-react"; import { Shapes } from "lucide-react";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// components // components
import { TopNavPowerK } from "@/components/navigation"; import { DeferredTopNavPowerK } from "@/components/navigation/deferred-top-nav-power-k";
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
@ -71,7 +71,7 @@ export const ExpandedProjectShellToolbarLayout = ({
<div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot /> <div className="nodedc-expanded-breadcrumbs-slot" data-nodedc-expanded-breadcrumbs-slot />
{!isWorkspaceHome && ( {!isWorkspaceHome && (
<div className="nodedc-expanded-main-tool-cluster"> <div className="nodedc-expanded-main-tool-cluster">
<TopNavPowerK variant="expanded-toolbar" /> <DeferredTopNavPowerK variant="expanded-toolbar" />
<div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot /> <div className="nodedc-expanded-header-filters-slot" data-nodedc-expanded-header-filters-slot />
</div> </div>
)} )}

View File

@ -10,9 +10,7 @@ import { WorkspaceContentWrapper } from "@/plane-web/components/workspace/conten
import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail"; import { AppRailVisibilityProvider } from "@/plane-web/hooks/app-rail";
import { GlobalModals } from "@/plane-web/components/common/modal/global"; import { GlobalModals } from "@/plane-web/components/common/modal/global";
import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper"; import { WorkspaceAuthWrapper } from "@/layouts/auth-layout/workspace-wrapper";
import { ProjectSettingsModal } from "@/components/project/settings/project-settings-modal"; import { LazyWorkspaceModals } from "@/components/common/modal/lazy-workspace-modals";
import { WorkspaceSettingsModal } from "@/components/workspace/settings/workspace-settings-modal";
import { WorkspaceNotificationsModal } from "@/components/workspace-notifications/notifications-modal";
import type { Route } from "./+types/layout"; import type { Route } from "./+types/layout";
export default function WorkspaceLayout(props: Route.ComponentProps) { export default function WorkspaceLayout(props: Route.ComponentProps) {
@ -24,9 +22,7 @@ export default function WorkspaceLayout(props: Route.ComponentProps) {
<AppRailVisibilityProvider> <AppRailVisibilityProvider>
<WorkspaceContentWrapper workspaceSlug={workspaceSlug}> <WorkspaceContentWrapper workspaceSlug={workspaceSlug}>
<GlobalModals workspaceSlug={workspaceSlug} /> <GlobalModals workspaceSlug={workspaceSlug} />
<WorkspaceSettingsModal /> <LazyWorkspaceModals />
<ProjectSettingsModal />
<WorkspaceNotificationsModal />
<Outlet /> <Outlet />
</WorkspaceContentWrapper> </WorkspaceContentWrapper>
</AppRailVisibilityProvider> </AppRailVisibilityProvider>

View File

@ -6,6 +6,7 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useCommandPalette } from "@/hooks/store/use-command-palette";
const ProfileSettingsModal = lazy(() => const ProfileSettingsModal = lazy(() =>
import("@/components/settings/profile/modal").then((module) => ({ import("@/components/settings/profile/modal").then((module) => ({
@ -24,6 +25,10 @@ type TGlobalModalsProps = {
* - Profile settings modal * - Profile settings modal
*/ */
export const GlobalModals = observer(function GlobalModals(_props: TGlobalModalsProps) { export const GlobalModals = observer(function GlobalModals(_props: TGlobalModalsProps) {
const { profileSettingsModal } = useCommandPalette();
if (!profileSettingsModal.isOpen) return null;
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
<ProfileSettingsModal /> <ProfileSettingsModal />

View File

@ -8,7 +8,7 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation"; import { useParams, usePathname } from "next/navigation";
import { cn } from "@plane/utils"; 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 { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
import { useAppRailPreferences } from "@/hooks/use-navigation-preferences"; import { useAppRailPreferences } from "@/hooks/use-navigation-preferences";
@ -57,7 +57,7 @@ export const TopNavigationRoot = observer(function TopNavigationRoot() {
</div> </div>
{/* Power K Search */} {/* Power K Search */}
<div className="shrink-0"> <div className="shrink-0">
<TopNavPowerK /> <DeferredTopNavPowerK />
</div> </div>
{/* Additional Actions */} {/* Additional Actions */}
<div className="flex flex-1 shrink-0 items-center justify-end gap-1"> <div className="flex flex-1 shrink-0 items-center justify-end gap-1">

View File

@ -7,14 +7,21 @@
// store // store
import { CoreRootStore } from "@/store/root.store"; import { CoreRootStore } from "@/store/root.store";
import type { ITimelineStore } from "./timeline"; import type { ITimelineStore } from "./timeline";
import { TimeLineStore } from "./timeline";
export class RootStore extends CoreRootStore { export class RootStore extends CoreRootStore {
timelineStore: ITimelineStore; timelineStore: ITimelineStore | undefined;
private timelineStorePromise: Promise<ITimelineStore> | undefined;
constructor() { loadTimelineStore = async (): Promise<ITimelineStore> => {
super(); 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;
} }
} }

View File

@ -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>
);
}

View File

@ -50,15 +50,17 @@ export const IssueLayoutHOC = observer(function IssueLayoutHOC(props: Props) {
const storeType = useIssueStoreType(); const storeType = useIssueStoreType();
const { issues } = useIssues(storeType); 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) { if (!issues) {
return <ActiveLoader layout={layout} />; return <ActiveLoader layout={layout} />;
} }
const issueCount = issues.getGroupIssueCount(undefined, undefined, false); if (issueLoader === "init-loader" || issueCount === undefined) {
if (issues?.getIssueLoader() === "init-loader" || issueCount === undefined) {
return <ActiveLoader layout={layout} />; return <ActiveLoader layout={layout} />;
} }

View File

@ -341,6 +341,7 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
horizontalOffset={100} horizontalOffset={100}
verticalOffset={200} verticalOffset={200}
defaultValue={shouldRenderByDefault} defaultValue={shouldRenderByDefault}
useIdletime
> >
<KanbanIssueDetailsBlock <KanbanIssueDetailsBlock
cardRef={cardRef} cardRef={cardRef}

View File

@ -67,7 +67,7 @@ export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(pro
issueId={issueId} issueId={issueId}
groupId={groupId} groupId={groupId}
subGroupId={sub_group_id} subGroupId={sub_group_id}
shouldRenderByDefault={index <= 10} shouldRenderByDefault={index < 3}
issuesMap={issuesMap} issuesMap={issuesMap}
displayProperties={displayProperties} displayProperties={displayProperties}
updateIssue={updateIssue} updateIssue={updateIssue}

View File

@ -210,7 +210,7 @@ export const KanBan = observer(function KanBan(props: IKanBan) {
shouldAnimate={false} shouldAnimate={false}
/> />
} }
defaultValue={groupIndex < 5 && subGroupIndex < 2} defaultValue={groupIndex < 3 && subGroupIndex < 2}
useIdletime useIdletime
> >
<KanbanGroup <KanbanGroup

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import React, { useState } from "react"; import { lazy, Suspense, useState } from "react";
import { isEmpty } from "lodash-es"; import { isEmpty } from "lodash-es";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; 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 { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
import { useCycle } from "@/hooks/store/use-cycle"; import { useCycle } from "@/hooks/store/use-cycle";
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
// local imports
import { IssuePeekOverview } from "../../peek-overview"; const IssuePeekOverview = lazy(() =>
import { CycleCalendarLayout } from "../calendar/roots/cycle-root"; import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview }))
import { BaseGanttRoot } from "../gantt"; );
import { CycleKanBanLayout } from "../kanban/roots/cycle-root"; const CycleCalendarLayout = lazy(() =>
import { CycleListLayout } from "../list/roots/cycle-root"; import("../calendar/roots/cycle-root").then((module) => ({ default: module.CycleCalendarLayout }))
import { CycleSpreadsheetLayout } from "../spreadsheet/roots/cycle-root"; );
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: { function CycleIssueLayout(props: {
activeLayout: EIssueLayoutTypes | undefined; activeLayout: EIssueLayoutTypes | undefined;
@ -58,6 +69,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
// store hooks // store hooks
const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
const { getCycleById } = useCycle(); const { getCycleById } = useCycle();
const { peekIssue } = useIssueDetail();
// state // state
const [transferIssuesModal, setTransferIssuesModal] = useState(false); const [transferIssuesModal, setTransferIssuesModal] = useState(false);
// derived values // derived values
@ -82,6 +94,7 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues ? cycleDetails.backlog_issues + cycleDetails.unstarted_issues + cycleDetails.started_issues
: 0; : 0;
const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0; const canTransferIssues = isProgressSnapshotEmpty && transferableIssuesCount > 0;
const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId;
if (!workspaceSlug || !projectId || !cycleId || !workItemFilters) return <></>; if (!workspaceSlug || !projectId || !cycleId || !workItemFilters) return <></>;
return ( return (
@ -120,10 +133,19 @@ export const CycleLayoutRoot = observer(function CycleLayoutRoot() {
/> />
)} )}
<div className="h-full w-full overflow-auto"> <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> </div>
{/* peek overview */} {shouldRenderPeekOverview && (
<IssuePeekOverview /> <Suspense fallback={null}>
<IssuePeekOverview />
</Suspense>
)}
</div> </div>
</> </>
)} )}

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import React from "react"; import { lazy, Suspense } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
@ -15,15 +15,26 @@ import { Row, ERowVariant } from "@plane/ui";
// hooks // hooks
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level"; import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; 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 { useIssues } from "@/hooks/store/use-issues";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
// local imports
import { IssuePeekOverview } from "../../peek-overview"; const IssuePeekOverview = lazy(() =>
import { ModuleCalendarLayout } from "../calendar/roots/module-root"; import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview }))
import { BaseGanttRoot } from "../gantt"; );
import { ModuleKanBanLayout } from "../kanban/roots/module-root"; const ModuleCalendarLayout = lazy(() =>
import { ModuleListLayout } from "../list/roots/module-root"; import("../calendar/roots/module-root").then((module) => ({ default: module.ModuleCalendarLayout }))
import { ModuleSpreadsheetLayout } from "../spreadsheet/roots/module-root"; );
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 }) { function ModuleIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined; moduleId: string }) {
switch (props.activeLayout) { switch (props.activeLayout) {
@ -50,9 +61,11 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() {
const moduleId = routerModuleId ? routerModuleId.toString() : undefined; const moduleId = routerModuleId ? routerModuleId.toString() : undefined;
// hooks // hooks
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
const { peekIssue } = useIssueDetail();
// derived values // derived values
const workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined; const workItemFilters = moduleId ? issuesFilter?.getIssueFilters(moduleId) : undefined;
const activeLayout = workItemFilters?.displayFilters?.layout || undefined; const activeLayout = workItemFilters?.displayFilters?.layout || undefined;
const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId;
useSWR( useSWR(
workspaceSlug && projectId && moduleId workspaceSlug && projectId && moduleId
@ -90,10 +103,15 @@ export const ModuleLayoutRoot = observer(function ModuleLayoutRoot() {
/> />
)} )}
<Row variant={ERowVariant.HUGGING} className="h-full w-full overflow-auto"> <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> </Row>
{/* peek overview */} {shouldRenderPeekOverview && (
<IssuePeekOverview /> <Suspense fallback={null}>
<IssuePeekOverview />
</Suspense>
)}
</div> </div>
)} )}
</ProjectLevelWorkItemFiltersHOC> </ProjectLevelWorkItemFiltersHOC>

View File

@ -4,6 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { lazy, Suspense } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; 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 { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { useIssues } from "@/hooks/store/use-issues"; import { useIssues } from "@/hooks/store/use-issues";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
// local imports
import { IssuePeekOverview } from "../../peek-overview"; const CalendarLayout = lazy(() =>
import { CalendarLayout } from "../calendar/roots/project-root"; import("../calendar/roots/project-root").then((module) => ({ default: module.CalendarLayout }))
import { BaseGanttRoot } from "../gantt"; );
import { KanBanLayout } from "../kanban/roots/project-root"; const BaseGanttRoot = lazy(() => import("../gantt").then((module) => ({ default: module.BaseGanttRoot })));
import { ListLayout } from "../list/roots/project-root"; const IssuePeekOverview = lazy(() =>
import { ProjectSpreadsheetLayout } from "../spreadsheet/roots/project-root"; 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 }) { function ProjectIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined }) {
switch (props.activeLayout) { switch (props.activeLayout) {
@ -49,9 +59,11 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
const projectId = routerProjectId ? routerProjectId.toString() : undefined; const projectId = routerProjectId ? routerProjectId.toString() : undefined;
// hooks // hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT); const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
const { peekIssue } = useIssueDetail();
// derived values // derived values
const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined; const workItemFilters = projectId ? issuesFilter?.getIssueFilters(projectId) : undefined;
const activeLayout = workItemFilters?.displayFilters?.layout; const activeLayout = workItemFilters?.displayFilters?.layout;
const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId;
useSWR( useSWR(
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null, workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
@ -93,10 +105,15 @@ export const ProjectLayoutRoot = observer(function ProjectLayoutRoot() {
<Spinner className="h-4 w-4" /> <Spinner className="h-4 w-4" />
</div> </div>
)} )}
<ProjectIssueLayout activeLayout={activeLayout} /> <Suspense fallback={null}>
<ProjectIssueLayout activeLayout={activeLayout} />
</Suspense>
</div> </div>
{/* peek overview */} {shouldRenderPeekOverview && (
<IssuePeekOverview /> <Suspense fallback={null}>
<IssuePeekOverview />
</Suspense>
)}
</div> </div>
)} )}
</ProjectLevelWorkItemFiltersHOC> </ProjectLevelWorkItemFiltersHOC>

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import React, { useEffect } from "react"; import { lazy, Suspense, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
@ -14,16 +14,27 @@ import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
// hooks // hooks
import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level"; import { ProjectLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/project-level";
import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row"; 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 { useIssues } from "@/hooks/store/use-issues";
import { useProjectView } from "@/hooks/store/use-project-view"; import { useProjectView } from "@/hooks/store/use-project-view";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
// local imports
import { IssuePeekOverview } from "../../peek-overview"; const IssuePeekOverview = lazy(() =>
import { ProjectViewCalendarLayout } from "../calendar/roots/project-view-root"; import("../../peek-overview/root").then((module) => ({ default: module.IssuePeekOverview }))
import { BaseGanttRoot } from "../gantt"; );
import { ProjectViewKanBanLayout } from "../kanban/roots/project-view-root"; const ProjectViewCalendarLayout = lazy(() =>
import { ProjectViewListLayout } from "../list/roots/project-view-root"; import("../calendar/roots/project-view-root").then((module) => ({ default: module.ProjectViewCalendarLayout }))
import { ProjectViewSpreadsheetLayout } from "../spreadsheet/roots/project-view-root"; );
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 }) { function ProjectViewIssueLayout(props: { activeLayout: EIssueLayoutTypes | undefined; viewId: string }) {
switch (props.activeLayout) { switch (props.activeLayout) {
@ -51,6 +62,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
// hooks // hooks
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
const { getViewById } = useProjectView(); const { getViewById } = useProjectView();
const { peekIssue } = useIssueDetail();
// derived values // derived values
const projectView = viewId ? getViewById(viewId) : undefined; const projectView = viewId ? getViewById(viewId) : undefined;
const workItemFilters = viewId ? issuesFilter?.getIssueFilters(viewId) : undefined; const workItemFilters = viewId ? issuesFilter?.getIssueFilters(viewId) : undefined;
@ -63,6 +75,7 @@ export const ProjectViewLayoutRoot = observer(function ProjectViewLayoutRoot() {
richFilters: projectView.rich_filters, richFilters: projectView.rich_filters,
} }
: undefined; : undefined;
const shouldRenderPeekOverview = !!peekIssue?.workspaceSlug && !!peekIssue?.projectId && !!peekIssue?.issueId;
useSWR( useSWR(
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}_${viewId}` : null, 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"> <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> </div>
{/* peek overview */} {shouldRenderPeekOverview && (
<IssuePeekOverview /> <Suspense fallback={null}>
<IssuePeekOverview />
</Suspense>
)}
</div> </div>
)} )}
</ProjectLevelWorkItemFiltersHOC> </ProjectLevelWorkItemFiltersHOC>

View File

@ -4,6 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { lazy, Suspense } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation"; import { useParams, useSearchParams } from "next/navigation";
// components // components
@ -14,7 +15,7 @@ import { EUserProjectRoles } from "@plane/types";
import { ContentWrapper, Row, ERowVariant } from "@plane/ui"; import { ContentWrapper, Row, ERowVariant } from "@plane/ui";
// components // components
import { ListLayout } from "@/components/core/list"; 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 { CycleModuleBoardLayoutLoader } from "@/components/ui/loader/cycle-module-board-loader";
import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader"; import { CycleModuleListLayoutLoader } from "@/components/ui/loader/cycle-module-list-loader";
import { GanttLayoutLoader } from "@/components/ui/loader/layouts/gantt-layout-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 { useModuleFilter } from "@/hooks/store/use-module-filter";
import { useUserPermissions } from "@/hooks/store/user"; 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() { export const ModulesListView = observer(function ModulesListView() {
// router // router
const { workspaceSlug, projectId } = useParams(); const { workspaceSlug, projectId } = useParams();
@ -105,7 +112,9 @@ export const ModulesListView = observer(function ModulesListView() {
)} )}
{displayFilters?.layout === "gantt" && ( {displayFilters?.layout === "gantt" && (
<div className="size-full overflow-hidden"> <div className="size-full overflow-hidden">
<ModulesListGanttChartView /> <Suspense fallback={<GanttLayoutLoader />}>
<ModulesListGanttChartView />
</Suspense>
</div> </div>
)} )}
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View File

@ -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>
);
};

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import React, { useEffect } from "react"; import React, { lazy, Suspense, useEffect } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams, useLocation, Link, useNavigate } from "react-router"; import { useParams, useLocation, Link, useNavigate } from "react-router";
import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants"; import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
@ -18,8 +18,6 @@ import { useUserPermissions } from "@/hooks/store/user";
// plane web imports // plane web imports
import { useNavigationItems } from "@/plane-web/components/navigations"; import { useNavigationItems } from "@/plane-web/components/navigations";
// local imports // local imports
import { LeaveProjectModal } from "../project/leave-project-modal";
import { PublishProjectModal } from "../project/publish-project/modal";
import { ProjectActionsMenu } from "./project-actions-menu"; import { ProjectActionsMenu } from "./project-actions-menu";
import { ProjectHeader } from "./project-header"; import { ProjectHeader } from "./project-header";
import { TabNavigationOverflowMenu } from "./tab-navigation-overflow-menu"; 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 { useResponsiveTabLayout } from "./use-responsive-tab-layout";
import { useTabPreferences } from "./use-tab-preferences"; 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 // Local type definition for navigation items with app-specific fields
export type TNavigationItem = { export type TNavigationItem = {
name: string; name: string;
@ -109,7 +119,7 @@ export const TabNavigationRoot = observer(function TabNavigationRoot(props: TTab
// Filter and sort navigation items // Filter and sort navigation items
const allNavigationItems = navigationItems const allNavigationItems = navigationItems
.filter((item) => item.shouldRender) .filter((item) => item.shouldRender)
.sort((a, b) => a.sortOrder - b.sortOrder); .toSorted((a, b) => a.sortOrder - b.sortOrder);
// Split items into two categories: // Split items into two categories:
// 1. visibleNavigationItems: Items NOT user-hidden (may still overflow due to space) // 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 ( return (
<> <>
<PublishProjectModal isOpen={publishModalOpen} projectId={projectId} onClose={() => handlePublishModal(false)} /> <Suspense fallback={null}>
<LeaveProjectModal {publishModalOpen && (
project={project} <PublishProjectModal isOpen={publishModalOpen} projectId={projectId} onClose={() => handlePublishModal(false)} />
isOpen={leaveProjectModalOpen} )}
onClose={() => handleLeaveProjectModal(false)} {leaveProjectModalOpen && (
/> <LeaveProjectModal
project={project}
isOpen={leaveProjectModalOpen}
onClose={() => handleLeaveProjectModal(false)}
/>
)}
</Suspense>
{/* container for the tab navigation */} {/* container for the tab navigation */}
<div className="flex size-full items-center gap-3 overflow-hidden"> <div className="flex size-full items-center gap-3 overflow-hidden">

View File

@ -4,7 +4,7 @@
* See the LICENSE file for details. * 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 { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
@ -15,20 +15,32 @@ import { CloseIcon, SearchIcon } from "@plane/propel/icons";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// power-k // power-k
import type { TPowerKCommandConfig, TPowerKContext } from "@/components/power-k/core/types"; 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 { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { usePowerK } from "@/hooks/store/use-power-k"; import { usePowerK } from "@/hooks/store/use-power-k";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router"; import { useAppRouter } from "@/hooks/use-app-router";
import { useExpandableSearch } from "@/hooks/use-expandable-search"; 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 = { type TTopNavPowerKProps = {
autoOpen?: boolean;
onAutoOpenComplete?: () => void;
variant?: "top-navigation" | "sidebar" | "expanded-toolbar"; variant?: "top-navigation" | "sidebar" | "expanded-toolbar";
}; };
export const TopNavPowerK = observer((props: TTopNavPowerKProps) => { export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const { variant = "top-navigation" } = props; const { autoOpen = false, onAutoOpenComplete, variant = "top-navigation" } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const isWideSearch = variant === "top-navigation" || variant === "expanded-toolbar"; const isWideSearch = variant === "top-navigation" || variant === "expanded-toolbar";
const isExpandedToolbar = variant === "expanded-toolbar"; const isExpandedToolbar = variant === "expanded-toolbar";
@ -51,6 +63,7 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
const sidebarSearchPortalRef = useRef<HTMLDivElement>(null); const sidebarSearchPortalRef = useRef<HTMLDivElement>(null);
const sidebarSearchButtonRef = useRef<HTMLButtonElement>(null); const sidebarSearchButtonRef = useRef<HTMLButtonElement>(null);
const autoOpenHandledRef = useRef(false);
// store hooks // store hooks
const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK(); const { activeContext, setActivePage, activePage, setTopNavInputRef } = usePowerK();
@ -76,6 +89,15 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
additionalRefs: [sidebarSearchPortalRef], additionalRefs: [sidebarSearchPortalRef],
}); });
useEffect(() => {
if (!autoOpen || autoOpenHandledRef.current) return;
autoOpenHandledRef.current = true;
openPanel();
requestAnimationFrame(() => inputRef.current?.focus());
onAutoOpenComplete?.();
}, [autoOpen, inputRef, onAutoOpenComplete, openPanel]);
// derived values // derived values
const { const {
issue: { getIssueById, getIssueIdByIdentifier }, issue: { getIssueById, getIssueIdByIdentifier },
@ -277,22 +299,26 @@ export const TopNavPowerK = observer((props: TTopNavPowerKProps) => {
> >
<Command.Input value={searchTerm} hidden /> <Command.Input value={searchTerm} hidden />
<Command.List className="vertical-scrollbar scrollbar-sm max-h-[60vh] overflow-y-auto px-2 pb-4 outline-none"> <Command.List className="vertical-scrollbar scrollbar-sm max-h-[60vh] overflow-y-auto px-2 pb-4 outline-none">
<ProjectsAppPowerKCommandsList <Suspense fallback={null}>
activePage={activePage} <ProjectsAppPowerKCommandsList
context={context} activePage={activePage}
handleCommandSelect={handleCommandSelect} context={context}
handlePageDataSelection={handlePageDataSelection} handleCommandSelect={handleCommandSelect}
isWorkspaceLevel={isWorkspaceLevel} handlePageDataSelection={handlePageDataSelection}
searchTerm={searchTerm} isWorkspaceLevel={isWorkspaceLevel}
setSearchTerm={setSearchTerm} searchTerm={searchTerm}
handleSearchMenuClose={() => closePanel()} setSearchTerm={setSearchTerm}
/> handleSearchMenuClose={() => closePanel()}
/>
</Suspense>
</Command.List> </Command.List>
<PowerKModalFooter <Suspense fallback={null}>
isWorkspaceLevel={isWorkspaceLevel} <PowerKModalFooter
projectId={context.params.projectId?.toString()} isWorkspaceLevel={isWorkspaceLevel}
onWorkspaceLevelChange={setIsWorkspaceLevel} projectId={context.params.projectId?.toString()}
/> onWorkspaceLevelChange={setIsWorkspaceLevel}
/>
</Suspense>
</Command> </Command>
); );

View File

@ -4,24 +4,53 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
import { useMemo, useState } from "react"; import { lazy, Suspense, useMemo, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// hooks // hooks
import { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useIssueDetail } from "@/hooks/store/use-issue-detail"; import { useIssueDetail } from "@/hooks/store/use-issue-detail";
import { usePowerK } from "@/hooks/store/use-power-k"; import { usePowerK } from "@/hooks/store/use-power-k";
import { useUser } from "@/hooks/store/user"; import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router"; 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 // local imports
import { useProjectsAppPowerKCommands } from "./config/commands"; import { useProjectsAppPowerKCommands } from "./config/commands";
import type { TPowerKCommandConfig, TPowerKContext } from "./core/types"; import type { TPowerKCommandConfig, TPowerKContext } from "./core/types";
import { GlobalShortcutsProvider } from "./global-shortcuts"; 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 * Projects App PowerK provider
@ -36,6 +65,16 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true); const [shouldShowContextBasedActions, setShouldShowContextBasedActions] = useState(true);
// store hooks // store hooks
const { activeContext, isPowerKModalOpen, togglePowerKModal, setActivePage } = usePowerK(); const { activeContext, isPowerKModalOpen, togglePowerKModal, setActivePage } = usePowerK();
const {
createPageModal,
isBulkDeleteIssueModalOpen,
isCreateCycleModalOpen,
isCreateIssueModalOpen,
isCreateModuleModalOpen,
isCreateProjectModalOpen,
isCreateViewModalOpen,
isDeleteIssueModalOpen,
} = useCommandPalette();
const { data: currentUser } = useUser(); const { data: currentUser } = useUser();
// derived values // derived values
const { const {
@ -46,6 +85,15 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
const workItemDetails = workItemId ? getIssueById(workItemId) : undefined; const workItemDetails = workItemId ? getIssueById(workItemId) : undefined;
const projectId: string | string[] | undefined | null = routerProjectId ?? workItemDetails?.project_id; const projectId: string | string[] | undefined | null = routerProjectId ?? workItemDetails?.project_id;
const commands = useProjectsAppPowerKCommands(); 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 // Build command context from props and store
const context: TPowerKContext = useMemo( const context: TPowerKContext = useMemo(
() => ({ () => ({
@ -79,17 +127,16 @@ export const ProjectsAppPowerKProvider = observer(function ProjectsAppPowerKProv
return ( return (
<> <>
<GlobalShortcutsProvider context={context} commands={commands} /> <GlobalShortcutsProvider context={context} commands={commands} />
{workspaceSlug && <WorkspaceLevelModals workspaceSlug={workspaceSlug.toString()} />} <Suspense fallback={null}>
{workspaceSlug && projectId && ( {shouldLoadWorkspaceLevelModals && <WorkspaceLevelModals workspaceSlug={workspaceSlug!.toString()} />}
<ProjectLevelModals workspaceSlug={workspaceSlug.toString()} projectId={projectId.toString()} /> {shouldLoadProjectLevelModals && (
)} <ProjectLevelModals workspaceSlug={workspaceSlug!.toString()} projectId={projectId!.toString()} />
<WorkItemLevelModals workItemIdentifier={workItemIdentifier?.toString()} /> )}
<ProjectsAppPowerKModalWrapper {shouldLoadWorkItemLevelModals && <WorkItemLevelModals workItemIdentifier={workItemIdentifier?.toString()} />}
commandsListComponent={ProjectsAppPowerKCommandsList} {isPowerKModalOpen && (
context={context} <ProjectsAppPowerKModal context={context} isOpen={isPowerKModalOpen} onClose={() => togglePowerKModal(false)} />
isOpen={isPowerKModalOpen} )}
onClose={() => togglePowerKModal(false)} </Suspense>
/>
</> </>
); );
}); });

View File

@ -11,7 +11,7 @@ import { useTranslation } from "@plane/i18n";
import { InboxIcon, PlusIcon } from "@plane/propel/icons"; import { InboxIcon, PlusIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
import useSWR from "swr"; 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 { useCommandPalette } from "@/hooks/store/use-command-palette";
import { useProject } from "@/hooks/store/use-project"; import { useProject } from "@/hooks/store/use-project";
import { useUserPermissions } from "@/hooks/store/user"; import { useUserPermissions } from "@/hooks/store/user";
@ -61,7 +61,7 @@ export const SidebarUtilityRail = observer(function SidebarUtilityRail() {
</button> </button>
</Tooltip> </Tooltip>
)} )}
<TopNavPowerK variant="sidebar" /> <DeferredTopNavPowerK variant="sidebar" />
<Tooltip tooltipContent="Уведомления" position="right"> <Tooltip tooltipContent="Уведомления" position="right">
<button <button
type="button" type="button"

View File

@ -75,7 +75,12 @@ const buildIssueStreamUrl = (workspaceSlug: string, projectId: string) => {
return url.toString(); return url.toString();
}; };
export const useIssueRealtimeEvents = (storeType: EIssuesStoreType, workspaceSlug?: string, projectId?: string) => { export const useIssueRealtimeEvents = (
storeType: EIssuesStoreType,
workspaceSlug?: string,
projectId?: string,
enabled = true
) => {
const { issueMap, issues, issuesFilter } = useIssues(storeType); const { issueMap, issues, issuesFilter } = useIssues(storeType);
const issueServiceRef = useRef(new IssueService()); const issueServiceRef = useRef(new IssueService());
const issueMapRef = useRef(issueMap); const issueMapRef = useRef(issueMap);
@ -98,7 +103,8 @@ export const useIssueRealtimeEvents = (storeType: EIssuesStoreType, workspaceSlu
}, [issuesFilter]); }, [issuesFilter]);
useEffect(() => { useEffect(() => {
if (!workspaceSlug || !projectId || !REALTIME_STORE_TYPES.has(storeType) || typeof window === "undefined") return; if (!enabled || !workspaceSlug || !projectId || !REALTIME_STORE_TYPES.has(storeType) || typeof window === "undefined")
return;
let socket: WebSocket | undefined; let socket: WebSocket | undefined;
let reconnectTimer: ReturnType<typeof setTimeout> | undefined; let reconnectTimer: ReturnType<typeof setTimeout> | undefined;
@ -280,5 +286,5 @@ export const useIssueRealtimeEvents = (storeType: EIssuesStoreType, workspaceSlu
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
socket?.close(); socket?.close();
}; };
}, [storeType, workspaceSlug, projectId]); }, [enabled, storeType, workspaceSlug, projectId]);
}; };

View File

@ -17,6 +17,7 @@ import { useTimeLineType } from "../components/gantt-chart/contexts";
export const useTimeLineChart = (timelineType: TTimelineType): IBaseTimelineStore => { export const useTimeLineChart = (timelineType: TTimelineType): IBaseTimelineStore => {
const context = useContext(StoreContext); const context = useContext(StoreContext);
if (!context) throw new Error("useTimeLineChart must be used within StoreProvider"); if (!context) throw new Error("useTimeLineChart must be used within StoreProvider");
if (!context.timelineStore) throw context.loadTimelineStore();
return getTimelineStore(context.timelineStore, timelineType); return getTimelineStore(context.timelineStore, timelineType);
}; };
@ -27,6 +28,7 @@ export const useTimeLineChartStore = (): IBaseTimelineStore => {
if (!context) throw new Error("useTimeLineChartStore must be used within StoreProvider"); if (!context) throw new Error("useTimeLineChartStore must be used within StoreProvider");
if (!timelineType) throw new Error("useTimeLineChartStore must be used within TimeLineTypeContext"); if (!timelineType) throw new Error("useTimeLineChartStore must be used within TimeLineTypeContext");
if (!context.timelineStore) throw context.loadTimelineStore();
return getTimelineStore(context.timelineStore, timelineType); return getTimelineStore(context.timelineStore, timelineType);
}; };

View File

@ -5,12 +5,11 @@
*/ */
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
// plane imports // plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { GANTT_TIMELINE_TYPE } from "@plane/types";
// components // components
import { ProjectAccessRestriction } from "@/components/auth-screens/project/project-access-restriction"; import { ProjectAccessRestriction } from "@/components/auth-screens/project/project-access-restriction";
import { import {
@ -36,7 +35,6 @@ import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state"; import { useProjectState } from "@/hooks/store/use-project-state";
import { useProjectView } from "@/hooks/store/use-project-view"; import { useProjectView } from "@/hooks/store/use-project-view";
import { useUser, useUserPermissions } from "@/hooks/store/user"; import { useUser, useUserPermissions } from "@/hooks/store/user";
import { useTimeLineChart } from "@/hooks/use-timeline-chart";
interface IProjectAuthWrapper { interface IProjectAuthWrapper {
workspaceSlug: string; workspaceSlug: string;
@ -55,7 +53,6 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
const { joinProject } = useUserPermissions(); const { joinProject } = useUserPermissions();
const { fetchAllCycles } = useCycle(); const { fetchAllCycles } = useCycle();
const { fetchModulesSlim, fetchModules } = useModule(); const { fetchModulesSlim, fetchModules } = useModule();
const { initGantt } = useTimeLineChart(GANTT_TIMELINE_TYPE.MODULE);
const { fetchViews } = useProjectView(); const { fetchViews } = useProjectView();
const { const {
project: { fetchProjectMembers, fetchProjectUserProperties }, project: { fetchProjectMembers, fetchProjectUserProperties },
@ -73,12 +70,6 @@ export const ProjectAuthWrapper = observer(function ProjectAuthWrapper(props: IP
); );
const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId); const currentProjectRole = getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug); const isWorkspaceAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE, workspaceSlug);
// Initialize module timeline chart
useEffect(() => {
initGantt();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// fetching project details // fetching project details
const { isLoading: isProjectDetailsLoading, error: projectDetailsError } = useSWR( const { isLoading: isProjectDetailsLoading, error: projectDetailsError } = useSWR(
PROJECT_DETAILS(workspaceSlug, projectId), PROJECT_DETAILS(workspaceSlug, projectId),

View File

@ -11,7 +11,7 @@ import { v4 as uuidv4 } from "uuid";
// plane types // plane types
import type { EFileAssetType, TFileEntityInfo, TFileSignedURLResponse } from "@plane/types"; import type { EFileAssetType, TFileEntityInfo, TFileSignedURLResponse } from "@plane/types";
// services // services
import { FileService } from "@/services/file.service"; import type { FileService } from "@/services/file.service";
import type { TAttachmentUploadStatus } from "../issue/issue-details/attachment.store"; import type { TAttachmentUploadStatus } from "../issue/issue-details/attachment.store";
export interface IEditorAssetStore { export interface IEditorAssetStore {
@ -52,7 +52,7 @@ export class EditorAssetStore implements IEditorAssetStore {
// observables // observables
assetsUploadStatus: Record<string, TAttachmentUploadStatus> = {}; assetsUploadStatus: Record<string, TAttachmentUploadStatus> = {};
// services // services
fileService: FileService; private fileService: FileService | undefined;
constructor() { constructor() {
makeObservable(this, { makeObservable(this, {
@ -63,8 +63,6 @@ export class EditorAssetStore implements IEditorAssetStore {
// actions // actions
uploadEditorAsset: action, uploadEditorAsset: action,
}); });
// services
this.fileService = new FileService();
} }
get assetsUploadPercentage() { get assetsUploadPercentage() {
@ -93,9 +91,19 @@ export class EditorAssetStore implements IEditorAssetStore {
}); });
}, 16); }, 16);
private getFileService = async () => {
if (!this.fileService) {
const { FileService } = await import("@/services/file.service");
this.fileService = new FileService();
}
return this.fileService;
};
uploadEditorAsset: IEditorAssetStore["uploadEditorAsset"] = async (args) => { uploadEditorAsset: IEditorAssetStore["uploadEditorAsset"] = async (args) => {
const { blockId, data, file, projectId, workspaceSlug } = args; const { blockId, data, file, projectId, workspaceSlug } = args;
const tempId = uuidv4(); const tempId = uuidv4();
const fileService = await this.getFileService();
try { try {
// update attachment upload status // update attachment upload status
@ -109,7 +117,7 @@ export class EditorAssetStore implements IEditorAssetStore {
}); });
}); });
if (projectId) { if (projectId) {
const response = await this.fileService.uploadProjectAsset( const response = await fileService.uploadProjectAsset(
workspaceSlug, workspaceSlug,
projectId, projectId,
data, data,
@ -121,7 +129,7 @@ export class EditorAssetStore implements IEditorAssetStore {
); );
return response; return response;
} else { } else {
const response = await this.fileService.uploadWorkspaceAsset(workspaceSlug, data, file, (progressEvent) => { const response = await fileService.uploadWorkspaceAsset(workspaceSlug, data, file, (progressEvent) => {
const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100); const progressPercentage = Math.round((progressEvent.progress ?? 0) * 100);
this.debouncedUpdateProgress(blockId, progressPercentage); this.debouncedUpdateProgress(blockId, progressPercentage);
}); });
@ -138,7 +146,8 @@ export class EditorAssetStore implements IEditorAssetStore {
}; };
duplicateEditorAsset: IEditorAssetStore["duplicateEditorAsset"] = async (args) => { duplicateEditorAsset: IEditorAssetStore["duplicateEditorAsset"] = async (args) => {
const { assetId, entityId, entityType, projectId, workspaceSlug } = args; const { assetId, entityId, entityType, projectId, workspaceSlug } = args;
const { asset_id } = await this.fileService.duplicateAsset(workspaceSlug, assetId, { const fileService = await this.getFileService();
const { asset_id } = await fileService.duplicateAsset(workspaceSlug, assetId, {
entity_id: entityId, entity_id: entityId,
entity_type: entityType, entity_type: entityType,
project_id: projectId, project_id: projectId,

View File

@ -11,7 +11,7 @@ import { v4 as uuidv4 } from "uuid";
// types // types
import type { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap, TIssueServiceType } from "@plane/types"; import type { TIssueAttachment, TIssueAttachmentMap, TIssueAttachmentIdMap, TIssueServiceType } from "@plane/types";
// services // services
import { IssueAttachmentService } from "@/services/issue"; import type { IssueAttachmentService } from "@/services/issue/issue_attachment.service";
import type { IIssueRootStore } from "../root.store"; import type { IIssueRootStore } from "../root.store";
import type { IIssueDetail } from "./root.store"; import type { IIssueDetail } from "./root.store";
@ -75,7 +75,8 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
rootIssueStore: IIssueRootStore; rootIssueStore: IIssueRootStore;
rootIssueDetailStore: IIssueDetail; rootIssueDetailStore: IIssueDetail;
// services // services
issueAttachmentService; private issueAttachmentService: IssueAttachmentService | undefined;
private serviceType: TIssueServiceType;
constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) { constructor(rootStore: IIssueRootStore, serviceType: TIssueServiceType) {
makeObservable(this, { makeObservable(this, {
@ -94,10 +95,18 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
// root store // root store
this.rootIssueStore = rootStore; this.rootIssueStore = rootStore;
this.rootIssueDetailStore = rootStore.issueDetail; this.rootIssueDetailStore = rootStore.issueDetail;
// services this.serviceType = serviceType;
this.issueAttachmentService = new IssueAttachmentService(serviceType);
} }
private getIssueAttachmentService = async () => {
if (!this.issueAttachmentService) {
const { IssueAttachmentService } = await import("@/services/issue/issue_attachment.service");
this.issueAttachmentService = new IssueAttachmentService(this.serviceType);
}
return this.issueAttachmentService;
};
// computed // computed
get issueAttachments() { get issueAttachments() {
const issueId = this.rootIssueDetailStore.peekIssue?.issueId; const issueId = this.rootIssueDetailStore.peekIssue?.issueId;
@ -143,7 +152,8 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
}; };
fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => { fetchAttachments = async (workspaceSlug: string, projectId: string, issueId: string) => {
const response = await this.issueAttachmentService.getIssueAttachments(workspaceSlug, projectId, issueId); const issueAttachmentService = await this.getIssueAttachmentService();
const response = await issueAttachmentService.getIssueAttachments(workspaceSlug, projectId, issueId);
runInAction(() => { runInAction(() => {
response.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment)); response.forEach((attachment) => set(this.attachmentMap, attachment.id, attachment));
set( set(
@ -166,6 +176,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, file: File) => { createAttachment = async (workspaceSlug: string, projectId: string, issueId: string, file: File) => {
const tempId = uuidv4(); const tempId = uuidv4();
const issueAttachmentService = await this.getIssueAttachmentService();
try { try {
// update attachment upload status // update attachment upload status
runInAction(() => { runInAction(() => {
@ -177,7 +188,7 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
type: file.type, type: file.type,
}); });
}); });
const response = await this.issueAttachmentService.uploadIssueAttachment( const response = await issueAttachmentService.uploadIssueAttachment(
workspaceSlug, workspaceSlug,
projectId, projectId,
issueId, issueId,
@ -220,7 +231,8 @@ export class IssueAttachmentStore implements IIssueAttachmentStore {
}; };
removeAttachment = async (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) => { removeAttachment = async (workspaceSlug: string, projectId: string, issueId: string, attachmentId: string) => {
const response = await this.issueAttachmentService.deleteIssueAttachment( const issueAttachmentService = await this.getIssueAttachmentService();
const response = await issueAttachmentService.deleteIssueAttachment(
workspaceSlug, workspaceSlug,
projectId, projectId,
issueId, issueId,

View File

@ -7,7 +7,7 @@
import { action, observable, makeObservable, runInAction } from "mobx"; import { action, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
// types // types
import { APITokenService } from "@plane/services"; import type { APITokenService } from "@plane/services";
import type { IApiToken } from "@plane/types"; import type { IApiToken } from "@plane/types";
// services // services
// store // store
@ -30,7 +30,7 @@ export class ApiTokenStore implements IApiTokenStore {
// observables // observables
apiTokens: Record<string, IApiToken> | null = null; apiTokens: Record<string, IApiToken> | null = null;
// services // services
apiTokenService; private apiTokenService: APITokenService | undefined;
// root store // root store
rootStore; rootStore;
@ -47,10 +47,17 @@ export class ApiTokenStore implements IApiTokenStore {
}); });
// root store // root store
this.rootStore = _rootStore; this.rootStore = _rootStore;
// services
this.apiTokenService = new APITokenService();
} }
private getApiTokenService = async () => {
if (!this.apiTokenService) {
const { APITokenService } = await import("@plane/services");
this.apiTokenService = new APITokenService();
}
return this.apiTokenService;
};
/** /**
* get API token by id * get API token by id
* @param apiTokenId * @param apiTokenId
@ -63,54 +70,58 @@ export class ApiTokenStore implements IApiTokenStore {
/** /**
* fetch all the API tokens * fetch all the API tokens
*/ */
fetchApiTokens = async () => fetchApiTokens = async () => {
await this.apiTokenService.list().then((response) => { const apiTokenService = await this.getApiTokenService();
const apiTokensObject: { [apiTokenId: string]: IApiToken } = response.reduce((accumulator, currentWebhook) => { const response = await apiTokenService.list();
if (currentWebhook && currentWebhook.id) { const apiTokensObject = response.reduce<Record<string, IApiToken>>((accumulator, currentWebhook) => {
return { ...accumulator, [currentWebhook.id]: currentWebhook }; if (currentWebhook && currentWebhook.id) {
} accumulator[currentWebhook.id] = currentWebhook;
return accumulator; }
}, {}); return accumulator;
runInAction(() => { }, {});
this.apiTokens = apiTokensObject; runInAction(() => {
}); this.apiTokens = apiTokensObject;
return response;
}); });
return response;
};
/** /**
* fetch API token details using token id * fetch API token details using token id
* @param tokenId * @param tokenId
*/ */
fetchApiTokenDetails = async (tokenId: string) => fetchApiTokenDetails = async (tokenId: string) => {
await this.apiTokenService.retrieve(tokenId).then((response) => { const apiTokenService = await this.getApiTokenService();
runInAction(() => { const response = await apiTokenService.retrieve(tokenId);
this.apiTokens = { ...this.apiTokens, [response.id]: response }; runInAction(() => {
}); this.apiTokens = { ...this.apiTokens, [response.id]: response };
return response;
}); });
return response;
};
/** /**
* create API token using data * create API token using data
* @param data * @param data
*/ */
createApiToken = async (data: Partial<IApiToken>) => createApiToken = async (data: Partial<IApiToken>) => {
await this.apiTokenService.create(data).then((response) => { const apiTokenService = await this.getApiTokenService();
runInAction(() => { const response = await apiTokenService.create(data);
this.apiTokens = { ...this.apiTokens, [response.id]: response }; runInAction(() => {
}); this.apiTokens = { ...this.apiTokens, [response.id]: response };
return response;
}); });
return response;
};
/** /**
* delete API token using token id * delete API token using token id
* @param tokenId * @param tokenId
*/ */
deleteApiToken = async (tokenId: string) => deleteApiToken = async (tokenId: string) => {
await this.apiTokenService.destroy(tokenId).then(() => { const apiTokenService = await this.getApiTokenService();
const updatedApiTokens = { ...this.apiTokens }; await apiTokenService.destroy(tokenId);
delete updatedApiTokens[tokenId]; const updatedApiTokens = { ...this.apiTokens };
runInAction(() => { delete updatedApiTokens[tokenId];
this.apiTokens = updatedApiTokens; runInAction(() => {
}); this.apiTokens = updatedApiTokens;
}); });
};
} }

View File

@ -9,6 +9,28 @@ http {
default_type application/octet-stream; default_type application/octet-stream;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
application/javascript
application/json
application/manifest+json
application/rss+xml
application/vnd.ms-fontobject
application/wasm
application/x-javascript
application/xml
font/ttf
font/otf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;
set_real_ip_from 0.0.0.0/0; set_real_ip_from 0.0.0.0/0;
real_ip_recursive on; real_ip_recursive on;
real_ip_header X-Forward-For; real_ip_header X-Forward-For;