import { Sparkle } from "lucide-react"; import { observer } from "mobx-react-lite"; import useSWR from "swr"; import { useRouter } from "next/router"; import { ReactElement, useEffect, useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; // hooks import { useApplication, usePage, useUser, useWorkspace } from "hooks/store"; import useReloadConfirmations from "hooks/use-reload-confirmation"; import useToast from "hooks/use-toast"; // services import { FileService } from "services/file.service"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { GptAssistantPopover } from "components/core"; import { PageDetailsHeader } from "components/headers/page-details"; // ui import { DocumentEditorWithRef, DocumentReadOnlyEditorWithRef } from "@plane/document-editor"; import { Spinner } from "@plane/ui"; // assets // helpers // types import { IPage } from "@plane/types"; import { NextPageWithLayout } from "lib/types"; // fetch-keys // constants import { EUserProjectRoles } from "constants/project"; import { useProjectPages } from "hooks/store/use-project-specific-pages"; import { IssuePeekOverview } from "components/issues"; // services const fileService = new FileService(); const PageDetailsPage: NextPageWithLayout = observer(() => { // states const [gptModalOpen, setGptModal] = useState(false); // refs const editorRef = useRef(null); // router const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string; // store hooks const { config: { envConfig }, } = useApplication(); const { currentUser, membership: { currentProjectRole }, } = useUser(); // toast alert const { setToastAlert } = useToast(); //TODO:fix reload confirmations, with mobx const { setShowAlert } = useReloadConfirmations(); const { handleSubmit, setValue, watch, getValues, control, reset } = useForm({ defaultValues: { name: "", description_html: "" }, }); const { archivePage: archivePageAction, restorePage: restorePageAction, createPage: createPageAction, projectPageMap, projectArchivedPageMap, fetchProjectPages, fetchArchivedProjectPages, } = useProjectPages(); useSWR( workspaceSlug && projectId ? `ALL_PAGES_LIST_${projectId}` : null, workspaceSlug && projectId && !projectPageMap[projectId as string] && !projectArchivedPageMap[projectId as string] ? () => fetchProjectPages(workspaceSlug.toString(), projectId.toString()) : null ); // fetching archived pages from API useSWR( workspaceSlug && projectId ? `ALL_ARCHIVED_PAGES_LIST_${projectId}` : null, workspaceSlug && projectId && !projectArchivedPageMap[projectId as string] && !projectPageMap[projectId as string] ? () => fetchArchivedProjectPages(workspaceSlug.toString(), projectId.toString()) : null ); const pageStore = usePage(pageId as string); useEffect( () => () => { if (pageStore) { pageStore.cleanup(); } }, [pageStore] ); if (!pageStore) { return (
); } // We need to get the values of title and description from the page store but we don't have to subscribe to those values const pageTitle = pageStore?.name; const pageDescription = pageStore?.description_html; const { lockPage: lockPageAction, unlockPage: unlockPageAction, updateName: updateNameAction, updateDescription: updateDescriptionAction, id: pageIdMobx, isSubmitting, setIsSubmitting, owned_by, is_locked, archived_at, created_at, created_by, updated_at, updated_by, } = pageStore; const updatePage = async (formData: IPage) => { if (!workspaceSlug || !projectId || !pageId) return; await updateDescriptionAction(formData.description_html); }; const handleAiAssistance = async (response: string) => { if (!workspaceSlug || !projectId || !pageId) return; const newDescription = `${watch("description_html")}

${response}

`; setValue("description_html", newDescription); editorRef.current?.setEditorValue(newDescription); updateDescriptionAction(newDescription); }; const actionCompleteAlert = ({ title, message, type, }: { title: string; message: string; type: "success" | "error" | "warning" | "info"; }) => { setToastAlert({ title, message, type, }); }; const updatePageTitle = (title: string) => { if (!workspaceSlug || !projectId || !pageId) return; updateNameAction(title); }; const createPage = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; await createPageAction(workspaceSlug as string, projectId as string, payload); }; // ================ Page Menu Actions ================== const duplicate_page = async () => { const currentPageValues = getValues(); if (!currentPageValues?.description_html) { // TODO: We need to get latest data the above variable will give us stale data currentPageValues.description_html = pageDescription as string; } const formData: Partial = { name: "Copy of " + pageTitle, description_html: currentPageValues.description_html, }; try { await createPage(formData); } catch (error) { actionCompleteAlert({ title: `Page could not be duplicated`, message: `Sorry, page could not be duplicated, please try again later`, type: "error", }); } }; const archivePage = async () => { if (!workspaceSlug || !projectId || !pageId) return; try { await archivePageAction(workspaceSlug as string, projectId as string, pageId as string); } catch (error) { actionCompleteAlert({ title: `Page could not be archived`, message: `Sorry, page could not be archived, please try again later`, type: "error", }); } }; const unArchivePage = async () => { if (!workspaceSlug || !projectId || !pageId) return; try { await restorePageAction(workspaceSlug as string, projectId as string, pageId as string); } catch (error) { actionCompleteAlert({ title: `Page could not be restored`, message: `Sorry, page could not be restored, please try again later`, type: "error", }); } }; const lockPage = async () => { if (!workspaceSlug || !projectId || !pageId) return; try { await lockPageAction(); } catch (error) { actionCompleteAlert({ title: `Page could not be locked`, message: `Sorry, page could not be locked, please try again later`, type: "error", }); } }; const unlockPage = async () => { if (!workspaceSlug || !projectId || !pageId) return; try { await unlockPageAction(); } catch (error) { actionCompleteAlert({ title: `Page could not be unlocked`, message: `Sorry, page could not be unlocked, please try again later`, type: "error", }); } }; const isPageReadOnly = is_locked || archived_at || (currentProjectRole && [EUserProjectRoles.VIEWER, EUserProjectRoles.GUEST].includes(currentProjectRole)); const isCurrentUserOwner = owned_by === currentUser?.id; const userCanDuplicate = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); const userCanArchive = isCurrentUserOwner || currentProjectRole === EUserProjectRoles.ADMIN; const userCanLock = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); return pageIdMobx ? (
{isPageReadOnly ? ( ) : (
( { setShowAlert(true); onChange(description_html); handleSubmit(updatePage)(); }} duplicationConfig={userCanDuplicate ? { action: duplicate_page } : undefined} pageArchiveConfig={ userCanArchive ? { is_archived: archived_at ? true : false, action: archived_at ? unArchivePage : archivePage, } : undefined } pageLockConfig={userCanLock ? { is_locked: false, action: lockPage } : undefined} /> )} /> {projectId && envConfig?.has_openai_configured && (
{ setGptModal((prevData) => !prevData); // this is done so that the title do not reset after gpt popover closed reset(getValues()); }} onResponse={(response) => { handleAiAssistance(response); }} placement="top-end" button={ } className="!min-w-[38rem]" />
)}
)}
) : (
); }); PageDetailsPage.getLayout = function getLayout(page: ReactElement) { return ( } withProjectWrapper> {page} ); }; export default PageDetailsPage;