blob: 08877d904442602711cc1e694e46c1582a380ed9 [file] [log] [blame]
// Copyright (C) 2008 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.changes;
import static com.google.gerrit.common.data.LabelValue.formatValue;
import com.google.gerrit.client.ConfirmationCallback;
import com.google.gerrit.client.ConfirmationDialog;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.AccountLinkPanel;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.ReviewerSuggestOracle;
import com.google.gerrit.common.data.ApprovalDetail;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
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.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Displays a table of {@link ApprovalDetail} objects for a change record. */
public class ApprovalTable extends Composite {
private final Grid table;
private final Widget missing;
private final Panel addReviewer;
private final ReviewerSuggestOracle reviewerSuggestOracle;
private final AddMemberBox addMemberBox;
private ChangeInfo lastChange;
private Map<Integer, Integer> rows;
public ApprovalTable() {
rows = new HashMap<Integer, Integer>();
table = new Grid(1, 3);
table.addStyleName(Gerrit.RESOURCES.css().infoTable());
missing = new Widget() {
{
setElement(DOM.createElement("ul"));
}
};
missing.setStyleName(Gerrit.RESOURCES.css().missingApprovalList());
addReviewer = new FlowPanel();
addReviewer.setStyleName(Gerrit.RESOURCES.css().addReviewer());
reviewerSuggestOracle = new ReviewerSuggestOracle();
addMemberBox =
new AddMemberBox(Util.C.approvalTableAddReviewer(),
Util.C.approvalTableAddReviewerHint(), reviewerSuggestOracle);
addMemberBox.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
doAddReviewer();
}
});
addReviewer.add(addMemberBox);
addReviewer.setVisible(false);
final FlowPanel fp = new FlowPanel();
fp.add(table);
fp.add(missing);
fp.add(addReviewer);
initWidget(fp);
setStyleName(Gerrit.RESOURCES.css().approvalTable());
}
private void displayHeader(Collection<String> labels) {
table.resizeColumns(2 + labels.size());
final CellFormatter fmt = table.getCellFormatter();
int col = 0;
table.setText(0, col, Util.C.approvalTableReviewer());
fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
col++;
table.clearCell(0, col);
fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
col++;
for (String name : labels) {
table.setText(0, col, name);
fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
col++;
}
fmt.addStyleName(0, col - 1, Gerrit.RESOURCES.css().rightmost());
}
void display(ChangeInfo change) {
lastChange = change;
reviewerSuggestOracle.setChange(change.legacy_id());
Map<Integer, ApprovalDetail> byUser =
new LinkedHashMap<Integer, ApprovalDetail>();
Map<Integer, AccountInfo> accounts =
new LinkedHashMap<Integer, AccountInfo>();
List<String> missingLabels = initLabels(change, accounts, byUser);
removeAllChildren(missing.getElement());
for (String label : missingLabels) {
addMissingLabel(Util.M.needApproval(label));
}
if (byUser.isEmpty()) {
table.setVisible(false);
} else {
displayHeader(change.labels());
table.resizeRows(1 + byUser.size());
int i = 1;
for (ApprovalDetail ad : ApprovalDetail.sort(
byUser.values(), change.owner()._account_id())) {
displayRow(i++, ad, change, accounts.get(ad.getAccount().get()));
}
table.setVisible(true);
}
if (Gerrit.getConfig().testChangeMerge()
&& change.status() != Change.Status.MERGED
&& !change.mergeable()) {
addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency());
}
missing.setVisible(DOM.getChildCount(missing.getElement()) > 0);
addReviewer.setVisible(Gerrit.isSignedIn());
}
private void removeAllChildren(Element el) {
for (int i = DOM.getChildCount(el) - 1; i >= 0; i--) {
DOM.removeChild(el, DOM.getChild(el, i));
}
}
private void addMissingLabel(String text) {
Element li = DOM.createElement("li");
li.setClassName(Gerrit.RESOURCES.css().missingApproval());
DOM.setInnerText(li, text);
DOM.appendChild(missing.getElement(), li);
}
private Set<Integer> removableReviewers(ChangeInfo change) {
Set<Integer> result =
new HashSet<Integer>(change.removable_reviewers().length());
for (int i = 0; i < change.removable_reviewers().length(); i++) {
result.add(change.removable_reviewers().get(i)._account_id());
}
return result;
}
private List<String> initLabels(ChangeInfo change,
Map<Integer, AccountInfo> accounts,
Map<Integer, ApprovalDetail> byUser) {
Set<Integer> removableReviewers = removableReviewers(change);
List<String> missing = new ArrayList<String>();
for (String name : change.labels()) {
LabelInfo label = change.label(name);
String min = null;
String max = null;
for (String v : label.values()) {
if (min == null) {
min = v;
}
if (v.startsWith("+")) {
max = v;
}
}
if (label.status() == SubmitRecord.Label.Status.NEED) {
missing.add(name);
}
if (label.all() != null) {
for (ApprovalInfo ai : Natives.asList(label.all())) {
if (!accounts.containsKey(ai._account_id())) {
accounts.put(ai._account_id(), ai);
}
int id = ai._account_id();
ApprovalDetail ad = byUser.get(id);
if (ad == null) {
ad = new ApprovalDetail(new Account.Id(id));
ad.setCanRemove(removableReviewers.contains(id));
byUser.put(id, ad);
}
if (ai.has_value()) {
ad.votable(name);
ad.value(name, ai.value());
String fv = formatValue(ai.value());
if (fv.equals(max)) {
ad.approved(name);
} else if (ai.value() < 0 && fv.equals(min)) {
ad.rejected(name);
}
}
}
}
}
return missing;
}
private void doAddReviewer() {
String reviewer = addMemberBox.getText();
if (!reviewer.isEmpty()) {
addMemberBox.setEnabled(false);
addReviewer(reviewer, false);
}
}
private static class PostInput extends JavaScriptObject {
static PostInput create(String reviewer, boolean confirmed) {
PostInput input = createObject().cast();
input.init(reviewer, confirmed);
return input;
}
private native void init(String reviewer, boolean confirmed) /*-{
this.reviewer = reviewer;
if (confirmed) {
this.confirmed = true;
}
}-*/;
protected PostInput() {
}
}
private static class ReviewerInfo extends AccountInfo {
final Set<String> approvals() {
return Natives.keys(_approvals());
}
final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
protected ReviewerInfo() {
}
}
private static class PostResult extends JavaScriptObject {
final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
final native boolean confirm() /*-{ return this.confirm || false; }-*/;
final native String error() /*-{ return this.error; }-*/;
protected PostResult() {
}
}
private void addReviewer(final String reviewer, boolean confirmed) {
ChangeApi.reviewers(lastChange.legacy_id().get()).post(
PostInput.create(reviewer, confirmed),
new GerritCallback<PostResult>() {
public void onSuccess(PostResult result) {
addMemberBox.setEnabled(true);
addMemberBox.setText("");
if (result.error() == null) {
reload();
} else if (result.confirm()) {
askForConfirmation(result.error());
} else {
new ErrorDialog(new SafeHtmlBuilder().append(result.error()));
}
}
private void askForConfirmation(String text) {
String title = Util.C
.approvalTableAddManyReviewersConfirmationDialogTitle();
ConfirmationDialog confirmationDialog = new ConfirmationDialog(
title, new SafeHtmlBuilder().append(text),
new ConfirmationCallback() {
@Override
public void onOk() {
addReviewer(reviewer, true);
}
});
confirmationDialog.center();
}
@Override
public void onFailure(final Throwable caught) {
addMemberBox.setEnabled(true);
if (isNoSuchEntity(caught)) {
new ErrorDialog(Util.M.reviewerNotFound(reviewer)).center();
} else {
super.onFailure(caught);
}
}
});
}
private void displayRow(int row, final ApprovalDetail ad, ChangeInfo change,
AccountInfo account) {
final CellFormatter fmt = table.getCellFormatter();
int col = 0;
table.setWidget(row, col++, new AccountLinkPanel(account));
rows.put(account._account_id(), row);
if (ad.canRemove()) {
final PushButton remove = new PushButton( //
new Image(Util.R.removeReviewerNormal()), //
new Image(Util.R.removeReviewerPressed()));
remove.setTitle(Util.M.removeReviewer(account.name()));
remove.setStyleName(Gerrit.RESOURCES.css().removeReviewer());
remove.addStyleName(Gerrit.RESOURCES.css().link());
remove.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
doRemove(ad, remove);
}
});
table.setWidget(row, col, remove);
} else {
table.clearCell(row, col);
}
fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
for (String labelName : change.labels()) {
fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
if (!ad.canVote(labelName)) {
fmt.addStyleName(row, col, Gerrit.RESOURCES.css().notVotable());
fmt.getElement(row, col).setTitle(Gerrit.C.userCannotVoteToolTip());
}
if (ad.isRejected(labelName)) {
table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
} else if (ad.isApproved(labelName)) {
table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
} else {
int v = ad.getValue(labelName);
if (v == 0) {
table.clearCell(row, col);
col++;
continue;
}
String vstr = String.valueOf(ad.getValue(labelName));
if (v > 0) {
vstr = "+" + vstr;
fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
} else {
fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
}
table.setText(row, col, vstr);
}
col++;
}
fmt.addStyleName(row, col - 1, Gerrit.RESOURCES.css().rightmost());
}
private void reload() {
ChangeApi.detail(lastChange.legacy_id().get(),
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
display(result);
}
});
}
private void doRemove(ApprovalDetail ad, final PushButton remove) {
remove.setEnabled(false);
ChangeApi.reviewer(lastChange.legacy_id().get(), ad.getAccount().get())
.delete(new GerritCallback<JavaScriptObject>() {
@Override
public void onSuccess(JavaScriptObject result) {
reload();
}
@Override
public void onFailure(final Throwable caught) {
remove.setEnabled(true);
super.onFailure(caught);
}
});
}
}