feat: sous-pages — parent_id, sidebar hiérarchique, bouton +New sub-page
- DB: parent_id dans table pages (ALTER TABLE migration auto) - Sidebar: pages parent_id IS NULL affichées, sous-pages indentées en dessous - API: create_page accepte parent_id (0 = racine) - JS: newSubPage extrait page ID et passe parent_id - page_editor: liste les sous-pages + bouton '+ New sub-page'
This commit is contained in:
@@ -137,10 +137,16 @@ def init_db():
|
|||||||
title TEXT NOT NULL DEFAULT 'New page',
|
title TEXT NOT NULL DEFAULT 'New page',
|
||||||
content TEXT NOT NULL DEFAULT '',
|
content TEXT NOT NULL DEFAULT '',
|
||||||
parent_section TEXT DEFAULT 'Private',
|
parent_section TEXT DEFAULT 'Private',
|
||||||
|
parent_id INTEGER REFERENCES pages(id),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
""")
|
""")
|
||||||
|
# Migration: add parent_id if missing (v1.0.0+)
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+17
-6
@@ -139,7 +139,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
# Load pages for this project workspace
|
# Load pages for this project workspace
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
pages = conn.execute(
|
pages = conn.execute(
|
||||||
"SELECT id, title, updated_at FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10",
|
"SELECT id, title, parent_id, updated_at FROM pages WHERE workspace=? AND parent_id IS NULL ORDER BY updated_at DESC LIMIT 10",
|
||||||
(ws_key,),
|
(ws_key,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for p in pages:
|
for p in pages:
|
||||||
@@ -147,6 +147,16 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
|||||||
"id": f"page/{p['id']}", "name": p["title"], "icon": "📄",
|
"id": f"page/{p['id']}", "name": p["title"], "icon": "📄",
|
||||||
"url": f"/pages/{p['id']}", "active": False, "indent": 0,
|
"url": f"/pages/{p['id']}", "active": False, "indent": 0,
|
||||||
})
|
})
|
||||||
|
# Load sub-pages
|
||||||
|
subs = conn.execute(
|
||||||
|
"SELECT id, title FROM pages WHERE parent_id=? ORDER BY updated_at DESC",
|
||||||
|
(p["id"],),
|
||||||
|
).fetchall()
|
||||||
|
for sp in subs:
|
||||||
|
recent.append({
|
||||||
|
"id": f"page/{sp['id']}", "name": sp["title"], "icon": "·",
|
||||||
|
"url": f"/pages/{sp['id']}", "active": False, "indent": 1,
|
||||||
|
})
|
||||||
return {"workspace_name": ws_name, "workspace_initial": ws_name[0].upper() if ws_name else "B",
|
return {"workspace_name": ws_name, "workspace_initial": ws_name[0].upper() if ws_name else "B",
|
||||||
"current_page": repo or "Dashboard", "last_edited": "now",
|
"current_page": repo or "Dashboard", "last_edited": "now",
|
||||||
"recent_pages": recent, "private_pages": [r for r in recent if r.get("active") or r["id"].startswith("page/")],
|
"recent_pages": recent, "private_pages": [r for r in recent if r.get("active") or r["id"].startswith("page/")],
|
||||||
@@ -439,18 +449,19 @@ async def extract_ai_keywords(owner: str, repo: str):
|
|||||||
@router.post("/api/pages")
|
@router.post("/api/pages")
|
||||||
async def create_page(request: Request, title: str = Query(default="New page"),
|
async def create_page(request: Request, title: str = Query(default="New page"),
|
||||||
section: str = Query(default="Private"),
|
section: str = Query(default="Private"),
|
||||||
project: str = Query(default="")):
|
project: str = Query(default=""),
|
||||||
"""Create a new Markdown page in the given project workspace."""
|
parent_id: int = Query(default=0)):
|
||||||
|
"""Create a new Markdown page, optionally as a sub-page."""
|
||||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
ws_key = project if project else (user.get("login", "Bruno") if user else "Bruno")
|
ws_key = project if project else (user.get("login", "Bruno") if user else "Bruno")
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"INSERT INTO pages (workspace, title, parent_section) VALUES (?,?,?)",
|
"INSERT INTO pages (workspace, title, parent_section, parent_id) VALUES (?,?,?,?)",
|
||||||
(ws_key, title, section),
|
(ws_key, title, section, parent_id if parent_id > 0 else None),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
page_id = cur.lastrowid
|
page_id = cur.lastrowid
|
||||||
return {"status": "ok", "id": page_id, "title": title, "workspace": ws_key}
|
return {"status": "ok", "id": page_id, "title": title, "workspace": ws_key, "parent_id": parent_id}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/pages/{page_id}")
|
@router.get("/api/pages/{page_id}")
|
||||||
|
|||||||
@@ -71,7 +71,13 @@ async def view_page_root(request: Request, page_id: int):
|
|||||||
parts = ws.split("/") if "/" in ws else ["", ""]
|
parts = ws.split("/") if "/" in ws else ["", ""]
|
||||||
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
|
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
|
||||||
sidebar = board_sidebar(request, owner, repo)
|
sidebar = board_sidebar(request, owner, repo)
|
||||||
ctx = {**sidebar, "page": page}
|
# Load sub-pages
|
||||||
|
with get_conn() as conn:
|
||||||
|
subs = conn.execute(
|
||||||
|
"SELECT id, title FROM pages WHERE parent_id=? ORDER BY updated_at DESC",
|
||||||
|
(page_id,),
|
||||||
|
).fetchall()
|
||||||
|
ctx = {**sidebar, "page": page, "sub_pages": [dict(s) for s in subs]}
|
||||||
template = env.get_template("page_editor.html")
|
template = env.get_template("page_editor.html")
|
||||||
return template.render(**ctx)
|
return template.render(**ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,8 @@
|
|||||||
},
|
},
|
||||||
newSubPage(parentId) {
|
newSubPage(parentId) {
|
||||||
const project = this.workspaceKey || '';
|
const project = this.workspaceKey || '';
|
||||||
fetch(`/board/api/pages?title=New%20page§ion=Private&project=${encodeURIComponent(project)}`, {
|
const pid = parentId.replace('page/', '');
|
||||||
|
fetch(`/board/api/pages?title=New%20sub-page§ion=Private&project=${encodeURIComponent(project)}&parent_id=${pid}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -69,11 +69,36 @@
|
|||||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('# Meeting Notes\n\n**Date:** \n**Attendees:** \n\n## Agenda\n\n- \n\n## Notes\n\n- \n\n## Action Items\n\n- [ ] \n', '')">🎤 Meeting Notes</button>
|
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('# Meeting Notes\n\n**Date:** \n**Attendees:** \n\n## Agenda\n\n- \n\n## Notes\n\n- \n\n## Action Items\n\n- [ ] \n', '')">🎤 Meeting Notes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-pages -->
|
||||||
|
<div style="margin-top:32px; border-top:1px solid var(--border); padding-top:16px;">
|
||||||
|
<h3 style="font-size:14px; font-weight:600; margin-bottom:12px;">Sub-pages</h3>
|
||||||
|
{% for sub in sub_pages %}
|
||||||
|
<a href="/pages/{{ sub.id }}" style="display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:6px; text-decoration:none; color:var(--text-primary); font-size:13px;"
|
||||||
|
onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
||||||
|
<span>📄</span> {{ sub.title }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
<button class="kanban-new-card" style="margin-top:8px;"
|
||||||
|
@click="createSubPage()">+ New sub-page</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
function createSubPage() {
|
||||||
|
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||||
|
const token = csrf ? csrf[1] : '';
|
||||||
|
const project = document.querySelector('[x-data]')?.__x?.$data?.workspaceKey || '';
|
||||||
|
fetch(`/board/api/pages?title=New%20sub-page§ion=Private&project=${encodeURIComponent(project)}&parent_id={{ page.id }}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-CSRF-Token': token }
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||||
|
.catch(err => { alert('Failed: ' + err.message); });
|
||||||
|
}
|
||||||
// Simple Markdown-to-HTML renderer
|
// Simple Markdown-to-HTML renderer
|
||||||
function renderMarkdown(md) {
|
function renderMarkdown(md) {
|
||||||
let html = md
|
let html = md
|
||||||
|
|||||||
Reference in New Issue
Block a user