From 3706689eca28425f0ce50f3a84311c940b7bddd6 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Tue, 6 Oct 2026 12:48:48 -0400 Subject: [PATCH] fix: side peek = document seul en edition + bouton Open calendrier/Kanban (v7.50.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 -> clic sur l'icone = e.target = , 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. --- CHANGELOG.md | 45 ++++++++++++++ VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- app/routers/dashboard/pages_html.py | 6 +- app/templates/base.html | 11 +++- app/templates/library.html | 4 +- docs/openapi-v2.json | 2 +- static/css/my_tasks.css | 8 +++ static/js/database_table.js | 10 +++- static/js/library.js | 6 +- static/js/local_workspace.js | 10 ++-- static/js/my_tasks.js | 18 ++++-- tests/test_my_tasks_workspace_coherence.py | 68 +++++++++++++++++----- 14 files changed, 151 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be27cbd..3e71779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # 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 + `` : cliquer l'icône donnait `e.target` = `` 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 ### Fixed diff --git a/VERSION b/VERSION index edbd674..cf89fc8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.49.2 +7.50.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index e9b865c..c48de54 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # 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` ## Avancement Global diff --git a/app/main.py b/app/main.py index 3324d4e..56f27a4 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.49.2", + version="7.50.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/routers/dashboard/pages_html.py b/app/routers/dashboard/pages_html.py index 082f992..f0b9526 100644 --- a/app/routers/dashboard/pages_html.py +++ b/app/routers/dashboard/pages_html.py @@ -217,8 +217,10 @@ def view_page_root(request: Request, page_id: int): "{% from '_icons.html' import fd_icon %}{{ fd_icon('paperclip', 14) }}" ).render(), ) - # Select template: collection pages use database table view - if page.get("content_format") == "collection" and not embed: + # Select template: collection pages use database table view (aussi en + # 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") else: template = env.get_template("page_editor_embed.html" if embed else "page_editor.html") diff --git a/app/templates/base.html b/app/templates/base.html index b9d7833..da33e31 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -2093,9 +2093,16 @@ case 'openTab': window.open('/' + pageId, '_blank'); break; - case 'openPeek': - this.toast('Side peek not available in browser', 'info'); + case 'openPeek': { + // 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; + } } this.contextMenu.visible = false; }, diff --git a/app/templates/library.html b/app/templates/library.html index 7aa017b..7c98790 100644 --- a/app/templates/library.html +++ b/app/templates/library.html @@ -450,8 +450,8 @@ + Charger `/pages/{id}?embed=1` : le peek affiche uniquement le + document en mode edition, sans sidebar ni barre de navigation. -->
Chargement…
diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 1030271..751d44a 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.49.2" + "version": "7.50.0" }, "paths": { "/auth/register": { diff --git a/static/css/my_tasks.css b/static/css/my_tasks.css index 8513054..b0bb04e 100644 --- a/static/css/my_tasks.css +++ b/static/css/my_tasks.css @@ -207,6 +207,14 @@ } .mt-card:active { cursor: grabbing; opacity: .7; } .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-src { font-size: 11px; color: var(--text-dim, #999); } .mt-card-src { opacity: .8; } diff --git a/static/js/database_table.js b/static/js/database_table.js index 4fef042..3c9783d 100644 --- a/static/js/database_table.js +++ b/static/js/database_table.js @@ -1311,7 +1311,9 @@ const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v= if (!peekEl.classList.contains('open')) return; if (peekTitle) peekTitle.textContent = ''; 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(){ // 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); var pf = document.getElementById('db-side-peek-full'); if (pf) pf.addEventListener('click', function() { - if (peekFrame && peekFrame.src && peekFrame.src !== 'about:blank') { - window.location.href = peekFrame.src; + // `dataset.page` et non `iframe.src` : le src porte `?embed=1`, + // 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) { diff --git a/static/js/library.js b/static/js/library.js index 1defa2f..38df98c 100644 --- a/static/js/library.js +++ b/static/js/library.js @@ -979,9 +979,9 @@ _renderIcon(item) { if (loading) loading.style.display = ''; frame.onload = function () { if (loading) loading.style.display = 'none'; }; frame.src = 'about:blank'; - // `/pages/{id}` sans `?embed=1` : le peek affiche le MÊME éditeur que la - // page pleine (sidebar, barre, actions compris), pas une variante. - frame.src = '/pages/' + item.id; + // `?embed=1` : le peek affiche SEULEMENT le document en mode édition + // (ni sidebar ni barre de navigation) — le cadre du panneau suffit. + frame.src = '/pages/' + item.id + '?embed=1'; }, closePeek() { diff --git a/static/js/local_workspace.js b/static/js/local_workspace.js index 952f907..cbe8f07 100644 --- a/static/js/local_workspace.js +++ b/static/js/local_workspace.js @@ -395,12 +395,12 @@ const _wsInitData = (function() { // lexical : NON propriété globalThis → window.showToast('Failed to copy', 'error'); }); }; - // Load the full-screen editor in the iframe (no `?embed=1`: the peek - // shows the same editor as the page itself). `about:blank` first, so - // opening another document really reloads the frame instead of keeping - // the previous one when the URL happens to be identical. + // Load ONLY the editable document in the iframe (`?embed=1`: no + // sidebar, no navbar). `about:blank` first, so opening another + // document really reloads the frame instead of keeping the previous + // one when the URL happens to be identical. 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 panel.style.display = 'flex'; requestAnimationFrame(function() { diff --git a/static/js/my_tasks.js b/static/js/my_tasks.js index 11f8c00..c7e497b 100644 --- a/static/js/my_tasks.js +++ b/static/js/my_tasks.js @@ -152,9 +152,10 @@ function flowDeckMyTasks() { f.style.display = ''; // `about:blank` d'abord : ouvrir une autre tâche doit REcharger // 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 = '/pages/' + pid; + f.src = '/pages/' + pid + '?embed=1'; }).catch(function (e) { if (loading) { loading.textContent = 'Impossible d’ouvrir le contenu : ' + e.message; @@ -389,6 +390,7 @@ function flowDeckMyTasks() { h += '
' + '' + esc(rowIcon(t.icon)) + '' + '' + esc(t.title) + '' + + openButton(t) + (t.due ? '📅 ' + esc(t.due) + '' : '') + '' + esc(t.collection_icon) + ' ' + @@ -878,17 +880,21 @@ function flowDeckMyTasks() { var t = e.target; 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` = sans + // dataset, et le bouton ne faisait rien. e.preventDefault(); // Le titre vient de l'attribut : il est disponible aussi bien dans // le tableau que dans le calendrier ou la vue Jour. - var label = t.dataset.openTitle; + var label = opener.dataset.openTitle; if (!label) { - var row = t.closest('.mt-row'); + var row = opener.closest('.mt-row'); var cell = row && row.querySelector('.mt-title-text'); label = cell ? cell.textContent : ''; } - openRowContent(Number(t.dataset.openRow), label); + openRowContent(Number(opener.dataset.openRow), label); return; } if (t.dataset && t.dataset.fixSource) { diff --git a/tests/test_my_tasks_workspace_coherence.py b/tests/test_my_tasks_workspace_coherence.py index 1fd14bd..62ee676 100644 --- a/tests/test_my_tasks_workspace_coherence.py +++ b/tests/test_my_tasks_workspace_coherence.py @@ -1388,13 +1388,23 @@ def test_library_peek_iframe_is_not_inside_the_x_if_template(): assert at > html.index(""), "l'iframe doit être hors du x-if" -def test_library_peek_loads_the_full_screen_editor(): - """Le peek charge `/pages/{id}` SANS `?embed=1` : exactement l'éditeur - de la page entière (le mode embed masquait sidebar, barre et actions).""" +def test_library_peek_loads_the_embed_editor_only(): + """Le peek charge `/pages/{id}?embed=1` : le document en mode édition + SEUL — le cadre du panneau suffit, ni sidebar ni barre de navigation.""" body = _js_func(_library_js(), "openPeek(item) {") - assert "frame.src = '/pages/' + item.id" in body - assert "embed=1" not in _strip_js_comments(body) + assert "frame.src = '/pages/' + item.id + '?embed=1'" in 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(): @@ -1419,16 +1429,6 @@ def test_close_peek_unloads_the_editor_iframe(): 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(): """Mois / Année / Semaine / Jour / Planning.""" js = _my_tasks_js() @@ -1451,6 +1451,40 @@ def test_calendar_events_have_a_side_peek_icon(): 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 = , 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(): """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`.""" @@ -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 "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')")