Add support for UiActions on Project Info Screen

Bug: issue 349
Change-Id: Ia29d08684aaf421fa0a16dd875af36667980fe2e
This commit is contained in:
David Ostrovsky 2013-08-30 08:09:53 +02:00 committed by David Pursehouse
parent 8ab4d6a845
commit 9e8b2fb8d1
17 changed files with 210 additions and 21 deletions

View File

@ -141,8 +141,8 @@ on a button associated with a server side `UiAction`.
Gerrit.onAction(type, view_name, callback);
----
* type: `'change'` or `'revision'`, indicating what sort of resource
the `UiAction` was bound to in the server.
* type: `'change'`, `'revision'` or `'project'`, indicating which type
of resource the `UiAction` was bound to in the server.
* view_name: string appearing in URLs to name the view. This is the
second argument of the `get()`, `post()`, `put()`, and `delete()`
@ -385,6 +385,11 @@ object instance describing the revision. Available fields of the
RevisionInfo may vary based on the options used by the UI when it
loaded the change.
[[context_project]]
context.project
~~~~~~~~~~~~~~~
When the action is invoked on a specific project,
the name of the project.
Action Context HTML Helpers
---------------------------

View File

@ -461,7 +461,15 @@ read access to `refs/meta/config`.
},
"submit_type": "MERGE_IF_NECESSARY",
"state": "ACTIVE",
"commentlinks": {}
"commentlinks": {},
"actions": {
"cookbook~hello-project": {
"method": "POST",
"label": "Say hello",
"title": "Say hello in different languages",
"enabled": true
}
}
}
----
@ -1184,6 +1192,10 @@ commentlink section] of `gerrit.config`.
|`theme` |optional|
The theme that is configured for the project as a link:#theme-info[
ThemeInfo] entity.
|`actions` |optional|
Actions the caller might be able to perform on this project. The
information is a map of view names to
link:rest-api-changes.html#action-info[ActionInfo] entities.
|=========================================
[[config-input]]

View File

@ -182,6 +182,7 @@ public interface GerritCss extends CssResource {
String patchSizeCell();
String pluginsTable();
String posscore();
String projectActions();
String projectAdminLabelRangeLine();
String projectAdminLabelValue();
String projectFilterLabel();

View File

@ -16,25 +16,38 @@ package com.google.gerrit.client.actions;
import com.google.gerrit.client.api.ActionContext;
import com.google.gerrit.client.api.ChangeGlue;
import com.google.gerrit.client.api.ProjectGlue;
import com.google.gerrit.client.api.RevisionGlue;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.reviewdb.client.Project;
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;
public class ActionButton extends Button implements ClickHandler {
private final Project.NameKey project;
private final ChangeInfo change;
private final RevisionInfo revision;
private final ActionInfo action;
private ActionContext ctx;
public ActionButton(Project.NameKey project, ActionInfo action) {
this(project, null, null, action);
}
public ActionButton(ChangeInfo change, ActionInfo action) {
this(change, null, action);
}
public ActionButton(ChangeInfo change, RevisionInfo revision, ActionInfo action) {
public ActionButton(ChangeInfo change, RevisionInfo revision,
ActionInfo action) {
this(null, change, revision, action);
}
private ActionButton(Project.NameKey project, ChangeInfo change,
RevisionInfo revision, ActionInfo action) {
super(new SafeHtmlBuilder()
.openDiv()
.append(action.label())
@ -44,6 +57,7 @@ public class ActionButton extends Button implements ClickHandler {
setEnabled(action.enabled());
addClickHandler(this);
this.project = project;
this.change = change;
this.revision = revision;
this.action = action;
@ -59,8 +73,10 @@ public class ActionButton extends Button implements ClickHandler {
if (revision != null) {
RevisionGlue.onAction(change, revision, action, this);
} else {
} else if (change != null) {
ChangeGlue.onAction(change, action, this);
} else if (project != null) {
ProjectGlue.onAction(project, action, this);
}
}

View File

@ -56,6 +56,8 @@ public interface AdminConstants extends Constants {
String headingOwner();
String headingDescription();
String headingProjectOptions();
String headingProjectCommands();
String headingCommands();
String headingMembers();
String headingIncludedGroups();
String noMembersInfo();

View File

@ -37,6 +37,8 @@ headingGroupUUID = Group UUID
headingOwner = Owners
headingDescription = Description
headingProjectOptions = Project Options
headingProjectCommands = Project Commands
headingCommands = Commands
headingMembers = Members
headingIncludedGroups = Included Groups
noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For external groups and Gerrit system groups the members cannot be displayed.

View File

@ -17,12 +17,16 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.access.AccessMap;
import com.google.gerrit.client.access.ProjectAccessInfo;
import com.google.gerrit.client.actions.ActionButton;
import com.google.gerrit.client.actions.ActionInfo;
import com.google.gerrit.client.change.Resources;
import com.google.gerrit.client.download.DownloadPanel;
import com.google.gerrit.client.projects.ConfigInfo;
import com.google.gerrit.client.projects.ConfigInfo.InheritedBooleanInfo;
import com.google.gerrit.client.projects.ProjectApi;
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.ScreenLoadCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.client.ui.SmallHeading;
@ -36,6 +40,7 @@ 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.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
@ -49,6 +54,7 @@ public class ProjectInfoScreen extends ProjectScreen {
private Project.NameKey parent;
private LabeledWidgetsGrid grid;
private LabeledWidgetsGrid actionsGrid;
// Section: Project Options
private ListBox requireChangeID;
@ -75,6 +81,7 @@ public class ProjectInfoScreen extends ProjectScreen {
protected void onInitUI() {
super.onInitUI();
Resources.I.style().ensureInjected();
saveProject = new Button(Util.C.buttonSaveChanges());
saveProject.addClickHandler(new ClickHandler() {
@Override
@ -87,10 +94,12 @@ public class ProjectInfoScreen extends ProjectScreen {
initDescription();
grid = new LabeledWidgetsGrid();
actionsGrid = new LabeledWidgetsGrid();
initProjectOptions();
initAgreements();
add(grid);
add(saveProject);
add(actionsGrid);
}
@Override
@ -321,6 +330,24 @@ public class ProjectInfoScreen extends ProjectScreen {
}
saveProject.setEnabled(false);
initProjectActions(result);
}
private void initProjectActions(ConfigInfo info) {
NativeMap<ActionInfo> actions = info.actions();
if (actions == null || actions.isEmpty()) {
return;
}
actions.copyKeysIntoChildren("id");
actionsGrid.addHeader(new SmallHeading(Util.C.headingProjectCommands()));
FlowPanel actionsPanel = new FlowPanel();
actionsPanel.setStyleName(Gerrit.RESOURCES.css().projectActions());
actionsPanel.setVisible(true);
actionsGrid.add(Util.C.headingCommands(), actionsPanel);
for (String id : actions.keySet()) {
actionsPanel.add(new ActionButton(getProjectKey(),
actions.get(id)));
}
}
private void doSave() {

View File

@ -21,6 +21,7 @@ import com.google.gerrit.client.changes.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.JavaScriptObject;
public class ActionContext extends JavaScriptObject {
@ -117,6 +118,7 @@ public class ActionContext extends JavaScriptObject {
final native void set(ActionInfo a) /*-{ this.action=a; }-*/;
final native void set(ChangeInfo c) /*-{ this.change=c; }-*/;
final native void set(Project.NameKey p) /*-{ this.project=p; }-*/;
final native void set(RevisionInfo r) /*-{ this.revision=r; }-*/;
final native void button(ActionButton b) /*-{ this._b=b; }-*/;

View File

@ -42,11 +42,13 @@ public class ApiGlue {
change_actions: {},
revision_actions: {},
project_actions: {},
onAction: function (t,n,c){this._onAction(this.getPluginName(),t,n,c)},
_onAction: function (p,t,n,c) {
var i = p+'~'+n;
if ('change' == t) this.change_actions[i]=c;
else if ('revision' == t) this.revision_actions[i]=c;
else if ('project' == t) this.project_actions[i]=c;
},
url: function (d) {

View File

@ -41,6 +41,27 @@ class DefaultActions {
Gerrit.display(PageLinks.toChange(id));
}
};
invoke(action, api, cb);
}
static void invokeProjectAction(ActionInfo action, RestApi api) {
AsyncCallback<JavaScriptObject> cb = new GerritCallback<JavaScriptObject>() {
@Override
public void onSuccess(JavaScriptObject msg) {
if (NativeString.is(msg)) {
NativeString str = (NativeString) msg;
if (!str.asString().isEmpty()) {
Window.alert(str.asString());
}
}
Gerrit.display(PageLinks.ADMIN_PROJECTS);
}
};
invoke(action, api, cb);
}
private static void invoke(ActionInfo action, RestApi api,
AsyncCallback<JavaScriptObject> cb) {
if ("PUT".equalsIgnoreCase(action.method())) {
api.put(JavaScriptObject.createObject(), cb);
} else if ("DELETE".equalsIgnoreCase(action.method())) {

View File

@ -0,0 +1,48 @@
// 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.api;
import com.google.gerrit.client.actions.ActionButton;
import com.google.gerrit.client.actions.ActionInfo;
import com.google.gerrit.client.projects.ProjectApi;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.JavaScriptObject;
public class ProjectGlue {
public static void onAction(
Project.NameKey project,
ActionInfo action,
ActionButton button) {
RestApi api = ProjectApi.project(project).view(action.id());
JavaScriptObject f = get(action.id());
if (f != null) {
ActionContext c = ActionContext.create(api);
c.set(action);
c.set(project);
c.button(button);
ApiGlue.invoke(f, c);
} else {
DefaultActions.invokeProjectAction(action, api);
}
}
private static final native JavaScriptObject get(String id) /*-{
return $wnd.Gerrit.project_actions[id];
}-*/;
private ProjectGlue() {
}
}

View File

@ -1383,7 +1383,9 @@ a:hover.downloadLink {
font-family: mono-font;
font-size: small;
}
.projectActions {
margin-bottom: 10px;
}
/** PublishCommentsScreen **/
.publishCommentsScreen .smallHeading {

View File

@ -14,6 +14,7 @@
package com.google.gerrit.client.projects;
import com.google.gerrit.client.actions.ActionInfo;
import com.google.gerrit.client.rpc.NativeMap;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
@ -28,6 +29,7 @@ import java.util.ArrayList;
import java.util.List;
public class ConfigInfo extends JavaScriptObject {
public final native String description()
/*-{ return this.description }-*/;
@ -46,6 +48,10 @@ public class ConfigInfo extends JavaScriptObject {
public final SubmitType submit_type() {
return SubmitType.valueOf(submit_typeRaw());
}
public final native NativeMap<ActionInfo> actions()
/*-{ return this.actions; }-*/;
private final native String submit_typeRaw()
/*-{ return this.submit_type }-*/;

View File

@ -127,7 +127,7 @@ public class ProjectApi {
}
}
private static RestApi project(Project.NameKey name) {
public static RestApi project(Project.NameKey name) {
return new RestApi("/projects/").id(name.get());
}

View File

@ -17,10 +17,17 @@ package com.google.gerrit.server.project;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.actions.ActionInfo;
import com.google.gerrit.server.extensions.webui.UiActions;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.Provider;
import java.util.Map;
@ -35,12 +42,17 @@ public class ConfigInfo {
public MaxObjectSizeLimitInfo maxObjectSizeLimit;
public SubmitType submitType;
public Project.State state;
public Map<String, ActionInfo> actions;
public Map<String, CommentLinkInfo> commentlinks;
public ThemeInfo theme;
public ConfigInfo(ProjectState state, TransferConfig config) {
Project p = state.getProject();
public ConfigInfo(ProjectControl control,
ProjectState projectState,
TransferConfig config,
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> currentUser) {
Project p = control.getProject();
this.description = Strings.emptyToNull(p.getDescription());
InheritedBooleanInfo useContributorAgreements =
@ -49,10 +61,10 @@ public class ConfigInfo {
InheritedBooleanInfo useContentMerge = new InheritedBooleanInfo();
InheritedBooleanInfo requireChangeId = new InheritedBooleanInfo();
useContributorAgreements.value = state.isUseContributorAgreements();
useSignedOffBy.value = state.isUseSignedOffBy();
useContentMerge.value = state.isUseContentMerge();
requireChangeId.value = state.isRequireChangeID();
useContributorAgreements.value = projectState.isUseContributorAgreements();
useSignedOffBy.value = projectState.isUseSignedOffBy();
useContentMerge.value = projectState.isUseContentMerge();
requireChangeId.value = projectState.isRequireChangeID();
useContributorAgreements.configuredValue =
p.getUseContributorAgreements();
@ -60,7 +72,8 @@ public class ConfigInfo {
useContentMerge.configuredValue = p.getUseContentMerge();
requireChangeId.configuredValue = p.getRequireChangeID();
ProjectState parentState = Iterables.getFirst(state.parents(), null);
ProjectState parentState = Iterables.getFirst(projectState
.parents(), null);
if (parentState != null) {
useContributorAgreements.inheritedValue =
parentState.isUseContributorAgreements();
@ -76,7 +89,7 @@ public class ConfigInfo {
MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
maxObjectSizeLimit.value =
config.getEffectiveMaxObjectSizeLimit(state) == config
config.getEffectiveMaxObjectSizeLimit(projectState) == config
.getMaxObjectSizeLimit() ? config
.getFormattedMaxObjectSizeLimit() : p.getMaxObjectSizeLimit();
maxObjectSizeLimit.configuredValue = p.getMaxObjectSizeLimit();
@ -88,11 +101,17 @@ public class ConfigInfo {
this.state = p.getState() != Project.State.ACTIVE ? p.getState() : null;
this.commentlinks = Maps.newLinkedHashMap();
for (CommentLinkInfo cl : state.getCommentLinks()) {
for (CommentLinkInfo cl : projectState.getCommentLinks()) {
this.commentlinks.put(cl.name, cl);
}
this.theme = state.getTheme();
actions = Maps.newTreeMap();
for (UiAction.Description d : UiActions.from(
views, new ProjectResource(control),
currentUser)) {
actions.put(d.getId(), new ActionInfo(d));
}
this.theme = projectState.getTheme();
}
public static class InheritedBooleanInfo {

View File

@ -14,21 +14,35 @@
package com.google.gerrit.server.project;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.TransferConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class GetConfig implements RestReadView<ProjectResource> {
private final TransferConfig config;
private final DynamicMap<RestView<ProjectResource>> views;
private final Provider<CurrentUser> currentUser;
@Inject
public GetConfig(TransferConfig config) {
public GetConfig(TransferConfig config,
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> currentUser) {
this.config = config;
this.views = views;
this.currentUser = currentUser;
}
@Override
public ConfigInfo apply(ProjectResource resource) {
return new ConfigInfo(resource.getControl().getProjectState(), config);
return new ConfigInfo(resource.getControl(),
resource.getControl().getProjectState(),
config,
views,
currentUser);
}
}

View File

@ -15,10 +15,12 @@
package com.google.gerrit.server.project;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
@ -52,18 +54,24 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
private final Provider<CurrentUser> self;
private final ProjectState.Factory projectStateFactory;
private final TransferConfig config;
private final DynamicMap<RestView<ProjectResource>> views;
private final Provider<CurrentUser> currentUser;
@Inject
PutConfig(MetaDataUpdate.User metaDataUpdateFactory,
ProjectCache projectCache,
Provider<CurrentUser> self,
ProjectState.Factory projectStateFactory,
TransferConfig config) {
TransferConfig config,
DynamicMap<RestView<ProjectResource>> views,
Provider<CurrentUser> currentUser) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectCache = projectCache;
this.self = self;
this.projectStateFactory = projectStateFactory;
this.config = config;
this.views = views;
this.currentUser = currentUser;
}
@Override
@ -131,7 +139,9 @@ public class PutConfig implements RestModifyView<ProjectResource, Input> {
throw new ResourceConflictException("Cannot update " + projectName);
}
}
return new ConfigInfo(projectStateFactory.create(projectConfig), config);
return new ConfigInfo(rsrc.getControl(),
projectStateFactory.create(projectConfig),
config, views, currentUser);
} catch (ConfigInvalidException err) {
throw new ResourceConflictException("Cannot read project " + projectName, err);
} catch (IOException err) {