feat: sticky slideover Save + uncategorised-view polish + regression plan
- Slideover Save action row is now sticky at the bottom of the scrolling
body (background + top border) so it stays reachable on entries with
many custom fields or attachments — was previously buried below the
fold.
- Uncategorised view (state.view = 'folder:All'):
- Header title reads "(no folder)" instead of the ambiguous "All".
- Sidebar pseudo-entry stays visible whenever the vault has at least
one real folder, so it can be used as a drag drop-target to move
entries out of a folder even when its own count is 0.
- Empty state gets its own copy ("No uncategorised entries" + hint
to drop entries here to uncategorise) instead of the generic
"Folder is empty".
- TEST_REGRESSION.md checked in — 16-section list scoped to what the
recent Esc / drag / sync / import work touched, so post-commit
regressions can be walked through methodically instead of poking
the app ad-hoc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
+13
-1
@@ -2180,7 +2180,19 @@ body[data-editor-position="center"]:has(#settingsPanel.is-open)::before {
|
|||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
}
|
}
|
||||||
.so-pw-row .so-input:focus { box-shadow: none; }
|
.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 ------------------------------------------ */
|
/* ---- 11. MODALS ------------------------------------------ */
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -2344,6 +2344,7 @@ function viewTitle() {
|
|||||||
if (state.view === 'authenticator') return 'Authenticator';
|
if (state.view === 'authenticator') return 'Authenticator';
|
||||||
if (state.view === 'health') return 'Vault health';
|
if (state.view === 'health') return 'Vault health';
|
||||||
if (state.view === 'audit') return 'Audit log';
|
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('folder:')) return state.view.slice(7);
|
||||||
if (state.view.startsWith('tag:')) return '# ' + state.view.slice(4);
|
if (state.view.startsWith('tag:')) return '# ' + state.view.slice(4);
|
||||||
return 'Items';
|
return 'Items';
|
||||||
@@ -2484,7 +2485,12 @@ function renderSidebar() {
|
|||||||
// No folder icon — visually communicates "this is the absence of a
|
// No folder icon — visually communicates "this is the absence of a
|
||||||
// folder, not a folder". Drag target so users can quickly uncategorise.
|
// folder, not a folder". Drag target so users can quickly uncategorise.
|
||||||
const uncatCount = state.entries.filter(e => !e.folder || e.folder === 'All').length;
|
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 key = 'folder:All';
|
||||||
const item = el('button', {
|
const item = el('button', {
|
||||||
class: 'nav-item is-uncategorized' + (state.view === key ? ' is-active' : ''),
|
class: 'nav-item is-uncategorized' + (state.view === key ? ' is-active' : ''),
|
||||||
@@ -3120,6 +3126,10 @@ function showEmptyState() {
|
|||||||
illustration.setAttribute('href', '#i-empty-vault');
|
illustration.setAttribute('href', '#i-empty-vault');
|
||||||
title.textContent = 'No favorites yet';
|
title.textContent = 'No favorites yet';
|
||||||
msg.innerHTML = 'Click the <b>★</b> on any entry to add it to favorites.';
|
msg.innerHTML = 'Click the <b>★</b> 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:')) {
|
} else if (state.view.startsWith('folder:')) {
|
||||||
illustration.setAttribute('href', '#i-empty-vault');
|
illustration.setAttribute('href', '#i-empty-vault');
|
||||||
title.textContent = 'Folder is empty';
|
title.textContent = 'Folder is empty';
|
||||||
@@ -4161,7 +4171,8 @@ async function batchDelete() {
|
|||||||
toast(ids.length + ' moved to trash');
|
toast(ids.length + ' moved to trash');
|
||||||
await loadEntries();
|
await loadEntries();
|
||||||
await loadTrash();
|
await loadTrash();
|
||||||
clearChecked();
|
state.checked.clear();
|
||||||
|
render(); // full render so sidebar counts (Trash, folders, tags) refresh
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchRestore() {
|
async function batchRestore() {
|
||||||
@@ -4175,7 +4186,8 @@ async function batchRestore() {
|
|||||||
toast(ids.length + ' restored');
|
toast(ids.length + ' restored');
|
||||||
await loadEntries();
|
await loadEntries();
|
||||||
await loadTrash();
|
await loadTrash();
|
||||||
clearChecked();
|
state.checked.clear();
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function batchPermDelete() {
|
async function batchPermDelete() {
|
||||||
@@ -4195,7 +4207,8 @@ async function batchPermDelete() {
|
|||||||
}
|
}
|
||||||
toast(ids.length + ' deleted permanently');
|
toast(ids.length + ' deleted permanently');
|
||||||
await loadTrash();
|
await loadTrash();
|
||||||
clearChecked();
|
state.checked.clear();
|
||||||
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBatchBar() {
|
function renderBatchBar() {
|
||||||
@@ -10597,6 +10610,13 @@ async function init() {
|
|||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
if (!$('#settingsPanel').classList.contains('is-open')) return;
|
if (!$('#settingsPanel').classList.contains('is-open')) return;
|
||||||
if (document.querySelector('.modal:not(.is-hidden)')) 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
|
// Don't let the global Esc-fallback handler fire on the same
|
||||||
// keystroke — its slideover-close branch would pop the discard
|
// keystroke — its slideover-close branch would pop the discard
|
||||||
// confirm in the background while Settings closes, surprising
|
// confirm in the background while Settings closes, surprising
|
||||||
|
|||||||
Reference in New Issue
Block a user