chore: Änderungen commited
This commit is contained in:
132
app.py
132
app.py
@@ -2403,6 +2403,138 @@ def search_mindmap_nodes():
|
|||||||
'results': []
|
'results': []
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
# Export/Import-Funktionen für Mindmaps
|
||||||
|
@app.route('/api/mindmap/<int:mindmap_id>/export', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def export_mindmap(mindmap_id):
|
||||||
|
"""
|
||||||
|
Exportiert eine Mindmap im angegebenen Format.
|
||||||
|
|
||||||
|
Query-Parameter:
|
||||||
|
- format: Format der Exportdatei (json, xml, csv)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Sicherheitscheck: Nur eigene Mindmaps oder Mindmaps, auf die der Benutzer Zugriff hat
|
||||||
|
mindmap = UserMindmap.query.get_or_404(mindmap_id)
|
||||||
|
|
||||||
|
# Prüfen, ob der Benutzer Zugriff auf diese Mindmap hat
|
||||||
|
can_access = mindmap.user_id == current_user.id
|
||||||
|
|
||||||
|
if not can_access and mindmap.is_private:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Keine Berechtigung für den Zugriff auf diese Mindmap'
|
||||||
|
}), 403
|
||||||
|
|
||||||
|
# Format aus Query-Parameter holen
|
||||||
|
export_format = request.args.get('format', 'json')
|
||||||
|
|
||||||
|
# Alle Knoten und ihre Positionen in dieser Mindmap holen
|
||||||
|
nodes_data = db.session.query(
|
||||||
|
MindMapNode, UserMindmapNode
|
||||||
|
).join(
|
||||||
|
UserMindmapNode, UserMindmapNode.node_id == MindMapNode.id
|
||||||
|
).filter(
|
||||||
|
UserMindmapNode.user_mindmap_id == mindmap_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
# Beziehungen zwischen Knoten holen
|
||||||
|
relationships = []
|
||||||
|
for node1, user_node1 in nodes_data:
|
||||||
|
for node2, user_node2 in nodes_data:
|
||||||
|
if node1.id != node2.id and node2 in node1.children:
|
||||||
|
relationships.append({
|
||||||
|
'source': node1.id,
|
||||||
|
'target': node2.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Exportdaten vorbereiten
|
||||||
|
export_data = {
|
||||||
|
'mindmap': {
|
||||||
|
'id': mindmap.id,
|
||||||
|
'name': mindmap.name,
|
||||||
|
'description': mindmap.description,
|
||||||
|
'created_at': mindmap.created_at.isoformat(),
|
||||||
|
'last_modified': mindmap.last_modified.isoformat()
|
||||||
|
},
|
||||||
|
'nodes': [{
|
||||||
|
'id': node.id,
|
||||||
|
'name': node.name,
|
||||||
|
'description': node.description or '',
|
||||||
|
'color_code': node.color_code or '#9F7AEA',
|
||||||
|
'x_position': user_node.x_position,
|
||||||
|
'y_position': user_node.y_position,
|
||||||
|
'scale': user_node.scale or 1.0
|
||||||
|
} for node, user_node in nodes_data],
|
||||||
|
'relationships': relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exportieren im angeforderten Format
|
||||||
|
if export_format == 'json':
|
||||||
|
response = app.response_class(
|
||||||
|
response=json.dumps(export_data, indent=2),
|
||||||
|
status=200,
|
||||||
|
mimetype='application/json'
|
||||||
|
)
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.json"
|
||||||
|
return response
|
||||||
|
|
||||||
|
elif export_format == 'xml':
|
||||||
|
import dicttoxml
|
||||||
|
xml_data = dicttoxml.dicttoxml(export_data)
|
||||||
|
response = app.response_class(
|
||||||
|
response=xml_data,
|
||||||
|
status=200,
|
||||||
|
mimetype='application/xml'
|
||||||
|
)
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}.xml"
|
||||||
|
return response
|
||||||
|
|
||||||
|
elif export_format == 'csv':
|
||||||
|
import io
|
||||||
|
import csv
|
||||||
|
|
||||||
|
# CSV kann nicht die gesamte Struktur darstellen, daher nur die Knotenliste
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
|
||||||
|
# Schreibe Header
|
||||||
|
writer.writerow(['id', 'name', 'description', 'color_code', 'x_position', 'y_position', 'scale'])
|
||||||
|
|
||||||
|
# Schreibe Knotendaten
|
||||||
|
for node, user_node in nodes_data:
|
||||||
|
writer.writerow([
|
||||||
|
node.id,
|
||||||
|
node.name,
|
||||||
|
node.description or '',
|
||||||
|
node.color_code or '#9F7AEA',
|
||||||
|
user_node.x_position,
|
||||||
|
user_node.y_position,
|
||||||
|
user_node.scale or 1.0
|
||||||
|
])
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
response = app.response_class(
|
||||||
|
response=output.getvalue(),
|
||||||
|
status=200,
|
||||||
|
mimetype='text/csv'
|
||||||
|
)
|
||||||
|
response.headers["Content-Disposition"] = f"attachment; filename=mindmap_{mindmap_id}_nodes.csv"
|
||||||
|
return response
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Nicht unterstütztes Format: {export_format}'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Exportieren der Mindmap: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Fehler beim Exportieren: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
|
# Automatische Datenbankinitialisierung - Aktualisiert für Flask 2.2+ Kompatibilität
|
||||||
def initialize_app():
|
def initialize_app():
|
||||||
"""Initialisierung der Anwendung"""
|
"""Initialisierung der Anwendung"""
|
||||||
|
|||||||
@@ -435,6 +435,19 @@
|
|||||||
<div id="public-cy" style="width: 100%; height: 250px;"></div>
|
<div id="public-cy" style="width: 100%; height: 250px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Suchergebnisse Container -->
|
||||||
|
<div id="search-results-container" class="search-results-container p-4">
|
||||||
|
<div class="flex justify-between items-center mb-3">
|
||||||
|
<h3 class="text-lg font-semibold">Suchergebnisse</h3>
|
||||||
|
<button id="close-search" class="text-sm opacity-70 hover:opacity-100">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="search-results-list" class="space-y-1">
|
||||||
|
<!-- Suchergebnisse werden hier dynamisch hinzugefügt -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Informationsanzeige für ausgewählten Knoten -->
|
<!-- Informationsanzeige für ausgewählten Knoten -->
|
||||||
<div id="node-info-panel" class="node-info-panel p-4">
|
<div id="node-info-panel" class="node-info-panel p-4">
|
||||||
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
|
<h3 class="text-xl font-bold gradient-text mb-2">Knotendetails</h3>
|
||||||
@@ -1051,6 +1064,272 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suchfunktionalität implementieren
|
||||||
|
const searchInput = document.getElementById('mindmap-search');
|
||||||
|
const searchBtn = document.getElementById('search-btn');
|
||||||
|
const searchResultsContainer = document.getElementById('search-results-container');
|
||||||
|
const searchResultsList = document.getElementById('search-results-list');
|
||||||
|
const closeSearchBtn = document.getElementById('close-search');
|
||||||
|
|
||||||
|
// Funktion zum Schließen der Suchergebnisse
|
||||||
|
function closeSearchResults() {
|
||||||
|
searchResultsContainer.classList.remove('visible');
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-Listener für die Suche
|
||||||
|
searchBtn.addEventListener('click', performSearch);
|
||||||
|
searchInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeSearchBtn.addEventListener('click', closeSearchResults);
|
||||||
|
|
||||||
|
// Funktion für die Suche
|
||||||
|
function performSearch() {
|
||||||
|
const query = searchInput.value.trim();
|
||||||
|
if (!query) {
|
||||||
|
showUINotification('Bitte geben Sie einen Suchbegriff ein.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lade-Animation im Suchergebnisbereich anzeigen
|
||||||
|
searchResultsList.innerHTML = '<div class="p-4 text-center"><i class="fas fa-spinner fa-spin mr-2"></i> Suche läuft...</div>';
|
||||||
|
searchResultsContainer.classList.add('visible');
|
||||||
|
|
||||||
|
// API-Anfrage für die Suche
|
||||||
|
fetch(`/api/search/mindmap?q=${encodeURIComponent(query)}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
displaySearchResults(data.results, query);
|
||||||
|
} else {
|
||||||
|
searchResultsList.innerHTML = `<div class="p-4 text-center text-red-500">Fehler: ${data.message}</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler bei der Suche:', error);
|
||||||
|
searchResultsList.innerHTML = '<div class="p-4 text-center text-red-500">Ein Fehler ist aufgetreten.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funktion zum Anzeigen der Suchergebnisse
|
||||||
|
function displaySearchResults(results, query) {
|
||||||
|
searchResultsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
searchResultsList.innerHTML = '<div class="p-4 text-center">Keine Ergebnisse gefunden.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hervorheben-Funktion für den Suchbegriff
|
||||||
|
function highlightText(text, query) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
|
||||||
|
return text.replace(regex, '<span class="bg-yellow-200 dark:bg-yellow-800">$1</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ergebnisse anzeigen
|
||||||
|
results.forEach(result => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'search-result-item';
|
||||||
|
|
||||||
|
// Inhalte mit hervorgehobenem Suchbegriff
|
||||||
|
const highlightedName = highlightText(result.name, query);
|
||||||
|
const highlightedDesc = highlightText(result.description, query);
|
||||||
|
|
||||||
|
item.innerHTML = `
|
||||||
|
<div class="search-result-title">
|
||||||
|
<span class="search-result-color" style="background-color: ${result.color_code}"></span>
|
||||||
|
<span>${highlightedName}</span>
|
||||||
|
</div>
|
||||||
|
<div class="search-result-desc">${highlightedDesc}</div>
|
||||||
|
<div class="search-result-source">
|
||||||
|
<i class="fas fa-map-marked-alt mr-1"></i> ${result.mindmap_name}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Event-Listener zum Springen zum Knoten
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
if (result.source === 'user_mindmap') {
|
||||||
|
// Knoten ist bereits in der aktuellen Mindmap
|
||||||
|
const node = cy.$id(result.id);
|
||||||
|
if (node.length > 0) {
|
||||||
|
// Knoten auswählen und zentrieren
|
||||||
|
node.select();
|
||||||
|
cy.animate({
|
||||||
|
center: { eles: node },
|
||||||
|
zoom: 1.5,
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aktualisiere das Info-Panel
|
||||||
|
nodeDescription.textContent = result.description || 'Keine Beschreibung für diesen Knoten.';
|
||||||
|
nodeInfoPanel.classList.add('visible');
|
||||||
|
|
||||||
|
// Suchergebnisse schließen
|
||||||
|
closeSearchResults();
|
||||||
|
} else {
|
||||||
|
// Knoten ist in einer anderen Mindmap des Benutzers
|
||||||
|
if (result.mindmap_id && result.mindmap_id !== parseInt("{{ mindmap.id }}")) {
|
||||||
|
if (confirm(`Dieser Knoten befindet sich in der Mindmap "${result.mindmap_name}". Möchten Sie zu dieser Mindmap wechseln?`)) {
|
||||||
|
window.location.href = `/my-mindmap/${result.mindmap_id}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showUINotification('Knoten konnte nicht gefunden werden.', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (result.source === 'public') {
|
||||||
|
// Knoten ist in der öffentlichen Mindmap, frage ob er hinzugefügt werden soll
|
||||||
|
if (confirm(`Möchten Sie den Knoten "${result.name}" aus der öffentlichen Mindmap zu Ihrer Mindmap hinzufügen?`)) {
|
||||||
|
// Dialog zum Hinzufügen des Knotens anzeigen
|
||||||
|
showAddNodeDialog(result.id, result.name, result.description);
|
||||||
|
closeSearchResults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
searchResultsList.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export-Funktionalität implementieren
|
||||||
|
document.getElementById('export-btn').addEventListener('click', function() {
|
||||||
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||||
|
|
||||||
|
// Dialog für Export-Format anzeigen
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||||
|
overlay.id = 'export-dialog-overlay';
|
||||||
|
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h3 class="text-xl font-bold mb-4">Mindmap exportieren</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2">Format auswählen:</label>
|
||||||
|
<select id="export-format" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||||
|
<option value="json">JSON</option>
|
||||||
|
<option value="xml">XML</option>
|
||||||
|
<option value="csv">CSV</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button id="cancel-export" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||||
|
<button id="confirm-export" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Exportieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Event-Listener für Export-Buttons
|
||||||
|
document.getElementById('cancel-export').addEventListener('click', function() {
|
||||||
|
document.getElementById('export-dialog-overlay').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('confirm-export').addEventListener('click', function() {
|
||||||
|
const format = document.getElementById('export-format').value;
|
||||||
|
|
||||||
|
// API-Anfrage für den Export
|
||||||
|
fetch(`/api/mindmap/${mindmapId}/export?format=${format}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
// Download der Export-Datei
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `mindmap_${mindmapId}.${format}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.getElementById('export-dialog-overlay').remove();
|
||||||
|
showUINotification(`Mindmap wurde erfolgreich als ${format.toUpperCase()} exportiert.`, 'success');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Exportieren der Mindmap:', error);
|
||||||
|
showUINotification('Fehler beim Exportieren der Mindmap.', 'error');
|
||||||
|
document.getElementById('export-dialog-overlay').remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import-Funktionalität implementieren
|
||||||
|
document.getElementById('import-btn').addEventListener('click', function() {
|
||||||
|
const mindmapId = parseInt("{{ mindmap.id }}");
|
||||||
|
|
||||||
|
// Dialog für Import-Format anzeigen
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
|
||||||
|
overlay.id = 'import-dialog-overlay';
|
||||||
|
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h3 class="text-xl font-bold mb-4">Mindmap importieren</h3>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block mb-2">Datei auswählen:</label>
|
||||||
|
<input type="file" id="import-file" class="w-full p-2 border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||||
|
accept=".json,.xml,.csv">
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button id="cancel-import" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg">Abbrechen</button>
|
||||||
|
<button id="confirm-import" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg">Importieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// Event-Listener für Import-Buttons
|
||||||
|
document.getElementById('cancel-import').addEventListener('click', function() {
|
||||||
|
document.getElementById('import-dialog-overlay').remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('confirm-import').addEventListener('click', function() {
|
||||||
|
const fileInput = document.getElementById('import-file');
|
||||||
|
|
||||||
|
if (!fileInput.files || fileInput.files.length === 0) {
|
||||||
|
showUINotification('Bitte wählen Sie eine Datei aus.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
// API-Anfrage für den Import
|
||||||
|
fetch(`/api/mindmap/${mindmapId}/import`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('import-dialog-overlay').remove();
|
||||||
|
showUINotification('Mindmap wurde erfolgreich importiert.', 'success');
|
||||||
|
|
||||||
|
// Seite nach kurzer Verzögerung neu laden, um die importierten Daten anzuzeigen
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
showUINotification(`Fehler beim Importieren: ${data.message}`, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Importieren der Mindmap:', error);
|
||||||
|
showUINotification('Fehler beim Importieren der Mindmap.', 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback, falls mindmapData null ist
|
// Fallback, falls mindmapData null ist
|
||||||
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
|
if(mindmapNameH1) mindmapNameH1.textContent = "Mindmap nicht gefunden";
|
||||||
|
|||||||
Reference in New Issue
Block a user