Forum Message UI Improvement (Thesis Board #3470) #61

Merged
tozh4728 merged 20 commits from 3470-forum-msg-ui-improvement into develop 2024-12-19 15:28:23 +01:00
9 changed files with 164 additions and 107 deletions
Showing only changes of commit 08f40959fd - Show all commits

View File

@ -5,10 +5,10 @@ import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.List;
import java.util.Set;
public interface BasicForumService extends Serializable { public interface BasicForumService extends Serializable {
ForumPost createReply(ForumThread forumThread, User poster, String content, Set<Attachment> attachments); ForumPost createReply(ForumThread forumThread, User poster, String content, Set<Attachment> attachments);
boolean setRead(User user, ForumPost post, boolean read); boolean setRead(User user, ForumPost post, boolean read);

View File

@ -1,20 +1,18 @@
package se.su.dsv.scipro.forum; package se.su.dsv.scipro.forum;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import se.su.dsv.scipro.system.JpaRepository; import java.util.List;
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread; import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread; import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.JpaRepository;
import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair; import se.su.dsv.scipro.util.Pair;
import java.util.List;
@Transactional @Transactional
public interface ForumPostRepository extends JpaRepository<ForumPost, Long>, QueryDslPredicateExecutor<ForumPost> { public interface ForumPostRepository extends JpaRepository<ForumPost, Long>, QueryDslPredicateExecutor<ForumPost> {
List<ForumPost> findByThread(ForumThread forumThread); List<ForumPost> findByThread(ForumThread forumThread);
List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount); List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount);

View File

@ -1,10 +1,13 @@
package se.su.dsv.scipro.forum; package se.su.dsv.scipro.forum;
import static com.querydsl.core.types.dsl.Expressions.allOf;
import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQuery;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import java.util.List;
import se.su.dsv.scipro.forum.dataobjects.ForumPost; import se.su.dsv.scipro.forum.dataobjects.ForumPost;
import se.su.dsv.scipro.forum.dataobjects.ForumThread; import se.su.dsv.scipro.forum.dataobjects.ForumThread;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread; import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
@ -17,11 +20,8 @@ import se.su.dsv.scipro.system.GenericRepo;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Pair; import se.su.dsv.scipro.util.Pair;
import java.util.List;
import static com.querydsl.core.types.dsl.Expressions.allOf;
public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implements ForumPostRepository { public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implements ForumPostRepository {
@Inject @Inject
public ForumPostRepositoryImpl(Provider<EntityManager> em) { public ForumPostRepositoryImpl(Provider<EntityManager> em) {
super(em, ForumPost.class, QForumPost.forumPost); super(em, ForumPost.class, QForumPost.forumPost);
@ -35,33 +35,36 @@ public class ForumPostRepositoryImpl extends GenericRepo<ForumPost, Long> implem
@Override @Override
public List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount) { public List<Pair<ProjectThread, ForumPost>> latestPost(Project project, int amount) {
return new JPAQuery<>(em()) return new JPAQuery<>(em())
.select(QProjectThread.projectThread, QForumPost.forumPost) .select(QProjectThread.projectThread, QForumPost.forumPost)
.from(QProjectThread.projectThread) .from(QProjectThread.projectThread)
.innerJoin(QProjectThread.projectThread.forumThread) .innerJoin(QProjectThread.projectThread.forumThread)
.innerJoin(QForumThread.forumThread.posts, QForumPost.forumPost) .innerJoin(QForumThread.forumThread.posts, QForumPost.forumPost)
.where(QProjectThread.projectThread.project.eq(project)) .where(QProjectThread.projectThread.project.eq(project))
.orderBy(QForumPost.forumPost.dateCreated.desc()) .orderBy(QForumPost.forumPost.dateCreated.desc())
.limit(amount) .limit(amount)
.fetch() .fetch()
.stream() .stream()
.map(tuple -> new Pair<>( .map(tuple -> new Pair<>(tuple.get(QProjectThread.projectThread), tuple.get(QForumPost.forumPost)))
tuple.get(QProjectThread.projectThread), .toList();
tuple.get(QForumPost.forumPost)))
.toList();
} }
@Override @Override
public long countUnreadThreads(List<ForumThread> forumThreadList, User user) { public long countUnreadThreads(List<ForumThread> forumThreadList, User user) {
return new JPAQuery<>(em()) return new JPAQuery<>(em())
.select(QForumThread.forumThread.id.countDistinct()) .select(QForumThread.forumThread.id.countDistinct())
.from(QForumThread.forumThread) .from(QForumThread.forumThread)
.leftJoin(QForumThread.forumThread.posts, QForumPost.forumPost) .leftJoin(QForumThread.forumThread.posts, QForumPost.forumPost)
.where(QForumPost.forumPost.notIn( .where(
JPAExpressions.select(QForumPostReadState.forumPostReadState.id.post) QForumPost.forumPost.notIn(
.from(QForumPostReadState.forumPostReadState) JPAExpressions.select(QForumPostReadState.forumPostReadState.id.post)
.where(QForumPostReadState.forumPostReadState.read.isTrue(), .from(QForumPostReadState.forumPostReadState)
QForumPostReadState.forumPostReadState.id.user.eq(user)) .where(
), QForumThread.forumThread.in(forumThreadList) QForumPostReadState.forumPostReadState.read.isTrue(),
).fetchOne(); QForumPostReadState.forumPostReadState.id.user.eq(user)
)
),
QForumThread.forumThread.in(forumThreadList)
)
.fetchOne();
} }
} }

View File

@ -127,9 +127,7 @@ public class ProjectForumServiceImpl implements ProjectForumService {
@Override @Override
public long getUnreadThreadsCount(Project project, User user) { public long getUnreadThreadsCount(Project project, User user) {
List<ProjectThread> threads = getThreads(project); List<ProjectThread> threads = getThreads(project);
List<ForumThread> list = threads.stream() List<ForumThread> list = threads.stream().map(ProjectThread::getForumThread).toList();
.map(ProjectThread::getForumThread)
.toList();
return basicForumService.countUnreadThreads(list, user); return basicForumService.countUnreadThreads(list, user);
} }

View File

@ -1,16 +1,14 @@
package se.su.dsv.scipro.forum; package se.su.dsv.scipro.forum;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import java.util.List;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread; import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.JpaRepository; import se.su.dsv.scipro.system.JpaRepository;
import se.su.dsv.scipro.system.QueryDslPredicateExecutor; import se.su.dsv.scipro.system.QueryDslPredicateExecutor;
import java.util.List;
@Transactional @Transactional
public interface ProjectThreadRepository extends JpaRepository<ProjectThread, Long>, QueryDslPredicateExecutor<ProjectThread> { public interface ProjectThreadRepository
extends JpaRepository<ProjectThread, Long>, QueryDslPredicateExecutor<ProjectThread> {
List<ProjectThread> findByProject(Project project); List<ProjectThread> findByProject(Project project);
} }

View File

@ -3,14 +3,14 @@ package se.su.dsv.scipro.forum;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import java.util.List;
import se.su.dsv.scipro.forum.dataobjects.ProjectThread; import se.su.dsv.scipro.forum.dataobjects.ProjectThread;
import se.su.dsv.scipro.forum.dataobjects.QProjectThread; import se.su.dsv.scipro.forum.dataobjects.QProjectThread;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.GenericRepo; import se.su.dsv.scipro.system.GenericRepo;
import java.util.List;
public class ProjectThreadRepositoryImpl extends GenericRepo<ProjectThread, Long> implements ProjectThreadRepository { public class ProjectThreadRepositoryImpl extends GenericRepo<ProjectThread, Long> implements ProjectThreadRepository {
@Inject @Inject
public ProjectThreadRepositoryImpl(Provider<EntityManager> em) { public ProjectThreadRepositoryImpl(Provider<EntityManager> em) {
super(em, ProjectThread.class, QProjectThread.projectThread); super(em, ProjectThread.class, QProjectThread.projectThread);

View File

@ -1,5 +1,6 @@
package se.su.dsv.scipro.forum.panels; package se.su.dsv.scipro.forum.panels;
import java.util.Optional;
import org.apache.wicket.Component; import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink;
@ -7,8 +8,6 @@ import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.html.WebComponent; import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.markup.html.panel.Panel;
import java.util.Optional;
public abstract class AbstractReadStatePanel extends Panel { public abstract class AbstractReadStatePanel extends Panel {
public static final String TOGGLE = "toggle"; public static final String TOGGLE = "toggle";
@ -16,7 +15,6 @@ public abstract class AbstractReadStatePanel extends Panel {
public AbstractReadStatePanel(final String id) { public AbstractReadStatePanel(final String id) {
super(id); super(id);
Component icon = new UpdatingImage(ICON); Component icon = new UpdatingImage(ICON);
icon.setOutputMarkupId(true); icon.setOutputMarkupId(true);
@ -35,9 +33,11 @@ public abstract class AbstractReadStatePanel extends Panel {
} }
protected abstract boolean isRead(); protected abstract boolean isRead();
protected abstract void onFlagClick(final AjaxRequestTarget target); protected abstract void onFlagClick(final AjaxRequestTarget target);
private class UpdatingImage extends WebComponent { private class UpdatingImage extends WebComponent {
public UpdatingImage(String id) { public UpdatingImage(String id) {
super(id); super(id);
} }

View File

@ -11,9 +11,7 @@ public abstract class NumberOfMessagesPanel extends Panel {
public NumberOfMessagesPanel(final String id) { public NumberOfMessagesPanel(final String id) {
super(id); super(id);
add(new Label("msgCount", model)); add(new Label("msgCount", model));
} }
@Override @Override

View File

@ -1,10 +1,14 @@
package se.su.dsv.scipro.supervisor.panels; package se.su.dsv.scipro.supervisor.panels;
import static java.util.Arrays.asList;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior; import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox; import org.apache.wicket.ajax.markup.html.form.AjaxCheckBox;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator; import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder; import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn; import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
@ -47,11 +51,6 @@ import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService; import se.su.dsv.scipro.system.ProjectTypeService;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import java.util.ArrayList;
import java.util.List;
import static java.util.Arrays.asList;
public class SupervisorMyProjectsPanel extends Panel { public class SupervisorMyProjectsPanel extends Panel {
public static final String FILTER_FORM = "filterForm"; public static final String FILTER_FORM = "filterForm";
@ -63,10 +62,13 @@ public class SupervisorMyProjectsPanel extends Panel {
@Inject @Inject
private ProjectTypeService projectTypeService; private ProjectTypeService projectTypeService;
@Inject @Inject
private ProjectService projectService; private ProjectService projectService;
@Inject @Inject
private UserProfileService profileService; private UserProfileService profileService;
@Inject @Inject
private ProjectForumService projectForumService; private ProjectForumService projectForumService;
@ -82,7 +84,10 @@ public class SupervisorMyProjectsPanel extends Panel {
} }
private void dataViewSetup() { private void dataViewSetup() {
SortableDataProvider<Project, String> provider = new FilteredDataProvider<>(projectService, Model.of(this.filter)); SortableDataProvider<Project, String> provider = new FilteredDataProvider<>(
projectService,
Model.of(this.filter)
);
provider.setSort("dateCreated", SortOrder.DESCENDING); provider.setSort("dateCreated", SortOrder.DESCENDING);
dataPanel = new ExportableDataPanel<>(DATA_PANEL, createColumns(), provider); dataPanel = new ExportableDataPanel<>(DATA_PANEL, createColumns(), provider);
add(dataPanel); add(dataPanel);
@ -95,14 +100,24 @@ public class SupervisorMyProjectsPanel extends Panel {
columns.add(new TemporalColumn<>(Model.of("Started"), "startDate", Project::getStartDate)); columns.add(new TemporalColumn<>(Model.of("Started"), "startDate", Project::getStartDate));
columns.add(new LambdaColumn<>(Model.of("Type"), "projectType.name", Project::getProjectTypeName)); columns.add(new LambdaColumn<>(Model.of("Type"), "projectType.name", Project::getProjectTypeName));
columns.add(new ProjectTitleColumn(Model.of("Title"), "title")); columns.add(new ProjectTitleColumn(Model.of("Title"), "title"));
columns.add(new MultipleUsersColumn<>(Model.of("Authors")) { columns.add(
@Override new MultipleUsersColumn<>(Model.of("Authors")) {
public ListAdapterModel<User> getUsers(final IModel<Project> rowModel) { @Override
return new ListAdapterModel<>(rowModel.map(Project::getProjectParticipants)); public ListAdapterModel<User> getUsers(final IModel<Project> rowModel) {
return new ListAdapterModel<>(rowModel.map(Project::getProjectParticipants));
}
} }
}); );
columns.add(new ProjectNoteColumn(Model.of("Note"), LoadableDetachableModel.of(this::currentUser), supervisorProjectNoteDisplayModel)); columns.add(
columns.add(new UserColumn<>(Model.of("Head supervisor"), "headSupervisor.fullName", Project::getHeadSupervisor)); new ProjectNoteColumn(
Model.of("Note"),
LoadableDetachableModel.of(this::currentUser),
supervisorProjectNoteDisplayModel
)
);
columns.add(
new UserColumn<>(Model.of("Head supervisor"), "headSupervisor.fullName", Project::getHeadSupervisor)
);
return columns; return columns;
} }
@ -139,47 +154,87 @@ public class SupervisorMyProjectsPanel extends Panel {
public FilterForm(String id, IModel<ProjectService.Filter> filterParams) { public FilterForm(String id, IModel<ProjectService.Filter> filterParams) {
super(id, filterParams); super(id, filterParams);
add(
add(new AjaxCheckBox(SUPERVISOR, LambdaModel.of(filterParams, ProjectService.Filter::isFilterSupervisor, ProjectService.Filter::setFilterSupervisor)) { new AjaxCheckBox(
@Override SUPERVISOR,
protected void onUpdate(AjaxRequestTarget target) { LambdaModel.of(
target.add(dataPanel); filterParams,
updateProfileWithCurrentFilter(); ProjectService.Filter::isFilterSupervisor,
ProjectService.Filter::setFilterSupervisor
)
) {
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.add(dataPanel);
updateProfileWithCurrentFilter();
}
} }
}); );
add(new AjaxCheckBoxMultipleChoice<>(ROLE_FILTER, LambdaModel.of(filterParams, ProjectService.Filter::getRoles, ProjectService.Filter::setRoles), List.of(ProjectTeamMemberRoles.CO_SUPERVISOR), new EnumChoiceRenderer<>(this)) { add(
@Override new AjaxCheckBoxMultipleChoice<>(
public void onUpdate(AjaxRequestTarget target) { ROLE_FILTER,
target.add(dataPanel); LambdaModel.of(filterParams, ProjectService.Filter::getRoles, ProjectService.Filter::setRoles),
updateProfileWithCurrentFilter(); List.of(ProjectTeamMemberRoles.CO_SUPERVISOR),
new EnumChoiceRenderer<>(this)
) {
@Override
public void onUpdate(AjaxRequestTarget target) {
target.add(dataPanel);
updateProfileWithCurrentFilter();
}
} }
}); );
add(new AjaxCheckBoxMultipleChoice<>(STATUS_FILTER, LambdaModel.of(filterParams, ProjectService.Filter::getStatuses, ProjectService.Filter::setStatuses), asList(ProjectStatus.values()), new EnumChoiceRenderer<>(this)) { add(
@Override new AjaxCheckBoxMultipleChoice<>(
public void onUpdate(AjaxRequestTarget target) { STATUS_FILTER,
target.add(dataPanel); LambdaModel.of(
updateProfileWithCurrentFilter(); filterParams,
ProjectService.Filter::getStatuses,
ProjectService.Filter::setStatuses
),
asList(ProjectStatus.values()),
new EnumChoiceRenderer<>(this)
) {
@Override
public void onUpdate(AjaxRequestTarget target) {
target.add(dataPanel);
updateProfileWithCurrentFilter();
}
} }
}); );
add(new AjaxCheckBoxMultipleChoice<>(TYPE_FILTER, LambdaModel.of(filterParams, ProjectService.Filter::getProjectTypes, ProjectService.Filter::setProjectTypes), projectTypeService.findBySupervisorProjects(SciProSession.get().getUser()), new LambdaChoiceRenderer<>(ProjectType::getName, ProjectType::getId)) { add(
@Override new AjaxCheckBoxMultipleChoice<>(
public void onUpdate(AjaxRequestTarget target) { TYPE_FILTER,
target.add(dataPanel); LambdaModel.of(
updateProfileWithCurrentFilter(); filterParams,
ProjectService.Filter::getProjectTypes,
ProjectService.Filter::setProjectTypes
),
projectTypeService.findBySupervisorProjects(SciProSession.get().getUser()),
new LambdaChoiceRenderer<>(ProjectType::getName, ProjectType::getId)
) {
@Override
public void onUpdate(AjaxRequestTarget target) {
target.add(dataPanel);
updateProfileWithCurrentFilter();
}
} }
}); );
BootstrapRadioChoice<SupervisorProjectNoteDisplay> noteDisplay = new BootstrapRadioChoice<>( BootstrapRadioChoice<SupervisorProjectNoteDisplay> noteDisplay = new BootstrapRadioChoice<>(
"note_display", "note_display",
supervisorProjectNoteDisplayModel, supervisorProjectNoteDisplayModel,
List.of(SupervisorProjectNoteDisplay.values()), List.of(SupervisorProjectNoteDisplay.values()),
new EnumChoiceRenderer<>(this)); new EnumChoiceRenderer<>(this)
noteDisplay.add(new AjaxFormChoiceComponentUpdatingBehavior() { );
@Override noteDisplay.add(
public void onUpdate(AjaxRequestTarget target) { new AjaxFormChoiceComponentUpdatingBehavior() {
target.add(dataPanel); @Override
updateProfileWithCurrentFilter(); public void onUpdate(AjaxRequestTarget target) {
target.add(dataPanel);
updateProfileWithCurrentFilter();
}
} }
}); );
add(noteDisplay); add(noteDisplay);
} }
@ -195,6 +250,7 @@ public class SupervisorMyProjectsPanel extends Panel {
} }
private class ProjectForumStateColumn extends AbstractColumn<Project, String> { private class ProjectForumStateColumn extends AbstractColumn<Project, String> {
public ProjectForumStateColumn(IModel<String> label) { public ProjectForumStateColumn(IModel<String> label) {
super(label); super(label);
} }
@ -206,8 +262,10 @@ public class SupervisorMyProjectsPanel extends Panel {
Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this); Fragment fragment = new Fragment(id, "readStateColumnMarkupId", SupervisorMyProjectsPanel.this);
long msgCount = projectForumService.getUnreadThreadsCount(projectModel.getObject(), long msgCount = projectForumService.getUnreadThreadsCount(
SciProSession.get().getUser()); projectModel.getObject(),
SciProSession.get().getUser()
);
boolean isRead = msgCount == 0; boolean isRead = msgCount == 0;
AbstractReadStatePanel readStatePanel = new AbstractReadStatePanel("flag") { AbstractReadStatePanel readStatePanel = new AbstractReadStatePanel("flag") {
@ -218,8 +276,10 @@ public class SupervisorMyProjectsPanel extends Panel {
@Override @Override
protected void onFlagClick(AjaxRequestTarget target) { protected void onFlagClick(AjaxRequestTarget target) {
setResponsePage(SupervisorThreadedForumPage.class, setResponsePage(
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())); SupervisorThreadedForumPage.class,
SupervisorThreadedForumPage.getPageParameters(projectModel.getObject())
);
} }
}; };
@ -229,12 +289,14 @@ public class SupervisorMyProjectsPanel extends Panel {
fragment.add(readStatePanel); fragment.add(readStatePanel);
fragment.add(new NumberOfMessagesPanel("counter") { fragment.add(
@Override new NumberOfMessagesPanel("counter") {
public long getMessageCount() { @Override
return msgCount; public long getMessageCount() {
return msgCount;
}
} }
}); );
item.add(fragment); item.add(fragment);
} }