Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

12565 refine file exists warning #12625

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
aacc156
enhancement: Changed error box dialog to Taget file already exists #3
Oscar2343 Feb 27, 2025
981fc85
Fix message box display issue for long file names ( #4 )
tsukii7 Feb 27, 2025
1f74bfb
Merge pull request #7 from elwi7331/issue3
elwi7331 Mar 1, 2025
5892a67
Add initial "keep both" functionality #1
elwi7331 Mar 1, 2025
bd6820d
fix: make content of message box not editable #5
Mar 2, 2025
9912201
Add initial "Provide alternative file name" functionality #2
Mar 2, 2025
248a2f9
Added the buttons without functionality or tool tip #6
Oscar2343 Mar 2, 2025
6d8f9db
Merge pull request #9 from elwi7331/addButtons
Oscar2343 Mar 3, 2025
df68a80
Merge dev branch into feature branch #1
elwi7331 Mar 3, 2025
cd65802
Merge pull request #10 from elwi7331/1-allow-keep-both
elwi7331 Mar 3, 2025
d3439f5
Merge branch '12565_refine_file_exists_warning' into 2-provide-alt-fi…
Mar 3, 2025
81ce88d
Merge branch '12565_refine_file_exists_warning' into contentUnEditable
Mar 3, 2025
0423e65
fix: fix feature that does not allow dialog box to be edited #5
Mar 3, 2025
7daa845
fix: add 2 fixes for showCustomButtonDialogAndWait()
Mar 3, 2025
3b7dc6a
Merge branch '12565_refine_file_exists_warning' into 4-fix-messagebox…
tsukii7 Mar 3, 2025
2584c56
Merge pull request #11 from elwi7331/contentUnEditable
adammeh Mar 3, 2025
f990da7
Merge pull request #12 from elwi7331/4-fix-messagebox-long-filename
tsukii7 Mar 3, 2025
4dd5364
Added hover tooltip for "Keep Both" button to preview target name ( #…
tsukii7 Mar 4, 2025
cf4d319
Merge branch '12565_refine_file_exists_warning' into 2-provide-alt-fi…
Mar 4, 2025
2f79b3e
Merge pull request #14 from elwi7331/2-provide-alt-file-name
mabeysekera Mar 4, 2025
bfca193
Fix dialog window size for showing the button name ( #13 )
tsukii7 Mar 5, 2025
80e258c
Merge branch '12565_refine_file_exists_warning' into 13-add-hover-too…
tsukii7 Mar 5, 2025
de2cba2
Merge pull request #15 from elwi7331/13-add-hover-tooltip-for-keep-bo…
Oscar2343 Mar 5, 2025
f2bf83a
fix: fixed broken override functionallity and cleaned commented code
Oscar2343 Mar 5, 2025
63f6d0e
doc: Added our changes to CHANGELOG.md #8
Oscar2343 Mar 5, 2025
e1f61fb
Merge branch 'main' into 12565_refine_file_exists_warning
Oscar2343 Mar 5, 2025
ac1fa53
fix: revert changes to showCustomButtonDialogAndWait() since we are n…
Mar 5, 2025
61065cb
fix: remove unused imports
Mar 5, 2025
7befdaf
Merge pull request #16 from elwi7331/12565_refine_file_exists_warning
elwi7331 Mar 5, 2025
3025337
fix checkstyle import order
elwi7331 Mar 5, 2025
30ac3c4
fix CHANGELOG markdown bare-url
elwi7331 Mar 5, 2025
019b53d
fix en_properties
elwi7331 Mar 6, 2025
28e050c
Merge branch 'main' into 12565_refine_file_exists_warning
elwi7331 Mar 6, 2025
efcbedb
tests: add tests for performRenameWithConflictCheck()
Mar 6, 2025
fc9365a
Merge pull request #18 from elwi7331/makeTests
adammeh Mar 6, 2025
f1ff56d
Fix checkstyle issues in test
elwi7331 Mar 6, 2025
945a81f
Fix OpenRewrite import guidelines
elwi7331 Mar 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added a new "Copy citation (text)" button in the context menu of the preview. [#12551](https://github.com/JabRef/jabref/issues/12551)
- We added a new "Export to clipboard" button in the context menu of the preview. [#12551](https://github.com/JabRef/jabref/issues/12551)
- We added an integrity check if a URL appears in a title. [#12354](https://github.com/JabRef/jabref/issues/12354)
- We added "Keep both" and "Provide alternative file name" options and improved dialog when renaming a file to an existing file. [#12565](https://github.com/JabRef/jabref/issues/12565)


### Changed

Expand Down
65 changes: 58 additions & 7 deletions src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Optional;
import java.util.function.BiPredicate;

import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
Expand All @@ -18,6 +19,13 @@
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;

import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
Expand All @@ -37,6 +45,7 @@
import org.jabref.logic.externalfiles.LinkedFileHandler;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.io.FileNameUniqueness;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
Expand Down Expand Up @@ -263,18 +272,60 @@ public void renameFileToName(String targetFileName) {
}
}

private void performRenameWithConflictCheck(String targetFileName) {
public void performRenameWithConflictCheck(String targetFileName) {
Optional<Path> existingFile = linkedFileHandler.findExistingFile(linkedFile, entry, targetFileName);
boolean overwriteFile = false;

if (existingFile.isPresent()) {
overwriteFile = dialogService.showConfirmationDialogAndWait(
Localization.lang("File exists"),
Localization.lang("'%0' exists. Overwrite file?", targetFileName),
Localization.lang("Overwrite"));
Path existingFileVal = existingFile.get();
// show when hovering over "Keep both" button
String suggestedFileName = FileNameUniqueness.getNonOverWritingFileName(
existingFileVal.getParent(),
existingFileVal.getFileName().toString()
);

Label contentLabel = new Label(Localization.lang("Target file name: \n'%0'", targetFileName));
contentLabel.setWrapText(true);
DialogPane dialogPane = new DialogPane();
dialogPane.setPrefSize(700, 200);
dialogPane.setContent(contentLabel);

ButtonType overrideButton = new ButtonType("Override", ButtonBar.ButtonData.OTHER);
Copy link
Member

@Siedlerchr Siedlerchr Mar 7, 2025

Choose a reason for hiding this comment

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

Add proper Localization for these button types to all buttons as well using Localization.lang

ButtonType keepBothButton = new ButtonType("Keep both", ButtonBar.ButtonData.OTHER);
ButtonType provideAltFileNameButton = new ButtonType("Provide alternative file name", ButtonBar.ButtonData.OTHER);

dialogPane.getButtonTypes().addAll(overrideButton, keepBothButton, provideAltFileNameButton);

dialogPane.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
Platform.runLater(() -> {
Button btnKeepBoth = (Button) dialogPane.lookupButton(keepBothButton);
if (btnKeepBoth != null) {
Tooltip tooltip = new Tooltip(Localization.lang("New name: %0", suggestedFileName));
tooltip.setShowDelay(Duration.millis(100));
tooltip.setStyle("-fx-max-width: 20px; -fx-wrap-text: true; -fx-background-color: #FFFFE0;");
Copy link
Member

Choose a reason for hiding this comment

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

px values dont scale. use em
dont manually set colors. use constants from base.css

Copy link
Member

Choose a reason for hiding this comment

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

put the whole style into base.css and only set a style class for this tooltip

Tooltip.install(btnKeepBoth, tooltip);
}
});
}
});

if (!overwriteFile) {
return;
Optional<ButtonType> result = dialogService.showCustomDialogAndWait(
Localization.lang("Target file already exists"),
dialogPane,
overrideButton, keepBothButton, provideAltFileNameButton
);

if (result.isPresent()) {
ButtonType buttonType = result.get();
if (buttonType == overrideButton) {
overwriteFile = true;
} else if (buttonType == keepBothButton) {
targetFileName = suggestedFileName;
} else if (buttonType == provideAltFileNameButton) {
askForNameAndRename();
return;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/csl-styles
Copy link
Member

Choose a reason for hiding this comment

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

Undo

Submodule csl-styles updated 42 files
+2 −2 Gemfile.lock
+0 −215 acta-adriatica.csl
+17 −19 american-college-of-clinical-pharmacy.csl
+10 −27 archaeonautica.csl
+23 −56 australian-archaeology.csl
+0 −231 aves.csl
+0 −8 bluebook-law-review.csl
+0 −154 brumaire-verlag.csl
+3 −10 cardiff-university-vancouver.csl
+2 −3 economie-et-statistique.csl
+0 −184 elsevier-vancouver-author-date-alphabetical.csl
+0 −162 elsevier-vancouver-short-author-list.csl
+4 −3 eneuro.csl
+9 −28 entomological-society-of-america.csl
+1 −1 european-journal-for-philosophy-of-religion.csl
+53 −52 hochschule-munchen-fakultat-fur-angewandte-sozialwissenschaften.csl
+13 −29 instituto-brasileiro-de-informacao-em-ciencia-e-tecnologia-abnt-initials.csl
+13 −29 instituto-brasileiro-de-informacao-em-ciencia-e-tecnologia-abnt.csl
+82 −135 international-journal-of-wildland-fire.csl
+11 −6 istanbul-universitesi-sosyal-bilimler-enstitusu.csl
+2 −3 journal-of-consumer-research.csl
+0 −356 journal-of-interactive-media-in-education-harvard.csl
+11 −19 journal-of-law-medicine-ethics.csl
+32 −399 mcgill-en.csl
+84 −627 mcgill-fr.csl
+33 −3 midwestern-baptist-theological-seminary.csl
+496 −0 modern-humanities-research-association-3rd-edition.csl
+175 −311 modern-humanities-research-association-author-date.csl
+293 −354 modern-humanities-research-association.csl
+0 −254 neues-jahrbuch-fur-geologie-und-palaontologie.csl
+27 −31 nottingham-trent-university-library-harvard.csl
+0 −230 nueva-norma-estudios-de-la-humanidad.csl
+2 −3 population.csl
+0 −201 radiation-research.csl
+0 −1 renamed-styles.json
+0 −979 revue-d-anthropologie-des-connaissances.csl
+89 −93 revue-francaise-de-sociologie.csl
+75 −77 spec/filters.yaml
+0 −246 the-coleopterists-bulletin.csl
+27 −38 the-isme-journal.csl
+153 −93 the-journal-of-egyptian-archaeology.csl
+12 −18 university-of-hull-harvard.csl
4 changes: 4 additions & 0 deletions src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2868,3 +2868,7 @@ Author\ related=Author related
Editor\ related=Editor related
Title\ related=Title related
Entry\ fields=Entry fields

New\ name\:\ %0=New name: %0
Target\ file\ already\ exists=Target file already exists
Target\ file\ name\:\ \n'%0'=Target file name: \n'%0'
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package test.java.org.jabref.gui.fieldeditors;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.TreeSet;

import javafx.collections.FXCollections;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogPane;

import org.jabref.gui.DialogService;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.fieldeditors.LinkedFileViewModel;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.externalfiles.LinkedFileHandler;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.io.FileNameUniqueness;
import org.jabref.logic.xmp.XmpPreferences;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockitoAnnotations;
import org.testfx.framework.junit5.ApplicationExtension;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(ApplicationExtension.class)
public class PerformRenameWithConflictCheckTest {

@TempDir
Path tempDir; // JUnit 5 annotation for creating temporary files/folders

private LinkedFile linkedFile;
private BibEntry entry;
private BibDatabaseContext databaseContext;
private TaskExecutor taskExecutor;
private DialogService dialogService;
private LinkedFileHandler linkedFileHandler;
private LinkedFileViewModel viewModel;

private final DialogService dialogServiceMock = mock(DialogService.class);
private final LinkedFileHandler linkedFileHandlerMock = mock(LinkedFileHandler.class);

private final ExternalApplicationsPreferences externalApplicationsPreferences = mock(ExternalApplicationsPreferences.class);
private final FilePreferences filePreferences = mock(FilePreferences.class);
private final GuiPreferences preferences = mock(GuiPreferences.class);

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
Copy link
Member

Choose a reason for hiding this comment

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

if there is no mock annotation, this line has no effect.


// Mock dependencies
// linkedFile = mock(LinkedFile.class);
entry = mock(BibEntry.class);
databaseContext = mock(BibDatabaseContext.class);
taskExecutor = mock(TaskExecutor.class);
dialogService = mock(DialogService.class);
linkedFileHandler = mock(LinkedFileHandler.class);

// Mock preferences
when(externalApplicationsPreferences.getExternalFileTypes()).thenReturn(FXCollections.observableSet(new TreeSet<>(ExternalFileTypes.getDefaultExternalFileTypes())));
when(filePreferences.confirmDeleteLinkedFile()).thenReturn(true);
when(preferences.getExternalApplicationsPreferences()).thenReturn(externalApplicationsPreferences);
when(preferences.getFilePreferences()).thenReturn(filePreferences);
when(preferences.getXmpPreferences()).thenReturn(mock(XmpPreferences.class));

// Mock database context behavior (use a temporary path)
when(databaseContext.getFileDirectories(any())).thenReturn(List.of(tempDir));

// Create temporary file
Path existingFile = tempDir.resolve("existingFile.pdf");
Assertions.assertDoesNotThrow(() -> {
Files.createFile(existingFile); // Create the real file
}, "Failed to create temporary file: ");

Path fileToRename = tempDir.resolve("fileToRename.pdf");
Assertions.assertDoesNotThrow(() -> {
Files.createFile(fileToRename); // Create the real file
}, "Failed to create temporary file: ");

// Create a real LinkedFile object
LinkedFile linkedFile = new LinkedFile("desc", fileToRename, "pdf");

if (linkedFile.getLink() == null) {
throw new IllegalStateException("linkedFile.getLink() returned null");
}

viewModel = new LinkedFileViewModel(linkedFile, entry, databaseContext, taskExecutor, dialogServiceMock, preferences);
}

// Test 1: No conflict, renames the file
@Test
void performRenameWithConflictCheckWhenNoConflictRenamesFile() throws IOException {
Path newFileName = tempDir.resolve("newFileName.pdf");

// Simulate no conflict, check that the file doesn't exist before renaming
assertFalse(Files.exists(newFileName), "File should not exist before renaming");

// Call the method to rename the file
viewModel.performRenameWithConflictCheck(newFileName.getFileName().toString());

// Verify the new file has been renamed
assertTrue(Files.exists(newFileName), "File should be renamed successfully");
assertFalse(Files.exists(tempDir.resolve("fileToRename.pdf")), "Original file should be renamed");
}

// Test 2: File exists, user chooses to override
@Test
void performRenameWithConflictCheckWhenOverrideChosenOverwritesFile() throws IOException {

Path existingFilePath = tempDir.resolve("existingFile.pdf");
Path fileToRename = tempDir.resolve("fileToRename.pdf");

// Simulate user choosing "Keep both"
when(dialogServiceMock.showCustomDialogAndWait(
anyString(),
any(DialogPane.class),
any(ButtonType.class),
any(ButtonType.class),
any(ButtonType.class)))
.thenAnswer(invocation -> Optional.of(invocation.getArgument(2))); // override is argument index 2

// Call the method
viewModel.performRenameWithConflictCheck(existingFilePath.getFileName().toString());

// Verify the file is overwritten
assertTrue(Files.exists(existingFilePath), "The file should be overwritten");
}

// Test 3: File exists, user chooses "Keep both"
@Test
void performRenameWithConflictCheckWhenKeepBothChosenKeepsBothFiles() throws IOException {

Path existingFilePath = tempDir.resolve("existingFile.pdf");
Path fileToRename = tempDir.resolve("fileToRename.pdf");

// Simulate user choosing "Keep both"
when(dialogServiceMock.showCustomDialogAndWait(
anyString(),
any(DialogPane.class),
any(ButtonType.class),
any(ButtonType.class),
any(ButtonType.class)))
.thenAnswer(invocation -> Optional.of(invocation.getArgument(3))); // keepBothButton is argument index 3

// Expected new file name (generated using `getNonOverWritingFileName`)
String newFileName = FileNameUniqueness.getNonOverWritingFileName(tempDir, existingFilePath.getFileName().toString());
Path expectedNewFilePath = tempDir.resolve(newFileName);

// Call the method
viewModel.performRenameWithConflictCheck(existingFilePath.getFileName().toString());

// Verify that both files exist and the new file is renamed correctly
assertTrue(Files.exists(existingFilePath), "The existing file should still exist");
assertTrue(Files.exists(expectedNewFilePath), "The renamed file should exist with a unique name");
// Verify that the original fileToRename no longer exists
assertFalse(Files.exists(fileToRename), "The original fileToRename should not exist anymore");
}


// Test 4: File exists, user chooses to provide an alternative file name
@Test
void performRenameWithConflictCheckWhenProvideAlternativeFileNameChosenUsesUserInput() {
Path existingFilePath = tempDir.resolve("existingFile.pdf");
Path fileToRename = tempDir.resolve("fileToRename.pdf");

// Simulate the user selecting "Provide alternative file name"
when(dialogServiceMock.showCustomDialogAndWait(
anyString(),
any(DialogPane.class),
any(ButtonType.class),
any(ButtonType.class),
any(ButtonType.class)))
.thenAnswer(invocation -> Optional.of(invocation.getArgument(4))); // provideAltFileNameButton is index 4

// Simulate the user entering a new file name
String userProvidedFileName = "newFileName.pdf";
when(dialogServiceMock.showInputDialogWithDefaultAndWait(anyString(), anyString(), anyString()))
.thenReturn(Optional.of(userProvidedFileName));

// Call the method
viewModel.performRenameWithConflictCheck(existingFilePath.getFileName().toString());

// Expected new path based on user input
Path expectedNewFilePath = tempDir.resolve(userProvidedFileName);

// Check that the renamed file exists
assertTrue(Files.exists(expectedNewFilePath), "The file should be renamed to the user-provided name");

// Verify that the original fileToRename no longer exists
assertFalse(Files.exists(fileToRename), "The original file should not exist anymore");

// Ensure the conflicting file was not overwritten
assertTrue(Files.exists(existingFilePath), "The existing file should still be there");
}

@AfterEach
void tearDown() throws IOException {
// Clean up any real files after each test
Files.walk(tempDir)
.map(Path::toFile)
.forEach(file -> {
if (!file.isDirectory()) {
file.delete();
}
});
}
Comment on lines +214 to +224
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this do JUnit automatically with TempDirs?

}
Loading