Skip to content

Add suggested jabref groups #12997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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)

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,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")),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label 'Add JabRef suggested groups' should be in sentence case, not title case, to maintain consistency with other labels.

GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")),
GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")),
GROUP_SUBGROUP_SORT(Localization.lang("Sort subgroups A-Z")),
Expand Down
16 changes: 16 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,22 @@ public boolean hasSubgroups() {
return !getChildren().isEmpty();
}

public boolean isAllEntriesGroup() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method isAllEntriesGroup() returns a boolean, which is not consistent with the special instruction to use Optional for new public methods instead of returning null. Consider using Optional.

return groupNode.getGroup() instanceof AllEntriesGroup;
}

public boolean hasSimilarSearchGroup(SearchGroup searchGroup) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method hasSimilarSearchGroup() returns a boolean, which is not consistent with the special instruction to use Optional for new public methods instead of returning null. Consider using Optional.

return getChildren().stream()
.filter(child -> child.getGroupNode().getGroup() instanceof SearchGroup)
.map(child -> (SearchGroup) child.getGroupNode().getGroup())
.anyMatch(group -> group.equals(searchGroup));
}

public boolean hasAllSuggestedGroups() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method hasAllSuggestedGroups() returns a boolean, which is not consistent with the special instruction to use Optional for new public methods instead of returning null. Consider using Optional.

return hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutFilesGroup())
&& hasSimilarSearchGroup(JabRefSuggestedGroups.createWithoutGroupsGroup());
}

public boolean canAddEntriesIn() {
AbstractGroup group = groupNode.getGroup();
if (group instanceof AllEntriesGroup) {
Expand Down
14 changes: 13 additions & 1 deletion jabgui/src/main/java/org/jabref/gui/groups/GroupTreeView.java
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,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)),
Expand Down Expand Up @@ -693,6 +701,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();
Expand Down Expand Up @@ -726,6 +736,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 ->
Expand Down
36 changes: 36 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method 'addSuggestedGroups' is public and should not return null. It should use Optional to handle cases where no groups are added.

currentDatabase.ifPresent(database -> {
GroupTreeNode rootNode = parent.getGroupNode();
List<GroupTreeNode> newSuggestedSubgroups = new ArrayList<>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use modern Java data structures. Prefer using List.of() for creating empty lists instead of 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))
.toList());
Comment on lines +237 to +240
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using Stream API to create a list, use toList() directly instead of more complex constructs like 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 <br>
* Group name change is handled separately
Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label 'Entries without linked files' should be in sentence case, not title case, to maintain consistency with other labels.

GroupHierarchyType.INDEPENDENT,
"file !=~.*",
EnumSet.noneOf(SearchFlags.class));
}

public static SearchGroup createWithoutGroupsGroup() {
return new SearchGroup(
Localization.lang("Entries without groups"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label 'Entries without groups' should be in sentence case, not title case, to maintain consistency with other labels.

GroupHierarchyType.INDEPENDENT,
"groups !=~.*",
EnumSet.noneOf(SearchFlags.class));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses assertFalse to check a boolean condition. It should assert the contents of objects using assertEquals for better clarity and precision.

}

@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());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses assertTrue to check a boolean condition. It should assert the contents of objects using assertEquals for better clarity and precision.

}

@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());
}
}
6 changes: 6 additions & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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'
Expand Down