Repository: PostHog/posthog
Pull request #30074: fix(devex): Improvements to panel layout/project tree
Author: login: adamleithp
Created: 2025-03-18T12:39:49Z | Merged: 2025-03-19T09:31:07Z

## Problem
Going through some issues mentioned in the team regarding the panel layout

## Changes
* FIX(temporary): Commented out persons panel (for now)
* FIX: Removed activity from rendered nav bar items below in list
* UX: Added active identifier state to logic to keep track of which panel is open (also used to show which button triggered it and show active state)
* FIX: Open/ close panel when clicking it's trigger fixed
* FIX: When panel is pinned, it will stay open regardless of nav item choice.
* UX: Added `mainContentRef` and `set` into logic, so... on new nav bar rendered items keyboard ENTER will focus `<main>` correctly.
* UX: added `sideIcon` prop to `LemonTreeItem` so to render something on the right of the node
* UX: nav bar rendered items correctly show ALPHA, BETA etc using new `sideIcon` prop, 
* UX: nav bar rendered item correctly show active state when the URL (active scene/breadcrumbs) match
* FIX: In `LemonTree` correctly calls `onFolderClick` on key input of `ARROW RIGHT`, which fixes `ProjectTree`'s handling of loading a folder
* FIX: When panel is pinned, and hidden, there was huge gap. GAP NO MORE.
* FIX: URL matching works great in nav bar and in project tree.

General stuff:
![2025-03-18 12 35 48](https://github.com/user-attachments/assets/aff3b4d6-3485-491d-9675-b0d6ad2a48ae)

Active scene matching + project tree href matching to URL
![2025-03-18 19 31 48](https://github.com/user-attachments/assets/a0824c7f-66df-4250-b8f6-9efc5c43edf1)


## Does this work well for both Cloud and self-hosted?
Should

## How did you test this code?
Locally


Diff:
diff --git a/frontend/__snapshots__/lemon-ui-lemon-tree--default--dark.png b/frontend/__snapshots__/lemon-ui-lemon-tree--default--dark.png
index 5e4a8ea4def0..d7cb0c8fc863 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-tree--default--dark.png and b/frontend/__snapshots__/lemon-ui-lemon-tree--default--dark.png differ
diff --git a/frontend/__snapshots__/lemon-ui-lemon-tree--default--light.png b/frontend/__snapshots__/lemon-ui-lemon-tree--default--light.png
index f88cc677b282..37f4e2630be5 100644
Binary files a/frontend/__snapshots__/lemon-ui-lemon-tree--default--light.png and b/frontend/__snapshots__/lemon-ui-lemon-tree--default--light.png differ
diff --git a/frontend/src/layout/panel-layout/PanelLayout.tsx b/frontend/src/layout/panel-layout/PanelLayout.tsx
index 71a7905ff775..9f2f4c524cf7 100644
--- a/frontend/src/layout/panel-layout/PanelLayout.tsx
+++ b/frontend/src/layout/panel-layout/PanelLayout.tsx
@@ -1,11 +1,11 @@
 import { cva } from 'class-variance-authority'
 import { useActions, useValues } from 'kea'
 import { cn } from 'lib/utils/css-classes'
+import { useEffect } from 'react'
 
 import { navigation3000Logic } from '../navigation-3000/navigationLogic'
 import { panelLayoutLogic } from './panelLayoutLogic'
 import { PanelLayoutNavBar } from './PanelLayoutNavBar'
-import { PersonsTree } from './PersonsTree/PersonsTree'
 import { ProjectTree } from './ProjectTree/ProjectTree'
 
 const panelLayoutStyles = cva('gap-0 w-fit relative h-screen z-[var(--z-project-panel-layout)]', {
@@ -61,14 +61,20 @@ export function PanelLayout({ mainRef }: { mainRef: React.RefObject<HTMLElement>
         isLayoutPanelVisible,
         isLayoutNavbarVisibleForMobile,
         isLayoutNavbarVisibleForDesktop,
-        activeLayoutNavBarItem,
+        activePanelIdentifier,
     } = useValues(panelLayoutLogic)
     const { mobileLayout: isMobileLayout } = useValues(navigation3000Logic)
-    const { showLayoutPanel, showLayoutNavBar, clearActiveLayoutNavBarItem } = useActions(panelLayoutLogic)
-
+    const { showLayoutPanel, showLayoutNavBar, clearActivePanelIdentifier, setMainContentRef } =
+        useActions(panelLayoutLogic)
     const showMobileNavbarOverlay = isLayoutNavbarVisibleForMobile
     const showDesktopNavbarOverlay = isLayoutNavbarVisibleForDesktop && !isLayoutPanelPinned && isLayoutPanelVisible
 
+    useEffect(() => {
+        if (mainRef.current) {
+            setMainContentRef(mainRef)
+        }
+    }, [mainRef, setMainContentRef])
+
     return (
         <div className="relative">
             <div
@@ -84,17 +90,18 @@ export function PanelLayout({ mainRef }: { mainRef: React.RefObject<HTMLElement>
                 )}
             >
                 <PanelLayoutNavBar>
-                    {activeLayoutNavBarItem === 'project' && <ProjectTree mainRef={mainRef} />}
-                    {activeLayoutNavBarItem === 'persons' && <PersonsTree mainRef={mainRef} />}
+                    {activePanelIdentifier === 'project' && <ProjectTree />}
+                    {/* {activePanelIdentifier === 'persons' && <PersonsTree />} */}
                 </PanelLayoutNavBar>
             </div>
 
             {isMobileLayout && showMobileNavbarOverlay && (
                 <div
                     onClick={() => {
+                        // Pinned or not, hide the navbar and panel
                         showLayoutNavBar(false)
                         showLayoutPanel(false)
-                        clearActiveLayoutNavBarItem()
+                        clearActivePanelIdentifier()
                     }}
                     className="z-[var(--z-project-panel-overlay)] fixed inset-0 w-screen h-screen"
                 />
@@ -102,8 +109,10 @@ export function PanelLayout({ mainRef }: { mainRef: React.RefObject<HTMLElement>
             {!isMobileLayout && showDesktopNavbarOverlay && (
                 <div
                     onClick={() => {
-                        showLayoutPanel(false)
-                        clearActiveLayoutNavBarItem()
+                        if (!isLayoutPanelPinned) {
+                            showLayoutPanel(false)
+                            clearActivePanelIdentifier()
+                        }
                     }}
                     className="z-[var(--z-project-panel-overlay)] fixed inset-0 w-screen h-screen"
                 />
diff --git a/frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx b/frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
index 8fa061644819..459a753bc03a 100644
--- a/frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
+++ b/frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
@@ -1,10 +1,8 @@
 import {
-    IconBolt,
     IconChevronRight,
     IconClock,
     IconFolderOpen,
     IconGear,
-    IconPeople,
     IconPlusSmall,
     IconSearch,
     IconToolbar,
@@ -12,9 +10,11 @@ import {
 import { cva } from 'class-variance-authority'
 import clsx from 'clsx'
 import { useActions, useValues } from 'kea'
+import { router } from 'kea-router'
 import { commandBarLogic } from 'lib/components/CommandBar/commandBarLogic'
 import { ScrollableShadows } from 'lib/components/ScrollableShadows/ScrollableShadows'
 import { LemonButton } from 'lib/lemon-ui/LemonButton'
+import { LemonTag } from 'lib/lemon-ui/LemonTag'
 import { Popover } from 'lib/lemon-ui/Popover'
 import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
 import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
@@ -22,14 +22,17 @@ import { IconWrapper } from 'lib/ui/IconWrapper/IconWrapper'
 import { cn } from 'lib/utils/css-classes'
 import { useRef } from 'react'
 import { sceneLogic } from 'scenes/sceneLogic'
+import { Scene } from 'scenes/sceneTypes'
 import { urls } from 'scenes/urls'
 import { userLogic } from 'scenes/userLogic'
 
-import { panelLayoutLogic, PanelLayoutNavItem } from '~/layout/panel-layout/panelLayoutLogic'
+import { panelLayoutLogic, PanelLayoutNavIdentifier } from '~/layout/panel-layout/panelLayoutLogic'
 
+import { breadcrumbsLogic } from '../navigation/Breadcrumbs/breadcrumbsLogic'
 import { navigationLogic } from '../navigation/navigationLogic'
 import { AccountPopoverOverlay } from '../navigation/TopBar/AccountPopover'
 import { navigation3000Logic } from '../navigation-3000/navigationLogic'
+import { NavbarItem } from '../navigation-3000/types'
 import { OrganizationDropdownMenu } from './OrganizationDropdownMenu'
 
 const panelStyles = cva('z-[var(--z-project-panel-layout)] h-screen left-0', {
@@ -47,19 +50,43 @@ const panelStyles = cva('z-[var(--z-project-panel-layout)] h-screen left-0', {
 export function PanelLayoutNavBar({ children }: { children: React.ReactNode }): JSX.Element {
     const { toggleSearchBar } = useActions(commandBarLogic)
     const containerRef = useRef<HTMLDivElement | null>(null)
-    const { showLayoutPanel, setActiveLayoutNavBarItem, clearActiveLayoutNavBarItem } = useActions(panelLayoutLogic)
-    const { isLayoutPanelVisible, activeLayoutNavBarItem } = useValues(panelLayoutLogic)
+    const { showLayoutPanel, setActivePanelIdentifier, clearActivePanelIdentifier } = useActions(panelLayoutLogic)
+    const { isLayoutPanelVisible, activePanelIdentifier, mainContentRef, isLayoutPanelPinned } =
+        useValues(panelLayoutLogic)
     const { featureFlags } = useValues(featureFlagLogic)
     const { navbarItems } = useValues(navigation3000Logic)
     const { activeScene } = useValues(sceneLogic)
     const { closeAccountPopover, toggleAccountPopover } = useActions(navigationLogic)
     const { user } = useValues(userLogic)
     const { isAccountPopoverOpen } = useValues(navigationLogic)
+    const { sceneBreadcrumbKeys } = useValues(breadcrumbsLogic)
 
-    function handleClick(item: PanelLayoutNavItem): void {
-        if (activeLayoutNavBarItem !== item) {
-            setActiveLayoutNavBarItem(item)
+    function handlePanelTriggerClick(item: PanelLayoutNavIdentifier): void {
+        if (!isLayoutPanelVisible) {
             showLayoutPanel(true)
+        } else {
+            if (!isLayoutPanelPinned) {
+                showLayoutPanel(false)
+                clearActivePanelIdentifier()
+            }
+        }
+
+        if (activePanelIdentifier !== item) {
+            setActivePanelIdentifier(item)
+        }
+    }
+
+    function handleStaticNavbarItemClick(navbarItem: NavbarItem, isKeyboardAction = false): void {
+        if (!isLayoutPanelPinned) {
+            clearActivePanelIdentifier()
+            showLayoutPanel(false)
+        }
+
+        if (isKeyboardAction) {
+            mainContentRef?.current?.focus()
+        }
+        if ('to' in navbarItem && navbarItem.to) {
+            router.actions.push(navbarItem.to)
         }
     }
 
@@ -82,7 +109,7 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                             onClick={() =>
                                 alert('global “new” button which would let you create a bunch of new things')
                             }
-                            className="hover:bg-fill-highlight-100 shrink-0"
+                            className="hover:bg-fill-highlight-50 shrink-0"
                             icon={
                                 <IconWrapper>
                                     <IconPlusSmall />
@@ -92,21 +119,26 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                     </div>
 
                     <div className="z-[var(--z-main-nav)] flex flex-col flex-1 overflow-y-auto pt-1">
-                        <ScrollableShadows innerClassName="overflow-y-auto px-2 " direction="vertical" className="pb-2">
-                            <div className="pb-3">
+                        <ScrollableShadows innerClassName="overflow-y-auto px-2 " direction="vertical">
+                            <div className="pb-1">
                                 <LemonButton
                                     className={cn(
-                                        'hover:bg-fill-highlight-100',
-                                        activeLayoutNavBarItem === 'project' && 'bg-fill-highlight-50'
+                                        'hover:bg-fill-highlight-50',
+                                        activePanelIdentifier === 'project' && 'bg-fill-highlight-100'
                                     )}
                                     icon={
                                         <IconWrapper>
                                             <IconFolderOpen className="stroke-[1.2]" />
                                         </IconWrapper>
                                     }
-                                    onClick={() => handleClick('project')}
+                                    onClick={() => handlePanelTriggerClick('project')}
                                     fullWidth
                                     size="small"
+                                    onKeyDown={(e) => {
+                                        if (e.key === 'Enter') {
+                                            handlePanelTriggerClick('project')
+                                        }
+                                    }}
                                     sideIcon={
                                         <IconWrapper size="sm">
                                             <IconChevronRight />
@@ -116,7 +148,7 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                     <span>Project</span>
                                 </LemonButton>
                                 <LemonButton
-                                    className="hover:bg-fill-highlight-100"
+                                    className="hover:bg-fill-highlight-50"
                                     fullWidth
                                     size="small"
                                     onClick={toggleSearchBar}
@@ -128,49 +160,10 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                 >
                                     <span>Search</span>
                                 </LemonButton>
-                                <LemonButton
-                                    className="hover:bg-fill-highlight-100"
-                                    fullWidth
-                                    disabledReason="Coming soon"
-                                    icon={
-                                        <IconWrapper>
-                                            <IconBolt />
-                                        </IconWrapper>
-                                    }
-                                    size="small"
-                                    sideIcon={
-                                        <IconWrapper size="sm">
-                                            <IconChevronRight />
-                                        </IconWrapper>
-                                    }
-                                >
-                                    <span>Data</span>
-                                </LemonButton>
                                 <LemonButton
                                     className={cn(
-                                        'hover:bg-fill-highlight-100',
-                                        activeLayoutNavBarItem === 'persons' && 'bg-fill-highlight-50'
-                                    )}
-                                    fullWidth
-                                    icon={
-                                        <IconWrapper>
-                                            <IconPeople />
-                                        </IconWrapper>
-                                    }
-                                    size="small"
-                                    sideIcon={
-                                        <IconWrapper size="sm">
-                                            <IconChevronRight />
-                                        </IconWrapper>
-                                    }
-                                    onClick={() => handleClick('persons')}
-                                >
-                                    <span>People</span>
-                                </LemonButton>
-                                <LemonButton
-                                    className={cn(
-                                        'hover:bg-fill-highlight-100',
-                                        activeLayoutNavBarItem === 'activity' && 'bg-fill-highlight-50'
+                                        'hover:bg-fill-highlight-50',
+                                        activePanelIdentifier === 'activity' && 'bg-fill-highlight-100'
                                     )}
                                     fullWidth
                                     icon={
@@ -181,10 +174,10 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                     size="small"
                                     to={urls.activity()}
                                     onClick={() => {
-                                        if (isLayoutPanelVisible) {
+                                        if (!isLayoutPanelPinned) {
                                             showLayoutPanel(false)
+                                            clearActivePanelIdentifier()
                                         }
-                                        setActiveLayoutNavBarItem('activity')
                                     }}
                                 >
                                     <span>Activity</span>
@@ -193,34 +186,67 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
 
                             <div className="border-b border-secondary h-px -mx-2" />
 
-                            <div className="pt-3">
+                            <div className="pt-1">
                                 <div className="flex justify-between items-center pt-1 pl-2 pr-0 pb-2">
                                     <span className="text-xs font-bold text-tertiary">Products</span>
                                 </div>
                                 {navbarItems.map((section, index) => (
                                     <ul key={index}>
-                                        {section.map((item) =>
-                                            item.featureFlag && !featureFlags[item.featureFlag] ? null : (
+                                        {section.map((item) => {
+                                            if (item.identifier === 'Activity') {
+                                                return null
+                                            }
+                                            return item.featureFlag && !featureFlags[item.featureFlag] ? null : (
                                                 <LemonButton
                                                     key={item.identifier}
                                                     className={cn(
-                                                        'hover:bg-fill-highlight-100',
-                                                        activeScene?.toLowerCase() === item.identifier.toLowerCase() &&
-                                                            'bg-fill-highlight-50'
+                                                        'hover:bg-fill-highlight-50',
+                                                        (activeScene === item.identifier ||
+                                                            sceneBreadcrumbKeys.includes(item.identifier)) &&
+                                                            'bg-fill-highlight-100'
                                                     )}
                                                     icon={<IconWrapper>{item.icon}</IconWrapper>}
+                                                    sideIcon={
+                                                        item.tag ? (
+                                                            <LemonTag
+                                                                type={
+                                                                    item.tag === 'alpha'
+                                                                        ? 'completion'
+                                                                        : item.tag === 'beta'
+                                                                        ? 'warning'
+                                                                        : 'success'
+                                                                }
+                                                                size="small"
+                                                                className="ml-2"
+                                                            >
+                                                                {item.tag.toUpperCase()}
+                                                            </LemonTag>
+                                                        ) : undefined
+                                                    }
                                                     fullWidth
                                                     size="small"
+                                                    // This makes it a link if it has a to
                                                     to={'to' in item ? item.to : undefined}
+                                                    // we handle routing in the handleStaticNavbarItemClick function
                                                     onClick={() => {
-                                                        clearActiveLayoutNavBarItem()
-                                                        item.onClick?.()
+                                                        handleStaticNavbarItemClick(item, false)
+                                                    }}
+                                                    // If enter, focus the main content
+                                                    onKeyDown={(e) => {
+                                                        if (e.key === 'Enter') {
+                                                            handleStaticNavbarItemClick(item, true)
+                                                        }
                                                     }}
+                                                    active={
+                                                        (activePanelIdentifier === item.identifier ||
+                                                            sceneBreadcrumbKeys.includes(item.identifier)) &&
+                                                        activeScene === item.identifier
+                                                    }
                                                 >
                                                     {item.label}
                                                 </LemonButton>
                                             )
-                                        )}
+                                        })}
                                     </ul>
                                 ))}
                             </div>
@@ -228,12 +254,14 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
 
                         <div className="border-b border-secondary h-px" />
 
-                        <div className="pt-3 px-2">
-                            <div className="flex justify-between items-center pt-1 pl-2 pr-0 pb-2">
-                                <span className="text-xs font-bold text-tertiary">Settings & tools</span>
-                            </div>
+                        <div className="pt-1 px-2">
                             <LemonButton
-                                className="hover:bg-fill-highlight-100"
+                                className={cn(
+                                    'hover:bg-fill-highlight-50',
+                                    (activeScene === Scene.ToolbarLaunch ||
+                                        sceneBreadcrumbKeys.includes(Scene.ToolbarLaunch)) &&
+                                        'bg-fill-highlight-100'
+                                )}
                                 icon={
                                     <IconWrapper>
                                         <IconToolbar />
@@ -243,16 +271,22 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                 size="small"
                                 to={urls.toolbarLaunch()}
                                 onClick={() => {
-                                    clearActiveLayoutNavBarItem()
+                                    clearActivePanelIdentifier()
                                 }}
+                                active={
+                                    activeScene === Scene.ToolbarLaunch ||
+                                    sceneBreadcrumbKeys.includes(Scene.ToolbarLaunch)
+                                }
                             >
                                 Toolbar
                             </LemonButton>
 
-                            {/* TODO: add other things from navbarBottom */}
-
                             <LemonButton
-                                className="hover:bg-fill-highlight-100"
+                                className={cn(
+                                    'hover:bg-fill-highlight-50',
+                                    (activeScene === Scene.Settings || sceneBreadcrumbKeys.includes(Scene.Settings)) &&
+                                        'bg-fill-highlight-100'
+                                )}
                                 icon={
                                     <IconWrapper>
                                         <IconGear />
@@ -262,8 +296,9 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                 size="small"
                                 to={urls.settings('project')}
                                 onClick={() => {
-                                    clearActiveLayoutNavBarItem()
+                                    clearActivePanelIdentifier()
                                 }}
+                                active={activeScene === Scene.Settings || sceneBreadcrumbKeys.includes(Scene.Settings)}
                             >
                                 Settings
                             </LemonButton>
@@ -276,6 +311,10 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                 className="min-w-70"
                             >
                                 <LemonButton
+                                    className={cn(
+                                        'hover:bg-fill-highlight-50',
+                                        isAccountPopoverOpen && 'bg-fill-highlight-100'
+                                    )}
                                     fullWidth
                                     size="small"
                                     sideIcon={
@@ -287,7 +326,7 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                     title={`Hi${user?.first_name ? `, ${user?.first_name}` : ''}!`}
                                     onClick={toggleAccountPopover}
                                 >
-                                    <span>Hi{user?.first_name ? `, ${user?.first_name}` : ''}!</span>
+                                    {user?.first_name ? <span>{user?.first_name}</span> : <span>{user?.email}</span>}
                                 </LemonButton>
                             </Popover>
                         </div>
diff --git a/frontend/src/layout/panel-layout/PersonsTree/PersonsTree.tsx b/frontend/src/layout/panel-layout/PersonsTree/PersonsTree.tsx
index f82e9eb9f3a5..a08ce49ed2f7 100644
--- a/frontend/src/layout/panel-layout/PersonsTree/PersonsTree.tsx
+++ b/frontend/src/layout/panel-layout/PersonsTree/PersonsTree.tsx
@@ -30,26 +30,7 @@ export function PersonsTree({ mainRef }: { mainRef: React.RefObject<HTMLElement>
 
     return (
         <PanelLayoutPanel searchPlaceholder="Search for people">
-            <LemonTree
-                ref={treeRef}
-                contentRef={mainRef}
-                className="px-0 py-1"
-                data={treeData}
-                // onNodeClick={(node) => {
-                // if (node?.record?.path) {
-                //     setLastViewedId(node?.id || '')
-                //     if (!isLayoutPanelPinned) {
-                //         showLayoutPanel(false)
-                //     }
-                // }
-                // if (node?.id.startsWith('project-load-more/')) {
-                //     const path = node.id.split('/').slice(1).join('/')
-                //     if (path) {
-                //         loadFolder(path)
-                //     }
-                // }
-                // }}
-            />
+            <LemonTree ref={treeRef} contentRef={mainRef} className="px-0 py-1" data={treeData} />
         </PanelLayoutPanel>
     )
 }
diff --git a/frontend/src/layout/panel-layout/ProjectTree/ProjectTree.tsx b/frontend/src/layout/panel-layout/ProjectTree/ProjectTree.tsx
index c5c7ced67d80..aa827b4c6e95 100644
--- a/frontend/src/layout/panel-layout/ProjectTree/ProjectTree.tsx
+++ b/frontend/src/layout/panel-layout/ProjectTree/ProjectTree.tsx
@@ -1,12 +1,11 @@
-import { IconSort } from '@posthog/icons'
-import { IconPlusSmall } from '@posthog/icons'
+import { IconPlusSmall, IconSort } from '@posthog/icons'
 import { LemonButton } from '@posthog/lemon-ui'
 import { useActions, useValues } from 'kea'
 import { More } from 'lib/lemon-ui/LemonButton/More'
 import { LemonTree, LemonTreeRef } from 'lib/lemon-ui/LemonTree/LemonTree'
 import { ContextMenuGroup, ContextMenuItem } from 'lib/ui/ContextMenu/ContextMenu'
 import { IconWrapper } from 'lib/ui/IconWrapper/IconWrapper'
-import { useEffect, useRef } from 'react'
+import { RefObject, useEffect, useRef } from 'react'
 
 import { panelLayoutLogic } from '~/layout/panel-layout/panelLayoutLogic'
 import { FileSystemEntry } from '~/queries/schema/schema-general'
@@ -15,8 +14,8 @@ import { PanelLayoutPanel } from '../PanelLayoutPanel'
 import { projectTreeLogic } from './projectTreeLogic'
 import { joinPath, splitPath } from './utils'
 
-export function ProjectTree({ mainRef }: { mainRef: React.RefObject<HTMLElement> }): JSX.Element {
-    const { treeData, loadingPaths, expandedFolders, lastViewedId, viableItems } = useValues(projectTreeLogic)
+export function ProjectTree(): JSX.Element {
+    const { treeData, loadingPaths, lastViewedId, viableItems } = useValues(projectTreeLogic)
 
     const {
         createFolder,
@@ -29,8 +28,8 @@ export function ProjectTree({ mainRef }: { mainRef: React.RefObject<HTMLElement>
         loadFolder,
     } = useActions(projectTreeLogic)
 
-    const { showLayoutPanel, setPanelTreeRef } = useActions(panelLayoutLogic)
-    const { isLayoutPanelPinned } = useValues(panelLayoutLogic)
+    const { showLayoutPanel, setPanelTreeRef, clearActivePanelIdentifier } = useActions(panelLayoutLogic)
+    const { mainContentRef, isLayoutPanelPinned } = useValues(panelLayoutLogic)
     const treeRef = useRef<LemonTreeRef>(null)
 
     const handleCopyPath = (path?: string): void => {
@@ -77,18 +76,27 @@ export function ProjectTree({ mainRef }: { mainRef: React.RefObject<HTMLElement>
         >
             <LemonTree
                 ref={treeRef}
-                contentRef={mainRef}
+                contentRef={mainContentRef as RefObject<HTMLElement>}
                 className="px-0 py-1"
                 data={treeData}
-                expandedItemIds={expandedFolders}
+                // Commented out until we fix the bug here where folders are not expanded/loaded, this is a bug in the projectTreeLogic + LemonTree
+                // expandedItemIds={expandedFolders}
                 isFinishedBuildingTreeData={Object.keys(loadingPaths).length === 0}
                 defaultSelectedFolderOrNodeId={lastViewedId || undefined}
+                isItemActive={(item) => {
+                    if (!item.record?.href) {
+                        return false
+                    }
+                    return window.location.href.includes(item.record?.href) ? true : false
+                }}
                 onNodeClick={(node) => {
+                    if (!isLayoutPanelPinned) {
+                        clearActivePanelIdentifier()
+                        showLayoutPanel(false)
+                    }
+
                     if (node?.record?.path) {
                         setLastViewedId(node?.id || '')
-                        if (!isLayoutPanelPinned) {
-                            showLayoutPanel(false)
-                        }
                     }
                     if (node?.id.startsWith('project-load-more/')) {
                         const path = node.id.split('/').slice(1).join('/')
diff --git a/frontend/src/layout/panel-layout/ProjectTree/projectTreeLogic.tsx b/frontend/src/layout/panel-layout/ProjectTree/projectTreeLogic.tsx
index 5532fa70d27d..d296337efcec 100644
--- a/frontend/src/layout/panel-layout/ProjectTree/projectTreeLogic.tsx
+++ b/frontend/src/layout/panel-layout/ProjectTree/projectTreeLogic.tsx
@@ -29,7 +29,7 @@ export const projectTreeLogic = kea<projectTreeLogicType>([
             featureFlagLogic,
             ['featureFlags'],
             panelLayoutLogic,
-            ['searchTerm'],
+            ['searchTerm', 'isLayoutPanelPinned'],
         ],
     }),
     actions({
@@ -167,6 +167,7 @@ export const projectTreeLogic = kea<projectTreeLogicType>([
         ],
         expandedFolders: [
             [] as string[],
+            { persist: true },
             {
                 setExpandedFolders: (_, { folderIds }) => folderIds,
             },
@@ -317,35 +318,30 @@ export const projectTreeLogic = kea<projectTreeLogicType>([
                 convertFileSystemEntryToTreeDataItem(getDefaultTree(groupNodes), folderStates, 'root'),
         ],
         projectRow: [
-            () => [],
-            (): TreeDataItem[] => [
-                {
-                    id: 'top-separator',
-                    name: '',
-                    type: 'separator',
-                },
+            (s) => [s.isLayoutPanelPinned],
+            (isLayoutPanelPinned): TreeDataItem[] => [
                 {
                     id: 'project',
                     name: 'Default Project',
                     icon: <IconBook />,
                     record: { type: 'project', id: 'project' },
-                    onClick: () => router.actions.push(urls.projectHomepage()),
+                    onClick: () => {
+                        if (!isLayoutPanelPinned) {
+                            panelLayoutLogic.actions.showLayoutPanel(false)
+                        }
+                        router.actions.push(urls.projectHomepage())
+                    },
                 },
             ],
         ],
         searchedTreeItems: [
-            (s) => [s.defaultTreeNodes, s.projectTree, s.projectRow, s.searchTerm],
-            (
-                defaultTreeNodes: TreeDataItem[],
-                projectTree: TreeDataItem[],
-                projectRow: TreeDataItem[],
-                searchTerm: string
-            ): TreeDataItem[] => {
+            (s) => [s.projectTree, s.projectRow, s.searchTerm],
+            (projectTree: TreeDataItem[], projectRow: TreeDataItem[], searchTerm: string): TreeDataItem[] => {
                 if (!searchTerm) {
-                    return [...defaultTreeNodes, ...projectRow, ...projectTree]
+                    return [...projectRow, ...projectTree]
                 }
 
-                const allItems = [...defaultTreeNodes, ...projectRow, ...projectTree]
+                const allItems = [...projectRow, ...projectTree]
 
                 // TODO: remove from client side @marius :)
                 const fuse = new Fuse(allItems, {
@@ -390,12 +386,12 @@ export const projectTreeLogic = kea<projectTreeLogicType>([
             },
         ],
         treeData: [
-            (s) => [s.searchTerm, s.searchedTreeItems, s.defaultTreeNodes, s.projectTree, s.projectRow],
-            (searchTerm, searchedTreeItems, defaultTreeNodes, projectTree, projectRow): TreeDataItem[] => {
+            (s) => [s.searchTerm, s.searchedTreeItems, s.projectTree, s.projectRow],
+            (searchTerm, searchedTreeItems, projectTree, projectRow): TreeDataItem[] => {
                 if (searchTerm) {
                     return searchedTreeItems
                 }
-                return [...defaultTreeNodes, ...projectRow, ...projectTree] as TreeDataItem[]
+                return [...projectRow, ...projectTree] as TreeDataItem[]
             },
         ],
     }),
diff --git a/frontend/src/layout/panel-layout/panelLayoutLogic.tsx b/frontend/src/layout/panel-layout/panelLayoutLogic.tsx
index d8c343f17b1c..59e2ae09ea32 100644
--- a/frontend/src/layout/panel-layout/panelLayoutLogic.tsx
+++ b/frontend/src/layout/panel-layout/panelLayoutLogic.tsx
@@ -4,8 +4,9 @@ import { LemonTreeRef } from 'lib/lemon-ui/LemonTree/LemonTree'
 import { navigation3000Logic } from '../navigation-3000/navigationLogic'
 import type { panelLayoutLogicType } from './panelLayoutLogicType'
 
-export type PanelLayoutNavItem = 'project' | 'activity' | 'persons'
+export type PanelLayoutNavIdentifier = 'project' // Add more identifiers here for more panels
 export type PanelLayoutTreeRef = React.RefObject<LemonTreeRef> | null
+export type PanelLayoutMainContentRef = React.RefObject<HTMLElement> | null
 
 export const panelLayoutLogic = kea<panelLayoutLogicType>({
     path: ['layout', 'panel-layout', 'panelLayoutLogic'],
@@ -18,11 +19,12 @@ export const panelLayoutLogic = kea<panelLayoutLogicType>({
         toggleLayoutPanelPinned: (pinned: boolean) => ({ pinned }),
         // TODO: This is a temporary action to set the active navbar item
         // We should remove this once we have a proper way to handle the navbar item
-        setActiveLayoutNavBarItem: (item: PanelLayoutNavItem) => ({ item }),
-        clearActiveLayoutNavBarItem: true,
+        setActivePanelIdentifier: (identifier: PanelLayoutNavIdentifier) => ({ identifier }),
+        clearActivePanelIdentifier: true,
         setSearchTerm: (searchTerm: string) => ({ searchTerm }),
         clearSearch: true,
         setPanelTreeRef: (ref: PanelLayoutTreeRef) => ({ ref }),
+        setMainContentRef: (ref: PanelLayoutMainContentRef) => ({ ref }),
     },
     reducers: {
         isLayoutNavbarVisibleForDesktop: [
@@ -39,6 +41,13 @@ export const panelLayoutLogic = kea<panelLayoutLogicType>({
                 mobileLayout: () => true,
             },
         ],
+        isLayoutPanelCloseable: [
+            true,
+            {
+                showLayoutPanel: () => true,
+                toggleLayoutPanelPinned: () => false,
+            },
+        ],
         isLayoutPanelVisible: [
             false,
             {
@@ -53,11 +62,11 @@ export const panelLayoutLogic = kea<panelLayoutLogicType>({
                 toggleLayoutPanelPinned: (_, { pinned }) => pinned,
             },
         ],
-        activeLayoutNavBarItem: [
+        activePanelIdentifier: [
             '',
             {
-                setActiveLayoutNavBarItem: (_, { item }) => item,
-                clearActiveLayoutNavBarItem: () => '',
+                setActivePanelIdentifier: (_, { identifier }) => identifier,
+                clearActivePanelIdentifier: () => '',
             },
         ],
         searchTerm: [
@@ -73,5 +82,11 @@ export const panelLayoutLogic = kea<panelLayoutLogicType>({
                 setPanelTreeRef: (_, { ref }) => ref,
             },
         ],
+        mainContentRef: [
+            null as PanelLayoutMainContentRef,
+            {
+                setMainContentRef: (_, { ref }) => ref,
+            },
+        ],
     },
 })
diff --git a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx
index a59dffae896f..0b9d23f7f336 100644
--- a/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx
+++ b/frontend/src/lib/lemon-ui/LemonButton/LemonButton.tsx
@@ -105,7 +105,7 @@ export interface LemonButtonWithoutSideActionProps extends LemonButtonPropsBase
 }
 /** A LemonButtonWithSideAction can't have a sideIcon - instead it has a clickable sideAction. */
 export interface LemonButtonWithSideActionProps extends LemonButtonPropsBase {
-    sideAction: SideAction
+    sideAction?: SideAction
     sideIcon?: null
 }
 export type LemonButtonProps = LemonButtonWithoutSideActionProps | LemonButtonWithSideActionProps
diff --git a/frontend/src/lib/lemon-ui/LemonTree/LemonTree.tsx b/frontend/src/lib/lemon-ui/LemonTree/LemonTree.tsx
index dee0b36d0ee3..18b56882fb61 100644
--- a/frontend/src/lib/lemon-ui/LemonTree/LemonTree.tsx
+++ b/frontend/src/lib/lemon-ui/LemonTree/LemonTree.tsx
@@ -35,7 +35,16 @@ export type TreeDataItem = {
     /** Disabled: The reason the item is disabled. */
     disabledReason?: string
 
+    /** The icon to use for the side action. */
+    sideIcon?: React.ReactNode
+
+    /** The type of item.
+     *
+     * Type node, normal behavior
+     * Type separator, render as separator
+     */
     type?: 'node' | 'separator'
+
     /**
      * Handle a click on the item.
      * @param open - boolean to indicate if it's a folder and it's open state
@@ -56,6 +65,9 @@ type LemonTreeBaseProps = Omit<HTMLAttributes<HTMLDivElement>, 'onDragEnd'> & {
     showFolderActiveState?: boolean
     /** Whether to enable drag and drop of items. */
     enableDragAndDrop?: boolean
+    /** Whether the item is active, useful for highlighting the current item against a URL path,
+     * this takes precedence over showFolderActiveState, and selectedId state */
+    isItemActive?: (item: TreeDataItem) => boolean
     /** Whether the item is draggable */
     isItemDraggable?: (item: TreeDataItem) => boolean
     /** Whether the item can accept drops */
@@ -72,7 +84,8 @@ type LemonTreeBaseProps = Omit<HTMLAttributes<HTMLDivElement>, 'onDragEnd'> & {
     renderItem?: (item: TreeDataItem, children: React.ReactNode) => React.ReactNode
     /** Set the IDs of the expanded items. */
     onSetExpandedItemIds?: (ids: string[]) => void
-    /** Pass true if you need to wait for async events to populate the tree. If present and true will trigger: scrolling to focused item */
+    /** Pass true if you need to wait for async events to populate the tree.
+     * If present and true will trigger: scrolling to focused item */
     isFinishedBuildingTreeData?: boolean
 }
 
@@ -120,6 +133,7 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
             onSetExpandedItemIds,
             defaultNodeIcon,
             showFolderActiveState,
+            isItemActive,
             isItemDraggable,
             isItemDroppable,
             depth = 0,
@@ -135,6 +149,15 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
 
         const [isContextMenuOpenForItem, setIsContextMenuOpenForItem] = useState<string | undefined>(undefined)
 
+        const getItemActiveState = (item: TreeDataItem): boolean => {
+            if (typeof isItemActive === 'function') {
+                return isItemActive(item)
+            }
+            return Boolean(
+                selectedId === item.id || (showFolderActiveState && item.children && expandedItemIds?.includes(item.id))
+            )
+        }
+
         if (!(data instanceof Array)) {
             data = [data]
         }
@@ -182,13 +205,13 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
                                         <ContextMenuTrigger asChild>
                                             <LemonButton
                                                 className={cn(
-                                                    'group/lemon-tree-button',
-                                                    'flex-1 flex items-center gap-2 font-normal',
-                                                    'cursor-pointer',
-                                                    (focusedId === item.id || isContextMenuOpenForItem === item.id) &&
-                                                        'ring-2 ring-inset ring-offset-[-1px] ring-accent-primary',
-                                                    selectedId === item.id &&
-                                                        'border-l-[4px] border-l-accent-primary rounded-tl-sm rounded-bl-sm'
+                                                    'group/lemon-tree-button flex-1 flex items-center gap-2 font-normal cursor-pointer',
+                                                    {
+                                                        'ring-2 ring-inset ring-offset-[-1px] ring-accent-primary':
+                                                            focusedId === item.id ||
+                                                            isContextMenuOpenForItem === item.id,
+                                                        'bg-fill-highlight-100': getItemActiveState(item),
+                                                    }
                                                 )}
                                                 onClick={() => {
                                                     handleClick(item)
@@ -200,12 +223,7 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
                                                 size="small"
                                                 fullWidth
                                                 data-id={item.id}
-                                                active={
-                                                    selectedId === item.id ||
-                                                    (showFolderActiveState &&
-                                                        item.children &&
-                                                        expandedItemIds?.includes(item.id))
-                                                }
+                                                active={getItemActiveState(item)}
                                                 icon={getIcon({
                                                     item,
                                                     expandedItemIds: expandedItemIds ?? [],
@@ -237,11 +255,13 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
                                                 ) : (
                                                     <span
                                                         className={cn('', {
-                                                            'font-bold': selectedId === item.id,
+                                                            'font-bold': getItemActiveState(item),
                                                             'text-secondary': item.disabledReason,
+                                                            'flex items-center justify-between gap-2': item.sideIcon,
                                                         })}
                                                     >
                                                         {displayName}
+                                                        {item.sideIcon}
                                                     </span>
                                                 )}
                                             </LemonButton>
@@ -268,6 +288,7 @@ const LemonTreeNode = forwardRef<HTMLDivElement, LemonTreeNodeProps>(
                                             itemSideAction={itemSideAction}
                                             className="deprecated-space-y-px"
                                             depth={depth + 1}
+                                            isItemActive={isItemActive}
                                             isItemDraggable={isItemDraggable}
                                             isItemDroppable={isItemDroppable}
                                             enableDragAndDrop={enableDragAndDrop}
@@ -345,9 +366,14 @@ const LemonTree = forwardRef<LemonTreeRef, LemonTreeProps>(
         // Scrollable container
         const containerRef = useRef<HTMLDivElement>(null)
 
+        // Current state (when matching defaultSelectedFolderOrNodeId)
         const [selectedId, setSelectedId] = useState<string | undefined>(defaultSelectedFolderOrNodeId)
+
+        // Focused state
         const [focusedId, setFocusedId] = useState<string | undefined>(defaultSelectedFolderOrNodeId)
+
         const [hasFocusedContent, setHasFocusedContent] = useState(false)
+
         // Add new state for type-ahead
         const [typeAheadBuffer, setTypeAheadBuffer] = useState<string>('')
         const typeAheadTimeoutRef = useRef<NodeJS.Timeout>()
@@ -536,15 +562,12 @@ const LemonTree = forwardRef<LemonTreeRef, LemonTreeProps>(
 
         const handleClick = useCallback(
             (item: TreeDataItem | undefined, isKeyboardAction = false): void => {
-                // Update focusedId when clicking
                 setFocusedId(item?.id)
-
                 const isFolder = (item?.children && item?.children?.length >= 0) || item?.record?.type === 'folder'
 
                 // Handle click on a node
                 if (!isFolder) {
                     if (onNodeClick) {
-                        setSelectedId(item?.id)
                         onNodeClick(item)
                         // Only focus content if this was triggered by a keyboard action
                         if (isKeyboardAction) {
@@ -607,6 +630,7 @@ const LemonTree = forwardRef<LemonTreeRef, LemonTreeProps>(
                         ) {
                             // If folder is not expanded, expand it
                             if (!expandedItemIdsState.includes(currentItem.id)) {
+                                onFolderClick?.(currentItem, false)
                                 const newExpandedIds = [...new Set([...expandedItemIdsState, currentItem.id])]
                                 setExpandedItemIdsState(newExpandedIds)
                                 onSetExpandedItemIds && onSetExpandedItemIds(newExpandedIds)
@@ -878,7 +902,7 @@ const LemonTree = forwardRef<LemonTreeRef, LemonTreeProps>(
                     className="flex-1"
                     innerClassName="p-2"
                 >
-                    <TreeNodeDroppable id="" isDroppable={enableDragAndDrop} className="h-full pb-32">
+                    <TreeNodeDroppable id="" isDroppable={enableDragAndDrop} className="h-full">
                         <LemonTreeNode
                             data={data}
                             selectedId={selectedId}


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR improves panel layout and project tree functionality with a focus on keyboard navigation, state management, and UI consistency.

- Added `mainContentRef` handling in `panelLayoutLogic` to properly focus main content on keyboard navigation
- Implemented proper folder loading states with pagination (100 items limit) in `projectTreeLogic`
- Added `sideIcon` prop to `LemonTreeItem` for displaying alpha/beta tags and other right-aligned content
- Fixed keyboard navigation with `ArrowRight` to correctly trigger `onFolderClick` when expanding folders
- Added persistence to `isLayoutPanelPinned` state to maintain panel visibility across sessions

The changes improve accessibility and user experience while maintaining core functionality. The code appears well-structured with clear separation of concerns between layout, navigation, and tree components.

<sub>8 file(s) reviewed, 4 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog_30074)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T12:42:20Z
user: login: mariusandra
body: Very nice!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T09:09:43Z

Review comments:
diff_hunk: @@ -0,0 +1,504 @@
+import { IconBook } from '@posthog/icons'
+import Fuse from 'fuse.js'
+import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
+import { loaders } from 'kea-loaders'
+import { router } from 'kea-router'
+import api from 'lib/api'
+import { GroupsAccessStatus } from 'lib/introductions/groupsAccessLogic'
+import { TreeDataItem } from 'lib/lemon-ui/LemonTree/LemonTree'
+import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
+import { capitalizeFirstLetter } from 'lib/utils'
+import { urls } from 'scenes/urls'
+
+import { groupsModel } from '~/models/groupsModel'
+import { FileSystemEntry } from '~/queries/schema/schema-general'
+
+import { getDefaultTree } from './defaultTree'
+import type { projectTreeLogicType } from './projectTreeLogicType'
+import { FileSystemImport, FolderState, ProjectTreeAction } from './types'
+import { convertFileSystemEntryToTreeDataItem, findInProjectTree, joinPath, splitPath } from './utils'
+
+const PAGINATION_LIMIT = 100
+
+export const projectTreeLogic = kea<projectTreeLogicType>([
+    path(['layout', 'navigation-3000', 'components', 'projectTreeLogic']),
+    connect({
+        values: [
+            groupsModel,
+            ['aggregationLabel', 'groupTypes', 'groupsAccessStatus'],
+            featureFlagLogic,
+            ['featureFlags'],
+        ],
+    }),
+    actions({
+        loadUnfiledItems: true,
+        addFolder: (folder: string) => ({ folder }),
+        deleteItem: (item: FileSystemEntry) => ({ item }),
+        moveItem: (oldPath: string, newPath: string) => ({ oldPath, newPath }),
+        queueAction: (action: ProjectTreeAction) => ({ action }),
+        removeQueuedAction: (action: ProjectTreeAction) => ({ action }),
+        applyPendingActions: true,
+        cancelPendingActions: true,
+        createSavedItem: (savedItem: FileSystemEntry) => ({ savedItem }),
+        updateSavedItem: (savedItem: FileSystemEntry, oldPath: string) => ({ savedItem, oldPath }),
+        deleteSavedItem: (savedItem: FileSystemEntry) => ({ savedItem }),
+        setExpandedFolders: (folderIds: string[]) => ({ folderIds }),
+        setLastViewedId: (id: string) => ({ id }),
+        toggleFolderOpen: (folderId: string, isExpanded: boolean) => ({ folderId, isExpanded }),
+        setHelpNoticeVisibility: (visible: boolean) => ({ visible }),
+        loadFolder: (folder: string) => ({ folder }),
+        loadFolderStart: (folder: string) => ({ folder }),
+        loadFolderSuccess: (folder: string, entries: FileSystemEntry[], hasMore: boolean = false) => ({
+            folder,
+            entries,
+            hasMore,
+        }),
+        loadFolderFailure: (folder: string, error: string) => ({ folder, error }),
+        rename: (path: string) => ({ path }),
+        createFolder: (parentPath: string) => ({ parentPath }),
+        setSearchTerm: (searchTerm: string) => ({ searchTerm }),
+        clearSearch: true,
+    }),
+    loaders(({ actions, values }) => ({
+        allUnfiledItems: [
+            [] as FileSystemEntry[],
+            {
+                loadUnfiledItems: async () => {
+                    const response = await api.fileSystem.unfiled()
+                    return [...values.allUnfiledItems, ...response.results]
+                },
+            },
+        ],
+        pendingLoader: [
+            false,
+            {
+                applyPendingActions: async () => {
+                    for (const action of values.pendingActions) {
+                        if (action.type === 'move' && action.newPath) {
+                            if (!action.item.id) {
+                                const response = await api.fileSystem.create({ ...action.item, path: action.newPath })
+                                actions.createSavedItem(response)
+                            } else {
+                                const response = await api.fileSystem.update(action.item.id, { path: action.newPath })
+                                actions.updateSavedItem(response, action.item.path)
+                            }
+                        } else if (action.type === 'create') {
+                            const response = await api.fileSystem.create(action.item)
+                            actions.createSavedItem(response)
+                        } else if (action.type === 'delete' && action.item.id) {
+                            await api.fileSystem.delete(action.item.id)
+                            actions.deleteSavedItem(action.item)
+                        }
+                        actions.removeQueuedAction(action)
+                    }
+                    return true
+                },
+                cancelPendingActions: async () => {
+                    for (const action of values.pendingActions) {
+                        actions.removeQueuedAction(action)
+                    }
+                    return true
+                },
+            },
+        ],
+    })),
+    reducers({
+        folders: [
+            {} as Record<string, FileSystemEntry[]>,
+            {
+                loadFolderSuccess: (state, { folder, entries }) => ({ ...state, [folder]: entries }),
+                createSavedItem: (state, { savedItem }) => {
+                    const folder = joinPath(splitPath(savedItem.path).slice(0, -1))
+                    return { ...state, [folder]: [...(state[folder] || []), savedItem] }
+                },
+                updateSavedItem: (state, { savedItem, oldPath }) => {
+                    const oldFolder = joinPath(splitPath(oldPath).slice(0, -1))
+                    const folder = joinPath(splitPath(savedItem.path).slice(0, -1))
+
+                    if (oldFolder === folder) {
+                        return {
+                            ...state,
+                            [folder]: (state[folder] ?? []).map((item) =>
+                                item.id === savedItem.id ? savedItem : item
+                            ),
+                        }
+                    }
+                    return {
+                        ...state,
+                        [oldFolder]: (state[oldFolder] ?? []).filter((item) => item.id !== savedItem.id),
+                        [folder]: [...(state[folder] ?? []), savedItem],
+                    }
+                },
+                deleteSavedItem: (state, { savedItem }) => {
+                    const folder = joinPath(splitPath(savedItem.path).slice(0, -1))
+                    return {
+                        ...state,
+                        [folder]: state[folder].filter((item) => item.id !== savedItem.id),
+                    }
+                },
+            },
+        ],
+        folderStates: [
+            {} as Record<string, FolderState>,
+            {
+                loadFolderStart: (state, { folder }) => ({ ...state, [folder]: 'loading' }),
+                loadFolderSuccess: (state, { folder, hasMore }) => ({
+                    ...state,
+                    [folder]: hasMore ? 'has-more' : 'loaded',
+                }),
+                loadFolderFailure: (state, { folder }) => ({ ...state, [folder]: 'error' }),
+            },
+        ],
+        unfiledLoadingCount: [
+            0,
+            {
+                loadUnfiledItems: (state) => state + 1,
+                loadUnfiledItemsSuccess: (state) => state - 1,
+                loadUnfiledItemsFailure: (state) => state - 1,
+            },
+        ],
+        pendingActions: [
+            [] as ProjectTreeAction[],
+            {
+                queueAction: (state, { action }) => [...state, action],
+                removeQueuedAction: (state, { action }) => state.filter((a) => a !== action),
+                cancelPendingActions: () => [],
+            },
+        ],
+        expandedFolders: [
+            [] as string[],
+            {
+                setExpandedFolders: (_, { folderIds }) => folderIds,
+            },
+        ],
+        lastViewedId: [
+            '',
+            {
+                setLastViewedId: (_, { id }) => id,
+            },
+        ],
+        helpNoticeVisible: [
+            true,
+            {
+                setHelpNoticeVisibility: (_, { visible }) => visible,
+            },
+        ],
+        searchTerm: [
+            '',
+            {
+                setSearchTerm: (_, { searchTerm }) => searchTerm,
+                clearSearch: () => '',
+            },
+        ],
+    }),
+    selectors({
+        savedItems: [
+            (s) => [s.folders, s.folderStates],
+            (folders): FileSystemEntry[] =>
+                Object.entries(folders).reduce((acc, [_, items]) => [...acc, ...items], [] as FileSystemEntry[]),
+        ],
+        savedItemsLoading: [
+            (s) => [s.folderStates],
+            (folderStates): boolean => Object.values(folderStates).some((state) => state === 'loading'),
+        ],
+        unfiledLoading: [(s) => [s.unfiledLoadingCount], (unfiledLoadingCount) => unfiledLoadingCount > 0],
+        unfiledItems: [
+            // Remove from unfiledItems the ones that are in "savedItems"
+            (s) => [s.savedItems, s.allUnfiledItems],
+            (savedItems, allUnfiledItems): FileSystemEntry[] => {
+                const urls = new Set<string>()
+                for (const item of [...savedItems]) {
+                    const key = `${item.type}/${item.ref}`
+                    if (!urls.has(key)) {
+                        urls.add(key)
+                    }
+                }
+                return allUnfiledItems.filter((item) => !urls.has(`${item.type}/${item.ref}`))
+            },
+        ],
+        viableItems: [
+            // Combine unfiledItems with savedItems and apply pendingActions
+            (s) => [s.unfiledItems, s.savedItems, s.pendingActions],
+            (unfiledItems, savedItems, pendingActions): FileSystemEntry[] => {
+                const items = [...unfiledItems, ...savedItems]
+                const itemsByPath = Object.fromEntries(items.map((item) => [item.path, item]))
+                for (const action of pendingActions) {
+                    if (action.type === 'move' && action.newPath) {
+                        const item = itemsByPath[action.path]
+                        if (item) {
+                            if (!itemsByPath[action.newPath]) {
+                                itemsByPath[action.newPath] = { ...item, path: action.newPath }
+                                delete itemsByPath[action.path]
+                            } else {
+                                console.error("Item already exists, can't move", action.newPath)
+                            }
+                        } else {
+                            console.error("Item not found, can't move", action.path)
+                        }
+                    } else if (action.type === 'create' && action.newPath) {
+                        if (!itemsByPath[action.newPath]) {
+                            itemsByPath[action.newPath] = { ...action.item, path: action.newPath }
+                        } else {
+                            console.error("Item already exists, can't create", action.item)
+                        }
+                    } else if (action.type === 'delete' && action.path) {
+                        delete itemsByPath[action.path]
+                    }
+                }
+                return Object.values(itemsByPath)
+            },
+        ],
+        unappliedPaths: [
+            // Paths that are currently being loaded
+            (s) => [s.pendingActions],
+            (pendingActions) => {
+                const unappliedPaths: Record<string, boolean> = {}
+                for (const action of pendingActions) {
+                    if (action.type === 'move-create' || action.type === 'move' || action.type === 'create') {
+                        if (action.newPath) {
+                            unappliedPaths[action.newPath] = true
+                            const split = splitPath(action.newPath)
+                            for (let i = 1; i < split.length; i++) {
+                                unappliedPaths[joinPath(split.slice(0, i))] = true
+                            }
+                        }
+                    }
+                }
+                return unappliedPaths
+            },
+        ],
+        loadingPaths: [
+            // Paths that are currently being loaded
+            (s) => [s.unfiledLoading, s.savedItemsLoading, s.pendingLoaderLoading, s.pendingActions],
+            (unfiledLoading, savedItemsLoading, pendingLoaderLoading, pendingActions) => {
+                const loadingPaths: Record<string, boolean> = {}
+                if (unfiledLoading) {
+                    loadingPaths['Unfiled'] = true
+                    loadingPaths[''] = true
+                }
+                if (savedItemsLoading) {
+                    loadingPaths[''] = true
+                }
+                if (pendingLoaderLoading && pendingActions.length > 0) {
+                    loadingPaths[pendingActions[0].newPath || pendingActions[0].path] = true
+                }
+                return loadingPaths
+            },
+        ],
+        pendingActionsCount: [(s) => [s.pendingActions], (pendingActions): number => pendingActions.length],
+        projectTree: [
+            (s) => [s.viableItems, s.folderStates],
+            (viableItems, folderStates): TreeDataItem[] =>
+                convertFileSystemEntryToTreeDataItem(viableItems, folderStates, 'project'),
+        ],
+        groupNodes: [
+            (s) => [s.groupTypes, s.groupsAccessStatus, s.aggregationLabel],
+            (groupTypes, groupsAccessStatus, aggregationLabel): FileSystemImport[] => {
+                const showGroupsIntroductionPage = [
+                    GroupsAccessStatus.HasAccess,
+                    GroupsAccessStatus.HasGroupTypes,
+                    GroupsAccessStatus.NoAccess,
+                ].includes(groupsAccessStatus)
+
+                const groupNodes: FileSystemImport[] = [
+                    ...(showGroupsIntroductionPage
+                        ? [
+                              {
+                                  path: 'Groups',
+                                  href: urls.groups(0),
+                              },
+                          ]
+                        : Array.from(groupTypes.values()).map((groupType) => ({
+                              path: capitalizeFirstLetter(aggregationLabel(groupType.group_type_index).plural),
+                              href: urls.groups(groupType.group_type_index),
+                          }))),
+                ]
+
+                return groupNodes
+            },
+        ],
+        defaultTreeNodes: [
+            (s) => [s.featureFlags, s.groupNodes, s.folderStates],
+            (_featureFlags, groupNodes: FileSystemImport[], folderStates) =>
+                // .filter(f => !f.flag || featureFlags[f.flag])
+                convertFileSystemEntryToTreeDataItem(getDefaultTree(groupNodes), folderStates, 'root'),
+        ],
+        projectRow: [
+            () => [],
+            (): TreeDataItem[] => [
+                {
+                    id: 'top-separator',
+                    name: '',
+                    type: 'separator',
+                },
+                {
+                    id: 'project',
+                    name: 'Default Project',
+                    icon: <IconBook />,
+                    type: 'node',
+                    record: { type: 'project', id: 'project' },
+                    onClick: () => router.actions.push(urls.projectHomepage()),
+                },
+            ],
+        ],
+        searchedTreeItems: [
+            (s) => [s.defaultTreeNodes, s.projectTree, s.projectRow, s.searchTerm],
+            (
+                defaultTreeNodes: TreeDataItem[],
+                projectTree: TreeDataItem[],
+                projectRow: TreeDataItem[],
+                searchTerm: string
+            ): TreeDataItem[] => {
+                if (!searchTerm) {
+                    return [...defaultTreeNodes, ...projectRow, ...projectTree]
+                }
+
+                const allItems = [...defaultTreeNodes, ...projectRow, ...projectTree]
+
+                // TODO: remove from client side @marius :)
+                const fuse = new Fuse(allItems, {
+                    keys: ['name'],
+                    threshold: 0.3,
+                    ignoreLocation: true,
+                })
+
+                const searchResults = fuse.search(searchTerm).map((result) => result.item)
+
+                // Helper function to check if any child matches the search
+                const hasMatchingChild = (item: TreeDataItem): boolean => {
+                    if (!item.children) {
+                        return false
+                    }
+                    return item.children.some(
+                        (child) => searchResults.some((result) => result.id === child.id) || hasMatchingChild(child)
+                    )
+                }
+
+                // Include parent folders of matching items
+                const resultWithParents = searchResults.reduce((acc: TreeDataItem[], item) => {
+                    if (!acc.some((i) => i.id === item.id)) {
+                        acc.push(item)
+                    }
+
+                    // If it's a matching folder, include its children
+                    if (item.children) {
+                        item.children.forEach((child) => {
+                            if (
+                                !acc.some((i) => i.id === child.id) &&
+                                (searchResults.some((r) => r.id === child.id) || hasMatchingChild(child))
+                            ) {
+                                acc.push(child)
+                            }
+                        })
+                    }
+                    return acc
+                }, [])
+
+                return resultWithParents
+            },
+        ],
+        treeData: [
+            (s) => [s.searchTerm, s.searchedTreeItems, s.defaultTreeNodes, s.projectTree, s.projectRow],
+            (searchTerm, searchedTreeItems, defaultTreeNodes, projectTree, projectRow): TreeDataItem[] => {
+                if (searchTerm) {
+                    return searchedTreeItems
+                }
+                return [...defaultTreeNodes, ...projectRow, ...projectTree] as TreeDataItem[]
+            },
+        ],
+    }),
+    listeners(({ actions, values }) => ({
+        loadFolder: async ({ folder }) => {
+            const currentState = values.folderStates[folder]
+            if (currentState === 'loading' || currentState === 'loaded') {
+                return
+            }
+            actions.loadFolderStart(folder)
+            try {
+                const previousFiles = values.folders[folder] || []
+                const response = await api.fileSystem.list(
+                    folder,
+                    splitPath(folder).length + 1,
+                    PAGINATION_LIMIT + 1,
+                    previousFiles.length
+                )
+
+                let files = response.results
+                let hasMore = false
+                if (files.length > PAGINATION_LIMIT) {
+                    files = files.slice(0, PAGINATION_LIMIT)
+                    hasMore = true
+                }
+                actions.loadFolderSuccess(folder, [...previousFiles, ...files], hasMore)
+            } catch (error) {
+                actions.loadFolderFailure(folder, String(error))
+            }
+        },
+        moveItem: async ({ oldPath, newPath }) => {
+            for (const item of values.viableItems) {
+                if (item.path === oldPath || item.path.startsWith(oldPath + '/')) {
+                    actions.queueAction({
+                        type: 'move',
+                        item,
+                        path: item.path,
+                        newPath: newPath + item.path.slice(oldPath.length),
+                    })
+                }
+            }
+        },
+        deleteItem: async ({ item }) => {
+            actions.queueAction({ type: 'delete', item, path: item.path })
+        },
+        addFolder: ({ folder }) => {
+            if (values.viableItems.find((item) => item.path === folder)) {
+                return
+            }
+            actions.queueAction({
+                type: 'create',
+                item: { id: `project/${folder}`, path: folder, type: 'folder' },
+                path: folder,
+                newPath: folder,
+            })
+        },
+        toggleFolderOpen: ({ folderId }) => {
+            if (values.expandedFolders.find((f) => f === folderId)) {
+                actions.setExpandedFolders(values.expandedFolders.filter((f) => f !== folderId))
+            } else {
+                actions.setExpandedFolders([...values.expandedFolders, folderId])
+                if (values.folderStates[folderId] !== 'loaded' && values.folderStates[folderId] !== 'loading') {
+                    const folder = findInProjectTree(folderId, values.projectTree)
+                    folder && actions.loadFolder(folder.record?.path)
+                }
+            }
+        },
+        cancelPendingActions: () => {
+            // Clear all pending actions without applying them
+            for (const action of values.pendingActions) {
+                actions.removeQueuedAction(action)
+            }
+        },
+        rename: ({ path }) => {
+            const splits = splitPath(path)
+            if (splits.length > 0) {
+                const currentName = splits[splits.length - 1].replace(/\\/g, '')
+                const folder = prompt('New name?', currentName)
+                if (folder) {
+                    actions.moveItem(path, joinPath([...splits.slice(0, -1), folder]))
+                }
+            }
+        },
path: frontend/src/layout/navigation-3000/components/ProjectTree/projectTreeLogic.tsx
user: login: greptile-apps[bot]
body: style: Using window.prompt() is not recommended for modern web apps. Consider using a modal dialog component instead
created_at: 2025-03-18T12:40:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -199,28 +184,58 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                 </div>
                                 {navbarItems.map((section, index) => (
                                     <ul key={index}>
-                                        {section.map((item) =>
-                                            item.featureFlag && !featureFlags[item.featureFlag] ? null : (
+                                        {section.map((item) => {
+                                            if (item.identifier === 'Activity') {
+                                                return null
+                                            }
path: frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
user: login: greptile-apps[bot]
body: style: Filtering out Activity here but keeping the Activity button above creates inconsistent navigation patterns.
created_at: 2025-03-18T12:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,7 +302,7 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
                                     title={`Hi${user?.first_name ? `, ${user?.first_name}` : ''}!`}
                                     onClick={toggleAccountPopover}
                                 >
-                                    <span>Hi{user?.first_name ? `, ${user?.first_name}` : ''}!</span>
+                                    <span>{user?.first_name ? <span>{user?.first_name}</span> : null}</span>
path: frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
user: login: greptile-apps[bot]
body: style: Unnecessary nested span elements when displaying user's first name.
created_at: 2025-03-18T12:40:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -317,35 +318,30 @@ export const projectTreeLogic = kea<projectTreeLogicType>([
                 convertFileSystemEntryToTreeDataItem(getDefaultTree(groupNodes), folderStates, 'root'),
         ],
         projectRow: [
-            () => [],
-            (): TreeDataItem[] => [
-                {
-                    id: 'top-separator',
-                    name: '',
-                    type: 'separator',
-                },
+            (s) => [s.isLayoutPanelCloseable],
+            (isLayoutPanelCloseable): TreeDataItem[] => [
                 {
                     id: 'project',
                     name: 'Default Project',
                     icon: <IconBook />,
                     record: { type: 'project', id: 'project' },
-                    onClick: () => router.actions.push(urls.projectHomepage()),
+                    onClick: () => {
+                        if (isLayoutPanelCloseable) {
+                            panelLayoutLogic.actions.showLayoutPanel(false)
+                        }
+                        router.actions.push(urls.projectHomepage())
+                    },
                 },
             ],
         ],
         searchedTreeItems: [
-            (s) => [s.defaultTreeNodes, s.projectTree, s.projectRow, s.searchTerm],
-            (
-                defaultTreeNodes: TreeDataItem[],
-                projectTree: TreeDataItem[],
-                projectRow: TreeDataItem[],
-                searchTerm: string
-            ): TreeDataItem[] => {
+            (s) => [s.projectTree, s.projectRow, s.searchTerm],
+            (projectTree: TreeDataItem[], projectRow: TreeDataItem[], searchTerm: string): TreeDataItem[] => {
                 if (!searchTerm) {
-                    return [...defaultTreeNodes, ...projectRow, ...projectTree]
+                    return [...projectRow, ...projectTree]
                 }
 
-                const allItems = [...defaultTreeNodes, ...projectRow, ...projectTree]
+                const allItems = [...projectRow, ...projectTree]
 
                 // TODO: remove from client side @marius :)
                 const fuse = new Fuse(allItems, {
path: frontend/src/layout/panel-layout/ProjectTree/projectTreeLogic.tsx
user: login: greptile-apps[bot]
body: style: Client-side search implementation marked as TODO for removal - should be addressed to improve performance
created_at: 2025-03-18T12:40:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -193,47 +186,82 @@ export function PanelLayoutNavBar({ children }: { children: React.ReactNode }):
 
                             <div className="border-b border-secondary h-px -mx-2" />
 
-                            <div className="pt-3">
+                            <div className="pt-1">
                                 <div className="flex justify-between items-center pt-1 pl-2 pr-0 pb-2">
                                     <span className="text-xs font-bold text-tertiary">Products</span>
                                 </div>
                                 {navbarItems.map((section, index) => (
                                     <ul key={index}>
-                                        {section.map((item) =>
-                                            item.featureFlag && !featureFlags[item.featureFlag] ? null : (
+                                        {section.map((item) => {
+                                            if (item.identifier === 'Activity') {
+                                                return null
+                                            }
path: frontend/src/layout/panel-layout/PanelLayoutNavBar.tsx
user: login: mariusandra
body: I wonder if there's a better way to filter it out... though we're probably going to change where we get the menu items soon anyway, so fine for me for now
created_at: 2025-03-19T08:49:49Z
author_association: COLLABORATOR
diff_hunk: @@ -30,26 +30,7 @@ export function PersonsTree({ mainRef }: { mainRef: React.RefObject<HTMLElement>
 
     return (
         <PanelLayoutPanel searchPlaceholder="Search for people">
-            <LemonTree
-                ref={treeRef}
-                contentRef={mainRef}
-                className="px-0 py-1"
-                data={treeData}
-                // onNodeClick={(node) => {
-                // if (node?.record?.path) {
-                //     setLastViewedId(node?.id || '')
-                //     if (!isLayoutPanelPinned) {
-                //         showLayoutPanel(false)
-                //     }
-                // }
-                // if (node?.id.startsWith('project-load-more/')) {
-                //     const path = node.id.split('/').slice(1).join('/')
-                //     if (path) {
-                //         loadFolder(path)
-                //     }
-                // }
-                // }}
path: frontend/src/layout/panel-layout/PersonsTree/PersonsTree.tsx
user: login: mariusandra
body: I'm still not the biggest fan of committing in commented out code, but 🙈 
created_at: 2025-03-19T08:50:19Z
author_association: COLLABORATOR

PR comments:
user: login: posthog-bot
created_at: 2025-03-18T13:00:01Z
body: ## 📸 UI snapshots have been updated

**2** snapshot changes in total. **0** added, **2** modified, **0** deleted:

- **`chromium`**: **0** added, **2** modified, **0** deleted ([diff for shard 1](https://github.com/PostHog/posthog/pull/30074/commits/11d6d3eabc74b320c1ea8a343d5b5352a530e82f))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30074/commits/b222cfa43db91725a2092ba26a48fc22ff3508dd).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30074/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T15:28:38Z
body: **Size Change:** +5 B (0%) 

**Total Size:** 9.74 MB



<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `frontend/dist/toolbar.js` | 9.74 MB | +5 B (0%) |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-03-18T19:59:38Z
body: ## 📸 UI snapshots have been updated

**2** snapshot changes in total. **0** added, **2** modified, **0** deleted:

- **`chromium`**: **0** added, **2** modified, **0** deleted ([diff for shard 2](https://github.com/PostHog/posthog/pull/30074/commits/f9bd84cfcb81cfcb125c89b39f409af647f2a690))
- `webkit`: **0** added, **0** modified, **0** deleted

Triggered by [this commit](https://github.com/PostHog/posthog/pull/30074/commits/2e1b65577d4db29914e53e025828b3a231aac415).

👉 **[Review this PR's diff of snapshots.](https://github.com/PostHog/posthog/pull/30074/files#:~:text=frontend/__snapshots__/)**
author_association: CONTRIBUTOR