// 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.NativeMap; import com.google.gerrit.client.rpc.NativeString; 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.SubmitTypeRecord; import com.google.gerrit.extensions.common.ListChangesOption; import com.google.gerrit.extensions.common.SubmitType; 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.JsArray; 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.EnumSet; 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 Collection<ValueRadioButton> approvalButtons; private ChangeDescriptionBlock descBlock; private ApprovalTable approvals; private Panel approvalPanel; private NpTextArea message; private FlowPanel draftsPanel; private Button send; private Button cancel; private boolean saveStateOnUnload = true; private List<CommentEditorPanel> commentEditors; private ChangeInfo change; private ChangeInfo detail; private NativeMap<JsArray<CommentInfo>> drafts; private SubmitTypeRecord submitTypeRecord; 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<>(); 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); 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); cancel.setEnabled(enabled); } @Override protected void onLoad() { super.onLoad(); CallbackGroup group = new CallbackGroup(); RestApi call = ChangeApi.detail(patchSetId.getParentKey().get()); ChangeList.addOptions(call, EnumSet.of( ListChangesOption.CURRENT_ACTIONS, ListChangesOption.ALL_REVISIONS, ListChangesOption.ALL_COMMITS)); call.get(group.add(new GerritCallback<ChangeInfo>() { @Override public void onSuccess(ChangeInfo result) { detail = result; } })); ChangeApi.revision(patchSetId) .view("submit_type") .get(group.add(new GerritCallback<NativeString>() { @Override public void onSuccess(NativeString result) { submitTypeRecord = SubmitTypeRecord.OK( SubmitType.valueOf(result.asString())); } public void onFailure(Throwable caught) {} })); ChangeApi.revision(patchSetId.getParentKey().get(), "" + patchSetId.get()) .view("drafts") .get(group.add(new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() { @Override public void onSuccess(NativeMap<JsArray<CommentInfo>> result) { drafts = result; } public void onFailure(Throwable caught) {} })); ChangeApi.revision(patchSetId).view("review") .get(group.addFinal(new GerritCallback<ChangeInfo>() { @Override public void onSuccess(ChangeInfo result) { result.init(); change = result; preDisplay(result); } })); } private void preDisplay(final ChangeInfo info) { ConfigInfoCache.get(info.project_name_key(), new ScreenLoadCallback<ConfigInfoCache.Entry>(this) { @Override protected void preDisplay(ConfigInfoCache.Entry result) { send.setEnabled(true); commentLinkProcessor = result.getCommentLinkProcessor(); setTheme(result.getTheme()); displayScreen(); } @Override protected void postDisplay() { message.setFocus(true); } }); } @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 (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<>(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()).equals(value)); } else { b.setValue(b.parseValue() == (prior != null ? prior : 0)); } approvalButtons.add(b); vp.add(b); } body.add(vp); } private void displayScreen() { ChangeDetail r = ChangeDetailCache.reverse(detail); setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(), patchSetId.get())); descBlock.display(r, null, false, r.getCurrentPatchSetDetail().getInfo(), r.getAccounts(), submitTypeRecord, 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<>(); if (!drafts.isEmpty()) { draftsPanel.add(new SmallHeading(Util.C.headingPatchComments())); Panel panel = null; String priorFile = ""; for (final PatchLineComment c : draftList()) { 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); } } } 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())); 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(patchSetId.get()).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 void submit() { ChangeApi.submit( patchSetId.getParentKey().get(), "" + patchSetId.get(), 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 List<PatchLineComment> draftList() { List<PatchLineComment> d = new ArrayList<>(); List<String> paths = new ArrayList<>(drafts.keySet()); Collections.sort(paths); for (String path : paths) { JsArray<CommentInfo> comments = drafts.get(path); for (int i = 0; i < comments.length(); i++) { d.add(CommentEditorPanel.toComment(patchSetId, path, comments.get(i))); } } return d; } 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<>(); for (final ValueRadioButton b : p.approvalButtons) { if (b.getValue()) { approvals.put(b.label.name(), b.value); } } } } }