[WEB-1322] dev: conflict free pages collaboration (#4463)

* chore: pages realtime

* chore: empty binary response

* chore: added a ypy package

* feat: pages collaboration

* chore: update fetching logic

* chore: degrade ypy version

* chore: replace useEffect fetch logic with useSWR

* chore: move all the update logic to the page store

* refactor: remove react-hook-form

* chore: save description_html as well

* chore: migrate old data logic

* fix: added description_binary as field name

* fix: code cleanup

* refactor: create separate hook to handle page description

* fix: build errors

* chore: combine updates instead of using the whole document

* chore: removed ypy package

* chore: added conflict resolving logic to the client side

* chore: add a save changes button

* chore: add read-only validation

* chore: remove saving state information

* chore: added permission class

* chore: removed the migration file

* chore: corrected the model field

* chore: rename pageStore to page

* chore: update collaboration provider

* chore: add try catch to handle error

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
Aaryan Khandelwal 2024-05-26 16:37:10 +05:30 committed by sriram veeraghanta
parent a04ce5abfc
commit ff03c0b718
42 changed files with 1134 additions and 509 deletions

View file

@ -1,30 +1,26 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { FileText } from "lucide-react";
// hooks
// ui
import { Breadcrumbs, Button } from "@plane/ui";
// helpers
import { BreadcrumbLink } from "@/components/common";
// components
import { BreadcrumbLink } from "@/components/common";
import { ProjectLogo } from "@/components/project";
import { useCommandPalette, usePage, useProject } from "@/hooks/store";
// hooks
import { usePage, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export interface IPagesHeaderProps {
showButton?: boolean;
}
export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
const { showButton = false } = props;
export const PageDetailsHeader = observer(() => {
// router
const router = useRouter();
const { workspaceSlug, pageId } = router.query;
// store hooks
const { toggleCreatePageModal } = useCommandPalette();
const { currentProjectDetails } = useProject();
const { name } = usePage(pageId?.toString() ?? "");
const { isContentEditable, isSubmitting, name } = usePage(pageId?.toString() ?? "");
// use platform
const { platform } = usePlatformOS();
// derived values
const isMac = platform === "MacOS";
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
@ -77,12 +73,24 @@ export const PageDetailsHeader: FC<IPagesHeaderProps> = observer((props) => {
</Breadcrumbs>
</div>
</div>
{showButton && (
<div className="flex items-center gap-2">
<Button variant="primary" size="sm" onClick={() => toggleCreatePageModal(true)}>
Add Page
</Button>
</div>
{isContentEditable && (
<Button
variant="primary"
size="sm"
onClick={() => {
// ctrl/cmd + s to save the changes
const event = new KeyboardEvent("keydown", {
key: "s",
ctrlKey: !isMac,
metaKey: isMac,
});
window.dispatchEvent(event);
}}
className="flex-shrink-0"
loading={isSubmitting === "submitting"}
>
{isSubmitting === "submitting" ? "Saving" : "Save changes"}
</Button>
)}
</div>
);

View file

@ -1,8 +1,7 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Control, Controller } from "react-hook-form";
// document editor
// document-editor
import {
DocumentEditorWithRef,
DocumentReadOnlyEditorWithRef,
@ -11,15 +10,15 @@ import {
IMarking,
} from "@plane/document-editor";
// types
import { IUserLite, TPage } from "@plane/types";
import { IUserLite } from "@plane/types";
// components
import { PageContentBrowser, PageContentLoader, PageEditorTitle } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useMember, useMention, useUser, useWorkspace } from "@/hooks/store";
import { usePageDescription } from "@/hooks/use-page-description";
import { usePageFilters } from "@/hooks/use-page-filters";
import useReloadConfirmations from "@/hooks/use-reload-confirmation";
// services
import { FileService } from "@/services/file.service";
// store
@ -28,13 +27,10 @@ import { IPageStore } from "@/store/pages/page.store";
const fileService = new FileService();
type Props = {
control: Control<TPage, any>;
editorRef: React.RefObject<EditorRefApi>;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
swrPageDetails: TPage | undefined;
handleSubmit: () => void;
markings: IMarking[];
pageStore: IPageStore;
page: IPageStore;
sidePeekVisible: boolean;
handleEditorReady: (value: boolean) => void;
handleReadOnlyEditorReady: (value: boolean) => void;
@ -43,15 +39,12 @@ type Props = {
export const PageEditorBody: React.FC<Props> = observer((props) => {
const {
control,
handleReadOnlyEditorReady,
handleEditorReady,
editorRef,
markings,
readOnlyEditorRef,
handleSubmit,
pageStore,
swrPageDetails,
page,
sidePeekVisible,
updateMarkings,
} = props;
@ -67,11 +60,19 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
} = useMember();
// derived values
const workspaceId = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString())?.id ?? "" : "";
const pageTitle = pageStore?.name ?? "";
const pageDescription = pageStore?.description_html;
const { description_html, isContentEditable, updateTitle, isSubmitting, setIsSubmitting } = pageStore;
const pageId = page?.id;
const pageTitle = page?.name ?? "";
const pageDescription = page?.description_html;
const { isContentEditable, updateTitle, setIsSubmitting } = page;
const projectMemberIds = projectId ? getProjectMemberIds(projectId.toString()) : [];
const projectMemberDetails = projectMemberIds?.map((id) => getUserDetails(id) as IUserLite);
// project-description
const { handleDescriptionChange, isDescriptionReady, pageDescriptionYJS } = usePageDescription({
editorRef,
page,
projectId,
workspaceSlug,
});
// use-mention
const { mentionHighlights, mentionSuggestions } = useMention({
workspaceSlug: workspaceSlug?.toString() ?? "",
@ -82,13 +83,11 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
// page filters
const { isFullWidth } = usePageFilters();
const { setShowAlert } = useReloadConfirmations(isSubmitting === "submitting");
useEffect(() => {
updateMarkings(description_html ?? "<p></p>");
}, [description_html, updateMarkings]);
updateMarkings(pageDescription ?? "<p></p>");
}, [pageDescription, updateMarkings]);
if (pageDescription === undefined) return <PageContentLoader />;
if (pageId === undefined || !pageDescriptionYJS || !isDescriptionReady) return <PageContentLoader />;
return (
<div className="flex items-center h-full w-full overflow-y-auto">
@ -122,35 +121,24 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
/>
</div>
{isContentEditable ? (
<Controller
name="description_html"
control={control}
render={({ field: { onChange } }) => (
<DocumentEditorWithRef
fileHandler={{
cancel: fileService.cancelUpload,
delete: fileService.getDeleteImageFunction(workspaceId),
restore: fileService.getRestoreImageFunction(workspaceId),
upload: fileService.getUploadFileFunction(workspaceSlug as string, setIsSubmitting),
}}
handleEditorReady={handleEditorReady}
initialValue={pageDescription ?? "<p></p>"}
value={swrPageDetails?.description_html ?? "<p></p>"}
ref={editorRef}
containerClassName="p-0 pb-64"
editorClassName="lg:px-10 pl-8"
onChange={(_description_json, description_html) => {
setIsSubmitting("submitting");
setShowAlert(true);
onChange(description_html);
handleSubmit();
}}
mentionHandler={{
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
/>
)}
<DocumentEditorWithRef
id={pageId}
fileHandler={{
cancel: fileService.cancelUpload,
delete: fileService.getDeleteImageFunction(workspaceId),
restore: fileService.getRestoreImageFunction(workspaceId),
upload: fileService.getUploadFileFunction(workspaceSlug as string, setIsSubmitting),
}}
handleEditorReady={handleEditorReady}
value={pageDescriptionYJS}
ref={editorRef}
containerClassName="p-0 pb-64"
editorClassName="pl-10"
onChange={handleDescriptionChange}
mentionHandler={{
highlights: mentionHighlights,
suggestions: mentionSuggestions,
}}
/>
) : (
<DocumentReadOnlyEditorWithRef
@ -158,7 +146,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
initialValue={pageDescription ?? "<p></p>"}
handleEditorReady={handleReadOnlyEditorReady}
containerClassName="p-0 pb-64 border-none"
editorClassName="lg:px-10 pl-8"
editorClassName="pl-10"
mentionHandler={{
highlights: mentionHighlights,
}}

View file

@ -1,6 +1,6 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { Lock, RefreshCw, Sparkle } from "lucide-react";
import { Lock, Sparkle } from "lucide-react";
// editor
import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/document-editor";
// ui
@ -9,7 +9,6 @@ import { ArchiveIcon } from "@plane/ui";
import { GptAssistantPopover } from "@/components/core";
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
// helpers
import { cn } from "@/helpers/common.helper";
import { renderFormattedDate } from "@/helpers/date-time.helper";
// hooks
import { useInstance } from "@/hooks/store";
@ -19,20 +18,19 @@ import { IPageStore } from "@/store/pages/page.store";
type Props = {
editorRef: React.RefObject<EditorRefApi>;
handleDuplicatePage: () => void;
isSyncing: boolean;
pageStore: IPageStore;
page: IPageStore;
projectId: string;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
};
export const PageExtraOptions: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, isSyncing, pageStore, projectId, readOnlyEditorRef } = props;
const { editorRef, handleDuplicatePage, page, projectId, readOnlyEditorRef } = props;
// states
const [gptModalOpen, setGptModal] = useState(false);
// store hooks
const { config } = useInstance();
// derived values
const { archived_at, isContentEditable, isSubmitting, is_locked } = pageStore;
const { archived_at, isContentEditable, is_locked } = page;
const handleAiAssistance = async (response: string) => {
if (!editorRef) return;
@ -41,22 +39,6 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
return (
<div className="flex flex-grow items-center justify-end gap-3">
{isContentEditable && (
<div
className={cn("fade-in flex items-center gap-x-2 transition-all duration-300", {
"fade-out": isSubmitting === "saved",
})}
>
{isSubmitting === "submitting" && <RefreshCw className="h-4 w-4 stroke-custom-text-300" />}
<span className="text-sm text-custom-text-300">{isSubmitting === "submitting" ? "Saving..." : "Saved"}</span>
</div>
)}
{isSyncing && (
<div className="flex items-center gap-x-2">
<RefreshCw className="h-4 w-4 stroke-custom-text-300" />
<span className="text-sm text-custom-text-300">Syncing...</span>
</div>
)}
{is_locked && (
<div className="flex h-7 items-center gap-2 rounded-full bg-custom-background-80 px-3 py-0.5 text-xs font-medium text-custom-text-300">
<Lock className="h-3 w-3" />
@ -93,11 +75,11 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
className="!min-w-[38rem]"
/>
)}
<PageInfoPopover pageStore={pageStore} />
<PageInfoPopover page={page} />
<PageOptionsDropdown
editorRef={isContentEditable ? editorRef.current : readOnlyEditorRef.current}
handleDuplicatePage={handleDuplicatePage}
pageStore={pageStore}
page={page}
/>
</div>
);

View file

@ -7,11 +7,11 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
import { IPageStore } from "@/store/pages/page.store";
type Props = {
pageStore: IPageStore;
page: IPageStore;
};
export const PageInfoPopover: React.FC<Props> = (props) => {
const { pageStore } = props;
const { page } = props;
// states
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
// refs
@ -22,7 +22,7 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
placement: "bottom-start",
});
// derived values
const { created_at, updated_at } = pageStore;
const { created_at, updated_at } = page;
return (
<div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>

View file

@ -11,9 +11,8 @@ type Props = {
editorRef: React.RefObject<EditorRefApi>;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
handleDuplicatePage: () => void;
isSyncing: boolean;
markings: IMarking[];
pageStore: IPageStore;
page: IPageStore;
projectId: string;
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
@ -29,14 +28,13 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
markings,
readOnlyEditorReady,
handleDuplicatePage,
isSyncing,
pageStore,
page,
projectId,
sidePeekVisible,
setSidePeekVisible,
} = props;
// derived values
const { isContentEditable } = pageStore;
const { isContentEditable } = page;
// page filters
const { isFullWidth } = usePageFilters();
@ -57,8 +55,7 @@ export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
<PageExtraOptions
editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage}
isSyncing={isSyncing}
pageStore={pageStore}
page={page}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef}
/>

View file

@ -16,11 +16,11 @@ import { IPageStore } from "@/store/pages/page.store";
type Props = {
editorRef: EditorRefApi | EditorReadOnlyRefApi | null;
handleDuplicatePage: () => void;
pageStore: IPageStore;
page: IPageStore;
};
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
const { editorRef, handleDuplicatePage, pageStore } = props;
const { editorRef, handleDuplicatePage, page } = props;
// store values
const {
archived_at,
@ -33,7 +33,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
canCurrentUserDuplicatePage,
canCurrentUserLockPage,
restore,
} = pageStore;
} = page;
// store hooks
const { workspaceSlug, projectId } = useAppRouter();
// page filters

View file

@ -13,9 +13,8 @@ type Props = {
editorRef: React.RefObject<EditorRefApi>;
readOnlyEditorRef: React.RefObject<EditorReadOnlyRefApi>;
handleDuplicatePage: () => void;
isSyncing: boolean;
markings: IMarking[];
pageStore: IPageStore;
page: IPageStore;
projectId: string;
sidePeekVisible: boolean;
setSidePeekVisible: (sidePeekState: boolean) => void;
@ -31,14 +30,13 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
markings,
readOnlyEditorReady,
handleDuplicatePage,
isSyncing,
pageStore,
page,
projectId,
sidePeekVisible,
setSidePeekVisible,
} = props;
// derived values
const { isContentEditable } = pageStore;
const { isContentEditable } = page;
// page filters
const { isFullWidth } = usePageFilters();
@ -67,8 +65,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
<PageExtraOptions
editorRef={editorRef}
handleDuplicatePage={handleDuplicatePage}
isSyncing={isSyncing}
pageStore={pageStore}
page={page}
projectId={projectId}
readOnlyEditorRef={readOnlyEditorRef}
/>
@ -81,8 +78,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
readOnlyEditorReady={readOnlyEditorReady}
markings={markings}
handleDuplicatePage={handleDuplicatePage}
isSyncing={isSyncing}
pageStore={pageStore}
page={page}
projectId={projectId}
sidePeekVisible={sidePeekVisible}
setSidePeekVisible={setSidePeekVisible}

View file

@ -33,7 +33,6 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
) : (
<>
<TextArea
onChange={(e) => updateTitle(e.target.value)}
className="w-full bg-custom-background text-[1.75rem] font-semibold outline-none p-0 border-none resize-none rounded-none"
style={{
lineHeight: "1.2",
@ -46,6 +45,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
}
}}
value={title}
onChange={(e) => updateTitle(e.target.value)}
maxLength={255}
onFocus={() => setIsLengthVisible(true)}
onBlur={() => setIsLengthVisible(false)}

View file

@ -4,6 +4,5 @@ export * from "./header";
export * from "./list";
export * from "./loaders";
export * from "./modals";
export * from "./page-detail";
export * from "./pages-list-main-content";
export * from "./pages-list-view";

View file

@ -2,27 +2,116 @@
import { Loader } from "@plane/ui";
export const PageContentLoader = () => (
<div className="flex">
<div className="w-[5%]" />
<Loader className="flex-shrink-0 flex-grow">
<div className="mt-10 space-y-2">
<Loader.Item height="20px" />
<Loader.Item height="20px" width="80%" />
<Loader.Item height="20px" width="80%" />
<div className="relative w-full h-full flex flex-col">
{/* header */}
<div className="px-4 flex-shrink-0 relative flex items-center justify-between h-12 border-b border-custom-border-100">
{/* left options */}
<Loader className="flex-shrink-0 w-[280px]">
<Loader.Item width="26px" height="26px" />
</Loader>
{/* editor options */}
<div className="w-full relative flex items-center divide-x divide-custom-border-100">
<Loader className="relative flex items-center gap-1 pr-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 px-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 px-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 pl-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
</div>
<div className="mt-12 space-y-10">
{Array.from(Array(4)).map((i) => (
<div key={i}>
<Loader.Item height="25px" width="20%" />
<div className="mt-5 space-y-3">
<Loader.Item height="15px" width="40%" />
<Loader.Item height="15px" width="30%" />
<Loader.Item height="15px" width="35%" />
{/* right options */}
<Loader className="w-full relative flex justify-end items-center gap-1">
<Loader.Item width="60px" height="26px" />
<Loader.Item width="40px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
</div>
{/* content */}
<div className="px-4 w-full h-full overflow-hidden relative flex">
{/* table of content loader */}
<div className="flex-shrink-0 w-[280px] pr-5 py-5">
<Loader className="w-full space-y-4">
<Loader.Item width="100%" height="24px" />
<div className="space-y-2">
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="100%" height="12px" />
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
</Loader>
</div>
{/* editor loader */}
<div className="w-full h-full py-5">
<Loader className="relative space-y-4">
<Loader.Item width="50%" height="36px" />
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<div className="py-2">
<Loader.Item width="30px" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
</div>
</div>
))}
</Loader>
</div>
</Loader>
<div className="w-[5%]" />
</div>
</div>
);

View file

@ -23,11 +23,11 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
// store hooks
const { removePage } = useProjectPages(projectId);
const { capturePageEvent } = useEventTracker();
const pageStore = usePage(pageId);
const page = usePage(pageId);
if (!pageStore) return null;
if (!page) return null;
const { name } = pageStore;
const { name } = page;
const handleClose = () => {
setIsDeleting(false);
@ -41,7 +41,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
...page,
state: "SUCCESS",
},
});
@ -56,7 +56,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
capturePageEvent({
eventName: PAGE_DELETED,
payload: {
...pageStore,
...page,
state: "FAILED",
},
});

View file

@ -1,3 +0,0 @@
export * from "./loader";
export * from "./root";

View file

@ -1,118 +0,0 @@
import { FC } from "react";
// components/ui
import { Loader } from "@plane/ui";
export const PageDetailRootLoader: FC = () => (
<div className=" relative w-full h-full flex flex-col">
{/* header */}
<div className="px-4 flex-shrink-0 relative flex items-center justify-between h-12 border-b border-custom-border-100">
{/* left options */}
<Loader className="flex-shrink-0 w-[280px]">
<Loader.Item width="26px" height="26px" />
</Loader>
{/* editor options */}
<div className="w-full relative flex items-center divide-x divide-custom-border-100">
<Loader className="relative flex items-center gap-1 pr-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 px-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 px-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
<Loader className="relative flex items-center gap-1 pl-2">
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
</div>
{/* right options */}
<Loader className="w-full relative flex justify-end items-center gap-1">
<Loader.Item width="60px" height="26px" />
<Loader.Item width="40px" height="26px" />
<Loader.Item width="26px" height="26px" />
<Loader.Item width="26px" height="26px" />
</Loader>
</div>
{/* content */}
<div className="px-4 w-full h-full overflow-hidden relative flex">
{/* table of content loader */}
<div className="flex-shrink-0 w-[280px] pr-5 py-5">
<Loader className="w-full space-y-4">
<Loader.Item width="100%" height="24px" />
<div className="space-y-2">
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="100%" height="12px" />
<Loader.Item width="60%" height="12px" />
<div className="ml-6 space-y-2">
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
<Loader.Item width="80%" height="12px" />
<Loader.Item width="100%" height="12px" />
</div>
</Loader>
</div>
{/* editor loader */}
<div className="w-full h-full py-5">
<Loader className="relative space-y-4">
<Loader.Item width="50%" height="36px" />
<div className="space-y-2">
<div className="py-2">
<Loader.Item width="100%" height="36px" />
</div>
<Loader.Item width="80%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="60%" height="36px" />
</div>
<Loader.Item width="70%" height="22px" />
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<Loader.Item width="30px" height="30px" />
<Loader.Item width="30%" height="22px" />
</div>
<div className="py-2">
<Loader.Item width="50%" height="30px" />
</div>
<Loader.Item width="100%" height="22px" />
<div className="py-2">
<Loader.Item width="30%" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
<div className="relative flex items-center gap-2">
<div className="py-2">
<Loader.Item width="30px" height="30px" />
</div>
<Loader.Item width="30%" height="22px" />
</div>
</div>
</Loader>
</div>
</div>
</div>
);

View file

@ -1,54 +0,0 @@
import { FC, Fragment } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { PageHead } from "@/components/core";
import { useProjectPages, usePage } from "@/hooks/store";
// components
import { PageDetailRootLoader } from "./";
type TPageDetailRoot = {
projectId: string;
pageId: string;
};
export const PageDetailRoot: FC<TPageDetailRoot> = observer((props) => {
const { projectId, pageId } = props;
// hooks
const { loader } = useProjectPages(projectId);
const { id, name } = usePage(pageId);
if (loader === "init-loader") return <PageDetailRootLoader />;
if (!id) return <div className="">No page is available.</div>;
return (
<Fragment>
<PageHead title={name || "Pages"} />
<div className="relative w-full h-full flex flex-col">
<div className="flex-shrink-0 px-4 relative flex items-center justify-between h-12 border-b border-custom-border-100">
{/* header left container */}
<div className="flex-shrink-0 w-[280px]">Icon</div>
{/* header editor tool container */}
<div className="w-full relative hidden md:flex items-center divide-x divide-custom-border-100 ">
Editor keys
</div>
{/* header right operations container */}
<div className="w-full relative flex justify-end">right saved</div>
</div>
{/* editor container for small screens */}
<div className="px-4 h-12 relative flex md:hidden items-center border-b border-custom-border-100">
Editor keys
</div>
<div className="px-4 w-full h-full overflow-hidden relative flex">
{/* editor table of content content container */}
<div className="flex-shrink-0 w-[280px] pr-5 py-5">Table of content</div>
{/* editor container */}
<div className="w-full h-full py-5">Editor Container</div>
</div>
</div>
</Fragment>
);
});