This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new dfea3c0  Port the move files script to TypeScript, and fix a few bugs
dfea3c0 is described below

commit dfea3c02a3d71f1926f3baef7af613e8ec6e44b0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 19 14:55:23 2025 +0100

    Port the move files script to TypeScript, and fix a few bugs
---
 Makefile                              |  13 +-
 atr/static/js/finish-selected-move.js | 547 +++++++++++++++++++---------------
 atr/static/ts/finish-selected-move.ts | 387 ++++++++++++++++++++++++
 3 files changed, 702 insertions(+), 245 deletions(-)

diff --git a/Makefile b/Makefile
index 6dd33d9..19920cc 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
-.PHONY: build build-alpine build-playwright build-ubuntu certs check \
-  docs generate-version obvfix report run run-dev run-playwright \
+.PHONY: build build-alpine build-playwright build-ts build-ubuntu certs \
+  check docs generate-version obvfix report run run-dev run-playwright \
   run-playwright-slow run-staging stop serve serve-local sync sync-dev
 
 BIND ?= 127.0.0.1:8080
@@ -17,6 +17,15 @@ build-alpine:
 build-playwright:
        docker build -t atr-playwright -f tests/Dockerfile.playwright playwright
 
+build-ts:
+       for ts_file in atr/static/ts/*.ts; \
+       do \
+         if [ -e "$$ts_file" ]; \
+         then \
+           tsc "$$ts_file" --outDir atr/static/js --lib es2015,dom --target 
es2015 --module none; \
+         fi; \
+       done
+
 build-ubuntu:
        $(SCRIPTS)/build Dockerfile.ubuntu $(IMAGE)
 
diff --git a/atr/static/js/finish-selected-move.js 
b/atr/static/js/finish-selected-move.js
index 6983728..875604c 100644
--- a/atr/static/js/finish-selected-move.js
+++ b/atr/static/js/finish-selected-move.js
@@ -1,255 +1,316 @@
-document.addEventListener("DOMContentLoaded", function() {
-    const fileFilterInput = document.getElementById("file-filter");
-    const fileListTableBody = document.getElementById("file-list-table-body");
-
-    let originalFilePaths = [];
-    let allTargetDirs = [];
-
-    try {
-        const fileDataElement = document.getElementById("file-data");
-        if (fileDataElement) {
-            originalFilePaths = JSON.parse(fileDataElement.textContent || 
"[]");
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, 
generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function 
(resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch 
(e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } 
catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : 
adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var ItemType;
+(function (ItemType) {
+    ItemType["File"] = "file";
+    ItemType["Dir"] = "dir";
+})(ItemType || (ItemType = {}));
+const CONFIRM_MOVE_BUTTON_ID = "confirm-move-button";
+const CURRENT_MOVE_SELECTION_INFO_ID = "current-move-selection-info";
+const DIR_DATA_ID = "dir-data";
+const DIR_FILTER_INPUT_ID = "dir-filter-input";
+const DIR_LIST_MORE_INFO_ID = "dir-list-more-info";
+const DIR_LIST_TABLE_BODY_ID = "dir-list-table-body";
+const FILE_DATA_ID = "file-data";
+const FILE_FILTER_ID = "file-filter";
+const FILE_LIST_MORE_INFO_ID = "file-list-more-info";
+const FILE_LIST_TABLE_BODY_ID = "file-list-table-body";
+const MAIN_SCRIPT_DATA_ID = "main-script-data";
+const MAX_FILES_INPUT_ID = "max-files-input";
+const SELECTED_FILE_NAME_TITLE_ID = "selected-file-name-title";
+const TXT_CHOOSE = "Choose";
+const TXT_CHOSEN = "Chosen";
+const TXT_SELECT = "Select";
+const TXT_SELECTED = "Selected";
+const MAX_FILES_FALLBACK = 5;
+let fileFilterInput;
+let fileListTableBody;
+let maxFilesInput;
+let selectedFileNameTitleElement;
+let dirFilterInput;
+let dirListTableBody;
+let confirmMoveButton;
+let currentMoveSelectionInfoElement;
+let uiState;
+function getParentPath(filePathString) {
+    if (!filePathString || typeof filePathString !== "string")
+        return ".";
+    const lastSlash = filePathString.lastIndexOf("/");
+    if (lastSlash === -1)
+        return ".";
+    if (lastSlash === 0)
+        return "/";
+    return filePathString.substring(0, lastSlash);
+}
+const toLower = (s) => (s || "").toLocaleLowerCase();
+const includesCaseInsensitive = (haystack, lowerNeedle) => 
toLower(haystack).includes(lowerNeedle);
+function assertElementPresent(element, selector) {
+    if (!element) {
+        throw new Error(`Required DOM element '${selector}' not found.`);
+    }
+    return element;
+}
+function updateMoveSelectionInfo() {
+    if (selectedFileNameTitleElement) {
+        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedFilePath
+            ? `Select a destination for ${uiState.currentlySelectedFilePath}`
+            : "Select a destination for the file";
+    }
+    let infoHTML = "";
+    let disableConfirm = true;
+    if (!uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Selected destination: 
<strong>${uiState.currentlyChosenDirectoryPath}</strong>. Please select a file 
to move.`;
+    }
+    else if (uiState.currentlySelectedFilePath && 
!uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Moving 
<strong>${uiState.currentlySelectedFilePath}</strong> to (select destination).`;
+    }
+    else if (uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Move <strong>${uiState.currentlySelectedFilePath}</strong> 
to <strong>${uiState.currentlyChosenDirectoryPath}</strong>`;
+        disableConfirm = false;
+    }
+    else {
+        infoHTML = "Please select a file and a destination.";
+    }
+    currentMoveSelectionInfoElement.innerHTML = infoHTML;
+    confirmMoveButton.disabled = disableConfirm;
+}
+function renderListItems(tbodyElement, items, config) {
+    const fragment = new DocumentFragment();
+    const itemsToShow = items.slice(0, uiState.maxFilesToShow);
+    itemsToShow.forEach(item => {
+        const itemPathString = config.itemType === ItemType.Dir && !item ? "." 
: String(item || "");
+        const row = document.createElement("tr");
+        const buttonCell = row.insertCell();
+        buttonCell.className = "page-table-button-cell";
+        const pathCell = row.insertCell();
+        pathCell.className = "page-table-path-cell";
+        const button = document.createElement("button");
+        button.type = "button";
+        button.className = `btn btn-sm m-1 ${config.buttonClassBase} 
${config.buttonClassOutline}`;
+        button.dataset[config.itemType === ItemType.File ? "filePath" : 
"dirPath"] = itemPathString;
+        if (itemPathString === config.selectedItem) {
+            row.classList.add("page-item-selected");
+            row.setAttribute("aria-selected", "true");
+            button.textContent = config.buttonTextSelected;
+            button.classList.remove(config.buttonClassOutline);
+            button.classList.add(config.buttonClassActive);
         }
-        const dirDataElement = document.getElementById("dir-data");
-        if (dirDataElement) {
-            allTargetDirs = JSON.parse(dirDataElement.textContent || "[]");
+        else {
+            row.setAttribute("aria-selected", "false");
+            button.textContent = config.buttonTextDefault;
         }
-    } catch (e) {
-        console.error("Error parsing JSON data:", e);
-        originalFilePaths = [];
-        allTargetDirs = [];
-    }
-
-    let maxFilesToShow = 5;
-    const maxFilesInput = document.getElementById("max-files-input");
-    if (maxFilesInput) {
-        maxFilesToShow = parseInt(maxFilesInput.value, 10);
-        maxFilesInput.addEventListener("change", function(event) {
-            const newValue = parseInt(event.target.value, 10);
-            if (newValue >= 1) {
-                maxFilesToShow = newValue;
-                const currentFileFilter = fileFilterInput.value.toLowerCase();
-                const currentDirFilter = dirFilterInput.value.toLowerCase();
-                renderFilesTable(originalFilePaths.filter(fp => String(fp || 
"").toLowerCase().includes(currentFileFilter)));
-                renderDirsTable(allTargetDirs.filter(dirP => String(dirP || 
"").toLowerCase().includes(currentDirFilter)));
-            } else {
-                event.target.value = maxFilesToShow;
-            }
-        });
-    }
-
-    let currentlySelectedFilePath = null;
-    let currentlyChosenDirectoryPath = null;
-
-    const selectedFileNameTitleElement = 
document.getElementById("selected-file-name-title");
-    const dirFilterInput = document.getElementById("dir-filter-input");
-    const dirListTableBody = document.getElementById("dir-list-table-body");
-    const confirmMoveButton = document.getElementById("confirm-move-button");
-    const currentMoveSelectionInfoElement = 
document.getElementById("current-move-selection-info");
-
-    function getParentPath(filePathString) {
-        if (!filePathString || typeof filePathString !== "string") return ".";
-        const lastSlash = filePathString.lastIndexOf("/");
-        if (lastSlash === -1) return ".";
-        if (lastSlash === 0) return "/";
-        return filePathString.substring(0, lastSlash);
-    }
-
-    function updateMoveSelectionInfo() {
-        if (!currentMoveSelectionInfoElement) return;
-
-        if (selectedFileNameTitleElement) {
-            if (currentlySelectedFilePath) {
-                selectedFileNameTitleElement.textContent = `Select a 
destination for ${currentlySelectedFilePath}`;
-            } else {
-                selectedFileNameTitleElement.textContent = "Select a 
destination for the file";
-            }
+        if (config.disableCondition(itemPathString, 
uiState.currentlySelectedFilePath, uiState.currentlyChosenDirectoryPath, 
getParentPath)) {
+            button.disabled = true;
         }
-
-        if ((!currentlySelectedFilePath) && currentlyChosenDirectoryPath) {
-            currentMoveSelectionInfoElement.innerHTML = `Selected destination: 
<strong>${currentlyChosenDirectoryPath}</strong>. Please select a file to 
move.`;
-            confirmMoveButton.disabled = true;
-        } else if (currentlySelectedFilePath && 
(!currentlyChosenDirectoryPath)) {
-            currentMoveSelectionInfoElement.innerHTML = `Moving 
<strong>${currentlySelectedFilePath}</strong> to (select destination).`;
-            confirmMoveButton.disabled = true;
-        } else if (currentlySelectedFilePath && currentlyChosenDirectoryPath) {
-            currentMoveSelectionInfoElement.innerHTML = `Move 
<strong>${currentlySelectedFilePath}</strong> to 
<strong>${currentlyChosenDirectoryPath}</strong>`;
-            confirmMoveButton.disabled = false;
-        } else {
-            currentMoveSelectionInfoElement.textContent = "Please select a 
file and a destination.";
-            confirmMoveButton.disabled = true;
+        const span = document.createElement("span");
+        span.className = "page-file-select-text";
+        span.textContent = itemPathString;
+        buttonCell.appendChild(button);
+        pathCell.appendChild(span);
+        fragment.appendChild(row);
+    });
+    tbodyElement.replaceChildren(fragment);
+    const moreInfoElement = document.getElementById(config.moreInfoId);
+    if (moreInfoElement) {
+        if (items.length > uiState.maxFilesToShow) {
+            moreInfoElement.textContent = `${items.length - 
uiState.maxFilesToShow} more available (filter to browse)...`;
+            moreInfoElement.style.display = "block";
+        }
+        else {
+            moreInfoElement.textContent = "";
+            moreInfoElement.style.display = "none";
         }
     }
-
-    function renderList(tbodyElement, items, config) {
-        tbodyElement.innerHTML = "";
-
-        items.slice(0, maxFilesToShow).forEach(item => {
-            const row = tbodyElement.insertRow();
-            const itemPathString = config.itemType === "dir" ? String(item || 
".") : String(item);
-
-            const buttonCell = row.insertCell();
-            buttonCell.classList.add("page-table-button-cell");
-            const pathCell = row.insertCell();
-            pathCell.classList.add("page-table-path-cell");
-
-            const button = document.createElement("button");
-            button.type = "button";
-            button.className = `btn btn-sm m-1 ${config.buttonClassBase} 
${config.buttonClassOutline}`;
-            button.dataset[config.itemType === "file" ? "filePath" : 
"dirPath"] = itemPathString;
-
-            if (itemPathString === config.selectedItem) {
-                row.classList.add("page-item-selected");
-                button.textContent = config.buttonTextSelected;
-                button.classList.remove(config.buttonClassOutline);
-                button.classList.add(config.buttonClassActive);
-            } else {
-                button.textContent = config.buttonTextDefault;
-            }
-
-            if (config.disableCondition(itemPathString, 
currentlySelectedFilePath, currentlyChosenDirectoryPath, getParentPath)) {
-                button.disabled = true;
-            }
-
-            button.addEventListener("click", config.eventHandler);
-
-            const span = document.createElement("span");
-            span.className = "page-file-select-text";
-            span.textContent = itemPathString;
-
-            buttonCell.appendChild(button);
-            pathCell.appendChild(span);
-        });
-
-        const moreInfoElement = document.getElementById(config.moreInfoId);
-        if (moreInfoElement) {
-            if (items.length > maxFilesToShow) {
-                moreInfoElement.textContent = `${items.length - 
maxFilesToShow} more available (filter to browse)...`;
-                moreInfoElement.style.display = "block";
-            } else {
-                moreInfoElement.textContent = "";
-                moreInfoElement.style.display = "none";
-            }
+}
+function renderAllLists() {
+    const lowerFileFilter = toLower(uiState.fileFilter);
+    const filteredFilePaths = uiState.originalFilePaths.filter(fp => 
includesCaseInsensitive(fp, lowerFileFilter));
+    const filesConfig = {
+        itemType: ItemType.File,
+        selectedItem: uiState.currentlySelectedFilePath,
+        buttonClassBase: "select-file-btn",
+        buttonClassOutline: "btn-outline-primary",
+        buttonClassActive: "btn-primary",
+        buttonTextSelected: TXT_SELECTED,
+        buttonTextDefault: TXT_SELECT,
+        moreInfoId: FILE_LIST_MORE_INFO_ID,
+        disableCondition: (itemPath, _selectedFile, chosenDir, getParent) => 
!!chosenDir && (getParent(itemPath) === chosenDir),
+    };
+    renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
+    const lowerDirFilter = toLower(uiState.dirFilter);
+    const filteredDirs = uiState.allTargetDirs.filter(dirP => 
includesCaseInsensitive(dirP, lowerDirFilter));
+    const dirsConfig = {
+        itemType: ItemType.Dir,
+        selectedItem: uiState.currentlyChosenDirectoryPath,
+        buttonClassBase: "choose-dir-btn",
+        buttonClassOutline: "btn-outline-secondary",
+        buttonClassActive: "btn-secondary",
+        buttonTextSelected: TXT_CHOSEN,
+        buttonTextDefault: TXT_CHOOSE,
+        moreInfoId: DIR_LIST_MORE_INFO_ID,
+        disableCondition: (itemPath, selectedFile, _chosenDir, getParent) => 
!!selectedFile && (getParent(selectedFile) === itemPath),
+    };
+    renderListItems(dirListTableBody, filteredDirs, dirsConfig);
+    updateMoveSelectionInfo();
+}
+function handleFileSelection(filePath) {
+    if (uiState.currentlyChosenDirectoryPath) {
+        const parentOfNewFile = getParentPath(filePath);
+        if (parentOfNewFile === uiState.currentlyChosenDirectoryPath) {
+            uiState.currentlyChosenDirectoryPath = null;
         }
     }
-
-    function renderDirsTable(dirsToShow) {
-        const dirsConfig = {
-            itemType: "dir",
-            selectedItem: currentlyChosenDirectoryPath,
-            buttonClassBase: "choose-dir-btn",
-            buttonClassOutline: "btn-outline-secondary",
-            buttonClassActive: "btn-secondary",
-            buttonTextSelected: "Chosen",
-            buttonTextDefault: "Choose",
-            eventHandler: handleDirChooseClick,
-            moreInfoId: "dir-list-more-info",
-            disableCondition: (itemPath, selectedFile, _chosenDir, getParent) 
=> selectedFile && (getParent(selectedFile) === itemPath)
-        };
-        renderList(dirListTableBody, dirsToShow, dirsConfig);
-    }
-
-    function handleDirChooseClick(event) {
-        currentlyChosenDirectoryPath = event.target.dataset.dirPath;
-        const filterText = dirFilterInput.value.toLowerCase();
-        const filteredDirs = allTargetDirs.filter(dirP => String(dirP || 
"").toLowerCase().includes(filterText));
-        renderDirsTable(filteredDirs);
-
-        const fileFilterText = fileFilterInput.value.toLowerCase();
-        const filteredFilePaths = originalFilePaths.filter(fp => String(fp || 
"").toLowerCase().includes(fileFilterText));
-        renderFilesTable(filteredFilePaths);
-
-        updateMoveSelectionInfo();
-    }
-
-    function handleFileSelectButtonClick(event) {
-        const newlySelectedFilePath = event.target.dataset.filePath;
-
-        if (currentlyChosenDirectoryPath) {
-            const parentOfNewFile = getParentPath(newlySelectedFilePath);
-            if (parentOfNewFile === currentlyChosenDirectoryPath) {
-                currentlyChosenDirectoryPath = null;
+    uiState.currentlySelectedFilePath = filePath;
+    renderAllLists();
+    if (!confirmMoveButton.disabled) {
+        confirmMoveButton.focus();
+    }
+}
+function handleDirSelection(dirPath) {
+    uiState.currentlyChosenDirectoryPath = dirPath;
+    renderAllLists();
+    if (!confirmMoveButton.disabled) {
+        confirmMoveButton.focus();
+    }
+}
+function onFileListClick(event) {
+    const targetElement = event.target;
+    const button = targetElement.closest("button.select-file-btn");
+    if (button && !button.disabled) {
+        const filePath = button.dataset.filePath || null;
+        handleFileSelection(filePath);
+    }
+}
+function onDirListClick(event) {
+    const targetElement = event.target;
+    const button = targetElement.closest("button.choose-dir-btn");
+    if (button && !button.disabled) {
+        const dirPath = button.dataset.dirPath || null;
+        handleDirSelection(dirPath);
+    }
+}
+function onFileFilterInput(event) {
+    uiState.fileFilter = event.target.value;
+    renderAllLists();
+}
+function onDirFilterInput(event) {
+    uiState.dirFilter = event.target.value;
+    renderAllLists();
+}
+function onMaxFilesChange(event) {
+    const newValue = parseInt(event.target.value, 10);
+    if (newValue >= 1) {
+        uiState.maxFilesToShow = newValue;
+        renderAllLists();
+    }
+    else {
+        event.target.value = String(uiState.maxFilesToShow);
+    }
+}
+function onConfirmMoveClick() {
+    return __awaiter(this, void 0, void 0, function* () {
+        if (uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
+            const formData = new FormData();
+            formData.append("csrf_token", uiState.csrfToken);
+            formData.append("source_file", uiState.currentlySelectedFilePath);
+            formData.append("target_directory", 
uiState.currentlyChosenDirectoryPath);
+            try {
+                const response = yield fetch(window.location.pathname, {
+                    method: "POST",
+                    body: formData,
+                    credentials: "same-origin",
+                    headers: {
+                        "Accept": "application/json",
+                    },
+                });
+                if (response.ok) {
+                    window.location.reload();
+                }
+                else {
+                    let errorMsg = `An error occurred while moving the file 
(Status: ${response.status})`;
+                    if (response.status === 403) {
+                        errorMsg = "Permission denied to move the file.";
+                    }
+                    else if (response.status === 400) {
+                        errorMsg = "Invalid request to move the file (e.g. 
source or target invalid).";
+                    }
+                    try {
+                        const errorData = yield response.json();
+                        if (errorData && errorData.error) {
+                            errorMsg = errorData.error;
+                        }
+                    }
+                    catch (e) { }
+                    alert(errorMsg);
+                }
+            }
+            catch (error) {
+                console.error("Network or fetch error:", error);
+                alert("A network error occurred. Please check your connection 
and try again.");
             }
         }
-
-        currentlySelectedFilePath = newlySelectedFilePath;
-
-        const fileFilterText = fileFilterInput.value.toLowerCase();
-        const filteredFilePaths = originalFilePaths.filter(fp => String(fp || 
"").toLowerCase().includes(fileFilterText));
-        renderFilesTable(filteredFilePaths);
-
-        const dirFilterText = dirFilterInput.value.toLowerCase();
-        const filteredDirs = allTargetDirs.filter(dirP => String(dirP || 
"").toLowerCase().includes(dirFilterText));
-        renderDirsTable(filteredDirs);
-
-        updateMoveSelectionInfo();
-    }
-
-    function renderFilesTable(pathsToShow) {
-        const filesConfig = {
-            itemType: "file",
-            selectedItem: currentlySelectedFilePath,
-            buttonClassBase: "select-file-btn",
-            buttonClassOutline: "btn-outline-primary",
-            buttonClassActive: "btn-primary",
-            buttonTextSelected: "Selected",
-            buttonTextDefault: "Select",
-            eventHandler: handleFileSelectButtonClick,
-            moreInfoId: "file-list-more-info",
-            disableCondition: (itemPath, _selectedFile, chosenDir, getParent) 
=> chosenDir && (getParent(itemPath) === chosenDir)
-        };
-        renderList(fileListTableBody, pathsToShow, filesConfig);
-    }
-
-    if (dirFilterInput) {
-        dirFilterInput.addEventListener("input", function() {
-            const filterText = dirFilterInput.value.toLowerCase();
-            const filteredDirs = allTargetDirs.filter(dirPath => {
-                return String(dirPath || 
"").toLowerCase().includes(filterText);
-            });
-            renderDirsTable(filteredDirs);
-        });
-    }
-
-    fileFilterInput.addEventListener("input", function() {
-        const filterText = fileFilterInput.value.toLowerCase();
-        const filteredPaths = originalFilePaths.filter(filePath => {
-            return String(filePath || "").toLowerCase().includes(filterText);
-        });
-        renderFilesTable(filteredPaths);
+        else {
+            alert("Please select both a file to move and a destination 
directory.");
+        }
     });
-
-    renderFilesTable(originalFilePaths);
-    renderDirsTable(allTargetDirs);
-
-    if (confirmMoveButton) {
-        confirmMoveButton.addEventListener("click", function() {
-            if (currentlySelectedFilePath && currentlyChosenDirectoryPath) {
-                const formData = new FormData();
-                const mainScriptElement = 
document.getElementById("main-script-data");
-                const csrfToken = mainScriptElement.dataset.csrfToken;
-                formData.append("csrf_token", csrfToken);
-                formData.append("source_file", currentlySelectedFilePath);
-                formData.append("target_directory", 
currentlyChosenDirectoryPath);
-                fetch(window.location.pathname, {
-                        method: "POST",
-                        body: formData,
-                    })
-                    .then(response => {
-                        if (response.ok) {
-                            window.location.reload();
-                        } else {
-                            alert("An error occurred while moving the file.");
-                        }
-                    })
-                    .catch(() => {
-                        alert("A network error occurred.");
-                    });
-            } else {
-                alert("Please select both a file to move and a destination 
directory.");
-            }
-        });
+}
+document.addEventListener("DOMContentLoaded", function () {
+    fileFilterInput = 
assertElementPresent(document.querySelector(`#${FILE_FILTER_ID}`), 
FILE_FILTER_ID);
+    fileListTableBody = 
assertElementPresent(document.querySelector(`#${FILE_LIST_TABLE_BODY_ID}`), 
FILE_LIST_TABLE_BODY_ID);
+    maxFilesInput = 
assertElementPresent(document.querySelector(`#${MAX_FILES_INPUT_ID}`), 
MAX_FILES_INPUT_ID);
+    selectedFileNameTitleElement = 
assertElementPresent(document.getElementById(SELECTED_FILE_NAME_TITLE_ID), 
SELECTED_FILE_NAME_TITLE_ID);
+    dirFilterInput = 
assertElementPresent(document.querySelector(`#${DIR_FILTER_INPUT_ID}`), 
DIR_FILTER_INPUT_ID);
+    dirListTableBody = 
assertElementPresent(document.querySelector(`#${DIR_LIST_TABLE_BODY_ID}`), 
DIR_LIST_TABLE_BODY_ID);
+    confirmMoveButton = 
assertElementPresent(document.querySelector(`#${CONFIRM_MOVE_BUTTON_ID}`), 
CONFIRM_MOVE_BUTTON_ID);
+    currentMoveSelectionInfoElement = 
assertElementPresent(document.getElementById(CURRENT_MOVE_SELECTION_INFO_ID), 
CURRENT_MOVE_SELECTION_INFO_ID);
+    currentMoveSelectionInfoElement.setAttribute("aria-live", "polite");
+    let initialFilePaths = [];
+    let initialTargetDirs = [];
+    try {
+        const fileDataElement = document.getElementById(FILE_DATA_ID);
+        if (fileDataElement === null || fileDataElement === void 0 ? void 0 : 
fileDataElement.textContent) {
+            initialFilePaths = JSON.parse(fileDataElement.textContent);
+        }
+        const dirDataElement = document.getElementById(DIR_DATA_ID);
+        if (dirDataElement === null || dirDataElement === void 0 ? void 0 : 
dirDataElement.textContent) {
+            initialTargetDirs = JSON.parse(dirDataElement.textContent);
+        }
     }
-
-    updateMoveSelectionInfo();
+    catch (e) {
+        console.error("Error parsing JSON data:", e);
+    }
+    if (initialFilePaths.length === 0 && initialTargetDirs.length === 0) {
+        alert("Warning: File and/or directory lists could not be loaded or are 
empty.");
+    }
+    const mainScriptDataElement = 
document.querySelector(`#${MAIN_SCRIPT_DATA_ID}`);
+    const initialCsrfToken = (mainScriptDataElement === null || 
mainScriptDataElement === void 0 ? void 0 : 
mainScriptDataElement.dataset.csrfToken) || null;
+    uiState = {
+        fileFilter: fileFilterInput.value || "",
+        dirFilter: dirFilterInput.value || "",
+        maxFilesToShow: parseInt(maxFilesInput.value, 10) || 
MAX_FILES_FALLBACK,
+        currentlySelectedFilePath: null,
+        currentlyChosenDirectoryPath: null,
+        originalFilePaths: initialFilePaths,
+        allTargetDirs: initialTargetDirs,
+        csrfToken: initialCsrfToken,
+    };
+    if (isNaN(uiState.maxFilesToShow) || uiState.maxFilesToShow < 1) {
+        uiState.maxFilesToShow = MAX_FILES_FALLBACK;
+        maxFilesInput.value = String(uiState.maxFilesToShow);
+    }
+    fileFilterInput.addEventListener("input", onFileFilterInput);
+    dirFilterInput.addEventListener("input", onDirFilterInput);
+    maxFilesInput.addEventListener("change", onMaxFilesChange);
+    fileListTableBody.addEventListener("click", onFileListClick);
+    dirListTableBody.addEventListener("click", onDirListClick);
+    confirmMoveButton.addEventListener("click", onConfirmMoveClick);
+    renderAllLists();
 });
diff --git a/atr/static/ts/finish-selected-move.ts 
b/atr/static/ts/finish-selected-move.ts
new file mode 100644
index 0000000..2b27c94
--- /dev/null
+++ b/atr/static/ts/finish-selected-move.ts
@@ -0,0 +1,387 @@
+"use strict";
+
+enum ItemType {
+    File = "file",
+    Dir = "dir",
+}
+
+const CONFIRM_MOVE_BUTTON_ID = "confirm-move-button";
+const CURRENT_MOVE_SELECTION_INFO_ID = "current-move-selection-info";
+const DIR_DATA_ID = "dir-data";
+const DIR_FILTER_INPUT_ID = "dir-filter-input";
+const DIR_LIST_MORE_INFO_ID = "dir-list-more-info";
+const DIR_LIST_TABLE_BODY_ID = "dir-list-table-body";
+const FILE_DATA_ID = "file-data";
+const FILE_FILTER_ID = "file-filter";
+const FILE_LIST_MORE_INFO_ID = "file-list-more-info";
+const FILE_LIST_TABLE_BODY_ID = "file-list-table-body";
+const MAIN_SCRIPT_DATA_ID = "main-script-data";
+const MAX_FILES_INPUT_ID = "max-files-input";
+const SELECTED_FILE_NAME_TITLE_ID = "selected-file-name-title";
+
+const TXT_CHOOSE = "Choose";
+const TXT_CHOSEN = "Chosen";
+const TXT_SELECT = "Select";
+const TXT_SELECTED = "Selected";
+
+const MAX_FILES_FALLBACK = 5;
+
+interface ButtonDataset extends DOMStringMap {
+    filePath?: string;
+    dirPath?: string;
+}
+
+type ButtonClickEvent = MouseEvent & {
+    currentTarget: HTMLButtonElement & { dataset: ButtonDataset };
+};
+
+type FilterInputEvent = Event & {
+    target: HTMLInputElement;
+};
+
+interface UIState {
+    fileFilter: string;
+    dirFilter: string;
+    maxFilesToShow: number;
+    currentlySelectedFilePath: string | null;
+    currentlyChosenDirectoryPath: string | null;
+    originalFilePaths: string[];
+    allTargetDirs: string[];
+    csrfToken: string | null;
+}
+
+interface RenderListDisplayConfig {
+    itemType: ItemType;
+    selectedItem: string | null;
+    buttonClassBase: string;
+    buttonClassOutline: string;
+    buttonClassActive: string;
+    buttonTextSelected: string;
+    buttonTextDefault: string;
+    moreInfoId: string;
+    disableCondition: (
+        itemPath: string,
+        selectedFile: string | null,
+        chosenDir: string | null,
+        getParent: (filePath: string) => string
+    ) => boolean;
+}
+
+let fileFilterInput: HTMLInputElement;
+let fileListTableBody: HTMLTableSectionElement;
+let maxFilesInput: HTMLInputElement;
+let selectedFileNameTitleElement: HTMLElement;
+let dirFilterInput: HTMLInputElement;
+let dirListTableBody: HTMLTableSectionElement;
+let confirmMoveButton: HTMLButtonElement;
+let currentMoveSelectionInfoElement: HTMLElement;
+
+let uiState: UIState;
+
+function getParentPath(filePathString: string | null | undefined): string {
+    if (!filePathString || typeof filePathString !== "string") return ".";
+    const lastSlash = filePathString.lastIndexOf("/");
+    if (lastSlash === -1) return ".";
+    if (lastSlash === 0) return "/";
+    return filePathString.substring(0, lastSlash);
+}
+
+const toLower = (s: string | null | undefined): string => (s || 
"").toLocaleLowerCase();
+
+const includesCaseInsensitive = (haystack: string | null | undefined, 
lowerNeedle: string): boolean =>
+    toLower(haystack).includes(lowerNeedle);
+
+function assertElementPresent<T extends HTMLElement>(element: T | null, 
selector: string): T {
+    if (!element) {
+        throw new Error(`Required DOM element '${selector}' not found.`);
+    }
+    return element;
+}
+
+function updateMoveSelectionInfo(): void {
+    if (selectedFileNameTitleElement) {
+        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedFilePath
+            ? `Select a destination for ${uiState.currentlySelectedFilePath}`
+            : "Select a destination for the file";
+    }
+
+    let infoHTML = "";
+    let disableConfirm = true;
+
+    if (!uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Selected destination: 
<strong>${uiState.currentlyChosenDirectoryPath}</strong>. Please select a file 
to move.`;
+    } else if (uiState.currentlySelectedFilePath && 
!uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Moving 
<strong>${uiState.currentlySelectedFilePath}</strong> to (select destination).`;
+    } else if (uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath) {
+        infoHTML = `Move <strong>${uiState.currentlySelectedFilePath}</strong> 
to <strong>${uiState.currentlyChosenDirectoryPath}</strong>`;
+        disableConfirm = false;
+    } else {
+        infoHTML = "Please select a file and a destination.";
+    }
+
+    currentMoveSelectionInfoElement.innerHTML = infoHTML;
+    confirmMoveButton.disabled = disableConfirm;
+}
+
+function renderListItems(
+    tbodyElement: HTMLTableSectionElement,
+    items: string[],
+    config: RenderListDisplayConfig
+): void {
+    const fragment = new DocumentFragment();
+    const itemsToShow = items.slice(0, uiState.maxFilesToShow);
+
+    itemsToShow.forEach(item => {
+        const itemPathString = config.itemType === ItemType.Dir && !item ? "." 
: String(item || "");
+        const row = document.createElement("tr");
+
+        const buttonCell = row.insertCell();
+        buttonCell.className = "page-table-button-cell";
+        const pathCell = row.insertCell();
+        pathCell.className = "page-table-path-cell";
+
+        const button = document.createElement("button") as HTMLButtonElement & 
{ dataset: ButtonDataset };
+        button.type = "button";
+        button.className = `btn btn-sm m-1 ${config.buttonClassBase} 
${config.buttonClassOutline}`;
+        button.dataset[config.itemType === ItemType.File ? "filePath" : 
"dirPath"] = itemPathString;
+
+        if (itemPathString === config.selectedItem) {
+            row.classList.add("page-item-selected");
+            row.setAttribute("aria-selected", "true");
+            button.textContent = config.buttonTextSelected;
+            button.classList.remove(config.buttonClassOutline);
+            button.classList.add(config.buttonClassActive);
+        } else {
+            row.setAttribute("aria-selected", "false");
+            button.textContent = config.buttonTextDefault;
+        }
+
+        if (config.disableCondition(itemPathString, 
uiState.currentlySelectedFilePath, uiState.currentlyChosenDirectoryPath, 
getParentPath)) {
+            button.disabled = true;
+        }
+
+        const span = document.createElement("span");
+        span.className = "page-file-select-text";
+        span.textContent = itemPathString;
+
+        buttonCell.appendChild(button);
+        pathCell.appendChild(span);
+        fragment.appendChild(row);
+    });
+
+    tbodyElement.replaceChildren(fragment);
+
+    const moreInfoElement = document.getElementById(config.moreInfoId) as 
HTMLElement | null;
+    if (moreInfoElement) {
+        if (items.length > uiState.maxFilesToShow) {
+            moreInfoElement.textContent = `${items.length - 
uiState.maxFilesToShow} more available (filter to browse)...`;
+            moreInfoElement.style.display = "block";
+        } else {
+            moreInfoElement.textContent = "";
+            moreInfoElement.style.display = "none";
+        }
+    }
+}
+
+function renderAllLists(): void {
+    const lowerFileFilter = toLower(uiState.fileFilter);
+    const filteredFilePaths = uiState.originalFilePaths.filter(fp =>
+        includesCaseInsensitive(fp, lowerFileFilter)
+    );
+    const filesConfig: RenderListDisplayConfig = {
+        itemType: ItemType.File,
+        selectedItem: uiState.currentlySelectedFilePath,
+        buttonClassBase: "select-file-btn",
+        buttonClassOutline: "btn-outline-primary",
+        buttonClassActive: "btn-primary",
+        buttonTextSelected: TXT_SELECTED,
+        buttonTextDefault: TXT_SELECT,
+        moreInfoId: FILE_LIST_MORE_INFO_ID,
+        disableCondition: (itemPath, _selectedFile, chosenDir, getParent) =>
+            !!chosenDir && (getParent(itemPath) === chosenDir),
+    };
+    renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
+
+    const lowerDirFilter = toLower(uiState.dirFilter);
+    const filteredDirs = uiState.allTargetDirs.filter(dirP =>
+        includesCaseInsensitive(dirP, lowerDirFilter)
+    );
+    const dirsConfig: RenderListDisplayConfig = {
+        itemType: ItemType.Dir,
+        selectedItem: uiState.currentlyChosenDirectoryPath,
+        buttonClassBase: "choose-dir-btn",
+        buttonClassOutline: "btn-outline-secondary",
+        buttonClassActive: "btn-secondary",
+        buttonTextSelected: TXT_CHOSEN,
+        buttonTextDefault: TXT_CHOOSE,
+        moreInfoId: DIR_LIST_MORE_INFO_ID,
+        disableCondition: (itemPath, selectedFile, _chosenDir, getParent) =>
+            !!selectedFile && (getParent(selectedFile) === itemPath),
+    };
+    renderListItems(dirListTableBody, filteredDirs, dirsConfig);
+
+    updateMoveSelectionInfo();
+}
+
+function handleFileSelection(filePath: string | null): void {
+    if (uiState.currentlyChosenDirectoryPath) {
+        const parentOfNewFile = getParentPath(filePath);
+        if (parentOfNewFile === uiState.currentlyChosenDirectoryPath) {
+            uiState.currentlyChosenDirectoryPath = null;
+        }
+    }
+    uiState.currentlySelectedFilePath = filePath;
+    renderAllLists();
+    if (!confirmMoveButton.disabled) {
+        confirmMoveButton.focus();
+    }
+}
+
+function handleDirSelection(dirPath: string | null): void {
+    uiState.currentlyChosenDirectoryPath = dirPath;
+    renderAllLists();
+    if (!confirmMoveButton.disabled) {
+        confirmMoveButton.focus();
+    }
+}
+
+function onFileListClick(event: Event): void {
+    const targetElement = event.target as HTMLElement;
+    const button = 
targetElement.closest<HTMLButtonElement>("button.select-file-btn");
+    if (button && !button.disabled) {
+        const filePath = button.dataset.filePath || null;
+        handleFileSelection(filePath);
+    }
+}
+
+function onDirListClick(event: Event): void {
+    const targetElement = event.target as HTMLElement;
+    const button = 
targetElement.closest<HTMLButtonElement>("button.choose-dir-btn");
+    if (button && !button.disabled) {
+        const dirPath = button.dataset.dirPath || null;
+        handleDirSelection(dirPath);
+    }
+}
+
+function onFileFilterInput(event: FilterInputEvent): void {
+    uiState.fileFilter = event.target.value;
+    renderAllLists();
+}
+
+function onDirFilterInput(event: FilterInputEvent): void {
+    uiState.dirFilter = event.target.value;
+    renderAllLists();
+}
+
+function onMaxFilesChange(event: FilterInputEvent): void {
+    const newValue = parseInt(event.target.value, 10);
+    if (newValue >= 1) {
+        uiState.maxFilesToShow = newValue;
+        renderAllLists();
+    } else {
+        event.target.value = String(uiState.maxFilesToShow);
+    }
+}
+
+async function onConfirmMoveClick(): Promise<void> {
+    if (uiState.currentlySelectedFilePath && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
+        const formData = new FormData();
+        formData.append("csrf_token", uiState.csrfToken);
+        formData.append("source_file", uiState.currentlySelectedFilePath);
+        formData.append("target_directory", 
uiState.currentlyChosenDirectoryPath);
+
+        try {
+            const response = await fetch(window.location.pathname, {
+                method: "POST",
+                body: formData,
+                credentials: "same-origin",
+                headers: {
+                    "Accept": "application/json",
+                },
+            });
+
+            if (response.ok) {
+                window.location.reload();
+            } else {
+                let errorMsg = `An error occurred while moving the file 
(Status: ${response.status})`;
+                if (response.status === 403) {
+                    errorMsg = "Permission denied to move the file.";
+                } else if (response.status === 400) {
+                    errorMsg = "Invalid request to move the file (e.g. source 
or target invalid).";
+                }
+                try {
+                    const errorData = await response.json();
+                    if (errorData && errorData.error) {
+                         errorMsg = errorData.error;
+                    }
+                } catch (e) {  }
+                alert(errorMsg);
+            }
+        } catch (error) {
+            console.error("Network or fetch error:", error);
+            alert("A network error occurred. Please check your connection and 
try again.");
+        }
+    } else {
+        alert("Please select both a file to move and a destination 
directory.");
+    }
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+    fileFilterInput = 
assertElementPresent(document.querySelector<HTMLInputElement>(`#${FILE_FILTER_ID}`),
 FILE_FILTER_ID);
+    fileListTableBody = 
assertElementPresent(document.querySelector<HTMLTableSectionElement>(`#${FILE_LIST_TABLE_BODY_ID}`),
 FILE_LIST_TABLE_BODY_ID);
+    maxFilesInput = 
assertElementPresent(document.querySelector<HTMLInputElement>(`#${MAX_FILES_INPUT_ID}`),
 MAX_FILES_INPUT_ID);
+    selectedFileNameTitleElement = 
assertElementPresent(document.getElementById(SELECTED_FILE_NAME_TITLE_ID) as 
HTMLElement, SELECTED_FILE_NAME_TITLE_ID);
+    dirFilterInput = 
assertElementPresent(document.querySelector<HTMLInputElement>(`#${DIR_FILTER_INPUT_ID}`),
 DIR_FILTER_INPUT_ID);
+    dirListTableBody = 
assertElementPresent(document.querySelector<HTMLTableSectionElement>(`#${DIR_LIST_TABLE_BODY_ID}`),
 DIR_LIST_TABLE_BODY_ID);
+    confirmMoveButton = 
assertElementPresent(document.querySelector<HTMLButtonElement>(`#${CONFIRM_MOVE_BUTTON_ID}`),
 CONFIRM_MOVE_BUTTON_ID);
+    currentMoveSelectionInfoElement = 
assertElementPresent(document.getElementById(CURRENT_MOVE_SELECTION_INFO_ID) as 
HTMLElement, CURRENT_MOVE_SELECTION_INFO_ID);
+    currentMoveSelectionInfoElement.setAttribute("aria-live", "polite");
+
+    let initialFilePaths: string[] = [];
+    let initialTargetDirs: string[] = [];
+    try {
+        const fileDataElement = document.getElementById(FILE_DATA_ID);
+        if (fileDataElement?.textContent) {
+            initialFilePaths = JSON.parse(fileDataElement.textContent);
+        }
+        const dirDataElement = document.getElementById(DIR_DATA_ID);
+        if (dirDataElement?.textContent) {
+            initialTargetDirs = JSON.parse(dirDataElement.textContent);
+        }
+    } catch (e) {
+        console.error("Error parsing JSON data:", e);
+    }
+
+    if (initialFilePaths.length === 0 && initialTargetDirs.length === 0) {
+        alert("Warning: File and/or directory lists could not be loaded or are 
empty.");
+    }
+
+    const mainScriptDataElement = document.querySelector<HTMLElement & { 
dataset: { csrfToken?: string } }>(`#${MAIN_SCRIPT_DATA_ID}`);
+    const initialCsrfToken = mainScriptDataElement?.dataset.csrfToken || null;
+
+    uiState = {
+        fileFilter: fileFilterInput.value || "",
+        dirFilter: dirFilterInput.value || "",
+        maxFilesToShow: parseInt(maxFilesInput.value, 10) || 
MAX_FILES_FALLBACK,
+        currentlySelectedFilePath: null,
+        currentlyChosenDirectoryPath: null,
+        originalFilePaths: initialFilePaths,
+        allTargetDirs: initialTargetDirs,
+        csrfToken: initialCsrfToken,
+    };
+    if (isNaN(uiState.maxFilesToShow) || uiState.maxFilesToShow < 1) {
+        uiState.maxFilesToShow = MAX_FILES_FALLBACK;
+        maxFilesInput.value = String(uiState.maxFilesToShow);
+    }
+
+    fileFilterInput.addEventListener("input", onFileFilterInput as 
EventListener);
+    dirFilterInput.addEventListener("input", onDirFilterInput as 
EventListener);
+    maxFilesInput.addEventListener("change", onMaxFilesChange as 
EventListener);
+
+    fileListTableBody.addEventListener("click", onFileListClick);
+    dirListTableBody.addEventListener("click", onDirListClick);
+
+    confirmMoveButton.addEventListener("click", onConfirmMoveClick);
+
+    renderAllLists();
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to