style: revamped page details UI (#2823)

* style: revamp page details UI

* chore: updated the info popover date format

* fix: page actions mutation

* style: made the page content responsive
This commit is contained in:
Aaryan Khandelwal 2023-11-22 12:32:49 +05:30 committed by sriram veeraghanta
parent e57b95f99e
commit d43db7fc88
15 changed files with 662 additions and 524 deletions

View File

@ -1,19 +1,21 @@
import { Icon } from "lucide-react" import { Icon } from "lucide-react";
interface IAlertLabelProps { interface IAlertLabelProps {
Icon: Icon, Icon?: Icon;
backgroundColor: string, backgroundColor: string;
textColor?: string, textColor?: string;
label: string, label: string;
} }
export const AlertLabel = ({ Icon, backgroundColor,textColor, label }: IAlertLabelProps) => { export const AlertLabel = (props: IAlertLabelProps) => {
const { Icon, backgroundColor, textColor, label } = props;
return ( return (
<div className={`text-xs flex items-center gap-1 ${backgroundColor} p-0.5 pl-3 pr-3 mr-1 rounded`}> <div
<Icon size={12} /> className={`h-7 flex items-center gap-2 font-medium py-0.5 px-3 rounded-full text-xs ${backgroundColor} ${textColor}`}
<span className={`normal-case ${textColor}`}>{label}</span> >
{Icon && <Icon className="h-3 w-3" />}
<span>{label}</span>
</div> </div>
) );
};
}

View File

@ -8,15 +8,13 @@ interface ContentBrowserProps {
markings: IMarking[]; markings: IMarking[];
} }
export const ContentBrowser = ({ export const ContentBrowser = (props: ContentBrowserProps) => {
editor, const { editor, markings } = props;
markings,
}: ContentBrowserProps) => ( return (
<div className="mt-4 flex w-[250px] flex-col h-full"> <div className="flex flex-col h-full overflow-hidden">
<h2 className="ml-4 border-b border-solid border-custom-border py-5 font-medium leading-[85.714%] tracking-tight max-md:ml-2.5"> <h2 className="font-medium">Table of Contents</h2>
Table of Contents <div className="h-full overflow-y-auto">
</h2>
<div className="mt-3 h-0.5 w-full self-stretch border-custom-border" />
{markings.length !== 0 ? ( {markings.length !== 0 ? (
markings.map((marking) => markings.map((marking) =>
marking.level === 1 ? ( marking.level === 1 ? (
@ -29,12 +27,14 @@ export const ContentBrowser = ({
onClick={() => scrollSummary(editor, marking)} onClick={() => scrollSummary(editor, marking)}
subHeading={marking.text} subHeading={marking.text}
/> />
) ),
) )
) : ( ) : (
<p className="ml-3 mr-3 flex h-full items-center px-5 text-center text-xs text-gray-500"> <p className="mt-3 text-xs text-custom-text-400">
{"Headings will be displayed here for Navigation"} Headings will be displayed here for navigation
</p> </p>
)} )}
</div> </div>
</div>
); );
};

View File

@ -1,79 +1,90 @@
import { Editor } from "@tiptap/react" import { Editor } from "@tiptap/react";
import { Lock, ArchiveIcon, MenuSquare } from "lucide-react" import { Archive, Info, Lock } from "lucide-react";
import { useRef, useState } from "react" import { IMarking, UploadImage } from "..";
import { usePopper } from "react-popper" import { FixedMenu } from "../menu";
import { IMarking, UploadImage } from ".." import { DocumentDetails } from "../types/editor-types";
import { FixedMenu } from "../menu" import { AlertLabel } from "./alert-label";
import { DocumentDetails } from "../types/editor-types" import {
import { AlertLabel } from "./alert-label" IVerticalDropdownItemProps,
import { ContentBrowser } from "./content-browser" VerticalDropdownMenu,
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu" } from "./vertical-dropdown-menu";
import { SummaryPopover } from "./summary-popover";
import { InfoPopover } from "./info-popover";
interface IEditorHeader { interface IEditorHeader {
editor: Editor, editor: Editor;
KanbanMenuOptions: IVerticalDropdownItemProps[], KanbanMenuOptions: IVerticalDropdownItemProps[];
sidePeakVisible: boolean, sidePeekVisible: boolean;
setSidePeakVisible: (currentState: boolean) => void, setSidePeekVisible: (sidePeekState: boolean) => void;
markings: IMarking[], markings: IMarking[];
isLocked: boolean, isLocked: boolean;
isArchived: boolean, isArchived: boolean;
archivedAt?: Date, archivedAt?: Date;
readonly: boolean, readonly: boolean;
uploadFile?: UploadImage, uploadFile?: UploadImage;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void, setIsSubmitting?: (
documentDetails: DocumentDetails isSubmitting: "submitting" | "submitted" | "saved",
) => void;
documentDetails: DocumentDetails;
} }
export const EditorHeader = ({ documentDetails, archivedAt, editor, sidePeakVisible, readonly, setSidePeakVisible, markings, uploadFile, setIsSubmitting, KanbanMenuOptions, isArchived, isLocked }: IEditorHeader) => { export const EditorHeader = (props: IEditorHeader) => {
const {
const summaryMenuRef = useRef(null); documentDetails,
const summaryButtonRef = useRef(null); archivedAt,
const [summaryPopoverVisible, setSummaryPopoverVisible] = useState(false); editor,
sidePeekVisible,
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(summaryButtonRef.current, summaryMenuRef.current, { readonly,
placement: "bottom-start" setSidePeekVisible,
}) markings,
uploadFile,
setIsSubmitting,
KanbanMenuOptions,
isArchived,
isLocked,
} = props;
return ( return (
<div className="flex items-center border-b border-custom-border-200 py-2 px-5">
<div className="border-custom-border self-stretch flex flex-col border-b border-solid max-md:max-w-full"> <div className="flex-shrink-0 w-56 lg:w-80">
<div <SummaryPopover
className="self-center flex ml-0 w-full items-start justify-between gap-5 max-md:max-w-full max-md:flex-wrap max-md:justify-center"> editor={editor}
<div className={"flex flex-row items-center"}> markings={markings}
<div sidePeekVisible={sidePeekVisible}
onMouseEnter={() => setSummaryPopoverVisible(true)} setSidePeekVisible={setSidePeekVisible}
onMouseLeave={() => setSummaryPopoverVisible(false)}
>
<button
ref={summaryButtonRef}
className={"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors"}
onClick={() => {
setSidePeakVisible(!sidePeakVisible)
setSummaryPopoverVisible(false)
}}
>
<MenuSquare
size={20}
/> />
</button>
{summaryPopoverVisible &&
<div style={summaryPopoverStyles.popper} {...summaryPopoverAttributes.popper} className="z-10 h-[300px] w-[300px] ml-[40px] mt-[40px] shadow-xl rounded border-custom-border border-solid border-2 bg-custom-background-100 border-b pl-3 pr-3 pb-3 overflow-scroll">
<ContentBrowser editor={editor} markings={markings} />
</div>
}
</div>
{isLocked && <AlertLabel Icon={Lock} backgroundColor={"bg-red-200"} label={"Locked"} />}
{(isArchived && archivedAt) && <AlertLabel Icon={ArchiveIcon} backgroundColor={"bg-blue-200"} label={`Archived at ${new Date(archivedAt).toLocaleString()}`} />}
</div> </div>
{(!readonly && uploadFile) && <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />} <div className="flex-shrink-0">
<div className="self-center flex items-start gap-3 my-auto max-md:justify-center" {!readonly && uploadFile && (
> <FixedMenu
{!isArchived && <p className="text-sm text-custom-text-300">{`Last updated at ${new Date(documentDetails.last_updated_at).toLocaleString()}`}</p>} editor={editor}
uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting}
/>
)}
</div>
<div className="flex-grow flex items-center justify-end gap-3">
{isLocked && (
<AlertLabel
Icon={Lock}
backgroundColor="bg-custom-background-80"
textColor="text-custom-text-300"
label="Locked"
/>
)}
{isArchived && archivedAt && (
<AlertLabel
Icon={Archive}
backgroundColor="bg-blue-500/20"
textColor="text-blue-500"
label={`Archived at ${new Date(archivedAt).toLocaleString()}`}
/>
)}
{!isArchived && <InfoPopover documentDetails={documentDetails} />}
<VerticalDropdownMenu items={KanbanMenuOptions} /> <VerticalDropdownMenu items={KanbanMenuOptions} />
</div> </div>
</div> </div>
</div> );
) };
}

View File

@ -0,0 +1,9 @@
export * from "./alert-label";
export * from "./content-browser";
export * from "./editor-header";
export * from "./heading-component";
export * from "./info-popover";
export * from "./page-renderer";
export * from "./summary-popover";
export * from "./summary-side-bar";
export * from "./vertical-dropdown-menu";

View File

@ -0,0 +1,79 @@
import { useState } from "react";
import { usePopper } from "react-popper";
import { Calendar, History, Info } from "lucide-react";
// types
import { DocumentDetails } from "../types/editor-types";
type Props = {
documentDetails: DocumentDetails;
};
// function to render a Date in the format- 25 May 2023 at 2:53PM
const renderDate = (date: Date): string => {
const options: Intl.DateTimeFormatOptions = {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
};
const formattedDate: string = new Intl.DateTimeFormat(
"en-US",
options,
).format(date);
return formattedDate;
};
export const InfoPopover: React.FC<Props> = (props) => {
const { documentDetails } = props;
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [referenceElement, setReferenceElement] =
useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null,
);
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } =
usePopper(referenceElement, popperElement, {
placement: "bottom-start",
});
return (
<div
onMouseEnter={() => setIsPopoverOpen(true)}
onMouseLeave={() => setIsPopoverOpen(false)}
>
<button type="button" ref={setReferenceElement} className="block mt-1.5">
<Info className="h-3.5 w-3.5" />
</button>
{isPopoverOpen && (
<div
className="z-10 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 space-y-2.5"
ref={setPopperElement}
style={infoPopoverStyles.popper}
{...infoPopoverAttributes.popper}
>
<div className="space-y-1.5">
<h6 className="text-custom-text-400 text-xs">Last updated on</h6>
<h5 className="text-sm flex items-center gap-1">
<History className="h-3 w-3" />
{renderDate(new Date(documentDetails.last_updated_at))}
</h5>
</div>
<div className="space-y-1.5">
<h6 className="text-custom-text-400 text-xs">Created on</h6>
<h5 className="text-sm flex items-center gap-1">
<Calendar className="h-3 w-3" />
{renderDate(new Date(documentDetails.created_on))}
</h5>
</div>
</div>
)}
</div>
);
};

