forked from github/plane
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 {
|
||||
Icon: Icon,
|
||||
backgroundColor: string,
|
||||
textColor?: string,
|
||||
label: string,
|
||||
Icon?: Icon;
|
||||
backgroundColor: string;
|
||||
textColor?: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const AlertLabel = ({ Icon, backgroundColor,textColor, label }: IAlertLabelProps) => {
|
||||
export const AlertLabel = (props: IAlertLabelProps) => {
|
||||
const { Icon, backgroundColor, textColor, label } = props;
|
||||
|
||||
return (
|
||||
<div className={`text-xs flex items-center gap-1 ${backgroundColor} p-0.5 pl-3 pr-3 mr-1 rounded`}>
|
||||
<Icon size={12} />
|
||||
<span className={`normal-case ${textColor}`}>{label}</span>
|
||||
<div
|
||||
className={`h-7 flex items-center gap-2 font-medium py-0.5 px-3 rounded-full text-xs ${backgroundColor} ${textColor}`}
|
||||
>
|
||||
{Icon && <Icon className="h-3 w-3" />}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -8,33 +8,33 @@ interface ContentBrowserProps {
|
||||
markings: IMarking[];
|
||||
}
|
||||
|
||||
export const ContentBrowser = ({
|
||||
editor,
|
||||
markings,
|
||||
}: ContentBrowserProps) => (
|
||||
<div className="mt-4 flex w-[250px] flex-col h-full">
|
||||
<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">
|
||||
Table of Contents
|
||||
</h2>
|
||||
<div className="mt-3 h-0.5 w-full self-stretch border-custom-border" />
|
||||
{markings.length !== 0 ? (
|
||||
markings.map((marking) =>
|
||||
marking.level === 1 ? (
|
||||
<HeadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
heading={marking.text}
|
||||
/>
|
||||
export const ContentBrowser = (props: ContentBrowserProps) => {
|
||||
const { editor, markings } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<h2 className="font-medium">Table of Contents</h2>
|
||||
<div className="h-full overflow-y-auto">
|
||||
{markings.length !== 0 ? (
|
||||
markings.map((marking) =>
|
||||
marking.level === 1 ? (
|
||||
<HeadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
heading={marking.text}
|
||||
/>
|
||||
) : (
|
||||
<SubheadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
subHeading={marking.text}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<SubheadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
subHeading={marking.text}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p className="ml-3 mr-3 flex h-full items-center px-5 text-center text-xs text-gray-500">
|
||||
{"Headings will be displayed here for Navigation"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
<p className="mt-3 text-xs text-custom-text-400">
|
||||
Headings will be displayed here for navigation
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,79 +1,90 @@
|
||||
import { Editor } from "@tiptap/react"
|
||||
import { Lock, ArchiveIcon, MenuSquare } from "lucide-react"
|
||||
import { useRef, useState } from "react"
|
||||
import { usePopper } from "react-popper"
|
||||
import { IMarking, UploadImage } from ".."
|
||||
import { FixedMenu } from "../menu"
|
||||
import { DocumentDetails } from "../types/editor-types"
|
||||
import { AlertLabel } from "./alert-label"
|
||||
import { ContentBrowser } from "./content-browser"
|
||||
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu"
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { Archive, Info, Lock } from "lucide-react";
|
||||
import { IMarking, UploadImage } from "..";
|
||||
import { FixedMenu } from "../menu";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
import { AlertLabel } from "./alert-label";
|
||||
import {
|
||||
IVerticalDropdownItemProps,
|
||||
VerticalDropdownMenu,
|
||||
} from "./vertical-dropdown-menu";
|
||||
import { SummaryPopover } from "./summary-popover";
|
||||
import { InfoPopover } from "./info-popover";
|
||||
|
||||
interface IEditorHeader {
|
||||
editor: Editor,
|
||||
KanbanMenuOptions: IVerticalDropdownItemProps[],
|
||||
sidePeakVisible: boolean,
|
||||
setSidePeakVisible: (currentState: boolean) => void,
|
||||
markings: IMarking[],
|
||||
isLocked: boolean,
|
||||
isArchived: boolean,
|
||||
archivedAt?: Date,
|
||||
readonly: boolean,
|
||||
uploadFile?: UploadImage,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
documentDetails: DocumentDetails
|
||||
editor: Editor;
|
||||
KanbanMenuOptions: IVerticalDropdownItemProps[];
|
||||
sidePeekVisible: boolean;
|
||||
setSidePeekVisible: (sidePeekState: boolean) => void;
|
||||
markings: IMarking[];
|
||||
isLocked: boolean;
|
||||
isArchived: boolean;
|
||||
archivedAt?: Date;
|
||||
readonly: boolean;
|
||||
uploadFile?: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
documentDetails: DocumentDetails;
|
||||
}
|
||||
|
||||
export const EditorHeader = ({ documentDetails, archivedAt, editor, sidePeakVisible, readonly, setSidePeakVisible, markings, uploadFile, setIsSubmitting, KanbanMenuOptions, isArchived, isLocked }: IEditorHeader) => {
|
||||
|
||||
const summaryMenuRef = useRef(null);
|
||||
const summaryButtonRef = useRef(null);
|
||||
const [summaryPopoverVisible, setSummaryPopoverVisible] = useState(false);
|
||||
|
||||
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(summaryButtonRef.current, summaryMenuRef.current, {
|
||||
placement: "bottom-start"
|
||||
})
|
||||
export const EditorHeader = (props: IEditorHeader) => {
|
||||
const {
|
||||
documentDetails,
|
||||
archivedAt,
|
||||
editor,
|
||||
sidePeekVisible,
|
||||
readonly,
|
||||
setSidePeekVisible,
|
||||
markings,
|
||||
uploadFile,
|
||||
setIsSubmitting,
|
||||
KanbanMenuOptions,
|
||||
isArchived,
|
||||
isLocked,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-custom-border-200 py-2 px-5">
|
||||
<div className="flex-shrink-0 w-56 lg:w-80">
|
||||
<SummaryPopover
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={setSidePeekVisible}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-custom-border self-stretch flex flex-col border-b border-solid max-md:max-w-full">
|
||||
<div
|
||||
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">
|
||||
<div className={"flex flex-row items-center"}>
|
||||
<div
|
||||
onMouseEnter={() => setSummaryPopoverVisible(true)}
|
||||
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 className="flex-shrink-0">
|
||||
{!readonly && uploadFile && (
|
||||
<FixedMenu
|
||||
editor={editor}
|
||||
uploadFile={uploadFile}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(!readonly && uploadFile) && <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />}
|
||||
<div className="self-center flex items-start gap-3 my-auto max-md:justify-center"
|
||||
>
|
||||
{!isArchived && <p className="text-sm text-custom-text-300">{`Last updated at ${new Date(documentDetails.last_updated_at).toLocaleString()}`}</p>}
|
||||
<VerticalDropdownMenu items={KanbanMenuOptions} />
|
||||
</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} />
|
||||
</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,43 +3,32 @@ import { Editor } from "@tiptap/react";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
|
||||
interface IPageRenderer {
|
||||
sidePeakVisible: boolean;
|
||||
documentDetails: DocumentDetails;
|
||||
editor: Editor;
|
||||
editorClassNames: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
}
|
||||
|
||||
export const PageRenderer = ({
|
||||
sidePeakVisible,
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
}: IPageRenderer) => {
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const {
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex h-[88vh] flex-col w-full max-md:w-full max-md:ml-0 transition-all duration-200 ease-in-out ${
|
||||
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}
|
||||
</h1>
|
||||
</div>
|
||||
<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}>
|
||||
<div className="flex flex-col h-full w-full">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
</div>
|
||||
</EditorContainer>
|
||||
</div>
|
||||
<div className="h-full w-full overflow-y-auto pl-7 py-5">
|
||||
<h1 className="text-4xl font-bold break-all pr-5 -mt-2">
|
||||
{documentDetails.title}
|
||||
</h1>
|
||||
<div className="flex flex-col h-full w-full pr-5">
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
</EditorContainer>
|
||||
</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 { IMarking } from ".."
|
||||
import { ContentBrowser } from "./content-browser"
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { IMarking } from "..";
|
||||
import { ContentBrowser } from "./content-browser";
|
||||
|
||||
interface ISummarySideBarProps {
|
||||
editor: Editor,
|
||||
markings: IMarking[],
|
||||
sidePeakVisible: boolean
|
||||
editor: Editor;
|
||||
markings: IMarking[];
|
||||
sidePeekVisible: boolean;
|
||||
}
|
||||
|
||||
export const SummarySideBar = ({ editor, markings, sidePeakVisible }: ISummarySideBarProps) => {
|
||||
export const SummarySideBar = ({
|
||||
editor,
|
||||
markings,
|
||||
sidePeekVisible,
|
||||
}: ISummarySideBarProps) => {
|
||||
return (
|
||||
|
||||
<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'}`}>
|
||||
<div
|
||||
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} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -1,41 +1,52 @@
|
||||
import { Button, CustomMenu } from "@plane/ui"
|
||||
import { ChevronUp, Icon, MoreVertical } from "lucide-react"
|
||||
import { Button, CustomMenu } from "@plane/ui";
|
||||
import { ChevronUp, Icon, MoreVertical } from "lucide-react";
|
||||
|
||||
|
||||
type TMenuItems = "archive_page" | "unarchive_page" | "lock_page" | "unlock_page" | "copy_markdown" | "close_page" | "copy_page_link" | "duplicate_page"
|
||||
type TMenuItems =
|
||||
| "archive_page"
|
||||
| "unarchive_page"
|
||||
| "lock_page"
|
||||
| "unlock_page"
|
||||
| "copy_markdown"
|
||||
| "close_page"
|
||||
| "copy_page_link"
|
||||
| "duplicate_page";
|
||||
|
||||
export interface IVerticalDropdownItemProps {
|
||||
key: number,
|
||||
type: TMenuItems,
|
||||
Icon: Icon,
|
||||
label: string,
|
||||
action: () => Promise<void> | void
|
||||
key: number;
|
||||
type: TMenuItems;
|
||||
Icon: Icon;
|
||||
label: string;
|
||||
action: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export interface IVerticalDropdownMenuProps {
|
||||
items: IVerticalDropdownItemProps[],
|
||||
items: IVerticalDropdownItemProps[];
|
||||
}
|
||||
|
||||
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => {
|
||||
|
||||
const VerticalDropdownItem = ({
|
||||
Icon,
|
||||
label,
|
||||
action,
|
||||
}: IVerticalDropdownItemProps) => {
|
||||
return (
|
||||
<CustomMenu.MenuItem>
|
||||
<Button variant={"neutral-primary"} onClick={action} className="flex flex-row border-none items-center m-1 max-md:pr-5 cursor-pointer">
|
||||
<Icon size={16} />
|
||||
<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 onClick={action} className="flex items-center gap-2">
|
||||
<Icon className="h-3 w-3" />
|
||||
<div className="text-custom-text-300">{label}</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
|
||||
|
||||
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={
|
||||
<MoreVertical size={18}/>
|
||||
}>
|
||||
<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={<MoreVertical size={14} />}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<VerticalDropdownItem
|
||||
key={index}
|
||||
@ -46,5 +57,5 @@ export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
|
||||
/>
|
||||
))}
|
||||
</CustomMenu>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -1,34 +1,40 @@
|
||||
"use client"
|
||||
import React, { useState } from 'react';
|
||||
import { cn, getEditorClassNames, useEditor } from '@plane/editor-core';
|
||||
import { DocumentEditorExtensions } from './extensions';
|
||||
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from './types/menu-actions';
|
||||
import { EditorHeader } from './components/editor-header';
|
||||
import { useEditorMarkings } from './hooks/use-editor-markings';
|
||||
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';
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { cn, getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "./extensions";
|
||||
import {
|
||||
IDuplicationConfig,
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
} from "./types/menu-actions";
|
||||
import { EditorHeader } from "./components/editor-header";
|
||||
import { useEditorMarkings } from "./hooks/use-editor-markings";
|
||||
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 DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||
|
||||
interface IDocumentEditor {
|
||||
documentDetails: DocumentDetails,
|
||||
documentDetails: DocumentDetails;
|
||||
value: string;
|
||||
uploadFile: UploadImage;
|
||||
deleteFile: DeleteImage;
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
duplicationConfig?: IDuplicationConfig,
|
||||
pageLockConfig?: IPageLockConfig,
|
||||
pageArchiveConfig?: IPageArchiveConfig
|
||||
duplicationConfig?: IDuplicationConfig;
|
||||
pageLockConfig?: IPageLockConfig;
|
||||
pageArchiveConfig?: IPageArchiveConfig;
|
||||
}
|
||||
interface DocumentEditorProps extends IDocumentEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
@ -40,10 +46,10 @@ interface EditorHandle {
|
||||
}
|
||||
|
||||
export interface IMarking {
|
||||
type: "heading",
|
||||
level: number,
|
||||
text: string,
|
||||
sequence: number
|
||||
type: "heading";
|
||||
level: number;
|
||||
text: string;
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
const DocumentEditor = ({
|
||||
@ -60,21 +66,20 @@ const DocumentEditor = ({
|
||||
forwardedRef,
|
||||
duplicationConfig,
|
||||
pageLockConfig,
|
||||
pageArchiveConfig
|
||||
pageArchiveConfig,
|
||||
}: IDocumentEditor) => {
|
||||
|
||||
// const [alert, setAlert] = useState<string>("")
|
||||
const { markings, updateMarkings } = useEditorMarkings()
|
||||
const [sidePeakVisible, setSidePeakVisible] = useState(true)
|
||||
const router = useRouter()
|
||||
const { markings, updateMarkings } = useEditorMarkings();
|
||||
const [sidePeekVisible, setSidePeekVisible] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
const editor = useEditor({
|
||||
onChange(json, html) {
|
||||
updateMarkings(json)
|
||||
onChange(json, html)
|
||||
updateMarkings(json);
|
||||
onChange(json, html);
|
||||
},
|
||||
onStart(json) {
|
||||
updateMarkings(json)
|
||||
updateMarkings(json);
|
||||
},
|
||||
debouncedUpdatesEnabled,
|
||||
setIsSubmitting,
|
||||
@ -87,65 +92,66 @@ const DocumentEditor = ({
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const KanbanMenuOptions = getMenuOptions(
|
||||
{
|
||||
editor: editor,
|
||||
router: router,
|
||||
duplicationConfig: duplicationConfig,
|
||||
pageLockConfig: pageLockConfig,
|
||||
pageArchiveConfig: pageArchiveConfig,
|
||||
}
|
||||
)
|
||||
const editorClassNames = getEditorClassNames({ noBorder: true, borderOnFocus: false, customClassName });
|
||||
const KanbanMenuOptions = getMenuOptions({
|
||||
editor: editor,
|
||||
router: router,
|
||||
duplicationConfig: duplicationConfig,
|
||||
pageLockConfig: pageLockConfig,
|
||||
pageArchiveConfig: pageArchiveConfig,
|
||||
});
|
||||
const editorClassNames = getEditorClassNames({
|
||||
noBorder: true,
|
||||
borderOnFocus: false,
|
||||
customClassName,
|
||||
});
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="top-0 sticky z-10 bg-custom-background-100">
|
||||
<EditorHeader
|
||||
readonly={false}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
editor={editor}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
setSidePeakVisible={setSidePeakVisible}
|
||||
markings={markings}
|
||||
uploadFile={uploadFile}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-center items-stretch w-full max-md:max-w-full h-full">
|
||||
<div className={cn("gap-5 flex max-md:flex-col max-md:items-stretch max-md:gap-0 h-full", { "justify-center": !sidePeakVisible })}>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
readonly={false}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
editor={editor}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={(val) => setSidePeekVisible(val)}
|
||||
markings={markings}
|
||||
uploadFile={uploadFile}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-80">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
<PageRenderer
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
editorClassNames={editorClassNames}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
{/* Page Element */}
|
||||
</div>
|
||||
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>((props, ref) => (
|
||||
<DocumentEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>(
|
||||
(props, ref) => <DocumentEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
|
||||
DocumentEditorWithRef.displayName = "DocumentEditorWithRef";
|
||||
|
||||
export { DocumentEditor, DocumentEditorWithRef }
|
||||
export { DocumentEditor, DocumentEditorWithRef };
|
||||
|
@ -1,7 +1,22 @@
|
||||
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 "..";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
@ -14,77 +29,69 @@ export interface BubbleMenuItem {
|
||||
type EditorBubbleMenuProps = {
|
||||
editor: Editor;
|
||||
uploadFile: UploadImage;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
}
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
const { editor, uploadFile, setIsSubmitting } = props;
|
||||
|
||||
const basicMarkItems: BubbleMenuItem[] = [
|
||||
HeadingOneItem(props.editor),
|
||||
HeadingTwoItem(props.editor),
|
||||
HeadingThreeItem(props.editor),
|
||||
BoldItem(props.editor),
|
||||
ItalicItem(props.editor),
|
||||
UnderLineItem(props.editor),
|
||||
StrikeThroughItem(props.editor),
|
||||
HeadingOneItem(editor),
|
||||
HeadingTwoItem(editor),
|
||||
HeadingThreeItem(editor),
|
||||
BoldItem(editor),
|
||||
ItalicItem(editor),
|
||||
UnderLineItem(editor),
|
||||
StrikeThroughItem(editor),
|
||||
];
|
||||
|
||||
const listItems: BubbleMenuItem[] = [
|
||||
BulletListItem(props.editor),
|
||||
NumberedListItem(props.editor),
|
||||
BulletListItem(editor),
|
||||
NumberedListItem(editor),
|
||||
];
|
||||
|
||||
const userActionItems: BubbleMenuItem[] = [
|
||||
QuoteItem(props.editor),
|
||||
CodeItem(props.editor),
|
||||
QuoteItem(editor),
|
||||
CodeItem(editor),
|
||||
];
|
||||
|
||||
const complexItems: BubbleMenuItem[] = [
|
||||
TableItem(props.editor),
|
||||
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
|
||||
TableItem(editor),
|
||||
ImageItem(editor, uploadFile, setIsSubmitting),
|
||||
];
|
||||
|
||||
// const handleAccessChange = (accessKey: string) => {
|
||||
// props.commentAccessSpecifier?.onAccessChange(accessKey);
|
||||
// };
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-fit rounded bg-custom-background-100"
|
||||
>
|
||||
<div className="flex">
|
||||
{basicMarkItems.map((item, index) => (
|
||||
<div className="flex items-center divide-x divide-custom-border-200">
|
||||
<div className="flex items-center gap-0.5 pr-2">
|
||||
{basicMarkItems.map((item) => (
|
||||
<button
|
||||
key={index}
|
||||
key={item.name}
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
size={ item.icon === Heading1 || item.icon === Heading2 || item.icon === Heading3 ? 20 : 15}
|
||||
className={cn({
|
||||
"text-custom-text-100": item.isActive(),
|
||||
})}
|
||||
/>
|
||||
<item.icon className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{listItems.map((item, index) => (
|
||||
<div className="flex items-center gap-0.5 px-2">
|
||||
{listItems.map((item) => (
|
||||
<button
|
||||
key={index}
|
||||
key={item.name}
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
@ -95,17 +102,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{userActionItems.map((item, index) => (
|
||||
<div className="flex items-center gap-0.5 px-2">
|
||||
{userActionItems.map((item) => (
|
||||
<button
|
||||
key={index}
|
||||
key={item.name}
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
@ -116,17 +123,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{complexItems.map((item, index) => (
|
||||
<div className="flex items-center gap-0.5 pl-2">
|
||||
{complexItems.map((item) => (
|
||||
<button
|
||||
key={index}
|
||||
key={item.name}
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
|
@ -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 { useState, forwardRef, useEffect } from 'react'
|
||||
import { useState, forwardRef, useEffect } from "react";
|
||||
import { EditorHeader } from "../components/editor-header";
|
||||
import { PageRenderer } from "../components/page-renderer";
|
||||
import { SummarySideBar } from "../components/summary-side-bar";
|
||||
import { useEditorMarkings } from "../hooks/use-editor-markings";
|
||||
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";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
value: string,
|
||||
noBorder: boolean,
|
||||
borderOnFocus: boolean,
|
||||
customClassName: string,
|
||||
documentDetails: DocumentDetails,
|
||||
pageLockConfig?: IPageLockConfig,
|
||||
pageArchiveConfig?: IPageArchiveConfig,
|
||||
pageDuplicationConfig?: IDuplicationConfig,
|
||||
value: string;
|
||||
noBorder: boolean;
|
||||
borderOnFocus: boolean;
|
||||
customClassName: string;
|
||||
documentDetails: DocumentDetails;
|
||||
pageLockConfig?: IPageLockConfig;
|
||||
pageArchiveConfig?: IPageArchiveConfig;
|
||||
pageDuplicationConfig?: IDuplicationConfig;
|
||||
}
|
||||
|
||||
interface DocumentReadOnlyEditorProps extends IDocumentReadOnlyEditor {
|
||||
forwardedRef?: React.Ref<EditorHandle>
|
||||
forwardedRef?: React.Ref<EditorHandle>;
|
||||
}
|
||||
|
||||
interface EditorHandle {
|
||||
@ -40,32 +44,30 @@ const DocumentReadOnlyEditor = ({
|
||||
pageLockConfig,
|
||||
pageArchiveConfig,
|
||||
}: DocumentReadOnlyEditorProps) => {
|
||||
|
||||
const router = useRouter()
|
||||
const [sidePeakVisible, setSidePeakVisible] = useState(true)
|
||||
const { markings, updateMarkings } = useEditorMarkings()
|
||||
const router = useRouter();
|
||||
const [sidePeekVisible, setSidePeekVisible] = useState(true);
|
||||
const { markings, updateMarkings } = useEditorMarkings();
|
||||
|
||||
const editor = useReadOnlyEditor({
|
||||
value,
|
||||
forwardedRef,
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
updateMarkings(editor.getJSON())
|
||||
updateMarkings(editor.getJSON());
|
||||
}
|
||||
}, [editor?.getJSON()])
|
||||
}, [editor?.getJSON()]);
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const editorClassNames = getEditorClassNames({
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName
|
||||
})
|
||||
customClassName,
|
||||
});
|
||||
|
||||
const KanbanMenuOptions = getMenuOptions({
|
||||
editor: editor,
|
||||
@ -73,43 +75,42 @@ const DocumentReadOnlyEditor = ({
|
||||
pageArchiveConfig: pageArchiveConfig,
|
||||
pageLockConfig: pageLockConfig,
|
||||
duplicationConfig: pageDuplicationConfig,
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="top-0 sticky z-10 bg-custom-background-100">
|
||||
<EditorHeader
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
readonly={true}
|
||||
editor={editor}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
setSidePeakVisible={setSidePeakVisible}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
markings={markings}
|
||||
documentDetails={documentDetails}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-center items-stretch w-full max-md:max-w-full overflow-y-hidden">
|
||||
<div className={cn("gap-5 flex max-md:flex-col max-md:items-stretch max-md:gap-0 overflow-y-hidden", { "justify-center": !sidePeakVisible })}>
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
readonly={true}
|
||||
editor={editor}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
setSidePeekVisible={setSidePeekVisible}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
markings={markings}
|
||||
documentDetails={documentDetails}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-hidden">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-80">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
<PageRenderer
|
||||
editor={editor}
|
||||
editorClassNames={editorClassNames}
|
||||
sidePeakVisible={sidePeakVisible}
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<
|
||||
EditorHandle,
|
||||
@ -118,4 +119,4 @@ const DocumentReadOnlyEditorWithRef = forwardRef<
|
||||
|
||||
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";
|
||||
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef }
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef };
|
||||
|
@ -1,75 +1,94 @@
|
||||
import { Editor } from "@tiptap/react"
|
||||
import { Archive, ArchiveIcon, ArchiveRestoreIcon, 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"
|
||||
import { Editor } from "@tiptap/react";
|
||||
import {
|
||||
Archive,
|
||||
ArchiveIcon,
|
||||
ArchiveRestoreIcon,
|
||||
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{
|
||||
editor: Editor,
|
||||
router: NextRouter,
|
||||
duplicationConfig?: IDuplicationConfig,
|
||||
pageLockConfig?: IPageLockConfig ,
|
||||
pageArchiveConfig?: IPageArchiveConfig,
|
||||
export interface MenuOptionsProps {
|
||||
editor: Editor;
|
||||
router: NextRouter;
|
||||
duplicationConfig?: IDuplicationConfig;
|
||||
pageLockConfig?: IPageLockConfig;
|
||||
pageArchiveConfig?: IPageArchiveConfig;
|
||||
}
|
||||
|
||||
export const getMenuOptions = ({ editor, router, duplicationConfig, pageLockConfig, pageArchiveConfig } : MenuOptionsProps) => {
|
||||
|
||||
export const getMenuOptions = ({
|
||||
editor,
|
||||
router,
|
||||
duplicationConfig,
|
||||
pageLockConfig,
|
||||
pageArchiveConfig,
|
||||
}: MenuOptionsProps) => {
|
||||
const KanbanMenuOptions: IVerticalDropdownItemProps[] = [
|
||||
{
|
||||
key: 1,
|
||||
key: 1,
|
||||
type: "copy_markdown",
|
||||
Icon: ClipboardIcon,
|
||||
action: () => copyMarkdownToClipboard(editor),
|
||||
label: "Copy Markdown"
|
||||
label: "Copy markdown",
|
||||
},
|
||||
// {
|
||||
// key: 2,
|
||||
// type: "close_page",
|
||||
// Icon: XCircle,
|
||||
// action: () => router.back(),
|
||||
// label: "Close page",
|
||||
// },
|
||||
{
|
||||
key: 2,
|
||||
type: "close_page",
|
||||
Icon: XCircle,
|
||||
action: () => router.back(),
|
||||
label: "Close the page"
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
key: 3,
|
||||
type: "copy_page_link",
|
||||
Icon: Link,
|
||||
action: () => CopyPageLink(),
|
||||
label: "Copy Page Link"
|
||||
label: "Copy page link",
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
// If duplicateConfig is given, page duplication will be allowed
|
||||
if (duplicationConfig) {
|
||||
KanbanMenuOptions.push({
|
||||
key: KanbanMenuOptions.length++,
|
||||
key: KanbanMenuOptions.length++,
|
||||
type: "duplicate_page",
|
||||
Icon: Copy,
|
||||
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 (pageLockConfig) {
|
||||
KanbanMenuOptions.push({
|
||||
key: KanbanMenuOptions.length++,
|
||||
key: KanbanMenuOptions.length++,
|
||||
type: pageLockConfig.is_locked ? "unlock_page" : "lock_page",
|
||||
Icon: pageLockConfig.is_locked ? Unlock : Lock,
|
||||
label: pageLockConfig.is_locked ? "Unlock Page" : "Lock Page",
|
||||
action: pageLockConfig.action
|
||||
})
|
||||
label: pageLockConfig.is_locked ? "Unlock page" : "Lock page",
|
||||
action: pageLockConfig.action,
|
||||
});
|
||||
}
|
||||
|
||||
// Archiving will be visible in the menu bar config once the pageArchiveConfig is given.
|
||||
if (pageArchiveConfig) {
|
||||
KanbanMenuOptions.push({
|
||||
key: KanbanMenuOptions.length++,
|
||||
key: KanbanMenuOptions.length++,
|
||||
type: pageArchiveConfig.is_archived ? "unarchive_page" : "archive_page",
|
||||
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,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return KanbanMenuOptions
|
||||
}
|
||||
return KanbanMenuOptions;
|
||||
};
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useRef, useState, ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import useSWR from "swr";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// services
|
||||
import { PageService } from "services/page.service";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { FileService } from "services/file.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
@ -24,7 +25,6 @@ import { NextPageWithLayout } from "types/app";
|
||||
import { IPage } from "types";
|
||||
// fetch-keys
|
||||
import { PAGE_DETAILS } from "constants/fetch-keys";
|
||||
import { FileService } from "services/file.service";
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
@ -45,10 +45,14 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
||||
});
|
||||
|
||||
// =================== Fetching Page Details ======================
|
||||
const { data: pageDetails, error } = useSWR(
|
||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => pageService.getPageDetails(workspaceSlug as string, projectId as string, pageId as string)
|
||||
const {
|
||||
data: pageDetails,
|
||||
mutate: mutatePageDetails,
|
||||
error,
|
||||
} = useSWR(
|
||||
workspaceSlug && projectId && pageId ? PAGE_DETAILS(pageId.toString()) : null,
|
||||
workspaceSlug && projectId && pageId
|
||||
? () => pageService.getPageDetails(workspaceSlug.toString(), projectId.toString(), pageId.toString())
|
||||
: null
|
||||
);
|
||||
|
||||
@ -57,20 +61,23 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
||||
|
||||
if (!formData.name || formData.name.length === 0 || formData.name === "") return;
|
||||
|
||||
await pageService.patchPage(workspaceSlug as string, projectId as string, pageId as string, formData).then(() => {
|
||||
mutate<IPage>(
|
||||
PAGE_DETAILS(pageId as string),
|
||||
(prevData) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
await pageService
|
||||
.patchPage(workspaceSlug.toString(), projectId.toString(), pageId.toString(), formData)
|
||||
.then(() => {
|
||||
mutatePageDetails(
|
||||
(prevData) => ({
|
||||
...prevData,
|
||||
...formData,
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 ==================
|
||||
@ -84,79 +91,79 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
||||
};
|
||||
|
||||
const archivePage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
try {
|
||||
await pageService.archivePage(workspaceSlug as string, projectId as string, pageId as string).then(() => {
|
||||
mutate<IPage>(
|
||||
PAGE_DETAILS(pageId as string),
|
||||
(prevData) => {
|
||||
if (prevData && prevData.is_locked) {
|
||||
prevData.archived_at = renderDateFormat(new Date());
|
||||
return prevData;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
mutatePageDetails((prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
archived_at: renderDateFormat(new Date()),
|
||||
};
|
||||
}, true);
|
||||
|
||||
await pageService.archivePage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
mutatePageDetails();
|
||||
}
|
||||
};
|
||||
|
||||
const unArchivePage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
try {
|
||||
await pageService.restorePage(workspaceSlug as string, projectId as string, pageId as string).then(() => {
|
||||
mutate<IPage>(
|
||||
PAGE_DETAILS(pageId as string),
|
||||
(prevData) => {
|
||||
if (prevData && prevData.is_locked) {
|
||||
prevData.archived_at = null;
|
||||
return prevData;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
mutatePageDetails((prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
archived_at: null,
|
||||
};
|
||||
}, false);
|
||||
|
||||
await pageService.restorePage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
mutatePageDetails();
|
||||
}
|
||||
};
|
||||
|
||||
// ========================= Page Lock ==========================
|
||||
const lockPage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
try {
|
||||
await pageService.lockPage(workspaceSlug as string, projectId as string, pageId as string).then(() => {
|
||||
mutate<IPage>(
|
||||
PAGE_DETAILS(pageId as string),
|
||||
(prevData) => {
|
||||
if (prevData && prevData.is_locked) {
|
||||
prevData.is_locked = true;
|
||||
}
|
||||
return prevData;
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
mutatePageDetails((prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
is_locked: true,
|
||||
};
|
||||
}, false);
|
||||
|
||||
await pageService.lockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
mutatePageDetails();
|
||||
}
|
||||
};
|
||||
|
||||
const unlockPage = async () => {
|
||||
if (!workspaceSlug || !projectId || !pageId) return;
|
||||
|
||||
try {
|
||||
await pageService.unlockPage(workspaceSlug as string, projectId as string, pageId as string).then(() => {
|
||||
mutate<IPage>(
|
||||
PAGE_DETAILS(pageId as string),
|
||||
(prevData) => {
|
||||
if (prevData && prevData.is_locked) {
|
||||
prevData.is_locked = false;
|
||||
return prevData;
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
mutatePageDetails((prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
is_locked: false,
|
||||
};
|
||||
}, false);
|
||||
|
||||
await pageService.unlockPage(workspaceSlug.toString(), projectId.toString(), pageId.toString());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
mutatePageDetails();
|
||||
}
|
||||
};
|
||||
|
||||
@ -185,8 +192,8 @@ const PageDetailsPage: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
) : pageDetails ? (
|
||||
<div className="flex h-full flex-col justify-between pl-5 pr-5">
|
||||
<div className="h-full w-full">
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{pageDetails.is_locked || pageDetails.archived_at ? (
|
||||
<DocumentReadOnlyEditorWithRef
|
||||
ref={editorRef}
|
||||
|
Loading…
Reference in New Issue
Block a user