From 079c3a3a990cda682f1f4b63f43dc3ed3192aca2 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 12 May 2025 19:15:39 +0530 Subject: [PATCH] [WEB-3978] chore: cmd k search result redirection improvements (#7012) * fix: work item tab highlight * chore: projectListOpen state and toggle method added to command palette store * chore: openProjectAndScrollToSidebar helper function and highlight keyframes added * chore: SidebarProjectsListItem updated * chore: openProjectAndScrollToSidebar implementation * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor * chore: code refactor --- .../command-palette/actions/helper.tsx | 25 +++++++++++++++++++ .../actions/search-results.tsx | 13 ++++++++-- .../workspace/sidebar/projects-list-item.tsx | 9 ++++--- web/core/store/base-command-palette.store.ts | 19 ++++++++++++++ web/styles/globals.css | 11 ++++++++ 5 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 web/core/components/command-palette/actions/helper.tsx diff --git a/web/core/components/command-palette/actions/helper.tsx b/web/core/components/command-palette/actions/helper.tsx new file mode 100644 index 000000000..5156cadf7 --- /dev/null +++ b/web/core/components/command-palette/actions/helper.tsx @@ -0,0 +1,25 @@ +import { store } from "@/lib/store-context"; + +export const openProjectAndScrollToSidebar = (itemProjectId: string | undefined) => { + if (!itemProjectId) { + console.warn("No project id provided. Cannot open project and scroll to sidebar."); + return; + } + // open the project list + store.commandPalette.toggleProjectListOpen(itemProjectId, true); + // scroll to the element + const scrollElementId = `sidebar-${itemProjectId}-JOINED`; + const scrollElement = document.getElementById(scrollElementId); + // if the element exists, scroll to it + if (scrollElement) { + setTimeout(() => { + scrollElement.scrollIntoView({ behavior: "smooth", block: "start" }); + // Restart the highlight animation every time + scrollElement.style.animation = "none"; + // Trigger a reflow to ensure the animation is restarted + void scrollElement.offsetWidth; + // Restart the highlight animation + scrollElement.style.animation = "highlight 2s ease-in-out"; + }); + } +}; diff --git a/web/core/components/command-palette/actions/search-results.tsx b/web/core/components/command-palette/actions/search-results.tsx index 18a8a75be..a33d85ff7 100644 --- a/web/core/components/command-palette/actions/search-results.tsx +++ b/web/core/components/command-palette/actions/search-results.tsx @@ -1,6 +1,7 @@ "use client"; import { Command } from "cmdk"; +import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports import { IWorkspaceSearchResults } from "@plane/types"; @@ -8,13 +9,15 @@ import { IWorkspaceSearchResults } from "@plane/types"; import { useAppRouter } from "@/hooks/use-app-router"; // plane web imports import { commandGroups } from "@/plane-web/components/command-palette"; +// helpers +import { openProjectAndScrollToSidebar } from "./helper"; type Props = { closePalette: () => void; results: IWorkspaceSearchResults; }; -export const CommandPaletteSearchResults: React.FC = (props) => { +export const CommandPaletteSearchResults: React.FC = observer((props) => { const { closePalette, results } = props; // router const router = useAppRouter(); @@ -38,6 +41,12 @@ export const CommandPaletteSearchResults: React.FC = (props) => { onSelect={() => { closePalette(); router.push(currentSection.path(item, projectId)); + const itemProjectId = + item?.project_id || + (Array.isArray(item?.project_ids) && item?.project_ids?.length > 0 + ? item?.project_ids[0] + : undefined); + if (itemProjectId) openProjectAndScrollToSidebar(itemProjectId); }} value={`${key}-${item?.id}-${item.name}-${item.project__identifier ?? ""}-${item.sequence_id ?? ""}`} className="focus:outline-none" @@ -54,4 +63,4 @@ export const CommandPaletteSearchResults: React.FC = (props) => { })} ); -}; +}); diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index 978fe0ba0..75b10aa4c 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -24,7 +24,7 @@ import { LeaveProjectModal, PublishProjectModal } from "@/components/project"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; +import { useAppTheme, useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane-web components import { ProjectNavigationRoot } from "@/plane-web/components/sidebar"; @@ -64,12 +64,13 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { getPartialProjectById } = useProject(); const { isMobile } = usePlatformOS(); const { allowPermissions } = useUserPermissions(); + const { getIsProjectListOpen, toggleProjectListOpen } = useCommandPalette(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); const [publishModalOpen, setPublishModal] = useState(false); const [isMenuActive, setIsMenuActive] = useState(false); const [isDragging, setIsDragging] = useState(false); - const [isProjectListOpen, setIsProjectListOpen] = useState(false); + const isProjectListOpen = getIsProjectListOpen(projectId); const [instruction, setInstruction] = useState<"DRAG_OVER" | "DRAG_BELOW" | undefined>(undefined); // refs const actionSectionRef = useRef(null); @@ -79,6 +80,8 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { const { workspaceSlug, projectId: URLProjectId } = useParams(); // derived values const project = getPartialProjectById(projectId); + // toggle project list open + const setIsProjectListOpen = (value: boolean) => toggleProjectListOpen(projectId, value); // auth const isAdmin = allowPermissions( [EUserPermissions.ADMIN], @@ -198,7 +201,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => { if (URLProjectId === project.id) setIsProjectListOpen(true); }, [URLProjectId]); - const handleItemClick = () => setIsProjectListOpen((prev) => !prev); + const handleItemClick = () => setIsProjectListOpen(!isProjectListOpen); return ( <> setPublishModal(false)} /> diff --git a/web/core/store/base-command-palette.store.ts b/web/core/store/base-command-palette.store.ts index aaf1170c0..7024daf4d 100644 --- a/web/core/store/base-command-palette.store.ts +++ b/web/core/store/base-command-palette.store.ts @@ -1,4 +1,5 @@ import { observable, action, makeObservable } from "mobx"; +import { computedFn } from "mobx-utils"; import { EIssuesStoreType, TCreateModalStoreTypes, @@ -26,6 +27,8 @@ export interface IBaseCommandPaletteStore { isBulkDeleteIssueModalOpen: boolean; createIssueStoreType: TCreateModalStoreTypes; allStickiesModal: boolean; + projectListOpenMap: Record; + getIsProjectListOpen: (projectId: string) => boolean; // toggle actions toggleCommandPaletteModal: (value?: boolean) => void; toggleShortcutModal: (value?: boolean) => void; @@ -38,6 +41,7 @@ export interface IBaseCommandPaletteStore { toggleDeleteIssueModal: (value?: boolean) => void; toggleBulkDeleteIssueModal: (value?: boolean) => void; toggleAllStickiesModal: (value?: boolean) => void; + toggleProjectListOpen: (projectId: string, value?: boolean) => void; } export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStore { @@ -54,6 +58,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: TCreatePageModal = DEFAULT_CREATE_PAGE_MODAL_DATA; createIssueStoreType: TCreateModalStoreTypes = EIssuesStoreType.PROJECT; allStickiesModal: boolean = false; + projectListOpenMap: Record = {}; constructor() { makeObservable(this, { @@ -70,6 +75,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor createPageModal: observable, createIssueStoreType: observable, allStickiesModal: observable, + projectListOpenMap: observable, // projectPages: computed, // toggle actions toggleCommandPaletteModal: action, @@ -83,6 +89,7 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor toggleDeleteIssueModal: action, toggleBulkDeleteIssueModal: action, toggleAllStickiesModal: action, + toggleProjectListOpen: action, }); } @@ -104,6 +111,18 @@ export abstract class BaseCommandPaletteStore implements IBaseCommandPaletteStor this.allStickiesModal ); } + // computedFn + getIsProjectListOpen = computedFn((projectId: string) => this.projectListOpenMap[projectId]); + + /** + * Toggles the project list open state + * @param projectId + * @param value + */ + toggleProjectListOpen = (projectId: string, value?: boolean) => { + if (value !== undefined) this.projectListOpenMap[projectId] = value; + else this.projectListOpenMap[projectId] = !this.projectListOpenMap[projectId]; + }; /** * Toggles the command palette modal diff --git a/web/styles/globals.css b/web/styles/globals.css index ff71ba5ac..c6e4654d0 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -942,3 +942,14 @@ div.web-view-spinner div.bar12 { .animate-fade-out { animation: fadeOut 500ms ease-in 100ms forwards; } + +@keyframes highlight { + 0% { + background-color: rgba(var(--color-background-90), 1); + border-radius: 4px; + } + 100% { + background-color: transparent; + border-radius: 4px; + } +}