Redesign change screen on new REST API

The new screen exists at #/c2/<id> as it is missing a large number of
features.  The proposal is to include the screen as-is and iterate in
tree, similar to the CodeMirror work.  This allows power users to
start trying the screen out by editing the URL.

Missing features:
- Add reviewer(s)
- Remove reviewer(s)
- Edit commit message
- Diff files in two patch sets
- Dependencies and dependents
- Download links by URL and action (cherry-pick, checkout, etc.)

Change-Id: Ie957fb85c873d044c947000f0f0207a66f87c784
This commit is contained in:
Shawn Pearce 2013-07-14 19:39:45 -07:00
parent 0058c0a049
commit 097b957915
45 changed files with 3587 additions and 18 deletions

View File

@ -54,6 +54,14 @@ public class PageLinks {
return "/c/" + c + "/";
}
public static String toChange2(final Change.Id c) {
return "/c2/" + c + "/";
}
public static String toChange2(Change.Id c, String p) {
return "/c2/" + c + "/" + p;
}
public static String toChange(final PatchSet.Id ps) {
return "/c/" + ps.getParentKey() + "/" + ps.get();
}

View File

@ -60,6 +60,7 @@ import com.google.gerrit.client.admin.ProjectDashboardsScreen;
import com.google.gerrit.client.admin.ProjectInfoScreen;
import com.google.gerrit.client.admin.ProjectListScreen;
import com.google.gerrit.client.admin.ProjectScreen;
import com.google.gerrit.client.change.ChangeScreen2;
import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.client.changes.ChangeScreen;
import com.google.gerrit.client.changes.CustomDashboardScreen;
@ -190,6 +191,9 @@ public class Dispatcher {
} else if (matchPrefix("/c/", token)) {
change(token);
} else if (matchPrefix("/c2/", token)) {
change2(token);
} else if (matchExact(MINE, token)) {
Gerrit.display(token, mine(token));
@ -504,6 +508,20 @@ public class Dispatcher {
}
}
private static void change2(final String token) {
String rest = skip(token);
Change.Id id;
int s = rest.indexOf('/');
if (0 <= s) {
id = Change.Id.parse(rest.substring(0, s));
rest = rest.substring(s + 1);
} else {
id = Change.Id.parse(rest);
rest = "";
}
Gerrit.display(token, new ChangeScreen2(id, rest));
}
private static void publish(final PatchSet.Id ps) {
String token = toPublish(ps);
new AsyncSplit(token) {

View File

@ -14,6 +14,7 @@
package com.google.gerrit.client;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.common.data.GitWebType;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Branch;
@ -45,19 +46,26 @@ public class GitwebLink {
return !ps.isDraft() || type.getLinkDrafts();
}
public boolean canLink(RevisionInfo revision) {
return revision.draft() || type.getLinkDrafts();
}
public String getLinkName() {
return "(" + type.getLinkName() + ")";
}
public String toRevision(final Project.NameKey project, final PatchSet ps) {
public String toRevision(String project, String commit) {
ParameterizedString pattern = new ParameterizedString(type.getRevision());
final Map<String, String> p = new HashMap<String, String>();
p.put("project", encode(project.get()));
p.put("commit", encode(ps.getRevision().get()));
Map<String, String> p = new HashMap<String, String>();
p.put("project", encode(project));
p.put("commit", encode(commit));
return baseUrl + pattern.replace(p);
}
public String toRevision(final Project.NameKey project, final PatchSet ps) {
return toRevision(project.get(), ps.getRevision().get());
}
public String toProject(final Project.NameKey project) {
ParameterizedString pattern = new ParameterizedString(type.getProject());

View File

@ -0,0 +1,42 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Button;
class AbandonAction extends ActionMessageBox {
private final Change.Id id;
AbandonAction(Button b, Change.Id id) {
super(b);
this.id = id;
}
void send(String message) {
ChangeApi.abandon(id.get(), message, new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
Gerrit.display(PageLinks.toChange2(id));
hide();
}
});
}
}

View File

@ -0,0 +1,108 @@
// 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.ErrorDialog;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo.ActionInfo;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
class ActionButton extends Button implements ClickHandler {
private final Change.Id changeId;
private final String revision;
private final ActionInfo action;
ActionButton(Change.Id changeId, ActionInfo action) {
this(changeId, null, action);
}
ActionButton(Change.Id changeId, String revision, ActionInfo action) {
super(new SafeHtmlBuilder()
.openDiv()
.append(action.label())
.closeDiv());
setStyleName("");
setTitle(action.title());
setEnabled(action.enabled());
addClickHandler(this);
this.changeId = changeId;
this.revision = revision;
this.action = action;
}
@Override
public void onClick(ClickEvent event) {
if (action.confirmation_message() != null
&& !action.confirmation_message().isEmpty()) {
new ConfirmationDialog(
action.title(),
new SafeHtmlBuilder().append(action.confirmation_message()),
new ConfirmationCallback() {
@Override
public void onOk() {
send();
}
}).center();
} else {
send();
}
}
private void send() {
setEnabled(false);
AsyncCallback<NativeString> cb = new AsyncCallback<NativeString>() {
@Override
public void onFailure(Throwable caught) {
setEnabled(true);
new ErrorDialog(caught).center();
}
@Override
public void onSuccess(NativeString msg) {
setEnabled(true);
if (msg != null && !msg.asString().isEmpty()) {
// TODO Support better UI on UiCommand results.
Window.alert(msg.asString());
}
}
};
RestApi api = revision != null
? ChangeApi.revision(changeId.get(), revision)
: ChangeApi.change(changeId.get());
api.view(action.id());
if ("PUT".equalsIgnoreCase(action.method())) {
api.put(JavaScriptObject.createObject(), cb);
} else if ("DELETE".equalsIgnoreCase(action.method())) {
api.delete(cb);
} else {
api.post(JavaScriptObject.createObject(), cb);
}
}
}

View File

@ -0,0 +1,104 @@
// 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.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.resources.client.CssResource;
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.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.PopupPanel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
abstract class ActionMessageBox extends Composite {
interface Binder extends UiBinder<HTMLPanel, ActionMessageBox> {}
private static Binder uiBinder = GWT.create(Binder.class);
static interface Style extends CssResource {
String popup();
}
private final Button activatingButton;
private PluginSafePopupPanel popup;
@UiField Style style;
@UiField NpTextArea message;
@UiField Button send;
ActionMessageBox(Button button) {
this.activatingButton = button;
initWidget(uiBinder.createAndBindUi(this));
send.setText(button.getText());
}
abstract void send(String message);
void show() {
if (popup != null) {
popup.hide();
popup = null;
return;
}
final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
p.setStyleName(style.popup());
p.addAutoHidePartner(activatingButton.getElement());
p.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
if (popup == p) {
popup = null;
}
}
});
p.add(this);
p.showRelativeTo(activatingButton);
GlobalKey.dialog(p);
message.setFocus(true);
popup = p;
}
void hide() {
if (popup != null) {
popup.hide();
popup = null;
}
}
@UiHandler("message")
void onMessageKey(KeyPressEvent event) {
if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
&& event.isControlKeyDown()) {
event.preventDefault();
event.stopPropagation();
onSend(null);
}
}
@UiHandler("send")
void onSend(ClickEvent e) {
send(message.getValue().trim());
}
}

View File

@ -0,0 +1,47 @@
<?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:g='urn:import:com.google.gwt.user.client.ui'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style type='com.google.gerrit.client.change.ActionMessageBox.Style'>
@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
.popup { background-color: trimColor; }
.section {
padding: 5px 5px;
border-bottom: 1px solid #b8b8b8;
}
</ui:style>
<g:HTMLPanel>
<div class='{style.section}'>
<c:NpTextArea
visibleLines='3'
characterWidth='40'
ui:field='message'/>
</div>
<div class='{style.section}'>
<g:Button ui:field='send'
title='(Shortcut: Ctrl-Enter)'
styleName='{res.style.button}'>
<ui:attribute name='title'/>
<div><ui:msg>Send</ui:msg></div>
</g:Button>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,171 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.ActionInfo;
import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
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.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import java.util.TreeSet;
class Actions extends Composite {
private static final String[] CORE = {
"abandon", "restore", "revert", "topic",
"cherrypick", "submit", "rebase"};
interface Binder extends UiBinder<FlowPanel, Actions> {}
private static Binder uiBinder = GWT.create(Binder.class);
@UiField Button cherrypick;
@UiField Button rebase;
@UiField Button revert;
@UiField Button submit;
@UiField Button abandon;
private AbandonAction abandonAction;
@UiField Button restore;
private RestoreAction restoreAction;
private Change.Id changeId;
private String revision;
private String project;
private String subject;
private String message;
Actions() {
initWidget(uiBinder.createAndBindUi(this));
getElement().setId("change_actions");
}
void display(ChangeInfo info, String revision, boolean canSubmit) {
this.revision = revision;
boolean hasUser = Gerrit.isSignedIn();
RevisionInfo revInfo = info.revision(revision);
CommitInfo commit = revInfo.commit();
changeId = info.legacy_id();
project = info.project();
subject = commit.subject();
message = commit.message();
initChangeActions(info, hasUser);
initRevisionActions(info, revInfo, canSubmit, hasUser);
}
private void initChangeActions(ChangeInfo info, boolean hasUser) {
NativeMap<ActionInfo> actions = info.has_actions()
? info.actions()
: NativeMap.<ActionInfo> create();
actions.copyKeysIntoChildren("id");
abandon.setVisible(hasUser && actions.containsKey("abandon"));
restore.setVisible(hasUser && actions.containsKey("restore"));
revert.setVisible(hasUser && actions.containsKey("revert"));
if (hasUser) {
for (String id : filterNonCore(actions)) {
add(new ActionButton(changeId, actions.get(id)));
}
}
}
private void initRevisionActions(ChangeInfo info, RevisionInfo revInfo,
boolean canSubmit, boolean hasUser) {
boolean hasConflict = Gerrit.getConfig().testChangeMerge()
&& !info.mergeable();
NativeMap<ActionInfo> actions = revInfo.has_actions()
? revInfo.actions()
: NativeMap.<ActionInfo> create();
actions.copyKeysIntoChildren("id");
cherrypick.setVisible(hasUser && actions.containsKey("cherrypick"));
rebase.setVisible(hasUser && actions.containsKey("rebase"));
submit.setVisible(hasUser && !hasConflict
&& canSubmit
&& actions.containsKey("submit"));
if (hasUser) {
for (String id : filterNonCore(actions)) {
add(new ActionButton(changeId, revision, actions.get(id)));
}
}
}
private void add(ActionButton b) {
((FlowPanel) getWidget()).add(b);
}
private static TreeSet<String> filterNonCore(NativeMap<ActionInfo> m) {
TreeSet<String> ids = new TreeSet<String>(m.keySet());
for (String id : CORE) {
ids.remove(id);
}
return ids;
}
boolean isSubmitEnabled() {
return submit.isVisible() && submit.isEnabled();
}
@UiHandler("abandon")
void onAbandon(ClickEvent e) {
if (abandonAction == null) {
abandonAction = new AbandonAction(abandon, changeId);
}
abandonAction.show();
}
@UiHandler("restore")
void onRestore(ClickEvent e) {
if (restoreAction == null) {
restoreAction = new RestoreAction(restore, changeId);
}
restoreAction.show();
}
@UiHandler("rebase")
void onRebase(ClickEvent e) {
RebaseAction.call(changeId, revision);
}
@UiHandler("submit")
void onSubmit(ClickEvent e) {
SubmitAction.call(changeId, revision);
}
@UiHandler("cherrypick")
void onCherryPick(ClickEvent e) {
CherryPickAction.call(cherrypick, changeId, revision, project, message);
}
@UiHandler("revert")
void onRevert(ClickEvent e) {
RevertAction.call(cherrypick, changeId, revision, project, subject);
}
}

