Improve the UX when creating groups as a supervisor #123

Merged
niat8586 merged 9 commits from group-creation-ux into develop 2025-03-05 11:01:37 +01:00
4 changed files with 103 additions and 190 deletions
Showing only changes of commit 5870e7ecc3 - Show all commits

View File

@ -2,86 +2,46 @@
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
<div class="row">
<div class="col-lg-12">
<div class="line-length-limit">
<form wicket:id="form">
<div class="row">
<div class="col-lg-12">
<div wicket:id="feedback"></div>
</div>
</div>
<div class="row">
<div class="col-lg-5 col-md-10">
<label wicket:for="title">Title: </label>
<div class="mb-3">
<label wicket:for="title" class="form-label">Title</label>
<input type="text" wicket:id="title" class="form-control">
</div>
</div>
<div class="row">
<div class="col-lg-5 col-md-10">
<label wicket:for="description">Description: </label>
<div class="mb-3">
<label wicket:for="description" class="form-label">Description</label>
<textarea wicket:id="description" class="form-control"></textarea>
</div>
</div>
<div class="row">
<div class="col-lg-5 col-md-10">
<div class="form-check">
<div class="form-check mb-3">
<input class="form-check-input" wicket:id="active" type="checkbox"/>
<label class="form-check-label" wicket:for="active">Active</label>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div wicket:id="wmc">
<div class="row">
<div class="col-lg-5 col-md-10">
<strong>Add projects to group: </strong>
<div wicket:id="projectTypes"></div>
<select class="form-select" wicket:id="addProjects"></select>
<fieldset class="mb-3">
<legend>Projects</legend>
<div class="group-project-grid">
<label wicket:id="available_projects">
<div>
<input class="form-check-input mt-0" type="checkbox" wicket:id="selected">
</div>
<div>
<h4 wicket:id="title"></h4>
<h5 class="" wicket:id="type"></h5>
<div wicket:id="authors">
<span wicket:id="author"></span>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<strong>Projects in group: </strong>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Title</th>
<th>Authors</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
<tr wicket:id="projects">
<td><span wicket:id="type"></span></td>
<td><span wicket:id="title"></span></td>
<td><div wicket:id="authors">
<div wicket:id="author"></div>
</div></td>
<td><a wicket:id="remove"><span class="fa fa-times"></span></a></td>
</tr>
</tbody>
</table>
<div wicket:id="noProjects"></div>
</label>
</div>
</div>
</div>
</div>
</div>
<br>
</fieldset>
<button type="submit" class="btn btn-success">Save</button>
</form>
</div>
</div>
</wicket:panel>
</body>
</html>

View File

@ -2,9 +2,7 @@ package se.su.dsv.scipro.group;
import jakarta.inject.Inject;
import java.util.*;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.extensions.model.AbstractCheckBoxModel;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.*;
import org.apache.wicket.markup.html.list.ListItem;
@ -14,10 +12,6 @@ import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.util.ListModel;
import se.su.dsv.scipro.components.AjaxCheckBoxMultipleChoice;
import se.su.dsv.scipro.components.AjaxDropDownChoice;
import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.project.Project;
@ -25,8 +19,6 @@ import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.project.ProjectTeamMemberRoles;
import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.system.User;
public class EditGroupPanel extends Panel {
@ -37,9 +29,6 @@ public class EditGroupPanel extends Panel {
@Inject
private GroupService groupService;
@Inject
private ProjectTypeService projectTypeService;
public EditGroupPanel(String id, final IModel<Group> model) {
super(id, model);
add(new GroupForm("form", model));
@ -47,139 +36,80 @@ public class EditGroupPanel extends Panel {
private class GroupForm extends Form<Group> {
private final AjaxDropDownChoice<Project> addProjects;
private final ListView<Project> projects;
private final List<Project> currentProjects;
private final AjaxCheckBoxMultipleChoice<ProjectType> projectTypes;
public GroupForm(String form, final IModel<Group> model) {
super(form, model);
final FeedbackPanel feedbackPanel = new FeedbackPanel("feedback");
feedbackPanel.setOutputMarkupId(true);
add(feedbackPanel);
currentProjects = new ArrayList<>(getModelObject().getProjects());
add(
new ListView<>("available_projects", LoadableDetachableModel.of(this::getAllRelevantProjects)) {
@Override
protected void populateItem(ListItem<Project> item) {
CheckBox checkbox = new CheckBox("selected", new SelectProjectModel(model, item.getModel()));
checkbox.setOutputMarkupId(true);
item.add(checkbox);
item.add(new Label("title", item.getModel().map(Project::getTitle)));
item.add(new Label("type", item.getModel().map(Project::getProjectTypeName)));
IModel<SortedSet<User>> authors = item.getModel().map(Project::getProjectParticipants);
item.add(
new ListView<>("authors", new ListAdapterModel<>(authors)) {
@Override
protected void populateItem(ListItem<User> item) {
item.add(new UserLinkPanel("author", item.getModel()));
}
}
);
}
}
);
add(new RequiredTextField<>("title", LambdaModel.of(model, Group::getTitle, Group::setTitle)));
add(new TextArea<>("description", LambdaModel.of(model, Group::getDescription, Group::setDescription)));
add(
new CheckBox("active", LambdaModel.of(model, Group::isActive, Group::setActive)).setOutputMarkupId(true)
);
final WebMarkupContainer wmc = new WebMarkupContainer("wmc");
wmc.setOutputMarkupId(true);
projectTypes = projectTypeSelection(wmc);
wmc.add(projectTypes);
addProjects = new AjaxDropDownChoice<>(
"addProjects",
new Model<>(),
getSelectableProjects(currentProjects),
new LambdaChoiceRenderer<>(Project::getTitle, Project::getId)
) {
@Override
public void onNewSelection(AjaxRequestTarget target, Project objectSelected) {
if (objectSelected != null && !currentProjects.contains(objectSelected)) {
currentProjects.add(objectSelected);
projects.setList(currentProjects);
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
}
}
};
addProjects.setRequired(false);
addProjects.setNullValid(true);
wmc.add(addProjects);
projects = new ListView<>("projects", new ArrayList<>(currentProjects)) {
@Override
protected void populateItem(final ListItem<Project> item) {
item.add(new Label("type", item.getModel().map(Project::getProjectTypeName)));
item.add(new Label("title", item.getModel().map(Project::getTitle)));
item.add(
new ListView<>(
"authors",
new ListAdapterModel<>(
getLoaded(item.getModelObject()).map(Project::getProjectParticipants)
)
) {
@Override
public void populateItem(ListItem<User> item) {
item.add(new UserLinkPanel("author", item.getModel()));
}
}
);
item.add(
new AjaxLink<>("remove", item.getModel()) {
@Override
public void onClick(AjaxRequestTarget target) {
currentProjects.remove(item.getModelObject());
projects.setList(currentProjects);
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
}
}
);
}
};
wmc.add(projects);
wmc.add(
new Label("noProjects", "None") {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(currentProjects.isEmpty());
}
}
);
add(wmc);
}
private AjaxCheckBoxMultipleChoice<ProjectType> projectTypeSelection(final WebMarkupContainer wmc) {
return new AjaxCheckBoxMultipleChoice<>(
"projectTypes",
projectTypeService.findAllActive(),
projectTypeService.findAllActive(),
new LambdaChoiceRenderer<>(ProjectType::getName, ProjectType::getId)
) {
@Override
public void onUpdate(AjaxRequestTarget target) {
addProjects.setChoices(getSelectableProjects(currentProjects));
target.add(wmc);
}
};
}
@Override
protected void onSubmit() {
Group group = getModelObject();
group.setProjects(new HashSet<>(currentProjects));
groupService.save(group);
info(getString("saved"));
}
private ListModel<Project> getSelectableProjects(List<Project> currentProjects) {
private List<Project> getAllRelevantProjects() {
final ProjectService.Filter filter = new ProjectService.Filter();
filter.setSupervisor(SciProSession.get().getUser());
filter.setRoles(Collections.singleton(ProjectTeamMemberRoles.CO_SUPERVISOR));
filter.setStatuses(Collections.singletonList(ProjectStatus.ACTIVE));
filter.setProjectTypes(projectTypes.getModelObject());
List<Project> all = projectService.findAll(filter);
all.removeAll(currentProjects);
all.remove(null);
return new ListModel<>(all);
return projectService.findAll(filter);
}
private static final class SelectProjectModel extends AbstractCheckBoxModel {
private final IModel<Group> groupModel;
private final IModel<Project> projectModel;
public SelectProjectModel(IModel<Group> groupModel, IModel<Project> projectModel) {
this.groupModel = groupModel;
this.projectModel = projectModel;
}
private LoadableDetachableModel<Project> getLoaded(final Project project) {
return new LoadableDetachableModel<>() {
@Override
protected Project load() {
return projectService.findOne(project.getId());
public boolean isSelected() {
return groupModel.getObject().getProjects().contains(projectModel.getObject());
}
@Override
public void select() {
groupModel.getObject().getProjects().add(projectModel.getObject());
}
@Override
public void unselect() {
groupModel.getObject().getProjects().remove(projectModel.getObject());
}
};
}
}
}

View File

@ -607,3 +607,27 @@ th.wicket_orderUp, th.sorting_asc {
.line-length-limit {
max-width: 80em;
}
.group-project-grid {
display: flex;
flex-wrap: wrap;
gap: 1em;
grid-template-columns: repeat(auto-fill, minmax(30em, 1fr));
}
.group-project-grid > * {
background: linear-gradient(to left, white 40%, var(--bs-success-bg-subtle) 60%) right;
background-size: 250% 100%;
transition: background 0.4s ease;
cursor: pointer;
border: 1px solid black;
border-radius: 0.25em;
display: flex;
padding: 0.5em;
align-items: center;
flex-grow: 1;
}
.group-project-grid > *:has(:checked) {
background-position: left;
}
.group-project-grid label {
font-weight: normal;
}

View File

@ -27,7 +27,6 @@ public class EditGroupPanelTest extends SciProTest {
group.setId(1L);
Project project = createProject();
group.setProjects(new HashSet<>(Collections.singletonList(project)));
when(projectService.findOne(anyLong())).thenReturn(project);
startPanel();
}