feat(templates): refonte complète des templates façon Notion + vues/agents-skills
FlowDeck CI / lint (push) Failing after 1m32s
FlowDeck CI / test (push) Failing after 27m50s
FlowDeck CI / docker (push) Skipped

Templates (v7.71.x) :

- registre unifié \	emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler)

- sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document

- 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée

- récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates

- correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title

- tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8)

Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
This commit is contained in:
2026-10-10 18:52:19 -04:00
parent d0452e10dd
commit fc8548194a
36 changed files with 12290 additions and 137 deletions
+119 -17
View File
@@ -703,32 +703,134 @@ class AddDependency(Tool):
class ApplyTemplate(Tool):
name = "apply_template"
description = "Applique un template de page dans une collection (crée une page)."
description = (
"Applique un template du registre unifié (crée une page, une ligne, "
"une base ou des blocs). Même service que l'UI (TemplateService)."
)
parameters = {
"type": "object",
"properties": {"collection_id": {"type": "integer"}, "template_id": {"type": "integer"}, "title": {"type": "string"}},
"required": ["collection_id", "template_id"],
"properties": {
"collection_id": {"type": "integer"},
"template_id": {"type": ["integer", "string"]},
"title": {"type": "string"},
"variables": {"type": "object"},
},
"required": ["template_id"],
}
async def execute(self, args, *, user_id=None) -> ToolResult:
cid, tid = args.get("collection_id"), args.get("template_id")
from app.services.fd_templates import TemplateService, registry_ready
tid = args.get("template_id")
cid = args.get("collection_id")
# Compat legacy : un id numérique de page_templates.
template_ref: str = str(tid)
with get_conn() as conn:
tmpl = conn.execute("SELECT * FROM page_templates WHERE id=? AND collection_id=?", (tid, cid)).fetchone()
if not tmpl:
return ToolResult(status="error", tool=self.name, message="Template introuvable")
max_pos = conn.execute("SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages WHERE collection_id=?", (cid,)).fetchone()[0]
cur = conn.execute(
"INSERT INTO collection_pages (collection_id, title, position, property_values_json) VALUES (?,?,?,?)",
(cid, args.get("title", "New Page"), max_pos, tmpl["property_values_json"]),
)
conn.commit()
actor = _actor_for(conn, user_id)
if registry_ready(conn) and isinstance(tid, int):
legacy = conn.execute(
"SELECT id FROM templates WHERE source_kind='legacy_page' AND source_id=?",
(str(tid),),
).fetchone()
if legacy:
template_ref = legacy["id"]
variables = dict(args.get("variables") or {})
if args.get("title"):
variables.setdefault("title", args["title"])
context: dict = {}
if cid:
context["collection_id"] = cid
try:
result = TemplateService.instantiate(
conn, actor, template_ref, variables, context,
run_source="agent",
)
except LookupError as exc:
return ToolResult(status="error", tool=self.name, message=str(exc))
except (ValueError, PermissionError) as exc:
return ToolResult(status="error", tool=self.name, message=str(exc))
except Exception as exc: # noqa: BLE001 — l'agent doit recevoir l'erreur
logger.exception("apply_template agent")
return ToolResult(status="error", tool=self.name, message=str(exc))
created_id = result.get("created_id") or None
try:
target_id = int(created_id) if created_id else 0
except (TypeError, ValueError):
target_id = 0
undo = None
if result.get("created_type") in ("page", "row", "collection") and target_id:
table = {"page": "pages", "row": "collection_pages",
"collection": "collections"}[result["created_type"]]
undo = {"action": "delete", "table": table, "id": target_id}
return ToolResult(
status="success", tool=self.name, target_type="page", target_id=cur.lastrowid,
data={"page_id": cur.lastrowid},
undo={"action": "delete", "table": "collection_pages", "id": cur.lastrowid},
status="success", tool=self.name,
target_type=result.get("created_type") or "page",
target_id=target_id,
data={"page_id": target_id, "url": result.get("url"),
"warnings": result.get("warnings", []),
"run_id": result.get("run_id")},
undo=undo,
)
class ListTemplates(Tool):
name = "list_templates"
description = "Liste les templates visibles par l'utilisateur (registre unifié)."
parameters = {
"type": "object",
"properties": {
"kind": {"type": "string"},
"query": {"type": "string"},
"target_collection_id": {"type": "integer"},
},
"required": [],
}
async def execute(self, args, *, user_id=None) -> ToolResult:
from app.services.fd_templates import TemplateService
with get_conn() as conn:
actor = _actor_for(conn, user_id)
try:
items = TemplateService.list_templates(
conn, actor, kind=args.get("kind"),
query=args.get("query"),
target_collection_id=args.get("target_collection_id"),
)
except Exception as exc: # noqa: BLE001
logger.exception("list_templates agent")
return ToolResult(status="error", tool=self.name, message=str(exc))
summary = [
{"id": t["id"], "kind": t["kind"], "name": t["name"],
"scope": t["scope"], "is_default": t.get("is_default", False)}
for t in items
]
return ToolResult(
status="success", tool=self.name, target_type="templates",
data={"templates": summary},
)
def _actor_for(conn, user_id: int | None) -> dict:
"""Acteur minimal pour le service (l'agent n'agit jamais au-delà de
l'utilisateur appelant, §6.1 de l'architecture)."""
actor: dict = {"id": user_id}
if user_id:
try:
row = conn.execute(
"SELECT id, login, full_name, is_admin FROM users WHERE id=?",
(user_id,),
).fetchone()
if row:
actor.update(
{"login": row["login"], "full_name": row["full_name"],
"is_admin": bool(row["is_admin"])}
)
except Exception:
pass
return actor
# ══════════════════════════ Destructive tools ══════════════════════════
@@ -1149,7 +1251,7 @@ TOOL_CLASSES = [
SearchWorkspace, ReadCollection, ReadPage, ReadWorkspaces, ReadDocument,
CreateCollection, CreateView, AddProperty, CreatePage, CreateDocument,
UpdatePage, WriteBlocks,
AddRelation, CreateSubItem, AddDependency, ApplyTemplate,
AddRelation, CreateSubItem, AddDependency, ApplyTemplate, ListTemplates,
DeletePage, DeleteCollection,
ReadGiteaIssues, SyncGitea, CreateGiteaIssue,
DeleteDocument,