Dateien nach "/" hochladen

This commit is contained in:
Oliver Braasch committed 2026-09-18 10:08:45 +02:00
1 parent 4c2656d7d7
commit cf85586446
1 file changed
+664 -86
+664 -86
View File
@@ -245,75 +245,126 @@
body.dark-mode .jse-table-mode,
body.dark-mode .jse-welcome,
body.dark-mode .jse-theme-dark {
--jse-bg-color: #16171a !important;
--jse-main-border: 1px solid #2a2b30 !important;
/* Grundfarben. --jse-background-color ist die wichtigste hier: Sie ist
der Hintergrund, auf den viele andere Variablen zurückfallen, wenn sie
selbst nicht gesetzt sind (u. a. die Box, die beim Umbenennen eines
Feldes erscheint - ohne diese Variable blieb sie hell). */
--jse-theme: dark !important;
--jse-background-color: #16171a !important;
/* Text/Tree/Table-Leiste an die App-eigene .top-bar angleichen (dunkel,
kein Akzentorange - siehe Kommentar weiter oben bei .editor-wrapper) */
--jse-theme-color: #1c1d21 !important;
--jse-theme-color-highlight: #24252b !important;
--jse-menu-color: #e7e8ea !important;
--jse-text-color: #e7e8ea !important;
--jse-text-color-min: #8b8d93 !important;
--jse-key-color: #6cb6ff !important;
--jse-value-color-string: #9fd6a0 !important;
--jse-value-color-number: #f7b267 !important;
--jse-value-color-boolean: #f47067 !important;
--jse-value-color-null: #8b8d93 !important;
--jse-hover-background-color: #22232a !important;
--jse-selection-background-color: #2a3340 !important;
--jse-active-line-background-color: rgba(255,255,255,0.04) !important;
--jse-text-readonly: #8b8d93 !important;
--jse-text-color-inverse: #e7e8ea !important;
--jse-error-color: #e5484d !important;
--jse-warning-color: #f7b267 !important;
--jse-info-color: #6cb6ff !important;
--jse-main-border: 1px solid #2a2b30 !important;
--jse-menu-color: #e7e8ea !important;
--jse-modal-background: #16171a !important;
--jse-overlay-background: rgba(0,0,0,0.6) !important;
--jse-modal-code-background: rgba(255,255,255,0.05) !important;
--jse-tooltip-color: #e7e8ea !important;
--jse-tooltip-background: #1c1d21 !important;
--jse-panel-background: #1c1d21 !important;
--jse-panel-background-hover: #24252b !important;
--jse-panel-color: #e7e8ea !important;
--jse-panel-color-readonly: #8b8d93 !important;
--jse-panel-border: 1px solid #2a2b30 !important;
--jse-panel-button-color: #c7c8cc !important;
--panel-button-color-highlight: #ffffff !important;
--jse-panel-button-color-highlight: #ffffff !important;
--jse-panel-button-background: transparent !important;
--jse-panel-button-background-highlight: rgba(255,255,255,0.08) !important;
--jse-navigation-bar-background: #1c1d21 !important;
--jse-navigation-bar-background-highlight: #24252b !important;
--jse-navigation-bar-dropdown-color: #e7e8ea !important;
--jse-context-menu-background: #1c1d21 !important;
--jse-context-menu-background-highlight: #24252b !important;
--jse-context-menu-color: #e7e8ea !important;
--jse-context-menu-color-disabled: #55565c !important;
--jse-context-menu-separator-color: #2a2b30 !important;
--jse-context-menu-pointer-hover-background: #33343b !important;
--jse-context-menu-pointer-background: #1c1d21 !important;
--jse-context-menu-pointer-background-highlight: #26272d !important;
--jse-context-menu-pointer-color: #e7e8ea !important;
--jse-context-menu-separator-color: #2a2b30 !important;
--jse-context-menu-hover-background-color: #26272d !important;
--jse-modal-background: #16171a !important;
--jse-modal-overlay-background: rgba(0,0,0,0.6) !important;
--jse-input-background: #101114 !important;
--jse-button-background: #26272d !important;
--jse-button-background-highlight: #33343b !important;
--jse-button-color: #e7e8ea !important;
--jse-tooltip-background: #1c1d21 !important;
--jse-tooltip-color: #e7e8ea !important;
--jse-tooltip-error-background: #3a1414 !important;
--jse-tooltip-error-color: #ff9a9a !important;
--jse-search-match-background: rgba(108,182,255,0.22) !important;
--jse-search-match-color: #e7e8ea !important;
--jse-search-match-outline: 1px solid #6cb6ff !important;
--jse-search-match-active-background: #3b78b0 !important;
--jse-search-match-active-color: #ffffff !important;
--jse-search-match-active-outline: 1px solid #8fcaff !important;
--jse-tag-background: #24252b !important;
--jse-tag-color: #e7e8ea !important;
/* Schlüssel/Werte im Baum */
--jse-key-color: #6cb6ff !important;
--jse-value-color: #e7e8ea !important;
--jse-value-color-string: #9fd6a0 !important;
--jse-value-color-number: #f7b267 !important;
--jse-value-color-boolean: #f47067 !important;
--jse-value-color-null: #8b8d93 !important;
--jse-value-color-url: #8fd1ff !important;
--jse-delimiter-color: #55565c !important;
--jse-edit-outline: 2px solid #6cb6ff !important;
/* Ausgewählt/gehovert - hier lag der gemeldete Fehler: Ein editiertes
Feld bekommt eine eigene Box auf Basis von --jse-background-color
(siehe oben) statt einer transparenten Fläche; ohne die beiden
folgenden Variablen blieb außerdem eine früher ausgewählte, aber
gerade inaktive Zeile hell. */
--jse-contents-background-color: transparent !important;
--jse-selection-background-color: #2a3340 !important;
--jse-selection-background-inactive-color: #20222a !important;
--jse-hover-background-color: #22232a !important;
--jse-active-line-background-color: rgba(255,255,255,0.04) !important;
--jse-search-match-background-color: rgba(255,214,51,0.30) !important;
--jse-collapsed-items-background-color: #1c1d21 !important;
--jse-collapsed-items-selected-background-color: #24252b !important;
--jse-collapsed-items-link-color: #6cb6ff !important;
--jse-collapsed-items-link-color-highlight: #8fcaff !important;
--jse-collapsed-items-selected-link-color: #ffffff !important;
--jse-delimiter-color: #55565c !important;
--jse-edit-outline: 2px solid #6cb6ff !important;
--jse-messages-info-background: #1c1d21 !important;
--jse-messages-info-color: #8fcaff !important;
--jse-messages-success-background: #123321 !important;
--jse-messages-success-color: #6bd8a1 !important;
--jse-messages-error-background: #3a1414 !important;
--jse-messages-error-color: #ff9a9a !important;
--jse-error-color: #e5484d !important;
--jse-warning-color: #f7b267 !important;
--jse-search-match-color: #16171a !important;
--jse-search-match-outline: 2px solid #ffd633 !important;
--jse-search-match-active-color: #16171a !important;
--jse-tag-background: #24252b !important;
--jse-tag-color: #e7e8ea !important;
/* Tabellen-Ansicht */
--jse-table-header-background: #1c1d21 !important;
--jse-table-header-background-highlight: #24252b !important;
--jse-table-row-odd-background: rgba(255,255,255,0.03) !important;
/* Eingaben und Buttons in den eigenen Dialogen des Editors */
--jse-input-background: #101114 !important;
--jse-input-background-readonly: rgba(255,255,255,0.03) !important;
--jse-input-color: #e7e8ea !important;
--jse-input-border: 1px solid #2a2b30 !important;
--jse-input-border-focus: 1px solid #6cb6ff !important;
--jse-button-background: #26272d !important;
--jse-button-background-highlight: #33343b !important;
--jse-button-color: #e7e8ea !important;
--jse-button-secondary-background: #26272d !important;
--jse-button-secondary-background-highlight: #33343b !important;
--jse-button-secondary-color: #e7e8ea !important;
--jse-a-color: #6cb6ff !important;
--jse-a-color-highlight: #8fcaff !important;
/* Meldungen (Fehler/Erfolg/Warnung/Info-Banner im Editor) */
--jse-message-info-background: #1c1d21 !important;
--jse-message-info-color: #8fcaff !important;
--jse-message-success-background: #163524 !important;
--message-success-background: #163524 !important;
--jse-message-success-color: #6bd8a1 !important;
--jse-message-warning-background: #3a2f10 !important;
--jse-message-warning-color: #f7c948 !important;
--jse-message-error-background: #3a1414 !important;
--jse-message-error-color: #ff9a9a !important;
--jse-message-action-background: rgba(255,255,255,0.12) !important;
--jse-message-action-background-highlight: rgba(255,255,255,0.2) !important;
--jse-svelte-select-background: #16171a !important;
background-color: #16171a !important;
color: #e7e8ea !important;
}
@@ -439,6 +490,52 @@
.btn-mini:hover { border-color: var(--accent); color: var(--accent); }
.btn-mini.danger:hover { border-color: var(--danger); color: var(--danger); }
/* Spalten-Typ-Editor im SQL-Dialog */
.coltype-editor {
border: 1px solid var(--border-color); border-radius: 8px;
padding: 10px 14px; margin-bottom: 16px; background: var(--panel-bg);
}
.coltype-editor summary {
cursor: pointer; font-weight: 700; font-size: 13px; padding: 4px 0;
list-style: none;
}
.coltype-editor summary::-webkit-details-marker { display: none; }
.coltype-editor summary::before { content: '\25b8\0020'; }
.coltype-editor[open] summary::before { content: '\25be\0020'; }
.coltype-row {
display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap;
padding: 10px 0; border-top: 1px solid var(--border-color);
}
.coltype-name { font-weight: 700; font-size: 12.5px; min-width: 130px; padding-top: 7px; flex-shrink: 0; }
.coltype-badge {
font-size: 10px; font-weight: 700; color: var(--accent); background: rgba(255,122,26,0.12);
border-radius: 10px; padding: 1px 7px; margin-left: 4px; white-space: nowrap;
}
.coltype-row select { flex: 1 1 190px; min-width: 170px; }
.coltype-row input.coltype-rename { flex: 1 1 150px; min-width: 130px; }
.coltype-row input.coltype-length { width: 110px; flex-shrink: 0; }
.coltype-row select, .coltype-row input[type="text"] {
padding: 6px 8px; border-radius: 6px; border: 1px solid var(--border-color);
background: var(--card-bg); color: var(--text-color); font-size: 12.5px; font-family: inherit;
}
.coltype-row select:focus, .coltype-row input:focus { outline: none; border-color: var(--accent); }
.coltype-row input:disabled { opacity: 0.45; }
.coltype-hint { flex-basis: 100%; font-size: 11.5px; color: var(--text-muted); line-height: 1.5; margin-top: 2px; }
/* Primärschlüssel-Auswahl (auch mehrere Spalten = zusammengesetzter Schlüssel) */
.pk-checklist {
display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 8px;
padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--panel-bg);
}
.pk-check { display: flex; align-items: center; gap: 6px; font-size: 12.5px; cursor: pointer; white-space: nowrap; }
.pk-check input { width: auto; margin: 0; accent-color: var(--accent); cursor: pointer; }
/* Zuletzt geöffnete Dateien im Öffnen-Menü */
.recent-file-item {
padding: 6px 14px; font-size: 11.5px; color: var(--text-muted);
display: flex; align-items: center; gap: 7px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
kbd {
background: var(--panel-bg); border: 1px solid var(--border-color); border-bottom-width: 2px;
border-radius: 5px; padding: 2px 7px; font-family: "SFMono-Regular", Consolas, monospace;
@@ -554,6 +651,7 @@
<div class="divider"></div>
<a href="#" onclick="openUrlImportModal('left'); return false;" title="Daten direkt von einer API-Adresse laden">&#127760; Von URL laden&hellip;</a>
<a href="#" onclick="openDbLoadModal('left'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">&#128451;&#65039; Aus Datenbank laden&hellip;</a>
<div id="recentFilesLeft"></div>
</div>
</div>
@@ -589,6 +687,7 @@
</div>
</div>
<button class="nav-btn" onclick="window.copyJsonPath('left')" title="Pfad des zuletzt angeklickten Feldes kopieren (z. B. für n8n-Ausdrücke)">&#128205; <span class="btn-text">Pfad</span></button>
<button class="nav-btn" onclick="toggleFullscreen('leftPane')" title="Diesen Editor im Vollbild anzeigen">&#10530; <span class="btn-text">Vollbild</span></button>
</div>
</div>
@@ -642,6 +741,7 @@
<div class="divider"></div>
<a href="#" onclick="openUrlImportModal('right'); return false;" title="Daten direkt von einer API-Adresse laden">&#127760; Von URL laden&hellip;</a>
<a href="#" onclick="openDbLoadModal('right'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">&#128451;&#65039; Aus Datenbank laden&hellip;</a>
<div id="recentFilesRight"></div>
</div>
</div>
@@ -677,6 +777,7 @@
</div>
</div>
<button class="nav-btn" onclick="window.copyJsonPath('right')" title="Pfad des zuletzt angeklickten Feldes kopieren (z. B. für n8n-Ausdrücke)">&#128205; <span class="btn-text">Pfad</span></button>
<button class="nav-btn" onclick="toggleFullscreen('rightPane')" title="Diesen Editor im Vollbild anzeigen">&#10530; <span class="btn-text">Vollbild</span></button>
</div>
</div>
@@ -795,7 +896,9 @@ const DEFAULT_SETTINGS = {
webhookUrl: '',
xmlRootName: 'root',
xmlItemName: 'item',
splitRatio: 0.5
splitRatio: 0.5,
autoSaveEnabled: false,
autoSaveIntervalMinutes: 10
};
function loadSettings() {
@@ -810,6 +913,42 @@ function saveSettings(s) {
}
window.wbSettings = loadSettings();
/* Merkt sich Primärschlüssel-Wahl, manuelle Spaltentypen und Spalten-Umbenennungen
aus dem MariaDB-Dialog getrennt für den linken und rechten Editor, damit sie
beim erneuten Öffnen des Dialogs für dasselbe Dokument erhalten bleiben. */
window.sqlColumnPrefs = {};
function loadSqlPrefs(side) {
if (window.sqlColumnPrefs[side]) return window.sqlColumnPrefs[side];
try {
const raw = localStorage.getItem('wb_sql_prefs_' + side);
if (raw) {
const parsed = JSON.parse(raw);
// Ältere Version speicherte "pk" als einzelnen Spaltennamen statt als Liste.
if (typeof parsed.pk === 'string') parsed.pk = [parsed.pk];
if (!Array.isArray(parsed.pk)) parsed.pk = [];
if (!parsed.overrides) parsed.overrides = {};
if (!parsed.renames) parsed.renames = {};
window.sqlColumnPrefs[side] = parsed;
return parsed;
}
} catch (e) { /* ignoriert */ }
window.sqlColumnPrefs[side] = { pk: [], overrides: {}, renames: {} };
return window.sqlColumnPrefs[side];
}
function saveSqlPrefs(side) {
try { localStorage.setItem('wb_sql_prefs_' + side, JSON.stringify(window.sqlColumnPrefs[side])); }
catch (e) { /* ignoriert */ }
}
/** Aktuellen Stand des Dialogs (Primärschlüssel, Typen, Umbenennungen) sichern. */
function persistSqlPrefs(st) {
window.sqlColumnPrefs[st.side] = { pk: st.pk, overrides: st.overrides, renames: st.renames };
saveSqlPrefs(st.side);
}
window.resetSqlPrefs = function(side) {
window.sqlColumnPrefs[side] = { pk: [], overrides: {}, renames: {} };
try { localStorage.removeItem('wb_sql_prefs_' + side); } catch (e) { /* ignoriert */ }
};
function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
@@ -873,13 +1012,51 @@ if (savedTitleRight) document.getElementById('titleRight').innerText = savedTitl
const savedDarkMode = localStorage.getItem('wb_dark_mode');
const isDarkDefault = savedDarkMode !== null ? savedDarkMode === 'true' : true;
window.lastSelectionPath = { left: null, right: null };
/** Wandelt einen JSON-Pfad (Liste aus Schl\u00fcsseln/Indizes) in eine lesbare
* Notation wie "Positionen[2].Preis" um, wie sie in n8n-Ausdr\u00fccken \u00fcblich ist. */
function formatJsonPath(path) {
if (!path || path.length === 0) return '(Wurzel)';
let out = '';
path.forEach((seg, i) => {
if (typeof seg === 'number') out += `[${seg}]`;
else if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(String(seg))) out += (i === 0 ? seg : '.' + seg);
else out += `["${String(seg).replace(/"/g, '\\"')}"]`;
});
return out;
}
function selectionToPath(selection) {
if (!selection) return null;
if (Array.isArray(selection.path)) return selection.path;
if (Array.isArray(selection.paths) && selection.paths.length) return selection.paths[0];
if (Array.isArray(selection.focusPath)) return selection.focusPath;
return null;
}
window.copyJsonPath = function(side) {
const path = window.lastSelectionPath[side];
if (!path) return window.showToast('Bitte zuerst ein Feld im Baum anklicken.', 'error');
const text = formatJsonPath(path);
navigator.clipboard.writeText(text).then(() => window.showToast('Pfad kopiert: ' + text, 'success'));
};
window.editorLeft = createJSONEditor({
target: document.getElementById('jsoneditorLeft'),
props: { content: loadSavedContent('wb_left_content'), mode: 'tree', onChange: () => window.saveAppState() }
props: {
content: loadSavedContent('wb_left_content'), mode: 'tree',
onChange: () => window.saveAppState(),
onSelect: (selection) => { window.lastSelectionPath.left = selectionToPath(selection); }
}
});
window.editorRight = createJSONEditor({
target: document.getElementById('jsoneditorRight'),
props: { content: loadSavedContent('wb_right_content'), mode: 'tree', onChange: () => window.saveAppState() }
props: {
content: loadSavedContent('wb_right_content'), mode: 'tree',
onChange: () => window.saveAppState(),
onSelect: (selection) => { window.lastSelectionPath.right = selectionToPath(selection); }
}
});
window.activeSide = 'left';
@@ -925,6 +1102,46 @@ window.applyTheme = function(isDark) {
window.cycleTheme = function() { window.applyTheme(!document.body.classList.contains('dark-mode')); };
window.applyTheme(isDarkDefault);
// "Zuletzt geöffnet"-Liste beim Start einmal befüllen
renderRecentFiles('left');
renderRecentFiles('right');
/* =========================================================
Automatische Sicherung als Download (optional, standardmäßig aus)
========================================================= */
window.autoSaveState = { left: { lastHash: null }, right: { lastHash: null } };
window.autoSaveTimer = null;
function autoSaveTick() {
['left', 'right'].forEach(side => {
const data = window.getJsonData(side);
if (data === null) return; // leer oder kaputtes JSON - nichts zu sichern
let text;
try { text = JSON.stringify(data); } catch (e) { return; }
if (text === '{}' || text === '[]' || text === '""') return; // praktisch leer
const state = window.autoSaveState[side];
if (state.lastHash === text) return; // seit der letzten Sicherung nichts geändert
state.lastHash = text;
const stamp = new Date().toISOString().replace(/[:T]/g, '-').slice(0, 16);
const filename = docName(side) + '_autosave_' + stamp + '.json';
downloadBlob(JSON.stringify(data, null, 2), filename, 'application/json');
window.showToast('Automatisch gesichert: ' + filename);
});
}
/** Startet/stoppt den Hintergrund-Timer passend zu den aktuellen Einstellungen.
* Wird beim Start und nach jeder Änderung in den Einstellungen aufgerufen. */
window.restartAutoSaveTimer = function() {
if (window.autoSaveTimer) { clearInterval(window.autoSaveTimer); window.autoSaveTimer = null; }
if (!window.wbSettings.autoSaveEnabled) return;
const minutes = Math.max(1, parseInt(window.wbSettings.autoSaveIntervalMinutes, 10) || 10);
window.autoSaveTimer = setInterval(autoSaveTick, minutes * 60 * 1000);
};
window.restartAutoSaveTimer();
/* ===========================================================
4. Daten-Helfer
=========================================================== */
@@ -965,7 +1182,54 @@ function readFileAsText(file, callback) {
reader.readAsText(file);
}
window.clearEditor = function(side) { ed(side).set({ text: '' }); window.saveAppState(); };
/** Setzt den Dokumentnamen oben auf den Dateinamen ohne Endung, damit sich
* eine geöffnete Datei nach dem Bearbeiten direkt wieder unter demselben
* Namen speichern lässt, und merkt sie sich in der "Zuletzt geöffnet"-Liste. */
function setTitleFromFilename(side, filename) {
const base = filename.replace(/\.[^./\\]+$/, '').trim();
if (base) {
document.getElementById(titleId(side)).innerText = base;
window.saveAppState();
}
recordRecentFile(side, filename);
}
function fileIconFor(filename) {
const ext = (filename.split('.').pop() || '').toLowerCase();
const map = { json: '\u{1F4C4}', csv: '\u{1F4CA}', xlsx: '\u{1F4C8}', xls: '\u{1F4C8}',
yaml: '\u{1F4D1}', yml: '\u{1F4D1}', xml: '\u{1F9EC}', sql: '\u{1F5C3}\uFE0F' };
return map[ext] || '\u{1F4C4}';
}
/** Merkt sich die letzten 5 pro Editor geöffneten Dateinamen (nur der Name,
* nicht der Inhalt) als kleine Erinnerungsstütze im "Öffnen"-Menü. */
function recordRecentFile(side, filename) {
const key = 'wb_recent_' + side;
let list = [];
try { list = JSON.parse(localStorage.getItem(key) || '[]'); } catch (e) { list = []; }
list = list.filter(f => f.name !== filename);
list.unshift({ name: filename, date: new Date().toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) });
list = list.slice(0, 5);
try { localStorage.setItem(key, JSON.stringify(list)); } catch (e) { /* ignoriert */ }
renderRecentFiles(side);
}
function renderRecentFiles(side) {
const container = document.getElementById(side === 'right' ? 'recentFilesRight' : 'recentFilesLeft');
if (!container) return;
let list = [];
try { list = JSON.parse(localStorage.getItem('wb_recent_' + side) || '[]'); } catch (e) { list = []; }
if (list.length === 0) { container.innerHTML = ''; return; }
container.innerHTML = '<div class="divider"></div><div class="group-label">Zuletzt ge\u00f6ffnet</div>' +
list.map(f => '<div class="recent-file-item" title="' + escapeHtml(f.name) + ' \u00b7 ' + escapeHtml(f.date) + '">' +
fileIconFor(f.name) + ' ' + escapeHtml(f.name) + '</div>').join('');
}
window.clearEditor = function(side) {
ed(side).set({ text: '' });
window.resetSqlPrefs(side);
window.saveAppState();
};
window.toggleFullscreen = function(paneId) {
const elem = document.getElementById(paneId);
@@ -983,6 +1247,7 @@ window.importJsonFile = function(event, side) {
readFileAsText(file, (text) => {
try { setData(side, JSON.parse(text)); }
catch (err) { ed(side).set({ text: text }); window.saveAppState(); }
setTitleFromFilename(side, file.name);
window.showToast('Geladen: ' + file.name, 'success');
});
event.target.value = '';
@@ -996,6 +1261,7 @@ window.importCsvFile = function(event, side) {
delimiter: window.wbSettings.csvDelimiter || '',
complete: (res) => {
setData(side, res.data);
setTitleFromFilename(side, file.name);
const d = res.meta && res.meta.delimiter ? res.meta.delimiter : '?';
window.showToast('CSV importiert: ' + file.name + ' (Trennzeichen "' + d + '")', 'success');
},
@@ -1015,10 +1281,12 @@ window.importExcelFile = function(event, side) {
const out = {};
wb.SheetNames.forEach(n => { out[n] = XLSX.utils.sheet_to_json(wb.Sheets[n], { defval: null }); });
setData(side, out);
setTitleFromFilename(side, file.name);
window.showToast('Excel importiert: ' + file.name + ' (' + wb.SheetNames.length + ' Bl\u00e4tter)', 'success');
} else {
const name = wb.SheetNames[0];
setData(side, XLSX.utils.sheet_to_json(wb.Sheets[name], { defval: null }));
setTitleFromFilename(side, file.name);
window.showToast('Excel importiert: ' + file.name + ' (' + name + ')', 'success');
}
} catch (err) { window.showToast('Excel-Fehler: ' + err.message, 'error'); }
@@ -1034,6 +1302,7 @@ window.importYamlFile = function(event, side) {
try {
const docs = jsyaml.loadAll(text);
setData(side, docs.length === 1 ? docs[0] : docs);
setTitleFromFilename(side, file.name);
window.showToast('YAML importiert: ' + file.name, 'success');
} catch (err) { window.showToast('YAML-Fehler: ' + err.message, 'error'); }
});
@@ -1049,6 +1318,7 @@ window.importXmlFile = function(event, side) {
if (doc.querySelector('parsererror')) throw new Error('XML ist nicht wohlgeformt.');
const root = doc.documentElement;
setData(side, { [root.nodeName]: xmlNodeToJson(root) });
setTitleFromFilename(side, file.name);
window.showToast('XML importiert: ' + file.name, 'success');
} catch (err) { window.showToast('XML-Fehler: ' + err.message, 'error'); }
});
@@ -1102,6 +1372,7 @@ window.importSqlFile = function(event, side) {
const names = Object.keys(tables);
if (names.length === 0) throw new Error('Keine INSERT-Anweisungen gefunden.');
setData(side, names.length === 1 ? tables[names[0]] : tables);
setTitleFromFilename(side, file.name);
window.showToast('SQL importiert: ' + names.length + ' Tabelle(n), ' +
names.reduce((s, n) => s + tables[n].length, 0) + ' Zeilen', 'success');
} catch (err) { window.showToast('SQL-Fehler: ' + err.message, 'error'); }
@@ -1382,8 +1653,40 @@ window.copyToClip = function(side, format) {
7. MariaDB-SQL erzeugen
=========================================================== */
/** Katalog wählbarer Spaltentypen mit Erklärung für Laien. */
const SQL_TYPE_CATALOG = {
'INT': { label: 'INT – Ganzzahl', hasLength: false,
desc: 'Ganze Zahlen von etwa -2,1 bis +2,1 Milliarden. Für die meisten IDs und Z\u00e4hler ausreichend.' },
'BIGINT': { label: 'BIGINT – gro\u00dfe Ganzzahl', hasLength: false,
desc: 'Ganze Zahlen bis etwa 9,2 Trillionen. N\u00f6tig, wenn IDs sehr gro\u00df werden, wie es bei manchen Shop- oder Billbee-IDs vorkommt.' },
'SMALLINT': { label: 'SMALLINT – kleine Ganzzahl', hasLength: false,
desc: 'Ganze Zahlen von -32.768 bis 32.767. Spart Platz bei kleinem Wertebereich, z. B. Jahrg\u00e4ngen oder St\u00fcckzahlen.' },
'TINYINT': { label: 'TINYINT – sehr kleine Ganzzahl', hasLength: false,
desc: 'Ganze Zahlen von -128 bis 127. F\u00fcr kleine Statuscodes oder Z\u00e4hler geeignet.' },
'TINYINT(1)': { label: 'TINYINT(1) – Ja/Nein', hasLength: false,
desc: 'Wird von MariaDB als boolescher Wert behandelt: 0 = falsch/nein, 1 = wahr/ja.' },
'DECIMAL': { label: 'DECIMAL – genaue Kommazahl', hasLength: true, lengthLabel: 'Gesamtstellen,Nachkommastellen', lengthDefault: '10,2',
desc: 'Exakte Kommazahl ohne Rundungsfehler. F\u00fcr Preise und Geldbetr\u00e4ge die richtige Wahl \u2013 DOUBLE rundet bei Geld manchmal minimal falsch.' },
'DOUBLE': { label: 'DOUBLE – Gleitkommazahl', hasLength: false,
desc: 'Kommazahl mit m\u00f6glichen kleinen Rundungsungenauigkeiten. F\u00fcr Messwerte und Berechnungen, nicht f\u00fcr Geldbetr\u00e4ge.' },
'VARCHAR': { label: 'VARCHAR – kurzer Text', hasLength: true, lengthLabel: 'Maximale Zeichenzahl', lengthDefault: '255',
desc: 'Text mit fester Obergrenze, schnell durchsuchbar und indizierbar. F\u00fcr Namen, E-Mail-Adressen, kurze Bezeichnungen.' },
'TEXT': { label: 'TEXT – langer Text', hasLength: false,
desc: 'Text ohne praktische L\u00e4ngenbegrenzung. F\u00fcr Notizen, Beschreibungen oder l\u00e4ngere Inhalte.' },
'DATE': { label: 'DATE – nur Datum', hasLength: false,
desc: 'Speichert nur ein Datum, z. B. 2026-09-16, ohne Uhrzeit.' },
'DATETIME': { label: 'DATETIME – Datum und Uhrzeit', hasLength: false,
desc: 'Datum plus Uhrzeit. Der \u00fcbliche Typ f\u00fcr Zeitstempel wie \u201eletzte \u00c4nderung\u201c.' },
'JSON': { label: 'JSON – strukturierte Daten', hasLength: false,
desc: 'F\u00fcr verschachtelte Objekte oder Listen innerhalb eines Feldes. MariaDB kann darin sogar gezielt suchen.' }
};
function sqlIntType(val) {
return (val > 2147483647 || val < -2147483648) ? 'BIGINT' : 'INT';
}
function inferSqlType(val) {
if (typeof val === 'number') return Number.isInteger(val) ? 'INT' : 'DOUBLE';
if (typeof val === 'number') return Number.isInteger(val) ? sqlIntType(val) : 'DOUBLE';
if (typeof val === 'boolean') return 'TINYINT(1)';
if (typeof val === 'object' && val !== null) return 'JSON';
if (typeof val === 'string') {
@@ -1400,68 +1703,304 @@ function formatSqlValue(val) {
return `'${String(val).replace(/'/g, "''")}'`;
}
function buildSql(tableName, items) {
// Rangfolge, in die ein Zahlentyp "aufsteigt", wenn spätere Zeilen einen
// größeren Wertebereich brauchen als die erste Zeile vermuten ließ.
const NUMERIC_WIDENING = { 'INT': 1, 'BIGINT': 2, 'DOUBLE': 3 };
function widenSqlType(current, next) {
if (current === next) return current;
const rc = NUMERIC_WIDENING[current], rn = NUMERIC_WIDENING[next];
if (rc && rn) return rc >= rn ? current : next; // beide numerisch: größeren Bereich behalten
return current; // uneinheitliche Typen: ersten Fund beibehalten, der Editor kann das korrigieren
}
/** Spalten und ihre Typen aus den Daten ableiten, ohne schon SQL zu bauen. */
function inferColumns(items) {
const columns = {};
items.forEach(item => {
if (typeof item === 'object' && item !== null) {
for (const [key, val] of Object.entries(item)) {
if (!columns[key]) columns[key] = inferSqlType(val);
else if (columns[key] === 'INT' && typeof val === 'number' && !Number.isInteger(val)) columns[key] = 'DOUBLE';
const t = inferSqlType(val);
columns[key] = columns[key] ? widenSqlType(columns[key], t) : t;
}
}
});
const colNames = Object.keys(columns);
return { columns, colNames: Object.keys(columns) };
}
/** "VARCHAR(255)" -> {base:'VARCHAR', length:'255'}; "TINYINT(1)" bleibt als eigener Katalog-Eintrag. */
function parseSqlType(typeStr) {
if (typeStr === 'TINYINT(1)') return { base: 'TINYINT(1)', length: '' };
const m = typeStr.match(/^([A-Z]+)(?:\(([^)]*)\))?$/);
if (!m) return { base: typeStr, length: '' };
return { base: m[1], length: m[2] || '' };
}
function composeSqlType(base, length) {
const info = SQL_TYPE_CATALOG[base];
if (!info) return base;
if (base === 'TINYINT(1)') return 'TINYINT(1)';
if (info.hasLength) return `${base}(${length || info.lengthDefault})`;
return base;
}
/** Für jede Spalte den tatsächlich zu verwendenden Typ ermitteln: die
* manuelle Auswahl im Editor hat Vorrang vor der automatischen Erkennung. */
function effectiveColumns() {
const st = window.sqlModalState;
const map = {};
st.colNames.forEach(c => {
const ov = st.overrides[c];
map[c] = ov ? composeSqlType(ov.base, ov.length) : st.columns[c];
});
return map;
}
/**
* pkColumns = [] -> es wird eine eigene, fortlaufende `id`-Spalte angelegt (Standard).
* pkColumns = ["Spalte"] -> diese wird zum Primärschlüssel, keine zusätzliche id-Spalte.
* pkColumns = ["A","B"] -> zusammengesetzter Schlüssel über mehrere Spalten
* (PRIMARY KEY (A, B)), praktisch wenn erst die Kombination eindeutig ist.
* renames: { urspr\u00fcnglicherName: 'neuer_name' } - \u00e4ndert nur den Namen im SQL,
* die Werte werden weiterhin unter dem urspr\u00fcnglichen JSON-Feldnamen gelesen.
* columns/colNames: bereits aufgel\u00f6ste Spaltentypen (inkl. manueller Korrekturen).
*/
function sanitizeColumnName(name) {
let n = String(name).trim().replace(/[^A-Za-z0-9_]/g, '_');
if (n === '') return '';
if (/^[0-9]/.test(n)) n = '_' + n;
return n;
}
/** Liefert je Originalspalte den tats\u00e4chlich zu verwendenden SQL-Namen.
* Nur explizit umbenannte Spalten weichen vom JSON-Feldnamen ab; Kollisionen
* (zwei Spalten enden auf denselben Namen) werden automatisch aufgel\u00f6st. */
function resolveOutputNames(colNames, renames) {
const used = new Set();
const map = {};
colNames.forEach(c => {
const wanted = (renames && renames[c]) ? renames[c] : c;
let finalName = wanted;
let i = 2;
while (used.has(finalName)) { finalName = wanted + '_' + i; i++; }
used.add(finalName);
map[c] = finalName;
});
return map;
}
function buildSql(tableName, items, pkColumns, resolved, renames) {
const { columns, colNames } = resolved || inferColumns(items);
const pkList = (Array.isArray(pkColumns) ? pkColumns : (pkColumns ? [pkColumns] : []))
.filter(c => colNames.includes(c));
const outNames = resolveOutputNames(colNames, renames);
let sql = `-- MariaDB Tabellenstruktur f\u00fcr \`${tableName}\`\n`;
sql += `CREATE TABLE IF NOT EXISTS \`${tableName}\` (\n`;
sql += ` \`id\` INT AUTO_INCREMENT PRIMARY KEY,\n`;
sql += colNames.map(c => ` \`${c}\` ${columns[c]}`).join(',\n');
const lines = [];
if (pkList.length === 0) {
lines.push(` \`id\` INT AUTO_INCREMENT PRIMARY KEY`);
colNames.forEach(c => lines.push(` \`${outNames[c]}\` ${columns[c]}`));
} else if (pkList.length === 1) {
colNames.forEach(c => {
let def = ` \`${outNames[c]}\` ${columns[c]}`;
if (c === pkList[0]) def += ' NOT NULL PRIMARY KEY';
lines.push(def);
});
} else {
colNames.forEach(c => {
let def = ` \`${outNames[c]}\` ${columns[c]}`;
if (pkList.includes(c)) def += ' NOT NULL';
lines.push(def);
});
lines.push(` PRIMARY KEY (${pkList.map(c => '\`' + outNames[c] + '\`').join(', ')})`);
}
sql += lines.join(',\n');
sql += `\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n`;
sql += `-- Daten-Imports f\u00fcr \`${tableName}\`\n`;
const outColList = colNames.map(c => outNames[c]).join('`, `');
items.forEach(item => {
const vals = colNames.map(c => formatSqlValue(item[c]));
sql += `INSERT INTO \`${tableName}\` (\`${colNames.join('`, `')}\`) VALUES (${vals.join(', ')});\n`;
sql += `INSERT INTO \`${tableName}\` (\`${outColList}\`) VALUES (${vals.join(', ')});\n`;
});
return { sql, columns, colNames };
return { sql, columns, colNames, outNames };
}
function buildSqlPreviewTable(items, colNames, columns, pkList, outNames) {
let html = '<div class="preview-table-container"><table class="sql-preview-table"><thead><tr>';
if (pkList.length === 0) html += '<th>id (PK, automatisch)</th>';
colNames.forEach(c => {
const marker = pkList.includes(c) ? ' &#128273; PK' : '';
const renamed = outNames[c] !== c;
const label = renamed
? escapeHtml(outNames[c]) + ' <span class="coltype-badge" title="Urspr\u00fcnglich: ' + escapeHtml(c) + '">umbenannt</span>'
: escapeHtml(c);
html += '<th>' + label + ' (' + columns[c] + ')' + marker + '</th>';
});
html += '</tr></thead><tbody>';
items.forEach((item, idx) => {
html += '<tr>';
if (pkList.length === 0) html += '<td>' + (idx + 1) + '</td>';
colNames.forEach(c => {
const v = item[c];
const d = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : String(v === undefined || v === null ? '' : v);
html += '<td>' + escapeHtml(d) + '</td>';
});
html += '</tr>';
});
html += '</tbody></table></div>';
return html;
}
function buildColumnTypeEditor() {
const st = window.sqlModalState;
const typeOptions = Object.keys(SQL_TYPE_CATALOG);
const resolvedColumns = effectiveColumns();
const rows = st.colNames.map(c => {
const current = parseSqlType(resolvedColumns[c]);
const info = SQL_TYPE_CATALOG[current.base] || SQL_TYPE_CATALOG['VARCHAR'];
const isRenamed = !!(st.renames[c] && st.renames[c] !== c);
const isCustomized = !!st.overrides[c] || isRenamed;
const options = typeOptions.map(t =>
'<option value="' + t + '"' + (t === current.base ? ' selected' : '') + '>' +
escapeHtml(SQL_TYPE_CATALOG[t].label) + '</option>'
).join('');
const lengthField = info.hasLength
? `<input type="text" class="coltype-length" value="${escapeHtml(current.length || info.lengthDefault)}"
onchange="window.onColumnLengthChange('${escapeHtml(c)}', this.value)"
title="${escapeHtml(info.lengthLabel)}">`
: '<input type="text" class="coltype-length" value="" disabled title="Für diesen Typ nicht nötig">';
return `
<div class="coltype-row">
<div class="coltype-name">${escapeHtml(c)}${isCustomized ? ' <span class="coltype-badge">angepasst</span>' : ''}</div>
<input type="text" class="coltype-rename" value="${escapeHtml(st.renames[c] || c)}"
onchange="window.onColumnRenameChange('${escapeHtml(c)}', this.value)"
title="Spaltenname im SQL - Standard: wie im JSON">
<select onchange="window.onColumnTypeChange('${escapeHtml(c)}', this.value)">${options}</select>
${lengthField}
<div class="coltype-hint">${escapeHtml(info.desc)}</div>
</div>`;
}).join('');
return `
<details class="coltype-editor" ${st.editorOpen ? 'open' : ''} ontoggle="window.sqlModalState.editorOpen = this.open">
<summary>&#9881;&#65039; Spalten anpassen (Typ &amp; Name)</summary>
<div class="modal-hint" style="margin-top:10px">
Die Typen werden automatisch aus den Werten geraten. Passt eine Zuordnung nicht &ndash;
etwa weil eine Spalte eigentlich Geldbetr&auml;ge enth&auml;lt oder eine ID gr&ouml;&szlig;er
ist als erwartet &ndash; kannst du sie hier gezielt &uuml;berschreiben. Im zweiten Feld l&auml;sst
sich au&szlig;erdem der Spaltenname im SQL &auml;ndern, falls er nicht dem JSON-Feldnamen
entsprechen soll.
</div>
${rows}
</details>`;
}
function renderSqlModalBody() {
const st = window.sqlModalState;
const resolvedColumns = effectiveColumns();
const resolved = { columns: resolvedColumns, colNames: st.colNames };
const { sql, outNames } = buildSql(st.tableName, st.items, st.pk, resolved, st.renames);
st.sql = sql;
const pkChecklist = st.colNames.map(c =>
`<label class="pk-check">
<input type="checkbox" ${st.pk.includes(c) ? 'checked' : ''} onchange="window.onSqlPkToggle('${escapeHtml(c)}', this.checked)">
${escapeHtml(c)}
</label>`
).join('');
document.getElementById('modalContent').innerHTML = `
<div class="form-row">
<label class="field-label">Prim&auml;rschl&uuml;ssel</label>
<div class="pk-checklist">${pkChecklist}</div>
<div class="field-hint">
Keine Auswahl &ndash; automatische, fortlaufende <code>id</code>-Spalte (Standard).
Ein Haken &ndash; diese Spalte wird zum Prim&auml;rschl&uuml;ssel; achte darauf, dass sie
in jeder Zeile eindeutig und nicht leer ist. Mehrere Haken &ndash; zusammengesetzter
Schl&uuml;ssel, der erst in Kombination eindeutig sein muss. Die Auswahl wird f&uuml;r
dieses Dokument gemerkt, bis du oben auf &bdquo;Neu&ldquo; klickst.
</div>
</div>
${buildColumnTypeEditor()}
<div class="modal-section-title">&#128202; Visuelle MariaDB Tabellen-Vorschau</div>
${buildSqlPreviewTable(st.items, st.colNames, resolvedColumns, st.pk, outNames)}
<div class="modal-section-title">&#128196; Generiertes SQL-Skript</div>
<pre class="sql-code">${escapeHtml(sql)}</pre>`;
}
window.onSqlPkToggle = function(colName, checked) {
const st = window.sqlModalState;
if (checked) { if (!st.pk.includes(colName)) st.pk.push(colName); }
else { st.pk = st.pk.filter(c => c !== colName); }
persistSqlPrefs(st);
renderSqlModalBody();
};
window.onColumnTypeChange = function(colName, base) {
const st = window.sqlModalState;
const info = SQL_TYPE_CATALOG[base];
st.overrides[colName] = { base, length: info.hasLength ? info.lengthDefault : '' };
st.editorOpen = true;
persistSqlPrefs(st);
renderSqlModalBody();
};
window.onColumnLengthChange = function(colName, length) {
const st = window.sqlModalState;
const ov = st.overrides[colName];
if (ov) ov.length = length;
st.editorOpen = true;
persistSqlPrefs(st);
renderSqlModalBody();
};
window.onColumnRenameChange = function(colName, value) {
const st = window.sqlModalState;
const sanitized = sanitizeColumnName(value);
if (!sanitized || sanitized === colName) delete st.renames[colName];
else st.renames[colName] = sanitized;
st.editorOpen = true;
persistSqlPrefs(st);
renderSqlModalBody();
};
window.openSqlModal = function(side) {
const data = window.getJsonData(side);
if (data === null) return window.showToast('Bitte zuerst g\u00fcltige Daten in den Editor laden.', 'error');
const tableName = docName(side).toLowerCase().replace(/[^a-z0-9_]/g, '_');
const items = toRows(data);
const { sql, columns, colNames } = buildSql(tableName, items);
const { columns, colNames } = inferColumns(items);
let htmlTable = '<div class="preview-table-container"><table class="sql-preview-table"><thead><tr><th>id (PK)</th>';
colNames.forEach(c => { htmlTable += '<th>' + escapeHtml(c) + ' (' + columns[c] + ')</th>'; });
htmlTable += '</tr></thead><tbody>';
items.forEach((item, idx) => {
htmlTable += '<tr><td>' + (idx + 1) + '</td>';
colNames.forEach(c => {
const v = item[c];
const d = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : String(v === undefined || v === null ? '' : v);
htmlTable += '<td>' + escapeHtml(d) + '</td>';
});
htmlTable += '</tr>';
});
htmlTable += '</tbody></table></div>';
// Zuvor für dieses Dokument gewählte Einstellungen übernehmen - aber nur,
// soweit sie noch zu den gerade vorhandenen Spalten passen.
const prefs = loadSqlPrefs(side);
const pk = prefs.pk.filter(c => colNames.includes(c));
const overrides = {};
colNames.forEach(c => { if (prefs.overrides[c]) overrides[c] = prefs.overrides[c]; });
const renames = {};
colNames.forEach(c => { if (prefs.renames[c]) renames[c] = prefs.renames[c]; });
const body = `
<div class="modal-section-title">&#128202; Visuelle MariaDB Tabellen-Vorschau</div>
${htmlTable}
<div class="modal-section-title">&#128196; Generiertes SQL-Skript</div>
<pre class="sql-code">${escapeHtml(sql)}</pre>`;
window.sqlModalState = { side, tableName, items, columns, colNames, pk, overrides, renames, editorOpen: false, sql: '' };
window.openModal('&#128451;&#65039; MariaDB Workbench: ' + escapeHtml(tableName), body, [
window.openModal('&#128451;&#65039; MariaDB Workbench: ' + escapeHtml(tableName), '<div>Wird geladen&hellip;</div>', [
{ label: 'Als .sql speichern', handler: () => {
downloadBlob(sql, tableName + '.sql', 'application/sql');
window.showToast('Gespeichert: ' + tableName + '.sql', 'success');
const st = window.sqlModalState;
downloadBlob(st.sql, st.tableName + '.sql', 'application/sql');
window.showToast('Gespeichert: ' + st.tableName + '.sql', 'success');
} },
{ label: 'SQL kopieren', primary: true, handler: () => {
navigator.clipboard.writeText(sql).then(() => window.showToast('SQL-Skript kopiert.', 'success'));
navigator.clipboard.writeText(window.sqlModalState.sql).then(() => window.showToast('SQL-Skript kopiert.', 'success'));
} }
]);
renderSqlModalBody();
};
/* ===========================================================
@@ -2076,6 +2615,30 @@ window.openSettingsModal = function() {
<label class="field-label">Sicherung einspielen</label>
<input type="file" id="importConfigFile" accept=".json" onchange="importConfig(event)" style="font-size:12px">
<div class="field-hint">Vorhandene Eintr&auml;ge werden erg&auml;nzt, nicht gel&ouml;scht. Snippets und Profile mit gleichem Namen kommen zus&auml;tzlich hinzu.</div>
</div>
<div class="modal-section-title">&#8987; Automatische Sicherung</div>
<div class="form-row">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer">
<input type="checkbox" id="setAutoSave" ${s.autoSaveEnabled ? 'checked' : ''} style="width:auto">
In regelm&auml;&szlig;igen Abst&auml;nden als Datei herunterladen
</label>
<div class="field-hint">
L&auml;dt jeden Editor mit ge&auml;ndertem Inhalt automatisch als JSON-Datei herunter
(Dateiname mit Zeitstempel, z. B. <code>bestellung_autosave_2026-09-17-1530.json</code>).
Nur ein zus&auml;tzliches Sicherheitsnetz &ndash; ersetzt nicht das gezielte Speichern per
&bdquo;Export&ldquo;, und die heruntergeladenen Dateien sammeln sich im Download-Ordner an,
du r&auml;umst sie also gelegentlich selbst auf.
</div>
</div>
<div class="form-row">
<label class="field-label">Abstand</label>
<select id="setAutoSaveInterval">
<option value="5"${sel(String(s.autoSaveIntervalMinutes), '5')}>Alle 5 Minuten</option>
<option value="10"${sel(String(s.autoSaveIntervalMinutes), '10')}>Alle 10 Minuten</option>
<option value="15"${sel(String(s.autoSaveIntervalMinutes), '15')}>Alle 15 Minuten</option>
<option value="30"${sel(String(s.autoSaveIntervalMinutes), '30')}>Alle 30 Minuten</option>
</select>
</div>`;
window.openModal('&#9881;&#65039; Einstellungen', body, [
@@ -2086,8 +2649,11 @@ window.openSettingsModal = function() {
csvExportDelimiter: document.getElementById('setCsvExportDelim').value,
xmlRootName: document.getElementById('setXmlRoot').value.trim() || 'root',
xmlItemName: document.getElementById('setXmlItem').value.trim() || 'item',
webhookUrl: document.getElementById('setWebhook').value.trim()
webhookUrl: document.getElementById('setWebhook').value.trim(),
autoSaveEnabled: document.getElementById('setAutoSave').checked,
autoSaveIntervalMinutes: parseInt(document.getElementById('setAutoSaveInterval').value, 10) || 10
}));
window.restartAutoSaveTimer();
window.closeModal();
window.showToast('Einstellungen gespeichert.', 'success');
} }
@@ -2178,14 +2744,27 @@ window.openHelpModal = function(startTab) {
<h4>Der Dokumentname ist wichtiger, als er aussieht</h4>
<p>Der Name oben links (&bdquo;Dokument 1&ldquo;) wird beim Export zum Dateinamen und beim
SQL-Export zum Tabellennamen. Vergib also am besten gleich einen sprechenden Namen.</p>
SQL-Export zum Tabellennamen. Vergib also am besten gleich einen sprechenden Namen &ndash;
beim &Ouml;ffnen einer Datei geschieht das ohnehin automatisch: Der Name wird auf den
Dateinamen ohne Endung gesetzt, damit sich eine bearbeitete Datei direkt wieder unter
demselben Namen speichern l&auml;sst. Im &Ouml;ffnen-Men&uuml; findest du au&szlig;erdem die
letzten f&uuml;nf Dateien, die du in diesem Editor ge&ouml;ffnet hast &ndash; nur als
Ged&auml;chtnisst&uuml;tze, ein erneutes Laden ersetzt das nicht.</p>
<h4>Pfad eines Feldes kopieren</h4>
<p>Ein Klick auf ein Feld im Baum, dann oben auf &bdquo;Pfad&ldquo; &ndash; kopiert eine
Notation wie <code>Positionen[2].Preis</code> in die Zwischenablage. Praktisch, um schnell
an einen n8n-Ausdruck oder eine Doku-Notiz zu kommen, ohne den Pfad von Hand abzuz&auml;hlen.</p>
<h4>Alles bleibt erhalten</h4>
<p>Inhalte, Namen, Einstellungen, Snippets und Datenbank-Profile liegen im lokalen Speicher
deines Browsers. Nach einem Neustart ist alles wieder da. Es wird nichts an einen fremden
Server gesendet &ndash; au&szlig;er du benutzt bewusst URL-Import, Webhook oder Datenbank.</p>
<p>Weil der Speicher am Browser h&auml;ngt: Beim Wechsel auf einen anderen Rechner nimmst du
deine Konfiguration &uuml;ber <b>Einstellungen &rarr; Sicherung exportieren</b> mit.</p>
deine Konfiguration &uuml;ber <b>Einstellungen &rarr; Sicherung exportieren</b> mit. Wer mag,
schaltet dort zus&auml;tzlich eine automatische Sicherung als Download in festem Abstand ein
(standardm&auml;&szlig;ig aus) &ndash; ein zus&auml;tzliches Sicherheitsnetz, kein Ersatz f&uuml;r
das gezielte Speichern.</p>
<h4>Die drei Ansichten des Editors</h4>
<ul>
@@ -2211,7 +2790,7 @@ window.openHelpModal = function(startTab) {
<ul>
<li><b>CSV / Excel</b> &ndash; brauchen eine Liste gleichartiger Objekte. Ein einzelnes Objekt wird zu einer Zeile, einfache Werte bekommen ersatzweise die Spalten <code>id</code> und <code>wert</code>.</li>
<li><b>XML</b> &ndash; Felder mit <code>@</code> werden wieder zu Attributen. Wurzel- und Listen-Tag stellst du in den Einstellungen ein.</li>
<li><b>MariaDB</b> &ndash; erzeugt <code>CREATE TABLE</code> samt <code>INSERT</code>s. Die Spaltentypen werden aus den Werten geraten; pr&uuml;fe sie, bevor du das Skript produktiv einspielst.</li>
<li><b>MariaDB</b> &ndash; erzeugt <code>CREATE TABLE</code> samt <code>INSERT</code>s. Die Spaltentypen werden aus den Werten geraten, inklusive automatischer Unterscheidung zwischen <code>INT</code> und dem gr&ouml;&szlig;eren <code>BIGINT</code> bei sehr gro&szlig;en Zahlen &ndash; passt eine Zuordnung trotzdem nicht, l&auml;sst sie sich im Bereich &bdquo;Spalten anpassen&ldquo; gezielt &uuml;berschreiben, inklusive L&auml;ngenangabe, Umbenennung und Erkl&auml;rung zu jedem Typ. Standardm&auml;&szlig;ig bekommt die Tabelle eine eigene, fortlaufende <code>id</code>-Spalte als Prim&auml;rschl&uuml;ssel &ndash; per Häkchen l&auml;sst sich stattdessen eine vorhandene Spalte dazu machen, oder mehrere zusammen als zusammengesetzter Schl&uuml;ssel.</li>
<li><b>JSON-Schema</b> &ndash; leitet eine Strukturbeschreibung ab, die du zur Validierung weiterverwenden kannst.</li>
</ul>
@@ -2329,8 +2908,7 @@ lib/jsoneditor/standalone.js</pre>
<p>Ver&ouml;ffentlicht unter der MIT-Lizenz. Du darfst sie frei verwenden, ver&auml;ndern
und weitergeben, solange der Copyright- und Lizenzhinweis erhalten bleibt. Sie wird
ohne Gew&auml;hrleistung bereitgestellt.</p>
<p>Quellcode: <a href="#" onclick="return false;">im Repository hinterlegt</a>
&ndash; die Adresse tr&auml;gst du hier im Quelltext ein, sobald das Repository steht.</p>
<p>Quellcode: <a href="https://git.obraasch.de/olli1986/JSON-Studio" target="_blank" rel="noopener noreferrer">git.obraasch.de/olli1986/JSON-Studio</a></p>
<h4>Verwendete Bibliotheken</h4>
<ul>