Skip to content
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 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)
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/jabref/gui/actions/StandardActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down
16 changes: 16 additions & 0 deletions 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() {
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) {
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/org/jabref/gui/groups/GroupTreeView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 ->
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<GroupTreeNode> 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 <br>
* Group name change is handled separately
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/org/jabref/gui/groups/JabRefSuggestedGroups.java
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"),
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));
}
}
6 changes: 6 additions & 0 deletions src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
45 changes: 45 additions & 0 deletions src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java
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());
}

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