Skip to content

Add suggested jabref groups 12659 #12975

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d4ccd57
Add "JabRef suggested groups" context menu entry
luks-santos Apr 21, 2025
2610926
Merge branch 'JabRef:main' into add-suggested-jabref-groups-12659
luks-santos Apr 21, 2025
86a6f9e
Add tests for suggested groups in GroupTreeViewModel
luks-santos Apr 22, 2025
001bde8
Refine documentation (#12972)
koppor Apr 21, 2025
62ed2b9
Enhance LTWA code (#12977)
subhramit Apr 21, 2025
6f6ec48
docs: update documentation images and fix some spelling in setting up…
Yubo-Cao Apr 21, 2025
8cd107f
Bump com.konghq:unirest-modules-gson from 4.4.5 to 4.4.6 (#12980)
dependabot[bot] Apr 21, 2025
55e0c1e
Bump src/main/resources/csl-styles from `6167631` to `28ee0a5` (#12981)
dependabot[bot] Apr 21, 2025
fa1f257
Bump org.junit.platform:junit-platform-launcher from 1.12.1 to 1.12.2…
dependabot[bot] Apr 21, 2025
8d20d9a
Bump src/main/resources/csl-locales from `606fa26` to `76a834b` (#12984)
dependabot[bot] Apr 21, 2025
9f823f4
Bump org.openrewrite.recipe:rewrite-recipe-bom from 3.5.0 to 3.6.1 (#…
dependabot[bot] Apr 21, 2025
5b7156e
Bump org.kordamp.ikonli:ikonli-javafx from 12.3.1 to 12.4.0 (#12987)
dependabot[bot] Apr 21, 2025
983dde2
Bump de.undercouch:citeproc-java from 3.2.0 to 3.2.1 (#12989)
dependabot[bot] Apr 21, 2025
3d38e44
Bump com.konghq:unirest-java-core from 4.4.5 to 4.4.6 (#12988)
dependabot[bot] Apr 21, 2025
f26fb76
Add test to PdfMergeMetadataImporterTest (#12976)
paudelritij Apr 21, 2025
f27d892
Bump org.kordamp.ikonli:ikonli-materialdesign2-pack from 12.3.1 to 12…
dependabot[bot] Apr 21, 2025
bb8300f
Use `bibEntryTypesManager` from constructor (#12983)
subhramit Apr 21, 2025
b996f90
Fix NPE when no citekey available (#12991)
Siedlerchr Apr 21, 2025
e1abc41
Merge branch 'main' of https://github.com/luks-santos/jabref into add…
luks-santos Apr 22, 2025
5e0aa94
Merge branch 'add-suggested-jabref-groups-12659' of https://github.co…
luks-santos Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
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() is a new public method and should not return null. It should return a boolean value directly.

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() is a new public method and should not return null. It should return a boolean value directly.

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() is a new public method and should not return null. It should return a boolean value directly.

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)) {
Copy link

Choose a reason for hiding this comment

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

The code does not follow the fail-fast principle. It should return early if the condition is false, instead of nesting the logic inside an if statement.

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)) {
Copy link

Choose a reason for hiding this comment

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

The code does not follow the fail-fast principle. It should return early if the condition is false, instead of nesting the logic inside an if statement.

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());
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 maintainability.

}

@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 maintainability.

}

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