From 9ab47d81135fa1232f36db58244748b54a2caf7f Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 14 Sep 2026 18:20:43 -0400 Subject: [PATCH] =?UTF-8?q?fix(workspace):=20menu=20contextuel=20apr=C3=A8?= =?UTF-8?q?s=20navigation=20partielle=20+=20sync=20sidebar/header=20au=20r?= =?UTF-8?q?ename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/dashboard.py | 1 + app/templates/_ctx_menu.html | 20 +++- app/templates/base.html | 21 ++++ app/templates/local_workspace.html | 14 +++ e2e/diag_ctx.spec.js | 171 +++++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 e2e/diag_ctx.spec.js diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 374a80f..3f46d71 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -986,6 +986,7 @@ async def local_workspace_page(request: Request, folder: int = None): "workspace_id": ws_id or 0, "nav_workspace_id": ws_id or 0, "breadcrumb": breadcrumb, + "breadcrumbs": breadcrumb, } template = env.get_template("local_workspace.html") return HTMLResponse( diff --git a/app/templates/_ctx_menu.html b/app/templates/_ctx_menu.html index 10ed9ca..4d8c01e 100644 --- a/app/templates/_ctx_menu.html +++ b/app/templates/_ctx_menu.html @@ -27,9 +27,15 @@ if (window.__fdCtxMenuRegistered) return; window.__fdCtxMenuRegistered = true; - document.addEventListener('alpine:init', function () { - if (window.Alpine && window.Alpine.__fdCtxStore) return; - if (window.Alpine) window.Alpine.__fdCtxStore = true; + // Sur un chargement complet de page, ce script inline s'exécute AVANT + // alpine.min.js (defer) → Alpine n'existe pas encore : on attend 'alpine:init'. + // Sur une navigation partielle (fdNavigate → htmx), Alpine est déjà démarré et + // 'alpine:init' ne sera plus jamais émis → on enregistre le store tout de suite, + // sinon le menu contextuel reste mort jusqu'au prochain chargement complet. + function registerFdCtx() { + if (!window.Alpine) return; + if (window.Alpine.__fdCtxStore) return; + window.Alpine.__fdCtxStore = true; Alpine.store('fdCtx', { open: false, x: 0, y: 0, node: null, page: null, @@ -166,7 +172,13 @@ this.close(); }, }); - }); + } + + if (window.Alpine) { + registerFdCtx(); + } else { + document.addEventListener('alpine:init', registerFdCtx); + } })(); diff --git a/app/templates/base.html b/app/templates/base.html index c308c93..7497f39 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -816,6 +816,26 @@ return m ? m[1] : ''; }, + /** Reflète le rename d'une page/dossier dans toute l'UI : sidebar gauche + (Récents, Privé, Favoris + arbre du workspace), breadcrumb du header + (event flowdeck:page-renamed) et frame parente (postMessage). */ + syncPageTitle: function(pid, title) { + var t = (title || '').trim() || 'Untitled'; + document.querySelectorAll('.sidebar-item[data-page-id="page/' + pid + '"]').forEach(function(item) { + var n = item.querySelector('.page-name'); + if (n) n.textContent = t; + }); + document.querySelectorAll('.sidebar-item.ws-tree-item[data-id="' + pid + '"]').forEach(function(item) { + item.setAttribute('data-name', t); + var n = item.querySelector('.page-name'); + if (n) n.textContent = t; + }); + window.dispatchEvent(new CustomEvent('flowdeck:page-renamed', { detail: { id: pid, title: t } })); + if (window.parent && window.parent !== window) { + try { window.parent.postMessage({ type: 'fd-page-renamed', id: pid, title: t }, window.location.origin); } catch (e) {} + } + }, + /** Refresh CSRF token from the server. Returns a promise resolving to the new token. */ refreshCsrfToken: async function() { try { @@ -1182,6 +1202,7 @@ body: JSON.stringify({ name: newName.trim() }) }).then(function(r) { if (r.ok) { + if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(id, newName.trim()); self.refreshSidebarTree(); document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed')); if (window._wsData && window._wsData._reloadAfterAction) { diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index 075c6f1..42e65f6 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -1519,6 +1519,20 @@ window._wsInitData = (function() { if (r.ok) { node.name = n; node.db_name = n; this._saveExpandedState(); + if (window.appState && window.appState.refreshSidebarTree) window.appState.refreshSidebarTree(); + if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(node.id, n); + var self = this; + if (this.folderStack && this.folderStack.length) { + this.folderStack.forEach(function(f) { if (String(f.id) === String(node.id)) f.name = n; }); + } + if (this.breadcrumb && this.breadcrumb.length) { + this.breadcrumb.forEach(function(b) { if (String(b.id) === String(node.id)) b.name = n; }); + } + if (this.previewItem && String(this.previewItem.id) === String(node.id)) { + this.previewItem.name = n; + var pt = document.getElementById('ws-preview-title'); + if (pt) pt.textContent = n; + } } }, cancelRename() { this.renamingId = null; }, diff --git a/e2e/diag_ctx.spec.js b/e2e/diag_ctx.spec.js new file mode 100644 index 0000000..aae023a --- /dev/null +++ b/e2e/diag_ctx.spec.js @@ -0,0 +1,171 @@ +const { test, expect } = require('@playwright/test'); + +const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8080'; +const FD_PASS = process.env.FD_PASS || 'e2e-secret-123'; + +test('diag v5: partial-nav ctx menu + rename sidebar/header sync', async ({ page }) => { + test.setTimeout(180000); + const errors = []; + page.on('console', m => { if (m.type() === 'error') errors.push('CONSOLE: ' + m.text()); }); + page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message)); + + // ── 1. Fresh user + workspace ── + await page.goto(`${FD_BASE}/auth/login?provider=local`); + await page.click('button:has-text("Register")'); + await page.fill('#name', 'Diag'); + await page.fill('#email', 'diag' + Date.now() + '@flowdeck.local'); + await page.fill('#password', FD_PASS); + await page.click('.btn-primary'); + await page.waitForURL('**/workspaces', { timeout: 20000 }); + await page.waitForTimeout(1200); + // Dismiss any onboarding agent dialog that may cover the page + await page.keyboard.press('Escape').catch(() => {}); + await page.waitForTimeout(300); + + // Create + select a workspace via API (CSRF-exempt), no fragile UI dialogs. + const ws = await page.evaluate(async () => { + const r = await fetch('/api/workspaces', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'Diag WS ' + Date.now() }) + }); + const d = await r.json(); + const s = await fetch('/api/workspaces/' + d.id + '/select', { method: 'POST' }); + return { id: d.id, ok: s.ok }; + }); + console.log('WS:', JSON.stringify(ws)); + await page.waitForTimeout(800); + + // Seed items via API. + await page.evaluate(async () => { + const mk = (name, type) => fetch('/api/local-workspace/items', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, type }) + }).then(r => r.json()); + const p = await mk('RenameMe Doc', 'page'); + await mk('Folder A', 'folder'); + window.__seedPageId = p.id; + }); + console.log('STEP1 ok — user + workspace + seeded (all via API)'); + + // ── 2. PARTIAL nav to /local-workspace (exactly the user's bug path) ── + await page.evaluate(() => window.fdNavigate('/local-workspace')); + await page.waitForURL('**/local-workspace**', { timeout: 10000 }); + await page.waitForFunction(() => document.querySelectorAll('[data-ws-id]').length >= 1, { timeout: 10000 }); + await page.waitForTimeout(1500); + console.log('STEP2 ok — partial-nav LW tree items:', await page.locator('[data-ws-id]').count()); + + // ── 3. PARTIAL-nav ctx menu (local-workspace) ── + const rows1 = page.locator('[data-ws-id]'); + if (await rows1.count()) { + await rows1.first().click({ button: 'right' }); + await page.waitForTimeout(700); + const st = await page.evaluate(() => { + const s = window.Alpine && Alpine.store('fdCtx'); + return s ? { open: s.open, node: s.node && s.node.name } : { undefined: true }; + }); + const vis = await page.locator('.fd-ctx-menu').isVisible().catch(() => false); + console.log('STEP3 LW ctx (partial-nav): visible=' + vis + ' fdCtx=' + JSON.stringify(st)); + await page.screenshot({ path: 'shots/diag_v5_lw_ctx.png' }).catch(() => {}); + await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); if (s) s.close(); }); + } + + // ── 4. PARTIAL-nav ctx menu (library) ── + await page.evaluate(() => window.fdNavigate('/library')); + await page.waitForFunction(() => typeof window._libData !== 'undefined', { timeout: 10000 }); + await page.waitForTimeout(1500); + const libRows = page.locator('.lib-row'); + console.log('STEP4 partial /library rows:', await libRows.count(), + 'libData:', await page.evaluate(() => typeof window._libData + ':' + typeof (window._libData && window._libData.onRowContextMenu))); + if (await libRows.count()) { + await libRows.first().click({ button: 'right' }); + await page.waitForTimeout(700); + const vis = await page.locator('.fd-ctx-menu').isVisible().catch(() => false); + const st = await page.evaluate(() => { + const s = window.Alpine && Alpine.store('fdCtx'); + return s ? { open: s.open, node: s.node && s.node.name } : { undefined: true }; + }); + console.log('STEP5 LIB ctx (partial-nav): visible=' + vis + ' fdCtx=' + JSON.stringify(st)); + await page.screenshot({ path: 'shots/diag_v5_lib_ctx.png' }).catch(() => {}); + await page.evaluate(() => { const s = window.Alpine && Alpine.store('fdCtx'); if (s) s.close(); }); + } + + // ── 5. Rename a page via local-workspace tree (partial-nav) ── + await page.evaluate(() => window.fdNavigate('/local-workspace')); + await page.waitForFunction(() => typeof window._wsData !== 'undefined' && typeof window._wsData.commitRename === 'function', { timeout: 10000 }); + await page.waitForFunction(() => document.querySelectorAll('[data-ws-id]').length >= 1, { timeout: 10000 }); + await page.waitForTimeout(1200); + + const rn = await page.evaluate(async () => { + const ws = window._wsData; + const find = (name) => { + const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.name === name) return n; const c = walk(n.children); if (c) return c; } return null; }; + return walk(ws.displayTree) || walk(ws.tree); + }; + const target = find('RenameMe Doc'); + if (!target) return { err: 'node RenameMe Doc not found' }; + ws.renamingId = target.id; + ws.renamingName = 'Renamed Via LW'; + await ws.commitRename(target); + return { id: target.id, name: target.name }; + }); + console.log('STEP6 rename page:', JSON.stringify(rn)); + await page.waitForTimeout(2000); + + const chk1 = await page.evaluate((pid) => { + const row = [...document.querySelectorAll('[data-ws-id]')].find(n => String(n.getAttribute('data-ws-id')) === String(pid)); + const sb = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + pid + '"] .page-name'); + const head = document.querySelector('.header-breadcrumb'); + const crumbs = head ? head.innerText.trim().split(/\s/).filter(Boolean) : []; + return { + inPageRow: row ? row.innerText.trim() : null, + sidebarWsName: sb ? sb.textContent : null, + headerCrumbs: crumbs, + }; + }, rn.id); + console.log('STEP7 after rename:', JSON.stringify(chk1, null, 2)); + + // ── 6. Rename the CURRENT folder → in-page breadcrumb must update (no reload) ── + const fld = await page.evaluate(async () => { + const ws = window._wsData; + const walk = (arr) => { if (!arr) return null; for (const n of arr) { if (n.is_folder && n.name === 'Folder A') return n; const c = walk(n.children); if (c) return c; } return null; }; + const folder = walk(ws.displayTree) || walk(ws.tree); + if (!folder) return { err: 'folder not found' }; + return { id: folder.id, name: folder.name }; + }); + // Partial-nav INTO the folder so the server renders the breadcrumb (folderStack) + await page.evaluate((fid) => window.fdNavigate('/local-workspace?folder=' + fid), fld.id); + await page.waitForFunction(() => { + const ws = window._wsData; + return ws && ws.folderStack && ws.folderStack.length >= 1; + }, { timeout: 10000 }); + await page.waitForTimeout(1200); + const beforeCrumb = await page.evaluate(() => { + const c = document.querySelector('.unified-breadcrumb .bc-current'); + return c ? c.textContent.trim() : null; + }); + console.log('STEP8 crumb before rename:', beforeCrumb); + const rnf = await page.evaluate(async (fid) => { + const ws = window._wsData; + const cur = ws.folderStack && ws.folderStack[ws.folderStack.length - 1]; + if (!cur) return { err: 'no folderStack' }; + ws.renamingId = cur.id; + ws.renamingName = 'Folder Renamed'; + await ws.commitRename(cur); + return { id: cur.id, name: cur.name, bc: ws.breadcrumb.map(b => b.name) }; + }, fld.id); + console.log('STEP9 rename folder:', JSON.stringify(rnf)); + await page.waitForTimeout(2000); + const chk2 = await page.evaluate((fid) => { + const crumb = document.querySelector('.unified-breadcrumb .bc-current'); + const sb = document.querySelector('.sidebar-item.ws-tree-item[data-id="' + fid + '"] .page-name'); + return { + crumbLabel: crumb ? crumb.textContent.trim() : null, + sidebarWsName: sb ? sb.textContent : null, + }; + }, fld.id); + console.log('STEP10 after folder rename:', JSON.stringify(chk2, null, 2)); + await page.screenshot({ path: 'shots/diag_v5_folder_rename.png' }).catch(() => {}); + + console.log('ERRORS:'); + errors.slice(0, 40).forEach(e => console.log(e)); +}); \ No newline at end of file