mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
e57b95f99e
commit
d43db7fc88
@ -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>
|
||||||
)
|
);
|
||||||
|
};
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
@ -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>
|
);
|
||||||
)
|
};
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -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";
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
@ -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 };
|
||||||
|
@ -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
|
||||||
|
@ -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 };
|
||||||
|
@ -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;
|
||||||
}
|
};
|
||||||
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user