fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped

Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
2026-10-09 16:58:04 -04:00
parent 7468e497c6
commit 0218d8f5e5
44 changed files with 12060 additions and 1729 deletions
+111 -12
View File
@@ -1,6 +1,12 @@
// Config de la page courante — relue à CHAQUE exécution du script : sur une
// navigation partielle htmx le <script> est ré-exécuté (avec le nouveau
// #lw-config) alors que le corps du fichier est verrouillé par le garde
// ci-dessous. Avec un `const` capturé à la première exécution, la page
// gardait le folder/workspace du premier chargement.
var LW = (function(){try{var el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
if (!window.__fdLocalWorkspaceScriptsLoaded) {
window.__fdLocalWorkspaceScriptsLoaded = true;
const LW=(()=>{try{const el=document.getElementById('lw-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
console.log('FLOWDECK v2.4.6 — IIFE running, about to set _wsInitData');
const _wsInitData = (function() { // lexical : NON propriété globalThis →
@@ -150,6 +156,19 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
try {
// Expose globally for event handlers (BEFORE await)
window._wsData = this;
// Navigation partielle : l'objet est partagé entre les montages →
// on recale l'état sur la page courante (#lw-config fraîchement lu)
// et on remet à zéro les panneaux/menus de la vue précédente.
if (LW && LW.current_folder_id != null) this.currentFolder = LW.current_folder_id;
this.searchQuery = '';
this.filterType = '';
this.editingPath = false;
this.previewItem = null;
this.uploading = false;
this.undoVisible = false;
this.moveModalOpen = false;
this.isDragging = false;
this.clearSelection();
var url = '/api/local-workspace/tree';
if (this.currentFolder && this.currentFolder > 0) {
url += '?folder=' + this.currentFolder;
@@ -197,6 +216,32 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
} catch(e) { console.error('Tree load error:', e); this.loaded = true; }
this._recompute();
this._initResizeHandle();
this._syncHeaderPath();
},
/** Recalcule le chemin du header (Home / <workspace> / <dossier> / …) à
partir de la pile de dossiers courante — même mécanique que le rendu
serveur, mais sans rechargement. */
_syncHeaderPath() {
var stack = this.folderStack || [];
var crumbs = [
{ label: 'Home', url: '/workspaces', id: null, icon: 'home', menu: false, home: true },
{ label: (LW && LW.workspace_name) || 'Workspace', url: '/local-workspace',
id: null, icon: 'folder', menu: false }
];
for (var i = 0; i < stack.length; i++) {
var last = (i === stack.length - 1);
crumbs.push({
label: stack[i].name,
url: last ? null : '/local-workspace?folder=' + stack[i].id,
id: stack[i].id,
icon: 'folder',
menu: true
});
}
window.dispatchEvent(new CustomEvent('flowdeck:breadcrumb-changed', {
detail: { workspace_id: (LW && LW.workspace_id) || 0, crumbs: crumbs }
}));
},
_initResizeHandle() {
@@ -486,6 +531,12 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
if (b.type === 'code' || b.type === 'code_block') {
tag = 'pre';
style += 'background:var(--bg-tertiary);padding:10px 14px;border-radius:6px;font-family:monospace;font-size:12px;white-space:pre-wrap;';
var cLang = (window.FDHighlight && b.language) ? FDHighlight.normalizeLang(b.language) : '';
var cAttr = (cLang && cLang !== 'plaintext') ? ' data-hljs data-hljs-lang="' + cLang + '" class="hljs language-' + cLang + '"' : '';
var cHtml = (window.FDHighlight && cLang && cLang !== 'plaintext')
? (function(){ try { return FDHighlight.highlightText(txt, cLang); } catch (e) { return ''; } })() || this._escHtml(txt)
: this._escHtml(txt);
return '<' + tag + ' style="' + style + '"><code' + cAttr + '>' + (txt ? cHtml : '<br>') + '</code></' + tag + '>';
}
return '<' + tag + ' style="' + style + '">' + (txt ? this._escHtml(txt) : '<br>') + '</' + tag + '>';
}, this).join('');
@@ -493,7 +544,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">Empty document</div>';
}
} catch {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(String(content).substring(0, 3000)) + '</pre>';
preview = this._codeHtml(String(content).substring(0, 3000), node.name, 3000);
}
} else if (format === 'markdown') {
@@ -501,15 +552,16 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
preview = '<div style="white-space:pre-wrap;line-height:1.6;font-size:13px;">' + this._escHtml(String(content).substring(0, 5000)) + '</div>';
} else {
// Unknown format — show as text
// Unknown format — show as (highlighted) text
var display = String(content || '').trim();
if (display) {
preview = '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">' + this._escHtml(display.substring(0, 5000)) + '</pre>';
preview = this._codeHtml(display, node.name, 5000);
} else {
preview = '<div style="text-align:center;padding:30px;color:var(--text-tertiary)">No content</div>';
}
}
body.innerHTML = preview || '<div style="color:var(--text-tertiary);text-align:center;padding:20px">(empty)</div>';
try { if (window.FDHighlight) FDHighlight.highlightAllIn(body); } catch (e) {}
} catch {
body.innerHTML = '<div style="text-align:center;padding:20px;color:var(--text-tertiary)">Preview not available</div>';
}
@@ -519,6 +571,23 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
},
// ── hljs: code-aware <pre> for previews (FDHighlight, ~230 file types).
// Falls back to escaped plain text when the helper is unavailable.
_codeHtml(text, name, maxLen) {
var t = String(text == null ? '' : text).substring(0, maxLen || 5000);
if (window.FDHighlight) {
try {
var lang = FDHighlight.langForFile(name || '');
var inner = FDHighlight.highlightText(t, lang);
var cls = lang !== 'plaintext' ? ' class="hljs language-' + lang + '"' : '';
return '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">'
+ '<code' + cls + '>' + inner + '</code></pre>';
} catch (e) { /* fallback below */ }
}
return '<pre style="background:var(--bg-tertiary);padding:14px;border-radius:6px;overflow:auto;font-size:12px;font-family:monospace;white-space:pre-wrap;">'
+ this._escHtml(t) + '</pre>';
},
// ── Content snippet for content view ──
contentSnippetMap: {},
@@ -765,7 +834,10 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
html += '<span class="icon">'+icon+'</span>';
if (c.is_folder) {
var countBadge = (c.child_count > 0) ? ' <span class="child-count">('+c.child_count+')</span>' : '';
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+c.name+countBadge+'</span>';
var statusBadges = (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ (c.favorited ? '<span class="ws-fav-badge" title="Favorite">★</span>' : '')
+ (c.published ? '<span class="ws-pub-badge" title="Published to web">🌐</span>' : '');
html += '<span class="name folder" onclick="window._wsData.navigateToFolder('+escId+')">'+statusBadges+c.name+countBadge+'</span>';
} else {
// Click to rename, double-click to open
html += '<span class="name file" onclick="event.stopPropagation();window._wsData.startRenderChildRename('+escId+',\''+escName+'\')"'
@@ -773,8 +845,10 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
+ ' title="Click to rename, double-click to open"'
+ ' onmouseenter="var d=window._wsData;if(d)d.showPreview(event,{id:'+escId+',name:\''+escName+'\',is_folder:false,content_format:\''+(c.content_format||'')+'\'})"'
+ ' onmouseleave="var d=window._wsData;if(d)d.hidePreview()">'
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ (c.is_shared ? '<span class="ws-shared-badge" title="Partag\u00e9" onclick="event.stopPropagation()">👥</span>' : '')
+ (c.favorited ? '<span class="ws-fav-badge" title="Favorite">★</span>' : '')
+ (c.published ? '<span class="ws-pub-badge" title="Published to web">🌐</span>' : '')
+ '<span id="rc-name-'+escId+'">'+c.name+'</span>'
+ '<input id="rc-input-'+escId+'" class="inline-rename-input" style="display:none"'
+ ' onkeydown="if(event.key===\'Enter\')window._wsData.commitRenderChildRename('+escId+')"'
+ ' onblur="window._wsData.commitRenderChildRename('+escId+')"'
@@ -956,6 +1030,7 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
this._saveExpandedState();
this.previewItem = null;
this._recompute();
this._syncHeaderPath();
var self = this;
this.$nextTick(function() { self._syncCheckboxes(); });
} catch(e) { console.error('Load folder error:', e); }
@@ -1230,8 +1305,9 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
} else {
this.previewContent = 'Loading...';
this._fetchRetry(url).then(function(t) {
var content = t.substring(0, 500);
self.previewContent = content; self.previewCache[cacheKey] = content;
var html = self._codeHtml(t, node.name, 500);
// Tooltip body is already monospace/pre-wrap: keep it lightweight.
self.previewContent = html; self.previewCache[cacheKey] = html;
}).catch(function() { self.previewContent = '(preview not available)'; });
}
} else {
@@ -2043,9 +2119,32 @@ console.log('FLOWDECK v2.4.6 — IIFE done, _wsInitData keys:', Object.keys(_wsI
// A20 ph3 : registre Alpine.data (le build CSP ne résout que le registre ;
// x-data="wsInitData()" au lieu de l'identifiant global _wsInitData).
var _regWs = function () {
// objet partagé JS↔Alpine via closure LEXICALE (const hors window →
// non snapshoté par ji ; probe4 : factory retournant l'objet = accepté).
Alpine.data('wsInitData', function () { return _wsInitData; });
// Objet partagé JS↔Alpine via closure LEXICALE (const hors window →
// non snapshoté par ji). L'état reste unique, MAIS la factory Alpine
// doit retourner une enveloppe FRAÎCHE à chaque montage : Alpine pose ses
// magics ($nextTick…) avec Object.defineProperty non-configurable. En
// retournant le même objet, le 2e montage (navigation partielle
// /local-workspace → /local-workspace?folder=) levait
// « Cannot redefine property: $nextTick », initTree échouait et TOUT le
// contenu restait au state brut (tous les x-show visibles à la fois).
// L'enveloppe délègue lecture/écriture au proxy réactif partagé.
Alpine.data('wsInitData', function () {
var R = Alpine.reactive(_wsInitData);
var t = {};
Object.keys(_wsInitData).forEach(function (k) {
if (typeof _wsInitData[k] === 'function') {
t[k] = _wsInitData[k];
} else {
Object.defineProperty(t, k, {
get: function () { return R[k]; },
set: function (v) { R[k] = v; },
enumerable: true,
configurable: true,
});
}
});
return t;
});
// Bloc preview : le parseur le place HORS de la div racine (structure
// pré-existante) → montage distinct DÉLÉGUANT vers l'objet partagé.
// Wrapper = objet unique (les magics $nextTick… ne sont redéfinissables