Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
41c6116
Fix bulk imports adding entries to navigation history
shubhamk0205 Dec 11, 2025
e760841
Trigger CI
shubhamk0205 Dec 11, 2025
3c5271f
Trigger CI
shubhamk0205 Dec 11, 2025
f1d5717
Trigger CI
shubhamk0205 Dec 11, 2025
bdb70b9
Suppress navigation history updates for bulk insertions
shubhamk0205 Dec 13, 2025
cbd9813
fix formatting issue
shubhamk0205 Dec 13, 2025
4be103c
fix formatting
shubhamk0205 Dec 14, 2025
4fd6f05
fix formatting
shubhamk0205 Dec 14, 2025
b6b2b62
fix formatting
shubhamk0205 Dec 14, 2025
08d6bbd
Trigger CI
shubhamk0205 Dec 14, 2025
0042466
Trigger CI
shubhamk0205 Dec 14, 2025
f43ade9
reverted unrelated formatting changes
shubhamk0205 Dec 15, 2025
db97184
fix CI errors
shubhamk0205 Dec 15, 2025
e4922b9
fix importHandler
shubhamk0205 Dec 15, 2025
3f4cbfb
reverted unrelated changes
shubhamk0205 Dec 15, 2025
7612988
reverted unrelated changes
shubhamk0205 Dec 15, 2025
a75ba39
fixed
shubhamk0205 Dec 15, 2025
db4a930
Refactor navigation suppression to use nesting-aware boolean
shubhamk0205 Dec 23, 2025
14faff5
Refactor navigation suppression to use nesting-aware boolean
shubhamk0205 Dec 23, 2025
2b3aed4
Fix: Prevent ghost navigation history entries from bulk imports (#13878)
shubhamk0205 Jan 3, 2026
efdd797
Fix: Prevent ghost navigation history entries from bulk imports (#13878)
shubhamk0205 Jan 3, 2026
8fd45e8
Merge branch 'main' into fix-for-issue-13878-v2
shubhamk0205 Jan 3, 2026
6ee1150
Move #13878 entry to Unreleased section
shubhamk0205 Jan 3, 2026
23a27e2
update comments
shubhamk0205 Jan 13, 2026
eb293dc
Merge remote-tracking branch 'upstream/main' into fix-for-issue-13878-v2
shubhamk0205 Jan 13, 2026
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
### Fixed

- We fixed an issue where the AI export button was enabled even when the chat history was empty. [#14640](https://github.com/JabRef/jabref/issues/14640)
- We fixed an issue where bulk import operations polluted the navigation history, making the Back/Forward buttons navigate through imported entries instead of only user-selected entries. [#13878](https://github.com/JabRef/jabref/issues/13878)
- We fixed an issue where pressing <kbd>ESC</kbd> in the preferences dialog would not always close the dialog. [#8888](https://github.com/JabRef/jabref/issues/8888)
- We fixed the checkbox in merge dialog "Treat duplicates the same way" to make it functional. [#14224](https://github.com/JabRef/jabref/pull/14224)
- We fixed the fallback window height (786 → 768) in JabRefGUI. [#14295](https://github.com/JabRef/jabref/pull/14295)
Expand Down
19 changes: 11 additions & 8 deletions jabgui/src/main/java/org/jabref/gui/LibraryTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -827,14 +827,17 @@ public void insertEntries(final List<BibEntry> entries) {
return;
}

importHandler.importCleanedEntries(null, entries);
getUndoManager().addEdit(new UndoableInsertEntries(bibDatabaseContext.getDatabase(), entries));
markBaseChanged();
stateManager.setSelectedEntries(entries);
if (preferences.getEntryEditorPreferences().shouldOpenOnNewEntry()) {
showAndEdit(entries.getFirst());
} else {
clearAndSelect(entries.getFirst());
// Suppress navigation history during bulk insert to prevent polluting back/forward navigation
try (NavigationHistory.Suppression ignored = navigationHistory.suppressUpdatesIf(entries.size() > 1)) {
Copy link
Member

Choose a reason for hiding this comment

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

A. Why is this in a try block
B. Why can't this be replaced with an if-condition inside NavigationHistory with equivalent logic without all the Suppression class complication

importHandler.importCleanedEntries(null, entries);
getUndoManager().addEdit(new UndoableInsertEntries(bibDatabaseContext.getDatabase(), entries));
markBaseChanged();
stateManager.setSelectedEntries(entries);
if (preferences.getEntryEditorPreferences().shouldOpenOnNewEntry()) {
showAndEdit(entries.getFirst());
} else {
clearAndSelect(entries.getFirst());
}
}
}

Expand Down
76 changes: 76 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/NavigationHistory.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,30 @@
* Manages the navigation history of viewed entries using two stacks.
* This class encapsulates the logic of moving back and forward by maintaining a "back" stack for past entries
* and a "forward" stack for future entries.
*
* <p>History updates can be suppressed during bulk operations (e.g., file imports, drag-and-drop)
* using the {@link #suppressUpdates()} method, which employs a nesting-aware boolean flag to handle
* multiple concurrent suppression contexts correctly.</p>
*/
public class NavigationHistory {
private final List<BibEntry> previousEntries = new ArrayList<>();
private final List<BibEntry> nextEntries = new ArrayList<>();
private BibEntry currentEntry;
private boolean suppressNavigation = false;

/**
* Sets a new entry as the current one, clearing the forward history.
* The previously current entry is moved to the back stack.
*
* <p>This operation is skipped if navigation updates are currently suppressed.</p>
*
* @param entry The BibEntry to add to the history.
*/
public void add(BibEntry entry) {
if (suppressNavigation) {
return;
}

if (Objects.equals(currentEntry, entry)) {
return;
}
Expand Down Expand Up @@ -74,4 +85,69 @@ public boolean canGoBack() {
public boolean canGoForward() {
return !nextEntries.isEmpty();
}

/**
* Suppresses navigation history updates while the returned guard is open.
* Handles nesting correctly: if already suppressed, the flag is only cleared when the outermost guard closes.
*
* @return A {@link Suppression} guard that restores navigation tracking when closed.
*/
public Suppression suppressUpdates() {
return new Suppression(this);
}

/**
* Convenience helper to suppress updates conditionally.
*
* @param active If true, returns an active suppression; otherwise returns a no-op suppression.
* @return A {@link Suppression} guard.
*/
public Suppression suppressUpdatesIf(boolean active) {
return active ? suppressUpdates() : Suppression.noOp();
}

/**
* AutoCloseable guard for suppressing navigation history updates.
*
* <p>Uses a nesting-aware approach: captures the suppression state when created,
* and only restores it if this instance was the one that activated suppression.</p>
*/
public static final class Suppression implements AutoCloseable {
Copy link
Member

Choose a reason for hiding this comment

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

do we need a separate suppression class anymore? It was made essentially to encapsulate the depth logic right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The class was originally made for the depth logic, but it's still valuable for the boolean approach because each instance stores its own nesting state (wasAlreadySuppressed) and ensures automatic cleanup.

if u need any changes do let me know !

Copy link
Member

Choose a reason for hiding this comment

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

@shubhamk0205 please either decide whether you wish to answer our questions using AI (which we can also do, in which case we would not wait for you) or decide to understand what you are doing and answer yourself.

private static final Suppression NO_OP = new Suppression();

private final NavigationHistory owner;
private boolean closed;
private final boolean active;
private final boolean wasAlreadySuppressed;

private Suppression() {
this.owner = null;
this.active = false;
this.wasAlreadySuppressed = false;
}

private Suppression(NavigationHistory owner) {
this.owner = owner;
this.active = true;
this.wasAlreadySuppressed = owner.suppressNavigation;
owner.suppressNavigation = true;
}

@Override
public void close() {
if (closed || !active) {
return;
}
closed = true;

// Only turn off suppression if we were the ones who turned it on
if (!wasAlreadySuppressed) {
owner.suppressNavigation = false;
}
}

public static Suppression noOp() {
return NO_OP;
}
}
}
60 changes: 60 additions & 0 deletions jabgui/src/test/java/org/jabref/gui/NavigationHistoryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.jabref.gui;

import java.util.Optional;

import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.types.StandardEntryType;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class NavigationHistoryTest {

@Test
void addsEntriesAndNavigatesBackAndForward() {
NavigationHistory history = new NavigationHistory();
BibEntry first = new BibEntry(StandardEntryType.Article);
BibEntry second = new BibEntry(StandardEntryType.Book);

history.add(first);
history.add(second);

assertTrue(history.canGoBack());
Optional<BibEntry> back = history.back();
assertTrue(back.isPresent());
assertEquals(first, back.get());

assertTrue(history.canGoForward());
Optional<BibEntry> forward = history.forward();
assertTrue(forward.isPresent());
assertEquals(second, forward.get());
}

@Test
void suppressedAddsDoNotAffectHistory() {
NavigationHistory history = new NavigationHistory();
BibEntry initial = new BibEntry(StandardEntryType.Article);
BibEntry suppressed = new BibEntry(StandardEntryType.Book);
BibEntry later = new BibEntry(StandardEntryType.InProceedings);

history.add(initial);

try (NavigationHistory.Suppression ignored = history.suppressUpdates()) {
history.add(suppressed);
}

// still only the initial entry is tracked
assertFalse(history.canGoBack());

history.add(later);

assertTrue(history.canGoBack());
Optional<BibEntry> back = history.back();
assertTrue(back.isPresent());
assertEquals(initial, back.get());
}
}