You've already forked wizgit-vscode-extension
feat: Add export functionality for issues and pull requests
- Implemented exportIssues and exportPullRequests functions to export data in markdown, CSV, and JSON formats. - Created helper functions for generating markdown and CSV formats for issues and pull requests. - Added showExportDialog for user interaction to select export options. - Implemented saveToFile function to handle file saving. feat: Implement favorites and recent repositories management - Added functionality to add, remove, and check favorites for repositories. - Implemented recent repositories tracking with a limit on the number of recent entries. - Created a quick pick interface to show favorites and recent repositories. feat: Introduce notifications management - Implemented functions to fetch, mark as read, and manage notifications. - Created a NotificationProvider class to display notifications in a tree view. - Added functionality to show notifications in a modal with quick actions. feat: Implement search functionality for repositories, issues, and pull requests - Added searchRepositories, searchIssues, and searchPullRequests functions with filtering options. - Implemented getLabels and getCollaborators functions for additional filtering capabilities. - Created a showFilterQuickPick function for user interaction to select filters. feat: Enhance status bar with shortcuts and dynamic updates - Added default keyboard shortcuts for various commands. - Implemented createStatusBarItem and updateStatusBar functions to manage status bar display. - Created a showStatusMenu function for quick actions related to the extension.
This commit is contained in:
72
package.json
72
package.json
@@ -2,8 +2,8 @@
|
|||||||
"name": "wizgit-in-vscode",
|
"name": "wizgit-in-vscode",
|
||||||
"displayName": "WizGIT in VS Code",
|
"displayName": "WizGIT in VS Code",
|
||||||
"description": "A VS Code extension to interact with WizGIT API for repository management.",
|
"description": "A VS Code extension to interact with WizGIT API for repository management.",
|
||||||
"version": "0.0.3",
|
"version": "0.0.4",
|
||||||
"publisher": "TerenceCarrera",
|
"publisher": "Terence Carrera",
|
||||||
"icon": "resources/wizgit-marketplace-icon.png",
|
"icon": "resources/wizgit-marketplace-icon.png",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -105,6 +105,48 @@
|
|||||||
"title": "Open Pull Request",
|
"title": "Open Pull Request",
|
||||||
"category": "WizGIT",
|
"category": "WizGIT",
|
||||||
"icon": "$(link-external)"
|
"icon": "$(link-external)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchRepositories",
|
||||||
|
"title": "Search Repositories",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(telescope)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchIssues",
|
||||||
|
"title": "Search Issues",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(search)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchPullRequests",
|
||||||
|
"title": "Search Pull Requests",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(search)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.openRecentRepositories",
|
||||||
|
"title": "Open Recent Repositories",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(history)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.exportIssues",
|
||||||
|
"title": "Export Issues",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(save)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.exportPullRequests",
|
||||||
|
"title": "Export Pull Requests",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(save)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.showNotifications",
|
||||||
|
"title": "Show Notifications",
|
||||||
|
"category": "WizGIT",
|
||||||
|
"icon": "$(bell)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
@@ -159,6 +201,32 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchRepositories",
|
||||||
|
"key": "ctrl+shift+g r",
|
||||||
|
"mac": "cmd+shift+g r",
|
||||||
|
"when": "wizgit:enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchIssues",
|
||||||
|
"key": "ctrl+shift+g i",
|
||||||
|
"mac": "cmd+shift+g i",
|
||||||
|
"when": "wizgit:enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.searchPullRequests",
|
||||||
|
"key": "ctrl+shift+g p",
|
||||||
|
"mac": "cmd+shift+g p",
|
||||||
|
"when": "wizgit:enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "wizgit-in-vscode.openRecentRepositories",
|
||||||
|
"key": "ctrl+shift+g t",
|
||||||
|
"mac": "cmd+shift+g t",
|
||||||
|
"when": "wizgit:enabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
"configuration": {
|
"configuration": {
|
||||||
"title": "WizGIT",
|
"title": "WizGIT",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
667
src/extension.ts
667
src/extension.ts
@@ -5,6 +5,13 @@ import * as fs from 'fs';
|
|||||||
import * as child_process from 'child_process';
|
import * as child_process from 'child_process';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
// Import new feature modules
|
||||||
|
import { searchRepositories, searchIssues, searchPullRequests, showFilterQuickPick } from './features/search';
|
||||||
|
import { showRepositoryQuickPick, addToRecent } from './features/favorites';
|
||||||
|
import { createStatusBarItem, registerStatusBarClickHandler } from './features/statusBar';
|
||||||
|
import { getUnreadNotifications, showNotificationModal } from './features/notifications';
|
||||||
|
import { exportIssues, exportPullRequests, showExportDialog, saveToFile } from './features/export';
|
||||||
|
|
||||||
const execAsync = promisify(child_process.exec);
|
const execAsync = promisify(child_process.exec);
|
||||||
|
|
||||||
// Interfaces for API responses
|
// Interfaces for API responses
|
||||||
@@ -838,6 +845,322 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
vscode.window.showInformationMessage('WizGIT credentials cleared successfully.');
|
vscode.window.showInformationMessage('WizGIT credentials cleared successfully.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Register Search Repositories command
|
||||||
|
let searchReposDisposable = vscode.commands.registerCommand('wizgit-in-vscode.searchRepositories', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = await vscode.window.showInputBox({
|
||||||
|
prompt: 'Search repositories',
|
||||||
|
ignoreFocusOut: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
try {
|
||||||
|
const result = await searchRepositories(credentials.apiEndpoint, credentials.token, query);
|
||||||
|
const repos = result.data || [];
|
||||||
|
|
||||||
|
if (repos.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No repositories found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(
|
||||||
|
repos.map((r: any) => ({
|
||||||
|
label: r.name,
|
||||||
|
description: r.full_name,
|
||||||
|
detail: r.description,
|
||||||
|
data: r
|
||||||
|
})),
|
||||||
|
{ placeHolder: 'Select a repository' }
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await addToRecent(context, `${selected.data.id}`, selected.data.name,
|
||||||
|
selected.data.full_name.split('/')[0], selected.data.full_name.split('/')[1]);
|
||||||
|
vscode.window.showInformationMessage(`Selected: ${selected.data.full_name}`);
|
||||||
|
|
||||||
|
// Prompt user to open in VS Code or Browser
|
||||||
|
const action = await vscode.window.showInformationMessage(
|
||||||
|
`Open repository ${selected.data.name} in:`,
|
||||||
|
'VS Code',
|
||||||
|
'Browser'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (action === 'VS Code') {
|
||||||
|
await cloneAndOpenRepository(selected.data.clone_url, selected.data.name);
|
||||||
|
} else if (action === 'Browser') {
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(selected.data.html_url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Search failed: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Search Issues command
|
||||||
|
let searchIssuesDisposable = vscode.commands.registerCommand('wizgit-in-vscode.searchIssues', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoInfo = await getWorkspaceRepoInfo();
|
||||||
|
if (!repoInfo) {
|
||||||
|
vscode.window.showErrorMessage('No WizGIT repository detected in workspace');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filterOptions = await showFilterQuickPick(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, 'issues');
|
||||||
|
|
||||||
|
const searchQuery = filterOptions?.state ? `state:${filterOptions.state}` : '';
|
||||||
|
|
||||||
|
const result = await searchIssues(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, {
|
||||||
|
query: searchQuery,
|
||||||
|
...filterOptions,
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
const issues = result.data || [];
|
||||||
|
if (issues.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No issues found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(
|
||||||
|
issues.map((i: any) => ({
|
||||||
|
label: `#${i.number} - ${i.title}`,
|
||||||
|
description: i.state,
|
||||||
|
detail: i.body?.substring(0, 50),
|
||||||
|
data: i
|
||||||
|
})),
|
||||||
|
{ placeHolder: 'Select an issue' }
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await openIssueViewer(context, selected.data, `${repoInfo.owner}/${repoInfo.repo}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Search failed: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Search Pull Requests command
|
||||||
|
let searchPRsDisposable = vscode.commands.registerCommand('wizgit-in-vscode.searchPullRequests', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoInfo = await getWorkspaceRepoInfo();
|
||||||
|
if (!repoInfo) {
|
||||||
|
vscode.window.showErrorMessage('No WizGIT repository detected in workspace');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filterOptions = await showFilterQuickPick(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, 'pulls');
|
||||||
|
|
||||||
|
const result = await searchPullRequests(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, {
|
||||||
|
query: '',
|
||||||
|
...filterOptions,
|
||||||
|
page: 1,
|
||||||
|
limit: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
const prs = result.data || [];
|
||||||
|
if (prs.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No pull requests found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(
|
||||||
|
prs.map((p: any) => ({
|
||||||
|
label: `#${p.number} - ${p.title}`,
|
||||||
|
description: p.state,
|
||||||
|
detail: `${p.head.ref} → ${p.base.ref}`,
|
||||||
|
data: p
|
||||||
|
})),
|
||||||
|
{ placeHolder: 'Select a pull request' }
|
||||||
|
) as any;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await openPullRequestViewer(context, selected.data, `${repoInfo.owner}/${repoInfo.repo}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Search failed: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Open Recent Repositories command
|
||||||
|
let recentReposDisposable = vscode.commands.registerCommand('wizgit-in-vscode.openRecentRepositories', async () => {
|
||||||
|
const selected = await showRepositoryQuickPick(context);
|
||||||
|
if (selected) {
|
||||||
|
// Prompt user to open in VS Code or Browser
|
||||||
|
const action = await vscode.window.showInformationMessage(
|
||||||
|
`Open repository ${selected.name} in:`,
|
||||||
|
'VS Code',
|
||||||
|
'Browser'
|
||||||
|
);
|
||||||
|
if (action === 'VS Code' && selected.cloneUrl) {
|
||||||
|
// Clone the repository first, then open it
|
||||||
|
await cloneAndOpenRepository(selected.cloneUrl, selected.name);
|
||||||
|
} else if (action === 'Browser' && selected.htmlUrl) {
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(selected.htmlUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Export Issues command
|
||||||
|
let exportIssuesDisposable = vscode.commands.registerCommand('wizgit-in-vscode.exportIssues', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoInfo = await getWorkspaceRepoInfo();
|
||||||
|
if (!repoInfo) {
|
||||||
|
vscode.window.showErrorMessage('No WizGIT repository detected in workspace');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchIssues(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, {
|
||||||
|
query: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
const issues = result.data || [];
|
||||||
|
if (issues.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No issues found to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await showExportDialog('issues');
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
const content = await exportIssues(issues, options);
|
||||||
|
if (content) {
|
||||||
|
const filename = `issues-export-${repoInfo.repo}-${Date.now()}.${options.format}`;
|
||||||
|
await saveToFile(content, filename, options.format);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Export failed: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Export Pull Requests command
|
||||||
|
let exportPRsDisposable = vscode.commands.registerCommand('wizgit-in-vscode.exportPullRequests', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoInfo = await getWorkspaceRepoInfo();
|
||||||
|
if (!repoInfo) {
|
||||||
|
vscode.window.showErrorMessage('No WizGIT repository detected in workspace');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await searchPullRequests(credentials.apiEndpoint, credentials.token,
|
||||||
|
repoInfo.owner, repoInfo.repo, {
|
||||||
|
query: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
const prs = result.data || [];
|
||||||
|
if (prs.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No pull requests found to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await showExportDialog('pullRequests');
|
||||||
|
if (!options) return;
|
||||||
|
|
||||||
|
const content = await exportPullRequests(prs, options);
|
||||||
|
if (content) {
|
||||||
|
const filename = `prs-export-${repoInfo.repo}-${Date.now()}.${options.format}`;
|
||||||
|
await saveToFile(content, filename, options.format);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Export failed: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register Show Notifications command
|
||||||
|
let notificationsDisposable = vscode.commands.registerCommand('wizgit-in-vscode.showNotifications', async () => {
|
||||||
|
const credentials = await getStoredCredentials(context);
|
||||||
|
if (!credentials) {
|
||||||
|
vscode.window.showErrorMessage('Please configure WizGIT credentials first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const notifications = await getUnreadNotifications(credentials.apiEndpoint, credentials.token);
|
||||||
|
|
||||||
|
if (notifications.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No unread notifications');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(
|
||||||
|
notifications.map(n => ({
|
||||||
|
label: `${n.subject.title}`,
|
||||||
|
description: n.repository.name,
|
||||||
|
detail: n.reason,
|
||||||
|
data: n
|
||||||
|
})),
|
||||||
|
{ placeHolder: 'Select a notification' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
await showNotificationModal(selected.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to fetch notifications: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create and register status bar (subscriptions handled by registerStatusBarClickHandler)
|
||||||
|
createStatusBarItem(context);
|
||||||
|
registerStatusBarClickHandler(context, async (action) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'search-repos':
|
||||||
|
await vscode.commands.executeCommand('wizgit-in-vscode.searchRepositories');
|
||||||
|
break;
|
||||||
|
case 'search-issues':
|
||||||
|
await vscode.commands.executeCommand('wizgit-in-vscode.searchIssues');
|
||||||
|
break;
|
||||||
|
case 'search-prs':
|
||||||
|
await vscode.commands.executeCommand('wizgit-in-vscode.searchPullRequests');
|
||||||
|
break;
|
||||||
|
case 'recent-repos':
|
||||||
|
await vscode.commands.executeCommand('wizgit-in-vscode.openRecentRepositories');
|
||||||
|
break;
|
||||||
|
case 'configure':
|
||||||
|
await vscode.commands.executeCommand('wizgit-in-vscode.configure');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
disposable,
|
disposable,
|
||||||
createIssueDisposable,
|
createIssueDisposable,
|
||||||
@@ -847,7 +1170,14 @@ export function activate(context: vscode.ExtensionContext) {
|
|||||||
refreshDisposable,
|
refreshDisposable,
|
||||||
cloneDisposable,
|
cloneDisposable,
|
||||||
openIssueDisposable,
|
openIssueDisposable,
|
||||||
openPRDisposable
|
openPRDisposable,
|
||||||
|
searchReposDisposable,
|
||||||
|
searchIssuesDisposable,
|
||||||
|
searchPRsDisposable,
|
||||||
|
recentReposDisposable,
|
||||||
|
exportIssuesDisposable,
|
||||||
|
exportPRsDisposable,
|
||||||
|
notificationsDisposable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,6 +1341,8 @@ async function createIssueWebview(context: vscode.ExtensionContext) {
|
|||||||
panel.webview.html = getIssueWebviewContent(credentials, repoInfo, branches);
|
panel.webview.html = getIssueWebviewContent(credentials, repoInfo, branches);
|
||||||
|
|
||||||
panel.webview.onDidReceiveMessage(async message => {
|
panel.webview.onDidReceiveMessage(async message => {
|
||||||
|
console.log('Issue webview received message:', message.command, message.data);
|
||||||
|
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'createIssue':
|
case 'createIssue':
|
||||||
await handleCreateIssue(message.data, panel, context);
|
await handleCreateIssue(message.data, panel, context);
|
||||||
@@ -1025,6 +1357,11 @@ async function createIssueWebview(context: vscode.ExtensionContext) {
|
|||||||
]);
|
]);
|
||||||
panel.webview.html = getIssueWebviewContent(updatedCredentials, updatedRepoInfo, updatedBranches);
|
panel.webview.html = getIssueWebviewContent(updatedCredentials, updatedRepoInfo, updatedBranches);
|
||||||
break;
|
break;
|
||||||
|
case 'openUrl':
|
||||||
|
if (message.url) {
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, undefined, context.subscriptions);
|
}, undefined, context.subscriptions);
|
||||||
}
|
}
|
||||||
@@ -1168,11 +1505,8 @@ function getIssueWebviewContent(credentials?: { apiEndpoint: string, token: stri
|
|||||||
<div class="gitea-form rounded-lg p-6 mb-6">
|
<div class="gitea-form rounded-lg p-6 mb-6">
|
||||||
<div class="flex items-center space-x-2 mb-6">
|
<div class="flex items-center space-x-2 mb-6">
|
||||||
<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zm-1 8a1 1 0 000-2v-3a1 1 0 001-1h1a1 1 0 100-2v3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||||
</svg>
|
|
||||||
<h2 class="text-lg font-semibold">Issue Information</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<div>
|
||||||
<label for="title" class="gitea-label block mb-2">Title *</label>
|
<label for="title" class="gitea-label block mb-2">Title *</label>
|
||||||
@@ -1254,27 +1588,83 @@ function getIssueWebviewContent(credentials?: { apiEndpoint: string, token: stri
|
|||||||
<script>
|
<script>
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
|
|
||||||
|
function showMessage(type, text, url) {
|
||||||
|
const messageEl = document.getElementById('message');
|
||||||
|
const iconPath = type === 'success'
|
||||||
|
? 'M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
|
||||||
|
: 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z';
|
||||||
|
|
||||||
|
let html = '<div class="' + type + ' border rounded-lg p-4 mb-4">' +
|
||||||
|
'<div class="flex items-center space-x-2">' +
|
||||||
|
'<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">' +
|
||||||
|
'<path fill-rule="evenodd" d="' + iconPath + '" clip-rule="evenodd"/>' +
|
||||||
|
'</svg>' +
|
||||||
|
'<span>' + text + '</span>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
html += '<div class="mt-3">' +
|
||||||
|
'<button onclick="openUrl(\\'' + url + '\\')" class="inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 bg-indigo-50 border border-indigo-200 rounded-md hover:bg-indigo-100 transition-colors duration-200">' +
|
||||||
|
'<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">' +
|
||||||
|
'<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/>' +
|
||||||
|
'<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/>' +
|
||||||
|
'</svg>' +
|
||||||
|
'Open in Browser' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
messageEl.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUrl(url) {
|
||||||
|
vscode.postMessage({ command: 'openUrl', url: url });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetButton() {
|
||||||
|
const submitBtn = document.querySelector('button[type="submit"]');
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.innerHTML = '<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">' +
|
||||||
|
'<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>' +
|
||||||
|
'</svg>Create Issue';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearForm() {
|
||||||
|
document.getElementById('title').value = '';
|
||||||
|
document.getElementById('body').value = '';
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
resetButton();
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for messages from the extension
|
// Listen for messages from the extension
|
||||||
window.addEventListener('message', event => {
|
window.addEventListener('message', event => {
|
||||||
const message = event.data;
|
const message = event.data;
|
||||||
|
console.log('Webview received message:', message);
|
||||||
|
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case 'success':
|
case 'issueCreated':
|
||||||
document.getElementById('message').innerHTML =
|
if (message.success) {
|
||||||
'<div class="success border rounded-lg p-4 mb-4"><div class="flex items-center space-x-2"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span>' + message.message + '</span></div>';
|
const successText = 'Issue #' + message.issue.number + ' created: ' + message.issue.title;
|
||||||
if (message.url) {
|
showMessage('success', successText, message.issue.url);
|
||||||
document.getElementById('message').innerHTML +=
|
clearForm();
|
||||||
'<div class="mt-3"><a href="' + message.url + '" onclick="vscode.postMessage({command: \'openUrl\', url: \'' + message.url + '\'}); return false;" class="inline-flex items-center px-4 py-2 text-sm font-medium text-indigo-600 bg-indigo-50 border border-indigo-200 rounded-md hover:bg-indigo-100 transition-colors duration-200"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>Open in Browser</a></div>';
|
} else {
|
||||||
|
showMessage('error', message.error || 'Failed to create issue');
|
||||||
|
resetButton();
|
||||||
}
|
}
|
||||||
document.getElementById('message').innerHTML += '</div>';
|
|
||||||
break;
|
break;
|
||||||
case 'error':
|
case 'resetButton':
|
||||||
document.getElementById('message').innerHTML =
|
resetButton();
|
||||||
'<div class="error border rounded-lg p-4 mb-4"><div class="flex items-center space-x-2"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg><span>' + message.message + '</span></div></div>';
|
break;
|
||||||
|
case 'clearForm':
|
||||||
|
clearForm();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add configure button handler if it exists
|
// Configure button handler
|
||||||
const configureBtn = document.getElementById('configureBtn');
|
const configureBtn = document.getElementById('configureBtn');
|
||||||
if (configureBtn) {
|
if (configureBtn) {
|
||||||
configureBtn.addEventListener('click', () => {
|
configureBtn.addEventListener('click', () => {
|
||||||
@@ -1282,29 +1672,36 @@ function getIssueWebviewContent(credentials?: { apiEndpoint: string, token: stri
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form submission handler
|
||||||
document.getElementById('issueForm').addEventListener('submit', (e) => {
|
document.getElementById('issueForm').addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('Form submitted');
|
||||||
|
|
||||||
const submitBtn = document.querySelector('button[type="submit"]');
|
const submitBtn = document.querySelector('button[type="submit"]');
|
||||||
const branchSelect = document.getElementById('branch');
|
|
||||||
const data = {
|
const data = {
|
||||||
owner: document.getElementById('owner').value,
|
owner: document.getElementById('owner').value.trim(),
|
||||||
repo: document.getElementById('repo').value,
|
repo: document.getElementById('repo').value.trim(),
|
||||||
title: document.getElementById('title').value,
|
title: document.getElementById('title').value.trim(),
|
||||||
body: document.getElementById('body').value,
|
body: document.getElementById('body').value.trim(),
|
||||||
branch: branchSelect ? branchSelect.value : ''
|
branch: document.getElementById('branch').value.trim()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('Form data:', data);
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
if (!data.owner || !data.repo || !data.title) {
|
if (!data.owner || !data.repo || !data.title) {
|
||||||
document.getElementById('message').innerHTML =
|
showMessage('error', 'Please fill in Owner, Repository, and Title fields.');
|
||||||
'<div class="error border rounded-lg p-4 mb-4"><div class="flex items-center space-x-2"><svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg><span>Please fill in all required fields.</span></div></div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.innerHTML = '<svg class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Creating...';
|
submitBtn.innerHTML = 'Creating...';
|
||||||
|
|
||||||
|
// Clear any previous messages
|
||||||
|
document.getElementById('message').innerHTML = '';
|
||||||
|
|
||||||
|
console.log('Sending createIssue message to extension');
|
||||||
vscode.postMessage({
|
vscode.postMessage({
|
||||||
command: 'createIssue',
|
command: 'createIssue',
|
||||||
data: data
|
data: data
|
||||||
@@ -1433,16 +1830,13 @@ function getPullRequestWebviewContent(credentials?: { apiEndpoint: string, token
|
|||||||
<div class="flex items-center space-x-2 mb-4">
|
<div class="flex items-center space-x-2 mb-4">
|
||||||
<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||||
</svg>
|
|
||||||
<h2 class="text-lg font-semibold">Comparing Changes</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="branch-indicator rounded-lg p-4 mb-6">
|
<div class="branch-indicator rounded-lg p-4 mb-6">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex items-center space-x-4 text-sm">
|
<div class="flex items-center space-x-4 text-sm">
|
||||||
<div class="flex items-center space-x-2 flex-1">
|
<div class="flex items-center space-x-2 flex-1">
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.293l-3-3a1 1 0 00-1.414-1.414L9 5.586 7.707 4.293a1 1 0 00-1.414 1.414L8.586 8l-2.293 2.293a1 1 0 101.414 1.414L9 10.414l.293.293a1 1 0 001.414-1.414L10.414 9l3.293-3.293z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
<label class="text-sm font-medium">Source Branch:</label>
|
<label class="text-sm font-medium">Source Branch:</label>
|
||||||
<select id="head" required class="gitea-input px-3 py-2 rounded-md text-sm flex-1 min-w-[150px]">
|
<select id="head" required class="gitea-input px-3 py-2 rounded-md text-sm flex-1 min-w-[150px]">
|
||||||
@@ -1472,7 +1866,7 @@ function getPullRequestWebviewContent(credentials?: { apiEndpoint: string, token
|
|||||||
<div class="flex items-center space-x-4 text-sm">
|
<div class="flex items-center space-x-4 text-sm">
|
||||||
<div class="flex items-center space-x-2 flex-1">
|
<div class="flex items-center space-x-2 flex-1">
|
||||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.293l-3-3a1 1 0 00-1.414-1.414L9 5.586 7.707 4.293a1 1 0 00-1.414 1.414L8.586 8l-2.293 2.293a1 1 0 101.414 1.414L9 10.414l.293.293a1 1 0 001.414-1.414L10.414 9l3.293-3.293z" clip-rule="evenodd"/>
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||||
</svg>
|
</svg>
|
||||||
<label class="text-sm font-medium">Target Branch:</label>
|
<label class="text-sm font-medium">Target Branch:</label>
|
||||||
<select id="base" required class="gitea-input px-3 py-2 rounded-md text-sm flex-1 min-w-[150px]">
|
<select id="base" required class="gitea-input px-3 py-2 rounded-md text-sm flex-1 min-w-[150px]">
|
||||||
@@ -1633,58 +2027,103 @@ function getPullRequestWebviewContent(credentials?: { apiEndpoint: string, token
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreateIssue(data: any, panel: vscode.WebviewPanel, context: vscode.ExtensionContext) {
|
async function handleCreateIssue(data: any, panel: vscode.WebviewPanel, context: vscode.ExtensionContext) {
|
||||||
|
console.log('handleCreateIssue called with data:', data);
|
||||||
|
|
||||||
|
// Reset button state first
|
||||||
|
panel.webview.postMessage({ command: 'resetButton' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validate input data
|
||||||
|
if (!data.owner || !data.repo || !data.title) {
|
||||||
|
throw new Error('Owner, repository, and title are required fields.');
|
||||||
|
}
|
||||||
|
|
||||||
const credentials = await getStoredCredentials(context);
|
const credentials = await getStoredCredentials(context);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
throw new Error('No WizGIT credentials found. Please configure them first.');
|
throw new Error('No WizGIT credentials found. Please configure them first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${credentials.apiEndpoint}/repos/${data.owner}/${data.repo}/issues`, {
|
console.log('Creating issue:', { owner: data.owner, repo: data.repo, title: data.title });
|
||||||
|
|
||||||
|
const url = `${credentials.apiEndpoint}/repos/${data.owner}/${data.repo}/issues`;
|
||||||
|
console.log('Making API request to:', url);
|
||||||
|
|
||||||
|
let issueBody = data.body ? data.body.trim() : '';
|
||||||
|
|
||||||
|
// Add branch information if specified
|
||||||
|
if (data.branch && data.branch !== '') {
|
||||||
|
const branchInfo = `\n\n**Target Branch:** \`${data.branch}\``;
|
||||||
|
issueBody = issueBody ? issueBody + branchInfo : branchInfo.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build request body with branch information
|
||||||
|
const requestBody: any = {
|
||||||
|
title: data.title.trim(),
|
||||||
|
body: issueBody
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: Gitea's labels field expects label IDs (integers), not names
|
||||||
|
// The ref field is not supported in Gitea's issue creation API
|
||||||
|
// Branch information is included in the issue body for now
|
||||||
|
|
||||||
|
console.log('Request body:', requestBody);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `token ${credentials.token}`
|
'Authorization': `token ${credentials.token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody)
|
||||||
title: data.title,
|
|
||||||
body: data.body
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status, response.statusText);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorText = await response.text();
|
||||||
throw new Error(`API request failed with status ${response.status}: ${errorBody}`);
|
console.error('API error response:', errorText);
|
||||||
|
throw new Error(`Failed to create issue (${response.status}): ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseData = await response.json() as {
|
const issueData: any = await response.json();
|
||||||
title: string;
|
console.log('Issue created successfully:', issueData);
|
||||||
number: number;
|
|
||||||
html_url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update webview with success message
|
// Send success message to webview
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'success',
|
command: 'issueCreated',
|
||||||
message: `Successfully created issue #${responseData.number}: ${responseData.title}`,
|
success: true,
|
||||||
url: responseData.html_url
|
issue: {
|
||||||
|
number: issueData.number,
|
||||||
|
title: issueData.title,
|
||||||
|
url: issueData.html_url
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show VS Code notification
|
// Show VS Code notification
|
||||||
const action = await vscode.window.showInformationMessage(
|
const action = await vscode.window.showInformationMessage(
|
||||||
`Successfully created issue #${responseData.number}: ${responseData.title}`,
|
`Issue #${issueData.number} created: ${issueData.title}`,
|
||||||
'Open in Browser'
|
'Open in Browser',
|
||||||
|
'Create Another'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (action === 'Open in Browser' && responseData.html_url) {
|
if (action === 'Open in Browser') {
|
||||||
vscode.env.openExternal(vscode.Uri.parse(responseData.html_url));
|
vscode.env.openExternal(vscode.Uri.parse(issueData.html_url));
|
||||||
|
} else if (action === 'Create Another') {
|
||||||
|
// Clear the form for another issue
|
||||||
|
panel.webview.postMessage({ command: 'clearForm' });
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
console.error('Issue creation failed:', errorMessage);
|
||||||
|
|
||||||
|
// Send error message to webview
|
||||||
panel.webview.postMessage({
|
panel.webview.postMessage({
|
||||||
command: 'error',
|
command: 'issueCreated',
|
||||||
message: `Failed to create issue: ${errorMessage}`
|
success: false,
|
||||||
|
error: errorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
vscode.window.showErrorMessage(`Failed to create issue: ${errorMessage}`);
|
vscode.window.showErrorMessage(`Failed to create issue: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1748,6 +2187,80 @@ async function handleCreatePullRequest(data: any, panel: vscode.WebviewPanel, co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clone and open repository from recent repos
|
||||||
|
async function cloneAndOpenRepository(cloneUrl: string, repoName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const folderUri = await vscode.window.showOpenDialog({
|
||||||
|
canSelectFolders: true,
|
||||||
|
canSelectFiles: false,
|
||||||
|
canSelectMany: false,
|
||||||
|
openLabel: 'Select folder to clone into'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folderUri || folderUri.length === 0) return;
|
||||||
|
|
||||||
|
const cloneFolder = folderUri[0].fsPath;
|
||||||
|
const targetPath = path.join(cloneFolder, repoName);
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
if (fs.existsSync(targetPath)) {
|
||||||
|
const choice = await vscode.window.showWarningMessage(
|
||||||
|
`Folder "${repoName}" already exists. Do you want to open it anyway?`,
|
||||||
|
'Open',
|
||||||
|
'Cancel'
|
||||||
|
);
|
||||||
|
if (choice !== 'Open') return;
|
||||||
|
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await vscode.window.withProgress({
|
||||||
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
title: `Cloning ${repoName}...`,
|
||||||
|
cancellable: false
|
||||||
|
}, async (progress) => {
|
||||||
|
progress.report({ increment: 50, message: 'Cloning repository...' });
|
||||||
|
|
||||||
|
// Use git to clone
|
||||||
|
const terminal = vscode.window.createTerminal({
|
||||||
|
name: `WizGIT Clone - ${repoName}`,
|
||||||
|
cwd: cloneFolder
|
||||||
|
});
|
||||||
|
|
||||||
|
terminal.sendText(`git clone ${cloneUrl}`);
|
||||||
|
terminal.show();
|
||||||
|
|
||||||
|
// Wait for clone to complete (simplified - just wait a bit)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
progress.report({ increment: 100, message: 'Clone completed!' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the cloned repository
|
||||||
|
const openChoice = await vscode.window.showInformationMessage(
|
||||||
|
`Repository cloned successfully to ${targetPath}`,
|
||||||
|
'Open in New Window',
|
||||||
|
'Open in Current Window',
|
||||||
|
'Add to Workspace'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (openChoice === 'Open in New Window') {
|
||||||
|
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), true);
|
||||||
|
} else if (openChoice === 'Open in Current Window') {
|
||||||
|
await vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.file(targetPath), false);
|
||||||
|
} else if (openChoice === 'Add to Workspace') {
|
||||||
|
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length || 0, 0, {
|
||||||
|
uri: vscode.Uri.file(targetPath),
|
||||||
|
name: repoName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
vscode.window.showErrorMessage(`Failed to clone repository: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clone repository functionality
|
// Clone repository functionality
|
||||||
async function cloneWizGitRepository(context: vscode.ExtensionContext) {
|
async function cloneWizGitRepository(context: vscode.ExtensionContext) {
|
||||||
try {
|
try {
|
||||||
@@ -2006,7 +2519,7 @@ async function detectWizGitRepositories(context: vscode.ExtensionContext): Promi
|
|||||||
const trimmedRemote = remoteUrl.trim();
|
const trimmedRemote = remoteUrl.trim();
|
||||||
const trimmedBranch = branch.trim() || 'main';
|
const trimmedBranch = branch.trim() || 'main';
|
||||||
|
|
||||||
// Check if remote URL matches WizGIT instance
|
// Check if remote URL matches WizGIT domain
|
||||||
const apiUrl = new URL(credentials.apiEndpoint);
|
const apiUrl = new URL(credentials.apiEndpoint);
|
||||||
if (trimmedRemote.includes(apiUrl.hostname)) {
|
if (trimmedRemote.includes(apiUrl.hostname)) {
|
||||||
wizgitRepos.push({
|
wizgitRepos.push({
|
||||||
@@ -2044,6 +2557,30 @@ async function openIssueViewer(context: vscode.ExtensionContext, issueData: WizG
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix repoPath if it's empty by extracting from issue data
|
||||||
|
if (!repoPath || repoPath.trim() === '' || repoPath === '/') {
|
||||||
|
// Try to extract from stored repoPath
|
||||||
|
if ((issueData as any).repoPath) {
|
||||||
|
repoPath = (issueData as any).repoPath;
|
||||||
|
console.log('Used stored repoPath from issue data:', repoPath);
|
||||||
|
} else if (issueData.html_url) {
|
||||||
|
// Try to extract from html_url like: http://192.168.0.94/owner/repo/issues/1
|
||||||
|
const urlMatch = issueData.html_url.match(/\/([^/]+\/[^/]+)\/issues\/\d+/);
|
||||||
|
if (urlMatch) {
|
||||||
|
repoPath = urlMatch[1];
|
||||||
|
console.log('Extracted repoPath from html_url:', repoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repoPath || repoPath.trim() === '') {
|
||||||
|
console.error('Could not determine repoPath for issue:', issueData);
|
||||||
|
panel.webview.html = getErrorWebviewContent('Could not determine repository path for this issue');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Using repoPath for issue comments:', repoPath);
|
||||||
|
|
||||||
// Fetch comments
|
// Fetch comments
|
||||||
const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${issueData.number}/comments`, {
|
const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${issueData.number}/comments`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -2173,6 +2710,30 @@ async function openPullRequestViewer(context: vscode.ExtensionContext, prData: W
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix repoPath if it's empty by extracting from PR data
|
||||||
|
if (!repoPath || repoPath.trim() === '' || repoPath === '/') {
|
||||||
|
// Try to extract from stored repoPath
|
||||||
|
if ((prData as any).repoPath) {
|
||||||
|
repoPath = (prData as any).repoPath;
|
||||||
|
console.log('Used stored repoPath from PR data:', repoPath);
|
||||||
|
} else if (prData.html_url) {
|
||||||
|
// Try to extract from html_url like: http://192.168.0.94/owner/repo/pulls/2
|
||||||
|
const urlMatch = prData.html_url.match(/\/([^/]+\/[^/]+)\/pulls\/\d+/);
|
||||||
|
if (urlMatch) {
|
||||||
|
repoPath = urlMatch[1];
|
||||||
|
console.log('Extracted repoPath from html_url:', repoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!repoPath || repoPath.trim() === '') {
|
||||||
|
console.error('Could not determine repoPath for PR:', prData);
|
||||||
|
panel.webview.html = getErrorWebviewContent('Could not determine repository path for this PR');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Using repoPath for PR files:', repoPath);
|
||||||
|
|
||||||
// Fetch comments
|
// Fetch comments
|
||||||
const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${prData.number}/comments`, {
|
const commentsResponse = await fetch(`${credentials.apiEndpoint}/repos/${repoPath}/issues/${prData.number}/comments`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
229
src/features/export.ts
Normal file
229
src/features/export.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export interface ExportOptions {
|
||||||
|
format: 'markdown' | 'csv' | 'json';
|
||||||
|
includeDetails: boolean;
|
||||||
|
includeComments: boolean;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export issues to file
|
||||||
|
*/
|
||||||
|
export async function exportIssues(
|
||||||
|
issues: any[],
|
||||||
|
options: ExportOptions
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
if (options.format === 'markdown') {
|
||||||
|
content = generateMarkdownIssues(issues, options.includeDetails, options.includeComments);
|
||||||
|
} else if (options.format === 'csv') {
|
||||||
|
content = generateCSVIssues(issues);
|
||||||
|
} else if (options.format === 'json') {
|
||||||
|
content = JSON.stringify(issues, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export issues error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export pull requests to file
|
||||||
|
*/
|
||||||
|
export async function exportPullRequests(
|
||||||
|
pullRequests: any[],
|
||||||
|
options: ExportOptions
|
||||||
|
): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
if (options.format === 'markdown') {
|
||||||
|
content = generateMarkdownPullRequests(pullRequests, options.includeDetails);
|
||||||
|
} else if (options.format === 'csv') {
|
||||||
|
content = generateCSVPullRequests(pullRequests);
|
||||||
|
} else if (options.format === 'json') {
|
||||||
|
content = JSON.stringify(pullRequests, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Export PRs error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate markdown format for issues
|
||||||
|
*/
|
||||||
|
function generateMarkdownIssues(
|
||||||
|
issues: any[],
|
||||||
|
includeDetails: boolean = false,
|
||||||
|
_includeComments: boolean = false
|
||||||
|
): string {
|
||||||
|
let markdown = '# Issues Export\n\n';
|
||||||
|
markdown += `Generated: ${new Date().toISOString()}\n\n`;
|
||||||
|
|
||||||
|
issues.forEach(issue => {
|
||||||
|
markdown += `## #${issue.number} - ${issue.title}\n\n`;
|
||||||
|
markdown += `**State:** ${issue.state}\n`;
|
||||||
|
markdown += `**Author:** ${issue.user?.login || 'Unknown'}\n`;
|
||||||
|
|
||||||
|
if (issue.assignees && issue.assignees.length > 0) {
|
||||||
|
markdown += `**Assignees:** ${issue.assignees.map((a: any) => a.login).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issue.labels && issue.labels.length > 0) {
|
||||||
|
markdown += `**Labels:** ${issue.labels.map((l: any) => l.name).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown += `**Created:** ${new Date(issue.created_at).toLocaleDateString()}\n`;
|
||||||
|
|
||||||
|
if (includeDetails && issue.body) {
|
||||||
|
markdown += `\n### Description\n\n${issue.body}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown += `\n[View on WizGIT](${issue.html_url})\n\n---\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate markdown format for pull requests
|
||||||
|
*/
|
||||||
|
function generateMarkdownPullRequests(
|
||||||
|
pullRequests: any[],
|
||||||
|
includeDetails: boolean = false
|
||||||
|
): string {
|
||||||
|
let markdown = '# Pull Requests Export\n\n';
|
||||||
|
markdown += `Generated: ${new Date().toISOString()}\n\n`;
|
||||||
|
|
||||||
|
pullRequests.forEach(pr => {
|
||||||
|
markdown += `## #${pr.number} - ${pr.title}\n\n`;
|
||||||
|
markdown += `**State:** ${pr.state}\n`;
|
||||||
|
markdown += `**Author:** ${pr.user?.login || 'Unknown'}\n`;
|
||||||
|
markdown += `**Base:** ${pr.base?.ref || 'Unknown'} ← **Head:** ${pr.head?.ref || 'Unknown'}\n`;
|
||||||
|
markdown += `**Mergeable:** ${pr.mergeable ? 'Yes' : 'No'}\n`;
|
||||||
|
markdown += `**Created:** ${new Date(pr.created_at).toLocaleDateString()}\n`;
|
||||||
|
|
||||||
|
if (includeDetails && pr.body) {
|
||||||
|
markdown += `\n### Description\n\n${pr.body}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown += `\n[View on WizGIT](${pr.html_url})\n\n---\n\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return markdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CSV format for issues
|
||||||
|
*/
|
||||||
|
function generateCSVIssues(issues: any[]): string {
|
||||||
|
const headers = ['ID', 'Number', 'Title', 'State', 'Author', 'Assignees', 'Labels', 'Created', 'Updated'];
|
||||||
|
const rows = issues.map(issue => [
|
||||||
|
issue.id,
|
||||||
|
issue.number,
|
||||||
|
`"${issue.title}"`,
|
||||||
|
issue.state,
|
||||||
|
issue.user?.login || '',
|
||||||
|
issue.assignees?.map((a: any) => a.login).join(';') || '',
|
||||||
|
issue.labels?.map((l: any) => l.name).join(';') || '',
|
||||||
|
new Date(issue.created_at).toLocaleDateString(),
|
||||||
|
new Date(issue.updated_at).toLocaleDateString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate CSV format for pull requests
|
||||||
|
*/
|
||||||
|
function generateCSVPullRequests(pullRequests: any[]): string {
|
||||||
|
const headers = ['ID', 'Number', 'Title', 'State', 'Author', 'Base', 'Head', 'Mergeable', 'Created'];
|
||||||
|
const rows = pullRequests.map(pr => [
|
||||||
|
pr.id,
|
||||||
|
pr.number,
|
||||||
|
`"${pr.title}"`,
|
||||||
|
pr.state,
|
||||||
|
pr.user?.login || '',
|
||||||
|
pr.base?.ref || '',
|
||||||
|
pr.head?.ref || '',
|
||||||
|
pr.mergeable ? 'Yes' : 'No',
|
||||||
|
new Date(pr.created_at).toLocaleDateString()
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show export dialog and save file
|
||||||
|
*/
|
||||||
|
export async function showExportDialog(dataType: 'issues' | 'pullRequests'): Promise<ExportOptions | null> {
|
||||||
|
const formatOptions = ['Markdown', 'CSV', 'JSON'];
|
||||||
|
const selectedFormat = await vscode.window.showQuickPick(
|
||||||
|
formatOptions,
|
||||||
|
{ placeHolder: 'Select export format' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!selectedFormat) return null;
|
||||||
|
|
||||||
|
const includeDetailsResponse = await vscode.window.showQuickPick(
|
||||||
|
['Yes', 'No'],
|
||||||
|
{ placeHolder: 'Include full details/descriptions?' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const includeDetails = includeDetailsResponse === 'Yes';
|
||||||
|
|
||||||
|
let _includeComments = false;
|
||||||
|
if (dataType === 'issues') {
|
||||||
|
const includeCommentsResponse = await vscode.window.showQuickPick(
|
||||||
|
['Yes', 'No'],
|
||||||
|
{ placeHolder: 'Include comments?' }
|
||||||
|
);
|
||||||
|
_includeComments = includeCommentsResponse === 'Yes';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
format: selectedFormat.toLowerCase() as 'markdown' | 'csv' | 'json',
|
||||||
|
includeDetails,
|
||||||
|
includeComments: _includeComments
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save content to file
|
||||||
|
*/
|
||||||
|
export async function saveToFile(
|
||||||
|
content: string,
|
||||||
|
defaultFilename: string,
|
||||||
|
_format: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const uri = await vscode.window.showSaveDialog({
|
||||||
|
defaultUri: vscode.Uri.file(defaultFilename),
|
||||||
|
filters: {
|
||||||
|
'All Files': ['*'],
|
||||||
|
'Markdown': ['md'],
|
||||||
|
'CSV': ['csv'],
|
||||||
|
'JSON': ['json']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!uri) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(uri.fsPath, content);
|
||||||
|
vscode.window.showInformationMessage(`Exported to ${path.basename(uri.fsPath)}`);
|
||||||
|
return uri.fsPath;
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(`Failed to save file: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
src/features/favorites.ts
Normal file
172
src/features/favorites.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export interface RepoReference {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
cloneUrl?: string;
|
||||||
|
htmlUrl?: string;
|
||||||
|
timestamp: number;
|
||||||
|
lastAccessed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAVORITES_KEY = 'wizgit.favorites';
|
||||||
|
const RECENT_KEY = 'wizgit.recent';
|
||||||
|
const MAX_RECENT = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a repository to favorites
|
||||||
|
*/
|
||||||
|
export async function addFavorite(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
repoId: string,
|
||||||
|
name: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<void> {
|
||||||
|
const favorites = getFavorites(context);
|
||||||
|
|
||||||
|
if (!favorites.find(f => f.id === repoId)) {
|
||||||
|
favorites.push({
|
||||||
|
id: repoId,
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.globalState.update(FAVORITES_KEY, favorites);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a repository from favorites
|
||||||
|
*/
|
||||||
|
export async function removeFavorite(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
repoId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const favorites = getFavorites(context);
|
||||||
|
const filtered = favorites.filter(f => f.id !== repoId);
|
||||||
|
await context.globalState.update(FAVORITES_KEY, filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all favorites
|
||||||
|
*/
|
||||||
|
export function getFavorites(context: vscode.ExtensionContext): RepoReference[] {
|
||||||
|
return context.globalState.get(FAVORITES_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a repository is favorited
|
||||||
|
*/
|
||||||
|
export function isFavorite(context: vscode.ExtensionContext, repoId: string): boolean {
|
||||||
|
return getFavorites(context).some(f => f.id === repoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a repository to recent list
|
||||||
|
*/
|
||||||
|
export async function addToRecent(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
repoId: string,
|
||||||
|
name: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<void> {
|
||||||
|
const recent = getRecent(context);
|
||||||
|
|
||||||
|
// Remove if already exists
|
||||||
|
const filtered = recent.filter(r => r.id !== repoId);
|
||||||
|
|
||||||
|
// Add to beginning
|
||||||
|
filtered.unshift({
|
||||||
|
id: repoId,
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
lastAccessed: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last MAX_RECENT
|
||||||
|
filtered.splice(MAX_RECENT);
|
||||||
|
|
||||||
|
await context.globalState.update(RECENT_KEY, filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent repositories
|
||||||
|
*/
|
||||||
|
export function getRecent(context: vscode.ExtensionContext): RepoReference[] {
|
||||||
|
return context.globalState.get(RECENT_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear recent repositories
|
||||||
|
*/
|
||||||
|
export async function clearRecent(context: vscode.ExtensionContext): Promise<void> {
|
||||||
|
await context.globalState.update(RECENT_KEY, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show favorites and recent repos in a quick pick
|
||||||
|
*/
|
||||||
|
export async function showRepositoryQuickPick(context: vscode.ExtensionContext): Promise<RepoReference | undefined> {
|
||||||
|
const favorites = getFavorites(context);
|
||||||
|
const recent = getRecent(context);
|
||||||
|
|
||||||
|
const items: vscode.QuickPickItem[] = [];
|
||||||
|
|
||||||
|
if (favorites.length > 0) {
|
||||||
|
items.push({ label: '$(star-full) Favorites', kind: vscode.QuickPickItemKind.Separator });
|
||||||
|
items.push(...favorites.map(f => ({
|
||||||
|
label: `$(repo) ${f.name}`,
|
||||||
|
description: `${f.owner}/${f.repo}`,
|
||||||
|
detail: `Added ${new Date(f.timestamp).toLocaleDateString()}`,
|
||||||
|
data: f
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recent.length > 0) {
|
||||||
|
items.push({ label: '$(history) Recent', kind: vscode.QuickPickItemKind.Separator });
|
||||||
|
items.push(...recent.map(r => ({
|
||||||
|
label: `$(repo) ${r.name}`,
|
||||||
|
description: `${r.owner}/${r.repo}`,
|
||||||
|
detail: `Accessed ${new Date(r.lastAccessed || r.timestamp).toLocaleDateString()}`,
|
||||||
|
data: r
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
vscode.window.showInformationMessage('No favorites or recent repositories yet');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = await vscode.window.showQuickPick(items, {
|
||||||
|
placeHolder: 'Select a repository'
|
||||||
|
});
|
||||||
|
|
||||||
|
return selected ? (selected as any).data : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle favorite status for a repository
|
||||||
|
*/
|
||||||
|
export async function toggleFavorite(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
repoId: string,
|
||||||
|
name: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (isFavorite(context, repoId)) {
|
||||||
|
await removeFavorite(context, repoId);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
await addFavorite(context, repoId, name, owner, repo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
231
src/features/notifications.ts
Normal file
231
src/features/notifications.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export interface NotificationData {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
repository: {
|
||||||
|
name: string;
|
||||||
|
full_name: string;
|
||||||
|
};
|
||||||
|
subject: {
|
||||||
|
title: string;
|
||||||
|
type: 'Issue' | 'PullRequest' | 'Commit';
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
reason: string;
|
||||||
|
unread: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch notifications for the current user
|
||||||
|
*/
|
||||||
|
export async function getNotifications(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 30
|
||||||
|
): Promise<NotificationData[]> {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${apiEndpoint}/notifications`);
|
||||||
|
url.searchParams.append('page', page.toString());
|
||||||
|
url.searchParams.append('limit', limit.toString());
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch notifications: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as NotificationData[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get notifications error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch unread notifications only
|
||||||
|
*/
|
||||||
|
export async function getUnreadNotifications(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string
|
||||||
|
): Promise<NotificationData[]> {
|
||||||
|
try {
|
||||||
|
const notifications = await getNotifications(apiEndpoint, token);
|
||||||
|
return notifications.filter(n => n.unread);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get unread notifications error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark notification as read
|
||||||
|
*/
|
||||||
|
export async function markNotificationAsRead(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
notificationId: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiEndpoint}/notifications/${notificationId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: 'read' })
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mark notification as read error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark all notifications as read
|
||||||
|
*/
|
||||||
|
export async function markAllNotificationsAsRead(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiEndpoint}/notifications`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mark all notifications as read error:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mentions (notifications mentioning the current user)
|
||||||
|
*/
|
||||||
|
export async function getMentions(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string
|
||||||
|
): Promise<NotificationData[]> {
|
||||||
|
try {
|
||||||
|
const notifications = await getUnreadNotifications(apiEndpoint, token);
|
||||||
|
return notifications.filter(n =>
|
||||||
|
n.reason === 'mention' || n.reason === '@mention'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get mentions error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notifications in a tree view
|
||||||
|
*/
|
||||||
|
export class NotificationProvider implements vscode.TreeDataProvider<NotificationItem> {
|
||||||
|
private _onDidChangeTreeData: vscode.EventEmitter<NotificationItem | undefined | null | void> =
|
||||||
|
new vscode.EventEmitter<NotificationItem | undefined | null | void>();
|
||||||
|
|
||||||
|
readonly onDidChangeTreeData: vscode.Event<NotificationItem | undefined | null | void> =
|
||||||
|
this._onDidChangeTreeData.event;
|
||||||
|
|
||||||
|
private notifications: NotificationData[] = [];
|
||||||
|
private apiEndpoint: string = '';
|
||||||
|
private token: string = '';
|
||||||
|
|
||||||
|
constructor(_context: vscode.ExtensionContext) { }
|
||||||
|
|
||||||
|
setCredentials(apiEndpoint: string, token: string): void {
|
||||||
|
this.apiEndpoint = apiEndpoint;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
if (this.apiEndpoint && this.token) {
|
||||||
|
this.notifications = await getUnreadNotifications(this.apiEndpoint, this.token);
|
||||||
|
this._onDidChangeTreeData.fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTreeItem(element: NotificationItem): vscode.TreeItem {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChildren(): Promise<NotificationItem[]> {
|
||||||
|
if (this.notifications.length === 0) {
|
||||||
|
return [new NotificationItem('No unread notifications', vscode.TreeItemCollapsibleState.None)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.notifications.map(n =>
|
||||||
|
new NotificationItem(
|
||||||
|
`${n.subject.title}`,
|
||||||
|
vscode.TreeItemCollapsibleState.None,
|
||||||
|
{
|
||||||
|
title: 'Open Notification',
|
||||||
|
command: 'wizgit.openNotification',
|
||||||
|
arguments: [n]
|
||||||
|
},
|
||||||
|
n
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationItem extends vscode.TreeItem {
|
||||||
|
constructor(
|
||||||
|
public readonly label: string,
|
||||||
|
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
||||||
|
public readonly command?: vscode.Command,
|
||||||
|
public readonly data?: NotificationData
|
||||||
|
) {
|
||||||
|
super(label, collapsibleState);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
this.description = `${data.repository.name}`;
|
||||||
|
this.iconPath = this.getIconForType(data.subject.type);
|
||||||
|
this.tooltip = `${data.reason}: ${data.subject.title}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIconForType(type: string): vscode.ThemeIcon {
|
||||||
|
switch (type) {
|
||||||
|
case 'Issue':
|
||||||
|
return new vscode.ThemeIcon('issues');
|
||||||
|
case 'PullRequest':
|
||||||
|
return new vscode.ThemeIcon('git-pull-request');
|
||||||
|
case 'Commit':
|
||||||
|
return new vscode.ThemeIcon('git-commit');
|
||||||
|
default:
|
||||||
|
return new vscode.ThemeIcon('bell');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show notifications in a modal with quick actions
|
||||||
|
*/
|
||||||
|
export async function showNotificationModal(notification: NotificationData): Promise<void> {
|
||||||
|
const actions = ['Open in Browser', 'Mark as Read', 'Cancel'];
|
||||||
|
const selected = await vscode.window.showInformationMessage(
|
||||||
|
`${notification.subject.title}\n\nFrom: ${notification.repository.name}\nReason: ${notification.reason}`,
|
||||||
|
...actions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selected === 'Open in Browser') {
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse(notification.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
265
src/features/search.ts
Normal file
265
src/features/search.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
query: string;
|
||||||
|
state?: 'open' | 'closed';
|
||||||
|
labels?: string[];
|
||||||
|
assignee?: string;
|
||||||
|
sort?: 'created' | 'updated' | 'popularity';
|
||||||
|
order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOptions {
|
||||||
|
state?: 'open' | 'closed';
|
||||||
|
labels?: string[];
|
||||||
|
assignee?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search repositories across WizGIT
|
||||||
|
*/
|
||||||
|
export async function searchRepositories(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
query: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 30
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${apiEndpoint}/repos/search`);
|
||||||
|
url.searchParams.append('q', query);
|
||||||
|
url.searchParams.append('page', page.toString());
|
||||||
|
url.searchParams.append('limit', limit.toString());
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Repository search error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search issues in a repository with filters
|
||||||
|
*/
|
||||||
|
export async function searchIssues(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
options: SearchOptions & { page?: number; limit?: number }
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${apiEndpoint}/repos/${owner}/${repo}/issues`);
|
||||||
|
|
||||||
|
url.searchParams.append('page', (options.page || 1).toString());
|
||||||
|
url.searchParams.append('limit', (options.limit || 30).toString());
|
||||||
|
|
||||||
|
if (options.query) {
|
||||||
|
url.searchParams.append('q', options.query);
|
||||||
|
}
|
||||||
|
if (options.state) {
|
||||||
|
url.searchParams.append('state', options.state);
|
||||||
|
}
|
||||||
|
if (options.sort) {
|
||||||
|
url.searchParams.append('sort', options.sort);
|
||||||
|
}
|
||||||
|
if (options.order) {
|
||||||
|
url.searchParams.append('order', options.order);
|
||||||
|
}
|
||||||
|
if (options.labels && options.labels.length > 0) {
|
||||||
|
url.searchParams.append('labels', options.labels.join(','));
|
||||||
|
}
|
||||||
|
if (options.assignee) {
|
||||||
|
url.searchParams.append('assignee', options.assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Issue search error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search pull requests with filters
|
||||||
|
*/
|
||||||
|
export async function searchPullRequests(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
options: SearchOptions & { page?: number; limit?: number }
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${apiEndpoint}/repos/${owner}/${repo}/pulls`);
|
||||||
|
|
||||||
|
url.searchParams.append('page', (options.page || 1).toString());
|
||||||
|
url.searchParams.append('limit', (options.limit || 30).toString());
|
||||||
|
|
||||||
|
if (options.query) {
|
||||||
|
url.searchParams.append('q', options.query);
|
||||||
|
}
|
||||||
|
if (options.state) {
|
||||||
|
url.searchParams.append('state', options.state);
|
||||||
|
}
|
||||||
|
if (options.sort) {
|
||||||
|
url.searchParams.append('sort', options.sort);
|
||||||
|
}
|
||||||
|
if (options.order) {
|
||||||
|
url.searchParams.append('order', options.order);
|
||||||
|
}
|
||||||
|
if (options.assignee) {
|
||||||
|
url.searchParams.append('assignee', options.assignee);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Search failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('PR search error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available labels for filtering
|
||||||
|
*/
|
||||||
|
export async function getLabels(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiEndpoint}/repos/${owner}/${repo}/labels`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch labels: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as any[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get labels error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get collaborators for assignee filtering
|
||||||
|
*/
|
||||||
|
export async function getCollaborators(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiEndpoint}/repos/${owner}/${repo}/collaborators`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `token ${token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch collaborators: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as any[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get collaborators error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a quick filter picker for issues/PRs
|
||||||
|
*/
|
||||||
|
export async function showFilterQuickPick(
|
||||||
|
apiEndpoint: string,
|
||||||
|
token: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
type: 'issues' | 'pulls'
|
||||||
|
): Promise<FilterOptions | null> {
|
||||||
|
const filterOptions: FilterOptions = {};
|
||||||
|
|
||||||
|
// State filter
|
||||||
|
const stateSelection = await vscode.window.showQuickPick(
|
||||||
|
['All', 'Open', 'Closed'],
|
||||||
|
{ placeHolder: 'Filter by state (optional)' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stateSelection && stateSelection !== 'All') {
|
||||||
|
filterOptions.state = stateSelection === 'Open' ? 'open' : 'closed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assignee filter
|
||||||
|
const collaborators = await getCollaborators(apiEndpoint, token, owner, repo);
|
||||||
|
if (collaborators.length > 0) {
|
||||||
|
const assigneeItems = ['No filter', ...collaborators.map(c => c.login)];
|
||||||
|
const assigneeSelection = await vscode.window.showQuickPick(
|
||||||
|
assigneeItems,
|
||||||
|
{ placeHolder: 'Filter by assignee (optional)' }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assigneeSelection && assigneeSelection !== 'No filter') {
|
||||||
|
filterOptions.assignee = assigneeSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels filter (only for issues)
|
||||||
|
if (type === 'issues') {
|
||||||
|
const labels = await getLabels(apiEndpoint, token, owner, repo);
|
||||||
|
if (labels.length > 0) {
|
||||||
|
const labelItems = labels.map(l => l.name);
|
||||||
|
const labelSelection = await vscode.window.showQuickPick(
|
||||||
|
labelItems,
|
||||||
|
{ placeHolder: 'Filter by label (optional)', canPickMany: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (labelSelection && labelSelection.length > 0) {
|
||||||
|
filterOptions.labels = labelSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(filterOptions).length > 0 ? filterOptions : null;
|
||||||
|
}
|
||||||
160
src/features/statusBar.ts
Normal file
160
src/features/statusBar.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export interface ShortcutCommand {
|
||||||
|
name: string;
|
||||||
|
shortcut: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SHORTCUTS: ShortcutCommand[] = [
|
||||||
|
{
|
||||||
|
name: 'wizgit.searchRepositories',
|
||||||
|
shortcut: 'ctrl+shift+g r',
|
||||||
|
description: 'Search repositories'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wizgit.searchIssues',
|
||||||
|
shortcut: 'ctrl+shift+g i',
|
||||||
|
description: 'Search issues'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wizgit.searchPullRequests',
|
||||||
|
shortcut: 'ctrl+shift+g p',
|
||||||
|
description: 'Search pull requests'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wizgit.openRecentRepos',
|
||||||
|
shortcut: 'ctrl+shift+g t',
|
||||||
|
description: 'Open recent repositories'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register keyboard shortcuts
|
||||||
|
*/
|
||||||
|
export async function registerKeybindings(context: vscode.ExtensionContext): Promise<void> {
|
||||||
|
// Shortcuts will be defined in package.json keybindings
|
||||||
|
// This function ensures the extension recognizes them
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('wizgit.openKeybindings', async () => {
|
||||||
|
await vscode.commands.executeCommand('workbench.action.openGlobalKeybindings', '@ext:terencecarrera.wizgit-in-vscode');
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create or update status bar item
|
||||||
|
*/
|
||||||
|
export function createStatusBarItem(context: vscode.ExtensionContext): vscode.StatusBarItem {
|
||||||
|
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
|
||||||
|
statusBarItem.command = 'wizgit.showStatusMenu';
|
||||||
|
statusBarItem.tooltip = 'WizGIT Status';
|
||||||
|
updateStatusBar(statusBarItem, {
|
||||||
|
configured: false,
|
||||||
|
hasRepositories: false,
|
||||||
|
notificationCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(statusBarItem);
|
||||||
|
return statusBarItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusBarData {
|
||||||
|
configured: boolean;
|
||||||
|
hasRepositories: boolean;
|
||||||
|
notificationCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update status bar display
|
||||||
|
*/
|
||||||
|
export function updateStatusBar(statusBarItem: vscode.StatusBarItem, data: StatusBarData): void {
|
||||||
|
if (!data.configured) {
|
||||||
|
statusBarItem.text = '$(warning) WizGIT Not Configured';
|
||||||
|
statusBarItem.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = '$(git-branch) WizGIT';
|
||||||
|
|
||||||
|
if (data.hasRepositories) {
|
||||||
|
text += ' ✓';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.notificationCount > 0) {
|
||||||
|
text += ` (${data.notificationCount})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusBarItem.text = text;
|
||||||
|
statusBarItem.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusMenuItem extends vscode.QuickPickItem {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show status menu with quick actions
|
||||||
|
*/
|
||||||
|
export async function showStatusMenu(): Promise<string | undefined> {
|
||||||
|
const quickPick = vscode.window.createQuickPick<StatusMenuItem>();
|
||||||
|
const items: StatusMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: '$(telescope) Search Repositories',
|
||||||
|
description: 'Find and navigate repositories',
|
||||||
|
id: 'search-repos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(issues) Search Issues',
|
||||||
|
description: 'Find issues across repositories',
|
||||||
|
id: 'search-issues'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(git-pull-request) Search Pull Requests',
|
||||||
|
description: 'Find pull requests',
|
||||||
|
id: 'search-prs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(history) Recent Repositories',
|
||||||
|
description: 'Open recently accessed repositories',
|
||||||
|
id: 'recent-repos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '$(gear) Settings',
|
||||||
|
description: 'Configure WizGIT',
|
||||||
|
id: 'configure'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
quickPick.items = items;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
quickPick.onDidChangeSelection((selection) => {
|
||||||
|
resolve(selection[0]?.id);
|
||||||
|
quickPick.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
quickPick.onDidHide(() => {
|
||||||
|
resolve(undefined);
|
||||||
|
quickPick.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
quickPick.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register status bar click handler
|
||||||
|
*/
|
||||||
|
export function registerStatusBarClickHandler(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
callback: (action: string) => Promise<void>
|
||||||
|
): void {
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('wizgit.showStatusMenu', async () => {
|
||||||
|
const action = await showStatusMenu();
|
||||||
|
if (action) {
|
||||||
|
await callback(action);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user