fix: peek des bases hydrate FlowDeckDB + Open de ligne, tableau pleine largeur (v7.50.1)
- 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.
This commit is contained in:
@@ -1,5 +1,32 @@
|
|||||||
# 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
|
## v7.50.0 (2026-10-06) — Side peek : le document seul (mode édition), sans sidebar ni barre
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# WORKLOAD — FlowDeck Notion Clone
|
# WORKLOAD — FlowDeck Notion Clone
|
||||||
|
|
||||||
> **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 🔄
|
> **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
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
|||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="FlowDeck",
|
title="FlowDeck",
|
||||||
version="7.50.0",
|
version="7.50.1",
|
||||||
docs_url="/docs",
|
docs_url="/docs",
|
||||||
redoc_url="/redoc",
|
redoc_url="/redoc",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.1.0",
|
"openapi": "3.1.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "FlowDeck",
|
"title": "FlowDeck",
|
||||||
"version": "7.50.0"
|
"version": "7.50.1"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/auth/register": {
|
"/auth/register": {
|
||||||
|
|||||||
+7
-6
@@ -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. */
|
||||||
|
|||||||
+75
-65
@@ -1283,76 +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';
|
|
||||||
// `?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
|
|
||||||
// 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() {
|
|
||||||
// `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) {
|
|
||||||
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')
|
||||||
|
|||||||
@@ -1485,6 +1485,22 @@ def test_base_page_collection_renders_the_table_when_embedded():
|
|||||||
assert "not embed" not in branch.split("template")[0]
|
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`."""
|
||||||
|
|||||||
Reference in New Issue
Block a user