fix(workspace): menu contextuel après navigation partielle + sync sidebar/header au rename
This commit is contained in:
@@ -986,6 +986,7 @@ async def local_workspace_page(request: Request, folder: int = None):
|
|||||||
"workspace_id": ws_id or 0,
|
"workspace_id": ws_id or 0,
|
||||||
"nav_workspace_id": ws_id or 0,
|
"nav_workspace_id": ws_id or 0,
|
||||||
"breadcrumb": breadcrumb,
|
"breadcrumb": breadcrumb,
|
||||||
|
"breadcrumbs": breadcrumb,
|
||||||
}
|
}
|
||||||
template = env.get_template("local_workspace.html")
|
template = env.get_template("local_workspace.html")
|
||||||
return HTMLResponse(
|
return HTMLResponse(
|
||||||
|
|||||||
@@ -27,9 +27,15 @@
|
|||||||
if (window.__fdCtxMenuRegistered) return;
|
if (window.__fdCtxMenuRegistered) return;
|
||||||
window.__fdCtxMenuRegistered = true;
|
window.__fdCtxMenuRegistered = true;
|
||||||
|
|
||||||
document.addEventListener('alpine:init', function () {
|
// Sur un chargement complet de page, ce script inline s'exécute AVANT
|
||||||
if (window.Alpine && window.Alpine.__fdCtxStore) return;
|
// alpine.min.js (defer) → Alpine n'existe pas encore : on attend 'alpine:init'.
|
||||||
if (window.Alpine) window.Alpine.__fdCtxStore = true;
|
// 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', {
|
Alpine.store('fdCtx', {
|
||||||
open: false, x: 0, y: 0, node: null, page: null,
|
open: false, x: 0, y: 0, node: null, page: null,
|
||||||
@@ -166,7 +172,13 @@
|
|||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (window.Alpine) {
|
||||||
|
registerFdCtx();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('alpine:init', registerFdCtx);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -816,6 +816,26 @@
|
|||||||
return m ? m[1] : '';
|
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. */
|
/** Refresh CSRF token from the server. Returns a promise resolving to the new token. */
|
||||||
refreshCsrfToken: async function() {
|
refreshCsrfToken: async function() {
|
||||||
try {
|
try {
|
||||||
@@ -1182,6 +1202,7 @@
|
|||||||
body: JSON.stringify({ name: newName.trim() })
|
body: JSON.stringify({ name: newName.trim() })
|
||||||
}).then(function(r) {
|
}).then(function(r) {
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
if (window.FlowDeck && window.FlowDeck.syncPageTitle) window.FlowDeck.syncPageTitle(id, newName.trim());
|
||||||
self.refreshSidebarTree();
|
self.refreshSidebarTree();
|
||||||
document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed'));
|
document.dispatchEvent(new CustomEvent('flowdeck:workspace-changed'));
|
||||||
if (window._wsData && window._wsData._reloadAfterAction) {
|
if (window._wsData && window._wsData._reloadAfterAction) {
|
||||||
|
|||||||
@@ -1519,6 +1519,20 @@ window._wsInitData = (function() {
|
|||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
node.name = n; node.db_name = n;
|
node.name = n; node.db_name = n;
|
||||||
this._saveExpandedState();
|
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; },
|
cancelRename() { this.renamingId = null; },
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user