Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ Open VS Code Settings (Ctrl+, / Cmd+,) and search for `adoext` to customize:
| `adoext.workItemQueries` | Custom saved work item query filters | (defaults) |
| `adoext.pullRequestQueries` | Custom saved PR query filters | (defaults) |
| `adoext.projectsByOrganization` | Multi-org project selection | `{}` |
| `adoext.enableWikiView` | Enable the optional Wiki view (read-only) | `false` |

---

Expand Down Expand Up @@ -286,6 +287,7 @@ Happy coding! 🎉
| `adoext.pullRequestFilter` | `mine` | Legacy pull request filter used as compatibility fallback when older settings are migrated. |
| `adoext.useRemoteWorkItemIcons` | `true` | Prefer Azure DevOps work item type icons (including custom process icons). Disable to force bundled fallback icons. |
| `adoext.planningAssignedFilter` | `all` | Assignee filter for Backlog/Sprint/Board views (`all` or `mine`). |
| `adoext.enableWikiView` | `false` | Enable the optional Wiki view (read-only) for browsing Azure DevOps wiki pages. |

### Query and bucket management

Expand Down
73 changes: 70 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@
"name": "Pipelines",
"icon": "media/icons/ado.svg",
"contextualTitle": "Azure DevOps Pipelines"
},
{
"id": "adoext.wiki",
"name": "Wiki",
"icon": "media/icons/ado.svg",
"contextualTitle": "Azure DevOps Wiki",
"when": "adoext.wikiEnabled"
}
]
},
Expand Down Expand Up @@ -166,6 +173,24 @@
"category": "ADOExt",
"icon": "$(open-preview)"
},
{
"command": "adoext.refreshWiki",
"title": "Refresh Wiki",
"category": "ADOExt",
"icon": "$(refresh)"
},
{
"command": "adoext.searchWikiPages",
"title": "Search Wiki Pages",
"category": "ADOExt",
"icon": "$(search)"
},
{
"command": "adoext.clearWikiSearch",
"title": "Clear Wiki Search",
"category": "ADOExt",
"icon": "$(clear-all)"
},
{
"command": "adoext.setPlanningAssignedFilter",
"title": "Filter Planning Items by Assignee",
Expand Down Expand Up @@ -268,6 +293,18 @@
"category": "ADOExt",
"icon": "$(output)"
},
{
"command": "adoext.openWikiPageInBrowser",
"title": "Open Wiki Page in Browser",
"category": "ADOExt",
"icon": "$(link-external)"
},
{
"command": "adoext.copyWikiPageLink",
"title": "Copy Wiki Page Link",
"category": "ADOExt",
"icon": "$(copy)"
},
{
"command": "adoext.rerunPipelineRun",
"title": "Re-run Pipeline",
Expand Down Expand Up @@ -574,17 +611,17 @@
},
{
"command": "adoext.selectOrganization",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines || view == adoext.wiki",
"group": "navigation"
},
{
"command": "adoext.detectRepoContext",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines",
"when": "view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines || view == adoext.wiki",
"group": "navigation"
},
{
"command": "adoext.signIn",
"when": "(view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines) && !adoext.isSignedIn",
"when": "(view == adoext.workItems || view == adoext.backlog || view == adoext.sprints || view == adoext.boards || view == adoext.pullRequests || view == adoext.pipelines || view == adoext.wiki) && !adoext.isSignedIn",
"group": "navigation"
},
{
Expand Down Expand Up @@ -626,6 +663,21 @@
"command": "adoext.setPipelineRunsGroupBy",
"when": "view == adoext.pipelines",
"group": "navigation"
},
{
"command": "adoext.refreshWiki",
"when": "view == adoext.wiki",
"group": "navigation"
},
{
"command": "adoext.searchWikiPages",
"when": "view == adoext.wiki",
"group": "navigation"
},
{
"command": "adoext.clearWikiSearch",
"when": "view == adoext.wiki && adoext.wikiHasSearch",
"group": "navigation"
}
],
"view/item/context": [
Expand Down Expand Up @@ -753,6 +805,16 @@
"command": "adoext.openPipelineStepLog",
"when": "viewItem == pipelineStepLog",
"group": "inline"
},
{
"command": "adoext.openWikiPageInBrowser",
"when": "viewItem == wikiPage",
"group": "navigation"
},
{
"command": "adoext.copyWikiPageLink",
"when": "viewItem == wikiPage",
"group": "navigation"
}
],
"comments/commentThread/context": [
Expand Down Expand Up @@ -1117,6 +1179,11 @@
"default": 300,
"minimum": 60,
"description": "How often (in seconds) ADOExt polls tracked pull requests for notifications. Ignored when all notifications are disabled."
},
"adoext.enableWikiView": {
"type": "boolean",
"default": false,
"description": "Enable the optional Wiki view (read-only) for browsing Azure DevOps wiki pages."
}
}
}
Expand Down
56 changes: 55 additions & 1 deletion src/api/adoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { IGitApi } from 'azure-devops-node-api/GitApi';
import type { ICoreApi } from 'azure-devops-node-api/CoreApi';
import type { IPolicyApi } from 'azure-devops-node-api/PolicyApi';
import type { IBuildApi } from 'azure-devops-node-api/BuildApi';
import type { IWikiApi } from 'azure-devops-node-api/WikiApi';
import { CommentExpandOptions, QueryExpand, TreeStructureGroup, WorkItemExpand } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
import { GitVersionType, VersionControlChangeType, GitStatusState, PullRequestAsyncStatus, PullRequestMergeFailureType, PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { GitVersionType, VersionControlChangeType, VersionControlRecursionType, GitStatusState, PullRequestAsyncStatus, PullRequestMergeFailureType, PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { BuildReason, BuildResult, BuildStatus } from 'azure-devops-node-api/interfaces/BuildInterfaces';
import { Operation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import { normalizeWorkItemTypeName, workItemTypeScopeKey } from '../utils/workItemTypeIcons';
Expand Down Expand Up @@ -35,6 +36,8 @@ import type { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterface
import type { IdentityRef, JsonPatchDocument, JsonPatchOperation, ResourceRef } from 'azure-devops-node-api/interfaces/common/VSSInterfaces';
import type { PolicyEvaluationRecord } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { PolicyEvaluationStatus } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import type { WikiPageDetail, WikiV2 } from 'azure-devops-node-api/interfaces/WikiInterfaces';
import type { IncomingMessage } from 'http';

export type {
WorkItem,
Expand All @@ -60,6 +63,13 @@ export { GitStatusState, PolicyEvaluationStatus, PullRequestAsyncStatus, PullReq

export type PipelineRunsFilter = 'all' | 'running' | 'failed' | 'mine';

export interface WikiPageContent {
path: string;
markdown: string;
lastModified?: string;
etag?: string;
}

/** A flattened representation of a saved query (non-folder). */
export interface SavedQuery {
id: string;
Expand Down Expand Up @@ -1472,6 +1482,50 @@ export class AdoClient {
}
}

async listWikis(project: string, organization?: string): Promise<WikiV2[]> {
const wikiApi: IWikiApi = await this.getConnectionFor(organization).getWikiApi();
return wikiApi.getAllWikis(project);
}

async listWikiPages(project: string, wikiIdentifier: string, organization?: string): Promise<WikiPageDetail[]> {
const wikiApi: IWikiApi = await this.getConnectionFor(organization).getWikiApi();
const pages = await wikiApi.getPagesBatch({ top: 1000 }, project, wikiIdentifier);
return Array.isArray(pages) ? pages : [];
}

async getWikiPageMarkdown(
project: string,
wikiIdentifier: string,
pagePath: string,
organization?: string
): Promise<WikiPageContent> {
const wikiApi: IWikiApi = await this.getConnectionFor(organization).getWikiApi();
const stream = await wikiApi.getPageText(
project,
wikiIdentifier,
pagePath,
VersionControlRecursionType.None
);
const message = stream as unknown as IncomingMessage;
const lastModified = typeof message.headers['last-modified'] === 'string'
? message.headers['last-modified']
: Array.isArray(message.headers['last-modified'])
? message.headers['last-modified'][0]
: undefined;
const etag = typeof message.headers.etag === 'string'
? message.headers.etag
: Array.isArray(message.headers.etag)
? message.headers.etag[0]
: undefined;

return {
path: pagePath,
markdown: await this.streamToString(message),
...(lastModified ? { lastModified } : {}),
...(etag ? { etag } : {})
};
}

private streamToString(stream: NodeJS.ReadableStream): Promise<string> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
Expand Down
73 changes: 73 additions & 0 deletions src/commands/wikiCommands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as vscode from 'vscode';
import type { AdoClient } from '../api/adoClient';
import type { ConfigManager } from '../config/configManager';
import type { WikiProvider, WikiPageNode } from '../providers/wikiProvider';
import { showInformationMessage } from '../utils/notifications';
import { WikiPagePanel } from '../views/wikiPagePanel';

export async function refreshWiki(provider: WikiProvider): Promise<void> {
provider.refresh();
}

export async function searchWikiPages(provider: WikiProvider): Promise<void> {
const value = await vscode.window.showInputBox({
prompt: 'Filter wiki pages (matches page path and name)',
placeHolder: 'e.g. onboarding, /Team/Docs'
});
if (value === undefined) {
return;
}
provider.setSearchQuery(value);
}

export async function clearWikiSearch(provider: WikiProvider): Promise<void> {
provider.clearSearchQuery();
}

export async function viewWikiPage(
context: vscode.ExtensionContext,
client: AdoClient,
config: ConfigManager,
node: WikiPageNode
): Promise<void> {
await WikiPagePanel.show(context, client, config, {
organization: node.scope.organization,
project: node.scope.project,
wikiId: node.wiki.id,
wikiName: node.wiki.name,
wikiRemoteUrl: node.wiki.remoteUrl,
pagePath: node.path
});
}

export async function openWikiPageInBrowser(node: WikiPageNode): Promise<void> {
const pageUrl = buildWikiPageUrl(node.wiki.remoteUrl, node.path);
if (!pageUrl) {
return;
}
await vscode.env.openExternal(vscode.Uri.parse(pageUrl));
}

export async function copyWikiPageLink(node: WikiPageNode): Promise<void> {
const pageUrl = buildWikiPageUrl(node.wiki.remoteUrl, node.path);
if (!pageUrl) {
return;
}
await vscode.env.clipboard.writeText(pageUrl);
showInformationMessage('Wiki page link copied to clipboard.');
}

function buildWikiPageUrl(wikiRemoteUrl: string | undefined, pagePath: string): string | undefined {
if (!wikiRemoteUrl) {
return undefined;
}
try {
const url = new URL(wikiRemoteUrl);
const normalized = pagePath.startsWith('/') ? pagePath : `/${pagePath}`;
url.searchParams.set('pagePath', normalized);
return url.toString();
} catch {
return undefined;
}
}

5 changes: 5 additions & 0 deletions src/config/configManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ export class ConfigManager {
return organizations.length > 0 && organizations.some(org => this.getProjectSelection(org).length > 0);
}

/** Enables the optional Wiki view contribution. */
get enableWikiView(): boolean {
return this.config.get<boolean>('enableWikiView', false);
}

private normalizeList(values: readonly string[]): string[] {
const seen = new Set<string>();
const normalized: string[] = [];
Expand Down
Loading