Compare commits

...
3 Commits
Author SHA1 Message Date
bruno 81746d9440 fix: peek de Library nu comme local-workspace - iframe pleine (v7.50.2)
FlowDeck CI / test (push) Failing after 13m34s
FlowDeck CI / lint (push) Successful in 2m32s
FlowDeck CI / docker (push) Skipped
.peek-body gardait padding:16px 20px + overflow-y:auto (heritage de l'ancien
peek a rendu HTML) : l'iframe etait amochee (519x816 mesures contre 559x848
en local-workspace) avec bande grise autour du document + double scrollbar.
Corps nu (padding:0, overflow:hidden) : l'editeur remplit le panneau,
exactement comme le peek de /local-workspace.

Mesure Playwright (cookie de session forge, les 14 boutons Open de Library) :
embed OK partout, bases hydratees, aucun jeton « Loading database... ».
Test: test_library_peek_body_has_no_inset_padding. Suite 1266/0 (-n auto)
+ ruff 0. Bump VERSION/main.py 7.50.2, CHANGELOG, WORKLOAD, OpenAPI.
2026-10-06 15:53:04 -04:00
bruno f53205bc45 fix: peek des bases hydrate FlowDeckDB + Open de ligne, tableau pleine largeur (v7.50.1)
FlowDeck CI / test (push) Failing after 14m37s
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / docker (push) Skipped
- page_editor_embed.html incluait seulement _page_editor_scripts.html :
  window.FlowDeckDB etait undefined dans l'iframe -> une base ouverte en side
  peek restait a « Loading database... ». Include _database_table_scripts.html
  ajoute (meme traitement que page_editor.html).
- Le panneau #db-side-peek n'existait que dans _database_table.html : hors page
  dediee, openPageInSidePeek etait un no-op. Desormais cree a la demande par
  ensureDbPeek (database_table.js) ; markup unique retire du partial.
- .database-table-container : plafond 1100px retire (regle heritee de Library)
  + max-width 100% pour .db-first et database-page-title : le tableau prend la
  largeur du document, plus de barre de scroll parasite sous le tableau.
- Tests: test_embed_editor_can_render_inline_databases. Suite 1265/0 (-n auto)
  + ruff 0. Bump VERSION/main.py 7.50.1, CHANGELOG, WORKLOAD, OpenAPI.
