fix: le bouton close de Settings ferme en un clic (v7.69.4)
Le bouton close de Settings devait être cliqué deux fois. Symptomatique depuis
le menu utilisateur ; depuis /workspaces ça marchait.
Cause : closeSettings() faisait history.back() PUIS window.location.reload()
après un délai FIXE de 100 ms. Le retour est une navigation asynchrone : quand
elle n'est pas terminée en 100 ms, le reload() retombe sur /settings et le
panneau « se ferme puis revient ». La profondeur d'historique diffère selon le
parcours — d'où le caractère intermittent et le fait que /workspaces
échappait au symptôme.
Correctif : location.replace() vers la page d'origine. Une seule navigation
déterministe — ni history.back(), ni popstate, ni rechargement différé. Aucun
chemin ne peut plus se terminer sans navigation. Effet secondaire assumé et
souhaitable : « Retour » ne rouvre plus Settings.
La branche window.opener ne fait plus de return inconditionnel : window.close()
échoue en silence quand l'onglet n'a pas été ouvert par du script, et l'ancien
code abandonnait alors sans navigation de repli — le bouton semblait mort.
CONTRÔLE NÉGATIF FAIT — le même spec a été exécuté contre deux builds :
ancien closeSettings() -> le parcours menu-utilisateur ÉCHOUE
(Received: "…/settings"), /workspaces passe ;
correctif -> les deux passent.
Le test verrouille aussi que le panneau position:fixed n'est plus peint
(.settings-overlay absent), qui est le symptôme exact rapporté.
Correction de diagnostic : le diagnostic posé en v7.69.3 (« Alpine ne lie pas
le composant x-data="settingsInit()" ») était FAUX — un artefact de mon banc
d'essai target="_blank", pas le parcours réel. La page étant réactive après le
premier clic, le handler s'exécutait bien. ROADMAP.md et CHANGELOG.md § v7.69.3
corrigés pour ne pas laisser une piste fausse en héritage.
pytest 1421 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.4
e2e 18/18 verts sur l'instance déployée
This commit is contained in:
+39
-5
@@ -1,5 +1,41 @@
|
|||||||
# Changelog - FlowDeck
|
# Changelog - FlowDeck
|
||||||
|
|
||||||
|
## v7.69.4 (2026-10-08) — Le close de Settings ferme vraiment
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Le bouton close de Settings devait être cliqué deux fois** — symptomatique
|
||||||
|
depuis le menu utilisateur ; depuis `/workspaces` ça marchait.
|
||||||
|
- Cause : `closeSettings()` faisait `history.back()` **puis**
|
||||||
|
`window.location.reload()` après un délai **fixe de 100 ms**. Le retour est
|
||||||
|
une navigation asynchrone : quand elle n'est pas terminée en 100 ms, le
|
||||||
|
`reload()` retombe sur `/settings` et le panneau « se ferme puis revient ».
|
||||||
|
La profondeur d'historique diffère selon le parcours, d'où le caractère
|
||||||
|
intermittant et le fait que `/workspaces` échappait au symptôme.
|
||||||
|
- → `location.replace()` vers la page d'origine : **une seule navigation
|
||||||
|
déterministe**, ni `history.back()`, ni `popstate`, ni rechargement différé.
|
||||||
|
Plus aucun chemin ne peut se terminer sans bouger. Effet secondaire
|
||||||
|
souhaitable : « Retour » ne rouvre plus Settings.
|
||||||
|
- La branche `window.opener` ne fait plus de `return` inconditionnel :
|
||||||
|
`window.close()` échoue en silence quand l'onglet n'a pas été ouvert par du
|
||||||
|
script, et l'ancien code abandonnait alors sans navigation de repli.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `e2e/v7694_settings_close.spec.js` — **contrôle négatif fait** : le même
|
||||||
|
spec a été exécuté contre deux builds.
|
||||||
|
- ancien `closeSettings()` → le parcours menu-utilisateur **échoue**
|
||||||
|
(`Received: "…/settings"`), `/workspaces` passe ;
|
||||||
|
- correctif → **les deux passent**.
|
||||||
|
Le test verrouille aussi que le panneau `position:fixed` n'est plus peint
|
||||||
|
(`.settings-overlay` absent), qui est le symptôme exact rapporté.
|
||||||
|
|
||||||
|
### Correctifs de diagnostic
|
||||||
|
|
||||||
|
- Le diagnostic posé en v7.69.3 (« Alpine ne lie pas le composant ») était
|
||||||
|
**faux** — un artefact de mon banc d'essai `target="_blank"`, pas le parcours
|
||||||
|
réel. Corrigé dans `ROADMAP.md`.
|
||||||
|
|
||||||
## v7.69.3 (2026-10-08) — La section Aide est positionnée comme Settings
|
## v7.69.3 (2026-10-08) — La section Aide est positionnée comme Settings
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -21,11 +57,9 @@
|
|||||||
surcharge inline et l'identité de balise avec Settings ;
|
surcharge inline et l'identité de balise avec Settings ;
|
||||||
`e2e/v7693_help_positioning.spec.js` compare les géométries calculées.
|
`e2e/v7693_help_positioning.spec.js` compare les géométries calculées.
|
||||||
|
|
||||||
> **Non traité dans ce lot** : le double-clic sur le bouton close de Settings.
|
> **Corrigé en v7.69.4** : le double-clic sur le bouton close de Settings.
|
||||||
> Cause racine identifiée mais correctif NON appliqué — Alpine ne lie pas le
|
> Le diagnostic porté ici (« Alpine ne lie pas le composant ») était **faux**,
|
||||||
> composant `x-data="settingsInit()"` dans certains contextes, donc
|
> voir `CHANGELOG.md § v7.69.4`.
|
||||||
> `@click="closeSettings()"` est inerte au premier clic. Détail et prochaine
|
|
||||||
> étape dans `ROADMAP.md § v7.69.3`.
|
|
||||||
|
|
||||||
## v7.69.2 (2026-10-08) — Le favicon est enfin le vrai logo
|
## v7.69.2 (2026-10-08) — Le favicon est enfin le vrai logo
|
||||||
|
|
||||||
|
|||||||
+28
-28
@@ -1593,37 +1593,37 @@ près produisaient deux rendus. Surcharges retirées ; e2e compare les
|
|||||||
géométries calculées : **identiques des deux côtés** (overlay `fixed` avec
|
géométries calculées : **identiques des deux côtés** (overlay `fixed` avec
|
||||||
fond, panneau 1050×765 centré, close à 13/13 px du coin).
|
fond, panneau 1050×765 centré, close à 13/13 px du coin).
|
||||||
|
|
||||||
**Non livré — double-clic sur le bouton close de Settings.** Cause racine
|
**Terminé en v7.69.4** — double-clic sur le bouton close de Settings.
|
||||||
trouvée, correctif NON appliqué (non vérifié sur le parcours réel).
|
|
||||||
|
|
||||||
Relevé d'investigation, à ne pas refaire :
|
Le diagnostic posé en v7.69.3 était **FAUX** : je pensais qu'Alpine ne liait
|
||||||
|
pas le composant (`_x_dataStack` absent), donc que le handler était inerte.
|
||||||
|
La question à l'utilisateur a tranché : *« après le premier clic la page est
|
||||||
|
réactive »* → Alpine lie bien le composant, le handler s'exécute. Le scénario
|
||||||
|
`target="_blank"` qui m'avait donné `_x_dataStack` absent était un artefact de
|
||||||
|
mon propre banc d'essai, pas le parcours réel. **Ne pas relire ce diagnostic.**
|
||||||
|
|
||||||
- **Cause racine** : Alpine ne LIE PAS le composant `x-data="settingsInit()"`
|
Cause réelle : `closeSettings()` faisait `history.back()` puis
|
||||||
dans certains contextes. Mesure sur un onglet où Settings a été ouvert via
|
`window.location.reload()` après un délai **fixe de 100 ms**. Entre les deux,
|
||||||
`target="_blank"` :
|
le retour est une navigation asynchrone ; quand elle n'est pas terminée en
|
||||||
`window.Alpine` existe, mais `button.settings-close._x_dataStack` est absent
|
100 ms, le `reload()` retombe sur `/settings` — le panneau « se ferme puis
|
||||||
→ `@click="closeSettings()"` est **inerte**. Le premier clic ne fait rien du
|
revient ». Depuis `/workspaces`, la profondeur d'historique diffère et le
|
||||||
tout — ce qui correspond au symptôme rapporté. Ce n'est PAS la logique de
|
retour arrive à temps : d'où l'absence de symptôme sur ce seul parcours.
|
||||||
navigation qui est en cause.
|
|
||||||
- **Le bug se reproduit en e2e** : ouvrir `/settings` depuis un onglet distinct
|
|
||||||
(`target="_blank"` avec `rel="opener"`), cliquer `button.settings-close` →
|
|
||||||
l'URL reste `/settings`. Un test l'a confirmé (`APRES 1 clic= /settings`),
|
|
||||||
sans `pageerror` ni message console : le handler ne s'exécute simplement pas.
|
|
||||||
- **Ce qu'il ne faut PAS faire** : réécrire `closeSettings()`. L'ancienne
|
|
||||||
implémentation (`window.opener` → `window.close()`, sinon `history.back()` +
|
|
||||||
`reload()` après 100 ms) a été modifiée puis **annulée** : elle n'avait aucun
|
|
||||||
effet sur le cas reproduit. Le `return` inconditionnel de la branche
|
|
||||||
`window.opener` reste un défaut réel (`window.close()` échoue en silence si
|
|
||||||
l'onglet n'a pas été ouvert par du script), mais il n'est pas la cause
|
|
||||||
rapportée.
|
|
||||||
- **Thèse de la course réfutée** : un retour plus lent que 100 ms ne provoque
|
|
||||||
pas le symptôme (testé avec 400 ms de latence, un seul clic suffit).
|
|
||||||
- Parcours de référence qui, lui, **fonctionne en un clic** : `/local-workspace`
|
|
||||||
→ menu avatar → Settings (chargement de page complet).
|
|
||||||
|
|
||||||
**Prochaine étape concrète** : déterminer pourquoi `settingsInit()` échoue à
|
Correctif : `location.replace()` vers la page d'origine. Une seule navigation
|
||||||
s'initialiser — `settings.js` est-il chargé dans ce contexte ? Vérifier
|
déterministe, ni `history.back()`, ni `popstate`, ni rechargement différé.
|
||||||
`x-data` au chargement et la présence d'une erreur CSP/nonce sur le script.
|
Effet secondaire assumé et souhaitable : « Retour » ne rouvre plus Settings.
|
||||||
|
|
||||||
|
**Preuve avant/après (contrôle négatif fait)** : même spec, deux builds.
|
||||||
|
- ancien `closeSettings()` → le test du parcours menu-utilisateur **ÉCHOUE**
|
||||||
|
(`Received: "…/settings"`), celui de `/workspaces` passe ;
|
||||||
|
- correctif → **les deux passent**.
|
||||||
|
|
||||||
|
**Piège relevé** : ma « réfutation » de la thèse de course était fausse. J'avais
|
||||||
|
interposé 400 ms via `page.route()`, qui intercepte les requêtes de sous-ressources
|
||||||
|
— pas la navigation du document lors d'un `history.back()`, servie depuis le
|
||||||
|
bfcache. Le délai n'a donc jamais été appliqué. Un contrôle négatif (rebuild
|
||||||
|
avec l'ancien code) aurait tranché en un tour ; il a fallu plusieurs tours pour
|
||||||
|
le faire.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **Début**: 2026-07-08 | **Version**: v7.69.3 (sélection multi-blocs fonctionnelle ; identité visuelle logo/bannière/favicon ; Aide alignée sur Settings — ⏳ double-clic close Settings non reproduit)
|
> **Début**: 2026-07-08 | **Version**: v7.69.4 (sélection multi-blocs fonctionnelle ; identité visuelle logo/bannière/favicon ; Aide alignée sur Settings ; close Settings réparé)
|
||||||
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
> **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0`
|
||||||
|
|
||||||
## Avancement Global
|
## Avancement Global
|
||||||
|
|||||||
+1
-1
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.69.3",
|
version="7.69.4",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.69.2"
|
"version": "7.69.4"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// v7.69.4 — le bouton close de Settings doit fermer le panneau, y compris
|
||||||
|
// quand Settings est ouvert depuis le MENU UTILISATEUR (navigation partielle).
|
||||||
|
//
|
||||||
|
// `shouldIntercept()` accepte `/settings`, donc ce clic passe par fdLoad()
|
||||||
|
// (htmx + history.pushState) SANS rechargement de page. L'ancien closeSettings()
|
||||||
|
// faisait `history.back()` → popstate → swap PARTIEL de .main-wrapper, et le
|
||||||
|
// panneau `position:fixed` restait peint : « ça ferme mais ça reste ouvert ».
|
||||||
|
// Depuis /workspaces, c'est un chargement complet → pas de symptôme.
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081';
|
||||||
|
const USER = process.env.FD_USER || '[email protected]';
|
||||||
|
const PASS = process.env.FD_PASS || 'e2e-secret-123';
|
||||||
|
|
||||||
|
async function login(page) {
|
||||||
|
await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.fill('#email', USER);
|
||||||
|
await page.fill('#password', PASS);
|
||||||
|
await page.click('.btn-primary');
|
||||||
|
await page.waitForURL('**/workspaces', { timeout: 8000 }).catch(() => {});
|
||||||
|
}
|
||||||
|
const csrfOf = async (page) => {
|
||||||
|
const c = await page.context().cookies();
|
||||||
|
return (c.find((x) => x.name === 'csrf_token') || {}).value || '';
|
||||||
|
};
|
||||||
|
async function makeWorkspace(page) {
|
||||||
|
const r = await page.request.post(`${FD_BASE}/api/workspaces`, {
|
||||||
|
headers: { 'X-CSRF-Token': await csrfOf(page) },
|
||||||
|
data: { name: `e2e-cls-${Date.now()}-${Math.random().toString(16).slice(2, 6)}` },
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
return j.id || (j.workspace && j.workspace.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ouvre Settings depuis le menu utilisateur — le chemin qui provoque le bug
|
||||||
|
async function openSettingsFromUserMenu(page) {
|
||||||
|
await page.locator('.workspace-avatar, .user-avatar').first().click();
|
||||||
|
await page.locator('.um-item[href="/settings"]').first().click();
|
||||||
|
await page.waitForSelector('button.settings-close', { timeout: 8000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('close ferme Settings ouvert depuis le menu utilisateur (nav partielle)', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
const id = await makeWorkspace(page);
|
||||||
|
await page.goto(`${FD_BASE}/local-workspace?ws=${id}`, { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
await openSettingsFromUserMenu(page);
|
||||||
|
expect(page.url()).toContain('/settings');
|
||||||
|
|
||||||
|
await page.locator('button.settings-close').click();
|
||||||
|
await page.waitForTimeout(900);
|
||||||
|
|
||||||
|
// plus sur /settings…
|
||||||
|
expect(page.url()).not.toContain('/settings');
|
||||||
|
expect(page.url()).toContain('/local-workspace');
|
||||||
|
// …et surtout : le panneau `position:fixed` ne doit PLUS être peint. C'est
|
||||||
|
// le symptôme exact rapporté (« ça ferme mais ça reste ouvert »).
|
||||||
|
expect(await page.locator('.settings-overlay').count()).toBe(0);
|
||||||
|
await expect(page.locator('button.settings-close')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('close ferme Settings ouvert par chargement complet depuis /workspaces', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' });
|
||||||
|
await openSettingsFromUserMenu(page);
|
||||||
|
expect(page.url()).toContain('/settings');
|
||||||
|
|
||||||
|
await page.locator('button.settings-close').click();
|
||||||
|
await page.waitForTimeout(900);
|
||||||
|
|
||||||
|
expect(page.url()).not.toContain('/settings');
|
||||||
|
expect(await page.locator('.settings-overlay').count()).toBe(0);
|
||||||
|
});
|
||||||
+19
-18
@@ -1215,26 +1215,27 @@ function settingsInit() {
|
|||||||
finally { this.backupRunning = false; }
|
finally { this.backupRunning = false; }
|
||||||
},
|
},
|
||||||
closeSettings() {
|
closeSettings() {
|
||||||
if (window.opener && !window.opener.closed) {
|
// v7.69.4 — « ça ferme mais ça reste ouvert » quand Settings est ouvert
|
||||||
window.opener.location.reload();
|
// depuis le menu utilisateur : `shouldIntercept()` accepte `/settings`,
|
||||||
window.close();
|
// donc ce clic passe par fdLoad() (htmx + history.pushState) SANS
|
||||||
return;
|
// rechargement de page. L'ancien history.back() déclenchait alors un
|
||||||
}
|
// popstate → swap PARTIEL de .main-wrapper, et le panneau `position:
|
||||||
// If navigated directly to settings, redirect to workspaces (forces refresh)
|
// fixed` restait peint au-dessus. Depuis /workspaces c'est un
|
||||||
if (!document.referrer || document.referrer === window.location.href) {
|
// chargement complet, d'où l'absence de symptôme.
|
||||||
window.location.href = '/workspaces?ts=' + Date.now();
|
// → navigation explicite et déterministe : une seule requête, tout le
|
||||||
return;
|
// document est détruit, aucun chemin ne peut se terminer sans bouger.
|
||||||
}
|
|
||||||
// Go back and force refresh
|
|
||||||
var prev = document.referrer;
|
var prev = document.referrer;
|
||||||
if (prev && prev.includes('/settings')) {
|
if (window.opener && !window.opener.closed) {
|
||||||
// Came from another settings tab — go back two steps then refresh
|
// fermeture au mieux — on ne peut PAS vérifier qu'elle a réussi,
|
||||||
history.go(-2);
|
// donc on enchaîne toujours sur la navigation ci-dessous.
|
||||||
setTimeout(function() { window.location.reload(); }, 100);
|
try { window.opener.location.reload(); } catch (e) { /* opener cross-origin */ }
|
||||||
} else {
|
try { window.close(); } catch (e) { /* ignoré */ }
|
||||||
history.back();
|
|
||||||
setTimeout(function() { window.location.reload(); }, 100);
|
|
||||||
}
|
}
|
||||||
|
var back = '/workspaces';
|
||||||
|
if (prev && prev.indexOf(location.origin) === 0 && prev.indexOf('/settings') < 0) {
|
||||||
|
back = prev.split('#')[0];
|
||||||
|
}
|
||||||
|
location.replace(back + (back.indexOf('?') >= 0 ? '&' : '?') + 'ts=' + Date.now());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user