labels: per-label color
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:
+25
-11
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user