admindb 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,314 +1,314 @@
1
- /* Table schema editor: rename table, add/rename/drop columns, drop table. */
2
- (function () {
3
- 'use strict';
4
- const cfgEl = document.getElementById('schema-config');
5
- if (!cfgEl) return;
6
- const cfg = JSON.parse(cfgEl.textContent);
7
- const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
8
- const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
9
- const t = encodeURIComponent(cfg.table);
10
-
11
- function escapeHtml(s) {
12
- return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
13
- }
14
-
15
- function showError(el, msg) {
16
- el.classList.remove('hidden');
17
- el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
18
- }
19
-
20
- // ---- Rename table ------------------------------------------------------
21
-
22
- const renameInput = document.getElementById('table-rename-input');
23
- const renameBtn = document.getElementById('rename-table');
24
-
25
- async function renameTable() {
26
- const name = (renameInput.value || '').trim();
27
- if (!name) return UI.showError('Table name is required.');
28
- if (name === cfg.table) return UI.showToast('Name unchanged.', 'info');
29
- try {
30
- const data = await Api.post('/api/tables/' + t + '/rename', { name });
31
- UI.showToast(data.message || 'Table renamed.', 'success');
32
- setTimeout(() => {
33
- window.location.href = Api.basePath + '/tables/' + encodeURIComponent(name) + '/schema';
34
- }, 400);
35
- } catch (e) {
36
- UI.showError(e.message);
37
- }
38
- }
39
- if (renameBtn) renameBtn.addEventListener('click', renameTable);
40
- if (renameInput) renameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') renameTable(); });
41
-
42
- // ---- Add column ---------------------------------------------------------
43
-
44
- const addBtn = document.getElementById('add-column');
45
- const panel = document.getElementById('add-column-panel');
46
- const formEl = document.getElementById('add-column-form');
47
- const errEl = document.getElementById('add-column-error');
48
- const submitBtn = document.getElementById('add-column-submit');
49
- const cancelBtn = document.getElementById('add-column-cancel');
50
- let fkTables = [];
51
-
52
- async function loadFkTables() {
53
- try {
54
- const tables = await Api.get('/api/tables');
55
- fkTables = tables.map((x) => x.name).filter((n) => n.charAt(0) !== '_');
56
- } catch (e) {
57
- fkTables = [];
58
- }
59
- }
60
-
61
- function buildAddForm() {
62
- formEl.innerHTML = '';
63
- formEl.innerHTML = [
64
- '<div class="col-span-2 sm:col-span-3"><label class="field-label">Name</label><input class="field-input ac-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
65
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ac-type">' + DESIGNER_TYPES.map((x) => '<option>' + x + '</option>').join('') + '</select></div>',
66
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label><select class="field-select ac-fk-table"><option value="">— none —</option>' + fkTables.map((x) => '<option>' + escapeHtml(x) + '</option>').join('') + '</select></div>',
67
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label><select class="field-select ac-fk-col"><option value="">—</option></select></div>',
68
- '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label><input class="field-input ac-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
69
- '<div class="col-span-2 flex items-end gap-1 pb-0.5 sm:col-span-1">',
70
- '<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs ac-notnull"><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>',
71
- '<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs ac-unique"><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>',
72
- '</div>',
73
- ].join('');
74
-
75
- const fkTableSel = formEl.querySelector('.ac-fk-table');
76
- const fkColSel = formEl.querySelector('.ac-fk-col');
77
- fkTableSel.addEventListener('change', async () => {
78
- fkColSel.innerHTML = '<option value="">—</option>';
79
- const target = fkTableSel.value;
80
- if (!target) return;
81
- try {
82
- const info = await Api.get('/api/tables/' + encodeURIComponent(target) + '/info');
83
- fkColSel.innerHTML = '<option value="">—</option>' + info.columns.map((c) => '<option>' + escapeHtml(c.name) + '</option>').join('');
84
- } catch (e) { /* ignore */ }
85
- });
86
- }
87
-
88
- async function addColumn() {
89
- errEl.classList.add('hidden');
90
- const name = (formEl.querySelector('.ac-name').value || '').trim();
91
- if (!name) return showError(errEl, 'Column name is required.');
92
- if (!NAME_RE.test(name)) return showError(errEl, 'Invalid column name: "' + name + '". Use letters, digits and underscores.');
93
- const fkTable = formEl.querySelector('.ac-fk-table').value;
94
- const fkCol = formEl.querySelector('.ac-fk-col').value;
95
- const column = {
96
- name,
97
- type: formEl.querySelector('.ac-type').value,
98
- notNull: formEl.querySelector('.ac-notnull').checked,
99
- unique: formEl.querySelector('.ac-unique').checked,
100
- defaultValue: formEl.querySelector('.ac-default').value.trim() || null,
101
- foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
102
- };
103
- try {
104
- const data = await Api.post('/api/tables/' + t + '/columns', { column });
105
- UI.showToast(data.message || 'Column added.', 'success');
106
- window.location.reload();
107
- } catch (e) {
108
- showError(errEl, e.message);
109
- }
110
- }
111
-
112
- if (addBtn) {
113
- addBtn.addEventListener('click', () => {
114
- panel.classList.toggle('hidden');
115
- if (!panel.classList.contains('hidden')) {
116
- errEl.classList.add('hidden');
117
- buildAddForm();
118
- }
119
- });
120
- }
121
- if (cancelBtn) cancelBtn.addEventListener('click', () => { panel.classList.add('hidden'); formEl.innerHTML = ''; });
122
- if (submitBtn) submitBtn.addEventListener('click', addColumn);
123
-
124
- // ---- Column rename (inline) ---------------------------------------------
125
-
126
- function inlineRename(cell, oldName) {
127
- const display = cell.querySelector('.name-display');
128
- if (!display) return;
129
- const input = document.createElement('input');
130
- input.type = 'text';
131
- input.value = oldName;
132
- input.className = 'field-input !py-1 !text-[13px]';
133
- input.spellcheck = false;
134
- let done = false;
135
- const restore = () => { if (display.parentNode) cell.replaceChild(display, input); };
136
- const finish = async (save) => {
137
- if (done) return;
138
- done = true;
139
- if (!save) return restore();
140
- const newName = input.value.trim();
141
- if (!newName || newName === oldName) return restore();
142
- try {
143
- await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(oldName), { name: newName });
144
- UI.showToast('Column renamed.', 'success');
145
- window.location.reload();
146
- } catch (e) {
147
- UI.showError(e.message);
148
- done = false;
149
- restore();
150
- }
151
- };
152
- input.addEventListener('keydown', (e) => {
153
- if (e.key === 'Enter') { e.preventDefault(); finish(true); }
154
- else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
155
- });
156
- input.addEventListener('blur', () => finish(false));
157
- cell.replaceChild(input, display);
158
- input.focus();
159
- input.select();
160
- }
161
-
162
- document.querySelectorAll('.col-rename').forEach((btn) => {
163
- btn.addEventListener('click', () => {
164
- const tr = btn.closest('tr');
165
- if (!tr) return;
166
- const cell = tr.querySelector('.col-name-cell');
167
- if (cell) inlineRename(cell, cell.dataset.column);
168
- });
169
- });
170
-
171
- // ---- Drop column ---------------------------------------------------------
172
-
173
- document.querySelectorAll('.col-drop').forEach((btn) => {
174
- btn.addEventListener('click', () => {
175
- const tr = btn.closest('tr');
176
- if (!tr) return;
177
- const cell = tr.querySelector('.col-name-cell');
178
- const name = cell.dataset.column;
179
- if (!window.confirm('Drop column "' + name + '" permanently? This cannot be undone.')) return;
180
- Api.del('/api/tables/' + t + '/columns/' + encodeURIComponent(name))
181
- .then((data) => {
182
- UI.showToast(data.message || 'Column dropped.', 'success');
183
- window.location.reload();
184
- })
185
- .catch((e) => UI.showError(e.message));
186
- });
187
- });
188
-
189
- // ---- Drop table -----------------------------------------------------------
190
-
191
- const dropBtn = document.getElementById('drop-table');
192
- if (dropBtn) {
193
- dropBtn.addEventListener('click', () => {
194
- if (!window.confirm('Drop table "' + cfg.table + '" permanently? All of its data will be lost.')) return;
195
- Api.del('/api/tables/' + t)
196
- .then(() => {
197
- UI.showToast('Table dropped.', 'success');
198
- setTimeout(() => { window.location.href = Api.basePath + '/'; }, 400);
199
- })
200
- .catch((e) => UI.showError(e.message));
201
- });
202
- }
203
-
204
- // ---- Index management ------------------------------------------------------
205
-
206
- const addIndexBtn = document.getElementById('add-index');
207
- const addIndexPanel = document.getElementById('add-index-panel');
208
- const indexNameInput = document.getElementById('index-name');
209
- const indexUniqueInput = document.getElementById('index-unique');
210
- const indexErrEl = document.getElementById('add-index-error');
211
- const addIndexSubmit = document.getElementById('add-index-submit');
212
- const addIndexCancel = document.getElementById('add-index-cancel');
213
- const indexPreview = document.getElementById('index-preview');
214
- const indexPreviewCode = document.getElementById('index-preview-code');
215
- const indexColBtns = Array.prototype.slice.call(document.querySelectorAll('.index-col-btn'));
216
-
217
- // Columns selected for the new index, in click order (index order matters!).
218
- let indexColumns = [];
219
-
220
- function renderIndexPreview() {
221
- if (!indexPreview || !indexPreviewCode) return;
222
- const cols = indexColumns.slice();
223
- const name = (indexNameInput.value || '').trim();
224
- const unique = indexUniqueInput && indexUniqueInput.checked ? 'UNIQUE ' : '';
225
- if (!cols.length) {
226
- indexPreview.classList.add('hidden');
227
- indexPreviewCode.textContent = '';
228
- return;
229
- }
230
- const finalName = name || 'idx_' + cfg.table + '_' + cols.join('_');
231
- indexPreviewCode.textContent =
232
- 'CREATE ' + unique + 'INDEX "' + finalName + '" ON "' + cfg.table + '" (' +
233
- cols.map((c) => '"' + c + '"').join(', ') + ');';
234
- indexPreview.classList.remove('hidden');
235
- }
236
-
237
- function toggleIndexColumn(btn) {
238
- const col = btn.dataset.column;
239
- const i = indexColumns.indexOf(col);
240
- const num = btn.querySelector('.index-col-num');
241
- if (i !== -1) {
242
- indexColumns.splice(i, 1);
243
- btn.classList.remove('border-primary', 'bg-primary/10');
244
- if (num) num.classList.add('hidden');
245
- } else {
246
- indexColumns.push(col);
247
- btn.classList.add('border-primary', 'bg-primary/10');
248
- if (num) num.classList.remove('hidden');
249
- }
250
- indexColumns.forEach((c, idx) => {
251
- const b = indexColBtns.find((x) => x.dataset.column === c);
252
- const n = b && b.querySelector('.index-col-num');
253
- if (n) n.textContent = String(idx + 1);
254
- });
255
- renderIndexPreview();
256
- }
257
-
258
- indexColBtns.forEach((btn) => btn.addEventListener('click', () => toggleIndexColumn(btn)));
259
- if (indexNameInput) indexNameInput.addEventListener('input', renderIndexPreview);
260
- if (indexUniqueInput) indexUniqueInput.addEventListener('change', renderIndexPreview);
261
-
262
- if (addIndexBtn && addIndexPanel) {
263
- addIndexBtn.addEventListener('click', () => {
264
- addIndexPanel.classList.toggle('hidden');
265
- if (!addIndexPanel.classList.contains('hidden')) {
266
- indexErrEl.classList.add('hidden');
267
- indexNameInput.focus();
268
- renderIndexPreview();
269
- }
270
- });
271
- }
272
- if (addIndexCancel) {
273
- addIndexCancel.addEventListener('click', () => {
274
- addIndexPanel.classList.add('hidden');
275
- indexErrEl.classList.add('hidden');
276
- });
277
- }
278
- if (addIndexSubmit) {
279
- addIndexSubmit.addEventListener('click', async () => {
280
- indexErrEl.classList.add('hidden');
281
- const columns = indexColumns.slice();
282
- if (!columns.length) return showError(indexErrEl, 'Select at least one column for the index.');
283
- const body = { columns, unique: !!(indexUniqueInput && indexUniqueInput.checked) };
284
- const name = (indexNameInput.value || '').trim();
285
- if (name) body.name = name;
286
- addIndexSubmit.disabled = true;
287
- try {
288
- const data = await Api.post('/api/tables/' + t + '/indexes', body);
289
- UI.showToast(data.message || 'Index created.', 'success');
290
- window.location.reload();
291
- } catch (e) {
292
- showError(indexErrEl, e.message);
293
- } finally {
294
- addIndexSubmit.disabled = false;
295
- }
296
- });
297
- }
298
-
299
- document.querySelectorAll('.index-drop').forEach((btn) => {
300
- btn.addEventListener('click', () => {
301
- const name = btn.dataset.index;
302
- if (!name) return;
303
- if (!window.confirm('Drop index "' + name + '" permanently?')) return;
304
- Api.del('/api/tables/' + t + '/indexes/' + encodeURIComponent(name))
305
- .then((data) => {
306
- UI.showToast(data.message || 'Index dropped.', 'success');
307
- window.location.reload();
308
- })
309
- .catch((e) => UI.showError(e.message));
310
- });
311
- });
312
-
313
- loadFkTables();
314
- })();
1
+ /* Table schema editor: rename table, add/rename/drop columns, drop table. */
2
+ (function () {
3
+ 'use strict';
4
+ const cfgEl = document.getElementById('schema-config');
5
+ if (!cfgEl) return;
6
+ const cfg = JSON.parse(cfgEl.textContent);
7
+ const DESIGNER_TYPES = ['TEXT', 'INTEGER', 'REAL', 'BLOB', 'BOOLEAN', 'DATE', 'DATETIME'];
8
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]*$/;
9
+ const t = encodeURIComponent(cfg.table);
10
+
11
+ function escapeHtml(s) {
12
+ return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
13
+ }
14
+
15
+ function showError(el, msg) {
16
+ el.classList.remove('hidden');
17
+ el.innerHTML = '<div class="flex items-start gap-2.5 rounded-xl border border-error/40 bg-error/10 px-4 py-3 text-sm font-medium text-error"><span>' + escapeHtml(msg) + '</span></div>';
18
+ }
19
+
20
+ // ---- Rename table ------------------------------------------------------
21
+
22
+ const renameInput = document.getElementById('table-rename-input');
23
+ const renameBtn = document.getElementById('rename-table');
24
+
25
+ async function renameTable() {
26
+ const name = (renameInput.value || '').trim();
27
+ if (!name) return UI.showError('Table name is required.');
28
+ if (name === cfg.table) return UI.showToast('Name unchanged.', 'info');
29
+ try {
30
+ const data = await Api.post('/api/tables/' + t + '/rename', { name });
31
+ UI.showToast(data.message || 'Table renamed.', 'success');
32
+ setTimeout(() => {
33
+ window.location.href = Api.basePath + '/tables/' + encodeURIComponent(name) + '/schema';
34
+ }, 400);
35
+ } catch (e) {
36
+ UI.showError(e.message);
37
+ }
38
+ }
39
+ if (renameBtn) renameBtn.addEventListener('click', renameTable);
40
+ if (renameInput) renameInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') renameTable(); });
41
+
42
+ // ---- Add column ---------------------------------------------------------
43
+
44
+ const addBtn = document.getElementById('add-column');
45
+ const panel = document.getElementById('add-column-panel');
46
+ const formEl = document.getElementById('add-column-form');
47
+ const errEl = document.getElementById('add-column-error');
48
+ const submitBtn = document.getElementById('add-column-submit');
49
+ const cancelBtn = document.getElementById('add-column-cancel');
50
+ let fkTables = [];
51
+
52
+ async function loadFkTables() {
53
+ try {
54
+ const tables = await Api.get('/api/tables');
55
+ fkTables = tables.map((x) => x.name).filter((n) => n.charAt(0) !== '_');
56
+ } catch (e) {
57
+ fkTables = [];
58
+ }
59
+ }
60
+
61
+ function buildAddForm() {
62
+ formEl.innerHTML = '';
63
+ formEl.innerHTML = [
64
+ '<div class="col-span-2 sm:col-span-3"><label class="field-label">Name</label><input class="field-input ac-name" placeholder="column_name" autocomplete="off" spellcheck="false"></div>',
65
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Type</label><select class="field-select ac-type">' + DESIGNER_TYPES.map((x) => '<option>' + x + '</option>').join('') + '</select></div>',
66
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK table</label><select class="field-select ac-fk-table"><option value="">— none —</option>' + fkTables.map((x) => '<option>' + escapeHtml(x) + '</option>').join('') + '</select></div>',
67
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">FK column</label><select class="field-select ac-fk-col"><option value="">—</option></select></div>',
68
+ '<div class="col-span-1 sm:col-span-2"><label class="field-label">Default</label><input class="field-input ac-default" placeholder="0, CURRENT_TIMESTAMP" autocomplete="off" spellcheck="false"></div>',
69
+ '<div class="col-span-2 flex items-end gap-1 pb-0.5 sm:col-span-1">',
70
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Not null"><input type="checkbox" class="checkbox checkbox-xs ac-notnull"><span class="text-[10px] font-semibold uppercase text-base-content/50">NN</span></label>',
71
+ '<label class="flex cursor-pointer flex-col items-center gap-1" title="Unique"><input type="checkbox" class="checkbox checkbox-xs ac-unique"><span class="text-[10px] font-semibold uppercase text-base-content/50">UQ</span></label>',
72
+ '</div>',
73
+ ].join('');
74
+
75
+ const fkTableSel = formEl.querySelector('.ac-fk-table');
76
+ const fkColSel = formEl.querySelector('.ac-fk-col');
77
+ fkTableSel.addEventListener('change', async () => {
78
+ fkColSel.innerHTML = '<option value="">—</option>';
79
+ const target = fkTableSel.value;
80
+ if (!target) return;
81
+ try {
82
+ const info = await Api.get('/api/tables/' + encodeURIComponent(target) + '/info');
83
+ fkColSel.innerHTML = '<option value="">—</option>' + info.columns.map((c) => '<option>' + escapeHtml(c.name) + '</option>').join('');
84
+ } catch (e) { /* ignore */ }
85
+ });
86
+ }
87
+
88
+ async function addColumn() {
89
+ errEl.classList.add('hidden');
90
+ const name = (formEl.querySelector('.ac-name').value || '').trim();
91
+ if (!name) return showError(errEl, 'Column name is required.');
92
+ if (!NAME_RE.test(name)) return showError(errEl, 'Invalid column name: "' + name + '". Use letters, digits and underscores.');
93
+ const fkTable = formEl.querySelector('.ac-fk-table').value;
94
+ const fkCol = formEl.querySelector('.ac-fk-col').value;
95
+ const column = {
96
+ name,
97
+ type: formEl.querySelector('.ac-type').value,
98
+ notNull: formEl.querySelector('.ac-notnull').checked,
99
+ unique: formEl.querySelector('.ac-unique').checked,
100
+ defaultValue: formEl.querySelector('.ac-default').value.trim() || null,
101
+ foreignKey: fkTable && fkCol ? { table: fkTable, column: fkCol } : null,
102
+ };
103
+ try {
104
+ const data = await Api.post('/api/tables/' + t + '/columns', { column });
105
+ UI.showToast(data.message || 'Column added.', 'success');
106
+ window.location.reload();
107
+ } catch (e) {
108
+ showError(errEl, e.message);
109
+ }
110
+ }
111
+
112
+ if (addBtn) {
113
+ addBtn.addEventListener('click', () => {
114
+ panel.classList.toggle('hidden');
115
+ if (!panel.classList.contains('hidden')) {
116
+ errEl.classList.add('hidden');
117
+ buildAddForm();
118
+ }
119
+ });
120
+ }
121
+ if (cancelBtn) cancelBtn.addEventListener('click', () => { panel.classList.add('hidden'); formEl.innerHTML = ''; });
122
+ if (submitBtn) submitBtn.addEventListener('click', addColumn);
123
+
124
+ // ---- Column rename (inline) ---------------------------------------------
125
+
126
+ function inlineRename(cell, oldName) {
127
+ const display = cell.querySelector('.name-display');
128
+ if (!display) return;
129
+ const input = document.createElement('input');
130
+ input.type = 'text';
131
+ input.value = oldName;
132
+ input.className = 'field-input !py-1 !text-[13px]';
133
+ input.spellcheck = false;
134
+ let done = false;
135
+ const restore = () => { if (display.parentNode) cell.replaceChild(display, input); };
136
+ const finish = async (save) => {
137
+ if (done) return;
138
+ done = true;
139
+ if (!save) return restore();
140
+ const newName = input.value.trim();
141
+ if (!newName || newName === oldName) return restore();
142
+ try {
143
+ await Api.put('/api/tables/' + t + '/columns/' + encodeURIComponent(oldName), { name: newName });
144
+ UI.showToast('Column renamed.', 'success');
145
+ window.location.reload();
146
+ } catch (e) {
147
+ UI.showError(e.message);
148
+ done = false;
149
+ restore();
150
+ }
151
+ };
152
+ input.addEventListener('keydown', (e) => {
153
+ if (e.key === 'Enter') { e.preventDefault(); finish(true); }
154
+ else if (e.key === 'Escape') { e.preventDefault(); finish(false); }
155
+ });
156
+ input.addEventListener('blur', () => finish(false));
157
+ cell.replaceChild(input, display);
158
+ input.focus();
159
+ input.select();
160
+ }
161
+
162
+ document.querySelectorAll('.col-rename').forEach((btn) => {
163
+ btn.addEventListener('click', () => {
164
+ const tr = btn.closest('tr');
165
+ if (!tr) return;
166
+ const cell = tr.querySelector('.col-name-cell');
167
+ if (cell) inlineRename(cell, cell.dataset.column);
168
+ });
169
+ });
170
+
171
+ // ---- Drop column ---------------------------------------------------------
172
+
173
+ document.querySelectorAll('.col-drop').forEach((btn) => {
174
+ btn.addEventListener('click', () => {
175
+ const tr = btn.closest('tr');
176
+ if (!tr) return;
177
+ const cell = tr.querySelector('.col-name-cell');
178
+ const name = cell.dataset.column;
179
+ if (!window.confirm('Drop column "' + name + '" permanently? This cannot be undone.')) return;
180
+ Api.del('/api/tables/' + t + '/columns/' + encodeURIComponent(name))
181
+ .then((data) => {
182
+ UI.showToast(data.message || 'Column dropped.', 'success');
183
+ window.location.reload();
184
+ })
185
+ .catch((e) => UI.showError(e.message));
186
+ });
187
+ });
188
+
189
+ // ---- Drop table -----------------------------------------------------------
190
+
191
+ const dropBtn = document.getElementById('drop-table');
192
+ if (dropBtn) {
193
+ dropBtn.addEventListener('click', () => {
194
+ if (!window.confirm('Drop table "' + cfg.table + '" permanently? All of its data will be lost.')) return;
195
+ Api.del('/api/tables/' + t)
196
+ .then(() => {
197
+ UI.showToast('Table dropped.', 'success');
198
+ setTimeout(() => { window.location.href = Api.basePath + '/'; }, 400);
199
+ })
200
+ .catch((e) => UI.showError(e.message));
201
+ });
202
+ }
203
+
204
+ // ---- Index management ------------------------------------------------------
205
+
206
+ const addIndexBtn = document.getElementById('add-index');
207
+ const addIndexPanel = document.getElementById('add-index-panel');
208
+ const indexNameInput = document.getElementById('index-name');
209
+ const indexUniqueInput = document.getElementById('index-unique');
210
+ const indexErrEl = document.getElementById('add-index-error');
211
+ const addIndexSubmit = document.getElementById('add-index-submit');
212
+ const addIndexCancel = document.getElementById('add-index-cancel');
213
+ const indexPreview = document.getElementById('index-preview');
214
+ const indexPreviewCode = document.getElementById('index-preview-code');
215
+ const indexColBtns = Array.prototype.slice.call(document.querySelectorAll('.index-col-btn'));
216
+
217
+ // Columns selected for the new index, in click order (index order matters!).
218
+ let indexColumns = [];
219
+
220
+ function renderIndexPreview() {
221
+ if (!indexPreview || !indexPreviewCode) return;
222
+ const cols = indexColumns.slice();
223
+ const name = (indexNameInput.value || '').trim();
224
+ const unique = indexUniqueInput && indexUniqueInput.checked ? 'UNIQUE ' : '';
225
+ if (!cols.length) {
226
+ indexPreview.classList.add('hidden');
227
+ indexPreviewCode.textContent = '';
228
+ return;
229
+ }
230
+ const finalName = name || 'idx_' + cfg.table + '_' + cols.join('_');
231
+ indexPreviewCode.textContent =
232
+ 'CREATE ' + unique + 'INDEX "' + finalName + '" ON "' + cfg.table + '" (' +
233
+ cols.map((c) => '"' + c + '"').join(', ') + ');';
234
+ indexPreview.classList.remove('hidden');
235
+ }
236
+
237
+ function toggleIndexColumn(btn) {
238
+ const col = btn.dataset.column;
239
+ const i = indexColumns.indexOf(col);
240
+ const num = btn.querySelector('.index-col-num');
241
+ if (i !== -1) {
242
+ indexColumns.splice(i, 1);
243
+ btn.classList.remove('border-primary', 'bg-primary/10');
244
+ if (num) num.classList.add('hidden');
245
+ } else {
246
+ indexColumns.push(col);
247
+ btn.classList.add('border-primary', 'bg-primary/10');
248
+ if (num) num.classList.remove('hidden');
249
+ }
250
+ indexColumns.forEach((c, idx) => {
251
+ const b = indexColBtns.find((x) => x.dataset.column === c);
252
+ const n = b && b.querySelector('.index-col-num');
253
+ if (n) n.textContent = String(idx + 1);
254
+ });
255
+ renderIndexPreview();
256
+ }
257
+
258
+ indexColBtns.forEach((btn) => btn.addEventListener('click', () => toggleIndexColumn(btn)));
259
+ if (indexNameInput) indexNameInput.addEventListener('input', renderIndexPreview);
260
+ if (indexUniqueInput) indexUniqueInput.addEventListener('change', renderIndexPreview);
261
+
262
+ if (addIndexBtn && addIndexPanel) {
263
+ addIndexBtn.addEventListener('click', () => {
264
+ addIndexPanel.classList.toggle('hidden');
265
+ if (!addIndexPanel.classList.contains('hidden')) {
266
+ indexErrEl.classList.add('hidden');
267
+ indexNameInput.focus();
268
+ renderIndexPreview();
269
+ }
270
+ });
271
+ }
272
+ if (addIndexCancel) {
273
+ addIndexCancel.addEventListener('click', () => {
274
+ addIndexPanel.classList.add('hidden');
275
+ indexErrEl.classList.add('hidden');
276
+ });
277
+ }
278
+ if (addIndexSubmit) {
279
+ addIndexSubmit.addEventListener('click', async () => {
280
+ indexErrEl.classList.add('hidden');
281
+ const columns = indexColumns.slice();
282
+ if (!columns.length) return showError(indexErrEl, 'Select at least one column for the index.');
283
+ const body = { columns, unique: !!(indexUniqueInput && indexUniqueInput.checked) };
284
+ const name = (indexNameInput.value || '').trim();
285
+ if (name) body.name = name;
286
+ addIndexSubmit.disabled = true;
287
+ try {
288
+ const data = await Api.post('/api/tables/' + t + '/indexes', body);
289
+ UI.showToast(data.message || 'Index created.', 'success');
290
+ window.location.reload();
291
+ } catch (e) {
292
+ showError(indexErrEl, e.message);
293
+ } finally {
294
+ addIndexSubmit.disabled = false;
295
+ }
296
+ });
297
+ }
298
+
299
+ document.querySelectorAll('.index-drop').forEach((btn) => {
300
+ btn.addEventListener('click', () => {
301
+ const name = btn.dataset.index;
302
+ if (!name) return;
303
+ if (!window.confirm('Drop index "' + name + '" permanently?')) return;
304
+ Api.del('/api/tables/' + t + '/indexes/' + encodeURIComponent(name))
305
+ .then((data) => {
306
+ UI.showToast(data.message || 'Index dropped.', 'success');
307
+ window.location.reload();
308
+ })
309
+ .catch((e) => UI.showError(e.message));
310
+ });
311
+ });
312
+
313
+ loadFkTables();
314
+ })();