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]