/** * Copyright (c) 2023-present Plane Software, Inc. and contributors * SPDX-License-Identifier: AGPL-3.0-only * See the LICENSE file for details. */ import { useCallback, useMemo, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { CloseOutline, DragDropOutline } from "@makeplane/propel/icons"; // plane imports import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { Checkbox } from "@makeplane/propel/components/checkbox"; import { EModalPosition, EModalWidth, ModalCore, Sortable } from "@plane/ui"; import { cn } from "@plane/utils"; // hooks import { useUserPermissions } from "@/hooks/store/user"; import { usePersonalNavigationPreferences, useProjectNavigationPreferences, useWorkspaceNavigationPreferences, } from "@/hooks/use-navigation-preferences"; // helpers import { getSidebarNavigationItemIcon } from "@/components/workspace/sidebar/helper"; // types import type { TPersonalNavigationItemKey } from "@plane/types"; type TCustomizeNavigationDialogProps = { isOpen: boolean; onClose: () => void; }; type TWorkspaceNavigationItem = { key: string; labelTranslationKey: string; isPinned: boolean; sortOrder: number; }; const PERSONAL_ITEMS: Array<{ key: TPersonalNavigationItemKey; labelTranslationKey: string }> = [ { key: "stickies", labelTranslationKey: "sidebar.stickies" }, { key: "your_work", labelTranslationKey: "sidebar.your_work" }, { key: "drafts", labelTranslationKey: "drafts" }, ]; export const CustomizeNavigationDialog = observer(function CustomizeNavigationDialog( props: TCustomizeNavigationDialogProps ) { const { isOpen, onClose } = props; const { t } = useTranslation(); // router const { workspaceSlug } = useParams(); // store hooks const { allowPermissions } = useUserPermissions(); const { preferences: personalPreferences, togglePersonalItem, updatePersonalItemOrder, } = usePersonalNavigationPreferences(); const { preferences: projectPreferences, updateNavigationMode, updateShowLimitedProjects, updateLimitedProjectsCount, } = useProjectNavigationPreferences(); const { preferences: workspacePreferences, toggleWorkspaceItem, updateWorkspaceItemOrder, } = useWorkspaceNavigationPreferences(); // local state for limited projects count input const [projectCountInput, setProjectCountInput] = useState(projectPreferences.limitedProjectsCount.toString()); // Filter personal items by feature flags const filteredPersonalItems = PERSONAL_ITEMS; // Filter workspace items by permissions and feature flags, then get pinned/unpinned items const workspaceItems = useMemo(() => { const items = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.filter((item) => { // Permission check const hasPermission = allowPermissions( item.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug?.toString() || "" ); return hasPermission; }).map((item) => { // Get pinned status and sort order from localStorage const preference = workspacePreferences.items[item.key]; const isPinned = preference?.is_pinned ?? false; const sortOrder = preference?.sort_order ?? 0; return { key: item.key, labelTranslationKey: item.labelTranslationKey, isPinned, sortOrder, }; }); // oxlint-disable-next-line unicorn/no-array-sort return items.sort((a, b) => a.sortOrder - b.sortOrder); }, [workspaceSlug, allowPermissions, workspacePreferences]); // Handle checkbox toggle const handleWorkspaceItemToggle = useCallback( (itemKey: string, checked: boolean) => { toggleWorkspaceItem(itemKey, checked); }, [toggleWorkspaceItem] ); // Handle reorder of pinned workspace items const handleReorder = useCallback( (newData: TWorkspaceNavigationItem[]) => { const itemsWithOrder = newData.map((item, index) => ({ key: item.key, sortOrder: index, })); updateWorkspaceItemOrder(itemsWithOrder); }, [updateWorkspaceItemOrder] ); // Handle reorder of enabled personal items const handlePersonalReorder = useCallback( (newData: Array<{ key: TPersonalNavigationItemKey; labelTranslationKey: string }>) => { const itemsWithOrder = newData.map((item, index) => ({ key: item.key, sortOrder: index, })); updatePersonalItemOrder(itemsWithOrder); }, [updatePersonalItemOrder] ); // Separate personal items into enabled/disabled const personalItems = useMemo(() => { const items = filteredPersonalItems.map((item) => { const itemState = personalPreferences.items[item.key]; const isEnabled = typeof itemState === "boolean" ? itemState : (itemState?.enabled ?? true); const sortOrder = typeof itemState === "boolean" ? 0 : (itemState?.sort_order ?? 0); return { ...item, isEnabled, sortOrder, }; }); // oxlint-disable-next-line unicorn/no-array-sort return items.sort((a, b) => a.sortOrder - b.sortOrder); }, [personalPreferences, filteredPersonalItems]); // Prevent typing invalid characters in number input // oxlint-disable-next-line unicorn/consistent-function-scoping const handleKeyDown = (e: React.KeyboardEvent) => { // Block: e, E, +, -, . if (["e", "E", "+", "-", "."].includes(e.key)) { e.preventDefault(); } }; // Handle project count input change const handleProjectCountChange = (value: string) => { // Strip any non-digit characters const cleanedValue = value.replace(/\D/g, ""); setProjectCountInput(cleanedValue); // Parse and validate the value const numValue = parseInt(cleanedValue, 10); // If valid number, enforce minimum of 1 if (!isNaN(numValue)) { const validValue = Math.max(1, numValue); updateLimitedProjectsCount(validValue); } }; return (
{/* Header */}

{t("customize_navigation")}

Selected items will always stay visible in your sidebar. You can still find the others anytime from the More menu. These changes are personal to you and won't affect anyone else on your workspace.

{/* Content */}
{/* Personal Section */}

{t("personal")}

item.key} id="personal-enabled-items" render={(item) => (
togglePersonalItem(item.key, checked)} aria-label={t(item.labelTranslationKey)} />
{getSidebarNavigationItemIcon(item.key)}
)} />
{/* Workspace Section */}

{t("common.workspace")}

{/* Pinned Items - Draggable */} item.key} id="workspace-pinned-items" render={(item) => { const icon = getSidebarNavigationItemIcon(item.key); return (
handleWorkspaceItemToggle(item.key, checked)} aria-label={t(item.labelTranslationKey)} />
{icon} {t(item.labelTranslationKey)}
); }} />
{/* Projects Section */}

{t("projects")}

{/* Navigation Mode Radio Buttons */}
{/* oxlint-disable-next-line jsx_a11y/label-has-associated-control */} {/* oxlint-disable-next-line jsx_a11y/label-has-associated-control */}
{/* Limited Projects Checkbox */}
{projectPreferences.showLimitedProjects && (
handleProjectCountChange(e.target.value)} className={cn( "w-full rounded-md px-2 py-1 text-13", "border bg-surface-2", "text-secondary", parseInt(projectCountInput) >= 1 ? "border-strong focus:border-accent-strong focus:ring-1 focus:ring-accent-strong" : "border-danger-strong focus:border-danger-strong focus:ring-1 focus:ring-danger-strong" )} />
{parseInt(projectCountInput) < 1 && projectCountInput !== "" && ( Minimum value is 1 )}
)}
); });