View File

@ -3,44 +3,33 @@ import { Editor } from "@tiptap/react";
import { DocumentDetails } from "../types/editor-types"; import { DocumentDetails } from "../types/editor-types";
interface IPageRenderer { interface IPageRenderer {
sidePeakVisible: boolean;
documentDetails: DocumentDetails; documentDetails: DocumentDetails;
editor: Editor; editor: Editor;
editorClassNames: string; editorClassNames: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
} }
export const PageRenderer = ({ export const PageRenderer = (props: IPageRenderer) => {
sidePeakVisible, const {
documentDetails, documentDetails,
editor, editor,
editorClassNames, editorClassNames,
editorContentCustomClassNames, editorContentCustomClassNames,
}: IPageRenderer) => { } = props;
return ( return (
<div <div className="h-full w-full overflow-y-auto pl-7 py-5">
className={`flex h-[88vh] flex-col w-full max-md:w-full max-md:ml-0 transition-all duration-200 ease-in-out ${ <h1 className="text-4xl font-bold break-all pr-5 -mt-2">
sidePeakVisible ? "ml-[3%] " : "ml-0"
}`}
>
<div className="items-start mt-4 h-full flex flex-col w-fit max-md:max-w-full overflow-auto">
<div className="flex flex-col py-2 max-md:max-w-full">
<h1 className="border-none outline-none bg-transparent text-4xl font-bold leading-8 tracking-tight self-center w-[700px] max-w-full">
{documentDetails.title} {documentDetails.title}
</h1> </h1>
</div> <div className="flex flex-col h-full w-full pr-5">
<div className="border-b border-custom-border-200 self-stretch w-full h-0.5 mt-3" />
<div className="flex flex-col h-full w-full max-md:max-w-full">
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col h-full w-full">
<EditorContentWrapper <EditorContentWrapper
editor={editor} editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames} editorContentCustomClassNames={editorContentCustomClassNames}
/> />
</div>
</EditorContainer> </EditorContainer>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -1,67 +0,0 @@
import React, { Fragment, useState } from "react";
import { usePopper } from "react-popper";
import { Popover, Transition } from "@headlessui/react";
import { Placement } from "@popperjs/core";
// ui
import { Button } from "@plane/ui";
// icons
import { ChevronUp, MenuIcon } from "lucide-react";
type Props = {
children: React.ReactNode;
title?: string;
placement?: Placement;
};
export const SummaryPopover: React.FC<Props> = (props) => {
const { children, title = "SummaryPopover", placement } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "auto",
});
return (
<Popover as="div">
{({ open }) => {
if (open) {
}
return (
<>
<Popover.Button as={React.Fragment}>
<Button
ref={setReferenceElement}
variant="neutral-primary"
size="sm"
>
<MenuIcon size={20} />
</Button>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel>
<div
className="z-10 bg-custom-background-100 border border-custom-border-200 shadow-custom-shadow-rg rounded overflow-hidden"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<div className="w-[18.75rem] max-h-[37.5rem] flex flex-col overflow-hidden">{children}</div>
</div>
</Popover.Panel>
</Transition>
</>
);
}}
</Popover>
);
};

View File

@ -0,0 +1,57 @@
import { useState } from "react";
import { Editor } from "@tiptap/react";
import { usePopper } from "react-popper";
import { List } from "lucide-react";
// components
import { ContentBrowser } from "./content-browser";
// types
import { IMarking } from "..";
type Props = {
editor: Editor;
markings: IMarking[];
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
};
export const SummaryPopover: React.FC<Props> = (props) => {
const { editor, markings, sidePeekVisible, setSidePeekVisible } = props;
const [referenceElement, setReferenceElement] =
useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null,
);
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } =
usePopper(referenceElement, popperElement, {
placement: "bottom-start",
});
return (
<div className="group/summary-popover w-min whitespace-nowrap">
<button
type="button"
ref={setReferenceElement}
className={`h-7 w-7 grid place-items-center rounded ${
sidePeekVisible
? "bg-custom-primary-100/20 text-custom-primary-100"
: "text-custom-text-300"
}`}
onClick={() => setSidePeekVisible(!sidePeekVisible)}
>
<List className="h-4 w-4" />
</button>
{!sidePeekVisible && (
<div
className="hidden group-hover/summary-popover:block z-10 h-80 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3"
ref={setPopperElement}
style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper}
>
<ContentBrowser editor={editor} markings={markings} />
</div>
)}
</div>
);
};

