feat: multi-word per object + {objectID.wordID} placeholders

- Annotate: multiple words per object via db_objects_db_words M2M, word chips with add/remove per object card
- Generate sidebar: objects shown with comma-separated word list as display name
- Generate pair form: all object words as suggestion chips, click inserts {objectId.wordId} at cursor
- Preview resolves {objectId.wordId} → actual word text
- Backend: POST adds single word (no replace), new DELETE /db-objects/<id>/words/<junctionId>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 16:49:24 +02:00
parent 214f8a2019
commit 40c36182f1
5 changed files with 191 additions and 108 deletions

29
app.py
View File

@@ -1916,24 +1916,37 @@ def directus_db_object_words(obj_id):
if word.get("status") == "archived":
continue
items.append({
"junction_id": entry.get("id"),
"word_id": word["id"],
"titel_de": word.get("titel_de", ""),
"level": word.get("level") or 50,
"status": word.get("status", ""),
})
return jsonify({"data": items})
else: # POST — replace with single word
else: # POST — add a single word to the object (M2M, allows multiple)
body = request.get_json(force=True, silent=True) or {}
titel_de = (body.get("titel_de") or "").strip()
level = int(body.get("level") or 50)
# Delete all existing junctions for this object
existing, _ = _directus("GET", f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}&fields=id&limit=20", token)
for e in (existing.get("data") or []):
_directus("DELETE", f"/items/db_objects_db_words/{e['id']}", token)
if not titel_de:
return jsonify({"ok": True, "cleared": True})
return jsonify({"error": "titel_de required"}), 400
wid, _ = _find_or_create_db_word(titel_de, level, token)
_directus("POST", "/items/db_objects_db_words", token, {"db_objects_id": obj_id, "db_words_id": wid})
return jsonify({"ok": True, "word_id": wid})
# Check if already linked to avoid duplicates
existing, _ = _directus("GET",
f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}&filter[db_words_id][_eq]={wid}&fields=id&limit=1",
token)
if existing.get("data"):
return jsonify({"ok": True, "already_exists": True, "word_id": wid, "junction_id": existing["data"][0]["id"]})
resp, s = _directus("POST", "/items/db_objects_db_words", token,
{"db_objects_id": obj_id, "db_words_id": wid})
junction_id = resp["data"]["id"] if s in (200, 201) else None
return jsonify({"ok": True, "word_id": wid, "junction_id": junction_id})
@app.route("/api/directus/db-objects/<obj_id>/words/<junction_id>", methods=["DELETE"])
def directus_db_object_word_delete(obj_id, junction_id):
token = request.headers.get("Authorization", "")
_directus("DELETE", f"/items/db_objects_db_words/{junction_id}", token)
return jsonify({"ok": True})
@app.route("/api/directus/db-pairs/<pair_id>", methods=["PATCH", "DELETE"])