feat(sort): enhance file sorting and order handling (#4813)

# Description of Changes

- Added async support to sorting functions for improved UI
responsiveness in merge.js.
- Enhanced logging for debugging file orders in both JavaScript and
Java.
- Improved file order validation and handling in the backend, ensuring
consistent sorting and upload order.
- Refactored file order processing with better trimming, handling for
empty entries, and logging unmatched filenames.

Closes: #4810


<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-11-07 14:35:18 +01:00 committed by GitHub
parent 2acb3aa6e5
commit abd1ae1bf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 18 deletions

View File

@ -72,19 +72,29 @@ public class MergeController {
// fileOrder is newline-delimited original filenames in the desired order. // fileOrder is newline-delimited original filenames in the desired order.
private static MultipartFile[] reorderFilesByProvidedOrder( private static MultipartFile[] reorderFilesByProvidedOrder(
MultipartFile[] files, String fileOrder) { MultipartFile[] files, String fileOrder) {
String[] desired = fileOrder.split("\n", -1); // Split by various line endings and trim each entry
String[] desired =
stirling.software.common.util.RegexPatternUtils.getInstance()
.getNewlineSplitPattern()
.split(fileOrder);
List<MultipartFile> remaining = new ArrayList<>(Arrays.asList(files)); List<MultipartFile> remaining = new ArrayList<>(Arrays.asList(files));
List<MultipartFile> ordered = new ArrayList<>(files.length); List<MultipartFile> ordered = new ArrayList<>(files.length);
for (String name : desired) { for (String name : desired) {
if (name == null || name.isEmpty()) continue; name = name.trim();
if (name.isEmpty()) {
log.debug("Skipping empty entry");
continue;
}
int idx = indexOfByOriginalFilename(remaining, name); int idx = indexOfByOriginalFilename(remaining, name);
if (idx >= 0) { if (idx >= 0) {
ordered.add(remaining.remove(idx)); ordered.add(remaining.remove(idx));
} else {
log.debug("Filename from order list not found in uploaded files: {}", name);
} }
} }
// Append any files not explicitly listed, preserving their relative order
ordered.addAll(remaining); ordered.addAll(remaining);
return ordered.toArray(new MultipartFile[0]); return ordered.toArray(new MultipartFile[0]);
} }
@ -252,8 +262,10 @@ public class MergeController {
// If front-end provided explicit visible order, honor it and override backend sorting // If front-end provided explicit visible order, honor it and override backend sorting
if (fileOrder != null && !fileOrder.isBlank()) { if (fileOrder != null && !fileOrder.isBlank()) {
log.info("Reordering files based on fileOrder parameter");
files = reorderFilesByProvidedOrder(files, fileOrder); files = reorderFilesByProvidedOrder(files, fileOrder);
} else { } else {
log.info("Sorting files based on sortType: {}", request.getSortType());
Arrays.sort( Arrays.sort(
files, files,
getSortComparator( getSortComparator(

View File

@ -74,6 +74,12 @@
showGameBtn.style.display = 'none'; showGameBtn.style.display = 'none';
} }
// Log fileOrder for debugging
const fileOrderValue = formData.get('fileOrder');
if (fileOrderValue) {
console.log('FormData fileOrder:', fileOrderValue);
}
// Remove empty file entries // Remove empty file entries
for (let [key, value] of formData.entries()) { for (let [key, value] of formData.entries()) {
if (value instanceof File && !value.name) { if (value instanceof File && !value.name) {

View File

@ -123,39 +123,38 @@ function attachMoveButtons() {
} }
} }
document.getElementById("sortByNameBtn").addEventListener("click", function () { document.getElementById("sortByNameBtn").addEventListener("click", async function () {
if (currentSort.field === "name" && !currentSort.descending) { if (currentSort.field === "name" && !currentSort.descending) {
currentSort.descending = true; currentSort.descending = true;
sortFiles((a, b) => b.name.localeCompare(a.name)); await sortFiles((a, b) => b.name.localeCompare(a.name));
} else { } else {
currentSort.field = "name"; currentSort.field = "name";
currentSort.descending = false; currentSort.descending = false;
sortFiles((a, b) => a.name.localeCompare(b.name)); await sortFiles((a, b) => a.name.localeCompare(b.name));
} }
}); });
document.getElementById("sortByDateBtn").addEventListener("click", function () { document.getElementById("sortByDateBtn").addEventListener("click", async function () {
if (currentSort.field === "lastModified" && !currentSort.descending) { if (currentSort.field === "lastModified" && !currentSort.descending) {
currentSort.descending = true; currentSort.descending = true;
sortFiles((a, b) => b.lastModified - a.lastModified); await sortFiles((a, b) => b.lastModified - a.lastModified);
} else { } else {
currentSort.field = "lastModified"; currentSort.field = "lastModified";
currentSort.descending = false; currentSort.descending = false;
sortFiles((a, b) => a.lastModified - b.lastModified); await sortFiles((a, b) => a.lastModified - b.lastModified);
} }
}); });
function sortFiles(comparator) { async function sortFiles(comparator) {
// Convert FileList to array and sort // Convert FileList to array and sort
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator); const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
// Refresh displayed list // Refresh displayed list (wait for it to complete since it's async)
displayFiles(sortedFilesArray); await displayFiles(sortedFilesArray);
// Update the files property // Update the file input and fileOrder based on the current display order
const dataTransfer = new DataTransfer(); // This ensures consistency between display and file input
sortedFilesArray.forEach((file) => dataTransfer.items.add(file)); updateFiles();
document.getElementById("fileInput-input").files = dataTransfer.files;
} }
function updateFiles() { function updateFiles() {
@ -163,25 +162,36 @@ function updateFiles() {
var liElements = document.querySelectorAll("#selectedFiles li"); var liElements = document.querySelectorAll("#selectedFiles li");
const files = document.getElementById("fileInput-input").files; const files = document.getElementById("fileInput-input").files;
console.log("updateFiles: found", liElements.length, "LI elements and", files.length, "files");
for (var i = 0; i < liElements.length; i++) { for (var i = 0; i < liElements.length; i++) {
var fileNameFromList = liElements[i].querySelector(".filename").innerText; var fileNameFromList = liElements[i].querySelector(".filename").innerText;
var fileFromFiles; var found = false;
for (var j = 0; j < files.length; j++) { for (var j = 0; j < files.length; j++) {
var file = files[j]; var file = files[j];
if (file.name === fileNameFromList) { if (file.name === fileNameFromList) {
dataTransfer.items.add(file); dataTransfer.items.add(file);
found = true;
break; break;
} }
} }
if (!found) {
console.warn("updateFiles: Could not find file:", fileNameFromList);
}
} }
document.getElementById("fileInput-input").files = dataTransfer.files; document.getElementById("fileInput-input").files = dataTransfer.files;
console.log("updateFiles: Updated file input with", dataTransfer.files.length, "files");
// Also populate hidden fileOrder to preserve visible order // Also populate hidden fileOrder to preserve visible order
const order = Array.from(liElements) const order = Array.from(liElements)
.map((li) => li.querySelector(".filename").innerText) .map((li) => li.querySelector(".filename").innerText)
.join("\n"); .join("\n");
const orderInput = document.getElementById("fileOrder"); const orderInput = document.getElementById("fileOrder");
if (orderInput) orderInput.value = order; if (orderInput) {
orderInput.value = order;
console.log("Updated fileOrder:", order);
}
} }
document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{ document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{