// 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.change.Reviewers.PostInput; import com.google.gerrit.client.change.Reviewers.PostResult; 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.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.dom.client.Element; 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.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.Collections; 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<>(); table = new Grid(1, 3); table.addStyleName(Gerrit.RESOURCES.css().infoTable()); missing = new Widget() { { setElement((Element)(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()); } /** * Sets the header row * * @param labels The list of labels to display in the header. This list does * not get resorted, so be sure that the list's elements are in the same * order as the list of labels passed to the {@code displayRow} method. */ 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<>(); Map<Integer, AccountInfo> accounts = new LinkedHashMap<>(); 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 { List<String> labels = new ArrayList<>(change.labels()); Collections.sort(labels); displayHeader(labels); table.resizeRows(1 + byUser.size()); int i = 1; for (ApprovalDetail ad : ApprovalDetail.sort( byUser.values(), change.owner()._account_id())) { displayRow(i++, ad, labels, accounts.get(ad.getAccount().get())); } table.setVisible(true); } if (change.status() != Change.Status.MERGED && !change.mergeable()) { addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency()); } missing.setVisible(missing.getElement().getChildCount() > 0); addReviewer.setVisible(Gerrit.isSignedIn()); } private void removeAllChildren(Element el) { for (int i = DOM.getChildCount(el) - 1; i >= 0; i--) { el.removeChild(DOM.getChild(el, i)); } } private void addMissingLabel(String text) { Element li = DOM.createElement("li"); li.setClassName(Gerrit.RESOURCES.css().missingApproval()); li.setInnerText(text); DOM.appendChild(missing.getElement(), li); } private Set<Integer> removableReviewers(ChangeInfo change) { Set<Integer> result = new HashSet<>(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<>(); 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 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); } } }); } /** * Sets the reviewer data for a row. * * @param row The number of the row on which to set the reviewer. * @param ad The details for this reviewer's approval. * @param labels The list of labels to show. This list does not get resorted, * so be sure that the list's elements are in the same order as the list * of labels passed to the {@code displayHeader} method. * @param account The account information for the approval. */ private void displayRow(int row, final ApprovalDetail ad, List<String> labels, 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 : 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); } }); } }