admindb 1.2.0 → 2.0.0
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.
- package/README.md +250 -352
- package/dist/app.d.ts +25 -4
- package/dist/app.js +79 -15
- package/dist/app.js.map +1 -1
- package/dist/auth/config.d.ts +19 -0
- package/dist/auth/config.js +147 -0
- package/dist/auth/config.js.map +1 -0
- package/dist/auth/crypto.d.ts +27 -0
- package/dist/auth/crypto.js +133 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +22 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +14 -0
- package/dist/auth/middleware.js +100 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/routes.d.ts +7 -0
- package/dist/auth/routes.js +78 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/types.d.ts +35 -0
- package/dist/auth/types.js +11 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +328 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/config.d.ts +56 -0
- package/dist/cli/config.js +133 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/runner.d.ts +1 -0
- package/dist/cli/runner.js +222 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli.js +2 -127
- package/dist/cli.js.map +1 -1
- package/dist/data/datasets.d.ts +17 -0
- package/dist/data/datasets.js +133 -0
- package/dist/data/datasets.js.map +1 -0
- package/dist/data/detector.d.ts +30 -0
- package/dist/data/detector.js +472 -0
- package/dist/data/detector.js.map +1 -0
- package/dist/data/engine.d.ts +10 -0
- package/dist/data/engine.js +211 -0
- package/dist/data/engine.js.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +22 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/strategies.d.ts +30 -0
- package/dist/data/strategies.js +343 -0
- package/dist/data/strategies.js.map +1 -0
- package/dist/data/types.d.ts +73 -0
- package/dist/data/types.js +8 -0
- package/dist/data/types.js.map +1 -0
- package/dist/db/database.d.ts +14 -142
- package/dist/db/database.js +209 -435
- package/dist/db/database.js.map +1 -1
- package/dist/db/export.d.ts +2 -2
- package/dist/db/export.js +7 -2
- package/dist/db/export.js.map +1 -1
- package/dist/db/filters.d.ts +22 -0
- package/dist/db/filters.js +158 -0
- package/dist/db/filters.js.map +1 -0
- package/dist/db/index.d.ts +8 -0
- package/dist/db/index.js +25 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/introspection.d.ts +6 -0
- package/dist/db/introspection.js +172 -0
- package/dist/db/introspection.js.map +1 -0
- package/dist/db/manager.d.ts +32 -19
- package/dist/db/manager.js +203 -44
- package/dist/db/manager.js.map +1 -1
- package/dist/db/migrations.d.ts +5 -0
- package/dist/db/migrations.js +110 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/postgres.d.ts +85 -0
- package/dist/db/postgres.js +745 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/types.d.ts +206 -0
- package/dist/db/types.js +7 -0
- package/dist/db/types.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +46 -4
- package/dist/index.js.map +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/css/input.css +83 -13
- package/dist/public/js/app.js +142 -0
- package/dist/public/js/browse.js +591 -104
- package/dist/public/js/databases.js +105 -13
- package/dist/public/js/designer.js +40 -2
- package/dist/public/js/forms.js +610 -28
- package/dist/public/js/icons.js +51 -0
- package/dist/public/js/inspector.js +638 -0
- package/dist/public/js/query.js +93 -3
- package/dist/public/js/schema.js +170 -52
- package/dist/public/js/seed.js +1090 -0
- package/dist/routes/api/helpers.d.ts +42 -0
- package/dist/routes/api/helpers.js +133 -0
- package/dist/routes/api/helpers.js.map +1 -0
- package/dist/routes/api/import-export.d.ts +3 -0
- package/dist/routes/api/import-export.js +85 -0
- package/dist/routes/api/import-export.js.map +1 -0
- package/dist/routes/api/index.d.ts +4 -0
- package/dist/routes/api/index.js +37 -0
- package/dist/routes/api/index.js.map +1 -0
- package/dist/routes/api/query.d.ts +3 -0
- package/dist/routes/api/query.js +76 -0
- package/dist/routes/api/query.js.map +1 -0
- package/dist/routes/api/rows.d.ts +3 -0
- package/dist/routes/api/rows.js +373 -0
- package/dist/routes/api/rows.js.map +1 -0
- package/dist/routes/api/seed.d.ts +3 -0
- package/dist/routes/api/seed.js +82 -0
- package/dist/routes/api/seed.js.map +1 -0
- package/dist/routes/api/tables.d.ts +3 -0
- package/dist/routes/api/tables.js +160 -0
- package/dist/routes/api/tables.js.map +1 -0
- package/dist/routes/databases.d.ts +8 -11
- package/dist/routes/databases.js +100 -66
- package/dist/routes/databases.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.js +20 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/pages.d.ts +3 -3
- package/dist/routes/pages.js +163 -54
- package/dist/routes/pages.js.map +1 -1
- package/dist/serverless.d.ts +40 -0
- package/dist/serverless.js +210 -0
- package/dist/serverless.js.map +1 -0
- package/dist/sql/generator.d.ts +4 -3
- package/dist/sql/generator.js +103 -6
- package/dist/sql/generator.js.map +1 -1
- package/dist/sql/index.d.ts +2 -0
- package/dist/sql/index.js +19 -0
- package/dist/sql/index.js.map +1 -0
- package/dist/types/api.d.ts +412 -0
- package/dist/types/api.js +9 -0
- package/dist/types/api.js.map +1 -0
- package/dist/utils/colors.d.ts +40 -0
- package/dist/utils/colors.js +64 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/{util.d.ts → utils/common.d.ts} +10 -6
- package/dist/{util.js → utils/common.js} +80 -13
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/datatype.d.ts +49 -0
- package/dist/utils/datatype.js +222 -0
- package/dist/utils/datatype.js.map +1 -0
- package/dist/utils/icons.d.ts +11 -0
- package/dist/utils/icons.js +72 -0
- package/dist/utils/icons.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/{logger.d.ts → utils/logger.d.ts} +1 -1
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/views/layouts/main.hbs +33 -11
- package/dist/views/pages/databases.hbs +182 -74
- package/dist/views/pages/designer.hbs +9 -6
- package/dist/views/pages/error.hbs +3 -3
- package/dist/views/pages/form.hbs +5 -3
- package/dist/views/pages/home.hbs +22 -13
- package/dist/views/pages/login.hbs +111 -0
- package/dist/views/pages/query.hbs +10 -8
- package/dist/views/pages/schema.hbs +41 -13
- package/dist/views/pages/seed.hbs +232 -0
- package/dist/views/pages/table.hbs +155 -70
- package/dist/views/partials/icon.hbs +2 -0
- package/dist/views/partials/navbar.hbs +70 -31
- package/dist/views/partials/sidebar.hbs +108 -99
- package/package.json +12 -5
- package/dist/args.d.ts +0 -28
- package/dist/args.js +0 -187
- package/dist/args.js.map +0 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.js +0 -23
- package/dist/config.js.map +0 -1
- package/dist/csv.js.map +0 -1
- package/dist/logger.js +0 -25
- package/dist/logger.js.map +0 -1
- package/dist/routes/api.d.ts +0 -9
- package/dist/routes/api.js +0 -786
- package/dist/routes/api.js.map +0 -1
- package/dist/test/classifier.test.d.ts +0 -1
- package/dist/test/classifier.test.js +0 -44
- package/dist/test/classifier.test.js.map +0 -1
- package/dist/test/csv.test.d.ts +0 -1
- package/dist/test/csv.test.js +0 -64
- package/dist/test/csv.test.js.map +0 -1
- package/dist/test/database.test.d.ts +0 -1
- package/dist/test/database.test.js +0 -402
- package/dist/test/database.test.js.map +0 -1
- package/dist/test/generator.test.d.ts +0 -1
- package/dist/test/generator.test.js +0 -132
- package/dist/test/generator.test.js.map +0 -1
- package/dist/test/manager.test.d.ts +0 -1
- package/dist/test/manager.test.js +0 -146
- package/dist/test/manager.test.js.map +0 -1
- package/dist/util.js.map +0 -1
- /package/dist/{csv.d.ts → utils/csv.d.ts} +0 -0
- /package/dist/{csv.js → utils/csv.js} +0 -0
package/dist/public/js/browse.js
CHANGED
|
@@ -46,11 +46,19 @@
|
|
|
46
46
|
|
|
47
47
|
function initRowDelete() {
|
|
48
48
|
document.querySelectorAll('.js-delete-row').forEach((btn) => {
|
|
49
|
-
btn.addEventListener('click', () => {
|
|
49
|
+
btn.addEventListener('click', async () => {
|
|
50
50
|
const table = btn.dataset.table;
|
|
51
51
|
const pk = btn.dataset.pk;
|
|
52
52
|
if (!table || !pk) return;
|
|
53
|
-
|
|
53
|
+
const confirmed = await UI.confirm({
|
|
54
|
+
title: 'Delete Row',
|
|
55
|
+
message: 'Are you sure you want to permanently delete this row? This action cannot be undone.',
|
|
56
|
+
item: pk,
|
|
57
|
+
itemType: 'Primary Key',
|
|
58
|
+
confirmText: 'Delete row',
|
|
59
|
+
danger: true,
|
|
60
|
+
});
|
|
61
|
+
if (!confirmed) return;
|
|
54
62
|
Api.del('/api/tables/' + encodeURIComponent(table) + '/row/' + encodeURIComponent(pk))
|
|
55
63
|
.then(() => {
|
|
56
64
|
UI.showToast('Row deleted.', 'success');
|
|
@@ -102,11 +110,20 @@
|
|
|
102
110
|
return c ? { min: c.value ?? null, max: c.max ?? null } : { min: null, max: null };
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
function toDateInputValue(v, dateOnly) {
|
|
106
|
-
|
|
113
|
+
function toDateInputValue(v, dateOnly, timeOnly) {
|
|
114
|
+
if (v == null || v === '') return '';
|
|
115
|
+
const s = String(v);
|
|
116
|
+
if (dateOnly) {
|
|
117
|
+
const dm = s.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
118
|
+
return dm ? dm[1] : '';
|
|
119
|
+
}
|
|
120
|
+
if (timeOnly) {
|
|
121
|
+
const tm = s.match(/(\d{2}:\d{2}(?::\d{2})?)/);
|
|
122
|
+
return tm ? tm[1] : '';
|
|
123
|
+
}
|
|
124
|
+
const m = s.match(/^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/);
|
|
107
125
|
if (!m) return '';
|
|
108
|
-
|
|
109
|
-
return m[1] + 'T' + (m[2] ? m[2] + ':' + m[3] : '00:00');
|
|
126
|
+
return m[1] + 'T' + (m[2] ? m[2] + ':' + m[3] + (m[4] ? ':' + m[4] : '') : '00:00:00');
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
function addOption(sel, value, label) {
|
|
@@ -127,9 +144,12 @@
|
|
|
127
144
|
|
|
128
145
|
const fk = fkFor(col.name);
|
|
129
146
|
const type = (col.type || '').toUpperCase();
|
|
130
|
-
const isBool = type === 'BOOLEAN';
|
|
131
|
-
const isNum = type.startsWith('
|
|
132
|
-
const
|
|
147
|
+
const isBool = type === 'BOOLEAN' || type === 'BOOL';
|
|
148
|
+
const isNum = type.startsWith('INT') || type === 'REAL' || type === 'NUMERIC' || type.startsWith('DECIMAL') || type === 'FLOAT' || type === 'DOUBLE' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL') || type === 'MONEY';
|
|
149
|
+
const isDateOnly = type === 'DATE';
|
|
150
|
+
const isTimeOnly = type === 'TIME' || type === 'TIMETZ' || type.startsWith('TIME ') || type.startsWith('TIME(');
|
|
151
|
+
const isTimestamp = type === 'DATETIME' || type.includes('TIMESTAMP');
|
|
152
|
+
const isDate = isDateOnly || isTimeOnly || isTimestamp;
|
|
133
153
|
const existing = cfg.filters[col.name];
|
|
134
154
|
const conds = filterCondArray(existing);
|
|
135
155
|
const legacy = legacyString(existing);
|
|
@@ -176,10 +196,12 @@
|
|
|
176
196
|
const lo = document.createElement('input');
|
|
177
197
|
const hi = document.createElement('input');
|
|
178
198
|
if (isDate) {
|
|
179
|
-
lo.type = hi.type =
|
|
199
|
+
lo.type = hi.type = isDateOnly ? 'date' : (isTimeOnly ? 'time' : 'datetime-local');
|
|
200
|
+
if (isTimeOnly || isTimestamp) lo.step = hi.step = '1';
|
|
180
201
|
} else {
|
|
181
202
|
lo.type = hi.type = 'number';
|
|
182
|
-
|
|
203
|
+
const isInteger = type.startsWith('INTEGER') || type === 'INT' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL');
|
|
204
|
+
lo.step = hi.step = isInteger ? '1' : 'any';
|
|
183
205
|
}
|
|
184
206
|
lo.className = 'filter-input field-input !py-1.5 !text-[13px]';
|
|
185
207
|
hi.className = 'filter-input field-input !py-1.5 !text-[13px]';
|
|
@@ -192,14 +214,15 @@
|
|
|
192
214
|
const bt = condBetween(conds);
|
|
193
215
|
const minV = condVal(conds, 'gte') ?? condVal(conds, 'gt') ?? bt.min;
|
|
194
216
|
const maxV = condVal(conds, 'lte') ?? condVal(conds, 'lt') ?? bt.max;
|
|
195
|
-
if (minV != null) lo.value = isDate ? toDateInputValue(minV,
|
|
196
|
-
if (maxV != null) hi.value = isDate ? toDateInputValue(maxV,
|
|
217
|
+
if (minV != null) lo.value = isDate ? toDateInputValue(minV, isDateOnly, isTimeOnly) : String(minV);
|
|
218
|
+
if (maxV != null) hi.value = isDate ? toDateInputValue(maxV, isDateOnly, isTimeOnly) : String(maxV);
|
|
197
219
|
row.appendChild(lo);
|
|
198
220
|
row.appendChild(hi);
|
|
199
221
|
wrap.appendChild(row);
|
|
200
222
|
return wrap;
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
|
|
203
226
|
// Text / BLOB / anything else — legacy operator syntax.
|
|
204
227
|
const input = document.createElement('input');
|
|
205
228
|
input.type = 'text';
|
|
@@ -285,7 +308,40 @@
|
|
|
285
308
|
if (clearBtn) clearBtn.addEventListener('click', () => navigate({}));
|
|
286
309
|
}
|
|
287
310
|
|
|
288
|
-
// ---- Inline (spreadsheet-style) cell editing
|
|
311
|
+
// ---- Inline (spreadsheet-style) cell editing & Keyboard Navigation --------
|
|
312
|
+
|
|
313
|
+
let focusedCell = null;
|
|
314
|
+
|
|
315
|
+
function clearFocusedCell() {
|
|
316
|
+
if (focusedCell) {
|
|
317
|
+
focusedCell.classList.remove('cell-focused');
|
|
318
|
+
focusedCell = null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function setFocusedCell(td) {
|
|
323
|
+
if (!td || td === focusedCell || td.classList.contains('js-editing')) return;
|
|
324
|
+
clearFocusedCell();
|
|
325
|
+
focusedCell = td;
|
|
326
|
+
td.classList.add('cell-focused');
|
|
327
|
+
td.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function revertCell(td) {
|
|
331
|
+
const tr = td.closest('tr');
|
|
332
|
+
const pk = tr && tr.dataset.pk;
|
|
333
|
+
const col = td.dataset.col;
|
|
334
|
+
if (!pk || !col) return;
|
|
335
|
+
const origVal = td.dataset.origValue;
|
|
336
|
+
const origNull = td.dataset.origNull === 'true';
|
|
337
|
+
renderCellValue(td, origNull ? null : origVal);
|
|
338
|
+
td.classList.remove('cell-dirty');
|
|
339
|
+
delete td.dataset.origValue;
|
|
340
|
+
delete td.dataset.origNull;
|
|
341
|
+
pendingChanges.delete(pk + '::' + col);
|
|
342
|
+
updateChangesBar();
|
|
343
|
+
UI.showToast('Reverted edit in column "' + col + '".', 'info');
|
|
344
|
+
}
|
|
289
345
|
|
|
290
346
|
function renderCellValue(td, value) {
|
|
291
347
|
const isNull = value === null || value === undefined;
|
|
@@ -293,10 +349,66 @@
|
|
|
293
349
|
td.dataset.null = String(isNull);
|
|
294
350
|
td.dataset.value = display;
|
|
295
351
|
td.classList.remove('js-editing', 'opacity-60');
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
352
|
+
|
|
353
|
+
const isDirty = td.dataset.origValue !== undefined;
|
|
354
|
+
const origVal = td.dataset.origValue;
|
|
355
|
+
const origNull = td.dataset.origNull === 'true';
|
|
356
|
+
const dirtyTitle = isDirty
|
|
357
|
+
? 'Staged edit: ' + (isNull ? 'NULL' : display) + ' (Saved: ' + (origNull ? 'NULL' : origVal) + ')'
|
|
358
|
+
: display;
|
|
359
|
+
|
|
360
|
+
const revertBtnHtml = isDirty
|
|
361
|
+
? '<button type="button" class="cell-revert-btn js-revert-cell" title="Revert this field to saved value">↺</button>'
|
|
362
|
+
: '';
|
|
363
|
+
|
|
364
|
+
if (isNull) {
|
|
365
|
+
td.innerHTML = '<div class="flex items-center justify-between gap-1"><span class="chip chip-null js-cell-value">NULL</span>' + revertBtnHtml + '</div>';
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const col = td.dataset.col;
|
|
370
|
+
const c = colInfo(col);
|
|
371
|
+
const colType = (c?.type || td.dataset.type || '').toUpperCase();
|
|
372
|
+
const isBlob = td.dataset.blob === 'true' || colType.includes('BLOB') || /^0x[0-9a-f]{8,}$/i.test(display);
|
|
373
|
+
const isJson = td.dataset.json === 'true' || ((display.startsWith('{') && display.endsWith('}')) || (display.startsWith('[') && display.endsWith(']')));
|
|
374
|
+
|
|
375
|
+
if (isBlob) {
|
|
376
|
+
const bytesLen = display.startsWith('0x') ? Math.floor((display.length - 2) / 2) : display.length;
|
|
377
|
+
td.innerHTML =
|
|
378
|
+
'<div class="flex items-center justify-between gap-1 js-cell-value">' +
|
|
379
|
+
'<div class="flex items-center gap-1.5 min-w-0">' +
|
|
380
|
+
'<span class="inline-flex items-center gap-1 rounded-md bg-secondary/10 px-1.5 py-0.5 text-[11px] font-mono font-medium text-secondary">🗃️ BLOB · ' + bytesLen + ' B</span>' +
|
|
381
|
+
'<button type="button" class="btn btn-ghost btn-xs btn-circle opacity-0 group-hover:opacity-100 transition-opacity js-inspect-btn" title="Inspect BLOB">' +
|
|
382
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>' +
|
|
383
|
+
'</button>' +
|
|
384
|
+
'</div>' +
|
|
385
|
+
revertBtnHtml +
|
|
386
|
+
'</div>';
|
|
387
|
+
} else if (isJson) {
|
|
388
|
+
td.innerHTML =
|
|
389
|
+
'<div class="flex items-center justify-between gap-1 js-cell-value">' +
|
|
390
|
+
'<div class="flex items-center gap-1.5 min-w-0">' +
|
|
391
|
+
'<span class="inline-flex items-center gap-1 rounded-md bg-info/10 px-1.5 py-0.5 text-[11px] font-mono font-medium text-info">{ } JSON</span>' +
|
|
392
|
+
'<span class="block max-w-[140px] truncate font-mono text-[12px] text-base-content/70" title="' + escapeHtml(dirtyTitle) + '">' + escapeHtml(display) + '</span>' +
|
|
393
|
+
'<button type="button" class="btn btn-ghost btn-xs btn-circle opacity-0 group-hover:opacity-100 transition-opacity js-inspect-btn" title="Inspect JSON">' +
|
|
394
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>' +
|
|
395
|
+
'</button>' +
|
|
396
|
+
'</div>' +
|
|
397
|
+
revertBtnHtml +
|
|
398
|
+
'</div>';
|
|
399
|
+
} else {
|
|
400
|
+
td.innerHTML =
|
|
401
|
+
'<div class="flex items-center justify-between gap-1 group/cell">' +
|
|
402
|
+
'<span class="js-cell-value block max-w-xs truncate font-mono text-[13px]" title="' + escapeHtml(dirtyTitle) + '">' + escapeHtml(display.length > 120 ? display.slice(0, 120) + '…' : display) + '</span>' +
|
|
403
|
+
'<div class="flex items-center gap-0.5 shrink-0">' +
|
|
404
|
+
revertBtnHtml +
|
|
405
|
+
(display.length > 30 ?
|
|
406
|
+
'<button type="button" class="btn btn-ghost btn-xs btn-circle opacity-0 group-hover/cell:opacity-100 transition-opacity js-inspect-btn shrink-0" title="Inspect text">' +
|
|
407
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>' +
|
|
408
|
+
'</button>' : '') +
|
|
409
|
+
'</div>' +
|
|
410
|
+
'</div>';
|
|
411
|
+
}
|
|
300
412
|
}
|
|
301
413
|
|
|
302
414
|
function currentCellValue(td) {
|
|
@@ -309,10 +421,19 @@
|
|
|
309
421
|
if (!c) return null;
|
|
310
422
|
const fk = fkFor(col);
|
|
311
423
|
const type = (c.type || '').toUpperCase();
|
|
312
|
-
const isBool = type === 'BOOLEAN';
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
424
|
+
const isBool = type === 'BOOLEAN' || type === 'BOOL';
|
|
425
|
+
const isDateOnly = type === 'DATE';
|
|
426
|
+
const isTimeOnly = type === 'TIME' || type === 'TIMETZ' || type.startsWith('TIME ') || type.startsWith('TIME(');
|
|
427
|
+
const isTimestamp = type === 'DATETIME' || type.includes('TIMESTAMP');
|
|
428
|
+
const isDate = isDateOnly || isTimeOnly || isTimestamp;
|
|
429
|
+
const isNum = type.startsWith('INT') || type === 'REAL' || type === 'NUMERIC' || type.startsWith('DECIMAL') || type === 'FLOAT' || type === 'DOUBLE' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL') || type === 'MONEY';
|
|
430
|
+
const isBlob = type === 'BLOB' || type === 'BYTEA' || type === 'BINARY';
|
|
431
|
+
const isUuid = type === 'UUID';
|
|
432
|
+
const isJson = type.includes('JSON');
|
|
433
|
+
const isArray = type.endsWith('[]') || type.startsWith('_') || type.includes('ARRAY');
|
|
434
|
+
const isInterval = type === 'INTERVAL';
|
|
435
|
+
const isGeometry = type === 'POINT' || type === 'GEOMETRY' || type === 'BOX' || type === 'CIRCLE' || type === 'POLYGON';
|
|
436
|
+
const isBit = type === 'BIT' || type === 'VARBIT' || type.startsWith('BIT(') || type.startsWith('VARBIT(');
|
|
316
437
|
const value = currentCellValue(td);
|
|
317
438
|
const nullish = value === null || value === '';
|
|
318
439
|
|
|
@@ -332,35 +453,70 @@
|
|
|
332
453
|
});
|
|
333
454
|
} else if (isDate) {
|
|
334
455
|
el = document.createElement('input');
|
|
335
|
-
el.type =
|
|
336
|
-
|
|
337
|
-
|
|
456
|
+
el.type = isDateOnly ? 'date' : (isTimeOnly ? 'time' : 'datetime-local');
|
|
457
|
+
if (isTimeOnly || isTimestamp) el.step = '1';
|
|
458
|
+
el.className = 'field-input !w-64 !py-1.5 !text-[13px] js-inline-ctrl font-mono';
|
|
459
|
+
if (!nullish) el.value = toDateInputValue(String(value), isDateOnly, isTimeOnly);
|
|
460
|
+
setTimeout(() => {
|
|
461
|
+
if (typeof el.showPicker === 'function') {
|
|
462
|
+
try { el.showPicker(); } catch (e) {}
|
|
463
|
+
}
|
|
464
|
+
}, 50);
|
|
338
465
|
} else if (isNum) {
|
|
339
466
|
el = document.createElement('input');
|
|
340
467
|
el.type = 'number';
|
|
341
|
-
|
|
342
|
-
el.
|
|
468
|
+
const isInteger = type.startsWith('INTEGER') || type === 'INT' || type.startsWith('BIGINT') || type.startsWith('SMALLINT') || type.startsWith('SERIAL');
|
|
469
|
+
el.step = isInteger ? '1' : 'any';
|
|
470
|
+
el.className = 'field-input !w-48 !py-1.5 !text-[13px] js-inline-ctrl font-mono';
|
|
471
|
+
if (!nullish) el.value = String(value);
|
|
472
|
+
} else if (isUuid) {
|
|
473
|
+
el = document.createElement('input');
|
|
474
|
+
el.type = 'text';
|
|
475
|
+
el.className = 'field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl';
|
|
476
|
+
el.placeholder = '00000000-0000-0000-0000-000000000000';
|
|
477
|
+
if (!nullish) el.value = String(value);
|
|
478
|
+
} else if (isArray) {
|
|
479
|
+
el = document.createElement('input');
|
|
480
|
+
el.type = 'text';
|
|
481
|
+
el.className = 'field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl';
|
|
482
|
+
el.placeholder = '{item1, item2}';
|
|
483
|
+
if (!nullish) el.value = String(value);
|
|
484
|
+
} else if (isInterval) {
|
|
485
|
+
el = document.createElement('input');
|
|
486
|
+
el.type = 'text';
|
|
487
|
+
el.className = 'field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl';
|
|
488
|
+
el.placeholder = "e.g. '1 day'";
|
|
489
|
+
if (!nullish) el.value = String(value);
|
|
490
|
+
} else if (isGeometry) {
|
|
491
|
+
el = document.createElement('input');
|
|
492
|
+
el.type = 'text';
|
|
493
|
+
el.className = 'field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl';
|
|
494
|
+
el.placeholder = "(x, y)";
|
|
495
|
+
if (!nullish) el.value = String(value);
|
|
496
|
+
} else if (isBit) {
|
|
497
|
+
el = document.createElement('input');
|
|
498
|
+
el.type = 'text';
|
|
499
|
+
el.className = 'field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl';
|
|
500
|
+
el.placeholder = "101010";
|
|
343
501
|
if (!nullish) el.value = String(value);
|
|
344
502
|
} else {
|
|
345
503
|
el = document.createElement('input');
|
|
346
504
|
el.type = 'text';
|
|
347
505
|
el.className = 'field-input !w-64 !py-1.5 !text-[13px] js-inline-ctrl';
|
|
348
|
-
el.placeholder = isBlob ? '0x… hex or text' : '';
|
|
506
|
+
el.placeholder = isBlob ? (type === 'BYTEA' ? '\\x… or 0x… hex' : '0x… hex or text') : (isJson ? '{"key":"value"}' : '');
|
|
349
507
|
if (!nullish) el.value = String(value);
|
|
350
508
|
}
|
|
351
509
|
return el;
|
|
352
510
|
}
|
|
353
511
|
|
|
512
|
+
|
|
513
|
+
|
|
354
514
|
function restoreCell(td) {
|
|
355
515
|
renderCellValue(td, td._origNull ? null : td._origValue);
|
|
356
516
|
td.classList.remove('js-editing');
|
|
357
517
|
}
|
|
358
518
|
|
|
359
|
-
// ---- Staged
|
|
360
|
-
// Edits are buffered locally and marked in the grid; nothing is written to
|
|
361
|
-
// the database until "Apply" sends every pending cell change to the
|
|
362
|
-
// bulk-update endpoint in a single transaction. "Discard" reverts everything
|
|
363
|
-
// back to the values loaded from the server.
|
|
519
|
+
// ---- Staged cell edits --------------------------------------------------
|
|
364
520
|
|
|
365
521
|
const pendingChanges = new Map(); // key: `${pk}::${col}` → staged change
|
|
366
522
|
|
|
@@ -381,13 +537,11 @@
|
|
|
381
537
|
else rawValue = v;
|
|
382
538
|
}
|
|
383
539
|
|
|
384
|
-
// Remember the server-loaded value the first time this cell is edited.
|
|
385
540
|
if (td.dataset.origValue === undefined) {
|
|
386
541
|
td.dataset.origValue = td.dataset.value;
|
|
387
542
|
td.dataset.origNull = td.dataset.null === 'true' ? 'true' : 'false';
|
|
388
543
|
}
|
|
389
544
|
|
|
390
|
-
// Reflect the staged value in the cell immediately.
|
|
391
545
|
renderCellValue(td, setNull ? null : rawValue);
|
|
392
546
|
|
|
393
547
|
pendingChanges.set(pk + '::' + col, {
|
|
@@ -438,7 +592,16 @@
|
|
|
438
592
|
});
|
|
439
593
|
}
|
|
440
594
|
|
|
441
|
-
function discardPending() {
|
|
595
|
+
async function discardPending() {
|
|
596
|
+
const n = pendingChanges.size;
|
|
597
|
+
if (!n) return;
|
|
598
|
+
const confirmed = await UI.confirm({
|
|
599
|
+
title: 'Discard Changes',
|
|
600
|
+
message: 'You have ' + n + ' pending inline edit(s). Are you sure you want to discard all staged changes and revert to the saved values?',
|
|
601
|
+
confirmText: 'Discard ' + n + ' change(s)',
|
|
602
|
+
danger: true,
|
|
603
|
+
});
|
|
604
|
+
if (!confirmed) return;
|
|
442
605
|
for (const ch of pendingChanges.values()) {
|
|
443
606
|
renderCellValue(ch.td, ch.origNull ? null : ch.origValue);
|
|
444
607
|
ch.td.classList.remove('cell-dirty');
|
|
@@ -447,21 +610,119 @@
|
|
|
447
610
|
}
|
|
448
611
|
pendingChanges.clear();
|
|
449
612
|
updateChangesBar();
|
|
613
|
+
UI.showToast('Pending edits discarded.', 'info');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function openReviewChangesModal() {
|
|
617
|
+
if (!pendingChanges.size) return;
|
|
618
|
+
var overlay = document.createElement('div');
|
|
619
|
+
overlay.className = 'fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-xs p-4 opacity-0 transition-opacity duration-200';
|
|
620
|
+
var card = document.createElement('div');
|
|
621
|
+
card.className = 'app-card w-full max-w-3xl overflow-hidden shadow-2xl border border-base-300 transform scale-95 transition-transform duration-200 max-h-[85vh] flex flex-col';
|
|
622
|
+
|
|
623
|
+
var rowsHtml = '';
|
|
624
|
+
pendingChanges.forEach((ch, key) => {
|
|
625
|
+
var oldDisplay = ch.origNull ? '<span class="chip chip-null text-[10px]">NULL</span>' : escapeHtml(ch.origValue);
|
|
626
|
+
var newDisplay = ch.setNull ? '<span class="chip chip-null text-[10px]">NULL</span>' : escapeHtml(ch.rawValue);
|
|
627
|
+
rowsHtml += '<tr class="hover:bg-base-200/50">' +
|
|
628
|
+
'<td class="font-mono text-xs font-semibold">' + escapeHtml(ch.pk) + '</td>' +
|
|
629
|
+
'<td class="font-mono text-xs text-primary font-medium">' + escapeHtml(ch.col) + '</td>' +
|
|
630
|
+
'<td class="font-mono text-xs text-error line-through opacity-80 max-w-xs truncate">' + oldDisplay + '</td>' +
|
|
631
|
+
'<td class="font-mono text-xs text-success font-medium max-w-xs truncate">' + newDisplay + '</td>' +
|
|
632
|
+
'<td class="text-right">' +
|
|
633
|
+
'<button type="button" class="btn btn-ghost btn-xs text-warning js-modal-revert-item" data-key="' + escapeHtml(key) + '">Revert</button>' +
|
|
634
|
+
'</td>' +
|
|
635
|
+
'</tr>';
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
card.innerHTML =
|
|
639
|
+
'<div class="flex items-center justify-between border-b border-base-200 px-6 py-4">' +
|
|
640
|
+
' <div class="flex items-center gap-2">' +
|
|
641
|
+
' <h3 class="text-base font-bold text-base-content">Staged Changes Diff</h3>' +
|
|
642
|
+
' <span class="chip bg-warning/15 text-warning font-mono">' + pendingChanges.size + ' edit(s)</span>' +
|
|
643
|
+
' </div>' +
|
|
644
|
+
' <button type="button" class="btn btn-ghost btn-xs btn-circle js-diff-close text-base-content/40 hover:text-base-content">×</button>' +
|
|
645
|
+
'</div>' +
|
|
646
|
+
'<div class="flex-1 overflow-y-auto p-4">' +
|
|
647
|
+
' <table class="table table-sm">' +
|
|
648
|
+
' <thead><tr class="text-xs uppercase text-base-content/50"><th>Row PK</th><th>Column</th><th>Original Value</th><th>Staged Value</th><th class="text-right">Action</th></tr></thead>' +
|
|
649
|
+
' <tbody id="diff-rows-body">' + rowsHtml + '</tbody>' +
|
|
650
|
+
' </table>' +
|
|
651
|
+
'</div>' +
|
|
652
|
+
'<div class="flex items-center justify-between border-t border-base-200 bg-base-200/30 px-6 py-4">' +
|
|
653
|
+
' <button type="button" id="diff-modal-discard" class="btn btn-ghost btn-sm text-error">Discard All</button>' +
|
|
654
|
+
' <div class="flex gap-2">' +
|
|
655
|
+
' <button type="button" class="btn btn-ghost btn-sm js-diff-close">Cancel</button>' +
|
|
656
|
+
' <button type="button" id="diff-modal-apply" class="btn btn-primary btn-sm gap-1">' +
|
|
657
|
+
' <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>' +
|
|
658
|
+
' Apply ' + pendingChanges.size + ' Change(s)' +
|
|
659
|
+
' </button>' +
|
|
660
|
+
' </div>' +
|
|
661
|
+
'</div>';
|
|
662
|
+
|
|
663
|
+
overlay.appendChild(card);
|
|
664
|
+
document.body.appendChild(overlay);
|
|
665
|
+
|
|
666
|
+
requestAnimationFrame(() => {
|
|
667
|
+
overlay.classList.remove('opacity-0');
|
|
668
|
+
overlay.classList.add('opacity-100');
|
|
669
|
+
card.classList.remove('scale-95');
|
|
670
|
+
card.classList.add('scale-100');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
function closeDiff() {
|
|
674
|
+
overlay.classList.remove('opacity-100');
|
|
675
|
+
overlay.classList.add('opacity-0');
|
|
676
|
+
card.classList.remove('scale-100');
|
|
677
|
+
card.classList.add('scale-95');
|
|
678
|
+
setTimeout(() => overlay.remove(), 180);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
card.querySelectorAll('.js-diff-close').forEach((b) => b.addEventListener('click', closeDiff));
|
|
682
|
+
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDiff(); });
|
|
683
|
+
|
|
684
|
+
card.querySelectorAll('.js-modal-revert-item').forEach((b) => {
|
|
685
|
+
b.addEventListener('click', () => {
|
|
686
|
+
const key = b.dataset.key;
|
|
687
|
+
const ch = pendingChanges.get(key);
|
|
688
|
+
if (ch) {
|
|
689
|
+
revertCell(ch.td);
|
|
690
|
+
b.closest('tr').remove();
|
|
691
|
+
if (!pendingChanges.size) closeDiff();
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
card.querySelector('#diff-modal-apply').addEventListener('click', () => {
|
|
697
|
+
closeDiff();
|
|
698
|
+
applyPending();
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
card.querySelector('#diff-modal-discard').addEventListener('click', async () => {
|
|
702
|
+
closeDiff();
|
|
703
|
+
await discardPending();
|
|
704
|
+
});
|
|
450
705
|
}
|
|
451
706
|
|
|
452
707
|
function buildChangesBar() {
|
|
453
708
|
const bar = document.createElement('div');
|
|
454
709
|
bar.id = 'changes-bar';
|
|
455
710
|
bar.className =
|
|
456
|
-
'fixed bottom-
|
|
711
|
+
'fixed bottom-5 left-1/2 z-50 flex -translate-x-1/2 items-center gap-3 rounded-2xl border border-base-300 bg-base-100/95 backdrop-blur-md px-4 py-2.5 shadow-2xl transition-all duration-200 hidden';
|
|
457
712
|
bar.innerHTML =
|
|
458
|
-
'<
|
|
459
|
-
'<span class="
|
|
713
|
+
'<div class="flex items-center gap-2">' +
|
|
714
|
+
'<span class="relative flex h-2.5 w-2.5"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-warning opacity-75"></span><span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-warning"></span></span>' +
|
|
715
|
+
'<span class="text-sm font-medium text-base-content"><b id="changes-count" class="tabular-nums text-warning font-bold">0</b> pending edit(s)</span>' +
|
|
716
|
+
'</div>' +
|
|
717
|
+
'<div class="h-4 w-px bg-base-300"></div>' +
|
|
460
718
|
'<div class="flex items-center gap-1.5">' +
|
|
461
|
-
'<button type="button" id="changes-
|
|
462
|
-
'<svg xmlns="http://www.w3.org/2000/svg" class="h-
|
|
719
|
+
'<button type="button" id="changes-review" class="btn btn-ghost btn-sm gap-1.5 text-xs font-medium">' +
|
|
720
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>' +
|
|
721
|
+
'Review Diff</button>' +
|
|
722
|
+
'<button type="button" id="changes-apply" class="btn btn-primary btn-sm gap-1 text-xs font-semibold">' +
|
|
723
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" /></svg>' +
|
|
463
724
|
'Apply</button>' +
|
|
464
|
-
'<button type="button" id="changes-discard" class="btn btn-ghost btn-sm">Discard</button>' +
|
|
725
|
+
'<button type="button" id="changes-discard" class="btn btn-ghost btn-sm text-xs text-base-content/60 hover:text-error">Discard</button>' +
|
|
465
726
|
'</div>';
|
|
466
727
|
document.body.appendChild(bar);
|
|
467
728
|
return bar;
|
|
@@ -479,7 +740,8 @@
|
|
|
479
740
|
const c = colInfo(td.dataset.col);
|
|
480
741
|
if (!c) return;
|
|
481
742
|
|
|
482
|
-
|
|
743
|
+
setFocusedCell(td);
|
|
744
|
+
|
|
483
745
|
const open = document.querySelector('.js-editing');
|
|
484
746
|
if (open && open._finish) open._finish(true);
|
|
485
747
|
|
|
@@ -493,18 +755,30 @@
|
|
|
493
755
|
td.appendChild(el);
|
|
494
756
|
|
|
495
757
|
let committed = false;
|
|
496
|
-
const finish = (commit) => {
|
|
758
|
+
const finish = (commit, navigateNext) => {
|
|
497
759
|
if (committed) return;
|
|
498
760
|
committed = true;
|
|
499
761
|
if (commit) stageCell(tr, td, el);
|
|
500
762
|
else restoreCell(td);
|
|
763
|
+
if (navigateNext === 'down') navigateCell('down');
|
|
764
|
+
else if (navigateNext === 'right') navigateCell('right');
|
|
765
|
+
else if (navigateNext === 'left') navigateCell('left');
|
|
501
766
|
};
|
|
502
767
|
td._finish = finish;
|
|
503
768
|
|
|
504
769
|
el.addEventListener('keydown', (e) => {
|
|
505
|
-
if (e.key === 'Enter') {
|
|
506
|
-
|
|
770
|
+
if (e.key === 'Enter') {
|
|
771
|
+
e.preventDefault();
|
|
772
|
+
finish(true, 'down');
|
|
773
|
+
} else if (e.key === 'Tab') {
|
|
774
|
+
e.preventDefault();
|
|
775
|
+
finish(true, e.shiftKey ? 'left' : 'right');
|
|
776
|
+
} else if (e.key === 'Escape') {
|
|
777
|
+
e.preventDefault();
|
|
778
|
+
finish(false);
|
|
779
|
+
}
|
|
507
780
|
});
|
|
781
|
+
|
|
508
782
|
if (el.tagName === 'SELECT') {
|
|
509
783
|
el.addEventListener('change', () => finish(true));
|
|
510
784
|
} else if (el.type === 'checkbox') {
|
|
@@ -516,17 +790,140 @@
|
|
|
516
790
|
if (typeof el.select === 'function') el.select();
|
|
517
791
|
}
|
|
518
792
|
|
|
793
|
+
function getEditableCells() {
|
|
794
|
+
return Array.prototype.slice.call(document.querySelectorAll('#main-table tbody tr td.cell-editable'));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function navigateCell(direction) {
|
|
798
|
+
if (!focusedCell) {
|
|
799
|
+
const cells = getEditableCells();
|
|
800
|
+
if (cells.length) setFocusedCell(cells[0]);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const tr = focusedCell.closest('tr');
|
|
805
|
+
if (!tr) return;
|
|
806
|
+
const rowCells = Array.prototype.slice.call(tr.querySelectorAll('td.cell-editable'));
|
|
807
|
+
const colIdx = rowCells.indexOf(focusedCell);
|
|
808
|
+
|
|
809
|
+
if (direction === 'right') {
|
|
810
|
+
if (colIdx < rowCells.length - 1) {
|
|
811
|
+
setFocusedCell(rowCells[colIdx + 1]);
|
|
812
|
+
} else {
|
|
813
|
+
const nextTr = tr.nextElementSibling;
|
|
814
|
+
if (nextTr) {
|
|
815
|
+
const nextCells = Array.prototype.slice.call(nextTr.querySelectorAll('td.cell-editable'));
|
|
816
|
+
if (nextCells.length) setFocusedCell(nextCells[0]);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} else if (direction === 'left') {
|
|
820
|
+
if (colIdx > 0) {
|
|
821
|
+
setFocusedCell(rowCells[colIdx - 1]);
|
|
822
|
+
} else {
|
|
823
|
+
const prevTr = tr.previousElementSibling;
|
|
824
|
+
if (prevTr) {
|
|
825
|
+
const prevCells = Array.prototype.slice.call(prevTr.querySelectorAll('td.cell-editable'));
|
|
826
|
+
if (prevCells.length) setFocusedCell(prevCells[prevCells.length - 1]);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} else if (direction === 'down') {
|
|
830
|
+
const nextTr = tr.nextElementSibling;
|
|
831
|
+
if (nextTr) {
|
|
832
|
+
const nextCells = Array.prototype.slice.call(nextTr.querySelectorAll('td.cell-editable'));
|
|
833
|
+
if (nextCells[colIdx]) setFocusedCell(nextCells[colIdx]);
|
|
834
|
+
}
|
|
835
|
+
} else if (direction === 'up') {
|
|
836
|
+
const prevTr = tr.previousElementSibling;
|
|
837
|
+
if (prevTr) {
|
|
838
|
+
const prevCells = Array.prototype.slice.call(prevTr.querySelectorAll('td.cell-editable'));
|
|
839
|
+
if (prevCells[colIdx]) setFocusedCell(prevCells[colIdx]);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
519
844
|
function initInlineEditing() {
|
|
520
|
-
if (readonly) return;
|
|
521
845
|
changesBar = buildChangesBar();
|
|
522
846
|
changesCount = document.getElementById('changes-count');
|
|
523
847
|
applyBtn = document.getElementById('changes-apply');
|
|
524
848
|
const discardBtn = document.getElementById('changes-discard');
|
|
849
|
+
const reviewBtn = document.getElementById('changes-review');
|
|
850
|
+
|
|
525
851
|
if (applyBtn) applyBtn.addEventListener('click', applyPending);
|
|
526
852
|
if (discardBtn) discardBtn.addEventListener('click', discardPending);
|
|
853
|
+
if (reviewBtn) reviewBtn.addEventListener('click', openReviewChangesModal);
|
|
854
|
+
|
|
855
|
+
// Global click listener for table cells and single-cell revert
|
|
856
|
+
document.addEventListener('click', (e) => {
|
|
857
|
+
const revertBtn = e.target.closest('.js-revert-cell');
|
|
858
|
+
if (revertBtn) {
|
|
859
|
+
e.stopPropagation();
|
|
860
|
+
e.preventDefault();
|
|
861
|
+
const td = revertBtn.closest('td');
|
|
862
|
+
if (td) revertCell(td);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const td = e.target.closest('td.cell-editable');
|
|
867
|
+
if (td && !td.classList.contains('js-editing')) {
|
|
868
|
+
setFocusedCell(td);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
if (!readonly) {
|
|
873
|
+
document.querySelectorAll('.js-inline-cell').forEach((td) => {
|
|
874
|
+
td.addEventListener('dblclick', () => beginEdit(td));
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Grid Keyboard Navigation
|
|
879
|
+
document.addEventListener('keydown', (e) => {
|
|
880
|
+
const isEditing = document.querySelector('.js-editing');
|
|
881
|
+
if (isEditing) return;
|
|
527
882
|
|
|
528
|
-
|
|
529
|
-
|
|
883
|
+
const activeTag = document.activeElement ? document.activeElement.tagName : '';
|
|
884
|
+
if (activeTag === 'INPUT' || activeTag === 'TEXTAREA' || activeTag === 'SELECT') return;
|
|
885
|
+
|
|
886
|
+
if (!focusedCell) return;
|
|
887
|
+
|
|
888
|
+
if (e.key === 'ArrowRight') {
|
|
889
|
+
e.preventDefault();
|
|
890
|
+
navigateCell('right');
|
|
891
|
+
} else if (e.key === 'ArrowLeft') {
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
navigateCell('left');
|
|
894
|
+
} else if (e.key === 'ArrowDown') {
|
|
895
|
+
e.preventDefault();
|
|
896
|
+
navigateCell('down');
|
|
897
|
+
} else if (e.key === 'ArrowUp') {
|
|
898
|
+
e.preventDefault();
|
|
899
|
+
navigateCell('up');
|
|
900
|
+
} else if (e.key === 'Tab') {
|
|
901
|
+
e.preventDefault();
|
|
902
|
+
navigateCell(e.shiftKey ? 'left' : 'right');
|
|
903
|
+
} else if (e.key === 'Enter') {
|
|
904
|
+
e.preventDefault();
|
|
905
|
+
if (!readonly) beginEdit(focusedCell);
|
|
906
|
+
} else if (e.key === 'Escape') {
|
|
907
|
+
clearFocusedCell();
|
|
908
|
+
} else if (e.key === ' ' && !readonly) {
|
|
909
|
+
const col = focusedCell.dataset.col;
|
|
910
|
+
const c = colInfo(col);
|
|
911
|
+
if (c && (c.type || '').toUpperCase() === 'BOOLEAN') {
|
|
912
|
+
e.preventDefault();
|
|
913
|
+
const tr = focusedCell.closest('tr');
|
|
914
|
+
const currentVal = currentCellValue(focusedCell);
|
|
915
|
+
const nextVal = (currentVal === 1 || currentVal === '1' || currentVal === true || currentVal === 'true') ? '0' : '1';
|
|
916
|
+
const fakeEl = { value: nextVal, type: 'checkbox', checked: nextVal === '1' };
|
|
917
|
+
stageCell(tr, focusedCell, fakeEl);
|
|
918
|
+
}
|
|
919
|
+
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') {
|
|
920
|
+
const val = currentCellValue(focusedCell);
|
|
921
|
+
if (val !== null && val !== undefined) {
|
|
922
|
+
navigator.clipboard.writeText(String(val)).then(() => {
|
|
923
|
+
UI.showToast('Cell value copied.', 'info');
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
530
927
|
});
|
|
531
928
|
}
|
|
532
929
|
|
|
@@ -609,62 +1006,32 @@
|
|
|
609
1006
|
const refs = (impact && impact.references) || [];
|
|
610
1007
|
const details = refs.length
|
|
611
1008
|
? '<p class="mb-2 font-medium text-warning">⚠ Other tables reference these records:</p>' +
|
|
612
|
-
'<ul class="mb-
|
|
1009
|
+
'<ul class="mb-2 list-disc space-y-1 pl-5 text-xs">' +
|
|
613
1010
|
refs.map((r) =>
|
|
614
|
-
'<li><code class="font-mono">' + escapeHtml(r.table) + '.' + escapeHtml(r.from) + '</code> — <b class="tabular-nums">' + r.count + '</b> row(s) point at them</li>'
|
|
1011
|
+
'<li><code class="font-mono font-bold">' + escapeHtml(r.table) + '.' + escapeHtml(r.from) + '</code> — <b class="tabular-nums">' + r.count + '</b> row(s) point at them</li>'
|
|
615
1012
|
).join('') +
|
|
616
1013
|
'</ul>' +
|
|
617
|
-
'<p class="text-
|
|
618
|
-
: '<p class="text-base-content/60">No other tables reference these rows —
|
|
619
|
-
|
|
620
|
-
const
|
|
621
|
-
'
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
confirmLabel: 'Delete ' + ids.length + ' row(s)',
|
|
1014
|
+
'<p class="text-[11px] text-base-content/55">Depending on foreign key rules, those rows may cascade-delete or block this action.</p>'
|
|
1015
|
+
: '<p class="text-xs text-base-content/60">No other tables reference these rows — safe to delete.</p>';
|
|
1016
|
+
|
|
1017
|
+
const confirmed = await UI.confirm({
|
|
1018
|
+
title: 'Delete Selected Rows',
|
|
1019
|
+
message: 'Delete ' + ids.length + ' selected row(s) from table "' + cfg.table + '"? This action cannot be undone.',
|
|
1020
|
+
item: cfg.table,
|
|
1021
|
+
itemType: 'Table',
|
|
1022
|
+
detailsHtml: details,
|
|
1023
|
+
confirmText: 'Delete ' + ids.length + ' row(s)',
|
|
628
1024
|
danger: true,
|
|
629
|
-
onConfirm: async () => {
|
|
630
|
-
try {
|
|
631
|
-
const data = await Api.post('/api/tables/' + t + '/rows/bulk-delete', { ids, confirmImpact: true });
|
|
632
|
-
UI.showToast(data.message || 'Rows deleted.', 'success');
|
|
633
|
-
window.location.reload();
|
|
634
|
-
} catch (e) {
|
|
635
|
-
UI.showError(e.message);
|
|
636
|
-
}
|
|
637
|
-
},
|
|
638
1025
|
});
|
|
639
|
-
|
|
1026
|
+
if (!confirmed) return;
|
|
640
1027
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
'<div class="max-h-[55vh] overflow-auto px-5 py-4 text-sm leading-relaxed">' + opts.message + '</div>' +
|
|
649
|
-
'<div class="flex flex-wrap justify-end gap-2 border-t border-base-200 px-5 py-3">' +
|
|
650
|
-
'<button type="button" class="btn btn-ghost btn-sm js-modal-cancel">Cancel</button>' +
|
|
651
|
-
'<button type="button" class="btn btn-sm ' + (opts.danger ? 'btn-error' : 'btn-primary') + ' js-modal-ok">' + escapeHtml(opts.confirmLabel || 'Confirm') + '</button>' +
|
|
652
|
-
'</div>';
|
|
653
|
-
overlay.appendChild(card);
|
|
654
|
-
document.body.appendChild(overlay);
|
|
655
|
-
|
|
656
|
-
const onKey = (e) => { if (e.key === 'Escape') close(false); };
|
|
657
|
-
const close = (result) => {
|
|
658
|
-
document.removeEventListener('keydown', onKey);
|
|
659
|
-
overlay.remove();
|
|
660
|
-
if (result && opts.onConfirm) opts.onConfirm();
|
|
661
|
-
};
|
|
662
|
-
document.addEventListener('keydown', onKey);
|
|
663
|
-
overlay.querySelector('.js-modal-cancel').addEventListener('click', () => close(false));
|
|
664
|
-
overlay.querySelector('.js-modal-ok').addEventListener('click', () => close(true));
|
|
665
|
-
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); });
|
|
666
|
-
const okBtn = overlay.querySelector('.js-modal-ok');
|
|
667
|
-
if (okBtn) okBtn.focus();
|
|
1028
|
+
try {
|
|
1029
|
+
const data = await Api.post('/api/tables/' + t + '/rows/bulk-delete', { ids, confirmImpact: true });
|
|
1030
|
+
UI.showToast(data.message || 'Rows deleted.', 'success');
|
|
1031
|
+
window.location.reload();
|
|
1032
|
+
} catch (e) {
|
|
1033
|
+
UI.showError(e.message);
|
|
1034
|
+
}
|
|
668
1035
|
}
|
|
669
1036
|
|
|
670
1037
|
// ---- CSV import ---------------------------------------------------------
|
|
@@ -741,24 +1108,26 @@
|
|
|
741
1108
|
|
|
742
1109
|
function renderMiniTable(ref, basePath) {
|
|
743
1110
|
const href = basePath + '/tables/' + encodeURIComponent(ref.table);
|
|
1111
|
+
const count = ref.count ?? ref.total ?? (ref.rows ? ref.rows.length : 0);
|
|
1112
|
+
const columns = ref.columns || (ref.rows && ref.rows.length > 0 ? Object.keys(ref.rows[0]).filter((k) => k !== '_rowid_') : []);
|
|
744
1113
|
let html =
|
|
745
1114
|
'<div class="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2">' +
|
|
746
1115
|
'<span class="min-w-0 truncate">' +
|
|
747
1116
|
'<a class="link link-primary font-mono text-xs font-semibold" href="' + href + '">' + escapeHtml(ref.table) + '</a>' +
|
|
748
|
-
'<span class="ml-1.5 text-xs text-base-content/45">via <code class="font-mono">' + escapeHtml(ref.from) + '</code> = <code class="font-mono">' + escapeHtml(String(ref.value)) + '</code
|
|
1117
|
+
'<span class="ml-1.5 text-xs text-base-content/45">via <code class="font-mono">' + escapeHtml(ref.from) + '</code>' + (ref.value !== undefined ? ' = <code class="font-mono">' + escapeHtml(String(ref.value)) + '</code>' : '') + '</span>' +
|
|
749
1118
|
'</span>' +
|
|
750
|
-
'<span class="chip ' + (
|
|
1119
|
+
'<span class="chip ' + (count ? 'chip-pk' : 'chip-type') + '">' + escapeHtml(count + ' row' + (count === 1 ? '' : 's')) + '</span>' +
|
|
751
1120
|
'</div>';
|
|
752
1121
|
|
|
753
|
-
if (!ref.rows.length) {
|
|
1122
|
+
if (!ref.rows || !ref.rows.length || !columns.length) {
|
|
754
1123
|
return html + '<div class="px-3 py-3 text-xs text-base-content/45">No matching rows.</div>';
|
|
755
1124
|
}
|
|
756
1125
|
|
|
757
|
-
const thead = '<tr>' +
|
|
1126
|
+
const thead = '<tr>' + columns.map(function (c) {
|
|
758
1127
|
return '<th class="whitespace-nowrap px-2.5 py-1.5 text-left text-[10px] font-semibold uppercase tracking-wide text-base-content/50"><span class="font-mono normal-case">' + escapeHtml(c) + '</span></th>';
|
|
759
1128
|
}).join('') + '</tr>';
|
|
760
1129
|
const tbody = ref.rows.map(function (r) {
|
|
761
|
-
return '<tr class="border-t border-base-200/60 hover:bg-primary/[0.04]">' +
|
|
1130
|
+
return '<tr class="border-t border-base-200/60 hover:bg-primary/[0.04]">' + columns.map(function (c) {
|
|
762
1131
|
const v = r[c];
|
|
763
1132
|
if (v === null || v === undefined) return '<td class="px-2.5 py-1.5 align-middle"><span class="text-[11px] text-base-content/40">NULL</span></td>';
|
|
764
1133
|
const s = String(v);
|
|
@@ -858,6 +1227,122 @@
|
|
|
858
1227
|
});
|
|
859
1228
|
}
|
|
860
1229
|
|
|
1230
|
+
// ---- Cell Inspector Trigger --------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
function initCellInspector() {
|
|
1233
|
+
document.addEventListener('click', (e) => {
|
|
1234
|
+
const btn = e.target.closest('.js-inspect-btn');
|
|
1235
|
+
if (!btn) return;
|
|
1236
|
+
e.stopPropagation();
|
|
1237
|
+
e.preventDefault();
|
|
1238
|
+
const td = btn.closest('td');
|
|
1239
|
+
const tr = btn.closest('tr');
|
|
1240
|
+
if (!td || !tr) return;
|
|
1241
|
+
|
|
1242
|
+
const col = td.dataset.col;
|
|
1243
|
+
const isNull = td.dataset.null === 'true';
|
|
1244
|
+
const val = td.dataset.value;
|
|
1245
|
+
const pk = tr.dataset.pk;
|
|
1246
|
+
const table = tr.dataset.table || cfg.table;
|
|
1247
|
+
const isBlob = td.dataset.blob === 'true';
|
|
1248
|
+
const isJson = td.dataset.json === 'true';
|
|
1249
|
+
const colType = td.dataset.type || (colInfo(col)?.type ?? '');
|
|
1250
|
+
|
|
1251
|
+
if (window.Inspector && window.Inspector.open) {
|
|
1252
|
+
window.Inspector.open({
|
|
1253
|
+
table,
|
|
1254
|
+
pk,
|
|
1255
|
+
col,
|
|
1256
|
+
colType,
|
|
1257
|
+
value: val,
|
|
1258
|
+
isNull,
|
|
1259
|
+
isBlob,
|
|
1260
|
+
isJson,
|
|
1261
|
+
readonly,
|
|
1262
|
+
onSave: (newVal) => {
|
|
1263
|
+
if (!readonly) {
|
|
1264
|
+
const fakeEl = { value: newVal, type: 'text' };
|
|
1265
|
+
stageCell(tr, td, fakeEl);
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
// ---- Row Quick Actions --------------------------------------------------
|
|
1276
|
+
|
|
1277
|
+
function initRowActions() {
|
|
1278
|
+
// Copy Row as JSON
|
|
1279
|
+
document.querySelectorAll('.js-row-copy-json').forEach((btn) => {
|
|
1280
|
+
btn.addEventListener('click', (e) => {
|
|
1281
|
+
e.stopPropagation();
|
|
1282
|
+
const tr = btn.closest('tr');
|
|
1283
|
+
if (!tr) return;
|
|
1284
|
+
const rowData = {};
|
|
1285
|
+
tr.querySelectorAll('td.cell-editable').forEach((td) => {
|
|
1286
|
+
const col = td.dataset.col;
|
|
1287
|
+
if (col) {
|
|
1288
|
+
rowData[col] = td.dataset.null === 'true' ? null : td.dataset.value;
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
navigator.clipboard.writeText(JSON.stringify(rowData, null, 2)).then(() => {
|
|
1292
|
+
UI.showToast('Row copied as JSON.', 'success');
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// Copy Row as SQL INSERT
|
|
1298
|
+
document.querySelectorAll('.js-row-copy-sql').forEach((btn) => {
|
|
1299
|
+
btn.addEventListener('click', (e) => {
|
|
1300
|
+
e.stopPropagation();
|
|
1301
|
+
const tr = btn.closest('tr');
|
|
1302
|
+
if (!tr) return;
|
|
1303
|
+
const cols = [];
|
|
1304
|
+
const vals = [];
|
|
1305
|
+
tr.querySelectorAll('td.cell-editable').forEach((td) => {
|
|
1306
|
+
const col = td.dataset.col;
|
|
1307
|
+
if (col) {
|
|
1308
|
+
cols.push('"' + col.replace(/"/g, '""') + '"');
|
|
1309
|
+
if (td.dataset.null === 'true') {
|
|
1310
|
+
vals.push('NULL');
|
|
1311
|
+
} else {
|
|
1312
|
+
const v = td.dataset.value;
|
|
1313
|
+
vals.push("'" + String(v).replace(/'/g, "''") + "'");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
const sql = 'INSERT INTO "' + cfg.table.replace(/"/g, '""') + '" (' + cols.join(', ') + ') VALUES (' + vals.join(', ') + ');';
|
|
1318
|
+
navigator.clipboard.writeText(sql).then(() => {
|
|
1319
|
+
UI.showToast('SQL INSERT copied to clipboard.', 'success');
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Duplicate Row
|
|
1325
|
+
document.querySelectorAll('.js-row-duplicate').forEach((btn) => {
|
|
1326
|
+
btn.addEventListener('click', (e) => {
|
|
1327
|
+
e.stopPropagation();
|
|
1328
|
+
const pk = btn.dataset.pk;
|
|
1329
|
+
if (!pk) return;
|
|
1330
|
+
window.location.href = base + '/tables/' + t + '/rows/new?duplicate=' + encodeURIComponent(pk);
|
|
1331
|
+
});
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
// Auto-position dropdown (dropdown-top if near bottom)
|
|
1335
|
+
document.querySelectorAll('.dropdown').forEach((dd) => {
|
|
1336
|
+
const btn = dd.querySelector('label[role="button"]') || dd.querySelector('button');
|
|
1337
|
+
if (!btn) return;
|
|
1338
|
+
btn.addEventListener('click', () => {
|
|
1339
|
+
const rect = btn.getBoundingClientRect();
|
|
1340
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
1341
|
+
dd.classList.toggle('dropdown-top', spaceBelow < 220);
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
861
1346
|
// ---- Init ---------------------------------------------------------------
|
|
862
1347
|
|
|
863
1348
|
async function init() {
|
|
@@ -872,7 +1357,9 @@
|
|
|
872
1357
|
fkOptions = {};
|
|
873
1358
|
}
|
|
874
1359
|
initFilters();
|
|
1360
|
+
initRowActions();
|
|
875
1361
|
initInlineEditing();
|
|
1362
|
+
initCellInspector();
|
|
876
1363
|
initBulk();
|
|
877
1364
|
initRowDelete();
|
|
878
1365
|
initPageSize();
|