[PE-182] refactor: pages' components and store for scalability (#6283)
* refactor: created a generic base page instance * refactor: project store hooks * chore: add missing prop declaration * refactor: editor page root and body * refactor: issue embed hook * chore: update search entity types * fix: version editor component * fix: add page to favorites action --------- Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
This commit is contained in:
parent
211d5e1cd0
commit
8d7425a3b7
34 changed files with 553 additions and 521 deletions
|
|
@ -1,5 +1,9 @@
|
|||
import { Extensions, JSONContent } from "@tiptap/core";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
// plane types
|
||||
import { TWebhookConnectionQueryParams } from "@plane/types";
|
||||
// extension types
|
||||
import { TTextAlign } from "@/extensions";
|
||||
// helpers
|
||||
import { IMarking } from "@/helpers/scroll-to-node";
|
||||
// types
|
||||
|
|
@ -15,7 +19,6 @@ import {
|
|||
TReadOnlyMentionHandler,
|
||||
TServerHandler,
|
||||
} from "@/types";
|
||||
import { TTextAlign } from "@/extensions";
|
||||
|
||||
export type TEditorCommands =
|
||||
| "text"
|
||||
|
|
@ -185,7 +188,5 @@ export type TUserDetails = {
|
|||
|
||||
export type TRealtimeConfig = {
|
||||
url: string;
|
||||
queryParams: {
|
||||
[key: string]: string;
|
||||
};
|
||||
queryParams: TWebhookConnectionQueryParams;
|
||||
};
|
||||
|
|
|
|||
20
packages/types/src/pages.d.ts
vendored
20
packages/types/src/pages.d.ts
vendored
|
|
@ -15,7 +15,8 @@ export type TPage = {
|
|||
label_ids: string[] | undefined;
|
||||
name: string | undefined;
|
||||
owned_by: string | undefined;
|
||||
project_ids: string[] | undefined;
|
||||
project_ids?: string[] | undefined;
|
||||
team: string | null | undefined;
|
||||
updated_at: Date | undefined;
|
||||
updated_by: string | undefined;
|
||||
workspace: string | undefined;
|
||||
|
|
@ -25,11 +26,7 @@ export type TPage = {
|
|||
// page filters
|
||||
export type TPageNavigationTabs = "public" | "private" | "archived";
|
||||
|
||||
export type TPageFiltersSortKey =
|
||||
| "name"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "opened_at";
|
||||
export type TPageFiltersSortKey = "name" | "created_at" | "updated_at" | "opened_at";
|
||||
|
||||
export type TPageFiltersSortBy = "asc" | "desc";
|
||||
|
||||
|
|
@ -63,10 +60,17 @@ export type TPageVersion = {
|
|||
updated_at: string;
|
||||
updated_by: string;
|
||||
workspace: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type TDocumentPayload = {
|
||||
description_binary: string;
|
||||
description_html: string;
|
||||
description: object;
|
||||
}
|
||||
};
|
||||
|
||||
export type TWebhookConnectionQueryParams = {
|
||||
documentType: "project_page" | "team_page" | "workspace_page";
|
||||
projectId?: string;
|
||||
teamId?: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
|
|
|||
18
packages/types/src/search.d.ts
vendored
18
packages/types/src/search.d.ts
vendored
|
|
@ -6,13 +6,7 @@ import { IProject } from "./project";
|
|||
import { IUser } from "./users";
|
||||
import { IWorkspace } from "./workspace";
|
||||
|
||||
export type TSearchEntities =
|
||||
| "user_mention"
|
||||
| "issue_mention"
|
||||
| "project_mention"
|
||||
| "cycle_mention"
|
||||
| "module_mention"
|
||||
| "page_mention";
|
||||
export type TSearchEntities = "user_mention" | "issue" | "project" | "cycle" | "module" | "page";
|
||||
|
||||
export type TUserSearchResponse = {
|
||||
member__avatar_url: IUser["avatar_url"];
|
||||
|
|
@ -66,11 +60,11 @@ export type TPageSearchResponse = {
|
|||
};
|
||||
|
||||
export type TSearchResponse = {
|
||||
cycle_mention?: TCycleSearchResponse[];
|
||||
issue_mention?: TIssueSearchResponse[];
|
||||
module_mention?: TModuleSearchResponse[];
|
||||
page_mention?: TPageSearchResponse[];
|
||||
project_mention?: TProjectSearchResponse[];
|
||||
cycle?: TCycleSearchResponse[];
|
||||
issue?: TIssueSearchResponse[];
|
||||
module?: TModuleSearchResponse[];
|
||||
page?: TPageSearchResponse[];
|
||||
project?: TProjectSearchResponse[];
|
||||
user_mention?: TUserSearchResponse[];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
import { MAX_FILE_SIZE } from "@plane/constants";
|
||||
import { getFileURL } from "./file";
|
||||
|
||||
// Define image-related types locally
|
||||
type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
|
||||
type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise<void>;
|
||||
type UploadImage = (file: File) => Promise<string>;
|
||||
|
||||
// Define the FileService interface based on usage
|
||||
interface IFileService {
|
||||
deleteOldEditorAsset: (workspaceId: string, src: string) => Promise<void>;
|
||||
deleteNewAsset: (url: string) => Promise<void>;
|
||||
restoreOldEditorAsset: (workspaceId: string, src: string) => Promise<void>;
|
||||
restoreNewAsset: (anchor: string, src: string) => Promise<void>;
|
||||
cancelUpload: () => void;
|
||||
}
|
||||
|
||||
// Define TFileHandler locally since we can't import from @plane/editor
|
||||
interface TFileHandler {
|
||||
getAssetSrc: (path: string) => Promise<string>;
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
validation: {
|
||||
maxFileSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description generate the file source using assetId
|
||||
* @param {string} anchor
|
||||
* @param {string} assetId
|
||||
*/
|
||||
export const getEditorAssetSrc = (anchor: string, assetId: string): string | undefined => {
|
||||
const url = getFileURL(`/api/public/assets/v2/anchor/${anchor}/${assetId}/`);
|
||||
return url;
|
||||
};
|
||||
|
||||
type TArgs = {
|
||||
anchor: string;
|
||||
uploadFile: (file: File) => Promise<string>;
|
||||
workspaceId: string;
|
||||
fileService: IFileService;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the editors
|
||||
* @param {TArgs} args
|
||||
*/
|
||||
export const getEditorFileHandlers = (args: TArgs): TFileHandler => {
|
||||
const { anchor, uploadFile, workspaceId, fileService } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: async (path: string) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
}
|
||||
},
|
||||
upload: uploadFile,
|
||||
delete: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.deleteOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.deleteNewAsset(getEditorAssetSrc(anchor, src) ?? "");
|
||||
}
|
||||
},
|
||||
restore: async (src: string) => {
|
||||
if (src?.startsWith("http")) {
|
||||
await fileService.restoreOldEditorAsset(workspaceId, src);
|
||||
} else {
|
||||
await fileService.restoreNewAsset(anchor, src);
|
||||
}
|
||||
},
|
||||
cancel: fileService.cancelUpload,
|
||||
validation: {
|
||||
maxFileSize: MAX_FILE_SIZE,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @description this function returns the file handler required by the read-only editors
|
||||
*/
|
||||
export const getReadOnlyEditorFileHandlers = (
|
||||
args: Pick<TArgs, "anchor">
|
||||
): { getAssetSrc: TFileHandler["getAssetSrc"] } => {
|
||||
const { anchor } = args;
|
||||
|
||||
return {
|
||||
getAssetSrc: async (path: string) => {
|
||||
if (!path) return "";
|
||||
if (path?.startsWith("http")) {
|
||||
return path;
|
||||
} else {
|
||||
return getEditorAssetSrc(anchor, path) ?? "";
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -4,7 +4,6 @@ export * from "./datetime";
|
|||
export * from "./color";
|
||||
export * from "./common";
|
||||
export * from "./datetime";
|
||||
export * from "./editor";
|
||||
export * from "./emoji";
|
||||
export * from "./file";
|
||||
export * from "./issue";
|
||||
|
|
|
|||
|
|
@ -1,29 +1,58 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams } from "next/navigation";
|
||||
import useSWR from "swr";
|
||||
// ui
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload } from "@plane/types";
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// plane ui
|
||||
import { getButtonStyling } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
import { PageRoot } from "@/components/pages";
|
||||
import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
// hooks
|
||||
import { usePage, useProjectPages } from "@/hooks/store";
|
||||
import { useProjectPage, useProjectPages, useWorkspace } from "@/hooks/store";
|
||||
// plane web hooks
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
const projectPageService = new ProjectPageService();
|
||||
const projectPageVersionService = new ProjectPageVersionService();
|
||||
|
||||
const PageDetailsPage = observer(() => {
|
||||
const { workspaceSlug, projectId, pageId } = useParams();
|
||||
|
||||
// store hooks
|
||||
const { getPageById } = useProjectPages();
|
||||
const page = usePage(pageId?.toString() ?? "");
|
||||
const { id, name } = page;
|
||||
|
||||
const { createPage, getPageById } = useProjectPages();
|
||||
const page = useProjectPage(pageId?.toString() ?? "");
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
|
||||
const { id, name, updateDescription } = page;
|
||||
// entity search handler
|
||||
const fetchEntityCallback = useCallback(
|
||||
async (payload: TSearchEntityRequestPayload) =>
|
||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||
...payload,
|
||||
project_id: projectId?.toString() ?? "",
|
||||
}),
|
||||
[projectId, workspaceSlug]
|
||||
);
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
// fetch page details
|
||||
const { error: pageDetailsError } = useSWR(
|
||||
workspaceSlug && projectId && pageId ? `PAGE_DETAILS_${pageId}` : null,
|
||||
|
|
@ -36,6 +65,62 @@ const PageDetailsPage = observer(() => {
|
|||
revalidateOnReconnect: true,
|
||||
}
|
||||
);
|
||||
// page root handlers
|
||||
const pageRootHandlers: TPageRootHandlers = useMemo(
|
||||
() => ({
|
||||
create: createPage,
|
||||
fetchAllVersions: async (pageId) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await projectPageVersionService.fetchAllVersions(workspaceSlug.toString(), projectId.toString(), pageId);
|
||||
},
|
||||
fetchDescriptionBinary: async () => {
|
||||
if (!workspaceSlug || !projectId || !page.id) return;
|
||||
return await projectPageService.fetchDescriptionBinary(workspaceSlug.toString(), projectId.toString(), page.id);
|
||||
},
|
||||
fetchEntity: fetchEntityCallback,
|
||||
fetchVersionDetails: async (pageId, versionId) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await projectPageVersionService.fetchVersionById(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
pageId,
|
||||
versionId
|
||||
);
|
||||
},
|
||||
getRedirectionLink: (pageId) => `/${workspaceSlug}/projects/${projectId}/pages/${pageId}`,
|
||||
updateDescription,
|
||||
}),
|
||||
[createPage, fetchEntityCallback, page.id, projectId, updateDescription, workspaceSlug]
|
||||
);
|
||||
// page root config
|
||||
const pageRootConfig: TPageRootConfig = useMemo(
|
||||
() => ({
|
||||
fileHandler: getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
uploadFile: async (file) => {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug?.toString() ?? "",
|
||||
projectId?.toString() ?? "",
|
||||
{
|
||||
entity_identifier: id ?? "",
|
||||
entity_type: EFileAssetType.PAGE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
return asset_id;
|
||||
},
|
||||
workspaceId,
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
}),
|
||||
webhookConnectionParams: {
|
||||
documentType: "project_page",
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
},
|
||||
}),
|
||||
[id, maxFileSize, projectId, workspaceId, workspaceSlug]
|
||||
);
|
||||
|
||||
if ((!page || !id) && !pageDetailsError)
|
||||
return (
|
||||
|
|
@ -65,7 +150,12 @@ const PageDetailsPage = observer(() => {
|
|||
<PageHead title={name} />
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="relative h-full w-full flex-shrink-0 flex flex-col overflow-hidden">
|
||||
<PageRoot page={page} projectId={projectId.toString()} workspaceSlug={workspaceSlug.toString()} />
|
||||
<PageRoot
|
||||
config={pageRootConfig}
|
||||
handlers={pageRootHandlers}
|
||||
page={page}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<IssuePeekOverview />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { PageEditInformationPopover } from "@/components/pages";
|
|||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { usePage, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { useProjectPage, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// plane web components
|
||||
import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages";
|
||||
|
|
@ -32,7 +32,7 @@ export const PageDetailsHeader = observer(() => {
|
|||
const [isOpen, setIsOpen] = useState(false);
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const page = usePage(pageId?.toString() ?? "");
|
||||
const page = useProjectPage(pageId?.toString() ?? "");
|
||||
const { name, logo_props, updatePageLogo, owned_by } = page;
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { data: currentUser } = useUser();
|
||||
|
|
@ -169,7 +169,7 @@ export const PageDetailsHeader = observer(() => {
|
|||
</Header.LeftItem>
|
||||
<Header.RightItem>
|
||||
<PageEditInformationPopover page={page} />
|
||||
<PageDetailsHeaderExtraActions />
|
||||
<PageDetailsHeaderExtraActions page={page} />
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,11 +51,7 @@ const ProjectPagesPage = observer(() => {
|
|||
projectId={projectId.toString()}
|
||||
pageType={currentPageType()}
|
||||
>
|
||||
<PagesListRoot
|
||||
pageType={currentPageType()}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
/>
|
||||
<PagesListRoot pageType={currentPageType()} />
|
||||
</PagesListView>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,13 +11,12 @@ type Props = {
|
|||
handleInsertText: (insertOnNextLine: boolean) => void;
|
||||
handleRegenerate: () => Promise<void>;
|
||||
isRegenerating: boolean;
|
||||
projectId: string;
|
||||
response: string | undefined;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const AskPiMenu: React.FC<Props> = (props) => {
|
||||
const { handleInsertText, handleRegenerate, isRegenerating, projectId, response, workspaceSlug } = props;
|
||||
const { handleInsertText, handleRegenerate, isRegenerating, response, workspaceSlug } = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
|
|
@ -42,7 +41,6 @@ export const AskPiMenu: React.FC<Props> = (props) => {
|
|||
containerClassName="!p-0 border-none"
|
||||
editorClassName="!pl-0"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<div className="mt-3 flex items-center gap-4">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ type Props = {
|
|||
editorRef: RefObject<EditorRefApi>;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
|
|
@ -59,7 +58,7 @@ const TONES_LIST = [
|
|||
];
|
||||
|
||||
export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||
const { editorRef, isOpen, onClose, projectId, workspaceSlug } = props;
|
||||
const { editorRef, isOpen, onClose, workspaceSlug } = props;
|
||||
// states
|
||||
const [activeTask, setActiveTask] = useState<AI_EDITOR_TASKS | null>(null);
|
||||
const [response, setResponse] = useState<string | undefined>(undefined);
|
||||
|
|
@ -193,7 +192,6 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
|||
handleInsertText={handleInsertText}
|
||||
handleRegenerate={handleRegenerate}
|
||||
isRegenerating={isRegenerating}
|
||||
projectId={projectId}
|
||||
response={response}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
|
|
@ -218,7 +216,6 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
|||
containerClassName="!p-0 border-none"
|
||||
editorClassName="!pl-0"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<div className="mt-3 flex items-center gap-4">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1 +1,8 @@
|
|||
export const PageDetailsHeaderExtraActions = () => null;
|
||||
// store
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type TPageHeaderExtraActionsProps = {
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const PageDetailsHeaderExtraActions: React.FC<TPageHeaderExtraActionsProps> = () => null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
// editor
|
||||
import { TEmbedConfig } from "@plane/editor";
|
||||
// types
|
||||
import { TPageEmbedType } from "@plane/types";
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload, TSearchResponse } from "@plane/types";
|
||||
// plane web components
|
||||
import { IssueEmbedUpgradeCard } from "@/plane-web/components/pages";
|
||||
|
||||
export type TIssueEmbedHookProps = {
|
||||
fetchEmbedSuggestions?: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||
projectId?: string;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const useIssueEmbed = (workspaceSlug: string, projectId: string, queryType: TPageEmbedType = "issue") => {
|
||||
export const useIssueEmbed = (props: TIssueEmbedHookProps) => {
|
||||
const widgetCallback = () => <IssueEmbedUpgradeCard />;
|
||||
|
||||
const issueEmbedProps: TEmbedConfig["issue"] = {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import { getFileURL } from "@/helpers/file.helper";
|
|||
// hooks
|
||||
import { useMember } from "@/hooks/store";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const PageEditInformationPopover: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ import { DeletePageModal } from "@/components/pages";
|
|||
// helpers
|
||||
import { copyUrlToClipboard } from "@/helpers/string.helper";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
pageLink: string;
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
};
|
||||
|
|
@ -60,7 +60,7 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
|
|||
title: "Success!",
|
||||
message: `The page has been marked ${changedPageType} and moved to the ${changedPageType} section.`,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
|
@ -104,7 +104,7 @@ export const PageQuickActions: React.FC<Props> = observer((props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} pageId={page.id ?? ""} />
|
||||
<DeletePageModal isOpen={deletePageModal} onClose={() => setDeletePageModal(false)} page={page} />
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu placement="bottom-end" ellipsis closeOnSelect>
|
||||
{MENU_ITEMS.map((item) => {
|
||||
|
|
|
|||
|
|
@ -1,87 +1,94 @@
|
|||
import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
// document-editor
|
||||
import {
|
||||
CollaborativeDocumentEditorWithRef,
|
||||
EditorRefApi,
|
||||
TAIMenuProps,
|
||||
TDisplayConfig,
|
||||
TFileHandler,
|
||||
TRealtimeConfig,
|
||||
TServerHandler,
|
||||
} from "@plane/editor";
|
||||
// types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// components
|
||||
// plane types
|
||||
import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types";
|
||||
// plane ui
|
||||
import { Row } from "@plane/ui";
|
||||
// components
|
||||
import { EditorMentionsRoot } from "@/components/editor";
|
||||
import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages";
|
||||
// helpers
|
||||
import { cn, LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper";
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
import { generateRandomColor } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useUser, useWorkspace } from "@/hooks/store";
|
||||
import { useUser } from "@/hooks/store";
|
||||
import { useEditorMention } from "@/hooks/use-editor-mention";
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// plane web components
|
||||
import { EditorAIMenu } from "@/plane-web/components/pages";
|
||||
// plane web hooks
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { useFileSize } from "@/plane-web/hooks/use-file-size";
|
||||
import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed";
|
||||
// plane web services
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
// services init
|
||||
const workspaceService = new WorkspaceService();
|
||||
const fileService = new FileService();
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type TEditorBodyConfig = {
|
||||
fileHandler: TFileHandler;
|
||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||
};
|
||||
|
||||
export type TEditorBodyHandlers = {
|
||||
fetchEntity: (payload: TSearchEntityRequestPayload) => Promise<TSearchResponse>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
config: TEditorBodyConfig;
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
editorReady: boolean;
|
||||
handleConnectionStatus: Dispatch<SetStateAction<boolean>>;
|
||||
handleEditorReady: Dispatch<SetStateAction<boolean>>;
|
||||
page: IPage;
|
||||
handlers: TEditorBodyHandlers;
|
||||
page: TPageInstance;
|
||||
sidePeekVisible: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||
const { editorRef, handleConnectionStatus, handleEditorReady, page, sidePeekVisible } = props;
|
||||
// router
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const {
|
||||
config,
|
||||
editorRef,
|
||||
handleConnectionStatus,
|
||||
handleEditorReady,
|
||||
handlers,
|
||||
page,
|
||||
sidePeekVisible,
|
||||
workspaceSlug,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { data: currentUser } = useUser();
|
||||
const { getWorkspaceBySlug } = useWorkspace();
|
||||
// derived values
|
||||
const workspaceId = workspaceSlug ? (getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "") : "";
|
||||
const pageId = page?.id;
|
||||
const pageTitle = page?.name ?? "";
|
||||
const { isContentEditable, updateTitle } = page;
|
||||
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
fetchEmbedSuggestions: handlers.fetchEntity,
|
||||
workspaceSlug,
|
||||
});
|
||||
// use editor mention
|
||||
const { fetchMentions } = useEditorMention({
|
||||
searchEntity: async (payload) =>
|
||||
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
|
||||
...payload,
|
||||
project_id: projectId?.toString() ?? "",
|
||||
}),
|
||||
searchEntity: handlers.fetchEntity,
|
||||
});
|
||||
// editor flaggings
|
||||
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString());
|
||||
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug);
|
||||
// page filters
|
||||
const { fontSize, fontStyle, isFullWidth } = usePageFilters();
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "");
|
||||
// file size
|
||||
const { maxFileSize } = useFileSize();
|
||||
|
||||
const displayConfig: TDisplayConfig = {
|
||||
fontSize,
|
||||
fontStyle,
|
||||
};
|
||||
// derived values
|
||||
const displayConfig: TDisplayConfig = useMemo(
|
||||
() => ({
|
||||
fontSize,
|
||||
fontStyle,
|
||||
}),
|
||||
[fontSize, fontStyle]
|
||||
);
|
||||
|
||||
const getAIMenu = useCallback(
|
||||
({ isOpen, onClose }: TAIMenuProps) => (
|
||||
|
|
@ -89,11 +96,10 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
editorRef={editorRef}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
),
|
||||
[editorRef, projectId, workspaceSlug]
|
||||
[editorRef, workspaceSlug]
|
||||
);
|
||||
|
||||
const handleServerConnect = useCallback(() => {
|
||||
|
|
@ -123,17 +129,13 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
// Construct realtime config
|
||||
return {
|
||||
url: WS_LIVE_URL.toString(),
|
||||
queryParams: {
|
||||
workspaceSlug: workspaceSlug?.toString(),
|
||||
projectId: projectId?.toString(),
|
||||
documentType: "project_page",
|
||||
},
|
||||
queryParams: config.webhookConnectionParams,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating realtime config", error);
|
||||
return undefined;
|
||||
}
|
||||
}, [projectId, workspaceSlug]);
|
||||
}, [config.webhookConnectionParams]);
|
||||
|
||||
const userConfig = useMemo(
|
||||
() => ({
|
||||
|
|
@ -173,24 +175,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
|||
<CollaborativeDocumentEditorWithRef
|
||||
editable={isContentEditable}
|
||||
id={pageId}
|
||||
fileHandler={getEditorFileHandlers({
|
||||
maxFileSize,
|
||||
projectId: projectId?.toString() ?? "",
|
||||
uploadFile: async (file) => {
|
||||
const { asset_id } = await fileService.uploadProjectAsset(
|
||||
workspaceSlug?.toString() ?? "",
|
||||
projectId?.toString() ?? "",
|
||||
{
|
||||
entity_identifier: pageId,
|
||||
entity_type: EFileAssetType.PAGE_DESCRIPTION,
|
||||
},
|
||||
file
|
||||
);
|
||||
return asset_id;
|
||||
},
|
||||
workspaceId,
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
})}
|
||||
fileHandler={config.fileHandler}
|
||||
handleEditorReady={handleEditorReady}
|
||||
ref={editorRef}
|
||||
containerClassName="h-full p-0 pb-64"
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
|||
// hooks
|
||||
import useOnlineStatus from "@/hooks/use-online-status";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
handleDuplicatePage: () => void;
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { observer } from "mobx-react";
|
||||
import { EditorReadOnlyRefApi, EditorRefApi, IMarking } from "@plane/editor";
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// components
|
||||
import { Header, EHeaderVariant } from "@plane/ui";
|
||||
import { PageExtraOptions, PageSummaryPopover, PageToolbar } from "@/components/pages";
|
||||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
editorReady: boolean;
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
handleDuplicatePage: () => void;
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
setSidePeekVisible: (sidePeekState: boolean) => void;
|
||||
sidePeekVisible: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ import { usePageFilters } from "@/hooks/use-page-filters";
|
|||
import { useParseEditorContent } from "@/hooks/use-parse-editor-content";
|
||||
import { useQueryParams } from "@/hooks/use-query-params";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
editorRef: EditorRefApi | null;
|
||||
handleDuplicatePage: () => void;
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
};
|
||||
|
||||
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import { cn } from "@/helpers/common.helper";
|
|||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
editorReady: boolean;
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
handleDuplicatePage: () => void;
|
||||
page: IPage;
|
||||
page: TPageInstance;
|
||||
setSidePeekVisible: (sidePeekState: boolean) => void;
|
||||
sidePeekVisible: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,31 +4,45 @@ import { useSearchParams } from "next/navigation";
|
|||
// editor
|
||||
import { EditorRefApi } from "@plane/editor";
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
import { TDocumentPayload, TPage, TPageVersion } from "@plane/types";
|
||||
// ui
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
// components
|
||||
import { PageEditorHeaderRoot, PageEditorBody, PageVersionsOverlay, PagesVersionEditor } from "@/components/pages";
|
||||
import {
|
||||
PageEditorHeaderRoot,
|
||||
PageEditorBody,
|
||||
PageVersionsOverlay,
|
||||
PagesVersionEditor,
|
||||
TEditorBodyHandlers,
|
||||
TEditorBodyConfig,
|
||||
} from "@/components/pages";
|
||||
// hooks
|
||||
import { useProjectPages } from "@/hooks/store";
|
||||
import { useAppRouter } from "@/hooks/use-app-router";
|
||||
import { usePageFallback } from "@/hooks/use-page-fallback";
|
||||
import { useQueryParams } from "@/hooks/use-query-params";
|
||||
// services
|
||||
import { ProjectPageService, ProjectPageVersionService } from "@/services/page";
|
||||
const projectPageService = new ProjectPageService();
|
||||
const projectPageVersionService = new ProjectPageVersionService();
|
||||
// store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
export type TPageRootHandlers = {
|
||||
create: (payload: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
|
||||
fetchAllVersions: (pageId: string) => Promise<TPageVersion[] | undefined>;
|
||||
fetchDescriptionBinary: () => Promise<any>;
|
||||
fetchVersionDetails: (pageId: string, versionId: string) => Promise<TPageVersion | undefined>;
|
||||
getRedirectionLink: (pageId: string) => string;
|
||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||
} & TEditorBodyHandlers;
|
||||
|
||||
export type TPageRootConfig = TEditorBodyConfig;
|
||||
|
||||
type TPageRootProps = {
|
||||
page: IPage;
|
||||
projectId: string;
|
||||
config: TPageRootConfig;
|
||||
handlers: TPageRootHandlers;
|
||||
page: TPageInstance;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PageRoot = observer((props: TPageRootProps) => {
|
||||
const { projectId, workspaceSlug, page } = props;
|
||||
const { config, handlers, page, workspaceSlug } = props;
|
||||
// states
|
||||
const [editorReady, setEditorReady] = useState(false);
|
||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||
|
|
@ -40,25 +54,18 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
const router = useAppRouter();
|
||||
// search params
|
||||
const searchParams = useSearchParams();
|
||||
// store hooks
|
||||
const { createPage } = useProjectPages();
|
||||
// derived values
|
||||
const { access, description_html, name, isContentEditable, updateDescription } = page;
|
||||
const { access, description_html, name, isContentEditable } = page;
|
||||
// page fallback
|
||||
usePageFallback({
|
||||
editorRef,
|
||||
fetchPageDescription: async () => {
|
||||
if (!page.id) return;
|
||||
return await projectPageService.fetchDescriptionBinary(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
fetchPageDescription: handlers.fetchDescriptionBinary,
|
||||
hasConnectionFailed,
|
||||
updatePageDescription: async (data) => await updateDescription(data),
|
||||
updatePageDescription: handlers.updateDescription,
|
||||
});
|
||||
// update query params
|
||||
const { updateQueryParams } = useQueryParams();
|
||||
|
||||
const handleCreatePage = async (payload: Partial<TPage>) => await createPage(payload);
|
||||
|
||||
const handleDuplicatePage = async () => {
|
||||
const formData: Partial<TPage> = {
|
||||
name: "Copy of " + name,
|
||||
|
|
@ -66,8 +73,9 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
access,
|
||||
};
|
||||
|
||||
await handleCreatePage(formData)
|
||||
.then((res) => router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res?.id}`))
|
||||
await handlers
|
||||
.create(formData)
|
||||
.then((res) => router.push(handlers.getRedirectionLink(res?.id ?? "")))
|
||||
.catch(() =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
|
|
@ -105,23 +113,8 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
activeVersion={version}
|
||||
currentVersionDescription={currentVersionDescription ?? null}
|
||||
editorComponent={PagesVersionEditor}
|
||||
fetchAllVersions={async (pageId) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await projectPageVersionService.fetchAllVersions(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
pageId
|
||||
);
|
||||
}}
|
||||
fetchVersionDetails={async (pageId, versionId) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await projectPageVersionService.fetchVersionById(
|
||||
workspaceSlug.toString(),
|
||||
projectId.toString(),
|
||||
pageId,
|
||||
versionId
|
||||
);
|
||||
}}
|
||||
fetchAllVersions={handlers.fetchAllVersions}
|
||||
fetchVersionDetails={handlers.fetchVersionDetails}
|
||||
handleRestore={handleRestoreVersion}
|
||||
isOpen={isVersionsOverlayOpen}
|
||||
onClose={handleCloseVersionsOverlay}
|
||||
|
|
@ -137,12 +130,15 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
|||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
<PageEditorBody
|
||||
config={config}
|
||||
editorReady={editorReady}
|
||||
editorRef={editorRef}
|
||||
handleConnectionStatus={setHasConnectionFailed}
|
||||
handleEditorReady={setEditorReady}
|
||||
handlers={handlers}
|
||||
page={page}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import { EditorRefApi } from "@plane/editor";
|
|||
import { TextArea } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||
|
||||
type Props = {
|
||||
editorRef: React.RefObject<EditorRefApi>;
|
||||
readOnly: boolean;
|
||||
title: string;
|
||||
title: string | undefined;
|
||||
updateTitle: (title: string) => void;
|
||||
};
|
||||
|
||||
|
|
@ -33,7 +34,17 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
|||
return (
|
||||
<div className="relative w-full flex-shrink-0 md:pl-5 px-4">
|
||||
{readOnly ? (
|
||||
<h6 className={cn(titleClassName, "break-words pb-1.5")}>{title}</h6>
|
||||
<h6
|
||||
className={cn(
|
||||
titleClassName,
|
||||
{
|
||||
"text-custom-text-400": !title,
|
||||
},
|
||||
"break-words pb-1.5"
|
||||
)}
|
||||
>
|
||||
{getPageName(title)}
|
||||
</h6>
|
||||
) : (
|
||||
<>
|
||||
<TextArea
|
||||
|
|
@ -62,10 +73,10 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
|||
>
|
||||
<span
|
||||
className={cn({
|
||||
"text-red-500": title.length > 255,
|
||||
"text-red-500": title && title.length > 255,
|
||||
})}
|
||||
>
|
||||
{title.length}
|
||||
{title?.length}
|
||||
</span>
|
||||
/255
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,19 +11,17 @@ import { PageQuickActions } from "@/components/pages/dropdowns";
|
|||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useMember, usePage } from "@/hooks/store";
|
||||
import { useMember } from "@/hooks/store";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
pageId: string;
|
||||
page: TPageInstance;
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
};
|
||||
|
||||
export const BlockItemAction: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, pageId, parentRef } = props;
|
||||
const { page, parentRef } = props;
|
||||
// store hooks
|
||||
const page = usePage(pageId);
|
||||
const { getUserDetails } = useMember();
|
||||
// derived values
|
||||
const {
|
||||
|
|
@ -34,6 +32,7 @@ export const BlockItemAction: FC<Props> = observer((props) => {
|
|||
canCurrentUserFavoritePage,
|
||||
addToFavorites,
|
||||
removePageFromFavorites,
|
||||
getRedirectionLink,
|
||||
} = page;
|
||||
const ownerDetails = owned_by ? getUserDetails(owned_by) : undefined;
|
||||
|
||||
|
|
@ -94,11 +93,7 @@ export const BlockItemAction: FC<Props> = observer((props) => {
|
|||
)}
|
||||
|
||||
{/* quick actions dropdown */}
|
||||
<PageQuickActions
|
||||
parentRef={parentRef}
|
||||
page={page}
|
||||
pageLink={`${workspaceSlug}/projects/${projectId}/pages/${pageId}`}
|
||||
/>
|
||||
<PageQuickActions parentRef={parentRef} page={page} pageLink={getRedirectionLink()} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,22 +10,23 @@ import { BlockItemAction } from "@/components/pages/list";
|
|||
// helpers
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { usePage } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import { TUsePage } from "@/store/pages/base-page";
|
||||
|
||||
type TPageListBlock = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
pageId: string;
|
||||
usePage: TUsePage;
|
||||
};
|
||||
|
||||
export const PageListBlock: FC<TPageListBlock> = observer((props) => {
|
||||
const { workspaceSlug, projectId, pageId } = props;
|
||||
const { pageId, usePage } = props;
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// hooks
|
||||
const { name, logo_props } = usePage(pageId);
|
||||
const page = usePage(pageId);
|
||||
const { isMobile } = usePlatformOS();
|
||||
// derived values
|
||||
const { name, logo_props, getRedirectionLink } = page;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
|
|
@ -39,10 +40,8 @@ export const PageListBlock: FC<TPageListBlock> = observer((props) => {
|
|||
</>
|
||||
}
|
||||
title={getPageName(name)}
|
||||
itemLink={`/${workspaceSlug}/projects/${projectId}/pages/${pageId}`}
|
||||
actionableItems={
|
||||
<BlockItemAction workspaceSlug={workspaceSlug} projectId={projectId} pageId={pageId} parentRef={parentRef} />
|
||||
}
|
||||
itemLink={getRedirectionLink()}
|
||||
actionableItems={<BlockItemAction page={page} parentRef={parentRef} />}
|
||||
isMobile={isMobile}
|
||||
parentRef={parentRef}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,18 +5,16 @@ import { TPageNavigationTabs } from "@plane/types";
|
|||
// components
|
||||
import { ListLayout } from "@/components/core/list";
|
||||
// hooks
|
||||
import { useProjectPages } from "@/hooks/store";
|
||||
import { useProjectPage, useProjectPages } from "@/hooks/store";
|
||||
// components
|
||||
import { PageListBlock } from "./";
|
||||
|
||||
type TPagesListRoot = {
|
||||
pageType: TPageNavigationTabs;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const PagesListRoot: FC<TPagesListRoot> = observer((props) => {
|
||||
const { pageType, projectId, workspaceSlug } = props;
|
||||
const { pageType } = props;
|
||||
// store hooks
|
||||
const { getCurrentProjectFilteredPageIds } = useProjectPages();
|
||||
// derived values
|
||||
|
|
@ -26,7 +24,7 @@ export const PagesListRoot: FC<TPagesListRoot> = observer((props) => {
|
|||
return (
|
||||
<ListLayout>
|
||||
{filteredPageIds.map((pageId) => (
|
||||
<PageListBlock key={pageId} workspaceSlug={workspaceSlug} projectId={projectId} pageId={pageId} />
|
||||
<PageListBlock key={pageId} pageId={pageId} usePage={useProjectPage} />
|
||||
))}
|
||||
</ListLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,26 +7,25 @@ import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
|||
// constants
|
||||
import { PAGE_DELETED } from "@/constants/event-tracker";
|
||||
// hooks
|
||||
import { useEventTracker, usePage, useProjectPages } from "@/hooks/store";
|
||||
import { useEventTracker, useProjectPages } from "@/hooks/store";
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
type TConfirmPageDeletionProps = {
|
||||
page: TPageInstance;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((props) => {
|
||||
const { pageId, isOpen, onClose } = props;
|
||||
const { page, isOpen, onClose } = props;
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const { removePage } = useProjectPages();
|
||||
const { capturePageEvent } = useEventTracker();
|
||||
const page = usePage(pageId);
|
||||
|
||||
if (!page) return null;
|
||||
|
||||
const { name } = page;
|
||||
if (!page || !page.id) return null;
|
||||
// derived values
|
||||
const { id: pageId, name } = page;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsDeleting(false);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ export const PagesVersionEditor: React.FC<TVersionEditorProps> = observer((props
|
|||
// editor flaggings
|
||||
const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString() ?? "");
|
||||
// issue-embed
|
||||
const { issueEmbedProps } = useIssueEmbed(workspaceSlug?.toString() ?? "", projectId?.toString() ?? "");
|
||||
const { issueEmbedProps } = useIssueEmbed({
|
||||
projectId: projectId?.toString() ?? "",
|
||||
workspaceSlug: workspaceSlug?.toString() ?? "",
|
||||
});
|
||||
// page filters
|
||||
const { fontSize, fontStyle } = usePageFilters();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// mobx store
|
||||
import { IPage } from "@/store/pages/page";
|
||||
import { TUsePage } from "@/store/pages/base-page";
|
||||
// store
|
||||
import { TProjectPage } from "@/store/pages/project-page";
|
||||
|
||||
export const usePage = (pageId: string | undefined): IPage => {
|
||||
export const useProjectPage: TUsePage = (pageId: string | undefined): TProjectPage => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
||||
|
||||
if (!pageId) return {} as IPage;
|
||||
if (!pageId) return {} as TProjectPage;
|
||||
|
||||
return context.projectPages.data?.[pageId] ?? {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
// plane editor
|
||||
import { EditorReadOnlyRefApi, EditorRefApi, TDocumentEventsServer } from "@plane/editor";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsClient, getServerEventName } from "@plane/editor/lib";
|
||||
// plane ui
|
||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { IPage } from "@/store/pages/page";
|
||||
// store
|
||||
import { TPageInstance } from "@/store/pages/base-page";
|
||||
|
||||
// Better type naming and structure
|
||||
type CollaborativeAction = {
|
||||
|
|
@ -14,7 +17,10 @@ type CollaborativeActionEvent =
|
|||
| { type: "sendMessageToServer"; message: TDocumentEventsServer }
|
||||
| { type: "receivedMessageFromServer"; message: TDocumentEventsClient };
|
||||
|
||||
export const useCollaborativePageActions = (editorRef: EditorRefApi | EditorReadOnlyRefApi | null, page: IPage) => {
|
||||
export const useCollaborativePageActions = (
|
||||
editorRef: EditorRefApi | EditorReadOnlyRefApi | null,
|
||||
page: TPageInstance
|
||||
) => {
|
||||
// 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);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
// helpers
|
||||
import { getPageName } from "@/helpers/page.helper";
|
||||
// hooks
|
||||
import { useProject, usePage, useProjectView, useCycle, useModule } from "@/hooks/store";
|
||||
import { useProject, useProjectPage, useProjectView, useCycle, useModule } from "@/hooks/store";
|
||||
|
||||
export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorite) => {
|
||||
const favoriteItemId = favorite?.entity_data?.id;
|
||||
|
|
@ -23,7 +23,7 @@ export const useFavoriteItemDetails = (workspaceSlug: string, favorite: IFavorit
|
|||
const { getModuleById } = useModule();
|
||||
|
||||
// derived values
|
||||
const pageDetail = usePage(favoriteItemId ?? "");
|
||||
const pageDetail = useProjectPage(favoriteItemId ?? "");
|
||||
const viewDetails = getViewById(favoriteItemId ?? "");
|
||||
const cycleDetail = getCycleById(favoriteItemId ?? "");
|
||||
const moduleDetail = getModuleById(favoriteItemId ?? "");
|
||||
|
|
|
|||
|
|
@ -47,7 +47,12 @@ export class ProjectPageService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateAccess(workspaceSlug: string, projectId: string, pageId: string, data: Partial<TPage>): Promise<void> {
|
||||
async updateAccess(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
data: Pick<TPage, "access">
|
||||
): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/access/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
|
|
@ -146,7 +151,7 @@ export class ProjectPageService extends APIService {
|
|||
});
|
||||
}
|
||||
|
||||
async updateDescriptionYJS(
|
||||
async updateDescription(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
|
|
|
|||
|
|
@ -4,32 +4,21 @@ import { action, computed, makeObservable, observable, reaction, runInAction } f
|
|||
import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
|
||||
// constants
|
||||
import { EPageAccess } from "@/constants/page";
|
||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||
// services
|
||||
import { ProjectPageService } from "@/services/page";
|
||||
// store
|
||||
import { CoreRootStore } from "../root.store";
|
||||
// plane web store
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
|
||||
export interface IPage extends TPage {
|
||||
export type TBasePage = TPage & {
|
||||
// observables
|
||||
isSubmitting: TNameDescriptionLoader;
|
||||
// computed
|
||||
asJSON: TPage | undefined;
|
||||
isCurrentUserOwner: boolean; // it will give the user is the owner of the page or not
|
||||
canCurrentUserEditPage: boolean; // it will give the user permission to read the page or write the page
|
||||
canCurrentUserDuplicatePage: boolean;
|
||||
canCurrentUserLockPage: boolean;
|
||||
canCurrentUserChangeAccess: boolean;
|
||||
canCurrentUserArchivePage: boolean;
|
||||
canCurrentUserDeletePage: boolean;
|
||||
canCurrentUserFavoritePage: boolean;
|
||||
isContentEditable: boolean;
|
||||
isCurrentUserOwner: boolean;
|
||||
// helpers
|
||||
oldName: string;
|
||||
setIsSubmitting: (value: TNameDescriptionLoader) => void;
|
||||
cleanup: () => void;
|
||||
// actions
|
||||
update: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
||||
update: (pageData: Partial<TPage>) => Promise<Partial<TPage> | undefined>;
|
||||
updateTitle: (title: string) => void;
|
||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||
makePublic: () => Promise<void>;
|
||||
|
|
@ -41,9 +30,39 @@ export interface IPage extends TPage {
|
|||
updatePageLogo: (logo_props: TLogoProps) => Promise<void>;
|
||||
addToFavorites: () => Promise<void>;
|
||||
removePageFromFavorites: () => Promise<void>;
|
||||
}
|
||||
};
|
||||
|
||||
export class Page implements IPage {
|
||||
export type TBasePagePermissions = {
|
||||
canCurrentUserEditPage: boolean;
|
||||
canCurrentUserDuplicatePage: boolean;
|
||||
canCurrentUserLockPage: boolean;
|
||||
canCurrentUserChangeAccess: boolean;
|
||||
canCurrentUserArchivePage: boolean;
|
||||
canCurrentUserDeletePage: boolean;
|
||||
canCurrentUserFavoritePage: boolean;
|
||||
isContentEditable: boolean;
|
||||
};
|
||||
|
||||
export type TBasePageServices = {
|
||||
update: (payload: Partial<TPage>) => Promise<Partial<TPage>>;
|
||||
updateDescription: (document: TDocumentPayload) => Promise<void>;
|
||||
updateAccess: (payload: Pick<TPage, "access">) => Promise<void>;
|
||||
lock: () => Promise<void>;
|
||||
unlock: () => Promise<void>;
|
||||
archive: () => Promise<{
|
||||
archived_at: string;
|
||||
}>;
|
||||
restore: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type TPageInstance = TBasePage &
|
||||
TBasePagePermissions & {
|
||||
getRedirectionLink: () => string;
|
||||
};
|
||||
|
||||
export type TUsePage = (pageId: string | undefined) => TPageInstance;
|
||||
|
||||
export class BasePage implements TBasePage {
|
||||
// loaders
|
||||
isSubmitting: TNameDescriptionLoader = "saved";
|
||||
// page properties
|
||||
|
|
@ -60,22 +79,24 @@ export class Page implements IPage {
|
|||
is_locked: boolean;
|
||||
archived_at: string | null | undefined;
|
||||
workspace: string | undefined;
|
||||
project_ids: string[] | undefined;
|
||||
project_ids?: string[] | undefined;
|
||||
team: string | null | undefined;
|
||||
created_by: string | undefined;
|
||||
updated_by: string | undefined;
|
||||
created_at: Date | undefined;
|
||||
updated_at: Date | undefined;
|
||||
// helpers
|
||||
oldName: string = "";
|
||||
// services
|
||||
services: TBasePageServices;
|
||||
// reactions
|
||||
disposers: Array<() => void> = [];
|
||||
// services
|
||||
pageService: ProjectPageService;
|
||||
// root store
|
||||
rootStore: CoreRootStore;
|
||||
rootStore: RootStore;
|
||||
constructor(
|
||||
private store: CoreRootStore,
|
||||
page: TPage
|
||||
private store: RootStore,
|
||||
page: TPage,
|
||||
services: TBasePageServices
|
||||
) {
|
||||
this.id = page?.id || undefined;
|
||||
this.name = page?.name;
|
||||
|
|
@ -91,6 +112,7 @@ export class Page implements IPage {
|
|||
this.archived_at = page?.archived_at || undefined;
|
||||
this.workspace = page?.workspace || undefined;
|
||||
this.project_ids = page?.project_ids || undefined;
|
||||
this.team = page?.team || undefined;
|
||||
this.created_by = page?.created_by || undefined;
|
||||
this.updated_by = page?.updated_by || undefined;
|
||||
this.created_at = page?.created_at || undefined;
|
||||
|
|
@ -126,14 +148,6 @@ export class Page implements IPage {
|
|||
// computed
|
||||
asJSON: computed,
|
||||
isCurrentUserOwner: computed,
|
||||
canCurrentUserEditPage: computed,
|
||||
canCurrentUserDuplicatePage: computed,
|
||||
canCurrentUserLockPage: computed,
|
||||
canCurrentUserChangeAccess: computed,
|
||||
canCurrentUserArchivePage: computed,
|
||||
canCurrentUserDeletePage: computed,
|
||||
canCurrentUserFavoritePage: computed,
|
||||
isContentEditable: computed,
|
||||
// actions
|
||||
update: action,
|
||||
updateTitle: action,
|
||||
|
|
@ -149,16 +163,15 @@ export class Page implements IPage {
|
|||
removePageFromFavorites: action,
|
||||
});
|
||||
|
||||
this.pageService = new ProjectPageService();
|
||||
this.rootStore = store;
|
||||
this.services = services;
|
||||
|
||||
const titleDisposer = reaction(
|
||||
() => this.name,
|
||||
(name) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return;
|
||||
this.isSubmitting = "submitting";
|
||||
this.pageService
|
||||
.update(workspaceSlug, projectId, this.id, {
|
||||
this.services
|
||||
.update({
|
||||
name,
|
||||
})
|
||||
.catch(() =>
|
||||
|
|
@ -174,7 +187,6 @@ export class Page implements IPage {
|
|||
},
|
||||
{ delay: 2000 }
|
||||
);
|
||||
|
||||
this.disposers.push(titleDisposer);
|
||||
}
|
||||
|
||||
|
|
@ -195,6 +207,7 @@ export class Page implements IPage {
|
|||
archived_at: this.archived_at,
|
||||
workspace: this.workspace,
|
||||
project_ids: this.project_ids,
|
||||
team: this.team,
|
||||
created_by: this.created_by,
|
||||
updated_by: this.updated_by,
|
||||
created_at: this.created_at,
|
||||
|
|
@ -208,116 +221,6 @@ export class Page implements IPage {
|
|||
return this.owned_by === currentUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can edit the page
|
||||
*/
|
||||
get canCurrentUserEditPage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
const isPagePublic = this.access === EPageAccess.PUBLIC;
|
||||
return (
|
||||
(isPagePublic && !!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER) ||
|
||||
(!isPagePublic && this.isCurrentUserOwner)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can create a duplicate the page
|
||||
*/
|
||||
get canCurrentUserDuplicatePage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return !!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can lock the page
|
||||
*/
|
||||
get canCurrentUserLockPage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can change the access of the page
|
||||
*/
|
||||
get canCurrentUserChangeAccess() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can archive the page
|
||||
*/
|
||||
get canCurrentUserArchivePage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can delete the page
|
||||
*/
|
||||
get canCurrentUserDeletePage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return this.isCurrentUserOwner || currentUserProjectRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can favorite the page
|
||||
*/
|
||||
get canCurrentUserFavoritePage() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
const currentUserProjectRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
return !!currentUserProjectRole && currentUserProjectRole >= EUserPermissions.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the page can be edited
|
||||
*/
|
||||
get isContentEditable() {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
|
||||
const isOwner = this.isCurrentUserOwner;
|
||||
const currentUserRole = this.store.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
const isPublic = this.access === EPageAccess.PUBLIC;
|
||||
const isArchived = this.archived_at;
|
||||
const isLocked = this.is_locked;
|
||||
|
||||
return (
|
||||
!isArchived &&
|
||||
!isLocked &&
|
||||
(isOwner || (isPublic && !!currentUserRole && currentUserRole >= EUserPermissions.MEMBER))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description update the submitting state
|
||||
* @param value
|
||||
|
|
@ -339,9 +242,6 @@ export class Page implements IPage {
|
|||
* @param {Partial<TPage>} pageData
|
||||
*/
|
||||
update = async (pageData: Partial<TPage>) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const currentPage = this.asJSON;
|
||||
try {
|
||||
runInAction(() => {
|
||||
|
|
@ -351,7 +251,7 @@ export class Page implements IPage {
|
|||
});
|
||||
});
|
||||
|
||||
await this.pageService.update(workspaceSlug, projectId, this.id, currentPage);
|
||||
return await this.services.update(currentPage);
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
Object.keys(pageData).forEach((key) => {
|
||||
|
|
@ -377,16 +277,13 @@ export class Page implements IPage {
|
|||
* @param {TDocumentPayload} document
|
||||
*/
|
||||
updateDescription = async (document: TDocumentPayload) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const currentDescription = this.description_html;
|
||||
runInAction(() => {
|
||||
this.description_html = document.description_html;
|
||||
});
|
||||
|
||||
try {
|
||||
await this.pageService.updateDescriptionYJS(workspaceSlug, projectId, this.id, document);
|
||||
await this.services.updateDescription(document);
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.description_html = currentDescription;
|
||||
|
|
@ -399,14 +296,11 @@ export class Page implements IPage {
|
|||
* @description make the page public
|
||||
*/
|
||||
makePublic = async () => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const pageAccess = this.access;
|
||||
runInAction(() => (this.access = EPageAccess.PUBLIC));
|
||||
|
||||
try {
|
||||
await this.pageService.updateAccess(workspaceSlug, projectId, this.id, {
|
||||
await this.services.updateAccess({
|
||||
access: EPageAccess.PUBLIC,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -421,14 +315,11 @@ export class Page implements IPage {
|
|||
* @description make the page private
|
||||
*/
|
||||
makePrivate = async () => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const pageAccess = this.access;
|
||||
runInAction(() => (this.access = EPageAccess.PRIVATE));
|
||||
|
||||
try {
|
||||
await this.pageService.updateAccess(workspaceSlug, projectId, this.id, {
|
||||
await this.services.updateAccess({
|
||||
access: EPageAccess.PRIVATE,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -443,14 +334,11 @@ export class Page implements IPage {
|
|||
* @description lock the page
|
||||
*/
|
||||
lock = async (shouldSync: boolean = true) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const pageIsLocked = this.is_locked;
|
||||
runInAction(() => (this.is_locked = true));
|
||||
|
||||
if (shouldSync) {
|
||||
await this.pageService.lock(workspaceSlug, projectId, this.id).catch((error) => {
|
||||
await this.services.lock().catch((error) => {
|
||||
runInAction(() => {
|
||||
this.is_locked = pageIsLocked;
|
||||
});
|
||||
|
|
@ -463,14 +351,11 @@ export class Page implements IPage {
|
|||
* @description unlock the page
|
||||
*/
|
||||
unlock = async (shouldSync: boolean = true) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const pageIsLocked = this.is_locked;
|
||||
runInAction(() => (this.is_locked = false));
|
||||
|
||||
if (shouldSync) {
|
||||
await this.pageService.unlock(workspaceSlug, projectId, this.id).catch((error) => {
|
||||
await this.services.unlock().catch((error) => {
|
||||
runInAction(() => {
|
||||
this.is_locked = pageIsLocked;
|
||||
});
|
||||
|
|
@ -483,8 +368,7 @@ export class Page implements IPage {
|
|||
* @description archive the page
|
||||
*/
|
||||
archive = async (shouldSync: boolean = true) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
if (!this.id) return undefined;
|
||||
|
||||
try {
|
||||
runInAction(() => {
|
||||
|
|
@ -494,7 +378,7 @@ export class Page implements IPage {
|
|||
if (this.rootStore.favorite.entityMap[this.id]) this.rootStore.favorite.removeFavoriteFromStore(this.id);
|
||||
|
||||
if (shouldSync) {
|
||||
const response = await this.pageService.archive(workspaceSlug, projectId, this.id);
|
||||
const response = await this.services.archive();
|
||||
runInAction(() => {
|
||||
this.archived_at = response.archived_at;
|
||||
});
|
||||
|
|
@ -511,9 +395,6 @@ export class Page implements IPage {
|
|||
* @description restore the page
|
||||
*/
|
||||
restore = async (shouldSync: boolean = true) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
|
||||
const archivedAtBeforeRestore = this.archived_at;
|
||||
|
||||
try {
|
||||
|
|
@ -522,7 +403,7 @@ export class Page implements IPage {
|
|||
});
|
||||
|
||||
if (shouldSync) {
|
||||
await this.pageService.restore(workspaceSlug, projectId, this.id);
|
||||
await this.services.restore();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
@ -533,9 +414,7 @@ export class Page implements IPage {
|
|||
};
|
||||
|
||||
updatePageLogo = async (logo_props: TLogoProps) => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
await this.pageService.update(workspaceSlug, projectId, this.id, {
|
||||
await this.services.update({
|
||||
logo_props,
|
||||
});
|
||||
runInAction(() => {
|
||||
|
|
@ -547,8 +426,9 @@ export class Page implements IPage {
|
|||
* @description add the page to favorites
|
||||
*/
|
||||
addToFavorites = async () => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
const { workspaceSlug } = this.store.router;
|
||||
const projectId = this.project_ids?.[0] ?? null;
|
||||
if (!workspaceSlug || !this.id) return undefined;
|
||||
|
||||
const pageIsFavorite = this.is_favorite;
|
||||
runInAction(() => {
|
||||
|
|
@ -573,8 +453,8 @@ export class Page implements IPage {
|
|||
* @description remove the page from favorites
|
||||
*/
|
||||
removePageFromFavorites = async () => {
|
||||
const { workspaceSlug, projectId } = this.store.router;
|
||||
if (!workspaceSlug || !projectId || !this.id) return undefined;
|
||||
const { workspaceSlug } = this.store.router;
|
||||
if (!workspaceSlug || !this.id) return undefined;
|
||||
|
||||
const pageIsFavorite = this.is_favorite;
|
||||
runInAction(() => {
|
||||
|
|
@ -8,11 +8,13 @@ import { TPage, TPageFilters, TPageNavigationTabs } from "@plane/types";
|
|||
import { filterPagesByPageType, getPageName, orderPages, shouldFilterPage } from "@/helpers/page.helper";
|
||||
// plane web constants
|
||||
import { EUserPermissions } from "@/plane-web/constants";
|
||||
// plane web store
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
// services
|
||||
import { ProjectPageService } from "@/services/page";
|
||||
// store
|
||||
import { IPage, Page } from "@/store/pages/page";
|
||||
import { CoreRootStore } from "../root.store";
|
||||
import { ProjectPage, TProjectPage } from "./project-page";
|
||||
|
||||
type TLoader = "init-loader" | "mutation-loader" | undefined;
|
||||
|
||||
|
|
@ -21,7 +23,7 @@ type TError = { title: string; description: string };
|
|||
export interface IProjectPageStore {
|
||||
// observables
|
||||
loader: TLoader;
|
||||
data: Record<string, IPage>; // pageId => Page
|
||||
data: Record<string, TProjectPage>; // pageId => Page
|
||||
error: TError | undefined;
|
||||
filters: TPageFilters;
|
||||
// computed
|
||||
|
|
@ -30,7 +32,7 @@ export interface IProjectPageStore {
|
|||
// helper actions
|
||||
getCurrentProjectPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
getCurrentProjectFilteredPageIds: (pageType: TPageNavigationTabs) => string[] | undefined;
|
||||
pageById: (pageId: string) => IPage | undefined;
|
||||
pageById: (pageId: string) => TProjectPage | undefined;
|
||||
updateFilters: <T extends keyof TPageFilters>(filterKey: T, filterValue: TPageFilters[T]) => void;
|
||||
clearAllFilters: () => void;
|
||||
// actions
|
||||
|
|
@ -47,7 +49,7 @@ export interface IProjectPageStore {
|
|||
export class ProjectPageStore implements IProjectPageStore {
|
||||
// observables
|
||||
loader: TLoader = "init-loader";
|
||||
data: Record<string, IPage> = {}; // pageId => Page
|
||||
data: Record<string, TProjectPage> = {}; // pageId => Page
|
||||
error: TError | undefined = undefined;
|
||||
filters: TPageFilters = {
|
||||
searchQuery: "",
|
||||
|
|
@ -58,7 +60,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||
service: ProjectPageService;
|
||||
rootStore: CoreRootStore;
|
||||
|
||||
constructor(private store: CoreRootStore) {
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable.ref,
|
||||
|
|
@ -184,7 +186,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||
|
||||
const pages = await this.service.fetchAll(workspaceSlug, projectId);
|
||||
runInAction(() => {
|
||||
for (const page of pages) if (page?.id) set(this.data, [page.id], new Page(this.store, page));
|
||||
for (const page of pages) if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
|
||||
this.loader = undefined;
|
||||
});
|
||||
|
||||
|
|
@ -217,7 +219,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
||||
runInAction(() => {
|
||||
if (page?.id) set(this.data, [page.id], new Page(this.store, page));
|
||||
if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
|
||||
this.loader = undefined;
|
||||
});
|
||||
|
||||
|
|
@ -250,7 +252,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
|||
|
||||
const page = await this.service.create(workspaceSlug, projectId, pageData);
|
||||
runInAction(() => {
|
||||
if (page?.id) set(this.data, [page.id], new Page(this.store, page));
|
||||
if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
|
||||
this.loader = undefined;
|
||||
});
|
||||
|
||||
|
|
|
|||
163
web/core/store/pages/project-page.ts
Normal file
163
web/core/store/pages/project-page.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { computed, makeObservable } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
// constants
|
||||
import { EPageAccess } from "@/constants/page";
|
||||
import { EUserPermissions } from "@/plane-web/constants/user-permissions";
|
||||
// plane web store
|
||||
import { RootStore } from "@/plane-web/store/root.store";
|
||||
// services
|
||||
import { ProjectPageService } from "@/services/page";
|
||||
const projectPageService = new ProjectPageService();
|
||||
// store
|
||||
import { BasePage, TPageInstance } from "./base-page";
|
||||
|
||||
export type TProjectPage = TPageInstance;
|
||||
|
||||
export class ProjectPage extends BasePage implements TProjectPage {
|
||||
constructor(store: RootStore, page: TPage) {
|
||||
// required fields for API calls
|
||||
const { workspaceSlug } = store.router;
|
||||
const projectId = page.project_ids?.[0];
|
||||
// initialize base instance
|
||||
super(store, page, {
|
||||
update: async (payload) => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
return await projectPageService.update(workspaceSlug, projectId, page.id, payload);
|
||||
},
|
||||
updateDescription: async (document) => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
await projectPageService.updateDescription(workspaceSlug, projectId, page.id, document);
|
||||
},
|
||||
updateAccess: async (payload) => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
await projectPageService.updateAccess(workspaceSlug, projectId, page.id, payload);
|
||||
},
|
||||
lock: async () => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
await projectPageService.lock(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
unlock: async () => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
await projectPageService.unlock(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
archive: async () => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
return await projectPageService.archive(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
restore: async () => {
|
||||
if (!workspaceSlug || !projectId || !page.id) throw new Error("Missing required fields.");
|
||||
await projectPageService.restore(workspaceSlug, projectId, page.id);
|
||||
},
|
||||
});
|
||||
makeObservable(this, {
|
||||
// computed
|
||||
canCurrentUserEditPage: computed,
|
||||
canCurrentUserDuplicatePage: computed,
|
||||
canCurrentUserLockPage: computed,
|
||||
canCurrentUserChangeAccess: computed,
|
||||
canCurrentUserArchivePage: computed,
|
||||
canCurrentUserDeletePage: computed,
|
||||
canCurrentUserFavoritePage: computed,
|
||||
isContentEditable: computed,
|
||||
});
|
||||
}
|
||||
|
||||
private getHighestRoleAcrossProjects = computedFn((): EUserPermissions | undefined => {
|
||||
const { workspaceSlug } = this.rootStore.router;
|
||||
if (!workspaceSlug || !this.project_ids?.length) return;
|
||||
let highestRole: EUserPermissions | undefined = undefined;
|
||||
this.project_ids.map((projectId) => {
|
||||
const currentUserProjectRole = this.rootStore.user.permission.projectPermissionsByWorkspaceSlugAndProjectId(
|
||||
workspaceSlug?.toString() || "",
|
||||
projectId?.toString() || ""
|
||||
);
|
||||
if (currentUserProjectRole) {
|
||||
if (!highestRole) highestRole = currentUserProjectRole;
|
||||
else if (currentUserProjectRole > highestRole) highestRole = currentUserProjectRole;
|
||||
}
|
||||
});
|
||||
return highestRole;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can edit the page
|
||||
*/
|
||||
get canCurrentUserEditPage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
const isPagePublic = this.access === EPageAccess.PUBLIC;
|
||||
return (
|
||||
(isPagePublic && !!highestRole && highestRole >= EUserPermissions.MEMBER) ||
|
||||
(!isPagePublic && this.isCurrentUserOwner)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can create a duplicate the page
|
||||
*/
|
||||
get canCurrentUserDuplicatePage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return !!highestRole && highestRole >= EUserPermissions.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can lock the page
|
||||
*/
|
||||
get canCurrentUserLockPage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can change the access of the page
|
||||
*/
|
||||
get canCurrentUserChangeAccess() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can archive the page
|
||||
*/
|
||||
get canCurrentUserArchivePage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can delete the page
|
||||
*/
|
||||
get canCurrentUserDeletePage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return this.isCurrentUserOwner || highestRole === EUserPermissions.ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the current logged in user can favorite the page
|
||||
*/
|
||||
get canCurrentUserFavoritePage() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
return !!highestRole && highestRole >= EUserPermissions.MEMBER;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if the page can be edited
|
||||
*/
|
||||
get isContentEditable() {
|
||||
const highestRole = this.getHighestRoleAcrossProjects();
|
||||
const isOwner = this.isCurrentUserOwner;
|
||||
const isPublic = this.access === EPageAccess.PUBLIC;
|
||||
const isArchived = this.archived_at;
|
||||
const isLocked = this.is_locked;
|
||||
|
||||
return (
|
||||
!isArchived && !isLocked && (isOwner || (isPublic && !!highestRole && highestRole >= EUserPermissions.MEMBER))
|
||||
);
|
||||
}
|
||||
|
||||
getRedirectionLink = computedFn(() => {
|
||||
const { workspaceSlug } = this.rootStore.router;
|
||||
return `/${workspaceSlug}/projects/${this.project_ids?.[0]}/pages/${this.id}`;
|
||||
});
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ export class CoreRootStore {
|
|||
this.eventTracker = new EventTrackerStore(this);
|
||||
this.multipleSelect = new MultipleSelectStore();
|
||||
this.projectInbox = new ProjectInboxStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
this.projectPages = new ProjectPageStore(this as unknown as RootStore);
|
||||
this.projectEstimate = new ProjectEstimateStore(this);
|
||||
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
||||
this.favorite = new FavoriteStore(this);
|
||||
|
|
@ -112,7 +112,7 @@ export class CoreRootStore {
|
|||
this.dashboard = new DashboardStore(this);
|
||||
this.eventTracker = new EventTrackerStore(this);
|
||||
this.projectInbox = new ProjectInboxStore(this);
|
||||
this.projectPages = new ProjectPageStore(this);
|
||||
this.projectPages = new ProjectPageStore(this as unknown as RootStore);
|
||||
this.multipleSelect = new MultipleSelectStore();
|
||||
this.projectEstimate = new ProjectEstimateStore(this);
|
||||
this.workspaceNotification = new WorkspaceNotificationStore(this);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue