fix: side peek = document seul en edition + bouton Open calendrier/Kanban (v7.50.0)
FlowDeck CI / lint (push) Successful in 2m15s
FlowDeck CI / test (push) Failing after 13m47s
FlowDeck CI / docker (push) Skipped

- Les 4 peeks (library.js, local_workspace.js, my_tasks.js, database_table.js)
  chargent /pages/{id}?embed=1 : le panneau ne montre plus que le document en
  mode edition, sans sidebar ni barre (regression v7.49.0). Le cadre du
  panneau (close/full/resize partages fdWirePeekResize) est inchange.
- Calendrier My Tasks : le handler testait e.target.dataset.openRow mais les
  boutons .mt-cal-open ne contiennent que leur <svg> -> clic sur l'icone =
  e.target = <svg>, rien ne partait. cause racine corrigee par
  t.closest('[data-open-row]') : un seul chemin tableau/calendrier/Kanban.
- Kanban My Tasks : meme bouton Open ajoute sur les tuiles (openButton(t),
  overlay absolu revele au survol de la carte .mt-card).
- Menu contextuel global « Open in side peek » : etait un toast d'erreur ;
  route maintenant vers le peek de la page courante (_libData.openPeekById /
  _wsData.openSidePeek), repli nouvel onglet ailleurs.
- /pages/{id}?embed=1 sur une page de base (content_format=collection)
  rendait un editeur de blocs vide : le template collection est garde en
  embed (body.embed-mode retire sidebar+barre, le tableau reste). Bouton
  pleine page des peeks de bases : navigue via dataset.page (URL propre)
  au lieu du src ?embed=1.
- Tests : garde de coherence inversee (?embed=1 exige sur les 4 fichiers),
  +3 nouveaux (closest, tuiles Kanban, collection embed). Fix d'un test
  pre-existant casse (assertion panel.style.display, reecriture vanilla JS
  v7.49.0). Suite 1264 passed / 0 failed (-n auto) + ruff 0.
