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
+84 -3
View File
@@ -510,6 +510,9 @@
<div class="more-menu-item" @click="moreSaveTemplate()">
📑 Save as template
</div>
<div class="more-menu-item" @click="openTplPicker()">
{{ fd_icon("file",14) }} Apply template…
</div>
<div class="more-menu-sep"></div>
<div class="more-menu-item danger" @click="deletePage">
{{ fd_icon("trash",14) }} Move to Trash
@@ -746,7 +749,7 @@
</button>
<button
class="gs-pill"
@click="gsMoreOpen = !gsMoreOpen"
@click="openTplPicker()"
>
<span class="gs-icon">{{ fd_icon("file",14) }}</span> Templates
</button>
@@ -797,8 +800,8 @@
<!-- ═══════════ Database template picker (v5.3.0) ═══════════ -->
<div class="fd-dbtpl-overlay" id="fd-dbtpl" x-show="dbTplOpen"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;
align-items:flex-start;justify-content:center;padding-top:10vh;opacity:0;transition:opacity .15s ease;"
style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2100;display:flex;align-items:center;justify-content:center;padding:20px;"
x-transition.opacity
@click.self="closeDbTemplatePicker()">
<div class="fd-dbtpl" style="width:620px;max-width:94vw;max-height:76vh;display:flex;flex-direction:column;
background:var(--bg-secondary,#232323);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:12px;
@@ -833,6 +836,84 @@
</div>
</div>
<!-- ═══════════ Unified template picker (registre unifié, §7.1) ═══════════
Toujours une issue visible (P5) : liste, état vide avec création,
ou erreur explicite avec Réessayer — jamais un no-op silencieux. -->
<div class="fd-tpl-overlay" id="fd-tpl-picker" data-testid="tpl-picker" x-show="tplOpen"
style="position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:2200;"
x-transition.opacity
@click.self="closeTplPicker()">
<!-- Enveloppe flex SÉPARÉE du x-show : Alpine restaure display:block
sur l'overlay (pas le flex inline) — sans cette enveloppe, le
dialogue reste collé en haut à gauche au lieu d'être centré. -->
<div style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:20px;box-sizing:border-box;">
<div class="fd-tpl" style="width:760px;max-width:96vw;max-height:80vh;display:flex;flex-direction:column;
background:var(--bg-secondary,#232323);border:1px solid var(--border,rgba(255,255,255,.08));border-radius:12px;
box-shadow:0 24px 70px rgba(0,0,0,.6);overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);">
<h3 style="margin:0;font-size:16px;color:var(--text,#fff);">Appliquer un template</h3>
<button class="flowdeck-modal-close" @click="closeTplPicker()">&times;</button>
</div>
<div style="padding:12px 20px 0;">
<input type="text" x-model="tplQuery" @input.debounce.300ms="loadTplList()" placeholder="🔍 Rechercher un template…"
style="width:100%;padding:9px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text-primary);font-size:14px;outline:none;box-sizing:border-box;">
<div class="fd-gtpl-filters" style="margin-top:10px;">
<template x-for="k in tplKindOptionsList" :key="k">
<button type="button" class="fd-tpl-filter" :class="tplKind === k ? 'active' : ''"
@click="setTplKind(k)" x-text="tplKindLabel(k)"></button>
</template>
<a href="/templates" style="margin-left:auto;font-size:12px;color:var(--text-dim);align-self:center;text-decoration:none;padding-right:6px;">Gérer les templates…</a>
</div>
</div>
<div style="display:flex;gap:0;flex:1;min-height:0;padding:0 20px 12px;">
<div style="flex:1;overflow-y:auto;min-height:120px;max-height:44vh;padding-right:8px;">
<div x-show="tplLoading" style="color:var(--text-dim,#999);font-size:13px;padding:20px 0;text-align:center;">Chargement…</div>
<div x-show="tplError" style="color:#e5484d;font-size:13px;padding:12px;background:rgba(229,72,77,.1);border-radius:8px;margin-bottom:8px;">
<span x-text="tplError"></span>
<button @click="loadTplList()" style="margin-left:8px;">Réessayer</button>
</div>
<template x-for="t in tplItems" :key="t.id">
<div data-testid="tpl-item" @click="selectTpl(t)" :style="tplSelected && tplSelected.id === t.id ? 'border-color:var(--accent);' : ''"
style="display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;">
<span style="font-size:18px;" x-text="t.icon || '📄'"></span>
<div style="flex:1;min-width:0;">
<div style="font-weight:600;color:var(--text,#fff);font-size:13px;" x-text="t.name"></div>
<div style="font-size:11px;color:var(--text-dim,#999);" x-text="(t.kind + ' · ' + t.scope)"></div>
</div>
</div>
</template>
<div x-show="!tplLoading && !tplError && !tplItems.length" style="color:var(--text-dim,#999);font-size:13px;padding:16px 0;text-align:center;">
Aucun template pour ce filtre — <a href="/templates">créez-en un</a> ou choisissez un preset système.
</div>
</div>
<div style="flex:1;overflow-y:auto;min-height:120px;max-height:44vh;border-left:1px solid var(--border);padding-left:12px;" x-show="tplSelected">
<div style="font-weight:600;color:var(--text,#fff);font-size:14px;margin-bottom:4px;" x-text="tplPreviewTitle"></div>
<div x-show="tplPreviewWarnings.length" style="color:#f5a524;font-size:12px;margin-bottom:6px;" x-text="tplPreviewWarnings.join(' · ')"></div>
<template x-for="b in tplPreviewBlocks.slice(0,8)" :key="b.id">
<div style="font-size:12px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" x-text="('• ' + (b.content || ('[' + (b.type||'?') + ']')))"></div>
</template>
<div x-show="tplVarDefs.length" style="margin-top:8px;">
<div style="font-size:12px;font-weight:600;color:var(--text,#fff);margin-bottom:4px;">Variables</div>
<template x-for="v in tplVarDefs" :key="v.name">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="font-size:12px;color:var(--text-dim);min-width:80px;" x-text="v.label || v.name"></span>
<input type="text" :placeholder="v.default || ''" @input="setTplVar(v.name, $event.target.value)"
style="flex:1;padding:5px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:12px;">
</div>
</template>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;padding:12px 20px;border-top:1px solid var(--border);">
<button class="flowdeck-modal-close" @click="closeTplPicker()" style="padding:8px 16px;position:static;">Annuler</button>
<button data-testid="tpl-apply" @click="applyTpl()" :disabled="!tplSelected || tplApplying"
style="padding:8px 20px;border-radius:8px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:.95;"
x-text="tplApplying ? 'Application…' : 'Utiliser'"></button>
</div>
</div>
</div>
</div>
<!-- ═══════════ v5.4.0: Version History Popover ═══════════ -->
<div class="versions-popover"
x-show="versionsOpen"