View File

@ -1,18 +1,25 @@
import { Editor } from "@tiptap/react" import { Editor } from "@tiptap/react";
import { IMarking } from ".." import { IMarking } from "..";
import { ContentBrowser } from "./content-browser" import { ContentBrowser } from "./content-browser";
interface ISummarySideBarProps { interface ISummarySideBarProps {
editor: Editor, editor: Editor;
markings: IMarking[], markings: IMarking[];
sidePeakVisible: boolean sidePeekVisible: boolean;
} }
export const SummarySideBar = ({ editor, markings, sidePeakVisible }: ISummarySideBarProps) => { export const SummarySideBar = ({
editor,
markings,
sidePeekVisible,
}: ISummarySideBarProps) => {
return ( return (
<div
<div className={`flex flex-col items-stretch w-[21%] max-md:w-full max-md:ml-0 border-custom-border border-r border-solid transition-all duration-200 ease-in-out transform ${sidePeakVisible ? 'translate-x-0' : '-translate-x-full'}`}> className={`h-full px-5 pt-5 transition-all duration-200 transform overflow-hidden ${
sidePeekVisible ? "translate-x-0" : "-translate-x-full"
}`}
>
<ContentBrowser editor={editor} markings={markings} /> <ContentBrowser editor={editor} markings={markings} />
</div> </div>
) );
} };

View File

