Files
flowdeck/app/db.py
T
bruno d4cd45c6cb
FlowDeck CI / test (push) Failing after 8s
FlowDeck CI / docker (push) Has been skipped
feat: Notion-style block editor v1 + Move to Trash + fixes
- Block editor: 13 block types with contentEditable + vanilla JS event delegation
- Slash menu /: 14 commands, 100% vanilla JS (zero Alpine conflicts)
- Keyboard: Enter=new block, Shift+Enter=soft break, arrows navigate blocks
- Drag & drop: SortableJS with 6-dot handles on block-wrapper
- Move to Trash: DELETE /board/api/pages/{id} + instant sidebar cleanup
- Placeholders: 'New page' + 'Type / for commands' in gray (CSS :empty::before)
- Redirect after delete stays in project context via workspaceKey
- Fixes: title auto-save, cursor positioning, x-for keys, Sortable guard
2026-07-09 11:29:36 -04:00

176 lines
7.2 KiB
Python

"""FlowDeck — Database layer (SQLite with WAL mode)."""
from __future__ import annotations
import sqlite3
from contextlib import contextmanager
from pathlib import Path
def get_db_path() -> Path:
from app.config import settings
return settings.db_path
def init_db():
"""Create tables if they don't exist."""
db_path = get_db_path()
if str(db_path) != ":memory:":
db_path.parent.mkdir(parents=True, exist_ok=True)
with get_conn() as conn:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT NOT NULL UNIQUE,
full_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
avatar_url TEXT NOT NULL DEFAULT '',
is_admin BOOLEAN NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS user_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gitea_user_id INTEGER NOT NULL UNIQUE,
gitea_token TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS boards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_owner TEXT NOT NULL,
project_name TEXT NOT NULL,
columns_json TEXT NOT NULL DEFAULT '["Backlog","À faire","En cours","Révision","Terminé"]',
wip_limits_json TEXT DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(project_owner, project_name)
);
CREATE TABLE IF NOT EXISTS cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
board_id INTEGER NOT NULL REFERENCES boards(id),
gitea_issue_id INTEGER,
column_name TEXT NOT NULL DEFAULT 'Backlog',
position INTEGER NOT NULL DEFAULT 0,
priority TEXT DEFAULT 'Medium',
due_date TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_owner TEXT NOT NULL,
project_name TEXT NOT NULL,
title TEXT NOT NULL DEFAULT 'Notes',
content TEXT NOT NULL DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(project_owner, project_name, title)
);
CREATE TABLE IF NOT EXISTS col_mapping (
id INTEGER PRIMARY KEY AUTOINCREMENT,
board_id INTEGER NOT NULL REFERENCES boards(id),
column_name TEXT NOT NULL,
gitea_label TEXT NOT NULL,
close_issue BOOLEAN NOT NULL DEFAULT 0,
UNIQUE(board_id, column_name)
);
CREATE TABLE IF NOT EXISTS checklists (
id INTEGER PRIMARY KEY AUTOINCREMENT,
board_id INTEGER NOT NULL,
gitea_issue_id INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT 'Checklist',
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(board_id, gitea_issue_id, title)
);
CREATE TABLE IF NOT EXISTS checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
content TEXT NOT NULL DEFAULT '',
checked BOOLEAN NOT NULL DEFAULT 0,
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS project_properties (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_owner TEXT NOT NULL,
project_name TEXT NOT NULL,
name TEXT NOT NULL,
prop_type TEXT NOT NULL DEFAULT 'select', -- select, multi_select, date, person, text
options_json TEXT DEFAULT '[]', -- for select/multi_select
position INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(project_owner, project_name, name)
);
CREATE TABLE IF NOT EXISTS property_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
property_id INTEGER NOT NULL REFERENCES project_properties(id) ON DELETE CASCADE,
gitea_issue_id INTEGER NOT NULL,
value TEXT NOT NULL DEFAULT '',
UNIQUE(property_id, gitea_issue_id)
);
CREATE TABLE IF NOT EXISTS ai_keywords (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_owner TEXT NOT NULL,
project_name TEXT NOT NULL,
keyword TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#787774',
usage_count INTEGER NOT NULL DEFAULT 1,
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',
parent_id INTEGER REFERENCES pages(id),
created_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
# Migration: add sort_order for drag & drop tree reordering (v1.1.0+)
try:
conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0")
except sqlite3.OperationalError:
pass
# Migration: add content_format for Notion-style block editor (v1.2.0+)
# 'markdown' = legacy textarea, 'blocks' = JSON array of block objects
try:
conn.execute("ALTER TABLE pages ADD COLUMN content_format TEXT NOT NULL DEFAULT 'markdown'")
except sqlite3.OperationalError:
pass
conn.commit()
@contextmanager
def get_conn():
"""Yield a SQLite connection (WAL mode, foreign keys on)."""
db_path = get_db_path()
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
try:
yield conn
finally:
conn.close()