feat: réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection (v7.67.0)
Tiroir Comments - Section « Reactions » au-dessus des fils : émoji, compte (si >1) et texte ancré lu dans le DOM (reactionList() / _anchorText(), offsets source hors pastilles) ; l'état vide ne s'affiche plus s'il reste des réactions. - Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets intacts). Deux entrées → removeReaction() → POST toggle → loadReactions(). Menu de sélection (maquette ASCII) - Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à droite (.ft-row-split) ; la réaction sort de la ligne 3. - Entête Skills avec ≡ à droite (::after), liste Skills scrollable. - ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied. - Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé. Fix racine (flake de gate) - _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans son except Exception → repli silencieux → AssertionError. - fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) : zéro vrai DNS dans les tests OG. Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ; ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081 (image flowdeck:v7.67.0 rebuild + redéployée).
This commit is contained in:
@@ -2695,6 +2695,30 @@ applyAIBlocks(text){
|
||||
else this.showToast((d&&d.detail)||'Réaction impossible','error');
|
||||
}catch(e){this.showToast('Réaction impossible','error');}
|
||||
},
|
||||
// ── v7.67 : les réactions vivent aussi dans le tiroir Comments ──
|
||||
// (une ligne par plage × émoji, déjà groupées par l'API)
|
||||
reactionList(){
|
||||
return (this.reactions||[]).map(r=>{
|
||||
const bid=r.block_id,start=parseInt(r.anchor_start,10),end=parseInt(r.anchor_end,10);
|
||||
return {key:bid+':'+start+':'+end+':'+r.emoji,bid:bid,start:start,end:end,emoji:r.emoji,
|
||||
count:parseInt(r.count,10)||1,mine:!!parseInt(r.mine,10),
|
||||
text:this._anchorText(bid,start,end)};
|
||||
});
|
||||
},
|
||||
// suppression : POST toggle (le backend retire MA ligne si elle existe)
|
||||
removeReaction(r){if(!r)return;this.toggleReaction(r.bid,r.start,r.end,r.emoji);},
|
||||
// texte source d'une plage ancrée (hors pastilles .fd-react-chip)
|
||||
_anchorText(bid,start,end){
|
||||
const el=this.getEl(bid);if(!el||!(end>start))return '';
|
||||
const w=document.createTreeWalker(el,NodeFilter.SHOW_TEXT);let n,pos=0,txt='';
|
||||
while((n=w.nextNode())){
|
||||
if(n.parentElement&&n.parentElement.closest('.fd-react-chip'))continue;
|
||||
const len=n.nodeValue.length,a=Math.max(start,pos),b=Math.min(end,pos+len);
|
||||
if(b>a)txt+=n.nodeValue.slice(a-pos,b-pos);
|
||||
pos+=len;if(pos>=end)break;
|
||||
}
|
||||
return txt;
|
||||
},
|
||||
openThread(cid){
|
||||
this.commentsOpen=true;this.commentMenu=null;
|
||||
const self=this;
|
||||
@@ -2751,7 +2775,13 @@ applyAIBlocks(text){
|
||||
chip.setAttribute('data-react-bid',g.bid);
|
||||
chip.setAttribute('data-react-start',String(g.start));
|
||||
chip.setAttribute('data-react-end',String(g.end));
|
||||
chip.innerHTML=g.items.map(function(i){return '<span class="fd-react-emo">'+_emojiHtml(i.emoji)+'</span>';}).join('')
|
||||
// chaque émoji porte son data-emoji ; `is-mine` = j'ai réagi →
|
||||
// la croix de suppression (CSS ::after au survol) ne s'affiche
|
||||
// que là, un clic sur les autres rouvre le sélecteur.
|
||||
chip.innerHTML=g.items.map(function(i){
|
||||
const q=String(i.emoji||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<');
|
||||
return '<span class="fd-react-emo'+(parseInt(i.mine,10)?' is-mine':'')+'" data-emoji="'+q+'">'+_emojiHtml(i.emoji)+'</span>';
|
||||
}).join('')
|
||||
+(n>1?'<span class="fd-react-n">'+n+'</span>':'');
|
||||
sp.parentNode.insertBefore(chip,sp.nextSibling);
|
||||
});
|
||||
@@ -3098,6 +3128,12 @@ applyAIBlocks(text){
|
||||
const bid=chip.getAttribute('data-react-bid');
|
||||
const start=parseInt(chip.getAttribute('data-react-start'),10);
|
||||
const end=parseInt(chip.getAttribute('data-react-end'),10);
|
||||
// v7.67 : clic sur la croix d'un émoji à moi → retire MA réaction
|
||||
const emo=t.closest('.fd-react-emo');
|
||||
if(emo&&emo.classList.contains('is-mine')){
|
||||
d.removeReaction({bid:bid,start:start,end:end,emoji:emo.getAttribute('data-emoji')});
|
||||
return;
|
||||
}
|
||||
const el=d.getEl(bid);const rect=chip.getBoundingClientRect();
|
||||
if(!window.FDIconPicker||!el||isNaN(start)||isNaN(end))return;
|
||||
window.FDIconPicker.openFor({
|
||||
|
||||
Reference in New Issue
Block a user