diff --git a/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/GroupCreationUXImprovement.java b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/GroupCreationUXImprovement.java new file mode 100644 index 0000000000..7d3c30523a --- /dev/null +++ b/test-data/src/main/java/se/su/dsv/scipro/testdata/populators/GroupCreationUXImprovement.java @@ -0,0 +1,65 @@ +package se.su.dsv.scipro.testdata.populators; + +import jakarta.inject.Inject; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import org.springframework.stereotype.Service; +import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.project.ProjectService; +import se.su.dsv.scipro.system.ProjectType; +import se.su.dsv.scipro.system.User; +import se.su.dsv.scipro.testdata.BaseData; +import se.su.dsv.scipro.testdata.Factory; +import se.su.dsv.scipro.testdata.TestDataPopulator; + +@Service +public class GroupCreationUXImprovement implements TestDataPopulator { + + private static final String[] STUDENT_NAMES = { "Alice", "Bob", "Charlie", "David", "Emma" }; + + private final ProjectService projectService; + + @Inject + public GroupCreationUXImprovement(ProjectService projectService) { + this.projectService = projectService; + } + + @Override + public void populate(BaseData baseData, Factory factory) { + User supervisor = factory.createSupervisor("Evan"); + List<User> students = createStudents(factory); + for (int i = 1; i <= 20; i++) { + projectService.save(createProject(baseData, i, supervisor, students)); + } + } + + private List<User> createStudents(Factory factory) { + return Arrays.stream(STUDENT_NAMES).map(factory::createAuthor).toList(); + } + + private Project createProject(BaseData baseData, int i, User supervisor, List<User> students) { + User author1 = students.get(i % students.size()); + User author2 = students.get((i + 1) % students.size()); + + String title = "Test project " + i; + if (i % 6 == 0) { + title = title + " with a very long title that makes the project special"; + } + + ProjectType projectType = + switch (i % 3) { + case 1 -> baseData.magister(); + case 2 -> baseData.master(); + default -> baseData.bachelor(); + }; + return Project.builder() + .title(title) + .projectType(projectType) + .startDate(LocalDate.now()) + .headSupervisor(supervisor) + .projectParticipants(Set.of(author1, author2)) + .build(); + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html index f6f9edc0a7..e0db52aaa7 100644 --- a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html @@ -2,85 +2,50 @@ <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"> - <form wicket:id="form"> + <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 wicket:id="feedback"></div> - <div class="row"> - <div class="col-lg-5 col-md-10"> - <label wicket:for="title">Title: </label> - <input type="text" wicket:id="title" class="form-control"> - </div> - </div> + <div class="mb-3"> + <label wicket:for="title" class="form-label">Title</label> + <input type="text" wicket:id="title" class="form-control"> + </div> - <div class="row"> - <div class="col-lg-5 col-md-10"> - <label wicket:for="description">Description: </label> - <textarea wicket:id="description" class="form-control"></textarea> - </div> - </div> + <div class="mb-3"> + <label wicket:for="description" class="form-label">Description</label> + <textarea wicket:id="description" class="form-control"></textarea> + </div> - <div class="row"> - <div class="col-lg-5 col-md-10"> - <div class="form-check"> - <input class="form-check-input" wicket:id="active" type="checkbox"/> - <label class="form-check-label" wicket:for="active">Active</label> + <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> + + <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> - </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> - </div> + <div> + <h4 wicket:id="title"></h4> + <span wicket:id="type"></span> + <br> + Started at <span wicket:id="start_date"></span> + <div wicket:id="authors"> + <span wicket:id="author"></span> </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> - </div> - </div> - </div> - </div> + </label> </div> - <br> - <button type="submit" class="btn btn-success" >Save</button> - </form> - </div> + </fieldset> + <button type="submit" class="btn btn-success">Save</button> + <button type="submit" wicket:id="save_and_close" class="btn btn-success">Save and close</button> + <button type="submit" wicket:id="save_and_create" class="btn btn-success">Save and create another</button> + <a wicket:id="cancel" class="btn btn-outline-secondary">Cancel</a> + </form> </div> </wicket:panel> </body> diff --git a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java index 853cedc848..831986ffad 100644 --- a/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java @@ -2,11 +2,10 @@ 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.link.BookmarkablePageLink; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.FeedbackPanel; @@ -14,10 +13,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 +20,8 @@ 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.supervisor.pages.SupervisorEditGroupPage; +import se.su.dsv.scipro.supervisor.pages.SupervisorMyGroupsPage; import se.su.dsv.scipro.system.User; public class EditGroupPanel extends Panel { @@ -37,9 +32,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,18 +39,46 @@ 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()); + IModel<List<Project>> availableProjects = LoadableDetachableModel.of(() -> { + Set<Project> projects = new HashSet<>(); + projects.addAll(getAllRelevantProjects()); + // Have to add the projects that are already in the group to the list of available projects + // since they may not be included in the relevant projects if they're inactive or completed. + // To allow them to be removed from the group, it will not be possible to add them again. + projects.addAll(model.getObject().getProjects()); + return projects + .stream() + .sorted(Comparator.comparing(Project::getStartDate).reversed().thenComparing(Project::getTitle)) + .toList(); + }); + add( + new ListView<>("available_projects", availableProjects) { + @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))); + item.add(new Label("start_date", item.getModel().map(Project::getStartDate))); + 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))); @@ -66,120 +86,64 @@ public class EditGroupPanel extends Panel { 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") { + add( + new SubmitLink("save_and_close") { @Override - protected void onConfigure() { - super.onConfigure(); - setVisibilityAllowed(currentProjects.isEmpty()); + public void onAfterSubmit() { + setResponsePage(SupervisorMyGroupsPage.class); } } ); - - 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); + add( + new SubmitLink("save_and_create") { + @Override + public void onAfterSubmit() { + setResponsePage(SupervisorEditGroupPage.class); + } } - }; + ); + add(new BookmarkablePageLink<>("cancel", SupervisorMyGroupsPage.class)); } @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 LoadableDetachableModel<Project> getLoaded(final Project project) { - return new LoadableDetachableModel<>() { - @Override - protected Project load() { - return projectService.findOne(project.getId()); - } - }; + 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; + } + + @Override + 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()); + } } } } diff --git a/view/src/main/webapp/css/scipro_m.css b/view/src/main/webapp/css/scipro_m.css index 7ce4954a00..49c8b96510 100755 --- a/view/src/main/webapp/css/scipro_m.css +++ b/view/src/main/webapp/css/scipro_m.css @@ -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; +} diff --git a/view/src/test/java/se/su/dsv/scipro/group/EditGroupPanelTest.java b/view/src/test/java/se/su/dsv/scipro/group/EditGroupPanelTest.java index d73dc26f54..2436037335 100644 --- a/view/src/test/java/se/su/dsv/scipro/group/EditGroupPanelTest.java +++ b/view/src/test/java/se/su/dsv/scipro/group/EditGroupPanelTest.java @@ -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(); }