From 858649e3c3f64e056d904e5a968f632209a23e0f Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 8 Oct 2026 11:48:16 -0400 Subject: [PATCH] fix: la touche Delete supprime maintenant le bloc vide (comme Backspace) (v7.64.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - onKd : la garde « bloc sans texte → supprimer le bloc » couvre Backspace ET Delete (une seule branche partagée) — Delete vide d'abord le bloc, puis le supprime au coup suivant ; le dernier bloc de la page n'est jamais supprimé (l'éditeur garde un paragraphe) - le vide est jugé avec gtTok(el) (le texte lu à la sauvegarde) et non textContent : une pastille de réaction seule n'empêche plus la suppression - Tests : e2e/v7641_delete_key.spec.js (texte → Delete vide le bloc → Delete supprime le bloc → dernier bloc jamais supprimé) ; 4/4 specs e2e verts, suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.1 (526 chemins) --- CHANGELOG.md | 20 ++++++++ ROADMAP.md | 4 ++ VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- docs/openapi-v2.json | 2 +- e2e/v7641_delete_key.spec.js | 80 ++++++++++++++++++++++++++++++++ static/js/page_editor_scripts.js | 5 +- 8 files changed, 112 insertions(+), 5 deletions(-) create mode 100644 e2e/v7641_delete_key.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ffba989..5f0aade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog - FlowDeck +## v7.64.1 (2026-10-08) — Fix : Delete sur un bloc vide supprime le bloc + +### Fixed + +- **La touche Delete ne supprimait pas le bloc vide** : seul `Backspace` avait la + garde « bloc sans texte → supprimer le bloc ». La branche couvre désormais les + **deux touches** (`onKd`, une seule garde partagée) — Delete vide le bloc puis + le supprime au coup suivant, comme Backspace ; le dernier bloc de la page n'est + jamais supprimé (l'éditeur garde un paragraphe). +- Le vide est jugé avec **`gtTok(el)`** (le texte lu à la sauvegarde) et non + `textContent` : une pastille de réaction seule dans le bloc ne l'empêche plus + d'être supprimé. + +### Tests + +- **e2e** `e2e/v7641_delete_key.spec.js` : texte → Delete (bloc vidé, pas + supprimé) → Delete (bloc supprimé, le précédent reste) → dernier bloc vide + jamais supprimé. + + ## v7.64.0 (2026-10-08) — Commentaires façon Notion : ancres jaunes, tiroir refondu, réactions sur texte ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 88373eb..8b934e7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1459,6 +1459,10 @@ Détails livrés : rouvrir - [x] **Tests** — `tests/test_v764_comments_reactions.py` (8, dont toggle API réel), e2e `e2e/v764_comments_reactions.spec.js` ; `VERSION` + `app/main.py` = 7.64.0 +- [x] **v7.64.1 — Fix Delete sur bloc vide** : la garde « texte vide → supprimer + le bloc » couvre `Backspace` **et** `Delete` (une seule branche dans `onKd`), + vide jugé via `gtTok()` (ignore les pastilles) ; e2e + `e2e/v7641_delete_key.spec.js` --- diff --git a/VERSION b/VERSION index 41f66b4..762011f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.64.0 +7.64.1 diff --git a/WORKLOAD.md b/WORKLOAD.md index 432e92a..292d058 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.64.0 (commentaires façon Notion : ancres jaunes, tiroir refondu, réactions emoji sur texte) | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.64.1 (fix Delete sur bloc vide → suppression du bloc) | **Statut**: EN COURS 🔄 > **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0` ## Avancement Global diff --git a/app/main.py b/app/main.py index 67abd28..b1dd7ad 100644 --- a/app/main.py +++ b/app/main.py @@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.64.0", + version="7.64.1", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 7f0825d..d6213d5 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.64.0" + "version": "7.64.1" }, "paths": { "/auth/register": { diff --git a/e2e/v7641_delete_key.spec.js b/e2e/v7641_delete_key.spec.js new file mode 100644 index 0000000..fadeaa4 --- /dev/null +++ b/e2e/v7641_delete_key.spec.js @@ -0,0 +1,80 @@ +const { test, expect } = require('@playwright/test'); + +/** + * v7.64.1 — Delete sur un bloc vide supprime le bloc (comme Backspace). + * Instance de test attendue sur FD_BASE_URL (défaut 8081). + */ +const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081'; +const USER = process.env.FD_USER || 'e2e@flowdeck.local'; +const PASS = process.env.FD_PASS || 'e2e-secret-123'; + +test.use({ serviceWorkers: 'block' }); + +async function login(page) { + await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); + await page.fill('#email', USER); + await page.fill('#password', PASS); + await page.click('.btn-primary'); + const ok = await page + .waitForURL('**/workspaces', { timeout: 8000 }) + .then(() => true) + .catch(() => false); + if (!ok) { + const resp = await page.request.post(`${FD_BASE}/auth/register`, { + data: { email: USER, password: PASS, name: 'E2E' }, + }); + if (resp.status() !== 409) expect(resp.ok()).toBeTruthy(); + await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' }); + } +} + +test('Delete : vide le bloc puis supprime le bloc vide', async ({ page }) => { + await login(page); + const cookies = await page.context().cookies(); + const csrf = (cookies.find((c) => c.name === 'csrf_token') || {}).value || ''; + const created = await page.request.post( + `${FD_BASE}/board/api/pages?title=Delete key ${Date.now()}`, + { headers: { 'X-CSRF-Token': csrf } } + ); + expect(created.status(), await created.text()).toBeLessThan(400); + const { id } = await created.json(); + const bs = await page.request.post(`${FD_BASE}/board/api/pages/${id}/blocks`, { + headers: { 'X-CSRF-Token': csrf }, + data: { title: 'Delete key', blocks: [ + { id: 'b1', type: 'paragraph', content: 'hello' }, + { id: 'b2', type: 'paragraph', content: 'world' }, + ] }, + }); + expect(bs.status(), await bs.text()).toBeLessThan(400); + await page.goto(`${FD_BASE}/pages/${id}`, { waitUntil: 'domcontentloaded' }); + await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); + + // ── 1. Delete avec du texte : suppression avant le curseur (comportement natif) ── + await page.locator('#_blocksCt [data-bid]').nth(1).click(); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Delete'); + await page.waitForTimeout(200); + const contents = await page.evaluate(() => { + window.E.sync(); + return window.E.blocks.map((b) => b.content); + }); + expect(contents[0]).toBe('hello'); + expect(contents[1].trim()).toBe(''); // bloc vidé (un
résiduel), mais toujours là + + // ── 2. Delete sur le bloc vide : le bloc est supprimé ── + await page.keyboard.press('Delete'); + await page.waitForTimeout(300); + expect(await page.evaluate(() => window.E.blocks.length)).toBe(1); + expect(await page.evaluate(() => window.E.blocks[0].content)).toBe('hello'); + await page.screenshot({ path: 'shots/v7641-delete.png' }); + + // ── 3. dernier bloc vide : jamais supprimé (l'éditeur garde un bloc) ── + await page.locator('#_blocksCt [data-bid]').first().click(); + await page.keyboard.press('Control+a'); + await page.keyboard.press('Delete'); + await page.waitForTimeout(150); + await page.keyboard.press('Delete'); + await page.waitForTimeout(300); + expect(await page.evaluate(() => window.E.blocks.length)).toBe(1); + expect(await page.evaluate(() => window.E.blocks[0].type)).toBe('paragraph'); +}); diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js index 85eaee4..b11696e 100644 --- a/static/js/page_editor_scripts.js +++ b/static/js/page_editor_scripts.js @@ -2417,7 +2417,10 @@ applyAIBlocks(text){ } if(e.key==='ArrowUp'){const _sp=splitCaret(el);if(_sp&&!_sp.before.trim()&&idx>0){e.preventDefault();const prev=this.getEl(this.blocks[idx-1]?.id);if(prev){prev.focus();ce(prev);}}return;} if(e.key==='ArrowDown'){const _sp=splitCaret(el);if(_sp&&!_sp.after.trim()&&idx{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;} + // Backspace / Delete sur un bloc vide → supprimer le bloc (même garde + // pour les deux touches ; le texte restant est lu comme à la sauvegarde + // pour ignorer les annotations .fd-react-chip) + if(e.key==='Backspace'||e.key==='Delete'){if(!(gtTok(el)||'').trim()){this.sync();if(block.type!=='paragraph'){e.preventDefault();this.pushHistory();block.type='paragraph';block.content='';this.render();_rtSync();setTimeout(()=>{const ne=this.getEl(block.id);if(ne)ne.focus();},60);}else if(this.blocks.length>1){e.preventDefault();this.removeBlock(idx);}}return;} if(e.key===' '){const i3=idx;setTimeout(()=>this.checkMd(i3),15);} if((e.ctrlKey||e.metaKey)&&!e.altKey){ if(e.key.toLowerCase()==='z'){e.preventDefault();if(e.shiftKey)this.redo();else this.undo();return;}