feat(wiki,templates): v5.11.0 wiki-links & mentions + v5.12.0 templates & page lock (release 5.12.0)
FlowDeck CI / lint (push) Successful in 50s
FlowDeck CI / test (push) Successful in 5m41s
FlowDeck CI / docker (push) Successful in 45s

v5.11.0 Wiki-links & mentions de page :
- tokens [[fdpage:ID]] / [[fddate:ISO]] dans le texte des blocs,
  service app/services/wiki_links.py (labels, rendu HTML, extraction)
- taper [[ ouvre le picker de pages (recherche floue, clavier) ;
  le menu @ gagne les sections Pages et Date (today/tomorrow/YYYY-MM-DD)
- chips atomiques contenteditable=false relues en tokens par gtTok()
  (autosave/drag/undo preservent les liens) ; renommage propage via
  GET /board/api/wiki/titles ; backlinks reconnaissent les tokens ;
  page publique rend les chips (echopee)

v5.12.0 Templates & verrouillage :
- template picker global sur + New page : 5 built-in
  (app/services/block_templates.py) + templates perso
  (table page_global_templates, migration 13)
- POST /board/api/page-templates (save current page) + /{id}/use
  (instantiate, id 0 = built-in par cle)
- page lock : POST /api/pages/{id}/lock, garde _ensure_page_editable
  -> 423 en ecriture pour les non-privileged, deblocage par
  locked_by ou admin seulement (403 sinon), banniere + read-only UI
- full-width / small text par page (pages.full_width/font_small,
  POST /api/pages/{id}/options, classes CSS)
- migration 13 : is_locked, locked_by, full_width, font_small,
  page_global_templates

Tests : tests/test_v511_v512_wiki_templates.py (15) ; suite complete
538 verte ; ruff OK ; node --check des templates JS OK.
This commit is contained in:
2026-09-14 06:38:09 -04:00
parent d4adf89db5
commit 9dfc38706c
14 changed files with 1439 additions and 95 deletions
+80 -29
View File
@@ -904,43 +904,94 @@
},
/**
* Create a new page/file in the correct workspace context.
* - Local workspace active → workspace item (prompts for name)
* - Gitea workspace active → workspace item with workspace_id (prompts for name)
* - No workspace → general Notion page (no prompt, title "New page")
* Create a new page: opens the v5.12.0 template picker (built-in + the
* user's saved templates), then instantiates the chosen one.
*/
createPage: function() {
var ctx = this._getContext();
var self = this;
// ── Workspace context → create workspace item ──
if (ctx.wsId > 0) {
var body = { name: '', type: 'page' };
if (ctx.isGitea) body.workspace_id = ctx.wsId;
fetch('/api/local-workspace/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify(body)
})
.then(function(r) { if (!r.ok) throw new Error('Failed'); return r.json(); })
.then(function(data) {
window.location.href = '/pages/' + data.id;
// Direct creation path (fallback / no-template choice).
this._createPlainPage = function() {
var ctx = self._getContext();
if (ctx.wsId > 0) {
var body = { name: '', type: 'page' };
if (ctx.isGitea) body.workspace_id = ctx.wsId;
fetch('/api/local-workspace/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
body: JSON.stringify(body)
})
.catch(function(err) { self._toast('Failed: ' + err.message, 'error'); });
return;
}
.then(function(r) { if (!r.ok) throw new Error('Failed'); return r.json(); })
.then(function(data) { window.location.href = '/pages/' + data.id; })
.catch(function(err) { self._toast('Failed: ' + err.message, 'error'); });
return;
}
var project = ctx.workspaceKey;
var url = '/board/api/pages?section=Private&project=' + encodeURIComponent(project);
fetch(url, {
method: 'POST',
headers: { 'X-CSRF-Token': self.getCsrfToken() }
})
.then(function(r) { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
.then(function(data) { window.location.href = '/pages/' + data.id; })
.catch(function(err) { self._toast('Failed to create page: ' + err.message, 'error'); });
};
this._openTemplatePicker();
},
// ── No workspace → general Notion page ──
var project = ctx.workspaceKey;
var url = '/board/api/pages?section=Private&project=' + encodeURIComponent(project);
_openTemplatePicker: function() {
var self = this;
function escAttr(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
if (document.getElementById('fd-tpl-picker')) { document.getElementById('fd-tpl-picker').style.display = 'flex'; return; }
var ov = document.createElement('div');
ov.id = 'fd-tpl-picker';
ov.className = 'tpl-picker-overlay';
ov.innerHTML = '<div class="tpl-picker-modal"><div class="tpl-picker-title">New page from template</div>'
+ '<div class="tpl-picker-sub">Pick a starting point — or start blank.</div>'
+ '<div class="tpl-grid" id="fd-tpl-grid"><div style="color:var(--text-dim);font-size:13px;padding:12px">Loading…</div></div>'
+ '<div style="text-align:right;margin-top:14px"><button class="btn-sm" id="fd-tpl-close" style="cursor:pointer;padding:6px 14px">Cancel</button></div></div>';
document.body.appendChild(ov);
ov.addEventListener('click', function(e){ if(e.target===ov) ov.style.display='none'; });
document.getElementById('fd-tpl-close').onclick = function(){ ov.style.display='none'; };
fetch('/board/api/page-templates', {credentials:'same-origin'})
.then(function(r){ return r.json(); })
.then(function(d){
var list = d.templates || [];
var h = '';
list.forEach(function(t){
var use = t.builtin
? "key:"+t.key
: "id:"+t.id;
h += '<div class="tpl-card" data-use="'+escAttr(use)+'">'
+ '<span class="tpl-card-icon">'+escAttr(t.icon||'📄')+'</span>'
+ '<span><span class="tpl-card-name">'+escAttr(t.name)+'</span>'
+ '<div class="tpl-card-desc">'+escAttr(t.description||'')+'</div></span></div>';
});
document.getElementById('fd-tpl-grid').innerHTML = h || '<div style="color:var(--text-dim)">No templates</div>';
document.querySelectorAll('#fd-tpl-grid .tpl-card').forEach(function(card){
card.onclick = function(){ self._useTemplate(card.dataset.use); };
});
}).catch(function(){ document.getElementById('fd-tpl-grid').innerHTML='<div style="color:var(--text-dim)">Failed to load templates</div>'; });
},
_useTemplate: function(use){
var ov = document.getElementById('fd-tpl-picker');
if (ov) ov.style.display = 'none';
var self = this;
if (!use || use === 'key:empty') { this._createPlainPage(); return; }
var parts = use.split(':');
var body = {};
var url;
if (parts[0] === 'key') { url = '/board/api/page-templates/0/use'; body = {key: parts.slice(1).join(':')}; }
else { url = '/board/api/page-templates/' + parts[1] + '/use'; }
fetch(url, {
method: 'POST',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
credentials: 'same-origin',
body: JSON.stringify(body)
})
.then(function(r) { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
.then(function(data) { window.location.href = '/pages/' + data.id; })
.catch(function(err) { self._toast('Failed to create page: ' + err.message, 'error'); });
.then(function(r){ if (!r.ok) throw new Error('Template failed'); return r.json(); })
.then(function(d){ window.location.href = '/pages/' + d.id; })
.catch(function(){ self._createPlainPage(); });
},
/** Show the global New Folder modal. */