View File

@ -0,0 +1,93 @@
<?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:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style>
#change_actions {
padding-top: 2px;
padding-bottom: 20px;
}
#change_actions button {
margin: 6px 3px 0 0;
border-color: rgba(0, 0, 0, 0.1);
text-align: center;
font-size: 11px;
font-weight: bold;
border: 1px solid;
cursor: pointer;
color: #444;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
-webkit-border-radius: 2px;
-webkit-box-sizing: content-box;
}
#change_actions button div {
color: #444;
height: 10px;
min-width: 54px;
line-height: 10px;
white-space: nowrap;
}
#change_actions button {
color: #444;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
}
#change_actions button div {color: #444;}
#change_actions button.red {
color: #d14836;
background-color: #d14836;
background-image: -webkit-linear-gradient(top, #d14836, #d14836);
}
#change_actions button.red div {color: #fff;}
#change_actions button.submit {
float: right;
color: white;
background-color: #4d90fe;
background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
}
#change_actions button.submit div {color: #fff;}
</ui:style>
<g:FlowPanel>
<g:Button ui:field='cherrypick' styleName=''>
<div><ui:msg>Cherry Pick</ui:msg></div>
</g:Button>
<g:Button ui:field='rebase' styleName=''>
<div><ui:msg>Rebase</ui:msg></div>
</g:Button>
<g:Button ui:field='revert' styleName=''>
<div><ui:msg>Revert</ui:msg></div>
</g:Button>
<g:Button ui:field='abandon' styleName='{style.red}'>
<div><ui:msg>Abandon</ui:msg></div>
</g:Button>
<g:Button ui:field='restore' styleName='{style.red}'>
<div><ui:msg>Restore</ui:msg></div>
</g:Button>
<g:Button ui:field='submit' styleName='{style.submit}'>
<div><ui:msg>Submit</ui:msg></div>
</g:Button>
</g:FlowPanel>
</ui:UiBinder>

View File

@ -0,0 +1,462 @@
// 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.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
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.CommitInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.changes.StarredChanges;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.diff.DiffApi;
import com.google.gerrit.client.diff.FileInfo;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.projects.ConfigInfoCache.Entry;
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.ScreenLoadCallback;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.changes.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.CssResource;
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.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.clippy.client.CopyableLabel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ChangeScreen2 extends Screen {
interface Binder extends UiBinder<HTMLPanel, ChangeScreen2> {}
private static Binder uiBinder = GWT.create(Binder.class);
interface Style extends CssResource {
String labelName();
String label_user();
String label_ok();
String label_reject();
String label_may();
String label_need();
String replyBox();
}
private final Change.Id changeId;
private String revision;
private CommentLinkProcessor commentLinkProcessor;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
private List<HandlerRegistration> keys = new ArrayList<HandlerRegistration>(2);
@UiField Style style;
@UiField ToggleButton star;
@UiField Reload reload;
@UiField AnchorElement permalink;
@UiField Element reviewersText;
@UiField Element ccText;
@UiField Element changeIdText;
@UiField Element ownerText;
@UiField Element statusText;
@UiField Element projectText;
@UiField Element branchText;
@UiField Element submitActionText;
@UiField Element notMergeable;
@UiField CopyableLabel idText;
@UiField Topic topic;
@UiField Element actionText;
@UiField Element actionDate;
@UiField Actions actions;
@UiField Element revisionParent;
@UiField ListBox revisionList;
@UiField Labels labels;
@UiField CommitBox commit;
@UiField FileTable files;
@UiField FlowPanel history;
@UiField Button reply;
@UiField QuickApprove quickApprove;
private ReplyAction replyAction;
public ChangeScreen2(Change.Id changeId, String revision) {
this.changeId = changeId;
this.revision = revision != null && !revision.isEmpty() ? revision : null;
add(uiBinder.createAndBindUi(this));
}
@Override
protected void onLoad() {
super.onLoad();
ChangeApi.detail(changeId.get(),
EnumSet.of(
ListChangesOption.ALL_REVISIONS,
ListChangesOption.CURRENT_ACTIONS),
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
info.init();
loadConfigInfo(info);
}
});
}
@Override
protected void onUnload() {
for (HandlerRegistration h : keys) {
h.removeHandler();
}
keys.clear();
super.onUnload();
}
@Override
protected void onInitUI() {
super.onInitUI();
setHeaderVisible(false);
Resources.I.style().ensureInjected();
star.setVisible(Gerrit.isSignedIn());
labels.init(style, statusText);
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
keysNavigation.add(new KeyCommand(0, 'u', Util.C.upToChangeList()) {
@Override
public void onKeyPress(final KeyPressEvent event) {
Gerrit.displayLastChangeList();
}
});
keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReload()) {
@Override
public void onKeyPress(final KeyPressEvent event) {
reload.reload();
}
});
keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
if (Gerrit.isSignedIn()) {
keysAction.add(new KeyCommand(0, 'r', Util.C.keyPublishComments()) {
@Override
public void onKeyPress(KeyPressEvent event) {
onReply(null);
}
});
keysAction.add(new KeyCommand(0, 's', Util.C.changeTableStar()) {
@Override
public void onKeyPress(KeyPressEvent event) {
star.setValue(!star.getValue(), true);
}
});
}
}
@Override
public void registerKeys() {
super.registerKeys();
keys.add(GlobalKey.add(this, keysNavigation));
keys.add(GlobalKey.add(this, keysAction));
files.registerKeys();
}
@UiHandler("star")
void onToggleStar(ValueChangeEvent<Boolean> e) {
StarredChanges.toggleStar(changeId, e.getValue());
}
@UiHandler("revisionList")
void onChangeRevision(ChangeEvent e) {
int idx = revisionList.getSelectedIndex();
if (0 <= idx) {
String n = revisionList.getValue(idx);
revisionList.setEnabled(false);
Gerrit.display(
PageLinks.toChange2(changeId, n),
new ChangeScreen2(changeId, n));
}
}
@UiHandler("reply")
void onReply(ClickEvent e) {
replyAction.onReply();
}
private void loadConfigInfo(final ChangeInfo info) {
info.revisions().copyKeysIntoChildren("name");
final RevisionInfo rev = resolveRevisionToDisplay(info);
CallbackGroup group = new CallbackGroup();
loadDiff(rev, group);
loadCommit(rev, group);
ConfigInfoCache.get(info.project_name_key(),
group.add(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) {
@Override
protected void preDisplay(Entry result) {
commentLinkProcessor = result.getCommentLinkProcessor();
setTheme(result.getTheme());
renderChangeInfo(info);
}
}));
group.done();
if (info.status().isOpen() && rev.name().equals(info.current_revision())) {
loadSubmitAction(rev);
}
}
private void loadDiff(final RevisionInfo rev, CallbackGroup group) {
DiffApi.list(changeId.get(),
rev.name(),
group.add(new AsyncCallback<NativeMap<FileInfo>>() {
@Override
public void onSuccess(NativeMap<FileInfo> m) {
files.setRevisions(null, new PatchSet.Id(changeId, rev._number()));
files.setValue(m);
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
private void loadCommit(final RevisionInfo rev, CallbackGroup group) {
ChangeApi.revision(changeId.get(), rev.name())
.view("commit")
.get(group.add(new AsyncCallback<CommitInfo>() {
@Override
public void onSuccess(CommitInfo info) {
rev.set_commit(info);
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
private void loadSubmitAction(final RevisionInfo rev) {
// Submit action is less important than other data.
// Defer so browser can start other requests first.
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
ChangeApi.revision(changeId.get(), rev.name())
.view("submit_type")
.get(new AsyncCallback<NativeString>() {
@Override
public void onSuccess(NativeString result) {
String action = result.asString();
try {
SubmitType type = Project.SubmitType.valueOf(action);
submitActionText.setInnerText(
com.google.gerrit.client.admin.Util.toLongString(type));
} catch (IllegalArgumentException e) {
submitActionText.setInnerText(action);
}
}
@Override
public void onFailure(Throwable caught) {
}
});
}
});
}
private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
if (revision == null) {
revision = info.current_revision();
} else if (!info.revisions().containsKey(revision)) {
JsArray<RevisionInfo> list = info.revisions().values();
for (int i = 0; i < list.length(); i++) {
RevisionInfo r = list.get(i);
if (revision.equals(String.valueOf(r._number()))) {
revision = r.name();
break;
}
}
}
return info.revision(revision);
}
private void renderChangeInfo(ChangeInfo info) {
statusText.setInnerText(Util.toLongString(info.status()));
boolean canSubmit = labels.set(info);
renderOwner(info);
renderReviewers(info);
renderActionTextDate(info);
renderRevisions(info);
renderHistory(info);
actions.display(info, revision, canSubmit);
star.setValue(info.starred());
permalink.setHref(ChangeLink.permalink(changeId));
changeIdText.setInnerText(String.valueOf(info.legacy_id()));
projectText.setInnerText(info.project());
branchText.setInnerText(info.branch());
idText.setText("Change-Id: " + info.change_id());
idText.setPreviewText(info.change_id());
reload.set(info);
topic.set(info);
commit.set(commentLinkProcessor, info, revision);
quickApprove.set(info, revision);
boolean hasConflict = Gerrit.getConfig().testChangeMerge() && !info.mergeable();
setVisible(notMergeable, hasConflict);
if (Gerrit.isSignedIn()) {
replyAction = new ReplyAction(info, revision, style, reply);
if (topic.canEdit()) {
keysAction.add(new KeyCommand(0, 't', Util.C.keyEditTopic()) {
@Override
public void onKeyPress(KeyPressEvent event) {
topic.onEdit();
}
});
}
}
reply.setVisible(replyAction != null);
if (canSubmit && !hasConflict && actions.isSubmitEnabled()) {
statusText.setInnerText(Util.C.readyToSubmit());
} else if (canSubmit && hasConflict) {
statusText.setInnerText(Util.C.mergeConflict());
}
StringBuilder sb = new StringBuilder();
sb.append(Util.M.changeScreenTitleId(info.id_abbreviated()));
if (info.subject() != null) {
sb.append(": ");
sb.append(info.subject());
}
setWindowTitle(sb.toString());
}
private void renderReviewers(ChangeInfo info) {
// TODO Fix approximation of reviewers and CC list(s).
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);
}
reviewersText.setInnerSafeHtml(labels.formatUserList(r.values()));
ccText.setInnerSafeHtml(labels.formatUserList(cc.values()));
}
private void renderRevisions(ChangeInfo info) {
if (info.revisions().size() == 1) {
UIObject.setVisible(revisionParent, false);
return;
}
JsArray<RevisionInfo> list = info.revisions().values();
Collections.sort(Natives.asList(list), new Comparator<RevisionInfo>() {
@Override
public int compare(RevisionInfo a, RevisionInfo b) {
return a._number() - b._number();
}
});
int selected = -1;
for (int i = 0; i < list.length(); i++) {
RevisionInfo r = list.get(i);
revisionList.addItem(
r._number() + ": " + r.name().substring(0, 6),
"" + r._number());
if (revision.equals(r.name())) {
selected = i;
}
}
if (0 <= selected) {
revisionList.setSelectedIndex(selected);
}
}
private void renderOwner(ChangeInfo info) {
// TODO info card hover
ownerText.setInnerText(info.owner().name() != null
? info.owner().name()
: Gerrit.getConfig().getAnonymousCowardName());
}
private void renderActionTextDate(ChangeInfo info) {
String action;
if (info.created().equals(info.updated())) {
action = Util.C.changeInfoBlockUploaded();
} else {
action = Util.C.changeInfoBlockUpdated();
}
actionText.setInnerText(action);
actionDate.setInnerText(FormatUtil.relativeFormat(info.updated()));
}
private void renderHistory(ChangeInfo info) {
JsArray<MessageInfo> messages = info.messages();
if (messages != null) {
for (int i = 0; i < messages.length(); i++) {
history.add(new Message(commentLinkProcessor, messages.get(i)));
}
}
}
}

View File

@ -0,0 +1,283 @@
<?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.gerrit.client.change'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:clippy='urn:import:com.google.gwtexpui.clippy.client'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style type='com.google.gerrit.client.change.ChangeScreen2.Style'>
@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
@def INFO_WIDTH 450px;
@def HEADER_HEIGHT 29px;
.headerLine {
position: relative;
background-color: trimColor;
height: HEADER_HEIGHT;
}
.sectionHeader {
background-color: trimColor;
font-weight: bold;
color: textColor;
padding: 7px 10px;
}
.historyHeader {
width: 1150px;
}
.idBlock {
position: relative;
width: INFO_WIDTH;
height: HEADER_HEIGHT;
background-color: trimColor;
color: textColor;
font-family: sans-serif;
font-weight: bold;
}
.star {
cursor: pointer;
outline: none;
position: absolute;
left: 5px;
top: 5px;
}
.idLine, .idStatus {
line-height: HEADER_HEIGHT;
}
.idLine {
position: absolute;
top: 0;
left: 29px;
}
.idStatus {
position: absolute;
top: 0;
right: 26px;
}
.reload {
display: block;
position: absolute;
top: 7px;
right: 5px;
cursor: pointer;
}
.headerButtons {
position: absolute;
top: 0;
left: INFO_WIDTH;
height: HEADER_HEIGHT;
padding-left: 5px;
}
.revisionList {
position: absolute;
top: 2px;
right: 10px;
}
.headerTable {
border-spacing: 0;
width: 100%;
}
.headerTable th {
width: 60px;
color: #444;
font-weight: normal;
vertical-align: top;
text-align: left;
padding-right: 5px;
}
.clippy div {
float: right;
}
.infoColumn {
width: 440px;
padding-right: 10px;
vertical-align: top;
}
#change_infoTable {
border-spacing: 0;
width: 100%;
margin-left: 2px;
margin-right: 5px;
}
.notMergeable {
float: right;
font-weight: bold;
color: red;
}
.commitColumn {
padding: 0;
vertical-align: top;
}
.labels {
border-spacing: 0;
padding: 0;
}
.labelName {
color: #444;
vertical-align: top;
text-align: left;
padding-right: 5px;
white-space: nowrap;
}
.label_user {white-space: nowrap;}
.label_ok {color: #060;}
.label_reject {color: #d14836;}
.label_need {color: rgb(189, 189, 67);}
.label_may {color: #fff;}
.headerButtons button {
margin: 6px 3px 0 0;
border-color: rgba(0, 0, 0, 0.1);
text-align: center;
font-size: 11px;
font-weight: bold;
border: 1px solid;
cursor: pointer;
color: #444;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(top, #f5f5f5, #f1f1f1);
-webkit-border-radius: 2px;
-webkit-box-sizing: content-box;
}
.headerButtons button div {
color: #444;
height: 10px;
min-width: 54px;
line-height: 10px;
white-space: nowrap;
}
button.quickApprove {
color: #d14836;
background-color: #d14836;
background-image: -webkit-linear-gradient(top, #d14836, #d14836);
}
button.quickApprove div { color: #fff; }
.replyBox {
background-color: trimColor;
}
</ui:style>
<g:HTMLPanel>
<div class='{style.headerLine}'>
<div class='{style.idBlock}'>
<c:StarIcon ui:field='star' styleName='{style.star}'/>
<div class='{style.idLine}'>
<ui:msg>Change <span ui:field='changeIdText'/> by <span ui:field='ownerText'/></ui:msg>
</div>
<div ui:field='statusText' class='{style.idStatus}'/>
<a ui:field='permalink' class='{style.reload}'>
<c:Reload ui:field='reload'
title='Reload the change (Shortcut: R)'>
<ui:attribute name='title'/>
</c:Reload>
</a>
</div>
<div class='{style.headerButtons}'>
<g:Button ui:field='reply'
styleName=''
title='Reply and score (Shortcut: r)'>
<ui:attribute name='title'/>
<div><ui:msg>Reply&#8230;</ui:msg></div>
</g:Button>
<c:QuickApprove ui:field='quickApprove'
styleName='{style.quickApprove}'
title='Apply score with one click'>
<ui:attribute name='title'/>
</c:QuickApprove>
</div>
<div class='{style.revisionList}' ui:field='revisionParent'>
<ui:msg>Revision <g:ListBox ui:field='revisionList'/></ui:msg>
</div>
</div>
<table class='{style.headerTable}'>
<tr>
<td class='{style.infoColumn}'>
<table id='change_infoTable'>
<tr>
<th><ui:msg>Reviewers</ui:msg></th>
<td ui:field='reviewersText'/>
</tr>
<tr>
<th><ui:msg>CC</ui:msg></th>
<td ui:field='ccText'/>
</tr>
<tr>
<th><ui:msg>Project</ui:msg></th>
<td ui:field='projectText'/>
</tr>
<tr>
<th><ui:msg>Branch</ui:msg></th>
<td ui:field='branchText'/>
</tr>
<tr>
<th><ui:msg>Strategy</ui:msg></th>
<td>
<span ui:field='submitActionText'/>
<div ui:field='notMergeable' class='{style.notMergeable}'>
<ui:msg>Cannot Merge</ui:msg>
</div>
</td>
</tr>
<tr><td colspan='2'><c:Actions ui:field='actions'/></td></tr>
<tr>
<th ui:field='actionText'/>
<td ui:field='actionDate'/>
</tr>
<tr>
<th><ui:msg>Id</ui:msg></th>
<td><clippy:CopyableLabel styleName='{style.clippy}' ui:field='idText'/></td>
</tr>
<tr>
<th><ui:msg>Topic</ui:msg></th>
<td><c:Topic ui:field='topic'/></td>
</tr>
</table>
<hr/>
<c:Labels ui:field='labels' styleName='{style.labels}'/>
</td>
<td class='{style.commitColumn}'>
<c:CommitBox ui:field='commit'/>
</td>
</tr>
</table>
<div class='{style.sectionHeader}'
><ui:msg>Files</ui:msg></div>
<c:FileTable ui:field='files'/>
<div class='{style.sectionHeader} {style.historyHeader}'
><ui:msg>History</ui:msg></div>
<g:FlowPanel ui:field='history'/>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,63 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.CherryPickDialog;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.user.client.ui.Button;
class CherryPickAction {
static void call(Button b, final Change.Id id, final String revision,
String project, final String commitMessage) {
// TODO Replace CherryPickDialog with a nicer looking display.
b.setEnabled(false);
new CherryPickDialog(b, new Project.NameKey(project)) {
{
sendButton.setText(Util.C.buttonCherryPickChangeSend());
message.setText(Util.M.cherryPickedChangeDefaultMessage(
commitMessage.trim(),
revision));
}
@Override
public void onSend() {
ChangeApi.cherrypick(id.get(), revision,
getDestinationBranch(),
getMessageText(),
new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
sent = true;
hide();
Gerrit.display(PageLinks.toChange2(result.legacy_id()));
}
@Override
public void onFailure(Throwable caught) {
enableButtons(true);
super.onFailure(caught);
}
});
}
}.center();
}
}

View File

@ -0,0 +1,76 @@
// 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.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.CommitInfo;
import com.google.gerrit.client.changes.ChangeInfo.GitPerson;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
class CommitBox extends Composite {
interface Binder extends UiBinder<HTMLPanel, CommitBox> {}
private static Binder uiBinder = GWT.create(Binder.class);
@UiField Element commitName;
@UiField AnchorElement browserLink;
@UiField Element authorNameEmail;
@UiField Element authorDate;
@UiField Element committerNameEmail;
@UiField Element committerDate;
@UiField Element commitMessageText;
CommitBox() {
initWidget(uiBinder.createAndBindUi(this));
}
void set(CommentLinkProcessor commentLinkProcessor,
ChangeInfo change,
String revision) {
RevisionInfo revInfo = change.revision(revision);
CommitInfo commit = revInfo.commit();
commitName.setInnerText(revision);
format(commit.author(), authorNameEmail, authorDate);
format(commit.committer(), committerNameEmail, committerDate);
commitMessageText.setInnerSafeHtml(commentLinkProcessor.apply(
new SafeHtmlBuilder().append(commit.message()).linkify()));
GitwebLink gw = Gerrit.getGitwebLink();
if (gw != null && gw.canLink(revInfo)) {
browserLink.setInnerText(gw.getLinkName());
browserLink.setHref(gw.toRevision(change.project(), revision));
} else {
UIObject.setVisible(browserLink, false);
}
}
private void format(GitPerson person, Element name, Element date) {
name.setInnerText(person.name() + " <" + person.email() + ">");
date.setInnerText(FormatUtil.mediumFormat(person.date()));
}
}

View File

@ -0,0 +1,63 @@
<?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:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style>
.commitHeader {
border-spacing: 0;
padding: 0;
width: 564px;
}
.commitHeader th { width: 70px; }
.commitHeader td { white-space: pre; }
.commitMessageBox { margin: 2px; }
.commitMessage {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border: 1px solid white;
background-color: white;
font-family: monospace;
white-space: pre;
width: 47em;
}
</ui:style>
<g:HTMLPanel>
<table class='{style.commitHeader}'>
<tr>
<th><ui:msg>Commit</ui:msg></th>
<td ui:field='commitName'/>
<td><a ui:field='browserLink' href=""/></td>
</tr>
<tr>
<th><ui:msg>Author</ui:msg></th>
<td ui:field='authorNameEmail'/>
<td ui:field='authorDate'/>
</tr>
<tr>
<th><ui:msg>Committer</ui:msg></th>
<td ui:field='committerNameEmail'/>
<td ui:field='committerDate'/>
</tr>
</table>
<div class='{style.commitMessageBox}'>
<div class='{style.commitMessage}' ui:field='commitMessageText'/>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,354 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.diff.FileInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwtexpui.progress.client.ProgressBar;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtorm.client.KeyUtil;
import java.util.Collections;
import java.util.Comparator;
class FileTable extends FlowPanel {
private static final FileTableResources R = GWT
.create(FileTableResources.class);
interface FileTableResources extends ClientBundle {
@Source("file_table.css")
FileTableCss css();
}
interface FileTableCss extends CssResource {
String pointer();
String pathColumn();
String deltaColumn1();
String deltaColumn2();
String inserted();
String deleted();
}
private PatchSet.Id base;
private PatchSet.Id curr;
private MyTable table;
private boolean register;
@Override
protected void onLoad() {
super.onLoad();
R.css().ensureInjected();
}
void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
this.base = base;
this.curr = curr;
}
void setValue(NativeMap<FileInfo> fileMap) {
JsArray<FileInfo> list = fileMap.values();
Collections.sort(Natives.asList(list), new Comparator<FileInfo>() {
@Override
public int compare(FileInfo a, FileInfo b) {
if (Patch.COMMIT_MSG.equals(a.path())) {
return -1;
} else if (Patch.COMMIT_MSG.equals(b.path())) {
return 1;
}
return a.path().compareTo(b.path());
}
});
DisplayCommand cmd = new DisplayCommand(list);
if (cmd.execute()) {
cmd.showProgressBar();
Scheduler.get().scheduleIncremental(cmd);
}
}
void registerKeys() {
register = true;
if (table != null) {
table.setRegisterKeys(true);
}
}
private void setTable(MyTable table) {
clear();
add(table);
this.table = table;
if (register) {
table.setRegisterKeys(true);
}
}
private String url(FileInfo info) {
// TODO(sop): Switch to Dispatcher.toPatchSideBySide.
Change.Id c = curr.getParentKey();
StringBuilder p = new StringBuilder();
p.append("/c/").append(c).append('/');
if (base != null) {
p.append(base.get()).append("..");
}
p.append(curr.get()).append('/').append(KeyUtil.encode(info.path()));
p.append(info.binary() ? ",unified" : ",codemirror");
return p.toString();
}
private final class MyTable extends NavigationTable<FileInfo> {
private final JsArray<FileInfo> list;
MyTable(JsArray<FileInfo> list) {
this.list = list;
table.setWidth("");
keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()));
keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
Util.C.patchTableOpenDiff()));
setSavePointerId(
(base != null ? base.toString() + ".." : "")
+ curr.toString());
}
@Override
protected Object getRowItemKey(FileInfo item) {
return item.path();
}
@Override
protected FileInfo getRowItem(int row) {
if (1 <= row && row <= list.length()) {
return list.get(row - 1);
}
return null;
}
@Override
protected void onOpenRow(int row) {
if (1 <= row && row <= list.length()) {
Gerrit.display(url(list.get(row - 1)));
}
}
}
private final class DisplayCommand implements RepeatingCommand {
private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
private final MyTable table;
private final JsArray<FileInfo> list;
private boolean attached;
private int row;
private double start;
private ProgressBar meter;
private int inserted;
private int deleted;
private DisplayCommand(JsArray<FileInfo> list) {
this.table = new MyTable(list);
this.list = list;
}
public boolean execute() {
boolean attachedNow = isAttached();
if (!attached && attachedNow) {
// Remember that we have been attached at least once. If
// later we find we aren't attached we should stop running.
attached = true;
} else if (attached && !attachedNow) {
// If the user navigated away, we aren't in the DOM anymore.
// Don't continue to render.
return false;
}
start = System.currentTimeMillis();
if (row == 0) {
header(sb);
computeInsertedDeleted();
}
while (row < list.length()) {
render(sb, list.get(row));
if ((++row % 10) == 0 && longRunning()) {
updateMeter();
return true;
}
}
footer(sb);
table.resetHtml(sb);
table.finishDisplay();
setTable(table);
return false;
}
private void computeInsertedDeleted() {
inserted = 0;
deleted = 0;
for (int i = 0; i < list.length(); i++) {
FileInfo info = list.get(i);
if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
inserted += info.lines_inserted();
deleted += info.lines_deleted();
}
}
}
void showProgressBar() {
if (meter == null) {
meter = new ProgressBar(Util.M.loadingPatchSet(curr.get()));
FileTable.this.clear();
FileTable.this.add(meter);
}
updateMeter();
}
void updateMeter() {
if (meter != null) {
int n = list.length();
meter.setValue((100 * row) / n);
}
}
private boolean longRunning() {
return System.currentTimeMillis() - start > 200;
}
private void header(SafeHtmlBuilder sb) {
sb.openTr();
sb.openTh().setStyleName(Gerrit.RESOURCES.css().iconCell()).closeTh();
sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
sb.openTh()
.setAttribute("colspan", 2)
.append(Util.C.patchTableColumnSize())
.closeTh();
sb.closeTr();
}
private void render(SafeHtmlBuilder sb, FileInfo info) {
sb.openTr();
sb.openTd().setStyleName(R.css().pointer()).closeTd();
columnPath(sb, info);
columnDelta1(sb, info);
columnDelta2(sb, info);
sb.closeTr();
}
private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
// TODO(sop): Use JS to link, avoiding early URL update.
sb.openTd()
.setStyleName(R.css().pathColumn())
.openAnchor()
.setAttribute("href", "#" + url(info))
.append(Patch.COMMIT_MSG.equals(info.path())
? Util.C.commitMessage()
: info.path())
.closeAnchor()
.closeTd();
}
private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().deltaColumn1());
if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
sb.append(info.lines_inserted() - info.lines_deleted());
}
sb.closeTd();
}
private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().deltaColumn2());
if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
&& (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
int w = 80;
int t = inserted + deleted;
int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
sb.setAttribute(
"title",
Util.M.patchTableSize_LongModify(info.lines_inserted(),
info.lines_deleted()));
if (0 < info.lines_inserted()) {
sb.openDiv()
.setStyleName(R.css().inserted())
.setAttribute("style", "width:" + i + "px")
.closeDiv();
}
if (0 < info.lines_deleted()) {
sb.openDiv()
.setStyleName(R.css().deleted())
.setAttribute("style", "width:" + d + "px")
.closeDiv();
}
}
sb.closeTd();
}
private void footer(SafeHtmlBuilder sb) {
sb.openTr();
sb.openTd().setStyleName(Gerrit.RESOURCES.css().iconCell()).closeTd();
sb.openTd().closeTd(); // path
// delta1
sb.openTh().setStyleName(R.css().deltaColumn1())
.append(Util.M.patchTableSize_Modify(inserted, deleted))
.closeTh();
// delta2
sb.openTh().setStyleName(R.css().deltaColumn2());
int w = 80;
int t = inserted + deleted;
int i = Math.max(1, (int) (((double) w) * inserted / t));
int d = Math.max(1, (int) (((double) w) * deleted / t));
if (i + d > w && i > d) {
i = w - d;
} else if (i + d > w && d > i) {
d = w - i;
}
if (0 < inserted) {
sb.openDiv()
.setStyleName(R.css().inserted())
.setAttribute("style", "width:" + i + "px")
.closeDiv();
}
if (0 < deleted) {
sb.openDiv()
.setStyleName(R.css().deleted())
.setAttribute("style", "width:" + d + "px")
.closeDiv();
}
sb.closeTh();
sb.closeTr();
}
}
}

View File

@ -0,0 +1,211 @@
// 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.account.AccountInfo;
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.rpc.Natives;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Displays a table of label and reviewer scores. */
class Labels extends Grid {
private ChangeScreen2.Style style;
private Element statusText;
void init(ChangeScreen2.Style style, Element statusText) {
this.style = style;
this.statusText = statusText;
}
boolean set(ChangeInfo info) {
List<String> names = new ArrayList<String>(info.labels());
Collections.sort(names);
boolean canSubmit = info.status().isOpen();
resize(names.size(), 2);
for (int row = 0; row < names.size(); row++) {
String name = names.get(row);
LabelInfo label = info.label(name);
setText(row, 0, name);
if (label.all() != null) {
setWidget(row, 1, renderUsers(label));
}
getCellFormatter().setStyleName(row, 0, style.labelName());
getCellFormatter().addStyleName(row, 0, getStyleForLabel(label));
if (canSubmit && info.status() == Change.Status.NEW) {
switch (label.status()) {
case NEED:
statusText.setInnerText("Needs " + name);
canSubmit = false;
break;
case REJECT:
case IMPOSSIBLE:
statusText.setInnerText("Not " + name);
canSubmit = false;
break;
default:
break;
}
}
}
return canSubmit;
}
private Widget renderUsers(LabelInfo label) {
Map<Integer, List<ApprovalInfo>> m = new HashMap<Integer, List<ApprovalInfo>>(4);
int approved = 0, rejected = 0;
for (ApprovalInfo ai : Natives.asList(label.all())) {
if (ai.value() != 0) {
List<ApprovalInfo> l = m.get(Integer.valueOf(ai.value()));
if (l == null) {
l = new ArrayList<ApprovalInfo>(label.all().length());
m.put(Integer.valueOf(ai.value()), l);
}
l.add(ai);
if (isRejected(label, ai)) {
rejected = ai.value();
} else if (isApproved(label, ai)) {
approved = ai.value();
}
}
}
SafeHtmlBuilder html = new SafeHtmlBuilder();
for (Integer v : sort(m.keySet(), approved, rejected)) {
if (!html.isEmpty()) {
html.append("; ");
}
String val = LabelValue.formatValue(v.shortValue());
html.openSpan();
html.setAttribute("title", label.value_text(val));
if (v.intValue() == approved) {
html.setStyleName(style.label_ok());
} else if (v.intValue() == rejected) {
html.setStyleName(style.label_reject());
}
html.append(val).append(" ");
html.append(formatUserList(m.get(v)));
html.closeSpan();
}
return html.toBlockWidget();
}
private static List<Integer> sort(Set<Integer> keySet, int a, int b) {
List<Integer> r = new ArrayList<Integer>(keySet);
Collections.sort(r);
if (keySet.contains(a)) {
r.remove(Integer.valueOf(a));
r.add(0, a);
} else if (keySet.contains(b)) {
r.remove(Integer.valueOf(b));
r.add(0, b);
}
return r;
}
private static boolean isApproved(LabelInfo label, ApprovalInfo ai) {
return label.approved() != null
&& label.approved()._account_id() == ai._account_id();
}
private static boolean isRejected(LabelInfo label, ApprovalInfo ai) {
return label.rejected() != null
&& label.rejected()._account_id() == ai._account_id();
}
private String getStyleForLabel(LabelInfo label) {
switch (label.status()) {
case OK:
return style.label_ok();
case NEED:
return style.label_need();
case REJECT:
case IMPOSSIBLE:
return style.label_reject();
default:
case MAY:
return style.label_may();
}
}
SafeHtml formatUserList(Collection<? extends AccountInfo> in) {
List<AccountInfo> users = new ArrayList<AccountInfo>(in);
Collections.sort(users, new Comparator<AccountInfo>() {
@Override
public int compare(AccountInfo a, AccountInfo b) {
String as = name(a);
String bs = name(b);
if (as.isEmpty()) {
return 1;
} else if (bs.isEmpty()) {
return -1;
}
return as.compareTo(bs);
}
private String name(AccountInfo a) {
if (a.name() != null) {
return a.name();
} else if (a.email() != null) {
return a.email();
}
return "";
}
});
SafeHtmlBuilder html = new SafeHtmlBuilder();
Iterator<? extends AccountInfo> itr = users.iterator();
while (itr.hasNext()) {
AccountInfo ai = itr.next();
html.openSpan();
html.setStyleName(style.label_user());
if (ai.name() != null) {
html.append(ai.name());
} else if (ai.email() != null) {
html.append(ai.email());
} else {
html.append(ai._account_id());
}
html.closeSpan();
if (itr.hasNext()) {
html.append(", ");
}
}
return html;
}
}

View File

@ -0,0 +1,101 @@
// 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.AvatarImage;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo.MessageInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.ui.CommentLinkProcessor;
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.ClickHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
class Message extends Composite {
interface Binder extends UiBinder<HTMLPanel, Message> {}
private static Binder uiBinder = GWT.create(Binder.class);
static interface Style extends CssResource {
String closed();
}
@UiField Style style;
@UiField Element name;
@UiField Element summary;
@UiField Element date;
@UiField Element message;
@UiField(provided = true)
AvatarImage avatar;
Message(CommentLinkProcessor clp, MessageInfo info) {
if (info.author() != null) {
avatar = new AvatarImage(info.author(), 26);
avatar.setSize("", "");
} else {
avatar = new AvatarImage();
}
initWidget(uiBinder.createAndBindUi(this));
addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
setOpen(!isOpen());
}
}, ClickEvent.getType());
name.setInnerText(authorName(info));
date.setInnerText(FormatUtil.shortFormatDayTime(info.date()));
if (info.message() != null) {
String msg = info.message().trim();
summary.setInnerText(msg);
message.setInnerSafeHtml(clp.apply(
new SafeHtmlBuilder().append(msg).wikify()));
}
}
private boolean isOpen() {
return UIObject.isVisible(message);
}
private void setOpen(boolean open) {
UIObject.setVisible(summary, !open);
UIObject.setVisible(message, open);
if (open) {
removeStyleName(style.closed());
} else {
addStyleName(style.closed());
}
}
private static String authorName(MessageInfo info) {
if (info.author() != null) {
if (info.author().name() != null) {
return info.author().name();
}
return Gerrit.getConfig().getAnonymousCowardName();
}
return Util.C.messageNoAuthor();
}
}

View File

@ -0,0 +1,93 @@
<?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.gerrit.client'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style type='com.google.gerrit.client.change.Message.Style'>
.messageBox {
width: 1168px;
border-left: 1px solid #e3e9ff;
border-right: 1px solid #e3e9ff;
border-bottom: 1px solid #e3e9ff;
-webkit-border-bottom-left-radius: 8px;
-webkit-border-bottom-right-radius: 8px;
}
.avatar {
width: 26px;
height: 26px;
}
.closed .avatar {
width: 16px;
height: 16px;
}
.contents {
margin-left: 28px;
position: relative;
}
.contents p {
-webkit-margin-before: 0;
-webkit-margin-after: 0.3em;
}
.name {
white-space: nowrap;
font-weight: bold;
}
.closed .name {
width: 120px;
overflow: hidden;
text-overflow: ellipsis;
font-weight: normal;
}
.summary {
color: #777;
position: absolute;
top: 0;
left: 120px;
width: 915px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.date {
white-space: nowrap;
position: absolute;
top: 0;
right: 5px;
}
</ui:style>
<g:HTMLPanel
styleName='{style.messageBox}'
addStyleNames='{style.closed}'>
<c:AvatarImage ui:field='avatar' styleName='{style.avatar}'/>
<div class='{style.contents}'>
<div class='{style.name}' ui:field='name'/>
<div ui:field='summary' class='{style.summary}'/>
<div class='{style.date}' ui:field='date'/>
<div ui:field='message'
aria-hidden='true'
style='display: NONE'/>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,119 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.changes.ReviewInput;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
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.ui.Button;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
/** Applies a label with one mouse click. */
class QuickApprove extends Button implements ClickHandler {
private Change.Id changeId;
private String revision;
private ReviewInput input;
QuickApprove() {
addClickHandler(this);
}
void set(ChangeInfo info, String commit) {
if (!info.has_permitted_labels() || !info.status().isOpen()) {
// Quick approve needs at least one label on an open change.
setVisible(false);
return;
}
String qName = null;
String qValueStr = null;
short qValue = 0;
for (LabelInfo label : Natives.asList(info.all_labels().values())) {
if (!info.permitted_labels().containsKey(label.name())) {
continue;
}
JsArrayString values = info.permitted_values(label.name());
if (values.length() == 0) {
continue;
}
switch (label.status()) {
case NEED: // Label is required for submit.
break;
case OK: // Label already applied.
case MAY: // Label is not required.
continue;
case REJECT: // Submit cannot happen, do not quick approve.
case IMPOSSIBLE:
setVisible(false);
return;
}
String s = values.get(values.length() - 1);
short v = LabelInfo.parseValue(s);
if (v > 0 && s.equals(label.max_value())) {
if (qName != null) {
// Quick approve is available for one label only.
setVisible(false);
return;
}
qName = label.name();
qValueStr = s;
qValue = v;
}
}
if (qName != null) {
changeId = info.legacy_id();
revision = commit;
input = ReviewInput.create();
input.label(qName, qValue);
setText(qName + qValueStr);
} else {
setVisible(false);
}
}
@Override
public void setText(String text) {
setHTML(new SafeHtmlBuilder().openDiv().append(text).closeDiv());
}
@Override
public void onClick(ClickEvent event) {
ChangeApi.revision(changeId.get(), revision)
.view("review")
.post(input, new GerritCallback<ReviewInput>() {
@Override
public void onSuccess(ReviewInput result) {
Gerrit.display(PageLinks.toChange2(changeId));
}
});
}
}

View File

@ -0,0 +1,33 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
class RebaseAction {
static void call(final Change.Id id, String revision) {
ChangeApi.rebase(id.get(), revision,
new GerritCallback<ChangeInfo>() {
public void onSuccess(ChangeInfo result) {
Gerrit.display(PageLinks.toChange2(id));
}
});
}
}

View File

@ -0,0 +1,56 @@
package com.google.gerrit.client.change;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.user.client.ui.Image;
class Reload extends Image implements ClickHandler,
MouseOverHandler, MouseOutHandler {
private Change.Id changeId;
private boolean in;
Reload() {
setResource(Resources.I.reload_black());
addClickHandler(this);
addMouseOverHandler(this);
addMouseOutHandler(this);
}
void set(ChangeInfo info) {
changeId = info.legacy_id();
}
void reload() {
Gerrit.display(PageLinks.toChange2(changeId));
}
@Override
public void onMouseOver(MouseOverEvent event) {
if (!in) {
in = true;
setResource(Resources.I.reload_white());
}
}
@Override
public void onMouseOut(MouseOutEvent event) {
if (in) {
in = false;
setResource(Resources.I.reload_black());
}
}
@Override
public void onClick(ClickEvent e) {
e.preventDefault();
e.stopPropagation();
}
}

View File

@ -0,0 +1,90 @@
// 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.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
class ReplyAction {
private final Change.Id changeId;
private final String revision;
private final ChangeScreen2.Style style;
private final Widget replyButton;
private NativeMap<LabelInfo> allLabels;
private NativeMap<JsArrayString> permittedLabels;
private ReplyBox replyBox;
private PopupPanel popup;
ReplyAction(
ChangeInfo info,
String revision,
ChangeScreen2.Style style,
Widget replyButton) {
this.changeId = info.legacy_id();
this.revision = revision;
this.style = style;
this.replyButton = replyButton;
allLabels = info.all_labels();
permittedLabels = info.has_permitted_labels()
? info.permitted_labels()
: NativeMap.<JsArrayString> create();
}
void onReply() {
if (popup != null) {
popup.hide();
return;
}
if (replyBox == null) {
replyBox = new ReplyBox(
changeId,
revision,
allLabels,
permittedLabels);
allLabels = null;
permittedLabels = null;
}
final PluginSafePopupPanel p = new PluginSafePopupPanel(true);
p.setStyleName(style.replyBox());
p.addAutoHidePartner(replyButton.getElement());
p.addCloseHandler(new CloseHandler<PopupPanel>() {
@Override
public void onClose(CloseEvent<PopupPanel> event) {
if (popup == p) {
popup = null;
}
}
});
p.add(replyBox);
p.showRelativeTo(replyButton);
GlobalKey.dialog(p);
popup = p;
}
}

View File

@ -0,0 +1,277 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
import com.google.gerrit.client.changes.ReviewInput;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
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.KeyPressEvent;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.resources.client.CssResource;
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.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
class ReplyBox extends Composite {
interface Binder extends UiBinder<HTMLPanel, ReplyBox> {}
private static Binder uiBinder = GWT.create(Binder.class);
interface Styles extends CssResource {
String label_name();
String label_value();
}
private final Change.Id changeId;
private final String revision;
private ReviewInput in = ReviewInput.create();
private List<Runnable> lgtm;
@UiField Styles style;
@UiField NpTextArea message;
@UiField Element labelsParent;
@UiField Grid labelsTable;
@UiField Button send;
@UiField CheckBox email;
ReplyBox(
Change.Id changeId,
String revision,
NativeMap<LabelInfo> all,
NativeMap<JsArrayString> permitted) {
this.changeId = changeId;
this.revision = revision;
initWidget(uiBinder.createAndBindUi(this));
List<String> names = new ArrayList<String>(permitted.keySet());
if (names.isEmpty()) {
UIObject.setVisible(labelsParent, false);
} else {
Collections.sort(names);
lgtm = new ArrayList<Runnable>(names.size());
renderLabels(names, all, permitted);
}
}
@Override
protected void onLoad() {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
message.setFocus(true);
}});
}
@UiHandler("message")
void onMessageKey(KeyPressEvent event) {
if ((event.getCharCode() == '\n' || event.getCharCode() == KeyCodes.KEY_ENTER)
&& event.isControlKeyDown()) {
event.preventDefault();
event.stopPropagation();
onSend(null);
} else if (lgtm != null
&& event.getCharCode() == 'M'
&& message.getValue().equals("LGT")) {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
if (message.getValue().startsWith("LGTM")) {
for (Runnable r : lgtm) {
r.run();
}
}
}
});
}
}
@UiHandler("email")
void onEmail(ValueChangeEvent<Boolean> e) {
if (e.getValue()) {
in.notify(ReviewInput.NotifyHandling.ALL);
} else {
in.notify(ReviewInput.NotifyHandling.NONE);
}
}
@UiHandler("send")
void onSend(ClickEvent e) {
in.message(message.getText().trim());
ChangeApi.revision(changeId.get(), revision)
.view("review")
.post(in, new GerritCallback<ReviewInput>() {
@Override
public void onSuccess(ReviewInput result) {
Gerrit.display(PageLinks.toChange2(changeId));
}
});
hide();
}
private void hide() {
for (Widget w = getParent(); w != null; w = w.getParent()) {
if (w instanceof PopupPanel) {
((PopupPanel) w).hide();
break;
}
}
}
private void renderLabels(
List<String> names,
NativeMap<LabelInfo> all,
NativeMap<JsArrayString> permitted) {
TreeSet<Short> values = new TreeSet<Short>();
for (String id : names) {
JsArrayString p = permitted.get(id);
if (p != null) {
for (int i = 0; i < p.length(); i++) {
values.add(LabelInfo.parseValue(p.get(i)));
}
}
}
List<Short> columns = new ArrayList<Short>(values);
labelsTable.resize(1 + permitted.size(), 1 + values.size());
for (int c = 0; c < columns.size(); c++) {
labelsTable.setText(0, 1 + c, LabelValue.formatValue(columns.get(c)));
labelsTable.getCellFormatter().setStyleName(0, 1 + c, style.label_value());
}
List<String> checkboxes = new ArrayList<String>(permitted.size());
int row = 1;
for (String id : names) {
Set<Short> vals = all.get(id).value_set();
if (isCheckBox(vals)) {
checkboxes.add(id);
} else {
renderRadio(row++, id, columns, vals, all.get(id));
}
}
for (String id : checkboxes) {
renderCheckBox(row++, id, all.get(id));
}
}
private void renderRadio(int row, final String id,
List<Short> columns,
Set<Short> values,
LabelInfo info) {
labelsTable.setText(row, 0, id);
labelsTable.getCellFormatter().setStyleName(row, 0, style.label_name());
ApprovalInfo self = Gerrit.isSignedIn()
? info.for_user(Gerrit.getUserAccount().getId().get())
: null;
final List<RadioButton> group = new ArrayList<RadioButton>(values.size());
for (int i = 0; i < columns.size(); i++) {
final Short v = columns.get(i);
if (values.contains(v)) {
RadioButton b = new RadioButton(id);
b.setTitle(info.value_text(LabelValue.formatValue(v)));
if ((self != null && v == self.value()) || (self == null && v == 0)) {
b.setValue(true);
}
b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
if (event.getValue()) {
in.label(id, v);
}
}
});
group.add(b);
labelsTable.setWidget(row, 1 + i, b);
}
}
if (!group.isEmpty()) {
lgtm.add(new Runnable() {
@Override
public void run() {
for (int i = 0; i < group.size() - 1; i++) {
group.get(i).setValue(false, false);
}
group.get(group.size() - 1).setValue(true, true);
}
});
}
}
private void renderCheckBox(int row, final String id, LabelInfo info) {
ApprovalInfo self = Gerrit.isSignedIn()
? info.for_user(Gerrit.getUserAccount().getId().get())
: null;
final CheckBox b = new CheckBox();
b.setText(id);
b.setTitle(info.value_text("+1"));
if (self != null && self.value() == 1) {
b.setValue(true);
}
b.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
in.label(id, event.getValue() ? (short) 1 : (short) 0);
}
});
b.setStyleName(style.label_name());
labelsTable.setWidget(row, 0, b);
lgtm.add(new Runnable() {
@Override
public void run() {
b.setValue(true, true);
}
});
}
private static boolean isCheckBox(Set<Short> values) {
return values.size() == 2
&& values.contains((short) 0)
&& values.contains((short) 1);
}
}

