From 51c81ee084c05f7c87a08439de149ac4071bb27c Mon Sep 17 00:00:00 2001 From: YaxPrajapti Date: Sat, 7 Jun 2025 22:41:26 +0530 Subject: [PATCH 1/4] Refactor searchForRelations function with hyperlink --- .../CitationRelationsTab.java | 265 +++++++++--------- 1 file changed, 129 insertions(+), 136 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index 3cffd4e906f..f12805fa91f 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -20,6 +20,7 @@ import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.DialogPane; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; @@ -30,6 +31,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import org.jabref.gui.DialogService; import org.jabref.gui.LibraryTab; @@ -51,6 +54,9 @@ import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.exporter.BibWriter; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; +import org.jabref.logic.importer.fetcher.CrossRef; import org.jabref.logic.importer.fetcher.citation.CitationFetcher; import org.jabref.logic.importer.fetcher.citation.semanticscholar.SemanticScholarFetcher; import org.jabref.logic.l10n.Localization; @@ -97,13 +103,7 @@ public class CitationRelationsTab extends EntryEditorTab { private final StateManager stateManager; private final UndoManager undoManager; - public CitationRelationsTab(DialogService dialogService, - UndoManager undoManager, - StateManager stateManager, - FileUpdateMonitor fileUpdateMonitor, - GuiPreferences preferences, - TaskExecutor taskExecutor, - BibEntryTypesManager bibEntryTypesManager) { + public CitationRelationsTab(DialogService dialogService, UndoManager undoManager, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, GuiPreferences preferences, TaskExecutor taskExecutor, BibEntryTypesManager bibEntryTypesManager) { this.dialogService = dialogService; this.preferences = preferences; this.taskExecutor = taskExecutor; @@ -115,8 +115,7 @@ public CitationRelationsTab(DialogService dialogService, this.entryTypesManager = bibEntryTypesManager; this.duplicateCheck = new DuplicateCheck(entryTypesManager); - this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()), - new BibEntryRelationsCache()); + this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()), new BibEntryRelationsCache()); citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(preferences, undoManager, stateManager, dialogService, fileUpdateMonitor, taskExecutor); } @@ -191,29 +190,18 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { citingVBox.getChildren().addAll(citingHBox, citingListView); citedByVBox.getChildren().addAll(citedByHBox, citedByListView); - refreshCitingButton.setOnMouseClicked(event -> searchForRelations( - entry, - citingListView, - abortCitingButton, - refreshCitingButton, - CitationFetcher.SearchType.CITES, - importCitingButton, - citingProgress, - true)); + refreshCitingButton.setOnMouseClicked(event -> searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true)); - refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, - refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); + refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); // Create SplitPane to hold all nodes above SplitPane container = new SplitPane(citingVBox, citedByVBox); styleFetchedListView(citedByListView); styleFetchedListView(citingListView); - searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, - CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); + searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); - searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, - CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); + searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); return container; } @@ -225,86 +213,82 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { */ private void styleFetchedListView(CheckListView listView) { PseudoClass entrySelected = PseudoClass.getPseudoClass("selected"); - new ViewModelListCellFactory() - .withGraphic(entry -> { - - HBox separator = new HBox(); - HBox.setHgrow(separator, Priority.SOMETIMES); - Node entryNode = BibEntryView.getEntryNode(entry.entry()); - HBox.setHgrow(entryNode, Priority.ALWAYS); - HBox hContainer = new HBox(); - hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); - - VBox vContainer = new VBox(); - - if (entry.isLocal()) { - hContainer.getStyleClass().add("duplicate-entry"); - Button jumpTo = IconTheme.JabRefIcons.LINK.asButton(); - jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library"))); - jumpTo.getStyleClass().add("addEntryButton"); - jumpTo.setOnMouseClicked(event -> jumpToEntry(entry)); - hContainer.setOnMouseClicked(event -> { - if (event.getClickCount() == 2) { - jumpToEntry(entry); - } - }); - vContainer.getChildren().add(jumpTo); - - Button compareButton = IconTheme.JabRefIcons.MERGE_ENTRIES.asButton(); - compareButton.setTooltip(new Tooltip(Localization.lang("Compare with existing entry"))); - compareButton.setOnMouseClicked(event -> openPossibleDuplicateEntriesWindow(entry, listView)); - vContainer.getChildren().add(compareButton); + new ViewModelListCellFactory().withGraphic(entry -> { + + HBox separator = new HBox(); + HBox.setHgrow(separator, Priority.SOMETIMES); + Node entryNode = BibEntryView.getEntryNode(entry.entry()); + HBox.setHgrow(entryNode, Priority.ALWAYS); + HBox hContainer = new HBox(); + hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); + + VBox vContainer = new VBox(); + + if (entry.isLocal()) { + hContainer.getStyleClass().add("duplicate-entry"); + Button jumpTo = IconTheme.JabRefIcons.LINK.asButton(); + jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library"))); + jumpTo.getStyleClass().add("addEntryButton"); + jumpTo.setOnMouseClicked(event -> jumpToEntry(entry)); + hContainer.setOnMouseClicked(event -> { + if (event.getClickCount() == 2) { + jumpToEntry(entry); + } + }); + vContainer.getChildren().add(jumpTo); + + Button compareButton = IconTheme.JabRefIcons.MERGE_ENTRIES.asButton(); + compareButton.setTooltip(new Tooltip(Localization.lang("Compare with existing entry"))); + compareButton.setOnMouseClicked(event -> openPossibleDuplicateEntriesWindow(entry, listView)); + vContainer.getChildren().add(compareButton); + } else { + ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton(); + addToggle.setTooltip(new Tooltip(Localization.lang("Select entry"))); + EasyBind.subscribe(addToggle.selectedProperty(), selected -> { + if (selected) { + addToggle.setGraphic(IconTheme.JabRefIcons.ADD_FILLED.withColor(IconTheme.SELECTED_COLOR).getGraphicNode()); } else { - ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton(); - addToggle.setTooltip(new Tooltip(Localization.lang("Select entry"))); - EasyBind.subscribe(addToggle.selectedProperty(), selected -> { - if (selected) { - addToggle.setGraphic(IconTheme.JabRefIcons.ADD_FILLED.withColor(IconTheme.SELECTED_COLOR).getGraphicNode()); - } else { - addToggle.setGraphic(IconTheme.JabRefIcons.ADD.getGraphicNode()); - } - }); - addToggle.getStyleClass().add("addEntryButton"); - addToggle.selectedProperty().bindBidirectional(listView.getItemBooleanProperty(entry)); - vContainer.getChildren().add(addToggle); + addToggle.setGraphic(IconTheme.JabRefIcons.ADD.getGraphicNode()); } + }); + addToggle.getStyleClass().add("addEntryButton"); + addToggle.selectedProperty().bindBidirectional(listView.getItemBooleanProperty(entry)); + vContainer.getChildren().add(addToggle); + } - if (entry.entry().getDOI().isPresent() || entry.entry().getField(StandardField.URL).isPresent()) { - Button openWeb = IconTheme.JabRefIcons.OPEN_LINK.asButton(); - openWeb.setTooltip(new Tooltip(Localization.lang("Open URL or DOI"))); - openWeb.setOnMouseClicked(event -> { - String url = entry.entry().getDOI().flatMap(DOI::getExternalURI).map(URI::toString) - .or(() -> entry.entry().getField(StandardField.URL)).orElse(""); - if (StringUtil.isNullOrEmpty(url)) { - return; - } - try { - NativeDesktop.openBrowser(url, preferences.getExternalApplicationsPreferences()); - } catch (IOException ex) { - dialogService.notify(Localization.lang("Unable to open link.")); - } - }); - vContainer.getChildren().addLast(openWeb); + if (entry.entry().getDOI().isPresent() || entry.entry().getField(StandardField.URL).isPresent()) { + Button openWeb = IconTheme.JabRefIcons.OPEN_LINK.asButton(); + openWeb.setTooltip(new Tooltip(Localization.lang("Open URL or DOI"))); + openWeb.setOnMouseClicked(event -> { + String url = entry.entry().getDOI().flatMap(DOI::getExternalURI).map(URI::toString).or(() -> entry.entry().getField(StandardField.URL)).orElse(""); + if (StringUtil.isNullOrEmpty(url)) { + return; } + try { + NativeDesktop.openBrowser(url, preferences.getExternalApplicationsPreferences()); + } catch ( + IOException ex) { + dialogService.notify(Localization.lang("Unable to open link.")); + } + }); + vContainer.getChildren().addLast(openWeb); + } - Button showEntrySource = IconTheme.JabRefIcons.SOURCE.asButton(); - showEntrySource.setTooltip(new Tooltip(Localization.lang("%0 source", "BibTeX"))); - showEntrySource.setOnMouseClicked(event -> showEntrySourceDialog(entry.entry())); + Button showEntrySource = IconTheme.JabRefIcons.SOURCE.asButton(); + showEntrySource.setTooltip(new Tooltip(Localization.lang("%0 source", "BibTeX"))); + showEntrySource.setOnMouseClicked(event -> showEntrySourceDialog(entry.entry())); - vContainer.getChildren().addLast(showEntrySource); + vContainer.getChildren().addLast(showEntrySource); - hContainer.getChildren().addAll(entryNode, separator, vContainer); - hContainer.getStyleClass().add("entry-container"); + hContainer.getChildren().addAll(entryNode, separator, vContainer); + hContainer.getStyleClass().add("entry-container"); - return hContainer; - }) - .withOnMouseClickedEvent((ee, event) -> { - if (!ee.isLocal()) { - listView.getCheckModel().toggleCheckState(ee); - } - }) - .withPseudoClass(entrySelected, listView::getItemBooleanProperty) - .install(listView); + return hContainer; + }).withOnMouseClickedEvent((ee, event) -> { + if (!ee.isLocal()) { + listView.getCheckModel().toggleCheckState(ee); + } + }).withPseudoClass(entrySelected, listView::getItemBooleanProperty).install(listView); listView.setSelectionModel(new NoSelectionModel<>()); } @@ -329,10 +313,10 @@ private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPrefer private void showEntrySourceDialog(BibEntry entry) { CodeArea ca = new CodeArea(); try { - BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode) - .orElse(BibDatabaseMode.BIBLATEX); + BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode).orElse(BibDatabaseMode.BIBLATEX); ca.appendText(getSourceString(entry, mode, preferences.getFieldPreferences(), this.entryTypesManager)); - } catch (IOException e) { + } catch ( + IOException e) { LOGGER.warn("Incorrect entry, could not load source:", e); return; } @@ -405,15 +389,30 @@ protected void bindToEntry(BibEntry entry) { * @param refreshButton refresh Button to use * @param searchType type of search (CITING / CITEDBY) */ - private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton, - Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress, boolean shouldRefresh) { + private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton, Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, ProgressIndicator progress, boolean shouldRefresh) { if (entry.getDOI().isEmpty()) { hideNodes(abortButton, progress); showNodes(refreshButton); listView.getItems().clear(); - listView.setPlaceholder( - new Label(Localization.lang("The selected entry doesn't have a DOI linked to it. Lookup a DOI and try again."))); + + Text doiLookUpText = new Text(Localization.lang("The selected entry doesn't have a DOI linked to it. ")); + Hyperlink doiLookUpHyperLink = new Hyperlink(Localization.lang("Lookup a DOI and try again.")); + TextFlow doiLookUpTextFlow = new TextFlow(doiLookUpText, doiLookUpHyperLink); + Label placeHolder = new Label("", doiLookUpTextFlow); + doiLookUpHyperLink.setOnAction(e -> { + CrossRef doiFetcher = new CrossRef(); + BackgroundTask.wrap(() -> doiFetcher.findIdentifier(entry)) + .onRunning(() -> listView.setPlaceholder(new Label("Looking up DOI..."))) + .onFinished(() -> listView.setPlaceholder(placeHolder)) + .onSuccess(identifier -> { + if (identifier.isPresent()) { + System.out.println(identifier.get()); + } else { + dialogService.notify("No DOI found"); + } + }).onFailure((exception) -> handleIdentifierFetchingError(exception, doiFetcher)).executeWith(taskExecutor); + }); + listView.setPlaceholder(placeHolder); return; } @@ -447,39 +446,34 @@ private void searchForRelations(BibEntry entry, CheckListView prepareToSearchForRelations(abortButton, refreshButton, importButton, progress, task)) - .onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, searchType, importButton, progress, fetchedList, observableList)) - .onFailure(exception -> { - LOGGER.error("Error while fetching citing Articles", exception); - hideNodes(abortButton, progress, importButton); - listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", - exception.getMessage()))); - - refreshButton.setVisible(true); - dialogService.notify(exception.getMessage()); - }) - .executeWith(taskExecutor); + task.onRunning(() -> prepareToSearchForRelations(abortButton, refreshButton, importButton, progress, task)).onSuccess(fetchedList -> onSearchForRelationsSucceed(entry, listView, abortButton, refreshButton, searchType, importButton, progress, fetchedList, observableList)).onFailure(exception -> { + LOGGER.error("Error while fetching citing Articles", exception); + hideNodes(abortButton, progress, importButton); + listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", exception.getMessage()))); + + refreshButton.setVisible(true); + dialogService.notify(exception.getMessage()); + }).executeWith(taskExecutor); + } + + private void handleIdentifierFetchingError(Exception exception, CrossRef fetcher){ + LOGGER.error("Error while fetching identifier", exception); + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Server not available")); + } else if (exception.getCause() != null) { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage())); + } else { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage())); + } } - private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, - Button abortButton, Button refreshButton, - CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress, List fetchedList, - ObservableList observableList) { + private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, Button abortButton, Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, ProgressIndicator progress, List fetchedList, ObservableList observableList) { hideNodes(abortButton, progress); - BibDatabase database = stateManager.getActiveDatabase().map(BibDatabaseContext::getDatabase) - .orElse(new BibDatabase()); - observableList.setAll( - fetchedList.stream().map(entr -> - duplicateCheck.containsDuplicate( - database, - entr, - BibDatabaseModeDetection.inferMode(database)) - .map(localEntry -> new CitationRelationItem(entr, localEntry, true)) - .orElseGet(() -> new CitationRelationItem(entr, false))) - .toList() - ); + BibDatabase database = stateManager.getActiveDatabase().map(BibDatabaseContext::getDatabase).orElse(new BibDatabase()); + observableList.setAll(fetchedList.stream().map(entr -> duplicateCheck.containsDuplicate(database, entr, BibDatabaseModeDetection.inferMode(database)).map(localEntry -> new CitationRelationItem(entr, localEntry, true)).orElseGet(() -> new CitationRelationItem(entr, false))).toList()); if (!observableList.isEmpty()) { listView.refresh(); @@ -493,8 +487,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView> task) { + private void prepareToSearchForRelations(Button abortButton, Button refreshButton, Button importButton, ProgressIndicator progress, BackgroundTask> task) { showNodes(abortButton, progress); hideNodes(refreshButton, importButton); @@ -532,7 +525,7 @@ private void importEntries(List entriesToImport, CitationF * Function to open possible duplicate entries window to compare duplicate entries * * @param citationRelationItem duplicate in the citation relations tab - * @param listView CheckListView to display citations + * @param listView CheckListView to display citations */ private void openPossibleDuplicateEntriesWindow(CitationRelationItem citationRelationItem, CheckListView listView) { BibEntry libraryEntry = citationRelationItem.localEntry(); From 12db8513938a58f8c6fd2cb2a3e805fef380238f Mon Sep 17 00:00:00 2001 From: YaxPrajapti Date: Sun, 8 Jun 2025 11:17:22 +0530 Subject: [PATCH 2/4] Refactored searchForRelations method to look up for doiIdentifier and display the citations --- .../citationrelationtab/CitationRelationsTab.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index f12805fa91f..5549e5347c9 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -403,10 +403,12 @@ private void searchForRelations(BibEntry entry, CheckListView doiFetcher.findIdentifier(entry)) .onRunning(() -> listView.setPlaceholder(new Label("Looking up DOI..."))) - .onFinished(() -> listView.setPlaceholder(placeHolder)) - .onSuccess(identifier -> { - if (identifier.isPresent()) { - System.out.println(identifier.get()); + .onSuccess(doiIdentifier -> { + if (doiIdentifier.isPresent()) { + System.out.println(doiIdentifier.get()); + entry.setField(StandardField.DOI, doiIdentifier.get().asString()); + searchForRelations(entry, listView, abortButton, refreshButton, searchType, importButton, progress, shouldRefresh); +// searchForRelations(entry, listView, abortButton, refreshButton, CitationFetcher.SearchType.CITED_BY, importButton, progress, shouldRefresh); } else { dialogService.notify("No DOI found"); } From 374ff6a08cbe65fad99ad9826bf1fec19ef49505 Mon Sep 17 00:00:00 2001 From: YaxPrajapti Date: Mon, 9 Jun 2025 16:41:19 +0530 Subject: [PATCH 3/4] Removed redundant function calls --- .../entryeditor/citationrelationtab/CitationRelationsTab.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index 5549e5347c9..26697e46ab1 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -405,10 +405,8 @@ private void searchForRelations(BibEntry entry, CheckListView listView.setPlaceholder(new Label("Looking up DOI..."))) .onSuccess(doiIdentifier -> { if (doiIdentifier.isPresent()) { - System.out.println(doiIdentifier.get()); entry.setField(StandardField.DOI, doiIdentifier.get().asString()); searchForRelations(entry, listView, abortButton, refreshButton, searchType, importButton, progress, shouldRefresh); -// searchForRelations(entry, listView, abortButton, refreshButton, CitationFetcher.SearchType.CITED_BY, importButton, progress, shouldRefresh); } else { dialogService.notify("No DOI found"); } From c58cea52cbf46a94af6e71ad8f8a4be49bbecd3c Mon Sep 17 00:00:00 2001 From: YaxPrajapti Date: Mon, 9 Jun 2025 17:00:33 +0530 Subject: [PATCH 4/4] Added change in changelog.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5027640ec86..ffc9eedfcf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We improved JabRef's internal document viewer. It now allows text section, searching and highlighting of search terms and page rotation [#13193](https://github.com/JabRef/jabref/pull/13193). - When importing a PDF, there is no empty entry column shown in the multi merge dialog. [#13132](https://github.com/JabRef/jabref/issues/13132) - We added a progress dialog to the "Check consistency" action and progress output to the corresponding cli command. [#12487](https://github.com/JabRef/jabref/issues/12487) +- We added Enhanced "Citation Relations" feature: "Look up a DOI and try again." is now a clickable hyperlink that triggers a DOI lookup. The link shows result states ("Looking up DOI...", "No DOI found", "List of citations") as appropriate. ### Fixed @@ -1568,6 +1569,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added the ability to use negation in export filter layouts. [#5138](https://github.com/JabRef/jabref/pull/5138) - Focus on Name Area instead of 'OK' button whenever user presses 'Add subgroup'. [#6307](https://github.com/JabRef/jabref/issues/6307) - We changed the behavior of merging that the entry which has "smaller" bibkey will be selected. [#7395](https://github.com/JabRef/jabref/issues/7395) +- we added a new ### Fixed