Skip to content

Commit

Permalink
Allow Mentioning of Project While Creating Issue or PR (#5749)
Browse files Browse the repository at this point in the history
* Allow adding a Project While Creating a PR
Part of #3062

* Also add projects to the new issue file
Fixes #3062
  • Loading branch information
alexr00 authored Feb 13, 2024
1 parent 90cce51 commit dcea54f
Show file tree
Hide file tree
Showing 14 changed files with 150 additions and 18 deletions.
4 changes: 3 additions & 1 deletion common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IAccount, ILabel, IMilestone, ITeam, MergeMethod, MergeMethodsAvailability } from '../src/github/interface';
import { IAccount, ILabel, IMilestone, IProject, ITeam, MergeMethod, MergeMethodsAvailability } from '../src/github/interface';

export interface RemoteInfo {
owner: string;
Expand Down Expand Up @@ -79,6 +79,7 @@ export interface CreatePullRequestNew {
autoMerge: boolean;
autoMergeMethod?: MergeMethod;
labels: ILabel[];
projects: IProject[];
assignees: IAccount[];
reviewers: (IAccount | ITeam)[];
milestone?: IMilestone;
Expand All @@ -103,6 +104,7 @@ export interface CreateParamsNew {
isDraftDefault: boolean;
isDraft?: boolean;
labels?: ILabel[];
projects?: IProject[];
assignees?: IAccount[];
reviewers?: (IAccount | ITeam)[];
milestone?: IMilestone;
Expand Down
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,12 @@
"category": "%command.pull.request.category%",
"icon": "$(milestone)"
},
{
"command": "pr.addProjectsToNewPr",
"title": "%command.pr.addProjectsToNewPr.title%",
"category": "%command.pull.request.category%",
"icon": "$(project)"
},
{
"command": "pr.addFileComment",
"title": "%command.pr.addFileComment.title%",
Expand Down Expand Up @@ -1781,6 +1787,10 @@
"command": "pr.addMilestoneToNewPr",
"when": "false"
},
{
"command": "pr.addProjectsToNewPr",
"when": "false"
},
{
"command": "pr.addFileComment",
"when": "false"
Expand Down Expand Up @@ -2050,6 +2060,11 @@
"when": "view == github:createPullRequestWebview && github:createPrPermissions != READ && github:createPrPermissions",
"group": "navigation@4"
},
{
"command": "pr.addProjectsToNewPr",
"when": "view == github:createPullRequestWebview && github:createPrPermissions != READ && github:createPrPermissions",
"group": "navigation@5"
},
{
"command": "pr.refreshComments",
"when": "view == workbench.panel.comments",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@
"command.pr.addReviewersToNewPr.title": "Add Reviewers",
"command.pr.addLabelsToNewPr.title": "Apply Labels",
"command.pr.addMilestoneToNewPr.title": "Set Milestone",
"command.pr.addProjectsToNewPr.title": "Add Projects",
"command.pr.addFileComment.title": "Add File Comment",
"command.review.diffWithPrHead.title": "Compare Base With Pull Request Head (readonly)",
"command.review.diffLocalWithPrHead.title": "Compare Pull Request Head with Local",
Expand Down
1 change: 1 addition & 0 deletions resources/icons/project.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 36 additions & 5 deletions src/github/createPRViewProviderNew.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ import {
titleAndBodyFrom,
} from './folderRepositoryManager';
import { GitHubRepository } from './githubRepository';
import { IAccount, ILabel, IMilestone, isTeam, ITeam, MergeMethod, RepoAccessAndMergeMethods } from './interface';
import { IAccount, ILabel, IMilestone, IProject, isTeam, ITeam, MergeMethod, RepoAccessAndMergeMethods } from './interface';
import { BaseBranchMetadata } from './pullRequestGitHelper';
import { PullRequestModel } from './pullRequestModel';
import { getDefaultMergeMethod } from './pullRequestOverview';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, reviewersQuickPick } from './quickPicks';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks';
import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils';

const ISSUE_CLOSING_KEYWORDS = new RegExp('closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve\s$', 'i'); // https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
Expand Down Expand Up @@ -663,16 +663,26 @@ export class CreatePullRequestViewProviderNew extends WebviewViewBase implements
}
}

