labels: per-label color
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build & push image (push) Successful in 32s

Give labels a color (migration 0011 adds labels.color, server_default
'default' so existing labels keep the neutral chip). The PATCH endpoint now
updates name and/or color; note serialization carries each label's color.
Frontend: a swatch picker per label in the Edit-labels modal, colored chips
on cards + in the editor (LABEL_CHIP_CLASSES), and a color dot on each
sidebar label. Reuses the note color vocabulary. (Graph node coloring rides
this in the graph-liveliness task.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-20 13:11:50 -04:00
co-authored by Claude Opus 4.8
parent f3145a5e3f
commit f321c2bf70
11 changed files with 133 additions and 22 deletions
+25 -11
View File
@@ -12,8 +12,15 @@ from .models.label import Label
bp = Blueprint("labels", __name__, url_prefix="/api/labels")
LABEL_COLORS = {"default", "red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"}
def _normalize_label_color(color: object) -> str:
return color if color in LABEL_COLORS else "default"
def _serialize_label(label: Label) -> dict:
return {"id": str(label.id), "name": label.name}
return {"id": str(label.id), "name": label.name, "color": label.color}
async def _get_owned_label(db, label_id: str) -> Label | None:
@@ -44,7 +51,7 @@ async def create_label():
existing = await db.scalar(select(Label).where(Label.owner_id == g.user_id, Label.name == name))
if existing is not None:
return jsonify(_serialize_label(existing)), 200
label = Label(owner_id=g.user_id, name=name)
label = Label(owner_id=g.user_id, name=name, color=_normalize_label_color(data.get("color")))
db.add(label)
await db.commit()
await db.refresh(label)
@@ -53,21 +60,28 @@ async def create_label():
@bp.patch("/<label_id>")
@login_required
async def rename_label(label_id: str):
async def update_label(label_id: str):
data = await request.get_json(silent=True) or {}
name = (data.get("name") or "").strip()
if not name:
has_name = "name" in data
has_color = "color" in data
if not has_name and not has_color:
return jsonify({"error": "nothing to update"}), 400
name = (data.get("name") or "").strip() if has_name else None
if has_name and not name:
return jsonify({"error": "label name is required"}), 400
async with session_scope() as db:
label = await _get_owned_label(db, label_id)
if label is None:
return jsonify({"error": "not found"}), 404
clash = await db.scalar(
select(Label).where(Label.owner_id == g.user_id, Label.name == name, Label.id != label.id)
)
if clash is not None:
return jsonify({"error": "a label with that name already exists"}), 409
label.name = name
if has_name:
clash = await db.scalar(
select(Label).where(Label.owner_id == g.user_id, Label.name == name, Label.id != label.id)
)
if clash is not None:
return jsonify({"error": "a label with that name already exists"}), 409
label.name = name
if has_color:
label.color = _normalize_label_color(data.get("color"))
await db.commit()
await db.refresh(label)
return jsonify(_serialize_label(label))
+1
View File
@@ -19,6 +19,7 @@ class Label(Base):
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False
)
name: Mapped[str] = mapped_column(Text(), nullable=False)
color: Mapped[str] = mapped_column(Text(), nullable=False, default="default", server_default="default")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
+3 -3
View File
@@ -62,13 +62,13 @@ async def _labels_for_notes(db, note_ids: list) -> dict:
if not note_ids:
return result
rows = await db.execute(
select(NoteLabel.note_id, Label.id, Label.name)
select(NoteLabel.note_id, Label.id, Label.name, Label.color)
.join(Label, Label.id == NoteLabel.label_id)
.where(NoteLabel.note_id.in_(note_ids))
.order_by(Label.name)
)
for note_id, label_id, name in rows.all():
result.setdefault(note_id, []).append({"id": str(label_id), "name": name})
for note_id, label_id, name, color in rows.all():
result.setdefault(note_id, []).append({"id": str(label_id), "name": name, "color": color})
return result