feat: init
This commit is contained in:
48
apps/web/core/hooks/context/app-rail-context.tsx
Normal file
48
apps/web/core/hooks/context/app-rail-context.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import React, { createContext } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// hooks
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export interface AppRailContextType {
|
||||
isEnabled: boolean;
|
||||
shouldRenderAppRail: boolean;
|
||||
toggleAppRail: (value?: boolean) => void;
|
||||
}
|
||||
|
||||
const AppRailContext = createContext<AppRailContextType | undefined>(undefined);
|
||||
|
||||
export { AppRailContext };
|
||||
|
||||
interface AppRailProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AppRailProvider = observer(({ children }: AppRailProviderProps) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const { storedValue: isAppRailVisible, setValue: setIsAppRailVisible } = useLocalStorage<boolean>(
|
||||
`APP_RAIL_${workspaceSlug}`,
|
||||
false
|
||||
);
|
||||
|
||||
const isEnabled = false;
|
||||
|
||||
const toggleAppRail = (value?: boolean) => {
|
||||
if (value === undefined) {
|
||||
setIsAppRailVisible(!isAppRailVisible);
|
||||
} else {
|
||||
setIsAppRailVisible(value);
|
||||
}
|
||||
};
|
||||
|
||||
const contextValue: AppRailContextType = {
|
||||
isEnabled,
|
||||
shouldRenderAppRail: !!isAppRailVisible && isEnabled,
|
||||
toggleAppRail,
|
||||
};
|
||||
|
||||
return <AppRailContext.Provider value={contextValue}>{children}</AppRailContext.Provider>;
|
||||
});
|
||||
10
apps/web/core/hooks/context/use-issue-modal.tsx
Normal file
10
apps/web/core/hooks/context/use-issue-modal.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// components
|
||||
import { IssueModalContext } from "@/components/issues/issue-modal/context";
|
||||
import type { TIssueModalContext } from "@/components/issues/issue-modal/context";
|
||||
|
||||
export const useIssueModal = (): TIssueModalContext => {
|
||||
const context = useContext(IssueModalContext);
|
||||
if (context === undefined) throw new Error("useIssueModal must be used within IssueModalProvider");
|
||||
return context;
|
||||
};
|
||||
2
apps/web/core/hooks/editor/index.ts
Normal file
2
apps/web/core/hooks/editor/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./use-editor-config";
|
||||
export * from "./use-editor-mention";
|
||||
100
apps/web/core/hooks/editor/use-editor-config.ts
Normal file
100
apps/web/core/hooks/editor/use-editor-config.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useCallback } from "react";
|
||||
// plane imports
|
||||
import type { TFileHandler } from "@plane/editor";
|
||||
import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils";
|
||||
// hooks
|
||||
import { useEditorAsset } from "@/hooks/store/use-editor-asset";
|
||||
// plane web hooks
|
||||
import { useExtendedEditorConfig } from "@/plane-web/hooks/editor/use-extended-editor-config";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type TArgs = {
|
||||
projectId?: string;
|
||||
uploadFile: TFileHandler["upload"];
|
||||
workspaceId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const useEditorConfig = () => {
|
||||
// store hooks
|
||||
const { assetsUploadPercentage } = useEditorAsset();
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
const { getExtendedEditorFileHandlers } = useExtendedEditorConfig();
|
||||
|
||||
const getEditorFileHandlers = useCallback(
|
||||
(args: TArgs): TFileHandler => {
|
||||
const { projectId, uploadFile, workspaceId, workspaceSlug } = args;
|
||||
|
||||
return {
|
||||
assetsUploadStatus: assetsUploadPercentage,
|
||||
cancel: fileService.cancelUpload,
|
||||
checkIfAssetExists: async (assetId: string) => {
|
||||
const res = await fileService.checkIfAssetExists(workspaceSlug, assetId);
|
||||
return res?.exists ?? false;
|
||||
},
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.deleteOldWorkspaceAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(
|
||||
getEditorAssetSrc({
|
||||
assetId: src,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
getAssetDownloadSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
getEditorAssetDownloadSrc({
|
||||
assetId: path,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
getAssetSrc: async (path) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return (
|
||||
getEditorAssetSrc({
|
||||
assetId: path,
|
||||
projectId,
|
||||
workspaceSlug,
|
||||
}) ?? ""
|
||||
);
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(workspaceSlug, src);
|
||||
}
|
||||
},
|
||||
upload: uploadFile,
|
||||
validation: {
|
||||
maxFileSize,
|
||||
},
|
||||
...getExtendedEditorFileHandlers({ projectId, workspaceSlug }),
|
||||
};
|
||||
},
|
||||
[assetsUploadPercentage, getExtendedEditorFileHandlers, maxFileSize]
|
||||
);
|
||||
|
||||
return {
|
||||
getEditorFileHandlers,
|
||||
};
|
||||
};
|
||||
76
apps/web/core/hooks/editor/use-editor-mention.tsx
Normal file
76
apps/web/core/hooks/editor/use-editor-mention.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useCallback } from "react";
|
||||
// plane editor
|
||||
import type { TMentionSection, TMentionSuggestion } from "@plane/editor";
|
||||
// plane types
|
||||
import type { TSearchEntities, TSearchEntityRequestPayload, TSearchResponse, TUserSearchResponse } from "@plane/types";
|
||||
// plane ui
|
||||
import { Avatar } from "@plane/ui";
|
||||
// helpers
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// plane web constants
|
||||
import { EDITOR_MENTION_TYPES } from "@/plane-web/constants/editor";
|
||||
// plane web hooks
|
||||
import { useAdditionalEditorMention } from "@/plane-web/hooks/use-additional-editor-mention";
|
||||
|
||||
type TArgs = {
|
||||
searchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||
};
|
||||
|
||||
export const useEditorMention = (args: TArgs) => {
|
||||
const { searchEntity } = args;
|
||||
// additional mentions
|
||||
const { updateAdditionalSections } = useAdditionalEditorMention();
|
||||
// fetch mentions handler
|
||||
const fetchMentions = useCallback(
|
||||
async (query: string): Promise<TMentionSection[]> => {
|
||||
try {
|
||||
const res = await searchEntity({
|
||||
count: 5,
|
||||
query_type: EDITOR_MENTION_TYPES,
|
||||
query,
|
||||
});
|
||||
const suggestionSections: TMentionSection[] = [];
|
||||
if (!res) {
|
||||
throw new Error("No response found");
|
||||
}
|
||||
Object.keys(res).map((key) => {
|
||||
const responseKey = key as TSearchEntities;
|
||||
const response = res[responseKey];
|
||||
if (responseKey === "user_mention" && response && response.length > 0) {
|
||||
const items: TMentionSuggestion[] = (response as TUserSearchResponse[]).map((user) => ({
|
||||
icon: (
|
||||
<Avatar
|
||||
className="flex-shrink-0"
|
||||
src={getFileURL(user.member__avatar_url)}
|
||||
name={user.member__display_name}
|
||||
/>
|
||||
),
|
||||
id: user.member__id,
|
||||
entity_identifier: user.member__id,
|
||||
entity_name: "user_mention",
|
||||
title: user.member__display_name,
|
||||
}));
|
||||
suggestionSections.push({
|
||||
key: "users",
|
||||
title: "Users",
|
||||
items,
|
||||
});
|
||||
}
|
||||
});
|
||||
updateAdditionalSections({
|
||||
response: res,
|
||||
sections: suggestionSections,
|
||||
});
|
||||
return suggestionSections;
|
||||
} catch (error) {
|
||||
console.error("Error in fetching mentions for project pages:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[searchEntity, updateAdditionalSections]
|
||||
);
|
||||
|
||||
return {
|
||||
fetchMentions,
|
||||
};
|
||||
};
|
||||
3
apps/web/core/hooks/store/estimates/index.ts
Normal file
3
apps/web/core/hooks/store/estimates/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./use-project-estimate";
|
||||
export * from "./use-estimate";
|
||||
export * from "./use-estimate-point";
|
||||
16
apps/web/core/hooks/store/estimates/use-estimate-point.ts
Normal file
16
apps/web/core/hooks/store/estimates/use-estimate-point.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import type { IEstimatePoint } from "@/store/estimates/estimate-point";
|
||||
|
||||
export const useEstimatePoint = (
|
||||
estimateId: string | undefined,
|
||||
estimatePointId: string | undefined
|
||||
): IEstimatePoint => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useEstimatePoint must be used within StoreProvider");
|
||||
if (!estimateId || !estimatePointId) return {} as IEstimatePoint;
|
||||
|
||||
return context.projectEstimate.estimates?.[estimateId]?.estimatePoints?.[estimatePointId] || {};
|
||||
};
|
||||
13
apps/web/core/hooks/store/estimates/use-estimate.ts
Normal file
13
apps/web/core/hooks/store/estimates/use-estimate.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import type { IEstimate } from "@/plane-web/store/estimates/estimate";
|
||||
|
||||
export const useEstimate = (estimateId: string | undefined): IEstimate => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useEstimate must be used within StoreProvider");
|
||||
if (!estimateId) return {} as IEstimate;
|
||||
|
||||
return context.projectEstimate.estimates?.[estimateId] ?? {};
|
||||
};
|
||||
12
apps/web/core/hooks/store/estimates/use-project-estimate.ts
Normal file
12
apps/web/core/hooks/store/estimates/use-project-estimate.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import type { IProjectEstimateStore } from "@/store/estimates/project-estimate.store";
|
||||
|
||||
export const useProjectEstimates = (): IProjectEstimateStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectPage must be used within StoreProvider");
|
||||
|
||||
return context.projectEstimate;
|
||||
};
|
||||
2
apps/web/core/hooks/store/notifications/index.ts
Normal file
2
apps/web/core/hooks/store/notifications/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./use-workspace-notifications";
|
||||
export * from "./use-notification";
|
||||
13
apps/web/core/hooks/store/notifications/use-notification.ts
Normal file
13
apps/web/core/hooks/store/notifications/use-notification.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import type { INotification } from "@/store/notifications/notification";
|
||||
|
||||
export const useNotification = (notificationId: string | undefined): INotification => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useNotification must be used within StoreProvider");
|
||||
if (!notificationId) return {} as INotification;
|
||||
|
||||
return context.workspaceNotification.notifications?.[notificationId] ?? {};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import type { IWorkspaceNotificationStore } from "@/store/notifications/workspace-notifications.store";
|
||||
|
||||
export const useWorkspaceNotifications = (): IWorkspaceNotificationStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspaceNotifications must be used within StoreProvider");
|
||||
|
||||
return context.workspaceNotification;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-analytics.ts
Normal file
11
apps/web/core/hooks/store/use-analytics.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IAnalyticsStore } from "@/plane-web/store/analytics.store";
|
||||
|
||||
export const useAnalytics = (): IAnalyticsStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useAnalytics must be used within StoreProvider");
|
||||
return context.analytics;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-app-theme.ts
Normal file
10
apps/web/core/hooks/store/use-app-theme.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IThemeStore } from "@/store/theme.store";
|
||||
|
||||
export const useAppTheme = (): IThemeStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useAppTheme must be used within StoreProvider");
|
||||
return context.theme;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-calendar-view.ts
Normal file
11
apps/web/core/hooks/store/use-calendar-view.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ICalendarStore } from "@/store/issue/issue_calendar_view.store";
|
||||
|
||||
export const useCalendarView = (): ICalendarStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
|
||||
return context.issue.issueCalendarView;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-command-palette.ts
Normal file
11
apps/web/core/hooks/store/use-command-palette.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ICommandPaletteStore } from "@/plane-web/store/command-palette.store";
|
||||
|
||||
export const useCommandPalette = (): ICommandPaletteStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useCommandPalette must be used within StoreProvider");
|
||||
return context.commandPalette;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-cycle-filter.ts
Normal file
11
apps/web/core/hooks/store/use-cycle-filter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ICycleFilterStore } from "@/store/cycle_filter.store";
|
||||
|
||||
export const useCycleFilter = (): ICycleFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useCycleFilter must be used within StoreProvider");
|
||||
return context.cycleFilter;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-cycle.ts
Normal file
11
apps/web/core/hooks/store/use-cycle.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ICycleStore } from "@/plane-web/store/cycle";
|
||||
|
||||
export const useCycle = (): ICycleStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useCycle must be used within StoreProvider");
|
||||
return context.cycle;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-dashboard.ts
Normal file
11
apps/web/core/hooks/store/use-dashboard.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IDashboardStore } from "@/store/dashboard.store";
|
||||
|
||||
export const useDashboard = (): IDashboardStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useDashboard must be used within StoreProvider");
|
||||
return context.dashboard;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-editor-asset.ts
Normal file
10
apps/web/core/hooks/store/use-editor-asset.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IEditorAssetStore } from "@/store/editor/asset.store";
|
||||
|
||||
export const useEditorAsset = (): IEditorAssetStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useEditorAsset must be used within StoreProvider");
|
||||
return context.editorAssetStore;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-favorite.ts
Normal file
10
apps/web/core/hooks/store/use-favorite.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IFavoriteStore } from "@/store/favorite.store";
|
||||
|
||||
export const useFavorite = (): IFavoriteStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useFavorites must be used within StoreProvider");
|
||||
return context.favorite;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-global-view.ts
Normal file
11
apps/web/core/hooks/store/use-global-view.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IGlobalViewStore } from "@/plane-web/store/global-view.store";
|
||||
|
||||
export const useGlobalView = (): IGlobalViewStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useGlobalView must be used within StoreProvider");
|
||||
return context.globalView;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-home.ts
Normal file
11
apps/web/core/hooks/store/use-home.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IHomeStore } from "@/store/workspace/home";
|
||||
|
||||
export const useHome = (): IHomeStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useDashboard must be used within StoreProvider");
|
||||
return context.workspaceRoot.home;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-inbox-issues.ts
Normal file
10
apps/web/core/hooks/store/use-inbox-issues.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
|
||||
|
||||
export const useInboxIssues = (inboxIssueId: string): IInboxIssueStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInboxIssues must be used within StoreProvider");
|
||||
return context.projectInbox.getIssueInboxByIssueId(inboxIssueId);
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-instance.ts
Normal file
10
apps/web/core/hooks/store/use-instance.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IInstanceStore } from "@/store/instance.store";
|
||||
|
||||
export const useInstance = (): IInstanceStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useInstance must be used within StoreProvider");
|
||||
return context.instance;
|
||||
};
|
||||
14
apps/web/core/hooks/store/use-issue-detail.ts
Normal file
14
apps/web/core/hooks/store/use-issue-detail.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useContext } from "react";
|
||||
import type { TIssueServiceType } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IIssueDetail } from "@/plane-web/store/issue/issue-details/root.store";
|
||||
|
||||
export const useIssueDetail = (serviceType: TIssueServiceType = EIssueServiceType.ISSUES): IIssueDetail => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useIssueDetail must be used within StoreProvider");
|
||||
if (serviceType === EIssueServiceType.EPICS) return context.issue.epicDetail;
|
||||
else return context.issue.issueDetail;
|
||||
};
|
||||
157
apps/web/core/hooks/store/use-issues.ts
Normal file
157
apps/web/core/hooks/store/use-issues.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useContext } from "react";
|
||||
import { merge } from "lodash-es";
|
||||
import type { TIssueMap } from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// plane web types
|
||||
import type { IProjectEpics, IProjectEpicsFilter } from "@/plane-web/store/issue/epic";
|
||||
// types
|
||||
import type { ITeamIssues, ITeamIssuesFilter } from "@/plane-web/store/issue/team";
|
||||
import type { ITeamProjectWorkItemsFilter, ITeamProjectWorkItems } from "@/plane-web/store/issue/team-project";
|
||||
import type { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/issue/team-views";
|
||||
import type { IWorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
|
||||
import type { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived";
|
||||
import type { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle";
|
||||
import type { IModuleIssues, IModuleIssuesFilter } from "@/store/issue/module";
|
||||
import type { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile";
|
||||
import type { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project";
|
||||
import type { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views";
|
||||
import type { IWorkspaceIssuesFilter } from "@/store/issue/workspace";
|
||||
import type { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft";
|
||||
// constants
|
||||
|
||||
type defaultIssueStore = {
|
||||
issueMap: TIssueMap;
|
||||
};
|
||||
|
||||
export type TStoreIssues = {
|
||||
[EIssuesStoreType.GLOBAL]: defaultIssueStore & {
|
||||
issues: IWorkspaceIssues;
|
||||
issuesFilter: IWorkspaceIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.WORKSPACE_DRAFT]: defaultIssueStore & {
|
||||
issues: IWorkspaceDraftIssues;
|
||||
issuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.PROFILE]: defaultIssueStore & {
|
||||
issues: IProfileIssues;
|
||||
issuesFilter: IProfileIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.TEAM]: defaultIssueStore & {
|
||||
issues: ITeamIssues;
|
||||
issuesFilter: ITeamIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.PROJECT]: defaultIssueStore & {
|
||||
issues: IProjectIssues;
|
||||
issuesFilter: IProjectIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.CYCLE]: defaultIssueStore & {
|
||||
issues: ICycleIssues;
|
||||
issuesFilter: ICycleIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.MODULE]: defaultIssueStore & {
|
||||
issues: IModuleIssues;
|
||||
issuesFilter: IModuleIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.TEAM_VIEW]: defaultIssueStore & {
|
||||
issues: ITeamViewIssues;
|
||||
issuesFilter: ITeamViewIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & {
|
||||
issues: IProjectViewIssues;
|
||||
issuesFilter: IProjectViewIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.ARCHIVED]: defaultIssueStore & {
|
||||
issues: IArchivedIssues;
|
||||
issuesFilter: IArchivedIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.DEFAULT]: defaultIssueStore & {
|
||||
issues: IProjectIssues;
|
||||
issuesFilter: IProjectIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.EPIC]: defaultIssueStore & {
|
||||
issues: IProjectEpics;
|
||||
issuesFilter: IProjectEpicsFilter;
|
||||
};
|
||||
[EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS]: defaultIssueStore & {
|
||||
issues: ITeamProjectWorkItems;
|
||||
issuesFilter: ITeamProjectWorkItemsFilter;
|
||||
};
|
||||
};
|
||||
|
||||
export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssues[T] => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useIssues must be used within StoreProvider");
|
||||
|
||||
const defaultStore: defaultIssueStore = {
|
||||
issueMap: context.issue.issues.issuesMap,
|
||||
};
|
||||
|
||||
switch (storeType) {
|
||||
case EIssuesStoreType.GLOBAL:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.workspaceIssues,
|
||||
issuesFilter: context.issue.workspaceIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.workspaceDraftIssues,
|
||||
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.PROFILE:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.profileIssues,
|
||||
issuesFilter: context.issue.profileIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.TEAM:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.teamIssues,
|
||||
issuesFilter: context.issue.teamIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.PROJECT:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.projectIssues,
|
||||
issuesFilter: context.issue.projectIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.CYCLE:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.cycleIssues,
|
||||
issuesFilter: context.issue.cycleIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.MODULE:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.moduleIssues,
|
||||
issuesFilter: context.issue.moduleIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.TEAM_VIEW:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.teamViewIssues,
|
||||
issuesFilter: context.issue.teamViewIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.PROJECT_VIEW:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.projectViewIssues,
|
||||
issuesFilter: context.issue.projectViewIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.ARCHIVED:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.archivedIssues,
|
||||
issuesFilter: context.issue.archivedIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.EPIC:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.projectEpics,
|
||||
issuesFilter: context.issue.projectEpicsFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.teamProjectWorkItems,
|
||||
issuesFilter: context.issue.teamProjectWorkItemsFilter,
|
||||
}) as TStoreIssues[T];
|
||||
default:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.projectIssues,
|
||||
issuesFilter: context.issue.projectIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
}
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-kanban-view.ts
Normal file
11
apps/web/core/hooks/store/use-kanban-view.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IIssueKanBanViewStore } from "@/store/issue/issue_kanban_view.store";
|
||||
|
||||
export const useKanbanView = (): IIssueKanBanViewStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
|
||||
return context.issue.issueKanBanView;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-label.ts
Normal file
11
apps/web/core/hooks/store/use-label.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ILabelStore } from "@/store/label.store";
|
||||
|
||||
export const useLabel = (): ILabelStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useLabel must be used within StoreProvider");
|
||||
return context.label;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-member.ts
Normal file
11
apps/web/core/hooks/store/use-member.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types;
|
||||
import type { IMemberRootStore } from "@/store/member";
|
||||
|
||||
export const useMember = (): IMemberRootStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useMember must be used within StoreProvider");
|
||||
return context.memberRoot;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-module-filter.ts
Normal file
11
apps/web/core/hooks/store/use-module-filter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IModuleFilterStore } from "@/store/module_filter.store";
|
||||
|
||||
export const useModuleFilter = (): IModuleFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useModuleFilter must be used within StoreProvider");
|
||||
return context.moduleFilter;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-module.ts
Normal file
11
apps/web/core/hooks/store/use-module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IModuleStore } from "@/store/module.store";
|
||||
|
||||
export const useModule = (): IModuleStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useModule must be used within StoreProvider");
|
||||
return context.module;
|
||||
};
|
||||
9
apps/web/core/hooks/store/use-multiple-select-store.ts
Normal file
9
apps/web/core/hooks/store/use-multiple-select-store.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
|
||||
export const useMultipleSelectStore = () => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useMultipleSelectStore must be used within StoreProvider");
|
||||
return context.multipleSelect;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-project-filter.ts
Normal file
11
apps/web/core/hooks/store/use-project-filter.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IProjectFilterStore } from "@/store/project/project_filter.store";
|
||||
|
||||
export const useProjectFilter = (): IProjectFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectFilter must be used within StoreProvider");
|
||||
return context.projectRoot.projectFilter;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-project-inbox.ts
Normal file
10
apps/web/core/hooks/store/use-project-inbox.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IProjectInboxStore } from "@/plane-web/store/project-inbox.store";
|
||||
|
||||
export const useProjectInbox = (): IProjectInboxStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectInbox must be used within StoreProvider");
|
||||
return context.projectInbox;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-project-publish.ts
Normal file
11
apps/web/core/hooks/store/use-project-publish.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IProjectPublishStore } from "@/store/project/project-publish.store";
|
||||
|
||||
export const useProjectPublish = (): IProjectPublishStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectPublish must be used within StoreProvider");
|
||||
return context.projectRoot.publish;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-project-state.ts
Normal file
11
apps/web/core/hooks/store/use-project-state.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// Plane-web
|
||||
import type { IStateStore } from "@/plane-web/store/state.store";
|
||||
|
||||
export const useProjectState = (): IStateStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectState must be used within StoreProvider");
|
||||
return context.state;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-project-view.ts
Normal file
11
apps/web/core/hooks/store/use-project-view.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IProjectViewStore } from "@/plane-web/store/project-view.store";
|
||||
|
||||
export const useProjectView = (): IProjectViewStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProjectView must be used within StoreProvider");
|
||||
return context.projectView;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-project.ts
Normal file
11
apps/web/core/hooks/store/use-project.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IProjectStore } from "@/store/project/project.store";
|
||||
|
||||
export const useProject = (): IProjectStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useProject must be used within StoreProvider");
|
||||
return context.projectRoot.project;
|
||||
};
|
||||
10
apps/web/core/hooks/store/use-router-params.ts
Normal file
10
apps/web/core/hooks/store/use-router-params.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
// store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IRouterStore } from "@/store/router.store";
|
||||
|
||||
export const useRouterParams = (): IRouterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useRouterParams must be used within StoreProvider");
|
||||
return context.router;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-transient.ts
Normal file
11
apps/web/core/hooks/store/use-transient.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { ITransientStore } from "@/store/transient.store";
|
||||
|
||||
export const useTransient = (): ITransientStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useTransient must be used within StoreProvider");
|
||||
return context.transient;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-webhook.ts
Normal file
11
apps/web/core/hooks/store/use-webhook.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IWebhookStore } from "@/store/workspace/webhook.store";
|
||||
|
||||
export const useWebhook = (): IWebhookStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWebhook must be used within StoreProvider");
|
||||
return context.workspaceRoot.webhook;
|
||||
};
|
||||
11
apps/web/core/hooks/store/use-workspace.ts
Normal file
11
apps/web/core/hooks/store/use-workspace.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IWorkspaceRootStore } from "@/store/workspace";
|
||||
|
||||
export const useWorkspace = (): IWorkspaceRootStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspace must be used within StoreProvider");
|
||||
return context.workspaceRoot;
|
||||
};
|
||||
4
apps/web/core/hooks/store/user/index.ts
Normal file
4
apps/web/core/hooks/store/user/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./user-user";
|
||||
export * from "./user-user-profile";
|
||||
export * from "./user-user-settings";
|
||||
export * from "./user-permissions";
|
||||
12
apps/web/core/hooks/store/user/user-permissions.ts
Normal file
12
apps/web/core/hooks/store/user/user-permissions.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// plane web imports
|
||||
import type { IUserPermissionStore } from "@/plane-web/store/user/permission.store";
|
||||
|
||||
export const useUserPermissions = (): IUserPermissionStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserPermissions must be used within StoreProvider");
|
||||
|
||||
return context.user.permission;
|
||||
};
|
||||
11
apps/web/core/hooks/store/user/user-user-profile.ts
Normal file
11
apps/web/core/hooks/store/user/user-user-profile.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IUserProfileStore } from "@/store/user/profile.store";
|
||||
|
||||
export const useUserProfile = (): IUserProfileStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
return context.user.userProfile;
|
||||
};
|
||||
11
apps/web/core/hooks/store/user/user-user-settings.ts
Normal file
11
apps/web/core/hooks/store/user/user-user-settings.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IUserSettingsStore } from "@/store/user/settings.store";
|
||||
|
||||
export const useUserSettings = (): IUserSettingsStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserSettings must be used within StoreProvider");
|
||||
return context.user.userSettings;
|
||||
};
|
||||
11
apps/web/core/hooks/store/user/user-user.ts
Normal file
11
apps/web/core/hooks/store/user/user-user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IUserStore } from "@/store/user";
|
||||
|
||||
export const useUser = (): IUserStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUser must be used within StoreProvider");
|
||||
return context.user;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
// plane imports
|
||||
import type { IWorkItemFilterInstance } from "@plane/shared-state";
|
||||
import type { EIssuesStoreType } from "@plane/types";
|
||||
// local imports
|
||||
import { useWorkItemFilters } from "./use-work-item-filters";
|
||||
|
||||
export const useWorkItemFilterInstance = (
|
||||
entityType: EIssuesStoreType,
|
||||
entityId: string
|
||||
): IWorkItemFilterInstance | undefined => {
|
||||
const { getFilter } = useWorkItemFilters();
|
||||
return getFilter(entityType, entityId);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// plane imports
|
||||
import type { IWorkItemFilterStore } from "@plane/shared-state";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
|
||||
export const useWorkItemFilters = (): IWorkItemFilterStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkItemFilters must be used within StoreProvider");
|
||||
return context.workItemFilters;
|
||||
};
|
||||
2
apps/web/core/hooks/store/workspace-draft/index.ts
Normal file
2
apps/web/core/hooks/store/workspace-draft/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./use-workspace-draft-issue";
|
||||
export * from "./use-workspace-draft-issue-filters";
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft";
|
||||
|
||||
export const useWorkspaceDraftIssueFilters = (): IWorkspaceDraftIssues => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspaceDraftIssueFilters must be used within StoreProvider");
|
||||
|
||||
return context.issue.workspaceDraftIssues;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import type { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft";
|
||||
|
||||
export const useWorkspaceDraftIssues = (): IWorkspaceDraftIssues => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspaceDraftIssues must be used within StoreProvider");
|
||||
|
||||
return context.issue.workspaceDraftIssues;
|
||||
};
|
||||
10
apps/web/core/hooks/use-app-rail.tsx
Normal file
10
apps/web/core/hooks/use-app-rail.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from "react";
|
||||
import { AppRailContext } from "./context/app-rail-context";
|
||||
|
||||
export const useAppRail = () => {
|
||||
const context = useContext(AppRailContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useAppRail must be used within AppRailProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
4
apps/web/core/hooks/use-app-router.tsx
Normal file
4
apps/web/core/hooks/use-app-router.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
// router from n-progress-bar
|
||||
import { useRouter } from "@/lib/b-progress";
|
||||
|
||||
export const useAppRouter = () => useRouter();
|
||||
73
apps/web/core/hooks/use-auto-save.tsx
Normal file
73
apps/web/core/hooks/use-auto-save.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { debounce } from "lodash-es";
|
||||
|
||||
const AUTO_SAVE_TIME = 30000;
|
||||
|
||||
const useAutoSave = (handleSaveDescription: () => void) => {
|
||||
const intervalIdRef = useRef<any>(null);
|
||||
const handleSaveDescriptionRef = useRef(handleSaveDescription);
|
||||
|
||||
// Update the ref to always point to the latest handleSaveDescription
|
||||
useEffect(() => {
|
||||
handleSaveDescriptionRef.current = handleSaveDescription;
|
||||
}, [handleSaveDescription]);
|
||||
|
||||
// Set up the interval to run every 10 seconds
|
||||
useEffect(() => {
|
||||
intervalIdRef.current = setInterval(() => {
|
||||
try {
|
||||
handleSaveDescriptionRef.current();
|
||||
} catch (error) {
|
||||
console.error("Autosave before manual save failed:", error);
|
||||
}
|
||||
}, AUTO_SAVE_TIME);
|
||||
|
||||
return () => {
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Debounced save function for manual save (Ctrl+S or Cmd+S) and clearing the
|
||||
// interval for auto save and setting up the interval again
|
||||
useEffect(() => {
|
||||
const debouncedSave = debounce(() => {
|
||||
try {
|
||||
handleSaveDescriptionRef.current();
|
||||
} catch (error) {
|
||||
console.error("Manual save failed:", error);
|
||||
}
|
||||
|
||||
if (intervalIdRef.current) {
|
||||
clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = setInterval(() => {
|
||||
try {
|
||||
handleSaveDescriptionRef.current();
|
||||
} catch (error) {
|
||||
console.error("Autosave after manual save failed:", error);
|
||||
}
|
||||
}, AUTO_SAVE_TIME);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const handleSave = (e: KeyboardEvent) => {
|
||||
const { ctrlKey, metaKey, key } = e;
|
||||
const cmdClicked = ctrlKey || metaKey;
|
||||
|
||||
if (cmdClicked && key.toLowerCase() === "s") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
debouncedSave();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleSave);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleSave);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useAutoSave;
|
||||
123
apps/web/core/hooks/use-auto-scroller.tsx
Normal file
123
apps/web/core/hooks/use-auto-scroller.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const SCROLL_BY = 3;
|
||||
|
||||
const AUTO_SCROLL_THRESHOLD = 15;
|
||||
const MAX_SPEED_THRESHOLD = 5;
|
||||
|
||||
export const useAutoScroller = (
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
shouldScroll = false,
|
||||
leftOffset = 0,
|
||||
topOffset = 0
|
||||
) => {
|
||||
const containerDimensions = useRef<DOMRect | undefined>();
|
||||
const intervalId = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
||||
const mousePosition = useRef<{ clientX: number; clientY: number } | undefined>(undefined);
|
||||
|
||||
const clearRegisteredTimeout = () => {
|
||||
clearInterval(intervalId.current);
|
||||
};
|
||||
|
||||
const onDragEnd = () => clearRegisteredTimeout();
|
||||
|
||||
const handleAutoScroll = (e: MouseEvent) => {
|
||||
const rect = containerDimensions.current;
|
||||
clearInterval(intervalId.current);
|
||||
|
||||
if (!rect || !shouldScroll || (e.clientX === 0 && e.clientY === 0)) return;
|
||||
|
||||
let diffX = 0,
|
||||
diffY = 0;
|
||||
|
||||
if (mousePosition.current) {
|
||||
diffX = e.clientX - mousePosition.current.clientX;
|
||||
diffY = e.clientY - mousePosition.current.clientY;
|
||||
}
|
||||
|
||||
mousePosition.current = { clientX: e.clientX, clientY: e.clientY };
|
||||
const { left, top, width, height } = rect;
|
||||
|
||||
const mouseX = e.clientX - left - leftOffset;
|
||||
const mouseY = e.clientY - top - topOffset;
|
||||
|
||||
const currWidth = width - leftOffset;
|
||||
const currHeight = height - topOffset;
|
||||
|
||||
// Get Threshold in percentages
|
||||
const thresholdX = (currWidth / 100) * AUTO_SCROLL_THRESHOLD;
|
||||
const thresholdY = (currHeight / 100) * AUTO_SCROLL_THRESHOLD;
|
||||
const maxSpeedX = (currWidth / 100) * MAX_SPEED_THRESHOLD;
|
||||
const maxSpeedY = (currHeight / 100) * MAX_SPEED_THRESHOLD;
|
||||
|
||||
let scrollByX = 0,
|
||||
scrollByY = 0;
|
||||
|
||||
// Check mouse positions against thresholds
|
||||
if (mouseX < thresholdX && diffX <= 0) {
|
||||
scrollByX = -1 * SCROLL_BY;
|
||||
if (mouseX < maxSpeedX) {
|
||||
scrollByX *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseX > currWidth - thresholdX && diffX >= 0) {
|
||||
scrollByX = SCROLL_BY;
|
||||
if (mouseX > currWidth - maxSpeedX) {
|
||||
scrollByX *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseY < thresholdY && diffY <= 0) {
|
||||
scrollByY = -1 * SCROLL_BY;
|
||||
if (mouseX < maxSpeedY) {
|
||||
scrollByY *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseY > currHeight - thresholdY && diffY >= 0) {
|
||||
scrollByY = SCROLL_BY;
|
||||
if (mouseY > currHeight - maxSpeedY) {
|
||||
scrollByY *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
// if mouse position breaches threshold, then start to scroll
|
||||
if (scrollByX || scrollByY) {
|
||||
containerRef.current?.scrollBy(scrollByX, scrollByY);
|
||||
intervalId.current = setInterval(() => {
|
||||
containerRef.current?.scrollBy(scrollByX, scrollByY);
|
||||
}, 16);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = containerRef.current;
|
||||
|
||||
if (!containerElement || !shouldScroll) return;
|
||||
|
||||
containerElement.addEventListener("drag", handleAutoScroll);
|
||||
containerElement.addEventListener("mousemove", handleAutoScroll);
|
||||
document.addEventListener("mouseup", onDragEnd);
|
||||
document.addEventListener("dragend", onDragEnd);
|
||||
|
||||
return () => {
|
||||
containerElement?.removeEventListener("drag", handleAutoScroll);
|
||||
containerElement?.removeEventListener("mousemove", handleAutoScroll);
|
||||
document.removeEventListener("mouseup", onDragEnd);
|
||||
document.removeEventListener("dragend", onDragEnd);
|
||||
};
|
||||
}, [shouldScroll, intervalId]);
|
||||
|
||||
useEffect(() => {
|
||||
const containerElement = containerRef.current;
|
||||
|
||||
if (!containerElement || !shouldScroll) {
|
||||
clearRegisteredTimeout();
|
||||
containerDimensions.current = undefined;
|
||||
}
|
||||
|
||||
containerDimensions.current = containerElement?.getBoundingClientRect();
|
||||
}, [shouldScroll]);
|
||||
};
|
||||
109
apps/web/core/hooks/use-collaborative-page-actions.tsx
Normal file
109
apps/web/core/hooks/use-collaborative-page-actions.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import type { EditorRefApi, TDocumentEventsServer } from "@plane/editor";
|
||||
import type { TDocumentEventsClient } from "@plane/editor/lib";
|
||||
import { DocumentCollaborativeEvents, getServerEventName } from "@plane/editor/lib";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type CollaborativeAction = {
|
||||
execute: (shouldSync?: boolean, recursive?: boolean) => Promise<void>;
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
type CollaborativeActionEvent =
|
||||
| { type: "sendMessageToServer"; message: TDocumentEventsServer; recursive?: boolean }
|
||||
| { type: "receivedMessageFromServer"; message: TDocumentEventsClient };
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const useCollaborativePageActions = (props: Props) => {
|
||||
const { page } = props;
|
||||
const editorRef = page.editor.editorRef;
|
||||
// currentUserAction local state to track if the current action is being processed, a
|
||||
// local action is basically the action performed by the current user to avoid double operations
|
||||
const [currentActionBeingProcessed, setCurrentActionBeingProcessed] = useState<TDocumentEventsClient | null>(null);
|
||||
|
||||
// @ts-expect-error - TODO: fix this
|
||||
const actionHandlerMap: Record<TDocumentEventsClient, CollaborativeAction> = useMemo(
|
||||
() => ({
|
||||
[DocumentCollaborativeEvents.lock.client]: {
|
||||
execute: (shouldSync?: boolean, recursive?: boolean) => page.lock({ shouldSync, recursive }),
|
||||
errorMessage: "Page could not be locked. Please try again later.",
|
||||
},
|
||||
[DocumentCollaborativeEvents.unlock.client]: {
|
||||
execute: (shouldSync?: boolean, recursive?: boolean) => page.unlock({ shouldSync, recursive }),
|
||||
errorMessage: "Page could not be unlocked. Please try again later.",
|
||||
},
|
||||
[DocumentCollaborativeEvents.archive.client]: {
|
||||
execute: (shouldSync?: boolean) => page.archive({ shouldSync }),
|
||||
errorMessage: "Page could not be archived. Please try again later.",
|
||||
},
|
||||
[DocumentCollaborativeEvents.unarchive.client]: {
|
||||
execute: (shouldSync?: boolean) => page.restore({ shouldSync }),
|
||||
errorMessage: "Page could not be restored. Please try again later.",
|
||||
},
|
||||
[DocumentCollaborativeEvents["make-public"].client]: {
|
||||
execute: (shouldSync?: boolean) => page.makePublic({ shouldSync }),
|
||||
errorMessage: "Page could not be made public. Please try again later.",
|
||||
},
|
||||
[DocumentCollaborativeEvents["make-private"].client]: {
|
||||
execute: (shouldSync?: boolean) => page.makePrivate({ shouldSync }),
|
||||
errorMessage: "Page could not be made private. Please try again later.",
|
||||
},
|
||||
}),
|
||||
[page]
|
||||
);
|
||||
|
||||
const executeCollaborativeAction = useCallback(
|
||||
async (event: CollaborativeActionEvent) => {
|
||||
const isPerformedByCurrentUser = event.type === "sendMessageToServer";
|
||||
const clientAction = isPerformedByCurrentUser ? DocumentCollaborativeEvents[event.message].client : event.message;
|
||||
const actionDetails = actionHandlerMap[clientAction];
|
||||
try {
|
||||
await actionDetails.execute(isPerformedByCurrentUser, isPerformedByCurrentUser ? event?.recursive : undefined);
|
||||
if (isPerformedByCurrentUser) {
|
||||
const serverEventName = getServerEventName(clientAction);
|
||||
if (serverEventName) {
|
||||
editorRef?.emitRealTimeUpdate(serverEventName);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (actionDetails?.errorMessage) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: actionDetails.errorMessage,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[actionHandlerMap, editorRef]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const realTimeStatelessMessageListener = editorRef?.listenToRealTimeUpdate();
|
||||
const handleStatelessMessage = (message: { payload: TDocumentEventsClient }) => {
|
||||
if (currentActionBeingProcessed === message.payload) {
|
||||
setCurrentActionBeingProcessed(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload) {
|
||||
executeCollaborativeAction({ type: "receivedMessageFromServer", message: message.payload });
|
||||
}
|
||||
};
|
||||
|
||||
realTimeStatelessMessageListener?.on("stateless", handleStatelessMessage);
|
||||
|
||||
return () => {
|
||||
realTimeStatelessMessageListener?.off("stateless", handleStatelessMessage);
|
||||
};
|
||||
}, [editorRef, currentActionBeingProcessed, executeCollaborativeAction]);
|
||||
|
||||
return {
|
||||
executeCollaborativeAction,
|
||||
};
|
||||
};
|
||||
17
apps/web/core/hooks/use-current-time.tsx
Normal file
17
apps/web/core/hooks/use-current-time.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useCurrentTime = () => {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
// update the current time every second
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
setCurrentTime(new Date());
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
currentTime,
|
||||
};
|
||||
};
|
||||
19
apps/web/core/hooks/use-debounce.tsx
Normal file
19
apps/web/core/hooks/use-debounce.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useDebounce = (value: any, milliSeconds: number) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, milliSeconds);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [value, milliSeconds]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
|
||||
export default useDebounce;
|
||||
36
apps/web/core/hooks/use-dropdown-key-down.tsx
Normal file
36
apps/web/core/hooks/use-dropdown-key-down.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(
|
||||
onEnterKeyDown: () => void,
|
||||
onEscKeyDown: () => void,
|
||||
stopPropagation?: boolean
|
||||
): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
|
||||
const stopEventPropagation = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (stopPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
[stopPropagation]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
stopEventPropagation(event);
|
||||
onEnterKeyDown();
|
||||
} else if (event.key === "Escape") {
|
||||
stopEventPropagation(event);
|
||||
onEscKeyDown();
|
||||
} else if (event.key === "Tab") onEscKeyDown();
|
||||
},
|
||||
[onEnterKeyDown, onEscKeyDown, stopEventPropagation]
|
||||
);
|
||||
|
||||
return handleKeyDown;
|
||||
};
|
||||
80
apps/web/core/hooks/use-dropdown.ts
Normal file
80
apps/web/core/hooks/use-dropdown.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useEffect } from "react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
import { usePlatformOS } from "./use-platform-os";
|
||||
|
||||
type TArguments = {
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>;
|
||||
isOpen: boolean;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => Promise<void> | void;
|
||||
query?: string;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setQuery?: React.Dispatch<React.SetStateAction<string>>;
|
||||
};
|
||||
|
||||
export const useDropdown = (args: TArguments) => {
|
||||
const { dropdownRef, inputRef, isOpen, onClose, onOpen, query, setIsOpen, setQuery } = args;
|
||||
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
/**
|
||||
* @description clear the search input when the user presses the escape key, if the search input is not empty
|
||||
* @param {React.KeyboardEvent<HTMLInputElement>} e
|
||||
*/
|
||||
const searchInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (query !== "" && e.key === "Escape") {
|
||||
e.stopPropagation();
|
||||
setQuery?.("");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description close the dropdown, clear the search input, and call the onClose callback
|
||||
*/
|
||||
const handleClose = () => {
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose?.();
|
||||
setQuery?.("");
|
||||
};
|
||||
|
||||
// toggle the dropdown, call the onOpen callback if the dropdown is closed, and call the onClose callback if the dropdown is open
|
||||
const toggleDropdown = () => {
|
||||
if (!isOpen) onOpen?.();
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
if (isOpen) onClose?.();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose);
|
||||
|
||||
/**
|
||||
* @description toggle the dropdown on click
|
||||
* @param {React.MouseEvent<HTMLButtonElement, MouseEvent>} e
|
||||
*/
|
||||
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
// close the dropdown when the user clicks outside of the dropdown
|
||||
useOutsideClickDetector(dropdownRef, handleClose);
|
||||
|
||||
// focus the search input when the dropdown is open
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef?.current && !isMobile) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef, isOpen, isMobile]);
|
||||
|
||||
return {
|
||||
handleClose,
|
||||
handleKeyDown,
|
||||
handleOnClick,
|
||||
searchInputKeyDown,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useExtendedSidebarOutsideClickDetector = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: () => void,
|
||||
targetId: string
|
||||
) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
// check for the closest element with attribute name data-prevent-outside-click
|
||||
const preventOutsideClickElement = (event.target as HTMLElement | undefined)?.closest(
|
||||
"[data-prevent-outside-click]"
|
||||
);
|
||||
// if the closest element with attribute name data-prevent-outside-click is found, return
|
||||
if (preventOutsideClickElement) {
|
||||
return;
|
||||
}
|
||||
// check if the click target is the current issue element or its children
|
||||
let targetElement = event.target as HTMLElement | null;
|
||||
while (targetElement) {
|
||||
if (targetElement.id === targetId) {
|
||||
// if the click target is the current issue element, return
|
||||
return;
|
||||
}
|
||||
targetElement = targetElement.parentElement;
|
||||
}
|
||||
const delayOutsideClickElement = (event.target as HTMLElement | undefined)?.closest("[data-delay-outside-click]");
|
||||
if (delayOutsideClickElement) {
|
||||
// if the click target is the closest element with attribute name data-delay-outside-click, delay the callback
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
// else, call the callback immediately
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default useExtendedSidebarOutsideClickDetector;
|
||||
77
apps/web/core/hooks/use-favorite-item-details.tsx
Normal file
77
apps/web/core/hooks/use-favorite-item-details.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
// plane imports
|
||||
import type { IFavorite } from "@plane/types";
|
||||
// components
|
||||
import { getPageName } from "@plane/utils";
|
||||
import {
|
||||
generateFavoriteItemLink,
|
||||
getFavoriteItemIcon,
|
||||
} from "@/components/workspace/sidebar/favorites/favorite-items/common";
|
||||
// helpers
|
||||
// hooks
|
||||
import { useCycle } from "@/hooks/store/use-cycle";
|
||||
import { useModule } from "@/hooks/store/use-module";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectView } from "@/hooks/store/use-project-view";
|
||||
// plane web hooks
|
||||
import { EPageStoreType, usePage } from "@/plane-web/hooks/store";
|
||||
import { useAdditionalFavoriteItemDetails } from "@/plane-web/hooks/use-additional-favorite-item-details";
|
||||
|
||||
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const {
|
||||
entity_identifier: favoriteItemId,
|
||||
entity_data: { logo_props: favoriteItemLogoProps },
|
||||
entity_type: favoriteItemEntityType,
|
||||
} = favorite;
|
||||
const favoriteItemName = favorite?.entity_data?.name || favorite?.name;
|
||||
// store hooks
|
||||
const { getViewById } = useProjectView();
|
||||
const { getProjectById } = useProject();
|
||||
const { getCycleById } = useCycle();
|
||||
const { getModuleById } = useModule();
|
||||
// additional details
|
||||
const { getAdditionalFavoriteItemDetails } = useAdditionalFavoriteItemDetails();
|
||||
// derived values
|
||||
const pageDetail = usePage({
|
||||
pageId: favoriteItemId ?? "",
|
||||
storeType: EPageStoreType.PROJECT,
|
||||
});
|
||||
const viewDetails = getViewById(favoriteItemId ?? "");
|
||||
const cycleDetail = getCycleById(favoriteItemId ?? "");
|
||||
const moduleDetail = getModuleById(favoriteItemId ?? "");
|
||||
const currentProjectDetails = getProjectById(favorite.project_id ?? "");
|
||||
|
||||
let itemIcon;
|
||||
let itemTitle;
|
||||
const itemLink = generateFavoriteItemLink(workspaceSlug.toString(), favorite);
|
||||
|
||||
switch (favoriteItemEntityType) {
|
||||
case "project":
|
||||
itemTitle = currentProjectDetails?.name ?? favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("project", currentProjectDetails?.logo_props || favoriteItemLogoProps);
|
||||
break;
|
||||
case "page":
|
||||
itemTitle = getPageName(pageDetail?.name ?? favoriteItemName);
|
||||
itemIcon = getFavoriteItemIcon("page", pageDetail?.logo_props ?? favoriteItemLogoProps);
|
||||
break;
|
||||
case "view":
|
||||
itemTitle = viewDetails?.name ?? favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("view", viewDetails?.logo_props || favoriteItemLogoProps);
|
||||
break;
|
||||
case "cycle":
|
||||
itemTitle = cycleDetail?.name ?? favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("cycle");
|
||||
break;
|
||||
case "module":
|
||||
itemTitle = moduleDetail?.name ?? favoriteItemName;
|
||||
itemIcon = getFavoriteItemIcon("module");
|
||||
break;
|
||||
default: {
|
||||
const additionalDetails = getAdditionalFavoriteItemDetails(workspaceSlug, favorite);
|
||||
itemTitle = additionalDetails.itemTitle;
|
||||
itemIcon = additionalDetails.itemIcon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { itemIcon, itemTitle, itemLink };
|
||||
};
|
||||
124
apps/web/core/hooks/use-group-dragndrop.ts
Normal file
124
apps/web/core/hooks/use-group-dragndrop.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import type { EIssuesStoreType, TIssue, TIssueGroupByOptions, TIssueOrderByOptions } from "@plane/types";
|
||||
import type { GroupDropLocation } from "@/components/issues/issue-layouts/utils";
|
||||
import { handleGroupDragDrop } from "@/components/issues/issue-layouts/utils";
|
||||
import { ISSUE_FILTER_DEFAULT_DATA } from "@/store/issue/helpers/base-issues.store";
|
||||
import { useIssueDetail } from "./store/use-issue-detail";
|
||||
import { useIssues } from "./store/use-issues";
|
||||
import { useIssuesActions } from "./use-issues-actions";
|
||||
|
||||
type DNDStoreType =
|
||||
| EIssuesStoreType.PROJECT
|
||||
| EIssuesStoreType.MODULE
|
||||
| EIssuesStoreType.CYCLE
|
||||
| EIssuesStoreType.PROJECT_VIEW
|
||||
| EIssuesStoreType.PROFILE
|
||||
| EIssuesStoreType.ARCHIVED
|
||||
| EIssuesStoreType.WORKSPACE_DRAFT
|
||||
| EIssuesStoreType.TEAM
|
||||
| EIssuesStoreType.TEAM_VIEW
|
||||
| EIssuesStoreType.EPIC
|
||||
| EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS;
|
||||
|
||||
export const useGroupIssuesDragNDrop = (
|
||||
storeType: DNDStoreType,
|
||||
orderBy: TIssueOrderByOptions | undefined,
|
||||
groupBy: TIssueGroupByOptions | undefined,
|
||||
subGroupBy?: TIssueGroupByOptions
|
||||
) => {
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { updateIssue } = useIssuesActions(storeType);
|
||||
const {
|
||||
issues: { getIssueIds, addCycleToIssue, removeCycleFromIssue, changeModulesInIssue },
|
||||
} = useIssues(storeType);
|
||||
|
||||
/**
|
||||
* update Issue on Drop, checks if modules or cycles are changed and then calls appropriate functions
|
||||
* @param projectId
|
||||
* @param issueId
|
||||
* @param data
|
||||
* @param issueUpdates
|
||||
*/
|
||||
const updateIssueOnDrop = async (
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
data: Partial<TIssue>,
|
||||
issueUpdates: {
|
||||
[groupKey: string]: {
|
||||
ADD: string[];
|
||||
REMOVE: string[];
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const errorToastProps = {
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Error while updating work item",
|
||||
};
|
||||
const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"];
|
||||
const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"];
|
||||
|
||||
const isModuleChanged = Object.keys(data).includes(moduleKey);
|
||||
const isCycleChanged = Object.keys(data).includes(cycleKey);
|
||||
|
||||
if (isCycleChanged && workspaceSlug) {
|
||||
if (data[cycleKey]) {
|
||||
addCycleToIssue(workspaceSlug.toString(), projectId, data[cycleKey]?.toString() ?? "", issueId).catch(() =>
|
||||
setToast(errorToastProps)
|
||||
);
|
||||
} else {
|
||||
removeCycleFromIssue(workspaceSlug.toString(), projectId, issueId).catch(() => setToast(errorToastProps));
|
||||
}
|
||||
delete data[cycleKey];
|
||||
}
|
||||
|
||||
if (isModuleChanged && workspaceSlug && issueUpdates[moduleKey]) {
|
||||
changeModulesInIssue(
|
||||
workspaceSlug.toString(),
|
||||
projectId,
|
||||
issueId,
|
||||
issueUpdates[moduleKey].ADD,
|
||||
issueUpdates[moduleKey].REMOVE
|
||||
).catch(() => setToast(errorToastProps));
|
||||
delete data[moduleKey];
|
||||
}
|
||||
|
||||
updateIssue && updateIssue(projectId, issueId, data).catch(() => setToast(errorToastProps));
|
||||
};
|
||||
|
||||
const handleOnDrop = async (source: GroupDropLocation, destination: GroupDropLocation) => {
|
||||
if (
|
||||
source.columnId &&
|
||||
destination.columnId &&
|
||||
destination.columnId === source.columnId &&
|
||||
destination.id === source.id
|
||||
)
|
||||
return;
|
||||
|
||||
await handleGroupDragDrop(
|
||||
source,
|
||||
destination,
|
||||
getIssueById,
|
||||
getIssueIds,
|
||||
updateIssueOnDrop,
|
||||
groupBy,
|
||||
subGroupBy,
|
||||
orderBy !== "sort_order"
|
||||
).catch((err) => {
|
||||
setToast({
|
||||
title: "Error!",
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: err?.detail ?? "Failed to perform this action",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return handleOnDrop;
|
||||
};
|
||||
62
apps/web/core/hooks/use-integration-popup.tsx
Normal file
62
apps/web/core/hooks/use-integration-popup.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
const useIntegrationPopup = ({
|
||||
provider,
|
||||
stateParams,
|
||||
github_app_name,
|
||||
slack_client_id,
|
||||
}: {
|
||||
provider: string | undefined;
|
||||
stateParams?: string;
|
||||
github_app_name?: string;
|
||||
slack_client_id?: string;
|
||||
}) => {
|
||||
const [authLoader, setAuthLoader] = useState(false);
|
||||
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
|
||||
const providerUrls: { [key: string]: string } = {
|
||||
github: `https://github.com/apps/${github_app_name}/installations/new?state=${workspaceSlug?.toString()}`,
|
||||
slack: `https://slack.com/oauth/v2/authorize?scope=chat:write,im:history,im:write,links:read,links:write,users:read,users:read.email&user_scope=&&client_id=${slack_client_id}&state=${workspaceSlug?.toString()}`,
|
||||
slackChannel: `https://slack.com/oauth/v2/authorize?scope=incoming-webhook&client_id=${slack_client_id}&state=${workspaceSlug?.toString()},${projectId?.toString()}${
|
||||
stateParams ? "," + stateParams : ""
|
||||
}`,
|
||||
};
|
||||
|
||||
const popup = useRef<any>();
|
||||
|
||||
const checkPopup = () => {
|
||||
const check = setInterval(() => {
|
||||
if (!popup || popup.current.closed || popup.current.closed === undefined) {
|
||||
clearInterval(check);
|
||||
setAuthLoader(false);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const openPopup = () => {
|
||||
if (!provider) return;
|
||||
|
||||
const width = 600,
|
||||
height = 600;
|
||||
const left = window.innerWidth / 2 - width / 2;
|
||||
const top = window.innerHeight / 2 - height / 2;
|
||||
const url = providerUrls[provider];
|
||||
|
||||
return window.open(url, "", `width=${width}, height=${height}, top=${top}, left=${left}`);
|
||||
};
|
||||
|
||||
const startAuth = () => {
|
||||
popup.current = openPopup();
|
||||
checkPopup();
|
||||
setAuthLoader(true);
|
||||
};
|
||||
|
||||
return {
|
||||
startAuth,
|
||||
isConnecting: authLoader,
|
||||
};
|
||||
};
|
||||
|
||||
export default useIntegrationPopup;
|
||||
42
apps/web/core/hooks/use-intersection-observer.ts
Normal file
42
apps/web/core/hooks/use-intersection-observer.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { RefObject } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export type UseIntersectionObserverProps = {
|
||||
containerRef: RefObject<HTMLDivElement | null> | undefined;
|
||||
elementRef: HTMLElement | null;
|
||||
callback: () => void;
|
||||
rootMargin?: string;
|
||||
};
|
||||
|
||||
export const useIntersectionObserver = (
|
||||
containerRef: RefObject<HTMLDivElement | null>,
|
||||
elementRef: HTMLElement | null,
|
||||
callback: (() => void) | undefined,
|
||||
rootMargin?: string
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (elementRef) {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[entries.length - 1].isIntersecting) {
|
||||
callback && callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: containerRef?.current,
|
||||
rootMargin,
|
||||
}
|
||||
);
|
||||
observer.observe(elementRef);
|
||||
return () => {
|
||||
if (elementRef) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
observer.unobserve(elementRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
// When i am passing callback as a dependency, it is causing infinite loop,
|
||||
// Please make sure you fix this eslint lint disable error with caution
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rootMargin, callback, elementRef, containerRef.current]);
|
||||
};
|
||||
43
apps/web/core/hooks/use-issue-layout-store.ts
Normal file
43
apps/web/core/hooks/use-issue-layout-store.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import { useIssues } from "./store/use-issues";
|
||||
|
||||
export const IssuesStoreContext = createContext<EIssuesStoreType | undefined>(undefined);
|
||||
|
||||
export const useIssueStoreType = () => {
|
||||
const storeType = useContext(IssuesStoreContext);
|
||||
const { globalViewId, viewId, projectId, cycleId, moduleId, userId, epicId, teamspaceId } = useParams();
|
||||
|
||||
// If store type exists in context, use that store type
|
||||
if (storeType) return storeType;
|
||||
|
||||
// else check the router params to determine the issue store
|
||||
if (globalViewId) return EIssuesStoreType.GLOBAL;
|
||||
|
||||
if (userId) return EIssuesStoreType.PROFILE;
|
||||
|
||||
if (teamspaceId && viewId) return EIssuesStoreType.TEAM_VIEW;
|
||||
|
||||
if (teamspaceId && projectId) return EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS;
|
||||
|
||||
if (viewId) return EIssuesStoreType.PROJECT_VIEW;
|
||||
|
||||
if (cycleId) return EIssuesStoreType.CYCLE;
|
||||
|
||||
if (moduleId) return EIssuesStoreType.MODULE;
|
||||
|
||||
if (epicId) return EIssuesStoreType.EPIC;
|
||||
|
||||
if (projectId) return EIssuesStoreType.PROJECT;
|
||||
|
||||
if (teamspaceId) return EIssuesStoreType.TEAM;
|
||||
|
||||
return EIssuesStoreType.PROJECT;
|
||||
};
|
||||
|
||||
export const useIssuesStore = () => {
|
||||
const storeType = useIssueStoreType();
|
||||
|
||||
return useIssues(storeType);
|
||||
};
|
||||
51
apps/web/core/hooks/use-issue-peek-overview-redirection.tsx
Normal file
51
apps/web/core/hooks/use-issue-peek-overview-redirection.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssueServiceType } from "@plane/types";
|
||||
// helpers
|
||||
import { generateWorkItemLink } from "@plane/utils";
|
||||
// hooks
|
||||
import { useIssueDetail } from "./store/use-issue-detail";
|
||||
import { useProject } from "./store/use-project";
|
||||
|
||||
const useIssuePeekOverviewRedirection = (isEpic: boolean = false) => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(
|
||||
isEpic ? EIssueServiceType.EPICS : EIssueServiceType.ISSUES
|
||||
);
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
|
||||
const handleRedirection = (
|
||||
workspaceSlug: string | undefined,
|
||||
issue: TIssue | undefined,
|
||||
isMobile = false,
|
||||
nestingLevel?: number
|
||||
) => {
|
||||
if (!issue) return;
|
||||
const { project_id, id, archived_at, tempId } = issue;
|
||||
const projectIdentifier = getProjectIdentifierById(issue?.project_id);
|
||||
|
||||
const workItemLink = generateWorkItemLink({
|
||||
workspaceSlug,
|
||||
projectId: project_id,
|
||||
issueId: id,
|
||||
projectIdentifier,
|
||||
sequenceId: issue?.sequence_id,
|
||||
isEpic,
|
||||
isArchived: !!archived_at,
|
||||
});
|
||||
if (workspaceSlug && project_id && id && !getIsIssuePeeked(id) && !tempId) {
|
||||
if (isMobile) {
|
||||
router.push(workItemLink);
|
||||
} else {
|
||||
setPeekIssue({ workspaceSlug, projectId: project_id, issueId: id, nestingLevel, isArchived: !!archived_at });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { handleRedirection };
|
||||
};
|
||||
|
||||
export default useIssuePeekOverviewRedirection;
|
||||
815
apps/web/core/hooks/use-issues-actions.tsx
Normal file
815
apps/web/core/hooks/use-issues-actions.tsx
Normal file
@@ -0,0 +1,815 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useCallback, useMemo } from "react";
|
||||
// types
|
||||
import { useParams } from "next/navigation";
|
||||
import type { TSupportedFilterTypeForUpdate } from "@plane/constants";
|
||||
import { EDraftIssuePaginationType } from "@plane/constants";
|
||||
import type {
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
IssuePaginationOptions,
|
||||
TIssue,
|
||||
TIssuesResponse,
|
||||
TLoader,
|
||||
TProfileViews,
|
||||
TSupportedFilterForUpdate,
|
||||
} from "@plane/types";
|
||||
import { EIssuesStoreType } from "@plane/types";
|
||||
import {
|
||||
useTeamIssueActions,
|
||||
useTeamProjectWorkItemsActions,
|
||||
useTeamViewIssueActions,
|
||||
} from "@/plane-web/helpers/issue-action-helper";
|
||||
import { useIssues } from "./store/use-issues";
|
||||
|
||||
export interface IssueActions {
|
||||
fetchIssues: (
|
||||
loadType: TLoader,
|
||||
options: IssuePaginationOptions,
|
||||
viewId?: string
|
||||
) => Promise<TIssuesResponse | undefined>;
|
||||
fetchNextIssues: (groupId?: string, subGroupId?: string) => Promise<TIssuesResponse | undefined>;
|
||||
removeIssue: (projectId: string | undefined | null, issueId: string) => Promise<void>;
|
||||
createIssue?: (projectId: string | undefined | null, data: Partial<TIssue>) => Promise<TIssue | undefined>;
|
||||
quickAddIssue?: (projectId: string | undefined | null, data: TIssue) => Promise<TIssue | undefined>;
|
||||
updateIssue?: (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
removeIssueFromView?: (projectId: string | undefined | null, issueId: string) => Promise<void>;
|
||||
archiveIssue?: (projectId: string | undefined | null, issueId: string) => Promise<void>;
|
||||
restoreIssue?: (projectId: string | undefined | null, issueId: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
projectId: string,
|
||||
filterType: TSupportedFilterTypeForUpdate,
|
||||
filters: TSupportedFilterForUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
||||
const teamIssueActions = useTeamIssueActions();
|
||||
const projectIssueActions = useProjectIssueActions();
|
||||
const projectEpicsActions = useProjectEpicsActions();
|
||||
const cycleIssueActions = useCycleIssueActions();
|
||||
const moduleIssueActions = useModuleIssueActions();
|
||||
const teamViewIssueActions = useTeamViewIssueActions();
|
||||
const projectViewIssueActions = useProjectViewIssueActions();
|
||||
const globalIssueActions = useGlobalIssueActions();
|
||||
const profileIssueActions = useProfileIssueActions();
|
||||
const archivedIssueActions = useArchivedIssueActions();
|
||||
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
|
||||
const teamProjectWorkItemsActions = useTeamProjectWorkItemsActions();
|
||||
|
||||
switch (storeType) {
|
||||
case EIssuesStoreType.TEAM_VIEW:
|
||||
return teamViewIssueActions;
|
||||
case EIssuesStoreType.PROJECT_VIEW:
|
||||
return projectViewIssueActions;
|
||||
case EIssuesStoreType.PROFILE:
|
||||
return profileIssueActions;
|
||||
case EIssuesStoreType.TEAM:
|
||||
return teamIssueActions;
|
||||
case EIssuesStoreType.ARCHIVED:
|
||||
return archivedIssueActions;
|
||||
case EIssuesStoreType.CYCLE:
|
||||
return cycleIssueActions;
|
||||
case EIssuesStoreType.MODULE:
|
||||
return moduleIssueActions;
|
||||
case EIssuesStoreType.GLOBAL:
|
||||
return globalIssueActions;
|
||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||
//@ts-expect-error type mismatch
|
||||
return workspaceDraftIssueActions;
|
||||
case EIssuesStoreType.EPIC:
|
||||
return projectEpicsActions;
|
||||
case EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS:
|
||||
return teamProjectWorkItemsActions;
|
||||
case EIssuesStoreType.PROJECT:
|
||||
default:
|
||||
return projectIssueActions;
|
||||
}
|
||||
};
|
||||
|
||||
const useProjectIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.createIssue, workspaceSlug]
|
||||
);
|
||||
const quickAddIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: TIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.quickAddIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.quickAddIssue, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
[issuesFilter.updateFilters, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, createIssue, quickAddIssue, updateIssue, removeIssue, archiveIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useProjectEpicsActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.EPIC);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.createIssue, workspaceSlug]
|
||||
);
|
||||
const quickAddIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: TIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.quickAddIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.quickAddIssue, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
[issuesFilter.updateFilters, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, createIssue, quickAddIssue, updateIssue, removeIssue, archiveIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useCycleIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, cycleId: routerCycleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
const cycleId = routerCycleId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions, cycleId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options, cycleId.toString());
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !cycleId) return;
|
||||
return issues.fetchNextIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
cycleId.toString(),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId, cycleId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!cycleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data, cycleId);
|
||||
},
|
||||
[issues.createIssue, cycleId, workspaceSlug]
|
||||
);
|
||||
const quickAddIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: TIssue) => {
|
||||
if (!cycleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.quickAddIssue(workspaceSlug, projectId, data, cycleId);
|
||||
},
|
||||
[issues.quickAddIssue, workspaceSlug, cycleId]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssueFromView = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!cycleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||
},
|
||||
[issues.removeIssueFromCycle, cycleId, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!cycleId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, cycleId);
|
||||
},
|
||||
[issuesFilter.updateFilters, cycleId, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const useModuleIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, moduleId: routerModuleId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
const moduleId = routerModuleId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions, moduleId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options, moduleId.toString());
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
return issues.fetchNextIssues(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
moduleId.toString(),
|
||||
groupId,
|
||||
subGroupId
|
||||
);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId, moduleId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!moduleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data, moduleId);
|
||||
},
|
||||
[issues.createIssue, moduleId, workspaceSlug]
|
||||
);
|
||||
const quickAddIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: TIssue) => {
|
||||
if (!moduleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.quickAddIssue(workspaceSlug, projectId, data, moduleId);
|
||||
},
|
||||
[issues.quickAddIssue, workspaceSlug, moduleId]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssueFromView = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!moduleId || !workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssuesFromModule(workspaceSlug, projectId, moduleId, [issueId]);
|
||||
},
|
||||
[issues.removeIssuesFromModule, moduleId, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, moduleId, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!moduleId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, moduleId);
|
||||
},
|
||||
[issuesFilter.updateFilters, moduleId]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeIssueFromView,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, createIssue, updateIssue, removeIssue, removeIssueFromView, archiveIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useProfileIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, userId: routerUserId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const userId = routerUserId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROFILE);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions, viewId?: string) => {
|
||||
if (!workspaceSlug || !userId || !viewId) return;
|
||||
return issues.fetchIssues(
|
||||
workspaceSlug.toString(),
|
||||
userId.toString(),
|
||||
loadType,
|
||||
options,
|
||||
viewId as TProfileViews
|
||||
);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, userId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), userId.toString(), groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, userId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.createIssue, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!userId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, userId);
|
||||
},
|
||||
[issuesFilter.updateFilters, userId, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, createIssue, updateIssue, removeIssue, archiveIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useProjectViewIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId, viewId: routerViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
const viewId = routerViewId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions, viewId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), viewId, loadType, options);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId || !viewId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), viewId, groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.createIssue, workspaceSlug]
|
||||
);
|
||||
const quickAddIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: TIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.quickAddIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.quickAddIssue, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
const archiveIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.archiveIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!viewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, viewId);
|
||||
},
|
||||
[issuesFilter.updateFilters, viewId, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
quickAddIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, createIssue, quickAddIssue, updateIssue, removeIssue, archiveIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useArchivedIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const projectId = routerProjectId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, projectId]
|
||||
);
|
||||
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue]
|
||||
);
|
||||
const restoreIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.restoreIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.restoreIssue]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
|
||||
},
|
||||
[issuesFilter.updateFilters]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
removeIssue,
|
||||
restoreIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, removeIssue, restoreIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useGlobalIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const globalViewId = routerGlobalViewId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.GLOBAL);
|
||||
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), globalViewId.toString(), loadType, options);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, globalViewId]
|
||||
);
|
||||
const fetchNextIssues = useCallback(
|
||||
async (groupId?: string, subGroupId?: string) => {
|
||||
if (!workspaceSlug || !globalViewId) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString(), globalViewId.toString(), groupId, subGroupId);
|
||||
},
|
||||
[issues.fetchIssues, workspaceSlug, globalViewId]
|
||||
);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, projectId, data);
|
||||
},
|
||||
[issues.createIssue, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
},
|
||||
[issues.updateIssue, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
},
|
||||
[issues.removeIssue, workspaceSlug]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
if (!globalViewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters, globalViewId);
|
||||
},
|
||||
[issuesFilter.updateFilters, globalViewId, workspaceSlug]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[createIssue, updateIssue, removeIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useWorkspaceDraftIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const globalViewId = routerGlobalViewId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, _options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), loadType, EDraftIssuePaginationType.INIT);
|
||||
},
|
||||
[workspaceSlug, issues]
|
||||
);
|
||||
|
||||
const fetchNextIssues = useCallback(async () => {
|
||||
if (!workspaceSlug) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), "pagination", EDraftIssuePaginationType.NEXT);
|
||||
}, [workspaceSlug, issues]);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createIssue(workspaceSlug, data);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateIssue(workspaceSlug, issueId, data);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.removeIssue(issueId);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
|
||||
// const moveToIssue = useCallback(
|
||||
// async (workspaceSlug: string, issueId: string, data: Partial<TIssue>) => {
|
||||
// if (!workspaceSlug || !issueId || !data) return;
|
||||
// return await issues.moveToIssues(workspaceSlug, issueId, data);
|
||||
// },
|
||||
// [issues]
|
||||
// );
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (projectId: string, filterType: TSupportedFilterTypeForUpdate, filters: TSupportedFilterForUpdate) => {
|
||||
filters = filters as IIssueDisplayFilterOptions | IIssueDisplayProperties;
|
||||
if (!globalViewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, filterType, filters);
|
||||
},
|
||||
[globalViewId, workspaceSlug, issuesFilter]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
updateFilters,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, createIssue, updateIssue, removeIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
19
apps/web/core/hooks/use-keypress.tsx
Normal file
19
apps/web/core/hooks/use-keypress.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useKeypress = (key: string, callback: (event: KeyboardEvent) => void) => {
|
||||
useEffect(() => {
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === key) {
|
||||
callback(event);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
};
|
||||
}, [key, callback]);
|
||||
};
|
||||
|
||||
export default useKeypress;
|
||||
58
apps/web/core/hooks/use-local-storage.tsx
Normal file
58
apps/web/core/hooks/use-local-storage.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export const getValueFromLocalStorage = (key: string, defaultValue: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return defaultValue;
|
||||
try {
|
||||
const item = window.localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
window.localStorage.removeItem(key);
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const setValueIntoLocalStorage = (key: string, value: any) => {
|
||||
if (typeof window === undefined || typeof window === "undefined") return false;
|
||||
try {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Remove this once we migrate to the new hooks from plane/helpers
|
||||
const useLocalStorage = <T,>(key: string, initialValue: T) => {
|
||||
const [storedValue, setStoredValue] = useState<T | null>(() => getValueFromLocalStorage(key, initialValue));
|
||||
|
||||
const setValue = useCallback(
|
||||
(value: T) => {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
setStoredValue(value);
|
||||
window.dispatchEvent(new Event(`local-storage:${key}`));
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const clearValue = useCallback(() => {
|
||||
window.localStorage.removeItem(key);
|
||||
setStoredValue(null);
|
||||
window.dispatchEvent(new Event(`local-storage:${key}`));
|
||||
}, [key]);
|
||||
|
||||
const reHydrate = useCallback(() => {
|
||||
const data = getValueFromLocalStorage(key, initialValue);
|
||||
setStoredValue(data);
|
||||
}, [key, initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(`local-storage:${key}`, reHydrate);
|
||||
return () => {
|
||||
window.removeEventListener(`local-storage:${key}`, reHydrate);
|
||||
};
|
||||
}, [key, reHydrate]);
|
||||
|
||||
return { storedValue, setValue, clearValue } as const;
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
407
apps/web/core/hooks/use-multiple-select.ts
Normal file
407
apps/web/core/hooks/use-multiple-select.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
// hooks
|
||||
import { useMultipleSelectStore } from "@/hooks/store/use-multiple-select-store";
|
||||
//
|
||||
import useReloadConfirmations from "./use-reload-confirmation";
|
||||
|
||||
export type TEntityDetails = {
|
||||
entityID: string;
|
||||
groupID: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
containerRef: React.MutableRefObject<HTMLElement | null>;
|
||||
disabled: boolean;
|
||||
entities: Record<string, string[]>; // { groupID: entityIds[] }
|
||||
};
|
||||
|
||||
export type TSelectionSnapshot = {
|
||||
isSelectionActive: boolean;
|
||||
selectedEntityIds: string[];
|
||||
};
|
||||
|
||||
export type TSelectionHelper = {
|
||||
handleClearSelection: () => void;
|
||||
handleEntityClick: (event: React.MouseEvent, entityID: string, groupId: string) => void;
|
||||
getIsEntitySelected: (entityID: string) => boolean;
|
||||
getIsEntityActive: (entityID: string) => boolean;
|
||||
handleGroupClick: (groupID: string) => void;
|
||||
isGroupSelected: (groupID: string) => "empty" | "partial" | "complete";
|
||||
isSelectionDisabled: boolean;
|
||||
};
|
||||
|
||||
export const useMultipleSelect = (props: Props) => {
|
||||
const { containerRef, disabled, entities } = props;
|
||||
// router
|
||||
// const router = useAppRouter();
|
||||
// store hooks
|
||||
const {
|
||||
selectedEntityIds,
|
||||
updateSelectedEntityDetails,
|
||||
bulkUpdateSelectedEntityDetails,
|
||||
getActiveEntityDetails,
|
||||
updateActiveEntityDetails,
|
||||
getPreviousActiveEntity,
|
||||
updatePreviousActiveEntity,
|
||||
getNextActiveEntity,
|
||||
updateNextActiveEntity,
|
||||
getLastSelectedEntityDetails,
|
||||
clearSelection,
|
||||
getIsEntitySelected,
|
||||
getIsEntityActive,
|
||||
getEntityDetailsFromEntityID,
|
||||
} = useMultipleSelectStore();
|
||||
|
||||
useReloadConfirmations(
|
||||
selectedEntityIds && selectedEntityIds.length > 0,
|
||||
"Are you sure you want to leave? Your current bulk operation selections will be lost.",
|
||||
true,
|
||||
() => {
|
||||
clearSelection();
|
||||
}
|
||||
);
|
||||
|
||||
const groups = useMemo(() => Object.keys(entities), [entities]);
|
||||
|
||||
const entitiesList: TEntityDetails[] = useMemo(
|
||||
() =>
|
||||
groups
|
||||
?.map((groupID) =>
|
||||
entities?.[groupID]?.map((entityID) => ({
|
||||
entityID,
|
||||
groupID,
|
||||
}))
|
||||
)
|
||||
.flat(1),
|
||||
[entities, groups]
|
||||
);
|
||||
|
||||
const getPreviousAndNextEntities = useCallback(
|
||||
(entityID: string) => {
|
||||
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
||||
|
||||
// entity position
|
||||
const isFirstEntity = currentEntityIndex === 0;
|
||||
const isLastEntity = currentEntityIndex === entitiesList.length - 1;
|
||||
|
||||
let previousEntity: TEntityDetails | null = null;
|
||||
let nextEntity: TEntityDetails | null = null;
|
||||
|
||||
if (isLastEntity) {
|
||||
nextEntity = null;
|
||||
} else {
|
||||
nextEntity = entitiesList[currentEntityIndex + 1];
|
||||
}
|
||||
|
||||
if (isFirstEntity) {
|
||||
previousEntity = null;
|
||||
} else {
|
||||
previousEntity = entitiesList[currentEntityIndex - 1];
|
||||
}
|
||||
|
||||
return {
|
||||
previousEntity,
|
||||
nextEntity,
|
||||
};
|
||||
},
|
||||
[entitiesList]
|
||||
);
|
||||
|
||||
const handleActiveEntityChange = useCallback(
|
||||
(entityDetails: TEntityDetails | null, shouldScroll: boolean = true) => {
|
||||
if (disabled) return;
|
||||
|
||||
if (!entityDetails) {
|
||||
updateActiveEntityDetails(null);
|
||||
updatePreviousActiveEntity(null);
|
||||
updateNextActiveEntity(null);
|
||||
return;
|
||||
}
|
||||
|
||||
updateActiveEntityDetails(entityDetails);
|
||||
|
||||
// scroll to get the active element in view
|
||||
const activeElement = document.querySelector(
|
||||
`[data-entity-id="${entityDetails.entityID}"][data-entity-group-id="${entityDetails.groupID}"]`
|
||||
);
|
||||
if (activeElement && containerRef.current && shouldScroll) {
|
||||
const SCROLL_OFFSET = 200;
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const elementRect = activeElement.getBoundingClientRect();
|
||||
|
||||
const isInView =
|
||||
elementRect.top >= containerRect.top + SCROLL_OFFSET &&
|
||||
elementRect.bottom <= containerRect.bottom - SCROLL_OFFSET;
|
||||
|
||||
if (!isInView) {
|
||||
containerRef.current.scrollBy({
|
||||
top: elementRect.top < containerRect.top + SCROLL_OFFSET ? -50 : 50,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { previousEntity: previousActiveEntity, nextEntity: nextActiveEntity } = getPreviousAndNextEntities(
|
||||
entityDetails.entityID
|
||||
);
|
||||
updatePreviousActiveEntity(previousActiveEntity);
|
||||
updateNextActiveEntity(nextActiveEntity);
|
||||
},
|
||||
[
|
||||
containerRef,
|
||||
disabled,
|
||||
getPreviousAndNextEntities,
|
||||
updateActiveEntityDetails,
|
||||
updateNextActiveEntity,
|
||||
updatePreviousActiveEntity,
|
||||
]
|
||||
);
|
||||
|
||||
const handleEntitySelection = useCallback(
|
||||
(
|
||||
entityDetails: TEntityDetails | TEntityDetails[],
|
||||
shouldScroll: boolean = true,
|
||||
forceAction: "force-add" | "force-remove" | null = null
|
||||
) => {
|
||||
if (disabled) return;
|
||||
|
||||
if (Array.isArray(entityDetails)) {
|
||||
bulkUpdateSelectedEntityDetails(entityDetails, forceAction === "force-add" ? "add" : "remove");
|
||||
if (forceAction === "force-add" && entityDetails.length > 0) {
|
||||
handleActiveEntityChange(entityDetails[entityDetails.length - 1], shouldScroll);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceAction) {
|
||||
if (forceAction === "force-add") {
|
||||
console.log("force adding");
|
||||
updateSelectedEntityDetails(entityDetails, "add");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
}
|
||||
if (forceAction === "force-remove") {
|
||||
updateSelectedEntityDetails(entityDetails, "remove");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = getIsEntitySelected(entityDetails.entityID);
|
||||
if (isSelected) {
|
||||
updateSelectedEntityDetails(entityDetails, "remove");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
} else {
|
||||
updateSelectedEntityDetails(entityDetails, "add");
|
||||
handleActiveEntityChange(entityDetails, shouldScroll);
|
||||
}
|
||||
},
|
||||
[
|
||||
bulkUpdateSelectedEntityDetails,
|
||||
disabled,
|
||||
getIsEntitySelected,
|
||||
handleActiveEntityChange,
|
||||
updateSelectedEntityDetails,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description toggle entity selection
|
||||
* @param {React.MouseEvent} event
|
||||
* @param {string} entityID
|
||||
* @param {string} groupID
|
||||
*/
|
||||
const handleEntityClick = useCallback(
|
||||
(e: React.MouseEvent, entityID: string, groupID: string) => {
|
||||
if (disabled) return;
|
||||
const lastSelectedEntityDetails = getLastSelectedEntityDetails();
|
||||
if (e.shiftKey && lastSelectedEntityDetails) {
|
||||
const currentEntityIndex = entitiesList.findIndex((entity) => entity?.entityID === entityID);
|
||||
|
||||
const lastEntityIndex = entitiesList.findIndex(
|
||||
(entity) => entity?.entityID === lastSelectedEntityDetails.entityID
|
||||
);
|
||||
if (lastEntityIndex < currentEntityIndex) {
|
||||
for (let i = lastEntityIndex + 1; i <= currentEntityIndex; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
} else if (lastEntityIndex > currentEntityIndex) {
|
||||
for (let i = currentEntityIndex; i <= lastEntityIndex - 1; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const startIndex = lastEntityIndex + 1;
|
||||
const endIndex = currentEntityIndex;
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const entityDetails = entitiesList[i];
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handleEntitySelection({ entityID, groupID }, false);
|
||||
},
|
||||
[disabled, entitiesList, handleEntitySelection, getLastSelectedEntityDetails]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description check if any entity of the group is selected
|
||||
* @param {string} groupID
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isGroupSelected = useCallback(
|
||||
(groupID: string) => {
|
||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
||||
const totalSelected = groupEntities.filter((entity) => getIsEntitySelected(entity?.entityID ?? "")).length;
|
||||
if (totalSelected === 0) return "empty";
|
||||
if (totalSelected === groupEntities.length) return "complete";
|
||||
return "partial";
|
||||
},
|
||||
[entitiesList, getIsEntitySelected]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description toggle group selection
|
||||
* @param {string} groupID
|
||||
*/
|
||||
const handleGroupClick = useCallback(
|
||||
(groupID: string) => {
|
||||
if (disabled) return;
|
||||
|
||||
const groupEntities = entitiesList.filter((entity) => entity.groupID === groupID);
|
||||
const groupSelectionStatus = isGroupSelected(groupID);
|
||||
handleEntitySelection(groupEntities, false, groupSelectionStatus === "empty" ? "force-add" : "force-remove");
|
||||
},
|
||||
[disabled, entitiesList, handleEntitySelection, isGroupSelected]
|
||||
);
|
||||
|
||||
// select entities on shift + arrow up/down key press
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (!e.shiftKey) return;
|
||||
|
||||
const activeEntityDetails = getActiveEntityDetails();
|
||||
const nextActiveEntity = getNextActiveEntity();
|
||||
const previousActiveEntity = getPreviousActiveEntity();
|
||||
|
||||
if (e.key === "ArrowDown" && activeEntityDetails) {
|
||||
if (!nextActiveEntity) return;
|
||||
handleEntitySelection(nextActiveEntity);
|
||||
}
|
||||
if (e.key === "ArrowUp" && activeEntityDetails) {
|
||||
if (!previousActiveEntity) return;
|
||||
handleEntitySelection(previousActiveEntity);
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [
|
||||
disabled,
|
||||
getActiveEntityDetails,
|
||||
handleEntitySelection,
|
||||
getLastSelectedEntityDetails,
|
||||
getNextActiveEntity,
|
||||
getPreviousActiveEntity,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey) return;
|
||||
const activeEntityDetails = getActiveEntityDetails();
|
||||
// set active entity id to the first entity
|
||||
if (["ArrowUp", "ArrowDown"].includes(e.key) && !activeEntityDetails) {
|
||||
const firstElementDetails = entitiesList[0];
|
||||
if (!firstElementDetails) return;
|
||||
handleActiveEntityChange(firstElementDetails);
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown" && activeEntityDetails) {
|
||||
if (!activeEntityDetails) return;
|
||||
const { nextEntity: nextActiveEntity } = getPreviousAndNextEntities(activeEntityDetails.entityID);
|
||||
if (nextActiveEntity) {
|
||||
handleActiveEntityChange(nextActiveEntity);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp" && activeEntityDetails) {
|
||||
if (!activeEntityDetails) return;
|
||||
const { previousEntity: previousActiveEntity } = getPreviousAndNextEntities(activeEntityDetails.entityID);
|
||||
if (previousActiveEntity) {
|
||||
handleActiveEntityChange(previousActiveEntity);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [disabled, getActiveEntityDetails, entitiesList, groups, getPreviousAndNextEntities, handleActiveEntityChange]);
|
||||
|
||||
// clear selection on route change
|
||||
// useEffect(() => {
|
||||
// const handleRouteChange = () => clearSelection();
|
||||
|
||||
// router.events.on("routeChangeComplete", handleRouteChange);
|
||||
|
||||
// return () => {
|
||||
// router.events.off("routeChangeComplete", handleRouteChange);
|
||||
// };
|
||||
// }, [clearSelection, router.events]);
|
||||
|
||||
// when entities list change, remove entityIds from the selected entities array, which are not present in the new list
|
||||
useEffect(() => {
|
||||
if (disabled) return;
|
||||
selectedEntityIds.map((entityID) => {
|
||||
const isEntityPresent = entitiesList.find((en) => en?.entityID === entityID);
|
||||
if (!isEntityPresent) {
|
||||
const entityDetails = getEntityDetailsFromEntityID(entityID);
|
||||
if (entityDetails) {
|
||||
handleEntitySelection(entityDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [disabled, entitiesList, getEntityDetailsFromEntityID, handleEntitySelection, selectedEntityIds]);
|
||||
|
||||
/**
|
||||
* @description helper functions for selection
|
||||
*/
|
||||
const helpers: TSelectionHelper = useMemo(
|
||||
() => ({
|
||||
handleClearSelection: clearSelection,
|
||||
handleEntityClick,
|
||||
getIsEntitySelected,
|
||||
getIsEntityActive,
|
||||
handleGroupClick,
|
||||
isGroupSelected,
|
||||
isSelectionDisabled: disabled,
|
||||
}),
|
||||
[
|
||||
clearSelection,
|
||||
disabled,
|
||||
getIsEntityActive,
|
||||
getIsEntitySelected,
|
||||
handleEntityClick,
|
||||
handleGroupClick,
|
||||
isGroupSelected,
|
||||
]
|
||||
);
|
||||
|
||||
return helpers;
|
||||
};
|
||||
22
apps/web/core/hooks/use-online-status.ts
Normal file
22
apps/web/core/hooks/use-online-status.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useOnlineStatus = () => {
|
||||
// states
|
||||
const [isOnline, setIsOnline] = useState(typeof navigator !== "undefined" ? navigator.onLine : true);
|
||||
|
||||
const updateOnlineStatus = () => setIsOnline(navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("online", updateOnlineStatus);
|
||||
window.addEventListener("offline", updateOnlineStatus);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("online", updateOnlineStatus);
|
||||
window.removeEventListener("offline", updateOnlineStatus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isOnline };
|
||||
};
|
||||
|
||||
export default useOnlineStatus;
|
||||
56
apps/web/core/hooks/use-page-fallback.ts
Normal file
56
apps/web/core/hooks/use-page-fallback.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
// plane editor
|
||||
import { getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
// plane types
|
||||
import type { TDocumentPayload } from "@plane/types";
|
||||
// hooks
|
||||
import useAutoSave from "@/hooks/use-auto-save";
|
||||
|
||||
type TArgs = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
fetchPageDescription: () => Promise<ArrayBuffer>;
|
||||
hasConnectionFailed: boolean;
|
||||
updatePageDescription: (data: TDocumentPayload) => Promise<void>;
|
||||
};
|
||||
|
||||
export const usePageFallback = (args: TArgs) => {
|
||||
const { editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription } = args;
|
||||
|
||||
const handleUpdateDescription = useCallback(async () => {
|
||||
if (!hasConnectionFailed) return;
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
|
||||
try {
|
||||
const latestEncodedDescription = await fetchPageDescription();
|
||||
let latestDecodedDescription: Uint8Array;
|
||||
if (latestEncodedDescription && latestEncodedDescription.byteLength > 0) {
|
||||
latestDecodedDescription = new Uint8Array(latestEncodedDescription);
|
||||
} else {
|
||||
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString("<p></p>");
|
||||
}
|
||||
|
||||
editor.setProviderDocument(latestDecodedDescription);
|
||||
const { binary, html, json } = editor.getDocument();
|
||||
if (!binary || !json) return;
|
||||
const encodedBinary = Buffer.from(binary).toString("base64");
|
||||
|
||||
await updatePageDescription({
|
||||
description_binary: encodedBinary,
|
||||
description_html: html,
|
||||
description: json,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in updating description using fallback logic:", error);
|
||||
}
|
||||
}, [editorRef, fetchPageDescription, hasConnectionFailed, updatePageDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasConnectionFailed) {
|
||||
handleUpdateDescription();
|
||||
}
|
||||
}, [handleUpdateDescription, hasConnectionFailed]);
|
||||
|
||||
useAutoSave(handleUpdateDescription);
|
||||
};
|
||||
116
apps/web/core/hooks/use-page-filters.ts
Normal file
116
apps/web/core/hooks/use-page-filters.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
// plane editor
|
||||
import type { TEditorFontSize, TEditorFontStyle } from "@plane/editor";
|
||||
// hooks
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
|
||||
export type TPagesPersonalizationConfig = {
|
||||
full_width: boolean;
|
||||
font_size: TEditorFontSize;
|
||||
font_style: TEditorFontStyle;
|
||||
sticky_toolbar: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PERSONALIZATION_VALUES: TPagesPersonalizationConfig = {
|
||||
full_width: false,
|
||||
font_size: "large-font",
|
||||
font_style: "sans-serif",
|
||||
sticky_toolbar: true,
|
||||
};
|
||||
|
||||
export const usePageFilters = () => {
|
||||
// local storage
|
||||
const { storedValue: pagesConfig, setValue: setPagesConfig } = useLocalStorage<TPagesPersonalizationConfig>(
|
||||
"pages_config",
|
||||
DEFAULT_PERSONALIZATION_VALUES
|
||||
);
|
||||
// stored values
|
||||
const isFullWidth = useMemo(
|
||||
() => (pagesConfig?.full_width === undefined ? DEFAULT_PERSONALIZATION_VALUES.full_width : pagesConfig?.full_width),
|
||||
[pagesConfig?.full_width]
|
||||
);
|
||||
const isStickyToolbarEnabled = useMemo(
|
||||
() =>
|
||||
pagesConfig?.sticky_toolbar === undefined
|
||||
? DEFAULT_PERSONALIZATION_VALUES.sticky_toolbar
|
||||
: pagesConfig?.sticky_toolbar,
|
||||
[pagesConfig?.sticky_toolbar]
|
||||
);
|
||||
const fontSize = useMemo(
|
||||
() => pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size,
|
||||
[pagesConfig?.font_size]
|
||||
);
|
||||
const fontStyle = useMemo(
|
||||
() => pagesConfig?.font_style ?? DEFAULT_PERSONALIZATION_VALUES.font_style,
|
||||
[pagesConfig?.font_style]
|
||||
);
|
||||
// update action
|
||||
const handleUpdateConfig = useCallback(
|
||||
(payload: Partial<TPagesPersonalizationConfig>) => {
|
||||
setPagesConfig({
|
||||
...(pagesConfig ?? DEFAULT_PERSONALIZATION_VALUES),
|
||||
...payload,
|
||||
});
|
||||
},
|
||||
[pagesConfig, setPagesConfig]
|
||||
);
|
||||
/**
|
||||
* @description action to update full_width value
|
||||
* @param {boolean} value
|
||||
*/
|
||||
const handleFullWidth = useCallback(
|
||||
(value: boolean) => {
|
||||
handleUpdateConfig({
|
||||
full_width: value,
|
||||
});
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
/**
|
||||
* @description action to update font_size value
|
||||
* @param {TEditorFontSize} value
|
||||
*/
|
||||
const handleFontSize = useCallback(
|
||||
(value: TEditorFontSize) => {
|
||||
handleUpdateConfig({
|
||||
font_size: value,
|
||||
});
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
/**
|
||||
* @description action to update font_size value
|
||||
* @param {TEditorFontSize} value
|
||||
*/
|
||||
const handleFontStyle = useCallback(
|
||||
(value: TEditorFontStyle) => {
|
||||
handleUpdateConfig({
|
||||
font_style: value,
|
||||
});
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
/**
|
||||
* @description action to update full_width value
|
||||
* @param {boolean} value
|
||||
*/
|
||||
const handleStickyToolbar = useCallback(
|
||||
(value: boolean) => {
|
||||
handleUpdateConfig({
|
||||
sticky_toolbar: value,
|
||||
});
|
||||
},
|
||||
[handleUpdateConfig]
|
||||
);
|
||||
|
||||
return {
|
||||
fontSize,
|
||||
handleFontSize,
|
||||
fontStyle,
|
||||
handleFontStyle,
|
||||
isFullWidth,
|
||||
handleFullWidth,
|
||||
isStickyToolbarEnabled,
|
||||
handleStickyToolbar,
|
||||
};
|
||||
};
|
||||
310
apps/web/core/hooks/use-page-operations.ts
Normal file
310
apps/web/core/hooks/use-page-operations.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import { useMemo } from "react";
|
||||
// plane imports
|
||||
import { IS_FAVORITE_MENU_OPEN, PROJECT_PAGE_TRACKER_EVENTS } from "@plane/constants";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { EPageAccess } from "@plane/types";
|
||||
import { copyUrlToClipboard } from "@plane/utils";
|
||||
// helpers
|
||||
import { captureSuccess, captureError } from "@/helpers/event-tracker.helper";
|
||||
// hooks
|
||||
import { useCollaborativePageActions } from "@/hooks/use-collaborative-page-actions";
|
||||
// store types
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
// local storage
|
||||
import useLocalStorage from "./use-local-storage";
|
||||
|
||||
export type TPageOperations = {
|
||||
toggleLock: () => void;
|
||||
toggleAccess: () => void;
|
||||
toggleFavorite: () => void;
|
||||
openInNewTab: () => void;
|
||||
copyLink: () => void;
|
||||
duplicate: () => void;
|
||||
toggleArchive: () => void;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const usePageOperations = (
|
||||
props: Props
|
||||
): {
|
||||
pageOperations: TPageOperations;
|
||||
} => {
|
||||
const { page } = props;
|
||||
// derived values
|
||||
const {
|
||||
access,
|
||||
addToFavorites,
|
||||
archived_at,
|
||||
duplicate,
|
||||
is_favorite,
|
||||
is_locked,
|
||||
getRedirectionLink,
|
||||
removePageFromFavorites,
|
||||
} = page;
|
||||
// collaborative actions
|
||||
const { executeCollaborativeAction } = useCollaborativePageActions(props);
|
||||
// local storage
|
||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||
IS_FAVORITE_MENU_OPEN,
|
||||
false
|
||||
);
|
||||
// page operations
|
||||
const pageOperations: TPageOperations = useMemo(() => {
|
||||
const pageLink = getRedirectionLink();
|
||||
|
||||
return {
|
||||
copyLink: () => {
|
||||
copyUrlToClipboard(pageLink).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Link Copied!",
|
||||
message: "Page link copied to clipboard.",
|
||||
});
|
||||
});
|
||||
},
|
||||
duplicate: async () => {
|
||||
try {
|
||||
await duplicate();
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.duplicate,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page duplicated successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.duplicate,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be duplicated. Please try again later.",
|
||||
});
|
||||
}
|
||||
},
|
||||
move: async () => {},
|
||||
openInNewTab: () => window.open(pageLink, "_blank"),
|
||||
toggleAccess: async () => {
|
||||
const changedPageType = access === EPageAccess.PUBLIC ? "private" : "public";
|
||||
const eventName = PROJECT_PAGE_TRACKER_EVENTS.access_update;
|
||||
|
||||
try {
|
||||
if (access === EPageAccess.PUBLIC)
|
||||
await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-private" });
|
||||
else await executeCollaborativeAction({ type: "sendMessageToServer", message: "make-public" });
|
||||
|
||||
captureSuccess({
|
||||
eventName,
|
||||
payload: {
|
||||
id: page.id,
|
||||
from_access: access === EPageAccess.PUBLIC ? "Public" : "Private",
|
||||
to_access: access === EPageAccess.PUBLIC ? "Private" : "Public",
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: `The page has been marked ${changedPageType} and moved to the ${changedPageType} section.`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: `The page couldn't be marked ${changedPageType}. Please try again.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleArchive: async () => {
|
||||
if (archived_at) {
|
||||
try {
|
||||
await executeCollaborativeAction({ type: "sendMessageToServer", message: "unarchive" });
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.restore,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page restored successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.restore,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be restored. Please try again later.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await executeCollaborativeAction({ type: "sendMessageToServer", message: "archive" });
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.archive,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page archived successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.archive,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be archived. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleFavorite: () => {
|
||||
if (is_favorite) {
|
||||
removePageFromFavorites()
|
||||
.then(() => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.unfavorite,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page removed from favorites.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.unfavorite,
|
||||
error,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
addToFavorites()
|
||||
.then(() => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.favorite,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page added to favorites.",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.favorite,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
toggleLock: async () => {
|
||||
if (is_locked) {
|
||||
try {
|
||||
await executeCollaborativeAction({ type: "sendMessageToServer", message: "unlock" });
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.unlock,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page unlocked successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.unlock,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be unlocked. Please try again later.",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await executeCollaborativeAction({ type: "sendMessageToServer", message: "lock" });
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.lock,
|
||||
payload: {
|
||||
id: page.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: "Page locked successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.lock,
|
||||
error,
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: "Page could not be locked. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [
|
||||
access,
|
||||
addToFavorites,
|
||||
archived_at,
|
||||
duplicate,
|
||||
executeCollaborativeAction,
|
||||
getRedirectionLink,
|
||||
is_favorite,
|
||||
is_locked,
|
||||
isFavoriteMenuOpen,
|
||||
page.id,
|
||||
removePageFromFavorites,
|
||||
toggleFavoriteMenu,
|
||||
]);
|
||||
return {
|
||||
pageOperations,
|
||||
};
|
||||
};
|
||||
215
apps/web/core/hooks/use-parse-editor-content.ts
Normal file
215
apps/web/core/hooks/use-parse-editor-content.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { useCallback } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
// plane types
|
||||
import type { TSearchEntities } from "@plane/types";
|
||||
// helpers
|
||||
import { getBase64Image } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
// plane web hooks
|
||||
import { useAdditionalEditorMention } from "@/plane-web/hooks/use-additional-editor-mention";
|
||||
|
||||
export const useParseEditorContent = () => {
|
||||
// params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
// parse additional content
|
||||
const { parseAdditionalEditorContent } = useAdditionalEditorMention();
|
||||
|
||||
/**
|
||||
* @description function to replace all the custom components from the html component to make it pdf compatible
|
||||
* @param props
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const replaceCustomComponentsFromHTMLContent = useCallback(
|
||||
async (props: { htmlContent: string; noAssets?: boolean }): Promise<string> => {
|
||||
const { htmlContent, noAssets = false } = props;
|
||||
// create a DOM parser
|
||||
const parser = new DOMParser();
|
||||
// parse the HTML string into a DOM document
|
||||
const doc = parser.parseFromString(htmlContent, "text/html");
|
||||
// replace all mention-component elements
|
||||
const mentionComponents = doc.querySelectorAll("mention-component");
|
||||
mentionComponents.forEach((component) => {
|
||||
// create a span element to replace the mention-component
|
||||
const span = doc.createElement("span");
|
||||
span.setAttribute("data-node-type", "mention-block");
|
||||
// get the user id from the component
|
||||
const id = component.getAttribute("entity_identifier") || "";
|
||||
const entityType = (component.getAttribute("entity_name") || "user_mention") as TSearchEntities;
|
||||
let textContent = "user";
|
||||
if (entityType === "user_mention") {
|
||||
const userDetails = getUserDetails(id);
|
||||
textContent = userDetails?.display_name ?? "";
|
||||
} else {
|
||||
const mentionDetails = parseAdditionalEditorContent({
|
||||
id,
|
||||
entityType,
|
||||
});
|
||||
if (mentionDetails) {
|
||||
textContent = mentionDetails.textContent;
|
||||
}
|
||||
}
|
||||
span.textContent = `@${textContent}`;
|
||||
// replace the mention-component with the span element
|
||||
component.replaceWith(span);
|
||||
});
|
||||
// handle code inside pre elements
|
||||
const preElements = doc.querySelectorAll("pre");
|
||||
preElements.forEach((preElement) => {
|
||||
const codeElement = preElement.querySelector("code");
|
||||
if (codeElement) {
|
||||
// create a div element with the required attributes for code blocks
|
||||
const div = doc.createElement("div");
|
||||
div.setAttribute("data-node-type", "code-block");
|
||||
div.setAttribute("class", "courier");
|
||||
// transfer the content from the code block
|
||||
div.innerHTML = codeElement.innerHTML.replace(/\n/g, "<br>") || "";
|
||||
// replace the pre element with the new div
|
||||
preElement.replaceWith(div);
|
||||
}
|
||||
});
|
||||
// handle inline code elements (not inside pre tags)
|
||||
const inlineCodeElements = doc.querySelectorAll("code");
|
||||
inlineCodeElements.forEach((codeElement) => {
|
||||
// check if the code element is inside a pre element
|
||||
if (!codeElement.closest("pre")) {
|
||||
// create a span element with the required attributes for inline code blocks
|
||||
const span = doc.createElement("span");
|
||||
span.setAttribute("data-node-type", "inline-code-block");
|
||||
span.setAttribute("class", "courier-bold");
|
||||
// transfer the code content
|
||||
span.textContent = codeElement.textContent || "";
|
||||
// replace the standalone code element with the new span
|
||||
codeElement.replaceWith(span);
|
||||
}
|
||||
});
|
||||
// handle image-component elements
|
||||
const imageComponents = doc.querySelectorAll("image-component");
|
||||
if (noAssets) {
|
||||
// if no assets is enabled, remove the image component elements
|
||||
imageComponents.forEach((component) => component.remove());
|
||||
// remove default img elements
|
||||
const imageElements = doc.querySelectorAll("img");
|
||||
imageElements.forEach((img) => img.remove());
|
||||
} else {
|
||||
// if no assets is not enabled, replace the image component elements with img elements
|
||||
imageComponents.forEach((component) => {
|
||||
// get the image src from the component
|
||||
const src = component.getAttribute("src") ?? "";
|
||||
const height = component.getAttribute("height") ?? "";
|
||||
const width = component.getAttribute("width") ?? "";
|
||||
// create an img element to replace the image-component
|
||||
const img = doc.createElement("img");
|
||||
img.src = src;
|
||||
img.style.height = height;
|
||||
img.style.width = width;
|
||||
// replace the image-component with the img element
|
||||
component.replaceWith(img);
|
||||
});
|
||||
}
|
||||
// convert all images to base64
|
||||
const imgElements = doc.querySelectorAll("img");
|
||||
await Promise.all(
|
||||
Array.from(imgElements).map(async (img) => {
|
||||
// get the image src from the img element
|
||||
const src = img.getAttribute("src");
|
||||
if (src) {
|
||||
try {
|
||||
const base64Image = await getBase64Image(src);
|
||||
img.src = base64Image;
|
||||
} catch (error) {
|
||||
// log the error if the image conversion fails
|
||||
console.error("Failed to convert image to base64:", error);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
// replace all checkbox elements
|
||||
const checkboxComponents = doc.querySelectorAll("input[type='checkbox']");
|
||||
checkboxComponents.forEach((component) => {
|
||||
// get the checked status from the element
|
||||
const checked = component.getAttribute("checked");
|
||||
// create a div element to replace the input element
|
||||
const div = doc.createElement("div");
|
||||
div.classList.value = "input-checkbox";
|
||||
// add the checked class if the checkbox is checked
|
||||
if (checked === "checked" || checked === "true") div.classList.add("checked");
|
||||
// replace the input element with the div element
|
||||
component.replaceWith(div);
|
||||
});
|
||||
// remove all issue-embed-component elements
|
||||
const issueEmbedComponents = doc.querySelectorAll("issue-embed-component");
|
||||
issueEmbedComponents.forEach((component) => component.remove());
|
||||
// serialize the document back into a string
|
||||
let serializedDoc = doc.body.innerHTML;
|
||||
// remove null colors from table elements
|
||||
serializedDoc = serializedDoc.replace(/background-color: null/g, "").replace(/color: null/g, "");
|
||||
return serializedDoc;
|
||||
},
|
||||
[getUserDetails]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description function to replace all the custom components from the markdown content
|
||||
* @param props
|
||||
* @returns {string}
|
||||
*/
|
||||
const replaceCustomComponentsFromMarkdownContent = useCallback(
|
||||
(props: { markdownContent: string; noAssets?: boolean }): string => {
|
||||
const start = performance.now();
|
||||
const { markdownContent, noAssets = false } = props;
|
||||
let parsedMarkdownContent = markdownContent;
|
||||
// replace the matched mention components with [display_name](redirect_url)
|
||||
const mentionRegex =
|
||||
/<mention-component[^>]*entity_identifier="([^"]+)"[^>]*entity_name="([^"]+)"[^>]*><\/mention-component>/g;
|
||||
const originUrl = typeof window !== "undefined" && (window.location.origin ?? "");
|
||||
parsedMarkdownContent = parsedMarkdownContent.replace(mentionRegex, (_match, id, entity_type) => {
|
||||
const entityType = entity_type as TSearchEntities;
|
||||
if (!id || !entityType) return "";
|
||||
if (entityType === "user_mention") {
|
||||
const userDetails = getUserDetails(id);
|
||||
if (!userDetails) return "";
|
||||
return `[${userDetails.display_name}](${originUrl}/${workspaceSlug}/profile/${id})`;
|
||||
} else {
|
||||
const mentionDetails = parseAdditionalEditorContent({
|
||||
id,
|
||||
entityType,
|
||||
});
|
||||
if (!mentionDetails) {
|
||||
return "";
|
||||
} else {
|
||||
const { redirectionPath, textContent } = mentionDetails;
|
||||
return `[${textContent}](${originUrl}/${redirectionPath})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
// replace the matched image components with <img src={src} >
|
||||
const imageComponentRegex = /<image-component[^>]*src="([^"]+)"[^>]*>[^]*<\/image-component>/g;
|
||||
const imgTagRegex = /<img[^>]*src="([^"]+)"[^>]*\/?>/g;
|
||||
if (noAssets) {
|
||||
// remove all image components
|
||||
parsedMarkdownContent = parsedMarkdownContent.replace(imageComponentRegex, "").replace(imgTagRegex, "");
|
||||
} else {
|
||||
// replace the matched image components with <img src={src} >
|
||||
parsedMarkdownContent = parsedMarkdownContent.replace(
|
||||
imageComponentRegex,
|
||||
(_match, src) => `<img src="${src}" >`
|
||||
);
|
||||
}
|
||||
// remove all issue-embed components
|
||||
const issueEmbedRegex = /<issue-embed-component[^>]*>[^]*<\/issue-embed-component>/g;
|
||||
parsedMarkdownContent = parsedMarkdownContent.replace(issueEmbedRegex, "");
|
||||
const end = performance.now();
|
||||
console.log("Exec time:", end - start);
|
||||
return parsedMarkdownContent;
|
||||
},
|
||||
[getUserDetails, workspaceSlug]
|
||||
);
|
||||
|
||||
return {
|
||||
replaceCustomComponentsFromHTMLContent,
|
||||
replaceCustomComponentsFromMarkdownContent,
|
||||
};
|
||||
};
|
||||
50
apps/web/core/hooks/use-peek-overview-outside-click.tsx
Normal file
50
apps/web/core/hooks/use-peek-overview-outside-click.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const usePeekOverviewOutsideClickDetector = (
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
callback: () => void,
|
||||
issueId: string
|
||||
) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
// check for the closest element with attribute name data-prevent-outside-click
|
||||
const preventOutsideClickElement = (event.target as HTMLElement | undefined)?.closest(
|
||||
"[data-prevent-outside-click]"
|
||||
);
|
||||
// if the closest element with attribute name data-prevent-outside-click is found, return
|
||||
if (preventOutsideClickElement) {
|
||||
return;
|
||||
}
|
||||
// check if the click target is the current issue element or its children
|
||||
let targetElement = event.target as HTMLElement | null;
|
||||
while (targetElement) {
|
||||
if (targetElement.id === `issue-${issueId}`) {
|
||||
// if the click target is the current issue element, return
|
||||
return;
|
||||
}
|
||||
targetElement = targetElement.parentElement;
|
||||
}
|
||||
const delayOutsideClickElement = (event.target as HTMLElement | undefined)?.closest("[data-delay-outside-click]");
|
||||
if (delayOutsideClickElement) {
|
||||
// if the click target is the closest element with attribute name data-delay-outside-click, delay the callback
|
||||
setTimeout(() => {
|
||||
callback();
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
// else, call the callback immediately
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClick);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClick);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default usePeekOverviewOutsideClickDetector;
|
||||
20
apps/web/core/hooks/use-platform-os.tsx
Normal file
20
apps/web/core/hooks/use-platform-os.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
export const usePlatformOS = () => {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
|
||||
let platform = "";
|
||||
|
||||
if (!isMobile) {
|
||||
if (userAgent.indexOf("Win") !== -1) {
|
||||
platform = "Windows";
|
||||
} else if (userAgent.indexOf("Mac") !== -1) {
|
||||
platform = "MacOS";
|
||||
} else if (userAgent.indexOf("Linux") !== -1) {
|
||||
platform = "Linux";
|
||||
} else {
|
||||
platform = "Unknown";
|
||||
}
|
||||
}
|
||||
return { isMobile, platform };
|
||||
};
|
||||
94
apps/web/core/hooks/use-project-issue-properties.ts
Normal file
94
apps/web/core/hooks/use-project-issue-properties.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useProjectEstimates } from "./store/estimates";
|
||||
import { useCycle } from "./store/use-cycle";
|
||||
import { useLabel } from "./store/use-label";
|
||||
import { useMember } from "./store/use-member";
|
||||
import { useModule } from "./store/use-module";
|
||||
import { useProjectState } from "./store/use-project-state";
|
||||
|
||||
export const useProjectIssueProperties = () => {
|
||||
const { fetchProjectStates } = useProjectState();
|
||||
const {
|
||||
project: { fetchProjectMembers },
|
||||
} = useMember();
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { fetchAllCycles: fetchProjectAllCycles } = useCycle();
|
||||
const { fetchModules: fetchProjectAllModules } = useModule();
|
||||
const { getProjectEstimates } = useProjectEstimates();
|
||||
|
||||
// fetching project states
|
||||
const fetchStates = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectStates(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project members
|
||||
const fetchMembers = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
|
||||
// fetching project labels
|
||||
const fetchLabels = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project cycles
|
||||
const fetchCycles = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectAllCycles(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project modules
|
||||
const fetchModules = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectAllModules(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project estimates
|
||||
const fetchEstimates = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await getProjectEstimates(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAll = async (workspaceSlug: string | string[] | undefined, projectId: string | string[] | undefined) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchStates(workspaceSlug, projectId);
|
||||
await fetchMembers(workspaceSlug, projectId);
|
||||
await fetchLabels(workspaceSlug, projectId);
|
||||
await fetchCycles(workspaceSlug, projectId);
|
||||
await fetchModules(workspaceSlug, projectId);
|
||||
await fetchEstimates(workspaceSlug, projectId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchAll,
|
||||
fetchStates,
|
||||
fetchMembers,
|
||||
fetchLabels,
|
||||
fetchCycles,
|
||||
fetchModules,
|
||||
fetchEstimates,
|
||||
};
|
||||
};
|
||||
38
apps/web/core/hooks/use-query-params.ts
Normal file
38
apps/web/core/hooks/use-query-params.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSearchParams, usePathname } from "next/navigation";
|
||||
|
||||
type TParamsToAdd = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export const useQueryParams = () => {
|
||||
// next navigation
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const updateQueryParams = useCallback(
|
||||
({ paramsToAdd = {}, paramsToRemove = [] }: { paramsToAdd?: TParamsToAdd; paramsToRemove?: string[] }) => {
|
||||
const currentParams = new URLSearchParams(searchParams.toString());
|
||||
|
||||
// add or update query parameters
|
||||
Object.keys(paramsToAdd).forEach((key) => {
|
||||
currentParams.set(key, paramsToAdd[key]);
|
||||
});
|
||||
|
||||
// remove specified query parameters
|
||||
paramsToRemove.forEach((key) => {
|
||||
currentParams.delete(key);
|
||||
});
|
||||
|
||||
// construct the new route with the updated query parameters
|
||||
const query = currentParams.toString();
|
||||
const newRoute = query ? `${pathname}?${query}` : pathname;
|
||||
return newRoute;
|
||||
},
|
||||
[pathname, searchParams]
|
||||
);
|
||||
|
||||
return {
|
||||
updateQueryParams,
|
||||
};
|
||||
};
|
||||
193
apps/web/core/hooks/use-realtime-page-events.tsx
Normal file
193
apps/web/core/hooks/use-realtime-page-events.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
// plane imports
|
||||
import type { EventToPayloadMap } from "@plane/editor";
|
||||
import { setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||
// types
|
||||
import type { IUserLite } from "@plane/types";
|
||||
// components
|
||||
import type { TEditorBodyHandlers } from "@/components/pages/editor/editor-body";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import type { EPageStoreType } from "@/plane-web/hooks/store";
|
||||
import { usePageStore } from "@/plane-web/hooks/store";
|
||||
// store
|
||||
import type { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
// Type for page update handlers with proper typing for action data
|
||||
export type PageUpdateHandler<T extends keyof EventToPayloadMap = keyof EventToPayloadMap> = (params: {
|
||||
pageIds: string[];
|
||||
data: EventToPayloadMap[T];
|
||||
performAction: boolean;
|
||||
}) => void;
|
||||
|
||||
// Type for custom event handlers that can be provided to override default behavior
|
||||
export type TCustomEventHandlers = {
|
||||
[K in keyof EventToPayloadMap]?: PageUpdateHandler<K>;
|
||||
};
|
||||
|
||||
interface UsePageEventsProps {
|
||||
page: TPageInstance;
|
||||
storeType: EPageStoreType;
|
||||
getUserDetails: (userId: string) => IUserLite | undefined;
|
||||
customRealtimeEventHandlers?: TCustomEventHandlers;
|
||||
handlers: TEditorBodyHandlers;
|
||||
}
|
||||
|
||||
export const useRealtimePageEvents = ({
|
||||
page,
|
||||
storeType,
|
||||
getUserDetails,
|
||||
customRealtimeEventHandlers,
|
||||
handlers,
|
||||
}: UsePageEventsProps) => {
|
||||
const router = useAppRouter();
|
||||
const { removePage, getPageById } = usePageStore(storeType);
|
||||
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
// Helper function to safely get user display text
|
||||
const getUserDisplayText = useCallback(
|
||||
(userId: string | undefined) => {
|
||||
if (!userId) return "";
|
||||
try {
|
||||
const userDetails = getUserDetails(userId as string);
|
||||
return userDetails?.display_name ? ` by ${userDetails.display_name}` : "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
[getUserDetails]
|
||||
);
|
||||
|
||||
const ACTION_HANDLERS = useMemo<
|
||||
Partial<{
|
||||
[K in keyof EventToPayloadMap]: PageUpdateHandler<K>;
|
||||
}>
|
||||
>(
|
||||
() => ({
|
||||
archived: ({ pageIds, data }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.archive({ archived_at: data.archived_at, shouldSync: false });
|
||||
});
|
||||
},
|
||||
unarchived: ({ pageIds }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.restore({ shouldSync: false });
|
||||
});
|
||||
},
|
||||
locked: ({ pageIds }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.lock({ shouldSync: false, recursive: false });
|
||||
});
|
||||
},
|
||||
unlocked: ({ pageIds }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.unlock({ shouldSync: false, recursive: false });
|
||||
});
|
||||
},
|
||||
"made-public": ({ pageIds }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.makePublic({ shouldSync: false });
|
||||
});
|
||||
},
|
||||
"made-private": ({ pageIds }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) pageItem.makePrivate({ shouldSync: false });
|
||||
});
|
||||
},
|
||||
deleted: ({ pageIds, data }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageItem = getPageById(pageId);
|
||||
if (pageItem) {
|
||||
removePage({ pageId, shouldSync: false });
|
||||
if (page.id === pageId && data?.user_id !== currentUser?.id) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Page deleted",
|
||||
message: `Page deleted${getUserDisplayText(data.user_id)}`,
|
||||
});
|
||||
router.push(handlers.getRedirectionLink());
|
||||
} else if (page.id === pageId) {
|
||||
router.push(handlers.getRedirectionLink());
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
property_updated: ({ pageIds, data }) => {
|
||||
pageIds.forEach((pageId) => {
|
||||
const pageInstance = getPageById(pageId);
|
||||
const { name: updatedName, ...rest } = data;
|
||||
if (updatedName != null) pageInstance?.updateTitle(updatedName);
|
||||
pageInstance?.mutateProperties(rest);
|
||||
});
|
||||
},
|
||||
error: ({ pageIds, data }) => {
|
||||
const errorType = data.error_type;
|
||||
const errorMessage = data.error_message || "An error occurred";
|
||||
const errorCode = data.error_code;
|
||||
|
||||
if (page.id && pageIds.includes(page.id)) {
|
||||
// Show toast notification
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: errorType === "fetch" ? "Failed to load page" : "Failed to save page",
|
||||
message: errorMessage,
|
||||
});
|
||||
|
||||
// Handle specific error codes
|
||||
const pageInstance = getPageById(page.id);
|
||||
if (pageInstance) {
|
||||
if (errorCode === "page_locked") {
|
||||
// Lock the page if not already locked
|
||||
if (!pageInstance.is_locked) {
|
||||
pageInstance.mutateProperties({ is_locked: true });
|
||||
}
|
||||
} else if (errorCode === "page_archived") {
|
||||
// Mark page as archived if not already
|
||||
if (!pageInstance.archived_at) {
|
||||
pageInstance.mutateProperties({ archived_at: new Date().toISOString() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...customRealtimeEventHandlers,
|
||||
}),
|
||||
[getPageById, page, router, getUserDisplayText, removePage, currentUser, customRealtimeEventHandlers, handlers]
|
||||
);
|
||||
|
||||
// The main function that will be returned from this hook
|
||||
const updatePageProperties = useCallback(
|
||||
<T extends keyof EventToPayloadMap>(
|
||||
pageIds: string | string[],
|
||||
actionType: T,
|
||||
data: EventToPayloadMap[T],
|
||||
performAction = false
|
||||
) => {
|
||||
// Convert to array if single string is passed
|
||||
const normalizedPageIds = Array.isArray(pageIds) ? pageIds : [pageIds];
|
||||
|
||||
if (normalizedPageIds.length === 0) return;
|
||||
|
||||
// Get the handler for this message type
|
||||
const handler = ACTION_HANDLERS[actionType];
|
||||
|
||||
if (handler) {
|
||||
// Now TypeScript knows that handler and data match in type
|
||||
handler({ pageIds: normalizedPageIds, data, performAction });
|
||||
} else {
|
||||
console.warn(`No handler for message type: ${actionType.toString()}`);
|
||||
}
|
||||
},
|
||||
[ACTION_HANDLERS]
|
||||
);
|
||||
|
||||
return { updatePageProperties };
|
||||
};
|
||||
61
apps/web/core/hooks/use-reload-confirmation.tsx
Normal file
61
apps/web/core/hooks/use-reload-confirmation.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
//TODO: remove temp flag isActive later and use showAlert as the source of truth
|
||||
const useReloadConfirmations = (isActive = true, message?: string, defaultShowAlert = false, onLeave?: () => void) => {
|
||||
const [showAlert, setShowAlert] = useState(defaultShowAlert);
|
||||
|
||||
const alertMessage = message ?? "Are you sure you want to leave? Changes you made may not be saved.";
|
||||
|
||||
const handleBeforeUnload = useCallback(
|
||||
(event: BeforeUnloadEvent) => {
|
||||
if (!isActive || !showAlert) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = "";
|
||||
},
|
||||
[isActive, showAlert]
|
||||
);
|
||||
|
||||
const handleAnchorClick = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!isActive || !showAlert) return;
|
||||
// Skip if event target is not available or defaultPrevented
|
||||
if (!event.target || event.defaultPrevented) return;
|
||||
// Skip control/command/option/alt+click
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
||||
// check if the event target is an anchor or a child of an anchor tag
|
||||
const eventTarget = event.target as HTMLElement;
|
||||
if (!eventTarget.closest("a")) return; // This is intentionally not type safe
|
||||
// check if anchor target is _blank
|
||||
const anchorElement = eventTarget.closest("a") as HTMLAnchorElement;
|
||||
const isAnchorTargetBlank = anchorElement.getAttribute("target") === "_blank";
|
||||
if (isAnchorTargetBlank) return;
|
||||
// show confirm dialog
|
||||
const isLeaving = confirm(alertMessage);
|
||||
if (isLeaving) {
|
||||
onLeave && onLeave();
|
||||
} else {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
[isActive, showAlert]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// handle browser refresh
|
||||
window.addEventListener("beforeunload", handleBeforeUnload, true);
|
||||
// handle anchor tag click
|
||||
window.addEventListener("click", handleAnchorClick, true);
|
||||
// TODO: handle back / forward button click
|
||||
|
||||
return () => {
|
||||
// cleanup
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload, true);
|
||||
window.removeEventListener("click", handleAnchorClick, true);
|
||||
};
|
||||
}, [handleAnchorClick, handleBeforeUnload]);
|
||||
|
||||
return { setShowAlert };
|
||||
};
|
||||
|
||||
export default useReloadConfirmations;
|
||||
26
apps/web/core/hooks/use-resolved-asset-path.tsx
Normal file
26
apps/web/core/hooks/use-resolved-asset-path.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
type AssetPathConfig = {
|
||||
basePath: string;
|
||||
additionalPath?: string;
|
||||
extension?: string;
|
||||
includeThemeInPath?: boolean;
|
||||
};
|
||||
|
||||
export const useResolvedAssetPath = ({
|
||||
basePath,
|
||||
additionalPath = "",
|
||||
extension = "webp",
|
||||
includeThemeInPath = true,
|
||||
}: AssetPathConfig) => {
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
// resolved theme
|
||||
const theme = resolvedTheme === "light" ? "light" : "dark";
|
||||
|
||||
if (!includeThemeInPath) {
|
||||
return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}.${extension}`;
|
||||
}
|
||||
|
||||
return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}-${theme}.${extension}`;
|
||||
};
|
||||
11
apps/web/core/hooks/use-stickies.tsx
Normal file
11
apps/web/core/hooks/use-stickies.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useContext } from "react";
|
||||
// context
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
import type { IStickyStore } from "@/store/sticky/sticky.store";
|
||||
// plane web stores
|
||||
|
||||
export const useSticky = (): IStickyStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useSticky must be used within StoreProvider");
|
||||
return context.stickyStore;
|
||||
};
|
||||
56
apps/web/core/hooks/use-table-keyboard-navigation.tsx
Normal file
56
apps/web/core/hooks/use-table-keyboard-navigation.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
export const useTableKeyboardNavigation = () => {
|
||||
const getPreviousRow = (element: HTMLElement) => {
|
||||
const previousRow = element.closest("tr")?.previousSibling;
|
||||
|
||||
if (previousRow) return previousRow;
|
||||
//if previous row does not exist in the parent check the row with the header of the table
|
||||
return element.closest("tbody")?.previousSibling?.childNodes?.[0];
|
||||
};
|
||||
|
||||
const getNextRow = (element: HTMLElement) => {
|
||||
const nextRow = element.closest("tr")?.nextSibling;
|
||||
|
||||
if (nextRow) return nextRow;
|
||||
//if next row does not exist in the parent check the row with the body of the table
|
||||
return element.closest("thead")?.nextSibling?.childNodes?.[0];
|
||||
};
|
||||
|
||||
const handleKeyBoardNavigation = function (e: React.KeyboardEvent<HTMLTableElement>) {
|
||||
const element = e.target as HTMLElement;
|
||||
|
||||
if (!(element?.tagName === "TD" || element?.tagName === "TH")) return;
|
||||
|
||||
let c: HTMLElement | null = null;
|
||||
if (e.key == "ArrowRight") {
|
||||
// Right Arrow
|
||||
c = element.nextSibling as HTMLElement;
|
||||
} else if (e.key == "ArrowLeft") {
|
||||
// Left Arrow
|
||||
c = element.previousSibling as HTMLElement;
|
||||
} else if (e.key == "ArrowUp") {
|
||||
// Up Arrow
|
||||
const index = Array.prototype.indexOf.call(element?.parentNode?.childNodes || [], element);
|
||||
const prevRow = getPreviousRow(element);
|
||||
|
||||
c = prevRow?.childNodes?.[index] as HTMLElement;
|
||||
} else if (e.key == "ArrowDown") {
|
||||
// Down Arrow
|
||||
const index = Array.prototype.indexOf.call(element?.parentNode?.childNodes || [], element);
|
||||
const nextRow = getNextRow(element);
|
||||
|
||||
c = nextRow?.childNodes[index] as HTMLElement;
|
||||
} else if (e.key == "Enter" || e.key == "Space") {
|
||||
e.preventDefault();
|
||||
(element?.querySelector(".clickable") as HTMLElement)?.click();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!c) return;
|
||||
|
||||
e.preventDefault();
|
||||
c?.focus();
|
||||
c?.scrollIntoView({ behavior: "smooth", block: "center", inline: "end" });
|
||||
};
|
||||
|
||||
return handleKeyBoardNavigation;
|
||||
};
|
||||
31
apps/web/core/hooks/use-timeline-chart.ts
Normal file
31
apps/web/core/hooks/use-timeline-chart.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useContext } from "react";
|
||||
// lib
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// Plane-web
|
||||
import type { IBaseTimelineStore } from "@/plane-web/store/timeline/base-timeline.store";
|
||||
//
|
||||
import { ETimeLineTypeType, useTimeLineType } from "../components/gantt-chart/contexts";
|
||||
|
||||
export const useTimeLineChart = (timeLineType: ETimeLineTypeType): IBaseTimelineStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useTimeLineChart must be used within StoreProvider");
|
||||
|
||||
switch (timeLineType) {
|
||||
case ETimeLineTypeType.ISSUE:
|
||||
return context.timelineStore.issuesTimeLineStore;
|
||||
case ETimeLineTypeType.MODULE:
|
||||
return context.timelineStore.modulesTimeLineStore as IBaseTimelineStore;
|
||||
case ETimeLineTypeType.PROJECT:
|
||||
return context.timelineStore.projectTimeLineStore as IBaseTimelineStore;
|
||||
case ETimeLineTypeType.GROUPED:
|
||||
return context.timelineStore.groupedTimeLineStore as IBaseTimelineStore;
|
||||
}
|
||||
};
|
||||
|
||||
export const useTimeLineChartStore = () => {
|
||||
const timelineType = useTimeLineType();
|
||||
|
||||
if (!timelineType) throw new Error("useTimeLineChartStore must be used within TimeLineTypeContext");
|
||||
|
||||
return useTimeLineChart(timelineType);
|
||||
};
|
||||
19
apps/web/core/hooks/use-timer.tsx
Normal file
19
apps/web/core/hooks/use-timer.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const TIMER = 30;
|
||||
|
||||
const useTimer = (initialValue: number = TIMER) => {
|
||||
const [timer, setTimer] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimer((prev) => prev - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return { timer, setTimer };
|
||||
};
|
||||
|
||||
export default useTimer;
|
||||
70
apps/web/core/hooks/use-timezone-converter.tsx
Normal file
70
apps/web/core/hooks/use-timezone-converter.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useUser } from "@/hooks/store/user";
|
||||
|
||||
export const useTimeZoneConverter = (projectId: string) => {
|
||||
const { data: user } = useUser();
|
||||
const { getProjectById } = useProject();
|
||||
const userTimezone = user?.user_timezone;
|
||||
const projectTimezone = getProjectById(projectId)?.timezone;
|
||||
|
||||
/**
|
||||
* Render a date in the user's timezone
|
||||
* @param date - The date to render
|
||||
* @param formatToken - The format token to use
|
||||
* @returns The formatted date
|
||||
*/
|
||||
const renderFormattedDateInUserTimezone = useCallback(
|
||||
(date: string, formatToken: string = "MMM dd, yyyy") => {
|
||||
// return if undefined
|
||||
if (!date || !userTimezone) return;
|
||||
// convert the date to the user's timezone
|
||||
const convertedDate = new Date(date).toLocaleString("en-US", { timeZone: userTimezone });
|
||||
// return the formatted date
|
||||
return format(convertedDate, formatToken);
|
||||
},
|
||||
[userTimezone]
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the project's UTC offset
|
||||
* @returns The project's UTC offset
|
||||
*/
|
||||
const getProjectUTCOffset = useCallback(() => {
|
||||
if (!projectTimezone) return;
|
||||
|
||||
// Get date in user's timezone
|
||||
const projectDate = new Date(new Date().toLocaleString("en-US", { timeZone: projectTimezone }));
|
||||
const utcDate = new Date(new Date().toLocaleString("en-US", { timeZone: "UTC" }));
|
||||
|
||||
// Calculate offset in minutes
|
||||
const offsetInMinutes = (projectDate.getTime() - utcDate.getTime()) / 60000;
|
||||
|
||||
// return if undefined
|
||||
if (!offsetInMinutes) return;
|
||||
|
||||
// Convert to hours and minutes
|
||||
const hours = Math.floor(Math.abs(offsetInMinutes) / 60);
|
||||
const minutes = Math.abs(offsetInMinutes) % 60;
|
||||
|
||||
// Format as +/-HH:mm
|
||||
const sign = offsetInMinutes >= 0 ? "+" : "-";
|
||||
return `UTC ${sign}${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
}, [projectTimezone]);
|
||||
|
||||
/**
|
||||
* Check if the project's timezone is different from the user's timezone
|
||||
* @returns True if the project's timezone is different from the user's timezone, false otherwise
|
||||
*/
|
||||
const isProjectTimeZoneDifferent = useCallback(() => {
|
||||
if (!projectTimezone || !userTimezone) return false;
|
||||
return projectTimezone !== userTimezone;
|
||||
}, [projectTimezone, userTimezone]);
|
||||
|
||||
return {
|
||||
renderFormattedDateInUserTimezone,
|
||||
getProjectUTCOffset,
|
||||
isProjectTimeZoneDifferent,
|
||||
};
|
||||
};
|
||||
80
apps/web/core/hooks/use-timezone.tsx
Normal file
80
apps/web/core/hooks/use-timezone.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import useSWR from "swr";
|
||||
import type { TTimezoneObject } from "@plane/types";
|
||||
// services
|
||||
import timezoneService from "@/services/timezone.service";
|
||||
|
||||
// group timezones by value
|
||||
const groupTimezones = (timezones: TTimezoneObject[]): TTimezoneObject[] => {
|
||||
const groupedMap = timezones.reduce((acc, timezone: TTimezoneObject) => {
|
||||
const key = timezone.value;
|
||||
|
||||
if (!acc.has(key)) {
|
||||
acc.set(key, {
|
||||
utc_offset: timezone.utc_offset,
|
||||
gmt_offset: timezone.gmt_offset,
|
||||
value: timezone.value,
|
||||
label: timezone.label,
|
||||
});
|
||||
} else {
|
||||
const existing = acc.get(key);
|
||||
existing.label = `${existing.label}, ${timezone.label}`;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
return Array.from(groupedMap.values());
|
||||
};
|
||||
|
||||
const useTimezone = () => {
|
||||
// fetching the timezone from the server
|
||||
const {
|
||||
data: timezones,
|
||||
isLoading: timezoneIsLoading,
|
||||
error: timezonesError,
|
||||
} = useSWR("TIMEZONES_LIST", () => timezoneService.fetch(), {
|
||||
refreshInterval: 0,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const isDisabled = timezoneIsLoading || timezonesError || !timezones;
|
||||
|
||||
const getTimeZoneLabel = (timezone: TTimezoneObject | undefined) => {
|
||||
if (!timezone) return undefined;
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
<span className="text-custom-text-400">{timezone.utc_offset}</span>
|
||||
<span className="text-custom-text-200">{timezone.label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const options = [
|
||||
...groupTimezones(timezones?.timezones || [])?.map((timezone) => ({
|
||||
value: timezone.value,
|
||||
query: `${timezone.value} ${timezone.label}, ${timezone.gmt_offset}, ${timezone.utc_offset}`,
|
||||
content: getTimeZoneLabel(timezone),
|
||||
})),
|
||||
{
|
||||
value: "UTC",
|
||||
query: "utc, coordinated universal time",
|
||||
content: "UTC",
|
||||
},
|
||||
{
|
||||
value: "Universal",
|
||||
query: "universal, coordinated universal time",
|
||||
content: "Universal",
|
||||
},
|
||||
];
|
||||
|
||||
const selectedTimezone = (value: string | undefined) => options.find((option) => option.value === value)?.content;
|
||||
|
||||
return {
|
||||
timezones: options,
|
||||
isLoading: timezoneIsLoading,
|
||||
error: timezonesError,
|
||||
disabled: isDisabled,
|
||||
selectedValue: selectedTimezone,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTimezone;
|
||||
19
apps/web/core/hooks/use-window-size.tsx
Normal file
19
apps/web/core/hooks/use-window-size.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const useSize = () => {
|
||||
const [windowSize, setWindowSize] = useState([window.innerWidth, window.innerHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const windowSizeHandler = () => {
|
||||
setWindowSize([window.innerWidth, window.innerHeight]);
|
||||
};
|
||||
window.addEventListener("resize", windowSizeHandler);
|
||||
return () => {
|
||||
window.removeEventListener("resize", windowSizeHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
};
|
||||
|
||||
export default useSize;
|
||||
85
apps/web/core/hooks/use-workspace-invitation.tsx
Normal file
85
apps/web/core/hooks/use-workspace-invitation.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useEffect } from "react";
|
||||
import type { Control, FieldArrayWithId, FormState, UseFormWatch } from "react-hook-form";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
// plane imports
|
||||
import { EUserPermissions } from "@plane/constants";
|
||||
|
||||
type EmailRole = {
|
||||
email: string;
|
||||
role: EUserPermissions;
|
||||
};
|
||||
|
||||
export type InvitationFormValues = {
|
||||
emails: EmailRole[];
|
||||
};
|
||||
|
||||
const SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES: InvitationFormValues = {
|
||||
emails: [
|
||||
{
|
||||
email: "",
|
||||
role: EUserPermissions.MEMBER,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
type TUseWorkspaceInvitationProps = {
|
||||
onSubmit: (data: InvitationFormValues) => Promise<void> | undefined;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type TUseWorkspaceInvitationReturn = {
|
||||
control: Control<InvitationFormValues>;
|
||||
fields: FieldArrayWithId<InvitationFormValues, "emails", "id">[];
|
||||
formState: FormState<InvitationFormValues>;
|
||||
watch: UseFormWatch<InvitationFormValues>;
|
||||
remove: (index: number) => void;
|
||||
onFormSubmit: () => void;
|
||||
handleClose: () => void;
|
||||
appendField: () => void;
|
||||
};
|
||||
|
||||
export const useWorkspaceInvitationActions = (props: TUseWorkspaceInvitationProps): TUseWorkspaceInvitationReturn => {
|
||||
const { onSubmit, onClose } = props;
|
||||
// form info
|
||||
const { control, reset, watch, handleSubmit, formState } = useForm<InvitationFormValues>({
|
||||
defaultValues: SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES,
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "emails",
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
const timeout = setTimeout(() => {
|
||||
reset(SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES);
|
||||
clearTimeout(timeout);
|
||||
}, 350);
|
||||
};
|
||||
|
||||
const appendField = () => {
|
||||
append({ email: "", role: EUserPermissions.MEMBER });
|
||||
};
|
||||
|
||||
const onSubmitForm = async (data: InvitationFormValues) => {
|
||||
await onSubmit(data)?.then(() => {
|
||||
reset(SEND_WORKSPACE_INVITATION_MODAL_DEFAULT_VALUES);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (fields.length === 0) append([{ email: "", role: EUserPermissions.MEMBER }]);
|
||||
}, [fields, append]);
|
||||
|
||||
return {
|
||||
control,
|
||||
fields,
|
||||
formState,
|
||||
watch,
|
||||
remove,
|
||||
onFormSubmit: handleSubmit(onSubmitForm),
|
||||
handleClose,
|
||||
appendField,
|
||||
};
|
||||
};
|
||||
49
apps/web/core/hooks/use-workspace-issue-properties.ts
Normal file
49
apps/web/core/hooks/use-workspace-issue-properties.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import useSWR from "swr";
|
||||
// plane web imports
|
||||
import { useWorkspaceIssuePropertiesExtended } from "@/plane-web/hooks/use-workspace-issue-properties-extended";
|
||||
// plane imports
|
||||
import { useProjectEstimates } from "./store/estimates";
|
||||
import { useCycle } from "./store/use-cycle";
|
||||
import { useLabel } from "./store/use-label";
|
||||
import { useModule } from "./store/use-module";
|
||||
|
||||
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
|
||||
const { fetchWorkspaceLabels } = useLabel();
|
||||
|
||||
const { getWorkspaceEstimates } = useProjectEstimates();
|
||||
|
||||
const { fetchWorkspaceModules } = useModule();
|
||||
|
||||
const { fetchWorkspaceCycles } = useCycle();
|
||||
|
||||
// fetch workspace Modules
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_MODULES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceModules(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace Cycles
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_CYCLES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceCycles(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace labels
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_LABELS_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorkspaceLabels(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch workspace estimates
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => getWorkspaceEstimates(workspaceSlug.toString()) : null,
|
||||
{ revalidateIfStale: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// fetch extended issue properties
|
||||
useWorkspaceIssuePropertiesExtended(workspaceSlug);
|
||||
};
|
||||
24
apps/web/core/hooks/use-workspace-paths.ts
Normal file
24
apps/web/core/hooks/use-workspace-paths.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Custom hook to detect different workspace paths
|
||||
* @returns Object containing boolean flags for different workspace paths
|
||||
*/
|
||||
export const useWorkspacePaths = () => {
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const isSettingsPath = pathname.includes(`/${workspaceSlug}/settings`);
|
||||
const isWikiPath = pathname.includes(`/${workspaceSlug}/pages`);
|
||||
const isAiPath = pathname.includes(`/${workspaceSlug}/pi-chat`);
|
||||
const isProjectsPath = pathname.includes(`/${workspaceSlug}/`) && !isWikiPath && !isAiPath && !isSettingsPath;
|
||||
|
||||
return {
|
||||
isSettingsPath,
|
||||
isWikiPath,
|
||||
isAiPath,
|
||||
isProjectsPath,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user