feat(templates): refonte complète des templates façon Notion + vues/agents-skills
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:
@@ -1802,3 +1802,226 @@ def _migration_meeting_notion_block(conn: sqlite3.Connection) -> None:
|
||||
"ON meeting_transcripts(event_occurrence_id) "
|
||||
"WHERE event_occurrence_id <> ''"
|
||||
)
|
||||
|
||||
|
||||
# ── Vues Notion §10 : migrations 44 à 47 (après 39-43 réservées Agents&Skills) ──
|
||||
|
||||
@register(44, "vues-notion: view_config_v2 (config_version + index view_type)")
|
||||
def _migration_view_config_v2(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 44 — versionnage des config_json de vues (phase 1, Chart)."""
|
||||
cols = columns(conn, "collection_views")
|
||||
if "config_version" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE collection_views ADD COLUMN config_version INTEGER NOT NULL DEFAULT 1"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_collection_views_type "
|
||||
"ON collection_views(collection_id, view_type)"
|
||||
)
|
||||
|
||||
|
||||
@register(45, "vues-notion: place_geocode_cache")
|
||||
def _migration_place_geocode_cache(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 45 — cache partage du Geocoding Service (phase 3, Map)."""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS geocode_cache (
|
||||
query_hash TEXT PRIMARY KEY,
|
||||
provider TEXT NOT NULL DEFAULT 'nominatim',
|
||||
query_text TEXT NOT NULL DEFAULT '',
|
||||
result_json TEXT NOT NULL DEFAULT '{}',
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
hit_count INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_geocode_cache_created ON geocode_cache(created_at)"
|
||||
)
|
||||
|
||||
|
||||
@register(46, "vues-notion: dashboard_widgets")
|
||||
def _migration_dashboard_widgets(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 46 — widgets de la vue Dashboard (phase 4)."""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS dashboard_widgets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
view_id INTEGER NOT NULL REFERENCES collection_views(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
row_index INTEGER NOT NULL DEFAULT 0,
|
||||
col_span INTEGER NOT NULL DEFAULT 1,
|
||||
height_units INTEGER NOT NULL DEFAULT 2,
|
||||
source_collection_id INTEGER REFERENCES collections(id) ON DELETE SET NULL,
|
||||
source_view_id INTEGER REFERENCES collection_views(id) ON DELETE SET NULL,
|
||||
inline_config_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_view "
|
||||
"ON dashboard_widgets(view_id, row_index, position)"
|
||||
)
|
||||
|
||||
|
||||
@register(47, "vues-notion: form_questions_v2")
|
||||
def _migration_form_questions_v2(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 47 — questions du Form builder + repondant (phase 5)."""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS form_questions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
view_id INTEGER NOT NULL REFERENCES collection_views(id) ON DELETE CASCADE,
|
||||
property_name TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
label_override TEXT,
|
||||
sync_label INTEGER NOT NULL DEFAULT 1,
|
||||
help_text TEXT NOT NULL DEFAULT '',
|
||||
required INTEGER NOT NULL DEFAULT 0,
|
||||
widget TEXT NOT NULL DEFAULT 'auto',
|
||||
max_selections INTEGER,
|
||||
conditional_json TEXT,
|
||||
UNIQUE(view_id, property_name)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_form_questions_view "
|
||||
"ON form_questions(view_id, position)"
|
||||
)
|
||||
for table, ddl, col in (
|
||||
("form_responses", "ALTER TABLE form_responses ADD COLUMN view_id INTEGER", "view_id"),
|
||||
("form_responses", "ALTER TABLE form_responses ADD COLUMN respondent_user_id INTEGER REFERENCES users(id)", "respondent_user_id"),
|
||||
("form_responses", "ALTER TABLE form_responses ADD COLUMN anonymous INTEGER NOT NULL DEFAULT 0", "anonymous"),
|
||||
):
|
||||
try:
|
||||
existing = columns(conn, table)
|
||||
except Exception:
|
||||
continue
|
||||
if col not in existing:
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Templates Notion §9 : migrations 48 à 49 (registre unifié + journal) ──
|
||||
|
||||
@register(48, "templates-notion: registre unifié `templates`")
|
||||
def _migration_templates_registry(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 48 — registre unifié des templates (phase 1).
|
||||
|
||||
Catalogue les trois silos legacy (``database_templates``,
|
||||
``page_templates``, ``page_global_templates``) SANS les détruire :
|
||||
colonnes ``source_kind``/``source_id`` + backfill idempotent côté
|
||||
service (``TemplateService.backfill_registry``), rejouable au boot.
|
||||
"""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS templates (
|
||||
id TEXT PRIMARY KEY,
|
||||
workspace_id INTEGER NOT NULL DEFAULT 1,
|
||||
teamspace_id INTEGER,
|
||||
kind TEXT NOT NULL DEFAULT 'page'
|
||||
CHECK (kind IN ('page','row','database','blocks')),
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
icon TEXT NOT NULL DEFAULT '📄',
|
||||
cover_url TEXT NOT NULL DEFAULT '',
|
||||
category TEXT NOT NULL DEFAULT '',
|
||||
scope TEXT NOT NULL DEFAULT 'personal'
|
||||
CHECK (scope IN ('personal','workspace','teamspace','system')),
|
||||
owner_id INTEGER,
|
||||
target_collection_id INTEGER,
|
||||
content_page_id INTEGER,
|
||||
source_kind TEXT NOT NULL DEFAULT 'native'
|
||||
CHECK (source_kind IN ('native','legacy_database','legacy_page','legacy_global')),
|
||||
source_id TEXT NOT NULL DEFAULT '',
|
||||
manifest_json TEXT NOT NULL DEFAULT '{}',
|
||||
variables_json TEXT NOT NULL DEFAULT '[]',
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
is_system INTEGER NOT NULL DEFAULT 0,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_by INTEGER,
|
||||
created_at TEXT NOT NULL DEFAULT '',
|
||||
updated_at TEXT NOT NULL DEFAULT '',
|
||||
archived_at TEXT,
|
||||
deleted_at TEXT,
|
||||
sync_version INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_templates_ws_kind "
|
||||
"ON templates(workspace_id, kind)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_templates_target "
|
||||
"ON templates(target_collection_id)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_templates_source "
|
||||
"ON templates(source_kind, source_id)"
|
||||
)
|
||||
# Un seul défaut actif par base (les lignes soft-deleted/archivées ne comptent pas).
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_templates_default "
|
||||
"ON templates(target_collection_id) "
|
||||
"WHERE is_default=1 AND deleted_at IS NULL AND archived_at IS NULL"
|
||||
)
|
||||
|
||||
|
||||
@register(49, "templates-notion: récurrences + journal `template_runs`")
|
||||
def _migration_template_runs(conn: sqlite3.Connection) -> None:
|
||||
"""Migration 49 — récurrences des templates de ligne + journal des
|
||||
instanciations (compteurs du gestionnaire, dédup scheduler, audit)."""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS template_recurrences (
|
||||
template_id TEXT PRIMARY KEY REFERENCES templates(id) ON DELETE CASCADE,
|
||||
rrule TEXT NOT NULL DEFAULT '',
|
||||
timezone TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
next_run_at TEXT,
|
||||
last_run_at TEXT,
|
||||
end_count INTEGER,
|
||||
updated_by INTEGER,
|
||||
updated_at TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS template_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
template_id TEXT NOT NULL REFERENCES templates(id) ON DELETE CASCADE,
|
||||
run_source TEXT NOT NULL DEFAULT 'ui'
|
||||
CHECK (run_source IN ('ui','api','agent','automation','recurrence')),
|
||||
actor_id INTEGER,
|
||||
occurrence_key TEXT,
|
||||
created_type TEXT NOT NULL DEFAULT '',
|
||||
created_id TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'ok'
|
||||
CHECK (status IN ('ok','error','undone')),
|
||||
warnings_json TEXT NOT NULL DEFAULT '[]',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
idempotency_key TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_template_runs_occurrence "
|
||||
"ON template_runs(occurrence_key) WHERE occurrence_key IS NOT NULL"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS uq_template_runs_idem "
|
||||
"ON template_runs(idempotency_key) WHERE idempotency_key IS NOT NULL"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_template_runs_tpl "
|
||||
"ON template_runs(template_id, created_at)"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user