diff --git a/CHANGELOG.md b/CHANGELOG.md index c91d50e..9c9dd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index f06905a..30e0d31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.59.2 +7.59.3 diff --git a/app/main.py b/app/main.py index c9340e8..c1841fe 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 5c617ba..fa08625 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.59.2" + "version": "7.59.3" }, "paths": { "/auth/register": { diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js index 837e915..8eafe00 100644 --- a/static/js/page_editor_scripts.js +++ b/static/js/page_editor_scripts.js @@ -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();}, diff --git a/tests/test_v7593_turn_into.py b/tests/test_v7593_turn_into.py new file mode 100644 index 0000000..389993d --- /dev/null +++ b/tests/test_v7593_turn_into.py @@ -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": ""}