- Bump VERSION/main.py 7.50.0, CHANGELOG, WORKLOAD, OpenAPI regenere.
This commit is contained in:
2026-10-06 12:48:48 -04:00
parent 7dbaefb381
commit 3706689eca
14 changed files with 151 additions and 43 deletions
+45
View File
@@ -1,5 +1,50 @@
# Changelog - FlowDeck # Changelog - FlowDeck
## v7.50.0 (2026-10-06) — Side peek : le document seul (mode édition), sans sidebar ni barre
### Fixed
- **Tous les boutons « Open » ouvraient l'habillage pleine page dans le panneau
latéral** — `library.js` (OPEN de hover), `local_workspace.js` (↢ Open +
« Open in side peek » du menu contextuel) et `my_tasks.js` (peek des tâches)
chargeaient `/pages/{id}` SANS `?embed=1` : le peek affichait sidebar +
barre de navigation + éditeur (régression v7.49.0). Tous repassent en
`/pages/{id}?embed=1` = le document en mode édition SEUL ; le cadre du
panneau (garde/close/resize partagés `fdWirePeekResize`) reste identique.
- **Le bouton « Open » du calendrier My Tasks ne faisait rien** — cause
racine : le handler de clic testait `e.target.dataset.openRow`, mais les
boutons `.mt-cal-open` du calendrier (et du Kanban) ne contiennent que leur
`<svg>` : cliquer l'icône donnait `e.target` = `<svg>` sans dataset. Le
handler résout désormais `t.closest('[data-open-row]')` — un seul chemin
pour tableau, calendrier (5 vues) et Kanban.
- **Menu contextuel global « Open in side peek »** — le clic ne faisait
qu'afficher « Side peek not available in browser » : il route maintenant
vers le peek de la page courante (`_libData.openPeekById` en Library,
`_wsData.openSidePeek` en workspace local), repli nouvel onglet ailleurs.
- **Peek d'une page de base (`content_format='collection'`)** — `?embed=1`
forçait le template éditeur : une base ouverte dans le peek montrait un
éditeur de blocs vide au lieu du tableau. Le template collection est gardé
en mode embed (body.embed-mode retire sidebar + barre, le tableau reste).
### Added
- **Bouton « Open » sur les tuiles du Kanban My Tasks** — le même que les
lignes du tableau (`openButton(t)`, `.mt-hover-only` révélé au survol de la
carte, overlay absolu pour ne pas déformer la tuile) ; ouvre le contenu de
la tâche dans le panneau latéral.
- **Bouton « pleine page » des peeks de bases** — navigue vers
`/pages/{pageId}` (pleine page, sidebar + barre conservées) au lieu de
l'URL `?embed=1` de l'iframe ; `dataset.page` porte l'id résolu.
### Tests
- `test_my_tasks_workspace_coherence.py` : les 4 peek JS (`library`,
`my_tasks`, `database_table`, `local_workspace`) doivent porter
`?embed=1` (gate de cohérence inversé — l'ancien test imposait l'URL nue) ;
nouveaux : résolution `closest('[data-open-row]')`, bouton Open sur tuiles
Kanban, collection embed = tableau. Suite 1264 passed / 0 failed
(-n auto) + ruff 0.
## v7.49.2 (2026-10-05) — Pages de base : titre au-dessus, tableau à gauche ## v7.49.2 (2026-10-05) — Pages de base : titre au-dessus, tableau à gauche
### Fixed ### Fixed
+1 -1
View File
@@ -1 +1 @@
7.49.2 7.50.0
+1 -1
View File
@@ -1,6 +1,6 @@
# WORKLOAD — FlowDeck Notion Clone # WORKLOAD — FlowDeck Notion Clone
> **Début**: 2026-07-08 | **Version**: v7.49.2 (pages de base : titre au-dessus du tableau, tableau calé à gauche au ras du sidebar) | **Statut**: EN COURS 🔄 > **Début**: 2026-07-08 | **Version**: v7.50.0 (side peek : document seul en mode édition, bouton Open calendrier/Kanban My Tasks) | **Statut**: EN COURS 🔄
> **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
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI( app = FastAPI(
title="FlowDeck", title="FlowDeck",
version="7.49.2", version="7.50.0",
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc", redoc_url="/redoc",
lifespan=lifespan, lifespan=lifespan,
+4 -2
View File
@@ -217,8 +217,10 @@ def view_page_root(request: Request, page_id: int):
"{% from '_icons.html' import fd_icon %}{{ fd_icon('paperclip', 14) }}" "{% from '_icons.html' import fd_icon %}{{ fd_icon('paperclip', 14) }}"
).render(), ).render(),
) )
# Select template: collection pages use database table view # Select template: collection pages use database table view (aussi en
if page.get("content_format") == "collection" and not embed: # mode embed : `page_editor_collection.html` + body.embed-mode = le
# tableau SANS sidebar ni barre — le peek d'une base reste le tableau).
if page.get("content_format") == "collection":
template = env.get_template("page_editor_collection.html") template = env.get_template("page_editor_collection.html")
else: else:
template = env.get_template("page_editor_embed.html" if embed else "page_editor.html") template = env.get_template("page_editor_embed.html" if embed else "page_editor.html")
+9 -2
View File
@@ -2093,9 +2093,16 @@
case 'openTab': case 'openTab':
window.open('/' + pageId, '_blank'); window.open('/' + pageId, '_blank');
break; break;
case 'openPeek': case 'openPeek': {
this.toast('Side peek not available in browser', 'info'); // Route vers le panneau de peek DE LA PAGE courante (même
// éditeur allégé `?embed=1` que le bouton « ↢ Open »), jamais une
// navigation. En dehors de ces pages : repli sur un nouvel onglet.
var num = pageId.replace('page/', '');
if (window._libData && window._libData.openPeekById) window._libData.openPeekById(parseInt(num));
else if (window._wsData && window._wsData.openSidePeek) window._wsData.openSidePeek(parseInt(num));
else window.open('/pages/' + num, '_blank');
break; break;
}
} }
this.contextMenu.visible = false; this.contextMenu.visible = false;
}, },
+2 -2
View File
@@ -450,8 +450,8 @@
<!-- Le corps et l'iframe sont HORS du <template x-if> : ils existent donc en <!-- Le corps et l'iframe sont HORS du <template x-if> : ils existent donc en
permanence, ce qui permet de piloter `src` en JS (le build CSP permanence, ce qui permet de piloter `src` en JS (le build CSP
d'Alpine n'evalue pas le binding `:src` — l'iframe restait donc vide). d'Alpine n'evalue pas le binding `:src` — l'iframe restait donc vide).
Charger `/pages/{id}` sans `?embed=1` : c'est exactement l'editeur Charger `/pages/{id}?embed=1` : le peek affiche uniquement le
plein ecran, le peek n'est plus une variante allegee. --> document en mode edition, sans sidebar ni barre de navigation. -->
<div class="peek-body" style="flex:1;display:flex;flex-direction:column;min-height:0;"> <div class="peek-body" style="flex:1;display:flex;flex-direction:column;min-height:0;">
<div class="peek-loading" id="lib-peek-loading">Chargement…</div> <div class="peek-loading" id="lib-peek-loading">Chargement…</div>
<iframe id="lib-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe> <iframe id="lib-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "FlowDeck", "title": "FlowDeck",
"version": "7.49.2" "version": "7.50.0"
}, },
"paths": { "paths": {
"/auth/register": { "/auth/register": {
+8
View File
@@ -207,6 +207,14 @@
} }
.mt-card:active { cursor: grabbing; opacity: .7; } .mt-card:active { cursor: grabbing; opacity: .7; }
.mt-card-title { font-weight: 500; } .mt-card-title { font-weight: 500; }
/* Bouton « Open » en overlay au survol de la tuile (la carte est une
colonne flex : une place en flux déformerait la tuile). */
.mt-card { position: relative; }
.mt-card .mt-open-btn {
position: absolute; top: 6px; right: 6px; margin-left: 0;
}
.mt-card:hover .mt-hover-only,
.mt-card:focus-within .mt-hover-only { opacity: 1; }
.mt-card-due, .mt-card-due,
.mt-card-src { font-size: 11px; color: var(--text-dim, #999); } .mt-card-src { font-size: 11px; color: var(--text-dim, #999); }
.mt-card-src { opacity: .8; } .mt-card-src { opacity: .8; }
+7 -3
View File
@@ -1311,7 +1311,9 @@ const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=
if (!peekEl.classList.contains('open')) return; if (!peekEl.classList.contains('open')) return;
if (peekTitle) peekTitle.textContent = ''; if (peekTitle) peekTitle.textContent = '';
if (peekLoading) peekLoading.style.display = 'none'; if (peekLoading) peekLoading.style.display = 'none';
if (iframe) iframe.src = '/pages/'+pid; // `?embed=1` : uniquement le document edite, pas l'habillage
// (sidebar + barre) — la pleine page reste via le bouton « full ».
if (iframe) { iframe.dataset.page = pid; iframe.src = '/pages/'+pid+'?embed=1'; }
}) })
.catch(function(){ .catch(function(){
// L'échec est rendu DANS le panneau (srcdoc) : sous le build // L'échec est rendu DANS le panneau (srcdoc) : sous le build
@@ -1327,8 +1329,10 @@ const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=
if (pc) pc.addEventListener('click', closeDbPeek); if (pc) pc.addEventListener('click', closeDbPeek);
var pf = document.getElementById('db-side-peek-full'); var pf = document.getElementById('db-side-peek-full');
if (pf) pf.addEventListener('click', function() { if (pf) pf.addEventListener('click', function() {
if (peekFrame && peekFrame.src && peekFrame.src !== 'about:blank') { // `dataset.page` et non `iframe.src` : le src porte `?embed=1`,
window.location.href = peekFrame.src; // la pleine page doit garder sa sidebar et sa barre.
if (peekFrame && peekFrame.dataset.page) {
window.location.href = '/pages/' + peekFrame.dataset.page;
} }
}); });
document.addEventListener('keydown', function(e) { document.addEventListener('keydown', function(e) {
+3 -3
View File
@@ -979,9 +979,9 @@ _renderIcon(item) {
if (loading) loading.style.display = ''; if (loading) loading.style.display = '';
frame.onload = function () { if (loading) loading.style.display = 'none'; }; frame.onload = function () { if (loading) loading.style.display = 'none'; };
frame.src = 'about:blank'; frame.src = 'about:blank';
// `/pages/{id}` sans `?embed=1` : le peek affiche le MÊME éditeur que la // `?embed=1` : le peek affiche SEULEMENT le document en mode édition
// page pleine (sidebar, barre, actions compris), pas une variante. // (ni sidebar ni barre de navigation) — le cadre du panneau suffit.
frame.src = '/pages/' + item.id; frame.src = '/pages/' + item.id + '?embed=1';
}, },
closePeek() { closePeek() {
+5 -5
View File
@@ -395,12 +395,12 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis →
window.showToast('Failed to copy', 'error'); window.showToast('Failed to copy', 'error');
}); });
}; };
// Load the full-screen editor in the iframe (no `?embed=1`: the peek // Load ONLY the editable document in the iframe (`?embed=1`: no
// shows the same editor as the page itself). `about:blank` first, so // sidebar, no navbar). `about:blank` first, so opening another
// opening another document really reloads the frame instead of keeping // document really reloads the frame instead of keeping the previous
// the previous one when the URL happens to be identical. // one when the URL happens to be identical.
var iframe = document.getElementById('ws-preview-iframe'); var iframe = document.getElementById('ws-preview-iframe');
if (iframe) { iframe.src = 'about:blank'; iframe.src = '/pages/' + node.id; } if (iframe) { iframe.src = 'about:blank'; iframe.src = '/pages/' + node.id + '?embed=1'; }
// Show panel // Show panel
panel.style.display = 'flex'; panel.style.display = 'flex';
requestAnimationFrame(function() { requestAnimationFrame(function() {
+12 -6
View File
@@ -152,9 +152,10 @@ function flowDeckMyTasks() {
f.style.display = ''; f.style.display = '';
// `about:blank` d'abord : ouvrir une autre tâche doit REcharger // `about:blank` d'abord : ouvrir une autre tâche doit REcharger
// l'éditeur, sinon l'URL identique laisse le document précédent // l'éditeur, sinon l'URL identique laisse le document précédent
// affiché. `/pages/{id}` sans `?embed=1` = l'éditeur plein écran. // affiché. `?embed=1` = le document en édition seule, sans
// sidebar ni barre de navigation (le cadre du panneau suffit).
f.src = 'about:blank'; f.src = 'about:blank';
f.src = '/pages/' + pid; f.src = '/pages/' + pid + '?embed=1';
}).catch(function (e) { }).catch(function (e) {
if (loading) { if (loading) {
loading.textContent = 'Impossible d’ouvrir le contenu : ' + e.message; loading.textContent = 'Impossible d’ouvrir le contenu : ' + e.message;
@@ -389,6 +390,7 @@ function flowDeckMyTasks() {
h += '<div class="mt-card" draggable="true" data-id="' + t.id + '">' + h += '<div class="mt-card" draggable="true" data-id="' + t.id + '">' +
'<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</span>' + '<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</span>' +
'<span class="mt-card-title">' + esc(t.title) + '</span>' + '<span class="mt-card-title">' + esc(t.title) + '</span>' +
openButton(t) +
(t.due ? '<span class="mt-card-due mt-due-' + esc(t.due_state) + '">📅 ' + (t.due ? '<span class="mt-card-due mt-due-' + esc(t.due_state) + '">📅 ' +
esc(t.due) + '</span>' : '') + esc(t.due) + '</span>' : '') +
'<span class="mt-card-src">' + esc(t.collection_icon) + ' ' + '<span class="mt-card-src">' + esc(t.collection_icon) + ' ' +
@@ -878,17 +880,21 @@ function flowDeckMyTasks() {
var t = e.target; var t = e.target;
if (t.id === 'mt-new-btn') { openNewModal(); return; } if (t.id === 'mt-new-btn') { openNewModal(); return; }
if (t.dataset && t.dataset.openRow) { var opener = t.closest ? t.closest('[data-open-row]') : null;
if (opener) {
// `closest` : dans le calendrier le bouton ne contient que son
// SVG — un clic sur l'icone donnait `e.target` = <svg> sans
// dataset, et le bouton ne faisait rien.
e.preventDefault(); e.preventDefault();
// Le titre vient de l'attribut : il est disponible aussi bien dans // Le titre vient de l'attribut : il est disponible aussi bien dans
// le tableau que dans le calendrier ou la vue Jour. // le tableau que dans le calendrier ou la vue Jour.
var label = t.dataset.openTitle; var label = opener.dataset.openTitle;
if (!label) { if (!label) {
var row = t.closest('.mt-row'); var row = opener.closest('.mt-row');
var cell = row && row.querySelector('.mt-title-text'); var cell = row && row.querySelector('.mt-title-text');
label = cell ? cell.textContent : ''; label = cell ? cell.textContent : '';
} }
openRowContent(Number(t.dataset.openRow), label); openRowContent(Number(opener.dataset.openRow), label);
return; return;
} }
if (t.dataset && t.dataset.fixSource) { if (t.dataset && t.dataset.fixSource) {
+52 -16
View File
@@ -1388,13 +1388,23 @@ def test_library_peek_iframe_is_not_inside_the_x_if_template():
assert at > html.index("</template>"), "l'iframe doit être hors du x-if" assert at > html.index("</template>"), "l'iframe doit être hors du x-if"
def test_library_peek_loads_the_full_screen_editor(): def test_library_peek_loads_the_embed_editor_only():
"""Le peek charge `/pages/{id}` SANS `?embed=1` : exactement l'éditeur """Le peek charge `/pages/{id}?embed=1` : le document en mode édition
de la page entière (le mode embed masquait sidebar, barre et actions).""" SEUL — le cadre du panneau suffit, ni sidebar ni barre de navigation."""
body = _js_func(_library_js(), "openPeek(item) {") body = _js_func(_library_js(), "openPeek(item) {")
assert "frame.src = '/pages/' + item.id" in body assert "frame.src = '/pages/' + item.id + '?embed=1'" in body
assert "embed=1" not in _strip_js_comments(body)
def test_every_side_peek_loads_the_embed_editor_only():
"""Library, My Tasks, bases et workspace local : le peek ne montre QUE le
document éditable (`?embed=1`), jamais l'habillage pleine page."""
from pathlib import Path
for name in ("library.js", "my_tasks.js", "database_table.js",
"local_workspace.js"):
src = _strip_js_comments((Path("static/js") / name).read_text(encoding="utf-8"))
assert "?embed=1" in src, name
def test_library_peek_reloads_when_switching_documents(): def test_library_peek_reloads_when_switching_documents():
@@ -1419,16 +1429,6 @@ def test_close_peek_unloads_the_editor_iframe():
assert "frame.onload = null" in body assert "frame.onload = null" in body
def test_every_side_peek_loads_the_plain_page_url():
"""Library, My Tasks, bases et workspace local : même éditeur partout."""
from pathlib import Path
for name in ("library.js", "my_tasks.js", "database_table.js",
"local_workspace.js"):
src = (Path("static/js") / name).read_text(encoding="utf-8")
assert "?embed=1" not in _strip_js_comments(src), name
def test_calendar_offers_five_views(): def test_calendar_offers_five_views():
"""Mois / Année / Semaine / Jour / Planning.""" """Mois / Année / Semaine / Jour / Planning."""
js = _my_tasks_js() js = _my_tasks_js()
@@ -1451,6 +1451,40 @@ def test_calendar_events_have_a_side_peek_icon():
assert "data-open-title=" in body assert "data-open-title=" in body
def test_open_buttons_resolve_with_closest_not_target():
"""Le gestionnaire de clic lit `data-open-row` via `closest` : les boutons
du calendrier et du Kanban ne contiennent que leur SVG — un clic sur
l'icone (e.target = <svg>, sans dataset) doit ouvrir le peek aussi."""
js = _my_tasks_js()
i = js.index("app.addEventListener('click'")
block = js[i:i + 900]
assert "closest('[data-open-row]')" in block
def test_kanban_cards_have_the_open_button():
"""Les tuiles du Kanban portent le même bouton « Open » que les lignes
du tableau (parité avec la page d'origine : même peek latéral)."""
body = _js_func(_my_tasks_js(), "function viewBoard(")
assert "openButton(t)" in body
# Le bouton est révélé au survol de la tuile (overlay, la carte est une
# colonne flex — pas de place en flux).
css = _my_tasks_css()
assert ".mt-card:hover .mt-hover-only" in css
def test_base_page_collection_renders_the_table_when_embedded():
"""`/pages/{id}?embed=1` sur une page de base garde le TABLEAU (template
collection) — le peek d'une base ouvre le tableau sans sidebar ni barre,
pas l'éditeur de blocs vide."""
from pathlib import Path
src = Path("app/routers/dashboard/pages_html.py").read_text(encoding="utf-8")
i = src.index('content_format") == "collection"')
branch = src[i:i + 220]
assert "not embed" not in branch.split("template")[0]
def test_open_button_carries_its_title_as_attribute(): def test_open_button_carries_its_title_as_attribute():
"""Le titre est lu depuis `dataset.openTitle` : disponible aussi dans le """Le titre est lu depuis `dataset.openTitle` : disponible aussi dans le
calendrier et la vue Jour, où il n'y a pas de ligne `.mt-row`.""" calendrier et la vue Jour, où il n'y a pas de ligne `.mt-row`."""
@@ -1520,4 +1554,6 @@ def test_db_side_peek_reports_resolution_failure_in_the_panel():
assert "srcdoc" in block, "erreur rendue dans le panneau" assert "srcdoc" in block, "erreur rendue dans le panneau"
assert "Impossible d’ouvrir le contenu" in block assert "Impossible d’ouvrir le contenu" in block
assert block.index("srcdoc") < block.index("panel.style.display = 'flex'") # Le panneau (révélé par `classList.add('open')` depuis la réécriture
# vanilla JS de v7.49.0) doit s'afficher APRÈS l'injection du message.
assert block.index("srcdoc") < block.index("classList.add('open')")