private setMilestone(pr: PullRequestModel, milestone: IMilestone | undefined): void {
private setMilestone(pr: PullRequestModel, milestone: IMilestone | undefined) {
if (milestone) {
pr.updateMilestone(milestone.id);
return pr.updateMilestone(milestone.id);
}
}

private setProjects(pr: PullRequestModel, projects: IProject[]) {
if (projects.length) {
return pr.updateProjects(projects);
}
}

private async getRemote(): Promise<GitHubRemote> {
return (await this._folderRepositoryManager.getGitHubRemotes()).find(remote => compareIgnoreCase(remote.owner, this._baseRemote.owner) === 0 && compareIgnoreCase(remote.repositoryName, this._baseRemote.repositoryName) === 0)!;
}

private getGitHubRepo(): GitHubRepository | undefined {
return this._folderRepositoryManager.gitHubRepositories.find(repo => compareIgnoreCase(repo.remote.owner, this._baseRemote.owner) === 0 && compareIgnoreCase(repo.remote.repositoryName, this._baseRemote.repositoryName) === 0);
}

private milestone: IMilestone | undefined;
public async addMilestone(): Promise<void> {
const remote = await this.getRemote();
Expand Down Expand Up @@ -743,6 +753,23 @@ export class CreatePullRequestViewProviderNew extends WebviewViewBase implements
});
}
}
private projects: IProject[] = [];
public async addProjects(): Promise<void> {
const githubRepo = this.getGitHubRepo();
if (!githubRepo) {
return;
}
await new Promise<void>((resolve) => {
getProjectFromQuickPick(this._folderRepositoryManager, githubRepo, this.projects, async (projects) => {
this.projects = projects;
this._postMessage({
command: 'set-projects',
params: { projects: this.projects }
});
resolve();
});
});
}

private labels: ILabel[] = [];
public async addLabels(): Promise<void> {
Expand Down Expand Up @@ -938,7 +965,8 @@ export class CreatePullRequestViewProviderNew extends WebviewViewBase implements
this.enableAutoMerge(createdPR, message.args.autoMerge, message.args.autoMergeMethod),
this.setAssignees(createdPR, message.args.assignees),
this.setReviewers(createdPR, message.args.reviewers),
this.setMilestone(createdPR, message.args.milestone)]);
this.setMilestone(createdPR, message.args.milestone),
this.setProjects(createdPR, message.args.projects)]);
};

