fix: navigation partielle Alpine sans course + scripts de page idempotents (v7.45.2)
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m16s

- app.js : x-ignore sur .main-wrapper au swap fdLoad (nœud remplacé
  uniquement), démontage unique quand tous les <script src> ont exécuté
  (load/error + filet 4s) via Alpine.initTree idempotent (_x_marker) —
  fini les cascades « Undefined variable » (éditeur) et « reading 'has' »
  ($store.fdCtx) : la zone s'initialisait avant composants et stores.
- 5 scripts de page gardés contre la ré-exécution (SyntaxError
  « Identifier 'LW' has already been declared » à la 2e visite
  partielle) : local_workspace, board, settings, database_table,
  page_editor_realtime.
- if (window.Alpine) → Alpine.data immédiat sinon listener alpine:init
  (déjà passé sur swap) : 8 scripts + 7 templates inline.
- app.css : purge des 7 blocs @font-face Inter orphelins (fichiers
  inexistants → 302 HTML → « Failed to decode downloaded font »).
- Gates : e2e/regression_editor_mount + e2e/regression_partial_nav
  (7 pages x complet/1er/2e passage) + regression_logout_dnd + smoke
  = 6/6 verts sur l'image rebuildée · pytest 1094 passed · ruff OK ·
  OpenAPI 7.45.2.
This commit is contained in:
2026-10-02 21:29:46 -04:00
parent c20aeaada1
commit afbc236cc2
27 changed files with 352 additions and 87 deletions
+61
View File
@@ -326,6 +326,67 @@
fdLoad(window.location.href, false);
});
// ── Swap partiel : monter proprement la zone swapée sous Alpine ──
// Lors d'une navigation partielle (fdLoad), les <script src> de la page
// s'exécutent APRÈS le microtask MutationObserver d'Alpine : toute la zone
// .main-wrapper serait initialisée avant que composants ET stores (menu
// contextuel $store.fdCtx, …) existent → « Undefined variable: … » puis
// « reading 'has' » sur les stores. On pose x-ignore sur TOUTE la zone
// pendant le chargement des scripts, puis on démarle quand tous les
// <script src> ont exécuté (load/error) — filet de sécurité 4 s.
// (initTree est idempotent : le _x_marker posé au premier passage évite
// tout double montage.)
var fdLastMw = document.querySelector('.main-wrapper'); // nœud du chargement complet
window.fdRefreshXData = function (name) {
document.querySelectorAll('[x-data="' + name + '()"]').forEach(function (el) {
if (el.__fdRefreshed) return;
var st = el._x_dataStack;
if (!st) return; // sous x-ignore → done() montera
if (st[0] && Object.keys(st[0]).length) return; // déjà monté correctement
el.__fdRefreshed = true; // monté cassé (hors watcher) → on remonte
var fresh = el.cloneNode(true);
el.parentNode.replaceChild(fresh, el);
if (window.htmx) { try { htmx.process(fresh); } catch { /* volontaire */ } }
});
};
document.addEventListener('htmx:afterSwap', function () {
var mw = document.querySelector('.main-wrapper');
// nœud inchangé = swap hors navigation (sidebar, vues) : rien à faire
if (!mw || mw === fdLastMw || mw.__fdSwapWatch) return;
fdLastMw = mw;
mw.__fdSwapWatch = true;
mw.setAttribute('x-ignore', '');
var pending = mw.querySelectorAll('script[src]').length;
var done = function () {
if (!mw.__fdSwapWatch) return;
mw.__fdSwapWatch = false;
document.removeEventListener('load', onScript, true);
document.removeEventListener('error', onScript, true);
if (!mw.isConnected || !mw.hasAttribute('x-ignore')) return;
mw.removeAttribute('x-ignore');
mw._x_ignore = false; // propriété posée par Alpine au checkpoint
try { Alpine.initTree(mw); } catch (e) { console.warn('[FlowDeck] mount', e); }
};
var onScript = function (e) {
if (!e.target || e.target.tagName !== 'SCRIPT') return;
if (--pending <= 0) done();
};
if (!pending) {
// Avant le microtask MutationObserver d'Alpine : retirer x-ignore suffit,
// le MO initialisera au checkpoint (initTree de sécurité = no-op).
mw.removeAttribute('x-ignore');
mw.__fdSwapWatch = false;
setTimeout(function () {
if (mw.isConnected) { try { Alpine.initTree(mw); } catch (e) { console.warn('[FlowDeck] mount', e); } }
}, 0);
return;
}
document.addEventListener('load', onScript, true);
document.addEventListener('error', onScript, true);
setTimeout(done, 4000);
});
// Ctrl/Cmd + \ : toggle the sidebar (Notion-style).
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey || e.metaKey) && (e.key === '\\' || e.code === 'Backslash')) {
+7 -4
View File
@@ -1,3 +1,5 @@
if (!window.__fdBoardScriptsLoaded) {
window.__fdBoardScriptsLoaded = true;
/* exported setActiveTab, kanbanBoard, filterSystem, sortSystem, newIssueForm, showNewIssue -- appeles depuis les attributs HTML des templates */
const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
@@ -22,7 +24,7 @@ const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.
// globales window — probe « Undefined variable: kanbanBoard »).
document.addEventListener('alpine:init', function () { Alpine.data('kanbanBoard', kanbanBoard); });
if (window.Alpine) { Alpine.data('kanbanBoard', kanbanBoard); fdRefreshXData('kanbanBoard'); } else document.addEventListener('alpine:init', function () { Alpine.data('kanbanBoard', kanbanBoard); });
function kanbanBoard() {
return {
collapsedGroups: [],
@@ -42,7 +44,7 @@ const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.
// globales window — probe « Undefined variable: filterSystem »).
document.addEventListener('alpine:init', function () { Alpine.data('filterSystem', filterSystem); });
if (window.Alpine) { Alpine.data('filterSystem', filterSystem); fdRefreshXData('filterSystem'); } else document.addEventListener('alpine:init', function () { Alpine.data('filterSystem', filterSystem); });
function filterSystem() {
return {
activeFilters: [],
@@ -84,7 +86,7 @@ const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.
// globales window — probe « Undefined variable: sortSystem »).
document.addEventListener('alpine:init', function () { Alpine.data('sortSystem', sortSystem); });
if (window.Alpine) { Alpine.data('sortSystem', sortSystem); fdRefreshXData('sortSystem'); } else document.addEventListener('alpine:init', function () { Alpine.data('sortSystem', sortSystem); });
function sortSystem() {
return {
sorts: [],
@@ -111,7 +113,7 @@ const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.
// globales window — probe « Undefined variable: newIssueForm »).
document.addEventListener('alpine:init', function () { Alpine.data('newIssueForm', newIssueForm); });
if (window.Alpine) { Alpine.data('newIssueForm', newIssueForm); fdRefreshXData('newIssueForm'); } else document.addEventListener('alpine:init', function () { Alpine.data('newIssueForm', newIssueForm); });
function newIssueForm() {
return {
title: '', status: 'todo',
@@ -169,3 +171,4 @@ const BD=(()=>{try{const el=document.getElementById('bd-config');return el?JSON.
});
}
});
}
+3
View File
@@ -1,3 +1,5 @@
if (!window.__fdDbTableScriptsLoaded) {
window.__fdDbTableScriptsLoaded = true;
const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=el?JSON.parse(el.textContent):null;return v===undefined?null:v}catch{return null}})();
(function() {
@@ -1309,3 +1311,4 @@ const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=
.catch(function(){ return rowId; });
}
})();
}
+8 -2
View File
@@ -1,5 +1,5 @@
document.addEventListener('alpine:init', () => {
var _regGiteaWs = () => {
Alpine.data('giteaWorkspace', () => {
const params = new URLSearchParams(window.location.search);
const owner = params.get('owner') || '';
@@ -637,4 +637,10 @@ document.addEventListener('alpine:init', () => {
},
};
});
});
};
if (window.Alpine) {
_regGiteaWs();
fdRefreshXData('giteaWorkspace');
} else {
document.addEventListener('alpine:init', _regGiteaWs);
}
+1 -1
View File
@@ -4,7 +4,7 @@
// globales window — probe « Undefined variable: importWizard »).
document.addEventListener('alpine:init', function () { Alpine.data('importWizard', importWizard); });
if (window.Alpine) { Alpine.data('importWizard', importWizard); fdRefreshXData('importWizard'); } else document.addEventListener('alpine:init', function () { Alpine.data('importWizard', importWizard); });
function importWizard() {
return {
sources: [],
+2 -3
View File
@@ -2,9 +2,8 @@
// A20 phase 3 : enregistrement Alpine.data — le build CSP ne résout que le
// registre (probe : globale window → « Undefined variable: libraryPage »).
document.addEventListener('alpine:init', function () {
Alpine.data('libraryPage', libraryPage);
});
if (window.Alpine) { Alpine.data('libraryPage', libraryPage); fdRefreshXData('libraryPage'); }
else document.addEventListener('alpine:init', function () { Alpine.data('libraryPage', libraryPage); });
function libraryPage() {
return {
+12 -2
View File
@@ -1,3 +1,5 @@
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');
@@ -2065,7 +2067,7 @@ 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).
document.addEventListener('alpine:init', function () {
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; });
@@ -2089,4 +2091,12 @@ document.addEventListener('alpine:init', function () {
};
return t;
});
});
};
if (window.Alpine) {
_regWs();
fdRefreshXData('wsInitData');
fdRefreshXData('wsPreview');
} else {
document.addEventListener('alpine:init', _regWs);
}
}
+3
View File
@@ -1,3 +1,5 @@
if (!window.__fdRtScriptsLoaded) {
window.__fdRtScriptsLoaded = true;
const RT=(()=>{try{const el=document.getElementById('rt-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
/* eslint-disable */
@@ -530,3 +532,4 @@ window.__fdRT = (function () {
return { start, syncNow };
})();
}
+5 -1
View File
@@ -2535,6 +2535,10 @@ applyAIBlocks(text){
// globales window — probe « Undefined variable: editorState »).
document.addEventListener('alpine:init', function () { Alpine.data('editorState', editorState); });
// Navigation partielle (fdLoad) : Alpine démarre déjà → 'alpine:init'
// ne sera plus émis, on enregistre tout de suite (pattern _ctx_menu.js)
// et on remonte la racine montée avant l'enregistrement.
if (window.Alpine) { Alpine.data('editorState', editorState); fdRefreshXData('editorState'); }
else document.addEventListener('alpine:init', function () { Alpine.data('editorState', editorState); });
window.editorState = editorState;
}
+4 -1
View File
@@ -1,3 +1,5 @@
if (!window.__fdSettingsScriptsLoaded) {
window.__fdSettingsScriptsLoaded = true;
/* exported settingsInit -- appeles depuis les attributs HTML des templates */
const ST=(()=>{try{const el=document.getElementById('st-config');return el?JSON.parse(el.textContent):{}}catch{return {}}})();
@@ -5,7 +7,7 @@ const ST=(()=>{try{const el=document.getElementById('st-config');return el?JSON.
// globales window — probe « Undefined variable: settingsInit »).
document.addEventListener('alpine:init', function () { Alpine.data('settingsInit', settingsInit); });
if (window.Alpine) { Alpine.data('settingsInit', settingsInit); fdRefreshXData('settingsInit'); } else document.addEventListener('alpine:init', function () { Alpine.data('settingsInit', settingsInit); });
function settingsInit() {
return {
// ── A20 ph3 : expressions hostiles au parseur CSP (window/Date) ──
@@ -1233,3 +1235,4 @@ function settingsInit() {
},
};
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
// globales window — probe « Undefined variable: workspacesPage »).
document.addEventListener('alpine:init', function () { Alpine.data('workspacesPage', workspacesPage); });
if (window.Alpine) { Alpine.data('workspacesPage', workspacesPage); fdRefreshXData('workspacesPage'); } else document.addEventListener('alpine:init', function () { Alpine.data('workspacesPage', workspacesPage); });
function workspacesPage() {
return {
workspaces: [],