From 5123067100e35f9d32809e0e576d4972bbea8533 Mon Sep 17 00:00:00 2001 From: kodi Date: Thu, 12 Mar 2026 10:01:21 +0100 Subject: [PATCH] feat: F6 Move functionaliteit aangepast --- project_docs/UI_F6_PURE_MOVE_V1.md | 189 ++++++++++++++++++ webui/backend/data/tasks.db | Bin 53248 -> 65536 bytes .../test_ui_smoke_golden.cpython-313.pyc | Bin 9271 -> 9580 bytes .../tests/golden/test_ui_smoke_golden.py | 8 +- webui/html/app.js | 89 ++++----- webui/html/index.html | 17 +- webui/html/style.css | 5 +- 7 files changed, 246 insertions(+), 62 deletions(-) create mode 100644 project_docs/UI_F6_PURE_MOVE_V1.md 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 0e0a3ab19a76f9ab42feee7bd5281a1bee58fe2e..dc3df1f2bfcd6cb199e6c3c1c8fda6d465d66e2a 100644 GIT binary patch delta 5033 zcma)=OKcri8OP_^_j=|&W+qO2Z6|i>rd8d77>J4?kny*n}=n(g%fsBIe$iDM}nU zk=yb})tj;ERHZ;2W&=@GHEKkKfCS46=%OlVSE!V-02PS^Z8nu4AR$#*kOh2a?!%8e zuEz_@p6{FU{lD*=^E>C83$J!xxR6~t)RU0}LD+Zc?&+?KwEoSjYG03f6P^hB)H~{J z^*`z@^<(ws#*ux$S1+B&b`>V0k4FwW3)y#tA`x-^pCv)QtUfLu&wruxp7=lIiJk*u zKKpL&AEk%HuStK%-<5w`I@$B1($&&ul(Krg`1Rb4;|o>#h}7AkmeDF5Mv;LpgHas1Xh(9Hq#6f zys1vGW?7c6Av36Y%rrco`Lo;+{aAH6R>ydcgB z;;iV3p?F2SA$}xfq$AR-OY zY=%we=&p8T+ZL`nGl>q^9HTbHP=V=P?bI-Jl={U2;bjldEoXKQ688b1A-)%V) z8#k=VV}k|ZbwQaZJzZFrZi)Bjs@bLPXS(`?*Dr-HPOO}o?0d945)OsEU0s3@4grQL zl}egb6difdiq0$#M<>2B7oB@3}?lkh&AC&R^6K`p8j82yfXtMp!RRhcebm5-OstJl@PD&^v} z;s?qT!0?Me=rwsnK3BS}yrJAI38jZ%sQF@Q$`14WnTE1M+%XMh4{^s-ls(8j(@=Jh zJEo!R0C!A9*?#VshO%Yun1-@_vB)N6mp+ZM2e@aN{jr}r?zL$7n(f^<_h!^JJS@)( z^1S>Fc}@Pd{I>ja`4jnX%BUhMGs;&sq~F~q*9W^<5>cJuL<6#%p=1Mc4((<*m}t6i3fB66Ju#+Xcwv+j^1C+k8F}9i5t$wnJbZU7nf0lBU4qBL3y-9 zi_?W?^MA>grA6`2;_P~OuZY8a!$lA_^jIt_%_S@P@2VXgyc} z@vv!N5naCi;FsgqVF_)mdFxp?dI*L*csd?ZNQ0F#OHcdfp7NiHZu~-v7GE%@TK55j zoN($O7uXn$rMck2bOh1+KgfS|%P5^XVdT+4uG2luQy0>(s;x78?Q_x0_dg$9cs}3O z3Tr)_8sJ(RBeFD51Fc4M;(7bwmR1<0Ln|M7xj$Z(BVjq7D9*vXE}Jm66hR)-#bR%wyl;1#1JFt z0FSp?J075-BPVzkE+2^FmBW!Zr=ZF6Ps4dLUaN**jJDQpw>V4tx79AqX|S_)6OH-d zx22uA#LqY|3RzuHek^~s_-5h3Jd&6=(9@s0H>9FAT!ey2>=kXrw;N=e==i-`NY&CaV#kEa_g=hkKcuQzv+n~-FL_|T+dM?2N7*k(7| zQ86$v_(Y-OPTjG|BG9F;oSurs0kY%iOTZuWTQI2YAup_4>W Q1Bj6@jr=VG8h^w2Ke&|16J2R8 zZ>SDJSxX3^8@fp7Vqh_FzT;>3e!rh&GLX#ZPRKwdgpk;4j@O`)H=TvLAiVhmG5CRR z_=GCFsoKl~xbID&^_Y=IW;gDl9~_B8m=eQuh+pGhIgcqH!xN-L8|ft(= zn-f?6hx~ZkQNzuhN7Qp$D-Fa*5vk_RF!r2%O%;3k$LoonK(d!FGmlhpdvGSOs3*LQ zSzRUE6vx7^u(Smi-?n(01Daa6b7h46Lt!H@A?gcViK>i44$87Y&ijgK=?GgEWJQIo g5uxlewNG!>)(a%XtO3ks`oXB6w z#JFYib-@Ov$v=g7C+`;GXP=O>z~u^u=41mAiOr=VjEr&@nWX})S&|tc4gr&_K$4k( zf$?)0>tsF_mCZXur!!A}EhaYkC%eGpw_+lbWjXjJ`%79)z9<@rQ1uI%I<*`kkCne6z8mv4yfK@>#lr2~V;qi0^P1ViE zq}Z9HCDKasl1no4^Az$6Qu9Eb3jldtL$k_qa-xhRGcYtJ*UN~pxtY1;2WameON@=A6_a9G~p;OXb>vkcp*6Q3P;rD*9xZ@IXoCy&A*2*sczn< ei+ngX6k{=0yq3FAN~whoZ^SDlz~FJ%o<{ delta 347 zcmaFkwcUg7GcPX}0}zC!Z_2zWzLD=AGt&-^&3r8C?34FN2uzM;6`5?x?J;>Fcf{mW z9+AnsJOUtE&@`0UlnJDdfkA;mpCQ<6au-iKqxohb-V7#vvCI@(rJ~fl#N1Tf-2Ads z-Gcmr(gLL_4Drmog3=PD&3pMvnHZ;UHWq4Nn(QUQ55}RQ{FDERe3H8$FBK@xlFSIP z3rw;CNoEEH#?N}JllfRwHXDjfXPzt|WyLHJ$};((q%tGR|p4Z^@L$HaT8eoS8LPadH5wf>0=1uoA*A=?t36o7<(?nKnO`{m;Y} z2ehl$bn^=ZCC1H~N{1LZ+!$HSzXvcWZ~m<8$tY69$Y?o(3?SZztjPhY FG5|O2TFn3e 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 @@ -