bug6239: addRemoveAction is refactored to be a core method for permission check with per row counter/button enabling

This commit is contained in:
masha.kashirina
2026-04-29 15:16:45 +02:00
parent caf50aba36
commit 345ddaba6d
4 changed files with 21 additions and 195 deletions
@@ -1,182 +0,0 @@
/*
* Copyright 2010 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.gwt.view.client;
import com.google.gwt.view.client.SelectionModel.AbstractSelectionModel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A simple selection model that allows multiple items to be selected.
*
* @param <T> the data type of the items
*/
public class MultiSelectionModel<T> extends AbstractSelectionModel<T>
implements SetSelectionModel<T> {
/**
* Stores an item and its pending selection state.
*
* @param <T> the data type of the item
*/
static class SelectionChange<T> {
private final T item;
private final boolean isSelected;
SelectionChange(T item, boolean isSelected) {
this.item = item;
this.isSelected = isSelected;
}
public T getItem() {
return item;
}
public boolean isSelected() {
return isSelected;
}
}
// Ensure one value per key
final Map<Object, T> selectedSet;
/**
* A map of keys to the item and its pending selection state.
*/
private final Map<Object, SelectionChange<T>> selectionChanges;
/**
* Constructs a MultiSelectionModel without a key provider.
*/
public MultiSelectionModel() {
this(null);
}
/**
* Constructs a MultiSelectionModel with the given key provider.
*
* @param keyProvider an instance of ProvidesKey<T>, or null if the item
* should act as its own key
*/
public MultiSelectionModel(ProvidesKey<T> keyProvider) {
this(keyProvider, new HashMap<Object, T>(), new HashMap<Object, SelectionChange<T>>());
}
/**
* Construct a MultiSelectionModel with the given key provider and
* implementations of selectedSet and selectionChanges. Different
* implementations allow for enforcing order on selection.
*
* @param keyProvider an instance of ProvidesKey<T>, or null if the item
* should act as its own key
* @param selectedSet an instance of Map
* @param selectionChanges an instance of Map
*/
MultiSelectionModel(ProvidesKey<T> keyProvider, Map<Object, T> selectedSet,
Map<Object, SelectionChange<T>> selectionChanges) {
super(keyProvider);
this.selectedSet = selectedSet;
this.selectionChanges = selectionChanges;
}
/**
* Deselect all selected values.
*/
@Override
public void clear() {
// Clear the current list of pending changes.
selectionChanges.clear();
/*
* Add a pending change to deselect each key that is currently selected. We
* cannot just clear the selected set, because then we would not know which
* keys were selected before we cleared, which we need to know to determine
* if we should fire an event.
*/
for (T value : selectedSet.values()) {
selectionChanges.put(getKey(value), new SelectionChange<T>(value, false));
}
scheduleSelectionChangeEvent();
}
/**
* Get the set of selected items as a copy. If multiple selected items share
* the same key, only the last selected item is included in the set.
*
* @return the set of selected items
*/
@Override
public Set<T> getSelectedSet() {
resolveChanges();
return new HashSet<T>(selectedSet.values());
}
@Override
public boolean isSelected(T item) {
resolveChanges();
return selectedSet.containsKey(getKey(item));
}
@Override
public void setSelected(T item, boolean selected) {
selectionChanges.put(getKey(item), new SelectionChange<T>(item, selected));
scheduleSelectionChangeEvent();
}
@Override
protected void fireSelectionChangeEvent() {
if (isEventScheduled()) {
setEventCancelled(true);
}
resolveChanges();
}
void resolveChanges() {
if (selectionChanges.isEmpty()) {
return;
}
boolean changed = false;
for (Map.Entry<Object, SelectionChange<T>> entry : selectionChanges.entrySet()) {
Object key = entry.getKey();
SelectionChange<T> value = entry.getValue();
boolean selected = value.isSelected;
T oldValue = selectedSet.get(key);
if (selected) {
selectedSet.put(key, value.item);
Object oldKey = getKey(oldValue);
if (!changed) {
changed = (oldKey == null) ? (key != null) : !oldKey.equals(key);
}
} else {
if (oldValue != null) {
selectedSet.remove(key);
changed = true;
}
}
}
selectionChanges.clear();
// Fire a selection change event.
if (changed) {
SelectionChangeEvent.fire(this);
}
}
}
@@ -111,10 +111,13 @@ public class AccessControlledButtonPanel extends Composite {
/**
* Like {@link #addRemoveAction(String, SetSelectionModel, boolean, Command)} but for rows that are not
* {@link SecuredDTO} instances themselves — use this when the permission to remove entries is governed by a parent
* secured object (e.g. UPDATE permission on the owning {@link UserGroup}) rather than per-row permissions. The
* button is {@link Button#setEnabled(boolean) enabled} when the selection is non-empty and the current user has
* {@link DefaultActions#UPDATE UPDATE} permission on the object supplied by {@code parentSecuredObject}; it shows
* the selected count in its label.
* secured object rather than per-row permissions. The button is {@link Button#setEnabled(boolean) enabled} when the
* selection is non-empty and the current user has the specified {@code permissionAction} on the object supplied by
* {@code parentSecuredObject}; it shows the selected count in its label.
*
* <p>Example: removing a role or user from a {@code UserGroup} is semantically an UPDATE to that group, so the
* caller should pass {@link DefaultActions#UPDATE} as {@code permissionAction} even though the button is labelled
* "Remove".
*
* @param text
* the {@link String text} to show on the button
@@ -122,20 +125,23 @@ public class AccessControlledButtonPanel extends Composite {
* the {@link MultiSelectionModel} of the sub-table; drives the count shown in the button label and the
* enabled state
* @param parentSecuredObject
* supplies the parent {@link SecuredDTO} whose UPDATE permission gates the button; may return
* {@code null} when nothing is selected, which disables the button
* supplies the parent {@link SecuredDTO} whose permission gates the button; may return {@code null} when
* nothing is selected, which disables the button
* @param permissionAction
* the {@link DefaultActions action} to check on the parent secured object (e.g.
* {@link DefaultActions#UPDATE} or {@link DefaultActions#DELETE})
* @param callback
* the {@link Command callback} to execute on button click, if permission is granted
* @return the created {@link Button} instance
*/
public <T> Button addRemoveAction(final String text, final MultiSelectionModel<T> selectionModel,
final Supplier<SecuredDTO> parentSecuredObject, final Command callback) {
public <T> Button addCountingActionWithParentPermission(final String text, final MultiSelectionModel<T> selectionModel,
final Supplier<SecuredDTO> parentSecuredObject, final DefaultActions permissionAction, final Command callback) {
final Button button = resolveButtonVisibility(removePermissionCheck,
new Button(text, wrap(removePermissionCheck, callback)));
selectionModel.addSelectionChangeHandler(event -> {
final int count = selectionModel.getSelectedSet().size();
button.setText(count > 0 ? text + " (" + count + ")" : text);
button.setEnabled(count > 0 && userService.hasPermission(parentSecuredObject.get(), DefaultActions.UPDATE));
button.setEnabled(count > 0 && userService.hasPermission(parentSecuredObject.get(), permissionAction));
});
button.setEnabled(false);
return button;
@@ -123,9 +123,10 @@ public class GroupRoleDefinitionPanel extends Composite
}
});
addButton.ensureDebugId("AddGroupUserButton");
buttonPanel.addRemoveAction(stringMessages.removeRole(),
// Removing a role from a group is semantically an UPDATE to the UserGroup, not a per-role DELETE.
buttonPanel.addCountingActionWithParentPermission(stringMessages.removeRole(),
roleDefinitionTableWrapper.getSelectionModel(),
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), () -> {
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), UPDATE, () -> {
final Pair<StrippedRoleDefinitionDTO, Boolean> selectedRole = TableWrapper.getSingleSelectedObjectOrNull(roleDefinitionTableWrapper.getSelectionModel());
if (selectedRole == null) {
Window.alert(stringMessages.youHaveToSelectAUserGroup());
@@ -99,9 +99,10 @@ public class UserGroupDetailPanel extends Composite
});
addButton.ensureDebugId("AddUserButton");
// add remove button
buttonPanel.addRemoveAction(stringMessages.actionRemove(),
// Removing a user from a group is semantically an UPDATE to the UserGroup, not a per-user DELETE.
buttonPanel.addCountingActionWithParentPermission(stringMessages.actionRemove(),
tenantUsersTable.getSelectionModel(),
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), () -> {
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), UPDATE, () -> {
final Set<UserGroupDTO> selectedUserGroups = userGroupSelectionModel.getSelectedSet();
if (selectedUserGroups != null && selectedUserGroups.size() == 1) {
final UserGroupDTO selectedUserGroup = selectedUserGroups.iterator().next();