mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-12 11:52:50 +02:00
Implement Structure view for dashboard with folder hierarchy display
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user