From cf855864469b3dfc247938fbbbe672cbb7ea856b Mon Sep 17 00:00:00 2001
From: Oliver Braasch
Date: Fri, 18 Sep 2026 10:08:45 +0200
Subject: [PATCH] Dateien nach "/" hochladen
---
json-workbench.html | 750 +++++++++++++++++++++++++++++++++++++++-----
1 file changed, 664 insertions(+), 86 deletions(-)
diff --git a/json-workbench.html b/json-workbench.html
index 74580ba..0da4bf1 100644
--- a/json-workbench.html
+++ b/json-workbench.html
@@ -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 @@
🌐 Von URL laden…
🗃️ Aus Datenbank laden…
+
@@ -589,6 +687,7 @@
+ 📍 Pfad
⤢ Vollbild
@@ -642,6 +741,7 @@
🌐 Von URL laden…
🗃️ Aus Datenbank laden…
+
@@ -677,6 +777,7 @@
+ 📍 Pfad
⤢ Vollbild
@@ -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, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
@@ -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 = '
Zuletzt ge\u00f6ffnet
' +
+ list.map(f => '' +
+ fileIconFor(f.name) + ' ' + escapeHtml(f.name) + '
').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 = '';
+ if (pkList.length === 0) html += 'id (PK, automatisch) ';
+ colNames.forEach(c => {
+ const marker = pkList.includes(c) ? ' 🔑 PK' : '';
+ const renamed = outNames[c] !== c;
+ const label = renamed
+ ? escapeHtml(outNames[c]) + ' umbenannt '
+ : escapeHtml(c);
+ html += '' + label + ' (' + columns[c] + ')' + marker + ' ';
+ });
+ html += ' ';
+ items.forEach((item, idx) => {
+ html += '';
+ if (pkList.length === 0) html += '' + (idx + 1) + ' ';
+ colNames.forEach(c => {
+ const v = item[c];
+ const d = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : String(v === undefined || v === null ? '' : v);
+ html += '' + escapeHtml(d) + ' ';
+ });
+ html += ' ';
+ });
+ html += '
';
+ 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 =>
+ '' +
+ escapeHtml(SQL_TYPE_CATALOG[t].label) + ' '
+ ).join('');
+
+ const lengthField = info.hasLength
+ ? ` `
+ : ' ';
+
+ return `
+
+
${escapeHtml(c)}${isCustomized ? ' angepasst ' : ''}
+
+
${options}
+ ${lengthField}
+
${escapeHtml(info.desc)}
+
`;
+ }).join('');
+
+ return `
+
+ ⚙️ Spalten anpassen (Typ & Name)
+
+ Die Typen werden automatisch aus den Werten geraten. Passt eine Zuordnung nicht –
+ etwa weil eine Spalte eigentlich Geldbeträge enthält oder eine ID größer
+ ist als erwartet – kannst du sie hier gezielt überschreiben. Im zweiten Feld lässt
+ sich außerdem der Spaltenname im SQL ändern, falls er nicht dem JSON-Feldnamen
+ entsprechen soll.
+
+ ${rows}
+ `;
+}
+
+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 =>
+ `
+
+ ${escapeHtml(c)}
+ `
+ ).join('');
+
+ document.getElementById('modalContent').innerHTML = `
+
+ ${buildColumnTypeEditor()}
+ 📊 Visuelle MariaDB Tabellen-Vorschau
+ ${buildSqlPreviewTable(st.items, st.colNames, resolvedColumns, st.pk, outNames)}
+ 📄 Generiertes SQL-Skript
+ ${escapeHtml(sql)} `;
+}
+
+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 = 'id (PK) ';
- colNames.forEach(c => { htmlTable += '' + escapeHtml(c) + ' (' + columns[c] + ') '; });
- htmlTable += '';
- items.forEach((item, idx) => {
- htmlTable += '' + (idx + 1) + ' ';
- colNames.forEach(c => {
- const v = item[c];
- const d = (typeof v === 'object' && v !== null) ? JSON.stringify(v) : String(v === undefined || v === null ? '' : v);
- htmlTable += '' + escapeHtml(d) + ' ';
- });
- htmlTable += ' ';
- });
- htmlTable += '
';
+ // 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 = `
- 📊 Visuelle MariaDB Tabellen-Vorschau
- ${htmlTable}
- 📄 Generiertes SQL-Skript
- ${escapeHtml(sql)} `;
+ window.sqlModalState = { side, tableName, items, columns, colNames, pk, overrides, renames, editorOpen: false, sql: '' };
- window.openModal('🗃️ MariaDB Workbench: ' + escapeHtml(tableName), body, [
+ window.openModal('🗃️ MariaDB Workbench: ' + escapeHtml(tableName), 'Wird geladen…
', [
{ 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() {
Sicherung einspielen
Vorhandene Einträge werden ergänzt, nicht gelöscht. Snippets und Profile mit gleichem Namen kommen zusätzlich hinzu.
+
+
+ ⌛ Automatische Sicherung
+
+
+ Abstand
+
+ Alle 5 Minuten
+ Alle 10 Minuten
+ Alle 15 Minuten
+ Alle 30 Minuten
+
`;
window.openModal('⚙️ 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) {
Der Dokumentname ist wichtiger, als er aussieht
Der Name oben links („Dokument 1“) wird beim Export zum Dateinamen und beim
- SQL-Export zum Tabellennamen. Vergib also am besten gleich einen sprechenden Namen.
+ SQL-Export zum Tabellennamen. Vergib also am besten gleich einen sprechenden Namen –
+ beim Ö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ässt. Im Öffnen-Menü findest du außerdem die
+ letzten fünf Dateien, die du in diesem Editor geöffnet hast – nur als
+ Gedächtnisstütze, ein erneutes Laden ersetzt das nicht.
+
+ Pfad eines Feldes kopieren
+ Ein Klick auf ein Feld im Baum, dann oben auf „Pfad“ – kopiert eine
+ Notation wie Positionen[2].Preis in die Zwischenablage. Praktisch, um schnell
+ an einen n8n-Ausdruck oder eine Doku-Notiz zu kommen, ohne den Pfad von Hand abzuzählen.
Alles bleibt erhalten
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 – außer du benutzt bewusst URL-Import, Webhook oder Datenbank.
Weil der Speicher am Browser hängt: Beim Wechsel auf einen anderen Rechner nimmst du
- deine Konfiguration über Einstellungen → Sicherung exportieren mit.
+ deine Konfiguration über Einstellungen → Sicherung exportieren mit. Wer mag,
+ schaltet dort zusätzlich eine automatische Sicherung als Download in festem Abstand ein
+ (standardmäßig aus) – ein zusätzliches Sicherheitsnetz, kein Ersatz für
+ das gezielte Speichern.
Die drei Ansichten des Editors
@@ -2211,7 +2790,7 @@ window.openHelpModal = function(startTab) {
CSV / Excel – brauchen eine Liste gleichartiger Objekte. Ein einzelnes Objekt wird zu einer Zeile, einfache Werte bekommen ersatzweise die Spalten id und wert.
XML – Felder mit @ werden wieder zu Attributen. Wurzel- und Listen-Tag stellst du in den Einstellungen ein.
- MariaDB – erzeugt CREATE TABLE samt INSERTs. Die Spaltentypen werden aus den Werten geraten; prüfe sie, bevor du das Skript produktiv einspielst.
+ MariaDB – erzeugt CREATE TABLE samt INSERTs. Die Spaltentypen werden aus den Werten geraten, inklusive automatischer Unterscheidung zwischen INT und dem größeren BIGINT bei sehr großen Zahlen – passt eine Zuordnung trotzdem nicht, lässt sie sich im Bereich „Spalten anpassen“ gezielt überschreiben, inklusive Längenangabe, Umbenennung und Erklärung zu jedem Typ. Standardmäßig bekommt die Tabelle eine eigene, fortlaufende id-Spalte als Primärschlüssel – per Häkchen lässt sich stattdessen eine vorhandene Spalte dazu machen, oder mehrere zusammen als zusammengesetzter Schlüssel.
JSON-Schema – leitet eine Strukturbeschreibung ab, die du zur Validierung weiterverwenden kannst.
@@ -2329,8 +2908,7 @@ lib/jsoneditor/standalone.js
Veröffentlicht unter der MIT-Lizenz. Du darfst sie frei verwenden, verändern
und weitergeben, solange der Copyright- und Lizenzhinweis erhalten bleibt. Sie wird
ohne Gewährleistung bereitgestellt.
- Quellcode: im Repository hinterlegt
- – die Adresse trägst du hier im Quelltext ein, sobald das Repository steht.
+ Quellcode: git.obraasch.de/olli1986/JSON-Studio
Verwendete Bibliotheken