Skip to content

Commit c06435a

Browse files
authored
Add pagination for search based fetchers (#13490)
* Add Pagination for fetchers * An Attempt to define pagination for arXiv - Clone and create web* (WebImportEntriesDialog and WebImportEntriesDialogViewModel for web implementation) - Cache prev pages in case of page based fetcher * Fix page number display state for pagination * Minor fixes - Enhance the background fetching mechanism for page-based search entries - Fix 'Import Entries' button functionality - Fix display of total and selected items counters - Fix selection buttons behavior (unselectAll, selectAllNewEntries, selectAllEntries) * Refactor fetchMoreEntries method - generalized fetching of entries for all page-search-based fetcher - improved pageNumberLabel UI - update JabRef_en.properties * Add CHANGELOG.md entry * Remove WebImportEntriesDialog, its ViewModel, and associated FXML in favor of integrating pagination into ImportEntriesDialog. * Refactor ImportEntriesDialog instantiation to remove unnecessary parameters * Minor fixes * Minor fix * Minor fix * Enhance UI - Added status label to display loading messages and entry status. - Implemented bindings to enable/disable pagination buttons based on loading state and current page. - Updated next/previous page button actions to handle fetching more entries when on the last page. - Introduced initialLoadComplete property to manage the loading state more effectively. * Update JabRef_en.properties * Apply boy scout principle - Refactor ImportEntriesViewModel and ImportEntriesDialog to use Optional for fetcher and query.
1 parent 1aae72d commit c06435a

File tree

7 files changed

+382
-28
lines changed

7 files changed

+382
-28
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
2828
- We added a new ID based fetcher for [EuropePMC](https://europepmc.org/). [#13389](https://github.com/JabRef/jabref/pull/13389)
2929
- We added quick settings for welcome tab. [#12664](https://github.com/JabRef/jabref/issues/12664)
3030
- We added an initial [cite as you write](https://retorque.re/zotero-better-bibtex/citing/cayw/) endpoint. [#13187](https://github.com/JabRef/jabref/issues/13187)
31+
- We added pagination support for the web search entries dialog, improving navigation for large search results. [#5507](https://github.com/JabRef/jabref/issues/5507)
3132
- We added "copy preview as markdown" feature. [#12552](https://github.com/JabRef/jabref/issues/12552)
3233
- In case no citation relation information can be fetched, we show the data providers reason. [#13549](https://github.com/JabRef/jabref/pull/13549)
3334
- When relativizing file names, symlinks are now taken into account. [#12995](https://github.com/JabRef/jabref/issues/12995)

jabgui/src/main/java/org/jabref/gui/importer/ImportEntriesDialog.java

Lines changed: 221 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
import javafx.beans.binding.Bindings;
88
import javafx.beans.binding.BooleanBinding;
9+
import javafx.beans.property.BooleanProperty;
10+
import javafx.collections.ListChangeListener;
911
import javafx.css.PseudoClass;
1012
import javafx.fxml.FXML;
1113
import javafx.geometry.Insets;
14+
import javafx.scene.Cursor;
1215
import javafx.scene.Node;
1316
import javafx.scene.control.Button;
1417
import javafx.scene.control.ButtonType;
@@ -35,7 +38,9 @@
3538
import org.jabref.gui.util.BaseDialog;
3639
import org.jabref.gui.util.NoSelectionModel;
3740
import org.jabref.gui.util.ViewModelListCellFactory;
41+
import org.jabref.logic.importer.PagedSearchBasedFetcher;
3842
import org.jabref.logic.importer.ParserResult;
43+
import org.jabref.logic.importer.SearchBasedFetcher;
3944
import org.jabref.logic.l10n.Localization;
4045
import org.jabref.logic.shared.DatabaseLocation;
4146
import org.jabref.logic.util.BackgroundTask;
@@ -53,7 +58,8 @@
5358
import org.fxmisc.richtext.CodeArea;
5459

5560
public class ImportEntriesDialog extends BaseDialog<Boolean> {
56-
61+
@FXML private HBox paginationBox;
62+
@FXML private Label pageNumberLabel;
5763
@FXML private CheckListView<BibEntry> entriesListView;
5864
@FXML private ComboBox<BibDatabaseContext> libraryListView;
5965
@FXML private ButtonType importButton;
@@ -64,10 +70,15 @@ public class ImportEntriesDialog extends BaseDialog<Boolean> {
6470
@FXML private CheckBox showEntryInformation;
6571
@FXML private CodeArea bibTeXData;
6672
@FXML private VBox bibTeXDataBox;
73+
@FXML private Button nextPageButton;
74+
@FXML private Button prevPageButton;
75+
@FXML private Label statusLabel;
6776

6877
private final BackgroundTask<ParserResult> task;
6978
private final BibDatabaseContext database;
7079
private ImportEntriesViewModel viewModel;
80+
private final Optional<SearchBasedFetcher> searchBasedFetcher;
81+
private final Optional<String> query;
7182

7283
@Inject private TaskExecutor taskExecutor;
7384
@Inject private DialogService dialogService;
@@ -78,42 +89,61 @@ public class ImportEntriesDialog extends BaseDialog<Boolean> {
7889
@Inject private FileUpdateMonitor fileUpdateMonitor;
7990

8091
/**
81-
* Imports the given entries into the given database. The entries are provided using the BackgroundTask
92+
* Creates an import dialog for entries from file sources.
93+
* This constructor is used for importing entries from local files, BibTeX files,
94+
* or other file-based sources that don't require pagination or search functionality.
8295
*
8396
* @param database the database to import into
8497
* @param task the task executed for parsing the selected files(s).
8598
*/
8699
public ImportEntriesDialog(BibDatabaseContext database, BackgroundTask<ParserResult> task) {
87100
this.database = database;
88101
this.task = task;
89-
ViewLoader.view(this)
90-
.load()
91-
.setAsDialogPane(this);
102+
this.searchBasedFetcher = Optional.empty();
103+
this.query = Optional.empty();
92104

93-
BooleanBinding booleanBind = Bindings.isEmpty(entriesListView.getCheckModel().getCheckedItems());
94-
Button btn = (Button) this.getDialogPane().lookupButton(importButton);
95-
btn.disableProperty().bind(booleanBind);
96-
97-
downloadLinkedOnlineFiles.setSelected(preferences.getFilePreferences().shouldDownloadLinkedFiles());
105+
initializeDialog();
106+
}
98107

99-
setResultConverter(button -> {
100-
if (button == importButton) {
101-
viewModel.importEntries(entriesListView.getCheckModel().getCheckedItems(), downloadLinkedOnlineFiles.isSelected());
102-
} else {
103-
dialogService.notify(Localization.lang("Import canceled"));
104-
}
108+
/**
109+
* Creates an import dialog for entries from web-based search sources.
110+
* This constructor is used for importing entries that support pagination and require search queries.
111+
*
112+
* @param database database where the imported entries will be added
113+
* @param task task that handles parsing and loading entries from the search results
114+
* @param fetcher the search-based fetcher implementation used to retrieve entries from the web source
115+
* @param query the search string used to find relevant entries
116+
*/
117+
public ImportEntriesDialog(BibDatabaseContext database, BackgroundTask<ParserResult> task, SearchBasedFetcher fetcher, String query) {
118+
this.database = database;
119+
this.task = task;
120+
this.searchBasedFetcher = Optional.of(fetcher);
121+
this.query = Optional.of(query);
105122

106-
return false;
107-
});
123+
initializeDialog();
108124
}
109125

110126
@FXML
111127
private void initialize() {
112-
viewModel = new ImportEntriesViewModel(task, taskExecutor, database, dialogService, undoManager, preferences, stateManager, entryTypesManager, fileUpdateMonitor);
128+
viewModel = new ImportEntriesViewModel(task, taskExecutor, database, dialogService, undoManager, preferences, stateManager, entryTypesManager, fileUpdateMonitor, searchBasedFetcher, query);
113129
Label placeholder = new Label();
114130
placeholder.textProperty().bind(viewModel.messageProperty());
115131
entriesListView.setPlaceholder(placeholder);
116132
entriesListView.setItems(viewModel.getEntries());
133+
entriesListView.getCheckModel().getCheckedItems().addListener((ListChangeListener<BibEntry>) change -> {
134+
while (change.next()) {
135+
if (change.wasAdded()) {
136+
for (BibEntry entry : change.getAddedSubList()) {
137+
viewModel.getCheckedEntries().add(entry);
138+
}
139+
}
140+
if (change.wasRemoved()) {
141+
for (BibEntry entry : change.getRemoved()) {
142+
viewModel.getCheckedEntries().remove(entry);
143+
}
144+
}
145+
}
146+
});
117147

118148
libraryListView.setEditable(false);
119149
libraryListView.getItems().addAll(stateManager.getOpenDatabases());
@@ -180,14 +210,144 @@ private void initialize() {
180210
.withPseudoClass(entrySelected, entriesListView::getItemBooleanProperty)
181211
.install(entriesListView);
182212

183-
selectedItems.textProperty().bind(Bindings.size(entriesListView.getCheckModel().getCheckedItems()).asString());
184-
totalItems.textProperty().bind(Bindings.size(entriesListView.getItems()).asString());
213+
selectedItems.textProperty().bind(Bindings.size(viewModel.getCheckedEntries()).asString());
214+
totalItems.textProperty().bind(Bindings.size(viewModel.getAllEntries()).asString());
185215
entriesListView.setSelectionModel(new NoSelectionModel<>());
186216
initBibTeX();
217+
if (searchBasedFetcher.isPresent()) {
218+
updatePageUI();
219+
setupPaginationBindings();
220+
}
221+
}
222+
223+
private void initializeDialog() {
224+
ViewLoader.view(this)
225+
.load()
226+
.setAsDialogPane(this);
227+
228+
paginationBox.setVisible(searchBasedFetcher.isPresent());
229+
paginationBox.setManaged(searchBasedFetcher.isPresent());
230+
231+
BooleanBinding booleanBind = Bindings.isEmpty(entriesListView.getCheckModel().getCheckedItems());
232+
Button btn = (Button) this.getDialogPane().lookupButton(importButton);
233+
btn.disableProperty().bind(booleanBind);
234+
235+
downloadLinkedOnlineFiles.setSelected(preferences.getFilePreferences().shouldDownloadLinkedFiles());
236+
237+
setResultConverter(button -> {
238+
if (button == importButton) {
239+
viewModel.importEntries(viewModel.getCheckedEntries().stream().toList(), downloadLinkedOnlineFiles.isSelected());
240+
} else {
241+
dialogService.notify(Localization.lang("Import canceled"));
242+
}
243+
244+
return false;
245+
});
246+
}
247+
248+
private void setupPaginationBindings() {
249+
BooleanProperty loading = viewModel.loadingProperty();
250+
BooleanProperty initialLoadComplete = viewModel.initialLoadCompleteProperty();
251+
252+
BooleanBinding isOnLastPage = Bindings.createBooleanBinding(() -> {
253+
int currentPage = viewModel.currentPageProperty().get();
254+
int totalPages = viewModel.totalPagesProperty().get();
255+
return currentPage >= totalPages - 1;
256+
}, viewModel.currentPageProperty(), viewModel.totalPagesProperty());
257+
258+
BooleanBinding isPagedFetcher = Bindings.createBooleanBinding(() ->
259+
searchBasedFetcher.isPresent() && searchBasedFetcher.get() instanceof PagedSearchBasedFetcher
260+
);
261+
262+
// Disable: during loading OR when on the last page for non-paged fetchers
263+
// OR when the initial load is not complete for paged fetchers
264+
nextPageButton.disableProperty().bind(
265+
loading.or(isOnLastPage.and(isPagedFetcher.not()))
266+
.or(isPagedFetcher.and(initialLoadComplete.not()))
267+
);
268+
prevPageButton.disableProperty().bind(loading.or(viewModel.currentPageProperty().isEqualTo(0)));
269+
270+
prevPageButton.textProperty().bind(
271+
Bindings.when(loading)
272+
.then("< " + Localization.lang("Loading..."))
273+
.otherwise("< " + Localization.lang("Previous"))
274+
);
275+
276+
nextPageButton.textProperty().bind(
277+
Bindings.when(loading)
278+
.then(Localization.lang("Loading...") + " >")
279+
.otherwise(
280+
Bindings.when(initialLoadComplete.not().and(isPagedFetcher))
281+
.then(Localization.lang("Loading initial entries..."))
282+
.otherwise(
283+
Bindings.when(isOnLastPage)
284+
.then(
285+
Bindings.when(isPagedFetcher)
286+
.then(Localization.lang("Load More") + " >>")
287+
.otherwise(Localization.lang("No more entries"))
288+
)
289+
.otherwise(Localization.lang("Next") + " >")
290+
)
291+
)
292+
);
293+
294+
statusLabel.textProperty().bind(
295+
Bindings.when(loading)
296+
.then(Localization.lang("Fetching more entries..."))
297+
.otherwise(
298+
Bindings.when(initialLoadComplete.not().and(isPagedFetcher))
299+
.then(Localization.lang("Loading initial results..."))
300+
.otherwise(
301+
Bindings.when(isOnLastPage)
302+
.then(
303+
Bindings.when(isPagedFetcher)
304+
.then(Localization.lang("Click 'Load More' to fetch additional entries"))
305+
.otherwise(Bindings.createStringBinding(() -> {
306+
int totalEntries = viewModel.getAllEntries().size();
307+
return totalEntries > 0 ?
308+
Localization.lang("All %0 entries loaded", String.valueOf(totalEntries)) :
309+
Localization.lang("No entries available");
310+
}, viewModel.getAllEntries()))
311+
)
312+
.otherwise("")
313+
)
314+
)
315+
);
316+
317+
loading.addListener((_, _, newVal) -> {
318+
getDialogPane().getScene().setCursor(newVal ? Cursor.WAIT : Cursor.DEFAULT);
319+
});
320+
321+
isOnLastPage.addListener((_, oldVal, newVal) -> {
322+
if (newVal && !oldVal) {
323+
statusLabel.getStyleClass().add("info-message");
324+
} else if (!newVal && oldVal) {
325+
statusLabel.getStyleClass().remove("info-message");
326+
}
327+
});
328+
}
329+
330+
private void updatePageUI() {
331+
pageNumberLabel.textProperty().bind(Bindings.createStringBinding(() -> {
332+
int totalPages = viewModel.totalPagesProperty().get();
333+
int currentPage = viewModel.currentPageProperty().get() + 1;
334+
if (totalPages != 0) {
335+
return Localization.lang("%0 of %1", currentPage, totalPages);
336+
}
337+
return "";
338+
}, viewModel.currentPageProperty(), viewModel.totalPagesProperty()));
339+
340+
viewModel.getAllEntries().addListener((ListChangeListener<BibEntry>) change -> {
341+
while (change.next()) {
342+
if (change.wasAdded() || change.wasRemoved()) {
343+
viewModel.updateTotalPages();
344+
}
345+
}
346+
});
187347
}
188348

189349
private void displayBibTeX(BibEntry entry, String bibTeX) {
190-
if (entriesListView.getCheckModel().isChecked(entry)) {
350+
if (viewModel.getCheckedEntries().contains(entry)) {
191351
bibTeXData.clear();
192352
bibTeXData.appendText(bibTeX);
193353
bibTeXData.moveTo(0);
@@ -208,14 +368,16 @@ private void initBibTeX() {
208368
}
209369

210370
public void unselectAll() {
211-
entriesListView.getCheckModel().clearChecks();
371+
viewModel.getCheckedEntries().clear();
372+
entriesListView.getItems().forEach(entry -> entriesListView.getCheckModel().clearCheck(entry));
212373
}
213374

214375
public void selectAllNewEntries() {
215376
unselectAll();
216-
for (BibEntry entry : entriesListView.getItems()) {
377+
for (BibEntry entry : viewModel.getAllEntries()) {
217378
if (!viewModel.hasDuplicate(entry)) {
218379
entriesListView.getCheckModel().check(entry);
380+
viewModel.getCheckedEntries().add(entry);
219381
displayBibTeX(entry, viewModel.getSourceString(entry));
220382
}
221383
}
@@ -224,5 +386,40 @@ public void selectAllNewEntries() {
224386
public void selectAllEntries() {
225387
unselectAll();
226388
entriesListView.getCheckModel().checkAll();
389+
viewModel.getCheckedEntries().addAll(viewModel.getAllEntries());
390+
}
391+
392+
private boolean isOnLastPageAndPagedFetcher() {
393+
if (searchBasedFetcher.isEmpty() || !(searchBasedFetcher.get() instanceof PagedSearchBasedFetcher)) {
394+
return false;
395+
}
396+
397+
int currentPage = viewModel.currentPageProperty().get();
398+
int totalPages = viewModel.totalPagesProperty().get();
399+
return currentPage >= totalPages - 1;
400+
}
401+
402+
@FXML
403+
private void onPrevPage() {
404+
viewModel.goToPrevPage();
405+
restoreCheckedEntries();
406+
}
407+
408+
@FXML
409+
private void onNextPage() {
410+
if (isOnLastPageAndPagedFetcher()) {
411+
viewModel.fetchMoreEntries();
412+
} else {
413+
viewModel.goToNextPage();
414+
}
415+
restoreCheckedEntries();
416+
}
417+
418+
private void restoreCheckedEntries() {
419+
for (BibEntry entry : viewModel.getEntries()) {
420+
if (viewModel.getCheckedEntries().contains(entry)) {
421+
entriesListView.getCheckModel().check(entry);
422+
}
423+
}
227424
}
228425
}

0 commit comments

Comments
 (0)