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/.
69 lines
2.7 KiB
Python
69 lines
2.7 KiB
Python
"""FlowDeck — Projection Form builder -> pipeline public /f/ (§16.5).
|
|
|
|
Le builder ecrit ``form_questions`` + reglages de la vue form ; le
|
|
projecteur regenere ``collections.form_config_json`` (format v6.8 inchange)
|
|
consomme par GET/POST /f/<token>. Le public ne lit jamais les tables du
|
|
builder : zero regression possible sur les liens deja diffuses.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
|
|
def project_form_config(view_config: dict, questions: list[dict]) -> dict:
|
|
"""Construit le ``form_config_json`` v6.8 depuis le builder."""
|
|
cfg = dict(view_config or {})
|
|
sharing = cfg.get("sharing") or {}
|
|
submit = cfg.get("submit_screen") or {}
|
|
fields = []
|
|
for q in sorted(questions, key=lambda x: x.get("position", 0)):
|
|
fields.append({
|
|
"property": q.get("property_name"),
|
|
"label": q.get("label_override") or q.get("property_name"),
|
|
"required": bool(q.get("required")),
|
|
"help": q.get("help_text") or "",
|
|
"widget": q.get("widget") or "auto",
|
|
"max_selections": q.get("max_selections"),
|
|
"conditional": json.loads(q["conditional_json"]) if q.get("conditional_json") else None,
|
|
})
|
|
return {
|
|
"title": cfg.get("title") or "Form",
|
|
"description": cfg.get("description") or "",
|
|
"fields": fields,
|
|
"sharing": {
|
|
"audience": sharing.get("audience", "workspace"),
|
|
"anonymous": bool(sharing.get("anonymous", False)),
|
|
"respondent_access": sharing.get("respondent_access", "none"),
|
|
"branding": sharing.get("branding", True),
|
|
},
|
|
"submit": {
|
|
"button_text": submit.get("button_text", "Envoyer"),
|
|
"button_color": submit.get("button_color", "blue"),
|
|
"confirmation_title": submit.get("confirmation_title", "Merci !"),
|
|
"confirmation_body": submit.get("confirmation_body", "Votre réponse a été enregistrée."),
|
|
},
|
|
"form_view_id": cfg.get("form_view_id"),
|
|
}
|
|
|
|
|
|
def default_questions_for(properties: list[dict]) -> list[dict]:
|
|
"""Questions initiales depuis les proprietes exposables (hors formules)."""
|
|
skip = {"formula", "rollup", "button", "unique_id",
|
|
"created_time", "created_by", "last_edited_time", "last_edited_by"}
|
|
out = []
|
|
for i, p in enumerate(properties):
|
|
if p.get("prop_type") in skip:
|
|
continue
|
|
out.append({
|
|
"property_name": p.get("name"),
|
|
"position": i,
|
|
"label_override": None,
|
|
"sync_label": 1,
|
|
"help_text": "",
|
|
"required": 0,
|
|
"widget": "auto",
|
|
"max_selections": None,
|
|
"conditional_json": None,
|
|
})
|
|
return out
|