fix: la touche Delete supprime maintenant le bloc vide (comme Backspace) (v7.64.1)
- 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)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.64.0"
|
||||
"version": "7.64.1"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
|
||||
@@ -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 || '[email protected]';
|
||||
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 <br> 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');
|
||||
});
|
||||
@@ -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<this.blocks.length-1){e.preventDefault();const next=this.getEl(this.blocks[idx+1]?.id);if(next){next.focus();cs(next);}}return;}
|
||||
if(e.key==='Backspace'){if(!(el.textContent||'').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;}
|
||||
// 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;}
|
||||
|
||||
Reference in New Issue
Block a user