From e4147eed09b54badb4391119843a4dd305bbe9d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:11:43 +0000 Subject: [PATCH 2/8] Implement Structure view for dashboard with folder hierarchy display Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com> --- l10n/bundle.l10n.json | 1 + .../components/Contents/List.tsx | 2 + .../components/Contents/Overview.tsx | 6 + .../components/Contents/StructureView.tsx | 180 ++++++++++++++++++ .../components/Header/ViewSwitch.tsx | 24 ++- .../models/DashboardViewType.ts | 3 +- src/localization/localization.enum.ts | 4 + 7 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 src/dashboardWebView/components/Contents/StructureView.tsx diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index bd58e0fd..b16ad718 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -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", diff --git a/src/dashboardWebView/components/Contents/List.tsx b/src/dashboardWebView/components/Contents/List.tsx index 1808b443..de011dc7 100644 --- a/src/dashboardWebView/components/Contents/List.tsx +++ b/src/dashboardWebView/components/Contents/List.tsx @@ -17,6 +17,8 @@ export const List: React.FunctionComponent = ({ 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 ( diff --git a/src/dashboardWebView/components/Contents/Overview.tsx b/src/dashboardWebView/components/Contents/Overview.tsx index ddeb8955..45dbedd1 100644 --- a/src/dashboardWebView/components/Contents/Overview.tsx +++ b/src/dashboardWebView/components/Contents/Overview.tsx @@ -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 = ({ ); } + // Handle Structure view first - it overrides all other display modes + if (view === DashboardViewType.Structure) { + return ; + } + if (grouping !== GroupOption.none) { return ( <> diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx new file mode 100644 index 00000000..0dedcf39 --- /dev/null +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -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 = ({ + pages +}: React.PropsWithChildren) => { + + const folderTree = useMemo(() => { + const root: FolderNode = { + name: '', + path: '', + children: [], + pages: [] + }; + + const folderMap = new Map(); + 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 ( +
+ {/* Root level pages */} + {node.pages.length > 0 && ( +
+

+ Root Files +

+
    + {node.pages.map((page, idx) => ( +
  • + +
  • + ))} +
+
+ )} + + {/* Root level folders */} + {node.children.map(child => renderFolderNode(child, depth + 1))} +
+ ); + } + + return ( +
+ + {({ open }) => ( + <> + + + + + {node.name} + {node.pages.length > 0 && ( + + ({node.pages.length} {node.pages.length === 1 ? 'file' : 'files'}) + + )} + + + + + {/* Pages in this folder */} + {node.pages.length > 0 && ( +
    + {node.pages.map((page, idx) => ( +
  • + +
  • + ))} +
+ )} + + {/* Child folders */} + {node.children.map(child => renderFolderNode(child, depth + 1))} +
+ + )} +
+
+ ); + }; + + return ( +
+ {renderFolderNode(folderTree)} +
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Header/ViewSwitch.tsx b/src/dashboardWebView/components/Header/ViewSwitch.tsx index eb125fae..6edf1e03 100644 --- a/src/dashboardWebView/components/Header/ViewSwitch.tsx +++ b/src/dashboardWebView/components/Header/ViewSwitch.tsx @@ -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 = ( 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 = ( }`} title={l10n.t(LocalizationKey.dashboardHeaderViewSwitchToGrid)} type={`button`} - onClick={toggleView} + onClick={() => handleViewChange(DashboardViewType.Grid)} > @@ -44,17 +42,29 @@ export const ViewSwitch: React.FunctionComponent = ( + ); }; diff --git a/src/dashboardWebView/models/DashboardViewType.ts b/src/dashboardWebView/models/DashboardViewType.ts index 43605dab..521c00d7 100644 --- a/src/dashboardWebView/models/DashboardViewType.ts +++ b/src/dashboardWebView/models/DashboardViewType.ts @@ -1,4 +1,5 @@ export enum DashboardViewType { Grid = 1, - List + List, + Structure } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 4845ee59..80ab834f 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -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 */ From 73e58c7b52011f500fbb04fbc8ddd4858f76e899 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:16:45 +0000 Subject: [PATCH 3/8] Add multi-language localization support for Structure view Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com> --- l10n/bundle.l10n.de.json | 1 + l10n/bundle.l10n.fr.json | 1 + l10n/bundle.l10n.ja.json | 1 + l10n/bundle.l10n.zh-cn.json | 1 + 4 files changed, 4 insertions(+) diff --git a/l10n/bundle.l10n.de.json b/l10n/bundle.l10n.de.json index 7b42c77e..5a2aa153 100644 --- a/l10n/bundle.l10n.de.json +++ b/l10n/bundle.l10n.de.json @@ -109,6 +109,7 @@ "dashboard.header.tabs.taxonomies": "Taxonomien", "dashboard.header.viewSwitch.toGrid": "Zur Rasteransicht wechseln", "dashboard.header.viewSwitch.toList": "Zur Listenansicht wechseln", + "dashboard.header.viewSwitch.toStructure": "Zur Strukturansicht wechseln", "dashboard.layout.sponsor.support.msg": "Unterstützen Sie Front Matter", "dashboard.layout.sponsor.review.label": "Bewerten", "dashboard.layout.sponsor.review.msg": "Bewerten Sie Front Matter", diff --git a/l10n/bundle.l10n.fr.json b/l10n/bundle.l10n.fr.json index 46e9acf7..12945e84 100644 --- a/l10n/bundle.l10n.fr.json +++ b/l10n/bundle.l10n.fr.json @@ -109,6 +109,7 @@ "dashboard.header.tabs.taxonomies": "Taxonomies", "dashboard.header.viewSwitch.toGrid": "Afficher en grille", "dashboard.header.viewSwitch.toList": "Afficher en liste", + "dashboard.header.viewSwitch.toStructure": "Afficher en structure", "dashboard.layout.sponsor.support.msg": "Soutenir Front Matter", "dashboard.layout.sponsor.review.label": "Donnez votre avis", "dashboard.layout.sponsor.review.msg": "Donnez votre avis sur Front Matter", diff --git a/l10n/bundle.l10n.ja.json b/l10n/bundle.l10n.ja.json index a931e9fe..17f2312b 100644 --- a/l10n/bundle.l10n.ja.json +++ b/l10n/bundle.l10n.ja.json @@ -214,6 +214,7 @@ "dashboard.header.viewSwitch.toGrid": "グリッド表示", "dashboard.header.viewSwitch.toList": "リスト表示", + "dashboard.header.viewSwitch.toStructure": "構造表示", "dashboard.layout.sponsor.support.msg": "Front Matterをサポートする", "dashboard.layout.sponsor.review.label": "評価する", diff --git a/l10n/bundle.l10n.zh-cn.json b/l10n/bundle.l10n.zh-cn.json index 81472430..0c87ecca 100644 --- a/l10n/bundle.l10n.zh-cn.json +++ b/l10n/bundle.l10n.zh-cn.json @@ -222,6 +222,7 @@ "dashboard.header.viewSwitch.toGrid": "切换到网格视图", "dashboard.header.viewSwitch.toList": "切换到列表视图", + "dashboard.header.viewSwitch.toStructure": "切换到结构视图", "dashboard.layout.sponsor.support.msg": "支持 Front Matter", "dashboard.layout.sponsor.review.label": "评价", From 65fc9f38ede58f3ad71a6f8ac8c3847f6d79c8a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:17:44 +0000 Subject: [PATCH 4/8] Fix Item rendering for Structure view type Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com> --- .../components/Contents/Item.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 74841042..49f93bc3 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -250,6 +250,49 @@ export const Item: React.FunctionComponent = ({ ); + } else if (view === DashboardViewType.Structure) { + return ( +
+
+ + + + + + +
+ {pageData.date && ( + + )} + + {draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" && ( + + )} + + +
+
+
+ ); } return null; From b62d1e8177a6a633bd49699079ea138b54ddfb59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:32:16 +0000 Subject: [PATCH 5/8] Fix folder hierarchy rendering in Structure view Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com> --- .../components/Contents/Item.tsx | 43 ---------- .../components/Contents/StructureItem.tsx | 79 +++++++++++++++++++ .../components/Contents/StructureView.tsx | 6 +- 3 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 src/dashboardWebView/components/Contents/StructureItem.tsx diff --git a/src/dashboardWebView/components/Contents/Item.tsx b/src/dashboardWebView/components/Contents/Item.tsx index 49f93bc3..74841042 100644 --- a/src/dashboardWebView/components/Contents/Item.tsx +++ b/src/dashboardWebView/components/Contents/Item.tsx @@ -250,49 +250,6 @@ export const Item: React.FunctionComponent = ({ ); - } else if (view === DashboardViewType.Structure) { - return ( -
-
- - - - - - -
- {pageData.date && ( - - )} - - {draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" && ( - - )} - - -
-
-
- ); } return null; diff --git a/src/dashboardWebView/components/Contents/StructureItem.tsx b/src/dashboardWebView/components/Contents/StructureItem.tsx new file mode 100644 index 00000000..3deb5225 --- /dev/null +++ b/src/dashboardWebView/components/Contents/StructureItem.tsx @@ -0,0 +1,79 @@ +import { useRecoilValue } from 'recoil'; +import { MarkdownIcon } from '../../../panelWebView/components/Icons/MarkdownIcon'; +import { Page } from '../../models/Page'; +import { SettingsSelector } from '../../state'; +import { DateField } from '../Common/DateField'; +import { ContentActions } from './ContentActions'; +import { useMemo } from 'react'; +import { Status } from './Status'; +import * as React from 'react'; +import * as l10n from '@vscode/l10n'; +import { LocalizationKey } from '../../../localization'; +import useCard from '../../hooks/useCard'; +import { ItemSelection } from '../Common/ItemSelection'; +import { openFile } from '../../utils'; +import useSelectedItems from '../../hooks/useSelectedItems'; +import { cn } from '../../../utils/cn'; + +export interface IStructureItemProps extends Page { } + +export const StructureItem: React.FunctionComponent = ({ + ...pageData +}: React.PropsWithChildren) => { + const { selectedFiles } = useSelectedItems(); + const settings = useRecoilValue(SettingsSelector); + const draftField = useMemo(() => settings?.draftField, [settings]); + const { escapedTitle } = useCard(pageData, settings?.dashboardState?.contents?.cardFields); + + const isSelected = useMemo(() => selectedFiles.includes(pageData.fmFilePath), [selectedFiles, pageData.fmFilePath]); + + const onOpenFile = React.useCallback(() => { + openFile(pageData.fmFilePath); + }, [pageData.fmFilePath]); + + return ( +
+
+ + + + + + +
+ {pageData.date && ( + + )} + + {draftField && draftField.name && typeof pageData[draftField.name] !== "undefined" && ( + + )} + + +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx index 0dedcf39..a429c292 100644 --- a/src/dashboardWebView/components/Contents/StructureView.tsx +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -3,7 +3,7 @@ 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'; +import { StructureItem } from './StructureItem'; export interface IStructureViewProps { pages: Page[]; @@ -112,7 +112,7 @@ export const StructureView: React.FunctionComponent = ({
    {node.pages.map((page, idx) => (
  • - +
  • ))}
@@ -156,7 +156,7 @@ export const StructureView: React.FunctionComponent = ({
    {node.pages.map((page, idx) => (
  • - +
  • ))}
From d4c5ca1c188ee363dd212f9ef4ca1fa3cda9eea5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:56:45 +0000 Subject: [PATCH 6/8] Fix folder path normalization in Structure view for proper nesting Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com> --- .../components/Contents/StructureView.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx index a429c292..819f22a8 100644 --- a/src/dashboardWebView/components/Contents/StructureView.tsx +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -38,7 +38,9 @@ export const StructureView: React.FunctionComponent = ({ } const folderPath = page.fmFolder; - const parts = folderPath.split('/').filter(part => part.length > 0); + // Normalize path separators and remove leading/trailing slashes + const normalizedPath = folderPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + const parts = normalizedPath.split('/').filter(part => part.length > 0); let currentPath = ''; let currentNode = root; @@ -70,9 +72,14 @@ export const StructureView: React.FunctionComponent = ({ if (!page.fmFolder) { root.pages.push(page); } else { - const folderNode = folderMap.get(page.fmFolder); + // Normalize the folder path for lookup + const normalizedPath = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + const folderNode = folderMap.get(normalizedPath); if (folderNode) { folderNode.pages.push(page); + } else { + // If folder not found, add to root as fallback + root.pages.push(page); } } }); From cda217ac76643a2cd182f46d9893bb4803d6d994 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 9 Sep 2025 19:14:38 +0200 Subject: [PATCH 7/8] Enhance StructureView to normalize folder paths and improve page assignment logic --- .../components/Contents/StructureView.tsx | 122 ++++++++++++------ src/dashboardWebView/models/Page.ts | 7 +- src/services/PagesParser.ts | 2 + 3 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx index 819f22a8..30b9c083 100644 --- a/src/dashboardWebView/components/Contents/StructureView.tsx +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useMemo } from 'react'; import { Page } from '../../models'; import { StructureItem } from './StructureItem'; +import { parseWinPath } from '../../../helpers/parseWinPath'; export interface IStructureViewProps { pages: Page[]; @@ -19,7 +20,7 @@ interface FolderNode { export const StructureView: React.FunctionComponent = ({ pages }: React.PropsWithChildren) => { - + const folderTree = useMemo(() => { const root: FolderNode = { name: '', @@ -31,23 +32,63 @@ export const StructureView: React.FunctionComponent = ({ const folderMap = new Map(); folderMap.set('', root); - // First pass: create all folder nodes - pages.forEach(page => { + // Helper to compute the normalized folder path for a page. + // It ensures the page's folder starts with the `fmFolder` segment and + // preserves any subpaths after that segment (so subfolders are created). + const computeNormalizedFolderPath = (page: Page): string => { if (!page.fmFolder) { - return; + return ''; } - - const folderPath = page.fmFolder; - // Normalize path separators and remove leading/trailing slashes - const normalizedPath = folderPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + + const fmFolder = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + + // If we have a file path, use its directory (exclude the filename) to compute + // the relative path. This avoids treating filenames as folder segments. + const filePath = page.fmFilePath ? parseWinPath(page.fmFilePath).replace(/^\/+|\/+$/g, '') : ''; + const fileDir = filePath && filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')).replace(/^\/+|\/+$/g, '') : ''; + + if (fileDir) { + // If the content folder is known, and the file directory starts with it, + // replace that root with the fmFolder (preserving subfolders after it). + if (page.fmPageFolder?.path) { + const contentFolderPath = parseWinPath(page.fmPageFolder.path).replace(/^\/+|\/+$/g, ''); + if (fileDir.startsWith(contentFolderPath)) { + const rel = fileDir.substring(contentFolderPath.length).replace(/^\/+|\/+$/g, ''); + return rel ? `${fmFolder}/${rel}` : fmFolder; + } + } + + // Otherwise try to find fmFolder as a directory segment in the fileDir + const segments = fileDir.split('/').filter(Boolean); + const fmIndex = segments.indexOf(fmFolder); + if (fmIndex >= 0) { + return segments.slice(fmIndex).join('/'); + } + } + + // Fallback: just use the fmFolder name + return fmFolder; + }; + + // First pass: create all folder nodes (ensure nodes exist even if a page lacks fmFilePath) + for (const page of pages) { + if (!page.fmFolder) { + continue; + } + + const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + if (!normalizedPath) { + continue; + } + const parts = normalizedPath.split('/').filter(part => part.length > 0); - + let currentPath = ''; let currentNode = root; - - parts.forEach(part => { + + for (const part of parts) { const fullPath = currentPath ? `${currentPath}/${part}` : part; - + if (!folderMap.has(fullPath)) { const newNode: FolderNode = { name: part, @@ -58,31 +99,31 @@ export const StructureView: React.FunctionComponent = ({ 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 => { + // Second pass: assign pages to their exact folder node (including subfolders) + for (const page of pages) { if (!page.fmFolder) { root.pages.push(page); - } else { - // Normalize the folder path for lookup - const normalizedPath = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); - const folderNode = folderMap.get(normalizedPath); - if (folderNode) { - folderNode.pages.push(page); - } else { - // If folder not found, add to root as fallback - root.pages.push(page); - } + continue; } - }); + + const normalizedPath = computeNormalizedFolderPath(page).replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); + const folderNode = normalizedPath ? folderMap.get(normalizedPath) : folderMap.get(page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')); + if (folderNode) { + folderNode.pages.push(page); + } else { + // If folder not found, add to root as fallback + root.pages.push(page); + } + } // Sort folders and pages const sortNode = (node: FolderNode) => { @@ -90,15 +131,15 @@ export const StructureView: React.FunctionComponent = ({ 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; } @@ -110,6 +151,9 @@ export const StructureView: React.FunctionComponent = ({ // For root node, render children and pages directly return (
+ {/* Root level folders */} + {node.children.map(child => renderFolderNode(child, depth + 1))} + {/* Root level pages */} {node.pages.length > 0 && (
@@ -125,9 +169,6 @@ export const StructureView: React.FunctionComponent = ({
)} - - {/* Root level folders */} - {node.children.map(child => renderFolderNode(child, depth + 1))}
); } @@ -137,14 +178,13 @@ export const StructureView: React.FunctionComponent = ({ {({ open }) => ( <> - @@ -158,6 +198,9 @@ export const StructureView: React.FunctionComponent = ({ + {/* Child folders */} + {node.children.map(child => renderFolderNode(child, depth + 1))} + {/* Pages in this folder */} {node.pages.length > 0 && (
    @@ -168,9 +211,6 @@ export const StructureView: React.FunctionComponent = ({ ))}
)} - - {/* Child folders */} - {node.children.map(child => renderFolderNode(child, depth + 1))}
)} diff --git a/src/dashboardWebView/models/Page.ts b/src/dashboardWebView/models/Page.ts index 034bdb66..29e784c0 100644 --- a/src/dashboardWebView/models/Page.ts +++ b/src/dashboardWebView/models/Page.ts @@ -1,4 +1,4 @@ -import { I18nConfig } from '../../models'; +import { ContentFolder, I18nConfig } from '../../models'; export interface Page { // Properties for caching @@ -20,15 +20,16 @@ export interface Page { fmCategories: string[]; fmContentType: string; fmDateFormat: string | undefined; + fmPageFolder: ContentFolder | undefined; // i18n fields fmDefaultLocale?: boolean; fmLocale?: I18nConfig; - fmTranslations?: { + fmTranslations?: { [locale: string]: { locale: I18nConfig; path: string; - } + }; }; title: string; diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index bd2e339f..8813ecdd 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -219,6 +219,7 @@ export class PagesParser { const isDefaultLanguage = await i18n.isDefaultLanguage(filePath); const locale = await i18n.getLocale(filePath); const translations = await i18n.getTranslations(filePath); + const pageFolder = await Folders.getPageFolderByFilePath(filePath); const page: Page = { ...article.data, @@ -241,6 +242,7 @@ export class PagesParser { fmContentType: contentType.name || DEFAULT_CONTENT_TYPE_NAME, fmBody: article?.content || '', fmDateFormat: dateFormat, + fmPageFolder: pageFolder, // i18n properties fmDefaultLocale: isDefaultLanguage, fmLocale: locale, From 24c26ac85553aa917f195d5dd934964b5f37c8e7 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 9 Sep 2025 19:50:21 +0200 Subject: [PATCH 8/8] Refactor StructureView and Overview components for improved readability; remove unused sorting logic and adjust layout styles --- .../components/Contents/Overview.tsx | 6 ++-- .../components/Contents/StructureView.tsx | 14 ++------- .../components/Header/Pagination.tsx | 30 +++++++++++-------- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/dashboardWebView/components/Contents/Overview.tsx b/src/dashboardWebView/components/Contents/Overview.tsx index 45dbedd1..7e7d4c2f 100644 --- a/src/dashboardWebView/components/Contents/Overview.tsx +++ b/src/dashboardWebView/components/Contents/Overview.tsx @@ -146,10 +146,10 @@ export const Overview: React.FunctionComponent = ({ /> {settings && settings?.contentFolders?.length > 0 ? (

{localize(LocalizationKey.dashboardContentsOverviewNoMarkdown)}

- + ) : (

{localize(LocalizationKey.dashboardContentsOverviewNoFolders)}

- + )} @@ -202,7 +202,7 @@ export const Overview: React.FunctionComponent = ({

{localize(LocalizationKey.dashboardContentsOverviewPinned)} - +

{pinnedPages.map((page, idx) => ( diff --git a/src/dashboardWebView/components/Contents/StructureView.tsx b/src/dashboardWebView/components/Contents/StructureView.tsx index 30b9c083..b73cc15c 100644 --- a/src/dashboardWebView/components/Contents/StructureView.tsx +++ b/src/dashboardWebView/components/Contents/StructureView.tsx @@ -20,7 +20,6 @@ interface FolderNode { export const StructureView: React.FunctionComponent = ({ pages }: React.PropsWithChildren) => { - const folderTree = useMemo(() => { const root: FolderNode = { name: '', @@ -125,15 +124,6 @@ export const StructureView: React.FunctionComponent = ({ } } - // 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]); @@ -150,13 +140,13 @@ export const StructureView: React.FunctionComponent = ({ if (isRoot) { // For root node, render children and pages directly return ( -
+
{/* Root level folders */} {node.children.map(child => renderFolderNode(child, depth + 1))} {/* Root level pages */} {node.pages.length > 0 && ( -
+

Root Files

diff --git a/src/dashboardWebView/components/Header/Pagination.tsx b/src/dashboardWebView/components/Header/Pagination.tsx index 5a68be1a..d044f11d 100644 --- a/src/dashboardWebView/components/Header/Pagination.tsx +++ b/src/dashboardWebView/components/Header/Pagination.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import { useCallback, useEffect, useMemo } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import usePagination from '../../hooks/usePagination'; -import { MediaTotalSelector, PageAtom, SettingsAtom } from '../../state'; +import { MediaTotalSelector, PageAtom, SettingsAtom, ViewSelector } from '../../state'; import { PaginationButton } from './PaginationButton'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; +import { DashboardViewType } from '../../models'; export interface IPaginationProps { totalPages?: number; @@ -17,6 +18,7 @@ export const Pagination: React.FunctionComponent = ({ const [page, setPage] = useRecoilState(PageAtom); const totalMedia = useRecoilValue(MediaTotalSelector); const settings = useRecoilValue(SettingsAtom); + const view = useRecoilValue(ViewSelector); const { pageSetNr, totalPagesNr } = usePagination( settings?.dashboardState.contents.pagination, totalPages, @@ -33,17 +35,17 @@ export const Pagination: React.FunctionComponent = ({ if (i >= 0 && i <= totalPagesNr) { buttons.push( + key={i} + disabled={i === page} + onClick={() => { + setPage(i); + }} + className={`max-h-8 rounded ${page === i + ? `px-2 bg-[var(--vscode-list-activeSelectionBackground)] text-[var(--vscode-list-activeSelectionForeground)]` + : `text-[var(--vscode-editor-foreground)] hover:text-[var(--vscode-list-activeSelectionForeground)]`}`} + > + {i + 1} + ); } } @@ -58,6 +60,10 @@ export const Pagination: React.FunctionComponent = ({ setPage(0); }, []); + if (view === DashboardViewType.Structure) { + return null; + } + return (