2026-10-06 15:15:01 -04:00
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
17 changed files with 307 additions and 125 deletions
+89
View File
@@ -1,5 +1,94 @@
# Changelog - FlowDeck
## v7.50.2 (2026-10-06) — Peek de Library : nu comme local-workspace (plus de bande ni double scrollbar)
### Fixed
- **Le document ouvert par le bouton « Open » de Library n'occupait pas le
panneau** — `.peek-body` gardait un `padding:16px 20px` + `overflow-y:auto`
(héritage de l'ancien peek à rendu HTML) : l'iframe était amochée
(519×816 mesurés contre 559×848 en local-workspace) avec une bande grise
autour du document et une double scrollbar. Corps nu (`padding:0`,
`overflow:hidden`) : l'éditeur remplit le panneau, exactement comme le peek
« parfait » de /local-workspace (mesuré Playwright, cookie de session forgé).
### Tests
- `test_library_peek_body_has_no_inset_padding`. Suite 1266 passed / 0 failed
(-n auto) + ruff 0.
## v7.50.1 (2026-10-06) — Peek des bases : le contenu se rend enfin + tableau pleine largeur
### Fixed
- **Une base ouverte en side peek restait à « Loading database… »** —
`page_editor_embed.html` (le peek de `/pages/{id}?embed=1`) n'incluait pas
`database_table.js` : `window.FlowDeckDB` était undefined dans l'iframe et
le bloc `embed/collection` ne s'hydrat jamais. Inclus désormais (même
traitement que `page_editor.html`).
- **L'icône « Open » de fin de ligne d'un tableau de base ne faisait rien
hors page dédiée** — le panneau `#db-side-peek` n'existait que dans le
markup de `_database_table.html` ; sans lui, `openPageInSidePeek` était un
no-op. Le panneau est créé à la demande par `database_table.js`
(`ensureDbPeek`, comme `mt-peek` de My Tasks) : markup unique, le partial
ne contient plus le panneau statique.
- **Barre de scroll sous le tableau des bases** — `.database-table-container`
plafonnait à 1100 px (règle héritée de Library) : le tableau ne prenait
pas la largeur du document. Plafond retiré ; `max-width: 100%` aussi pour
les conteneurs `.db-first` (base embarquée en premier bloc) et le titre
des pages de base — les colonnes dictent seules le scroll horizontal.
### Tests
- `test_embed_editor_can_render_inline_databases` (include peek + panneau
créé à la demande, source unique de markup). Suite 1265 passed / 0 failed
(-n auto) + ruff 0.
## 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
+1 -1
View File
@@ -1 +1 @@
7.49.2
7.50.2
+1 -1
View File
@@ -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.2 (peek de Library nu — iframe pleine, comme local-workspace) | **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.2",
version="7.50.2",
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")
+3 -17
View File
@@ -125,21 +125,7 @@
<div class="db-col-menu-item" id="db-col-insert-right"><span>→┤ Insert right</span></div>
</div>
<!-- Side Peek Panel for Database Pages (vanilla JS, piloté par database_table.js) -->
<div id="db-side-peek" class="db-side-peek">
<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div>
<div class="db-side-peek-header">
<button id="db-side-peek-close" title="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div class="db-side-peek-title" id="db-side-peek-title"></div>
<button id="db-side-peek-full" title="Open full page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
</button>
</div>
<div class="db-side-peek-body">
<div class="db-side-peek-loading" id="db-side-peek-loading">Chargement…</div>
<iframe id="db-side-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>
</div>
</div>
<!-- Le panneau de peek (#db-side-peek) est créé à la demande par
database_table.js (ensureDbPeek) : une seule source de markup pour
la page de base et les bases embarquées dans un document. -->
</div>
+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;
},
+3 -3
View File
@@ -150,7 +150,7 @@
.peek-header button{background:transparent;border:none;color:var(--text-secondary);cursor:pointer;padding:6px;border-radius:4px;font-size:13px;display:flex;align-items:center;}
.peek-header button:hover{background:var(--bg-hover);color:var(--text-primary);}
.peek-header button svg{width:15px;height:15px;}
.peek-body{flex:1;overflow-y:auto;padding:16px 20px;}
.peek-body{flex:1;overflow:hidden;padding:0;}
.peek-body .peek-empty{text-align:center;padding:60px 20px;color:var(--text-dim);}
.peek-loading{text-align:center;padding:40px;color:var(--text-dim);}
@@ -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
View File
@@ -8,4 +8,9 @@
{% endblock %}
{% block scripts %}
{% include '_page_editor_scripts.html' %}
{# database_table.js = FlowDeckDB + le panneau de peek des bases : sans cet
include, une base embarquée dans un document restait bloquée à
« Loading database… » dans le peek et l'icône Open de fin de ligne ne
faisait rien. #}
{% include '_database_table_scripts.html' %}
{% endblock %}
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
"version": "7.49.2"
"version": "7.50.2"
},
"paths": {
"/auth/register": {
+7 -6
View File
@@ -2246,13 +2246,15 @@ button.breadcrumb-current { max-width: 260px; }
/* 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. */
dans la colonne 900px — le tableau démarre au ras du sidebar et prend la
LARGEUR DU DOCUMENT (les colonnes dictent le scroll horizontal). */
.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;
max-width: 100%;
margin: 0;
padding-left: var(--space-lg);
padding-right: var(--space-lg);
}
/* Une base embarquée ne doit jamais déborder de sa colonne de blocs :
@@ -2262,7 +2264,7 @@ button.breadcrumb-current { max-width: 260px; }
/* 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;
max-width: 100%;
margin: 0;
padding: 28px var(--space-lg) 0;
font-size: 30px;
@@ -3899,11 +3901,10 @@ button.breadcrumb-current { max-width: 260px; }
═══════════════════════════════════════════════════════════ */
/* Base = viewport du contenu ; les vues tabulaires larges (bases de
données) s'alignent à gauche, au ras de la marge, à la même largeur
que Library (1100px). */
données) prennent la LARGEUR DU DOCUMENT (les colonnes dictent le scroll
horizontal), alignées à gauche au ras de la marge. */
.database-table-container {
width: 100%;
max-width: 1100px;
margin: 0;
/* var(--space-lg) = padding de .topbar : le bord gauche du tableau
s'aligne sur le titre du fil d'ariane, au ras de la marge. */
+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; }
+75 -61
View File
@@ -1283,72 +1283,86 @@ const DB_CONFIG=(()=>{try{const el=document.getElementById('db-config');const v=
}
// ── Side peek : panneau latéral des lignes de base (vanilla JS) ──
// Le panneau vit dans _database_table.html (page de base ET bases
// embarquées inline). Il doit être câblé SUR le document, jamais via
// container.__x : le conteneur n'a pas de x-data (les bindings Alpine
// du panneau restaient inertes — clic sur une ligne = rien).
var peekEl = document.getElementById('db-side-peek');
// Sans panneau (collections embarquées inline dans l'éditeur), le clic ne
// doit pas lever de ReferenceError : repli discret, comme l'ancien code.
if (!peekEl) window.openPageInSidePeek = function() {};
if (peekEl) {
// Le panneau vit dans _database_table.html (page de base). Une base
// EMBARQUÉE dans un document n'a pas ce markup : on le crée à la demande
// (comme mt-peek de My Tasks), sinon l'icône Open de fin de ligne était
// un no-op silencieux. Il doit être câblé SUR le document, jamais via
// container.__x : le conteneur n'a pas de x-data.
var PEEK_HTML =
'<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div>' +
'<div class="db-side-peek-header">' +
'<button id="db-side-peek-close" title="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>' +
'<div class="db-side-peek-title" id="db-side-peek-title"></div>' +
'<button id="db-side-peek-full" title="Open full page"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>' +
'</div>' +
'<div class="db-side-peek-body">' +
'<div class="db-side-peek-loading" id="db-side-peek-loading">Chargement…</div>' +
'<iframe id="db-side-peek-iframe" src="about:blank" title="Document" style="width:100%;height:100%;border:none;flex:1;min-height:0;"></iframe>' +
'</div>';
function ensureDbPeek() {
var p = document.getElementById('db-side-peek');
if (p) return p;
p = document.createElement('div');
p.id = 'db-side-peek';
p.className = 'db-side-peek';
p.innerHTML = PEEK_HTML;
document.body.appendChild(p);
document.getElementById('db-side-peek-close').addEventListener('click', closeDbPeek);
document.getElementById('db-side-peek-full').addEventListener('click', function () {
var f = document.getElementById('db-side-peek-iframe');
// `dataset.page` et non `iframe.src` : le src porte `?embed=1`,
// la pleine page doit garder sa sidebar et sa barre.
if (f && f.dataset.page) window.location.href = '/pages/' + f.dataset.page;
});
if (window.fdWirePeekResize) window.fdWirePeekResize(p, p.querySelector('.peek-resize-handle'), closeDbPeek);
return p;
}
function closeDbPeek() {
var p = document.getElementById('db-side-peek');
if (!p) return;
p.classList.remove('open');
document.getElementById('db-side-peek-iframe').src='about:blank';
var t = document.getElementById('db-side-peek-title');
if (t) t.textContent = '';
}
window.openPageInSidePeek = function(pageId) {
if (!pageId) return;
var peekEl = ensureDbPeek();
var peekFrame = document.getElementById('db-side-peek-iframe');
var peekTitle = document.getElementById('db-side-peek-title');
var peekLoading = document.getElementById('db-side-peek-loading');
function closeDbPeek() {
peekEl.classList.remove('open');
document.getElementById('db-side-peek-iframe').src='about:blank';
if (peekTitle) { peekTitle.textContent = ''; }
}
window.openPageInSidePeek = function(pageId) {
if (!pageId) return;
peekEl.classList.add('open');
if (peekLoading) peekLoading.style.display = '';
var iframe = document.getElementById('db-side-peek-iframe');
if (iframe) iframe.src='about:blank';
if (peekTitle) peekTitle.textContent = '';
resolveRowContentPage(pageId).then(function(pid){
if (!peekEl.classList.contains('open')) return;
if (peekTitle) peekTitle.textContent = '';
if (peekLoading) peekLoading.style.display = 'none';
if (iframe) iframe.src = '/pages/'+pid;
})
.catch(function(){
// L'échec est rendu DANS le panneau (srcdoc) : sous le build
// CSP d'Alpine les toasts ne s'affichent pas partout.
if (iframe) {
iframe.srcdoc = '<p style="font-family:sans-serif;color:#888;padding:40px;text-align:center">Impossible d’ouvrir le contenu de cette ligne</p>';
}
peekEl.classList.add('open');
toast('Impossible d’ouvrir le contenu de cette ligne', 'error');
});
};
var pc = document.getElementById('db-side-peek-close');
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;
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && peekEl.classList.contains('open')) closeDbPeek();
});
document.addEventListener('pointerdown', function(e) {
peekEl.classList.add('open');
if (peekLoading) peekLoading.style.display = '';
if (peekFrame) peekFrame.src = 'about:blank';
if (peekTitle) peekTitle.textContent = '';
resolveRowContentPage(pageId).then(function(pid){
if (!peekEl.classList.contains('open')) return;
if (peekEl.contains(e.target)) return;
if (e.target.closest('#db-table-container, .database-table-container, [id^="inline-db-"]')) return;
closeDbPeek();
});
function wireDbPeekResize() {
if (window.fdWirePeekResize) {
window.fdWirePeekResize(peekEl, peekEl.querySelector('.peek-resize-handle'), closeDbPeek);
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wireDbPeekResize);
if (peekLoading) peekLoading.style.display = 'none';
// `?embed=1` : uniquement le document edite, pas l'habillage
// (sidebar + barre) — la pleine page reste via le bouton « full ».
if (peekFrame) { peekFrame.dataset.page = pid; peekFrame.src = '/pages/'+pid+'?embed=1'; }
})
.catch(function(){
// L'échec est rendu DANS le panneau (srcdoc) : sous le build
// CSP d'Alpine les toasts ne s'affichent pas partout.
if (peekFrame) {
peekFrame.srcdoc = '<p style="font-family:sans-serif;color:#888;padding:40px;text-align:center">Impossible d’ouvrir le contenu de cette ligne</p>';
}
}
wireDbPeekResize();
}
peekEl.classList.add('open');
toast('Impossible d’ouvrir le contenu de cette ligne', 'error');
});
};
document.addEventListener('keydown', function(e) {
var p = document.getElementById('db-side-peek');
if (e.key === 'Escape' && p && p.classList.contains('open')) closeDbPeek();
});
document.addEventListener('pointerdown', function(e) {
var p = document.getElementById('db-side-peek');
if (!p || !p.classList.contains('open')) return;
if (p.contains(e.target)) return;
if (e.target.closest('#db-table-container, .database-table-container, [id^="inline-db-"]')) return;
closeDbPeek();
});
function resolveRowContentPage(rowId){
return fetch('/db/pages/'+rowId+'/open/api')
+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) {
+80 -16
View File
@@ -1388,13 +1388,35 @@ 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_library_peek_body_has_no_inset_padding():
"""Le corps du peek de Library est nu (padding 0, overflow hidden) :
l'iframe doit remplir le panneau comme dans local-workspace, sans double
scrollbar ni bande grise autour du document."""
from pathlib import Path
html = Path("app/templates/library.html").read_text(encoding="utf-8")
rule = html.split(".peek-body{")[1].split("}")[0]
assert "padding:0" in rule
assert "overflow-y:auto" not in rule
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 +1441,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 +1463,56 @@ 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_embed_editor_can_render_inline_databases():
"""Le peek (`page_editor_embed.html`) doit charger database_table.js :
sans lui, une base embarquée restait bloquée à « Loading database… » et
l'icône Open de fin de ligne ne faisait rien. Le panneau #db-side-peek
est créé à la demande par database_table.js (markup unique)."""
from pathlib import Path
html = Path("app/templates/page_editor_embed.html").read_text(encoding="utf-8")
assert "_database_table_scripts.html" in html
js = Path("static/js/database_table.js").read_text(encoding="utf-8")
assert "ensureDbPeek" in js
# Pas de second markup statique du panneau (source unique).
tbl = Path("app/templates/_database_table.html").read_text(encoding="utf-8")
assert 'id="db-side-peek"' not in tbl
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 +1582,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')")