@ -1,41 +1,52 @@
import { Button, CustomMenu } from "@plane/ui" import { Button, CustomMenu } from "@plane/ui";
import { ChevronUp, Icon, MoreVertical } from "lucide-react" import { ChevronUp, Icon, MoreVertical } from "lucide-react";
type TMenuItems =
type TMenuItems = "archive_page" | "unarchive_page" | "lock_page" | "unlock_page" | "copy_markdown" | "close_page" | "copy_page_link" | "duplicate_page" | "archive_page"
| "unarchive_page"
| "lock_page"
| "unlock_page"
| "copy_markdown"
| "close_page"
| "copy_page_link"
| "duplicate_page";
export interface IVerticalDropdownItemProps { export interface IVerticalDropdownItemProps {
key: number, key: number;
type: TMenuItems, type: TMenuItems;
Icon: Icon, Icon: Icon;
label: string, label: string;
action: () => Promise<void> | void action: () => Promise<void> | void;
} }
export interface IVerticalDropdownMenuProps { export interface IVerticalDropdownMenuProps {
items: IVerticalDropdownItemProps[], items: IVerticalDropdownItemProps[];
} }
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => { const VerticalDropdownItem = ({
Icon,
label,
action,
}: IVerticalDropdownItemProps) => {
return ( return (
<CustomMenu.MenuItem> <CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
<Button variant={"neutral-primary"} onClick={action} className="flex flex-row border-none items-center m-1 max-md:pr-5 cursor-pointer"> <Icon className="h-3 w-3" />
<Icon size={16} /> <div className="text-custom-text-300">{label}</div>
<div className="text-custom-text-300 ml-2 mr-2 leading-5 tracking-tight whitespace-nowrap self-start text-md">
{label}
</div>
</Button>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
) );
} };
export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => { export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
return ( return (
<CustomMenu maxHeight={"lg"} className={"h-4"} placement={"bottom-start"} optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "} customButton={ <CustomMenu
<MoreVertical size={18}/> maxHeight={"lg"}
}> className={"h-4"}
placement={"bottom-start"}
optionsClassName={
"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "
}
customButton={<MoreVertical size={14} />}
>
{items.map((item, index) => ( {items.map((item, index) => (
<VerticalDropdownItem <VerticalDropdownItem
key={index} key={index}
@ -46,5 +57,5 @@ export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
/> />
))} ))}
</CustomMenu> </CustomMenu>
) );
} };

View File

