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 7010f69  Allow directories to be moved too in the movement form
7010f69 is described below

commit 7010f69996f330955ef842811caa6a0f311703e0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed May 28 14:50:15 2025 +0100

    Allow directories to be moved too in the movement form
---
 atr/revision.py                           |  16 +++--
 atr/routes/finish.py                      |  76 +++++++++++++---------
 atr/ssh.py                                |   4 +-
 atr/static/js/finish-selected-move.js     |  92 +++++++++++++++-----------
 atr/static/js/finish-selected-move.js.map |   2 +-
 atr/static/ts/finish-selected-move.ts     | 104 ++++++++++++++++++------------
 atr/templates/finish-selected.html        |   8 +--
 7 files changed, 178 insertions(+), 124 deletions(-)

diff --git a/atr/revision.py b/atr/revision.py
index deac150..5c0e08f 100644
--- a/atr/revision.py
+++ b/atr/revision.py
@@ -33,12 +33,16 @@ import atr.tasks as tasks
 import atr.util as util
 
 
+class FailedError(Exception):
+    pass
+
+
 @dataclasses.dataclass
 class Creating:
     old: models.Revision | None
     interim_path: pathlib.Path
     new: models.Revision | None
-    failed: bool = False
+    failed: FailedError | None = None
 
 
 # NOTE: The create_directory parameter is not used anymore
