diff --git a/app/routers/board.py b/app/routers/board.py index 97d0e46..09bc9ac 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -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 diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 8e2b50e..374a80f 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -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") diff --git a/app/templates/_header.html b/app/templates/_header.html index e10e79e..2f4cd6f 100644 --- a/app/templates/_header.html +++ b/app/templates/_header.html @@ -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() { diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 6289e5e..dd8c8f5 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -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;}); }, diff --git a/app/templates/base.html b/app/templates/base.html index 0dd6953..c308c93 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -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', { diff --git a/app/templates/library.html b/app/templates/library.html index 19bcc15..1d60530 100644 --- a/app/templates/library.html +++ b/app/templates/library.html @@ -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'); }, diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index 4440f4c..075c6f1 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -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