feat: Add description/permission list to role selector (OD-2335)

This commit is contained in:
Robin Shen 2025-03-24 22:47:08 +08:00
parent 3ff5e40419
commit 8951b29ad8
13 changed files with 307 additions and 63 deletions

View File

@ -41,6 +41,8 @@ public class Role extends AbstractEntity implements BasePermission {
@Column(nullable=false, unique=true)
private String name;
private String description;
private boolean manageProject;
@ -104,6 +106,15 @@ public class Role extends AbstractEntity implements BasePermission {
this.name = name;
}
@Editable(order=110)
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean isOwner() {
return getId().equals(OWNER_ID);
}

View File

@ -0,0 +1,42 @@
package io.onedev.server.web.component.rolechoice;
import java.util.Collection;
import java.util.List;
import org.hibernate.Hibernate;
import org.json.JSONException;
import org.json.JSONWriter;
import org.unbescape.html.HtmlEscape;
import com.google.common.collect.Lists;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.RoleManager;
import io.onedev.server.model.Role;
import io.onedev.server.web.component.select2.ChoiceProvider;
public abstract class AbstractRoleChoiceProvider extends ChoiceProvider<Role> {
private static final long serialVersionUID = 1L;
@Override
public void toJson(Role choice, JSONWriter writer) throws JSONException {
writer.key("id").value(choice.getId()).key("name").value(HtmlEscape.escapeHtml5(choice.getName()));
if (choice.getDescription() != null)
writer.key("description").value(HtmlEscape.escapeHtml5(choice.getDescription()));
}
@Override
public Collection<Role> toChoices(Collection<String> ids) {
List<Role> roles = Lists.newArrayList();
RoleManager roleManager = OneDev.getInstance(RoleManager.class);
for (String each : ids) {
Role role = roleManager.load(Long.valueOf(each));
Hibernate.initialize(role);
roles.add(role);
}
return roles;
}
}

View File

@ -0,0 +1,46 @@
package io.onedev.server.web.component.rolechoice;
import java.util.Collection;
import java.util.List;
import org.apache.wicket.model.IModel;
import io.onedev.server.model.Role;
import io.onedev.server.util.Similarities;
import io.onedev.server.web.WebConstants;
import io.onedev.server.web.component.select2.Response;
import io.onedev.server.web.component.select2.ResponseFiller;
public class RoleChoiceProvider extends AbstractRoleChoiceProvider {
private static final long serialVersionUID = 1L;
private final IModel<Collection<Role>> choicesModel;
public RoleChoiceProvider(IModel<Collection<Role>> choicesModel) {
this.choicesModel = choicesModel;
}
@Override
public void detach() {
choicesModel.detach();
super.detach();
}
@Override
public void query(String term, int page, Response<Role> response) {
List<Role> similarities = new Similarities<Role>(choicesModel.getObject()) {
private static final long serialVersionUID = 1L;
@Override
public double getSimilarScore(Role object) {
return Similarities.getSimilarScore(object.getName(), term);
}
};
new ResponseFiller<Role>(response).fill(similarities, page, WebConstants.PAGE_SIZE);
}
}

View File

@ -0,0 +1,13 @@
package io.onedev.server.web.component.rolechoice;
import io.onedev.server.web.page.base.BaseDependentResourceReference;
public class RoleChoiceResourceReference extends BaseDependentResourceReference {
private static final long serialVersionUID = 1L;
public RoleChoiceResourceReference() {
super(RoleChoiceResourceReference.class, "role-choice.js");
}
}

View File

@ -0,0 +1,38 @@
package io.onedev.server.web.component.rolechoice;
import java.util.Collection;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.model.IModel;
import io.onedev.server.model.Role;
import io.onedev.server.web.component.select2.Select2MultiChoice;
public class RoleMultiChoice extends Select2MultiChoice<Role> {
public RoleMultiChoice(String id, IModel<Collection<Role>> selectionsModel, IModel<Collection<Role>> choicesModel) {
super(id, selectionsModel, new RoleChoiceProvider(choicesModel));
}
@Override
protected void onInitialize() {
super.onInitialize();
if (isRequired())
getSettings().setPlaceholder("Choose roles...");
else
getSettings().setPlaceholder("Not specified");
getSettings().setFormatResult("onedev.server.roleChoiceFormatter.formatResult");
getSettings().setFormatSelection("onedev.server.roleChoiceFormatter.formatSelection");
getSettings().setEscapeMarkup("onedev.server.roleChoiceFormatter.escapeMarkup");
setConvertEmptyInputStringToNull(true);
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(JavaScriptHeaderItem.forReference(new RoleChoiceResourceReference()));
}
}

View File

@ -0,0 +1,40 @@
package io.onedev.server.web.component.rolechoice;
import java.util.Collection;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.model.IModel;
import io.onedev.server.model.Role;
import io.onedev.server.web.component.select2.Select2Choice;
public class RoleSingleChoice extends Select2Choice<Role> {
public RoleSingleChoice(String id, IModel<Role> selectionModel, IModel<Collection<Role>> choicesModel) {
super(id, selectionModel, new RoleChoiceProvider(choicesModel));
}
@Override
protected void onInitialize() {
super.onInitialize();
getSettings().setAllowClear(!isRequired());
if (isRequired())
getSettings().setPlaceholder("Choose role...");
else
getSettings().setPlaceholder("Not specified");
getSettings().setFormatResult("onedev.server.roleChoiceFormatter.formatResult");
getSettings().setFormatSelection("onedev.server.roleChoiceFormatter.formatSelection");
getSettings().setEscapeMarkup("onedev.server.roleChoiceFormatter.escapeMarkup");
setConvertEmptyInputStringToNull(true);
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(JavaScriptHeaderItem.forReference(new RoleChoiceResourceReference()));
}
}

View File

@ -0,0 +1,13 @@
onedev.server.roleChoiceFormatter = {
formatSelection: function(role) {
return role.name;
},
formatResult: function(role) {
return role.name + (role.description ? " <span class='text-muted font-size-sm'>" + role.description + "</span>" : "");
},
escapeMarkup: function(m) {
return m;
}
};

View File

@ -2,7 +2,7 @@ package io.onedev.server.web.editable.rolechoice;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Collection;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.basic.Label;
@ -22,22 +22,21 @@ public class RoleChoiceEditSupport implements EditSupport {
@Override
public PropertyContext<?> getEditContext(PropertyDescriptor descriptor) {
Method propertyGetter = descriptor.getPropertyGetter();
RoleChoice roleChoice = propertyGetter.getAnnotation(RoleChoice.class);
if (roleChoice != null) {
if (List.class.isAssignableFrom(propertyGetter.getReturnType())
Method propertyGetter = descriptor.getPropertyGetter();
if (propertyGetter.getAnnotation(RoleChoice.class) != null) {
if (Collection.class.isAssignableFrom(propertyGetter.getReturnType())
&& ReflectionUtils.getCollectionElementClass(propertyGetter.getGenericReturnType()) == String.class) {
return new PropertyContext<List<String>>(descriptor) {
return new PropertyContext<Collection<String>>(descriptor) {
@Override
public PropertyViewer renderForView(String componentId, final IModel<List<String>> model) {
public PropertyViewer renderForView(String componentId, final IModel<Collection<String>> model) {
return new PropertyViewer(componentId, descriptor) {
@Override
protected Component newContent(String id, PropertyDescriptor propertyDescriptor) {
List<String> roleNames = model.getObject();
if (roleNames != null && !roleNames.isEmpty()) {
return new Label(id, StringUtils.join(roleNames, ", " ));
Collection<String> teamNames = model.getObject();
if (teamNames != null && !teamNames.isEmpty()) {
return new Label(id, StringUtils.join(teamNames, ", " ));
} else {
return new EmptyValueLabel(id) {
@ -54,7 +53,7 @@ public class RoleChoiceEditSupport implements EditSupport {
}
@Override
public PropertyEditor<List<String>> renderForEdit(String componentId, IModel<List<String>> model) {
public PropertyEditor<Collection<String>> renderForEdit(String componentId, IModel<Collection<String>> model) {
return new RoleMultiChoiceEditor(componentId, descriptor, model);
}
@ -68,8 +67,9 @@ public class RoleChoiceEditSupport implements EditSupport {
@Override
protected Component newContent(String id, PropertyDescriptor propertyDescriptor) {
if (model.getObject() != null) {
return new Label(id, model.getObject());
String teamName = model.getObject();
if (teamName != null) {
return new Label(id, teamName);
} else {
return new EmptyValueLabel(id) {
@ -92,7 +92,7 @@ public class RoleChoiceEditSupport implements EditSupport {
};
} else {
throw new RuntimeException("Annotation 'RoleChoice' should be applied to property with type 'String' or 'List<String>'.");
throw new RuntimeException("Annotation 'TeamChoice' should be applied to property with type String or Collection<String>");
}
} else {
return null;

View File

@ -1,3 +1,3 @@
<wicket:panel>
<input wicket:id="input" type="hidden"></input>
<input wicket:id="input" type="hidden">
</wicket:panel>

View File

@ -2,9 +2,8 @@ package io.onedev.server.web.editable.rolechoice;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
@ -12,37 +11,53 @@ import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.convert.ConversionException;
import com.google.common.base.Preconditions;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.RoleChoice;
import io.onedev.server.entitymanager.RoleManager;
import io.onedev.server.model.Role;
import io.onedev.server.web.component.stringchoice.StringMultiChoice;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.web.component.rolechoice.RoleMultiChoice;
import io.onedev.server.web.editable.PropertyDescriptor;
import io.onedev.server.web.editable.PropertyEditor;
public class RoleMultiChoiceEditor extends PropertyEditor<List<String>> {
public class RoleMultiChoiceEditor extends PropertyEditor<Collection<String>> {
private StringMultiChoice input;
private RoleMultiChoice input;
public RoleMultiChoiceEditor(String id, PropertyDescriptor propertyDescriptor,
IModel<List<String>> propertyModel) {
public RoleMultiChoiceEditor(String id, PropertyDescriptor propertyDescriptor, IModel<Collection<String>> propertyModel) {
super(id, propertyDescriptor, propertyModel);
}
@Override
protected void onInitialize() {
super.onInitialize();
List<Role> choices = new ArrayList<>();
Map<String, String> roleNames = new LinkedHashMap<>();
for (Role role: OneDev.getInstance(RoleManager.class).query())
roleNames.put(role.getName(), role.getName());
ComponentContext componentContext = new ComponentContext(this);
ComponentContext.push(componentContext);
try {
RoleChoice roleChoice = descriptor.getPropertyGetter().getAnnotation(RoleChoice.class);
Preconditions.checkNotNull(roleChoice);
choices.addAll(OneDev.getInstance(RoleManager.class).query());
choices.sort(Comparator.comparing(Role::getName));
} finally {
ComponentContext.pop();
}
List<Role> selections = new ArrayList<>();
if (getModelObject() != null) {
RoleManager roleManager = OneDev.getInstance(RoleManager.class);
for (String roleName: getModelObject()) {
Role role = roleManager.find(roleName);
if (role != null && choices.contains(role))
selections.add(role);
}
}
Collection<String> selections = new ArrayList<>();
if (getModelObject() != null)
selections.addAll(getModelObject());
selections.retainAll(roleNames.keySet());
input = new StringMultiChoice("input", Model.of(selections), Model.ofMap(roleNames), false) {
input = new RoleMultiChoice("input", Model.of(selections), Model.of(choices)) {
@Override
protected void onInitialize() {
@ -51,7 +66,6 @@ public class RoleMultiChoiceEditor extends PropertyEditor<List<String>> {
}
};
input.setConvertEmptyInputStringToNull(true);
input.setLabel(Model.of(getDescriptor().getDisplayName()));
@ -69,11 +83,13 @@ public class RoleMultiChoiceEditor extends PropertyEditor<List<String>> {
@Override
protected List<String> convertInputToValue() throws ConversionException {
Collection<String> roleNames = input.getConvertedInput();
if (roleNames != null)
return new ArrayList<>(roleNames);
else
return new ArrayList<>();
List<String> roleNames = new ArrayList<>();
Collection<Role> roles = input.getConvertedInput();
if (roles != null) {
for (Role role: roles)
roleNames.add(role.getName());
}
return roleNames;
}
@Override

View File

@ -1,3 +1,3 @@
<wicket:panel>
<input wicket:id="input" type="hidden"></input>
<input wicket:id="input" type="hidden" class="select2"></input>
</wicket:panel>

View File

@ -1,29 +1,31 @@
package io.onedev.server.web.editable.rolechoice;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.convert.ConversionException;
import org.hibernate.criterion.Order;
import com.google.common.base.Preconditions;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.RoleChoice;
import io.onedev.server.entitymanager.RoleManager;
import io.onedev.server.model.Role;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.web.component.stringchoice.StringSingleChoice;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.web.component.rolechoice.RoleSingleChoice;
import io.onedev.server.web.editable.PropertyDescriptor;
import io.onedev.server.web.editable.PropertyEditor;
public class RoleSingleChoiceEditor extends PropertyEditor<String> {
private StringSingleChoice input;
public RoleSingleChoiceEditor(String id, PropertyDescriptor propertyDescriptor,
IModel<String> propertyModel) {
private RoleSingleChoice input;
public RoleSingleChoiceEditor(String id, PropertyDescriptor propertyDescriptor, IModel<String> propertyModel) {
super(id, propertyDescriptor, propertyModel);
}
@ -31,20 +33,30 @@ public class RoleSingleChoiceEditor extends PropertyEditor<String> {
protected void onInitialize() {
super.onInitialize();
Map<String, String> roleNames = new LinkedHashMap<>();
List<Role> choices = new ArrayList<>();
var criteria = EntityCriteria.of(Role.class);
criteria.addOrder(Order.asc(Role.PROP_NAME));
for (Role role: OneDev.getInstance(RoleManager.class).query(criteria))
roleNames.put(role.getName(), role.getName());
ComponentContext componentContext = new ComponentContext(this);
ComponentContext.push(componentContext);
try {
RoleChoice roleChoice = descriptor.getPropertyGetter().getAnnotation(RoleChoice.class);
Preconditions.checkNotNull(roleChoice);
choices.addAll(OneDev.getInstance(RoleManager.class).query());
choices.sort(Comparator.comparing(Role::getName));
} finally {
ComponentContext.pop();
}
Role role;
if (getModelObject() != null)
role = OneDev.getInstance(RoleManager.class).find(getModelObject());
else
role = null;
String selection = getModelObject();
if (!roleNames.containsKey(selection))
selection = null;
input = new StringSingleChoice("input", Model.of(selection), Model.ofMap(roleNames), false) {
if (role != null && !choices.contains(role))
role = null;
@Override
input = new RoleSingleChoice("input", Model.of(role), Model.of(choices)) {
@Override
protected void onInitialize() {
super.onInitialize();
getSettings().configurePlaceholder(descriptor);
@ -52,9 +64,8 @@ public class RoleSingleChoiceEditor extends PropertyEditor<String> {
}
};
input.setLabel(Model.of(getDescriptor().getDisplayName()));
input.add(new AjaxFormComponentUpdatingBehavior("change"){
@Override
@ -63,12 +74,17 @@ public class RoleSingleChoiceEditor extends PropertyEditor<String> {
}
});
add(input);
add(input);
}
@Override
protected String convertInputToValue() throws ConversionException {
return input.getConvertedInput();
Role role = input.getConvertedInput();
if (role != null)
return role.getName();
else
return null;
}
@Override

View File

@ -163,6 +163,15 @@ public class RoleListPage extends AdministrationPage {
cellItem.add(fragment);
}
});
columns.add(new AbstractColumn<Role, Void>(Model.of("Description")) {
@Override
public void populateItem(Item<ICellPopulator<Role>> cellItem, String componentId, IModel<Role> rowModel) {
cellItem.add(new Label(componentId, rowModel.getObject().getDescription()));
}
});
columns.add(new AbstractColumn<Role, Void>(Model.of("")) {