M6: label management — usage counts + merge
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 8s
CI & Build / Python tests (push) Successful in 16s
CI & Build / Build & push image (push) Successful in 50s

Extends the existing label manager (create/rename/color/delete) with the two missing maintenance tools (task 1904), so the label list stays clean — which matters more now that #tags mint labels automatically.

Backend: GET /api/labels returns a per-label note count (one grouped query); new POST /api/labels/<id>/merge moves the source label's notes onto a target and deletes the source (repoint via delete+reinsert to avoid mutating the composite PK; preserves via_tag; dedupes notes already on the target). Body #tags are NOT rewritten, so a tag-sourced label re-mints on next edit if its #tag text remains — a documented nuance.

Frontend: LabelsModal shows each label's note count and a 'merge into…' picker; also restores the previously-missing close (x) and merge icons.

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-22 12:36:50 -04:00
co-authored by Claude Opus 4.8
parent 95b0e30fc7
commit ae0c748507
5 changed files with 158 additions and 15 deletions
+16 -3
View File
@@ -6,6 +6,8 @@ export interface Label {
id: string;
name: string;
color: string;
// Number of notes carrying this label (from GET /api/labels; used in label management).
count?: number;
}
export const useLabelsStore = defineStore("labels", () => {
@@ -34,14 +36,15 @@ export const useLabelsStore = defineStore("labels", () => {
async function rename(id: string, name: string): Promise<void> {
const updated = await api.patch<Label>(`/api/labels/${id}`, { name });
const idx = items.value.findIndex((lb) => lb.id === id);
if (idx >= 0) items.value[idx] = updated;
// The single-label PATCH doesn't recompute the count — keep the one we have.
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
sort();
}
async function setColor(id: string, color: string): Promise<void> {
const updated = await api.patch<Label>(`/api/labels/${id}`, { color });
const idx = items.value.findIndex((lb) => lb.id === id);
if (idx >= 0) items.value[idx] = updated;
if (idx >= 0) items.value[idx] = { ...updated, count: items.value[idx].count };
}
async function remove(id: string): Promise<void> {
@@ -49,5 +52,15 @@ export const useLabelsStore = defineStore("labels", () => {
items.value = items.value.filter((lb) => lb.id !== id);
}
return { items, loaded, load, create, rename, setColor, remove };
// Merge `sourceId` into `targetId`: the server moves the source's notes onto the
// target and deletes the source; the response carries the target's new count.
async function mergeInto(sourceId: string, targetId: string): Promise<void> {
const target = await api.post<Label>(`/api/labels/${sourceId}/merge`, { into: targetId });
items.value = items.value.filter((lb) => lb.id !== sourceId);
const idx = items.value.findIndex((lb) => lb.id === targetId);
if (idx >= 0) items.value[idx] = target;
sort();
}
return { items, loaded, load, create, rename, setColor, remove, mergeInto };
});