Dateien nach "/" hochladen
This commit is contained in:
1 parent
4c2656d7d7
commit
cf85586446
1 file changed
+664
-86
+664
-86
@@ -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">🌐 Von URL laden…</a>
|
<a href="#" onclick="openUrlImportModal('left'); return false;" title="Daten direkt von einer API-Adresse laden">🌐 Von URL laden…</a>
|
||||||
<a href="#" onclick="openDbLoadModal('left'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">🗃️ Aus Datenbank laden…</a>
|
<a href="#" onclick="openDbLoadModal('left'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">🗃️ Aus Datenbank laden…</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)">📍 <span class="btn-text">Pfad</span></button>
|
||||||
<button class="nav-btn" onclick="toggleFullscreen('leftPane')" title="Diesen Editor im Vollbild anzeigen">⤢ <span class="btn-text">Vollbild</span></button>
|
<button class="nav-btn" onclick="toggleFullscreen('leftPane')" title="Diesen Editor im Vollbild anzeigen">⤢ <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">🌐 Von URL laden…</a>
|
<a href="#" onclick="openUrlImportModal('right'); return false;" title="Daten direkt von einer API-Adresse laden">🌐 Von URL laden…</a>
|
||||||
<a href="#" onclick="openDbLoadModal('right'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">🗃️ Aus Datenbank laden…</a>
|
<a href="#" onclick="openDbLoadModal('right'); return false;" title="Tabelle aus einer angebundenen Datenbank laden">🗃️ Aus Datenbank laden…</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)">📍 <span class="btn-text">Pfad</span></button>
|
||||||
<button class="nav-btn" onclick="toggleFullscreen('rightPane')" title="Diesen Editor im Vollbild anzeigen">⤢ <span class="btn-text">Vollbild</span></button>
|
<button class="nav-btn" onclick="toggleFullscreen('rightPane')" title="Diesen Editor im Vollbild anzeigen">⤢ <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, '&').replace(/</g, '<').replace(/>/g, '>')
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
.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 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) ? ' 🔑 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>⚙️ Spalten anpassen (Typ & Name)</summary>
|
||||||
|
<div class="modal-hint" style="margin-top:10px">
|
||||||
|
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.
|
||||||
|
</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ärschlüssel</label>
|
||||||
|
<div class="pk-checklist">${pkChecklist}</div>
|
||||||
|
<div class="field-hint">
|
||||||
|
Keine Auswahl – automatische, fortlaufende <code>id</code>-Spalte (Standard).
|
||||||
|
Ein Haken – diese Spalte wird zum Primärschlüssel; achte darauf, dass sie
|
||||||
|
in jeder Zeile eindeutig und nicht leer ist. Mehrere Haken – zusammengesetzter
|
||||||
|
Schlüssel, der erst in Kombination eindeutig sein muss. Die Auswahl wird für
|
||||||
|
dieses Dokument gemerkt, bis du oben auf „Neu“ klickst.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${buildColumnTypeEditor()}
|
||||||
|
<div class="modal-section-title">📊 Visuelle MariaDB Tabellen-Vorschau</div>
|
||||||
|
${buildSqlPreviewTable(st.items, st.colNames, resolvedColumns, st.pk, outNames)}
|
||||||
|
<div class="modal-section-title">📄 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">📊 Visuelle MariaDB Tabellen-Vorschau</div>
|
|
||||||
${htmlTable}
|
|
||||||
<div class="modal-section-title">📄 Generiertes SQL-Skript</div>
|
|
||||||
<pre class="sql-code">${escapeHtml(sql)}</pre>`;
|
|
||||||
|
|
||||||
window.openModal('🗃️ MariaDB Workbench: ' + escapeHtml(tableName), body, [
|
window.openModal('🗃️ MariaDB Workbench: ' + escapeHtml(tableName), '<div>Wird geladen…</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äge werden ergänzt, nicht gelöscht. Snippets und Profile mit gleichem Namen kommen zusätzlich hinzu.</div>
|
<div class="field-hint">Vorhandene Einträge werden ergänzt, nicht gelöscht. Snippets und Profile mit gleichem Namen kommen zusätzlich hinzu.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-section-title">⌛ 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äßigen Abständen als Datei herunterladen
|
||||||
|
</label>
|
||||||
|
<div class="field-hint">
|
||||||
|
Lädt jeden Editor mit geändertem Inhalt automatisch als JSON-Datei herunter
|
||||||
|
(Dateiname mit Zeitstempel, z. B. <code>bestellung_autosave_2026-09-17-1530.json</code>).
|
||||||
|
Nur ein zusätzliches Sicherheitsnetz – ersetzt nicht das gezielte Speichern per
|
||||||
|
„Export“, und die heruntergeladenen Dateien sammeln sich im Download-Ordner an,
|
||||||
|
du rä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('⚙️ Einstellungen', body, [
|
window.openModal('⚙️ 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 („Dokument 1“) wird beim Export zum Dateinamen und beim
|
<p>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.</p>
|
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.</p>
|
||||||
|
|
||||||
|
<h4>Pfad eines Feldes kopieren</h4>
|
||||||
|
<p>Ein Klick auf ein Feld im Baum, dann oben auf „Pfad“ – 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ä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 – außer du benutzt bewusst URL-Import, Webhook oder Datenbank.</p>
|
Server gesendet – außer du benutzt bewusst URL-Import, Webhook oder Datenbank.</p>
|
||||||
<p>Weil der Speicher am Browser hängt: Beim Wechsel auf einen anderen Rechner nimmst du
|
<p>Weil der Speicher am Browser hängt: Beim Wechsel auf einen anderen Rechner nimmst du
|
||||||
deine Konfiguration über <b>Einstellungen → Sicherung exportieren</b> mit.</p>
|
deine Konfiguration über <b>Einstellungen → Sicherung exportieren</b> 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.</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> – 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> – 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> – Felder mit <code>@</code> werden wieder zu Attributen. Wurzel- und Listen-Tag stellst du in den Einstellungen ein.</li>
|
<li><b>XML</b> – Felder mit <code>@</code> werden wieder zu Attributen. Wurzel- und Listen-Tag stellst du in den Einstellungen ein.</li>
|
||||||
<li><b>MariaDB</b> – erzeugt <code>CREATE TABLE</code> samt <code>INSERT</code>s. Die Spaltentypen werden aus den Werten geraten; prüfe sie, bevor du das Skript produktiv einspielst.</li>
|
<li><b>MariaDB</b> – 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ößeren <code>BIGINT</code> 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 <code>id</code>-Spalte als Primärschlüssel – per Häkchen lässt sich stattdessen eine vorhandene Spalte dazu machen, oder mehrere zusammen als zusammengesetzter Schlüssel.</li>
|
||||||
<li><b>JSON-Schema</b> – leitet eine Strukturbeschreibung ab, die du zur Validierung weiterverwenden kannst.</li>
|
<li><b>JSON-Schema</b> – leitet eine Strukturbeschreibung ab, die du zur Validierung weiterverwenden kannst.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -2329,8 +2908,7 @@ lib/jsoneditor/standalone.js</pre>
|
|||||||
<p>Veröffentlicht unter der MIT-Lizenz. Du darfst sie frei verwenden, verändern
|
<p>Veröffentlicht unter der MIT-Lizenz. Du darfst sie frei verwenden, verä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ährleistung bereitgestellt.</p>
|
ohne Gewä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>
|
||||||
– die Adresse trägst du hier im Quelltext ein, sobald das Repository steht.</p>
|
|
||||||
|
|
||||||
<h4>Verwendete Bibliotheken</h4>
|
<h4>Verwendete Bibliotheken</h4>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
Reference in new issue
Block a user