Implement Structure view for dashboard with folder hierarchy display

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-08 20:11:43 +00:00
parent f3df0f6856
commit e4147eed09
7 changed files with 212 additions and 8 deletions
+1
View File
@@ -222,6 +222,7 @@
"dashboard.header.viewSwitch.toGrid": "Change to grid",
"dashboard.header.viewSwitch.toList": "Change to list",
"dashboard.header.viewSwitch.toStructure": "Change to structure",
"dashboard.layout.sponsor.support.msg": "Support Front Matter",
"dashboard.layout.sponsor.review.label": "Review",
@@ -17,6 +17,8 @@ export const List: React.FunctionComponent<IListProps> = ({
className = `grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-5 gap-4`;
} else if (view === DashboardViewType.List) {
className = `-mx-4`;
} else if (view === DashboardViewType.Structure) {
className = `structure-view`;
}
return (
@@ -9,6 +9,7 @@ import { GroupOption } from '../../constants/GroupOption';
import { GroupingSelector, PageAtom, PagedItems, ViewSelector } from '../../state';
import { Item } from './Item';
import { List } from './List';
import { StructureView } from './StructureView';
import usePagination from '../../hooks/usePagination';
import { LocalizationKey, localize } from '../../../localization';
import { PinnedItemsAtom } from '../../state/atom/PinnedItems';
@@ -155,6 +156,11 @@ export const Overview: React.FunctionComponent<IOverviewProps> = ({
);
}
// Handle Structure view first - it overrides all other display modes
if (view === DashboardViewType.Structure) {
return <StructureView pages={pages} />;
}
if (grouping !== GroupOption.none) {
return (
<>
@@ -0,0 +1,180 @@
import { Disclosure } from '@headlessui/react';
import { ChevronRightIcon, FolderIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { useMemo } from 'react';
import { Page } from '../../models';
import { Item } from './Item';
export interface IStructureViewProps {
pages: Page[];
}
interface FolderNode {
name: string;
path: string;
children: FolderNode[];
pages: Page[];
}
export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
pages
}: React.PropsWithChildren<IStructureViewProps>) => {
const folderTree = useMemo(() => {
const root: FolderNode = {
name: '',
path: '',
children: [],
pages: []
};
const folderMap = new Map<string, FolderNode>();
folderMap.set('', root);
// First pass: create all folder nodes
pages.forEach(page => {
if (!page.fmFolder) {
return;
}
const folderPath = page.fmFolder;
const parts = folderPath.split('/').filter(part => part.length > 0);
let currentPath = '';
let currentNode = root;
parts.forEach(part => {
const fullPath = currentPath ? `${currentPath}/${part}` : part;
if (!folderMap.has(fullPath)) {
const newNode: FolderNode = {
name: part,
path: fullPath,
children: [],
pages: []
};
folderMap.set(fullPath, newNode);
currentNode.children.push(newNode);
}
const nextNode = folderMap.get(fullPath);
if (nextNode) {
currentNode = nextNode;
}
currentPath = fullPath;
});
});
// Second pass: assign pages to their folders
pages.forEach(page => {
if (!page.fmFolder) {
root.pages.push(page);
} else {
const folderNode = folderMap.get(page.fmFolder);
if (folderNode) {
folderNode.pages.push(page);
}
}
});
// Sort folders and pages
const sortNode = (node: FolderNode) => {
node.children.sort((a, b) => a.name.localeCompare(b.name));
node.pages.sort((a, b) => a.title.localeCompare(b.title));
node.children.forEach(sortNode);
};
sortNode(root);
return root;
}, [pages]);
const renderFolderNode = (node: FolderNode, depth = 0): React.ReactNode => {
const hasContent = node.pages.length > 0 || node.children.length > 0;
if (!hasContent) {
return null;
}
const isRoot = depth === 0;
const paddingLeft = depth * 20;
if (isRoot) {
// For root node, render children and pages directly
return (
<div>
{/* Root level pages */}
{node.pages.length > 0 && (
<div className="mb-6">
<h3 className="text-lg font-medium mb-3 text-[var(--vscode-editor-foreground)]">
Root Files
</h3>
<ul className="space-y-2">
{node.pages.map((page, idx) => (
<li key={`${page.slug}-${idx}`}>
<Item {...page} />
</li>
))}
</ul>
</div>
)}
{/* Root level folders */}
{node.children.map(child => renderFolderNode(child, depth + 1))}
</div>
);
}
return (
<div key={node.path} className="mb-4">
<Disclosure defaultOpen={depth <= 1}>
{({ open }) => (
<>
<Disclosure.Button
className="flex items-center w-full text-left"
style={{ paddingLeft: `${paddingLeft}px` }}
>
<ChevronRightIcon
className={`w-4 h-4 mr-2 transform transition-transform ${
open ? 'rotate-90' : ''
}`}
/>
<FolderIcon className="w-4 h-4 mr-2 text-[var(--vscode-symbolIcon-folderForeground)]" />
<span className="font-medium text-[var(--vscode-editor-foreground)]">
{node.name}
{node.pages.length > 0 && (
<span className="ml-2 text-sm text-[var(--vscode-descriptionForeground)]">
({node.pages.length} {node.pages.length === 1 ? 'file' : 'files'})
</span>
)}
</span>
</Disclosure.Button>
<Disclosure.Panel className="mt-2">
{/* Pages in this folder */}
{node.pages.length > 0 && (
<ul className="space-y-1 mb-3">
{node.pages.map((page, idx) => (
<li key={`${page.slug}-${idx}`} style={{ paddingLeft: `${paddingLeft + 20}px` }}>
<Item {...page} />
</li>
))}
</ul>
)}
{/* Child folders */}
{node.children.map(child => renderFolderNode(child, depth + 1))}
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
);
};
return (
<div className="structure-view">
{renderFolderNode(folderTree)}
</div>
);
};
@@ -1,7 +1,7 @@
import * as React from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { ViewAtom, SettingsSelector } from '../../state';
import { Bars4Icon, Squares2X2Icon } from '@heroicons/react/24/solid';
import { Bars4Icon, Squares2X2Icon, FolderIcon } from '@heroicons/react/24/solid';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { DashboardViewType } from '../../models';
@@ -16,9 +16,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
const [view, setView] = useRecoilState(ViewAtom);
const settings = useRecoilValue(SettingsSelector);
const toggleView = () => {
const newView =
view === DashboardViewType.Grid ? DashboardViewType.List : DashboardViewType.Grid;
const handleViewChange = (newView: DashboardViewType) => {
setView(newView);
Messenger.send(DashboardMessage.setPageViewType, newView);
};
@@ -36,7 +34,7 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
}`}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)}
type={`button`}
onClick={toggleView}
onClick={() => handleViewChange(DashboardViewType.Grid)}
>
<Squares2X2Icon className={`w-4 h-4`} />
<span className={`sr-only`}>
@@ -44,17 +42,29 @@ export const ViewSwitch: React.FunctionComponent<IViewSwitchProps> = (
</span>
</button>
<button
className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.List ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
className={`flex items-center px-2 py-1 ${view === DashboardViewType.List ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
}`}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
type={`button`}
onClick={toggleView}
onClick={() => handleViewChange(DashboardViewType.List)}
>
<Bars4Icon className={`w-4 h-4`} />
<span className={`sr-only`}>
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToList)}
</span>
</button>
<button
className={`flex items-center px-2 py-1 rounded-r-sm ${view === DashboardViewType.Structure ? `bg-[var(--frontmatter-button-background)] text-[var(--vscode-button-foreground)]` : 'text-[var(--vscode-button-secondaryForeground)] hover:bg-[var(--vscode-button-secondaryHoverBackground)]'
}`}
title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
type={`button`}
onClick={() => handleViewChange(DashboardViewType.Structure)}
>
<FolderIcon className={`w-4 h-4`} />
<span className={`sr-only`}>
{l10n.t(LocalizationKey.dashboardHeaderViewSwitchToStructure)}
</span>
</button>
</div>
);
};
@@ -1,4 +1,5 @@
export enum DashboardViewType {
Grid = 1,
List
List,
Structure
}
+4
View File
@@ -727,6 +727,10 @@ export enum LocalizationKey {
* Change to list
*/
dashboardHeaderViewSwitchToList = 'dashboard.header.viewSwitch.toList',
/**
* Change to structure
*/
dashboardHeaderViewSwitchToStructure = 'dashboard.header.viewSwitch.toStructure',
/**
* Support Front Matter
*/