Compare commits

...
2 Commits
Author SHA1 Message Date
bruno 3706689eca 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.
2026-10-06 12:48:48 -04:00
bruno 7dbaefb381 fix: page de base - titre au-dessus du tableau, tableau au ras du sidebar (v7.49.2)
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / test (push) Failing after 13m46s
FlowDeck CI / docker (push) Skipped
- .db-first (page_editor_scripts._applyLayout) quand le premier bloc est une
  base embarquee : .blocks-container/.page-title-block passent a margin:0 +
  padding-left:var(--space-lg) au lieu de max-width:900px centre -> le tableau
  demarre a x=256 (marge topbar) au lieu de x=694 sur 1920px (mesure Playwright).
- .block-embed-collection { max-width:100%; overflow:hidden } : l'embed ne
  deborde plus de sa colonne (les popovers fixes restent hors du clipping).
- page_editor_collection.html : h1.database-page-title au-dessus du conteneur
  (les pages collection dediees n'avaient pas de titre).
- Bump VERSION/main.py 7.49.2, CHANGELOG, WORKLOAD, OpenAPI regenere.
- Instance locale flowdeck-final rebuildee flowdeck:v7.49.2 :8081, health OK.
2026-10-06 09:49:16 -04:00
17 changed files with 200 additions and 43 deletions
+63
View File
@@ -1,5 +1,68 @@
# 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
### Fixed
- **Le tableau d'une page de base commençait au 1/3 de la page** — une base
créée depuis un document est un bloc `embed collection` rendu dans la
colonne éditeur `.blocks-container` (`max-width: 900px; margin: 0 auto`) :
centrée sur un grand écran, donc décalée loin du sidebar (mesuré x=694 sur
1920 px). Nouvelle classe `.db-first` sur le wrapper éditeur quand le
premier bloc est une base : titre et blocs passent à `margin: 0` +
`padding-left: var(--space-lg)` (liseré de la topbar) — tableau au ras du
sidebar (x=256 mesuré), titre au-dessus, plus de débordement de l'embed
(`max-width: 100%`, le scroll horizontal reste géré par `.db-table-wrapper`).
- **Les pages de base dédiées (`content_format='collection'`) n'avaient pas
de titre** — `page_editor_collection.html` rend le tableau sans en-tête de
page. Un `<h1 class="database-page-title">` est ajouté au-dessus du
conteneur, avec le même liseré gauche que le tableau.
## v7.49.1 (2026-10-05) — Tableau des bases aligné sur le titre
### Fixed
+1 -1
View File
@@ -1 +1 @@
7.49.1
7.50.0
+1 -1
View File
@@ -1,6 +1,6 @@
# WORKLOAD — FlowDeck Notion Clone
> **Début**: 2026-07-08 | **Version**: v7.49.1 (tableau des bases aligné sur le titre, marge gauche = topbar) | **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
+1 -1
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="7.49.1",
version="7.50.0",
docs_url="/docs",
redoc_url="/redoc",
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) }}"
).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")
+9 -2
View File
@@ -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;
},
+2 -2
View File
@@ -450,8 +450,8 @@
<!-- 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
d'Alpine n'evalue pas le binding `:src` — l'iframe restait donc vide).
Charger `/pages/{id}` sans `?embed=1` : c'est exactement l'editeur
plein ecran, le peek n'est plus une variante allegee. -->
Charger `/pages/{id}?embed=1` : le peek affiche uniquement le
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-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>
@@ -5,6 +5,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
{% set right_actions %}<span class="topbar-edited" style="cursor:pointer;" @click="edCall('toggleActivityOpen')">Edited <span x-text="edTimeAgo()"></span> ▾</span><button class="topbar-btn share-btn" @click="edCall('toggleShareOpen')"><span x-show="!edShared()">{{ fd_icon("lock",14) }} Share ▾</span><span x-show="edShared()" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="edCall('copyPageLink')" title="Copy link">{{ fd_icon("link",14) }}</button><button class="topbar-btn star-btn" @click="edCall('toggleFavorite')" x-init="bindStar($el)"></button><button class="topbar-btn relative" @click="edCall('toggleMoreOpen')">⋯</button>{% endset %}
{% include '_header.html' %}
{% endblock %} {% block content %}
<h1 class="database-page-title">{{ page.title }}</h1>
{% include "_database_table.html" %}
{% endblock %} {% block scripts %}
<script data-cfasync="false" nonce="{{ csp_nonce() }}">
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
"version": "7.49.1"
"version": "7.50.0"
},
"paths": {
"/auth/register": {
+26
View File
@@ -2244,6 +2244,32 @@ button.breadcrumb-current { max-width: 260px; }
min-height: 300px;
}
/* Page de base (premier bloc = embed collection) : le titre et le tableau
s'alignent sur la marge gauche de la topbar (16px) au lieu d'être centrés
dans la colonne 900px — le tableau démarre au ras du sidebar. */
.page-editor-wrapper.db-first .page-cover-area,
.page-editor-wrapper.db-first .page-title-block,
.page-editor-wrapper.db-first .blocks-container {
max-width: 1100px;
margin: 0;
padding-left: var(--space-lg);
}
/* Une base embarquée ne doit jamais déborder de sa colonne de blocs :
.db-table-wrapper interne gère déjà le scroll horizontal. */
.block-embed-collection { max-width: 100%; overflow: hidden; }
/* Titre des pages de base dédiées (page_editor_collection.html) — même
liseré gauche que .database-table-container. */
.database-page-title {
max-width: 1100px;
margin: 0;
padding: 28px var(--space-lg) 0;
font-size: 30px;
font-weight: 700;
color: var(--text-primary);
}
.blocks-empty {
padding: 40px 0;
text-align: center;
+8
View File
@@ -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; }
+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 (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) {
+3 -3
View File
@@ -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() {
+5 -5
View File
@@ -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() {
+12 -6
View File
@@ -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 += '<div class="mt-card" draggable="true" data-id="' + t.id + '">' +
'<span class="mt-icon">' + esc(rowIcon(t.icon)) + '</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) + '">📅 ' +
esc(t.due) + '</span>' : '') +
'<span class="mt-card-src">' + 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` = <svg> 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) {
+4
View File
@@ -1124,6 +1124,10 @@ const PD=(()=>{try{const el=document.getElementById('page-data');return el?JSON.
if(!wrap)return;
wrap.classList.toggle('full-width',!!this.fullWidth);
wrap.classList.toggle('small-text',!!this.fontSmall);
// Page dont le premier bloc est une base : layout « page de base »
// Notion — titre et tableau calés à gauche, pas centrés.
const b0=this.blocks&&this.blocks[0];
wrap.classList.toggle('db-first',!!(b0&&b0.type==='embed'&&b0.embed_type==='collection'));
},
closeInviteSuggest(){ this.inviteSuggestOpen = false; this.inviteUsers = []; },
setSharePerm(a, perm){ this.updateShare(a.id, perm); a.permOpen = false; },
+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"
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 = <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():
"""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')")