import { DndContext, DragEndEvent, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core" import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { DotsSix, Eye, EyeSlash } from "@medusajs/icons" import { IconButton, Kbd, Text, clx } from "@medusajs/ui" import { Collapsible as RadixCollapsible } from "radix-ui" import { PropsWithChildren, ReactNode, useCallback, useEffect, useState, } from "react" import { useTranslation } from "react-i18next" import { NavLink, To, useLocation } from "react-router-dom" import { useLayoutEdit } from "../../../hooks/use-layout-edit" import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks" import { ConditionalTooltip } from "../../common/conditional-tooltip" type ItemType = "core" | "extension" | "setting" type NestedItemProps = { label: string to: string translationNs?: string } export type INavItem = { icon?: ReactNode label: string to: string items?: NestedItemProps[] type?: ItemType from?: string nested?: string translationNs?: string } const BASE_NAV_LINK_CLASSES = "text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-0.5 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus" const ACTIVE_NAV_LINK_CLASSES = "bg-ui-bg-base shadow-elevation-card-rest text-ui-fg-base hover:bg-ui-bg-base" const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 py-1 w-full text-ui-fg-muted" const SETTING_NAV_LINK_CLASSES = "pl-2 py-1" // Prefix match that respects path-segment boundaries, so that a route like // "/inventory-sources" is not considered a match for "/inventory". This keeps // nested-route highlighting (e.g. "/inventory/123") while avoiding false // positives for sibling routes that merely share a string prefix. const isPathActive = (pathname: string, to: string) => { return pathname === to || pathname.startsWith(to + "/") } const getIsOpen = ( to: string, items: NestedItemProps[] | undefined, pathname: string ) => { return [to, ...(items?.map((i) => i.to) ?? [])].some((p) => isPathActive(pathname, p) ) } const NavItemTooltip = ({ to, children, }: PropsWithChildren<{ to: string }>) => { const { t } = useTranslation() const globalShortcuts = useGlobalShortcuts() const shortcut = globalShortcuts.find((s) => s.to === to) return ( {shortcut?.label}
{shortcut?.keys.Mac?.map((key, index) => (
{key} {index < (shortcut.keys.Mac?.length || 0) - 1 && ( {t("app.keyboardShortcuts.then")} )}
))}
} side="right" delayDuration={1500} >
{children}
) } type SubItemLinkProps = { item: NestedItemProps isSetting: boolean navLinkClassNames: (props: { to: string isActive: boolean isNested?: boolean isSetting?: boolean }) => string getLinkTarget: (target: string) => To } const SubItemLink = ({ item, isSetting, navLinkClassNames, getLinkTarget, }: SubItemLinkProps) => { const { t } = useTranslation(item.translationNs as any) const itemLabel: string = item.translationNs ? t(item.label) : item.label return ( { return clx( navLinkClassNames({ to: item.to, isActive, isSetting, isNested: true, }) ) }} > {itemLabel} ) } const NavItemSubItem = (props: SubItemLinkProps) => { return (
  • ) } const SortableSubItem = ({ id, hidden, onToggleHidden, ...props }: SubItemLinkProps & { id: string hidden: boolean onToggleHidden: () => void }) => { const { t } = useTranslation() const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id }) const style = { transform: CSS.Translate.toString(transform), transition, } return (
  • {hidden ? : }
  • ) } /** * Renders a route's nested children, applying the saved order. In layout edit * mode they become a self-contained vertical sortable (its own `DndContext`, so * it doesn't interfere with the composer's entry-level drag), scoped to this * route — child ids embed the parent path, so moves never cross routes. */ const NavSubItems = ({ parentTo, items, isSetting, navLinkClassNames, getLinkTarget, }: { parentTo: string items: NestedItemProps[] isSetting: boolean navLinkClassNames: SubItemLinkProps["navLinkClassNames"] getLinkTarget: SubItemLinkProps["getLinkTarget"] }) => { const { editMode, orderChildren, setChildrenOrder, isHidden, toggleHidden } = useLayoutEdit() const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }) ) const childId = useCallback( (to: string) => `nav-child:${parentTo}:${to}`, [parentTo] ) const ordered = orderChildren(items, (it) => childId(it.to)) const shared = { isSetting, navLinkClassNames, getLinkTarget } if (!editMode) { return ( <> {ordered .filter((item) => !isHidden(childId(item.to))) .map((item) => ( ))} ) } const ids = ordered.map((it) => childId(it.to)) const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) { return } const oldIndex = ids.indexOf(active.id as string) const newIndex = ids.indexOf(over.id as string) if (oldIndex < 0 || newIndex < 0) { return } setChildrenOrder(arrayMove(ids, oldIndex, newIndex)) } return ( {ordered.map((item) => ( ) } export const NavItem = ({ icon, label, to, items, type = "core", from, translationNs, }: INavItem) => { const { t } = useTranslation(translationNs as any) const { pathname, search } = useLocation() const [open, setOpen] = useState(getIsOpen(to, items, pathname)) // While the layout is being edited, force the nested list open so every // route's children are visible and reorderable, not just the active route's. const { editMode } = useLayoutEdit() // Use translation if translationNs is provided, otherwise use label as-is const displayLabel: string = translationNs ? t(label) : label useEffect(() => { setOpen(getIsOpen(to, items, pathname)) }, [pathname, to, items]) const navLinkClassNames = useCallback( ({ to, isActive, isNested = false, isSetting = false, }: { to: string isActive: boolean isNested?: boolean isSetting?: boolean }) => { if (["core", "setting"].includes(type)) { isActive = isPathActive(pathname, to) } return clx(BASE_NAV_LINK_CLASSES, { [NESTED_NAV_LINK_CLASSES]: isNested, [ACTIVE_NAV_LINK_CLASSES]: isActive && !editMode, [SETTING_NAV_LINK_CLASSES]: isSetting, }) }, [type, pathname, editMode] ) const getLinkTarget = useCallback( (target: string) => { if (pathname === target && search) { return { pathname: target, search } } return target }, [pathname, search] ) const isSetting = type === "setting" return (
    i.to === pathname)} state={ from ? { from, } : undefined } className={({ isActive }) => { return clx(navLinkClassNames({ isActive, isSetting, to }), { "max-lg:hidden": !!items?.length, }) }} > {type !== "setting" && (
    )} {displayLabel}
    {items && items.length > 0 && (
    {displayLabel}
    • { return clx( navLinkClassNames({ to, isActive, isSetting, isNested: true, }) ) }} > {displayLabel}
    )}
    ) } const Icon = ({ icon, type }: { icon?: ReactNode; type: ItemType }) => { if (!icon) { return null } return type === "extension" ? (
    {icon}
    ) : ( icon ) }