[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:
parent
a04ce5abfc
commit
ff03c0b718
42 changed files with 1134 additions and 509 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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)}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./loader";
|
||||
|
||||
export * from "./root";
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue