Add group editing support to the UI

Admins and members who have the "owner" flag set to true may edit
the contents of a group by changing its description or editing its
membership list.  Changes are mostly done live, as soon as the user
presses the action keys.

Account lookup is completed through the suggest service, helping
the user to complete an account identity by name and/or the user's
preferred email address.

Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2008-12-29 18:54:25 -08:00
parent 5ac1f40833
commit 2b2b728b93
26 changed files with 1031 additions and 5 deletions

View File

@@ -22,6 +22,8 @@
class='com.google.gerrit.server.AccountServiceSrv'/>
<servlet path='/rpc/AccountSecurity'
class='com.google.gerrit.server.AccountSecuritySrv'/>
<servlet path='/rpc/AdminService'
class='com.google.gerrit.server.AdminServiceSrv'/>
<servlet path='/rpc/ChangeDetailService'
class='com.google.gerrit.server.ChangeDetailServiceSrv'/>
<servlet path='/rpc/ChangeListService'

View File

@@ -15,6 +15,7 @@
package com.google.gerrit.client;
import com.google.gerrit.client.account.AccountSettings;
import com.google.gerrit.client.admin.AccountGroupScreen;
import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.client.changes.ChangeScreen;
import com.google.gerrit.client.changes.MineStarredScreen;
@@ -23,6 +24,7 @@ import com.google.gerrit.client.data.ChangeInfo;
import com.google.gerrit.client.patches.PatchSideBySideScreen;
import com.google.gerrit.client.patches.PatchUnifiedScreen;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.Change;
import com.google.gerrit.client.reviewdb.Patch;
import com.google.gerrit.client.rpc.RpcUtil;
@@ -124,6 +126,10 @@ public class Link implements HistoryListener {
if (token.startsWith(p))
return new AccountDashboardScreen(Account.Id.parse(skip(p, token)));
p = "admin,group,";
if (token.startsWith(p))
return new AccountGroupScreen(AccountGroup.Id.parse(skip(p, token)));
return null;
}

View File

@@ -0,0 +1,81 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gerrit.client.data.AccountInfo;
import com.google.gerrit.client.data.AccountInfoCache;
import com.google.gerrit.client.data.AccountInfoCacheFactory;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.AccountGroupMember;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gwtorm.client.OrmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class AccountGroupDetail {
protected AccountInfoCache accounts;
protected AccountGroup group;
protected List<AccountGroupMember> members;
public AccountGroupDetail() {
}
public void load(final ReviewDb db, final AccountInfoCacheFactory acc,
final AccountGroup g) throws OrmException {
group = g;
members = db.accountGroupMembers().byGroup(group.getId()).toList();
for (final AccountGroupMember m : members) {
acc.want(m.getAccountId());
}
accounts = acc.create();
Collections.sort(members, new Comparator<AccountGroupMember>() {
public int compare(final AccountGroupMember o1,
final AccountGroupMember o2) {
final AccountInfo a = accounts.get(o1.getAccountId());
final AccountInfo b = accounts.get(o2.getAccountId());
return n(a).compareTo(n(b));
}
private String n(final AccountInfo a) {
String n = a.getFullName();
if (n != null && n.length() > 0) {
return n;
}
n = a.getPreferredEmail();
if (n != null && n.length() > 0) {
return n;
}
return a.getId().toString();
}
});
}
public void loadOneMember(final ReviewDb db, final Account a,
final AccountGroupMember m) throws OrmException {
final AccountInfoCacheFactory acc = new AccountInfoCacheFactory(db);
acc.put(a);
acc.want(m.getAccountId());
members = new ArrayList<AccountGroupMember>(1);
members.add(m);
accounts = acc.create();
}
}

View File

@@ -0,0 +1,345 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gerrit.client.data.AccountInfoCache;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.AccountGroupMember;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.AccountSuggestOracle;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.client.ui.TextSaveButtonListener;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusListenerAdapter;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SourcesTableEvents;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.TableListener;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwtjsonrpc.client.VoidResult;
import java.util.HashSet;
import java.util.List;
public class AccountGroupScreen extends Screen {
private AccountGroup.Id groupId;
private AccountInfoCache accounts = AccountInfoCache.empty();
private MemberTable members;
private TextArea descTxt;
private Button saveDesc;
private Button addMember;
private TextBox nameTxtBox;
private SuggestBox nameTxt;
private Button delMember;
public AccountGroupScreen(final AccountGroup.Id toShow) {
groupId = toShow;
}
@Override
public void onLoad() {
if (members == null) {
initUI();
}
enableForm(false);
saveDesc.setEnabled(false);
super.onLoad();
Util.ADMIN_SVC.groupDetail(groupId,
new GerritCallback<AccountGroupDetail>() {
public void onSuccess(final AccountGroupDetail result) {
enableForm(true);
saveDesc.setEnabled(false);
display(result);
}
});
}
private void enableForm(final boolean on) {
descTxt.setEnabled(on);
addMember.setEnabled(on);
nameTxtBox.setEnabled(on);
delMember.setEnabled(on);
}
private void initUI() {
{
final VerticalPanel vp = new VerticalPanel();
final Label descHdr = new Label(Util.C.headingDescription());
descHdr.setStyleName("gerrit-SmallHeading");
vp.add(descHdr);
descTxt = new TextArea();
descTxt.setVisibleLines(6);
descTxt.setCharacterWidth(60);
new TextSaveButtonListener(descTxt, saveDesc);
vp.add(descTxt);
saveDesc = new Button(Util.C.buttonSaveDescription());
saveDesc.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
final String txt = descTxt.getText().trim();
Util.ADMIN_SVC.changeGroupDescription(groupId, txt,
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
saveDesc.setEnabled(false);
}
});
}
});
vp.add(saveDesc);
add(vp);
}
{
final Label memberHdr = new Label(Util.C.headingMembers());
memberHdr.setStyleName("gerrit-SmallHeading");
add(memberHdr);
}
{
final FlowPanel fp = new FlowPanel();
fp.setStyleName("gerrit-ProjectWatchPanel-AddPanel");
nameTxtBox = new TextBox();
nameTxt = new SuggestBox(new AccountSuggestOracle(), nameTxtBox);
nameTxtBox.setVisibleLength(50);
nameTxtBox.setText(Util.C.defaultAccountName());
nameTxtBox.addStyleName("gerrit-InputFieldTypeHint");
nameTxtBox.addFocusListener(new FocusListenerAdapter() {
@Override
public void onFocus(Widget sender) {
if (Util.C.defaultAccountName().equals(nameTxtBox.getText())) {
nameTxtBox.setText("");
nameTxtBox.removeStyleName("gerrit-InputFieldTypeHint");
}
}
@Override
public void onLostFocus(Widget sender) {
if ("".equals(nameTxtBox.getText())) {
nameTxtBox.setText(Util.C.defaultAccountName());
nameTxtBox.addStyleName("gerrit-InputFieldTypeHint");
}
}
});
fp.add(nameTxt);
addMember = new Button(Util.C.buttonAddGroupMember());
addMember.addClickListener(new ClickListener() {
public void onClick(final Widget sender) {
doAddNew();
}
});
fp.add(addMember);
add(fp);
}
members = new MemberTable();
add(members);
{
final FlowPanel fp = new FlowPanel();
delMember = new Button(Util.C.buttonDeleteGroupMembers());
delMember.addClickListener(new ClickListener() {
public void onClick(final Widget sender) {
members.deleteChecked();
}
});
fp.add(delMember);
add(fp);
}
}
private void display(final AccountGroupDetail result) {
setTitleText(Util.M.group(result.group.getName()));
descTxt.setText(result.group.getDescription());
accounts = result.accounts;
members.display(result.members);
members.finishDisplay(true);
}
void doAddNew() {
final String nameEmail = nameTxt.getText();
if (nameEmail == null || nameEmail.length() == 0) {
return;
}
addMember.setEnabled(false);
Util.ADMIN_SVC.addGroupMember(groupId, nameEmail,
new GerritCallback<AccountGroupDetail>() {
public void onSuccess(final AccountGroupDetail result) {
addMember.setEnabled(true);
nameTxt.setText("");
if (result.accounts != null && result.members != null) {
accounts.merge(result.accounts);
for (final AccountGroupMember m : result.members) {
members.insertMember(m);
}
}
}
@Override
public void onFailure(final Throwable caught) {
addMember.setEnabled(true);
super.onFailure(caught);
}
});
}
private class MemberTable extends FancyFlexTable<AccountGroupMember> {
MemberTable() {
table.setText(0, 2, Util.C.columnMember());
table.setText(0, 3, Util.C.columnEmailAddress());
table.setText(0, 4, Util.C.columnOwner());
table.addTableListener(new TableListener() {
public void onCellClicked(SourcesTableEvents sender, int row, int cell) {
if (cell != 1 && getRowItem(row) != null) {
movePointerTo(row);
}
}
});
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(0, 1, S_ICON_HEADER);
fmt.addStyleName(0, 2, S_DATA_HEADER);
fmt.addStyleName(0, 3, S_DATA_HEADER);
fmt.addStyleName(0, 4, S_ICON_HEADER);
}
@Override
protected Object getRowItemKey(final AccountGroupMember item) {
return item.getKey();
}
@Override
protected boolean onKeyPress(final char keyCode, final int modifiers) {
if (super.onKeyPress(keyCode, modifiers)) {
return true;
}
if (modifiers == 0) {
switch (keyCode) {
case 's':
case 'c':
toggleCurrentRow();
return true;
}
}
return false;
}
@Override
protected void onOpenItem(final AccountGroupMember item) {
toggleCurrentRow();
}
private void toggleCurrentRow() {
final CheckBox cb = (CheckBox) table.getWidget(getCurrentRow(), 1);
cb.setChecked(!cb.isChecked());
}
void deleteChecked() {
final HashSet<AccountGroupMember.Key> ids =
new HashSet<AccountGroupMember.Key>();
for (int row = 1; row < table.getRowCount(); row++) {
final AccountGroupMember k = getRowItem(row);
if (k != null && ((CheckBox) table.getWidget(row, 1)).isChecked()) {
ids.add(k.getKey());
}
}
if (!ids.isEmpty()) {
Util.ADMIN_SVC.deleteGroupMembers(ids,
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
for (int row = 1; row < table.getRowCount();) {
final AccountGroupMember k = getRowItem(row);
if (k != null && ids.contains(k.getKey())) {
table.removeRow(row);
} else {
row++;
}
}
}
});
}
}
void insertMember(final AccountGroupMember k) {
final int row = table.getRowCount();
table.insertRow(row);
populate(row, k);
}
void display(final List<AccountGroupMember> result) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
for (final AccountGroupMember k : result) {
final int row = table.getRowCount();
table.insertRow(row);
populate(row, k);
}
}
void populate(final int row, final AccountGroupMember k) {
final Account.Id accountId = k.getAccountId();
table.setWidget(row, 1, new CheckBox());
table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
final CheckBox owner = new CheckBox();
owner.setChecked(k.isGroupOwner());
owner.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
final boolean oldValue = k.isGroupOwner();
final boolean newValue = owner.isChecked();
Util.ADMIN_SVC.changeGroupOwner(k.getKey(), newValue,
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
k.setGroupOwner(newValue);
}
@Override
public void onFailure(final Throwable caught) {
owner.setChecked(oldValue);
k.setGroupOwner(oldValue);
super.onFailure(caught);
}
});
}
});
table.setWidget(row, 4, owner);
final FlexCellFormatter fmt = table.getFlexCellFormatter();
fmt.addStyleName(row, 1, S_ICON_CELL);
fmt.addStyleName(row, 2, S_DATA_CELL);
fmt.addStyleName(row, 3, S_DATA_CELL);
fmt.addStyleName(row, 4, S_ICON_CELL);
setRowItem(row, k);
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gwt.i18n.client.Constants;
public interface AdminConstants extends Constants {
String defaultAccountName();
String buttonDeleteGroupMembers();
String buttonAddGroupMember();
String buttonSaveDescription();
String headingDescription();
String headingMembers();
String columnMember();
String columnEmailAddress();
String columnOwner();
}

View File

@@ -0,0 +1,12 @@
defaultAccountName = Name or Email
buttonDeleteGroupMembers = Delete
buttonAddGroupMember = Add
buttonSaveDescription = Save Description
headingDescription = Description
headingMembers = Members
columnMember = Member
columnEmailAddress = Email Address
columnOwner = Owner

View File

@@ -0,0 +1,21 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gwt.i18n.client.Messages;
public interface AdminMessages extends Messages {
public String group(String name);
}

View File

@@ -0,0 +1 @@
group = Group {0}

View File

@@ -0,0 +1,46 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.AccountGroupMember;
import com.google.gerrit.client.rpc.SignInRequired;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.RemoteJsonService;
import com.google.gwtjsonrpc.client.VoidResult;
import java.util.Set;
public interface AdminService extends RemoteJsonService {
@SignInRequired
void groupDetail(AccountGroup.Id groupId,
AsyncCallback<AccountGroupDetail> callback);
@SignInRequired
void changeGroupDescription(AccountGroup.Id groupId, String description,
AsyncCallback<VoidResult> callback);
@SignInRequired
void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
AsyncCallback<AccountGroupDetail> callback);
@SignInRequired
void deleteGroupMembers(Set<AccountGroupMember.Key> keys,
AsyncCallback<VoidResult> callback);
@SignInRequired
void changeGroupOwner(AccountGroupMember.Key key, boolean owner,
AsyncCallback<VoidResult> callback);
}

View File

@@ -0,0 +1,177 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gerrit.client.data.AccountInfoCacheFactory;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.AccountGroupMember;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.BaseServiceImplementation;
import com.google.gerrit.client.rpc.NoSuchEntityException;
import com.google.gerrit.client.rpc.RpcUtil;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class AdminServiceImpl extends BaseServiceImplementation implements
AdminService {
public AdminServiceImpl(final SchemaFactory<ReviewDb> rdf) {
super(rdf);
}
public void groupDetail(final AccountGroup.Id groupId,
final AsyncCallback<AccountGroupDetail> callback) {
run(callback, new Action<AccountGroupDetail>() {
public AccountGroupDetail run(ReviewDb db) throws OrmException, Failure {
assertAmGroupOwner(db, groupId);
final AccountGroup group = db.accountGroups().get(groupId);
if (group == null) {
throw new Failure(new NoSuchEntityException());
}
final AccountGroupDetail d = new AccountGroupDetail();
d.load(db, new AccountInfoCacheFactory(db), group);
return d;
}
});
}
public void changeGroupDescription(final AccountGroup.Id groupId,
final String description, final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
assertAmGroupOwner(db, groupId);
final AccountGroup group = db.accountGroups().get(groupId);
if (group == null) {
throw new Failure(new NoSuchEntityException());
}
group.setDescription(description);
db.accountGroups().update(Collections.singleton(group));
return VoidResult.INSTANCE;
}
});
}
public void addGroupMember(final AccountGroup.Id groupId,
final String nameOrEmail, final AsyncCallback<AccountGroupDetail> callback) {
run(callback, new Action<AccountGroupDetail>() {
public AccountGroupDetail run(ReviewDb db) throws OrmException, Failure {
assertAmGroupOwner(db, groupId);
final Account a = findAccount(db, nameOrEmail);
final AccountGroupMember.Key key =
new AccountGroupMember.Key(a.getId(), groupId);
if (db.accountGroupMembers().get(key) != null) {
return new AccountGroupDetail();
}
final AccountGroupMember m = new AccountGroupMember(key);
db.accountGroupMembers().insert(Collections.singleton(m));
final AccountGroupDetail d = new AccountGroupDetail();
d.loadOneMember(db, a, m);
return d;
}
});
}
public void deleteGroupMembers(final Set<AccountGroupMember.Key> keys,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
final Set<AccountGroup.Id> owned = myOwnedGroups(db);
Boolean amAdmin = null;
for (final AccountGroupMember.Key k : keys) {
if (!owned.contains(k.getAccountGroupId())) {
if (amAdmin == null) {
amAdmin = amAdmin(db);
}
if (!amAdmin) {
throw new Failure(new NoSuchEntityException());
}
}
}
for (final AccountGroupMember.Key k : keys) {
final AccountGroupMember m = db.accountGroupMembers().get(k);
if (m != null) {
db.accountGroupMembers().delete(Collections.singleton(m));
}
}
return VoidResult.INSTANCE;
}
});
}
public void changeGroupOwner(final AccountGroupMember.Key key,
final boolean owner, final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
assertAmGroupOwner(db, key.getAccountGroupId());
final AccountGroupMember m = db.accountGroupMembers().get(key);
if (m == null) {
throw new Failure(new NoSuchEntityException());
}
if (m.isGroupOwner() != owner) {
m.setGroupOwner(owner);
db.accountGroupMembers().update(Collections.singleton(m));
}
return VoidResult.INSTANCE;
}
});
}
private static boolean amAdmin(final ReviewDb db) throws OrmException {
final AccountGroup admin =
db.accountGroups().get(new AccountGroup.NameKey("admin"));
if (admin == null) {
return false;
}
return db.accountGroupMembers().get(
new AccountGroupMember.Key(RpcUtil.getAccountId(), admin.getId())) != null;
}
private static void assertAmGroupOwner(final ReviewDb db,
final AccountGroup.Id groupId) throws OrmException, Failure {
final AccountGroupMember m =
db.accountGroupMembers().get(
new AccountGroupMember.Key(RpcUtil.getAccountId(), groupId));
if ((m == null || !m.isGroupOwner()) && !amAdmin(db)) {
throw new Failure(new NoSuchEntityException());
}
}
private static Set<AccountGroup.Id> myOwnedGroups(final ReviewDb db)
throws OrmException {
final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>();
for (final AccountGroupMember m : db.accountGroupMembers().owned(
RpcUtil.getAccountId())) {
r.add(m.getAccountGroupId());
}
return r;
}
private static Account findAccount(final ReviewDb db, final String nameOrEmail)
throws OrmException, Failure {
final Account r = Account.find(db, nameOrEmail);
if (r == null) {
throw new Failure(new NoSuchEntityException());
}
return r;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2008 Google Inc.
//
// 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.admin;
import com.google.gwt.core.client.GWT;
import com.google.gwtjsonrpc.client.JsonUtil;
public class Util {
public static final AdminConstants C = GWT.create(AdminConstants.class);
public static final AdminMessages M = GWT.create(AdminMessages.class);
public static final AdminService ADMIN_SVC;
static {
ADMIN_SVC = GWT.create(AdminService.class);
JsonUtil.bind(ADMIN_SVC, "rpc/AdminService");
}
}

View File

@@ -70,4 +70,10 @@ public class AccountInfoCache {
}
return r;
}
/** Merge the information from another cache into this one. */
public void merge(final AccountInfoCache other) {
assert this != EMPTY;
accounts.putAll(other.accounts);
}
}

View File

@@ -89,6 +89,12 @@ public class AccountInfoCacheFactory {
return a;
}
/** Remember one account that was previously loaded. */
public void put(final Account a) {
toFetch.remove(a.getId());
cache.put(a.getId(), a);
}
/**
* Create an AccountInfoCache with the currently loaded Account entities.
* <p>

View File

@@ -16,8 +16,11 @@ package com.google.gerrit.client.reviewdb;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.ResultSet;
import java.sql.Timestamp;
import java.util.List;
/** Preferences and information about a single user. */
public final class Account {
@@ -27,6 +30,37 @@ public final class Account {
/** Typical valid choices for the default context setting. */
public static final short[] CONTEXT_CHOICES = {3, 10, 25, 50, 75, 100};
/**
* Locate exactly one account matching the name or name/email string.
*
* @param db open database handle to use for the query.
* @param nameOrEmail a string of the format
* "Full Name &lt;email@example&gt;", or just the preferred email
* address ("email@example"), or a full name.
* @return the single account that matches; null if no account matches or
* there are multiple candidates.
*/
public static Account find(final ReviewDb db, final String nameOrEmail)
throws OrmException {
final int lt = nameOrEmail.indexOf('<');
final int gt = nameOrEmail.indexOf('>');
if (lt >= 0 && gt > lt) {
final String email = nameOrEmail.substring(lt + 1, gt);
return one(db.accounts().byPreferredEmail(email));
}
if (nameOrEmail.contains("@")) {
return one(db.accounts().byPreferredEmail(nameOrEmail));
}
return one(db.accounts().suggestByFullName(nameOrEmail, nameOrEmail, 2));
}
private static Account one(final ResultSet<Account> rs) {
final List<Account> r = rs.toList();
return r.size() == 1 ? r.get(0) : null;
}
/** Key local to Gerrit to identify a user. */
public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
@Column

View File

@@ -28,4 +28,12 @@ public interface AccountAccess extends Access<Account, Account.Id> {
@Query("WHERE preferredEmail = ? LIMIT 2")
ResultSet<Account> byPreferredEmail(String email) throws OrmException;
@Query("WHERE fullName >= ? AND fullName <= ? ORDER BY fullName LIMIT ?")
ResultSet<Account> suggestByFullName(String nameA, String nameB, int limit)
throws OrmException;
@Query("WHERE preferredEmail >= ? AND preferredEmail <= ? ORDER BY preferredEmail LIMIT ?")
ResultSet<Account> suggestByPreferredEmail(String nameA, String nameB,
int limit) throws OrmException;
}

View File

@@ -65,6 +65,13 @@ public final class AccountGroup {
protected void set(int newValue) {
id = newValue;
}
/** Parse an AccountGroup.Id out of a string representation. */
public static Id parse(final String str) {
final Id r = new Id();
r.fromString(str);
return r;
}
}
@Column

View File

@@ -41,6 +41,10 @@ public final class AccountGroupMember {
return accountId;
}
public AccountGroup.Id getAccountGroupId(){
return groupId;
}
@Override
public com.google.gwtorm.client.Key<?>[] members() {
return new com.google.gwtorm.client.Key<?>[] {groupId};
@@ -61,6 +65,10 @@ public final class AccountGroupMember {
key = k;
}
public AccountGroupMember.Key getKey() {
return key;
}
public Account.Id getAccountId() {
return key.accountId;
}

View File

@@ -28,6 +28,9 @@ public interface AccountGroupMemberAccess extends
@Query("WHERE key.accountId = ?")
ResultSet<AccountGroupMember> byAccount(Account.Id id) throws OrmException;
@Query("WHERE key.accountId = ? AND owner = 'Y'")
ResultSet<AccountGroupMember> owned(Account.Id id) throws OrmException;
@Query("WHERE key.groupId = ?")
ResultSet<AccountGroupMember> byGroup(AccountGroup.Id id) throws OrmException;
}

View File

@@ -0,0 +1,62 @@
// Copyright 2008 Google Inc.
//
// 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.ui;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.RpcStatus;
import com.google.gerrit.client.data.AccountInfo;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gwt.user.client.ui.SuggestOracle;
import java.util.ArrayList;
import java.util.List;
/** Suggestion Oracle for Account entities. */
public class AccountSuggestOracle extends SuggestOracle {
@Override
public void requestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() {
public void run() {
SuggestUtil.SVC.suggestAccount(req.getQuery(), req.getLimit(),
new GerritCallback<List<AccountInfo>>() {
public void onSuccess(final List<AccountInfo> result) {
final ArrayList<AccountSuggestion> r =
new ArrayList<AccountSuggestion>(result.size());
for (final AccountInfo p : result) {
r.add(new AccountSuggestion(p));
}
callback.onSuggestionsReady(req, new Response(r));
}
});
}
});
}
private static class AccountSuggestion implements SuggestOracle.Suggestion {
private final AccountInfo info;
AccountSuggestion(final AccountInfo k) {
info = k;
}
public String getDisplayString() {
return FormatUtil.nameEmail(info);
}
public String getReplacementString() {
return FormatUtil.nameEmail(info);
}
}
}

