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-table-mode,
body.dark-mode .jse-welcome, body.dark-mode .jse-welcome,
body.dark-mode .jse-theme-dark { body.dark-mode .jse-theme-dark {
--jse-bg-color: #16171a !important; /* Grundfarben. --jse-background-color ist die wichtigste hier: Sie ist
--jse-main-border: 1px solid #2a2b30 !important; 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: #1c1d21 !important;
--jse-theme-color-highlight: #24252b !important; --jse-theme-color-highlight: #24252b !important;
--jse-menu-color: #e7e8ea !important;
--jse-text-color: #e7e8ea !important; --jse-text-color: #e7e8ea !important;
--jse-text-color-min: #8b8d93 !important; --jse-text-readonly: #8b8d93 !important;
--jse-key-color: #6cb6ff !important; --jse-text-color-inverse: #e7e8ea !important;
--jse-value-color-string: #9fd6a0 !important; --jse-error-color: #e5484d !important;
--jse-value-color-number: #f7b267 !important; --jse-warning-color: #f7b267 !important;
--jse-value-color-boolean: #f47067 !important; --jse-info-color: #6cb6ff !important;
--jse-value-color-null: #8b8d93 !important;
--jse-hover-background-color: #22232a !important; --jse-main-border: 1px solid #2a2b30 !important;
--jse-selection-background-color: #2a3340 !important; --jse-menu-color: #e7e8ea !important;
--jse-active-line-background-color: rgba(255,255,255,0.04) !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: #1c1d21 !important;
--jse-panel-background-hover: #24252b !important;
--jse-panel-color: #e7e8ea !important; --jse-panel-color: #e7e8ea !important;
--jse-panel-color-readonly: #8b8d93 !important; --jse-panel-color-readonly: #8b8d93 !important;
--jse-panel-border: 1px solid #2a2b30 !important; --jse-panel-border: 1px solid #2a2b30 !important;
--jse-panel-button-color: #c7c8cc !important; --jse-panel-button-color: #c7c8cc !important;
--panel-button-color-highlight: #ffffff !important;
--jse-panel-button-color-highlight: #ffffff !important; --jse-panel-button-color-highlight: #ffffff !important;
--jse-panel-button-background: transparent !important; --jse-panel-button-background: transparent !important;
--jse-panel-button-background-highlight: rgba(255,255,255,0.08) !important; --jse-panel-button-background-highlight: rgba(255,255,255,0.08) !important;
--jse-navigation-bar-background: #1c1d21 !important; --jse-navigation-bar-background: #1c1d21 !important;
--jse-navigation-bar-background-highlight: #24252b !important; --jse-navigation-bar-background-highlight: #24252b !important;
--jse-navigation-bar-dropdown-color: #e7e8ea !important; --jse-navigation-bar-dropdown-color: #e7e8ea !important;
--jse-context-menu-background: #1c1d21 !important; --jse-context-menu-background: #1c1d21 !important;
--jse-context-menu-background-highlight: #24252b !important; --jse-context-menu-background-highlight: #24252b !important;
--jse-context-menu-color: #e7e8ea !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: #1c1d21 !important;
--jse-context-menu-pointer-background-highlight: #26272d !important; --jse-context-menu-pointer-background-highlight: #26272d !important;
--jse-context-menu-pointer-color: #e7e8ea !important; --jse-context-menu-pointer-color: #e7e8ea !important;
--jse-context-menu-separator-color: #2a2b30 !important;
--jse-context-menu-hover-background-color: #26272d !important; /* Schlüssel/Werte im Baum */
--jse-modal-background: #16171a !important; --jse-key-color: #6cb6ff !important;
--jse-modal-overlay-background: rgba(0,0,0,0.6) !important; --jse-value-color: #e7e8ea !important;
--jse-input-background: #101114 !important; --jse-value-color-string: #9fd6a0 !important;
--jse-button-background: #26272d !important; --jse-value-color-number: #f7b267 !important;
--jse-button-background-highlight: #33343b !important; --jse-value-color-boolean: #f47067 !important;
--jse-button-color: #e7e8ea !important; --jse-value-color-null: #8b8d93 !important;
--jse-tooltip-background: #1c1d21 !important; --jse-value-color-url: #8fd1ff !important;
--jse-tooltip-color: #e7e8ea !important; --jse-delimiter-color: #55565c !important;
--jse-tooltip-error-background: #3a1414 !important; --jse-edit-outline: 2px solid #6cb6ff !important;
--jse-tooltip-error-color: #ff9a9a !important;
--jse-search-match-background: rgba(108,182,255,0.22) !important; /* Ausgewählt/gehovert - hier lag der gemeldete Fehler: Ein editiertes
--jse-search-match-color: #e7e8ea !important; Feld bekommt eine eigene Box auf Basis von --jse-background-color
--jse-search-match-outline: 1px solid #6cb6ff !important; (siehe oben) statt einer transparenten Fläche; ohne die beiden
--jse-search-match-active-background: #3b78b0 !important; folgenden Variablen blieb außerdem eine früher ausgewählte, aber
--jse-search-match-active-color: #ffffff !important; gerade inaktive Zeile hell. */
--jse-search-match-active-outline: 1px solid #8fcaff !important; --jse-contents-background-color: transparent !important;
--jse-tag-background: #24252b !important; --jse-selection-background-color: #2a3340 !important;
--jse-tag-color: #e7e8ea !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-background-color: #1c1d21 !important;
--jse-collapsed-items-selected-background-color: #24252b !important; --jse-collapsed-items-selected-background-color: #24252b !important;
--jse-collapsed-items-link-color: #6cb6ff !important; --jse-collapsed-items-link-color: #6cb6ff !important;
--jse-collapsed-items-link-color-highlight: #8fcaff !important; --jse-collapsed-items-link-color-highlight: #8fcaff !important;
--jse-collapsed-items-selected-link-color: #ffffff !important;
--jse-delimiter-color: #55565c !important; --jse-search-match-color: #16171a !important;
--jse-edit-outline: 2px solid #6cb6ff !important; --jse-search-match-outline: 2px solid #ffd633 !important;
--jse-messages-info-background: #1c1d21 !important; --jse-search-match-active-color: #16171a !important;
--jse-messages-info-color: #8fcaff !important;
--jse-messages-success-background: #123321 !important; --jse-tag-background: #24252b !important;
--jse-messages-success-color: #6bd8a1 !important; --jse-tag-color: #e7e8ea !important;
--jse-messages-error-background: #3a1414 !important;
--jse-messages-error-color: #ff9a9a !important; /* Tabellen-Ansicht */
--jse-error-color: #e5484d !important; --jse-table-header-background: #1c1d21 !important;
--jse-warning-color: #f7b267 !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: #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; background-color: #16171a !important;
color: #e7e8ea !important; color: #e7e8ea !important;
} }
@@ -439,6 +490,52 @@
.btn-mini:hover { border-color: var(--accent); color: var(--accent); } .btn-mini:hover { border-color: var(--accent); color: var(--accent); }
.btn-mini.danger:hover { border-color: var(--danger); color: var(--danger); } .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 { kbd {
background: var(--panel-bg); border: 1px solid var(--border-color); border-bottom-width: 2px; 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; border-radius: 5px; padding: 2px 7px; font-family: "SFMono-Regular", Consolas, monospace;
@@ -554,6 +651,7 @@
<div class="divider"></div> <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="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> <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>
</div> </div>
@@ -589,6 +687,7 @@
</div> </div>
</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> <button class="nav-btn" onclick="toggleFullscreen('leftPane')" title="Diesen Editor im Vollbild anzeigen">&#10530; <span class="btn-text">Vollbild</span></button>
</div> </div>
</div> </div>
@@ -642,6 +741,7 @@
<div class="divider"></div> <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="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> <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>
</div> </div>
@@ -677,6 +777,7 @@
</div> </div>
</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> <button class="nav-btn" onclick="toggleFullscreen('rightPane')" title="Diesen Editor im Vollbild anzeigen">&#10530; <span class="btn-text">Vollbild</span></button>
</div> </div>
</div> </div>
@@ -795,7 +896,9 @@ const DEFAULT_SETTINGS = {
webhookUrl: '', webhookUrl: '',
xmlRootName: 'root', xmlRootName: 'root',
xmlItemName: 'item', xmlItemName: 'item',
splitRatio: 0.5 splitRatio: 0.5,
autoSaveEnabled: false,
autoSaveIntervalMinutes: 10
}; };
function loadSettings() { function loadSettings() {
@@ -810,6 +913,42 @@ function saveSettings(s) {
} }
window.wbSettings = loadSettings(); 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) { function escapeHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;'); .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 savedDarkMode = localStorage.getItem('wb_dark_mode');
const isDarkDefault = savedDarkMode !== null ? savedDarkMode === 'true' : true; 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({ window.editorLeft = createJSONEditor({
target: document.getElementById('jsoneditorLeft'), 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({ window.editorRight = createJSONEditor({
target: document.getElementById('jsoneditorRight'), 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'; window.activeSide = 'left';
@@ -925,6 +1102,46 @@ window.applyTheme = function(isDark) {
window.cycleTheme = function() { window.applyTheme(!document.body.classList.contains('dark-mode')); }; window.cycleTheme = function() { window.applyTheme(!document.body.classList.contains('dark-mode')); };
window.applyTheme(isDarkDefault); 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 4. Daten-Helfer
=========================================================== */ =========================================================== */
@@ -965,7 +1182,54 @@ function readFileAsText(file, callback) {
reader.readAsText(file); 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) { window.toggleFullscreen = function(paneId) {
const elem = document.getElementById(paneId); const elem = document.getElementById(paneId);
@@ -983,6 +1247,7 @@ window.importJsonFile = function(event, side) {
readFileAsText(file, (text) => { readFileAsText(file, (text) => {
try { setData(side, JSON.parse(text)); } try { setData(side, JSON.parse(text)); }
catch (err) { ed(side).set({ text: text }); window.saveAppState(); } catch (err) { ed(side).set({ text: text }); window.saveAppState(); }
setTitleFromFilename(side, file.name);
window.showToast('Geladen: ' + file.name, 'success'); window.showToast('Geladen: ' + file.name, 'success');
}); });
event.target.value = ''; event.target.value = '';
@@ -996,6 +1261,7 @@ window.importCsvFile = function(event, side) {
delimiter: window.wbSettings.csvDelimiter || '', delimiter: window.wbSettings.csvDelimiter || '',
complete: (res) => { complete: (res) => {
setData(side, res.data); setData(side, res.data);
setTitleFromFilename(side, file.name);
const d = res.meta && res.meta.delimiter ? res.meta.delimiter : '?'; const d = res.meta && res.meta.delimiter ? res.meta.delimiter : '?';
window.showToast('CSV importiert: ' + file.name + ' (Trennzeichen "' + d + '")', 'success'); window.showToast('CSV importiert: ' + file.name + ' (Trennzeichen "' + d + '")', 'success');
}, },
@@ -1015,10 +1281,12 @@ window.importExcelFile = function(event, side) {
const out = {}; const out = {};
wb.SheetNames.forEach(n => { out[n] = XLSX.utils.sheet_to_json(wb.Sheets[n], { defval: null }); }); wb.SheetNames.forEach(n => { out[n] = XLSX.utils.sheet_to_json(wb.Sheets[n], { defval: null }); });
setData(side, out); setData(side, out);
setTitleFromFilename(side, file.name);
window.showToast('Excel importiert: ' + file.name + ' (' + wb.SheetNames.length + ' Bl\u00e4tter)', 'success'); window.showToast('Excel importiert: ' + file.name + ' (' + wb.SheetNames.length + ' Bl\u00e4tter)', 'success');
} else { } else {
const name = wb.SheetNames[0]; const name = wb.SheetNames[0];
setData(side, XLSX.utils.sheet_to_json(wb.Sheets[name], { defval: null })); setData(side, XLSX.utils.sheet_to_json(wb.Sheets[name], { defval: null }));
setTitleFromFilename(side, file.name);
window.showToast('Excel importiert: ' + file.name + ' (' + name + ')', 'success'); window.showToast('Excel importiert: ' + file.name + ' (' + name + ')', 'success');
} }
} catch (err) { window.showToast('Excel-Fehler: ' + err.message, 'error'); } } catch (err) { window.showToast('Excel-Fehler: ' + err.message, 'error'); }
@@ -1034,6 +1302,7 @@ window.importYamlFile = function(event, side) {
try { try {
const docs = jsyaml.loadAll(text); const docs = jsyaml.loadAll(text);
setData(side, docs.length === 1 ? docs[0] : docs); setData(side, docs.length === 1 ? docs[0] : docs);
setTitleFromFilename(side, file.name);
window.showToast('YAML importiert: ' + file.name, 'success'); window.showToast('YAML importiert: ' + file.name, 'success');
} catch (err) { window.showToast('YAML-Fehler: ' + err.message, 'error'); } } 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.'); if (doc.querySelector('parsererror')) throw new Error('XML ist nicht wohlgeformt.');
const root = doc.documentElement; const root = doc.documentElement;
setData(side, { [root.nodeName]: xmlNodeToJson(root) }); setData(side, { [root.nodeName]: xmlNodeToJson(root) });
setTitleFromFilename(side, file.name);
window.showToast('XML importiert: ' + file.name, 'success'); window.showToast('XML importiert: ' + file.name, 'success');
} catch (err) { window.showToast('XML-Fehler: ' + err.message, 'error'); } } catch (err) { window.showToast('XML-Fehler: ' + err.message, 'error'); }
}); });
@@ -1102,6 +1372,7 @@ window.importSqlFile = function(event, side) {
const names = Object.keys(tables); const names = Object.keys(tables);
if (names.length === 0) throw new Error('Keine INSERT-Anweisungen gefunden.'); if (names.length === 0) throw new Error('Keine INSERT-Anweisungen gefunden.');
setData(side, names.length === 1 ? tables[names[0]] : tables); setData(side, names.length === 1 ? tables[names[0]] : tables);
setTitleFromFilename(side, file.name);
window.showToast('SQL importiert: ' + names.length + ' Tabelle(n), ' + window.showToast('SQL importiert: ' + names.length + ' Tabelle(n), ' +
names.reduce((s, n) => s + tables[n].length, 0) + ' Zeilen', 'success'); names.reduce((s, n) => s + tables[n].length, 0) + ' Zeilen', 'success');
} catch (err) { window.showToast('SQL-Fehler: ' + err.message, 'error'); } } catch (err) { window.showToast('SQL-Fehler: ' + err.message, 'error'); }
@@ -1382,8 +1653,40 @@ window.copyToClip = function(side, format) {
7. MariaDB-SQL erzeugen 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) { 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 === 'boolean') return 'TINYINT(1)';
if (typeof val === 'object' && val !== null) return 'JSON'; if (typeof val === 'object' && val !== null) return 'JSON';
if (typeof val === 'string') { if (typeof val === 'string') {
@@ -1400,68 +1703,304 @@ function formatSqlValue(val) {
return `'${String(val).replace(/'/g, "''")}'`; 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 = {}; const columns = {};
items.forEach(item => { items.forEach(item => {
if (typeof item === 'object' && item !== null) { if (typeof item === 'object' && item !== null) {
for (const [key, val] of Object.entries(item)) { for (const [key, val] of Object.entries(item)) {
if (!columns[key]) columns[key] = inferSqlType(val); const t = inferSqlType(val);
else if (columns[key] === 'INT' && typeof val === 'number' && !Number.isInteger(val)) columns[key] = 'DOUBLE'; 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`; let sql = `-- MariaDB Tabellenstruktur f\u00fcr \`${tableName}\`\n`;
sql += `CREATE TABLE IF NOT EXISTS \`${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 += `\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n`;
sql += `-- Daten-Imports f\u00fcr \`${tableName}\`\n`; sql += `-- Daten-Imports f\u00fcr \`${tableName}\`\n`;
const outColList = colNames.map(c => outNames[c]).join('`, `');
items.forEach(item => { items.forEach(item => {
const vals = colNames.map(c => formatSqlValue(item[c])); 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) { window.openSqlModal = function(side) {
const data = window.getJsonData(side); const data = window.getJsonData(side);
if (data === null) return window.showToast('Bitte zuerst g\u00fcltige Daten in den Editor laden.', 'error'); 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 tableName = docName(side).toLowerCase().replace(/[^a-z0-9_]/g, '_');
const items = toRows(data); 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>'; // Zuvor für dieses Dokument gewählte Einstellungen übernehmen - aber nur,
colNames.forEach(c => { htmlTable += '<th>' + escapeHtml(c) + ' (' + columns[c] + ')</th>'; }); // soweit sie noch zu den gerade vorhandenen Spalten passen.
htmlTable += '</tr></thead><tbody>'; const prefs = loadSqlPrefs(side);
items.forEach((item, idx) => { const pk = prefs.pk.filter(c => colNames.includes(c));
htmlTable += '<tr><td>' + (idx + 1) + '</td>'; const overrides = {};
colNames.forEach(c => { colNames.forEach(c => { if (prefs.overrides[c]) overrides[c] = prefs.overrides[c]; });
const v = item[c]; const renames = {};
const d = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : String(v === undefined || v === null ? '' : v); colNames.forEach(c => { if (prefs.renames[c]) renames[c] = prefs.renames[c]; });
htmlTable += '<td>' + escapeHtml(d) + '</td>';
});
htmlTable += '</tr>';
});
htmlTable += '</tbody></table></div>';
const body = ` window.sqlModalState = { side, tableName, items, columns, colNames, pk, overrides, renames, editorOpen: false, sql: '' };
<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.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: () => { { label: 'Als .sql speichern', handler: () => {
downloadBlob(sql, tableName + '.sql', 'application/sql'); const st = window.sqlModalState;
window.showToast('Gespeichert: ' + tableName + '.sql', 'success'); downloadBlob(st.sql, st.tableName + '.sql', 'application/sql');
window.showToast('Gespeichert: ' + st.tableName + '.sql', 'success');
} }, } },
{ label: 'SQL kopieren', primary: true, handler: () => { { 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> <label class="field-label">Sicherung einspielen</label>
<input type="file" id="importConfigFile" accept=".json" onchange="importConfig(event)" style="font-size:12px"> <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 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>`; </div>`;
window.openModal('&#9881;&#65039; Einstellungen', body, [ window.openModal('&#9881;&#65039; Einstellungen', body, [
@@ -2086,8 +2649,11 @@ window.openSettingsModal = function() {
csvExportDelimiter: document.getElementById('setCsvExportDelim').value, csvExportDelimiter: document.getElementById('setCsvExportDelim').value,
xmlRootName: document.getElementById('setXmlRoot').value.trim() || 'root', xmlRootName: document.getElementById('setXmlRoot').value.trim() || 'root',
xmlItemName: document.getElementById('setXmlItem').value.trim() || 'item', 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.closeModal();
window.showToast('Einstellungen gespeichert.', 'success'); window.showToast('Einstellungen gespeichert.', 'success');
} } } }
@@ -2178,14 +2744,27 @@ window.openHelpModal = function(startTab) {
<h4>Der Dokumentname ist wichtiger, als er aussieht</h4> <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 <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> <h4>Alles bleibt erhalten</h4>
<p>Inhalte, Namen, Einstellungen, Snippets und Datenbank-Profile liegen im lokalen Speicher <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 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> 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 <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> <h4>Die drei Ansichten des Editors</h4>
<ul> <ul>
@@ -2211,7 +2790,7 @@ window.openHelpModal = function(startTab) {
<ul> <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>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>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> <li><b>JSON-Schema</b> &ndash; leitet eine Strukturbeschreibung ab, die du zur Validierung weiterverwenden kannst.</li>
</ul> </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 <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 und weitergeben, solange der Copyright- und Lizenzhinweis erhalten bleibt. Sie wird
ohne Gew&auml;hrleistung bereitgestellt.</p> ohne Gew&auml;hrleistung bereitgestellt.</p>
<p>Quellcode: <a href="#" onclick="return false;">im Repository hinterlegt</a> <p>Quellcode: <a href="https://git.obraasch.de/olli1986/JSON-Studio" target="_blank" rel="noopener noreferrer">git.obraasch.de/olli1986/JSON-Studio</a></p>
&ndash; die Adresse tr&auml;gst du hier im Quelltext ein, sobald das Repository steht.</p>
<h4>Verwendete Bibliotheken</h4> <h4>Verwendete Bibliotheken</h4>
<ul> <ul>