Implement right-click context menu for Structure view with create content functionality

Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-09-13 10:16:05 +00:00
parent 206198efcd
commit 1977347196
4 changed files with 113 additions and 3 deletions

View File

@@ -2246,6 +2246,11 @@
"title": "%command.frontMatter.createContent%",
"category": "Front Matter"
},
{
"command": "frontMatter.structure.createContentInFolder",
"title": "Create Content in Folder",
"category": "Front Matter"
},
{
"command": "frontMatter.createTag",
"title": "%command.frontMatter.createTag%",
@@ -2484,6 +2489,11 @@
{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
},
{
"command": "frontMatter.structure.createContentInFolder",
"when": "webview == frontMatterDashboard && webviewItem == folder",
"group": "1_structure@1"
}
],
"editor/title": [

View File

@@ -24,6 +24,7 @@ export const COMMAND_NAME = {
createByContentType: getCommandName('createByContentType'),
createByTemplate: getCommandName('createByTemplate'),
createContentInFolder: getCommandName('createContentInFolder'),
structureCreateContentInFolder: getCommandName('structure.createContentInFolder'),
createTemplate: getCommandName('createTemplate'),
initTemplate: getCommandName('initTemplate'),
collapseSections: getCommandName('collapseSections'),

View File

@@ -1,7 +1,7 @@
import { Disclosure } from '@headlessui/react';
import { ChevronRightIcon, FolderIcon, PlusIcon } from '@heroicons/react/24/solid';
import * as React from 'react';
import { useMemo } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { Page } from '../../models';
import { StructureItem } from './StructureItem';
import { parseWinPath } from '../../../helpers/parseWinPath';
@@ -19,9 +19,22 @@ interface FolderNode {
pages: Page[];
}
interface ContextMenuState {
visible: boolean;
x: number;
y: number;
folderPath: string;
}
export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
pages
}: React.PropsWithChildren<IStructureViewProps>) => {
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
visible: false,
x: 0,
y: 0,
folderPath: ''
});
const createContentInFolder = React.useCallback((folderPath: string, nodePagesOnly: Page[]) => {
// Find a page from this folder to get the base content folder information
@@ -31,7 +44,9 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
if (!samplePage) {
// If no pages in this specific folder, find any page that has the same base folder structure
samplePage = pages.find(page => {
if (!page.fmFolder || !page.fmPageFolder) return false;
if (!page.fmFolder || !page.fmPageFolder) {
return false;
}
const normalizedFmFolder = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
return folderPath.startsWith(normalizedFmFolder) || normalizedFmFolder.startsWith(folderPath.split('/')[0]);
});
@@ -47,6 +62,38 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
}
}, [pages]);
const handleContextMenu = useCallback((e: React.MouseEvent, folderPath: string) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
folderPath
});
}, []);
const hideContextMenu = useCallback(() => {
setContextMenu(prev => ({ ...prev, visible: false }));
}, []);
const handleCreateContent = useCallback(() => {
if (contextMenu.folderPath) {
createContentInFolder(contextMenu.folderPath, []);
}
hideContextMenu();
}, [contextMenu.folderPath, createContentInFolder, hideContextMenu]);
// Close context menu when clicking outside
React.useEffect(() => {
const handleClick = () => hideContextMenu();
if (contextMenu.visible) {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}
}, [contextMenu.visible, hideContextMenu]);
const folderTree = useMemo(() => {
const root: FolderNode = {
name: '',
@@ -195,7 +242,13 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
<Disclosure defaultOpen={depth <= 1}>
{({ open }) => (
<>
<div className="flex items-center justify-between w-full group">
<div
className="flex items-center justify-between w-full group"
onContextMenu={(e) => handleContextMenu(e, node.path)}
data-webview-item="folder"
data-webview-item-element="name"
data-folder-path={node.path}
>
<Disclosure.Button
className="flex items-center flex-1 text-left"
style={{ paddingLeft: `${paddingLeft}px` }}
@@ -252,6 +305,26 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
return (
<div className="structure-view">
{renderFolderNode(folderTree)}
{/* Custom Context Menu */}
{contextMenu.visible && (
<div
className="fixed bg-[var(--vscode-menu-background)] border border-[var(--vscode-menu-border)] rounded shadow-lg py-1 z-50"
style={{
left: `${contextMenu.x}px`,
top: `${contextMenu.y}px`,
}}
onClick={(e) => e.stopPropagation()}
>
<button
className="w-full px-3 py-2 text-left text-[var(--vscode-menu-foreground)] hover:bg-[var(--vscode-menu-selectionBackground)] hover:text-[var(--vscode-menu-selectionForeground)] flex items-center space-x-2"
onClick={handleCreateContent}
>
<PlusIcon className="w-4 h-4" />
<span>Create Content in Folder</span>
</button>
</div>
)}
</div>
);
};

View File

@@ -59,6 +59,10 @@ export class ContentType {
commands.registerCommand(COMMAND_NAME.createContentInFolder, ContentType.createContentInFolder)
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.structureCreateContentInFolder, ContentType.structureCreateContentInFolder)
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate)
);
@@ -187,6 +191,28 @@ export class ContentType {
}
}
/**
* Create content in a specific folder from Structure view context menu
* @param webviewContext - The webview context data containing folder path
*/
public static async structureCreateContentInFolder(webviewContext?: any) {
let folderPath: string | undefined;
// VS Code webview context menu passes the element's data attributes
// The data-folder-path attribute will be available in the context
if (webviewContext) {
folderPath = webviewContext.folderPath || webviewContext['folder-path'];
}
if (!folderPath) {
Notifications.warning('Unable to determine folder path for content creation.');
return;
}
// Reuse the existing createContentInFolder logic
await ContentType.createContentInFolder({ folderPath });
}
/**
* Retrieve all content types
* @returns