View File

@@ -29,6 +29,7 @@ import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.KeyboardListenerAdapter;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -36,6 +37,7 @@ import java.util.Map.Entry;
public abstract class FancyFlexTable<RowItem> extends Composite implements
HasFocus {
protected static final String S_ACTIVE_ROW = "ActiveRow";
protected static final String MY_STYLE = "gerrit-ChangeTable";
protected static final String S_ICON_HEADER = "IconHeader";
protected static final String S_DATA_HEADER = "DataHeader";
@@ -170,9 +172,20 @@ public abstract class FancyFlexTable<RowItem> extends Composite implements
}
protected void movePointerTo(final int newRow) {
final CellFormatter fmt = table.getCellFormatter();
if (currentRow >= 0) {
final int n = table.getCellCount(currentRow);
for (int cell = 0; cell < n; cell++) {
fmt.removeStyleName(currentRow, cell, S_ACTIVE_ROW);
}
}
if (newRow >= 0) {
table.setWidget(newRow, C_ARROW, pointer);
table.getCellFormatter().getElement(newRow, C_ARROW).scrollIntoView();
final int n = table.getCellCount(newRow);
for (int cell = 0; cell < n; cell++) {
fmt.addStyleName(newRow, cell, S_ACTIVE_ROW);
}
fmt.getElement(newRow, C_ARROW).scrollIntoView();
} else if (currentRow >= 0) {
table.setWidget(currentRow, C_ARROW, null);
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.data.AccountInfo;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.AllowCrossSiteRequest;
@@ -25,4 +26,8 @@ public interface SuggestService extends RemoteJsonService {
@AllowCrossSiteRequest
void suggestProjectNameKey(String query, int limit,
AsyncCallback<List<Project.NameKey>> callback);
@AllowCrossSiteRequest
void suggestAccount(String query, int limit,
AsyncCallback<List<AccountInfo>> callback);
}

View File

@@ -14,6 +14,8 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.data.AccountInfo;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.BaseServiceImplementation;
@@ -47,4 +49,28 @@ public class SuggestServiceImpl extends BaseServiceImplementation implements
}
});
}
public void suggestAccount(final String query, final int limit,
final AsyncCallback<List<AccountInfo>> callback) {
run(callback, new Action<List<AccountInfo>>() {
public List<AccountInfo> run(final ReviewDb db) throws OrmException {
final String a = query;
final String b = a + "\uffff";
final int max = 10;
final int n = limit <= 0 ? max : Math.min(limit, max);
final List<AccountInfo> r = new ArrayList<AccountInfo>();
for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
r.add(new AccountInfo(p));
}
if (r.size() < n) {
for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
n - r.size())) {
r.add(new AccountInfo(p));
}
}
return r;
}
});
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2008 Google Inc.
//
// 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.ui;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.KeyboardListenerAdapter;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.google.gwt.user.client.ui.Widget;
/** Enables an action (e.g. a Button) if the text box is modified. */
public class TextSaveButtonListener extends KeyboardListenerAdapter {
private final TextBoxBase descText;
private final FocusWidget descAction;
public TextSaveButtonListener(final TextBoxBase text, final FocusWidget action) {
descText = text;
descAction = action;
descText.addKeyboardListener(this);
}
@Override
public void onKeyPress(final Widget sender, final char key, final int mod) {
if (mod == 0) {
switch (key) {
case KEY_UP:
case KEY_DOWN:
case KEY_LEFT:
case KEY_RIGHT:
case KEY_HOME:
case KEY_END:
case KEY_PAGEUP:
case KEY_PAGEDOWN:
case KEY_ALT:
case KEY_CTRL:
case KEY_SHIFT:
break;
default:
descAction.setEnabled(descText.isEnabled());
break;
}
}
}
}

View File

@@ -35,10 +35,11 @@
}
.gerrit-InputFieldTypeHint {
color: grey;
color: grey;
}
.gerrit-SmallHeading {
margin-top: 5px;
font-weight: bold;
}
@@ -178,6 +179,10 @@
border-bottom: 1px solid #d4e9a9;
}
.gerrit-ChangeTable td.ActiveRow {
background: #ecffc0;
}
.gerrit-ChangeTable .C_ID {
width: 3.5em;
text-align: right;
@@ -507,8 +512,8 @@
}
.gerrit-WatchedProjectPanel {
margin-top: 10px;
padding: 5px 5px 5px 5px;
display: table;
margin-top: 10px;
padding: 5px 5px 5px 5px;
display: table;
border: 1px solid #B0BDCC;
}

View File

@@ -0,0 +1,25 @@
// Copyright 2008 Google Inc.
//
// 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.server;
import com.google.gerrit.client.admin.AdminServiceImpl;
/** Publishes {@link AdminServiceImpl} over JSON. */
public class AdminServiceSrv extends GerritJsonServlet {
@Override
protected Object createServiceHandle() throws Exception {
return new AdminServiceImpl(GerritServer.getInstance().getDatabase());
}
}