Compare commits

..
2 Commits
Author SHA1 Message Date
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 277 additions and 124 deletions
+72
View File
@@ -1,5 +1,77 @@
# Changelog - FlowDeck # Changelog - FlowDeck
## 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 ## 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.1
+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.1 (peek des bases : hydratation FlowDeckDB + Open de ligne, tableau pleine largeur) | **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.1",
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")
+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 class="db-col-menu-item" id="db-col-insert-right"><span>→┤ Insert right</span></div>
</div> </div>
<!-- Side Peek Panel for Database Pages (vanilla JS, piloté par database_table.js) --> <!-- Le panneau de peek (#db-side-peek) est créé à la demande par
<div id="db-side-peek" class="db-side-peek"> database_table.js (ensureDbPeek) : une seule source de markup pour
<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div> la page de base et les bases embarquées dans un document. -->
<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>
</div> </div>
+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>
+5
View File
@@ -8,4 +8,9 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{% include '_page_editor_scripts.html' %} {% 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 %} {% endblock %}
+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.1"
}, },
"paths": { "paths": {
"/auth/register": { "/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 /* 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 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-cover-area,
.page-editor-wrapper.db-first .page-title-block, .page-editor-wrapper.db-first .page-title-block,
.page-editor-wrapper.db-first .blocks-container { .page-editor-wrapper.db-first .blocks-container {
max-width: 1100px; max-width: 100%;
margin: 0; margin: 0;
padding-left: var(--space-lg); padding-left: var(--space-lg);
padding-right: var(--space-lg);
} }
/* Une base embarquée ne doit jamais déborder de sa colonne de blocs : /* 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 /* Titre des pages de base dédiées (page_editor_collection.html) — même
liseré gauche que .database-table-container. */ liseré gauche que .database-table-container. */
.database-page-title { .database-page-title {
max-width: 1100px; max-width: 100%;
margin: 0; margin: 0;
padding: 28px var(--space-lg) 0; padding: 28px var(--space-lg) 0;
font-size: 30px; font-size: 30px;
@@ -3899,11 +3901,10 @@ button.breadcrumb-current { max-width: 260px; }
═══════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════ */
/* Base = viewport du contenu ; les vues tabulaires larges (bases de /* Base = viewport du contenu ; les vues tabulaires larges (bases de
données) s'alignent à gauche, au ras de la marge, à la même largeur données) prennent la LARGEUR DU DOCUMENT (les colonnes dictent le scroll
que Library (1100px). */ horizontal), alignées à gauche au ras de la marge. */
.database-table-container { .database-table-container {
width: 100%; width: 100%;
max-width: 1100px;
margin: 0; margin: 0;
/* var(--space-lg) = padding de .topbar : le bord gauche du tableau /* 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. */ 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: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; }
+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) ── // ── Side peek : panneau latéral des lignes de base (vanilla JS) ──
// Le panneau vit dans _database_table.html (page de base ET bases // Le panneau vit dans _database_table.html (page de base). Une base
// embarquées inline). Il doit être câblé SUR le document, jamais via // EMBARQUÉE dans un document n'a pas ce markup : on le crée à la demande
// container.__x : le conteneur n'a pas de x-data (les bindings Alpine // (comme mt-peek de My Tasks), sinon l'icône Open de fin de ligne était
// du panneau restaient inertes — clic sur une ligne = rien). // un no-op silencieux. Il doit être câblé SUR le document, jamais via
var peekEl = document.getElementById('db-side-peek'); // container.__x : le conteneur n'a pas de x-data.
// Sans panneau (collections embarquées inline dans l'éditeur), le clic ne var PEEK_HTML =
// doit pas lever de ReferenceError : repli discret, comme l'ancien code. '<div class="peek-resize-handle" title="Glisser pour redimensionner — cliquer pour fermer"></div>' +
if (!peekEl) window.openPageInSidePeek = function() {}; '<div class="db-side-peek-header">' +
if (peekEl) { '<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 peekFrame = document.getElementById('db-side-peek-iframe');
var peekTitle = document.getElementById('db-side-peek-title'); var peekTitle = document.getElementById('db-side-peek-title');
var peekLoading = document.getElementById('db-side-peek-loading'); var peekLoading = document.getElementById('db-side-peek-loading');
function closeDbPeek() { peekEl.classList.add('open');
peekEl.classList.remove('open'); if (peekLoading) peekLoading.style.display = '';
document.getElementById('db-side-peek-iframe').src='about:blank'; if (peekFrame) peekFrame.src = 'about:blank';
if (peekTitle) { peekTitle.textContent = ''; } if (peekTitle) peekTitle.textContent = '';
} resolveRowContentPage(pageId).then(function(pid){
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) {
if (!peekEl.classList.contains('open')) return; if (!peekEl.classList.contains('open')) return;
if (peekEl.contains(e.target)) return; if (peekLoading) peekLoading.style.display = 'none';
if (e.target.closest('#db-table-container, .database-table-container, [id^="inline-db-"]')) return; // `?embed=1` : uniquement le document edite, pas l'habillage
closeDbPeek(); // (sidebar + barre) — la pleine page reste via le bouton « full ».
}); if (peekFrame) { peekFrame.dataset.page = pid; peekFrame.src = '/pages/'+pid+'?embed=1'; }
function wireDbPeekResize() { })
if (window.fdWirePeekResize) { .catch(function(){
window.fdWirePeekResize(peekEl, peekEl.querySelector('.peek-resize-handle'), closeDbPeek); // L'échec est rendu DANS le panneau (srcdoc) : sous le build
} else if (document.readyState === 'loading') { // CSP d'Alpine les toasts ne s'affichent pas partout.
document.addEventListener('DOMContentLoaded', wireDbPeekResize); 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>';
} }
} peekEl.classList.add('open');
wireDbPeekResize(); 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){ function resolveRowContentPage(rowId){
return fetch('/db/pages/'+rowId+'/open/api') return fetch('/db/pages/'+rowId+'/open/api')
+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) {
+68 -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,56 @@ 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_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(): 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 +1570,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')")