fix: aide (bouton ✕), Insert below qui perdait le format, listes numérotées invisibles (v7.59.2)
- aide : bouton ✕ en haut à droite du panneau (classe partagée settings-close, 44 px mobile) + helpInit.close() (retour en arrière, sinon /workspaces) ; .settings-panel en position:relative pour ancrer hamburger + ✕ au panneau - éditeur « Insert below » (/Write with AI) : insère le markdown source conservé à la génération (AIC._md) au lieu du texte extrait du HTML rendu (_resultText() reste seulement en repli) → titres/listes/gras conservés - listes numérotées : aucun numéro n'était affiché (CSS = retrait seul) ; le rang des blocs numbered_list contigus est calculé au rendu (data-num) et rendu via content: attr(data-num) — /numbered list et l'insertion agent affichent 1, 2, 3 - md2b : cases à cocher - [ ] / - [x] testées avant la branche des puces (aligné sur paste2b) + séparateur 1) accepté (CommonMark) - menu slash : requête tapée dans le bloc (/numbered list) répercutée dans le filtre du menu quand le focus n'a pas rejoint l'entrée - tests : tests/test_v7592_ui_fixes.py (5 tests, dont 1 comportemental node qui exécute le vrai md2b) ; ruff I001 préexistant sur test_agent.py - livraison : VERSION + app/main = 7.59.2, OpenAPI 525 chemins, CHANGELOG
This commit is contained in:
@@ -1,5 +1,34 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.59.2 (2026-10-07) — Corrections : aide, « Insert below », listes numérotées
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Aide** — bouton **✕** en haut à droite du panneau (classe partagée
|
||||
`settings-close` : 32 px desktop / **44 px mobile**) + méthode `close()` dans
|
||||
`help.js` (retour à la page précédente, sinon `/workspaces`) ;
|
||||
`.settings-panel` passé en `position:relative` pour ancrer les boutons au
|
||||
panneau (hamburger + ✕) plutôt qu'à la page.
|
||||
- **Éditeur — « Insert below » (/Write with AI)** : on insère le **markdown
|
||||
source** conservé à la génération (`AIC._md`) au lieu du texte extrait du
|
||||
HTML rendu (`_resultText()` ne reste qu'en repli pour les messages d'erreur) —
|
||||
titres, listes, gras et numéros survivent à l'insertion.
|
||||
- **Listes numérotées** — elles n'avaient **aucun numéro au rendu** : le CSS
|
||||
ne déclarait que le retrait, jamais le marqueur. Le rang des blocs
|
||||
`numbered_list` contigus est désormais calculé au rendu (`data-num`) et
|
||||
affiché via `content: attr(data-num) ". "` → **`/numbered list` et
|
||||
l'insertion depuis l'assistant affichent enfin 1, 2, 3**.
|
||||
- **`md2b`** (markdown → blocs, chemin agent) : les cases à cocher `- [ ]` /
|
||||
`- [x]` sont testées **avant** la branche des puces (une ligne `- [ ]`
|
||||
était avalée par cette branche) — aligné sur l'ordre déjà correct de
|
||||
`paste2b` ; le séparateur `1)` est accepté (CommonMark).
|
||||
- **Menu slash** — une requête tapée **dans le bloc** (`/numbered list`, focus
|
||||
qui n'a pas rejoint l'entrée du menu) est répercutée dans le filtre au lieu
|
||||
d'être ignorée.
|
||||
- **Tests** — `tests/test_v7592_ui_fixes.py` : **5 tests**, dont 1 test
|
||||
comportemental **node** qui exécute le vrai `md2b` extrait de la source.
|
||||
- `ruff` : ordre d'imports de `tests/test_agent.py` (I001 préexistant au pull).
|
||||
|
||||
## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed)
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.59.1",
|
||||
version="7.59.2",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
||||
</button>
|
||||
<button class="settings-close" @click="close()" title="Close" aria-label="Close help">×</button>
|
||||
|
||||
<!-- Mobile drawer backdrop -->
|
||||
<div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.58.0"
|
||||
"version": "7.59.2"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
@@ -1098,7 +1098,7 @@
|
||||
"dashboard"
|
||||
],
|
||||
"summary": "Help Page",
|
||||
"description": "Comprehensive help & documentation page.",
|
||||
"description": "Help & documentation page — settings-style panel with side navigation.",
|
||||
"operationId": "help_page_help_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
||||
+11
-2
@@ -2476,11 +2476,20 @@ button.breadcrumb-current { max-width: 260px; }
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Numbered list */
|
||||
/* Numbered list — data-num est calculé au rendu (rangée contiguë) */
|
||||
.block-numbered {
|
||||
padding-left: 22px;
|
||||
padding-left: 32px;
|
||||
position: relative;
|
||||
}
|
||||
.block-numbered[data-num]::before {
|
||||
content: attr(data-num) ". ";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-width: 24px;
|
||||
color: var(--text-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* To-do */
|
||||
.block-todo {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* ═══ Panel skeleton shared by /settings and /help (Notion-style modal) ═══ */
|
||||
.settings-overlay{position:fixed;inset:0;z-index:500;display:flex;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);}
|
||||
.settings-panel{display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;}
|
||||
.settings-panel{position:relative;display:flex;width:1050px;max-width:98vw;height:85vh;margin:auto;background:var(--bg-primary);border:1px solid var(--border-strong);border-radius:14px;box-shadow:var(--shadow-modal);overflow:hidden;}
|
||||
.settings-nav{width:200px;min-width:200px;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:8px 0;overflow-y:auto;}
|
||||
.settings-nav-header{padding:12px 16px;font-size:13px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
|
||||
.settings-nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;font-size:13px;color:var(--text-secondary);cursor:pointer;transition:background 100ms;border-radius:0;}
|
||||
|
||||
@@ -7,6 +7,11 @@ function helpInit() {
|
||||
section: 'start',
|
||||
navOpen: false,
|
||||
closeNavOnMobile() { this.navOpen = false; },
|
||||
// Bouton ✕ (haut droite, visible aussi en mobile) : ferme l'aide.
|
||||
close() {
|
||||
if (document.referrer && document.referrer !== location.href) { history.back(); }
|
||||
else { location.href = '/workspaces'; }
|
||||
},
|
||||
go(sec) {
|
||||
this.section = sec;
|
||||
this.navOpen = false;
|
||||
|
||||
@@ -419,7 +419,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
// ═══════════ Inline AI composer (Notion AI-style) ═══════════
|
||||
// Space on an empty paragraph → "Edit with AI" pill → Enter → "Brewing…"
|
||||
// → framed result → "Insert below" → applyAIBlocks.
|
||||
const AIC={_open:false,_idx:-1,_stage:'',_html:'',
|
||||
const AIC={_open:false,_idx:-1,_stage:'',_html:'',_md:'',
|
||||
ROOT:null,
|
||||
mk(){ if(this.ROOT) return this.ROOT;
|
||||
const d=document.createElement('div'); d.id='_aiC'; d.style.cssText='position:fixed;z-index:1200;width:420px;max-width:92vw;display:none';
|
||||
@@ -432,7 +432,7 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
this.paint(); this.ROOT.style.display='block'; this._open=true; this._focusIngest();
|
||||
document.addEventListener('mousedown',function onDoc(ev){ if(!self.ROOT.contains(ev.target)){ self.close(); document.removeEventListener('mousedown',onDoc); } });
|
||||
},
|
||||
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; if(window.E)window.E.dirty=true; this._stage=''; },
|
||||
close(){ if(this.ROOT)this.ROOT.style.display='none'; this._open=false; this._html=''; this._md=''; if(window.E)window.E.dirty=true; this._stage=''; },
|
||||
_anchorRect(idx){ const E=window.E; if(!E||!E.blocks)return{left:Math.max(24,window.innerWidth/2-210),top:window.innerHeight/2,bottom:window.innerHeight/2,width:420};
|
||||
const b=E.blocks[idx]; const el=E.getEl(b?b.id:null); if(el){const r=el.getBoundingClientRect();return r;} return {left:24,top:120,bottom:180,width:420}; },
|
||||
_focusIngest(){ const i=this.ROOT.querySelector('.aici-input'); if(i){i.focus();} },
|
||||
@@ -478,12 +478,17 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
const E=window.E; const ctx=(typeof window.fdGetPageContext==='function')?String(window.fdGetPageContext()||''):'';
|
||||
if(!(window.fdAgent&&window.fdAgent.generate)){ this._err('Agent IA non configuré'); return; }
|
||||
window.fdAgent.generate(prompt, ctx).then(function(text){
|
||||
self._html=self._mdToHtml(text||''); self._stage='result'; self.paint();
|
||||
// On conserve le MARKDOWN source : c'est lui qui est inséré (md2b),
|
||||
// pas le texte extrait du HTML rendu qui perd titres/listes/gras.
|
||||
self._md=String(text||'');
|
||||
self._html=self._mdToHtml(self._md); self._stage='result'; self.paint();
|
||||
}).catch(function(err){ self._err((err&&err.message)?err.message:'Erreur'); });
|
||||
},
|
||||
_err(msg){ this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
|
||||
_err(msg){ this._md=''; this._html='<p style="color:#e5484d">⚠ '+esc(msg)+'</p>'; this._stage='result'; this.paint(); },
|
||||
insert(){ const E=window.E; if(!E||typeof E.applyAIBlocks!=='function')return; E.sync();
|
||||
const result=this._resultText(); if(!result)return; E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
|
||||
// Markdown source d'abord ; repli sur le texte visible (erreurs).
|
||||
const result=String(this._md||'').trim()||this._resultText(); if(!result)return;
|
||||
E.applyAIBlocks(result); E.dirty=true; E.autoSave(); this.close(); },
|
||||
_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result'); if(!el)return ''; const r=document.createRange();r.selectNodeContents(el);return r.toString(); },
|
||||
toast(m){ if(window.showToast)window.showToast(m,'info'); }
|
||||
};
|
||||
@@ -595,11 +600,19 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
|
||||
const cls=tag==='div'?`block-content ${b.type==='bulleted_list'?'block-bullet':b.type==='numbered_list'?'block-numbered':b.type==='to_do'?'block-todo':b.type==='toggle'?'block-toggle':b.type==='code'?'block-code':b.type==='callout'?'block-callout':''}`:`block-content block-${tag}`;
|
||||
const style=b.type==='callout'?` style="background:${(b.style&&b.style.bgColor)||'var(--blue-bg)'}${(b.style&&b.style.color)?';color:'+b.style.color:''}"`:((b.style&&(b.style.bgColor||b.style.color))?` style="${(b.style.bgColor?'background:'+b.style.bgColor+';':'')+(b.style.color?'color:'+b.style.color+';':'')}"`:'');
|
||||
const bidAttr=b.type.startsWith('heading_')?` id="h-${(b.id||'').replace(/[^a-zA-Z0-9]/g,'')}"`:'';
|
||||
// Numéro d'ordre : rang des blocs `numbered_list` contigus (1,2,3…) ;
|
||||
// passé en attribut data-num → le CSS le rend hors du contenu éditable.
|
||||
let numAttr='';
|
||||
if(b.type==='numbered_list'&&idx>=0){
|
||||
const bs=(window.E&&window.E.blocks)||[];
|
||||
let s=idx; while(s>0&&bs[s-1]&&bs[s-1].type==='numbered_list')s--;
|
||||
numAttr=` data-num="${idx-s+1}"`;
|
||||
}
|
||||
const actions=idx>=0?`<div class="block-actions"><button class="block-add-btn" onclick="E.addAfter(${idx})">+</button></div>`:'';
|
||||
return `<div class="block-wrapper" data-id="${b.id}">
|
||||
<div class="block-handle"><svg viewBox="0 0 16 16" width="14" height="14"><circle cx="4" cy="3" r="1.3"/><circle cx="11" cy="3" r="1.3"/><circle cx="4" cy="8" r="1.3"/><circle cx="11" cy="8" r="1.3"/><circle cx="4" cy="13" r="1.3"/><circle cx="11" cy="13" r="1.3"/></svg></div>
|
||||
${actions}
|
||||
<${tag}${style}${bidAttr} class="${cls}">${inner}</${tag}>
|
||||
<${tag}${style}${bidAttr}${numAttr} class="${cls}">${inner}</${tag}>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1970,6 +1983,12 @@ applyAIBlocks(text){
|
||||
if(e.key==='Escape'){e.preventDefault();SM.close();return;}
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'||e.key==='Enter'){e.preventDefault();return;}
|
||||
if(e.key==='Backspace'&&!SM._q&&(el.textContent||'').trim()==='/'){e.preventDefault();SM.close();return;}
|
||||
// La requête peut être tapée DANS le bloc (« /numbered list ») si le
|
||||
// focus n'a pas rejoint l'entrée du menu → on la répercute au filtre.
|
||||
if(SM._in&&(e.key.length===1||e.key==='Backspace')){
|
||||
const q=(el.textContent||'').replace(/^\s*\//,'');
|
||||
if(SM._in.value!==q){SM._in.value=q;SM.flt();}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(window.AIAC&&window.AIAC.active){
|
||||
@@ -2486,10 +2505,12 @@ applyAIBlocks(text){
|
||||
else if(t.startsWith('### ')&&!t.startsWith('#### '))bl.push(this.mkB('heading_3',t.substring(4)));
|
||||
else if(t.startsWith('#### '))bl.push(this.mkB('heading_4',t.substring(5)));
|
||||
else if(t==='---'||t==='***')bl.push(this.mkB('divider',''));
|
||||
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
|
||||
else if(/^\d+\. /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+\. /,'')));
|
||||
// AVANT les puces : « - [ ] … » commence aussi par « - » →
|
||||
// sinon la case à cocher de l'agent devenait une puce.
|
||||
else if(t.startsWith('- [ ] ')||t.startsWith('* [ ] '))bl.push(this.mkB('to_do',t.substring(6)));
|
||||
else if(t.startsWith('- [x] ')||t.startsWith('* [x] '))bl.push(this.mkB('to_do',t.substring(6),{checked:true}));
|
||||
else if(/^[-*+] /.test(t))bl.push(this.mkB('bulleted_list',t.substring(2)));
|
||||
else if(/^\d+[.)] /.test(t))bl.push(this.mkB('numbered_list',t.replace(/^\d+[.)] /,'')));
|
||||
else if(t.startsWith('> '))bl.push(this.mkB('quote',t.substring(2)));
|
||||
else bl.push(this.mkB('paragraph',t));
|
||||
}return bl;},
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ def test_mistral_defaults_are_tier_safe(client):
|
||||
"""La clé Mistral plan basique → 403 « tier_not_allowed » sur mistral-large/pixtral-large.
|
||||
Le modèle par défaut et le premier de la liste du test de connexion doivent
|
||||
être des modèles servis par tous les plans."""
|
||||
from app.services.llm_client import PROVIDERS, PROVIDER_MODELS
|
||||
from app.services.llm_client import PROVIDER_MODELS, PROVIDERS
|
||||
tier_blocked = {"mistral-large-latest", "pixtral-large-latest"}
|
||||
assert PROVIDERS["mistral"][1] not in tier_blocked
|
||||
assert PROVIDER_MODELS["mistral"][0] not in tier_blocked
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""7.59.2 — corrections : bouton ✕ de l'aide, « Insert below » qui perd le
|
||||
format, listes numérotées invisibles (agent + /numbered list)."""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
HELP_HTML = ROOT / "app" / "templates" / "help.html"
|
||||
HELP_JS = ROOT / "static" / "js" / "help.js"
|
||||
EDITOR_JS = ROOT / "static" / "js" / "page_editor_scripts.js"
|
||||
APP_CSS = ROOT / "static" / "css" / "app.css"
|
||||
SETTINGS_CSS = ROOT / "static" / "css" / "settings.css"
|
||||
|
||||
|
||||
# ── 1. Aide : fermeture en haut à droite (mobile compris) ───────────────────
|
||||
|
||||
def test_help_page_has_close_button():
|
||||
html = HELP_HTML.read_text(encoding="utf-8")
|
||||
js = HELP_JS.read_text(encoding="utf-8")
|
||||
css = SETTINGS_CSS.read_text(encoding="utf-8")
|
||||
assert 'class="settings-close"' in html # bouton ✕ présent
|
||||
assert '@click="close()"' in html
|
||||
assert "close()" in js and "history.back()" in js # méthode réelle
|
||||
assert ".settings-close{" in css # style partagé (32/44px)
|
||||
assert "position:relative" in css.split(".settings-panel{")[1][:40]
|
||||
|
||||
|
||||
# ── 2. Insert below : on insère le MARKDOWN, pas le texte du HTML rendu ─────
|
||||
|
||||
def test_ai_insert_below_keeps_source_markdown():
|
||||
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||
assert "self._md=String(text||'')" in src # source conservée
|
||||
assert "String(this._md||'').trim()||this._resultText()" in src # md d'abord
|
||||
assert src.count("this._md='';") >= 2 # close() + _err()
|
||||
# l'ancien chemin (texte extrait du DOM rendu) n'est plus que le repli
|
||||
assert "_resultText(){ const el=this.ROOT&&this.ROOT.querySelector('.aici-result')" in src
|
||||
|
||||
|
||||
# ── 3. Listes numérotées : numéros réellement affichés ──────────────────────
|
||||
|
||||
def test_numbered_list_renders_its_number():
|
||||
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||
css = APP_CSS.read_text(encoding="utf-8")
|
||||
assert "let numAttr=''" in src and "data-num=" in src # rangée contiguë → rang
|
||||
assert ".block-numbered[data-num]::before" in css
|
||||
assert 'content: attr(data-num) ". "' in css
|
||||
# md2b accepte « 1. » et « 1) » (CommonMark) pour l'insertion agent
|
||||
assert r"/^\d+[.)] /" in src
|
||||
|
||||
|
||||
def test_slash_query_typed_in_block_is_synced_to_filter():
|
||||
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||
# « /numbered list » tapé dans le bloc → répercuté dans l'entrée du menu
|
||||
assert "SM._in.value=q" in src
|
||||
assert "SM.flt();" in src
|
||||
|
||||
|
||||
# ── md2b : test comportemental sur la source réelle (node) ──────────────────
|
||||
|
||||
def _md2b_source() -> str:
|
||||
src = EDITOR_JS.read_text(encoding="utf-8")
|
||||
i = src.index("md2b(md){")
|
||||
j = src.index("return bl;},", i) + len("return bl;},")
|
||||
snippet = src[i:j].rstrip().rstrip(",") # « md2b(md){…} »
|
||||
return snippet
|
||||
|
||||
|
||||
def test_md2b_converts_numbered_lists_for_real():
|
||||
node = shutil.which("node")
|
||||
if not node:
|
||||
pytest.skip("node absent : test comportemental md2b ignoré")
|
||||
md = ("# Titre\n"
|
||||
"1. premier\n"
|
||||
"2. second\n"
|
||||
"3) troisième\n"
|
||||
"- puce\n"
|
||||
"- [ ] à faire\n"
|
||||
"> citation\n")
|
||||
script = (
|
||||
"function _tblIsData(){return false;}\n"
|
||||
"function _tblSplitLine(){return [];}\n"
|
||||
"const editor={mkB(t,c,e){const o={type:t,content:c||''};"
|
||||
"if(e)Object.assign(o,e);return o;},\n"
|
||||
f"{_md2b_source()}}};\n"
|
||||
f"console.log(JSON.stringify(editor.md2b({json.dumps(md)})));\n"
|
||||
)
|
||||
out = subprocess.run([node, "-e", script], capture_output=True, text=True,
|
||||
timeout=30, encoding="utf-8")
|
||||
assert out.returncode == 0, out.stderr
|
||||
blocks = json.loads(out.stdout)
|
||||
assert [b["type"] for b in blocks] == [
|
||||
"heading_1",
|
||||
"numbered_list", "numbered_list", "numbered_list", # 1. 2. 3)
|
||||
"bulleted_list", "to_do", "quote",
|
||||
]
|
||||
assert [b["content"] for b in blocks[1:4]] == ["premier", "second", "troisième"]
|
||||
Reference in New Issue
Block a user