feat: sidebar Kanban board + pages Markdown + boutons + et 📚 fonctionnels
- Sidebar: 'Kanban board' affiché avant les 4 sous-vues
- DB: nouvelle table 'pages' (workspace, title, content, parent_section)
- API: POST /board/api/pages, GET/PUT /board/api/pages/{id}
- UI: page_editor.html — éditeur Markdown avec preview live
- UI: boutons + créent une page Markdown, bouton 📚 ouvre la page
- Dashboard: sidebar_data utilise BDD pages + workspace
- board.py: _sidebar_data enrichi (Kanban board + pages DB)
This commit is contained in:
@@ -130,6 +130,16 @@ def init_db():
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(project_owner, project_name, keyword)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
workspace TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT 'New page',
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
parent_section TEXT DEFAULT 'Private',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
+79
-10
@@ -115,18 +115,34 @@ 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", ""))
|
||||
ws = user.get("login", "Bruno") if user else "Bruno"
|
||||
recent = [{"id": f"{owner}/{repo}", "name": repo, "icon": "📁",
|
||||
"url": f"/board/{owner}/{repo}", "active": True, "indent": 0}]
|
||||
for sv in ["Detailed board", "Table view", "Status overview", "Team Load"]:
|
||||
recent.append({"id": f"{owner}/{repo}/{sv.lower().replace(' ', '-')}",
|
||||
"name": sv, "icon": "·", "url": f"/board/{owner}/{repo}",
|
||||
"active": False, "indent": 1})
|
||||
recent = []
|
||||
if owner and repo:
|
||||
recent = [
|
||||
{"id": f"{owner}/{repo}", "name": "Kanban board", "icon": "📁",
|
||||
"url": f"/board/{owner}/{repo}", "active": True, "indent": 0},
|
||||
]
|
||||
for sv in ["Detailed board", "Table view", "Status overview", "Team Load"]:
|
||||
recent.append({"id": f"{owner}/{repo}/{sv.lower().replace(' ', '-')}",
|
||||
"name": sv, "icon": "·", "url": f"/board/{owner}/{repo}",
|
||||
"active": False, "indent": 1})
|
||||
# Load user-created pages from DB
|
||||
with get_conn() as conn:
|
||||
pages = conn.execute(
|
||||
"SELECT id, title, updated_at FROM pages WHERE workspace=? ORDER BY updated_at DESC LIMIT 10",
|
||||
(ws,),
|
||||
).fetchall()
|
||||
for p in pages:
|
||||
recent.append({
|
||||
"id": f"page/{p['id']}", "name": p["title"], "icon": "📄",
|
||||
"url": f"/pages/{p['id']}", "active": False, "indent": 0,
|
||||
})
|
||||
return {"workspace_name": ws, "workspace_initial": ws[0].upper() if ws else "B",
|
||||
"current_page": repo, "last_edited": "2m ago",
|
||||
"recent_pages": recent, "private_pages": recent[:1], "user": user}
|
||||
"current_page": repo or "Dashboard", "last_edited": "now",
|
||||
"recent_pages": recent, "private_pages": [r for r in recent if r.get("active")],
|
||||
"user": user}
|
||||
|
||||
|
||||
def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""):
|
||||
@@ -391,7 +407,60 @@ async def extract_ai_keywords(owner: str, repo: str):
|
||||
return {"status": "ok", "issues_scanned": len(issues)}
|
||||
|
||||
|
||||
# ═══════════ v0.9.0: Sync endpoint ═══════════
|
||||
# ═══════════ Pages Markdown ═══════════
|
||||
|
||||
@router.post("/api/pages")
|
||||
async def create_page(request: Request, title: str = Query(default="New page"),
|
||||
section: str = Query(default="Private")):
|
||||
"""Create a new Markdown page."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
ws = user.get("login", "Bruno") if user else "Bruno"
|
||||
with get_conn() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO pages (workspace, title, parent_section) VALUES (?,?,?)",
|
||||
(ws, title, section),
|
||||
)
|
||||
conn.commit()
|
||||
page_id = cur.lastrowid
|
||||
return {"status": "ok", "id": page_id, "title": title}
|
||||
|
||||
|
||||
@router.get("/api/pages/{page_id}")
|
||||
async def get_page(page_id: int):
|
||||
"""Get a Markdown page."""
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Page not found")
|
||||
return dict(row)
|
||||
|
||||
|
||||
@router.put("/api/pages/{page_id}")
|
||||
async def update_page(page_id: int, title: str = Query(default=""),
|
||||
content: str = Query(default="")):
|
||||
"""Update a page's title and/or content."""
|
||||
with get_conn() as conn:
|
||||
if title:
|
||||
conn.execute("UPDATE pages SET title=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (title, page_id))
|
||||
if content:
|
||||
conn.execute("UPDATE pages SET content=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (content, page_id))
|
||||
conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/pages/{page_id}", response_class=HTMLResponse)
|
||||
async def view_page(request: Request, page_id: int):
|
||||
"""Render a page as HTML."""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
|
||||
if not row:
|
||||
return HTMLResponse("<h2>Page not found</h2>", status_code=404)
|
||||
sidebar = _sidebar_data(request)
|
||||
ctx = {**sidebar, "page": dict(row)}
|
||||
template = env.get_template("page_editor.html")
|
||||
return template.render(**ctx)
|
||||
|
||||
@router.post("/api/sync/{owner}/{repo}")
|
||||
async def sync_project(owner: str, repo: str):
|
||||
|
||||
+22
-28
@@ -8,41 +8,46 @@ from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.services.gitea_client import gitea
|
||||
from app.auth.session import SessionManager
|
||||
from app.db import get_conn
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
|
||||
|
||||
def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
||||
"""Build sidebar context from Gitea repos."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
workspace_name = user.get("login", "Bruno") if user else "Bruno"
|
||||
workspace_initial = workspace_name[0].upper() if workspace_name else "B"
|
||||
ws = user.get("login", "Bruno") if user else "Bruno"
|
||||
initial = ws[0].upper() if ws else "B"
|
||||
|
||||
# Build recent pages from repos
|
||||
recent_pages = []
|
||||
private_pages = []
|
||||
for repo in repos[:10]:
|
||||
full_name = repo.get("full_name", "")
|
||||
parts = full_name.split("/") if "/" in full_name else [workspace_name, full_name]
|
||||
page = {
|
||||
recent_pages.append({
|
||||
"id": full_name,
|
||||
"name": repo.get("name", full_name),
|
||||
"icon": "📁",
|
||||
"url": f"/board/{full_name}",
|
||||
"active": False,
|
||||
"indent": 0,
|
||||
}
|
||||
recent_pages.append(page)
|
||||
private_pages.append({**page})
|
||||
})
|
||||
|
||||
# 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 {
|
||||
"workspace_name": workspace_name,
|
||||
"workspace_initial": workspace_initial,
|
||||
"current_page": "Dashboard",
|
||||
"last_edited": "now",
|
||||
"workspace_name": ws, "workspace_initial": initial,
|
||||
"current_page": "Dashboard", "last_edited": "now",
|
||||
"recent_pages": recent_pages,
|
||||
"private_pages": private_pages,
|
||||
"private_pages": [p for p in recent_pages if p["id"].startswith("page/")],
|
||||
"user": user,
|
||||
}
|
||||
|
||||
@@ -53,33 +58,22 @@ async def dashboard(
|
||||
search: str = Query(default=""),
|
||||
show_archived: bool = Query(default=False),
|
||||
):
|
||||
"""Dashboard: liste tous les projets Gitea (user + orgs)."""
|
||||
try:
|
||||
repos = await gitea.get_user_repos(page=1, limit=50)
|
||||
|
||||
if search:
|
||||
q = search.lower()
|
||||
repos = [r for r in repos if q in r.get("full_name", "").lower()
|
||||
or q in (r.get("description") or "").lower()]
|
||||
|
||||
if not show_archived:
|
||||
repos = [r for r in repos if not r.get("archived", False)]
|
||||
|
||||
repos.sort(key=lambda r: r.get("updated_at", ""), reverse=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Dashboard error: %s", e)
|
||||
repos = []
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request, repos)
|
||||
template = env.get_template("dashboard.html")
|
||||
return template.render(
|
||||
request=request,
|
||||
repos=repos,
|
||||
search=search,
|
||||
show_archived=show_archived,
|
||||
**sidebar,
|
||||
)
|
||||
return template.render(request=request, repos=repos, search=search,
|
||||
show_archived=show_archived, **sidebar)
|
||||
|
||||
+32
-10
@@ -48,8 +48,8 @@
|
||||
<span class="chevron open">▶</span> Recents
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" title="Open in Library">📚</button>
|
||||
<button class="section-action-btn" title="More">⋯</button>
|
||||
<button class="section-action-btn" title="Open in Library" @click="openLibrary('Recents')">📚</button>
|
||||
<button class="section-action-btn" title="Add page" @click="newPage('Recents')">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="sidebar-items">
|
||||
@@ -60,8 +60,8 @@
|
||||
<span class="page-icon">{{ page.icon }}</span>
|
||||
<span class="page-name">{{ page.name }}</span>
|
||||
<span class="item-actions">
|
||||
<button class="item-action-btn" @click.stop="openInLibrary('{{ page.id }}')">📚</button>
|
||||
<button class="item-action-btn" @click.stop="showMore('{{ page.id }}')">⋯</button>
|
||||
<button class="item-action-btn" @click.stop="openLibrary('{{ page.id }}')">📚</button>
|
||||
<button class="item-action-btn" @click.stop="newSubPage('{{ page.id }}')">+</button>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -81,7 +81,8 @@
|
||||
<span class="chevron open">▶</span> Private
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" @click="addPage('private')" title="Add page">+</button>
|
||||
<button class="section-action-btn" title="Open in Library" @click="openLibrary('Private')">📚</button>
|
||||
<button class="section-action-btn" title="Add page" @click="newPage('Private')">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="sidebar-items">
|
||||
@@ -92,11 +93,17 @@
|
||||
<span class="page-icon">{{ page.icon }}</span>
|
||||
<span class="page-name">{{ page.name }}</span>
|
||||
<span class="item-actions">
|
||||
<button class="item-action-btn" @click.stop="showMore('{{ page.id }}')">⋯</button>
|
||||
<button class="item-action-btn" @click.stop="addSubPage('{{ page.id }}')">+</button>
|
||||
<button class="item-action-btn" @click.stop="openLibrary('{{ page.id }}')">📚</button>
|
||||
<button class="item-action-btn" @click.stop="newSubPage('{{ page.id }}')">+</button>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not private_pages %}
|
||||
<li class="sidebar-item">
|
||||
<span class="page-icon">📄</span>
|
||||
<span class="page-name text-dim">No pages yet</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -318,10 +325,25 @@
|
||||
this.contextMenu.visible = false;
|
||||
},
|
||||
|
||||
openInLibrary(id) { console.log('Open in library:', id); },
|
||||
openLibrary(id) {
|
||||
// Open the library view for this section/page
|
||||
console.log('Open library for:', id);
|
||||
window.location.href = '/pages/' + id.replace('page/', '');
|
||||
},
|
||||
openInLibrary(id) { this.openLibrary(id); },
|
||||
showMore(id) { console.log('Show more:', id); },
|
||||
addPage(section) { console.log('Add page to:', section); },
|
||||
addSubPage(id) { console.log('Add sub-page to:', id); },
|
||||
addPage(section) { this.newPage(section); },
|
||||
addSubPage(id) { this.newSubPage(id); },
|
||||
newPage(section) {
|
||||
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
},
|
||||
newSubPage(parentId) {
|
||||
fetch(`/board/api/pages?title=New%20sub-page§ion=Private`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
{% extends "base.html" %}
|
||||
{% block page_icon %}📄{% endblock %}
|
||||
{% block page_title %}{{ page.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-title-area">
|
||||
<div class="page-title">
|
||||
<span class="page-icon-lg">📄</span>
|
||||
<h1 contenteditable="true"
|
||||
@blur="saveTitle($event.target.textContent)"
|
||||
x-data>{{ page.title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-data="pageEditor('{{ page.content|e }}')" style="padding: 0 24px 48px; max-width: 900px;">
|
||||
<!-- Toolbar -->
|
||||
<div style="display:flex; gap:4px; margin-bottom:12px; flex-wrap:wrap;">
|
||||
<button class="toolbar-btn" @click="insertMd('**', '**')" title="Bold">B</button>
|
||||
<button class="toolbar-btn" @click="insertMd('*', '*')" title="Italic"><i>I</i></button>
|
||||
<button class="toolbar-btn" @click="insertMd('# ', '')" title="Heading 1">H1</button>
|
||||
<button class="toolbar-btn" @click="insertMd('## ', '')" title="Heading 2">H2</button>
|
||||
<button class="toolbar-btn" @click="insertMd('- ', '')" title="Bullet list">•</button>
|
||||
<button class="toolbar-btn" @click="insertMd('1. ', '')" title="Numbered list">1.</button>
|
||||
<button class="toolbar-btn" @click="insertMd('> ', '')" title="Quote">"</button>
|
||||
<button class="toolbar-btn" @click="insertMd('`', '`')" title="Code"></></button>
|
||||
<button class="toolbar-btn" @click="insertMd('```\n', '\n```')" title="Code block">```</button>
|
||||
<button class="toolbar-btn" @click="insertMd('---\n', '')" title="Divider">—</button>
|
||||
<button class="toolbar-btn" @click="insertMd('[', '](url)')" title="Link">🔗</button>
|
||||
<button class="toolbar-btn" @click="insertMd('- [ ] ', '')" title="Todo">☐</button>
|
||||
</div>
|
||||
|
||||
<!-- Editor -->
|
||||
<div style="display:flex; gap:0; min-height:400px; border:1px solid var(--border); border-radius:8px; overflow:hidden;">
|
||||
<!-- Markdown source -->
|
||||
<div style="flex:1; display:flex; flex-direction:column;">
|
||||
<div style="padding:4px 12px; background:var(--bg-tertiary); font-size:12px; color:var(--text-dim);">Markdown</div>
|
||||
<textarea x-ref="editor" x-model="content"
|
||||
@input="autoSave()"
|
||||
style="flex:1; width:100%; min-height:400px; background:var(--bg-secondary);
|
||||
border:none; padding:12px; color:var(--text-primary); font-family:var(--font-mono);
|
||||
font-size:13px; line-height:1.6; resize:none; outline:none; tab-size:2;"
|
||||
placeholder="Start writing in Markdown..."></textarea>
|
||||
</div>
|
||||
<!-- Preview -->
|
||||
<div style="flex:1; border-left:1px solid var(--border); display:flex; flex-direction:column;">
|
||||
<div style="padding:4px 12px; background:var(--bg-tertiary); font-size:12px; color:var(--text-dim);">Preview</div>
|
||||
<div style="flex:1; padding:12px; overflow-y:auto; font-size:14px; line-height:1.7; color:var(--text-primary);"
|
||||
x-html="renderedContent()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-top:8px; font-size:12px; color:var(--text-dim);">
|
||||
<span x-show="saved">✓ Saved</span>
|
||||
<span x-show="dirty && !saving">● Unsaved</span>
|
||||
<span x-text="wordCount + ' words'"></span>
|
||||
<span style="margin-left:auto;" x-text="'Last edited: ' + lastSaved"></span>
|
||||
</div>
|
||||
|
||||
<!-- Get started block (shown for empty pages) -->
|
||||
<div x-show="!content.trim()" style="margin-top:48px; text-align:center; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">📝</div>
|
||||
<h3 style="color:var(--text-secondary); margin-bottom:8px;">Get started with</h3>
|
||||
<div style="display:flex; gap:12px; justify-content:center; flex-wrap:wrap;">
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('# ', '')">📄 Empty page</button>
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('- [ ] Task 1\n- [ ] Task 2\n', '')">✅ To-do list</button>
|
||||
<button class="sidebar-new-chat" style="width:auto; padding:8px 16px;" @click="insertMd('| Col 1 | Col 2 |\n|-------|-------|\n| | |\n', '')">📊 Table</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>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Simple Markdown-to-HTML renderer
|
||||
function renderMarkdown(md) {
|
||||
let html = md
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
// Headers
|
||||
.replace(/^### (.+)$/gm, '<h3 style="font-size:16px; margin:12px 0 4px;">$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2 style="font-size:18px; margin:16px 0 4px;">$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1 style="font-size:22px; margin:20px 0 6px;">$1</h1>')
|
||||
// Bold/italic
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code style="background:var(--bg-tertiary); padding:1px 4px; border-radius:3px; font-size:12px;">$1</code>')
|
||||
// Code blocks
|
||||
.replace(/```[\s\S]*?```/g, (m) => '<pre style="background:var(--bg-tertiary); padding:12px; border-radius:4px; overflow-x:auto; font-size:12px;"><code>' + m.slice(3, -3).trim() + '</code></pre>')
|
||||
// Links
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:var(--blue);">$1</a>')
|
||||
// Horizontal rules
|
||||
.replace(/^---$/gm, '<hr style="border:none; border-top:1px solid var(--border); margin:16px 0;">')
|
||||
// Blockquotes
|
||||
.replace(/^> (.+)$/gm, '<blockquote style="border-left:2px solid var(--blue); padding-left:12px; margin:8px 0; color:var(--text-secondary);">$1</blockquote>')
|
||||
// Lists
|
||||
.replace(/^- (.+)$/gm, '<li style="margin-left:20px;">$1</li>')
|
||||
.replace(/^\d+\. (.+)$/gm, '<li style="margin-left:20px;">$1</li>')
|
||||
// Checkboxes
|
||||
.replace(/^- \[ \] (.+)$/gm, '<div style="display:flex; align-items:center; gap:8px; margin:2px 0;"><input type="checkbox" style="accent-color:var(--blue);">$1</div>')
|
||||
.replace(/^- \[x\] (.+)$/gm, '<div style="display:flex; align-items:center; gap:8px; margin:2px 0;"><input type="checkbox" checked style="accent-color:var(--blue);"><span style="text-decoration:line-through; color:var(--text-dim);">$1</span></div>')
|
||||
// Paragraphs
|
||||
.replace(/\n\n/g, '<br><br>');
|
||||
return html;
|
||||
}
|
||||
|
||||
function pageEditor(initialContent) {
|
||||
return {
|
||||
content: initialContent || '',
|
||||
saved: true,
|
||||
dirty: false,
|
||||
saving: false,
|
||||
lastSaved: 'now',
|
||||
saveTimeout: null,
|
||||
renderedContent() { return renderMarkdown(this.content); },
|
||||
get wordCount() { return this.content.trim() ? this.content.trim().split(/\s+/).length : 0; },
|
||||
insertMd(before, after) {
|
||||
const el = this.$refs.editor;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
const selected = this.content.substring(start, end);
|
||||
const replacement = before + selected + after;
|
||||
this.content = this.content.substring(0, start) + replacement + this.content.substring(end);
|
||||
this.autoSave();
|
||||
el.focus();
|
||||
el.setSelectionRange(start + before.length, start + before.length + selected.length);
|
||||
},
|
||||
autoSave() {
|
||||
this.dirty = true;
|
||||
this.saved = false;
|
||||
clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = setTimeout(() => this.save(), 1500);
|
||||
},
|
||||
save() {
|
||||
this.saving = true;
|
||||
const title = document.querySelector('.page-title h1')?.textContent || 'New page';
|
||||
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(title)}&content=${encodeURIComponent(this.content)}`, { method: 'PUT' })
|
||||
.then(() => {
|
||||
this.saved = true;
|
||||
this.dirty = false;
|
||||
this.saving = false;
|
||||
this.lastSaved = new Date().toLocaleTimeString();
|
||||
});
|
||||
},
|
||||
saveTitle(newTitle) {
|
||||
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(newTitle)}`, { method: 'PUT' });
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user