feat: menu + — Design System – Canevas — phase 3/8 (v7.53.0)
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / test (push) Failing after 14m56s
FlowDeck CI / docker (push) Skipped

- Section « Design System – Canevas » : liste des canevas (intégrés +
  personnels, GET /board/api/page-templates) puis page de détail avec
  « Créer une page à partir du canevas », « Insérer dans le document ouvert »
  (désactivé sans document ouvert) et « Enregistrer le document ouvert comme
  canevas ».
- GET /board/api/page-templates/{id}/blocks (nouvelle route) : blocs d'un
  canevas — intégré via template_id=0&key=, sinon id ; 404 clé/id inconnu.
  Insertion côté front via ensureBlockIds() global + concat dans E.blocks
  + autoSave/render.
- Canevas « Design System » ajouté aux builtins (callout tokens, TOC, toggles
  composants, grille, checklist revue UI, citation).
- Fix : le front du panneau appelait /api/page-templates (404 silencieux) —
  le router est sous prefix=/board → /board/api/page-templates.
- Piège : pour /use la clé builtin se passe DANS LE BODY (body.key), pas en
  query string — test garde-fou ajouté.
- Tests : tests/test_v753_canvases.py (5) + test_v751 adapté. Suite complète
  1285 verts (-n auto), ruff 0, eslint 0 problème. OpenAPI 516 chemins.
