feat: SMW-Importer Frontend (Phase 2 complete)
Phase 2A: API Functions - 5 MediaWiki import functions in api.js - previewMediaWikiImport, executeMediaWikiImport - getMediaWikiImportStatus, listMediaWikiImportLogs - deleteMediaWikiImportReference Phase 2B: UI Component - MediaWikiImportPage.jsx (3-tab interface) - Preview Tab: Category selection, preview with accordions - Execute Tab: Import form with status polling - History Tab: Import log list with refresh Phase 2C: Routing - Added /admin/mediawiki-import route in App.jsx - Import and ProtectedRoute wrapper Issue: SMW-Importer Frontend (Option C from handover) Related: backend/routers/csv_import.py (MediaWiki endpoints)
This commit is contained in:
parent
a67cc5f812
commit
46d000d6b3
|
|
@ -13,6 +13,7 @@ import TrainingPlanningPage from './pages/TrainingPlanningPage'
|
|||
import AdminCatalogsPage from './pages/AdminCatalogsPage'
|
||||
import AdminHierarchyPage from './pages/AdminHierarchyPage'
|
||||
import TrainerContextsPage from './pages/TrainerContextsPage'
|
||||
import MediaWikiImportPage from './pages/MediaWikiImportPage'
|
||||
import './app.css'
|
||||
|
||||
// Bottom Navigation (Mobile)
|
||||
|
|
@ -199,6 +200,14 @@ function AppRoutes() {
|
|||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/mediawiki-import"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<MediaWikiImportPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/trainer-contexts"
|
||||
element={
|
||||
|
|
|
|||
532
frontend/src/pages/MediaWikiImportPage.jsx
Normal file
532
frontend/src/pages/MediaWikiImportPage.jsx
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import api from '../utils/api'
|
||||
|
||||
export default function MediaWikiImportPage() {
|
||||
const [activeTab, setActiveTab] = useState('preview')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
// Preview Tab State
|
||||
const [previewCategory, setPreviewCategory] = useState('Übungen')
|
||||
const [previewType, setPreviewType] = useState('exercise')
|
||||
const [previewLimit, setPreviewLimit] = useState(10)
|
||||
const [previewData, setPreviewData] = useState(null)
|
||||
|
||||
// Execute Tab State
|
||||
const [executeCategory, setExecuteCategory] = useState('Übungen')
|
||||
const [executeType, setExecuteType] = useState('exercise')
|
||||
const [executeReimport, setExecuteReimport] = useState(false)
|
||||
const [executeDryRun, setExecuteDryRun] = useState(false)
|
||||
const [executeLimit, setExecuteLimit] = useState(null)
|
||||
const [currentImport, setCurrentImport] = useState(null)
|
||||
const [pollingInterval, setPollingInterval] = useState(null)
|
||||
|
||||
// History Tab State
|
||||
const [logs, setLogs] = useState([])
|
||||
|
||||
// Load logs on mount and tab switch
|
||||
useEffect(() => {
|
||||
if (activeTab === 'history') {
|
||||
loadLogs()
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
// Polling cleanup
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval)
|
||||
}
|
||||
}
|
||||
}, [pollingInterval])
|
||||
|
||||
const loadLogs = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const data = await api.listMediaWikiImportLogs()
|
||||
setLogs(data)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const data = await api.previewMediaWikiImport(previewCategory, previewType, previewLimit)
|
||||
setPreviewData(data)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExecute = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await api.executeMediaWikiImport({
|
||||
category: executeCategory,
|
||||
import_type: executeType,
|
||||
reimport_existing: executeReimport,
|
||||
dry_run: executeDryRun,
|
||||
limit: executeLimit || null
|
||||
})
|
||||
setCurrentImport(result)
|
||||
|
||||
// Start polling
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const status = await api.getMediaWikiImportStatus(result.log_id)
|
||||
setCurrentImport(status)
|
||||
|
||||
if (status.import_status === 'completed' || status.import_status === 'failed') {
|
||||
clearInterval(interval)
|
||||
setPollingInterval(null)
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Polling error:', err)
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
setPollingInterval(interval)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>MediaWiki Import (Semantic MediaWiki)</h1>
|
||||
<p style={{ color: 'var(--text2)', marginBottom: '24px' }}>
|
||||
Importiere Übungen, Fähigkeiten und Methoden aus karatetrainer.net
|
||||
</p>
|
||||
|
||||
{/* Tabs */}
|
||||
<div style={{ borderBottom: '2px solid var(--border)', marginBottom: '24px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{['preview', 'execute', 'history'].map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
background: activeTab === tab ? 'var(--accent)' : 'transparent',
|
||||
color: activeTab === tab ? 'white' : 'var(--text1)',
|
||||
border: 'none',
|
||||
borderBottom: activeTab === tab ? '2px solid var(--accent)' : '2px solid transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
fontWeight: activeTab === tab ? 'bold' : 'normal',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
{tab === 'preview' && '👁️ Vorschau'}
|
||||
{tab === 'execute' && '▶️ Ausführen'}
|
||||
{tab === 'history' && '📜 Historie'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
background: '#FEE',
|
||||
border: '1px solid #C00',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<strong>Fehler:</strong> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview Tab */}
|
||||
{activeTab === 'preview' && (
|
||||
<div>
|
||||
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
|
||||
<h2>Import-Vorschau</h2>
|
||||
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Kategorie
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={previewCategory}
|
||||
onChange={(e) => setPreviewCategory(e.target.value)}
|
||||
placeholder="Übungen"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Import-Typ
|
||||
</label>
|
||||
<select
|
||||
value={previewType}
|
||||
onChange={(e) => setPreviewType(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
>
|
||||
<option value="exercise">Übungen</option>
|
||||
<option value="skill">Fähigkeiten</option>
|
||||
<option value="method">Methoden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Limit
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={previewLimit}
|
||||
onChange={(e) => setPreviewLimit(parseInt(e.target.value) || 10)}
|
||||
min="1"
|
||||
max="100"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handlePreview}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: 'var(--accent)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? 'Lade...' : '👁️ Vorschau laden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview Results */}
|
||||
{previewData && (
|
||||
<div>
|
||||
<h3>
|
||||
{previewData.total_found} Einträge gefunden in Kategorie "{previewData.category}"
|
||||
</h3>
|
||||
<div style={{ marginTop: '16px', display: 'grid', gap: '12px' }}>
|
||||
{previewData.preview.map((item, idx) => (
|
||||
<details
|
||||
key={idx}
|
||||
style={{
|
||||
background: 'var(--surface)',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${item.errors.length > 0 ? '#C00' : item.warnings.length > 0 ? '#F90' : 'var(--border)'}`
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 'bold', fontSize: '16px' }}>
|
||||
{item.already_imported && '✅ '}
|
||||
{item.wiki_page_title}
|
||||
{item.errors.length > 0 && ' ❌'}
|
||||
{item.warnings.length > 0 && ' ⚠️'}
|
||||
</summary>
|
||||
<div style={{ marginTop: '12px', fontSize: '14px', color: 'var(--text2)' }}>
|
||||
{item.already_imported && (
|
||||
<p><strong>Bereits importiert:</strong> {new Date(item.last_imported_at).toLocaleString('de-DE')}</p>
|
||||
)}
|
||||
{item.warnings.length > 0 && (
|
||||
<div style={{ background: '#FFC', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
|
||||
<strong>Warnungen:</strong>
|
||||
<ul>{item.warnings.map((w, i) => <li key={i}>{w}</li>)}</ul>
|
||||
</div>
|
||||
)}
|
||||
{item.errors.length > 0 && (
|
||||
<div style={{ background: '#FEE', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
|
||||
<strong>Fehler:</strong>
|
||||
<ul>{item.errors.map((e, i) => <li key={i}>{e}</li>)}</ul>
|
||||
</div>
|
||||
)}
|
||||
<details style={{ marginTop: '8px' }}>
|
||||
<summary style={{ cursor: 'pointer', color: 'var(--accent)' }}>
|
||||
Gemappte Felder anzeigen
|
||||
</summary>
|
||||
<pre style={{
|
||||
background: 'var(--bg)',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto',
|
||||
fontSize: '12px',
|
||||
marginTop: '8px'
|
||||
}}>
|
||||
{JSON.stringify(item.mapped_fields, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Execute Tab */}
|
||||
{activeTab === 'execute' && (
|
||||
<div>
|
||||
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
|
||||
<h2>Import ausführen</h2>
|
||||
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Kategorie
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={executeCategory}
|
||||
onChange={(e) => setExecuteCategory(e.target.value)}
|
||||
placeholder="Übungen"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Import-Typ
|
||||
</label>
|
||||
<select
|
||||
value={executeType}
|
||||
onChange={(e) => setExecuteType(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
>
|
||||
<option value="exercise">Übungen</option>
|
||||
<option value="skill">Fähigkeiten</option>
|
||||
<option value="method">Methoden</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={executeReimport}
|
||||
onChange={(e) => setExecuteReimport(e.target.checked)}
|
||||
/>
|
||||
<span>Existierende überschreiben (Reimport)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={executeDryRun}
|
||||
onChange={(e) => setExecuteDryRun(e.target.checked)}
|
||||
/>
|
||||
<span>Dry-Run (nicht speichern)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
|
||||
Limit (optional)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={executeLimit || ''}
|
||||
onChange={(e) => setExecuteLimit(e.target.value ? parseInt(e.target.value) : null)}
|
||||
placeholder="Kein Limit"
|
||||
min="1"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
fontSize: '16px',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '16px',
|
||||
background: executeDryRun ? 'var(--accent-dark)' : 'var(--accent)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
{loading ? '⏳ Import läuft...' : executeDryRun ? '🧪 Dry-Run starten' : '▶️ Import starten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Status */}
|
||||
{currentImport && (
|
||||
<div style={{
|
||||
background: 'var(--surface)',
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
border: `2px solid ${
|
||||
currentImport.import_status === 'completed' ? '#0A0' :
|
||||
currentImport.import_status === 'failed' ? '#C00' :
|
||||
'var(--accent)'
|
||||
}`
|
||||
}}>
|
||||
<h3>
|
||||
{currentImport.import_status === 'running' && '⏳ Import läuft...'}
|
||||
{currentImport.import_status === 'completed' && '✅ Import abgeschlossen'}
|
||||
{currentImport.import_status === 'failed' && '❌ Import fehlgeschlagen'}
|
||||
</h3>
|
||||
<div style={{ marginTop: '16px', display: 'grid', gap: '8px', fontSize: '14px' }}>
|
||||
<div><strong>Log ID:</strong> {currentImport.id}</div>
|
||||
<div><strong>Kategorie:</strong> {currentImport.category}</div>
|
||||
<div><strong>Typ:</strong> {currentImport.import_type}</div>
|
||||
<div><strong>Total:</strong> {currentImport.items_total}</div>
|
||||
<div style={{ color: '#0A0' }}><strong>✅ Importiert:</strong> {currentImport.items_imported}</div>
|
||||
<div style={{ color: '#F90' }}><strong>⏭️ Übersprungen:</strong> {currentImport.items_skipped}</div>
|
||||
<div style={{ color: '#C00' }}><strong>❌ Fehlgeschlagen:</strong> {currentImport.items_failed}</div>
|
||||
{currentImport.started_at && (
|
||||
<div><strong>Gestartet:</strong> {new Date(currentImport.started_at).toLocaleString('de-DE')}</div>
|
||||
)}
|
||||
{currentImport.finished_at && (
|
||||
<div><strong>Beendet:</strong> {new Date(currentImport.finished_at).toLocaleString('de-DE')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentImport.error_log && currentImport.error_log.length > 0 && (
|
||||
<details style={{ marginTop: '16px' }}>
|
||||
<summary style={{ cursor: 'pointer', color: '#C00', fontWeight: 'bold' }}>
|
||||
❌ Fehler anzeigen ({currentImport.error_log.length})
|
||||
</summary>
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
background: '#FEE',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
{currentImport.error_log.map((err, idx) => (
|
||||
<div key={idx} style={{ marginBottom: '8px', paddingBottom: '8px', borderBottom: '1px solid #CCC' }}>
|
||||
<strong>{err.item}:</strong> {err.error}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* History Tab */}
|
||||
{activeTab === 'history' && (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h2>Import-Historie</h2>
|
||||
<button
|
||||
onClick={loadLogs}
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: 'var(--accent)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.6 : 1
|
||||
}}
|
||||
>
|
||||
🔄 Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{logs.length === 0 && !loading && (
|
||||
<p style={{ color: 'var(--text2)', textAlign: 'center', padding: '40px' }}>
|
||||
Noch keine Imports durchgeführt
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
{logs.map((log) => (
|
||||
<div
|
||||
key={log.id}
|
||||
style={{
|
||||
background: 'var(--surface)',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${
|
||||
log.import_status === 'completed' ? '#0A0' :
|
||||
log.import_status === 'failed' ? '#C00' :
|
||||
'var(--accent)'
|
||||
}`
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }}>
|
||||
{log.import_status === 'completed' && '✅ '}
|
||||
{log.import_status === 'failed' && '❌ '}
|
||||
{log.import_status === 'running' && '⏳ '}
|
||||
{log.category} ({log.import_type})
|
||||
{log.dry_run && ' [Dry-Run]'}
|
||||
</div>
|
||||
<div style={{ fontSize: '14px', color: 'var(--text2)', display: 'grid', gap: '4px' }}>
|
||||
<div>{new Date(log.started_at).toLocaleString('de-DE')}</div>
|
||||
<div>
|
||||
<span style={{ color: '#0A0' }}>✅ {log.items_imported}</span>
|
||||
{' | '}
|
||||
<span style={{ color: '#F90' }}>⏭️ {log.items_skipped}</span>
|
||||
{' | '}
|
||||
<span style={{ color: '#C00' }}>❌ {log.items_failed}</span>
|
||||
{' | '}
|
||||
<span>Total: {log.items_total}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -595,7 +595,26 @@ export const api = {
|
|||
|
||||
// System
|
||||
getVersion,
|
||||
healthCheck
|
||||
healthCheck,
|
||||
|
||||
// MediaWiki Import
|
||||
previewMediaWikiImport: (category, importType = 'exercise', limit = 10) =>
|
||||
request(`/api/import/mediawiki/preview?category=${encodeURIComponent(category)}&import_type=${importType}&limit=${limit}`),
|
||||
|
||||
executeMediaWikiImport: (data) =>
|
||||
request('/api/import/mediawiki/execute', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
}),
|
||||
|
||||
getMediaWikiImportStatus: (logId) =>
|
||||
request(`/api/import/mediawiki/status/${logId}`),
|
||||
|
||||
listMediaWikiImportLogs: () =>
|
||||
request('/api/import/mediawiki/logs'),
|
||||
|
||||
deleteMediaWikiImportReference: (refId) =>
|
||||
request(`/api/import/mediawiki/references/${refId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export default api
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user