Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions tests/webui_file_browser_state.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,34 @@ test("file-browser persistence, restoration, refresh and execution outcomes", as
assert.deepEqual([...context.expandedFoldersRef.current], ["/data/a/b"]);
context.expandedFoldersRef.current = context.getStoredExpandedFolders();

const refreshStates = [];
const manualSearches = [];
context.fileBrowserRefreshing = false;
context.setFileBrowserRefreshing = (value) => refreshStates.push(value);
context.fileBrowserSearchQuery = { current: "new download" };
let finishManualSearch;
let manualSearchStarted;
const searchStarted = new Promise((resolve) => {
manualSearchStarted = resolve;
});
context.handleFileBrowserSearch = (query) => {
manualSearches.push(query);
manualSearchStarted();
return new Promise((resolve) => {
finishManualSearch = resolve;
});
};
load("refreshFileBrowser");
requests.length = 0;
const manualRefresh = context.refreshFileBrowser();
await searchStarted;
assert.deepEqual(refreshStates, [true]);
assert.deepEqual(requests, Object.keys(responses));
assert.deepEqual(manualSearches, ["new download"]);
finishManualSearch();
await manualRefresh;
assert.deepEqual(refreshStates, [true, false]);

let delayResolve;
let delay;
context.setTimeout = (fn, ms) => {
Expand Down Expand Up @@ -155,6 +183,7 @@ test("file-browser persistence, restoration, refresh and execution outcomes", as
assert.equal(searches.length, 1);

context.fileBrowserSearchTimer = { current: null };
context.fileBrowserSearchCompletion = { current: null };
context.fileBrowserSearchId = { current: 0 };
context.setFileBrowserSearch = () => {};
let searchLoading = false;
Expand Down
199 changes: 134 additions & 65 deletions web_ui/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2163,7 +2163,9 @@ function AudionutsUAGUI() {
useState(null);
const [fileBrowserSearchLoading, setFileBrowserSearchLoading] =
useState(false);
const [fileBrowserRefreshing, setFileBrowserRefreshing] = useState(false);
const fileBrowserSearchTimer = useRef(null);
const fileBrowserSearchCompletion = useRef(null);
const fileBrowserSearchQuery = useRef("");
const fileBrowserSearchId = useRef(0);

Expand Down Expand Up @@ -3376,6 +3378,21 @@ function AudionutsUAGUI() {
}
};

/** Reload visible file-browser data without resetting navigation state. */
const refreshFileBrowser = async () => {
if (fileBrowserRefreshing) return;
setFileBrowserRefreshing(true);
try {
setFileBrowserRestoring(true);
await loadBrowseRoots();
Comment thread
wastaken7 marked this conversation as resolved.
if (fileBrowserSearchQuery.current) {
await handleFileBrowserSearch(fileBrowserSearchQuery.current);
}
} finally {
setFileBrowserRefreshing(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
};

// Load description file browser roots
const loadDescBrowseRoots = async () => {
try {
Expand Down Expand Up @@ -3538,6 +3555,8 @@ function AudionutsUAGUI() {
if (fileBrowserSearchTimer.current) {
clearTimeout(fileBrowserSearchTimer.current);
}
fileBrowserSearchCompletion.current?.();
fileBrowserSearchCompletion.current = null;
};
}, []);

Expand Down Expand Up @@ -4079,68 +4098,88 @@ function AudionutsUAGUI() {

// File Browser search
const handleFileBrowserSearch = (value, signal) => {
if (signal?.aborted) return;
const searchId = ++fileBrowserSearchId.current;
setFileBrowserSearch(value);
const searchQuery = value.trim();
fileBrowserSearchQuery.current = searchQuery;
if (signal?.aborted) return Promise.resolve();
fileBrowserSearchCompletion.current?.();
if (fileBrowserSearchTimer.current) {
clearTimeout(fileBrowserSearchTimer.current);
}
if (!searchQuery) {
setFileBrowserSearchResults(null);
setFileBrowserSearchLoading(false);
return;
}
setFileBrowserSearchLoading(true);
const onAbort = () => {
if (fileBrowserSearchId.current === searchId) {
clearTimeout(fileBrowserSearchTimer.current);
return new Promise((resolve) => {
const searchId = ++fileBrowserSearchId.current;
let settled = false;
const settle = () => {
if (settled) return;
settled = true;
if (fileBrowserSearchCompletion.current === settle) {
fileBrowserSearchCompletion.current = null;
}
resolve();
};
fileBrowserSearchCompletion.current = settle;
setFileBrowserSearch(value);
const searchQuery = value.trim();
fileBrowserSearchQuery.current = searchQuery;
if (!searchQuery) {
setFileBrowserSearchResults(null);
setFileBrowserSearchLoading(false);
settle();
return;
}
};
signal?.addEventListener("abort", onAbort, { once: true });
fileBrowserSearchTimer.current = setTimeout(async () => {
if (signal?.aborted) return;
try {
const response = await apiFetch(
`${API_BASE}/browse_search?q=${encodeURIComponent(searchQuery)}`,
{ signal },
);
if (signal?.aborted) return;
if (!response.ok) {
throw new Error(`Search request failed (${response.status})`);
}
const data = await response.json();
if (signal?.aborted) return;
// Early return if the search has changed since this request
if (fileBrowserSearchId.current !== searchId) return;
if (data.success) {
setFileBrowserSearchResults(data);
} else {
setFileBrowserSearchResults({
items: [],
query: searchQuery,
count: 0,
});
}
} catch (error) {
if (signal?.aborted) return;
console.error("File browser search failed:", error);
setFileBrowserSearchLoading(true);
const onAbort = () => {
if (fileBrowserSearchId.current === searchId) {
setFileBrowserSearchResults({
items: [],
query: searchQuery,
count: 0,
});
clearTimeout(fileBrowserSearchTimer.current);
setFileBrowserSearchLoading(false);
}
} finally {
signal?.removeEventListener("abort", onAbort);
if (!signal?.aborted && fileBrowserSearchId.current === searchId) {
setFileBrowserSearchLoading(false);
settle();
};
signal?.addEventListener("abort", onAbort, { once: true });
fileBrowserSearchTimer.current = setTimeout(async () => {
if (signal?.aborted) {
settle();
return;
}
}
}, 300); //300ms debounce so we dont spam requests for every keystroke
try {
const response = await apiFetch(
`${API_BASE}/browse_search?q=${encodeURIComponent(searchQuery)}`,
{ signal },
);
if (signal?.aborted) return;
if (!response.ok) {
throw new Error(`Search request failed (${response.status})`);
}
const data = await response.json();
if (signal?.aborted) return;
// Early return if the search has changed since this request
if (fileBrowserSearchId.current !== searchId) return;
if (data.success) {
setFileBrowserSearchResults(data);
} else {
setFileBrowserSearchResults({
items: [],
query: searchQuery,
count: 0,
});
}
} catch (error) {
if (signal?.aborted) return;
console.error("File browser search failed:", error);
if (fileBrowserSearchId.current === searchId) {
setFileBrowserSearchResults({
items: [],
query: searchQuery,
count: 0,
});
}
} finally {
signal?.removeEventListener("abort", onAbort);
if (!signal?.aborted && fileBrowserSearchId.current === searchId) {
setFileBrowserSearchLoading(false);
}
settle();
}
}, 300); //300ms debounce so we dont spam requests for every keystroke
});
};

const refreshFileBrowserAfterUpload = async (signal) => {
Expand Down Expand Up @@ -6131,12 +6170,27 @@ function AudionutsUAGUI() {
) : (
<div className="flex flex-col h-full">
<div className="ua-upload-panel-header p-3 border-b flex-shrink-0">
<h2
className={`text-base font-bold ${isDarkMode ? "text-white" : "text-gray-800"} flex items-center gap-2`}
>
<FolderIcon />
File Browser
</h2>
<div className="flex items-center justify-between gap-2">
<h2
className={`text-base font-bold ${isDarkMode ? "text-white" : "text-gray-800"} flex items-center gap-2`}
>
<FolderIcon />
File Browser
</h2>
<button
type="button"
onClick={refreshFileBrowser}
disabled={fileBrowserRefreshing}
aria-label="Refresh file browser"
title="Refresh file browser"
className={`rounded p-1.5 transition-colors disabled:cursor-wait disabled:opacity-60 ${isDarkMode ? "text-gray-400 hover:bg-gray-700 hover:text-gray-200" : "text-gray-500 hover:bg-gray-200 hover:text-gray-700"}`}
>
<LucideIcon
name="refresh-cw"
className={`h-4 w-4 ${fileBrowserRefreshing ? "animate-spin" : ""}`}
/>
</button>
</div>
<div className="relative mt-2">
<input
type="text"
Expand Down Expand Up @@ -6857,12 +6911,27 @@ function AudionutsUAGUI() {
) : (
<>
<div className="ua-upload-panel-header p-4 border-b">
<h2
className={`text-lg font-bold ${isDarkMode ? "text-white" : "text-gray-800"} flex items-center gap-2`}
>
<FolderIcon />
File Browser
</h2>
<div className="flex items-center justify-between gap-2">
<h2
className={`text-lg font-bold ${isDarkMode ? "text-white" : "text-gray-800"} flex items-center gap-2`}
>
<FolderIcon />
File Browser
</h2>
<button
type="button"
onClick={refreshFileBrowser}
disabled={fileBrowserRefreshing}
aria-label="Refresh file browser"
title="Refresh file browser"
className={`rounded p-1.5 transition-colors disabled:cursor-wait disabled:opacity-60 ${isDarkMode ? "text-gray-400 hover:bg-gray-700 hover:text-gray-200" : "text-gray-500 hover:bg-gray-200 hover:text-gray-700"}`}
>
<LucideIcon
name="refresh-cw"
className={`h-4 w-4 ${fileBrowserRefreshing ? "animate-spin" : ""}`}
/>
</button>
</div>
<div className="relative mt-2">
<input
type="text"
Expand Down
Loading