This commit is contained in:
2026-10-06 21:59:34 -04:00
parent 18a2d45d46
commit a68e8848ea
11 changed files with 380 additions and 23 deletions
+39
View File
@@ -1,5 +1,44 @@
# Changelog - FlowDeck # Changelog - FlowDeck
## v7.53.0 (2026-10-06) — Menu + : Design System – Canevas (phase 3/8)
### Added
- **Section « Design System – Canevas »** — liste des canevas
(`GET /board/api/page-templates` : intégrés + personnels), puis une page de
détail avec 3 actions :
- **Créer une page à partir du canevas** (`POST …/0/use` avec `{key}` *dans le
body* pour un intégré, `POST …/{id}/use` pour un personnel) ;
- **Insérer dans le document ouvert** — `GET …/blocks` (nouvelle route) →
`ensureBlockIds()` (global côté éditeur) → concaténation dans `E.blocks` +
`autoSave/render`, désactivé si aucun document éditable n'est ouvert ;
- **Enregistrer le document ouvert comme canevas**
(`POST /board/api/page-templates` avec `page_id`).
- **`GET /board/api/page-templates/{template_id}/blocks`** — blocs d'un canevas
( intégré via `template_id=0&key=`, sinon id) ; 404 clé/id inconnu,
500 si le JSON est corrompu. **OpenAPI : 516 chemins**, `info.version` 7.53.0.
- **Canevas « Design System »** intégré (`design_system` dans
`app/services/block_templates.py`) : callout tokens, TOC, 3 toggles
composants, grille & espacement, checklist revue UI, citation.
- **Navigation** : en-tête ← gère le niveau canevas → liste.
### Fixed
- **Prefixe des canevas** : le front du panneau utilisait `/api/page-templates`
(404 silencieux) — la route réelle est `/board/api/page-templates` (router
`prefix="/board"`), comme `base.html` le fait déjà.
### Tests
- `tests/test_v753_canvases.py` — **5 tests** : canevas `design_system` listé,
route blocks intégré (sans `id`, 404 clé inconnue, `empty` par défaut),
route blocks personnel + 404, création de page depuis le canevas (blocs lus
en base, ids assignés), câblage des sections (3 « bientôt » restantes) +
garde-fou « `key` dans le body ». `test_v751` adapté (3 sections désactivées).
- **Suite complète : 1285 passed / 0 failed** (`-n auto`), `ruff` 0,
`eslint` 0 problème.
## v7.52.0 (2026-10-06) — Menu + : Compétences-skills « Gérer » + « Parcourir » (phase 2/8) ## v7.52.0 (2026-10-06) — Menu + : Compétences-skills « Gérer » + « Parcourir » (phase 2/8)
### Added ### Added
+27 -13
View File
@@ -284,7 +284,7 @@ Propriétés custom, AI keywords, sync API, 12 tables DB
--- ---
## v7.51.0 → v7.58.0 — Menu + de l'assistant : hub de contexte (phases 1-2 ✅ 2026-10-06 · phases 3-8 planifiées) ## v7.51.0 → v7.58.0 — Menu + de l'assistant : hub de contexte (phases 1-3 ✅ 2026-10-06 · phases 4-8 planifiées)
> **Objectif** : faire du bouton **+** du panneau agent (à gauche de la zone d'édition) > **Objectif** : faire du bouton **+** du panneau agent (à gauche de la zone d'édition)
> un menu à sections, tel que demandé : > un menu à sections, tel que demandé :
@@ -366,16 +366,29 @@ CHANGELOG). Chiffrage : S < ½ j · M = 1–2 j · L = 3–5 j.
- [x] **Livraison** — `VERSION` + `app/main.py` = **7.52.0**, OpenAPI régénéré - [x] **Livraison** — `VERSION` + `app/main.py` = **7.52.0**, OpenAPI régénéré
(515 chemins), CHANGELOG, ce ROADMAP (515 chemins), CHANGELOG, ce ROADMAP
### Phase 3 — v7.53.0 — Design System – Canevas · effort M ### Phase 3 — v7.53.0 — Design System – Canevas · effort M ✅ (livrée 2026-10-06)
- [ ] **Sous-menu Canevas** — `GET /api/page-templates` (built-ins + personnels) - [x] **Sous-menu Canevas** — `GET /board/api/page-templates` (built-ins **+
- [ ] **Deux actions** — « Créer une page à partir du canevas » personnels**) ; **piège confirmé** : le router est sous `prefix="/board"` →
(`POST /api/page-templates/{id}/use`) et « Insérer dans le document ouvert » l'URL réelle est `/board/api/page-templates`, pas `/api/page-templates`
(blocs poussés dans l'éditeur quand un document est ouvert) (404 silencieux corrigé dans le front)
- [ ] **Canevas « design system »** — presets maison dans `block_templates.py` - [x] **Deux actions (3 en réalité)** — page de détail par canevas :
(callout, TOC, colonnes, grille de composants) réutilisant `design-tokens.css` « Créer une page à partir du canevas » (`POST …/0/use` avec `{key}` **dans le
- [ ] **Sauver le document ouvert comme canevas** — `POST /api/page-templates` existe body** — la clé en query est ignorée par la route), « Insérer dans le document
- [ ] **Tests** — création, insertion dans l'éditeur, sauvegarde ouvert » (`GET …/blocks` **nouvelle route** → `ensureBlockIds()` global →
concat dans `E.blocks` + `autoSave/render`, désactivé sans document ouvert)
et « Enregistrer le document ouvert comme canevas » (`POST` + `page_id`)
- [x] **Canevas « design system »** — preset `design_system` dans
`block_templates.py` (callout tokens, TOC, toggles composants, grille,
checklist revue UI) ; `design-tokens.css` référencé dans le callout
- [x] **Sauver le doc ouvert comme canevas** — `POST /board/api/page-templates`
(existante) branchée depuis le menu, nom via `prompt()` natif
- [x] **Tests** — `tests/test_v753_canvases.py` : **5 tests** (preset listé,
route blocks intégré/personnel + 404, création de page vérifiée en base avec
ids, câblage 3 « bientôt » + garde-fou `key`-dans-le-body) ; `test_v751`
adapté ; **suite complète 1285 verts**, `ruff` 0, `eslint` 0 problème
- [x] **Livraison** — `VERSION` + `app/main.py` = **7.53.0**, OpenAPI régénéré
(516 chemins), CHANGELOG, ce ROADMAP
### Phase 4 — v7.54.0 — Mémoire de l'agent · effort M ### Phase 4 — v7.54.0 — Mémoire de l'agent · effort M
@@ -429,9 +442,10 @@ CHANGELOG). Chiffrage : S < ½ j · M = 1–2 j · L = 3–5 j.
--- ---
*Plan produit le 2026-10-06 à partir du code réel — **phases 1 et 2 livrées le *Plan produit le 2026-10-06 à partir du code réel — **phases 1 à 3 livrées le
2026-10-06 (v7.51.0, v7.52.0)**, phases 3-8 à valider une par une avant « go ». 2026-10-06 (v7.51.0, v7.52.0, v7.53.0)**, phases 4-8 à valider une par une avant
Chaque phase se clôt par tests verts, bump de version, CHANGELOG et ROADMAP à jour.* « go ». Chaque phase se clôt par tests verts, bump de version, CHANGELOG et
ROADMAP à jour.*
--- ---
+1 -1
View File
@@ -1 +1 @@
7.52.0 7.53.0
+1 -1
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI( app = FastAPI(
title="FlowDeck", title="FlowDeck",
version="7.52.0", version="7.53.0",
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc", redoc_url="/redoc",
lifespan=lifespan, lifespan=lifespan,
+34
View File
@@ -226,4 +226,38 @@ def use_page_template(request: Request, template_id: int, body: dict = Body(defa
return {"status": "ok", "id": page_id, "title": title or name} return {"status": "ok", "id": page_id, "title": title or name}
@router.get("/api/page-templates/{template_id}/blocks")
def page_template_blocks(request: Request, template_id: int, key: str = ""):
"""v7.53.0 : blocs d'un canevas, pour l'insérer dans le document ouvert.
Builtin : ``template_id=0`` + ``?key=`` (même convention que ``/use``).
Canevas personnel : son ``id``. Les blocs builtin sont sans ``id`` — le
front appelle ``ensureBlockIds()`` (déjà global côté éditeur).
"""
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if template_id == 0:
from app.services.block_templates import blocks_json_for
raw = blocks_json_for(key or "empty")
if raw is None:
raise HTTPException(404, "Unknown built-in template")
else:
uid = (user or {}).get("id")
with get_conn() as conn:
row = conn.execute(
"SELECT blocks_json FROM page_global_templates "
"WHERE id=? AND (created_by IS NULL OR created_by=?)",
(template_id, uid),
).fetchone()
if not row:
raise HTTPException(404, "Template not found")
raw = row["blocks_json"]
try:
blocks = json.loads(raw)
except (json.JSONDecodeError, TypeError) as exc:
raise HTTPException(500, "Template content corrupted") from exc
if not isinstance(blocks, list):
raise HTTPException(500, "Template content corrupted")
return {"blocks": blocks}
# ── Core helpers ── # ── Core helpers ──
+28
View File
@@ -86,6 +86,34 @@ BUILTIN_TEMPLATES: dict[str, dict] = {
_b("bulleted_list"), _b("bulleted_list"),
], ],
}, },
"design_system": {
"name": "Design System",
"icon": "🎨",
"description": "Tokens, composants et grille — canevas de référence UI.",
"blocks": [
_b("heading_1", "Design System"),
_b("callout",
"Tokens : couleur, espacement, typographie, rayon, ombres "
"(static/css/design-tokens.css).",
icon="🎨"),
_b("table_of_contents"),
_b("heading_2", "Composants"),
_b("toggle", "Boutons & actions", expanded=False,
children=[_b("paragraph", "Primaire · secondaire · danger — états idle, hover, focus, disabled.")]),
_b("toggle", "Cartes & panneaux", expanded=False,
children=[_b("paragraph", "Élévation, rayon, bordure, padding, zones de clic.")]),
_b("toggle", "Formulaires", expanded=False,
children=[_b("paragraph", "Champs, labels, hints, erreurs, focus visible.")]),
_b("heading_2", "Grille & espacement"),
_b("bulleted_list", "Base 4 px — pas d'espacement hors échelle."),
_b("bulleted_list", "Colonnes : 12 / 8 / 4 selon le point de rupture."),
_b("heading_2", "Revue UI"),
_b("to_do", "Contraste AA vérifié", checked=False),
_b("to_do", "États hover / focus / disabled", checked=False),
_b("to_do", "Responsive mobile", checked=False),
_b("quote", "Une décision de design vaut mieux qu'une justification après coup."),
],
},
} }
+6 -4
View File
@@ -1,10 +1,12 @@
# V74 — Menu + de l'assistant : hub de contexte (design) # V74 — Menu + de l'assistant : hub de contexte (design)
> **Statut** : design — **phases 1 et 2 livrées (v7.51.0, v7.52.0, 2026-10-06)** ; > **Statut** : design — **phases 1 à 3 livrées (v7.51.0 → v7.53.0, 2026-10-06)** ;
> phases 3-8 restent à livrer. Adaptations faites en cours de route : le parcours > phases 4-8 restent à livrer. Adaptations faites en cours de route : le parcours
> utilise `GET /api/nav/menu?parent_id=` (un niveau par appel, contrat > utilise `GET /api/nav/menu?parent_id=` (un niveau par appel, contrat
> dossier = `icon === 'folder'`) et la gestion des skills est une **section du menu > dossier = `icon === 'folder'`), la gestion des skills est une **section du menu
> avec formulaire intégré** (pas une modale) — `PATCH /api/agent/skills/{id}` ajouté. > avec formulaire intégré** (pas une modale — `PATCH /api/agent/skills/{id}`
> ajouté), et les canevas vivent sous **`/board/api/page-templates`** (prefix
> `/board`) avec la nouvelle route `GET …/{id}/blocks` pour l'insertion.
> **Plan phasé** : `ROADMAP.md` → section « v7.51.0 → v7.58.0 — Menu + de l'assistant ». > **Plan phasé** : `ROADMAP.md` → section « v7.51.0 → v7.58.0 — Menu + de l'assistant ».
> **Date** : 2026-10-06 · Version cible de départ : **v7.51.0** (phase 1). > **Date** : 2026-10-06 · Version cible de départ : **v7.51.0** (phase 1).
+52 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "FlowDeck", "title": "FlowDeck",
"version": "7.52.0" "version": "7.53.0"
}, },
"paths": { "paths": {
"/auth/register": { "/auth/register": {
@@ -3706,6 +3706,57 @@
} }
} }
}, },
"/board/api/page-templates/{template_id}/blocks": {
"get": {
"tags": [
"board"
],
"summary": "Page Template Blocks",
"description": "v7.53.0 : blocs d'un canevas, pour l'insérer dans le document ouvert.\n\nBuiltin : ``template_id=0`` + ``?key=`` (même convention que ``/use``).\nCanevas personnel : son ``id``. Les blocs builtin sont sans ``id`` — le\nfront appelle ``ensureBlockIds()`` (déjà global côté éditeur).",
"operationId": "page_template_blocks_board_api_page_templates__template_id__blocks_get",
"parameters": [
{
"name": "template_id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"title": "Template Id"
}
},
{
"name": "key",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "",
"title": "Key"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/board/library": { "/board/library": {
"get": { "get": {
"tags": [ "tags": [
+117 -1
View File
@@ -42,7 +42,7 @@
{key:'connectors', icon:'🔌', label:'Connecteurs', {key:'connectors', icon:'🔌', label:'Connecteurs',
sub:'Parcourir / connecteur personnalisé — bientôt (phase 5)', disabled:true}, sub:'Parcourir / connecteur personnalisé — bientôt (phase 5)', disabled:true},
{key:'canvases', icon:'🎨', label:'Design System – Canevas', {key:'canvases', icon:'🎨', label:'Design System – Canevas',
sub:'Galerie de canevas — bientôt (phase 3)', disabled:true}, sub:'Galerie de canevas : créer, insérer, enregistrer', action:'canvases', chev:true},
{key:'plugins', icon:'🧩', label:'Add plugins', {key:'plugins', icon:'🧩', label:'Add plugins',
sub:'Modules on/off — bientôt (phase 8)', disabled:true}, sub:'Modules on/off — bientôt (phase 8)', disabled:true},
{key:'memory', icon:'🧠', label:'Mémoire (on/off)', {key:'memory', icon:'🧠', label:'Mémoire (on/off)',
@@ -73,6 +73,7 @@
plusBrowse: [], plusCrumb: [], plusBrowse: [], plusCrumb: [],
skillForm: {id: null, name: '', description: '', prompt_template: ''}, skillForm: {id: null, name: '', description: '', prompt_template: ''},
galleryFilter: '', galleryFilter: '',
canvases: [], canvas: null,
cmdFocus: -1, slashDismiss: false, cmdFocus: -1, slashDismiss: false,
toastMsg: '', toastErr: false, _toastTimer: null, toastMsg: '', toastErr: false, _toastTimer: null,
_livePh: null, _livePh: null,
@@ -1125,6 +1126,8 @@
if(s === 'skills-manage') return 'Gérer les compétences'; if(s === 'skills-manage') return 'Gérer les compétences';
if(s === 'skills-gallery') return 'Parcourir les compétences'; if(s === 'skills-gallery') return 'Parcourir les compétences';
if(s === 'skills-edit') return this.skillForm.id ? 'Modifier le skill' : 'Nouveau skill'; if(s === 'skills-edit') return this.skillForm.id ? 'Modifier le skill' : 'Nouveau skill';
if(s === 'canvases') return 'Design System – Canevas';
if(s === 'canvas') return this.canvas ? this.canvas.name : 'Canevas';
return 'Ajouter au contexte'; return 'Ajouter au contexte';
}, },
get plusItems(){ get plusItems(){
@@ -1135,6 +1138,8 @@
if(s === 'skills-manage') return this._manageItems(); if(s === 'skills-manage') return this._manageItems();
if(s === 'skills-gallery') return this._galleryItems(); if(s === 'skills-gallery') return this._galleryItems();
if(s === 'skills-edit') return []; // formulaire, pas de liste if(s === 'skills-edit') return []; // formulaire, pas de liste
if(s === 'canvases') return this._canvasItems();
if(s === 'canvas') return this._canvasActionItems();
return FD_PLUS_MENU; return FD_PLUS_MENU;
}, },
// Sous-menu Compétences : builtin (alias affiché) + installés, puis les 2 actions. // Sous-menu Compétences : builtin (alias affiché) + installés, puis les 2 actions.
@@ -1187,6 +1192,106 @@
if(!out.length) out.push({key:'g-none', _sep: f ? 'Aucun résultat' : 'Galerie vide'}); if(!out.length) out.push({key:'g-none', _sep: f ? 'Aucun résultat' : 'Galerie vide'});
return out; return out;
}, },
// ── Design System – Canevas (v7.53.0) ──
fetchCanvases(){
var self = this;
fetch('/board/api/page-templates').then(function(r){ return r.json(); })
.then(function(d){ self.canvases = d.templates || []; })
.catch(function(){ self.canvases = []; });
},
_canvasItems(){
return (this.canvases || []).map(function(t){
return {key: 'cv-' + (t.builtin ? ('b-' + t.key) : ('u-' + t.id)),
icon: t.icon || '📄', label: t.name,
sub: (t.description || (t.builtin ? 'Canevas intégré' : 'Canevas personnel')),
action: 'canvas', canvas: t, chev: true};
});
},
_canvasActionItems(){
var c = this.canvas;
if(!c) return [];
var pageId = this._openPageId();
return [
{key:'cv-meta', _sep: (c.builtin ? 'Canevas intégré · ' : 'Canevas personnel · ') + c.name},
{key:'cv-create', icon:'+', label:'Créer une page à partir du canevas',
sub:'Nouvelle page dans le workspace courant', action:'canvas-create'},
{key:'cv-insert', icon:'⇩', label:'Insérer dans le document ouvert',
sub: pageId ? 'Ajoute les blocs en fin de page' : 'Aucun document éditable ouvert',
action:'canvas-insert', disabled: !pageId},
{key:'cv-save', icon:'⭐', label:'Enregistrer le document ouvert comme canevas',
sub: pageId ? 'Réutilisable depuis ce menu' : 'Aucun document ouvert',
action:'canvas-save', disabled: !pageId}
];
},
// Id de la page ouverte dans l'éditeur (refId du contexte, sinon l'URL).
_openPageId(){
try{
var c = window.FlowDeckCtx && window.FlowDeckCtx.current();
if(c && c.kind === 'page' && c.refId != null && /^\d+$/.test(String(c.refId))) return String(c.refId);
}catch{ /* volontaire */ }
var m = /\/pages\/(\d+)/.exec(window.location.pathname);
return m ? m[1] : null;
},
_canvasUrl(suffix, t){
t = t || this.canvas;
return t.builtin
? ('/board/api/page-templates/0/' + suffix + '?key=' + encodeURIComponent(t.key))
: ('/board/api/page-templates/' + t.id + '/' + suffix);
},
canvasCreate(){
var self = this, c = this.canvas;
if(!c) return;
// le builtin se choisit par `key` DANS LE BODY (route /use), pas en query
fetch(this._canvasUrl('use'), {
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
body: JSON.stringify(c.builtin ? {key: c.key} : {})
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
.then(function(res){
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Création impossible.', true); return; }
self.toast('Page créée : « ' + ((res.d && res.d.title) || c.name) + ' » (Workspace).');
self.plusSection = 'canvases'; self.plusFocus = 0;
}).catch(function(){ self.toast('Création impossible (réseau).', true); });
},
canvasInsert(){
var self = this, c = this.canvas;
if(!c) return;
fetch(this._canvasUrl('blocks')).then(function(r){ return r.json(); })
.then(function(d){
var E = window.E;
if(!E || !E.blocks || !d || !Array.isArray(d.blocks)){
self.toast('Aucun document éditable ouvert.', true); return;
}
if(window.ensureBlockIds) window.ensureBlockIds(d.blocks);
try{
if(E.sync) E.sync();
var empty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph'
&& !(E.blocks[0].content || '').trim();
E.blocks = empty ? d.blocks : E.blocks.concat(d.blocks);
E.dirty = true;
if(E.autoSave) E.autoSave();
if(E.render) E.render();
self.toast('Canevas inséré dans le document.');
}catch{ self.toast('Insertion impossible.', true); }
}).catch(function(){ self.toast('Lecture du canevas impossible.', true); });
},
canvasSave(){
var self = this, c = this.canvas;
var pageId = this._openPageId();
if(!c || !pageId) return;
var def = (this.activeContext && this.activeContext.label) || c.name || 'Mon canevas';
var name = window.prompt('Nom du canevas à enregistrer :', def);
if(!name || !name.trim()) return;
name = name.trim();
fetch('/board/api/page-templates', {
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
body: JSON.stringify({name: name, page_id: Number(pageId)})
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
.then(function(res){
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Enregistrement impossible.', true); return; }
self.toast('Canevas enregistré : « ' + name + ' »');
self.fetchCanvases();
}).catch(function(){ self.toast('Enregistrement impossible (réseau).', true); });
},
_plusSelectable(){ _plusSelectable(){
var out = []; var out = [];
this.plusItems.forEach(function(it, i){ if(!it.disabled && !it._sep) out.push(i); }); this.plusItems.forEach(function(it, i){ if(!it.disabled && !it._sep) out.push(i); });
@@ -1242,6 +1347,16 @@
if(this.$refs.skillImport) this.$refs.skillImport.click(); if(this.$refs.skillImport) this.$refs.skillImport.click();
return; return;
} }
// ── Design System – Canevas (v7.53.0) ──
if(it.action === 'canvases'){
this.fetchCanvases(); this.plusSection = 'canvases'; this.plusFocus = 0; return;
}
if(it.action === 'canvas'){
this.canvas = it.canvas; this.plusSection = 'canvas'; this.plusFocus = 0; return;
}
if(it.action === 'canvas-create'){ this.canvasCreate(); return; }
if(it.action === 'canvas-insert'){ this.canvasInsert(); return; }
if(it.action === 'canvas-save'){ this.canvasSave(); return; }
if(it._folder){ // entrer dans un sous-dossier if(it._folder){ // entrer dans un sous-dossier
this.plusCrumb.push({id: it._folder, name: it.label}); this.plusCrumb.push({id: it._folder, name: it.label});
this.plusLoadBrowse(it._folder); this.plusLoadBrowse(it._folder);
@@ -1261,6 +1376,7 @@
if(sec === 'skills-manage' || sec === 'skills-gallery'){ if(sec === 'skills-manage' || sec === 'skills-gallery'){
this.plusSection = 'skills'; this.plusFocus = 0; return; this.plusSection = 'skills'; this.plusFocus = 0; return;
} }
if(sec === 'canvas'){ this.plusSection = 'canvases'; this.plusFocus = 0; return; }
this.plusSection = 'root'; this.plusSection = 'root';
this.plusFocus = 0; this.plusFocus = 0;
}, },
+2 -2
View File
@@ -104,8 +104,8 @@ def test_plus_menu_sections_and_disabled_states():
for key in ("'files'", "'skills'", "'connectors'", "'canvases'", "'plugins'", "'memory'"): for key in ("'files'", "'skills'", "'connectors'", "'canvases'", "'plugins'", "'memory'"):
assert f"key:{key}" in root, key assert f"key:{key}" in root, key
assert "action:'files'" in root and "action:'skills'" in root assert "action:'files'" in root and "action:'skills'" in root
# phases 1 + 2 livrées → il reste 4 sections « bientôt » # phases 1-3 livrées → il reste 3 sections « bientôt » (connecteurs, plugins, mémoire)
assert root.count("disabled:true") == 4 assert root.count("disabled:true") == 3
files = src.split("var FD_PLUS_FILES = [", 1)[1].split("];", 1)[0] files = src.split("var FD_PLUS_FILES = [", 1)[1].split("];", 1)[0]
assert "action:'search'" in files and "action:'browse'" in files assert "action:'search'" in files and "action:'browse'" in files
+73
View File
@@ -0,0 +1,73 @@
"""v7.53.0 — Menu + : Design System – Canevas (créer / insérer / enregistrer)."""
import json
from pathlib import Path
from app.db import get_conn
ROOT = Path(__file__).resolve().parent.parent
JS_PATH = ROOT / "static" / "js" / "agent_panel_2.js"
def test_canvases_list_includes_design_system(client):
tpl = {t["key"]: t for t in client.get("/board/api/page-templates").json()["templates"] if t.get("builtin")}
assert "design_system" in tpl
assert tpl["design_system"]["name"] == "Design System"
assert tpl["design_system"]["icon"] == "🎨"
def test_blocks_route_builtin(client):
r = client.get("/board/api/page-templates/0/blocks", params={"key": "design_system"})
assert r.status_code == 200, r.text
blocks = r.json()["blocks"]
assert isinstance(blocks, list) and blocks
assert blocks[0]["type"] == "heading_1" and blocks[0]["content"] == "Design System"
# les builtins sont livrés sans id : le front appelle ensureBlockIds()
assert "id" not in blocks[0]
assert client.get("/board/api/page-templates/0/blocks", params={"key": "nope"}).status_code == 404
empty = client.get("/board/api/page-templates/0/blocks", params={"key": "empty"})
assert empty.status_code == 200 and empty.json()["blocks"]
def test_blocks_route_user_template(client):
blocks = [{"type": "heading_1", "content": "Mon canevas"},
{"type": "paragraph", "content": "Contenu."}]
created = client.post("/board/api/page-templates", json={"name": "Mon canevas", "blocks": blocks})
assert created.status_code == 200, created.text
tid = created.json()["id"]
got = client.get(f"/board/api/page-templates/{tid}/blocks")
assert got.status_code == 200
assert [b["content"] for b in got.json()["blocks"]] == ["Mon canevas", "Contenu."]
assert client.get("/board/api/page-templates/999999/blocks").status_code == 404
def test_use_design_system_creates_page(client):
r = client.post("/board/api/page-templates/0/use", json={"key": "design_system"})
assert r.status_code == 200, r.text
page_id = r.json()["id"]
with get_conn() as conn:
row = conn.execute(
"SELECT content, content_format FROM pages WHERE id=?", (page_id,)
).fetchone()
assert row["content_format"] == "blocks"
blocks = json.loads(row["content"])
texts = [b.get("content", "") for b in blocks]
assert "Design System" in texts
# les ids sont assignés côté serveur (_ensure_block_ids)
assert all(b.get("id") for b in blocks)
def test_canvases_menu_wired(client):
src = JS_PATH.read_text(encoding="utf-8")
root = src.split("var FD_PLUS_MENU = [", 1)[1].split("];", 1)[0]
assert "action:'canvases'" in root
# phases 1-3 livrées → 3 sections « bientôt » restantes (connecteurs, plugins, mémoire)
assert root.count("disabled:true") == 3
for action in ("'canvas'", "'canvas-create'", "'canvas-insert'", "'canvas-save'"):
assert action in src, action
# piège /use : la clé builtin part DANS LE BODY, pas en query string
assert "body: JSON.stringify(c.builtin ? {key: c.key} : {})" in src
assert "ensureBlockIds" in src