diff --git a/project_docs/UI_F6_PURE_MOVE_V1.md b/project_docs/UI_F6_PURE_MOVE_V1.md new file mode 100644 index 0000000..9bbfb40 --- /dev/null +++ b/project_docs/UI_F6_PURE_MOVE_V1.md @@ -0,0 +1,189 @@ +# UI_F6_PURE_MOVE_V1 + +## 1. Doel + +`F6` moet in de UI een expliciete pure `Move`-actie worden. + +Waarom: +- `F2` heeft nu een eigen rename-flow +- daarmee is de oorspronkelijke gecombineerde `Rename/Move`-semantiek in de UI niet meer nodig +- een pure `Move`-flow maakt de functietoetsen voorspelbaarder: + - `F2 = Rename` + - `F6 = Move` +- dit sluit beter aan op klassieke file-manager verwachtingen en vermindert cognitieve ruis in de popup-flow + +Belangrijk bestaande context: +- move-functionaliteit bestaat backendmatig al +- move-functionaliteit bestaat UI-matig al +- file move, directory move binnen huidige scope en batch move binnen huidige scope werken al +- deze stap gaat dus niet over nieuwe move-capaciteit, maar over het vereenvoudigen van de UI-flow rond de bestaande move-actie + +## 2. Scope + +De pure `Move`-flow in v1 moet de bestaande move-scope UI-matig netjes afdekken voor zover die al ondersteund wordt. + +In scope: +- exact 1 file +- exact 1 directory +- meerdere files +- meerdere directories +- gemengde selectie van files + directories + +Voorwaarde: +- alleen binnen de bestaande huidige move-scope +- alles wat backendmatig al geblokkeerd is, blijft geblokkeerd +- de UI mag dat duidelijker presenteren, maar mag de scope niet verbreden + +Niet in scope: +- nieuwe backend move-capaciteit +- nieuwe tasksemantiek +- nieuwe batch-semantieken buiten de bestaande backend +- rename binnen `F6` + +## 3. Gewenst gedrag + +`F6` en de `Move`-knop moeten exact dezelfde frontendflow gebruiken. + +Kernregel: +- `F6` keyboard shortcut = pure move-flow +- `Move`-knop in functiebalk = exact dezelfde pure move-flow +- beide gebruiken dezelfde centrale frontendhandler + +Popupregel: +- de bestaande move-popup wordt een pure move-popup +- geen rename-invoerveld in deze flow +- geen naamwijzigingssemantiek in deze flow +- de popup richt zich alleen op doelpad of doelmap voor verplaatsen + +Interactie: +- `Enter` bevestigt +- `Escape` annuleert +- `X` sluit popup + +Gedragslijn per context: +- exact 1 item: popup toont expliciete move-context voor dat item +- meerdere items: popup toont batch move-context +- popuptekst en labels moeten spreken in termen van `Move`, niet `Rename/Move` + +## 4. Relatie met bestaande move-scope + +Wat al bestaat en alleen hergebruikt moet worden: +- single file move +- single directory move binnen de huidige scope +- batch move binnen de huidige scope +- task-based move +- bestaande validaties en blokkades + +Dat betekent: +- de pure `Move`-popup hoeft geen nieuwe backendbeslissingen te nemen +- de frontend mag alleen een duidelijkere presentatie en dispatchlaag geven +- validatiefouten zoals cross-root blokkades, subtree-blokkades, mixed-root blokkades en bestaande destination-conflicten blijven door de bestaande backend en bestaande UI-validatie worden afgevangen + +Pragmatische consequentie: +- de move-popup moet vooral destination-gericht zijn +- submit moet direct de bestaande move-logica aanroepen +- geen impliciete keuze meer tussen rename en move in deze flow + +## 5. Relatie met F2 Rename + +Na invoering van pure `F6 Move` geldt: +- `F2` blijft exclusief voor rename +- `Rename`-knop blijft exclusief voor rename +- `F6` wordt exclusief voor move +- `Move`-knop wordt exclusief voor move + +Belangrijk ontwerpprincipe: +- geen gedeelde rename/move-popup meer +- `F2` en `F6` krijgen elk een eigen, semantisch heldere popupflow +- dit maakt toekomstige onderhoud en UX-consistentie eenvoudiger + +## 6. UI-semantiek + +### Welke contextinformatie is nuttig + +Voor een pure move-popup is nuttig: +- broninformatie: welk item of hoeveel items geselecteerd zijn +- destination-informatie: waarheen wordt verplaatst +- bij batch move: doelmap in plaats van volledig doelpad per item + +### Default destination + +Aanbevolen defaultvoorstel: +- gebruik het current path van het inactieve paneel als destination-basis + +Voor exact 1 item: +- toon een destination pad of destination map duidelijk, afhankelijk van de bestaande implementatierichting +- aanbevolen: toon volledig destination path, vooraf ingevuld op basis van het inactieve paneel + itemnaam +- dit houdt aan bij de bestaande move-aanroepsemantiek voor single-item move + +Voor batch move: +- toon aantal geselecteerde items +- toon doelmap = current path van het inactieve paneel +- geen rename-achtige tekst of naamveld per item + +### Foutmeldingen en blokkades + +Compact tonen in de popup of bestaande errorzone: +- destination exists +- mixed roots not allowed +- destination inside source not allowed +- cross-root directory move not supported +- andere bestaande backendfouten + +Belangrijk: +- foutmeldingen moeten move-gericht geformuleerd zijn +- geen verwijzing naar rename of gecombineerde semantiek + +## 7. Regressierisico + +Belangrijkste risico's: +- per ongeluk opnieuw bouwen van bestaande move-functionaliteit in plaats van die te hergebruiken +- verwarring tussen oude gecombineerde popup en nieuwe pure move-popup +- inconsistente keyboard- en knopwiring tussen `F6` en `Move` +- onbedoelde impact op de al werkende `F2 Rename`-flow + +Mitigatie: +- `F6` en `Move` één centrale pure move-handler laten delen +- bestaande backend move-calls intact laten +- `F2` volledig gescheiden houden +- popup-tekst en labels expliciet move-only maken +- geen backendwijzigingen in deze stap + +## 8. Teststrategie + +UI smoke/regressietests: +- functiebalk bevat `Move` met `F6` +- `F6` wiring blijft aanwezig +- `Move`-knop gebruikt dezelfde handler als `F6` +- move-popupcontainer aanwezig en move-only geëtiketteerd +- rename-popup blijft apart aanwezig voor `F2` + +Handmatige validatie: +- exact 1 file -> `F6` opent pure move-popup +- exact 1 directory -> `F6` opent pure move-popup binnen huidige scope +- meerdere files -> batch move-popup blijft logisch werken +- meerdere directories -> batch move-popup blijft logisch werken binnen huidige scope +- gemengde selectie -> bestaande batch move-scope en blokkades blijven correct +- `F2 Rename` blijft volledig los en ongewijzigd + +Bestaande regressies die bewaakt moeten blijven: +- single file move same-root +- single file move cross-root +- single directory move binnen huidige scope +- batch move file-only +- batch move met directories binnen huidige scope +- blokkades voor mixed roots, subtree en symlink source + +## 9. Aanbeveling + +Aanbevolen v1-richting met laag regressierisico: +- maak `F6` en `Move` UI-semantisch puur move-only +- verwijder rename-semantiek uit de `F6`-popupflow +- behoud en hergebruik alle bestaande move-logica, validaties en backendcalls +- houd `F2 Rename` volledig apart + +Waarom dit de veiligste richting is: +- maximale herbruik van bestaande backend en UI-logica +- minimale semantische overlap tussen `Rename` en `Move` +- duidelijkere functietoetsbetekenis +- laag regressierisico omdat de verandering vooral UI-opschoning is, geen capability-uitbreiding diff --git a/webui/backend/data/tasks.db b/webui/backend/data/tasks.db index 0e0a3ab..dc3df1f 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 59d139b..44b2448 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 2cc623b..5d22614 100644 --- a/webui/backend/tests/golden/test_ui_smoke_golden.py +++ b/webui/backend/tests/golden/test_ui_smoke_golden.py @@ -62,8 +62,10 @@ class UiSmokeGoldenTest(unittest.TestCase): self.assertIn('id="editor-content"', body) self.assertIn('id="editor-save-btn"', body) self.assertIn('id="editor-cancel-btn"', body) - self.assertIn('id="rename-move-popup"', body) - self.assertIn('id="rename-move-input"', body) + self.assertIn('id="move-popup"', body) + self.assertIn('id="move-input"', body) + self.assertIn(">Move", body) + self.assertIn(">Target path", body) self.assertIn('id="batch-move-popup"', body) self.assertIn('id="batch-move-apply-btn"', body) self.assertIn('id="mkdir-btn"', body) @@ -108,6 +110,8 @@ class UiSmokeGoldenTest(unittest.TestCase): 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('function openMovePopup()', app_js) + self.assertIn('document.getElementById("move-btn").onclick = openF6Flow;', 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 e445ddf..64c0ddc 100644 --- a/webui/html/app.js +++ b/webui/html/app.js @@ -30,7 +30,7 @@ let editorState = { originalContent: "", modified: null, }; -let renameMoveState = { +let moveState = { source: null, destination: "", }; @@ -140,14 +140,15 @@ function editorElements() { }; } -function renameMoveElements() { +function moveElements() { return { - overlay: document.getElementById("rename-move-popup"), - source: document.getElementById("rename-move-source"), - input: document.getElementById("rename-move-input"), - error: document.getElementById("rename-move-error"), - applyButton: document.getElementById("rename-move-apply-btn"), - cancelButton: document.getElementById("rename-move-cancel-btn"), + overlay: document.getElementById("move-popup"), + source: document.getElementById("move-source"), + input: document.getElementById("move-input"), + error: document.getElementById("move-error"), + applyButton: document.getElementById("move-apply-btn"), + cancelButton: document.getElementById("move-cancel-btn"), + closeButton: document.getElementById("move-close-btn"), }; } @@ -1002,8 +1003,8 @@ function isWildcardPopupOpen() { return !wildcardPopupElements().overlay.classList.contains("hidden"); } -function isRenameMovePopupOpen() { - return !renameMoveElements().overlay.classList.contains("hidden"); +function isMovePopupOpen() { + return !moveElements().overlay.classList.contains("hidden"); } function isRenamePopupOpen() { @@ -1088,8 +1089,8 @@ function showBatchDirectoryMoveNotSupported() { setStatus(message); } -function resetRenameMoveState() { - renameMoveState = { +function resetMoveState() { + moveState = { source: null, destination: "", }; @@ -1134,12 +1135,12 @@ function resetBatchMoveState() { }; } -function closeRenameMovePopup() { - const elements = renameMoveElements(); +function closeMovePopup() { + const elements = moveElements(); elements.overlay.classList.add("hidden"); elements.error.textContent = ""; elements.input.value = ""; - resetRenameMoveState(); + resetMoveState(); } function closeBatchMovePopup() { @@ -1149,16 +1150,16 @@ function closeBatchMovePopup() { resetBatchMoveState(); } -function openRenameMovePopup() { +function openMovePopup() { const selectedItems = activePaneState().selectedItems; if (selectedItems.length !== 1) { return false; } const source = selectedItems[0]; const destination = defaultDestination(source.path, paneState(otherPane(state.activePane)).currentPath); - const elements = renameMoveElements(); - renameMoveState.source = source; - renameMoveState.destination = destination; + const elements = moveElements(); + moveState.source = source; + moveState.destination = destination; elements.source.textContent = `Source: ${source.path}`; elements.input.value = destination; elements.error.textContent = ""; @@ -1190,7 +1191,7 @@ function openF6Flow() { return false; } if (selectedItems.length === 1) { - return openRenameMovePopup(); + return openMovePopup(); } if (selectedItems.some((item) => item.kind !== "file")) { const roots = uniqueRootKeysForItems(selectedItems); @@ -1244,20 +1245,19 @@ async function submitRenamePopup() { } } -async function submitRenameMovePopup() { - const elements = renameMoveElements(); - const source = renameMoveState.source; +async function submitMovePopup() { + const elements = moveElements(); + const source = moveState.source; if (!source) { return; } const destination = elements.input.value.trim(); const sourceParent = currentParentPath(source.path); const destinationParent = currentParentPath(destination); - const destinationName = baseName(destination); elements.error.textContent = ""; if (!destination) { - elements.error.textContent = "Destination path is required"; + elements.error.textContent = "Target path is required"; return; } if (destination === source.path) { @@ -1278,25 +1278,13 @@ async function submitRenameMovePopup() { } try { - if (destinationParent === sourceParent) { - await apiRequest("POST", "/api/files/rename", { - path: source.path, - new_name: destinationName, - }); - closeRenameMovePopup(); - setSelectedItem(state.activePane, null); - await loadBrowsePane(state.activePane); - setStatus(`Renamed ${source.path}`); - return; - } - const result = await apiRequest("POST", "/api/files/move", { source: source.path, destination, }); state.selectedTaskId = result.task_id; await refreshTasksSnapshot(); - closeRenameMovePopup(); + closeMovePopup(); setSelectedItem(state.activePane, null); await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]); setStatus(`Move: 1 success, 0 failed`); @@ -1656,15 +1644,15 @@ function handleKeyboardShortcuts(event) { } return; } - if (isRenameMovePopupOpen()) { + if (isMovePopupOpen()) { if (event.key === "Escape") { event.preventDefault(); - closeRenameMovePopup(); + closeMovePopup(); return; } if (event.key === "Enter") { event.preventDefault(); - submitRenameMovePopup(); + submitMovePopup(); } return; } @@ -1842,23 +1830,24 @@ function setupEvents() { } }; - const renameMove = renameMoveElements(); - renameMove.cancelButton.onclick = closeRenameMovePopup; - renameMove.applyButton.onclick = submitRenameMovePopup; - renameMove.input.onkeydown = (event) => { + const move = moveElements(); + move.closeButton.onclick = closeMovePopup; + move.cancelButton.onclick = closeMovePopup; + move.applyButton.onclick = submitMovePopup; + move.input.onkeydown = (event) => { if (event.key === "Enter") { event.preventDefault(); - submitRenameMovePopup(); + submitMovePopup(); return; } if (event.key === "Escape") { event.preventDefault(); - closeRenameMovePopup(); + closeMovePopup(); } }; - renameMove.overlay.onclick = (event) => { - if (event.target === renameMove.overlay) { - closeRenameMovePopup(); + move.overlay.onclick = (event) => { + if (event.target === move.overlay) { + closeMovePopup(); } }; diff --git a/webui/html/index.html b/webui/html/index.html index c03f376..3f0d3e4 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -113,16 +113,17 @@ -