Skip to content

Fix/bug date range preservation#15511

Merged
Siedlerchr merged 29 commits into
JabRef:mainfrom
jadegold55:fix/bug-date-range-preservation
Apr 19, 2026
Merged

Fix/bug date range preservation#15511
Siedlerchr merged 29 commits into
JabRef:mainfrom
jadegold55:fix/bug-date-range-preservation

Conversation

@jadegold55

@jadegold55 jadegold55 commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Related issues and pull requests

Closes #8902

PR Description

  1. Typed date input is now preserved as freeform text in the entry editor, so date ranges and other non-single-date values.
  2. On Enter, Tab, or focus changes, and the Normalize date save action now keeps both ends of a parsed date range instead of truncating to the start date.

After change, with ENTER, TAB, and Focus change

Recording.2026-04-13.001053.mp4

Steps to test

  1. Create an empty library
  2. Go to library properties and switch to biblatex
  3. Create a new entry with a date range, e.g 2020/2021
  4. Verify that the value remains after pressing enter, tab, or clicking off
  5. Go back to library properties and go to saving tab
  6. Delete the normalize date range field formatter
  7. Repeat steps 3-4

TODO: take videos of new behavior

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • I added screenshots in the PR description (if change is visible to the user)
  • I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • [/] I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

@calixtus

Copy link
Copy Markdown
Member

whats the status here? Is this rfr or draft status deliberately chosen?

@jadegold55

jadegold55 commented Apr 12, 2026

Copy link
Copy Markdown
Contributor Author

whats the status here? Is this rfr or draft status deliberately chosen?

I apologize! Last week has been very busy. I hope to work more on this issue sometime today and have it done by this week. Will most likely commit today with an updated version.

Also yes the draft status was deliberately chosen.

@jadegold55 jadegold55 marked this pull request as ready for review April 13, 2026 03:57
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Preserve BibLaTeX date ranges in editor and normalize action

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve date ranges in entry editor instead of truncating to start date
• Synchronize text field and date picker with bidirectional updates
• Keep both ends of date range in normalized date formatter output
• Add text field with commit logic on Enter, Tab, and focus loss
Diagram
flowchart LR
  A["User enters date range"] --> B["Text field captures input"]
  B --> C["Commit on Enter/Tab/Focus loss"]
  C --> D["Parse and validate date"]
  D --> E["Sync picker with text"]
  E --> F["Preserve full range in model"]
  F --> G["Normalize keeps both dates"]
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java ✨ Enhancement +101/-2

Implement text field with date range preservation logic

• Added EditorTextField component alongside date picker for text input
• Implemented bidirectional synchronization between text field and picker
• Added commit logic triggered on Enter, Tab, and focus loss events
• Added helper methods for date parsing, formatting, and normalization
• Tracks last accepted text to preserve date ranges during editing

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java


2. jablib/src/main/java/org/jabref/model/entry/Date.java 🐞 Bug fix +5/-1

Include end date in normalized date output

• Modified getNormalized() to include end date when present
• Formats date ranges as "startDate/endDate" instead of truncating

jablib/src/main/java/org/jabref/model/entry/Date.java


3. jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeDateFormatterTest.java 🧪 Tests +5/-0

Add date range normalization test

• Added test case for date range formatting
• Verifies that date ranges like "2020/2021" are preserved

jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeDateFormatterTest.java


View more (3)
4. jablib/src/test/java/org/jabref/model/entry/DateTest.java 🧪 Tests +5/-0

Add date range normalization preservation test

• Added test to verify normalized date ranges keep both dates
• Tests that "2020/2021" is preserved through normalization

jablib/src/test/java/org/jabref/model/entry/DateTest.java


5. jabgui/src/main/resources/org/jabref/gui/fieldeditors/DateEditor.fxml ⚙️ Configuration changes +4/-2

Update layout with text field and picker button

• Added EditorTextField as primary input component
• Modified TemporalAccessorPicker to be non-editable and non-focusable
• Adjusted picker size to 32x32 pixels for calendar button only

jabgui/src/main/resources/org/jabref/gui/fieldeditors/DateEditor.fxml


6. CHANGELOG.md 📝 Documentation +1/-0

Document date range preservation fix

• Added entry documenting fix for BibLaTeX date range preservation
• References issue #8902

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (6)
4. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Non-FX UI updates🐞 Bug ☼ Reliability
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

10. Uses isPresent() + get()📘 Rule violation ⚙ Maintainability
Description
The new commitTextFieldValue() unwraps Optional via isPresent() followed by get(), which the
checklist discourages in favor of ifPresent/ifPresentOrElse for clearer, safer Optional
handling. This introduces a non-compliant Optional usage pattern in newly added code.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R102-105]

+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
Evidence
PR Compliance ID 6 requires using functional Optional APIs and explicitly lists isPresent() +
manual branching as a failure pattern when ifPresentOrElse fits. The added code checks
pickerCompatibleDate.isPresent() and then calls pickerCompatibleDate.get() to extract the value.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commitTextFieldValue()` unwraps an `Optional` using `isPresent()` followed by `get()`, which violates the project's Optional-handling guideline.
## Issue Context
Refactor the Optional handling to use `Optional.ifPresent(...)` or `Optional.ifPresentOrElse(...)` so the control flow remains explicit without calling `get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Invalid input cleared🐞 Bug ≡ Correctness
Description
commitTextFieldValue() clears the field to an empty string when the text is not parseable by
Date.parse, silently discarding what the user typed on Enter or focus loss. This behavior turns a
validation failure into data loss and differs from existing date-picker behavior that preserves
invalid text.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R93-116]

+    private void commitTextFieldValue() {
+        String currentText = normalizeText(textField.getText());
+
+        if (currentText.isEmpty()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
+        }
+
+        if (Date.parse(currentText).isPresent()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        syncPickerWithValue(null);
+        acceptCommittedText("");
+    }
Evidence
When Date.parse fails, DateEditor forces the committed text to "" which propagates via the
editor/viewmodel binding into the BibEntry field. TemporalAccessorPicker’s internal converter
explicitly preserves original text when the parsed value is null (invalid), indicating the
established component behavior is not to wipe invalid user input.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
jabgui/src/main/java/org/jabref/gui/util/component/TemporalAccessorPicker.java[139-146]
jabgui/src/main/java/org/jabref/gui/fieldeditors/AbstractEditorViewModel.java[63-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor.commitTextFieldValue()` replaces unparseable user input with an empty string. This causes silent loss of what the user typed.
### Issue Context
The editor already tracks `lastAcceptedText`, and other date UI logic (`TemporalAccessorPicker`) is designed to keep the original text when parsing fails.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[136-142]
### Suggested fix
In the invalid-parse path, do not call `acceptCommittedText("")`. Instead, keep the current text (so the user can correct it) **or** revert to `lastAcceptedText` (so no invalid value is persisted). If reverting, ensure you don’t overwrite `lastAcceptedText` with the invalid value during the revert.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. CFF emits date ranges🐞 Bug ≡ Correctness
Description
Date.getNormalized() now returns "start/end" for ranges, and CffExporter writes this string into the
date-released field whenever the start date has year/month/day. This can produce a non-single-date
value in an exporter code path that currently assumes a single ISO date string.
Code

jablib/src/main/java/org/jabref/model/entry/Date.java[R343-347]

+        String normalizedStartDate = NORMALIZED_DATE_FORMATTER.format(date);
+        if (endDate != null) {
+            return normalizedStartDate + "/" + NORMALIZED_DATE_FORMATTER.format(endDate);
+        }
+        return normalizedStartDate;
Evidence
Date.parse constructs Date instances with a non-null endDate for strings containing '/'. With the
new getNormalized behavior, any consumer that expects a single normalized start date (like
CffExporter’s date-released path) will now receive a range string containing '/'.

jablib/src/main/java/org/jabref/model/entry/Date.java[134-152]
jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Date.getNormalized()` now preserves ranges (`start/end`). `CffExporter.parseDate` uses `parsedDate.getNormalized()` as the value for `date-released` when the *start* date is complete, so ranged inputs can flow into `date-released`.
### Issue Context
`Date` does not expose `endDate`, but you can normalize only the start by constructing a new `Date` from `parsedDate.toTemporalAccessor()` (which is the start date) and calling `getNormalized()` on that.
### Fix Focus Areas
- jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
- jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-261]
### Suggested fix
In `CffExporter.parseDate`, when setting `date-released`, normalize only the start date (e.g., `new Date(parsedDate.toTemporalAccessor()).getNormalized()`) or explicitly detect ranges and fall back to `issue-date` instead of populating `date-released` with a range string.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (11)
13. Deferred picker sync churn 🐞 Bug ➹ Performance
Description
DateEditor schedules picker synchronization via UiTaskExecutor.runInJavaFXThread(Runnable), which
always queues with Platform.runLater even when already on the FX thread, creating avoidable
event-queue churn and making sync timing depend on later UI state. This can increase UI work during
frequent programmatic updates (e.g., save actions / entry switches) and makes the picker update less
deterministic than necessary.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R56-61]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> UiTaskExecutor.runInJavaFXThread(() -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        }));
Evidence
DateEditor uses UiTaskExecutor.runInJavaFXThread(Runnable) for every viewModel text change before
checking focus and syncing the picker. That overload unconditionally queues work with
Platform.runLater, even if already on the FX thread, so this listener always defers execution and
can accumulate queued tasks during bursts of updates.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[53-61]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[90-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` currently uses `UiTaskExecutor.runInJavaFXThread(Runnable)` for picker synchronization. That overload always uses `Platform.runLater`, even when already on the JavaFX thread, causing unnecessary deferral and extra queued UI work.
### Issue Context
The code is already performing a focus check and only syncing the picker when the user is not actively editing. Syncing immediately when already on the FX thread reduces churn and makes ordering more predictable.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[56-61]
### Suggested change
Replace the unconditional `UiTaskExecutor.runInJavaFXThread(() -> { ... })` call with:
- `if (Platform.isFxApplicationThread()) { ... } else { Platform.runLater(() -> { ... }); }`
This preserves thread-safety for background-triggered updates while avoiding needless queuing on the FX thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Uses isPresent() + get()📘 Rule violation ⚙ Maintainability
Description
The new commitTextFieldValue() unwraps Optional via isPresent() followed by get(), which the
checklist discourages in favor of ifPresent/ifPresentOrElse for clearer, safer Optional
handling. This introduces a non-compliant Optional usage pattern in newly added code.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R102-105]

+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
Evidence
PR Compliance ID 6 requires using functional Optional APIs and explicitly lists isPresent() +
manual branching as a failure pattern when ifPresentOrElse fits. The added code checks
pickerCompatibleDate.isPresent() and then calls pickerCompatibleDate.get() to extract the value.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commitTextFieldValue()` unwraps an `Optional` using `isPresent()` followed by `get()`, which violates the project's Optional-handling guideline.
## Issue Context
Refactor the Optional handling to use `Optional.ifPresent(...)` or `Optional.ifPresentOrElse(...)` so the control flow remains explicit without calling `get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Invalid input cleared🐞 Bug ≡ Correctness
Description
commitTextFieldValue() clears the field to an empty string when the text is not parseable by
Date.parse, silently discarding what the user typed on Enter or focus loss. This behavior turns a
validation failure into data loss and differs from existing date-picker behavior that preserves
invalid text.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R93-116]

+    private void commitTextFieldValue() {
+        String currentText = normalizeText(textField.getText());
+
+        if (currentText.isEmpty()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
+        }
+
+        if (Date.parse(currentText).isPresent()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        syncPickerWithValue(null);
+        acceptCommittedText("");
+    }
Evidence
When Date.parse fails, DateEditor forces the committed text to "" which propagates via the
editor/viewmodel binding into the BibEntry field. TemporalAccessorPicker’s internal converter
explicitly preserves original text when the parsed value is null (invalid), indicating the
established component behavior is not to wipe invalid user input.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
jabgui/src/main/java/org/jabref/gui/util/component/TemporalAccessorPicker.java[139-146]
jabgui/src/main/java/org/jabref/gui/fieldeditors/AbstractEditorViewModel.java[63-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor.commitTextFieldValue()` replaces unparseable user input with an empty string. This causes silent loss of what the user typed.
### Issue Context
The editor already tracks `lastAcceptedText`, and other date UI logic (`TemporalAccessorPicker`) is designed to keep the original text when parsing fails.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[136-142]
### Suggested fix
In the invalid-parse path, do not call `acceptCommittedText("")`. Instead, keep the current text (so the user can correct it) **or** revert to `lastAcceptedText` (so no invalid value is persisted). If reverting, ensure you don’t overwrite `lastAcceptedText` with the invalid value during the revert.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. CFF emits date ranges🐞 Bug ≡ Correctness
Description
Date.getNormalized() now returns "start/end" for ranges, and CffExporter writes this string into the
date-released field whenever the start date has year/month/day. This can produce a non-single-date
value in an exporter code path that currently assumes a single ISO date string.
Code

jablib/src/main/java/org/jabref/model/entry/Date.java[R343-347]

+        String normalizedStartDate = NORMALIZED_DATE_FORMATTER.format(date);
+        if (endDate != null) {
+            return normalizedStartDate + "/" + NORMALIZED_DATE_FORMATTER.format(endDate);
+        }
+        return normalizedStartDate;
Evidence
Date.parse constructs Date instances with a non-null endDate for strings containing '/'. With the
new getNormalized behavior, any consumer that expects a single normalized start date (like
CffExporter’s date-released path) will now receive a range string containing '/'.

jablib/src/main/java/org/jabref/model/entry/Date.java[134-152]
jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Date.getNormalized()` now preserves ranges (`start/end`). `CffExporter.parseDate` uses `parsedDate.getNormalized()` as the value for `date-released` when the *start* date is complete, so ranged inputs can flow into `date-released`.
### Issue Context
`Date` does not expose `endDate`, but you can normalize only the start by constructing a new `Date` from `parsedDate.toTemporalAccessor()` (which is the start date) and calling `getNormalized()` on that.
### Fix Focus Areas
- jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
- jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-261]
### Suggested fix
In `CffExporter.parseDate`, when setting `date-released`, normalize only the start date (e.g., `new Date(parsedDate.toTemporalAccessor()).getNormalized()`) or explicitly detect ranges and fall back to `issue-date` instead of populating `date-released` with a range string.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Deferred picker sync churn 🐞 Bug ➹ Performance
Description
DateEditor schedules picker synchronization via UiTaskExecutor.runInJavaFXThread(Runnable), which
always queues with Platform.runLater even when already on the FX thread, creating avoidable
event-queue churn and making sync timing depend on later UI state. This can increase UI work during
frequent programmatic updates (e.g., save actions / entry switches) and makes the picker update less
deterministic than necessary.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R56-61]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> UiTaskExecutor.runInJavaFXThread(() -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        }));
Evidence
DateEditor uses UiTaskExecutor.runInJavaFXThread(Runnable) for every viewModel text change before
checking focus and syncing the picker. That overload unconditionally queues work with
Platform.runLater, even if already on the FX thread, so this listener always defers execution and
can accumulate queued tasks during bursts of updates.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[53-61]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[90-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` currently uses `UiTaskExecutor.runInJavaFXThread(Runnable)` for picker synchronization. That overload always uses `Platform.runLater`, even when already on the JavaFX thread, causing unnecessary deferral and extra queued UI work.
### Issue Context
The code is already performing a focus check and only syncing the picker when the user is not actively editing. Syncing immediately when already on the FX thread reduces churn and makes ordering more predictable.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[56-61]
### Suggested change
Replace the unconditional `UiTaskExecutor.runInJavaFXThread(() -> { ... })` call with:
- `if (Platform.isFxApplicationThread()) { ... } else { Platform.runLater(() -> { ... }); }`
This preserves thread-safety for background-triggered updates while avoiding needless queuing on the FX thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. Uses isPresent() + get()📘 Rule violation ⚙ Maintainability
Description
The new commitTextFieldValue() unwraps Optional via isPresent() followed by get(), which the
checklist discourages in favor of ifPresent/ifPresentOrElse for clearer, safer Optional
handling. This introduces a non-compliant Optional usage pattern in newly added code.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R102-105]

+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
Evidence
PR Compliance ID 6 requires using functional Optional APIs and explicitly lists isPresent() +
manual branching as a failure pattern when ifPresentOrElse fits. The added code checks
pickerCompatibleDate.isPresent() and then calls pickerCompatibleDate.get() to extract the value.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commitTextFieldValue()` unwraps an `Optional` using `isPresent()` followed by `get()`, which violates the project's Optional-handling guideline.
## Issue Context
Refactor the Optional handling to use `Optional.ifPresent(...)` or `Optional.ifPresentOrElse(...)` so the control flow remains explicit without calling `get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


19. Invalid input cleared🐞 Bug ≡ Correctness
Description
commitTextFieldValue() clears the field to an empty string when the text is not parseable by
Date.parse, silently discarding what the user typed on Enter or focus loss. This behavior turns a
validation failure into data loss and differs from existing date-picker behavior that preserves
invalid text.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R93-116]

+    private void commitTextFieldValue() {
+        String currentText = normalizeText(textField.getText());
+
+        if (currentText.isEmpty()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
+        }
+
+        if (Date.parse(currentText).isPresent()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        syncPickerWithValue(null);
+        acceptCommittedText("");
+    }
Evidence
When Date.parse fails, DateEditor forces the committed text to "" which propagates via the
editor/viewmodel binding into the BibEntry field. TemporalAccessorPicker’s internal converter
explicitly preserves original text when the parsed value is null (invalid), indicating the
established component behavior is not to wipe invalid user input.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
jabgui/src/main/java/org/jabref/gui/util/component/TemporalAccessorPicker.java[139-146]
[jabgui/src/mai...

@github-actions github-actions Bot added status: changes-required Pull requests that are not yet complete status: no-bot-comments and removed status: no-bot-comments status: changes-required Pull requests that are not yet complete labels Apr 13, 2026
@Siedlerchr Siedlerchr added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Apr 14, 2026
@Siedlerchr

Copy link
Copy Markdown
Member

I tested it, works, needs a second lool

@Siedlerchr Siedlerchr closed this Apr 14, 2026
@Siedlerchr Siedlerchr reopened this Apr 14, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Preserve date ranges in BibLaTeX date editor

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Preserve date ranges in BibLaTeX date fields during editing
• Prevent truncation of date ranges by normalize date action
• Add text field to date editor for freeform input
• Synchronize picker and text field with bidirectional updates
Diagram
flowchart LR
  A["User enters date range"] --> B["DateEditor text field"]
  B --> C["Commit on Enter/Tab/Focus loss"]
  C --> D["Parse and validate date"]
  D --> E["Sync picker if lossless"]
  D --> F["Accept freeform text if valid"]
  F --> G["Preserve full date range"]
  G --> H["getNormalized includes both dates"]
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java ✨ Enhancement +101/-2

Refactor date editor with text field and sync logic

• Add EditorTextField for freeform date input alongside picker
• Implement bidirectional synchronization between text field and date picker
• Add commit logic on Enter, Tab, and focus loss events
• Track last accepted text and synchronization state to prevent loops
• Add helper methods for date parsing, formatting, and normalization

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java


2. jabgui/src/main/resources/org/jabref/gui/fieldeditors/DateEditor.fxml ✨ Enhancement +4/-2

Update date editor layout with text field

• Add EditorTextField as primary input component
• Make date picker non-editable and non-focusable with fixed width
• Reorder components to show text field first

jabgui/src/main/resources/org/jabref/gui/fieldeditors/DateEditor.fxml


3. jablib/src/main/java/org/jabref/model/entry/Date.java 🐞 Bug fix +5/-1

Preserve date range in normalized output

• Modify getNormalized() to preserve both dates in date ranges
• Append end date with "/" separator when present

jablib/src/main/java/org/jabref/model/entry/Date.java


View more (4)
4. jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java 🐞 Bug fix +1/-2

Fix date conversion in CFF exporter

• Convert parsed date to temporal accessor before normalization
• Ensures proper handling of date objects in CFF export

jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java


5. jablib/src/test/java/org/jabref/model/entry/DateTest.java 🧪 Tests +5/-0

Add date range normalization test

• Add test case for date range normalization
• Verify that "2020/2021" is preserved through getNormalized()

jablib/src/test/java/org/jabref/model/entry/DateTest.java


6. jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeDateFormatterTest.java 🧪 Tests +5/-0

Add date range formatter test

• Add test case for date range formatting
• Verify that "2020/2021" is preserved by formatter

jablib/src/test/java/org/jabref/logic/formatter/bibtexfields/NormalizeDateFormatterTest.java


7. CHANGELOG.md 📝 Documentation +1/-0

Document date range preservation fix

• Document fix for BibLaTeX date range preservation
• Reference issue #8902

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2)   📘 Rule violations (0)   📎 Requirement gaps (0)
🐞\ ⚙ Maintainability (1) ➹ Performance (1) ⭐ New (2)

Grey Divider


Action required

1. Non-FX UI updates🐞
Description
DateEditor adds a listener on viewModel.textProperty() that reads/updates JavaFX controls
(textField/datePicker) without ensuring it runs on the JavaFX Application Thread. Since the codebase
explicitly expects viewModel text updates to arrive from background threads, this can trigger
off-FX-thread UI access and runtime exceptions.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R55-60]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        });
Evidence
FieldEditorFX establishes bindings assuming view model updates may come from background threads and
marshals UI updates onto the FX thread; DateEditor’s additional listener bypasses that and calls
syncPickerWithText→datePicker.setTemporalAccessorValue directly.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor` registers a `viewModel.textProperty().addListener(...)` that touches JavaFX UI controls. If `viewModel.textProperty()` is updated from a non-JavaFX thread (which the codebase already anticipates), the listener will execute off the FX thread and can throw JavaFX thread confinement exceptions.
### Issue Context
`FieldEditorFX.establishBinding` explicitly guards against view model text updates coming from background threads by switching UI writes to the JavaFX thread. The new listener in `DateEditor` should apply the same pattern.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[55-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[144-156]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/FieldEditorFX.java[62-69]
### Suggested fix
Wrap the listener body in an FX-thread check and dispatch via `UiTaskExecutor.runInJavaFXThread(...)` (or `Platform.runLater(...)`) before calling `textField.isFocused()`, `syncPickerWithText(...)`, or any other UI-mutating method.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Deferred picker sync churn 🐞
Description
DateEditor schedules picker synchronization via UiTaskExecutor.runInJavaFXThread(Runnable), which
always queues with Platform.runLater even when already on the FX thread, creating avoidable
event-queue churn and making sync timing depend on later UI state.
This can increase UI work during frequent programmatic updates (e.g., save actions / entry switches)
and makes the picker update less deterministic than necessary.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R56-61]

+        viewModel.textProperty().addListener((observable, oldValue, newValue) -> UiTaskExecutor.runInJavaFXThread(() -> {
+            if (!textField.isFocused()) {
+                lastAcceptedText = normalizeText(newValue);
+                syncPickerWithText(lastAcceptedText);
+            }
+        }));
Evidence
DateEditor uses UiTaskExecutor.runInJavaFXThread(Runnable) for every viewModel text change before
checking focus and syncing the picker. That overload unconditionally queues work with
Platform.runLater, even if already on the FX thread, so this listener always defers execution and
can accumulate queued tasks during bursts of updates.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[53-61]
jabgui/src/main/java/org/jabref/gui/util/UiTaskExecutor.java[90-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`DateEditor` currently uses `UiTaskExecutor.runInJavaFXThread(Runnable)` for picker synchronization. That overload always uses `Platform.runLater`, even when already on the JavaFX thread, causing unnecessary deferral and extra queued UI work.

### Issue Context
The code is already performing a focus check and only syncing the picker when the user is not actively editing. Syncing immediately when already on the FX thread reduces churn and makes ordering more predictable.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[56-61]

### Suggested change
Replace the unconditional `UiTaskExecutor.runInJavaFXThread(() -> { ... })` call with:
- `if (Platform.isFxApplicationThread()) { ... } else { Platform.runLater(() -> { ... }); }`

This preserves thread-safety for background-triggered updates while avoiding needless queuing on the FX thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Uses isPresent() + get()📘
Description
The new commitTextFieldValue() unwraps Optional via isPresent() followed by get(), which the
checklist discourages in favor of ifPresent/ifPresentOrElse for clearer, safer Optional
handling. This introduces a non-compliant Optional usage pattern in newly added code.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R102-105]

+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
Evidence
PR Compliance ID 6 requires using functional Optional APIs and explicitly lists isPresent() +
manual branching as a failure pattern when ifPresentOrElse fits. The added code checks
pickerCompatibleDate.isPresent() and then calls pickerCompatibleDate.get() to extract the value.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commitTextFieldValue()` unwraps an `Optional` using `isPresent()` followed by `get()`, which violates the project's Optional-handling guideline.
## Issue Context
Refactor the Optional handling to use `Optional.ifPresent(...)` or `Optional.ifPresentOrElse(...)` so the control flow remains explicit without calling `get()`.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[102-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Invalid input cleared🐞
Description
commitTextFieldValue() clears the field to an empty string when the text is not parseable by
Date.parse, silently discarding what the user typed on Enter or focus loss. This behavior turns a
validation failure into data loss and differs from existing date-picker behavior that preserves
invalid text.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R93-116]

+    private void commitTextFieldValue() {
+        String currentText = normalizeText(textField.getText());
+
+        if (currentText.isEmpty()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        Optional<TemporalAccessor> pickerCompatibleDate = getLosslessPickerDate(currentText);
+        if (pickerCompatibleDate.isPresent()) {
+            datePicker.setTemporalAccessorValue(pickerCompatibleDate.get());
+            return;
+        }
+
+        if (Date.parse(currentText).isPresent()) {
+            syncPickerWithValue(null);
+            acceptCommittedText(currentText);
+            return;
+        }
+
+        syncPickerWithValue(null);
+        acceptCommittedText("");
+    }
Evidence
When Date.parse fails, DateEditor forces the committed text to "" which propagates via the
editor/viewmodel binding into the BibEntry field. TemporalAccessorPicker’s internal converter
explicitly preserves original text when the parsed value is null (invalid), indicating the
established component behavior is not to wipe invalid user input.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
jabgui/src/main/java/org/jabref/gui/util/component/TemporalAccessorPicker.java[139-146]
jabgui/src/main/java/org/jabref/gui/fieldeditors/AbstractEditorViewModel.java[63-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DateEditor.commitTextFieldValue()` replaces unparseable user input with an empty string. This causes silent loss of what the user typed.
### Issue Context
The editor already tracks `lastAcceptedText`, and other date UI logic (`TemporalAccessorPicker`) is designed to keep the original text when parsing fails.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[93-116]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[136-142]
### Suggested fix
In the invalid-parse path, do not call `acceptCommittedText("")`. Instead, keep the current text (so the user can correct it) **or** revert to `lastAcceptedText` (so no invalid value is persisted). If reverting, ensure you don’t overwrite `lastAcceptedText` with the invalid value during the revert.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
5. CFF emits date ranges🐞
Description
Date.getNormalized() now returns "start/end" for ranges, and CffExporter writes this string into the
date-released field whenever the start date has year/month/day. This can produce a non-single-date
value in an exporter code path that currently assumes a single ISO date string.
Code

jablib/src/main/java/org/jabref/model/entry/Date.java[R343-347]

+        String normalizedStartDate = NORMALIZED_DATE_FORMATTER.format(date);
+        if (endDate != null) {
+            return normalizedStartDate + "/" + NORMALIZED_DATE_FORMATTER.format(endDate);
+        }
+        return normalizedStartDate;
Evidence
Date.parse constructs Date instances with a non-null endDate for strings containing '/'. With the
new getNormalized behavior, any consumer that expects a single normalized start date (like
CffExporter’s date-released path) will now receive a range string containing '/'.

jablib/src/main/java/org/jabref/model/entry/Date.java[134-152]
jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-257]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Date.getNormalized()` now preserves ranges (`start/end`). `CffExporter.parseDate` uses `parsedDate.getNormalized()` as the value for `date-released` when the *start* date is complete, so ranged inputs can flow into `date-released`.
### Issue Context
`Date` does not expose `endDate`, but you can normalize only the start by constructing a new `Date` from `parsedDate.toTemporalAccessor()` (which is the start date) and calling `getNormalized()` on that.
### Fix Focus Areas
- jablib/src/main/java/org/jabref/model/entry/Date.java[342-348]
- jablib/src/main/java/org/jabref/logic/exporter/CffExporter.java[248-261]
### Suggested fix
In `CffExporter.parseDate`, when setting `date-released`, normalize only the start date (e.g., `new Date(parsedDate.toTemporalAccessor()).getNormalized()`) or explicitly detect ranges and fall back to `issue-date` instead of populating `date-released` with a range string.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

6. Redundant lastAcceptedText state 🐞
Description
DateEditor maintains lastAcceptedText as mutable state, but it is not used for any decision-making
(e.g., revert/restore) and is effectively redundant with the text value passed to
syncPickerWithText.
This increases cognitive load and suggests missing logic (e.g., rollback to last accepted value)
that is not actually implemented.
Code

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[R40-41]

+    private String lastAcceptedText = "";
+    private boolean synchronizingPicker;
Evidence
lastAcceptedText is written in multiple places (including commit), but there is no behavioral
branch that consults it; it is only used as a temporary holder passed straight into
syncPickerWithText. This makes the field misleading and easy to misinterpret as a real source of
truth for validation/rollback.

jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[40-41]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[56-60]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[82-87]
jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[136-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`lastAcceptedText` is stored on the `DateEditor` instance but does not affect behavior beyond being an intermediate variable passed to `syncPickerWithText`. This is redundant state that can mislead maintainers into thinking rollback/validation behavior exists.

### Issue Context
Current logic can be simplified by directly syncing the picker from `newValue`/`viewModel.textProperty().get()` without persisting the value, unless you intend to implement a true "revert-to-last-accepted" behavior.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[40-41]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[56-60]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[82-87]
- jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java[136-142]

### Suggested change
Either:
1) Remove `lastAcceptedText` entirely and call `syncPickerWithText(normalizeText(newValue))` / `syncPickerWithText(normalizeText(viewModel.textProperty().get()))`, and drop the assignment in `acceptCommittedText`,

or
2) If rollback is intended, implement actual usage (e.g., when commit fails to parse, restore `textField.setText(lastAcceptedText)`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@Siedlerchr

Copy link
Copy Markdown
Member

Sorry I accidentally closed the PR!

@JabRef JabRef deleted a comment from github-actions Bot Apr 14, 2026
Comment thread CHANGELOG.md Outdated
@calixtus calixtus self-assigned this Apr 15, 2026
jadegold55 and others added 2 commits April 15, 2026 10:56
Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
@jadegold55

Copy link
Copy Markdown
Contributor Author

not sure whats going on with the failing tests, mine are green locally

@subhramit subhramit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just some nits. Thanks for adding tests.

Comment thread jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java Outdated
Comment thread jabgui/src/main/java/org/jabref/gui/fieldeditors/DateEditor.java Outdated
@Override
public void bindToEntry(BibEntry entry) {
viewModel.bindToEntry(entry);
syncPickerWithText(normalizeText(viewModel.textProperty().get()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
syncPickerWithText(normalizeText(viewModel.textProperty().get()));
syncPickerWithText(viewModel.textProperty().getValueSafe());

I would suggest to remove normalizeText completely - it sounds slightly misleading as it is just ensuring null safety. The other places can use Objects.requireNonNullElse(textField.getText(), ""); so that when the code is read, one doesn't have to figure out what normalizeText does.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is still not addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

got it! I'll get to making the rest of the changes tomorrow when I'm at my computer

jadegold55 and others added 4 commits April 18, 2026 20:00
Co-authored-by: Subhramit Basu <subhramit.bb@live.in>
Co-authored-by: Subhramit Basu <subhramit.bb@live.in>
Co-authored-by: Subhramit Basu <subhramit.bb@live.in>
Co-authored-by: Subhramit Basu <subhramit.bb@live.in>
@jadegold55

Copy link
Copy Markdown
Contributor Author

Just some nits. Thanks for adding tests.

Thanks for the review!

@Siedlerchr Siedlerchr added this pull request to the merge queue Apr 19, 2026
@github-actions github-actions Bot added the status: to-be-merged PRs which are accepted and should go into the merge-queue. label Apr 19, 2026
Merged via the queue into JabRef:main with commit 7e63e8f Apr 19, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: cleanup-ops component: preferences good third issue status: no-bot-comments status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers status: to-be-merged PRs which are accepted and should go into the merge-queue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save action "Normalize date" truncates an end of a range of dates

4 participants