ChangeScreen2: Support adding reviewers

Bug: issue 2066
Change-Id: I3654d019ce09c1b2c234c0b73f5d6c0e6c8ae0b9
This commit is contained in:
David Ostrovsky
2013-08-24 22:01:12 +02:00
committed by Shawn Pearce
parent 0775a800e3
commit a19be89e15
10 changed files with 419 additions and 13 deletions

View File

@@ -120,7 +120,7 @@ public class ChangeScreen2 extends Screen {
@UiField AnchorElement permalink; @UiField AnchorElement permalink;
@UiField Element reviewersText; @UiField Element reviewersText;
@UiField Element ccText; @UiField Reviewers reviewers;
@UiField Element changeIdText; @UiField Element changeIdText;
@UiField Element ownerText; @UiField Element ownerText;
@UiField Element statusText; @UiField Element statusText;
@@ -208,6 +208,7 @@ public class ChangeScreen2 extends Screen {
Resources.I.style().ensureInjected(); Resources.I.style().ensureInjected();
star.setVisible(Gerrit.isSignedIn()); star.setVisible(Gerrit.isSignedIn());
labels.init(style, statusText); labels.init(style, statusText);
reviewers.init(style);
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) { keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@@ -241,6 +242,12 @@ public class ChangeScreen2 extends Screen {
star.setValue(!star.getValue(), true); star.setValue(!star.getValue(), true);
} }
}); });
keysAction.add(new KeyCommand(0, 'c', Util.C.keyAddReviewers()) {
@Override
public void onKeyPress(KeyPressEvent event) {
reviewers.onOpenForm();
}
});
} }
} }
@@ -654,8 +661,9 @@ public class ChangeScreen2 extends Screen {
} }
r.remove(info.owner()._account_id()); r.remove(info.owner()._account_id());
cc.remove(info.owner()._account_id()); cc.remove(info.owner()._account_id());
reviewersText.setInnerSafeHtml(labels.formatUserList(r.values())); reviewersText.setInnerSafeHtml(Labels.formatUserList(style, r.values()));
ccText.setInnerSafeHtml(labels.formatUserList(cc.values())); reviewers.set(info.legacy_id());
reviewers.setReviewers(Labels.formatUserList(style, cc.values()));
} }
private void renderOwner(ChangeInfo info) { private void renderOwner(ChangeInfo info) {

View File

@@ -281,7 +281,9 @@ limitations under the License.
</tr> </tr>
<tr> <tr>
<th><ui:msg>CC</ui:msg></th> <th><ui:msg>CC</ui:msg></th>
<td ui:field='ccText'/> <td>
<c:Reviewers ui:field='reviewers'/>
</td>
</tr> </tr>
<tr> <tr>
<th><ui:msg>Project</ui:msg></th> <th><ui:msg>Project</ui:msg></th>

View File

@@ -123,7 +123,7 @@ class Labels extends Grid {
html.setStyleName(style.label_reject()); html.setStyleName(style.label_reject());
} }
html.append(val).append(" "); html.append(val).append(" ");
html.append(formatUserList(m.get(v))); html.append(formatUserList(style, m.get(v)));
html.closeSpan(); html.closeSpan();
} }
return html.toBlockWidget(); return html.toBlockWidget();
@@ -167,7 +167,8 @@ class Labels extends Grid {
} }
} }
SafeHtml formatUserList(Collection<? extends AccountInfo> in) { static SafeHtml formatUserList(ChangeScreen2.Style style,
Collection<? extends AccountInfo> in) {
List<AccountInfo> users = new ArrayList<AccountInfo>(in); List<AccountInfo> users = new ArrayList<AccountInfo>(in);
Collections.sort(users, new Comparator<AccountInfo>() { Collections.sort(users, new Comparator<AccountInfo>() {
@Override @Override

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client.change;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.admin.Util;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.groups.GroupBaseInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.ui.SuggestOracle;
import java.util.ArrayList;
import java.util.List;
/** REST API based suggestion Oracle for reviewers. */
public class RestReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
private Change.Id changeId;
@Override
protected void _onRequestSuggestions(final Request req, final Callback callback) {
ChangeApi.suggestReviewers(changeId.get(), req.getQuery(),
req.getLimit()).get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
@Override
public void onSuccess(JsArray<SuggestReviewerInfo> result) {
final List<RestReviewerSuggestion> r =
new ArrayList<RestReviewerSuggestion>(result.length());
for (final SuggestReviewerInfo reviewer : Natives.asList(result)) {
r.add(new RestReviewerSuggestion(reviewer));
}
callback.onSuggestionsReady(req, new Response(r));
}
});
}
public void setChange(Change.Id changeId) {
this.changeId = changeId;
}
private static class RestReviewerSuggestion implements SuggestOracle.Suggestion {
private final SuggestReviewerInfo reviewer;
RestReviewerSuggestion(final SuggestReviewerInfo reviewer) {
this.reviewer = reviewer;
}
public String getDisplayString() {
if (reviewer.account() != null) {
return FormatUtil.nameEmail(reviewer.account());
}
return reviewer.group().name()
+ " ("
+ Util.C.suggestedGroupLabel()
+ ")";
}
public String getReplacementString() {
if (reviewer.account() != null) {
return FormatUtil.nameEmail(reviewer.account());
}
return reviewer.group().name();
}
}
public static class SuggestReviewerInfo extends JavaScriptObject {
public final native AccountInfo account() /*-{ return this.account; }-*/;
public final native GroupBaseInfo group() /*-{ return this.group; }-*/;
protected SuggestReviewerInfo() {
}
}
}

View File

@@ -0,0 +1,225 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client.change;
import com.google.gerrit.client.ConfirmationCallback;
import com.google.gerrit.client.ConfirmationDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.changes.ApprovalTable.PostInput;
import com.google.gerrit.client.changes.ApprovalTable.PostResult;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.HintTextBox;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.HashMap;
import java.util.Map;
/** Add reviewers. */
class Reviewers extends Composite {
interface Binder extends UiBinder<HTMLPanel, Reviewers> {}
private static final Binder uiBinder = GWT.create(Binder.class);
@UiField Button openForm;
@UiField Element reviewers;
@UiField Element form;
@UiField Element error;
@UiField(provided = true)
SuggestBox suggestBox;
private RestReviewerSuggestOracle reviewerSuggestOracle;
private HintTextBox nameTxtBox;
private Change.Id changeId;
private ChangeScreen2.Style style;
private boolean submitOnSelection;
Reviewers() {
reviewerSuggestOracle = new RestReviewerSuggestOracle();
nameTxtBox = new HintTextBox();
suggestBox = new SuggestBox(reviewerSuggestOracle, nameTxtBox);
initWidget(uiBinder.createAndBindUi(this));
nameTxtBox.setVisibleLength(55);
nameTxtBox.setHintText(Util.C.approvalTableAddReviewerHint());
nameTxtBox.addKeyDownHandler(new KeyDownHandler() {
@Override
public void onKeyDown(KeyDownEvent e) {
submitOnSelection = false;
if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
onCancel(null);
} else if (e.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
if (((DefaultSuggestionDisplay) suggestBox.getSuggestionDisplay())
.isSuggestionListShowing()) {
submitOnSelection = true;
} else {
onAdd(null);
}
}
}
});
suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
@Override
public void onSelection(SelectionEvent<Suggestion> event) {
nameTxtBox.setFocus(true);
if (submitOnSelection) {
onAdd(null);
}
}
});
}
void set(Change.Id changeId) {
this.changeId = changeId;
reviewerSuggestOracle.setChange(changeId);
}
void init(ChangeScreen2.Style style) {
this.style = style;
openForm.setVisible(Gerrit.isSignedIn());
}
void setReviewers(SafeHtml formatUserList) {
reviewers.setInnerSafeHtml(formatUserList);
}
@UiHandler("openForm")
void onOpenForm(ClickEvent e) {
onOpenForm();
}
void onOpenForm() {
UIObject.setVisible(form, true);
UIObject.setVisible(error, false);
openForm.setVisible(false);
suggestBox.setFocus(true);
}
@UiHandler("add")
void onAdd(ClickEvent e) {
String reviewer = suggestBox.getText();
if (!reviewer.isEmpty()) {
addReviewer(reviewer, false);
}
}
@UiHandler("cancel")
void onCancel(ClickEvent e) {
openForm.setVisible(true);
UIObject.setVisible(form, false);
suggestBox.setFocus(false);
}
private void addReviewer(final String reviewer, boolean confirmed) {
ChangeApi.reviewers(changeId.get()).post(
PostInput.create(reviewer, confirmed),
new GerritCallback<PostResult>() {
public void onSuccess(PostResult result) {
nameTxtBox.setEnabled(true);
if (result.confirm()) {
askForConfirmation(result.error());
} else if (result.error() != null) {
UIObject.setVisible(error, true);
error.setInnerText(result.error());
} else {
UIObject.setVisible(error, false);
error.setInnerText("");
nameTxtBox.setText("");
if (result.reviewers() != null
&& result.reviewers().length() > 0) {
updateReviewerList();
}
}
}
private void askForConfirmation(String text) {
new ConfirmationDialog(
Util.C.approvalTableAddManyReviewersConfirmationDialogTitle(),
new SafeHtmlBuilder().append(text),
new ConfirmationCallback() {
@Override
public void onOk() {
addReviewer(reviewer, true);
}
}).center();
}
@Override
public void onFailure(Throwable err) {
UIObject.setVisible(error, true);
error.setInnerText(err instanceof StatusCodeException
? ((StatusCodeException) err).getEncodedResponse()
: err.getMessage());
nameTxtBox.setEnabled(true);
}
});
}
private void updateReviewerList() {
ChangeApi.detail(changeId.get(),
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
display(result);
}
});
}
private void display(ChangeInfo info) {
Map<Integer, AccountInfo> r = new HashMap<Integer, AccountInfo>();
Map<Integer, AccountInfo> cc = new HashMap<Integer, AccountInfo>();
for (LabelInfo label : Natives.asList(info.all_labels().values())) {
if (label.all() != null) {
for (ApprovalInfo ai : Natives.asList(label.all())) {
(ai.value() != 0 ? r : cc).put(ai._account_id(), ai);
}
}
}
for (Integer i : r.keySet()) {
cc.remove(i);
}
cc.remove(info.owner()._account_id());
setReviewers(Labels.formatUserList(style, cc.values()));
}
}

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ui:UiBinder
xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style>
.openAdd {
cursor: pointer;
float: right;
padding: 0;
margin: 0;
border: 0;
background-color: transparent;
}
.suggestBox {
margin-bottom: 2px;
}
.error {
color: #D33D3D;
font-weight: bold;
}
.cancel {
float: right;
}
</ui:style>
<g:HTMLPanel>
<div>
<span ui:field='reviewers'/>
<g:Button ui:field='openForm'
title='Add reviewers to this change'
styleName='{style.openAdd}'
visible='false'>
<ui:attribute name='title'/>
<div>[+]</div>
</g:Button>
</div>
<div ui:field='form' style='display: none' aria-hidden='true'>
<g:SuggestBox ui:field='suggestBox' styleName='{style.suggestBox}'/>
<div ui:field='error'
class='{style.error}'
style='display: none' aria-hidden='true'/>
<div>
<g:Button ui:field='add' styleName='{res.style.button}'>
<div>Add</div>
</g:Button>
<g:Button ui:field='cancel'
styleName='{res.style.button}'
addStyleNames='{style.cancel}'>
<div>Cancel</div>
</g:Button>
</div>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -254,8 +254,8 @@ public class ApprovalTable extends Composite {
} }
} }
private static class PostInput extends JavaScriptObject { public static class PostInput extends JavaScriptObject {
static PostInput create(String reviewer, boolean confirmed) { public static PostInput create(String reviewer, boolean confirmed) {
PostInput input = createObject().cast(); PostInput input = createObject().cast();
input.init(reviewer, confirmed); input.init(reviewer, confirmed);
return input; return input;
@@ -272,7 +272,7 @@ public class ApprovalTable extends Composite {
} }
} }
private static class ReviewerInfo extends AccountInfo { public static class ReviewerInfo extends AccountInfo {
final Set<String> approvals() { final Set<String> approvals() {
return Natives.keys(_approvals()); return Natives.keys(_approvals());
} }
@@ -283,10 +283,10 @@ public class ApprovalTable extends Composite {
} }
} }
private static class PostResult extends JavaScriptObject { public static class PostResult extends JavaScriptObject {
final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/; public final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
final native boolean confirm() /*-{ return this.confirm || false; }-*/; public final native boolean confirm() /*-{ return this.confirm || false; }-*/;
final native String error() /*-{ return this.error; }-*/; public final native String error() /*-{ return this.error; }-*/;
protected PostResult() { protected PostResult() {
} }

View File

@@ -86,6 +86,12 @@ public class ChangeApi {
return change(id).view("reviewers"); return change(id).view("reviewers");
} }
public static RestApi suggestReviewers(int id, String q, int n) {
return change(id).view("suggest_reviewers")
.addParameter("q", q)
.addParameter("n", n);
}
public static RestApi reviewer(int id, int reviewer) { public static RestApi reviewer(int id, int reviewer) {
return change(id).view("reviewers").id(reviewer); return change(id).view("reviewers").id(reviewer);
} }

View File

@@ -61,6 +61,7 @@ public interface ChangeConstants extends Constants {
String keyPublishComments(); String keyPublishComments();
String keyEditTopic(); String keyEditTopic();
String keyEditMessage(); String keyEditMessage();
String keyAddReviewers();
String patchTableColumnName(); String patchTableColumnName();
String patchTableColumnComments(); String patchTableColumnComments();

View File

@@ -41,6 +41,7 @@ keyReloadSearch = Reload change list
keyPublishComments = Review and publish comments keyPublishComments = Review and publish comments
keyEditTopic = Edit change topic keyEditTopic = Edit change topic
keyEditMessage = Edit commit message keyEditMessage = Edit commit message
keyAddReviewers = Add reviewers
patchTableColumnName = File Path patchTableColumnName = File Path