From a6fd905644ff4dd998f576dd76b18bccf3ab684f Mon Sep 17 00:00:00 2001 From: Palanikannan M Date: Sat, 22 Mar 2025 19:36:34 +0530 Subject: [PATCH] chore: revamp page actions --- .../[projectId]/pages/(detail)/header.tsx | 227 +++++++++++++++++- .../components/icons/locked-component.tsx | 6 +- .../pages/editor/header/extra-options.tsx | 11 - web/styles/animations.css | 87 +++++++ web/styles/globals.css | 1 + 5 files changed, 313 insertions(+), 19 deletions(-) create mode 100644 web/styles/animations.css diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx index f8292f4426..869dafaaa1 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(detail)/header.tsx @@ -1,27 +1,41 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { FileText } from "lucide-react"; +import { FileText, Link, LockKeyhole, LockKeyholeOpen } from "lucide-react"; +// constants +import { IS_FAVORITE_MENU_OPEN } from "@plane/constants"; +// hooks +import { useLocalStorage } from "@plane/hooks"; // types import { TLogoProps } from "@plane/types"; // ui -import { Breadcrumbs, EmojiIconPicker, EmojiIconPickerTypes, TOAST_TYPE, Tooltip, setToast, Header } from "@plane/ui"; +import { + Breadcrumbs, + EmojiIconPicker, + EmojiIconPickerTypes, + FavoriteStar, + Header, + TOAST_TYPE, + Tooltip, + setToast, +} from "@plane/ui"; // components import { BreadcrumbLink, Logo } from "@/components/common"; -import { PageEditInformationPopover } from "@/components/pages"; // helpers import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper"; import { getPageName } from "@/helpers/page.helper"; // hooks import { useProject } from "@/hooks/store"; +import { usePageOperations } from "@/hooks/use-page-operations"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs"; -import { PageDetailsHeaderExtraActions } from "@/plane-web/components/pages"; // plane web hooks import { EPageStoreType, usePage } from "@/plane-web/hooks/store"; +// store +import { TPageInstance } from "@/store/pages/base-page"; export interface IPagesHeaderProps { showButton?: boolean; @@ -159,9 +173,208 @@ export const PageDetailsHeader = observer(() => { - - +
+ {/* */} + +
); }); + +// PageActions Component +export type TPageActions = "toggle-lock" | "copy-link"; + +// Lock states +type LockDisplayState = "icon-only" | "locked" | "unlocked"; + +// Enhanced lock component with multiple states +const EnhancedLockControl = ({ + isLocked, + canToggle, + onToggle, +}: { + isLocked: boolean; + canToggle: boolean; + onToggle: () => void; +}) => { + // Track visual display state + const [displayState, setDisplayState] = useState(isLocked ? "locked" : "icon-only"); + // Use ref to track timeout + const timerRef = useRef(null); + + // Clear any existing timers on unmount + useEffect( + () => () => { + if (timerRef.current) clearTimeout(timerRef.current); + }, + [] + ); + + // Watch only for isLocked changes and update display state accordingly + useEffect(() => { + // Clear any existing timers + if (timerRef.current) { + clearTimeout(timerRef.current); + timerRef.current = null; + } + + if (isLocked) { + // If locked, always show locked state + setDisplayState("locked"); + } else { + // If not locked and was previously showing something other than icon-only + // First show unlocked state, then transition back + if (displayState !== "icon-only") { + setDisplayState("unlocked"); + timerRef.current = setTimeout(() => { + setDisplayState("icon-only"); + timerRef.current = null; + }, 600); // Adjusted from 800ms to 600ms for better timing + } else { + // Otherwise, just show icon-only + setDisplayState("icon-only"); + } + } + }, [isLocked, displayState]); // Only depend on isLocked, not displayState + + if (!canToggle) return null; + // Handle click with state transitions + const handleClick = () => { + onToggle(); // This will update isLocked which will trigger the useEffect + }; + + // Determine what to render based on current display state + const renderLockControl = () => { + switch (displayState) { + case "icon-only": + return ( + +
+ +
+
+ ); + + case "locked": + return ( +
+ + + Locked + +
+ ); + + case "unlocked": + return ( +
+ + + Unlocked + +
+ ); + } + }; + + return renderLockControl(); +}; + +export const PageActionButtons = observer(({ page, storeType }: { page: TPageInstance; storeType: EPageStoreType }) => { + // page operations + const { pageOperations } = usePageOperations({ + page, + }); + + // derived values + const { + is_locked, + is_favorite, + canCurrentUserLockPage, + canCurrentUserFavoritePage, + addToFavorites, + removePageFromFavorites, + } = page; + + // local storage for favorites menu + const { setValue: toggleFavoriteMenu, storedValue: isFavoriteMenuOpen } = useLocalStorage( + IS_FAVORITE_MENU_OPEN, + false + ); + + // favorite handler + const handleFavorite = useCallback(() => { + 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.", + }); + }); + } + }, [is_favorite, addToFavorites, removePageFromFavorites, isFavoriteMenuOpen, toggleFavoriteMenu]); + + return ( +
+
+ + + +
+ +
+
+
+ {canCurrentUserFavoritePage && ( + + )} +
+ ); +}); diff --git a/web/core/components/icons/locked-component.tsx b/web/core/components/icons/locked-component.tsx index 36230a093c..3f6d6527d1 100644 --- a/web/core/components/icons/locked-component.tsx +++ b/web/core/components/icons/locked-component.tsx @@ -12,7 +12,11 @@ export const LockedComponent = (props: { toolTipContent?: string }) => { return ( <> - {toolTipContent ? {lockedComponent} : <>{lockedComponent}} + {toolTipContent === "Lock" ? ( + {lockedComponent} + ) : ( + <>{lockedComponent} + )} ); }; diff --git a/web/core/components/pages/editor/header/extra-options.tsx b/web/core/components/pages/editor/header/extra-options.tsx index 0b81d14522..9c80f4b0ba 100644 --- a/web/core/components/pages/editor/header/extra-options.tsx +++ b/web/core/components/pages/editor/header/extra-options.tsx @@ -70,7 +70,6 @@ export const PageExtraOptions: React.FC = observer((props) => { return (
- {is_locked && } {archived_at && (
@@ -88,16 +87,6 @@ export const PageExtraOptions: React.FC = observer((props) => {
)} - {canCurrentUserFavoritePage && ( - - )} - -
); }); diff --git a/web/styles/animations.css b/web/styles/animations.css new file mode 100644 index 0000000000..6eb4c68257 --- /dev/null +++ b/web/styles/animations.css @@ -0,0 +1,87 @@ +/* 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; +} diff --git a/web/styles/globals.css b/web/styles/globals.css index 7d44ad0d1f..32fdaa0b8b 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -1,4 +1,5 @@ @import url("fonts/main.css"); +@import url("animations.css"); @tailwind base; @tailwind components;