diff --git a/project_docs/UI_F2_RENAME_V1.md b/project_docs/UI_F2_RENAME_V1.md new file mode 100644 index 0000000..195890c --- /dev/null +++ b/project_docs/UI_F2_RENAME_V1.md @@ -0,0 +1,164 @@ +# UI_F2_RENAME_V1 + +## 1. Doel + +`Rename` moet een expliciete eigen UI-flow krijgen via `F2`, los van de bestaande `F6` move-flow. + +Waarom: +- `Rename` is conceptueel een andere actie dan `Move` +- een eigen `F2`-flow sluit beter aan op klassieke file-manager bediening +- het maakt de UI voorspelbaarder: `F2` = naam wijzigen, `F6` = verplaatsen +- het vereenvoudigt later de verdere scheiding tussen rename en move in de UI, zonder backendwijziging + +Bestaande context: +- rename-functionaliteit bestaat backendmatig al via het bestaande rename-endpoint +- move-functionaliteit bestaat backendmatig al via het bestaande move-endpoint +- `F6`/move-flow bestaat al in de UI +- in deze stap moet dus niets nieuws in backend of API worden ontworpen; alleen een nette eigen rename-flow in de frontend + +## 2. Scope + +In scope voor v1: +- exact 1 file geselecteerd +- exact 1 directory geselecteerd +- `F2` keyboard shortcut +- `Rename`-knop in de functiebalk +- compacte rename-popup + +Out of scope: +- batch rename +- rename van meerdere selectie-items +- herontwerp van `F6` +- backendwijzigingen +- nieuwe dependencies + +## 3. Gewenst gedrag + +`F2` en de bestaande `Rename`-knop moeten exact dezelfde UI-flow gebruiken. + +Voorstel: +- `F2` keyboard shortcut activeert de rename-flow +- `Rename`-knop in functiebalk activeert exact dezelfde flow +- beide openen een compacte rename-popup + +Popup-eigenschappen: +- titel: `Rename` +- toont context van het geselecteerde item +- invoerveld bevat alleen de huidige naam +- dus niet het volledige pad +- focus direct in het invoerveld +- tekst vooraf geselecteerd zodat overschrijven snel kan +- `Enter` bevestigt +- `Escape` annuleert +- `X` rechtsboven sluit de popup + +Belangrijk semantisch verschil met `F6`: +- `F2 Rename` werkt op naamniveau binnen dezelfde parent +- de popup toont en bewerkt alleen de naam +- geen destination pad, geen implicit move-semantiek + +## 4. Validatie + +Frontendvalidatie in v1 moet klein blijven en vooral duidelijke UX geven. De backend blijft de bron van waarheid. + +Frontend moet minimaal blokkeren of afvangen: +- lege naam +- ongewijzigde naam +- namen met `/` +- namen die triviaal ongeldig zijn zoals `.` of `..` + +Aanpak: +- lichte pre-validatie in de popup voor snelle feedback +- daarna altijd het bestaande rename-endpoint gebruiken +- backend-validatie blijft leidend voor definitieve afhandeling + +Geen nieuwe rename-semantiek ontwerpen: +- geen padbewerkingen in de popup +- geen move-achtige fallback +- geen root- of parent-wijziging + +## 5. Files en directories + +Exact 1 file: +- rename toegestaan +- popup opent met huidige bestandsnaam +- bevestigen gebruikt bestaande backend-rename + +Exact 1 directory: +- rename toegestaan +- popup opent met huidige mapnaam +- bevestigen gebruikt bestaande backend-rename + +Meerdere geselecteerde items: +- in v1 niet ondersteunen +- `F2` en `Rename` doen functioneel niets destructiefs +- aanbevolen UX: knop disabled bij `selectedItems.length !== 1` +- voor keyboardshortcut: geen actie als rename in de huidige context disabled zou zijn + +Dit sluit aan op de bestaande regel dat keyboardshortcuts dezelfde enabled/disabled toestand moeten respecteren als de functiebalkknoppen. + +## 6. Relatie met bestaande flows + +Herbruik: +- bestaande backend rename-functionaliteit hergebruiken +- bestaande move-functionaliteit ongemoeid laten +- bestaande selectie- en active-pane-logica hergebruiken + +Regels: +- `F2` en `Rename`-knop delen exact dezelfde frontendflow +- `F6` blijft ongewijzigd in deze stap +- bestaande `F6` rename/move-popup wordt niet herontworpen in deze stap +- geen dubbele implementatie van backendlogica; alleen een aparte UI-laag voor rename + +Pragmatische richting: +- introduceer een aparte compacte rename-popup +- submit roept hetzelfde backend-endpoint aan als de huidige renameknop al gebruikt +- succesvolle rename refresht alleen het actieve paneel, zoals nu al logisch is voor rename + +## 7. Regressierisico + +Belangrijkste risico's: +- selectieflow: `F2` mag niet reageren bij ongeldige selectie +- popup-focus: paneelkeyboard mag niet doorwerken terwijl de rename-popup open is +- interactie met `F6`: geen verwarring of gedeelde state tussen rename-popup en bestaande rename/move-popup +- onbedoeld herbouwen van bestaande rename/move-logica in plaats van hergebruik + +Mitigatie: +- `F2` dezelfde disabled-context laten volgen als de `Rename`-knop +- aparte popup-state voor rename, niet hergebruik van de complexere `F6` destination-popup +- bestaande backend-rename endpoint direct blijven gebruiken +- geen aanpassing aan `F6` in deze stap + +## 8. Teststrategie + +UI smoke/regressietests: +- functiebalk bevat `Rename` met `F2`-hint +- rename-popupcontainer aanwezig in HTML +- rename-popup bevat invoerveld en sluitknop +- `F2` wiring aanwezig in frontendcode +- bestaande `F6` wiring blijft aanwezig + +Handmatige validatie: +- exact 1 file geselecteerd -> `F2` opent rename-popup met alleen naam +- exact 1 directory geselecteerd -> `F2` opent rename-popup met alleen naam +- `Enter` bevestigt +- `Escape` sluit +- `X` sluit +- meerdere selectie -> `F2` doet niets / rename blijft disabled +- succesvolle rename refresht actief paneel +- `F6` move-flow blijft ongewijzigd werken + +## 9. Aanbeveling + +Aanbevolen v1-richting met laag regressierisico: +- voeg een aparte compacte rename-popup toe voor `F2` en de `Rename`-knop +- werk alleen met de naam van exact 1 geselecteerd item +- gebruik het bestaande backend rename-endpoint zonder nieuwe semantiek +- laat `F2` en de `Rename`-knop exact dezelfde flow delen +- laat `F6` volledig ongemoeid in deze stap + +Waarom dit de veiligste richting is: +- duidelijke scheiding tussen rename en move in de UI +- minimaal risico op regressie in bestaande move-flow +- geen backendwerk nodig +- sluit goed aan op klassieke file-manager verwachtingen diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 47f4131..0e0a3ab 100644 Binary files a/webui/backend/data/tasks.db and b/webui/backend/data/tasks.db differ diff --git a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc index c1a1600..59d139b 100644 Binary files a/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc and b/webui/backend/tests/golden/__pycache__/test_ui_smoke_golden.cpython-313.pyc differ diff --git a/webui/backend/tests/golden/test_ui_smoke_golden.py b/webui/backend/tests/golden/test_ui_smoke_golden.py index c124ff5..2cc623b 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -38,7 +38,7 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="right-items"', body) self.assertIn('id="function-bar"', body) self.assertIn('id="settings-btn"', body) - self.assertIn('id="rename-placeholder-btn"', body) + self.assertIn('id="rename-btn"', body) self.assertIn('id="view-btn"', body) self.assertIn('id="edit-btn"', body) self.assertIn("F1", body) @@ -51,6 +51,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn("F8", body) self.assertIn('id="viewer-modal"', body) self.assertIn('id="settings-modal"', body) + self.assertIn('id="rename-popup"', body) + self.assertIn('id="rename-input"', body) + self.assertIn('id="rename-apply-btn"', body) self.assertIn('id="settings-general-tab"', body) self.assertIn('id="settings-logs-tab"', body) self.assertIn('id="settings-logs-list"', body) @@ -77,20 +80,16 @@ class UiSmokeGoldenTest(unittest.TestCase): ordered_ids = [ 'id="settings-btn"', - 'id="rename-placeholder-btn"', + 'id="rename-btn"', 'id="view-btn"', 'id="edit-btn"', 'id="copy-btn"', 'id="move-btn"', - 'id="rename-btn"', 'id="mkdir-btn"', 'id="delete-btn"', ] positions = [body.index(marker) for marker in ordered_ids] self.assertEqual(positions, sorted(positions)) - rename_placeholder_index = body.index('id="rename-placeholder-btn"') - disabled_index = body.index("disabled", rename_placeholder_index) - self.assertLess(rename_placeholder_index, disabled_index) def test_ui_static_assets_are_present_and_mapped(self) -> None: mount = self._ui_mount() @@ -106,6 +105,9 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('if (event.key === "F1") {', app_js) self.assertIn('if (event.key === "F2") {', app_js) self.assertIn('function openSettings(tab = "general")', app_js) + self.assertIn('function openRenamePopup()', app_js) + self.assertIn('document.getElementById("rename-btn").onclick = openRenamePopup;', app_js) + self.assertIn('return triggerActionButton("rename-btn");', app_js) self.assertIn('await apiRequest("GET", "/api/history")', app_js) self.assertIn('Cross-root directory move is not supported in v1', app_js) self.assertIn('Batch directory move is not supported in v1', app_js) diff --git a/webui/html/app.js b/webui/html/app.js index a062602..e445ddf 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -34,6 +34,10 @@ let renameMoveState = { source: null, destination: "", }; +let renameState = { + source: null, + name: "", +}; let batchMoveState = { destinationBase: "", count: 0, @@ -147,6 +151,17 @@ function renameMoveElements() { }; } +function renameElements() { + return { + overlay: document.getElementById("rename-popup"), + input: document.getElementById("rename-input"), + error: document.getElementById("rename-error"), + applyButton: document.getElementById("rename-apply-btn"), + cancelButton: document.getElementById("rename-cancel-btn"), + closeButton: document.getElementById("rename-close-btn"), + }; +} + function batchMoveElements() { return { overlay: document.getElementById("batch-move-popup"), @@ -920,7 +935,7 @@ function actionShortcutHandled(event) { return triggerActionButton("settings-btn"); } if (event.key === "F2") { - return false; + return triggerActionButton("rename-btn"); } if (event.key === "F3") { return triggerActionButton("view-btn"); @@ -991,6 +1006,10 @@ function isRenameMovePopupOpen() { return !renameMoveElements().overlay.classList.contains("hidden"); } +function isRenamePopupOpen() { + return !renameElements().overlay.classList.contains("hidden"); +} + function isBatchMovePopupOpen() { return !batchMoveElements().overlay.classList.contains("hidden"); } @@ -1076,6 +1095,38 @@ function resetRenameMoveState() { }; } +function resetRenameState() { + renameState = { + source: null, + name: "", + }; +} + +function closeRenamePopup() { + const elements = renameElements(); + elements.overlay.classList.add("hidden"); + elements.error.textContent = ""; + elements.input.value = ""; + resetRenameState(); +} + +function openRenamePopup() { + const selectedItems = activePaneState().selectedItems; + if (selectedItems.length !== 1) { + return false; + } + const source = selectedItems[0]; + const elements = renameElements(); + renameState.source = source; + renameState.name = source.name; + elements.input.value = source.name; + elements.error.textContent = ""; + elements.overlay.classList.remove("hidden"); + elements.input.focus(); + elements.input.select(); + return true; +} + function resetBatchMoveState() { batchMoveState = { destinationBase: "", @@ -1154,6 +1205,45 @@ function openF6Flow() { return openBatchMovePopup(selectedItems); } +async function submitRenamePopup() { + const elements = renameElements(); + const source = renameState.source; + if (!source) { + return; + } + const newName = elements.input.value.trim(); + elements.error.textContent = ""; + if (!newName) { + elements.error.textContent = "Name is required"; + return; + } + if (newName === source.name) { + elements.error.textContent = "Name must differ from current name"; + return; + } + if (newName.includes("/")) { + elements.error.textContent = "Name cannot contain /"; + return; + } + if (newName === "." || newName === "..") { + elements.error.textContent = "Invalid name"; + return; + } + + try { + await apiRequest("POST", "/api/files/rename", { + path: source.path, + new_name: newName, + }); + closeRenamePopup(); + setSelectedItem(state.activePane, null); + await loadBrowsePane(state.activePane); + setStatus(`Renamed ${source.path}`); + } catch (err) { + elements.error.textContent = err.message; + } +} + async function submitRenameMovePopup() { const elements = renameMoveElements(); const source = renameMoveState.source; @@ -1532,6 +1622,19 @@ function clearSelectionForActivePane() { } function handleKeyboardShortcuts(event) { + if (isRenamePopupOpen()) { + if (event.key === "Escape") { + event.preventDefault(); + closeRenamePopup(); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + submitRenamePopup(); + return; + } + return; + } if (isSettingsOpen()) { if (event.key === "Escape") { event.preventDefault(); @@ -1679,12 +1782,33 @@ function setupEvents() { document.getElementById("settings-btn").onclick = () => openSettings("general"); document.getElementById("view-btn").onclick = openViewer; document.getElementById("edit-btn").onclick = openEditor; - document.getElementById("rename-btn").onclick = renameSelected; + document.getElementById("rename-btn").onclick = openRenamePopup; document.getElementById("delete-btn").onclick = deleteSelected; document.getElementById("copy-btn").onclick = startCopySelected; document.getElementById("move-btn").onclick = openF6Flow; document.getElementById("mkdir-btn").onclick = createFolderForActivePane; + const rename = renameElements(); + rename.closeButton.onclick = closeRenamePopup; + rename.cancelButton.onclick = closeRenamePopup; + rename.applyButton.onclick = submitRenamePopup; + rename.input.onkeydown = (event) => { + if (event.key === "Enter") { + event.preventDefault(); + submitRenamePopup(); + return; + } + if (event.key === "Escape") { + event.preventDefault(); + closeRenamePopup(); + } + }; + rename.overlay.onclick = (event) => { + if (event.target === rename.overlay) { + closeRenamePopup(); + } + }; + const settings = settingsElements(); settings.closeButton.onclick = closeSettings; settings.generalTab.onclick = () => setSettingsTab("general"); diff --git a/webui/html/index.html b/webui/html/index.html index 30e1c68..c03f376 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -68,12 +68,11 @@
left