diff --git a/README.md b/README.md index a37cbe4..b468798 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Close reviews that are ready to close. `Upsource: Refresh` Fetches reviews and refreshes upsource custom view. +`Upsource: Add participant to Review` +Add a participan to the current review. + ## Settings `upsource.checkForOpenReviewsOnLaunch` diff --git a/package.json b/package.json index dc0944e..897283b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "onCommand:upsource.showReviews", "onCommand:upsource.createReview", "onCommand:upsource.closeReview", + "onCommand:upsource.addParticipantToReview", "workspaceContains:upsource.json", "onView:upsourceView" ], @@ -70,6 +71,10 @@ "dark": "resources/dark/plus.svg" } }, + { + "command": "upsource.addParticipantToReview", + "title": "Upsource: Add participant to Review" + }, { "command": "upsource.closeReviewAndRefresh", "title": "Upsource: Close Review" diff --git a/src/Config.ts b/src/Config.ts index c955642..6b38def 100644 --- a/src/Config.ts +++ b/src/Config.ts @@ -55,6 +55,11 @@ export default class Config { prompt: question + 'project ID', placeHolder: defaultConfig.projectId || 'Project ID', password: false + }, + { + prompt: question + 'reviewers comma separated', + placeHolder: 'Reviewers', + password: false } ]; @@ -74,7 +79,12 @@ export default class Config { if (typeof input == 'undefined') return; if (input) settings.projectId = input; - this.createAndOpenConfigFileIfNotExists(settings); + vscode.window.showInputBox(steps[4]).then(input => { + if (typeof input == 'undefined') return; + if (input) settings.reviewers = input.split(',').map(reviewer => reviewer.trim()); + + this.createAndOpenConfigFileIfNotExists(settings); + }); }); }); }); diff --git a/src/Upsource.ts b/src/Upsource.ts index 46349c4..cc1c3d2 100644 --- a/src/Upsource.ts +++ b/src/Upsource.ts @@ -11,6 +11,10 @@ import { ReviewIdDTO } from './models/ReviewIdDTO'; import { ReviewListDTO } from './models/ReviewListDTO'; import { RevisionDescriptorListDTO } from './models/RevisionDescriptorListDTO'; import { UpsConfig } from './models/UpsConfig'; +import { UsersForReviewDTO } from './models/UsersForReviewDTO'; +import { UsersOthersDTO } from './models/UsersOthersDTO'; +import { UserInfoResponseDTO } from './models/UserInfoResponseDTO'; +import { RoleInReviewEnum } from './models/Enums'; const Config = new ConfigService; @@ -132,6 +136,58 @@ export default class Upsource { }); } + public addParticipantToReview(reviewId: ReviewIdDTO, participant: ParticipantInReviewDTO): Promise { + let params = { + reviewId, + participant + }; + + return new Promise((resolve, reject) => { + this.sendAPIRequest('addParticipantToReview', 'POST', params).then( + res => resolve(), + err => reject(err) + ); + }); + } + + public getUsersForReview(reviewId: ReviewIdDTO): Promise { + const params = { + 'reviewId': reviewId, + 'role': RoleInReviewEnum.Reviewer, + 'limit': 99 + } + return new Promise((resolve, reject) => { + this.sendAPIRequest('getUsersForReview', 'POST', params).then( + res => resolve(res.result.result), + err => reject(err) + ); + }); + } + + public getUserInfo(ids: string[]): Promise { + const params = { + 'ids': ids + } + return new Promise((resolve, reject) => { + this.sendAPIRequest('getUserInfo', 'POST', params).then( + res => resolve(res.result), + err => reject(err) + ); + }); + } + + public getUsersInfoForReview(reviewId: ReviewIdDTO): Promise { + + return new Promise((resolve, reject) => { + this.getUsersForReview(reviewId).then((users) => { + this.getUserInfo(users.others).then( + res => resolve(res), + err => reject(err) + ); + }); + }); + } + private sendAPIRequest(path: string, method: string, params: Object = {}): Promise { return new Promise((resolve, reject) => { Config.get().then( diff --git a/src/extension.ts b/src/extension.ts index 1d36354..6ae5c6c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,6 +13,7 @@ import { ReviewListDTO } from './models/ReviewListDTO'; import { ReviewStateEnum, RoleInReviewEnum } from './models/Enums'; import { ReviewTreeItem } from './models/ReviewTreeItem'; import { UpsConfig } from './models/UpsConfig'; +import { ParticipantInReviewDTO } from './models/ParticipantInReviewDTO'; const { rootPath } = vscode.workspace; const Upsource = new UpsourceService; @@ -34,7 +35,8 @@ export function activate(context: vscode.ExtensionContext) { { name: 'setup', callback: showSetupDialog }, { name: 'showReviews', callback: showReviews }, { name: 'createReview', callback: showCreateReviewQuickPicks }, - { name: 'closeReview', callback: showCloseReviewQuickPicks } + { name: 'closeReview', callback: showCloseReviewQuickPicks }, + { name: 'addParticipantToReview', callback: showParticipantsQuickPicks } ]; @@ -96,6 +98,11 @@ function setRefreshInterval(): void { function showReviews(): void { let customQueries = vscode.workspace.getConfiguration().get('upsource.customQueries'), items: any[] = [ + { + label: 'Current', + description: 'Current branch', + query: git.branch(rootPath) + }, { label: 'All', description: 'All open & closed reviews', @@ -298,7 +305,6 @@ function showRevisionsQuickPicks(): void { revisions: [revision.revisionId] }; }); - vscode.window.showQuickPick(items).then(selectedItem => { if (!selectedItem) return; createReview(null, selectedItem.revisions); @@ -308,27 +314,37 @@ function showRevisionsQuickPicks(): void { ); } -function createReview(branch = null, revisions = null): void { - Upsource.createReview(branch, revisions).then( - review => { - if (!review) return; - - vscode.window.showInformationMessage( - "Review '" + review.reviewId.reviewId + "' successfully created." - ); +const createReview = async(branch = null, revisions = null) => { + const review = await Upsource.createReview(branch, revisions); + if (!review) return; + vscode.window.showInformationMessage( + "Review '" + review.reviewId.reviewId + "' successfully created." + ); - let resetParticipants = vscode.workspace.getConfiguration('upsource').get('resetParticipantsOnCreate'); - if (resetParticipants) { - let participants = review.participants.filter((participant) => participant.role != RoleInReviewEnum.Author); - participants.forEach((participant) => { - Upsource.removeParticipantFromReview(review.reviewId, participant); - }); - } + let resetParticipants = vscode.workspace.getConfiguration('upsource').get('resetParticipantsOnCreate'); + if (resetParticipants) { + let participants = review.participants.filter((participant) => participant.role != RoleInReviewEnum.Author); + participants.forEach((participant) => { + Upsource.removeParticipantFromReview(review.reviewId, participant); + }); + } - _reviewDataProvider.refresh(); - }, - err => showError(err) + const config:UpsConfig = await Config.get(); + const users = await Upsource.getUsersInfoForReview(review.reviewId); + const validUsers = users.infos.filter( user => + config.reviewers.find(reviewer => reviewer.toLowerCase() === user.name.toLowerCase()) ); + const all = await Promise.all(validUsers.map(user => { + return { + userId: user.userId, + role: RoleInReviewEnum.Reviewer + }; + }).map((participant) => Upsource.addParticipantToReview(review.reviewId, participant)) + ); + vscode.window.showInformationMessage( + `Reviewers '${validUsers.map((user)=>user.name).join(', ')}' successfully added to review.` + ); + _reviewDataProvider.refresh(); } function closeReview(review: ReviewDescriptorDTO) { @@ -342,6 +358,36 @@ function closeReview(review: ReviewDescriptorDTO) { ); } +const showParticipantsQuickPicks = async() => { + const res = await Upsource.getReviewList(git.branch(rootPath)); + if (res.totalCount !== 1 ) { + vscode.window.showErrorMessage("Please create a review for the current branch first"); + return; + } + const review = res.reviews[0]; + const users = await Upsource.getUsersInfoForReview(review.reviewId); + /* + * Construct the menu with possible reviewers. + */ + const selectedItem = await vscode.window.showQuickPick(users.infos.map(user => + ({ + label: user.name, + description: user.email, + user: user + })) + ); + if(!selectedItem) { + return; + } + + await Upsource.addParticipantToReview(review.reviewId, { + userId: selectedItem.user.userId, + role: RoleInReviewEnum.Reviewer + }); + vscode.window.showInformationMessage(`Participant '${selectedItem.user.name}' successfully added to review '${review.reviewId.reviewId}'.`); +} + + function showError(err) { vscode.window.showErrorMessage(err); } diff --git a/src/models/ParticipantInReviewDTO.ts b/src/models/ParticipantInReviewDTO.ts index 92d5daa..3a23dd3 100644 --- a/src/models/ParticipantInReviewDTO.ts +++ b/src/models/ParticipantInReviewDTO.ts @@ -4,6 +4,6 @@ export class ParticipantInReviewDTO { constructor( public userId: string, public role: RoleInReviewEnum, - public state: ParticipantStateEnum, + public state?: ParticipantStateEnum, ) {} }; \ No newline at end of file diff --git a/src/models/UpsConfig.ts b/src/models/UpsConfig.ts index c16f044..7a1a813 100644 --- a/src/models/UpsConfig.ts +++ b/src/models/UpsConfig.ts @@ -4,5 +4,6 @@ export class UpsConfig { public login: string = '', public projectId: string = '', public password: string = '', + public reviewers: string[] = [] ) {} }; \ No newline at end of file diff --git a/src/models/UsersForReviewDTO.ts b/src/models/UsersForReviewDTO.ts new file mode 100644 index 0000000..80b9f7b --- /dev/null +++ b/src/models/UsersForReviewDTO.ts @@ -0,0 +1,7 @@ +import { UsersOthersDTO } from './UsersOthersDTO'; + +export class UsersForReviewDTO { + constructor( + public ids: UsersOthersDTO + ) {} +}; \ No newline at end of file diff --git a/src/models/UsersOthersDTO.ts b/src/models/UsersOthersDTO.ts new file mode 100644 index 0000000..b1070b7 --- /dev/null +++ b/src/models/UsersOthersDTO.ts @@ -0,0 +1,6 @@ +export class UsersOthersDTO { + constructor( + public others: string[], + public hasMore: boolean + ) {} +}; \ No newline at end of file