feat(workspace,editor): creation dans le dossier courant + sync live du titre (sidebar/header)
- creation fichier/dossier a la racine du dossier courant ou d'un dossier cible (context-menu, boutons de survol) via createPage/showCreateFolderModal(parentId)
- instances de modeles en tant qu'enfant d'un dossier (parent_id) ; nom vide -> 'Untitled'
- sidebar de la librairie rafraichi apres delete/move/duplicate/rename (_syncSidebar)
- editeur: le titre se synchronise en direct dans le sidebar, le breadcrumb et l'onglet (pages et fichiers) et persiste via PUT /board/api/pages/{id}
This commit is contained in:
+15
-4
@@ -292,6 +292,12 @@ async def use_page_template(request: Request, template_id: int):
|
||||
body_ws = (body.get("workspace") or "").strip()
|
||||
if body_ws:
|
||||
ws_key = body_ws
|
||||
# Optional target folder: instantiate the template as a child of it.
|
||||
parent_id = body.get("parent_id")
|
||||
try:
|
||||
parent_id = int(parent_id) if parent_id not in (None, "", 0, "0") else None
|
||||
except (TypeError, ValueError):
|
||||
parent_id = None
|
||||
try:
|
||||
parsed_blocks = json.loads(blocks_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
@@ -299,7 +305,12 @@ async def use_page_template(request: Request, template_id: int):
|
||||
_ensure_block_ids(parsed_blocks)
|
||||
blocks_json = json.dumps(parsed_blocks)
|
||||
with get_conn() as conn:
|
||||
if ws_id is not None:
|
||||
if parent_id is not None:
|
||||
next_order = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order), -1) + 1 FROM pages WHERE parent_id=?",
|
||||
(parent_id,),
|
||||
).fetchone()[0]
|
||||
elif ws_id is not None:
|
||||
next_order = conn.execute(
|
||||
"SELECT COALESCE(MAX(sort_order), -1) + 1 FROM pages WHERE workspace_id=? AND parent_id IS NULL",
|
||||
(ws_id,),
|
||||
@@ -310,9 +321,9 @@ async def use_page_template(request: Request, template_id: int):
|
||||
(ws_key,),
|
||||
).fetchone()[0]
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, sort_order)
|
||||
VALUES (?,?,?,?,?, 'Private', ?)""",
|
||||
(ws_key, ws_id, title or name, blocks_json, "blocks", next_order),
|
||||
"""INSERT INTO pages (workspace, workspace_id, title, content, content_format, parent_section, parent_id, sort_order)
|
||||
VALUES (?,?,?,?,?, 'Private', ?, ?)""",
|
||||
(ws_key, ws_id, title or name, blocks_json, "blocks", parent_id, next_order),
|
||||
)
|
||||
conn.commit()
|
||||
page_id = cur.lastrowid
|
||||
|
||||
@@ -1352,7 +1352,7 @@ async def nav_menu(request: Request, workspace_id: int = None, parent_id: int =
|
||||
async def create_local_workspace_item(request: Request):
|
||||
"""Create a new file in the active workspace."""
|
||||
body = await request.json()
|
||||
name = body.get("name", "Untitled").strip()
|
||||
name = (body.get("name") or "").strip() or "Untitled"
|
||||
item_type = body.get("type", "page")
|
||||
parent_id = body.get("parent_id")
|
||||
explicit_ws_id = body.get("workspace_id")
|
||||
|
||||
@@ -168,6 +168,16 @@ document.addEventListener('alpine:init', function () {
|
||||
this.wsId = d.workspace_id || 0;
|
||||
} catch (e) { this.crumbs = []; }
|
||||
}
|
||||
// Keep the breadcrumb in sync when the open page is renamed in the editor.
|
||||
var self = this;
|
||||
window.addEventListener('flowdeck:page-renamed', function (e) {
|
||||
var d = e && e.detail;
|
||||
if (!d || d.id == null) return;
|
||||
var id = String(d.id);
|
||||
self.crumbs = self.crumbs.map(function (c) {
|
||||
return String(c.id) === id ? Object.assign({}, c, { label: d.title || 'Untitled' }) : c;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
get display() {
|
||||
|
||||
@@ -2274,16 +2274,45 @@ applyAIBlocks(text){
|
||||
setTimeout(()=>{const fe=this.getEl(focusId);if(fe){fe.focus();ce(fe);}},60);
|
||||
},
|
||||
autoSave(){if(this.fileData)return;clearTimeout(this.st);this.st=setTimeout(()=>this.save(),1500);},
|
||||
// Reflect the page title in the left navigation, the header breadcrumb
|
||||
// and the browser tab as soon as it changes.
|
||||
_syncTitleUI(){
|
||||
const title=(this.pageTitle||'').trim()||'Untitled';
|
||||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=title;});
|
||||
document.querySelectorAll(`.sidebar-item.ws-tree-item[data-id="${this.pid}"]`).forEach(item=>{item.setAttribute('data-name',title);const n=item.querySelector('.page-name');if(n)n.textContent=title;});
|
||||
document.title='FlowDeck — '+title;
|
||||
window.dispatchEvent(new CustomEvent('flowdeck:page-renamed',{detail:{id:this.pid,title:title}}));
|
||||
if(window.parent&&window.parent!==window){try{window.parent.postMessage({type:'fd-page-renamed',id:this.pid,title:title},window.location.origin);}catch(e){}}
|
||||
},
|
||||
save(cb){
|
||||
if(this.fileData)return;
|
||||
if(this.fileData){
|
||||
// File page: only the title is editable here. Sync the sidebar,
|
||||
// header breadcrumb and browser tab live, and persist the title
|
||||
// without rewriting the file's blocks.
|
||||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||||
this._syncTitleUI();
|
||||
if(this._fileTitleT)clearTimeout(this._fileTitleT);
|
||||
this._fileTitleT=setTimeout(()=>{
|
||||
this._fileTitleT=null;
|
||||
const title=(this.pageTitle||'').trim();
|
||||
if(!title)return;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/pages/${this.pid}?title=${encodeURIComponent(title)}`,{method:'PUT',headers:{'X-CSRF-Token':csrf?csrf[1]:''}})
|
||||
.then(()=>{this.dirty=false;})
|
||||
.catch(()=>{});
|
||||
},600);
|
||||
if(cb)cb();
|
||||
return;
|
||||
}
|
||||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||||
this._syncTitleUI();
|
||||
if(this.saving){ if(cb) cb(); return; }
|
||||
this.sync();
|
||||
const t=document.getElementById('_titleEl');if(t)this.pageTitle=t.textContent?.trim()||'';
|
||||
this.saving=true;
|
||||
const csrf=document.cookie.match(/csrf_token=([^;]+)/);
|
||||
fetch(`/board/api/pages/${this.pid}/blocks`,{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-Token':csrf?csrf[1]:''},body:JSON.stringify({title:this.pageTitle,blocks:this.blocks.map(b=>{const c={id:b.id,type:b.type,content:b.content};['checked','expanded','language','icon','src','embed_src','embed_provider','alt','style','embed_type','collection_id','dbs','file_name','file_size','file_mime','children','rows','align','has_header','first_col_header','colsW','meeting','automation_id','automation_name'].forEach(k=>{if(b[k]!=null)c[k]=b[k];});return c;})})})
|
||||
.then(r=>r.json()).then(()=>{this.saving=false;this.dirty=false;this.lastSaved=new Date().toLocaleTimeString();
|
||||
document.querySelectorAll(`.sidebar-item[data-page-id="page/${this.pid}"]`).forEach(item=>{const n=item.querySelector('.page-name');if(n)n.textContent=this.pageTitle||'New page';});
|
||||
this._syncTitleUI();
|
||||
if(cb)cb();
|
||||
}).catch(()=>{this.saving=false;});
|
||||
},
|
||||
|
||||
+22
-3
@@ -897,6 +897,15 @@
|
||||
return { wsId: 0, isGitea: false, workspaceKey: wk };
|
||||
},
|
||||
|
||||
/** Folder currently open on the local-workspace page (0 when at root). */
|
||||
_currentFolderId: function() {
|
||||
var ws = window._wsData;
|
||||
if (ws && typeof ws.currentFolder === 'number' && ws.currentFolder > 0) {
|
||||
return ws.currentFolder;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
/** Return a toast function that works across pages. */
|
||||
_toast: function(msg, type) {
|
||||
if (window.appState && window.appState.toast) window.appState.toast(msg, type);
|
||||
@@ -907,14 +916,19 @@
|
||||
* 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() {
|
||||
createPage: function(parentId) {
|
||||
var self = this;
|
||||
// Explicit folder (row/context-menu button) wins; otherwise create in the
|
||||
// folder the user is currently browsing on the local-workspace page.
|
||||
if (parentId == null || parentId === '') parentId = this._currentFolderId();
|
||||
this._createParentId = parentId || 0;
|
||||
// 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;
|
||||
if (self._createParentId) body.parent_id = self._createParentId;
|
||||
fetch('/api/local-workspace/items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': self.getCsrfToken() },
|
||||
@@ -986,6 +1000,7 @@
|
||||
var ctx = self._getContext();
|
||||
if (ctx.wsId > 0) { body.workspace_id = ctx.wsId; }
|
||||
else if (ctx.workspaceKey) { body.workspace = ctx.workspaceKey; }
|
||||
if (self._createParentId) { body.parent_id = self._createParentId; }
|
||||
if (parts[0] === 'key') { body.key = parts.slice(1).join(':'); url = '/board/api/page-templates/0/use'; }
|
||||
else { url = '/board/api/page-templates/' + parts[1] + '/use'; }
|
||||
fetch(url, {
|
||||
@@ -999,8 +1014,10 @@
|
||||
.catch(function(){ self._createPlainPage(); });
|
||||
},
|
||||
|
||||
/** Show the global New Folder modal. */
|
||||
showCreateFolderModal: function() {
|
||||
/** Show the global New Folder modal (parentId = target folder, optional). */
|
||||
showCreateFolderModal: function(parentId) {
|
||||
if (parentId == null || parentId === '') parentId = this._currentFolderId();
|
||||
this._folderParentId = parentId || 0;
|
||||
var modal = document.getElementById('fd-global-folder-modal');
|
||||
if (!modal) return;
|
||||
modal.style.display = 'flex';
|
||||
@@ -1019,6 +1036,8 @@
|
||||
var ctx = this._getContext();
|
||||
var body = { name: name, type: 'folder' };
|
||||
if (ctx.wsId > 0 && ctx.isGitea) body.workspace_id = ctx.wsId;
|
||||
if (this._folderParentId) body.parent_id = this._folderParentId;
|
||||
this._folderParentId = 0;
|
||||
|
||||
var self = this;
|
||||
fetch('/api/local-workspace/items', {
|
||||
|
||||
@@ -824,6 +824,11 @@ function libraryPage() {
|
||||
return m ? m[1] : '';
|
||||
},
|
||||
|
||||
_syncSidebar() {
|
||||
if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree();
|
||||
if (window.appState && window.appState.refreshFavorites) window.appState.refreshFavorites();
|
||||
},
|
||||
|
||||
_isDescendant(node, targetId) {
|
||||
if (!node || !node.children) return false;
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
@@ -846,6 +851,7 @@ function libraryPage() {
|
||||
}
|
||||
this.clearSelection();
|
||||
this.loadTab();
|
||||
this._syncSidebar();
|
||||
},
|
||||
|
||||
// ── Empty states ──
|
||||
@@ -1088,6 +1094,7 @@ function libraryPage() {
|
||||
}
|
||||
this.clearSelection();
|
||||
this.loadTab();
|
||||
this._syncSidebar();
|
||||
if (window.showToast) window.showToast('Moved to trash', 'success');
|
||||
},
|
||||
|
||||
@@ -1097,6 +1104,7 @@ function libraryPage() {
|
||||
var csrf = this._getCsrf();
|
||||
await fetch('/board/api/pages/' + item.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
|
||||
this.loadTab();
|
||||
this._syncSidebar();
|
||||
if (window.showToast) window.showToast('Moved to trash', 'success');
|
||||
},
|
||||
|
||||
@@ -1257,6 +1265,7 @@ function libraryPage() {
|
||||
await fetch(updUrl, { method: 'PUT', headers: {'X-CSRF-Token': csrf} });
|
||||
}
|
||||
this.loadTab();
|
||||
this._syncSidebar();
|
||||
if (window.showToast) window.showToast('Page duplicated', 'success');
|
||||
} catch(e) {
|
||||
if (window.showToast) window.showToast('Failed to duplicate page', 'error');
|
||||
@@ -1332,6 +1341,7 @@ function libraryPage() {
|
||||
}
|
||||
} catch(e) {}
|
||||
this._renderTable();
|
||||
this._syncSidebar();
|
||||
},
|
||||
|
||||
// ── Add new ──
|
||||
@@ -1423,6 +1433,7 @@ function libraryPage() {
|
||||
await fetch('/board/api/pages/' + this.peekItem.id, {method:'DELETE', headers:{'X-CSRF-Token': csrf}});
|
||||
this.closePeek();
|
||||
this.loadTab();
|
||||
this._syncSidebar();
|
||||
if (window.showToast) window.showToast('Moved to trash', 'success');
|
||||
},
|
||||
|
||||
|
||||
@@ -648,6 +648,19 @@ window._wsInitData = (function() {
|
||||
var self = this;
|
||||
window.addEventListener('online', function() { self.online = true; });
|
||||
window.addEventListener('offline', function() { self.online = false; });
|
||||
// Side-peek editor posts renames here so the tree/peek title stay in sync.
|
||||
window.addEventListener('message', function(ev) {
|
||||
if (ev.origin !== window.location.origin) return;
|
||||
var m = ev.data;
|
||||
if (!m || m.type !== 'fd-page-renamed' || m.id == null) return;
|
||||
var node = self._findNodeById(self.tree, m.id);
|
||||
if (node) node.name = m.title || 'Untitled';
|
||||
self.displayTree = self.displayTree.slice();
|
||||
var t = document.getElementById('ws-preview-title');
|
||||
if (t && self.previewItem && self.previewItem.id === m.id) t.textContent = m.title || 'Untitled';
|
||||
var side = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + m.id + '"] .page-name');
|
||||
if (side) side.textContent = m.title || 'Untitled';
|
||||
});
|
||||
// Load workspace tags
|
||||
this.loadWorkspaceTags();
|
||||
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
|
||||
@@ -864,6 +877,7 @@ window._wsInitData = (function() {
|
||||
selectForPreview(node) {
|
||||
var panel = document.getElementById('ws-preview-panel');
|
||||
if (!panel) return;
|
||||
this.previewItem = node;
|
||||
document.getElementById('ws-preview-icon').innerHTML = this._fileIcon(node.name, node.is_folder, node.content_format, node.page_icon);
|
||||
document.getElementById('ws-preview-title').textContent = node.name;
|
||||
var openBtn = document.getElementById('ws-preview-open-btn');
|
||||
@@ -1360,10 +1374,10 @@ window._wsInitData = (function() {
|
||||
|
||||
switch(action) {
|
||||
case 'new-file':
|
||||
window.FlowDeck.createPage();
|
||||
window.FlowDeck.createPage(id);
|
||||
break;
|
||||
case 'new-folder':
|
||||
window.FlowDeck.showCreateFolderModal();
|
||||
window.FlowDeck.showCreateFolderModal(id);
|
||||
break;
|
||||
case 'rename':
|
||||
this.startRename({db_id: id, name: name});
|
||||
@@ -2724,8 +2738,8 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(wind
|
||||
<div class="actions">
|
||||
<template x-if="node.is_folder">
|
||||
<span style="display:flex;gap:2px">
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.createPage()" title="New file">{{ fd_icon("file",14) }}</button>
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.showCreateFolderModal()" title="New folder">{{ fd_icon("folder",14) }}</button>
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.createPage(node.id)" title="New file">{{ fd_icon("file",14) }}</button>
|
||||
<button class="ws-act" @click.stop="window.FlowDeck.showCreateFolderModal(node.id)" title="New folder">{{ fd_icon("folder",14) }}</button>
|
||||
<a class="ws-act" href="/local-workspace" title="Open workspace page" style="text-decoration:none;display:inline-flex;align-items:center;">{{ fd_icon("external-link",14) }}</a>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
|
||||
{% set page_icon = "file" if page.content_format != 'file' else "paperclip" %}
|
||||
{% set page_title = page.title %}
|
||||
{% set nav_page_id = page.id %}
|
||||
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn" @click="window.E && window.E.toggleComments()" title="Comments"><span class="fd-comment-btn-ico">💬</span><span class="fd-comment-count" x-text="window.E && window.E.commentCount>0 ? window.E.commentCount : \'\'"></span></button><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()"><span x-show="!window.E || !window.E.pageIsShared">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="window.E && window.E.pageIsShared" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
|
||||
{% include '_header.html' %}
|
||||
{% endblock %} {% block content %}
|
||||
|
||||
Reference in New Issue
Block a user