diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0aade..2fe6659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog - FlowDeck +## v7.65.0 (2026-10-08) — Comment ancré sur la SÉLECTION de texte (pas le bloc) + retrait du bouton bleu « 💬 Comment » + +### Changed + +- **`commentBlock()` ancre sur la sélection de texte** : la plage vivante dans le + bloc est utilisée d'abord (plage de la toolbar de sélection en repli, le clic + sur le bouton ayant pu la faire disparaître du DOM), sinon tout le bloc. + Les offsets passent par `_srcOffset()` → `anchor_start/anchor_end` = offsets + dans le texte source, donc l'ancre jaune ne couvre que le texte sélectionné + (`_paintAnchors` / réactions inchangés). +- **Retiré le bouton bleu flottant « 💬 Comment »** (`#_commentSelBtn`) qui + apparaissait au-dessus de la sélection : le déclencheur est le bouton 💬 de + la toolbar de sélection, le menu de bloc (💬 Comment, Ctrl+⇧+M) et le raccourci. + `showCommentBtn()`/`hideCommentBtn()` et le listener `mousedown` associés sont + supprimés. +- Fermer le tiroir (`toggleComments`) remet `_commentSel` à zéro : un brouillon + abandonné ne peut plus attacher le commentaire suivant à une ancienne plage. + +### Tests + +- **e2e** `e2e/v764_comments_reactions.spec.js` : sélection d'une **sous-chaîne** + (`reaction a un`) → toolbar 💬 → tiroir → envoi ; assert `#_commentSelBtn` + absent et `.fd-anchor[data-cid]` = **uniquement** la plage sélectionnée + (avant **et** après reload). +- `pytest tests/ -q -n auto` → **1383 passed**, `ruff check app tests` → OK. + + ## v7.64.1 (2026-10-08) — Fix : Delete sur un bloc vide supprime le bloc ### Fixed diff --git a/ROADMAP.md b/ROADMAP.md index 8b934e7..fe98df5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1464,6 +1464,24 @@ Détails livrés : vide jugé via `gtTok()` (ignore les pastilles) ; e2e `e2e/v7641_delete_key.spec.js` +### v7.65.0 — Comment ancré sur la sélection de texte ✅ (2026-10-08) +> **Objectif** : le commentaire porte sur le texte sélectionné dans le bloc, +> pas sur le bloc entier, et le bouton bleu flottant disparaît. **COMPLETED**. + +- [x] **Ancrage sur la plage** — `commentBlock()` prend la sélection vivante dans + le bloc (repli : la plage mémorisée par la toolbar, qui survit au clic du + bouton), sinon tout le bloc ; offsets calculés par `_srcOffset()` → + `anchor_start/anchor_end` exacts dans le texte source +- [x] **Bouton bleu « 💬 Comment » retiré** — `#_commentSelBtn`, + `showCommentBtn()`, `hideCommentBtn()` et leur listener `mousedown` supprimés ; + déclencheurs restants = 💬 de la toolbar de sélection, menu de bloc, + Ctrl+⇧+M +- [x] **Tiroir** — fermer remet `_commentSel` à zéro (pas d'ancrage périmé) +- [x] **Tests** — e2e `e2e/v764_comments_reactions.spec.js` : sous-sélection + `reaction a un` → `.fd-anchor[data-cid]` = la plage seule, avant/après + reload, `#_commentSelBtn` absent ; `pytest` 1383 passed, `ruff` OK ; + `VERSION` + `app/main.py` = 7.65.0 + --- ## ✅ Fonctionnalités livrées hors roadmap (bonus détectés dans le code) diff --git a/VERSION b/VERSION index 762011f..1a97721 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.64.1 +7.65.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index 292d058..dca987b 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.64.1 (fix Delete sur bloc vide → suppression du bloc) | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.65.0 (comment ancré sur la sélection de texte, bouton bleu retiré) | **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 b1dd7ad..d26c21f 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.1", + version="7.65.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/templates/_page_editor_content.html b/app/templates/_page_editor_content.html index 827dc91..97be069 100644 --- a/app/templates/_page_editor_content.html +++ b/app/templates/_page_editor_content.html @@ -578,15 +578,6 @@ background:var(--bg-modal,#1f1f1f);border:1px solid var(--border,#333);border-radius:10px; box-shadow:var(--shadow-modal);padding:6px;"> - - -
{ + const el = document.querySelector('#_blocksCt [data-bid]'); + const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); + let n; + while ((n = w.nextNode())) { + if (n.parentElement && n.parentElement.closest('.fd-react-chip')) continue; + const i = n.nodeValue.indexOf(sub); + if (i < 0) continue; + const r = document.createRange(); + r.setStart(n, i); + r.setEnd(n, i + sub.length); + const s = getSelection(); + s.removeAllRanges(); + s.addRange(r); + el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); + return; + } + throw new Error('sous-chaîne introuvable : ' + sub); + }, sub); + await expect(page.locator('.format-toolbar')).toBeVisible({ timeout: 5000 }); +} + // sélectionne tout le bloc et simule la fin du glisser-souris async function selectBlock(page) { await page.evaluate(() => { @@ -100,10 +124,10 @@ test('réaction emoji + commentaires ancrés', async ({ page }) => { expect(content).toBe('Prise de reaction a un texte'); // la pastille n'est pas du contenu await page.screenshot({ path: 'shots/v764-chip.png' }); - // ── 4. commentaire : bouton 💬 → tiroir refondu → envoi ── - await selectBlock(page); - await expect(page.locator('#_commentSelBtn')).toBeVisible(); - await page.locator('#_commentSelBtn').click(); + // ── 4. commentaire sur une SOUS-sélection : toolbar 💬 → tiroir refondu → envoi ── + await expect(page.locator('#_commentSelBtn')).toHaveCount(0); // bouton bleu retiré + await selectSub(page, 'reaction a un'); + await page.locator('.format-toolbar .ft-item[data-tooltip="Comment — Ctrl+⇧+M"]').click(); const drawer = page.locator('.comments-drawer'); await expect(drawer).toBeVisible(); await drawer.locator('textarea').fill('Ceci est mon premier commentaire'); @@ -115,7 +139,9 @@ test('réaction emoji + commentaires ancrés', async ({ page }) => { await expect(thread.locator('.ct-name')).not.toHaveText(''); await expect(thread.locator('.ct-act')).toHaveCount(2); // ✓ + ⋯ await expect(thread.locator('.ct-body')).toHaveText('Ceci est mon premier commentaire'); + // l'ancre jaune = UNIQUEMENT la plage sélectionnée, pas le bloc await expect(page.locator('#_blocksCt .fd-anchor[data-cid]')).toHaveCount(1); + await expect(page.locator('#_blocksCt .fd-anchor[data-cid]')).toHaveText('reaction a un'); await page.screenshot({ path: 'shots/v764-drawer.png' }); // ── 5. menu ⋯ du fil (Copy link / Delete) ── @@ -140,5 +166,6 @@ test('réaction emoji + commentaires ancrés', async ({ page }) => { await page.reload({ waitUntil: 'domcontentloaded' }); await page.waitForSelector('#_blocksCt [data-bid]', { timeout: 15000 }); await expect(page.locator('#_blocksCt .fd-anchor[data-cid]')).toHaveCount(1, { timeout: 10000 }); + await expect(page.locator('#_blocksCt .fd-anchor[data-cid]')).toHaveText('reaction a un'); await expect(page.locator('#_blocksCt .fd-react-chip')).toHaveCount(1, { timeout: 10000 }); }); diff --git a/static/js/page_editor_scripts.js b/static/js/page_editor_scripts.js index b11696e..2af7cc5 100644 --- a/static/js/page_editor_scripts.js +++ b/static/js/page_editor_scripts.js @@ -1865,11 +1865,24 @@ replaceBlock(idx,type){const b=this.blocks[idx];if(!b)return;this.sync();this.pu 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;});if(v!=null){try{localStorage.setItem('fdLastBlockColor',key+'|'+v);}catch(e){}}this.dirty=true;this.autoSave();this.render();_rtSync();_selClear();}, // ── v7.61 : actions du menu contextuel de bloc ── commentBlock(idx){ + // L'ancre est la SÉLECTION de texte si elle vit dans ce bloc (la + // sélection de la toolbar aussi : le clic sur le bouton peut l'avoir + // fait disparaître du DOM), sinon tout le bloc. const b=this.blocks[idx];if(!b)return;const el=this.getEl(b.id);if(!el)return; - const r=document.createRange();r.selectNodeContents(el); - const s=window.getSelection();s.removeAllRanges();s.addRange(r); - this.showCommentBtn(); - if(!this._commentSel||!this._commentSel.range){this.showToast('Sélectionnez du texte pour commenter','error');return;} + let r=null;const s=window.getSelection(); + if(s&&s.rangeCount&&!s.isCollapsed){ + const rr=s.getRangeAt(0); + if(el.contains(rr.startContainer)&&el.contains(rr.endContainer))r=rr.cloneRange(); + } + if(!r&&this._sel&&this._sel.range&&this._sel.idx===idx + &&el.contains(this._sel.range.startContainer)&&el.contains(this._sel.range.endContainer)){ + r=this._sel.range.cloneRange(); + } + if(!r){r=document.createRange();r.selectNodeContents(el);} + const start=_srcOffset(el,r.startContainer,r.startOffset); + const end=_srcOffset(el,r.endContainer,r.endOffset); + if(!(end>start)){this.showToast('Sélectionnez du texte pour commenter','error');return;} + this._commentSel={range:r,bid:el.dataset.bid,start:start,end:end}; this.commentOnSelection(); }, // v7.62 : renommé — « askAI() » (sans argument, plus bas) l'écrasait, @@ -2607,7 +2620,7 @@ applyAIBlocks(text){ }); }, // ── v4.9.0: Collaboration — comments & mentions ── - toggleComments(){this.commentsOpen=!this.commentsOpen;if(this.commentsOpen)this.loadComments();}, + toggleComments(){if(this.commentsOpen){this.commentsOpen=false;this._commentSel={range:null,bid:null,start:0,end:0};return;}this.commentsOpen=true;this.loadComments();}, async loadComments(){ try{const r=await fetch('/api/pages/'+this.pid+'/comments',{credentials:'same-origin'});const d=await r.json(); this.comments=d.comments||[];this.commentCount=this.comments.length;} @@ -2630,7 +2643,7 @@ applyAIBlocks(text){ headers:{'Content-Type':'application/json','X-CSRF-Token':this.csrfTok()}, body:JSON.stringify({body:text,anchor_block_id:sel.bid||null,anchor_start:sel.bid?sel.start:null,anchor_end:sel.bid?sel.end:null})}); const d=await r.json(); - if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};this.hideCommentBtn();await this.loadComments();this.showToast('Comment added');} + if(d.status==='created'){this.commentDraft='';this._commentSel={range:null,bid:null,start:0,end:0};await this.loadComments();this.showToast('Comment added');} else{this.showToast(d.detail||'Failed to add comment');} }catch(e){this.showToast('Failed to add comment');} }, @@ -2724,32 +2737,9 @@ applyAIBlocks(text){ }, commentOnSelection(){ const sel=this._commentSel;if(!sel.range)return; - const bid=sel.bid||'';this.commentsOpen=true;this.commentDraft=''; - const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none'; + this.commentsOpen=true;this.commentDraft=''; this.$nextTick(()=>{const ta=document.querySelector('.comments-drawer textarea');if(ta)ta.focus();}); }, - showCommentBtn(){ - const s=window.getSelection();if(!s||s.isCollapsed)return; - const ct=document.getElementById('_blocksCt');if(!ct||!ct.contains(s.anchorNode))return; - // ancre = nœud texte ou élément (comme showFmt depuis v7.62) - const anEl=s.anchorNode?(s.anchorNode.nodeType===1?s.anchorNode:s.anchorNode.parentElement):null; - const bel=anEl?anEl.closest('[data-bid]'):null; - if(!bel)return; - const bid=bel.dataset.bid; - const span0=this._selSpan(bel,s.getRangeAt(0)); - const start=span0.start; - const end=span0.end; - if(end<=start)return; - this._commentSel={range:s.getRangeAt(0).cloneRange(),bid:bid,start:start,end:end}; - const r=s.getRangeAt(0).getBoundingClientRect(); - const btn=document.getElementById('_commentSelBtn');if(!btn)return; - btn.style.display='block'; - const bw=btn.offsetWidth||110,bh=btn.offsetHeight||34; - let x=r.left+r.width/2-bw/2,y=r.top-bh-8; - x=Math.min(Math.max(8,x),window.innerWidth-bw-8);if(y<8)y=r.bottom+8; - btn.style.left=x+'px';btn.style.top=y+'px'; - }, - hideCommentBtn(){const btn=document.getElementById('_commentSelBtn');if(btn)btn.style.display='none';}, // ── @mention autocomplete (v4.9.0 users + v5.11.0 pages & dates) ── mentionOpen:false,mentionQuery:'',mentionResults:[],mentionPages:[],mentionEl:null, openMention(el){ @@ -3074,7 +3064,7 @@ applyAIBlocks(text){ }; } // v7.62 : data Alpine du wrapper éditeur — Alpine v3 n'expose plus __x - // (l'ancien accès rendait muets showFmt/hideCommentBtn/mentions). + // (l'ancien accès rendait muets showFmt/mentions). function _xed(el){ if(!el) return null; if(el.__x&&el.__x.$data) return el.__x.$data; return (el._x_dataStack&&el._x_dataStack[0])||null; } // v7.64 : clic sur une ancre jaune → ouvre et centre le fil de discussion ; @@ -3114,9 +3104,8 @@ applyAIBlocks(text){ if(!ct?.contains(s.anchorNode)){d.fmt.open=false;d._sel=null;return;} const bel0=s.anchorNode&&(s.anchorNode.nodeType===1?s.anchorNode:s.anchorNode.parentElement); const bel=bel0?bel0.closest('[data-bid]'):null; - if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);d.showCommentBtn();}else{d.hideCommentBtn();} + if(bel){const idx=d.getIdx(bel.dataset.bid);if(idx>=0)d.showFmt(idx);} },80);}); - document.addEventListener('mousedown',()=>{setTimeout(()=>{const d=_xed(document.querySelector('.page-editor-wrapper'));if(d)d.hideCommentBtn();},10);}); document.addEventListener('keyup',(e)=>{ const d=_xed(document.querySelector('.page-editor-wrapper'));if(!d)return; if(!d.mentionOpen){if(e.key!=='@')return;}