Skip to content

Commit

Permalink
feat: new module tree layout
Browse files Browse the repository at this point in the history
  • Loading branch information
wesbillman committed Jan 6, 2025
1 parent 0af296f commit 4bfe796
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 50 deletions.
170 changes: 120 additions & 50 deletions frontend/console/src/features/modules/ModulesTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import type { DeclInfo, ModuleTreeItem } from './module.utils'
import {
addModuleToLocalStorageIfMissing,
collapseAllModulesInLocalStorage,
declIcon,
declSumTypeIsExported,
declTypeName,
declUrlFromInfo,
Expand All @@ -18,53 +17,21 @@ import {
listExpandedModulesFromLocalStorage,
setHideUnexportedFromLocalStorage,
toggleModuleExpansionInLocalStorage,
getExpandedDeclTypesFromLocalStorage,
setExpandedDeclTypesInLocalStorage,
declIcon,
} from './module.utils'
import { declTypeMultiselectOpts } from './schema/schema.utils'

const DeclNode = ({ decl, href, isSelected }: { decl: DeclInfo; href: string; isSelected: boolean }) => {
const declRef = useRef<HTMLDivElement>(null)

// Scroll to the selected decl on page load
useEffect(() => {
if (isSelected && declRef.current) {
const { top } = declRef.current.getBoundingClientRect()
const { innerHeight } = window
if (top < 64 || top > innerHeight) {
declRef.current.scrollIntoView({ behavior: 'smooth' })
}
}
}, [isSelected])

const Icon = useMemo(() => declIcon(decl.declType, decl.value), [decl])
const declType = useMemo(() => declTypeName(decl.declType, decl.value), [decl])
return (
<li className='my-1'>
<Link id={`decl-${decl.value.name}`} to={href}>
<div
ref={declRef}
className={classNames(
isSelected ? 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600' : 'hover:bg-gray-200 hover:dark:bg-gray-700',
declSumTypeIsExported(decl.value) ? '' : 'text-gray-400 dark:text-gray-500',
'group flex items-center gap-x-2 pl-4 pr-2 text-sm font-light leading-6 w-full cursor-pointer scroll-mt-10 rounded-md',
)}
>
<span title={declType}>
<Icon aria-hidden='true' className='size-4 shrink-0 ml-3' />
</span>
{decl.value.name}
</div>
</Link>
</li>
)
}

const ModuleSection = ({
module,
isExpanded,
toggleExpansion,
selectedDeclTypes,
hideUnexported,
}: { module: ModuleTreeItem; isExpanded: boolean; toggleExpansion: (m: string) => void; selectedDeclTypes: MultiselectOpt[]; hideUnexported: boolean }) => {
expandedDeclTypes,
toggleDeclType,
}: { module: ModuleTreeItem; isExpanded: boolean; toggleExpansion: (m: string) => void; selectedDeclTypes: MultiselectOpt[]; hideUnexported: boolean; expandedDeclTypes: string[]; toggleDeclType: (moduleName: string, declType: string) => void }) => {
const { moduleName, declName } = useParams()
const isSelected = useMemo(() => moduleName === module.name, [moduleName, module.name])
const moduleRef = useRef<HTMLDivElement>(null)
Expand All @@ -88,33 +55,116 @@ const ModuleSection = ({
[module.decls, selectedDeclTypes, hideUnexported, isSelected, declName],
)

// Group declarations by their type
const groupedDecls = useMemo(() => {
const groups: Record<string, DeclInfo[]> = {}
filteredDecls.forEach((decl) => {
const type = declTypeName(decl.declType, decl.value)
if (!groups[type]) {
groups[type] = []
}
groups[type].push(decl)
})
return groups
}, [filteredDecls])

const [expandedGroups, setExpandedGroups] = useState<string[]>(() => {
if (isSelected && declName) {
const selectedDeclType = module.decls.find(d => d.value.name === declName)
if (selectedDeclType) {
return [declTypeName(selectedDeclType.declType, selectedDeclType.value)]
}
}
return []
})

useEffect(() => {
if (isSelected && declName) {
const selectedDeclType = module.decls.find(d => d.value.name === declName)
if (selectedDeclType) {
const typeName = declTypeName(selectedDeclType.declType, selectedDeclType.value)
if (!expandedGroups.includes(typeName)) {
setExpandedGroups(prev => [...prev, typeName])
}
}
}
}, [isSelected, declName, module.decls, expandedGroups])

return (
<li key={module.name} id={`module-tree-module-${module.name}`} className='mb-2'>
<div
ref={moduleRef}
id={`module-${module.name}-tree-group`}
className={classNames(
isSelected ? 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600' : 'hover:bg-gray-200 hover:dark:bg-gray-700',
'group flex w-full modules-center gap-x-2 space-y-1 text-left text-sm font-medium cursor-pointer leading-6 rounded-md',
'group flex w-full items-center gap-x-2 text-left text-sm font-medium cursor-pointer leading-6 rounded-md px-2',
)}
onClick={() => toggleExpansion(module.name)}
>
<span title='module'>
<CodeFolderIcon aria-hidden='true' className='size-4 my-1 ml-3 shrink-0' />
</span>
<ArrowRight01Icon
aria-hidden='true'
className={`h-4 w-4 shrink-0 ${isExpanded ? 'rotate-90 text-gray-500' : ''}`}
/>
{module.name}
<Link to={`/modules/${module.name}`} onClick={(e) => e.stopPropagation()}>
<CircleArrowRight02Icon id={`module-${module.name}-view-icon`} className='size-4 shrink-0 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600' />
</Link>
{filteredDecls.length === 0 || (
<ArrowRight01Icon aria-hidden='true' className={`ml-auto mr-2 h-4 w-4 shrink-0 ${isExpanded ? 'rotate-90 text-gray-500' : ''}`} />
)}
</div>
{isExpanded && (
<ul>
{filteredDecls.map((d, i) => (
<DeclNode key={i} decl={d} href={declUrlFromInfo(module.name, d)} isSelected={isSelected && declName === d.value.name} />
))}
<ul className="pl-4">
{Object.entries(groupedDecls).map(([groupName, decls]) => {
const declTypeKey = `${module.name}:${groupName}`
const isGroupExpanded = expandedDeclTypes.includes(declTypeKey)
const DeclTypeIcon = declIcon(decls[0].declType, decls[0].value)

return (
<li key={groupName} className="my-1">
<div
className={classNames(
'group flex w-full items-center gap-x-2 text-left text-sm font-medium cursor-pointer leading-6 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700',
)}
onClick={() => toggleDeclType(module.name, groupName)}
>
<ArrowRight01Icon
aria-hidden='true'
className={`ml-1 h-4 w-4 shrink-0 ${isGroupExpanded ? 'rotate-90 text-gray-500' : ''}`}
/>
<span title={groupName}>
<DeclTypeIcon aria-hidden='true' className='size-4 shrink-0' />
</span>
{groupName}
<span className="text-xs text-gray-500 ml-1">({decls.length})</span>
</div>
{isGroupExpanded && (
<ul>
{decls.map((d, i) => {
const DeclIcon = declIcon(d.declType, d.value)
return (
<li key={i} className='my-1'>
<Link id={`decl-${d.value.name}`} to={declUrlFromInfo(module.name, d)}>
<div
className={classNames(
isSelected && declName === d.value.name
? 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-gray-600'
: 'hover:bg-gray-200 hover:dark:bg-gray-700',
declSumTypeIsExported(d.value) ? '' : 'text-gray-400 dark:text-gray-500',
'group flex items-center gap-x-2 pl-7 pr-2 text-sm font-light leading-6 w-full cursor-pointer scroll-mt-10 rounded-md',
)}
>
<span title={d.value.name}>
<DeclIcon aria-hidden='true' className='size-4 shrink-0' />
</span>
{d.value.name}
</div>
</Link>
</li>
)
})}
</ul>
)}
</li>
)
})}
</ul>
)}
</li>
Expand Down Expand Up @@ -160,18 +210,36 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
}

function collapseAll() {
// Collapse all modules
collapseAllModulesInLocalStorage()
if (moduleName && declName) {
addModuleToLocalStorageIfMissing(moduleName)
}
setExpandedModules(listExpandedModulesFromLocalStorage())

// Collapse all decl types
setExpandedDeclTypes([])
setExpandedDeclTypesInLocalStorage([])
}

function setHideUnexportedState(val: boolean) {
setHideUnexportedFromLocalStorage(val)
setHideUnexported(val)
}

const [expandedDeclTypes, setExpandedDeclTypes] = useState<string[]>(() => {
return getExpandedDeclTypesFromLocalStorage()
})

function toggleDeclType(moduleName: string, declType: string) {
const key = `${moduleName}:${declType}`
const newExpanded = expandedDeclTypes.includes(key)
? expandedDeclTypes.filter(t => t !== key)
: [...expandedDeclTypes, key]
setExpandedDeclTypes(newExpanded)
setExpandedDeclTypesInLocalStorage(newExpanded)
}

modules.sort((m1, m2) => Number(m1.isBuiltin) - Number(m2.isBuiltin))

return (
Expand Down Expand Up @@ -201,6 +269,8 @@ export const ModulesTree = ({ modules }: { modules: ModuleTreeItem[] }) => {
toggleExpansion={toggle}
selectedDeclTypes={selectedDeclTypes}
hideUnexported={hideUnexported}
expandedDeclTypes={expandedDeclTypes}
toggleDeclType={toggleDeclType}
/>
))}
</ul>
Expand Down
11 changes: 11 additions & 0 deletions frontend/console/src/features/modules/module.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,14 @@ const treeWidthStorageKey = 'tree_w'
export const getTreeWidthFromLS = () => Number(localStorage.getItem(treeWidthStorageKey)) || 300

export const setTreeWidthInLS = (newWidth: number) => localStorage.setItem(treeWidthStorageKey, `${newWidth}`)

const EXPANDED_DECL_TYPES_KEY = 'expanded-decl-types'

export function getExpandedDeclTypesFromLocalStorage(): string[] {
const stored = localStorage.getItem(EXPANDED_DECL_TYPES_KEY)
return stored ? JSON.parse(stored) : []
}

export function setExpandedDeclTypesInLocalStorage(types: string[]) {
localStorage.setItem(EXPANDED_DECL_TYPES_KEY, JSON.stringify(types))
}

0 comments on commit 4bfe796

Please sign in to comment.