@ -1,34 +1,40 @@
"use client" "use client";
import React, { useState } from 'react'; import React, { useState } from "react";
import { cn, getEditorClassNames, useEditor } from '@plane/editor-core'; import { cn, getEditorClassNames, useEditor } from "@plane/editor-core";
import { DocumentEditorExtensions } from './extensions'; import { DocumentEditorExtensions } from "./extensions";
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from './types/menu-actions'; import {
import { EditorHeader } from './components/editor-header'; IDuplicationConfig,
import { useEditorMarkings } from './hooks/use-editor-markings'; IPageArchiveConfig,
import { SummarySideBar } from './components/summary-side-bar'; IPageLockConfig,
import { DocumentDetails } from './types/editor-types'; } from "./types/menu-actions";
import { PageRenderer } from './components/page-renderer'; import { EditorHeader } from "./components/editor-header";
import { getMenuOptions } from './utils/menu-options'; import { useEditorMarkings } from "./hooks/use-editor-markings";
import { useRouter } from 'next/router'; import { SummarySideBar } from "./components/summary-side-bar";
import { DocumentDetails } from "./types/editor-types";
import { PageRenderer } from "./components/page-renderer";
import { getMenuOptions } from "./utils/menu-options";
import { useRouter } from "next/router";
export type UploadImage = (file: File) => Promise<string>; export type UploadImage = (file: File) => Promise<string>;
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>; export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
interface IDocumentEditor { interface IDocumentEditor {
documentDetails: DocumentDetails, documentDetails: DocumentDetails;
value: string; value: string;
uploadFile: UploadImage; uploadFile: UploadImage;
deleteFile: DeleteImage; deleteFile: DeleteImage;
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange: (json: any, html: string) => void; onChange: (json: any, html: string) => void;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
debouncedUpdatesEnabled?: boolean; debouncedUpdatesEnabled?: boolean;
duplicationConfig?: IDuplicationConfig, duplicationConfig?: IDuplicationConfig;
pageLockConfig?: IPageLockConfig, pageLockConfig?: IPageLockConfig;
pageArchiveConfig?: IPageArchiveConfig pageArchiveConfig?: IPageArchiveConfig;
} }
interface DocumentEditorProps extends IDocumentEditor { interface DocumentEditorProps extends IDocumentEditor {
forwardedRef?: React.Ref<EditorHandle>; forwardedRef?: React.Ref<EditorHandle>;
@ -40,10 +46,10 @@ interface EditorHandle {
} }
export interface IMarking { export interface IMarking {
type: "heading", type: "heading";
level: number, level: number;
text: string, text: string;
sequence: number sequence: number;
} }
const DocumentEditor = ({ const DocumentEditor = ({
@ -60,21 +66,20 @@ const DocumentEditor = ({
forwardedRef, forwardedRef,
duplicationConfig, duplicationConfig,
pageLockConfig, pageLockConfig,
pageArchiveConfig pageArchiveConfig,
}: IDocumentEditor) => { }: IDocumentEditor) => {
// const [alert, setAlert] = useState<string>("") // const [alert, setAlert] = useState<string>("")
const { markings, updateMarkings } = useEditorMarkings() const { markings, updateMarkings } = useEditorMarkings();
const [sidePeakVisible, setSidePeakVisible] = useState(true) const [sidePeekVisible, setSidePeekVisible] = useState(true);
const router = useRouter() const router = useRouter();
const editor = useEditor({ const editor = useEditor({
onChange(json, html) { onChange(json, html) {
updateMarkings(json) updateMarkings(json);
onChange(json, html) onChange(json, html);
}, },
onStart(json) { onStart(json) {
updateMarkings(json) updateMarkings(json);
}, },
debouncedUpdatesEnabled, debouncedUpdatesEnabled,
setIsSubmitting, setIsSubmitting,
@ -87,31 +92,32 @@ const DocumentEditor = ({
}); });
if (!editor) { if (!editor) {
return null return null;
} }
const KanbanMenuOptions = getMenuOptions( const KanbanMenuOptions = getMenuOptions({
{
editor: editor, editor: editor,
router: router, router: router,
duplicationConfig: duplicationConfig, duplicationConfig: duplicationConfig,
pageLockConfig: pageLockConfig, pageLockConfig: pageLockConfig,
pageArchiveConfig: pageArchiveConfig, pageArchiveConfig: pageArchiveConfig,
} });
) const editorClassNames = getEditorClassNames({
const editorClassNames = getEditorClassNames({ noBorder: true, borderOnFocus: false, customClassName }); noBorder: true,
borderOnFocus: false,
customClassName,
});
if (!editor) return null; if (!editor) return null;
return ( return (
<div className="flex flex-col"> <div className="h-full w-full flex flex-col overflow-hidden">
<div className="top-0 sticky z-10 bg-custom-background-100">
<EditorHeader <EditorHeader
readonly={false} readonly={false}
KanbanMenuOptions={KanbanMenuOptions} KanbanMenuOptions={KanbanMenuOptions}
editor={editor} editor={editor}
sidePeakVisible={sidePeakVisible} sidePeekVisible={sidePeekVisible}
setSidePeakVisible={setSidePeakVisible} setSidePeekVisible={(val) => setSidePeekVisible(val)}
markings={markings} markings={markings}
uploadFile={uploadFile} uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
@ -120,32 +126,32 @@ const DocumentEditor = ({
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at} archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
</div> <div className="h-full w-full flex overflow-hidden">
<div className="self-center items-stretch w-full max-md:max-w-full h-full"> <div className="flex-shrink-0 h-full w-56 lg:w-80">
<div className={cn("gap-5 flex max-md:flex-col max-md:items-stretch max-md:gap-0 h-full", { "justify-center": !sidePeakVisible })}>
<SummarySideBar <SummarySideBar
editor={editor} editor={editor}
markings={markings} markings={markings}
sidePeakVisible={sidePeakVisible} sidePeekVisible={sidePeekVisible}
/> />
</div>
<div className="h-full w-full">
<PageRenderer <PageRenderer
editor={editor} editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames} editorContentCustomClassNames={editorContentCustomClassNames}
editorClassNames={editorClassNames} editorClassNames={editorClassNames}
sidePeakVisible={sidePeakVisible}
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
{/* Page Element */}
</div> </div>
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" />
</div> </div>
</div> </div>
); );
} };
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>((props, ref) => ( const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>(
<DocumentEditor {...props} forwardedRef={ref} /> (props, ref) => <DocumentEditor {...props} forwardedRef={ref} />,
)); );
DocumentEditorWithRef.displayName = "DocumentEditorWithRef"; DocumentEditorWithRef.displayName = "DocumentEditorWithRef";
export { DocumentEditor, DocumentEditorWithRef } export { DocumentEditor, DocumentEditorWithRef };

View File

@ -1,7 +1,22 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { BoldIcon, Heading1, Heading2, Heading3 } from "lucide-react"; import { BoldIcon } from "lucide-react";
import { BoldItem, BulletListItem, cn, CodeItem, ImageItem, ItalicItem, NumberedListItem, QuoteItem, StrikeThroughItem, TableItem, UnderLineItem, HeadingOneItem, HeadingTwoItem, HeadingThreeItem } from "@plane/editor-core"; import {
BoldItem,
BulletListItem,
cn,
CodeItem,
ImageItem,
ItalicItem,
NumberedListItem,
QuoteItem,
StrikeThroughItem,
TableItem,
UnderLineItem,
HeadingOneItem,
HeadingTwoItem,
HeadingThreeItem,
} from "@plane/editor-core";
import { UploadImage } from ".."; import { UploadImage } from "..";
export interface BubbleMenuItem { export interface BubbleMenuItem {
@ -14,77 +29,69 @@ export interface BubbleMenuItem {
type EditorBubbleMenuProps = { type EditorBubbleMenuProps = {
editor: Editor; editor: Editor;
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void; setIsSubmitting?: (
} isSubmitting: "submitting" | "submitted" | "saved",
) => void;
};
export const FixedMenu = (props: EditorBubbleMenuProps) => { export const FixedMenu = (props: EditorBubbleMenuProps) => {
const { editor, uploadFile, setIsSubmitting } = props;
const basicMarkItems: BubbleMenuItem[] = [ const basicMarkItems: BubbleMenuItem[] = [
HeadingOneItem(props.editor), HeadingOneItem(editor),
HeadingTwoItem(props.editor), HeadingTwoItem(editor),
HeadingThreeItem(props.editor), HeadingThreeItem(editor),
BoldItem(props.editor), BoldItem(editor),
ItalicItem(props.editor), ItalicItem(editor),
UnderLineItem(props.editor), UnderLineItem(editor),
StrikeThroughItem(props.editor), StrikeThroughItem(editor),
]; ];
const listItems: BubbleMenuItem[] = [ const listItems: BubbleMenuItem[] = [
BulletListItem(props.editor), BulletListItem(editor),
NumberedListItem(props.editor), NumberedListItem(editor),
]; ];
const userActionItems: BubbleMenuItem[] = [ const userActionItems: BubbleMenuItem[] = [
QuoteItem(props.editor), QuoteItem(editor),
CodeItem(props.editor), CodeItem(editor),
]; ];
const complexItems: BubbleMenuItem[] = [ const complexItems: BubbleMenuItem[] = [
TableItem(props.editor), TableItem(editor),
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting), ImageItem(editor, uploadFile, setIsSubmitting),
]; ];
// const handleAccessChange = (accessKey: string) => {
// props.commentAccessSpecifier?.onAccessChange(accessKey);
// };
return ( return (
<div <div className="flex items-center divide-x divide-custom-border-200">
className="flex w-fit rounded bg-custom-background-100" <div className="flex items-center gap-0.5 pr-2">
> {basicMarkItems.map((item) => (
<div className="flex">
{basicMarkItems.map((item, index) => (
<button <button
key={index} key={item.name}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80": item.isActive(),
} },
)} )}
> >
<item.icon <item.icon className="h-4 w-4" />
size={ item.icon === Heading1 || item.icon === Heading2 || item.icon === Heading3 ? 20 : 15}
className={cn({
"text-custom-text-100": item.isActive(),
})}
/>
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-center gap-0.5 px-2">
{listItems.map((item, index) => ( {listItems.map((item) => (
<button <button
key={index} key={item.name}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80": item.isActive(),
} },
)} )}
> >
<item.icon <item.icon
@ -95,17 +102,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-center gap-0.5 px-2">
{userActionItems.map((item, index) => ( {userActionItems.map((item) => (
<button <button
key={index} key={item.name}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80": item.isActive(),
} },
)} )}
> >
<item.icon <item.icon
@ -116,17 +123,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</button> </button>
))} ))}
</div> </div>
<div className="flex"> <div className="flex items-center gap-0.5 pl-2">
{complexItems.map((item, index) => ( {complexItems.map((item) => (
<button <button
key={index} key={item.name}
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded",
{ {
"text-custom-text-100 bg-custom-primary-100/5": item.isActive(), "text-custom-text-100 bg-custom-background-80": item.isActive(),
} },
)} )}
> >
<item.icon <item.icon

View File

@ -1,27 +1,31 @@
import { cn, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core" import { cn, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useState, forwardRef, useEffect } from 'react' import { useState, forwardRef, useEffect } from "react";
import { EditorHeader } from "../components/editor-header"; import { EditorHeader } from "../components/editor-header";
import { PageRenderer } from "../components/page-renderer"; import { PageRenderer } from "../components/page-renderer";
import { SummarySideBar } from "../components/summary-side-bar"; import { SummarySideBar } from "../components/summary-side-bar";
import { useEditorMarkings } from "../hooks/use-editor-markings"; import { useEditorMarkings } from "../hooks/use-editor-markings";
import { DocumentDetails } from "../types/editor-types"; import { DocumentDetails } from "../types/editor-types";
import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "../types/menu-actions"; import {
IPageArchiveConfig,
IPageLockConfig,
IDuplicationConfig,
} from "../types/menu-actions";
import { getMenuOptions } from "../utils/menu-options"; import { getMenuOptions } from "../utils/menu-options";
interface IDocumentReadOnlyEditor { interface IDocumentReadOnlyEditor {
value: string, value: string;
noBorder: boolean, noBorder: boolean;
borderOnFocus: boolean, borderOnFocus: boolean;
customClassName: string, customClassName: string;
documentDetails: DocumentDetails, documentDetails: DocumentDetails;
pageLockConfig?: IPageLockConfig, pageLockConfig?: IPageLockConfig;
pageArchiveConfig?: IPageArchiveConfig, pageArchiveConfig?: IPageArchiveConfig;
pageDuplicationConfig?: IDuplicationConfig, pageDuplicationConfig?: IDuplicationConfig;
} }
interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor { interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor {
forwardedRef?: React.Ref<EditorHandle> forwardedRef?: React.Ref<EditorHandle>;
} }
interface EditorHandle { interface EditorHandle {
@ -40,32 +44,30 @@ const DocumentReadOnlyEditor = ({
pageLockConfig, pageLockConfig,
pageArchiveConfig, pageArchiveConfig,
}: DocumentReadOnlyEditorProps) => { }: DocumentReadOnlyEditorProps) => {
const router = useRouter();
const router = useRouter() const [sidePeekVisible, setSidePeekVisible] = useState(true);
const [sidePeakVisible, setSidePeakVisible] = useState(true) const { markings, updateMarkings } = useEditorMarkings();
const { markings, updateMarkings } = useEditorMarkings()
const editor = useReadOnlyEditor({ const editor = useReadOnlyEditor({
value, value,
forwardedRef, forwardedRef,
}) });
useEffect(() => { useEffect(() => {
if (editor) { if (editor) {
updateMarkings(editor.getJSON()) updateMarkings(editor.getJSON());
} }
}, [editor?.getJSON()]) }, [editor?.getJSON()]);
if (!editor) { if (!editor) {
return null return null;
} }
const editorClassNames = getEditorClassNames({ const editorClassNames = getEditorClassNames({
noBorder, noBorder,
borderOnFocus, borderOnFocus,
customClassName customClassName,
}) });
const KanbanMenuOptions = getMenuOptions({ const KanbanMenuOptions = getMenuOptions({
editor: editor, editor: editor,
@ -73,43 +75,42 @@ const DocumentReadOnlyEditor = ({
pageArchiveConfig: pageArchiveConfig, pageArchiveConfig: pageArchiveConfig,
pageLockConfig: pageLockConfig, pageLockConfig: pageLockConfig,
duplicationConfig: pageDuplicationConfig, duplicationConfig: pageDuplicationConfig,
}) });
return ( return (
<div className="flex flex-col"> <div className="h-full w-full flex flex-col overflow-hidden">
<div className="top-0 sticky z-10 bg-custom-background-100">
<EditorHeader <EditorHeader
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked} isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived} isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
readonly={true} readonly={true}
editor={editor} editor={editor}
sidePeakVisible={sidePeakVisible} sidePeekVisible={sidePeekVisible}
setSidePeakVisible={setSidePeakVisible} setSidePeekVisible={setSidePeekVisible}
KanbanMenuOptions={KanbanMenuOptions} KanbanMenuOptions={KanbanMenuOptions}
markings={markings} markings={markings}
documentDetails={documentDetails} documentDetails={documentDetails}
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at} archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
/> />
</div> <div className="h-full w-full flex overflow-hidden">
<div className="self-center items-stretch w-full max-md:max-w-full overflow-y-hidden"> <div className="flex-shrink-0 h-full w-56 lg:w-80">
<div className={cn("gap-5 flex max-md:flex-col max-md:items-stretch max-md:gap-0 overflow-y-hidden", { "justify-center": !sidePeakVisible })}>
<SummarySideBar <SummarySideBar
editor={editor} editor={editor}
markings={markings} markings={markings}
sidePeakVisible={sidePeakVisible} sidePeekVisible={sidePeekVisible}
/> />
</div>
<div className="h-full w-full">
<PageRenderer <PageRenderer
editor={editor} editor={editor}
editorClassNames={editorClassNames} editorClassNames={editorClassNames}
sidePeakVisible={sidePeakVisible}
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
</div> </div>
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" />
</div> </div>
</div> </div>
) );
} };
const DocumentReadOnlyEditorWithRef = forwardRef< const DocumentReadOnlyEditorWithRef = forwardRef<
EditorHandle, EditorHandle,
@ -118,4 +119,4 @@ const DocumentReadOnlyEditorWithRef = forwardRef<
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef"; DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef };

View File

@ -1,43 +1,62 @@
import { Editor } from "@tiptap/react" import { Editor } from "@tiptap/react";
import { Archive, ArchiveIcon, ArchiveRestoreIcon, ClipboardIcon, Copy, Link, Lock, Unlock, XCircle } from "lucide-react" import {
import { NextRouter } from "next/router" Archive,
import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu" ArchiveIcon,
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "../types/menu-actions" ArchiveRestoreIcon,
import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions" ClipboardIcon,
Copy,
Link,
Lock,
Unlock,
XCircle,
} from "lucide-react";
import { NextRouter } from "next/router";
import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu";
import {
IDuplicationConfig,
IPageArchiveConfig,
IPageLockConfig,
} from "../types/menu-actions";
import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions";
export interface MenuOptionsProps { export interface MenuOptionsProps {
editor: Editor, editor: Editor;
router: NextRouter, router: NextRouter;
duplicationConfig?: IDuplicationConfig, duplicationConfig?: IDuplicationConfig;
pageLockConfig?: IPageLockConfig , pageLockConfig?: IPageLockConfig;
pageArchiveConfig?: IPageArchiveConfig, pageArchiveConfig?: IPageArchiveConfig;
} }
export const getMenuOptions = ({ editor, router, duplicationConfig, pageLockConfig, pageArchiveConfig } : MenuOptionsProps) => { export const getMenuOptions = ({
editor,
router,
duplicationConfig,
pageLockConfig,
pageArchiveConfig,
}: MenuOptionsProps) => {
const KanbanMenuOptions: IVerticalDropdownItemProps[] = [ const KanbanMenuOptions: IVerticalDropdownItemProps[] = [
{ {
key: 1, key: 1,
type: "copy_markdown", type: "copy_markdown",
Icon: ClipboardIcon, Icon: ClipboardIcon,
action: () => copyMarkdownToClipboard(editor), action: () => copyMarkdownToClipboard(editor),
label: "Copy Markdown" label: "Copy markdown",
},
{
key: 2,
type: "close_page",
Icon: XCircle,
action: () => router.back(),
label: "Close the page"
}, },
// {
// key: 2,
// type: "close_page",
// Icon: XCircle,
// action: () => router.back(),
// label: "Close page",
// },
{ {
key: 3, key: 3,
type: "copy_page_link", type: "copy_page_link",
Icon: Link, Icon: Link,
action: () => CopyPageLink(), action: () => CopyPageLink(),
label: "Copy Page Link" label: "Copy page link",
}, },
] ];
// If duplicateConfig is given, page duplication will be allowed // If duplicateConfig is given, page duplication will be allowed
if (duplicationConfig) { if (duplicationConfig) {
@ -46,8 +65,8 @@ export const getMenuOptions = ({ editor, router, duplicationConfig, pageLockConf
type: "duplicate_page", type: "duplicate_page",
Icon: Copy, Icon: Copy,
action: duplicationConfig.action, action: duplicationConfig.action,
label: "Make a copy" label: "Make a copy",
}) });
} }
// If Lock Configuration is given then, lock page option will be available in the kanban menu // If Lock Configuration is given then, lock page option will be available in the kanban menu
if (pageLockConfig) { if (pageLockConfig) {
@ -55,9 +74,9 @@ export const getMenuOptions = ({ editor, router, duplicationConfig, pageLockConf
key: KanbanMenuOptions.length++, key: KanbanMenuOptions.length++,
type: pageLockConfig.is_locked ? "unlock_page" : "lock_page", type: pageLockConfig.is_locked ? "unlock_page" : "lock_page",
Icon: pageLockConfig.is_locked ? Unlock : Lock, Icon: pageLockConfig.is_locked ? Unlock : Lock,
label: pageLockConfig.is_locked ? "Unlock Page" : "Lock Page", label: pageLockConfig.is_locked ? "Unlock page" : "Lock page",
action: pageLockConfig.action action: pageLockConfig.action,
}) });
} }
// Archiving will be visible in the menu bar config once the pageArchiveConfig is given. // Archiving will be visible in the menu bar config once the pageArchiveConfig is given.
@ -66,10 +85,10 @@ export const getMenuOptions = ({ editor, router, duplicationConfig, pageLockConf
key: KanbanMenuOptions.length++, key: KanbanMenuOptions.length++,
type: pageArchiveConfig.is_archived ? "unarchive_page" : "archive_page", type: pageArchiveConfig.is_archived ? "unarchive_page" : "archive_page",
Icon: pageArchiveConfig.is_archived ? ArchiveRestoreIcon : Archive, Icon: pageArchiveConfig.is_archived ? ArchiveRestoreIcon : Archive,
label: pageArchiveConfig.is_archived ? "Restore Page" : "Archive Page", label: pageArchiveConfig.is_archived ? "Restore page" : "Archive page",
action: pageArchiveConfig.action, action: pageArchiveConfig.action,
}) });
} }
return KanbanMenuOptions return KanbanMenuOptions;
} };