@@ -64,6 +68,7 @@ async def create_and_manage(
     # Use the tmp subdirectory of state, to ensure that it is on the same 
filesystem
     temp_dir: str = await asyncio.to_thread(tempfile.mkdtemp, 
dir=util.get_tmp_dir())
     temp_dir_path = pathlib.Path(temp_dir)
+    creating = Creating(old=old_revision, interim_path=temp_dir_path, 
new=None, failed=None)
     try:
         # The directory was created by mkdtemp, but it's empty
         if old_revision is not None:
@@ -71,16 +76,15 @@ async def create_and_manage(
             old_release_dir = util.release_directory(release)
             await util.create_hard_link_clone(old_release_dir, temp_dir_path, 
do_not_create_dest_dir=True)
         # The directory is either empty or its files are hard linked to the 
previous revision
-        creating = Creating(old=old_revision, interim_path=temp_dir_path, 
new=None, failed=False)
         yield creating
+    except FailedError as e:
+        await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
+        creating.failed = e
+        return
     except Exception:
         await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
         raise
 
-    if creating.failed:
-        await aioshutil.rmtree(temp_dir)  # type: ignore[call-arg]
-        return
-
     # Create a revision row, holding the write lock
     async with db.session() as data:
         # This is the only place where models.Revision is constructed
diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 7629b25..5c9455e 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -122,6 +122,7 @@ async def selected(
         can_move=can_move,
         user_ssh_keys=user_ssh_keys,
         target_dirs=sorted(list(target_dirs)),
+        max_files_to_show=10,
     )
 
 
@@ -245,14 +246,14 @@ async def _move_file_to_revision(
                 skipped_files_names,
             )
 
-        if creating.failed:
+        if creating.failed is not None:
             return await _respond(
                 session,
                 project_name,
                 version_name,
                 wants_json,
                 False,
-                "Directory names must not start with '.' or be '..'.",
+                str(creating.failed),
                 409,
             )
 
@@ -289,9 +290,7 @@ async def _move_file_to_revision(
         return await _respond(session, project_name, version_name, wants_json, 
False, f"Error moving file: {e}", 500)
     except Exception as e:
         _LOGGER.exception("Unexpected error during file move")
-        return await _respond(
-            session, project_name, version_name, wants_json, False, f"An 
unexpected error occurred: {e!s}", 500
-        )
+        return await _respond(session, project_name, version_name, wants_json, 
False, f"ERROR: {e!s}", 500)
 
 
 def _related_files(path: pathlib.Path) -> list[pathlib.Path]:
@@ -334,31 +333,30 @@ async def _setup_revision(
         target_path.resolve().relative_to(creating.interim_path.resolve())
     except ValueError:
         # Path traversal detected
-        creating.failed = True
-        return
+        raise revision.FailedError("Paths must be restricted to the release 
directory")
 
     if not await aiofiles.os.path.exists(target_path):
         for part in target_path.parts:
-            if (part == "..") or part.startswith("."):
-                creating.failed = True
-                return
+            # TODO: This .prefix check could include some existing directory 
segment
+            if part.startswith("."):
+                raise revision.FailedError("Segments must not start with '.'")
+            if ".." in part:
+                raise revision.FailedError("Segments must not contain '..'")
 
         try:
             await aiofiles.os.makedirs(target_path)
         except OSError:
-            creating.failed = True
-            return
+            raise revision.FailedError("Failed to create target directory")
     elif not await aiofiles.os.path.isdir(target_path):
-        creating.failed = True
-        return
+        raise revision.FailedError("Target path is not a directory")
 
     for source_file_rel in source_files_rel:
-        await _setup_revision_file(
+        await _setup_revision_item(
             source_file_rel, target_dir_rel, creating, moved_files_names, 
skipped_files_names, target_path
         )
 
 
-async def _setup_revision_file(
+async def _setup_revision_item(
     source_file_rel: pathlib.Path,
     target_dir_rel: pathlib.Path,
     creating: revision.Creating,
@@ -370,25 +368,45 @@ async def _setup_revision_file(
         skipped_files_names.append(source_file_rel.name)
         return
 
-    related_files = _related_files(source_file_rel)
-    bundle = [f for f in related_files if await 
aiofiles.os.path.exists(creating.interim_path / f)]
-    collisions = [f.name for f in bundle if await 
aiofiles.os.path.exists(target_path / f.name)]
-    if collisions:
-        creating.failed = True
-        return
+    full_source_item_path = creating.interim_path / source_file_rel
 
-    for f in bundle:
-        await aiofiles.os.rename(creating.interim_path / f, target_path / 
f.name)
-        if f == source_file_rel:
-            moved_files_names.append(f.name)
+    if await aiofiles.os.path.isdir(full_source_item_path):
+        if (target_dir_rel == source_file_rel) or (creating.interim_path / 
target_dir_rel).resolve().is_relative_to(
+            full_source_item_path.resolve()
+        ):
+            raise revision.FailedError("Cannot move a directory into itself or 
a subdirectory of itself")
+
+        final_target_for_item = target_path / source_file_rel.name
+        if await aiofiles.os.path.exists(final_target_for_item):
+            raise revision.FailedError("Target name already exists")
+
+        await aiofiles.os.rename(full_source_item_path, final_target_for_item)
+        moved_files_names.append(source_file_rel.name)
+    else:
+        related_files = _related_files(source_file_rel)
+        bundle = [f for f in related_files if await 
aiofiles.os.path.exists(creating.interim_path / f)]
+        for f_check in bundle:
+            if await aiofiles.os.path.isdir(creating.interim_path / f_check):
+                raise revision.FailedError("A related 'file' is actually a 
directory")
+
+        collisions = [f.name for f in bundle if await 
aiofiles.os.path.exists(target_path / f.name)]
+        if collisions:
+            raise revision.FailedError("A related file already exists in the 
target directory")
+
+        for f in bundle:
+            await aiofiles.os.rename(creating.interim_path / f, target_path / 
f.name)
+            if f == source_file_rel:
+                moved_files_names.append(f.name)
 
 
 async def _sources_and_targets(latest_revision_dir: pathlib.Path) -> 
tuple[list[pathlib.Path], set[pathlib.Path]]:
-    source_files_rel: list[pathlib.Path] = []
+    source_items_rel: list[pathlib.Path] = []
     target_dirs: set[pathlib.Path] = {pathlib.Path(".")}
 
     async for item_rel_path in util.paths_recursive_all(latest_revision_dir):
         current_parent = item_rel_path.parent
+        source_items_rel.append(item_rel_path)
+
         while True:
             target_dirs.add(current_parent)
             if current_parent == pathlib.Path("."):
@@ -397,8 +415,8 @@ async def _sources_and_targets(latest_revision_dir: 
pathlib.Path) -> tuple[list[
 
         item_abs_path = latest_revision_dir / item_rel_path
         if await aiofiles.os.path.isfile(item_abs_path):
-            source_files_rel.append(item_rel_path)
+            pass
         elif await aiofiles.os.path.isdir(item_abs_path):
             target_dirs.add(item_rel_path)
 
-    return source_files_rel, target_dirs
+    return source_items_rel, target_dirs
diff --git a/atr/ssh.py b/atr/ssh.py
index 8824c72..9bb3fe5 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -505,7 +505,7 @@ async def _step_07b_process_validated_rsync_write(
                     f"rsync upload failed with exit status {exit_status} for 
{for_revision}. "
                     f"Command: {process.command} (run as {' '.join(argv)})"
                 )
-                creating.failed = True
+                raise revision.FailedError(f"rsync upload failed with exit 
status {exit_status} for {for_revision}")
         if creating.new is not None:
             _LOGGER.info(f"rsync upload successful for revision 
{creating.new.number}")
             host = config.get().APP_HOST
@@ -515,7 +515,7 @@ async def _step_07b_process_validated_rsync_write(
                 process.stderr.write(message.encode())
                 await process.stderr.drain()
         else:
-            _LOGGER.info(f"rsync upload successful for release 
{project_name}-{version_name}")
+            _LOGGER.info(f"rsync upload unsuccessful for release 
{project_name}-{version_name}")
         if not process.is_closing():
             process.exit(exit_status)
 
diff --git a/atr/static/js/finish-selected-move.js 
b/atr/static/js/finish-selected-move.js
index 8ccf4c1..122bedb 100644
--- a/atr/static/js/finish-selected-move.js
+++ b/atr/static/js/finish-selected-move.js
@@ -36,7 +36,7 @@ const TXT = Object.freeze({
     Selected: "Selected",
     MoreItemsHint: "more available (filter to browse)..."
 });
-const MAX_FILES_FALLBACK = 5;
+const MAX_FILES_FALLBACK = 10;
 let fileFilterInput;
 let fileListTableBody;
 let maxFilesInput;
@@ -79,39 +79,39 @@ function $(id) {
 }
 function updateMoveSelectionInfo() {
     if (selectedFileNameTitleElement) {
-        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedFilePaths.size > 0
-            ? `Select a destination for 
${uiState.currentlySelectedFilePaths.size} file(s)`
-            : "Select a destination for the file";
+        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedPaths.size > 0
+            ? `Select a destination for ${uiState.currentlySelectedPaths.size} 
item(s)`
+            : "Select a destination for the item";
     }
-    const numSelectedFiles = uiState.currentlySelectedFilePaths.size;
+    const numSelectedItems = uiState.currentlySelectedPaths.size;
     const destinationDir = uiState.currentlyChosenDirectoryPath;
-    let message = "Please select files and a destination.";
+    let message = "Please select items and a destination.";
     let disableConfirmButton = true;
     currentMoveSelectionInfoElement.innerHTML = '';
-    if (!numSelectedFiles && destinationDir) {
+    if (!numSelectedItems && destinationDir) {
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Selected 
destination: "));
         const strongDest = document.createElement("strong");
-        strongDest.textContent = destinationDir;
+        strongDest.textContent = (destinationDir && destinationDir !== "." && 
!destinationDir.endsWith("/")) ? destinationDir + "/" : destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
-        currentMoveSelectionInfoElement.appendChild(document.createTextNode(". 
Please select file(s) to move."));
+        currentMoveSelectionInfoElement.appendChild(document.createTextNode(". 
Please select item(s) to move."));
     }
-    else if (numSelectedFiles && !destinationDir) {
+    else if (numSelectedItems && !destinationDir) {
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Moving "));
         const strongN = document.createElement("strong");
-        strongN.textContent = `${numSelectedFiles} file(s)`;
+        strongN.textContent = `${numSelectedItems} item(s)`;
         currentMoveSelectionInfoElement.appendChild(strongN);
         currentMoveSelectionInfoElement.appendChild(document.createTextNode(" 
to (select destination)."));
     }
-    else if (numSelectedFiles && destinationDir) {
-        const filesArray = Array.from(uiState.currentlySelectedFilePaths);
-        const displayFiles = filesArray.length > 1 ? `${filesArray[0]} and 
${filesArray.length - 1} other(s)` : filesArray[0];
+    else if (numSelectedItems && destinationDir) {
+        const itemsArray = Array.from(uiState.currentlySelectedPaths);
+        const displayItems = itemsArray.length > 1 ? `${itemsArray[0]} and 
${itemsArray.length - 1} other(s)` : itemsArray[0];
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Move "));
-        const strongDisplayFiles = document.createElement("strong");
-        strongDisplayFiles.textContent = displayFiles;
-        currentMoveSelectionInfoElement.appendChild(strongDisplayFiles);
+        const strongDisplayItems = document.createElement("strong");
+        strongDisplayItems.textContent = displayItems;
+        currentMoveSelectionInfoElement.appendChild(strongDisplayItems);
         currentMoveSelectionInfoElement.appendChild(document.createTextNode(" 
to "));
         const strongDest = document.createElement("strong");
-        strongDest.textContent = destinationDir;
+        strongDest.textContent = (destinationDir && destinationDir !== "." && 
!destinationDir.endsWith("/")) ? destinationDir + "/" : destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
         if (destinationDir && uiState.allTargetDirs.indexOf(destinationDir) 
=== -1 && isValidNewDirName(destinationDir)) {
             const newDirSpan = document.createElement("span");
@@ -146,13 +146,22 @@ function renderListItems(tbodyElement, items, config) {
                 const checkbox = document.createElement("input");
                 checkbox.type = "checkbox";
                 checkbox.className = "form-check-input ms-2";
-                checkbox.dataset.filePath = itemPathString;
-                checkbox.checked = 
uiState.currentlySelectedFilePaths.has(itemPathString);
-                checkbox.setAttribute("aria-label", `Select file 
${itemPathString}`);
+                checkbox.dataset.itemPath = itemPathString;
+                checkbox.checked = 
uiState.currentlySelectedPaths.has(itemPathString);
+                checkbox.setAttribute("aria-label", `Select item 
${itemPathString}`);
+                const isKnownSourceDir = 
uiState.allTargetDirs.indexOf(itemPathString) !== -1;
+                if (isKnownSourceDir && itemPathString !== "." && 
!itemPathString.endsWith("/")) {
+                    span.textContent = itemPathString + "/";
+                }
                 if (uiState.currentlyChosenDirectoryPath && 
getParentPath(itemPathString) === uiState.currentlyChosenDirectoryPath) {
                     checkbox.disabled = true;
                     span.classList.add("page-extra-muted");
                 }
+                if (isKnownSourceDir && uiState.currentlyChosenDirectoryPath &&
+                    (uiState.currentlyChosenDirectoryPath === itemPathString 
|| uiState.currentlyChosenDirectoryPath.startsWith(itemPathString + "/"))) {
+                    checkbox.disabled = true;
+                    span.classList.add("page-extra-muted");
+                }
                 controlCell.appendChild(checkbox);
                 break;
             }
@@ -165,6 +174,11 @@ function renderListItems(tbodyElement, items, config) {
                 radio.dataset.dirPath = itemPathString;
                 radio.checked = itemPathString === config.selectedItem;
                 radio.setAttribute("aria-label", `Choose directory 
${itemPathString}`);
+                let displayDirPath = itemPathString;
+                if (itemPathString !== "." && !itemPathString.endsWith("/")) {
+                    displayDirPath = itemPathString + "/";
+                }
+                span.textContent = displayDirPath;
                 if (itemPathString === config.selectedItem) {
                     row.classList.add("page-item-selected");
                     row.setAttribute("aria-selected", "true");
@@ -200,13 +214,13 @@ function renderListItems(tbodyElement, items, config) {
     }
 }
 function renderAllLists() {
-    const filteredFilePaths = uiState.originalFilePaths.filter(fp => 
includesCaseInsensitive(fp, uiState.filters.file));
-    const filesConfig = {
+    const filteredSourcePaths = uiState.originalSourcePaths.filter(fp => 
includesCaseInsensitive(fp, uiState.filters.file));
+    const itemsConfig = {
         itemType: ItemType.File,
         selectedItem: null,
         moreInfoId: ID.fileListMoreInfo
     };
-    renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
+    renderListItems(fileListTableBody, filteredSourcePaths, itemsConfig);
     const displayDirs = [...uiState.allTargetDirs];
     const trimmedDirFilter = uiState.filters.dir.trim();
     if (isValidNewDirName(trimmedDirFilter) && 
uiState.allTargetDirs.indexOf(trimmedDirFilter) === -1) {
@@ -237,14 +251,14 @@ function delegate(parent, selector, handler) {
         }
     });
 }
-function handleFileCheckbox(checkbox) {
-    const filePath = checkbox.dataset.filePath;
-    if (filePath) {
+function handleItemCheckbox(checkbox) {
+    const itemPath = checkbox.dataset.itemPath;
+    if (itemPath) {
         if (checkbox.checked) {
-            uiState.currentlySelectedFilePaths.add(filePath);
+            uiState.currentlySelectedPaths.add(itemPath);
         }
         else {
-            uiState.currentlySelectedFilePaths.delete(filePath);
+            uiState.currentlySelectedPaths.delete(itemPath);
         }
         renderAllLists();
     }
@@ -359,16 +373,16 @@ function onConfirmMoveClick() {
         errorAlert.textContent = "";
         const controller = new AbortController();
         window.addEventListener("beforeunload", () => controller.abort());
-        if (uiState.currentlySelectedFilePaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
-            const { toMove, alreadyThere: filesAlreadyInDest } = 
splitMoveCandidates(uiState.currentlySelectedFilePaths, 
uiState.currentlyChosenDirectoryPath);
-            if (toMove.length === 0 && filesAlreadyInDest.length > 0 && 
uiState.currentlySelectedFilePaths.size > 0) {
+        if (uiState.currentlySelectedPaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
+            const { toMove, alreadyThere: itemsAlreadyInDest } = 
splitMoveCandidates(uiState.currentlySelectedPaths, 
uiState.currentlyChosenDirectoryPath);
+            if (toMove.length === 0 && itemsAlreadyInDest.length > 0 && 
uiState.currentlySelectedPaths.size > 0) {
                 errorAlert.classList.remove("d-none");
-                errorAlert.textContent = `All selected files 
(${filesAlreadyInDest.join(", ")}) are already in the target directory. No 
files were moved.`;
+                errorAlert.textContent = `All selected items 
(${itemsAlreadyInDest.join(", ")}) are already in the target directory. No 
items were moved.`;
                 confirmMoveButton.disabled = false;
                 return;
             }
-            if (filesAlreadyInDest.length > 0) {
-                const alreadyInDestMsg = `Note: ${filesAlreadyInDest.join(", 
")} ${filesAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
+            if (itemsAlreadyInDest.length > 0) {
+                const alreadyInDestMsg = `Note: ${itemsAlreadyInDest.join(", 
")} ${itemsAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
                 const existingError = errorAlert.textContent;
                 errorAlert.textContent = existingError ? `${existingError} 
${alreadyInDestMsg}` : alreadyInDestMsg;
             }
@@ -383,7 +397,7 @@ function onConfirmMoveClick() {
         }
         else {
             errorAlert.classList.remove("d-none");
-            errorAlert.textContent = "Please select file(s) and a destination 
directory.";
+            errorAlert.textContent = "Please select item(s) and a destination 
directory.";
         }
     });
 }
@@ -420,9 +434,9 @@ document.addEventListener("DOMContentLoaded", () => {
             dir: dirFilterInput.value || "",
         },
         maxFilesToShow: Math.max(parseInt(maxFilesInput.value, 10) || 0, 1) || 
MAX_FILES_FALLBACK,
-        currentlySelectedFilePaths: new Set(),
+        currentlySelectedPaths: new Set(),
         currentlyChosenDirectoryPath: null,
-        originalFilePaths: initialFilePaths,
+        originalSourcePaths: initialFilePaths,
         allTargetDirs: initialTargetDirs,
         csrfToken,
     };
@@ -430,7 +444,7 @@ document.addEventListener("DOMContentLoaded", () => {
     fileFilterInput.addEventListener("input", onFileFilterInput);
     dirFilterInput.addEventListener("input", onDirFilterInput);
     maxFilesInput.addEventListener("change", onMaxFilesChange);
-    delegate(fileListTableBody, "input[type='checkbox'][data-file-path]", 
handleFileCheckbox);
+    delegate(fileListTableBody, "input[type='checkbox'][data-item-path]", 
handleItemCheckbox);
     delegate(dirListTableBody, 
"input[type='radio'][name='target-directory-radio']", handleDirRadio);
     confirmMoveButton.addEventListener("click", () => {
         onConfirmMoveClick().catch(_err => {
diff --git a/atr/static/js/finish-selected-move.js.map 
b/atr/static/js/finish-selected-move.js.map
index 8f6d5e9..2b2e98c 100644
--- a/atr/static/js/finish-selected-move.js.map
+++ b/atr/static/js/finish-selected-move.js.map
@@ -1 +1 @@
-{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,
 [...]
+{"version":3,"file":"finish-selected-move.js","sourceRoot":"","sources":["../ts/finish-selected-move.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;;;;;;;;;AAEb,IAAK,QAGJ;AAHD,WAAK,QAAQ;IACT,yBAAa,CAAA;IACb,uBAAW,CAAA;AACf,CAAC,EAHI,QAAQ,KAAR,QAAQ,QAGZ;AAED,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACrB,iBAAiB,EAAE,qBAAqB;IACxC,wBAAwB,EAAE,6BAA6B;IACvD,OAAO,EAAE,UAAU;IACnB,cAAc,EAAE,kBAAkB;IAClC,eAAe,EAAE,oBAAoB;IACrC,gBAAgB,EAAE,qBAAqB;IACvC,UAAU,EAAE,kBAAkB;IAC9B,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,
 [...]
diff --git a/atr/static/ts/finish-selected-move.ts 
b/atr/static/ts/finish-selected-move.ts
index 5d81587..4722952 100644
--- a/atr/static/ts/finish-selected-move.ts
+++ b/atr/static/ts/finish-selected-move.ts
@@ -30,7 +30,7 @@ const TXT = Object.freeze({
     MoreItemsHint: "more available (filter to browse)..."
 } as const);
 
-const MAX_FILES_FALLBACK = 5;
+const MAX_FILES_FALLBACK = 10;
 
 interface UIState {
     filters: {
@@ -38,9 +38,9 @@ interface UIState {
         dir: string;
     };
     maxFilesToShow: number;
-    currentlySelectedFilePaths: Set<string>;
+    currentlySelectedPaths: Set<string>;
     currentlyChosenDirectoryPath: string | null;
-    readonly originalFilePaths: readonly string[];
+    readonly originalSourcePaths: readonly string[];
     readonly allTargetDirs: readonly string[];
     csrfToken: string | null;
 }
@@ -95,40 +95,40 @@ function $<T extends HTMLElement = HTMLElement>(id: 
string): T {
 
 function updateMoveSelectionInfo(): void {
     if (selectedFileNameTitleElement) {
-        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedFilePaths.size > 0
-            ? `Select a destination for 
${uiState.currentlySelectedFilePaths.size} file(s)`
-            : "Select a destination for the file";
+        selectedFileNameTitleElement.textContent = 
uiState.currentlySelectedPaths.size > 0
+            ? `Select a destination for ${uiState.currentlySelectedPaths.size} 
item(s)`
+            : "Select a destination for the item";
     }
 
-    const numSelectedFiles = uiState.currentlySelectedFilePaths.size;
+    const numSelectedItems = uiState.currentlySelectedPaths.size;
     const destinationDir = uiState.currentlyChosenDirectoryPath;
-    let message = "Please select files and a destination.";
+    let message = "Please select items and a destination.";
     let disableConfirmButton = true;
 
     currentMoveSelectionInfoElement.innerHTML = '';
 
-    if (!numSelectedFiles && destinationDir) {
+    if (!numSelectedItems && destinationDir) {
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Selected 
destination: "));
         const strongDest = document.createElement("strong");
-        strongDest.textContent = destinationDir;
+        strongDest.textContent = (destinationDir && destinationDir !== "." && 
!destinationDir.endsWith("/")) ? destinationDir + "/" : destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
-        currentMoveSelectionInfoElement.appendChild(document.createTextNode(". 
Please select file(s) to move."));
-    } else if (numSelectedFiles && !destinationDir) {
+        currentMoveSelectionInfoElement.appendChild(document.createTextNode(". 
Please select item(s) to move."));
+    } else if (numSelectedItems && !destinationDir) {
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Moving "));
         const strongN = document.createElement("strong");
-        strongN.textContent = `${numSelectedFiles} file(s)`;
+        strongN.textContent = `${numSelectedItems} item(s)`;
         currentMoveSelectionInfoElement.appendChild(strongN);
         currentMoveSelectionInfoElement.appendChild(document.createTextNode(" 
to (select destination)."));
-    } else if (numSelectedFiles && destinationDir) {
-        const filesArray = Array.from(uiState.currentlySelectedFilePaths);
-        const displayFiles = filesArray.length > 1 ? `${filesArray[0]} and 
${filesArray.length -1} other(s)` : filesArray[0];
+    } else if (numSelectedItems && destinationDir) {
+        const itemsArray = Array.from(uiState.currentlySelectedPaths);
+        const displayItems = itemsArray.length > 1 ? `${itemsArray[0]} and 
${itemsArray.length -1} other(s)` : itemsArray[0];
         
currentMoveSelectionInfoElement.appendChild(document.createTextNode("Move "));
-        const strongDisplayFiles = document.createElement("strong");
-        strongDisplayFiles.textContent = displayFiles;
-        currentMoveSelectionInfoElement.appendChild(strongDisplayFiles);
+        const strongDisplayItems = document.createElement("strong");
+        strongDisplayItems.textContent = displayItems;
+        currentMoveSelectionInfoElement.appendChild(strongDisplayItems);
         currentMoveSelectionInfoElement.appendChild(document.createTextNode(" 
to "));
         const strongDest = document.createElement("strong");
-        strongDest.textContent = destinationDir;
+        strongDest.textContent = (destinationDir && destinationDir !== "." && 
!destinationDir.endsWith("/")) ? destinationDir + "/" : destinationDir;
         currentMoveSelectionInfoElement.appendChild(strongDest);
         if (destinationDir && uiState.allTargetDirs.indexOf(destinationDir) 
=== -1 && isValidNewDirName(destinationDir)) {
             const newDirSpan = document.createElement("span");
@@ -174,13 +174,25 @@ function renderListItems(
                 const checkbox = document.createElement("input");
                 checkbox.type = "checkbox";
                 checkbox.className = "form-check-input ms-2";
-                checkbox.dataset.filePath = itemPathString;
-                checkbox.checked = 
uiState.currentlySelectedFilePaths.has(itemPathString);
-                checkbox.setAttribute("aria-label", `Select file 
${itemPathString}`);
+                checkbox.dataset.itemPath = itemPathString;
+                checkbox.checked = 
uiState.currentlySelectedPaths.has(itemPathString);
+                checkbox.setAttribute("aria-label", `Select item 
${itemPathString}`);
+
+                const isKnownSourceDir = 
uiState.allTargetDirs.indexOf(itemPathString) !== -1;
+
+                if (isKnownSourceDir && itemPathString !== "." && 
!itemPathString.endsWith("/")) {
+                    span.textContent = itemPathString + "/";
+                }
+
                 if (uiState.currentlyChosenDirectoryPath && 
getParentPath(itemPathString) === uiState.currentlyChosenDirectoryPath) {
                     checkbox.disabled = true;
                     span.classList.add("page-extra-muted");
                 }
+                if (isKnownSourceDir && uiState.currentlyChosenDirectoryPath &&
+                    (uiState.currentlyChosenDirectoryPath === itemPathString 
|| uiState.currentlyChosenDirectoryPath.startsWith(itemPathString + "/"))) {
+                    checkbox.disabled = true;
+                    span.classList.add("page-extra-muted");
+                }
                 controlCell.appendChild(checkbox);
                 break;
             }
@@ -194,6 +206,12 @@ function renderListItems(
                 radio.checked = itemPathString === config.selectedItem;
                 radio.setAttribute("aria-label", `Choose directory 
${itemPathString}`);
 
+                let displayDirPath = itemPathString;
+                if (itemPathString !== "." && !itemPathString.endsWith("/")) {
+                    displayDirPath = itemPathString + "/";
+                }
+                span.textContent = displayDirPath;
+
                 if (itemPathString === config.selectedItem) {
                     row.classList.add("page-item-selected");
                     row.setAttribute("aria-selected", "true");
@@ -231,15 +249,15 @@ function renderListItems(
 }
 
 function renderAllLists(): void {
-    const filteredFilePaths = uiState.originalFilePaths.filter(fp =>
+    const filteredSourcePaths = uiState.originalSourcePaths.filter(fp =>
         includesCaseInsensitive(fp, uiState.filters.file)
     );
-    const filesConfig: RenderListDisplayConfig = {
+    const itemsConfig: RenderListDisplayConfig = {
         itemType: ItemType.File,
         selectedItem: null,
         moreInfoId: ID.fileListMoreInfo
     };
-    renderListItems(fileListTableBody, filteredFilePaths, filesConfig);
+    renderListItems(fileListTableBody, filteredSourcePaths, itemsConfig);
 
     const displayDirs = [...uiState.allTargetDirs];
     const trimmedDirFilter = uiState.filters.dir.trim();
@@ -282,13 +300,13 @@ function delegate<T extends HTMLElement>(
   });
 }
 
-function handleFileCheckbox(checkbox: HTMLInputElement): void {
-    const filePath = checkbox.dataset.filePath;
-    if (filePath) {
+function handleItemCheckbox(checkbox: HTMLInputElement): void {
+    const itemPath = checkbox.dataset.itemPath;
+    if (itemPath) {
         if (checkbox.checked) {
-            uiState.currentlySelectedFilePaths.add(filePath);
+            uiState.currentlySelectedPaths.add(itemPath);
         } else {
-            uiState.currentlySelectedFilePaths.delete(filePath);
+            uiState.currentlySelectedPaths.delete(itemPath);
         }
         renderAllLists();
     }
@@ -418,21 +436,21 @@ async function onConfirmMoveClick(): Promise<void> {
     const controller = new AbortController();
     window.addEventListener("beforeunload", () => controller.abort());
 
-    if (uiState.currentlySelectedFilePaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
-        const { toMove, alreadyThere: filesAlreadyInDest } = 
splitMoveCandidates(
-            uiState.currentlySelectedFilePaths,
+    if (uiState.currentlySelectedPaths.size > 0 && 
uiState.currentlyChosenDirectoryPath && uiState.csrfToken) {
+        const { toMove, alreadyThere: itemsAlreadyInDest } = 
splitMoveCandidates(
+            uiState.currentlySelectedPaths,
             uiState.currentlyChosenDirectoryPath
         );
 
-        if (toMove.length === 0 && filesAlreadyInDest.length > 0 && 
uiState.currentlySelectedFilePaths.size > 0) {
+        if (toMove.length === 0 && itemsAlreadyInDest.length > 0 && 
uiState.currentlySelectedPaths.size > 0) {
             errorAlert.classList.remove("d-none");
-            errorAlert.textContent = `All selected files 
(${filesAlreadyInDest.join(", ")}) are already in the target directory. No 
files were moved.`;
+            errorAlert.textContent = `All selected items 
(${itemsAlreadyInDest.join(", ")}) are already in the target directory. No 
items were moved.`;
             confirmMoveButton.disabled = false;
             return;
         }
 
-        if (filesAlreadyInDest.length > 0) {
-            const alreadyInDestMsg = `Note: ${filesAlreadyInDest.join(", ")} 
${filesAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
+        if (itemsAlreadyInDest.length > 0) {
+            const alreadyInDestMsg = `Note: ${itemsAlreadyInDest.join(", ")} 
${itemsAlreadyInDest.length === 1 ? "is" : "are"} already in the target 
directory and will not be moved.`;
             const existingError = errorAlert.textContent;
             errorAlert.textContent = existingError ? `${existingError} 
${alreadyInDestMsg}` : alreadyInDestMsg;
         }
@@ -447,7 +465,7 @@ async function onConfirmMoveClick(): Promise<void> {
         }
     } else {
         errorAlert.classList.remove("d-none");
-        errorAlert.textContent = "Please select file(s) and a destination 
directory.";
+        errorAlert.textContent = "Please select item(s) and a destination 
directory.";
     }
 }
 
@@ -487,9 +505,9 @@ document.addEventListener("DOMContentLoaded", () => {
     },
     maxFilesToShow:
       Math.max(parseInt(maxFilesInput.value, 10) || 0, 1) || 
MAX_FILES_FALLBACK,
-    currentlySelectedFilePaths: new Set(),
+    currentlySelectedPaths: new Set(),
     currentlyChosenDirectoryPath: null,
-    originalFilePaths: initialFilePaths,
+    originalSourcePaths: initialFilePaths,
     allTargetDirs: initialTargetDirs,
     csrfToken,
   };
@@ -501,8 +519,8 @@ document.addEventListener("DOMContentLoaded", () => {
 
   delegate<HTMLInputElement>(
     fileListTableBody,
-    "input[type='checkbox'][data-file-path]",
-    handleFileCheckbox,
+    "input[type='checkbox'][data-item-path]",
+    handleItemCheckbox,
   );
   delegate<HTMLInputElement>(
     dirListTableBody,
diff --git a/atr/templates/finish-selected.html 
b/atr/templates/finish-selected.html
index 5a56731..5d4335b 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -108,7 +108,7 @@
   </div>
 
   {% if can_move %}
-    <h2>Move files to a different directory</h2>
+    <h2>Move items to a different directory</h2>
     <p>
       Note that files with associated metadata (e.g. <code>.asc</code> or 
<code>.sha512</code> files) are treated as a single unit and will be moved 
together if any one of them is selected for movement.
     </p>
@@ -121,13 +121,13 @@
         <div class="col-lg-6">
           <div class="card mb-4">
             <div class="card-header bg-light">
-              <h3 class="mb-0">Select files to move</h3>
+              <h3 class="mb-0">Select items to move</h3>
             </div>
             <div class="card-body">
               <input type="text"
                      id="file-filter"
                      class="form-control mb-2"
-                     placeholder="Search for a file to move..." />
+                     placeholder="Search for an item to move..." />
               <table class="table table-sm table-striped border mt-3">
                 <tbody id="file-list-table-body">
                 </tbody>
@@ -164,7 +164,7 @@
           <input type="number"
                  class="form-control form-control-sm w-25"
                  id="max-files-input"
-                 value="5"
+                 value="{{ max_files_to_show }}"
                  min="1" />
         </div>
         <div id="current-move-selection-info" class="text-muted">Please select 
a file and a destination.</div>


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


Reply via email to