import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import useSWR, { mutate } from "swr"; // react-hook-form import { useForm } from "react-hook-form"; // headless ui import { Popover, Transition } from "@headlessui/react"; // react-color import { TwitterPicker } from "react-color"; // react-beautiful-dnd import { DragDropContext, DropResult } from "react-beautiful-dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; // services import projectService from "services/project.service"; import pagesService from "services/pages.service"; import issuesService from "services/issues.service"; // hooks import useToast from "hooks/use-toast"; import useUser from "hooks/use-user"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout"; // components import { CreateUpdateBlockInline, SinglePageBlock } from "components/pages"; import { CreateLabelModal } from "components/labels"; import { CreateBlock } from "components/pages/create-block"; // ui import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; import { CustomSearchSelect, EmptyState, Loader, TextArea, ToggleSwitch, Tooltip, } from "components/ui"; // images import emptyPage from "public/empty-state/page.svg"; // icons import { ArrowLeftIcon, LockClosedIcon, LockOpenIcon, PlusIcon, StarIcon, LinkIcon, XMarkIcon, ChevronDownIcon, } from "@heroicons/react/24/outline"; import { ColorPalletteIcon } from "components/icons"; // helpers import { render24HourFormatTime, renderShortDate } from "helpers/date-time.helper"; import { copyTextToClipboard, truncateText } from "helpers/string.helper"; import { orderArrayBy } from "helpers/array.helper"; // types import type { NextPage } from "next"; import { IIssueLabels, IPage, IPageBlock, IProjectMember } from "types"; // fetch-keys import { PAGE_BLOCKS_LIST, PAGE_DETAILS, PROJECT_DETAILS, PROJECT_ISSUE_LABELS, USER_PROJECT_VIEW, } from "constants/fetch-keys"; const SinglePage: NextPage = () => { const [createBlockForm, setCreateBlockForm] = useState(false); const [labelModal, setLabelModal] = useState(false); const [showBlock, setShowBlock] = useState(false); const scrollToRef = useRef(null); const router = useRouter(); const { workspaceSlug, projectId, pageId } = router.query; const { setToastAlert } = useToast(); const { user } = useUser(); const { handleSubmit, reset, watch, setValue } = useForm({ defaultValues: { name: "" }, }); const { data: projectDetails } = useSWR( workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null ); const { data: pageDetails, error } = useSWR( workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null, workspaceSlug && projectId ? () => pagesService.getPageDetails( workspaceSlug as string, projectId as string, pageId as string ) : null ); const { data: pageBlocks } = useSWR( workspaceSlug && projectId && pageId ? PAGE_BLOCKS_LIST(pageId as string) : null, workspaceSlug && projectId ? () => pagesService.listPageBlocks( workspaceSlug as string, projectId as string, pageId as string ) : null ); const { data: labels } = useSWR( workspaceSlug && projectId ? PROJECT_ISSUE_LABELS(projectId as string) : null, workspaceSlug && projectId ? () => issuesService.getIssueLabels(workspaceSlug as string, projectId as string) : null ); const { data: memberDetails } = useSWR( workspaceSlug && projectId ? USER_PROJECT_VIEW(projectId.toString()) : null, workspaceSlug && projectId ? () => projectService.projectMemberMe(workspaceSlug.toString(), projectId.toString()) : null ); const updatePage = async (formData: IPage) => { if (!workspaceSlug || !projectId || !pageId) return; if (!formData.name || formData.name.length === 0 || formData.name === "") return; await pagesService .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate( PAGE_DETAILS(pageId as string), (prevData) => ({ ...prevData, ...formData, }), false ); }); }; const partialUpdatePage = async (formData: Partial) => { if (!workspaceSlug || !projectId || !pageId) return; mutate( PAGE_DETAILS(pageId as string), (prevData) => ({ ...(prevData as IPage), ...formData, labels: formData.labels_list ? formData.labels_list : (prevData as IPage).labels, }), false ); await pagesService .patchPage(workspaceSlug as string, projectId as string, pageId as string, formData, user) .then(() => { mutate(PAGE_DETAILS(pageId as string)); }); }; const handleAddToFavorites = () => { if (!workspaceSlug || !projectId || !pageId) return; mutate( PAGE_DETAILS(pageId as string), (prevData) => ({ ...(prevData as IPage), is_favorite: true, }), false ).then(() => { setToastAlert({ type: "success", title: "Success", message: "Added to favorites", }); }); pagesService.addPageToFavorites(workspaceSlug as string, projectId as string, { page: pageId as string, }); }; const handleRemoveFromFavorites = () => { if (!workspaceSlug || !projectId || !pageId) return; mutate( PAGE_DETAILS(pageId as string), (prevData) => ({ ...(prevData as IPage), is_favorite: false, }), false ).then(() => { setToastAlert({ type: "success", title: "Success", message: "Removed from favorites", }); }); pagesService.removePageFromFavorites( workspaceSlug as string, projectId as string, pageId as string ); }; const handleOnDragEnd = (result: DropResult) => { if (!result.destination || !workspaceSlug || !projectId || !pageId || !pageBlocks) return; const { source, destination } = result; let newSortOrder = pageBlocks.find((p) => p.id === result.draggableId)?.sort_order ?? 65535; if (destination.index === 0) newSortOrder = pageBlocks[0].sort_order - 10000; else if (destination.index === pageBlocks.length - 1) newSortOrder = pageBlocks[pageBlocks.length - 1].sort_order + 10000; else { if (destination.index > source.index) newSortOrder = (pageBlocks[destination.index].sort_order + pageBlocks[destination.index + 1].sort_order) / 2; else if (destination.index < source.index) newSortOrder = (pageBlocks[destination.index - 1].sort_order + pageBlocks[destination.index].sort_order) / 2; } const newBlocksList = pageBlocks.map((p) => ({ ...p, sort_order: p.id === result.draggableId ? newSortOrder : p.sort_order, })); mutate( PAGE_BLOCKS_LIST(pageId as string), orderArrayBy(newBlocksList, "sort_order", "ascending"), false ); pagesService.patchPageBlock( workspaceSlug as string, projectId as string, pageId as string, result.draggableId, { sort_order: newSortOrder, }, user ); }; const handleCopyText = () => { const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/pages/${pageId}`).then( () => { setToastAlert({ type: "success", title: "Link Copied!", message: "Page link copied to clipboard.", }); } ); }; const handleShowBlockToggle = async () => { if (!workspaceSlug || !projectId) return; const payload: Partial = { preferences: { pages: { block_display: !showBlock, }, }, }; mutate( (workspaceSlug as string) && (projectId as string) ? USER_PROJECT_VIEW(projectId as string) : null, (prevData) => { if (!prevData) return prevData; return { ...prevData, ...payload, }; }, false ); await projectService .setProjectView(workspaceSlug as string, projectId as string, payload) .catch(() => { setToastAlert({ type: "error", title: "Error!", message: "Something went wrong. Please try again.", }); }); }; const options = labels?.map((label) => ({ value: label.id, query: label.name, content: (
{label.name}
), })); useEffect(() => { if (!pageDetails) return; reset({ ...pageDetails, }); }, [reset, pageDetails]); useEffect(() => { if (!memberDetails) return; setShowBlock(memberDetails.preferences.pages.block_display); }, [memberDetails]); return ( } > {error ? ( router.push(`/${workspaceSlug}/projects/${projectId}/pages`), }} /> ) : pageDetails ? (