blob: 4e710db05330dff265086103c671e7da762c2643 [file] [log] [blame]
// Copyright (C) 2009 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 com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.patches.AbstractPatchContentTable;
import com.google.gerrit.client.patches.CommentEditorContainer;
import com.google.gerrit.client.patches.CommentEditorPanel;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.AccountScreen;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.PatchLink;
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.PatchSetPublishDetail;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.common.VoidResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PublishCommentScreen extends AccountScreen implements
ClickHandler, CommentEditorContainer {
private static SavedState lastState;
private final PatchSet.Id patchSetId;
private String revision;
private Collection<ValueRadioButton> approvalButtons;
private ChangeDescriptionBlock descBlock;
private ApprovalTable approvals;
private Panel approvalPanel;
private NpTextArea message;
private FlowPanel draftsPanel;
private Button send;
private Button submit;
private Button cancel;
private boolean saveStateOnUnload = true;
private List<CommentEditorPanel> commentEditors;
private ChangeInfo change;
private CommentLinkProcessor commentLinkProcessor;
public PublishCommentScreen(final PatchSet.Id psi) {
patchSetId = psi;
}
@Override
protected void onInitUI() {
super.onInitUI();
addStyleName(Gerrit.RESOURCES.css().publishCommentsScreen());
approvalButtons = new ArrayList<ValueRadioButton>();
descBlock = new ChangeDescriptionBlock(null);
add(descBlock);
approvals = new ApprovalTable();
add(approvals);
final FormPanel form = new FormPanel();
final FlowPanel body = new FlowPanel();
form.setWidget(body);
form.addSubmitHandler(new FormPanel.SubmitHandler() {
@Override
public void onSubmit(final SubmitEvent event) {
event.cancel();
}
});
add(form);
approvalPanel = new FlowPanel();
body.add(approvalPanel);
initMessage(body);
draftsPanel = new FlowPanel();
body.add(draftsPanel);
final FlowPanel buttonRow = new FlowPanel();
buttonRow.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
body.add(buttonRow);
send = new Button(Util.C.buttonPublishCommentsSend());
send.addClickHandler(this);
buttonRow.add(send);
submit = new Button(Util.C.buttonPublishSubmitSend());
submit.addClickHandler(this);
buttonRow.add(submit);
cancel = new Button(Util.C.buttonPublishCommentsCancel());
cancel.addClickHandler(this);
buttonRow.add(cancel);
}
private void enableForm(final boolean enabled) {
for (final ValueRadioButton approvalButton : approvalButtons) {
approvalButton.setEnabled(enabled);
}
message.setEnabled(enabled);
for (final CommentEditorPanel commentEditor : commentEditors) {
commentEditor.enableButtons(enabled);
}
send.setEnabled(enabled);
submit.setEnabled(enabled);
cancel.setEnabled(enabled);
}
@Override
protected void onLoad() {
super.onLoad();
CallbackGroup cbs = new CallbackGroup();
ChangeApi.revision(patchSetId).view("review")
.get(cbs.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
result.init();
change = result;
}
@Override
public void onFailure(Throwable caught) {
// Handled by ScreenLoadCallback.onFailure().
}
}));
Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addGwtjsonrpc(
new ScreenLoadCallback<PatchSetPublishDetail>(this) {
@Override
protected void preDisplay(final PatchSetPublishDetail result) {
send.setEnabled(true);
PublishCommentScreen.this.preDisplay(result, this);
}
@Override
protected void postDisplay() {
message.setFocus(true);
}
}));
}
private void preDisplay(final PatchSetPublishDetail pubDetail,
final ScreenLoadCallback<PatchSetPublishDetail> origCb) {
ConfigInfoCache.get(pubDetail.getChange().getProject(),
new AsyncCallback<ConfigInfoCache.Entry>() {
@Override
public void onSuccess(ConfigInfoCache.Entry result) {
commentLinkProcessor = result.getCommentLinkProcessor();
setTheme(result.getTheme());
display(pubDetail);
}
@Override
public void onFailure(Throwable caught) {
origCb.onFailure(caught);
}
});
}
@Override
protected void onUnload() {
super.onUnload();
lastState = saveStateOnUnload ? new SavedState(this) : null;
}
@Override
public void onClick(final ClickEvent event) {
final Widget sender = (Widget) event.getSource();
if (send == sender) {
onSend(false);
} else if (submit == sender) {
onSend(true);
} else if (cancel == sender) {
saveStateOnUnload = false;
goChange();
}
}
@Override
public void notifyDraftDelta(int delta) {
}
@Override
public void remove(CommentEditorPanel editor) {
commentEditors.remove(editor);
// The editor should be embedded into a panel holding all
// editors for the same file.
//
FlowPanel parent = (FlowPanel) editor.getParent();
parent.remove(editor);
// If the panel now holds no editors, remove it.
//
int editorCount = 0;
for (Widget w : parent) {
if (w instanceof CommentEditorPanel) {
editorCount++;
}
}
if (editorCount == 0) {
parent.removeFromParent();
}
// If that was the last file with a draft, remove the heading.
//
if (draftsPanel.getWidgetCount() == 1) {
draftsPanel.clear();
}
}
private void initMessage(final Panel body) {
body.add(new SmallHeading(Util.C.headingCoverMessage()));
final VerticalPanel mwrap = new VerticalPanel();
mwrap.setStyleName(Gerrit.RESOURCES.css().coverMessage());
body.add(mwrap);
message = new NpTextArea();
message.setCharacterWidth(60);
message.setVisibleLines(10);
message.setSpellCheck(true);
mwrap.add(message);
}
private void initApprovals(Panel body) {
for (String labelName : change.labels()) {
initLabel(labelName, body);
}
}
private void initLabel(String labelName, Panel body) {
if (!change.has_permitted_labels()) {
return;
}
JsArrayString nativeValues = change.permitted_values(labelName);
if (nativeValues == null || nativeValues.length() == 0) {
return;
}
List<String> values = new ArrayList<String>(nativeValues.length());
for (int i = 0; i < nativeValues.length(); i++) {
values.add(nativeValues.get(i));
}
Collections.reverse(values);
LabelInfo label = change.label(labelName);
body.add(new SmallHeading(label.name() + ":"));
VerticalPanel vp = new VerticalPanel();
vp.setStyleName(Gerrit.RESOURCES.css().labelList());
Short prior = null;
if (label.all() != null) {
for (ApprovalInfo app : Natives.asList(label.all())) {
if (app._account_id() == Gerrit.getUserAccount().getId().get()) {
prior = app.value();
break;
}
}
}
for (String value : values) {
ValueRadioButton b = new ValueRadioButton(label, value);
SafeHtml buf = new SafeHtmlBuilder().append(b.format());
buf = commentLinkProcessor.apply(buf);
SafeHtml.set(b, buf);
if (lastState != null && patchSetId.equals(lastState.patchSetId)
&& lastState.approvals.containsKey(label.name())) {
b.setValue(lastState.approvals.get(label.name()) == value);
} else {
b.setValue(b.parseValue() == (prior != null ? prior : 0));
}
approvalButtons.add(b);
vp.add(b);
}
body.add(vp);
}
private void display(final PatchSetPublishDetail r) {
ChangeDetail changeDetail = new ChangeDetail();
changeDetail.setChange(r.getChange());
setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
patchSetId.get()));
descBlock.display(changeDetail, null, false, r.getPatchSetInfo(), r.getAccounts(),
r.getSubmitTypeRecord(), commentLinkProcessor);
if (r.getChange().getStatus().isOpen()) {
initApprovals(approvalPanel);
approvals.display(change);
} else {
approvals.setVisible(false);
}
if (lastState != null && patchSetId.equals(lastState.patchSetId)) {
message.setText(lastState.message);
}
draftsPanel.clear();
commentEditors = new ArrayList<CommentEditorPanel>();
revision = r.getPatchSetInfo().getRevId();
if (!r.getDrafts().isEmpty()) {
draftsPanel.add(new SmallHeading(Util.C.headingPatchComments()));
Panel panel = null;
String priorFile = "";
for (final PatchLineComment c : r.getDrafts()) {
final Patch.Key patchKey = c.getKey().getParentKey();
final String fn = patchKey.get();
if (!fn.equals(priorFile)) {
panel = new FlowPanel();
panel.addStyleName(Gerrit.RESOURCES.css().patchComments());
draftsPanel.add(panel);
// Parent table can be null here since we are not showing any
// next/previous links
panel.add(new PatchLink.SideBySide(
PatchTable.getDisplayFileName(patchKey), null, patchKey, 0, null, null));
priorFile = fn;
}
final CommentEditorPanel editor =
new CommentEditorPanel(c, commentLinkProcessor);
if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
Util.C.fileCommentHeader());
} else {
editor.setAuthorNameText(Gerrit.getUserAccountInfo(),
Util.M.lineHeader(c.getLine()));
}
editor.setOpen(true);
commentEditors.add(editor);
panel.add(editor);
}
}
submit.setVisible(r.canSubmit());
if (Gerrit.getConfig().testChangeMerge()) {
submit.setEnabled(r.getChange().isMergeable());
}
}
private void onSend(final boolean submit) {
if (commentEditors.isEmpty()) {
onSend2(submit);
} else {
final GerritCallback<VoidResult> afterSaveDraft =
new GerritCallback<VoidResult>() {
private int done;
@Override
public void onSuccess(final VoidResult result) {
if (++done == commentEditors.size()) {
onSend2(submit);
}
}
};
for (final CommentEditorPanel p : commentEditors) {
p.saveDraft(afterSaveDraft);
}
}
}
private void onSend2(final boolean submit) {
ReviewInput data = ReviewInput.create();
data.message(ChangeApi.emptyToNull(message.getText().trim()));
data.init();
for (final ValueRadioButton b : approvalButtons) {
if (b.getValue()) {
data.label(b.label.name(), b.parseValue());
}
}
enableForm(false);
new RestApi("/changes/")
.id(String.valueOf(patchSetId.getParentKey().get()))
.view("revisions").id(revision).view("review")
.post(data, new GerritCallback<ReviewInput>() {
@Override
public void onSuccess(ReviewInput result) {
if (submit) {
submit();
} else {
saveStateOnUnload = false;
goChange();
}
}
@Override
public void onFailure(Throwable caught) {
super.onFailure(caught);
enableForm(true);
}
});
}
private static class ReviewInput extends JavaScriptObject {
static ReviewInput create() {
return (ReviewInput) createObject();
}
final native void message(String m) /*-{ if(m)this.message=m; }-*/;
final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
final native void init() /*-{
this.labels = {};
this.strict_labels = true;
this.drafts = 'PUBLISH';
}-*/;
protected ReviewInput() {
}
}
private void submit() {
ChangeApi.submit(patchSetId.getParentKey().get(), revision,
new GerritCallback<SubmitInfo>() {
public void onSuccess(SubmitInfo result) {
saveStateOnUnload = false;
goChange();
}
@Override
public void onFailure(Throwable err) {
if (SubmitFailureDialog.isConflict(err)) {
new SubmitFailureDialog(err.getMessage()).center();
} else {
super.onFailure(err);
}
goChange();
}
});
}
private void goChange() {
final Change.Id ck = patchSetId.getParentKey();
Gerrit.display(PageLinks.toChange(ck), new ChangeScreen(ck));
}
private static class ValueRadioButton extends RadioButton {
final LabelInfo label;
final String value;
ValueRadioButton(LabelInfo label, String value) {
super(label.name());
this.label = label;
this.value = value;
}
String format() {
return new StringBuilder().append(value).append(' ')
.append(label.value_text(value)).toString();
}
short parseValue() {
String value = this.value;
if (value.startsWith(" ") || value.startsWith("+")) {
value = value.substring(1);
}
return Short.parseShort(value);
}
}
private static class SavedState {
final PatchSet.Id patchSetId;
final String message;
final Map<String, String> approvals;
SavedState(final PublishCommentScreen p) {
patchSetId = p.patchSetId;
message = p.message.getText();
approvals = new HashMap<String, String>();
for (final ValueRadioButton b : p.approvalButtons) {
if (b.getValue()) {
approvals.put(b.label.name(), b.value);
}
}
}
}
}