CreatePullRequestViewProviderNew.withProgress(() => {
Expand Down Expand Up @@ -1103,6 +1131,9 @@ export class CreatePullRequestViewProviderNew extends WebviewViewBase implements
case 'pr.changeMilestone':
return this.addMilestone();

case 'pr.changeProjects':
return this.addProjects();

case 'pr.removeLabel':
return this.removeLabel(message);

Expand Down
6 changes: 6 additions & 0 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,12 @@ export class FolderRepositoryManager implements vscode.Disposable {
return globalStateProjects ?? this.createFetchOrgProjectsPromise();
}

async getAllProjects(githubRepository: GitHubRepository, clearOrgCache?: boolean): Promise<IProject[]> {
const isInOrganization = !!(await githubRepository.getMetadata()).organization;
const [repoProjects, orgProjects] = (await Promise.all([githubRepository.getProjects(), (isInOrganization ? this.getOrgProjects(clearOrgCache) : undefined)]));
return [...(repoProjects ?? []), ...(orgProjects ? orgProjects[githubRepository.remote.remoteName] : [])];
}

async getOrgTeamsCount(repository: GitHubRepository): Promise<number> {
if ((await repository.getMetadata()).organization) {
return repository.getOrgTeamsCount();
Expand Down
2 changes: 1 addition & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
}

private async changeProjects(message: IRequestMessage<void>): Promise<void> {
return getProjectFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.remote.remoteName, this._item.base.isInOrganization, this._item.item.projectItems, (project) => this.updateProjects(project, message));
return getProjectFromQuickPick(this._folderRepositoryManager, this._item.githubRepository, this._item.item.projectItems?.map(item => item.project), (project) => this.updateProjects(project, message));
}

private async updateProjects(projects: IProject[] | undefined, message: IRequestMessage<void>) {
Expand Down
9 changes: 4 additions & 5 deletions src/github/quickPicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DataUri } from '../common/uri';
import { formatError } from '../common/utils';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository';
import { IAccount, ILabel, IMilestone, IProject, IProjectItem, isSuggestedReviewer, isTeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface';
import { IAccount, ILabel, IMilestone, IProject, isSuggestedReviewer, isTeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface';
import { PullRequestModel } from './pullRequestModel';

async function getItems<T extends IAccount | ITeam | ISuggestedReviewer>(context: vscode.ExtensionContext, skipList: Set<string>, users: T[], picked: boolean, tooManyAssignable: boolean = false): Promise<(vscode.QuickPickItem & { user?: T })[]> {
Expand Down Expand Up @@ -214,12 +214,11 @@ function isProjectQuickPickItem(x: vscode.QuickPickItem | ProjectQuickPickItem):
return !!(x as ProjectQuickPickItem).id && !!(x as ProjectQuickPickItem).project;
}

export async function getProjectFromQuickPick(folderRepoManager: FolderRepositoryManager, githubRepository: GitHubRepository, remoteName: string, isInOrganization: boolean, currentProjects: IProjectItem[] | undefined, callback: (projects: IProject[]) => Promise<void>): Promise<void> {
export async function getProjectFromQuickPick(folderRepoManager: FolderRepositoryManager, githubRepository: GitHubRepository, currentProjects: IProject[] | undefined, callback: (projects: IProject[]) => Promise<void>): Promise<void> {
try {
let selectedItems: vscode.QuickPickItem[] = [];
async function getProjectOptions(): Promise<(ProjectQuickPickItem | vscode.QuickPickItem)[]> {
const [repoProjects, orgProjects] = (await Promise.all([githubRepository.getProjects(), (isInOrganization ? folderRepoManager.getOrgProjects() : undefined)]));
const projects = [...(repoProjects ?? []), ...(orgProjects ? orgProjects[remoteName] : [])];
const projects = await folderRepoManager.getAllProjects(githubRepository);
if (!projects || !projects.length) {
return [
{
Expand All @@ -235,7 +234,7 @@ export async function getProjectFromQuickPick(folderRepoManager: FolderRepositor
id: result.id,
project: result
};
if (currentProjects && currentProjects.find(project => project.project.id === result.id)) {
if (currentProjects && currentProjects.find(project => project.id === result.id)) {
selectedItems.push(item);
}
return item;
Expand Down
27 changes: 25 additions & 2 deletions src/issues/issueFeatureRegistrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { ITelemetry } from '../common/telemetry';
import { OctokitCommon } from '../github/common';
import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager';
import { IProject } from '../github/interface';
import { IssueModel } from '../github/issueModel';
import { RepositoriesManager } from '../github/repositoriesManager';
import { getRepositoryForFile, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput } from '../github/utils';
Expand All @@ -34,6 +35,7 @@ import {
NEW_ISSUE_SCHEME,
NewIssueCache,
NewIssueFileCompletionProvider,
PROJECTS,
} from './issueFile';
import { IssueHoverProvider } from './issueHoverProvider';
import { openCodeLink } from './issueLinkLookup';
Expand Down Expand Up @@ -647,6 +649,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
metadata.assignees,
metadata.labels,
metadata.milestone,
metadata.projects,
this.createIssueInfo?.lineNumber,
this.createIssueInfo?.insertIndex,
metadata.originUri
Expand Down Expand Up @@ -926,7 +929,7 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
title = quickInput.value;
if (title) {
quickInput.busy = true;
await this.doCreateIssue(document, newIssue, title, body, assignee, undefined, undefined, lineNumber, insertIndex);
await this.doCreateIssue(document, newIssue, title, body, assignee, undefined, undefined, undefined, lineNumber, insertIndex);
quickInput.busy = false;
}
quickInput.hide();
Expand Down Expand Up @@ -965,11 +968,13 @@ export class IssueFeatureRegistrar implements vscode.Disposable {
}`;
const labelLine = `${LABELS} `;
const milestoneLine = `${MILESTONE} `;
const projectsLine = `${PROJECTS} `;
const cached = this._newIssueCache.get();
const text = (cached && cached !== '') ? cached : `${title ?? vscode.l10n.t('Issue Title')}\n
${assigneeLine}
${labelLine}
${milestoneLine}\n
${milestoneLine}
${projectsLine}\n
${body ?? ''}\n
<!-- ${vscode.l10n.t('Edit the body of your new issue then click the ✓ \"Create Issue\" button in the top right of the editor. The first line will be the issue title. Assignees and Labels follow after a blank line. Leave an empty line before beginning the body of the issue.')} -->`;
await vscode.workspace.fs.writeFile(bodyPath, this.stringToUint8Array(text));
Expand All @@ -987,6 +992,13 @@ ${body ?? ''}\n
color: new vscode.ThemeColor('issues.newIssueDecoration'),
},
});
const projectsDecoration = vscode.window.createTextEditorDecorationType({
after: {
contentText: vscode.l10n.t(' Comma-separated projects.'),
fontStyle: 'italic',
color: new vscode.ThemeColor('issues.newIssueDecoration'),
},
});
const editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor(textEditor => {
if (textEditor?.document.uri.scheme === NEW_ISSUE_SCHEME) {
const assigneeFullLine = textEditor.document.lineAt(2);
Expand All @@ -1004,6 +1016,12 @@ ${body ?? ''}\n
new vscode.Range(new vscode.Position(3, 0), new vscode.Position(3, labelFullLine.text.length)),
]);
}
const projectsFullLine = textEditor.document.lineAt(5);
if (projectsFullLine.text.startsWith(PROJECTS)) {
textEditor.setDecorations(projectsDecoration, [
new vscode.Range(new vscode.Position(5, 0), new vscode.Position(5, projectsFullLine.text.length)),
]);
}
}
});

Expand Down Expand Up @@ -1149,6 +1167,7 @@ ${body ?? ''}\n
assignees: string[] | undefined,
labels: string[] | undefined,
milestone: number | undefined,
projects: IProject[] | undefined,
lineNumber: number | undefined,
insertIndex: number | undefined,
originUri?: vscode.Uri,
Expand Down Expand Up @@ -1189,12 +1208,16 @@ ${body ?? ''}\n
labels,
milestone
};

if (!(await this.verifyLabels(folderManager, createParams))) {
return false;
}
progress.report({ message: vscode.l10n.t('Creating issue in {0}...', `${createParams.owner}/${createParams.repo}`) });
const issue = await folderManager.createIssue(createParams);
if (issue) {
if (projects) {
await issue.updateProjects(projects);
}
if (document !== undefined && insertIndex !== undefined && lineNumber !== undefined) {
const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit();
const insertText: string =
Expand Down
35 changes: 32 additions & 3 deletions src/issues/issueFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

import * as vscode from 'vscode';
import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager';
import { IProject } from '../github/interface';
import { RepositoriesManager } from '../github/repositoriesManager';

export const NEW_ISSUE_SCHEME = 'newIssue';
export const NEW_ISSUE_FILE = 'NewIssue.md';
export const ASSIGNEES = vscode.l10n.t('Assignees:');
export const LABELS = vscode.l10n.t('Labels:');
export const MILESTONE = vscode.l10n.t('Milestone:');
export const PROJECTS = vscode.l10n.t('Projects:');

const NEW_ISSUE_CACHE = 'newIssue.cache';

Expand Down Expand Up @@ -91,7 +93,7 @@ export class NewIssueFileCompletionProvider implements vscode.CompletionItemProv
_context: vscode.CompletionContext,
): Promise<vscode.CompletionItem[]> {
const line = document.lineAt(position.line).text;
if (!line.startsWith(LABELS) && !line.startsWith(MILESTONE)) {
if (!line.startsWith(LABELS) && !line.startsWith(MILESTONE) && !line.startsWith(PROJECTS)) {
return [];
}
const originFile = extractIssueOriginFromQuery(document.uri);
Expand All @@ -108,6 +110,8 @@ export class NewIssueFileCompletionProvider implements vscode.CompletionItemProv
return this.provideLabelCompletionItems(folderManager, defaults);
} else if (line.startsWith(MILESTONE)) {
return this.provideMilestoneCompletionItems(folderManager);
} else if (line.startsWith(PROJECTS)) {
return this.provideProjectCompletionItems(folderManager);
} else {
return [];
}
Expand All @@ -131,6 +135,15 @@ export class NewIssueFileCompletionProvider implements vscode.CompletionItemProv
return item;
});
}

private async provideProjectCompletionItems(folderManager: FolderRepositoryManager): Promise<vscode.CompletionItem[]> {
const projects = await (await folderManager.getPullRequestDefaultRepo())?.getProjects() ?? [];
return projects.map(project => {
const item = new vscode.CompletionItem(project.title, vscode.CompletionItemKind.Event);
item.commitCharacters = [' ', ','];
return item;
});
}
}

export class NewIssueCache {
Expand All @@ -154,7 +167,7 @@ export class NewIssueCache {
}
}

export async function extractMetadataFromFile(repositoriesManager: RepositoriesManager): Promise<{ labels: string[] | undefined, milestone: number | undefined, assignees: string[] | undefined, title: string, body: string | undefined, originUri: vscode.Uri } | undefined> {
export async function extractMetadataFromFile(repositoriesManager: RepositoriesManager): Promise<{ labels: string[] | undefined, milestone: number | undefined, projects: IProject[] | undefined, assignees: string[] | undefined, title: string, body: string | undefined, originUri: vscode.Uri } | undefined> {
let text: string;
if (
!vscode.window.activeTextEditor ||
Expand Down Expand Up @@ -232,6 +245,22 @@ export async function extractMetadataFromFile(repositoriesManager: RepositoriesM
text = text.substring(lines[0].length).trim();
}
}
let projects: IProject[] | undefined;
if (text.startsWith(PROJECTS)) {
const lines = text.split(/\r\n|\n/, 1);
if (lines.length === 1) {
const repoProjects = await folderManager.getAllProjects(repo);
projects = lines[0].substring(PROJECTS.length)
.split(',')
.map(value => {
value = value.trim();
return repoProjects.find(project => project.title === value);
})
.filter<IProject>((project): project is IProject => !!project);

text = text.substring(lines[0].length).trim();
}
}
const body = text ?? '';
return { labels, milestone, assignees, title, body, originUri };
return { labels, milestone, projects, assignees, title, body, originUri };
}
Loading

0 comments on commit dcea54f

Please sign in to comment.