fix: « Turn into » du menu contextuel ne fonctionnait pas (v7.59.3)
- cause : la garde de turnInto() faisait _BLOCK_TURN_TYPES.indexOf(type) sur
un tableau d'objets {id,name} → toujours -1 → retour immédiat après
l'ouverture du sous-menu, aucun changement de format
- correction : validation par id (_BLOCK_TURN_TYPES.some(t=>t.id===type))
- même fonction : convertir un texte en To-do / Toggle vidait le contenu
(exclusion par erreur) ; seuls les types sans champ texte (image, embed,
meeting, divider, table_of_contents, database, button, columns) le vident
- tests : tests/test_v7593_turn_into.py (4 tests, dont 1 comportemental node
qui exécute la vraie turnInto() extraite de la source)
- livraison : VERSION + app/main = 7.59.3, OpenAPI 525 chemins, CHANGELOG
This commit is contained in:
@@ -1,5 +1,23 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.59.3 (2026-10-07) — Correction : « Turn into » du menu contextuel
|
||||
|
||||
### Fixed
|
||||
|
||||
- **« Turn into… » ne faisait rien** — la garde validait le type avec
|
||||
`_BLOCK_TURN_TYPES.indexOf(type)` alors que ce tableau contient des **objets**
|
||||
`{id, name}` : `indexOf` d'une chaîne renvoyait toujours `-1` → `turnInto()`
|
||||
retournait immédiatement après l'ouverture du sous-menu. Correction :
|
||||
`_BLOCK_TURN_TYPES.some(t=>t.id===type)`.
|
||||
- **Texte conservé** — convertir un bloc en **To-do** ou **Toggle** vidait le
|
||||
contenu (ces deux types étaient exclus de la conservation par erreur) ; seul
|
||||
un type sans champ texte (`image`, `embed`, `divider`, `button`, `columns`,
|
||||
…) le vide désormais, comme dans `Notion`.
|
||||
- **Tests** — `tests/test_v7593_turn_into.py` : **4 tests**, dont 1
|
||||
comportemental **node** qui exécute la vraie `turnInto()` extraite de la
|
||||
source : conversion effective du type, texte conservé (To-do = étiquette +
|
||||
case décochée), contenu vidé pour un type sans champ texte.
|
||||
|
||||
## v7.59.2 (2026-10-07) — Corrections : aide, « Insert below », listes numérotées
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.59.2",
|
||||
version="7.59.3",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.59.2"
|
||||
"version": "7.59.3"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
|
||||
@@ -1523,7 +1523,7 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu
|
||||
|
||||
// ── v5.10.0: Turn into / couleurs / liens / déplacement ──
|
||||
_blockTargets(idx){const sel=_selIndicesSorted();return(sel.includes(idx))?sel:[idx];},
|
||||
turnInto(idx,type){if(idx<0||idx>=this.blocks.length)return;if(_BLOCK_TURN_TYPES.indexOf(type)<0)return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(type!=='to_do'&&type!=='toggle'&&keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='button'){b.automation_id=0;b.automation_name='';}});
|
||||
turnInto(idx,type){if(idx<0||idx>=this.blocks.length)return;if(!_BLOCK_TURN_TYPES.some(t=>t.id===type))return;const targets=this._blockTargets(idx);const focusId=this.blocks[idx].id;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(b.type===type)return;const keep=(b.content||'').trim();b.type=type;if(keep&&['image','embed','meeting','divider','table_of_contents','database','button','columns'].indexOf(b.type)<0){b.content=keep;}else{b.content='';}if(type==='toggle'){b.expanded=true;if(!Array.isArray(b.children))b.children=[];}if(type==='to_do')b.checked=false;if(type==='table'&&!Array.isArray(b.rows))b.rows=_tblNew(3,3);if(type==='code'&&!b.language)b.language='Plain Text';if(type==='columns'&&(!Array.isArray(b.children)||!b.children.length))b.children=[{id:genId(),type:'paragraph',content:''}];if(type==='button'){b.automation_id=0;b.automation_name='';}});
|
||||
this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();
|
||||
setTimeout(()=>{const bid=this.blocks[idx]?this.blocks[idx].id:focusId;if(type==='table'){_tblFocus(bid,0,0);}else{const el=this.getEl(bid);if(el){el.focus();ce(el);}}},60);},
|
||||
setBlockColor(idx,key,val){if(idx<0||idx>=this.blocks.length)return;const targets=this._blockTargets(idx);const v=(!val||val==='default')?null:val;this.sync();this.pushHistory();targets.forEach(i=>{const b=this.blocks[i];if(!b)return;if(!b.style)b.style={};if(v==null)delete b.style[key];else b.style[key]=v;});this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""7.59.3 — « Turn into » du menu contextuel : garde cassée + texte conservé."""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
EDITOR_JS = ROOT / "static" / "js" / "page_editor_scripts.js"
|
||||
SRC = EDITOR_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _slice(start: str, end: str) -> str:
|
||||
i = SRC.index(start)
|
||||
j = SRC.index(end, i)
|
||||
return SRC[i:j].rstrip()
|
||||
|
||||
|
||||
def test_turn_into_guard_matches_objects_not_strings():
|
||||
# BUG : indexOf() sur un tableau d'objets → toujours -1 → retour immédiat.
|
||||
assert "_BLOCK_TURN_TYPES.indexOf(type)" not in SRC
|
||||
assert "_BLOCK_TURN_TYPES.some(t=>t.id===type)" in SRC
|
||||
|
||||
|
||||
def test_turn_into_menu_wiring():
|
||||
# menu ⋮⋮ → « Turn into » → sous-menu → choix de type
|
||||
assert 'data-bm-act="turn"' in SRC
|
||||
assert "data-bm-act=\\\"turn-type\\\"" in SRC or 'data-bm-act="turn-type"' in SRC
|
||||
assert "act==='turn-type'" in SRC
|
||||
assert "_blockTurnMenuOpen(" in SRC
|
||||
|
||||
|
||||
def test_turn_into_keeps_text_for_to_do_and_toggle():
|
||||
# Convertir un texte en To-do / Toggle ne doit plus effacer le contenu.
|
||||
line = [ln for ln in SRC.splitlines()
|
||||
if ln.strip().startswith("turnInto(idx,type){")][0]
|
||||
assert "type!=='to_do'&&type!=='toggle'" not in line
|
||||
assert "if(keep&&['image','embed','meeting','divider'" in line
|
||||
|
||||
|
||||
# ── Comportement réel : la méthode extraite de la source, exécutée sous node ─
|
||||
|
||||
def test_turn_into_converts_blocks_for_real():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node absent : test comportemental turnInto ignoré")
|
||||
types = _slice("const _BLOCK_TURN_TYPES=[", "];")
|
||||
method = _slice("turnInto(idx,type){", "setBlockColor(idx,key,val){")
|
||||
script = f"""
|
||||
function _rtSync(){{}}
|
||||
function _selClear(){{}}
|
||||
{types}];
|
||||
const E = {{
|
||||
blocks: [{{id:'b1',type:'paragraph',content:'Bonjour'}}],
|
||||
_blockTargets(i){{return [i];}},
|
||||
sync(){{}}, pushHistory(){{}}, dirty:false,
|
||||
autoSave(){{}}, render(){{}}, getEl(){{return null;}},
|
||||
{method}
|
||||
}};
|
||||
E.turnInto(0,'heading_2');
|
||||
const heading = {{type:E.blocks[0].type, content:E.blocks[0].content}};
|
||||
E.turnInto(0,'to_do');
|
||||
const to_do = {{type:E.blocks[0].type, content:E.blocks[0].content, checked:E.blocks[0].checked}};
|
||||
E.turnInto(0,'button'); // type sans texte → contenu vidé
|
||||
const button = {{type:E.blocks[0].type, content:E.blocks[0].content}};
|
||||
console.log(JSON.stringify({{heading:heading, to_do:to_do, button:button}}));
|
||||
"""
|
||||
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
|
||||
timeout=30, encoding="utf-8")
|
||||
assert out.returncode == 0, out.stderr
|
||||
res = json.loads(out.stdout)
|
||||
|
||||
# 1) le changement de format a lieu (avant la correction : aucune conversion)
|
||||
assert res["heading"] == {"type": "heading_2", "content": "Bonjour"}
|
||||
|
||||
# 2) To-do : texte conservé comme étiquette, case décochée
|
||||
assert res["to_do"] == {"type": "to_do", "content": "Bonjour", "checked": False}
|
||||
|
||||
# 3) Button (type sans texte) → contenu vidé
|
||||
assert res["button"] == {"type": "button", "content": ""}
|
||||
Reference in New Issue
Block a user