Add drag-and-drop reordering for note tabs, and a user-facing guide

Tabs can now be dragged to reorder within a group and to move between
sections in one gesture: dropping on a tab's left/right half inserts
before/after it and adopts that tab's section, while a dedicated zone
past the last group ungroups and appends at the end. A blue indicator
line shows the drop position while dragging.

core/notes.reorder() takes the full new page-id order computed
client-side (NotePanel.vue has the reactive list already) rather than
recomputing positions server-side, validates it against the current
page-id set, sets the moved page's section, then renumbers position.
POST /api/notes/reorder exposes it.

Also adds docs/notes-guide.md, an end-user walkthrough of the shared
notes panel (pages, sections, drag-and-drop, AI interaction) — the
existing docs were all developer-facing specs with no operating
instructions for the feature itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 07:27:38 +09:00
co-authored by Claude Opus 5
parent 1c21fc3406
commit 8b2231ea97
8 changed files with 245 additions and 3 deletions
+16 -1
View File
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session as DBSession
from app.core.notes import next_position, rename_section, set_section
from app.core.notes import next_position, reorder, rename_section, set_section
from app.db.database import get_db
from app.db.models import NotePage
@@ -40,6 +40,12 @@ class SectionRenameRequest(BaseModel):
new_name: str | None = None
class ReorderRequest(BaseModel):
page_id: int
section: str | None = None
order: list[int]
@router.get("", response_model=list[NotePageResponse])
def list_pages(db: DBSession = Depends(get_db)):
return db.query(NotePage).order_by(NotePage.position, NotePage.id).all()
@@ -64,6 +70,15 @@ def rename_section_endpoint(req: SectionRenameRequest, db: DBSession = Depends(g
return {"renamed": count}
@router.post("/reorder")
def reorder_pages(req: ReorderRequest, db: DBSession = Depends(get_db)):
"""タブのドラッグ&ドロップによる並べ替え。order は全ページIDを新しい順序で並べたもの。"""
ok = reorder(db, req.page_id, req.section, req.order)
if not ok:
raise HTTPException(status_code=400, detail="order must contain exactly the current page ids")
return {"reordered": len(req.order)}
@router.patch("/{page_id}", response_model=NotePageResponse)
def update_page(page_id: int, req: NotePageUpdate, db: DBSession = Depends(get_db)):
page = db.get(NotePage, page_id)
+21
View File
@@ -104,6 +104,27 @@ def set_section(db: DBSession, page_id: int, section: str | None) -> NotePage |
return page
def reorder(db: DBSession, page_id: int, section: str | None, order: list[int]) -> bool:
"""タブのドラッグ&ドロップによる並べ替えを反映する。
order にはドラッグ後の全ページIDを新しい順序で渡す(フロント側で計算する)。
page_id のページには先に section を設定し、その後 order の順に position を振り直す。
こうすることで、別セクションのタブの前後にドロップした場合はそのセクションへの
移動を兼ね、同じセクション内でのドロップなら単純な並べ替えになる。
order が現在の全ページIDの集合と一致しない場合は何もせず False を返す。
"""
pages = {p.id: p for p in db.query(NotePage).all()}
if page_id not in pages or set(order) != set(pages.keys()):
return False
pages[page_id].section = (section or "").strip() or None
for i, pid in enumerate(order):
pages[pid].position = i
db.commit()
return True
def rename_section(db: DBSession, old_name: str, new_name: str | None) -> int:
"""あるセクションに属する全ページのセクション名を一括で変更する。new_name が空ならグループ解除になる。