View File

@ -0,0 +1,71 @@
<?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:g='urn:import:com.google.gwt.user.client.ui'
xmlns:c='urn:import:com.google.gwtexpui.globalkey.client'>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style type='com.google.gerrit.client.change.ReplyBox.Styles'>
@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
.replyBox {
max-height: 260px;
}
.section {
padding: 5px 5px;
border-bottom: 1px solid #b8b8b8;
}
.label_name {
font-weight: bold;
text-align: left;
}
.label_name input { margin-left: 0; }
.label_value {
text-align: center;
}
.email {
display: inline-block;
margin-left: 2em;
}
</ui:style>
<g:HTMLPanel styleName='{style.replyBox}'>
<div class='{style.section}'>
<c:NpTextArea
visibleLines='5'
characterWidth='70'
ui:field='message'/>
</div>
<div class='{style.section}' ui:field='labelsParent'>
<g:Grid ui:field='labelsTable'/>
</div>
<div class='{style.section}'>
<g:Button ui:field='send'
title='Send reply (Shortcut: Ctrl-Enter)'
styleName='{res.style.button}'>
<ui:attribute name='title'/>
<div><ui:msg>Send</ui:msg></div>
</g:Button>
<div class='{style.email}'>
<ui:msg>and <g:CheckBox ui:field='email' value='true'>send email</g:CheckBox></ui:msg>
</div>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@ -0,0 +1,34 @@
// 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.gwt.core.client.GWT;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
interface Resources extends ClientBundle {
static final Resources I = GWT.create(Resources.class);
@Source("star_open.png") ImageResource star_open();
@Source("star_filled.png") ImageResource star_filled();
@Source("reload_black.png") ImageResource reload_black();
@Source("reload_white.png") ImageResource reload_white();
@Source("common.css") Style style();
interface Style extends CssResource {
String button();
}
}

View File

@ -0,0 +1,42 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Button;
class RestoreAction extends ActionMessageBox {
private final Change.Id id;
RestoreAction(Button b, Change.Id id) {
super(b);
this.id = id;
}
void send(String message) {
ChangeApi.restore(id.get(), message, new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
Gerrit.display(PageLinks.toChange2(id));
hide();
}
});
}
}

View File

@ -0,0 +1,61 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.ActionDialog;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Button;
class RevertAction {
static void call(Button b, final Change.Id id, final String revision,
String project, final String commitSubject) {
// TODO Replace ActionDialog with a nicer looking display.
b.setEnabled(false);
new ActionDialog(b, false,
Util.C.revertChangeTitle(),
Util.C.headingRevertMessage()) {
{
sendButton.setText(Util.C.buttonRevertChangeSend());
message.setText(Util.M.revertChangeDefaultMessage(
commitSubject, revision));
}
@Override
public void onSend() {
ChangeApi.revert(id.get(),
getMessageText(), new GerritCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo result) {
sent = true;
hide();
Gerrit.display(PageLinks.toChange2(result.legacy_id()));
}
@Override
public void onFailure(Throwable caught) {
enableButtons(true);
super.onFailure(caught);
}
});
}
}.center();
}
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2012 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.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.ToggleButton;
class StarIcon extends ToggleButton {
StarIcon() {
super(
new Image(Resources.I.star_open()),
new Image(Resources.I.star_filled()));
}
}

View File

@ -0,0 +1,47 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.SubmitFailureDialog;
import com.google.gerrit.client.changes.SubmitInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Change;
class SubmitAction {
static void call(final Change.Id id, String revision) {
ChangeApi.submit(id.get(), revision,
new GerritCallback<SubmitInfo>() {
public void onSuccess(SubmitInfo result) {
redisplay();
}
public void onFailure(Throwable err) {
if (SubmitFailureDialog.isConflict(err)) {
new SubmitFailureDialog(err.getMessage()).center();
redisplay();
} else {
super.onFailure(err);
}
}
private void redisplay() {
Gerrit.display(PageLinks.toChange2(id));
}
});
}
}

View File

@ -0,0 +1,140 @@
// 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.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.rpc.GerritCallback;
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.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
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.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.globalkey.client.NpTextBox;
/** Displays (and edits) the change topic string. */
class Topic extends Composite {
interface Binder extends UiBinder<HTMLPanel, Topic> {}
private static Binder uiBinder = GWT.create(Binder.class);
private int changeId;
private boolean canEdit;
@UiField FlowPanel show;
@UiField InlineLabel text;
@UiField Image editIcon;
@UiField Element form;
@UiField NpTextBox input;
@UiField NpTextArea message;
@UiField Button save;
@UiField Button cancel;
Topic() {
initWidget(uiBinder.createAndBindUi(this));
show.addDomHandler(
new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
onEdit();
}
},
ClickEvent.getType());
}
void set(ChangeInfo info) {
canEdit = info.has_actions()
&& info.actions().containsKey("topic")
&& info.actions().get("topic").enabled();
changeId = info.legacy_id().get();
text.setText(info.topic());
editIcon.setVisible(canEdit);
if (!canEdit) {
show.setTitle(null);
}
}
boolean canEdit() {
return canEdit;
}
void onEdit() {
if (canEdit) {
show.setVisible(false);
UIObject.setVisible(form, true);
input.setText(text.getText());
input.setFocus(true);
}
}
@UiHandler("cancel")
void onCancel(ClickEvent e) {
input.setFocus(false);
show.setVisible(true);
UIObject.setVisible(form, false);
}
@UiHandler("input")
void onKeyDownInput(KeyDownEvent e) {
if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
onCancel(null);
} else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
onSave(null);
}
}
@UiHandler("message")
void onKeyDownMessage(KeyDownEvent e) {
if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
onCancel(null);
} else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER
&& e.isControlKeyDown()) {
onSave(null);
}
}
@UiHandler("save")
void onSave(ClickEvent e) {
ChangeApi.topic(
changeId,
input.getValue().trim(),
message.getValue().trim(),
new GerritCallback<String>() {
@Override
public void onSuccess(String result) {
// Cheat and just patch the UI with the current topic.
// This saves a full redraw of the change screen but
// will cause the message to be missed in the History.
text.setText(result);
onCancel(null);
}
});
}
}

View File

@ -0,0 +1,59 @@
<?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='ico' type='com.google.gerrit.client.GerritResources'/>
<ui:with field='res' type='com.google.gerrit.client.change.Resources'/>
<ui:style>
.show { cursor: pointer; }
.edit, .cancel { float: right; }
</ui:style>
<g:HTMLPanel>
<g:FlowPanel ui:field='show'
styleName='{style.show}'
title='Click to edit topic (Shortcut: t)'>
<ui:attribute name='title'/>
<g:InlineLabel ui:field='text'/>
<g:Image ui:field='editIcon'
resource='{ico.edit}'
styleName='{style.edit}'/>
</g:FlowPanel>
<div ui:field='form' style='display: none' aria-hidden='true'>
<div>
<c:NpTextBox ui:field='input' visibleLength='55'/>
</div>
<div>
<c:NpTextArea ui:field='message'
visibleLines='3'
characterWidth='45'/>
</div>
<div>
<g:Button ui:field='save' styleName='{res.style.button}'>
<div>Update</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

@ -0,0 +1,37 @@
/* 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.
*/
.button {
margin: 0 3px 0 0;
border-color: rgba(0, 0, 0, 0.1);
text-align: center;
font-size: 11px;
font-weight: bold;
border: 1px solid;
cursor: pointer;
color: #fff;
background-color: #4d90fe;
background-image: -webkit-linear-gradient(top, #4d90fe, #4d90fe);
-webkit-border-radius: 2px;
-webkit-box-sizing: content-box;
}
.button div {
width: 54px;
white-space: nowrap;
color: #fff;
height: 10px;
line-height: 10px;
}

View File

@ -0,0 +1,47 @@
/* 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.
*/
.pointer {
width: 1px;
padding: 0px;
vertical-align: top;
}
.pathColumn {
white-space: nowrap;
}
.deltaColumn1 {
white-space: nowrap;
text-align: right;
}
.deltaColumn2 {
padding-left: 5px;
white-space: nowrap;
text-align: right;
}
.inserted {
height: 10px;
display: inline-block;
background-color: #4d4;
}
.deleted {
height: 10px;
display: inline-block;
background-color: #d44;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

View File

@ -164,7 +164,7 @@ public class ChangeApi {
return change(id).view("revisions").id(commit).view(action);
}
private static RestApi change(int id) {
public static RestApi change(int id) {
// TODO Switch to triplet project~branch~id format in URI.
return new RestApi("/changes/").id(String.valueOf(id));
}

View File

@ -22,6 +22,8 @@ public interface ChangeConstants extends Constants {
String statusLongMerged();
String statusLongAbandoned();
String statusLongDraft();
String readyToSubmit();
String mergeConflict();
String myDashboardTitle();
String unknownDashboardTitle();
@ -53,7 +55,9 @@ public interface ChangeConstants extends Constants {
String expandCollapseDependencies();
String previousPatchSet();
String nextPatchSet();
String keyReload();
String keyPublishComments();
String keyEditTopic();
String patchTableColumnName();
String patchTableColumnComments();
@ -63,6 +67,7 @@ public interface ChangeConstants extends Constants {
String patchTableDiffUnified();
String patchTableDownloadPreImage();
String patchTableDownloadPostImage();
String patchTableBinary();
String commitMessage();
String fileCommentHeader();

View File

@ -3,6 +3,8 @@ statusLongSubmitted = Submitted, Merge Pending
statusLongMerged = Merged
statusLongAbandoned = Abandoned
statusLongDraft = Draft
readyToSubmit = Ready to Submit
mergeConflict = Merge Conflict
starredHeading = Starred Changes
watchedHeading = Open Changes of Watched Projects
@ -33,7 +35,9 @@ upToChangeList = Up to change list
expandCollapseDependencies = Expands / Collapses dependencies section
previousPatchSet = Previous patch set
nextPatchSet = Next patch set
keyReload = Reload change
keyPublishComments = Review and publish comments
keyEditTopic = Edit change topic
patchTableColumnName = File Path
patchTableColumnComments = Comments
@ -43,6 +47,7 @@ patchTableDiffSideBySide = Side-by-Side
patchTableDiffUnified = Unified
patchTableDownloadPreImage = old
patchTableDownloadPostImage = new
patchTableBinary = Binary
commitMessage = Commit Message
fileCommentHeader = File Comment:

View File

@ -15,9 +15,11 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.diff.FileInfo;
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.common.data.LabelValue;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@ -28,11 +30,13 @@ import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
import java.sql.Timestamp;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
public class ChangeInfo extends JavaScriptObject {
public final void init() {
if (labels0() != null) {
labels0().copyKeysIntoChildren("_name");
if (all_labels() != null) {
all_labels().copyKeysIntoChildren("_name");
}
}
@ -69,7 +73,7 @@ public class ChangeInfo extends JavaScriptObject {
}
public final Set<String> labels() {
return labels0().keySet();
return all_labels().keySet();
}
public final native String id() /*-{ return this.id; }-*/;
@ -86,22 +90,26 @@ public class ChangeInfo extends JavaScriptObject {
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native String _sortkey() /*-{ return this._sortkey; }-*/;
private final native NativeMap<LabelInfo> labels0() /*-{ return this.labels; }-*/;
public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
public final native String current_revision() /*-{ return this.current_revision; }-*/;
public final native NativeMap<RevisionInfo> revisions() /*-{ return this.revisions; }-*/;
public final native RevisionInfo revision(String n) /*-{ return this.revisions[n]; }-*/;
public final native JsArray<MessageInfo> messages() /*-{ return this.messages; }-*/;
public final native boolean has_permitted_labels()
/*-{ return this.hasOwnProperty('permitted_labels') }-*/;
private final native NativeMap<JavaScriptObject> _permitted_labels()
public final native NativeMap<JsArrayString> permitted_labels()
/*-{ return this.permitted_labels; }-*/;
public final Set<String> permitted_labels() {
return Natives.keys(_permitted_labels());
}
public final native JsArrayString permitted_values(String n)
/*-{ return this.permitted_labels[n]; }-*/;
public final native JsArray<AccountInfo> removable_reviewers()
/*-{ return this.removable_reviewers; }-*/;
public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
final native int _number() /*-{ return this._number; }-*/;
final native boolean _more_changes()
/*-{ return this._more_changes ? true : false; }-*/;
@ -130,9 +138,17 @@ public class ChangeInfo extends JavaScriptObject {
public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
public final ApprovalInfo for_user(int user) {
JsArray<ApprovalInfo> all = all();
for (int i = 0; all != null && i < all.length(); i++) {
if (all.get(i)._account_id() == user) {
return all.get(i);
}
}
return null;
}
private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
public final Set<String> values() {
return Natives.keys(_values());
}
@ -147,15 +163,102 @@ public class ChangeInfo extends JavaScriptObject {
return 0;
}-*/;
public final String max_value() {
return LabelValue.formatValue(value_set().last());
}
public final SortedSet<Short> value_set() {
SortedSet<Short> values = new TreeSet<Short>();
for (String v : values()) {
values.add(parseValue(v));
}
return values;
}
public static final short parseValue(String formatted) {
if (formatted.startsWith("+")) {
formatted = formatted.substring(1);
} else if (formatted.startsWith(" ")) {
formatted = formatted.trim();
}
return Short.parseShort(formatted);
}
protected LabelInfo() {
}
}
public static class ApprovalInfo extends AccountInfo {
public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
public final native short value() /*-{ return this.value; }-*/;
public final native short value() /*-{ return this.value || 0; }-*/;
protected ApprovalInfo() {
}
}
public static class RevisionInfo extends JavaScriptObject {
public final native int _number() /*-{ return this._number; }-*/;
public final native String name() /*-{ return this.name; }-*/;
public final native boolean draft() /*-{ return this.draft || false; }-*/;
public final native CommitInfo commit() /*-{ return this.commit; }-*/;
public final native void set_commit(CommitInfo c) /*-{ this.commit = c; }-*/;
public final native boolean has_files() /*-{ return this.hasOwnProperty('files') }-*/;
public final native NativeMap<FileInfo> files() /*-{ return this.files; }-*/;
public final native boolean has_actions() /*-{ return this.hasOwnProperty('actions') }-*/;
public final native NativeMap<ActionInfo> actions() /*-{ return this.actions; }-*/;
protected RevisionInfo () {
}
}
public static class CommitInfo extends JavaScriptObject {
public final native String commit() /*-{ return this.commit; }-*/;
public final native GitPerson author() /*-{ return this.author; }-*/;
public final native GitPerson committer() /*-{ return this.committer; }-*/;
public final native String subject() /*-{ return this.subject; }-*/;
public final native String message() /*-{ return this.message; }-*/;
protected CommitInfo() {
}
}
public static class GitPerson extends JavaScriptObject {
public final native String name() /*-{ return this.name; }-*/;
public final native String email() /*-{ return this.email; }-*/;
private final native String dateRaw() /*-{ return this.date; }-*/;
public final Timestamp date() {
return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
}
protected GitPerson() {
}
}
public static class ActionInfo extends JavaScriptObject {
public final native String id() /*-{ return this.id; }-*/;
public final native String method() /*-{ return this.method; }-*/;
public final native String label() /*-{ return this.label; }-*/;
public final native String title() /*-{ return this.title; }-*/;
public final native boolean enabled() /*-{ return this.enabled || false; }-*/;
public final native String confirmation_message() /*-{ return this.confirmation_message; }-*/;
protected ActionInfo() {
}
}
public static class MessageInfo extends JavaScriptObject {
public final native AccountInfo author() /*-{ return this.author; }-*/;
public final native String message() /*-{ return this.message; }-*/;
private final native String dateRaw() /*-{ return this.date; }-*/;
public final Timestamp date() {
return JavaSqlTimestamp_JsonSerializer.parseTimestamp(dateRaw());
}
protected MessageInfo() {
}
}
}

View File

@ -34,6 +34,7 @@ public interface ChangeMessages extends Messages {
String patchTableComments(@PluralCount int count);
String patchTableDrafts(@PluralCount int count);
String patchTableSize_Modify(int insertions, int deletions);
String patchTableSize_LongModify(int insertions, int deletions);
String patchTableSize_Lines(@PluralCount int insertions);
String removeReviewer(String fullName);

View File

@ -17,6 +17,7 @@ submitPatchSet = Submit Patch Set {0}
patchTableComments = {0} comments
patchTableDrafts = {0} drafts
patchTableSize_Modify = +{0}, -{1}
patchTableSize_LongModify = {0} inserted, {1} deleted
patchTableSize_Lines = {0} lines
removeReviewer = Remove reviewer {0}

View File

@ -17,6 +17,10 @@ package com.google.gerrit.client.changes;
import com.google.gwt.core.client.JavaScriptObject;
public class ReviewInput extends JavaScriptObject {
public static enum NotifyHandling {
NONE, OWNER, OWNER_REVIEWERS, ALL;
}
public static ReviewInput create() {
ReviewInput r = createObject().cast();
r.init();
@ -26,6 +30,11 @@ public class ReviewInput extends JavaScriptObject {
public final native void message(String m) /*-{ if(m)this.message=m; }-*/;
public final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
public final void notify(NotifyHandling e) {
_notify(e.name());
}
private final native void _notify(String n) /*-{ this.notify=n; }-*/;
private final native void init() /*-{
this.labels = {};
this.strict_labels = true;

View File

@ -17,7 +17,6 @@ package com.google.gerrit.client.diff;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeScreen;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.diff.DiffInfo.Region;
@ -744,7 +743,9 @@ public class CodeMirrorDemo extends Screen {
private Runnable upToChange() {
return new Runnable() {
public void run() {
Gerrit.display(PageLinks.toChange(revision), new ChangeScreen(revision));
Gerrit.display(PageLinks.toChange2(
revision.getParentKey(),
String.valueOf(revision.get())));
}
};
}