Enhance MediaWiki import functionality with category normalization and skill attributes
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m17s
Test Suite / pytest-backend (pull_request) Successful in 35s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
- Introduced `_normalize_mw_category` function to clean category names for API calls, ensuring consistent handling of category prefixes. - Updated `SmwClient` methods to utilize normalized category names, improving data retrieval accuracy. - Added `_wiki_category_or_default` function to provide default categories based on import type, enhancing user experience during imports. - Integrated new fields `karate_relevance` and `relevance_level` into various admin components, allowing for better skill management. - Incremented app version to 0.8.145 and updated changelog to reflect these changes.
This commit is contained in:
parent
949a77fe38
commit
623af621b4
|
|
@ -30,6 +30,17 @@ CATEGORY_METHODS = os.getenv("MEDIAWIKI_CATEGORY_METHODS", "Methodenbeschrei
|
|||
CATEGORY_MODELS = os.getenv("MEDIAWIKI_CATEGORY_MODELS", "Reifegradmodelle")
|
||||
|
||||
|
||||
def _wiki_category_or_default(category: Optional[str], import_type: str) -> str:
|
||||
"""Leeres category ⇒ Standard je import_type."""
|
||||
if (category or "").strip():
|
||||
return category.strip()
|
||||
if import_type == "skill":
|
||||
return CATEGORY_SKILLS
|
||||
if import_type == "method":
|
||||
return CATEGORY_METHODS
|
||||
return CATEGORY_EXERCISES
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Pydantic Models #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
|
@ -59,7 +70,7 @@ def require_admin(session: dict = Depends(require_auth)) -> dict:
|
|||
|
||||
@router.get("/import/mediawiki/preview")
|
||||
async def preview_import(
|
||||
category: str = Query(default=CATEGORY_EXERCISES),
|
||||
category: Optional[str] = Query(default=None),
|
||||
import_type: str = Query(default="exercise"),
|
||||
limit: int = Query(default=10, ge=1, le=500),
|
||||
session: dict = Depends(require_admin),
|
||||
|
|
@ -68,9 +79,10 @@ async def preview_import(
|
|||
Zeigt Vorschau: Welche Seiten würden importiert werden?
|
||||
Überprüft Duplikate und mapped Felder ohne zu speichern.
|
||||
"""
|
||||
resolved_category = _wiki_category_or_default(category, import_type)
|
||||
client = SmwClient()
|
||||
try:
|
||||
members = await client.get_category_members(category, limit=limit)
|
||||
members = await client.get_category_members(resolved_category, limit=limit)
|
||||
except SmwClientError as e:
|
||||
raise HTTPException(status_code=502, detail=f"Wiki-API nicht erreichbar: {e}")
|
||||
|
||||
|
|
@ -129,7 +141,7 @@ async def preview_import(
|
|||
})
|
||||
|
||||
return {
|
||||
"category": category,
|
||||
"category": resolved_category,
|
||||
"import_type": import_type,
|
||||
"total_found": len(members),
|
||||
"preview": preview,
|
||||
|
|
@ -151,6 +163,7 @@ async def execute_import(
|
|||
Gibt sofort log_id zurück – Status via GET /import/mediawiki/status/{log_id}.
|
||||
"""
|
||||
profile_id = session["profile_id"]
|
||||
resolved_category = _wiki_category_or_default(body.category, body.import_type)
|
||||
|
||||
# Log-Eintrag anlegen
|
||||
with get_db() as conn:
|
||||
|
|
@ -160,7 +173,7 @@ async def execute_import(
|
|||
(import_type, import_status, category, dry_run, reimport, imported_by)
|
||||
VALUES (%s, 'running', %s, %s, %s, %s)
|
||||
RETURNING id""",
|
||||
(body.import_type, body.category, body.dry_run, body.reimport_existing, profile_id)
|
||||
(body.import_type, resolved_category, body.dry_run, body.reimport_existing, profile_id)
|
||||
)
|
||||
log_id = cur.fetchone()['id']
|
||||
conn.commit()
|
||||
|
|
@ -169,7 +182,7 @@ async def execute_import(
|
|||
background_tasks.add_task(
|
||||
_run_import,
|
||||
log_id=log_id,
|
||||
category=body.category,
|
||||
category=resolved_category,
|
||||
import_type=body.import_type,
|
||||
reimport=body.reimport_existing,
|
||||
dry_run=body.dry_run,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,26 @@ class SmwClientError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
def _normalize_mw_category(category: str) -> str:
|
||||
"""
|
||||
Bereinigt Kategorienamen für API cmtitle=Kategorie:…
|
||||
Erlaubt z. B. 'Fähigkeitsbeschreibung', ' Kategorie:X ', 'kategorie:X' ohne Doppel-Prefix.
|
||||
"""
|
||||
c = (category or "").strip()
|
||||
if not c:
|
||||
raise SmwClientError("Kategorie (Seitenlisten-Name ohne Präfix) darf nicht leer sein")
|
||||
|
||||
pref = "kategorie:"
|
||||
while c.lower().startswith(pref):
|
||||
c = c[len(pref) :].lstrip()
|
||||
|
||||
remaining = (c or "").strip()
|
||||
if not remaining:
|
||||
raise SmwClientError("Kategorie (Seitenlisten-Name ohne Präfix) darf nicht leer sein")
|
||||
|
||||
return remaining
|
||||
|
||||
|
||||
class SmwClient:
|
||||
"""Stateless MediaWiki/SMW API Client mit Session-Login."""
|
||||
|
||||
|
|
@ -110,12 +130,13 @@ class SmwClient:
|
|||
"""
|
||||
members = []
|
||||
cmcontinue = None
|
||||
cat = _normalize_mw_category(category)
|
||||
|
||||
while True:
|
||||
params = {
|
||||
"action": "query",
|
||||
"list": "categorymembers",
|
||||
"cmtitle": f"Kategorie:{category}",
|
||||
"cmtitle": f"Kategorie:{cat}",
|
||||
"cmlimit": min(limit, 500),
|
||||
"cmtype": "page", # Nur Seiten, keine Unterkategorien
|
||||
"cmprop": "ids|title",
|
||||
|
|
@ -134,8 +155,8 @@ class SmwClient:
|
|||
|
||||
# Rekursiv durch Unterkategorien gehen
|
||||
if recursive:
|
||||
subcats = await self._get_subcategories(category)
|
||||
logger.info(f"Kategorie '{category}': {len(members)} direkte Seiten, {len(subcats)} Unterkategorien")
|
||||
subcats = await self._get_subcategories(cat)
|
||||
logger.info(f"Kategorie '{cat}': {len(members)} direkte Seiten, {len(subcats)} Unterkategorien")
|
||||
|
||||
for subcat in subcats:
|
||||
if len(members) >= limit:
|
||||
|
|
@ -150,12 +171,13 @@ class SmwClient:
|
|||
"""Gibt alle Unterkategorien einer Kategorie zurück."""
|
||||
subcats = []
|
||||
cmcontinue = None
|
||||
cat = _normalize_mw_category(category)
|
||||
|
||||
while True:
|
||||
params = {
|
||||
"action": "query",
|
||||
"list": "categorymembers",
|
||||
"cmtitle": f"Kategorie:{category}",
|
||||
"cmtitle": f"Kategorie:{cat}",
|
||||
"cmlimit": 500,
|
||||
"cmtype": "subcat", # Nur Unterkategorien
|
||||
"cmprop": "ids|title",
|
||||
|
|
|
|||
|
|
@ -408,9 +408,9 @@ def map_wiki_to_skill(
|
|||
first_value = values[0] if isinstance(values, list) else values
|
||||
|
||||
if target == "description":
|
||||
description_text = wikitext_to_plaintext(first_value)
|
||||
description_text = wikitext_to_plaintext(str(first_value))
|
||||
elif target == "karate_relevance":
|
||||
mapped["karate_relevance"] = wikitext_to_plaintext(first_value)
|
||||
mapped["karate_relevance"] = wikitext_to_plaintext(str(first_value))
|
||||
elif target == "relevance_level":
|
||||
parsed = parse_wiki_relevance_level(first_value if isinstance(first_value, str) else str(first_value))
|
||||
if parsed is None:
|
||||
|
|
|
|||
18
backend/tests/test_import_wiki_category_default.py
Normal file
18
backend/tests/test_import_wiki_category_default.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
"""Resolver für leere/fehlende Kategorie je import_type."""
|
||||
|
||||
from routers.import_wiki import (
|
||||
CATEGORY_EXERCISES,
|
||||
CATEGORY_METHODS,
|
||||
CATEGORY_SKILLS,
|
||||
_wiki_category_or_default,
|
||||
)
|
||||
|
||||
|
||||
def test_wiki_category_or_default_explicit():
|
||||
assert _wiki_category_or_default(" MeineKat ", "skill") == "MeineKat"
|
||||
|
||||
|
||||
def test_wiki_category_or_default_falls_through():
|
||||
assert _wiki_category_or_default(None, "exercise") == CATEGORY_EXERCISES
|
||||
assert _wiki_category_or_default("", "skill") == CATEGORY_SKILLS
|
||||
assert _wiki_category_or_default(" ", "method") == CATEGORY_METHODS
|
||||
21
backend/tests/test_smw_client_category_normalize.py
Normal file
21
backend/tests/test_smw_client_category_normalize.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import pytest
|
||||
|
||||
from smw_client import _normalize_mw_category, SmwClientError
|
||||
|
||||
|
||||
def test_normalize_strips_optional_kategorie_prefix():
|
||||
assert _normalize_mw_category("Fähigkeitsbeschreibung") == "Fähigkeitsbeschreibung"
|
||||
assert _normalize_mw_category(" Kategorie:Fähigkeitsbeschreibung ") == "Fähigkeitsbeschreibung"
|
||||
assert _normalize_mw_category("kategorie:test") == "test"
|
||||
|
||||
|
||||
def test_normalize_strips_duplicate_prefix_chain():
|
||||
assert _normalize_mw_category("Kategorie:Kategorie:Foo") == "Foo"
|
||||
|
||||
|
||||
def test_normalize_rejects_empty():
|
||||
with pytest.raises(SmwClientError, match="leer"):
|
||||
_normalize_mw_category("")
|
||||
with pytest.raises(SmwClientError, match="leer"):
|
||||
_normalize_mw_category(" Kategorie: ")
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.143"
|
||||
APP_VERSION = "0.8.145"
|
||||
BUILD_DATE = "2026-05-16"
|
||||
DB_SCHEMA_VERSION = "20260516065"
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ MODULE_VERSIONS = {
|
|||
"planning": "0.13.0", # Vorlagen/Framework/Module/Graphs: RBAC wie Übungen (edit/delete/governance transition); Planungs-UI Sichtbarkeit neue Vorlage
|
||||
"dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine)
|
||||
"training_modules": "1.1.0", # PUT/DELETE: assert_library_content_* (Vereinsadmin löscht Vereins-Inhalt, Trainer bearbeitet club wie Übungen)
|
||||
"import_wiki": "1.0.1", # Skills: KarateRelevanz + RelevanzLevel → DB-Spalten
|
||||
"import_wiki": "1.0.3", # Default-Kategorie Fähigkeiten: Fähigkeitsbeschreibung; cmtitle-Normalisierung; UI Preview/Execute Defaults je Typ
|
||||
"admin": "1.0.0",
|
||||
"membership": "1.0.0",
|
||||
"catalogs": "1.5.0", # Updated: Trainer Contexts API (Migration 012)
|
||||
|
|
@ -36,6 +36,21 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.145",
|
||||
"date": "2026-05-16",
|
||||
"changes": [
|
||||
"Wiki-Import: Standard-Kategorie Fähigkeiten korrigiert auf „Fähigkeitsbeschreibung“ (korrekter Wiki-Name).",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.144",
|
||||
"date": "2026-05-16",
|
||||
"changes": [
|
||||
"Wiki-Import: Default-Kategorie Fähigkeiten; SMW-Client entfernt versehentliches „Kategorie:“-Prefix; Vorschau/Ausführung nutzen Default-Kategorie je import_type wenn leer.",
|
||||
"Admin UI Wiki-Import: Wechsel Import-Typ setzt passende Standard-Kategorie.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.143",
|
||||
"date": "2026-05-16",
|
||||
|
|
|
|||
|
|
@ -3342,6 +3342,12 @@ a.analysis-split__nav-item {
|
|||
.admin-matrix-skill-list__name {
|
||||
font-size: 15px;
|
||||
}
|
||||
.admin-matrix-skill-list__buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.admin-matrix-skill-list__path {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
|
|
@ -3388,6 +3394,16 @@ a.analysis-split__nav-item {
|
|||
margin-top: 6px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.admin-matrix-matrix__skill-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.admin-matrix-matrix__skill-head .btn-tiny {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.admin-matrix-matrix__cell {
|
||||
vertical-align: top;
|
||||
padding: 6px;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ export default function MaturityModelsAdminPanel() {
|
|||
const [levelsForm, setLevelsForm] = useState([])
|
||||
const [cellDraft, setCellDraft] = useState({})
|
||||
const [skillToAdd, setSkillToAdd] = useState('')
|
||||
/** Modal: Stammdaten Fähigkeit (Wiki-Felder) aus Matrix-Kontext */
|
||||
const [skillWikiModal, setSkillWikiModal] = useState(null)
|
||||
const [skillWikiForm, setSkillWikiForm] = useState({
|
||||
karate_relevance: '',
|
||||
relevance_level: ''
|
||||
})
|
||||
const [skillWikiLoading, setSkillWikiLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
|
@ -219,6 +226,57 @@ export default function MaturityModelsAdminPanel() {
|
|||
}
|
||||
}
|
||||
|
||||
async function openSkillWikiModal(skillId, skillName) {
|
||||
setSkillWikiModal({ skill_id: skillId, skill_name: skillName })
|
||||
setSkillWikiLoading(true)
|
||||
setSkillWikiForm({ karate_relevance: '', relevance_level: '' })
|
||||
try {
|
||||
const s = await api.getSkill(skillId)
|
||||
setSkillWikiForm({
|
||||
karate_relevance: s.karate_relevance || '',
|
||||
relevance_level:
|
||||
s.relevance_level != null && s.relevance_level !== ''
|
||||
? String(s.relevance_level)
|
||||
: ''
|
||||
})
|
||||
} catch (e) {
|
||||
setError(e.message || String(e))
|
||||
setSkillWikiModal(null)
|
||||
} finally {
|
||||
setSkillWikiLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function closeSkillWikiModal() {
|
||||
setSkillWikiModal(null)
|
||||
}
|
||||
|
||||
async function handleSaveSkillWikiFields(e) {
|
||||
e.preventDefault()
|
||||
if (!skillWikiModal) return
|
||||
setError('')
|
||||
setSkillWikiLoading(true)
|
||||
try {
|
||||
await api.updateSkill(skillWikiModal.skill_id, {
|
||||
karate_relevance:
|
||||
skillWikiForm.karate_relevance && skillWikiForm.karate_relevance.trim()
|
||||
? skillWikiForm.karate_relevance.trim()
|
||||
: null,
|
||||
relevance_level:
|
||||
skillWikiForm.relevance_level === '' || skillWikiForm.relevance_level == null
|
||||
? null
|
||||
: Number(skillWikiForm.relevance_level)
|
||||
})
|
||||
const sk = await api.listSkills({ status: 'active' })
|
||||
setAllSkills(sk)
|
||||
closeSkillWikiModal()
|
||||
} catch (e) {
|
||||
setError(e.message || String(e))
|
||||
} finally {
|
||||
setSkillWikiLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteModel() {
|
||||
if (!selectedId || !isSuperadmin) return
|
||||
if (!confirm('Reifegradmodell dauerhaft löschen?')) return
|
||||
|
|
@ -552,6 +610,16 @@ export default function MaturityModelsAdminPanel() {
|
|||
<li key={ms.skill_id} className="admin-matrix-skill-list__item">
|
||||
<div className="admin-matrix-skill-list__row">
|
||||
<strong className="admin-matrix-skill-list__name">{ms.skill_name}</strong>
|
||||
<span className="admin-matrix-skill-list__buttons">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-small"
|
||||
disabled={saving || skillWikiLoading}
|
||||
title="Karate-Relevanz und Relevanzgrad bearbeiten"
|
||||
onClick={() => openSkillWikiModal(ms.skill_id, ms.skill_name)}
|
||||
>
|
||||
Wiki-Felder
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-small"
|
||||
|
|
@ -559,6 +627,7 @@ export default function MaturityModelsAdminPanel() {
|
|||
>
|
||||
Entfernen
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||
<div className="admin-matrix-skill-list__path muted">
|
||||
|
|
@ -595,6 +664,8 @@ export default function MaturityModelsAdminPanel() {
|
|||
{(detail.model_skills || []).map((ms) => (
|
||||
<tr key={ms.skill_id}>
|
||||
<td className="admin-matrix-matrix__skill-cell">
|
||||
<div className="admin-matrix-matrix__skill-head">
|
||||
<div>
|
||||
<div>{ms.skill_name}</div>
|
||||
{(ms.skill_main_category_name || ms.skill_subcategory_name) ? (
|
||||
<div className="admin-matrix-matrix__skill-path muted">
|
||||
|
|
@ -602,6 +673,17 @@ export default function MaturityModelsAdminPanel() {
|
|||
{ms.skill_subcategory_name ? ` › ${ms.skill_subcategory_name}` : ''}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-tiny"
|
||||
title="Karate-Relevanz und Relevanzgrad bearbeiten"
|
||||
disabled={saving || skillWikiLoading}
|
||||
onClick={() => openSkillWikiModal(ms.skill_id, ms.skill_name)}
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{(detail.levels || []).map((l) => {
|
||||
const key = `${ms.skill_id}-${l.level_number}`
|
||||
|
|
@ -643,6 +725,90 @@ export default function MaturityModelsAdminPanel() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skillWikiModal ? (
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
role="presentation"
|
||||
onClick={closeSkillWikiModal}
|
||||
>
|
||||
<div
|
||||
className="admin-modal-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="matrix-skill-wiki-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="admin-modal-sheet__header">
|
||||
<h3 id="matrix-skill-wiki-title" className="admin-modal-sheet__title">
|
||||
Fähigkeit: {skillWikiModal.skill_name}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary admin-modal-sheet__close"
|
||||
disabled={skillWikiLoading}
|
||||
onClick={closeSkillWikiModal}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-sheet__body">
|
||||
{skillWikiLoading ? (
|
||||
<p className="muted">Lade Daten…</p>
|
||||
) : (
|
||||
<form className="skills-catalog-form" onSubmit={handleSaveSkillWikiFields}>
|
||||
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={4}
|
||||
value={skillWikiForm.karate_relevance}
|
||||
onChange={(e) =>
|
||||
setSkillWikiForm((f) => ({ ...f, karate_relevance: e.target.value }))
|
||||
}
|
||||
disabled={skillWikiLoading}
|
||||
placeholder="Freitext (Wiki-Eigenschaft KarateRelevanz)"
|
||||
/>
|
||||
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={
|
||||
skillWikiForm.relevance_level === ''
|
||||
? ''
|
||||
: String(skillWikiForm.relevance_level)
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSkillWikiForm((f) => ({ ...f, relevance_level: e.target.value }))
|
||||
}
|
||||
disabled={skillWikiLoading}
|
||||
>
|
||||
<option value="">– nicht gesetzt –</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
<p className="muted admin-matrix-hint">
|
||||
Änderungen gelten für die Fähigkeit im gesamten Katalog (gemeinsame Stammdaten).
|
||||
</p>
|
||||
<div className="skills-catalog-form__actions">
|
||||
<button type="submit" className="btn btn-primary" disabled={skillWikiLoading}>
|
||||
Speichern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={skillWikiLoading}
|
||||
onClick={closeSkillWikiModal}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ export default function SkillsCatalogAdmin() {
|
|||
keywords: '',
|
||||
status: 'active',
|
||||
sort_order: '',
|
||||
category_id: ''
|
||||
category_id: '',
|
||||
karate_relevance: '',
|
||||
relevance_level: ''
|
||||
})
|
||||
|
||||
const [newMainName, setNewMainName] = useState('')
|
||||
|
|
@ -176,7 +178,12 @@ export default function SkillsCatalogAdmin() {
|
|||
keywords: s.keywords || '',
|
||||
status: s.status || 'active',
|
||||
sort_order: s.sort_order ?? '',
|
||||
category_id: s.category_id ?? ''
|
||||
category_id: s.category_id ?? '',
|
||||
karate_relevance: s.karate_relevance || '',
|
||||
relevance_level:
|
||||
s.relevance_level != null && s.relevance_level !== ''
|
||||
? String(s.relevance_level)
|
||||
: ''
|
||||
})
|
||||
setEditDialog({ type: 'skill', id: s.id })
|
||||
}
|
||||
|
|
@ -304,7 +311,15 @@ export default function SkillsCatalogAdmin() {
|
|||
skillForm.sort_order === '' || skillForm.sort_order == null
|
||||
? null
|
||||
: Number(skillForm.sort_order),
|
||||
category_id: cid
|
||||
category_id: cid,
|
||||
karate_relevance:
|
||||
typeof skillForm.karate_relevance === 'string' && skillForm.karate_relevance.trim()
|
||||
? skillForm.karate_relevance.trim()
|
||||
: null,
|
||||
relevance_level:
|
||||
skillForm.relevance_level === '' || skillForm.relevance_level == null
|
||||
? null
|
||||
: Number(skillForm.relevance_level)
|
||||
})
|
||||
})
|
||||
if (ok) {
|
||||
|
|
@ -922,6 +937,31 @@ export default function SkillsCatalogAdmin() {
|
|||
onChange={(e) => setSkillForm((f) => ({ ...f, description: e.target.value }))}
|
||||
disabled={busy}
|
||||
/>
|
||||
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={3}
|
||||
value={skillForm.karate_relevance}
|
||||
onChange={(e) =>
|
||||
setSkillForm((f) => ({ ...f, karate_relevance: e.target.value }))
|
||||
}
|
||||
disabled={busy}
|
||||
placeholder="Freitext aus Wiki / eigene Erläuterung"
|
||||
/>
|
||||
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={skillForm.relevance_level === '' ? '' : String(skillForm.relevance_level)}
|
||||
onChange={(e) =>
|
||||
setSkillForm((f) => ({ ...f, relevance_level: e.target.value }))
|
||||
}
|
||||
disabled={busy}
|
||||
>
|
||||
<option value="">– nicht gesetzt –</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
<label className="form-label">Sortierung (optional)</label>
|
||||
<input
|
||||
className="form-input"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ const WIKI_IMPORT_TABS = [
|
|||
{ id: 'history', label: 'Historie', icon: History },
|
||||
]
|
||||
|
||||
/** MediaWiki-Kategorienamen ohne Präfix „Kategorie:“ — karatetrainer.net aktueller Stand */
|
||||
function defaultWikiCategory(importType) {
|
||||
if (importType === 'skill') return 'Fähigkeitsbeschreibung'
|
||||
if (importType === 'method') return 'Methodenbeschreibung'
|
||||
return 'Übungen'
|
||||
}
|
||||
|
||||
export default function MediaWikiImportPage() {
|
||||
const [activeTab, setActiveTab] = useState('preview')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
|
@ -169,7 +176,11 @@ export default function MediaWikiImportPage() {
|
|||
</label>
|
||||
<select
|
||||
value={previewType}
|
||||
onChange={(e) => setPreviewType(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const t = e.target.value
|
||||
setPreviewType(t)
|
||||
setPreviewCategory(defaultWikiCategory(t))
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
|
|
@ -315,7 +326,11 @@ export default function MediaWikiImportPage() {
|
|||
</label>
|
||||
<select
|
||||
value={executeType}
|
||||
onChange={(e) => setExecuteType(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const t = e.target.value
|
||||
setExecuteType(t)
|
||||
setExecuteCategory(defaultWikiCategory(t))
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ function SkillsPage() {
|
|||
description: '',
|
||||
importance: 3,
|
||||
keywords: [],
|
||||
status: 'active'
|
||||
status: 'active',
|
||||
karate_relevance: '',
|
||||
relevance_level: ''
|
||||
})
|
||||
} else {
|
||||
setFormData({
|
||||
|
|
@ -102,10 +104,20 @@ function SkillsPage() {
|
|||
|
||||
try {
|
||||
if (modalType === 'skill') {
|
||||
const raw = { ...formData }
|
||||
raw.karate_relevance =
|
||||
typeof raw.karate_relevance === 'string' && raw.karate_relevance.trim()
|
||||
? raw.karate_relevance.trim()
|
||||
: null
|
||||
raw.relevance_level =
|
||||
raw.relevance_level === '' || raw.relevance_level == null || raw.relevance_level === undefined
|
||||
? null
|
||||
: Number(raw.relevance_level)
|
||||
|
||||
if (editing) {
|
||||
await api.updateSkill(editing.id, formData)
|
||||
await api.updateSkill(editing.id, raw)
|
||||
} else {
|
||||
await api.createSkill(formData)
|
||||
await api.createSkill(raw)
|
||||
}
|
||||
} else {
|
||||
if (editing) {
|
||||
|
|
@ -394,6 +406,41 @@ function SkillsPage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{modalType === 'skill' && (
|
||||
<>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Karate-Relevanz (Wiki)</label>
|
||||
<textarea
|
||||
className="form-input"
|
||||
rows={3}
|
||||
value={formData.karate_relevance || ''}
|
||||
onChange={(e) => updateFormField('karate_relevance', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Relevanzgrad (Wiki, 1–3)</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={
|
||||
formData.relevance_level === null ||
|
||||
formData.relevance_level === undefined ||
|
||||
formData.relevance_level === ''
|
||||
? ''
|
||||
: String(formData.relevance_level)
|
||||
}
|
||||
onChange={(e) =>
|
||||
updateFormField('relevance_level', e.target.value === '' ? '' : e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">– nicht gesetzt –</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{modalType === 'skill' && (
|
||||
<div className="form-row">
|
||||
<label className="form-label">Wichtigkeit (1-5)</label>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user