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]