[WIKI-320] refactor: page header actions (#6946)
* refactor: page header actions * chore: update toolbar component * chore: update archived and lock badge colors * chore: added observer to favorite control
This commit is contained in:
parent
8166a757a7
commit
eac1115566
29 changed files with 557 additions and 290 deletions
|
|
@ -161,7 +161,6 @@ const PageDetailsPage = observer(() => {
|
||||||
config={pageRootConfig}
|
config={pageRootConfig}
|
||||||
handlers={pageRootHandlers}
|
handlers={pageRootHandlers}
|
||||||
page={page}
|
page={page}
|
||||||
storeType={EPageStoreType.PROJECT}
|
|
||||||
webhookConnectionParams={webhookConnectionParams}
|
webhookConnectionParams={webhookConnectionParams}
|
||||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import { ICustomSearchSelectOption } from "@plane/types";
|
||||||
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
import { Breadcrumbs, Header, CustomSearchSelect } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink, PageAccessIcon, SwitcherLabel } from "@/components/common";
|
import { BreadcrumbLink, PageAccessIcon, SwitcherLabel } from "@/components/common";
|
||||||
import { PageEditInformationPopover } from "@/components/pages";
|
import { PageHeaderActions } from "@/components/pages/header/actions";
|
||||||
// helpers
|
// helpers
|
||||||
// hooks
|
|
||||||
import { getPageName } from "@/helpers/page.helper";
|
import { getPageName } from "@/helpers/page.helper";
|
||||||
|
// hooks
|
||||||
import { useProject } from "@/hooks/store";
|
import { useProject } from "@/hooks/store";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
|
|
@ -24,21 +24,22 @@ export interface IPagesHeaderProps {
|
||||||
showButton?: boolean;
|
showButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storeType = EPageStoreType.PROJECT;
|
||||||
|
|
||||||
export const PageDetailsHeader = observer(() => {
|
export const PageDetailsHeader = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useAppRouter();
|
const router = useAppRouter();
|
||||||
const { workspaceSlug, pageId, projectId } = useParams();
|
const { workspaceSlug, pageId, projectId } = useParams();
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentProjectDetails, loader } = useProject();
|
const { currentProjectDetails, loader } = useProject();
|
||||||
|
const { getPageById, getCurrentProjectPageIds } = usePageStore(storeType);
|
||||||
const page = usePage({
|
const page = usePage({
|
||||||
pageId: pageId?.toString() ?? "",
|
pageId: pageId?.toString() ?? "",
|
||||||
storeType: EPageStoreType.PROJECT,
|
storeType,
|
||||||
});
|
});
|
||||||
const { getPageById, getCurrentProjectPageIds } = usePageStore(EPageStoreType.PROJECT);
|
|
||||||
// derived values
|
// derived values
|
||||||
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
const projectPageIds = getCurrentProjectPageIds(projectId?.toString());
|
||||||
|
|
||||||
if (!page) return null;
|
|
||||||
const switcherOptions = projectPageIds
|
const switcherOptions = projectPageIds
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
const _page = id === pageId ? page : getPageById(id);
|
const _page = id === pageId ? page : getPageById(id);
|
||||||
|
|
@ -109,8 +110,8 @@ export const PageDetailsHeader = observer(() => {
|
||||||
</div>
|
</div>
|
||||||
</Header.LeftItem>
|
</Header.LeftItem>
|
||||||
<Header.RightItem>
|
<Header.RightItem>
|
||||||
<PageEditInformationPopover page={page} />
|
|
||||||
<PageDetailsHeaderExtraActions page={page} />
|
<PageDetailsHeaderExtraActions page={page} />
|
||||||
|
<PageHeaderActions page={page} storeType={storeType} />
|
||||||
</Header.RightItem>
|
</Header.RightItem>
|
||||||
</Header>
|
</Header>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { RefObject, useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { ChevronRight, CornerDownRight, LucideIcon, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react";
|
import { ChevronRight, CornerDownRight, LucideIcon, RefreshCcw, Sparkles, TriangleAlert } from "lucide-react";
|
||||||
// plane editor
|
// plane editor
|
||||||
import { EditorRefApi } from "@plane/editor";
|
import { EditorRefApi } from "@plane/editor";
|
||||||
|
|
@ -18,7 +18,7 @@ import { AskPiMenu } from "./ask-pi-menu";
|
||||||
const aiService = new AIService();
|
const aiService = new AIService();
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: RefObject<EditorRefApi>;
|
editorRef: EditorRefApi | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
@ -73,7 +73,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
// handle task click
|
// handle task click
|
||||||
const handleClick = async (key: AI_EDITOR_TASKS) => {
|
const handleClick = async (key: AI_EDITOR_TASKS) => {
|
||||||
const selection = editorRef.current?.getSelectedText();
|
const selection = editorRef?.getSelectedText();
|
||||||
if (!selection || activeTask === key) return;
|
if (!selection || activeTask === key) return;
|
||||||
setActiveTask(key);
|
setActiveTask(key);
|
||||||
if (key === AI_EDITOR_TASKS.ASK_ANYTHING) return;
|
if (key === AI_EDITOR_TASKS.ASK_ANYTHING) return;
|
||||||
|
|
@ -86,7 +86,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
// handle re-generate response
|
// handle re-generate response
|
||||||
const handleRegenerate = async () => {
|
const handleRegenerate = async () => {
|
||||||
const selection = editorRef.current?.getSelectedText();
|
const selection = editorRef?.getSelectedText();
|
||||||
if (!selection || !activeTask) return;
|
if (!selection || !activeTask) return;
|
||||||
setIsRegenerating(true);
|
setIsRegenerating(true);
|
||||||
await handleGenerateResponse({
|
await handleGenerateResponse({
|
||||||
|
|
@ -104,7 +104,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||||
// handle re-generate response
|
// handle re-generate response
|
||||||
const handleToneChange = async (key: string) => {
|
const handleToneChange = async (key: string) => {
|
||||||
const selectedTone = TONES_LIST.find((t) => t.key === key);
|
const selectedTone = TONES_LIST.find((t) => t.key === key);
|
||||||
const selection = editorRef.current?.getSelectedText();
|
const selection = editorRef?.getSelectedText();
|
||||||
if (!selectedTone || !selection || !activeTask) return;
|
if (!selectedTone || !selection || !activeTask) return;
|
||||||
setResponse(undefined);
|
setResponse(undefined);
|
||||||
setIsRegenerating(false);
|
setIsRegenerating(false);
|
||||||
|
|
@ -123,7 +123,7 @@ export const EditorAIMenu: React.FC<Props> = (props) => {
|
||||||
// handle replace selected text with the response
|
// handle replace selected text with the response
|
||||||
const handleInsertText = (insertOnNextLine: boolean) => {
|
const handleInsertText = (insertOnNextLine: boolean) => {
|
||||||
if (!response) return;
|
if (!response) return;
|
||||||
editorRef.current?.insertText(response, insertOnNextLine);
|
editorRef?.insertText(response, insertOnNextLine);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
10
web/ce/components/pages/header/collaborators-list.tsx
Normal file
10
web/ce/components/pages/header/collaborators-list.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
export type TPageCollaboratorsListProps = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageCollaboratorsList = ({}: TPageCollaboratorsListProps) => null;
|
||||||
116
web/ce/components/pages/header/lock-control.tsx
Normal file
116
web/ce/components/pages/header/lock-control.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { LockKeyhole, LockKeyholeOpen } from "lucide-react";
|
||||||
|
// plane imports
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { usePageOperations } from "@/hooks/use-page-operations";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
// Define our lock display states, renaming "icon-only" to "neutral"
|
||||||
|
type LockDisplayState = "neutral" | "locked" | "unlocked";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageLockControl = observer(({ page }: Props) => {
|
||||||
|
// Initial state: if locked, then "locked", otherwise default to "neutral"
|
||||||
|
const [displayState, setDisplayState] = useState<LockDisplayState>(page.is_locked ? "locked" : "neutral");
|
||||||
|
// derived values
|
||||||
|
const { canCurrentUserLockPage, is_locked } = page;
|
||||||
|
// Ref for the transition timer
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
// Ref to store the previous value of isLocked for detecting transitions
|
||||||
|
const prevLockedRef = useRef(is_locked);
|
||||||
|
// page operations
|
||||||
|
const {
|
||||||
|
pageOperations: { toggleLock },
|
||||||
|
} = usePageOperations({
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup any running timer on unmount
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update display state when isLocked changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Clear any previous timer to avoid overlapping transitions
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transition logic:
|
||||||
|
// If locked, ensure the display state is "locked"
|
||||||
|
// If unlocked after being locked, show "unlocked" briefly then revert to "neutral"
|
||||||
|
if (is_locked) {
|
||||||
|
setDisplayState("locked");
|
||||||
|
} else if (prevLockedRef.current === true) {
|
||||||
|
setDisplayState("unlocked");
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setDisplayState("neutral");
|
||||||
|
timerRef.current = null;
|
||||||
|
}, 600);
|
||||||
|
} else {
|
||||||
|
setDisplayState("neutral");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the previous locked state
|
||||||
|
prevLockedRef.current = is_locked;
|
||||||
|
}, [is_locked]);
|
||||||
|
|
||||||
|
if (!canCurrentUserLockPage) return null;
|
||||||
|
|
||||||
|
// Render different UI based on the current display state
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{displayState === "neutral" && (
|
||||||
|
<Tooltip tooltipContent="Lock" position="bottom">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLock}
|
||||||
|
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors"
|
||||||
|
aria-label="Lock"
|
||||||
|
>
|
||||||
|
<LockKeyhole className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{displayState === "locked" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLock}
|
||||||
|
className="h-6 flex items-center gap-1 px-2 rounded text-custom-primary-100 bg-custom-primary-100/20 hover:bg-custom-primary-100/30 transition-colors"
|
||||||
|
aria-label="Locked"
|
||||||
|
>
|
||||||
|
<LockKeyhole className="flex-shrink-0 size-3.5 animate-lock-icon" />
|
||||||
|
<span className="text-xs font-medium whitespace-nowrap overflow-hidden transition-all duration-500 ease-out animate-text-slide-in">
|
||||||
|
Locked
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{displayState === "unlocked" && (
|
||||||
|
<div
|
||||||
|
className="h-6 flex items-center gap-1 px-2 rounded text-custom-text-200 animate-fade-out"
|
||||||
|
aria-label="Unlocked"
|
||||||
|
>
|
||||||
|
<LockKeyholeOpen className="flex-shrink-0 size-3.5 animate-unlock-icon" />
|
||||||
|
<span className="text-xs font-medium whitespace-nowrap overflow-hidden transition-all duration-500 ease-out animate-text-slide-in animate-text-fade-out">
|
||||||
|
Unlocked
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
10
web/ce/components/pages/header/move-control.tsx
Normal file
10
web/ce/components/pages/header/move-control.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
export type TPageMoveControlProps = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageMoveControl = ({}: TPageMoveControlProps) => null;
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
// helpers
|
|
||||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
|
||||||
// store types
|
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
page: TPageInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageEditInformationPopover: React.FC<Props> = observer((props) => {
|
|
||||||
const { page } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex-shrink-0 whitespace-nowrap">
|
|
||||||
<span className="text-sm text-custom-text-300">Edited {calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
export * from "./actions";
|
export * from "./actions";
|
||||||
export * from "./edit-information-popover";
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,10 @@ export type TEditorBodyHandlers = {
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
config: TEditorBodyConfig;
|
config: TEditorBodyConfig;
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
|
||||||
editorReady: boolean;
|
editorReady: boolean;
|
||||||
|
editorForwardRef: React.RefObject<EditorRefApi>;
|
||||||
handleConnectionStatus: Dispatch<SetStateAction<boolean>>;
|
handleConnectionStatus: Dispatch<SetStateAction<boolean>>;
|
||||||
handleEditorReady: Dispatch<SetStateAction<boolean>>;
|
handleEditorReady: (status: boolean) => void;
|
||||||
handlers: TEditorBodyHandlers;
|
handlers: TEditorBodyHandlers;
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||||
|
|
@ -56,7 +56,7 @@ type Props = {
|
||||||
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
editorRef,
|
editorForwardRef,
|
||||||
handleConnectionStatus,
|
handleConnectionStatus,
|
||||||
handleEditorReady,
|
handleEditorReady,
|
||||||
handlers,
|
handlers,
|
||||||
|
|
@ -70,7 +70,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||||
const { getUserDetails } = useMember();
|
const { getUserDetails } = useMember();
|
||||||
|
|
||||||
// derived values
|
// derived values
|
||||||
const { id: pageId, name: pageTitle, isContentEditable, updateTitle } = page;
|
const { id: pageId, name: pageTitle, isContentEditable, updateTitle, editorRef } = page;
|
||||||
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? "";
|
||||||
// issue-embed
|
// issue-embed
|
||||||
const { issueEmbedProps } = useIssueEmbed({
|
const { issueEmbedProps } = useIssueEmbed({
|
||||||
|
|
@ -172,10 +172,10 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||||
<div className="sticky top-[72px]">
|
<div className="sticky top-[72px]">
|
||||||
<div className="group/page-toc relative px-page-x">
|
<div className="group/page-toc relative px-page-x">
|
||||||
<div className="cursor-pointer max-h-[50vh] overflow-hidden">
|
<div className="cursor-pointer max-h-[50vh] overflow-hidden">
|
||||||
<PageContentBrowser editorRef={editorRef?.current} showOutline />
|
<PageContentBrowser editorRef={editorRef} showOutline />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-0 right-0 opacity-0 translate-x-1/2 pointer-events-none group-hover/page-toc:opacity-100 group-hover/page-toc:-translate-x-1/4 group-hover/page-toc:pointer-events-auto transition-all duration-300 w-52 max-h-[70vh] overflow-y-scroll vertical-scrollbar scrollbar-sm whitespace-nowrap bg-custom-background-90 p-4 rounded">
|
<div className="absolute top-0 right-0 opacity-0 translate-x-1/2 pointer-events-none group-hover/page-toc:opacity-100 group-hover/page-toc:-translate-x-1/4 group-hover/page-toc:pointer-events-auto transition-all duration-300 w-52 max-h-[70vh] overflow-y-scroll vertical-scrollbar scrollbar-sm whitespace-nowrap bg-custom-background-90 p-4 rounded">
|
||||||
<PageContentBrowser editorRef={editorRef?.current} />
|
<PageContentBrowser editorRef={editorRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -196,7 +196,7 @@ export const PageEditorBody: React.FC<Props> = observer((props) => {
|
||||||
id={pageId}
|
id={pageId}
|
||||||
fileHandler={config.fileHandler}
|
fileHandler={config.fileHandler}
|
||||||
handleEditorReady={handleEditorReady}
|
handleEditorReady={handleEditorReady}
|
||||||
ref={editorRef}
|
ref={editorForwardRef}
|
||||||
containerClassName="h-full p-0 pb-64"
|
containerClassName="h-full p-0 pb-64"
|
||||||
displayConfig={displayConfig}
|
displayConfig={displayConfig}
|
||||||
mentionHandler={{
|
mentionHandler={{
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const PageEditorHeaderLogoPicker: React.FC<Props> = observer((props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(className, "max-h-0 pointer-events-none transition-all ease-linear duration-200", {
|
className={cn(className, "max-h-0 pointer-events-none transition-all ease-linear duration-300", {
|
||||||
"max-h-[56px] pointer-events-auto": isLogoSelected,
|
"max-h-[56px] pointer-events-auto": isLogoSelected,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
// editor
|
// editor
|
||||||
|
|
@ -18,8 +18,6 @@ import {
|
||||||
import { useAppRouter } from "@/hooks/use-app-router";
|
import { useAppRouter } from "@/hooks/use-app-router";
|
||||||
import { usePageFallback } from "@/hooks/use-page-fallback";
|
import { usePageFallback } from "@/hooks/use-page-fallback";
|
||||||
import { useQueryParams } from "@/hooks/use-query-params";
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
// plane web hooks
|
|
||||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
|
||||||
// store
|
// store
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
|
@ -38,13 +36,12 @@ type TPageRootProps = {
|
||||||
config: TPageRootConfig;
|
config: TPageRootConfig;
|
||||||
handlers: TPageRootHandlers;
|
handlers: TPageRootHandlers;
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
storeType: EPageStoreType;
|
|
||||||
webhookConnectionParams: TWebhookConnectionQueryParams;
|
webhookConnectionParams: TWebhookConnectionQueryParams;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageRoot = observer((props: TPageRootProps) => {
|
export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
const { config, handlers, page, storeType, webhookConnectionParams, workspaceSlug } = props;
|
const { config, handlers, page, webhookConnectionParams, workspaceSlug } = props;
|
||||||
// states
|
// states
|
||||||
const [editorReady, setEditorReady] = useState(false);
|
const [editorReady, setEditorReady] = useState(false);
|
||||||
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
const [hasConnectionFailed, setHasConnectionFailed] = useState(false);
|
||||||
|
|
@ -56,7 +53,7 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
// search params
|
// search params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
// derived values
|
// derived values
|
||||||
const { isContentEditable } = page;
|
const { isContentEditable, setEditorRef } = page;
|
||||||
// page fallback
|
// page fallback
|
||||||
usePageFallback({
|
usePageFallback({
|
||||||
editorRef,
|
editorRef,
|
||||||
|
|
@ -67,6 +64,16 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
// update query params
|
// update query params
|
||||||
const { updateQueryParams } = useQueryParams();
|
const { updateQueryParams } = useQueryParams();
|
||||||
|
|
||||||
|
const handleEditorReady = useCallback(
|
||||||
|
(status: boolean) => {
|
||||||
|
setEditorReady(status);
|
||||||
|
if (editorRef.current && !page.editorRef) {
|
||||||
|
setEditorRef(editorRef.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[page.editorRef, setEditorRef]
|
||||||
|
);
|
||||||
|
|
||||||
const version = searchParams.get("version");
|
const version = searchParams.get("version");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
|
|
@ -89,6 +96,14 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
};
|
};
|
||||||
const currentVersionDescription = editorRef.current?.getDocument().html;
|
const currentVersionDescription = editorRef.current?.getDocument().html;
|
||||||
|
|
||||||
|
// reset editor ref on unmount
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
setEditorRef(null);
|
||||||
|
},
|
||||||
|
[setEditorRef]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageVersionsOverlay
|
<PageVersionsOverlay
|
||||||
|
|
@ -103,13 +118,13 @@ export const PageRoot = observer((props: TPageRootProps) => {
|
||||||
pageId={page.id ?? ""}
|
pageId={page.id ?? ""}
|
||||||
restoreEnabled={isContentEditable}
|
restoreEnabled={isContentEditable}
|
||||||
/>
|
/>
|
||||||
<PageEditorToolbarRoot editorReady={editorReady} editorRef={editorRef} page={page} storeType={storeType} />
|
<PageEditorToolbarRoot page={page} />
|
||||||
<PageEditorBody
|
<PageEditorBody
|
||||||
config={config}
|
config={config}
|
||||||
editorReady={editorReady}
|
editorReady={editorReady}
|
||||||
editorRef={editorRef}
|
editorForwardRef={editorRef}
|
||||||
handleConnectionStatus={setHasConnectionFailed}
|
handleConnectionStatus={setHasConnectionFailed}
|
||||||
handleEditorReady={setEditorReady}
|
handleEditorReady={handleEditorReady}
|
||||||
handlers={handlers}
|
handlers={handlers}
|
||||||
page={page}
|
page={page}
|
||||||
webhookConnectionParams={webhookConnectionParams}
|
webhookConnectionParams={webhookConnectionParams}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
// plane editor
|
// plane editor
|
||||||
import { EditorRefApi, IMarking } from "@plane/editor";
|
import { EditorRefApi, IMarking } from "@plane/editor";
|
||||||
// components
|
// components
|
||||||
|
|
@ -24,10 +24,13 @@ export const PageContentBrowser: React.FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
}, [editorRef]);
|
}, [editorRef]);
|
||||||
|
|
||||||
const handleOnClick = (marking: IMarking) => {
|
const handleOnClick = useCallback(
|
||||||
editorRef?.scrollSummary(marking);
|
(marking: IMarking) => {
|
||||||
if (setSidePeekVisible) setSidePeekVisible(false);
|
editorRef?.scrollSummary(marking);
|
||||||
};
|
setSidePeekVisible?.(false);
|
||||||
|
},
|
||||||
|
[editorRef, setSidePeekVisible]
|
||||||
|
);
|
||||||
|
|
||||||
const HeadingComponent: {
|
const HeadingComponent: {
|
||||||
[key: number]: React.FC<{ marking: IMarking; onClick: () => void }>;
|
[key: number]: React.FC<{ marking: IMarking; onClick: () => void }>;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { getPageName } from "@/helpers/page.helper";
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
editorRef: EditorRefApi | null;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
title: string | undefined;
|
title: string | undefined;
|
||||||
updateTitle: (title: string) => void;
|
updateTitle: (title: string) => void;
|
||||||
|
|
@ -53,7 +53,7 @@ export const PageEditorTitle: React.FC<Props> = observer((props) => {
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
editorRef.current?.setFocusAtPosition(0);
|
editorRef?.setFocusAtPosition(0);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value={title}
|
value={title}
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
// constants
|
|
||||||
import { IS_FAVORITE_MENU_OPEN } from "@plane/constants";
|
|
||||||
// editor
|
|
||||||
import { EditorRefApi } from "@plane/editor";
|
|
||||||
// plane hooks
|
|
||||||
import { useLocalStorage } from "@plane/hooks";
|
|
||||||
// ui
|
|
||||||
import { ArchiveIcon, FavoriteStar, setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { LockedComponent } from "@/components/icons/locked-component";
|
|
||||||
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
|
|
||||||
// helpers
|
|
||||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
|
||||||
// hooks
|
|
||||||
import useOnlineStatus from "@/hooks/use-online-status";
|
|
||||||
// plane web hooks
|
|
||||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
|
||||||
// store
|
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
editorRef: EditorRefApi;
|
|
||||||
page: TPageInstance;
|
|
||||||
storeType: EPageStoreType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageExtraOptions: React.FC<Props> = observer((props) => {
|
|
||||||
const { editorRef, page, storeType } = props;
|
|
||||||
// derived values
|
|
||||||
const {
|
|
||||||
archived_at,
|
|
||||||
isContentEditable,
|
|
||||||
is_favorite,
|
|
||||||
is_locked,
|
|
||||||
canCurrentUserFavoritePage,
|
|
||||||
addToFavorites,
|
|
||||||
removePageFromFavorites,
|
|
||||||
} = page;
|
|
||||||
// use online status
|
|
||||||
const { isOnline } = useOnlineStatus();
|
|
||||||
// local storage
|
|
||||||
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
|
||||||
IS_FAVORITE_MENU_OPEN,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
// favorite handler
|
|
||||||
const handleFavorite = () => {
|
|
||||||
if (is_favorite) {
|
|
||||||
removePageFromFavorites().then(() =>
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.SUCCESS,
|
|
||||||
title: "Success!",
|
|
||||||
message: "Page removed from favorites.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
addToFavorites().then(() => {
|
|
||||||
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
|
||||||
setToast({
|
|
||||||
type: TOAST_TYPE.SUCCESS,
|
|
||||||
title: "Success!",
|
|
||||||
message: "Page added to favorites.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{is_locked && <LockedComponent />}
|
|
||||||
{archived_at && (
|
|
||||||
<div className="flex-shrink-0 flex h-7 items-center gap-2 rounded-full bg-blue-500/20 px-3 py-0.5 text-xs font-medium text-blue-500">
|
|
||||||
<ArchiveIcon className="flex-shrink-0 size-3" />
|
|
||||||
<span>Archived at {renderFormattedDate(archived_at)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isContentEditable && !isOnline && (
|
|
||||||
<Tooltip
|
|
||||||
tooltipHeading="You are offline."
|
|
||||||
tooltipContent="You can continue making changes. They will be synced when you are back online."
|
|
||||||
>
|
|
||||||
<div className="flex-shrink-0 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">
|
|
||||||
<span className="flex-shrink-0 size-1.5 rounded-full bg-custom-text-300" />
|
|
||||||
<span>Offline</span>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{canCurrentUserFavoritePage && (
|
|
||||||
<FavoriteStar
|
|
||||||
selected={is_favorite}
|
|
||||||
onClick={handleFavorite}
|
|
||||||
buttonClassName="flex-shrink-0"
|
|
||||||
iconClassName="text-custom-text-100"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<PageInfoPopover editorRef={editorRef} page={page} />
|
|
||||||
<PageOptionsDropdown editorRef={editorRef} page={page} storeType={storeType} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
export * from "./color-dropdown";
|
export * from "./color-dropdown";
|
||||||
export * from "./extra-options";
|
|
||||||
export * from "./info-popover";
|
export * from "./info-popover";
|
||||||
export * from "./options-dropdown";
|
export * from "./options-dropdown";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./mobile-root";
|
|
||||||
export * from "./toolbar";
|
export * from "./toolbar";
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,25 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { usePopper } from "react-popper";
|
import { usePopper } from "react-popper";
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
// plane editor
|
// plane imports
|
||||||
import { EditorRefApi } from "@plane/editor";
|
|
||||||
// plane ui
|
|
||||||
import { Avatar } from "@plane/ui";
|
import { Avatar } from "@plane/ui";
|
||||||
// plane utils
|
|
||||||
import { getFileURL, renderFormattedDate } from "@plane/utils";
|
import { getFileURL, renderFormattedDate } from "@plane/utils";
|
||||||
// helpers
|
// helpers
|
||||||
import { getReadTimeFromWordsCount } from "@/helpers/date-time.helper";
|
import { calculateTimeAgoShort, getReadTimeFromWordsCount } from "@/helpers/date-time.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMember } from "@/hooks/store";
|
import { useMember } from "@/hooks/store";
|
||||||
// store types
|
// store types
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: EditorRefApi | null;
|
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageInfoPopover: React.FC<Props> = (props) => {
|
export const PageInfoPopover: React.FC<Props> = observer((props) => {
|
||||||
const { editorRef, page } = props;
|
const { page } = props;
|
||||||
// states
|
// states
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||||
// refs
|
// refs
|
||||||
|
|
@ -40,7 +37,7 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||||
const editorInformation = page.updated_by ? getUserDetails(page.updated_by) : undefined;
|
const editorInformation = page.updated_by ? getUserDetails(page.updated_by) : undefined;
|
||||||
const creatorInformation = page.created_by ? getUserDetails(page.created_by) : undefined;
|
const creatorInformation = page.created_by ? getUserDetails(page.created_by) : undefined;
|
||||||
|
|
||||||
const documentsInfo = editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 };
|
const documentsInfo = page.editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 };
|
||||||
|
|
||||||
const secondsToReadableTime = () => {
|
const secondsToReadableTime = () => {
|
||||||
const wordsCount = documentsInfo.words;
|
const wordsCount = documentsInfo.words;
|
||||||
|
|
@ -72,8 +69,16 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
|
<div
|
||||||
<button type="button" ref={setReferenceElement} className="block">
|
className="flex-shrink-0"
|
||||||
|
onMouseEnter={() => setIsPopoverOpen(true)}
|
||||||
|
onMouseLeave={() => setIsPopoverOpen(false)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
ref={setReferenceElement}
|
||||||
|
className="size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors"
|
||||||
|
>
|
||||||
<Info className="size-3.5" />
|
<Info className="size-3.5" />
|
||||||
</button>
|
</button>
|
||||||
{isPopoverOpen && (
|
{isPopoverOpen && (
|
||||||
|
|
@ -106,7 +111,7 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{editorInformation?.display_name}{" "}
|
{editorInformation?.display_name}{" "}
|
||||||
<span className="text-custom-text-300">{renderFormattedDate(page.updated_at)}</span>
|
<span className="text-custom-text-300">{calculateTimeAgoShort(page.updated_at ?? "")} ago</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,4 +138,4 @@ export const PageInfoPopover: React.FC<Props> = (props) => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { observer } from "mobx-react";
|
|
||||||
// plane imports
|
|
||||||
import { EditorRefApi } from "@plane/editor";
|
|
||||||
import { Header, EHeaderVariant } from "@plane/ui";
|
|
||||||
// components
|
|
||||||
import { PageExtraOptions, PageToolbar } from "@/components/pages";
|
|
||||||
// hooks
|
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
|
||||||
// plane web hooks
|
|
||||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
|
||||||
// store
|
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
editorRef: EditorRefApi;
|
|
||||||
page: TPageInstance;
|
|
||||||
storeType: EPageStoreType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageEditorMobileHeaderRoot: React.FC<Props> = observer((props) => {
|
|
||||||
const { editorRef, page, storeType } = props;
|
|
||||||
// derived values
|
|
||||||
const { isContentEditable } = page;
|
|
||||||
// page filters
|
|
||||||
const { isStickyToolbarEnabled } = usePageFilters();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header variant={EHeaderVariant.SECONDARY}>
|
|
||||||
<PageExtraOptions editorRef={editorRef} page={page} storeType={storeType} />
|
|
||||||
</Header>
|
|
||||||
<Header variant={EHeaderVariant.TERNARY}>
|
|
||||||
{isContentEditable && editorRef && <PageToolbar editorRef={editorRef} isHidden={!isStickyToolbarEnabled} />}
|
|
||||||
</Header>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
@ -4,9 +4,7 @@ import { useMemo, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ArrowUpToLine, Clipboard, History } from "lucide-react";
|
import { ArrowUpToLine, Clipboard, History } from "lucide-react";
|
||||||
// document editor
|
// plane imports
|
||||||
import { EditorRefApi } from "@plane/editor";
|
|
||||||
// ui
|
|
||||||
import { TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
import { TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ExportPageModal, PageActions, TPageActions } from "@/components/pages";
|
import { ExportPageModal, PageActions, TPageActions } from "@/components/pages";
|
||||||
|
|
@ -21,19 +19,18 @@ import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: EditorRefApi | null;
|
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
storeType: EPageStoreType;
|
storeType: EPageStoreType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||||
const { editorRef, page, storeType } = props;
|
const { page, storeType } = props;
|
||||||
// states
|
// states
|
||||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// store values
|
// store values
|
||||||
const { name, isContentEditable } = page;
|
const { name, isContentEditable, editorRef } = page;
|
||||||
// page filters
|
// page filters
|
||||||
const { isFullWidth, handleFullWidth, isStickyToolbarEnabled, handleStickyToolbar } = usePageFilters();
|
const { isFullWidth, handleFullWidth, isStickyToolbarEnabled, handleStickyToolbar } = usePageFilters();
|
||||||
// update query params
|
// update query params
|
||||||
|
|
@ -127,10 +124,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
|
||||||
optionsOrder={[
|
optionsOrder={[
|
||||||
"full-screen",
|
"full-screen",
|
||||||
"sticky-toolbar",
|
"sticky-toolbar",
|
||||||
"copy-link",
|
|
||||||
"make-a-copy",
|
"make-a-copy",
|
||||||
"move",
|
|
||||||
"toggle-lock",
|
|
||||||
"toggle-access",
|
"toggle-access",
|
||||||
"archive-restore",
|
"archive-restore",
|
||||||
"delete",
|
"delete",
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,48 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { EditorRefApi } from "@plane/editor";
|
|
||||||
// components
|
// components
|
||||||
import { PageEditorMobileHeaderRoot, PageExtraOptions, PageToolbar } from "@/components/pages";
|
import { PageToolbar } from "@/components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePageFilters } from "@/hooks/use-page-filters";
|
import { usePageFilters } from "@/hooks/use-page-filters";
|
||||||
// plane web hooks
|
// plane web components
|
||||||
import { EPageStoreType } from "@/plane-web/hooks/store";
|
import { PageCollaboratorsList } from "@/plane-web/components/pages/header/collaborators-list";
|
||||||
// store
|
// store
|
||||||
import { TPageInstance } from "@/store/pages/base-page";
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorReady: boolean;
|
|
||||||
editorRef: React.RefObject<EditorRefApi>;
|
|
||||||
page: TPageInstance;
|
page: TPageInstance;
|
||||||
storeType: EPageStoreType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageEditorToolbarRoot: React.FC<Props> = observer((props) => {
|
export const PageEditorToolbarRoot: React.FC<Props> = observer((props) => {
|
||||||
const { editorReady, editorRef, page, storeType } = props;
|
const { page } = props;
|
||||||
// derived values
|
// derived values
|
||||||
const { isContentEditable } = page;
|
const { isContentEditable, editorRef } = page;
|
||||||
// page filters
|
// page filters
|
||||||
const { isFullWidth, isStickyToolbarEnabled } = usePageFilters();
|
const { isFullWidth, isStickyToolbarEnabled } = usePageFilters();
|
||||||
// derived values
|
// derived values
|
||||||
const resolvedEditorRef = editorRef.current;
|
|
||||||
const shouldHideToolbar = !isStickyToolbarEnabled || !isContentEditable;
|
const shouldHideToolbar = !isStickyToolbarEnabled || !isContentEditable;
|
||||||
|
|
||||||
if (!resolvedEditorRef) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="page-toolbar-container">
|
<div
|
||||||
|
id="page-toolbar-container"
|
||||||
|
className={cn("max-h-[52px] transition-all ease-linear duration-300 overflow-auto", {
|
||||||
|
"max-h-0 overflow-hidden": shouldHideToolbar,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden md:flex items-center relative min-h-[52px] page-toolbar-content px-page-x border-b border-transparent transition-all duration-200 ease-in-out",
|
"hidden md:flex items-center relative min-h-[52px] page-toolbar-content px-page-x transition-all duration-200 ease-in-out",
|
||||||
{
|
{
|
||||||
"wide-layout": isFullWidth,
|
"wide-layout": isFullWidth,
|
||||||
"border-custom-border-200": true,
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="max-w-full w-full flex items-center justify-between">
|
<div className="max-w-full w-full flex items-center justify-between">
|
||||||
<div>
|
{editorRef && <PageToolbar editorRef={editorRef} />}
|
||||||
{editorReady && resolvedEditorRef && (
|
<PageCollaboratorsList page={page} />
|
||||||
<PageToolbar editorRef={resolvedEditorRef} isHidden={shouldHideToolbar} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<PageExtraOptions editorRef={resolvedEditorRef} page={page} storeType={storeType} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden">
|
|
||||||
<PageEditorMobileHeaderRoot editorRef={resolvedEditorRef} page={page} storeType={storeType} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import { cn } from "@/helpers/common.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editorRef: EditorRefApi;
|
editorRef: EditorRefApi;
|
||||||
isHidden: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolbarButtonProps = {
|
type ToolbarButtonProps = {
|
||||||
|
|
@ -65,7 +64,7 @@ ToolbarButton.displayName = "ToolbarButton";
|
||||||
const toolbarItems = TOOLBAR_ITEMS.document;
|
const toolbarItems = TOOLBAR_ITEMS.document;
|
||||||
|
|
||||||
export const PageToolbar: React.FC<Props> = (props) => {
|
export const PageToolbar: React.FC<Props> = (props) => {
|
||||||
const { editorRef, isHidden } = props;
|
const { editorRef } = props;
|
||||||
// states
|
// states
|
||||||
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
|
@ -98,14 +97,7 @@ export const PageToolbar: React.FC<Props> = (props) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex items-center divide-x divide-custom-border-200 overflow-x-scroll">
|
||||||
className={cn(
|
|
||||||
"flex flex-wrap items-center divide-x divide-custom-border-200 opacity-100 transition-opacity duration-200 ease-in-out",
|
|
||||||
{
|
|
||||||
"opacity-0 pointer-events-none": isHidden,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButton={
|
customButton={
|
||||||
<span className="text-custom-text-300 text-sm border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 h-7 w-24 rounded px-2 flex items-center justify-between gap-2 whitespace-nowrap text-left">
|
<span className="text-custom-text-300 text-sm border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 h-7 w-24 rounded px-2 flex items-center justify-between gap-2 whitespace-nowrap text-left">
|
||||||
|
|
@ -139,20 +131,22 @@ export const PageToolbar: React.FC<Props> = (props) => {
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
))}
|
))}
|
||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
<ColorDropdown
|
<div className="flex-shrink-0">
|
||||||
handleColorSelect={(key, color) =>
|
<ColorDropdown
|
||||||
editorRef.executeMenuItemCommand({
|
handleColorSelect={(key, color) =>
|
||||||
itemKey: key,
|
editorRef.executeMenuItemCommand({
|
||||||
color,
|
itemKey: key,
|
||||||
})
|
color,
|
||||||
}
|
})
|
||||||
isColorActive={(key, color) =>
|
}
|
||||||
editorRef.isMenuItemActive({
|
isColorActive={(key, color) =>
|
||||||
itemKey: key,
|
editorRef.isMenuItemActive({
|
||||||
color,
|
itemKey: key,
|
||||||
})
|
color,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{Object.keys(toolbarItems).map((key) => (
|
{Object.keys(toolbarItems).map((key) => (
|
||||||
<div key={key} className="flex items-center gap-0.5 px-2 first:pl-0 last:pr-0">
|
<div key={key} className="flex items-center gap-0.5 px-2 first:pl-0 last:pr-0">
|
||||||
{toolbarItems[key].map((item) => (
|
{toolbarItems[key].map((item) => (
|
||||||
|
|
|
||||||
37
web/core/components/pages/header/actions.tsx
Normal file
37
web/core/components/pages/header/actions.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// components
|
||||||
|
import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages";
|
||||||
|
// plane web components
|
||||||
|
import { PageLockControl } from "@/plane-web/components/pages/header/lock-control";
|
||||||
|
import { PageMoveControl } from "@/plane-web/components/pages/header/move-control";
|
||||||
|
// plane web hooks
|
||||||
|
import { EPageStoreType } from "@/plane-web/hooks/store";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
// local imports
|
||||||
|
import { PageArchivedBadge } from "./archived-badge";
|
||||||
|
import { PageCopyLinkControl } from "./copy-link-control";
|
||||||
|
import { PageOfflineBadge } from "./offline-badge";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
storeType: EPageStoreType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageHeaderActions: React.FC<Props> = observer((props) => {
|
||||||
|
const { page, storeType } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<PageArchivedBadge page={page} />
|
||||||
|
<PageOfflineBadge page={page} />
|
||||||
|
<PageLockControl page={page} />
|
||||||
|
<PageMoveControl page={page} />
|
||||||
|
<PageInfoPopover page={page} />
|
||||||
|
<PageCopyLinkControl page={page} />
|
||||||
|
<PageOptionsDropdown page={page} storeType={storeType} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
21
web/core/components/pages/header/archived-badge.tsx
Normal file
21
web/core/components/pages/header/archived-badge.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { ArchiveIcon } from "@plane/ui";
|
||||||
|
import { renderFormattedDate } from "@plane/utils";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageArchivedBadge = observer(({ page }: Props) => {
|
||||||
|
if (!page.archived_at) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 h-6 flex items-center gap-1 px-2 rounded text-custom-primary-100 bg-custom-primary-100/20">
|
||||||
|
<ArchiveIcon className="flex-shrink-0 size-3.5" />
|
||||||
|
<span className="text-xs font-medium">Archived at {renderFormattedDate(page.archived_at)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
27
web/core/components/pages/header/copy-link-control.tsx
Normal file
27
web/core/components/pages/header/copy-link-control.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { Link } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { usePageOperations } from "@/hooks/use-page-operations";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageCopyLinkControl = observer(({ page }: Props) => {
|
||||||
|
// page operations
|
||||||
|
const { pageOperations } = usePageOperations({
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={pageOperations.copyLink}
|
||||||
|
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-200 hover:text-custom-text-100 hover:bg-custom-background-80 transition-colors"
|
||||||
|
>
|
||||||
|
<Link className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
31
web/core/components/pages/header/favorite-control.tsx
Normal file
31
web/core/components/pages/header/favorite-control.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { FavoriteStar } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { usePageOperations } from "@/hooks/use-page-operations";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageFavoriteControl = observer(({ page }: Props) => {
|
||||||
|
// derived values
|
||||||
|
const { is_favorite, canCurrentUserFavoritePage } = page;
|
||||||
|
// page operations
|
||||||
|
const { pageOperations } = usePageOperations({
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!canCurrentUserFavoritePage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FavoriteStar
|
||||||
|
selected={is_favorite}
|
||||||
|
onClick={pageOperations.toggleFavorite}
|
||||||
|
buttonClassName="flex-shrink-0 size-6 group rounded hover:bg-custom-background-80 transition-colors"
|
||||||
|
iconClassName="size-3.5 text-custom-text-200 group-hover:text-custom-text-10"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
30
web/core/components/pages/header/offline-badge.tsx
Normal file
30
web/core/components/pages/header/offline-badge.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
// plane imports
|
||||||
|
import { Tooltip } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import useOnlineStatus from "@/hooks/use-online-status";
|
||||||
|
// store
|
||||||
|
import { TPageInstance } from "@/store/pages/base-page";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
page: TPageInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageOfflineBadge = observer(({ page }: Props) => {
|
||||||
|
// use online status
|
||||||
|
const { isOnline } = useOnlineStatus();
|
||||||
|
|
||||||
|
if (!page.isContentEditable || isOnline) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
tooltipHeading="You are offline."
|
||||||
|
tooltipContent="You can continue making changes. They will be synced when you are back online."
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 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">
|
||||||
|
<span className="flex-shrink-0 size-1.5 rounded-full bg-custom-text-300" />
|
||||||
|
<span>Offline</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -47,7 +47,7 @@ export const usePageOperations = (
|
||||||
// collaborative actions
|
// collaborative actions
|
||||||
const { executeCollaborativeAction } = useCollaborativePageActions(props);
|
const { executeCollaborativeAction } = useCollaborativePageActions(props);
|
||||||
// local storage
|
// local storage
|
||||||
const { setValue: toggleFavoriteMenu, storedValue: isfavoriteMenuOpen } = useLocalStorage<boolean>(
|
const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage<boolean>(
|
||||||
IS_FAVORITE_MENU_OPEN,
|
IS_FAVORITE_MENU_OPEN,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
@ -147,7 +147,7 @@ export const usePageOperations = (
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
addToFavorites().then(() => {
|
addToFavorites().then(() => {
|
||||||
if (!isfavoriteMenuOpen) toggleFavoriteMenu(true);
|
if (!isFavoriteMenuOpen) toggleFavoriteMenu(true);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
|
|
@ -199,7 +199,9 @@ export const usePageOperations = (
|
||||||
getRedirectionLink,
|
getRedirectionLink,
|
||||||
is_favorite,
|
is_favorite,
|
||||||
is_locked,
|
is_locked,
|
||||||
|
isFavoriteMenuOpen,
|
||||||
removePageFromFavorites,
|
removePageFromFavorites,
|
||||||
|
toggleFavoriteMenu,
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
pageOperations,
|
pageOperations,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import set from "lodash/set";
|
||||||
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { EPageAccess } from "@plane/constants";
|
import { EPageAccess } from "@plane/constants";
|
||||||
|
import { EditorRefApi } from "@plane/editor";
|
||||||
import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
|
import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types";
|
||||||
import { TChangeHandlerProps } from "@plane/ui";
|
import { TChangeHandlerProps } from "@plane/ui";
|
||||||
import { convertHexEmojiToDecimal } from "@plane/utils";
|
import { convertHexEmojiToDecimal } from "@plane/utils";
|
||||||
|
|
@ -11,6 +12,7 @@ import { RootStore } from "@/plane-web/store/root.store";
|
||||||
export type TBasePage = TPage & {
|
export type TBasePage = TPage & {
|
||||||
// observables
|
// observables
|
||||||
isSubmitting: TNameDescriptionLoader;
|
isSubmitting: TNameDescriptionLoader;
|
||||||
|
editorRef: EditorRefApi | null;
|
||||||
// computed
|
// computed
|
||||||
asJSON: TPage | undefined;
|
asJSON: TPage | undefined;
|
||||||
isCurrentUserOwner: boolean;
|
isCurrentUserOwner: boolean;
|
||||||
|
|
@ -32,6 +34,8 @@ export type TBasePage = TPage & {
|
||||||
addToFavorites: () => Promise<void>;
|
addToFavorites: () => Promise<void>;
|
||||||
removePageFromFavorites: () => Promise<void>;
|
removePageFromFavorites: () => Promise<void>;
|
||||||
duplicate: () => Promise<TPage | undefined>;
|
duplicate: () => Promise<TPage | undefined>;
|
||||||
|
mutateProperties: (data: Partial<TPage>, shouldUpdateName?: boolean) => void;
|
||||||
|
setEditorRef: (editorRef: EditorRefApi | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TBasePagePermissions = {
|
export type TBasePagePermissions = {
|
||||||
|
|
@ -68,6 +72,7 @@ export type TPageInstance = TBasePage &
|
||||||
export class BasePage implements TBasePage {
|
export class BasePage implements TBasePage {
|
||||||
// loaders
|
// loaders
|
||||||
isSubmitting: TNameDescriptionLoader = "saved";
|
isSubmitting: TNameDescriptionLoader = "saved";
|
||||||
|
editorRef: EditorRefApi | null = null;
|
||||||
// page properties
|
// page properties
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
name: string | undefined;
|
name: string | undefined;
|
||||||
|
|
@ -125,6 +130,7 @@ export class BasePage implements TBasePage {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
// loaders
|
// loaders
|
||||||
isSubmitting: observable.ref,
|
isSubmitting: observable.ref,
|
||||||
|
editorRef: observable.ref,
|
||||||
// page properties
|
// page properties
|
||||||
id: observable.ref,
|
id: observable.ref,
|
||||||
name: observable.ref,
|
name: observable.ref,
|
||||||
|
|
@ -165,6 +171,8 @@ export class BasePage implements TBasePage {
|
||||||
addToFavorites: action,
|
addToFavorites: action,
|
||||||
removePageFromFavorites: action,
|
removePageFromFavorites: action,
|
||||||
duplicate: action,
|
duplicate: action,
|
||||||
|
mutateProperties: action,
|
||||||
|
setEditorRef: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = store;
|
this.rootStore = store;
|
||||||
|
|
@ -426,25 +434,34 @@ export class BasePage implements TBasePage {
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePageLogo = async (value: TChangeHandlerProps) => {
|
updatePageLogo = async (value: TChangeHandlerProps) => {
|
||||||
let logoValue = {};
|
const originalLogoProps = { ...this.logo_props };
|
||||||
if (value?.type === "emoji")
|
try {
|
||||||
logoValue = {
|
let logoValue = {};
|
||||||
value: convertHexEmojiToDecimal(value.value.unified),
|
if (value?.type === "emoji")
|
||||||
url: value.value.imageUrl,
|
logoValue = {
|
||||||
|
value: convertHexEmojiToDecimal(value.value.unified),
|
||||||
|
url: value.value.imageUrl,
|
||||||
|
};
|
||||||
|
else if (value?.type === "icon") logoValue = value.value;
|
||||||
|
|
||||||
|
const logoProps: TLogoProps = {
|
||||||
|
in_use: value?.type,
|
||||||
|
[value?.type]: logoValue,
|
||||||
};
|
};
|
||||||
else if (value?.type === "icon") logoValue = value.value;
|
|
||||||
|
|
||||||
const logoProps: TLogoProps = {
|
runInAction(() => {
|
||||||
in_use: value?.type,
|
this.logo_props = logoProps;
|
||||||
[value?.type]: logoValue,
|
});
|
||||||
};
|
await this.services.update({
|
||||||
|
logo_props: logoProps,
|
||||||
await this.services.update({
|
});
|
||||||
logo_props: logoProps,
|
} catch (error) {
|
||||||
});
|
console.error("Error in updating page logo", error);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.logo_props = logoProps;
|
this.logo_props = originalLogoProps as TLogoProps;
|
||||||
});
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -498,4 +515,23 @@ export class BasePage implements TBasePage {
|
||||||
* @description duplicate the page
|
* @description duplicate the page
|
||||||
*/
|
*/
|
||||||
duplicate = async () => await this.services.duplicate();
|
duplicate = async () => await this.services.duplicate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description mutate multiple properties at once
|
||||||
|
* @param data Partial<TPage>
|
||||||
|
*/
|
||||||
|
mutateProperties = (data: Partial<TPage>, shouldUpdateName: boolean = true) => {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
const value = data[key as keyof TPage];
|
||||||
|
if (key === "name" && !shouldUpdateName) return;
|
||||||
|
set(this, key, value);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
setEditorRef = (editorRef: EditorRefApi | null) => {
|
||||||
|
console.log("store editorRef", editorRef);
|
||||||
|
runInAction(() => {
|
||||||
|
this.editorRef = editorRef;
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,19 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||||
|
|
||||||
const pages = await this.service.fetchAll(workspaceSlug, projectId);
|
const pages = await this.service.fetchAll(workspaceSlug, projectId);
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
for (const page of pages) if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
|
for (const page of pages) {
|
||||||
|
if (page?.id) {
|
||||||
|
const existingPage = this.getPageById(page.id);
|
||||||
|
if (existingPage) {
|
||||||
|
// If page already exists, update all fields except name
|
||||||
|
const { name, ...otherFields } = page;
|
||||||
|
existingPage.mutateProperties(otherFields, false);
|
||||||
|
} else {
|
||||||
|
// If new page, create a new instance with all data
|
||||||
|
set(this.data, [page.id], new ProjectPage(this.store, page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.loader = undefined;
|
this.loader = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -238,8 +250,16 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||||
});
|
});
|
||||||
|
|
||||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
||||||
|
const pageInstance = page?.id ? this.getPageById(page.id) : undefined;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
if (page?.id) set(this.data, [page.id], new ProjectPage(this.store, page));
|
if (page?.id) {
|
||||||
|
if (pageInstance) {
|
||||||
|
pageInstance.mutateProperties(page, false);
|
||||||
|
} else {
|
||||||
|
set(this.data, [page.id], new ProjectPage(this.store, page));
|
||||||
|
}
|
||||||
|
}
|
||||||
this.loader = undefined;
|
this.loader = undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -854,3 +854,91 @@ div.web-view-spinner div.bar12 {
|
||||||
.epr-search-container > .epr-icn-search {
|
.epr-search-container > .epr-icn-search {
|
||||||
color: rgb(var(--color-text-400)) !important;
|
color: rgb(var(--color-text-400)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Lock icon animations */
|
||||||
|
@keyframes textSlideIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-8px);
|
||||||
|
max-width: 0px;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 0.7;
|
||||||
|
max-width: 60px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
max-width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textFadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lockIconAnimation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(-5deg) scale(1);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(0deg) scale(1.15);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: rotate(5deg) scale(1.08);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes unlockIconAnimation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: rotate(-8deg) scale(1.15);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: rotate(3deg) scale(1.05);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-text-slide-in {
|
||||||
|
animation: textSlideIn 400ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-text-fade-out {
|
||||||
|
animation: textFadeOut 600ms ease-in 300ms forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-lock-icon {
|
||||||
|
animation: lockIconAnimation 600ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-unlock-icon {
|
||||||
|
animation: unlockIconAnimation 600ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-out {
|
||||||
|
animation: fadeOut 500ms ease-in 100ms forwards;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue