mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-24 02:39:42 +02:00
Merge pull request #76 from multica-ai/feat/file-list-enhance
feat: add hidden files toggle and improve file tree UX
This commit is contained in:
@@ -15,7 +15,7 @@ import { RightPanel, RightPanelHeader, RightPanelContent } from './components/la
|
||||
import { FileTree } from './components/FileTree'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { useChatScroll } from './hooks/useChatScroll'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { ChevronDown, Eye, EyeOff } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function AppContent(): React.JSX.Element {
|
||||
@@ -46,6 +46,8 @@ function AppContent(): React.JSX.Element {
|
||||
// UI state
|
||||
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
|
||||
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
|
||||
const showHiddenFiles = useUIStore((s) => s.showHiddenFiles)
|
||||
const toggleShowHiddenFiles = useUIStore((s) => s.toggleShowHiddenFiles)
|
||||
|
||||
// Permission state - get the session ID that has a pending permission request
|
||||
const pendingPermission = usePermissionStore((s) => s.pendingRequests[0] ?? null)
|
||||
@@ -218,14 +220,31 @@ function AppContent(): React.JSX.Element {
|
||||
|
||||
{/* Right panel - file tree */}
|
||||
<RightPanel>
|
||||
<RightPanelHeader>
|
||||
<RightPanelHeader className="justify-between">
|
||||
<span className="text-sm font-medium">All files</span>
|
||||
<button
|
||||
onClick={toggleShowHiddenFiles}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-accent transition-colors',
|
||||
showHiddenFiles ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
title={showHiddenFiles ? 'Hide hidden files' : 'Show hidden files'}
|
||||
aria-label={showHiddenFiles ? 'Hide hidden files' : 'Show hidden files'}
|
||||
aria-pressed={showHiddenFiles}
|
||||
>
|
||||
{showHiddenFiles ? (
|
||||
<Eye className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<EyeOff className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</RightPanelHeader>
|
||||
<RightPanelContent className="p-0">
|
||||
{currentSession ? (
|
||||
<FileTree
|
||||
rootPath={currentSession.workingDirectory}
|
||||
directoryExists={currentSession.directoryExists}
|
||||
onCreateSession={handleCreateSession}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground p-4">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* FileTree component for displaying directory structure
|
||||
* With lazy loading and context menu support
|
||||
*/
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
FolderIcon,
|
||||
@@ -10,7 +10,12 @@ import {
|
||||
FileIcon,
|
||||
ImageIcon,
|
||||
LockIcon,
|
||||
FolderX
|
||||
FolderX,
|
||||
Terminal,
|
||||
Code2,
|
||||
Hammer,
|
||||
Copy,
|
||||
AppWindowMac
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
@@ -36,10 +41,13 @@ import {
|
||||
} from './FileIcons'
|
||||
import type { FileTreeNode, DetectedApp } from '../../../shared/electron-api'
|
||||
import { useFileChangeStore } from '../stores/fileChangeStore'
|
||||
import { useUIStore } from '../stores/uiStore'
|
||||
import { excludeHiddenFiles } from '../utils/fileTree'
|
||||
|
||||
interface FileTreeProps {
|
||||
rootPath: string
|
||||
directoryExists?: boolean
|
||||
onCreateSession?: (cwd: string) => void
|
||||
}
|
||||
|
||||
interface TreeItemProps {
|
||||
@@ -47,9 +55,10 @@ interface TreeItemProps {
|
||||
level: number
|
||||
expandedPaths: Set<string>
|
||||
onToggle: (path: string) => void
|
||||
childrenCache: Map<string, FileTreeNode[]>
|
||||
getChildren: (path: string) => FileTreeNode[] | undefined
|
||||
loadChildren: (path: string) => Promise<void>
|
||||
availableApps: DetectedApp[]
|
||||
onCreateSession?: (cwd: string) => void
|
||||
}
|
||||
|
||||
// Get the appropriate icon for a file/directory
|
||||
@@ -121,13 +130,14 @@ function TreeItem({
|
||||
level,
|
||||
expandedPaths,
|
||||
onToggle,
|
||||
childrenCache,
|
||||
getChildren,
|
||||
loadChildren,
|
||||
availableApps
|
||||
availableApps,
|
||||
onCreateSession
|
||||
}: TreeItemProps): React.JSX.Element {
|
||||
const isExpanded = expandedPaths.has(node.path)
|
||||
const isDirectory = node.type === 'directory'
|
||||
const children = childrenCache.get(node.path)
|
||||
const children = getChildren(node.path)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [hasError, setHasError] = useState(false)
|
||||
|
||||
@@ -214,29 +224,41 @@ function TreeItem({
|
||||
{finderApp && (
|
||||
<ContextMenuItem onClick={() => handleOpenWith('finder')}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>📁</span>
|
||||
<FolderOpenIcon className="h-4 w-4" />
|
||||
<span>Finder</span>
|
||||
</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">⌘O</span>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{/* Editors */}
|
||||
{editorApps.length > 0 && (
|
||||
{/* Apps (Multica + Editors) */}
|
||||
{(isDirectory && onCreateSession) || editorApps.length > 0 ? (
|
||||
<>
|
||||
<ContextMenuSeparator />
|
||||
{/* Multica - only for directories */}
|
||||
{isDirectory && onCreateSession && (
|
||||
<ContextMenuItem onClick={() => onCreateSession(node.path)}>
|
||||
<span className="flex items-center gap-2">
|
||||
<AppWindowMac className="h-4 w-4" />
|
||||
<span>Multica</span>
|
||||
</span>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
{/* Editors */}
|
||||
{editorApps.map((app) => (
|
||||
<ContextMenuItem key={app.id} onClick={() => handleOpenWith(app.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
{app.id === 'cursor' && <span>🔵</span>}
|
||||
{app.id === 'vscode' && <span>🔷</span>}
|
||||
{app.id === 'xcode' && <span>🔧</span>}
|
||||
{app.id === 'xcode' ? (
|
||||
<Hammer className="h-4 w-4" />
|
||||
) : (
|
||||
<Code2 className="h-4 w-4" />
|
||||
)}
|
||||
<span>{app.name}</span>
|
||||
</span>
|
||||
</ContextMenuItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Terminals */}
|
||||
{terminalApps.length > 0 && (
|
||||
@@ -245,9 +267,7 @@ function TreeItem({
|
||||
{terminalApps.map((app) => (
|
||||
<ContextMenuItem key={app.id} onClick={() => handleOpenWith(app.id)}>
|
||||
<span className="flex items-center gap-2">
|
||||
{app.id === 'ghostty' && <span>👻</span>}
|
||||
{app.id === 'iterm' && <span>📟</span>}
|
||||
{app.id === 'terminal' && <span>⬛</span>}
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span>{app.name}</span>
|
||||
</span>
|
||||
</ContextMenuItem>
|
||||
@@ -261,7 +281,7 @@ function TreeItem({
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={() => handleOpenWith('copy-path')}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>📋</span>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span>Copy path</span>
|
||||
</span>
|
||||
</ContextMenuItem>
|
||||
@@ -288,9 +308,10 @@ function TreeItem({
|
||||
level={level + 1}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={onToggle}
|
||||
childrenCache={childrenCache}
|
||||
getChildren={getChildren}
|
||||
loadChildren={loadChildren}
|
||||
availableApps={availableApps}
|
||||
onCreateSession={onCreateSession}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -312,14 +333,34 @@ function DirectoryNotFound(): React.JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
// Filter out hidden files (starting with .)
|
||||
const filterHidden = (nodes: FileTreeNode[]): FileTreeNode[] =>
|
||||
nodes.filter((n) => !n.name.startsWith('.'))
|
||||
export function FileTree({
|
||||
rootPath,
|
||||
directoryExists = true,
|
||||
onCreateSession
|
||||
}: FileTreeProps): React.JSX.Element {
|
||||
// Hidden files toggle state (controlled by parent via uiStore)
|
||||
const showHiddenFiles = useUIStore((s) => s.showHiddenFiles)
|
||||
|
||||
export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): React.JSX.Element {
|
||||
const [rootChildren, setRootChildren] = useState<FileTreeNode[]>([])
|
||||
// Store raw (unfiltered) data
|
||||
const [rawRootChildren, setRawRootChildren] = useState<FileTreeNode[]>([])
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set())
|
||||
const [childrenCache, setChildrenCache] = useState<Map<string, FileTreeNode[]>>(new Map())
|
||||
const [rawChildrenCache, setRawChildrenCache] = useState<Map<string, FileTreeNode[]>>(new Map())
|
||||
|
||||
// Compute filtered root children based on showHiddenFiles preference
|
||||
const rootChildren = useMemo(
|
||||
() => (showHiddenFiles ? rawRootChildren : excludeHiddenFiles(rawRootChildren)),
|
||||
[rawRootChildren, showHiddenFiles]
|
||||
)
|
||||
|
||||
// Helper to get filtered children for a given path (filters on demand)
|
||||
const getFilteredChildren = useCallback(
|
||||
(path: string): FileTreeNode[] | undefined => {
|
||||
const children = rawChildrenCache.get(path)
|
||||
if (!children) return undefined
|
||||
return showHiddenFiles ? children : excludeHiddenFiles(children)
|
||||
},
|
||||
[rawChildrenCache, showHiddenFiles]
|
||||
)
|
||||
const [availableApps, setAvailableApps] = useState<DetectedApp[]>([])
|
||||
// Initialize loading state based on directoryExists
|
||||
const [isLoading, setIsLoading] = useState(directoryExists)
|
||||
@@ -342,7 +383,7 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
window.electronAPI.listDirectory(rootPath),
|
||||
window.electronAPI.detectApps()
|
||||
])
|
||||
setRootChildren(filterHidden(children))
|
||||
setRawRootChildren(children)
|
||||
setAvailableApps(apps)
|
||||
} catch (error) {
|
||||
console.error('Failed to load file tree:', error)
|
||||
@@ -377,14 +418,13 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
// Re-fetch root directory
|
||||
console.log('[FileTree] Fetching root directory:', rootPath)
|
||||
const children = await window.electronAPI.listDirectory(rootPath)
|
||||
const filtered = filterHidden(children)
|
||||
console.log(
|
||||
'[FileTree] Root directory result:',
|
||||
filtered.length,
|
||||
children.length,
|
||||
'items',
|
||||
filtered.map((c) => c.name)
|
||||
children.map((c) => c.name)
|
||||
)
|
||||
setRootChildren(filtered)
|
||||
setRawRootChildren(children)
|
||||
|
||||
// Re-fetch all expanded directories to update them
|
||||
const currentExpanded = Array.from(expandedPathsRef.current)
|
||||
@@ -395,7 +435,7 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
currentExpanded.map(async (path) => {
|
||||
try {
|
||||
const dirChildren = await window.electronAPI.listDirectory(path)
|
||||
return { path, children: filterHidden(dirChildren) }
|
||||
return { path, children: dirChildren }
|
||||
} catch {
|
||||
// Directory may no longer exist
|
||||
return { path, children: [] }
|
||||
@@ -405,7 +445,7 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
for (const { path, children: dirChildren } of results) {
|
||||
newCache.set(path, dirChildren)
|
||||
}
|
||||
setChildrenCache(newCache)
|
||||
setRawChildrenCache(newCache)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FileTree] Failed to refresh file tree:', error)
|
||||
@@ -430,11 +470,10 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
const loadChildren = useCallback(async (path: string) => {
|
||||
// Fetch children first, then check cache in the setter to avoid stale closure
|
||||
const children = await window.electronAPI.listDirectory(path)
|
||||
const filtered = filterHidden(children)
|
||||
setChildrenCache((prev) => {
|
||||
setRawChildrenCache((prev) => {
|
||||
// Skip if already cached (handles race conditions)
|
||||
if (prev.has(path)) return prev
|
||||
return new Map(prev).set(path, filtered)
|
||||
return new Map(prev).set(path, children)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -468,9 +507,10 @@ export function FileTree({ rootPath, directoryExists = true }: FileTreeProps): R
|
||||
level={0}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={handleToggle}
|
||||
childrenCache={childrenCache}
|
||||
getChildren={getFilteredChildren}
|
||||
loadChildren={loadChildren}
|
||||
availableApps={availableApps}
|
||||
onCreateSession={onCreateSession}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,10 @@ interface UIStore {
|
||||
toggleRightPanel: () => void
|
||||
setRightPanelOpen: (open: boolean) => void
|
||||
setRightPanelWidth: (width: number) => void
|
||||
|
||||
// Hidden files visibility
|
||||
showHiddenFiles: boolean
|
||||
toggleShowHiddenFiles: () => void
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStore>()(
|
||||
@@ -49,7 +53,11 @@ export const useUIStore = create<UIStore>()(
|
||||
setRightPanelWidth: (width) =>
|
||||
set({
|
||||
rightPanelWidth: Math.max(RIGHT_PANEL_MIN_WIDTH, Math.min(RIGHT_PANEL_MAX_WIDTH, width))
|
||||
})
|
||||
}),
|
||||
|
||||
// Hidden files - default hidden
|
||||
showHiddenFiles: false,
|
||||
toggleShowHiddenFiles: () => set((state) => ({ showHiddenFiles: !state.showHiddenFiles }))
|
||||
}),
|
||||
{
|
||||
name: 'ui-state' // localStorage key
|
||||
|
||||
10
src/renderer/src/utils/fileTree.ts
Normal file
10
src/renderer/src/utils/fileTree.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Utility functions for file tree operations
|
||||
*/
|
||||
import type { FileTreeNode } from '../../../shared/electron-api'
|
||||
|
||||
/**
|
||||
* Exclude hidden files (those starting with '.') from the list
|
||||
*/
|
||||
export const excludeHiddenFiles = (nodes: FileTreeNode[]): FileTreeNode[] =>
|
||||
nodes.filter((n) => !n.name.startsWith('.'))
|
||||
108
tests/unit/renderer/components/FileTree.test.ts
Normal file
108
tests/unit/renderer/components/FileTree.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Tests for FileTree helper functions
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { excludeHiddenFiles } from '../../../../src/renderer/src/utils/fileTree'
|
||||
import type { FileTreeNode } from '../../../../src/shared/electron-api'
|
||||
|
||||
describe('excludeHiddenFiles', () => {
|
||||
const createNode = (name: string, type: 'file' | 'directory' = 'file'): FileTreeNode => ({
|
||||
name,
|
||||
path: `/test/${name}`,
|
||||
type,
|
||||
extension: type === 'file' ? name.split('.').pop() : undefined
|
||||
})
|
||||
|
||||
it('returns an empty array when given an empty array', () => {
|
||||
const result = excludeHiddenFiles([])
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('removes files starting with a dot', () => {
|
||||
const nodes = [createNode('.gitignore'), createNode('README.md'), createNode('.env')]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe('README.md')
|
||||
})
|
||||
|
||||
it('removes directories starting with a dot', () => {
|
||||
const nodes = [
|
||||
createNode('.git', 'directory'),
|
||||
createNode('src', 'directory'),
|
||||
createNode('.vscode', 'directory'),
|
||||
createNode('node_modules', 'directory')
|
||||
]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((n) => n.name)).toEqual(['src', 'node_modules'])
|
||||
})
|
||||
|
||||
it('keeps all nodes when none are hidden', () => {
|
||||
const nodes = [
|
||||
createNode('index.ts'),
|
||||
createNode('package.json'),
|
||||
createNode('src', 'directory')
|
||||
]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result).toEqual(nodes)
|
||||
})
|
||||
|
||||
it('removes all nodes when all are hidden', () => {
|
||||
const nodes = [createNode('.gitignore'), createNode('.env'), createNode('.git', 'directory')]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles mixed hidden files and directories', () => {
|
||||
const nodes = [
|
||||
createNode('.git', 'directory'),
|
||||
createNode('.gitignore'),
|
||||
createNode('src', 'directory'),
|
||||
createNode('README.md'),
|
||||
createNode('.env'),
|
||||
createNode('package.json'),
|
||||
createNode('.vscode', 'directory')
|
||||
]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result.map((n) => n.name)).toEqual(['src', 'README.md', 'package.json'])
|
||||
})
|
||||
|
||||
it('does not modify the original array', () => {
|
||||
const nodes = [createNode('.gitignore'), createNode('README.md')]
|
||||
const originalLength = nodes.length
|
||||
|
||||
excludeHiddenFiles(nodes)
|
||||
|
||||
expect(nodes).toHaveLength(originalLength)
|
||||
})
|
||||
|
||||
it('handles files with dots in the middle of the name', () => {
|
||||
const nodes = [
|
||||
createNode('file.test.ts'),
|
||||
createNode('component.spec.tsx'),
|
||||
createNode('.hidden.file'),
|
||||
createNode('normal.config.js')
|
||||
]
|
||||
|
||||
const result = excludeHiddenFiles(nodes)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result.map((n) => n.name)).toEqual([
|
||||
'file.test.ts',
|
||||
'component.spec.tsx',
|
||||
'normal.config.js'
|
||||
])
|
||||
})
|
||||
})
|
||||
102
tests/unit/renderer/stores/uiStore.test.ts
Normal file
102
tests/unit/renderer/stores/uiStore.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Tests for uiStore - specifically showHiddenFiles state
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
useUIStore,
|
||||
SIDEBAR_MIN_WIDTH,
|
||||
SIDEBAR_MAX_WIDTH,
|
||||
RIGHT_PANEL_MIN_WIDTH,
|
||||
RIGHT_PANEL_MAX_WIDTH
|
||||
} from '../../../../src/renderer/src/stores/uiStore'
|
||||
|
||||
describe('uiStore', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to default state
|
||||
useUIStore.setState({
|
||||
sidebarOpen: true,
|
||||
sidebarWidth: 256,
|
||||
rightPanelOpen: true,
|
||||
rightPanelWidth: 320,
|
||||
showHiddenFiles: false
|
||||
})
|
||||
})
|
||||
|
||||
describe('showHiddenFiles', () => {
|
||||
it('defaults to false (hidden files are not shown)', () => {
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(false)
|
||||
})
|
||||
|
||||
it('toggles showHiddenFiles from false to true', () => {
|
||||
useUIStore.getState().toggleShowHiddenFiles()
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(true)
|
||||
})
|
||||
|
||||
it('toggles showHiddenFiles from true to false', () => {
|
||||
useUIStore.setState({ showHiddenFiles: true })
|
||||
useUIStore.getState().toggleShowHiddenFiles()
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(false)
|
||||
})
|
||||
|
||||
it('toggles multiple times correctly', () => {
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(false)
|
||||
|
||||
useUIStore.getState().toggleShowHiddenFiles()
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(true)
|
||||
|
||||
useUIStore.getState().toggleShowHiddenFiles()
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(false)
|
||||
|
||||
useUIStore.getState().toggleShowHiddenFiles()
|
||||
expect(useUIStore.getState().showHiddenFiles).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sidebar state', () => {
|
||||
it('toggles sidebar open state', () => {
|
||||
expect(useUIStore.getState().sidebarOpen).toBe(true)
|
||||
useUIStore.getState().toggleSidebar()
|
||||
expect(useUIStore.getState().sidebarOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('sets sidebar open state directly', () => {
|
||||
useUIStore.getState().setSidebarOpen(false)
|
||||
expect(useUIStore.getState().sidebarOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('clamps sidebar width to min/max constraints', () => {
|
||||
useUIStore.getState().setSidebarWidth(100) // below min
|
||||
expect(useUIStore.getState().sidebarWidth).toBe(SIDEBAR_MIN_WIDTH)
|
||||
|
||||
useUIStore.getState().setSidebarWidth(1000) // above max
|
||||
expect(useUIStore.getState().sidebarWidth).toBe(SIDEBAR_MAX_WIDTH)
|
||||
|
||||
useUIStore.getState().setSidebarWidth(300) // within range
|
||||
expect(useUIStore.getState().sidebarWidth).toBe(300)
|
||||
})
|
||||
})
|
||||
|
||||
describe('right panel state', () => {
|
||||
it('toggles right panel open state', () => {
|
||||
expect(useUIStore.getState().rightPanelOpen).toBe(true)
|
||||
useUIStore.getState().toggleRightPanel()
|
||||
expect(useUIStore.getState().rightPanelOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('sets right panel open state directly', () => {
|
||||
useUIStore.getState().setRightPanelOpen(false)
|
||||
expect(useUIStore.getState().rightPanelOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('clamps right panel width to min/max constraints', () => {
|
||||
useUIStore.getState().setRightPanelWidth(100) // below min
|
||||
expect(useUIStore.getState().rightPanelWidth).toBe(RIGHT_PANEL_MIN_WIDTH)
|
||||
|
||||
useUIStore.getState().setRightPanelWidth(1000) // above max
|
||||
expect(useUIStore.getState().rightPanelWidth).toBe(RIGHT_PANEL_MAX_WIDTH)
|
||||
|
||||
useUIStore.getState().setRightPanelWidth(350) // within range
|
||||
expect(useUIStore.getState().rightPanelWidth).toBe(350)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user