Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
1977347196 Implement right-click context menu for Structure view with create content functionality
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-13 10:16:05 +00:00
copilot-swe-agent[bot]
206198efcd Add click-to-create content in specific folders for Structure view
Co-authored-by: estruyf <2900833+estruyf@users.noreply.github.com>
2025-09-12 13:37:44 +00:00
Elio Struyf
8def864af0 Merge branch 'beta' into copilot/fix-937 2025-09-10 13:56:43 +02:00
7 changed files with 219 additions and 22 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

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

View File

@@ -23,6 +23,7 @@ export enum DashboardMessage {
createContent = 'createContent',
createByContentType = 'createByContentType',
createByTemplate = 'createByTemplate',
createContentInFolder = 'createContentInFolder',
refreshPages = 'refreshPages',
searchPages = 'searchPages',
openFile = 'openFile',

View File

@@ -1,10 +1,12 @@
import { Disclosure } from '@headlessui/react';
import { ChevronRightIcon, FolderIcon } from '@heroicons/react/24/solid';
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';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
export interface IStructureViewProps {
pages: Page[];
@@ -17,9 +19,81 @@ 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
// First try to find from the specific folder, then from all pages if not found
let samplePage = nodePagesOnly.find(page => page.fmPageFolder);
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;
}
const normalizedFmFolder = page.fmFolder.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
return folderPath.startsWith(normalizedFmFolder) || normalizedFmFolder.startsWith(folderPath.split('/')[0]);
});
}
if (samplePage && samplePage.fmPageFolder) {
// Construct the full folder path by combining the base content folder with the structure path
const baseFolderPath = samplePage.fmPageFolder.path.replace(/\\/g, '/').replace(/\/+$/, '');
const relativePath = folderPath.replace(/^\/+|\/+$/g, '');
const fullFolderPath = `${baseFolderPath}/${relativePath}`;
messageHandler.send(DashboardMessage.createContentInFolder, { folderPath: fullFolderPath });
}
}, [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: '',
@@ -168,24 +242,43 @@ export const StructureView: React.FunctionComponent<IStructureViewProps> = ({
<Disclosure defaultOpen={depth <= 1}>
{({ open }) => (
<>
<Disclosure.Button
className="flex items-center w-full text-left"
style={{ paddingLeft: `${paddingLeft}px` }}
<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}
>
<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.Button
className="flex items-center flex-1 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>
<button
onClick={(e) => {
e.stopPropagation();
createContentInFolder(node.path, node.pages);
}}
className="opacity-0 group-hover:opacity-100 p-1 ml-2 mr-2 rounded hover:bg-[var(--vscode-list-hoverBackground)] transition-opacity"
title="Create content in this folder"
>
<PlusIcon className="w-4 h-4 text-[var(--vscode-editor-foreground)]" />
</button>
</div>
<Disclosure.Panel className="mt-2">
{/* Child folders */}
@@ -212,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

@@ -55,6 +55,14 @@ export class ContentType {
commands.registerCommand(COMMAND_NAME.createByContentType, ContentType.createContent)
);
subscriptions.push(
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)
);
@@ -144,6 +152,67 @@ export class ContentType {
}
}
/**
* Create content in a specific folder based on content types
* @param folderData - Object containing folder path information
* @returns
*/
public static async createContentInFolder(folderData: { folderPath: string }) {
if (!folderData || !folderData.folderPath) {
return;
}
const contentTypes = ContentType.getAll();
let folders = await Folders.get();
folders = folders.filter((f) => !f.disableCreation);
// Find the folder that matches the provided path
const folder = folders.find((f) => {
const folderPath = Folders.getFolderPath(Uri.file(f.path));
// Check if the folderData.folderPath is within this content folder
return folderData.folderPath.includes(folderPath || '');
});
if (!folder) {
return;
}
const selectedContentType = await Questions.SelectContentType(folder.contentTypes || []);
if (!selectedContentType) {
return;
}
if (contentTypes && folder) {
const contentType = contentTypes.find((ct) => ct.name === selectedContentType);
if (contentType) {
// Use the specific folder path provided instead of the base folder path
ContentType.create(contentType, folderData.folderPath);
}
}
}
/**
* 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

View File

@@ -45,6 +45,9 @@ export class PagesListener extends BaseListener {
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
case DashboardMessage.createContentInFolder:
await commands.executeCommand(COMMAND_NAME.createContentInFolder, msg.payload);
break;
case DashboardMessage.refreshPages:
this.getPagesData(true);
break;

View File

@@ -33,8 +33,7 @@ export class Copilot {
}
const copilotExt = extensions.getExtension(`GitHub.copilot`);
const copilotChatExt = extensions.getExtension(`GitHub.copilot-chat`);
return !!copilotExt || !!copilotChatExt;
return !!copilotExt;
}
public static async suggestTitles(title: string): Promise<string[] | undefined> {
@@ -270,7 +269,7 @@ Example: SEO, website optimization, digital marketing.`
// console.log(models);
const [model] = await lm.selectChatModels({
vendor: 'copilot',
family: Settings.get<string>(SETTING_COPILOT_FAMILY) || 'gpt-4.1'
family: Settings.get<string>(SETTING_COPILOT_FAMILY) || 'gpt-4o-mini'
});
if ((!model || !model.sendRequest) && retry <= 5) {