feat: commentaires façon Notion — ancres jaunes, tiroir refondu, réactions emoji sur texte (v7.64.0)
- Réaction sur texte : bouton 🙂+ (« React to selected text ») dans la toolbar de sélection → sélecteur d'émoji EXISTANT en mode réaction (en-tête = texte sélectionné + ✋, onglets masqués, ligne Recent au-dessus de la grille People, Filter… et barre de catégories avec + custom inchangés) - Backend : migration 37 text_reactions (UNIQUE plage × emoji × utilisateur → POST idempotent) + GET/POST /api/pages/{id}/reactions (GET groupé par plage emoji/count/mine, POST toggle, 400 sur plage vide) - Affichage : plage commentée/réagie peinte en jaune (_paintAnchors après render/loadComments/loadReactions), pastille 😀+compte après la plage, clic pastille = rouvrir le sélecteur, clic ancre = ouvrir et centrer le fil - Pastilles = annotations : gtTok/gtMd les ignorent (y compris le chemin rapide innerText) et les offsets _srcOffset/_srcNodeLen les excluent → le texte sauvé est identique (vérifié dans le e2e) - Tiroir refondu : carte de fil (avatar, nom, horodatage, ✓ toggle resolve/reopen, ⋯ → Copy link #cmt-<id>/Delete), saisie « Add a comment... » avec @ et envoi ↑/Entrée ; #cmt-<id> ouvre et centre le fil au chargement ; loadComments + loadReactions à l'ouverture (commentCount enfin renseigné) - Fixes : resolveComment ne pouvait pas rouvrir (toujours resolved:true) ; showCommentBtn ignorait les sélections à ancre élément (bouton 💬 jamais affiché) - Tests : tests/test_v764_comments_reactions.py (8, dont toggle API réel), e2e/v764_comments_reactions.spec.js (8 étapes) — 3/3 specs e2e verts, suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.0 (526 chemins)
This commit is contained in:
@@ -16,6 +16,9 @@
|
||||
|
||||
<style>
|
||||
.fd-icon-picker{position:fixed;top:0;left:0;width:344px;background:var(--bg-modal);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.28);z-index:2200;display:flex;flex-direction:column;max-height:440px;overflow:hidden;padding:0;}
|
||||
.fd-icon-picker .fdip-title{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);}
|
||||
.fd-icon-picker .fdip-title-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:600;color:var(--text-primary);}
|
||||
.fd-icon-picker .fdip-title-grip{opacity:.6;font-size:14px;}
|
||||
.fd-icon-picker .fdip-tabs{display:flex;align-items:center;gap:2px;padding:8px 10px 6px;border-bottom:1px solid var(--border);}
|
||||
.fd-icon-picker .fdip-tab{background:transparent;border:none;color:var(--text-secondary);font-size:13px;padding:5px 10px;border-radius:6px;cursor:pointer;}
|
||||
.fd-icon-picker .fdip-tab:hover{background:var(--bg-hover);}
|
||||
@@ -70,7 +73,11 @@
|
||||
@click.outside="$store.fdIconPicker.customModal || $store.fdIconPicker.close()"
|
||||
@keydown.escape.window="$store.fdIconPicker.close()"
|
||||
:style="'top:' + $store.fdIconPicker.y + 'px;left:' + $store.fdIconPicker.x + 'px'">
|
||||
<div class="fdip-tabs">
|
||||
<div class="fdip-title" x-show="$store.fdIconPicker.title" x-cloak>
|
||||
<span class="fdip-title-text" x-text="$store.fdIconPicker.title"></span>
|
||||
<span class="fdip-title-grip" title="Drag">✋</span>
|
||||
</div>
|
||||
<div class="fdip-tabs" x-show="!$store.fdIconPicker.title">
|
||||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'emoji' }" @click="$store.fdIconPicker.setTab('emoji')">Emoji</button>
|
||||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'icons' }" @click="$store.fdIconPicker.setTab('icons')">Icons</button>
|
||||
<button class="fdip-tab" :class="{ active: $store.fdIconPicker.tab === 'upload' }" @click="$store.fdIconPicker.setTab('upload')">Upload</button>
|
||||
@@ -93,6 +100,15 @@
|
||||
<div class="fdip-body">
|
||||
<template x-if="$store.fdIconPicker.tab === 'emoji'">
|
||||
<div>
|
||||
<!-- v7.64 : mode réaction — les récents au-dessus de la grille (façon Notion) -->
|
||||
<div x-show="$store.fdIconPicker.title && $store.fdIconPicker.category !== 'recent' && !$store.fdIconPicker.query && $store.fdIconPicker.recent.length">
|
||||
<div class="fdip-section">Recent</div>
|
||||
<div class="fdip-grid">
|
||||
<template x-for="e in $store.fdIconPicker.recent.slice(0, 8)" :key="'rt' + e">
|
||||
<button class="fdip-emoji" @click="$store.fdIconPicker.pick($store.fdIconPicker.withTone(e))" x-text="$store.fdIconPicker.withTone(e)"></button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fdip-section" x-show="$store.fdIconPicker.category === 'recent' && !$store.fdIconPicker.query">Recent</div>
|
||||
<div class="fdip-grid">
|
||||
<template x-for="e in $store.fdIconPicker.items()" :key="e">
|
||||
|
||||
@@ -587,15 +587,15 @@
|
||||
💬 Comment
|
||||
</button>
|
||||
|
||||
<!-- ═══════════ Comments drawer (v4.9.0) ═══════════ -->
|
||||
<!-- ═══════════ Comments drawer (v4.9.0 · refonte v7.64 façon Notion) ═══════════ -->
|
||||
<div class="comments-drawer" x-show="commentsOpen" x-cloak x-transition
|
||||
style="position:fixed;top:var(--topbar-height,44px);right:0;bottom:0;width:320px;max-width:92vw;
|
||||
style="position:fixed;top:var(--topbar-height,44px);right:0;bottom:0;width:340px;max-width:92vw;
|
||||
background:var(--bg-primary,#1c1c1c);border-left:1px solid var(--border,#333);
|
||||
box-shadow:-8px 0 30px rgba(0,0,0,.25);z-index:900;display:flex;flex-direction:column;">
|
||||
<div class="comments-drawer-header"
|
||||
style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;
|
||||
border-bottom:1px solid var(--border,#333);font-weight:600;font-size:14px;">
|
||||
<span>Comments</span>
|
||||
<span>Comments <span style="color:var(--text-dim);font-weight:400;" x-text="'(' + commentCount + ')'"></span></span>
|
||||
<button type="button" class="topbar-btn" @click="toggleComments()" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="comments-drawer-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
|
||||
@@ -603,34 +603,43 @@
|
||||
No comments yet. Select some text and click 💬 Comment.
|
||||
</div>
|
||||
<template x-for="c in comments" :key="c.id">
|
||||
<div class="comment-thread" style="margin-bottom:18px;"
|
||||
:style="c.anchor_block_id ? 'border-left:3px solid var(--accent);padding-left:10px;' : ''">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
|
||||
<div style="width:22px;height:22px;border-radius:50%;background:#3A3A3A;color:#fff;
|
||||
display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;"
|
||||
<div class="comment-thread" :data-thread="c.id"
|
||||
:class="{ 'is-resolved': c.resolved, 'has-anchor': !!c.anchor_block_id }">
|
||||
<div class="ct-head">
|
||||
<div class="ct-avatar"
|
||||
:style="{ background: c.author && c.author.avatar_color ? c.author.avatar_color : '#3A3A3A' }">
|
||||
<span x-text="(c.author ? (c.author.full_name || c.author.login || '?') : '?').charAt(0).toUpperCase()"></span>
|
||||
</div>
|
||||
<span style="font-size:12px;font-weight:600;" x-text="c.author ? (c.author.full_name || c.author.login) : 'Unknown'"></span>
|
||||
<span style="font-size:11px;color:var(--text-dim,#999);margin-left:auto;" x-text="fmtTime(c.created_at)"></span>
|
||||
</div>
|
||||
<div style="font-size:13px;white-space:pre-wrap;margin-bottom:6px;" x-text="c.body"></div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn-sm" style="font-size:11px;" @click="resolveComment(c.id)"
|
||||
x-text="c.resolved ? '↪ Reopen' : '✔ Resolve'"></button>
|
||||
<button class="btn-sm" style="font-size:11px;" x-show="c.user_id === currentUserId"
|
||||
@click="deleteComment(c.id)">🗑 Delete</button>
|
||||
<span class="ct-name" x-text="c.author ? (c.author.full_name || c.author.login) : 'Unknown'"></span>
|
||||
<span class="ct-time" x-text="fmtTime(c.created_at)"></span>
|
||||
<div class="ct-actions">
|
||||
<button type="button" class="ct-act" :title="c.resolved ? 'Reopen' : 'Resolve'"
|
||||
@click="resolveComment(c)"
|
||||
:class="{ 'is-on': c.resolved }">✓</button>
|
||||
<button type="button" class="ct-act" title="More"
|
||||
x-show="c.user_id === currentUserId"
|
||||
@click.stop="commentMenu = commentMenu === c.id ? null : c.id">⋯</button>
|
||||
</div>
|
||||
<div class="ct-menu" x-cloak x-show="commentMenu === c.id" @click.outside="commentMenu = null">
|
||||
<button type="button" class="ct-menu-item" @click="copyCommentLink(c.id)">🔗 Copy link</button>
|
||||
<button type="button" class="ct-menu-item is-danger" @click="deleteComment(c.id)">🗑 Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ct-body" x-text="c.body"></div>
|
||||
<div class="ct-anchor-label" x-show="!!c.anchor_block_id && !c.resolved">💬 on selected text</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="comments-drawer-input" style="border-top:1px solid var(--border,#333);padding:12px 16px;">
|
||||
<textarea x-model="commentDraft" rows="2"
|
||||
placeholder="Add a comment... Use @ to mention someone."
|
||||
style="width:100%;background:var(--bg-secondary,#2a2a2a);border:1px solid var(--border,#333);
|
||||
border-radius:8px;color:var(--text);font-size:13px;padding:8px 10px;resize:none;outline:none;"></textarea>
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
|
||||
<button class="btn btn-primary" style="font-size:12px;padding:6px 14px;" @click="addPageComment()">Comment</button>
|
||||
<div class="comments-drawer-input">
|
||||
<div class="ct-input-row">
|
||||
<textarea x-model="commentDraft" rows="1"
|
||||
placeholder="Add a comment..."
|
||||
@keydown.enter.exact.prevent="addPageComment()"></textarea>
|
||||
<div class="ct-input-actions">
|
||||
<button type="button" class="ct-act" title="Mention" @click="commentAt()">@</button>
|
||||
<button type="button" class="ct-send" title="Send"
|
||||
:disabled="!(commentDraft || '').trim()" @click="addPageComment()">↑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,6 +675,7 @@
|
||||
<div class="ft-row">
|
||||
<button type="button" class="ft-item" @click="fmtLink()" data-tooltip="Link">{{ fd_icon("link",15) }}</button>
|
||||
<button type="button" class="ft-item" @click="fmtHighlight()" data-tooltip="Highlight"><span class="ft-hl">S</span></button>
|
||||
<button type="button" class="ft-item ft-react" @click="fmtReact()" data-tooltip="React to selected text"><span class="ft-react-ico">🙂<span class="ft-react-plus">+</span></span></button>
|
||||
<button type="button" class="ft-item ft-code-ico" @click="fmtCodeBlock()" data-tooltip="Code block"></></button>
|
||||
<button type="button" class="ft-item" @click="fmtEquation()" data-tooltip="Equation">√x</button>
|
||||
<button type="button" class="ft-item ft-more" @click="fmtMore($event)" data-tooltip="More actions">…</button>
|
||||
|
||||
Reference in New Issue
Block a user