View File

@ -1,12 +1,13 @@
import React, { useEffect, useRef, useState, ReactElement } from "react"; import React, { useEffect, useRef, useState, ReactElement } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR, { mutate } from "swr"; import useSWR from "swr";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
// services // services
import { PageService } from "services/page.service"; import { PageService } from "services/page.service";
import { useDebouncedCallback } from "use-debounce"; import { FileService } from "services/file.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import { useDebouncedCallback } from "use-debounce";
// layouts // layouts
import { AppLayout } from "layouts/app-layout"; import { AppLayout } from "layouts/app-layout";
// components // components
@ -24,7 +25,6 @@ import { NextPageWithLayout } from "types/app";
import { IPage } from "types"; import { IPage } from "types";
// fetch-keys // fetch-keys
import { PAGE_DETAILS } from "constants/fetch-keys"; import { PAGE_DETAILS } from "constants/fetch-keys";
import { FileService } from "services/file.service";
// services // services
const fileService = new FileService(); const fileService = new FileService();
@ -45,10 +45,14 @@ const PageDetailsPage: NextPageWithLayout = () => {
}); });
// =================== Fetching Page Details ====================== // =================== Fetching Page Details ======================
const { data: pageDetails, error } = useSWR( const {
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null, data: pageDetails,
workspaceSlug && projectId mutate: mutatePageDetails,
? () => pageService.getPageDetails(workspaceSlug as string, projectId as string, pageId as string) error,
} = useSWR(
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId.toString()) : null,
workspaceSlug && projectId && pageId
? () => pageService.getPageDetails(workspaceSlug.toString(), projectId.toString(), pageId.toString())
: null : null
); );
@ -57,9 +61,10 @@ const PageDetailsPage: NextPageWithLayout = () => {
if (!formData.name || formData.name.length === 0 || formData.name === "") return; if (!formData.name || formData.name.length === 0 || formData.name === "") return;
await pageService.patchPage(workspaceSlug as string, projectId as string, pageId as string, formData).then(() => { await pageService
mutate<IPage>( .patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), formData)
PAGE_DETAILS(pageId as string), .then(() => {
mutatePageDetails(
(prevData) => ({ (prevData) => ({
...prevData, ...prevData,
...formData, ...formData,
@ -70,7 +75,9 @@ const PageDetailsPage: NextPageWithLayout = () => {
}; };
const createPage = async (payload: Partial<IPage>) => { const createPage = async (payload: Partial<IPage>) => {
await pageService.createPage(workspaceSlug as string, projectId as string, payload); if (!workspaceSlug || !projectId) return;
await pageService.createPage(workspaceSlug.toString(), projectId.toString(), payload);
}; };
// ================ Page Menu Actions ================== // ================ Page Menu Actions ==================
@ -84,79 +91,79 @@ const PageDetailsPage: NextPageWithLayout = () => {
}; };
const archivePage = async () => { const archivePage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
try { try {
await pageService.archivePage(workspaceSlug as string, projectId as string, pageId as string).then(() => { mutatePageDetails((prevData) => {
mutate<IPage>( if (!prevData) return;
PAGE_DETAILS(pageId as string),
(prevData) => { return {
if (prevData && prevData.is_locked) { ...prevData,
prevData.archived_at = renderDateFormat(new Date()); archived_at: renderDateFormat(new Date()),
return prevData; };
} }, true);
},
true await pageService.archivePage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
);
});
} catch (e) { } catch (e) {
console.log(e); mutatePageDetails();
} }
}; };
const unArchivePage = async () => { const unArchivePage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
try { try {
await pageService.restorePage(workspaceSlug as string, projectId as string, pageId as string).then(() => { mutatePageDetails((prevData) => {
mutate<IPage>( if (!prevData) return;
PAGE_DETAILS(pageId as string),
(prevData) => { return {
if (prevData && prevData.is_locked) { ...prevData,
prevData.archived_at = null; archived_at: null,
return prevData; };
} }, false);
},
true await pageService.restorePage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
);
});
} catch (e) { } catch (e) {
console.log(e); mutatePageDetails();
} }
}; };
// ========================= Page Lock ========================== // ========================= Page Lock ==========================
const lockPage = async () => { const lockPage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
try { try {
await pageService.lockPage(workspaceSlug as string, projectId as string, pageId as string).then(() => { mutatePageDetails((prevData) => {
mutate<IPage>( if (!prevData) return;
PAGE_DETAILS(pageId as string),
(prevData) => { return {
if (prevData && prevData.is_locked) { ...prevData,
prevData.is_locked = true; is_locked: true,
} };
return prevData; }, false);
},
true await pageService.lockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
);
});
} catch (e) { } catch (e) {
console.log(e); mutatePageDetails();
} }
}; };
const unlockPage = async () => { const unlockPage = async () => {
if (!workspaceSlug || !projectId || !pageId) return;
try { try {
await pageService.unlockPage(workspaceSlug as string, projectId as string, pageId as string).then(() => { mutatePageDetails((prevData) => {
mutate<IPage>( if (!prevData) return;
PAGE_DETAILS(pageId as string),
(prevData) => { return {
if (prevData && prevData.is_locked) { ...prevData,
prevData.is_locked = false; is_locked: false,
return prevData; };
} }, false);
},
true await pageService.unlockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
);
});
} catch (e) { } catch (e) {
console.log(e); mutatePageDetails();
} }
}; };
@ -185,8 +192,8 @@ const PageDetailsPage: NextPageWithLayout = () => {
}} }}
/> />
) : pageDetails ? ( ) : pageDetails ? (
<div className="flex h-full flex-col justify-between pl-5 pr-5"> <div className="flex h-full flex-col justify-between">
<div className="h-full w-full"> <div className="h-full w-full overflow-hidden">
{pageDetails.is_locked || pageDetails.archived_at ? ( {pageDetails.is_locked || pageDetails.archived_at ? (
<DocumentReadOnlyEditorWithRef <DocumentReadOnlyEditorWithRef
ref={editorRef} ref={editorRef}