From 5870e7ecc3c822352f6428c63987e8dd674d7d93 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Mon, 3 Mar 2025 12:14:45 +0100 Subject: [PATCH 1/7] Better UX while creating groups --- .../su/dsv/scipro/group/EditGroupPanel.html | 102 ++++------- .../su/dsv/scipro/group/EditGroupPanel.java | 166 +++++------------- view/src/main/webapp/css/scipro_m.css | 24 +++ .../dsv/scipro/group/EditGroupPanelTest.java | 1 - 4 files changed, 103 insertions(+), 190 deletions(-) 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..239d4ec0c3 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,45 @@ -
-
-
+
+ -
-
-
-
-
+
-
-
- - -
-
+
+ + +
-
-
- - -
-
+
+ + +
-
-
-
- - +
+ + +
+ +
+ Projects +
+
-
- -
-
-
- -
-
- Add projects to group: -
- -
+
+

+
+
+
- -
-
- Projects in group: - - - - - - - - - - - - - - - - - -
TypeTitleAuthorsRemove
-
-
-
-
-
-
-
+
-
- - -
+ + +
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..be0d59af6b 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,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 model) { super(id, model); add(new GroupForm("form", model)); @@ -47,139 +36,80 @@ public class EditGroupPanel extends Panel { private class GroupForm extends Form { - private final AjaxDropDownChoice addProjects; - private final ListView projects; - private final List currentProjects; - private final AjaxCheckBoxMultipleChoice projectTypes; - public GroupForm(String form, final IModel 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 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> authors = item.getModel().map(Project::getProjectParticipants); + item.add( + new ListView<>("authors", new ListAdapterModel<>(authors)) { + @Override + protected void populateItem(ListItem 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 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 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 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 getSelectableProjects(List currentProjects) { + private List 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 all = projectService.findAll(filter); - all.removeAll(currentProjects); - all.remove(null); - return new ListModel<>(all); + return projectService.findAll(filter); } - private LoadableDetachableModel 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 groupModel; + private final IModel projectModel; + + public SelectProjectModel(IModel groupModel, IModel 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(); } -- 2.39.5 From 510cf9526ff7d965354b121e9e9f35ee7466db48 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Mon, 3 Mar 2025 14:22:40 +0100 Subject: [PATCH 2/7] Add test data --- .../GroupCreationUXImprovement.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 test-data/src/main/java/se/su/dsv/scipro/testdata/populators/GroupCreationUXImprovement.java 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 students = createStudents(factory); + for (int i = 1; i <= 20; i++) { + projectService.save(createProject(baseData, i, supervisor, students)); + } + } + + private List createStudents(Factory factory) { + return Arrays.stream(STUDENT_NAMES).map(factory::createAuthor).toList(); + } + + private Project createProject(BaseData baseData, int i, User supervisor, List 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(); + } +} -- 2.39.5 From d68414947ae2775c3bf4c2a9514aaecbb5bd4252 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 4 Mar 2025 10:48:22 +0100 Subject: [PATCH 3/7] Include projects in group in available list If a project was inactive or completed it was not included in the "relevant projects" list so that they could never be removed from the group. Now all current projects in the group are always included. If such a project is removed it can however not be added back. --- .../java/se/su/dsv/scipro/group/EditGroupPanel.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 be0d59af6b..f5e00ef0e6 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 @@ -42,8 +42,17 @@ public class EditGroupPanel extends Panel { feedbackPanel.setOutputMarkupId(true); add(feedbackPanel); + IModel> availableProjects = LoadableDetachableModel.of(() -> { + Set 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().toList(); + }); add( - new ListView<>("available_projects", LoadableDetachableModel.of(this::getAllRelevantProjects)) { + new ListView<>("available_projects", availableProjects) { @Override protected void populateItem(ListItem item) { CheckBox checkbox = new CheckBox("selected", new SelectProjectModel(model, item.getModel())); -- 2.39.5 From 5ae62e771ff5b74df5a4e6f1f505a893667524f7 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 4 Mar 2025 10:49:41 +0100 Subject: [PATCH 4/7] Sort projects by start date (latest first) and then title since those are the most likely relevant projects. --- .../src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 f5e00ef0e6..6aa52c90d2 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 @@ -49,7 +49,10 @@ public class EditGroupPanel extends Panel { // 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().toList(); + return projects + .stream() + .sorted(Comparator.comparing(Project::getStartDate).reversed().thenComparing(Project::getTitle)) + .toList(); }); add( new ListView<>("available_projects", availableProjects) { -- 2.39.5 From 5493c3582734187cbb2d31ea9d9c662dbfd9fd16 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 4 Mar 2025 11:14:02 +0100 Subject: [PATCH 5/7] Show project start date in group project selection --- view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html | 1 + view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.java | 1 + 2 files changed, 2 insertions(+) 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 239d4ec0c3..00d2fc6b9b 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 @@ -32,6 +32,7 @@

+ Started at
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 6aa52c90d2..44929b3368 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 @@ -63,6 +63,7 @@ public class EditGroupPanel extends Panel { 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> authors = item.getModel().map(Project::getProjectParticipants); item.add( new ListView<>("authors", new ListAdapterModel<>(authors)) { -- 2.39.5 From 3776def0431589e2b8a96b4198bfe0d8a7358ed4 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 4 Mar 2025 11:15:01 +0100 Subject: [PATCH 6/7] Minor styling changes --- view/src/main/java/se/su/dsv/scipro/group/EditGroupPanel.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 00d2fc6b9b..818891f62d 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 @@ -31,7 +31,8 @@

-
+ +
Started at
-- 2.39.5 From 3fbfc4b310f375cbbf2fb537b0179620b338db58 Mon Sep 17 00:00:00 2001 From: Andreas Svanberg Date: Tue, 4 Mar 2025 11:25:38 +0100 Subject: [PATCH 7/7] Add extra buttons to make it easier to create multiple groups and overall reduce the number of clicks --- .../su/dsv/scipro/group/EditGroupPanel.html | 3 +++ .../su/dsv/scipro/group/EditGroupPanel.java | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) 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 818891f62d..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 @@ -42,6 +42,9 @@
+ + + Cancel
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 44929b3368..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 @@ -5,6 +5,7 @@ import java.util.*; 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; @@ -19,6 +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.supervisor.pages.SupervisorEditGroupPage; +import se.su.dsv.scipro.supervisor.pages.SupervisorMyGroupsPage; import se.su.dsv.scipro.system.User; public class EditGroupPanel extends Panel { @@ -82,6 +85,24 @@ public class EditGroupPanel extends Panel { add( new CheckBox("active", LambdaModel.of(model, Group::isActive, Group::setActive)).setOutputMarkupId(true) ); + + add( + new SubmitLink("save_and_close") { + @Override + public void onAfterSubmit() { + setResponsePage(SupervisorMyGroupsPage.class); + } + } + ); + add( + new SubmitLink("save_and_create") { + @Override + public void onAfterSubmit() { + setResponsePage(SupervisorEditGroupPage.class); + } + } + ); + add(new BookmarkablePageLink<>("cancel", SupervisorMyGroupsPage.class)); } @Override -- 2.39.5