filebrowsers working
Continuous Integration / backend-tests (push) Successful in 1m0s
Continuous Integration / frontend-check (push) Successful in 31s
Continuous Integration / e2e-tests (push) Successful in 16m44s

This commit is contained in:
2026-05-04 14:01:12 -04:00
parent 40db1251e1
commit 2f8e343b6d
4 changed files with 164 additions and 9 deletions
+8 -3
View File
@@ -1485,6 +1485,8 @@ def get_discrepancies_tree(
return result
# Return immediate children of the given path
if path is None:
return []
result = []
for dir_path, node in sorted(dir_nodes.items()):
if dir_path == path:
@@ -1587,16 +1589,19 @@ def browse_discrepancies(
child_name = file_path
else:
# Check if this file is under the requested path
if file_path != path and not file_path.startswith(path + "/"):
if path is None or (
file_path != path and not file_path.startswith(path + "/")
):
continue
# Get immediate child relative to path
rel_path = file_path[len(path) :].strip("/")
path_str = path or ""
rel_path = file_path[len(path_str) :].strip("/")
if "/" in rel_path:
# It's a subdirectory - get immediate child
child_name = rel_path.split("/")[0]
child_path = (
path + "/" + child_name if path != "/" else "/" + child_name
path_str + "/" + child_name if path_str != "/" else "/" + child_name
)
else:
# It's a file
@@ -32,6 +32,7 @@
currentPath = $bindable("ROOT"),
searchQuery = $bindable(""),
files = [],
selectedPaths = $bindable(new Set<string>()),
onNavigate = (path: string) => {},
onToggleTrack = (item: FileItem) => {},
onSelect = (item: FileItem) => {},
@@ -44,6 +45,7 @@
currentPath?: string;
searchQuery?: string;
files?: FileItem[];
selectedPaths?: Set<string>;
onNavigate?: (path: string) => void;
onToggleTrack?: (item: FileItem) => void;
onSelect?: (item: FileItem) => void;
@@ -54,7 +56,6 @@
pendingChanges?: Map<string, boolean>;
}>();
let selectedPaths = $state<Set<string>>(new Set());
let lastSelectedPath = $state<string | null>(null);
let sortColumn = $state<"name" | "size" | "mtime" | "type">("name");
let sortDirection = $state<"asc" | "desc">("asc");
@@ -284,6 +285,13 @@
}
function handleRowClick(e: MouseEvent, item: FileItem) {
// For discrepancies mode, row click navigates (dirs) or shows metadata (files)
// Checkbox handles selection
if (mode === 'discrepancies') {
onSelect(item);
return;
}
if (e.shiftKey && lastSelectedPath) {
const lastIndex = filteredFiles.findIndex((f: FileItem) => f.path === lastSelectedPath);
const currentIndex = filteredFiles.findIndex((f: FileItem) => f.path === item.path);
@@ -321,14 +329,51 @@
}
}
// Recursively add directory and all its children to selection
function addItemAndChildren(path: string, newSelection: Set<string>) {
newSelection.add(path);
// Find all files/dirs that are children of this path
for (const f of (files as FileItem[])) {
if (f.path.startsWith(path + "/") || f.path === path) {
newSelection.add(f.path);
}
}
}
// Recursively remove directory and all its children from selection
function removeItemAndChildren(path: string, newSelection: Set<string>) {
newSelection.delete(path);
for (const f of (files as FileItem[])) {
if (f.path.startsWith(path + "/")) {
newSelection.delete(f.path);
}
}
}
function handleSelectAll(checked: boolean | "indeterminate") {
if (checked === true) {
selectedPaths = new Set(filteredFiles.map((f: FileItem) => f.path));
const newSelection = new Set<string>();
for (const f of (filteredFiles as FileItem[])) {
addItemAndChildren(f.path, newSelection);
}
selectedPaths = newSelection;
} else {
selectedPaths = new Set();
}
}
function handleToggleItem(item: FileItem) {
const newSelection = new Set(selectedPaths as Set<string>);
if (newSelection.has(item.path)) {
// Deselecting - remove item and all children if it's a directory
removeItemAndChildren(item.path, newSelection);
} else {
// Selecting - add item and all children if it's a directory
addItemAndChildren(item.path, newSelection);
}
selectedPaths = newSelection;
}
let isEditingPath = $state(false);
let pathInputValue = $state("");
@@ -605,6 +650,7 @@
onClick={(e) => handleRowClick(e, item)}
onDoubleClick={() => handleRowDoubleClick(item)}
onToggleTrack={() => onToggleTrack(item)}
onToggleSelect={() => handleToggleItem(item)}
onAddToCart={() => onAddToCart(item)}
onDelete={() => onDelete(item)}
/>
@@ -28,6 +28,7 @@
onClick = (e: MouseEvent) => {},
onDoubleClick = () => {},
onToggleTrack = () => {},
onToggleSelect = () => {},
onAddToCart = () => {},
onDelete = () => {},
mode = "host",
@@ -39,6 +40,7 @@
onClick?: (e: MouseEvent) => void;
onDoubleClick?: () => void;
onToggleTrack?: () => void;
onToggleSelect?: () => void;
onAddToCart?: () => void;
onDelete?: () => void;
mode?: "host" | "index" | "live" | "cart" | "discrepancies";
@@ -122,12 +124,16 @@
ondblclick={(e) => { e.stopPropagation(); onDoubleClick(); }}
onkeydown={(e) => e.key === "Enter" && onDoubleClick()}
>
<!-- TRACKING STATUS / SELECTION -->
<!-- SELECTION CHECKBOX -->
<div
class="flex h-10 w-12 shrink-0 items-center justify-center border-r border-border-color/10"
onclick={(e) => {
e.stopPropagation();
onToggleTrack();
if (mode === 'discrepancies') {
onToggleSelect();
} else {
onToggleTrack();
}
}}
onkeydown={(e) => e.key === " " && e.stopPropagation()}
role="none"
@@ -150,6 +156,11 @@
<Square size={16} />
</div>
{/if}
{:else if mode === 'discrepancies'}
<Checkbox
checked={isSelected}
onCheckedChange={onToggleSelect}
/>
{:else}
<Checkbox
checked={item.selected}
+95 -2
View File
@@ -25,8 +25,7 @@
let files = $state<FileItem[]>([]);
let loading = $state(true);
let currentPath = $state("ROOT");
let selectedIds = $state<Set<number>>(new Set());
let batchAction = $state<'acknowledge' | 'recover' | null>(null);
let selectedPaths = $state<Set<string>>(new Set());
let batchLoading = $state(false);
async function loadDiscrepancies() {
@@ -105,6 +104,78 @@
}
}
async function batchDismiss() {
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
if (ids.length === 0) {
toast.error("No files selected");
return;
}
batchLoading = true;
try {
await batchDismissSystemDiscrepanciesBatchDismissPost({
body: { ids }
});
toast.success(`Dismissed ${ids.length} files`);
selectedPaths = new Set();
await loadDiscrepancies();
} catch (error: any) {
toast.error(error.body?.detail || "Failed to dismiss files");
} finally {
batchLoading = false;
}
}
async function batchDelete() {
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
if (ids.length === 0) {
toast.error("No files selected");
return;
}
batchLoading = true;
try {
await batchHardDeleteSystemDiscrepanciesBatchDeletePost({
body: { ids }
});
toast.success(`Deleted ${ids.length} file records`);
selectedPaths = new Set();
await loadDiscrepancies();
} catch (error: any) {
toast.error(error.body?.detail || "Failed to delete files");
} finally {
batchLoading = false;
}
}
async function batchAddToCart() {
const ids = getDiscrepancyIdsFromPaths(selectedPaths);
if (ids.length === 0) {
toast.error("No files selected");
return;
}
batchLoading = true;
try {
await batchAddToRecoveryQueueRestoresQueueBatchPost({
body: { ids }
});
toast.success(`Added ${ids.length} files to restore cart`);
selectedPaths = new Set();
} catch (error: any) {
toast.error(error.body?.detail || "Failed to add files to cart");
} finally {
batchLoading = false;
}
}
function getDiscrepancyIdsFromPaths(paths: Set<string>): number[] {
const ids: number[] = [];
for (const item of files) {
if (paths.has(item.path) && item.discrepancy_id) {
ids.push(item.discrepancy_id);
}
}
return ids;
}
function navigateTo(path: string) {
currentPath = path;
loadFiles(path);
@@ -176,10 +247,32 @@
/>
</div>
<!-- Batch Actions Bar -->
{#if selectedPaths.size > 0}
<div class="flex items-center gap-3 p-3 bg-bg-tertiary/50 rounded-lg border border-border-color">
<span class="text-sm text-text-secondary">
{selectedPaths.size} file(s) selected
</span>
<div class="flex gap-2 ml-auto">
<Button size="sm" variant="outline" onclick={batchDismiss} disabled={batchLoading}>
Dismiss Selected
</Button>
<Button size="sm" variant="outline" onclick={batchAddToCart} disabled={batchLoading}>
<HardDriveDownload size={14} class="mr-1" />
Add to Cart
</Button>
<Button size="sm" variant="destructive" onclick={batchDelete} disabled={batchLoading}>
Delete Records
</Button>
</div>
</div>
{/if}
<!-- FileBrowser Component in discrepancies mode -->
<div class="flex-1 min-h-[600px] bg-bg-secondary border border-border-color shadow-2xl rounded-lg flex flex-col relative overflow-hidden">
<FileBrowser
bind:currentPath={currentPath}
bind:selectedPaths={selectedPaths}
files={files}
mode="discrepancies"
onNavigate={navigateTo}