chore: revamp page actions

This commit is contained in:
Palanikannan M
2025-03-22 19:36:34 +05:30
parent 72307ec100
commit a6fd905644
5 changed files with 313 additions and 19 deletions

View File

@@ -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(() => {
</div>
</Header.LeftItem>
<Header.RightItem>
<PageEditInformationPopover page={page} />
<PageDetailsHeaderExtraActions page={page} />
<div className="flex items-center gap-3">
{/* <PageEditInformationPopover page={page} /> */}
<PageActionButtons page={page} storeType={EPageStoreType.PROJECT} />
</div>
</Header.RightItem>
</Header>
);
});
// 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<LockDisplayState>(isLocked ? "locked" : "icon-only");
// Use ref to track timeout
const timerRef = useRef<NodeJS.Timeout | null>(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 (
<Tooltip tooltipContent="Lock" position="bottom">
<div
onClick={handleClick}
className="w-6 h-6 flex items-center justify-center rounded-full cursor-pointer
transition-all duration-700 text-custom-text-400
hover:bg-custom-background-90 hover:text-custom-text-100"
aria-label="Lock"
>
<LockKeyhole className="h-3.5 w-3.5 transition-all hover:scale-110" />
</div>
</Tooltip>
);
case "locked":
return (
<div
onClick={handleClick}
className="h-6 flex items-center gap-1 px-2 py-0.5 rounded-md cursor-pointer
text-blue-500 transition-all duration-500 hover:bg-blue-50/10"
aria-label="Locked"
>
<LockKeyhole className="h-3.5 w-3.5 flex-shrink-0 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>
</div>
);
case "unlocked":
return (
<div
className="h-6 flex items-center gap-1 px-2 py-0.5 rounded-md cursor-pointer
text-custom-text-800 transition-all duration-500 animate-fade-out"
aria-label="Unlocked"
>
<LockKeyholeOpen className="h-3.5 w-3.5 flex-shrink-0 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>
);
}
};
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<boolean>(
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 (
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<EnhancedLockControl
isLocked={is_locked}
canToggle={canCurrentUserLockPage}
onToggle={pageOperations.toggleLock}
/>
<Tooltip tooltipContent="Copy link" position="bottom">
<div
onClick={pageOperations.copyLink}
className="w-7 h-7 flex items-center justify-center rounded-full text-custom-text-400
hover:bg-custom-background-90 hover:text-custom-text-100 transition-all
duration-200 cursor-pointer"
aria-label="Copy link"
>
<Link className="h-3.5 w-3.5" />
</div>
</Tooltip>
</div>
{canCurrentUserFavoritePage && (
<FavoriteStar
selected={is_favorite}
onClick={handleFavorite}
buttonClassName="flex-shrink-0"
iconClassName="text-custom-text-400"
/>
)}
</div>
);
});

View File

@@ -12,7 +12,11 @@ export const LockedComponent = (props: { toolTipContent?: string }) => {
return (
<>
{toolTipContent ? <Tooltip tooltipContent={toolTipContent}>{lockedComponent}</Tooltip> : <>{lockedComponent}</>}
{toolTipContent === "Lock" ? (
<Tooltip tooltipContent={toolTipContent}>{lockedComponent}</Tooltip>
) : (
<>{lockedComponent}</>
)}
</>
);
};

View File

@@ -70,7 +70,6 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
return (
<div className="flex items-center justify-end 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" />
@@ -88,16 +87,6 @@ export const PageExtraOptions: React.FC<Props> = observer((props) => {
</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>
);
});

87
web/styles/animations.css Normal file
View File

@@ -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;
}

View File

@@ -1,4 +1,5 @@
@import url("fonts/main.css");
@import url("animations.css");
@tailwind base;
@tailwind components;