feat: edit and delete pairs in GenerateIt
- Backend: PATCH /api/directus/db-pairs/<id> updates level, statement, question (creates question if new, removes if cleared) - Backend: DELETE /api/directus/db-pairs/<id> removes pair + all junctions, questions and statements - Frontend: inline edit form per pair (level slider, statement, question) - Frontend: delete button per pair with confirm dialog - api.ts: updateDbPair, deleteDbPair Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
82
app.py
82
app.py
@@ -1877,6 +1877,88 @@ def directus_db_object_pairs(obj_id):
|
|||||||
return jsonify({"ok": True, "pair_id": pair_id, "statement_id": stmt_id, "question_id": q_id})
|
return jsonify({"ok": True, "pair_id": pair_id, "statement_id": stmt_id, "question_id": q_id})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/directus/db-pairs/<pair_id>", methods=["PATCH", "DELETE"])
|
||||||
|
def directus_db_pair(pair_id):
|
||||||
|
"""PATCH: level + question/statement inline aktualisieren.
|
||||||
|
DELETE: Pair + alle verknüpften Junctions + Question + Statement entfernen."""
|
||||||
|
token = request.headers.get("Authorization", "")
|
||||||
|
|
||||||
|
if request.method == "PATCH":
|
||||||
|
body = request.get_json(force=True, silent=True) or {}
|
||||||
|
|
||||||
|
# Pair-Level updaten
|
||||||
|
if "level" in body:
|
||||||
|
_directus("PATCH", f"/items/db_pairs/{pair_id}", token, {"level": body["level"]})
|
||||||
|
|
||||||
|
# Statement updaten (nur erstes verknüpftes)
|
||||||
|
if "statement_de" in body:
|
||||||
|
s_junc, _ = _directus("GET",
|
||||||
|
f"/items/db_pairs_db_statement?filter[db_pairs_id][_eq]={pair_id}&fields=db_statement_id&limit=1", token)
|
||||||
|
for e in (s_junc.get("data") or []):
|
||||||
|
sid = e.get("db_statement_id")
|
||||||
|
if sid:
|
||||||
|
_directus("PATCH", f"/items/db_statement/{sid}", token,
|
||||||
|
{"statement_de": body["statement_de"], "level": body.get("level", 50)})
|
||||||
|
|
||||||
|
# Question updaten/anlegen
|
||||||
|
if "question_de" in body:
|
||||||
|
question_de = (body["question_de"] or "").strip()
|
||||||
|
q_junc, _ = _directus("GET",
|
||||||
|
f"/items/db_pairs_db_question?filter[db_pairs_id][_eq]={pair_id}&fields=db_question_id&limit=1", token)
|
||||||
|
existing_q = (q_junc.get("data") or [])
|
||||||
|
if existing_q:
|
||||||
|
qid = existing_q[0].get("db_question_id")
|
||||||
|
if question_de:
|
||||||
|
_directus("PATCH", f"/items/db_question/{qid}", token,
|
||||||
|
{"question_de": question_de, "level": body.get("level", 50)})
|
||||||
|
else:
|
||||||
|
# Frage gelöscht → Junction + Item entfernen
|
||||||
|
_directus("DELETE", f"/items/db_pairs_db_question", token,
|
||||||
|
[e["id"] for e in existing_q if e.get("id")])
|
||||||
|
_directus("DELETE", f"/items/db_question/{qid}", token)
|
||||||
|
elif question_de:
|
||||||
|
# Neue Frage anlegen und verknüpfen
|
||||||
|
q_resp, s = _directus("POST", "/items/db_question", token,
|
||||||
|
{"status": "draft", "question_de": question_de,
|
||||||
|
"level": body.get("level", 50)})
|
||||||
|
if s in (200, 201):
|
||||||
|
qid = q_resp["data"]["id"]
|
||||||
|
_directus("POST", "/items/db_pairs_db_question", token,
|
||||||
|
{"db_pairs_id": pair_id, "db_question_id": qid})
|
||||||
|
|
||||||
|
return jsonify({"ok": True})
|
||||||
|
|
||||||
|
else: # DELETE
|
||||||
|
# Junction zu Objekten entfernen
|
||||||
|
obj_junc, _ = _directus("GET",
|
||||||
|
f"/items/db_objects_db_pairs?filter[db_pairs_id][_eq]={pair_id}&fields=id&limit=100", token)
|
||||||
|
obj_junc_ids = [e["id"] for e in (obj_junc.get("data") or []) if e.get("id")]
|
||||||
|
if obj_junc_ids:
|
||||||
|
_directus("DELETE", "/items/db_objects_db_pairs", token, obj_junc_ids)
|
||||||
|
|
||||||
|
# Questions löschen
|
||||||
|
q_junc, _ = _directus("GET",
|
||||||
|
f"/items/db_pairs_db_question?filter[db_pairs_id][_eq]={pair_id}&fields=id,db_question_id&limit=100", token)
|
||||||
|
for e in (q_junc.get("data") or []):
|
||||||
|
if e.get("id"):
|
||||||
|
_directus("DELETE", f"/items/db_pairs_db_question/{e['id']}", token)
|
||||||
|
if e.get("db_question_id"):
|
||||||
|
_directus("DELETE", f"/items/db_question/{e['db_question_id']}", token)
|
||||||
|
|
||||||
|
# Statements löschen
|
||||||
|
s_junc, _ = _directus("GET",
|
||||||
|
f"/items/db_pairs_db_statement?filter[db_pairs_id][_eq]={pair_id}&fields=id,db_statement_id&limit=100", token)
|
||||||
|
for e in (s_junc.get("data") or []):
|
||||||
|
if e.get("id"):
|
||||||
|
_directus("DELETE", f"/items/db_pairs_db_statement/{e['id']}", token)
|
||||||
|
if e.get("db_statement_id"):
|
||||||
|
_directus("DELETE", f"/items/db_statement/{e['db_statement_id']}", token)
|
||||||
|
|
||||||
|
# Pair selbst löschen
|
||||||
|
_, s = _directus("DELETE", f"/items/db_pairs/{pair_id}", token)
|
||||||
|
return jsonify({"ok": s in (200, 204)})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||||
|
|
||||||
|
|||||||
@@ -453,3 +453,24 @@ export async function createDbPair(
|
|||||||
if (!res.ok) throw new Error(data.error || 'Fehler beim Erstellen des Pairs')
|
if (!res.ok) throw new Error(data.error || 'Fehler beim Erstellen des Pairs')
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateDbPair(
|
||||||
|
pairId: string,
|
||||||
|
payload: { level?: number; question_de?: string; statement_de?: string },
|
||||||
|
token: string
|
||||||
|
): Promise<void> {
|
||||||
|
const res = await fetch(`/api/directus/db-pairs/${pairId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Aktualisieren des Pairs')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteDbPair(pairId: string, token: string): Promise<void> {
|
||||||
|
const res = await fetch(`/api/directus/db-pairs/${pairId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error('Fehler beim Löschen des Pairs')
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
getDbObjects,
|
getDbObjects,
|
||||||
getDbObjectPairs,
|
getDbObjectPairs,
|
||||||
createDbPair,
|
createDbPair,
|
||||||
|
updateDbPair,
|
||||||
|
deleteDbPair,
|
||||||
directusAssetUrl,
|
directusAssetUrl,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
@@ -207,14 +209,44 @@ interface PairsListProps {
|
|||||||
|
|
||||||
function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProps) {
|
function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProps) {
|
||||||
const [showForm, setShowForm] = useState(false)
|
const [showForm, setShowForm] = useState(false)
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
|
const [editLevel, setEditLevel] = useState(50)
|
||||||
|
const [editQuestion, setEditQuestion] = useState('')
|
||||||
|
const [editStatement, setEditStatement] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
const handleSaved = () => {
|
const startEdit = (pair: DbPair) => {
|
||||||
setShowForm(false)
|
setEditingId(pair.id)
|
||||||
|
setEditLevel(pair.level)
|
||||||
|
setEditStatement(pair.statements[0]?.statement_de ?? '')
|
||||||
|
setEditQuestion(pair.questions[0]?.question_de ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEdit = async () => {
|
||||||
|
if (!editingId) return
|
||||||
|
setSaving(true)
|
||||||
|
try {
|
||||||
|
await updateDbPair(editingId, {
|
||||||
|
level: editLevel,
|
||||||
|
statement_de: editStatement,
|
||||||
|
question_de: editQuestion,
|
||||||
|
}, token)
|
||||||
|
setEditingId(null)
|
||||||
|
onRefresh()
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (pairId: string) => {
|
||||||
|
if (!confirm('Pair wirklich löschen?')) return
|
||||||
|
await deleteDbPair(pairId, token)
|
||||||
onRefresh()
|
onRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="empty-state">Lade…</div>
|
const handleSaved = () => { setShowForm(false); onRefresh() }
|
||||||
|
|
||||||
|
if (loading) return <div className="empty-state">Lade…</div>
|
||||||
if (!objectId) return <div className="empty-state">Kein Objekt gewählt.</div>
|
if (!objectId) return <div className="empty-state">Kein Objekt gewählt.</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,7 +260,39 @@ function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProp
|
|||||||
|
|
||||||
{pairs.map(pair => (
|
{pairs.map(pair => (
|
||||||
<div key={pair.id} style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
|
<div key={pair.id} style={{ padding: '8px 0', borderBottom: '1px solid var(--border)' }}>
|
||||||
{/* Level badge */}
|
{editingId === pair.id ? (
|
||||||
|
/* ── Inline-Edit-Formular ── */
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<label style={{ fontSize: 11, color: 'var(--text-2)', whiteSpace: 'nowrap' }}>Level {editLevel}</label>
|
||||||
|
<input type="range" min={1} max={100} value={editLevel}
|
||||||
|
onChange={e => setEditLevel(Number(e.target.value))}
|
||||||
|
style={{ flex: 1 }} />
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={editStatement}
|
||||||
|
onChange={e => setEditStatement(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Aussage (Pflicht)…"
|
||||||
|
style={{ width: '100%', padding: '5px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12, resize: 'vertical', boxSizing: 'border-box' }}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
value={editQuestion}
|
||||||
|
onChange={e => setEditQuestion(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
placeholder="Frage (optional)…"
|
||||||
|
style={{ width: '100%', padding: '5px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12, resize: 'vertical', boxSizing: 'border-box' }}
|
||||||
|
/>
|
||||||
|
<div style={{ display: 'flex', gap: 4 }}>
|
||||||
|
<button className="btn-primary btn-sm" style={{ flex: 1 }} onClick={saveEdit} disabled={saving || !editStatement.trim()}>
|
||||||
|
{saving ? 'Speichere…' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
<button className="btn-ghost btn-sm" onClick={() => setEditingId(null)}>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ── Anzeige-Modus ── */
|
||||||
|
<>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 10, fontWeight: 700, color: 'var(--primary)',
|
fontSize: 10, fontWeight: 700, color: 'var(--primary)',
|
||||||
@@ -236,23 +300,25 @@ function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProp
|
|||||||
padding: '1px 6px', border: '1px solid color-mix(in srgb, var(--primary) 30%, transparent)',
|
padding: '1px 6px', border: '1px solid color-mix(in srgb, var(--primary) 30%, transparent)',
|
||||||
}}>L{pair.level}</span>
|
}}>L{pair.level}</span>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-2)' }}>{pair.status}</span>
|
<span style={{ fontSize: 10, color: 'var(--text-2)' }}>{pair.status}</span>
|
||||||
|
<div style={{ marginLeft: 'auto', display: 'flex', gap: 4 }}>
|
||||||
|
<button onClick={() => startEdit(pair)}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--text-2)', padding: '0 3px' }} title="Bearbeiten">✏️</button>
|
||||||
|
<button onClick={() => handleDelete(pair.id)}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--danger, #dc2626)', padding: '0 3px' }} title="Löschen">✕</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statements */}
|
|
||||||
{pair.statements.map(s => (
|
{pair.statements.map(s => (
|
||||||
<div key={s.id} style={{ fontSize: 12, color: 'var(--text-1)', marginBottom: 3, paddingLeft: 4 }}>
|
<div key={s.id} style={{ fontSize: 12, color: 'var(--text-1)', marginBottom: 3, paddingLeft: 4 }}>
|
||||||
<span style={{ fontSize: 10, color: 'var(--text-2)', marginRight: 4 }}>Aussage:</span>
|
<span style={{ fontSize: 10, color: 'var(--text-2)', marginRight: 4 }}>Aussage:</span>{s.statement_de}
|
||||||
{s.statement_de}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Questions */}
|
|
||||||
{pair.questions.map(q => (
|
{pair.questions.map(q => (
|
||||||
<div key={q.id} style={{ fontSize: 12, color: 'var(--text-2)', fontStyle: 'italic', paddingLeft: 4 }}>
|
<div key={q.id} style={{ fontSize: 12, color: 'var(--text-2)', fontStyle: 'italic', paddingLeft: 4 }}>
|
||||||
<span style={{ fontSize: 10, marginRight: 4, fontStyle: 'normal' }}>Frage:</span>
|
<span style={{ fontSize: 10, marginRight: 4, fontStyle: 'normal' }}>Frage:</span>{q.question_de}
|
||||||
{q.question_de}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user