From 8d7425a3b71bc1c220d406dfffc073b95e0972e0 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Fri, 27 Dec 2024 20:41:38 +0530 Subject: [PATCH] [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 --- packages/editor/src/core/types/editor.ts | 9 +- packages/types/src/pages.d.ts | 20 +- packages/types/src/search.d.ts | 18 +- packages/utils/src/editor.ts | 103 ------- packages/utils/src/index.ts | 1 - .../pages/(detail)/[pageId]/page.tsx | 110 +++++++- .../[projectId]/pages/(detail)/header.tsx | 6 +- .../[projectId]/pages/(list)/page.tsx | 6 +- .../pages/editor/ai/ask-pi-menu.tsx | 4 +- web/ce/components/pages/editor/ai/menu.tsx | 5 +- web/ce/components/pages/extra-actions.tsx | 9 +- web/ce/hooks/use-issue-embed.tsx | 12 +- .../dropdowns/edit-information-popover.tsx | 4 +- .../pages/dropdowns/quick-actions.tsx | 8 +- .../components/pages/editor/editor-body.tsx | 115 ++++---- .../pages/editor/header/extra-options.tsx | 4 +- .../pages/editor/header/mobile-root.tsx | 6 +- .../pages/editor/header/options-dropdown.tsx | 4 +- .../components/pages/editor/header/root.tsx | 4 +- .../components/pages/editor/page-root.tsx | 76 +++--- web/core/components/pages/editor/title.tsx | 19 +- .../pages/list/block-item-action.tsx | 17 +- web/core/components/pages/list/block.tsx | 17 +- web/core/components/pages/list/root.tsx | 8 +- .../pages/modals/delete-page-modal.tsx | 15 +- web/core/components/pages/version/editor.tsx | 5 +- web/core/hooks/store/pages/use-page.ts | 9 +- .../hooks/use-collaborative-page-actions.tsx | 10 +- web/core/hooks/use-favorite-item-details.tsx | 4 +- .../services/page/project-page.service.ts | 9 +- .../store/pages/{page.ts => base-page.ts} | 252 +++++------------- web/core/store/pages/project-page.store.ts | 18 +- web/core/store/pages/project-page.ts | 163 +++++++++++ web/core/store/root.store.ts | 4 +- 34 files changed, 553 insertions(+), 521 deletions(-) delete mode 100644 packages/utils/src/editor.ts rename web/core/store/pages/{page.ts => base-page.ts} (58%) create mode 100644 web/core/store/pages/project-page.ts diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 27a719f04..cdb469f8b 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -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; }; diff --git a/packages/types/src/pages.d.ts b/packages/types/src/pages.d.ts index 011f92d69..183d015bf 100644 --- a/packages/types/src/pages.d.ts +++ b/packages/types/src/pages.d.ts @@ -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; -} \ No newline at end of file +}; + +export type TWebhookConnectionQueryParams = { + documentType: "project_page" | "team_page" | "workspace_page"; + projectId?: string; + teamId?: string; + workspaceSlug: string; +}; diff --git a/packages/types/src/search.d.ts b/packages/types/src/search.d.ts index 6eb147512..41f6a1021 100644 --- a/packages/types/src/search.d.ts +++ b/packages/types/src/search.d.ts @@ -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[]; }; diff --git a/packages/utils/src/editor.ts b/packages/utils/src/editor.ts deleted file mode 100644 index 809c1dd3d..000000000 --- a/packages/utils/src/editor.ts +++ /dev/null @@ -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; -type RestoreImage = (assetUrlWithWorkspaceId: string) => Promise; -type UploadImage = (file: File) => Promise; - -// Define the FileService interface based on usage -interface IFileService { - deleteOldEditorAsset: (workspaceId: string, src: string) => Promise; - deleteNewAsset: (url: string) => Promise; - restoreOldEditorAsset: (workspaceId: string, src: string) => Promise; - restoreNewAsset: (anchor: string, src: string) => Promise; - cancelUpload: () => void; -} - -// Define TFileHandler locally since we can't import from @plane/editor -interface TFileHandler { - getAssetSrc: (path: string) => Promise; - 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; - 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 -): { 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) ?? ""; - } - }, - }; -}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a7d6a7960..510155f6a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -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"; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx index 4d3f395ea..1aabb1418 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/[pageId]/page.tsx @@ -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(() => {
- +
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index 1c3d96b57..a6b2b83a8 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -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(() => { - + ); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx index 4171e1f33..93f37ea83 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx @@ -51,11 +51,7 @@ const ProjectPagesPage = observer(() => { projectId={projectId.toString()} pageType={currentPageType()} > - + ); diff --git a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx index 211155d37..b9d6c85ef 100644 --- a/web/ce/components/pages/editor/ai/ask-pi-menu.tsx +++ b/web/ce/components/pages/editor/ai/ask-pi-menu.tsx @@ -11,13 +11,12 @@ type Props = { handleInsertText: (insertOnNextLine: boolean) => void; handleRegenerate: () => Promise; isRegenerating: boolean; - projectId: string; response: string | undefined; workspaceSlug: string; }; export const AskPiMenu: React.FC = (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) => { containerClassName="!p-0 border-none" editorClassName="!pl-0" workspaceSlug={workspaceSlug} - projectId={projectId} />