diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index 27a719f0476..cdb469f8b52 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 011f92d69ba..183d015bf69 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 6eb1475129c..41f6a102130 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 809c1dd3d2a..00000000000 --- 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 a7d6a79609d..510155f6a1e 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 4d3f395ea00..1aabb14181b 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 1c3d96b5718..a6b2b83a894 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 4171e1f332d..93f37ea83b0 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 211155d37d7..b9d6c85ef57 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} />