Skip to content

Commit 6fa3f53

Browse files
authored
Add suggested jabref groups (#12997)
* 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 * 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 * Add entry in changelog for suggested groups feature * Refactor GroupTreeViewModel to use toList() for collecting suggested subgroups * Update CHANGELOG to include "Add JabRef suggested groups" feature
1 parent fad9026 commit 6fa3f53

File tree

8 files changed

+145
-1
lines changed

8 files changed

+145
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
1111

1212
### Added
1313

14+
- We added a new "Add JabRef suggested groups" option in the context menu of "All entries". [#12659](https://github.com/JabRef/jabref/issues/12659)
1415
- We added an option to create entries directly from Bib(La)TeX sources to the 'Create New Entry' tool. [#8808](https://github.com/JabRef/jabref/issues/8808)
1516

1617
### Changed

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

+1
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ public enum StandardActions implements Action {
203203
GROUP_EDIT(Localization.lang("Edit group")),
204204
GROUP_GENERATE_SUMMARIES(Localization.lang("Generate summaries for entries in the group")),
205205
GROUP_GENERATE_EMBEDDINGS(Localization.lang("Generate embeddings for linked files in the group")),
206+
GROUP_SUGGESTED_GROUPS_ADD(Localization.lang("Add JabRef suggested groups")),
206207
GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")),
207208
GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")),
208209
GROUP_SUBGROUP_SORT(Localization.lang("Sort subgroups A-Z")),

jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java

+16
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,22 @@ public boolean hasSubgroups() {
412412
return !getChildren().isEmpty();
413413
}
414414

415+
public boolean isAllEntriesGroup() {
416+
return groupNode.getGroup() instanceof AllEntriesGroup;
417+
}
418+
419+
public boolean hasSimilarSearchGroup(SearchGroup searchGroup) {
420+
return getChildren().stream()
421+
.filter(child -> child.getGroupNode().getGroup() instanceof SearchGroup)
422+
.map(child -> (SearchGroup) child.getGroupNode().getGroup())
423+
.anyMatch(group -> group.equals(searchGroup));
424+
}
425+
426+
public boolean hasAllSuggestedGroups() {
427+
return hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutFilesGroup())
428+
&& hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutGroupsGroup());
429+
}
430+
415431
public boolean canAddEntriesIn() {
416432
AbstractGroup group = groupNode.getGroup();
417433
if (group instanceof AllEntriesGroup) {

jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,15 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) {
599599
factory.createMenuItem(StandardActions.GROUP_GENERATE_EMBEDDINGS, new ContextAction(StandardActions.GROUP_GENERATE_EMBEDDINGS, group)),
600600
factory.createMenuItem(StandardActions.GROUP_GENERATE_SUMMARIES, new ContextAction(StandardActions.GROUP_GENERATE_SUMMARIES, group)),
601601
removeGroup,
602-
new SeparatorMenuItem(),
602+
new SeparatorMenuItem()
603+
);
604+
605+
if (group.isAllEntriesGroup()) {
606+
contextMenu.getItems().add(factory.createMenuItem(StandardActions.GROUP_SUGGESTED_GROUPS_ADD,
607+
new ContextAction(StandardActions.GROUP_SUGGESTED_GROUPS_ADD, group)));
608+
}
609+
610+
contextMenu.getItems().addAll(
603611
factory.createMenuItem(StandardActions.GROUP_SUBGROUP_ADD, new ContextAction(StandardActions.GROUP_SUBGROUP_ADD, group)),
604612
factory.createMenuItem(StandardActions.GROUP_SUBGROUP_RENAME, new ContextAction(StandardActions.GROUP_SUBGROUP_RENAME, group)),
605613
factory.createMenuItem(StandardActions.GROUP_SUBGROUP_REMOVE, new ContextAction(StandardActions.GROUP_SUBGROUP_REMOVE, group)),
@@ -693,6 +701,8 @@ public ContextAction(StandardActions command, GroupNodeViewModel group) {
693701
group.isEditable();
694702
case GROUP_REMOVE, GROUP_REMOVE_WITH_SUBGROUPS, GROUP_REMOVE_KEEP_SUBGROUPS ->
695703
group.isEditable() && group.canRemove();
704+
case GROUP_SUGGESTED_GROUPS_ADD ->
705+
!group.hasAllSuggestedGroups();
696706
case GROUP_SUBGROUP_ADD ->
697707
group.isEditable() && group.canAddGroupsIn()
698708
|| group.isRoot();
@@ -726,6 +736,8 @@ public void execute() {
726736
viewModel.generateSummaries(group);
727737
case GROUP_CHAT ->
728738
viewModel.chatWithGroup(group);
739+
case GROUP_SUGGESTED_GROUPS_ADD ->
740+
viewModel.addSuggestedGroups(group);
729741
case GROUP_SUBGROUP_ADD ->
730742
viewModel.addNewSubgroup(group, GroupDialogHeader.SUBGROUP);
731743
case GROUP_SUBGROUP_REMOVE ->

jabgui/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java

+36
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,42 @@ private boolean isGroupTypeEqual(AbstractGroup oldGroup, AbstractGroup newGroup)
209209
return oldGroup.getClass().equals(newGroup.getClass());
210210
}
211211

212+
/**
213+
* Adds JabRef suggested groups under the "All Entries" parent node.
214+
* Assumes the parent is already validated as "All Entries" by the caller.
215+
*
216+
* @param parent The "All Entries" parent node.
217+
*/
218+
public void addSuggestedGroups(GroupNodeViewModel parent) {
219+
currentDatabase.ifPresent(database -> {
220+
GroupTreeNode rootNode = parent.getGroupNode();
221+
List<GroupTreeNode> newSuggestedSubgroups = new ArrayList<>();
222+
223+
// 1. Create "Entries without linked files" group if it doesn't exist
224+
SearchGroup withoutFilesGroup = JabRefSuggestedGroups.createWithoutFilesGroup();
225+
if (!parent.hasSimilarSearchGroup(withoutFilesGroup)) {
226+
GroupTreeNode subGroup = rootNode.addSubgroup(withoutFilesGroup);
227+
newSuggestedSubgroups.add(subGroup);
228+
}
229+
230+
// 2. Create "Entries without groups" group if it doesn't exist
231+
SearchGroup withoutGroupsGroup = JabRefSuggestedGroups.createWithoutGroupsGroup();
232+
if (!parent.hasSimilarSearchGroup(withoutGroupsGroup)) {
233+
GroupTreeNode subGroup = rootNode.addSubgroup(withoutGroupsGroup);
234+
newSuggestedSubgroups.add(subGroup);
235+
}
236+
237+
selectedGroups.setAll(newSuggestedSubgroups
238+
.stream()
239+
.map(newSubGroup -> new GroupNodeViewModel(database, stateManager, taskExecutor, newSubGroup, localDragboard, preferences))
240+
.toList());
241+
242+
writeGroupChangesToMetaData();
243+
244+
dialogService.notify(Localization.lang("Created %0 suggested groups.", String.valueOf(newSuggestedSubgroups.size())));
245+
});
246+
}
247+
212248
/**
213249
* Check if it is necessary to show a group modified, reassign entry dialog <br>
214250
* Group name change is handled separately
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.jabref.gui.groups;
2+
3+
import java.util.EnumSet;
4+
5+
import org.jabref.logic.l10n.Localization;
6+
import org.jabref.model.groups.GroupHierarchyType;
7+
import org.jabref.model.groups.SearchGroup;
8+
import org.jabref.model.search.SearchFlags;
9+
10+
public class JabRefSuggestedGroups {
11+
12+
public static SearchGroup createWithoutFilesGroup() {
13+
return new SearchGroup(
14+
Localization.lang("Entries without linked files"),
15+
GroupHierarchyType.INDEPENDENT,
16+
"file !=~.*",
17+
EnumSet.noneOf(SearchFlags.class));
18+
}
19+
20+
public static SearchGroup createWithoutGroupsGroup() {
21+
return new SearchGroup(
22+
Localization.lang("Entries without groups"),
23+
GroupHierarchyType.INDEPENDENT,
24+
"groups !=~.*",
25+
EnumSet.noneOf(SearchFlags.class));
26+
}
27+
}

jabgui/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java

+45
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,49 @@ void shouldShowDialogWhenCaseSensitivyDiffers() {
139139
GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard());
140140
assertFalse(model.onlyMinorChanges(oldGroup, newGroup));
141141
}
142+
143+
@Test
144+
void rootNodeShouldNotHaveSuggestedGroupsByDefault() {
145+
GroupNodeViewModel rootGroup = groupTree.rootGroupProperty().getValue();
146+
assertFalse(rootGroup.hasAllSuggestedGroups());
147+
}
148+
149+
@Test
150+
void shouldAddsAllSuggestedGroupsWhenNoneExist() {
151+
GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard());
152+
GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue();
153+
assertFalse(rootGroup.hasAllSuggestedGroups());
154+
155+
model.addSuggestedGroups(rootGroup);
156+
157+
assertEquals(2, rootGroup.getChildren().size());
158+
assertTrue(rootGroup.hasAllSuggestedGroups());
159+
}
160+
161+
@Test
162+
void shouldAddOnlyMissingGroup() {
163+
GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard());
164+
GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue();
165+
rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutFilesGroup());
166+
assertEquals(1, rootGroup.getChildren().size());
167+
168+
model.addSuggestedGroups(rootGroup);
169+
170+
assertEquals(2, rootGroup.getChildren().size());
171+
assertTrue(rootGroup.hasAllSuggestedGroups());
172+
}
173+
174+
@Test
175+
void shouldNotAddSuggestedGroupsWhenAllExist() {
176+
GroupTreeViewModel model = new GroupTreeViewModel(stateManager, dialogService, mock(AiService.class), preferences, taskExecutor, new CustomLocalDragboard());
177+
GroupNodeViewModel rootGroup = model.rootGroupProperty().getValue();
178+
rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutFilesGroup());
179+
rootGroup.getGroupNode().addSubgroup(JabRefSuggestedGroups.createWithoutGroupsGroup());
180+
assertEquals(2, rootGroup.getChildren().size());
181+
182+
model.addSuggestedGroups(rootGroup);
183+
184+
assertEquals(2, rootGroup.getChildren().size());
185+
assertTrue(rootGroup.hasAllSuggestedGroups());
186+
}
142187
}

jablib/src/main/resources/l10n/JabRef_en.properties

+6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ Add\ selected\ entries\ to\ this\ group=Add selected entries to this group
4646
Add\ subgroup=Add subgroup
4747
Rename\ subgroup=Rename subgroup
4848

49+
Add\ JabRef\ suggested\ groups=Add JabRef suggested groups
50+
Created\ %0\ suggested\ groups.=Created %0 suggested groups.
51+
52+
Entries\ without\ groups=Entries without groups
53+
Entries\ without\ linked\ files=Entries without linked files
54+
4955
Added\ group\ "%0".=Added group "%0".
5056

5157
Added\ string\:\ '%0'=Added string: '%0'

0 commit comments

Comments
 (0)