diff --git a/TEST_REGRESSION.md b/TEST_REGRESSION.md new file mode 100644 index 0000000..6abfe97 --- /dev/null +++ b/TEST_REGRESSION.md @@ -0,0 +1,154 @@ +# Regression test plan — commits `6869b7c` → `4cb45ec` + +Cible : les zones touchées ces dernières heures (slideover, dirty, Esc, +batch DnD, sync, import, shutdown). Coche au fur et à mesure. + +> Préalable : `BuildAssets.cmd` + F9. Sinon embedded ≠ disque. + +--- + +## 1. Slideover — open / close / Esc + +- [✅] Ouvrir login → Esc → ferme direct (clean) +- [✅] Ouvrir login → modifier title → Esc → **modal "Discard unsaved changes?"** apparaît +- [✅] Discard confirm → Discard → ferme +- [✅] Discard confirm → Cancel → reste ouvert avec modif intacte +- [✅] Discard confirm → Esc → cancel (modal se ferme, slideover reste) +- [✅] Ouvrir NOTE (sans modifier) → Esc → ferme direct (pas de faux discard) +- [✅] Ouvrir entry avec **custom fields** existants → Esc → ferme direct (pas de faux dirty) +- [✅] Ouvrir entry avec **TOTP** + custom fields → Esc → ferme direct +- [✅] X header → même comportement que Esc +- [✅] Click outside (en center mode) → idem + +## 2. Slideover — modify + save (no duplicate) + +- [✅] New entry → remplir → Save → toast Saved → slideover re-ouvre avec entry sauvée +- [✅] **PAS** de modal "Discard" qui apparaît après Save +- [✅] Re-cliquer Save sur la même entry → toast Updated (PAS de duplicate dans la grille) +- [✅] Verifier dans grille : 1 seule entry créée +- [✅] Modifier custom field d'une entry existante → Save → reste ouvert → custom field préservé + +## 3. Settings panel + +- [✅] Click ⚙ Settings → panel s'ouvre +- [✅] Search input focused au top → tape "sort" → seule la row "Sort entries by" visible dans Appearance, autres rows hidden +- [✅] Search "PIN" → seule la section PIN visible +- [✅] Search "azertyqsd" → "No matches" banner affiché +- [✅] Clear button (X) → reset, panel complet +- [✅] Esc dans search avec query → clear (panel reste ouvert) +- [✅] Esc dans search vide → ferme Settings +- [✅] Re-ouvrir Settings → search vide (auto-reset) + +## 4. Editor position (right / left / center) + +- [✅] Right → slideover entre par la droite, app shell margin-right +- [✅] Left → slideover par la gauche +- [✅] Center → modal centré avec backdrop flou +- [✅] Click sur backdrop ferme le slideover (avec confirm si dirty) +- [✅] Click sur backdrop ferme Settings (idem) +- [✅] Click sur le panel lui-même ne ferme pas +- [✅] Switch right → center → left : pas de glitch visuel + +## 5. Esc priority + +- [✅] Carte dirty + Esc → 1 Esc = discard confirm (pas de double action) +- [✅] Carte dirty + Settings open + Esc(1) → ferme Settings (carte intacte) +- [✅] Carte dirty + Settings open + Esc(2) → discard confirm +- [✅] Discard confirm OPEN + Esc → cancel discard, slideover toujours là +- [✅] Modal confirm (autre) + Esc → ferme modal, ne touche pas slideover + +## 6. Batch drag-drop + +- [✅] Sélectionne 3 cards → drag l'une d'elles vers folder X → 3 entries déplacées +- [✅] TOTP / custom_fields / kind préservés (ouvrir une après pour vérifier) +- [✅] Sélectionne 2 cards → drag vers "(no folder)" → 2 déplacées vers "All" +- [✅] Sélectionne 4 cards → drag vers Trash sidebar → 4 soft-deleted +- [✅] **Single drag** (rien de checked) → marche pareil (1 seule entry move) +- [✅] Vue table → drag row vers folder → marche aussi (avant : draggable=false) +- [✅] Après batch op : batch action bar disparaît (state.checked clear) + +## 7. Trash actions + batch bar + +- [✅] Vue trash → sélectionne 2 → "Restore" bar → restored, bar disparaît +- [✅] Vue trash → sélectionne 2 → "Delete forever" bar → deleted, bar disparaît +- [✅] Vue trash → restore individuel via bouton card → bar count décrémente / disparaît +- [✅] Vue normale → "Empty trash" header button → bar disparaît si une entry trash était checked + +## 8. Sync (WebDAV) — quick smoke (full plan dans TEST_SYNC.md) + +- [ ] wsgidav running anonymous +- [ ] Test connection → "OK · snapshot not created yet" (404 OK) +- [ ] Sync now → toast `0 added · 0 updated · 0 deleted` + `vault-sync.json` créé +- [ ] Sync now sans `cfg.user` → ne renvoie PAS "Sync not configured" (anon mode supporté) + +## 9. Bitwarden CSV import + +- [✅] Export Bitwarden → CSV +- [✅] Settings → Import vault → choose .csv → preview montre count +- [✅] Confirm → toast "Imported N" +- [✅] **Folders** : sidebar montre les folders du CSV (Personal, etc.) +- [✅] Notes des logins → custom field "Notes" (PAS un tag) +- [✅] Logins importés OK (site, user, password déchiffrable) +- [✅] Secure notes → kind=note dans la grille +- [✅] **Fields** column Bitwarden → custom fields (label: value parsé) + +## 10. Custom fields normalize (faux-dirty fix) + +- [ ] Crée entry avec template "credit-card" → Save +- [ ] Re-ouvre → Esc → ferme direct (pas de faux discard) +- [ ] Re-ouvre + Save sans modif → bouton Save absent / no-op +- [ ] Importe une entry avec custom_fields → ouvre → Esc → ferme direct + +## 11. Autocomplete suppression (Edge form data) + +- [ ] Ouvre une carte → title focused → tape qq chars → Esc → ferme +- [ ] Ré-ouvre la même carte → focus title → **flèche bas** → PAS de dropdown "Informations enregistrées" +- [ ] Same pour password / TOTP / custom field + +## 12. Delete keyboard shortcut + +- [✅] Sélectionne 3 cards (Ctrl+click ou checkbox) → press `Delete` +- [✅] Modal "Move to trash N entries" → OK → trash +- [✅] Vue trash → sélectionne 2 → press `Delete` → "Delete forever" modal +- [✅] Focus dans input (search bar) + Delete → comportement normal de l'input (PAS de batch action) +- [✅] Slideover open + Delete → pas de batch action (gating) + +## 13. Toggle alignment + +- [✅] Settings → tous les toggles (Compact, Mask, Autofill, etc.) → cercle vertically centered, pas désaxé top/bottom +- [✅] Toggle OFF → ON → cercle slide horizontalement, reste centré + +## 14. Clean shutdown (DB WAL) + +- [✅] Lance app, unlock vault, fais quelques modifs +- [✅] Minimize to tray (X) +- [✅] Arrête Windows / Redémarre (ou logout) +- [✅] Vérifier au reboot : `vault.db` seul (pas `-shm` ou `-wal` résiduels) +- [✅] Si Task Manager → End task : OK les résidus apparaissent (force-kill normal, SQLite recover au prochain open) + +## 15. Quick search hotkey + +- [✅] Settings → "Quick search picker" row visible avec Ctrl+Shift+Q +- [✅] Click la pill → press Ctrl+Alt+S → label devient "Ctrl+Alt+S" +- [✅] Reset to defaults → revient à Ctrl+Shift+Q +- [✅] Press le combo n'importe où dans Windows → quick search modal apparaît + +## 16. PIN mode (regressions possibles avec les fixes Esc) + +- [ ] Mode PIN seul → lock → input PIN affiché → enter PIN → unlock +- [ ] PIN wrong → toast erreur +- [ ] Esc sur PIN input → ne ferme rien d'inattendu + +--- + +## Critères + +- ✅ Aucun bug bloquant +- ✅ Aucune perte de données silencieuse (TOTP, custom_fields, attachments) +- ✅ Esc fait UNE SEULE chose à la fois (pas de cascade invisible) +- ⚠️ Si bug : noter le scénario EXACT (section.N + étapes) avant de me dire + +## Si tout passe + +→ Tu peux merger / release sereinement. +→ Sinon on fix bug par bug avec le numéro de section comme référence. diff --git a/css/style.css b/css/style.css index 3dc0dab..f423976 100644 --- a/css/style.css +++ b/css/style.css @@ -2180,7 +2180,19 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before { padding: 8px 4px; } .so-pw-row .so-input:focus { box-shadow: none; } -.slideover-actions { display: flex; gap: 8px; margin-top: 24px; } +/* Sticky action row — stays glued to the bottom of the slideover body + while it scrolls, so Save is reachable without scrolling all the way + down on entries with lots of custom fields / attachments. */ +.slideover-actions { + display: flex; gap: 8px; + position: sticky; + bottom: -20px; /* cancel the .slideover-body padding-bottom */ + margin: 24px -20px -20px; /* extend to the edges */ + padding: 12px 20px; + background: var(--bg-elev); + border-top: 1px solid var(--border); + z-index: 2; +} /* ---- 11. MODALS ------------------------------------------ */ diff --git a/delphi-backend/assets/assets.res b/delphi-backend/assets/assets.res index 7a54d96..d3152b3 100644 Binary files a/delphi-backend/assets/assets.res and b/delphi-backend/assets/assets.res differ diff --git a/js/app.js b/js/app.js index 08ea7f7..6be184a 100644 --- a/js/app.js +++ b/js/app.js @@ -2344,6 +2344,7 @@ function viewTitle() { if (state.view === 'authenticator') return 'Authenticator'; if (state.view === 'health') return 'Vault health'; if (state.view === 'audit') return 'Audit log'; + if (state.view === 'folder:All') return '(no folder)'; if (state.view.startsWith('folder:')) return state.view.slice(7); if (state.view.startsWith('tag:')) return '# ' + state.view.slice(4); return 'Items'; @@ -2484,7 +2485,12 @@ function renderSidebar() { // No folder icon — visually communicates "this is the absence of a // folder, not a folder". Drag target so users can quickly uncategorise. const uncatCount = state.entries.filter(e => !e.folder || e.folder === 'All').length; - if (uncatCount > 0) { + // Show the pseudo-entry whenever the user has real folders in the + // sidebar — it doubles as a drag target to uncategorise entries. + // Also stays visible in the currently-viewed folder:All so a mid- + // action move-out doesn't strand the user. + const hasRealFolders = state.folders.some(f => f && f.name && f.name !== 'All'); + if (uncatCount > 0 || state.view === 'folder:All' || hasRealFolders) { const key = 'folder:All'; const item = el('button', { class: 'nav-item is-uncategorized' + (state.view === key ? ' is-active' : ''), @@ -3120,6 +3126,10 @@ function showEmptyState() { illustration.setAttribute('href', '#i-empty-vault'); title.textContent = 'No favorites yet'; msg.innerHTML = 'Click the on any entry to add it to favorites.'; + } else if (state.view === 'folder:All') { + illustration.setAttribute('href', '#i-empty-vault'); + title.textContent = 'No uncategorised entries'; + msg.innerHTML = 'Every entry currently belongs to a folder. Drag one here to remove it from its folder.'; } else if (state.view.startsWith('folder:')) { illustration.setAttribute('href', '#i-empty-vault'); title.textContent = 'Folder is empty'; @@ -4161,7 +4171,8 @@ async function batchDelete() { toast(ids.length + ' moved to trash'); await loadEntries(); await loadTrash(); - clearChecked(); + state.checked.clear(); + render(); // full render so sidebar counts (Trash, folders, tags) refresh } async function batchRestore() { @@ -4175,7 +4186,8 @@ async function batchRestore() { toast(ids.length + ' restored'); await loadEntries(); await loadTrash(); - clearChecked(); + state.checked.clear(); + render(); } async function batchPermDelete() { @@ -4195,7 +4207,8 @@ async function batchPermDelete() { } toast(ids.length + ' deleted permanently'); await loadTrash(); - clearChecked(); + state.checked.clear(); + render(); } function renderBatchBar() { @@ -10597,6 +10610,13 @@ async function init() { if (e.key !== 'Escape') return; if (!$('#settingsPanel').classList.contains('is-open')) return; if (document.querySelector('.modal:not(.is-hidden)')) return; + // When the search box has focus AND a query, the input's own + // handler clears the query (and stops propagation) — let it run. + // Capture phase fires document handlers BEFORE element handlers, + // so without this guard we'd close Settings before the search + // input ever saw the Esc. + const si = $('#settingsSearch'); + if (si && si === document.activeElement && si.value) return; // Don't let the global Esc-fallback handler fire on the same // keystroke — its slideover-close branch would pop the discard // confirm in the background while Settings closes, surprising