From 9cd4b2b934f89ff09bf16573f468a53c1b5974ef Mon Sep 17 00:00:00 2001 From: luks-santos <57961643+luks-santos@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:47:37 -0300 Subject: [PATCH 1/5] Add "JabRef suggested groups" context menu entry Implements a new context menu entry for the "All entries" group to add two predefined groups if they don't already exist: - "Entries without linked files" - A search group that finds entries with no file links - "Entries without groups" - A search group that finds entries not assigned to any group The menu item is disabled automatically when both suggested groups already exist in the library. The implementation includes: - A utility class with factory methods for creating the suggested groups - Logic to check for existence of similar groups before adding Fixes #12659 --- .../jabref/gui/actions/StandardActions.java | 1 + .../jabref/gui/groups/GroupNodeViewModel.java | 16 +++++++++ .../org/jabref/gui/groups/GroupTreeView.java | 14 +++++++- .../jabref/gui/groups/GroupTreeViewModel.java | 36 +++++++++++++++++++ .../gui/groups/JabRefSuggestedGroups.java | 27 ++++++++++++++ src/main/resources/l10n/JabRef_en.properties | 6 ++++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/jabref/gui/groups/JabRefSuggestedGroups.java diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index cdaf38a91cf..4e353207f87 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -201,6 +201,7 @@ public enum StandardActions implements Action { GROUP_EDIT(Localization.lang("Edit group")), GROUP_GENERATE_SUMMARIES(Localization.lang("Generate summaries for entries in the group")), GROUP_GENERATE_EMBEDDINGS(Localization.lang("Generate embeddings for linked files in the group")), + GROUP_SUGGESTED_GROUPS_ADD(Localization.lang("Add JabRef suggested groups")), GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")), GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")), GROUP_SUBGROUP_SORT(Localization.lang("Sort subgroups A-Z")), diff --git a/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java index 718fc45229d..8fed92ce48f 100644 --- a/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java @@ -412,6 +412,22 @@ public boolean hasSubgroups() { return !getChildren().isEmpty(); } + public boolean isAllEntriesGroup() { + return groupNode.getGroup() instanceof AllEntriesGroup; + } + + public boolean hasSimilarSearchGroup(SearchGroup searchGroup) { + return getChildren().stream() + .filter(child -> child.getGroupNode().getGroup() instanceof SearchGroup) + .map(child -> (SearchGroup) child.getGroupNode().getGroup()) + .anyMatch(group -> group.equals(searchGroup)); + } + + public boolean hasAllSuggestedGroups() { + return hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutFilesGroup()) + && hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutGroupsGroup()); + } + public boolean canAddEntriesIn() { AbstractGroup group = groupNode.getGroup(); if (group instanceof AllEntriesGroup) { diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeView.java b/src/main/java/org/jabref/gui/groups/GroupTreeView.java index d28b037b530..1ebe0ca2ac7 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeView.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeView.java @@ -600,7 +600,15 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) { factory.createMenuItem(StandardActions.GROUP_GENERATE_EMBEDDINGS, new ContextAction(StandardActions.GROUP_GENERATE_EMBEDDINGS, group)), factory.createMenuItem(StandardActions.GROUP_GENERATE_SUMMARIES, new ContextAction(StandardActions.GROUP_GENERATE_SUMMARIES, group)), removeGroup, - new SeparatorMenuItem(), + new SeparatorMenuItem() + ); + + if (group.isAllEntriesGroup()) { + contextMenu.getItems().add(factory.createMenuItem(StandardActions.GROUP_SUGGESTED_GROUPS_ADD, + new ContextAction(StandardActions.GROUP_SUGGESTED_GROUPS_ADD, group))); + } + + contextMenu.getItems().addAll( factory.createMenuItem(StandardActions.GROUP_SUBGROUP_ADD, new ContextAction(StandardActions.GROUP_SUBGROUP_ADD, group)), factory.createMenuItem(StandardActions.GROUP_SUBGROUP_RENAME, new ContextAction(StandardActions.GROUP_SUBGROUP_RENAME, group)), factory.createMenuItem(StandardActions.GROUP_SUBGROUP_REMOVE, new ContextAction(StandardActions.GROUP_SUBGROUP_REMOVE, group)), @@ -694,6 +702,8 @@ public ContextAction(StandardActions command, GroupNodeViewModel group) { group.isEditable(); case GROUP_REMOVE, GROUP_REMOVE_WITH_SUBGROUPS, GROUP_REMOVE_KEEP_SUBGROUPS -> group.isEditable() && group.canRemove(); + case GROUP_SUGGESTED_GROUPS_ADD -> + !group.hasAllSuggestedGroups(); case GROUP_SUBGROUP_ADD -> group.isEditable() && group.canAddGroupsIn() || group.isRoot(); @@ -727,6 +737,8 @@ public void execute() { viewModel.generateSummaries(group); case GROUP_CHAT -> viewModel.chatWithGroup(group); + case GROUP_SUGGESTED_GROUPS_ADD -> + viewModel.addSuggestedGroups(group); case GROUP_SUBGROUP_ADD -> viewModel.addNewSubgroup(group, GroupDialogHeader.SUBGROUP); case GROUP_SUBGROUP_REMOVE -> diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java index 48def6325c8..a3923d6959c 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java @@ -210,6 +210,42 @@ private boolean isGroupTypeEqual(AbstractGroup oldGroup, AbstractGroup newGroup) return oldGroup.getClass().equals(newGroup.getClass()); } + /** + * Adds JabRef suggested groups under the "All Entries" parent node. + * Assumes the parent is already validated as "All Entries" by the caller. + * + * @param parent The "All Entries" parent node. + */ + public void addSuggestedGroups(GroupNodeViewModel parent) { + currentDatabase.ifPresent(database -> { + GroupTreeNode rootNode = parent.getGroupNode(); + List newSuggestedSubgroups = new ArrayList<>(); + + // 1. Create "Entries without linked files" group if it doesn't exist + SearchGroup withoutFilesGroup = JabRefSuggestedGroups.createWithoutFilesGroup(); + if (!parent.hasSimilarSearchGroup(withoutFilesGroup)) { + GroupTreeNode subGroup = rootNode.addSubgroup(withoutFilesGroup); + newSuggestedSubgroups.add(subGroup); + } + + // 2. Create "Entries without groups" group if it doesn't exist + SearchGroup withoutGroupsGroup = JabRefSuggestedGroups.createWithoutGroupsGroup(); + if (!parent.hasSimilarSearchGroup(withoutGroupsGroup)) { + GroupTreeNode subGroup = rootNode.addSubgroup(withoutGroupsGroup); + newSuggestedSubgroups.add(subGroup); + } + + selectedGroups.setAll(newSuggestedSubgroups + .stream() + .map(newSubGroup -> new GroupNodeViewModel(database, stateManager, taskExecutor, newSubGroup, localDragboard, preferences)) + .collect(Collectors.toList())); + + writeGroupChangesToMetaData(); + + dialogService.notify(Localization.lang("Created %0 suggested groups.", String.valueOf(newSuggestedSubgroups.size()))); + }); + } + /** * Check if it is necessary to show a group modified, reassign entry dialog
* Group name change is handled separately diff --git a/src/main/java/org/jabref/gui/groups/JabRefSuggestedGroups.java b/src/main/java/org/jabref/gui/groups/JabRefSuggestedGroups.java new file mode 100644 index 00000000000..644452de5db --- /dev/null +++ b/src/main/java/org/jabref/gui/groups/JabRefSuggestedGroups.java @@ -0,0 +1,27 @@ +package org.jabref.gui.groups; + +import java.util.EnumSet; + +import org.jabref.logic.l10n.Localization; +import org.jabref.model.groups.GroupHierarchyType; +import org.jabref.model.groups.SearchGroup; +import org.jabref.model.search.SearchFlags; + +public class JabRefSuggestedGroups { + + public static SearchGroup createWithoutFilesGroup() { + return new SearchGroup( + Localization.lang("Entries without linked files"), + GroupHierarchyType.INDEPENDENT, + "file !=~.*", + EnumSet.noneOf(SearchFlags.class)); + } + + public static SearchGroup createWithoutGroupsGroup() { + return new SearchGroup( + Localization.lang("Entries without groups"), + GroupHierarchyType.INDEPENDENT, + "groups !=~.*", + EnumSet.noneOf(SearchFlags.class)); + } +} diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 0b4b2bc6b97..1852a258ff1 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -48,6 +48,12 @@ Add\ selected\ entries\ to\ this\ group=Add selected entries to this group Add\ subgroup=Add subgroup Rename\ subgroup=Rename subgroup +Add\ JabRef\ suggested\ groups=Add JabRef suggested groups +Created\ %0\ suggested\ groups.=Created %0 suggested groups. + +Entries\ without\ groups=Entries without groups +Entries\ without\ linked\ files=Entries without linked files + Added\ group\ "%0".=Added group "%0". Added\ string\:\ '%0'=Added string: '%0' From cb3f11678d2293975670e2fe0d31ab237f1be315 Mon Sep 17 00:00:00 2001 From: luks-santos <57961643+luks-santos@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:48:48 -0300 Subject: [PATCH 2/5] Add unit tests for handling suggested groups in the GroupTreeViewModel: - Test that root node has no suggested groups by default - Test addition of all suggested groups when none exist - Test addition of only missing suggested groups - Test that no groups are added when all suggested groups already exist --- .../gui/groups/GroupTreeViewModelTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java index fd3b781a4b6..95810540759 100644 --- a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java +++ b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java @@ -139,4 +139,49 @@ void shouldShowDialogWhenCaseSensitivyDiffers() { GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard()); assertFalse(model.onlyMinorChanges(oldGroup, newGroup)); } + + @Test + void rootNodeShouldNotHaveSuggestedGroupsByDefault() { + GroupNodeViewModel rootGroup = groupTree.rootGroupProperty().getValue(); + assertFalse(rootGroup.hasAllSuggestedGroups()); + } + + @Test + void shouldAddsAllSuggestedGroupsWhenNoneExist() { + GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard()); + GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue(); + assertFalse(rootGroup.hasAllSuggestedGroups()); + + model.addSuggestedGroups(rootGroup); + + assertEquals(2, rootGroup.getChildren().size()); + assertTrue(rootGroup.hasAllSuggestedGroups()); + } + + @Test + void shouldAddOnlyMissingGroup() { + GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard()); + GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue(); + rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutFilesGroup()); + assertEquals(1, rootGroup.getChildren().size()); + + model.addSuggestedGroups(rootGroup); + + assertEquals(2, rootGroup.getChildren().size()); + assertTrue(rootGroup.hasAllSuggestedGroups()); + } + + @Test + void shouldNotAddSuggestedGroupsWhenAllExist() { + GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard()); + GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue(); + rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutFilesGroup()); + rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutGroupsGroup()); + assertEquals(2, rootGroup.getChildren().size()); + + model.addSuggestedGroups(rootGroup); + + assertEquals(2, rootGroup.getChildren().size()); + assertTrue(rootGroup.hasAllSuggestedGroups()); + } } From 902c1c26e2e8f56ff363489b1a699e436ed04806 Mon Sep 17 00:00:00 2001 From: luks-santos <57961643+luks-santos@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:52:48 -0300 Subject: [PATCH 3/5] Add entry in changelog for suggested groups feature --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a7fa5ac1d..8ef2f36dba9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added a new "Add JabRef suggested groups" option in the context menu of "All entries". [#12659](https://github.com/JabRef/jabref/issues/12659) - We added a button in Privacy notice and Mr. DLib Privacy settings notice for hiding related tabs. [#11707](https://github.com/JabRef/jabref/issues/11707) - We added buttons "Add example entry" and "Import existing PDFs" when a library is empty, making it easier for new users to get started. [#12662](https://github.com/JabRef/jabref/issues/12662) - In the Open/LibreOffice integration, we added the provision to modify the bibliography title and its format for CSL styles, in the "Select style" dialog. [#12663](https://github.com/JabRef/jabref/issues/12663) From eef666c4e82a937ba51827461290a655742abcc9 Mon Sep 17 00:00:00 2001 From: luks-santos <57961643+luks-santos@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:22:16 -0300 Subject: [PATCH 4/5] Refactor GroupTreeViewModel to use toList() for collecting suggested subgroups --- src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java index a3923d6959c..5909cd162d7 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java @@ -238,7 +238,7 @@ public void addSuggestedGroups(GroupNodeViewModel parent) { selectedGroups.setAll(newSuggestedSubgroups .stream() .map(newSubGroup -> new GroupNodeViewModel(database, stateManager, taskExecutor, newSubGroup, localDragboard, preferences)) - .collect(Collectors.toList())); + .toList()); writeGroupChangesToMetaData(); From 9af8581fa428db3317bf5c238145b6afa310719a Mon Sep 17 00:00:00 2001 From: luks-santos <57961643+luks-santos@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:20:06 -0300 Subject: [PATCH 5/5] Update CHANGELOG to include "Add JabRef suggested groups" feature --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5ad22fe8e..5cda06cf51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added - We added a new "Add JabRef suggested groups" option in the context menu of "All entries". [#12659](https://github.com/JabRef/jabref/issues/12659) + ### Changed ### Fixed