feat: workspace contextuel — pages liées au projet, sidebar vues cliquables
- pages.workspace = owner/repo (ex: bruno/flowdeck) au lieu du nom user
- Sidebar: Kanban board + 4 sous-vues avec URLs distinctes (?view=kanban|table|...)
- Cliquer sur 'Table view' dans sidebar → change la vue à droite
- Dashboard (/): sidebar montre uniquement les projets Gitea
- Board (/board/{o}/{r}): sidebar montre les pages de ce projet
- Changement de projet = changement de contexte (pages isolées)
- newPage/newSubPage passent workspace_key à l'API
- Auto-switch view depuis paramètre URL dans board.html
This commit is contained in:
+31
-20
@@ -117,32 +117,41 @@ def _map_issue_to_card(issue: dict, owner: str = "", repo: str = "") -> dict:
|
|||||||
|
|
||||||
def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
ws = user.get("login", "Bruno") if user else "Bruno"
|
ws_name = user.get("login", "Bruno") if user else "Bruno"
|
||||||
|
ws_key = f"{owner}/{repo}" if owner and repo else ws_name # workspace = project context
|
||||||
recent = []
|
recent = []
|
||||||
if owner and repo:
|
if owner and repo:
|
||||||
recent = [
|
view_map = {
|
||||||
{"id": f"{owner}/{repo}", "name": "Kanban board", "icon": "📁",
|
"Kanban board": "kanban", "Detailed board": "detailed",
|
||||||
"url": f"/board/{owner}/{repo}", "active": True, "indent": 0},
|
"Table view": "table", "Status overview": "status", "Team Load": "teamload",
|
||||||
]
|
}
|
||||||
for sv in ["Detailed board", "Table view", "Status overview", "Team Load"]:
|
first = True
|
||||||
recent.append({"id": f"{owner}/{repo}/{sv.lower().replace(' ', '-')}",
|
for label, view in view_map.items():
|
||||||
"name": sv, "icon": "·", "url": f"/board/{owner}/{repo}",
|
indent = 0 if first else 1
|
||||||
"active": False, "indent": 1})
|
active = first
|
||||||
# Load user-created pages from DB
|
recent.append({
|
||||||
|
"id": f"{owner}/{repo}/{view}",
|
||||||
|
"name": label, "icon": "📁" if first else "·",
|
||||||
|
"url": f"/board/{owner}/{repo}?view={view}",
|
||||||
|
"active": active, "indent": indent,
|
||||||
|
})
|
||||||
|
first = False
|
||||||
|
# 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, updated_at FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10",
|
||||||
(ws,),
|
(ws_key,),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
for p in pages:
|
for p in pages:
|
||||||
recent.append({
|
recent.append({
|
||||||
"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,
|
||||||
})
|
})
|
||||||
return {"workspace_name": ws, "workspace_initial": ws[0].upper() if ws 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/")],
|
||||||
"favorite_pages": [], "user": user}
|
"favorite_pages": [], "user": user,
|
||||||
|
"workspace_key": ws_key}
|
||||||
|
|
||||||
|
|
||||||
def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""):
|
def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""):
|
||||||
@@ -261,12 +270,13 @@ async def library_page(request: Request):
|
|||||||
# ═══════════ Board page ═══════════
|
# ═══════════ Board page ═══════════
|
||||||
|
|
||||||
@router.get("/{owner}/{repo}", response_class=HTMLResponse)
|
@router.get("/{owner}/{repo}", response_class=HTMLResponse)
|
||||||
async def board(request: Request, owner: str, repo: str):
|
async def board(request: Request, owner: str, repo: str, view: str = Query(default="")):
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||||
sidebar = _sidebar_data(request, owner, repo)
|
sidebar = _sidebar_data(request, owner, repo)
|
||||||
template = env.get_template("board.html")
|
template = env.get_template("board.html")
|
||||||
return template.render(request=request, owner=owner, repo=repo, groups=[], **sidebar)
|
return template.render(request=request, owner=owner, repo=repo, groups=[],
|
||||||
|
initial_view=view, **sidebar)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════ View fragments ═══════════
|
# ═══════════ View fragments ═══════════
|
||||||
@@ -428,18 +438,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"),
|
||||||
"""Create a new Markdown page."""
|
project: str = Query(default="")):
|
||||||
|
"""Create a new Markdown page in the given project workspace."""
|
||||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
ws = 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) VALUES (?,?,?)",
|
||||||
(ws, title, section),
|
(ws_key, title, section),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
page_id = cur.lastrowid
|
page_id = cur.lastrowid
|
||||||
return {"status": "ok", "id": page_id, "title": title}
|
return {"status": "ok", "id": page_id, "title": title, "workspace": ws_key}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/pages/{page_id}")
|
@router.get("/api/pages/{page_id}")
|
||||||
|
|||||||
@@ -31,25 +31,14 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
|||||||
"indent": 0,
|
"indent": 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Load user-created pages
|
|
||||||
with get_conn() as conn:
|
|
||||||
pages = conn.execute(
|
|
||||||
"SELECT id, title FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10",
|
|
||||||
(ws,),
|
|
||||||
).fetchall()
|
|
||||||
for p in pages:
|
|
||||||
recent_pages.append({
|
|
||||||
"id": f"page/{p['id']}", "name": p["title"], "icon": "📄",
|
|
||||||
"url": f"/pages/{p['id']}", "active": False, "indent": 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"workspace_name": ws, "workspace_initial": initial,
|
"workspace_name": ws, "workspace_initial": initial,
|
||||||
"current_page": "Dashboard", "last_edited": "now",
|
"current_page": "Dashboard", "last_edited": "now",
|
||||||
"recent_pages": recent_pages,
|
"recent_pages": recent_pages,
|
||||||
"private_pages": [p for p in recent_pages if p["id"].startswith("page/")],
|
"private_pages": [],
|
||||||
"favorite_pages": [],
|
"favorite_pages": [],
|
||||||
"user": user,
|
"user": user,
|
||||||
|
"workspace_key": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,7 @@
|
|||||||
pageIcon: '{% block page_icon %}📁{% endblock %}',
|
pageIcon: '{% block page_icon %}📁{% endblock %}',
|
||||||
pageTitle: '{% block page_title %}FlowDeck{% endblock %}',
|
pageTitle: '{% block page_title %}FlowDeck{% endblock %}',
|
||||||
lastEdited: '{{ last_edited }}',
|
lastEdited: '{{ last_edited }}',
|
||||||
|
workspaceKey: '{{ workspace_key }}',
|
||||||
currentView: 'home',
|
currentView: 'home',
|
||||||
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
contextMenu: { visible: false, x: 0, y: 0, pageId: null, pageName: '' },
|
||||||
contextActions: {},
|
contextActions: {},
|
||||||
@@ -383,7 +384,8 @@
|
|||||||
return m ? m[1] : '';
|
return m ? m[1] : '';
|
||||||
},
|
},
|
||||||
newPage(section) {
|
newPage(section) {
|
||||||
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}`, {
|
const project = this.workspaceKey || '';
|
||||||
|
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}&project=${encodeURIComponent(project)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||||
})
|
})
|
||||||
@@ -392,7 +394,8 @@
|
|||||||
.catch(err => { alert('Failed to create page: ' + err.message); });
|
.catch(err => { alert('Failed to create page: ' + err.message); });
|
||||||
},
|
},
|
||||||
newSubPage(parentId) {
|
newSubPage(parentId) {
|
||||||
fetch(`/board/api/pages?title=New%20page§ion=Private`, {
|
const project = this.workspaceKey || '';
|
||||||
|
fetch(`/board/api/pages?title=New%20page§ion=Private&project=${encodeURIComponent(project)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -153,6 +153,15 @@
|
|||||||
<script>
|
<script>
|
||||||
const owner = '{{ owner }}';
|
const owner = '{{ owner }}';
|
||||||
const repo = '{{ repo }}';
|
const repo = '{{ repo }}';
|
||||||
|
const initialView = '{{ initial_view }}';
|
||||||
|
|
||||||
|
// Auto-switch to view from URL param
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (initialView && initialView !== 'kanban') {
|
||||||
|
const tab = document.querySelector(`.view-tab[data-view="${initialView}"]`);
|
||||||
|
if (tab) tab.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function setActiveTab(el) {
|
function setActiveTab(el) {
|
||||||
document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active'));
|
document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
|||||||
Reference in New Issue
Block a user