admindb 2.1.0 → 2.2.1
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 +41 -12
- package/dist/app.d.ts +1 -69
- package/dist/app.js +3 -30
- package/dist/auth/config.d.ts +1 -19
- package/dist/auth/config.js +0 -26
- package/dist/auth/crypto.d.ts +1 -27
- package/dist/auth/crypto.js +1 -26
- package/dist/auth/index.d.ts +1 -5
- package/dist/auth/index.js +0 -1
- package/dist/auth/middleware.d.ts +1 -14
- package/dist/auth/middleware.js +0 -15
- package/dist/auth/routes.d.ts +1 -7
- package/dist/auth/routes.js +0 -8
- package/dist/auth/types.d.ts +1 -35
- package/dist/auth/types.js +1 -5
- package/dist/cli/args.d.ts +1 -45
- package/dist/cli/args.js +3 -11
- package/dist/cli/config.d.ts +1 -56
- package/dist/cli/config.js +0 -5
- package/dist/cli/index.d.ts +1 -4
- package/dist/cli/index.js +0 -1
- package/dist/cli/runner.d.ts +1 -1
- package/dist/cli/runner.js +6 -17
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +0 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +707 -0
- package/dist/data/datasets.d.ts +1 -17
- package/dist/data/datasets.js +0 -4
- package/dist/data/detector.d.ts +1 -30
- package/dist/data/detector.js +53 -14
- package/dist/data/engine.d.ts +1 -10
- package/dist/data/engine.js +82 -48
- package/dist/data/index.d.ts +1 -5
- package/dist/data/index.js +1 -1
- package/dist/data/strategies.d.ts +1 -30
- package/dist/data/strategies.js +4 -8
- package/dist/data/types.d.ts +1 -73
- package/dist/data/types.js +0 -3
- package/dist/db/database.d.ts +1 -119
- package/dist/db/database.js +20 -14
- package/dist/db/export.d.ts +1 -12
- package/dist/db/export.js +54 -6
- package/dist/db/filters.d.ts +1 -22
- package/dist/db/filters.js +0 -16
- package/dist/db/index.d.ts +1 -8
- package/dist/db/index.js +0 -1
- package/dist/db/introspection.d.ts +1 -6
- package/dist/db/introspection.js +0 -5
- package/dist/db/manager.d.ts +1 -81
- package/dist/db/manager.js +13 -59
- package/dist/db/migrations.d.ts +1 -5
- package/dist/db/migrations.js +2 -5
- package/dist/db/postgres.d.ts +1 -85
- package/dist/db/postgres.js +21 -31
- package/dist/db/types.d.ts +1 -206
- package/dist/db/types.js +0 -1
- package/dist/index.d.ts +1 -23
- package/dist/index.js +0 -14
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -47
- package/dist/public/js/app.js +1 -262
- package/dist/public/js/browse.js +1 -1371
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -235
- package/dist/public/js/designer.js +1 -258
- package/dist/public/js/forms.js +1 -826
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -638
- package/dist/public/js/query.js +1 -322
- package/dist/public/js/schema.js +1 -447
- package/dist/public/js/seed.js +1 -1090
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +50 -0
- package/dist/routes/api/helpers.d.ts +1 -42
- package/dist/routes/api/helpers.js +33 -3
- package/dist/routes/api/import-export.d.ts +1 -3
- package/dist/routes/api/import-export.js +0 -2
- package/dist/routes/api/index.d.ts +1 -4
- package/dist/routes/api/index.js +15 -4
- package/dist/routes/api/query.d.ts +1 -3
- package/dist/routes/api/query.js +13 -11
- package/dist/routes/api/rows.d.ts +1 -3
- package/dist/routes/api/rows.js +0 -11
- package/dist/routes/api/seed.d.ts +1 -3
- package/dist/routes/api/seed.js +79 -8
- package/dist/routes/api/tables.d.ts +1 -3
- package/dist/routes/api/tables.js +77 -6
- package/dist/routes/databases.d.ts +1 -26
- package/dist/routes/databases.js +1 -3
- package/dist/routes/index.d.ts +1 -3
- package/dist/routes/index.js +0 -1
- package/dist/routes/pages.d.ts +1 -9
- package/dist/routes/pages.js +18 -11
- package/dist/serverless.d.ts +1 -40
- package/dist/serverless.js +3 -39
- package/dist/sql/classifier.d.ts +1 -19
- package/dist/sql/classifier.js +0 -13
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.d.ts +1 -81
- package/dist/sql/generator.js +0 -29
- package/dist/sql/index.d.ts +1 -2
- package/dist/sql/index.js +0 -1
- package/dist/types/api.d.ts +1 -412
- package/dist/types/api.js +0 -7
- package/dist/utils/colors.d.ts +1 -40
- package/dist/utils/colors.js +0 -10
- package/dist/utils/common.d.ts +1 -50
- package/dist/utils/common.js +19 -23
- package/dist/utils/csv.d.ts +1 -15
- package/dist/utils/csv.js +0 -13
- package/dist/utils/datatype.d.ts +1 -49
- package/dist/utils/datatype.js +0 -41
- package/dist/utils/icons.d.ts +1 -11
- package/dist/utils/icons.js +25 -16
- package/dist/utils/index.d.ts +1 -4
- package/dist/utils/index.js +0 -1
- package/dist/utils/logger.d.ts +1 -10
- package/dist/utils/logger.js +0 -4
- package/dist/views/layouts/main.hbs +14 -5
- package/dist/views/pages/databases.hbs +0 -18
- package/dist/views/pages/designer.hbs +0 -10
- package/dist/views/pages/erd.hbs +1241 -0
- package/dist/views/pages/error.hbs +0 -2
- package/dist/views/pages/form.hbs +0 -5
- package/dist/views/pages/home.hbs +98 -10
- package/dist/views/pages/info.hbs +107 -0
- package/dist/views/pages/login.hbs +0 -13
- package/dist/views/pages/query.hbs +54 -21
- package/dist/views/pages/schema.hbs +0 -20
- package/dist/views/pages/seed.hbs +435 -202
- package/dist/views/pages/table.hbs +0 -14
- package/dist/views/partials/icon.hbs +0 -1
- package/dist/views/partials/navbar.hbs +0 -6
- package/dist/views/partials/sidebar.hbs +12 -8
- package/docs/API.md +6 -1
- package/docs/EXAMPLES.md +3 -0
- package/package.json +7 -5
- package/dist/app.js.map +0 -1
- package/dist/auth/config.js.map +0 -1
- package/dist/auth/crypto.js.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/middleware.js.map +0 -1
- package/dist/auth/routes.js.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/config.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/runner.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/data/datasets.js.map +0 -1
- package/dist/data/detector.js.map +0 -1
- package/dist/data/engine.js.map +0 -1
- package/dist/data/index.js.map +0 -1
- package/dist/data/strategies.js.map +0 -1
- package/dist/data/types.js.map +0 -1
- package/dist/db/database.js.map +0 -1
- package/dist/db/export.js.map +0 -1
- package/dist/db/filters.js.map +0 -1
- package/dist/db/index.js.map +0 -1
- package/dist/db/introspection.js.map +0 -1
- package/dist/db/manager.js.map +0 -1
- package/dist/db/migrations.js.map +0 -1
- package/dist/db/postgres.js.map +0 -1
- package/dist/db/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/public/css/input.css +0 -302
- package/dist/public/js/icons.js +0 -51
- package/dist/routes/api/helpers.js.map +0 -1
- package/dist/routes/api/import-export.js.map +0 -1
- package/dist/routes/api/index.js.map +0 -1
- package/dist/routes/api/query.js.map +0 -1
- package/dist/routes/api/rows.js.map +0 -1
- package/dist/routes/api/seed.js.map +0 -1
- package/dist/routes/api/tables.js.map +0 -1
- package/dist/routes/databases.js.map +0 -1
- package/dist/routes/index.js.map +0 -1
- package/dist/routes/pages.js.map +0 -1
- package/dist/serverless.js.map +0 -1
- package/dist/sql/classifier.js.map +0 -1
- package/dist/sql/generator.js.map +0 -1
- package/dist/sql/index.js.map +0 -1
- package/dist/types/api.js.map +0 -1
- package/dist/utils/colors.js.map +0 -1
- package/dist/utils/common.js.map +0 -1
- package/dist/utils/csv.js.map +0 -1
- package/dist/utils/datatype.js.map +0 -1
- package/dist/utils/icons.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
package/dist/public/js/browse.js
CHANGED
|
@@ -1,1371 +1 @@
|
|
|
1
|
-
/* Table browse page.
|
|
2
|
-
*
|
|
3
|
-
* Features:
|
|
4
|
-
* - Inline (spreadsheet-style) cell editing — double-click a cell, edit in
|
|
5
|
-
* place with a type-aware control (FK dropdown, boolean toggle, date picker,
|
|
6
|
-
* number input, text input). Changes are staged in the grid and marked as
|
|
7
|
-
* pending; press "Apply" to write them all at once (single transaction), or
|
|
8
|
-
* "Discard" to revert every pending cell back to the server values.
|
|
9
|
-
* - Type-aware row filters — the filter form adapts to each column's type
|
|
10
|
-
* (FK dropdowns, boolean toggles, date/number ranges, plain text).
|
|
11
|
-
* - Bulk row operations — row checkboxes + batch delete (with FK-impact
|
|
12
|
-
* warnings) and batch CSV/JSON export.
|
|
13
|
-
* - Plus the existing single-row delete, page-size, CSV import and
|
|
14
|
-
* related-rows expander.
|
|
15
|
-
*/
|
|
16
|
-
(function () {
|
|
17
|
-
'use strict';
|
|
18
|
-
|
|
19
|
-
const base = (window.APP && window.APP.basePath) || '';
|
|
20
|
-
const readonly = !!(window.APP && window.APP.readonly);
|
|
21
|
-
|
|
22
|
-
const cfgEl = document.getElementById('browse-config');
|
|
23
|
-
const cfg = cfgEl
|
|
24
|
-
? JSON.parse(cfgEl.textContent)
|
|
25
|
-
: { table: '', filters: {}, pkCols: [], hasPk: false, readonly: readonly };
|
|
26
|
-
const t = encodeURIComponent(cfg.table);
|
|
27
|
-
|
|
28
|
-
function escapeHtml(s) {
|
|
29
|
-
return (window.UI && UI.escapeHtml) ? UI.escapeHtml(s) : String(s == null ? '' : s);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let info = null; // TableInfoData from /api/tables/:table/info
|
|
33
|
-
let fkOptions = {}; // { [from]: [{ value, label }] }
|
|
34
|
-
|
|
35
|
-
function colInfo(name) {
|
|
36
|
-
return info ? info.columns.find((c) => c.name === name) : null;
|
|
37
|
-
}
|
|
38
|
-
function isPk(name) {
|
|
39
|
-
return !!info && info.primaryKey.indexOf(name) !== -1;
|
|
40
|
-
}
|
|
41
|
-
function fkFor(name) {
|
|
42
|
-
return info ? info.foreignKeys.find((f) => f.from === name) : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---- Single row delete --------------------------------------------------
|
|
46
|
-
|
|
47
|
-
function initRowDelete() {
|
|
48
|
-
document.querySelectorAll('.js-delete-row').forEach((btn) => {
|
|
49
|
-
btn.addEventListener('click', async () => {
|
|
50
|
-
const table = btn.dataset.table;
|
|
51
|
-
const pk = btn.dataset.pk;
|
|
52
|
-
if (!table || !pk) return;
|
|
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;
|
|
62
|
-
Api.del('/api/tables/' + encodeURIComponent(table) + '/row/' + encodeURIComponent(pk))
|
|
63
|
-
.then(() => {
|
|
64
|
-
UI.showToast('Row deleted.', 'success');
|
|
65
|
-
window.location.reload();
|
|
66
|
-
})
|
|
67
|
-
.catch((e) => UI.showError(e.message));
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---- Page size (keeps current filters) ----------------------------------
|
|
73
|
-
|
|
74
|
-
function initPageSize() {
|
|
75
|
-
const sizeSel = document.getElementById('page-size');
|
|
76
|
-
if (!sizeSel) return;
|
|
77
|
-
sizeSel.addEventListener('change', () => {
|
|
78
|
-
const table = sizeSel.dataset.table;
|
|
79
|
-
const size = sizeSel.value;
|
|
80
|
-
const orderBy = sizeSel.dataset.orderby || '';
|
|
81
|
-
const orderDir = sizeSel.dataset.orderdir || 'asc';
|
|
82
|
-
const filters = sizeSel.dataset.filters || '';
|
|
83
|
-
let href =
|
|
84
|
-
base + '/tables/' + encodeURIComponent(table) + '?page=1&size=' + encodeURIComponent(size) +
|
|
85
|
-
'&orderBy=' + encodeURIComponent(orderBy) + '&orderDir=' + encodeURIComponent(orderDir);
|
|
86
|
-
if (filters) href += '&' + filters;
|
|
87
|
-
window.location.href = href;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- Type-aware row filters ---------------------------------------------
|
|
92
|
-
|
|
93
|
-
function filterCondArray(existing) {
|
|
94
|
-
if (existing == null) return [];
|
|
95
|
-
if (Array.isArray(existing)) {
|
|
96
|
-
return existing.filter((x) => x && typeof x === 'object' && typeof x.op === 'string');
|
|
97
|
-
}
|
|
98
|
-
if (typeof existing === 'object') return [existing];
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
function legacyString(existing) {
|
|
102
|
-
return typeof existing === 'string' ? existing : null;
|
|
103
|
-
}
|
|
104
|
-
function condVal(conds, op) {
|
|
105
|
-
const c = conds.find((x) => x.op === op);
|
|
106
|
-
return c ? (c.value ?? null) : null;
|
|
107
|
-
}
|
|
108
|
-
function condBetween(conds) {
|
|
109
|
-
const c = conds.find((x) => x.op === 'between');
|
|
110
|
-
return c ? { min: c.value ?? null, max: c.max ?? null } : { min: null, max: null };
|
|
111
|
-
}
|
|
112
|
-
|
|
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}))?)?/);
|
|
125
|
-
if (!m) return '';
|
|
126
|
-
return m[1] + 'T' + (m[2] ? m[2] + ':' + m[3] + (m[4] ? ':' + m[4] : '') : '00:00:00');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function addOption(sel, value, label) {
|
|
130
|
-
const opt = document.createElement('option');
|
|
131
|
-
opt.value = value;
|
|
132
|
-
opt.textContent = label;
|
|
133
|
-
sel.appendChild(opt);
|
|
134
|
-
return opt;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function buildFilterField(col) {
|
|
138
|
-
const wrap = document.createElement('label');
|
|
139
|
-
wrap.className = 'block';
|
|
140
|
-
const label = document.createElement('span');
|
|
141
|
-
label.className = 'field-label !mb-1 font-mono';
|
|
142
|
-
label.textContent = col.name;
|
|
143
|
-
wrap.appendChild(label);
|
|
144
|
-
|
|
145
|
-
const fk = fkFor(col.name);
|
|
146
|
-
const type = (col.type || '').toUpperCase();
|
|
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;
|
|
153
|
-
const existing = cfg.filters[col.name];
|
|
154
|
-
const conds = filterCondArray(existing);
|
|
155
|
-
const legacy = legacyString(existing);
|
|
156
|
-
|
|
157
|
-
// FK dropdown — exact match on the referenced value.
|
|
158
|
-
if (fk) {
|
|
159
|
-
const sel = document.createElement('select');
|
|
160
|
-
sel.className = 'filter-input field-select !py-1.5 !text-[13px]';
|
|
161
|
-
sel.dataset.column = col.name;
|
|
162
|
-
sel.dataset.kind = 'fk';
|
|
163
|
-
addOption(sel, '', '(any)');
|
|
164
|
-
addOption(sel, '__NULL__', '(not set / NULL)');
|
|
165
|
-
(fkOptions[col.name] || []).forEach((o) => addOption(sel, String(o.value), o.label === '' ? String(o.value) : o.label));
|
|
166
|
-
let selVal = condVal(conds, 'eq');
|
|
167
|
-
if (selVal == null && legacy && legacy.startsWith('=')) selVal = legacy.slice(1);
|
|
168
|
-
if (selVal === '__NULL__') sel.value = '__NULL__';
|
|
169
|
-
else if (selVal != null && [...sel.options].some((o) => o.value === String(selVal))) sel.value = String(selVal);
|
|
170
|
-
wrap.appendChild(sel);
|
|
171
|
-
return wrap;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Boolean toggle.
|
|
175
|
-
if (isBool) {
|
|
176
|
-
const sel = document.createElement('select');
|
|
177
|
-
sel.className = 'filter-input field-select !py-1.5 !text-[13px]';
|
|
178
|
-
sel.dataset.column = col.name;
|
|
179
|
-
sel.dataset.kind = 'bool';
|
|
180
|
-
addOption(sel, '', '(any)');
|
|
181
|
-
addOption(sel, '1', 'true');
|
|
182
|
-
addOption(sel, '0', 'false');
|
|
183
|
-
let selVal = condVal(conds, 'eq');
|
|
184
|
-
if (selVal == null && legacy && legacy.startsWith('=')) selVal = legacy.slice(1);
|
|
185
|
-
if (selVal === '1' || selVal === '0' || selVal === 'true' || selVal === 'false') {
|
|
186
|
-
sel.value = selVal === 'true' || selVal === '1' ? '1' : '0';
|
|
187
|
-
}
|
|
188
|
-
wrap.appendChild(sel);
|
|
189
|
-
return wrap;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Date / datetime range, or numeric range.
|
|
193
|
-
if (isDate || isNum) {
|
|
194
|
-
const row = document.createElement('div');
|
|
195
|
-
row.className = 'flex items-center gap-1.5';
|
|
196
|
-
const lo = document.createElement('input');
|
|
197
|
-
const hi = document.createElement('input');
|
|
198
|
-
if (isDate) {
|
|
199
|
-
lo.type = hi.type = isDateOnly ? 'date' : (isTimeOnly ? 'time' : 'datetime-local');
|
|
200
|
-
if (isTimeOnly || isTimestamp) lo.step = hi.step = '1';
|
|
201
|
-
} else {
|
|
202
|
-
lo.type = hi.type = 'number';
|
|
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';
|
|
205
|
-
}
|
|
206
|
-
lo.className = 'filter-input field-input !py-1.5 !text-[13px]';
|
|
207
|
-
hi.className = 'filter-input field-input !py-1.5 !text-[13px]';
|
|
208
|
-
lo.dataset.column = col.name;
|
|
209
|
-
lo.dataset.bound = 'min';
|
|
210
|
-
lo.placeholder = isDate ? 'from' : 'min';
|
|
211
|
-
hi.dataset.column = col.name;
|
|
212
|
-
hi.dataset.bound = 'max';
|
|
213
|
-
hi.placeholder = isDate ? 'to' : 'max';
|
|
214
|
-
const bt = condBetween(conds);
|
|
215
|
-
const minV = condVal(conds, 'gte') ?? condVal(conds, 'gt') ?? bt.min;
|
|
216
|
-
const maxV = condVal(conds, 'lte') ?? condVal(conds, 'lt') ?? bt.max;
|
|
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);
|
|
219
|
-
row.appendChild(lo);
|
|
220
|
-
row.appendChild(hi);
|
|
221
|
-
wrap.appendChild(row);
|
|
222
|
-
return wrap;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Text / BLOB / anything else — legacy operator syntax.
|
|
227
|
-
const input = document.createElement('input');
|
|
228
|
-
input.type = 'text';
|
|
229
|
-
input.className = 'filter-input field-input !py-1.5 !text-[13px]';
|
|
230
|
-
input.dataset.column = col.name;
|
|
231
|
-
input.dataset.kind = 'text';
|
|
232
|
-
input.placeholder = type === 'BLOB' ? '0x… or text' : 'filter…';
|
|
233
|
-
input.autocomplete = 'off';
|
|
234
|
-
input.spellcheck = false;
|
|
235
|
-
let textVal = legacy;
|
|
236
|
-
if (textVal == null) {
|
|
237
|
-
const prefix = condVal(conds, 'prefix');
|
|
238
|
-
const eq = condVal(conds, 'eq');
|
|
239
|
-
const like = condVal(conds, 'like');
|
|
240
|
-
if (prefix != null) textVal = String(prefix) + '*';
|
|
241
|
-
else if (eq != null) textVal = '=' + String(eq);
|
|
242
|
-
else if (like != null) textVal = String(like);
|
|
243
|
-
}
|
|
244
|
-
if (textVal != null) input.value = textVal;
|
|
245
|
-
wrap.appendChild(input);
|
|
246
|
-
return wrap;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function collectFilters() {
|
|
250
|
-
const filters = {};
|
|
251
|
-
const byCol = {};
|
|
252
|
-
document.querySelectorAll('#filter-fields .filter-input').forEach((el) => {
|
|
253
|
-
const col = el.dataset.column;
|
|
254
|
-
if (!col) return;
|
|
255
|
-
(byCol[col] = byCol[col] || []).push(el);
|
|
256
|
-
});
|
|
257
|
-
for (const col of Object.keys(byCol)) {
|
|
258
|
-
const els = byCol[col];
|
|
259
|
-
const first = els[0];
|
|
260
|
-
const kind = first.dataset.kind || '';
|
|
261
|
-
if (kind === 'fk') {
|
|
262
|
-
const v = first.value;
|
|
263
|
-
if (v === '') continue;
|
|
264
|
-
filters[col] = v === '__NULL__' ? { op: 'null' } : { op: 'eq', value: v };
|
|
265
|
-
} else if (kind === 'bool') {
|
|
266
|
-
const v = first.value;
|
|
267
|
-
if (v === '') continue;
|
|
268
|
-
filters[col] = { op: 'eq', value: v };
|
|
269
|
-
} else if (els.length === 2 && first.dataset.bound) {
|
|
270
|
-
const minEl = els.find((e) => e.dataset.bound === 'min');
|
|
271
|
-
const maxEl = els.find((e) => e.dataset.bound === 'max');
|
|
272
|
-
const minV = minEl && minEl.value !== '' ? minEl.value : null;
|
|
273
|
-
const maxV = maxEl && maxEl.value !== '' ? maxEl.value : null;
|
|
274
|
-
if (minV != null && maxV != null) filters[col] = { op: 'between', value: minV, max: maxV };
|
|
275
|
-
else if (minV != null) filters[col] = { op: 'gte', value: minV };
|
|
276
|
-
else if (maxV != null) filters[col] = { op: 'lte', value: maxV };
|
|
277
|
-
} else {
|
|
278
|
-
const v = (first.value || '').trim();
|
|
279
|
-
if (v) filters[col] = v; // legacy string syntax
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return filters;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function navigate(filters) {
|
|
286
|
-
let table = '';
|
|
287
|
-
const m = window.location.pathname.match(/\/tables\/([^/]+)(?:\/|$)/);
|
|
288
|
-
if (m) table = decodeURIComponent(m[1]);
|
|
289
|
-
if (!table) return;
|
|
290
|
-
const sizeSel = document.getElementById('page-size');
|
|
291
|
-
const qs = new URLSearchParams({ page: '1', size: sizeSel ? sizeSel.value : '50' });
|
|
292
|
-
const keys = Object.keys(filters);
|
|
293
|
-
if (keys.length) qs.set('f', JSON.stringify(filters));
|
|
294
|
-
window.location.href = base + '/tables/' + encodeURIComponent(table) + '?' + qs.toString();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function initFilters() {
|
|
298
|
-
const filterForm = document.getElementById('filter-form');
|
|
299
|
-
const clearBtn = document.getElementById('clear-filter');
|
|
300
|
-
const fields = document.getElementById('filter-fields');
|
|
301
|
-
if (filterForm && fields && info) {
|
|
302
|
-
info.columns.forEach((col) => fields.appendChild(buildFilterField(col)));
|
|
303
|
-
filterForm.addEventListener('submit', (e) => {
|
|
304
|
-
e.preventDefault();
|
|
305
|
-
navigate(collectFilters());
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
if (clearBtn) clearBtn.addEventListener('click', () => navigate({}));
|
|
309
|
-
}
|
|
310
|
-
|
|
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
|
-
}
|
|
345
|
-
|
|
346
|
-
function renderCellValue(td, value) {
|
|
347
|
-
const isNull = value === null || value === undefined;
|
|
348
|
-
const display = isNull ? '' : String(value);
|
|
349
|
-
td.dataset.null = String(isNull);
|
|
350
|
-
td.dataset.value = display;
|
|
351
|
-
td.classList.remove('js-editing', 'opacity-60');
|
|
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
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function currentCellValue(td) {
|
|
415
|
-
return td.dataset.null === 'true' ? null : (td.dataset.value ?? '');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function createEditor(td) {
|
|
419
|
-
const col = td.dataset.col;
|
|
420
|
-
const c = colInfo(col);
|
|
421
|
-
if (!c) return null;
|
|
422
|
-
const fk = fkFor(col);
|
|
423
|
-
const type = (c.type || '').toUpperCase();
|
|
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(');
|
|
437
|
-
const value = currentCellValue(td);
|
|
438
|
-
const nullish = value === null || value === '';
|
|
439
|
-
|
|
440
|
-
let el;
|
|
441
|
-
if (isBool) {
|
|
442
|
-
el = document.createElement('input');
|
|
443
|
-
el.type = 'checkbox';
|
|
444
|
-
el.className = 'checkbox checkbox-primary checkbox-sm js-inline-ctrl';
|
|
445
|
-
el.checked = value === 1 || value === true || value === '1' || value === 'true';
|
|
446
|
-
} else if (fk) {
|
|
447
|
-
el = document.createElement('select');
|
|
448
|
-
el.className = 'field-select !w-64 !py-1.5 !text-[13px] js-inline-ctrl';
|
|
449
|
-
addOption(el, '', '(not set / NULL)');
|
|
450
|
-
(fkOptions[col] || []).forEach((o) => {
|
|
451
|
-
const opt = addOption(el, String(o.value), o.label === '' ? String(o.value) : o.label);
|
|
452
|
-
if (!nullish && String(value) === String(o.value)) opt.selected = true;
|
|
453
|
-
});
|
|
454
|
-
} else if (isDate) {
|
|
455
|
-
el = document.createElement('input');
|
|
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);
|
|
465
|
-
} else if (isNum) {
|
|
466
|
-
el = document.createElement('input');
|
|
467
|
-
el.type = 'number';
|
|
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";
|
|
501
|
-
if (!nullish) el.value = String(value);
|
|
502
|
-
} else {
|
|
503
|
-
el = document.createElement('input');
|
|
504
|
-
el.type = 'text';
|
|
505
|
-
el.className = 'field-input !w-64 !py-1.5 !text-[13px] js-inline-ctrl';
|
|
506
|
-
el.placeholder = isBlob ? (type === 'BYTEA' ? '\\x… or 0x… hex' : '0x… hex or text') : (isJson ? '{"key":"value"}' : '');
|
|
507
|
-
if (!nullish) el.value = String(value);
|
|
508
|
-
}
|
|
509
|
-
return el;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
function restoreCell(td) {
|
|
515
|
-
renderCellValue(td, td._origNull ? null : td._origValue);
|
|
516
|
-
td.classList.remove('js-editing');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ---- Staged cell edits --------------------------------------------------
|
|
520
|
-
|
|
521
|
-
const pendingChanges = new Map(); // key: `${pk}::${col}` → staged change
|
|
522
|
-
|
|
523
|
-
function stageCell(tr, td, el) {
|
|
524
|
-
const pk = tr.dataset.pk;
|
|
525
|
-
const col = td.dataset.col;
|
|
526
|
-
if (!pk || !col) return;
|
|
527
|
-
const c = colInfo(col);
|
|
528
|
-
if (!c) return;
|
|
529
|
-
|
|
530
|
-
let rawValue;
|
|
531
|
-
let setNull = false;
|
|
532
|
-
if (el.type === 'checkbox') {
|
|
533
|
-
rawValue = el.checked ? '1' : '0';
|
|
534
|
-
} else {
|
|
535
|
-
const v = el.value === undefined ? '' : String(el.value);
|
|
536
|
-
if (v === '' || v === '__NULL__') { rawValue = ''; setNull = true; }
|
|
537
|
-
else rawValue = v;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (td.dataset.origValue === undefined) {
|
|
541
|
-
td.dataset.origValue = td.dataset.value;
|
|
542
|
-
td.dataset.origNull = td.dataset.null === 'true' ? 'true' : 'false';
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
renderCellValue(td, setNull ? null : rawValue);
|
|
546
|
-
|
|
547
|
-
pendingChanges.set(pk + '::' + col, {
|
|
548
|
-
td,
|
|
549
|
-
pk,
|
|
550
|
-
col,
|
|
551
|
-
rawValue,
|
|
552
|
-
setNull,
|
|
553
|
-
isPk: isPk(col),
|
|
554
|
-
isFk: !!fkFor(col),
|
|
555
|
-
origValue: td.dataset.origValue,
|
|
556
|
-
origNull: td.dataset.origNull === 'true',
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
td.classList.add('cell-dirty');
|
|
560
|
-
updateChangesBar();
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function updateChangesBar() {
|
|
564
|
-
if (!changesBar || !changesCount) return;
|
|
565
|
-
const n = pendingChanges.size;
|
|
566
|
-
changesCount.textContent = String(n);
|
|
567
|
-
changesBar.classList.toggle('hidden', n === 0);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function applyPending() {
|
|
571
|
-
if (!pendingChanges.size || readonly) return;
|
|
572
|
-
const byRow = new Map();
|
|
573
|
-
for (const ch of pendingChanges.values()) {
|
|
574
|
-
let entry = byRow.get(ch.pk);
|
|
575
|
-
if (!entry) {
|
|
576
|
-
entry = { id: ch.pk, values: {}, nulls: [] };
|
|
577
|
-
byRow.set(ch.pk, entry);
|
|
578
|
-
}
|
|
579
|
-
if (ch.setNull) entry.nulls.push(ch.col);
|
|
580
|
-
else entry.values[ch.col] = ch.rawValue;
|
|
581
|
-
}
|
|
582
|
-
const updates = [...byRow.values()];
|
|
583
|
-
if (applyBtn) applyBtn.disabled = true;
|
|
584
|
-
Api.post('/api/tables/' + t + '/rows/bulk-update', { updates })
|
|
585
|
-
.then((data) => {
|
|
586
|
-
UI.showToast(data.message || 'Changes applied.', 'success');
|
|
587
|
-
window.location.reload();
|
|
588
|
-
})
|
|
589
|
-
.catch((e) => {
|
|
590
|
-
UI.showError(e.message);
|
|
591
|
-
if (applyBtn) applyBtn.disabled = false;
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
|
|
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;
|
|
605
|
-
for (const ch of pendingChanges.values()) {
|
|
606
|
-
renderCellValue(ch.td, ch.origNull ? null : ch.origValue);
|
|
607
|
-
ch.td.classList.remove('cell-dirty');
|
|
608
|
-
delete ch.td.dataset.origValue;
|
|
609
|
-
delete ch.td.dataset.origNull;
|
|
610
|
-
}
|
|
611
|
-
pendingChanges.clear();
|
|
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
|
-
});
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function buildChangesBar() {
|
|
708
|
-
const bar = document.createElement('div');
|
|
709
|
-
bar.id = 'changes-bar';
|
|
710
|
-
bar.className =
|
|
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';
|
|
712
|
-
bar.innerHTML =
|
|
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>' +
|
|
718
|
-
'<div class="flex items-center gap-1.5">' +
|
|
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>' +
|
|
724
|
-
'Apply</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>' +
|
|
726
|
-
'</div>';
|
|
727
|
-
document.body.appendChild(bar);
|
|
728
|
-
return bar;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
let changesBar = null;
|
|
732
|
-
let changesCount = null;
|
|
733
|
-
let applyBtn = null;
|
|
734
|
-
|
|
735
|
-
function beginEdit(td) {
|
|
736
|
-
if (readonly) return;
|
|
737
|
-
if (td.classList.contains('js-editing')) return;
|
|
738
|
-
const tr = td.closest('tr');
|
|
739
|
-
if (!tr || !tr.dataset.pk || !tr.dataset.table) return;
|
|
740
|
-
const c = colInfo(td.dataset.col);
|
|
741
|
-
if (!c) return;
|
|
742
|
-
|
|
743
|
-
setFocusedCell(td);
|
|
744
|
-
|
|
745
|
-
const open = document.querySelector('.js-editing');
|
|
746
|
-
if (open && open._finish) open._finish(true);
|
|
747
|
-
|
|
748
|
-
td._origNull = td.dataset.null === 'true';
|
|
749
|
-
td._origValue = currentCellValue(td);
|
|
750
|
-
|
|
751
|
-
const el = createEditor(td);
|
|
752
|
-
if (!el) return;
|
|
753
|
-
td.classList.add('js-editing');
|
|
754
|
-
td.innerHTML = '';
|
|
755
|
-
td.appendChild(el);
|
|
756
|
-
|
|
757
|
-
let committed = false;
|
|
758
|
-
const finish = (commit, navigateNext) => {
|
|
759
|
-
if (committed) return;
|
|
760
|
-
committed = true;
|
|
761
|
-
if (commit) stageCell(tr, td, el);
|
|
762
|
-
else restoreCell(td);
|
|
763
|
-
if (navigateNext === 'down') navigateCell('down');
|
|
764
|
-
else if (navigateNext === 'right') navigateCell('right');
|
|
765
|
-
else if (navigateNext === 'left') navigateCell('left');
|
|
766
|
-
};
|
|
767
|
-
td._finish = finish;
|
|
768
|
-
|
|
769
|
-
el.addEventListener('keydown', (e) => {
|
|
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
|
-
}
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
if (el.tagName === 'SELECT') {
|
|
783
|
-
el.addEventListener('change', () => finish(true));
|
|
784
|
-
} else if (el.type === 'checkbox') {
|
|
785
|
-
el.addEventListener('change', () => finish(true));
|
|
786
|
-
} else {
|
|
787
|
-
el.addEventListener('blur', () => finish(true));
|
|
788
|
-
}
|
|
789
|
-
el.focus();
|
|
790
|
-
if (typeof el.select === 'function') el.select();
|
|
791
|
-
}
|
|
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
|
-
|
|
844
|
-
function initInlineEditing() {
|
|
845
|
-
changesBar = buildChangesBar();
|
|
846
|
-
changesCount = document.getElementById('changes-count');
|
|
847
|
-
applyBtn = document.getElementById('changes-apply');
|
|
848
|
-
const discardBtn = document.getElementById('changes-discard');
|
|
849
|
-
const reviewBtn = document.getElementById('changes-review');
|
|
850
|
-
|
|
851
|
-
if (applyBtn) applyBtn.addEventListener('click', applyPending);
|
|
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;
|
|
882
|
-
|
|
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
|
-
}
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// ---- Bulk row operations ------------------------------------------------
|
|
931
|
-
|
|
932
|
-
function selectedRows() {
|
|
933
|
-
return Array.prototype.slice
|
|
934
|
-
.call(document.querySelectorAll('.row-checkbox:checked'))
|
|
935
|
-
.map((cb) => cb.value)
|
|
936
|
-
.filter(Boolean);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function initBulk() {
|
|
940
|
-
const bulkBar = document.getElementById('bulk-bar');
|
|
941
|
-
if (!bulkBar) return;
|
|
942
|
-
const selectAll = document.getElementById('select-all-rows');
|
|
943
|
-
const countEl = document.getElementById('bulk-count');
|
|
944
|
-
const deleteBtn = document.getElementById('bulk-delete');
|
|
945
|
-
const exportCsv = document.getElementById('bulk-export-csv');
|
|
946
|
-
const exportJson = document.getElementById('bulk-export-json');
|
|
947
|
-
const clearBtn = document.getElementById('bulk-clear');
|
|
948
|
-
const checkboxes = Array.prototype.slice.call(document.querySelectorAll('.row-checkbox'));
|
|
949
|
-
|
|
950
|
-
function update() {
|
|
951
|
-
const ids = selectedRows();
|
|
952
|
-
const n = ids.length;
|
|
953
|
-
if (countEl) countEl.textContent = String(n);
|
|
954
|
-
bulkBar.classList.toggle('hidden', n === 0);
|
|
955
|
-
if (selectAll) selectAll.checked = n > 0 && n === checkboxes.length;
|
|
956
|
-
if (deleteBtn) deleteBtn.disabled = readonly || n === 0;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
checkboxes.forEach((cb) => cb.addEventListener('change', update));
|
|
960
|
-
if (selectAll) {
|
|
961
|
-
selectAll.addEventListener('change', () => {
|
|
962
|
-
checkboxes.forEach((cb) => { cb.checked = selectAll.checked; });
|
|
963
|
-
update();
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
if (clearBtn) clearBtn.addEventListener('click', () => {
|
|
967
|
-
checkboxes.forEach((cb) => { cb.checked = false; });
|
|
968
|
-
update();
|
|
969
|
-
});
|
|
970
|
-
if (exportCsv) exportCsv.addEventListener('click', () => bulkExport('csv'));
|
|
971
|
-
if (exportJson) exportJson.addEventListener('click', () => bulkExport('json'));
|
|
972
|
-
if (deleteBtn) deleteBtn.addEventListener('click', bulkDelete);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
async function bulkExport(format) {
|
|
976
|
-
const ids = selectedRows();
|
|
977
|
-
if (!ids.length) return;
|
|
978
|
-
try {
|
|
979
|
-
const data = await Api.post('/api/tables/' + t + '/rows/bulk-export', { ids, format });
|
|
980
|
-
const blob = new Blob([data.content], {
|
|
981
|
-
type: format === 'json' ? 'application/json;charset=utf-8' : 'text/csv;charset=utf-8',
|
|
982
|
-
});
|
|
983
|
-
const a = document.createElement('a');
|
|
984
|
-
a.href = URL.createObjectURL(blob);
|
|
985
|
-
a.download = data.filename;
|
|
986
|
-
document.body.appendChild(a);
|
|
987
|
-
a.click();
|
|
988
|
-
a.remove();
|
|
989
|
-
URL.revokeObjectURL(a.href);
|
|
990
|
-
UI.showToast('Exported ' + data.rowCount + ' row(s).', 'success');
|
|
991
|
-
} catch (e) {
|
|
992
|
-
UI.showError(e.message);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
async function bulkDelete() {
|
|
997
|
-
const ids = selectedRows();
|
|
998
|
-
if (!ids.length || readonly) return;
|
|
999
|
-
let impact;
|
|
1000
|
-
try {
|
|
1001
|
-
impact = await Api.post('/api/tables/' + t + '/rows/bulk-impact', { ids });
|
|
1002
|
-
} catch (e) {
|
|
1003
|
-
UI.showError(e.message);
|
|
1004
|
-
return;
|
|
1005
|
-
}
|
|
1006
|
-
const refs = (impact && impact.references) || [];
|
|
1007
|
-
const details = refs.length
|
|
1008
|
-
? '<p class="mb-2 font-medium text-warning">⚠ Other tables reference these records:</p>' +
|
|
1009
|
-
'<ul class="mb-2 list-disc space-y-1 pl-5 text-xs">' +
|
|
1010
|
-
refs.map((r) =>
|
|
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>'
|
|
1012
|
-
).join('') +
|
|
1013
|
-
'</ul>' +
|
|
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)',
|
|
1024
|
-
danger: true,
|
|
1025
|
-
});
|
|
1026
|
-
if (!confirmed) return;
|
|
1027
|
-
|
|
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
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// ---- CSV import ---------------------------------------------------------
|
|
1038
|
-
|
|
1039
|
-
function initImport() {
|
|
1040
|
-
const importBtn = document.getElementById('import-csv');
|
|
1041
|
-
const importPanel = document.getElementById('import-panel');
|
|
1042
|
-
const importClose = document.getElementById('import-csv-close');
|
|
1043
|
-
const importText = document.getElementById('import-csv-text');
|
|
1044
|
-
const importFile = document.getElementById('import-csv-file');
|
|
1045
|
-
const importSubmit = document.getElementById('import-csv-submit');
|
|
1046
|
-
|
|
1047
|
-
if (importBtn && importPanel) {
|
|
1048
|
-
importBtn.addEventListener('click', () => {
|
|
1049
|
-
importPanel.classList.toggle('hidden');
|
|
1050
|
-
if (!importPanel.classList.contains('hidden')) importText.focus();
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
if (importClose) {
|
|
1054
|
-
importClose.addEventListener('click', () => importPanel.classList.add('hidden'));
|
|
1055
|
-
}
|
|
1056
|
-
if (importFile) {
|
|
1057
|
-
importFile.addEventListener('change', () => {
|
|
1058
|
-
const file = importFile.files && importFile.files[0];
|
|
1059
|
-
if (!file) return;
|
|
1060
|
-
const reader = new FileReader();
|
|
1061
|
-
reader.onload = () => { importText.value = String(reader.result || ''); };
|
|
1062
|
-
reader.readAsText(file);
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
if (importSubmit && importText) {
|
|
1066
|
-
importSubmit.addEventListener('click', async () => {
|
|
1067
|
-
const csv = importText.value.trim();
|
|
1068
|
-
if (!csv) return UI.showError('Paste or choose a CSV file first.');
|
|
1069
|
-
const m = window.location.pathname.match(/\/tables\/([^/]+)(?:\/|$)/);
|
|
1070
|
-
const table = m ? decodeURIComponent(m[1]) : '';
|
|
1071
|
-
if (!table) return UI.showError('Could not determine the table.');
|
|
1072
|
-
importSubmit.disabled = true;
|
|
1073
|
-
try {
|
|
1074
|
-
const data = await Api.post('/api/tables/' + encodeURIComponent(table) + '/rows/import', { csv });
|
|
1075
|
-
UI.showToast(data.message || 'Import complete.', 'success');
|
|
1076
|
-
window.location.reload();
|
|
1077
|
-
} catch (e) {
|
|
1078
|
-
UI.showError(e.message);
|
|
1079
|
-
} finally {
|
|
1080
|
-
importSubmit.disabled = false;
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// ---- Related rows (Neon-style per-row expander) -------------------------
|
|
1087
|
-
|
|
1088
|
-
function rotateChevron(btn, open) {
|
|
1089
|
-
const chev = btn.querySelector('.js-ref-chevron');
|
|
1090
|
-
if (chev) chev.classList.toggle('rotate-180', open);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
let refOpenBtn = null;
|
|
1094
|
-
|
|
1095
|
-
function closeRefPop() {
|
|
1096
|
-
const pop = document.querySelector('.js-ref-pop');
|
|
1097
|
-
if (pop) {
|
|
1098
|
-
if (pop._cleanup) pop._cleanup();
|
|
1099
|
-
pop.remove();
|
|
1100
|
-
}
|
|
1101
|
-
if (refOpenBtn) {
|
|
1102
|
-
rotateChevron(refOpenBtn, false);
|
|
1103
|
-
refOpenBtn.setAttribute('aria-expanded', 'false');
|
|
1104
|
-
refOpenBtn.classList.remove('opacity-50');
|
|
1105
|
-
refOpenBtn = null;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function renderMiniTable(ref, basePath) {
|
|
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_') : []);
|
|
1113
|
-
let html =
|
|
1114
|
-
'<div class="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2">' +
|
|
1115
|
-
'<span class="min-w-0 truncate">' +
|
|
1116
|
-
'<a class="link link-primary font-mono text-xs font-semibold" href="' + href + '">' + escapeHtml(ref.table) + '</a>' +
|
|
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>' +
|
|
1118
|
-
'</span>' +
|
|
1119
|
-
'<span class="chip ' + (count ? 'chip-pk' : 'chip-type') + '">' + escapeHtml(count + ' row' + (count === 1 ? '' : 's')) + '</span>' +
|
|
1120
|
-
'</div>';
|
|
1121
|
-
|
|
1122
|
-
if (!ref.rows || !ref.rows.length || !columns.length) {
|
|
1123
|
-
return html + '<div class="px-3 py-3 text-xs text-base-content/45">No matching rows.</div>';
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
const thead = '<tr>' + columns.map(function (c) {
|
|
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>';
|
|
1128
|
-
}).join('') + '</tr>';
|
|
1129
|
-
const tbody = ref.rows.map(function (r) {
|
|
1130
|
-
return '<tr class="border-t border-base-200/60 hover:bg-primary/[0.04]">' + columns.map(function (c) {
|
|
1131
|
-
const v = r[c];
|
|
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>';
|
|
1133
|
-
const s = String(v);
|
|
1134
|
-
const short = s.length > 120 ? s.slice(0, 120) + '…' : s;
|
|
1135
|
-
return '<td class="max-w-[180px] truncate px-2.5 py-1.5 align-middle font-mono text-[11px] text-base-content/80" title="' + escapeHtml(s) + '">' + escapeHtml(short) + '</td>';
|
|
1136
|
-
}).join('') + '</tr>';
|
|
1137
|
-
}).join('');
|
|
1138
|
-
|
|
1139
|
-
return html +
|
|
1140
|
-
'<div class="max-h-64 overflow-auto">' +
|
|
1141
|
-
'<table class="w-full"><thead><tr class="bg-base-200/40">' + thead + '</tr></thead>' +
|
|
1142
|
-
'<tbody>' + tbody + '</tbody></table>' +
|
|
1143
|
-
'</div>' +
|
|
1144
|
-
'<div class="flex justify-end border-t border-base-200 px-3 py-1.5">' +
|
|
1145
|
-
'<a class="btn btn-ghost btn-xs text-base-content/60 hover:text-primary" href="' + href + '">Open table</a>' +
|
|
1146
|
-
'</div>';
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function showRefPop(btn, refs) {
|
|
1150
|
-
closeRefPop();
|
|
1151
|
-
const pop = document.createElement('div');
|
|
1152
|
-
pop.className = 'js-ref-pop app-card js-ref-anim z-50 min-w-[280px] max-w-[460px] overflow-hidden shadow-lift';
|
|
1153
|
-
pop.innerHTML = refs.length
|
|
1154
|
-
? refs.map((ref) => renderMiniTable(ref, base)).join('<div class="h-px bg-base-200"></div>')
|
|
1155
|
-
: '<div class="px-4 py-3 text-sm text-base-content/45">No related rows for this record.</div>';
|
|
1156
|
-
|
|
1157
|
-
pop.style.visibility = 'hidden';
|
|
1158
|
-
document.body.appendChild(pop);
|
|
1159
|
-
const pad = 8;
|
|
1160
|
-
const popW = Math.min(460, Math.max(280, pop.offsetWidth));
|
|
1161
|
-
const popH = pop.offsetHeight;
|
|
1162
|
-
|
|
1163
|
-
const rect = btn.getBoundingClientRect();
|
|
1164
|
-
let left = rect.left;
|
|
1165
|
-
if (left + popW > window.innerWidth - pad) left = Math.max(pad, window.innerWidth - popW - pad);
|
|
1166
|
-
let top = rect.bottom + 6;
|
|
1167
|
-
if (top + popH > window.innerHeight - pad) top = Math.max(pad, rect.top - popH - 6);
|
|
1168
|
-
|
|
1169
|
-
pop.style.position = 'fixed';
|
|
1170
|
-
pop.style.left = left + 'px';
|
|
1171
|
-
pop.style.top = top + 'px';
|
|
1172
|
-
pop.style.visibility = 'visible';
|
|
1173
|
-
refOpenBtn = btn;
|
|
1174
|
-
|
|
1175
|
-
// Close when the page behind the popover scrolls or the window resizes,
|
|
1176
|
-
// but NOT when the nested table's own scroll container is scrolled — the
|
|
1177
|
-
// capture-phase listener fires for every scroll target, including the
|
|
1178
|
-
// mini-table inside the popover, which would otherwise close it instantly.
|
|
1179
|
-
const onViewChange = (e) => {
|
|
1180
|
-
if (e && e.target && pop.contains(e.target)) return;
|
|
1181
|
-
closeRefPop();
|
|
1182
|
-
};
|
|
1183
|
-
window.addEventListener('scroll', onViewChange, true);
|
|
1184
|
-
window.addEventListener('resize', onViewChange);
|
|
1185
|
-
const onClickOutside = (e) => {
|
|
1186
|
-
if (!pop.contains(e.target) && e.target !== btn && !btn.contains(e.target)) closeRefPop();
|
|
1187
|
-
};
|
|
1188
|
-
document.addEventListener('click', onClickOutside, true);
|
|
1189
|
-
const onKey = (e) => { if (e.key === 'Escape') closeRefPop(); };
|
|
1190
|
-
document.addEventListener('keydown', onKey);
|
|
1191
|
-
|
|
1192
|
-
pop._cleanup = () => {
|
|
1193
|
-
window.removeEventListener('scroll', onViewChange, true);
|
|
1194
|
-
window.removeEventListener('resize', onViewChange);
|
|
1195
|
-
document.removeEventListener('click', onClickOutside, true);
|
|
1196
|
-
document.removeEventListener('keydown', onKey);
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function initRefToggles() {
|
|
1201
|
-
document.querySelectorAll('.js-ref-toggle').forEach((btn) => {
|
|
1202
|
-
btn.addEventListener('click', (e) => {
|
|
1203
|
-
e.stopPropagation();
|
|
1204
|
-
const open = btn.getAttribute('aria-expanded') === 'true';
|
|
1205
|
-
if (open) {
|
|
1206
|
-
closeRefPop();
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
|
-
const table = btn.dataset.table;
|
|
1210
|
-
const pk = btn.dataset.pk;
|
|
1211
|
-
if (!table || !pk) return;
|
|
1212
|
-
btn.classList.add('opacity-50');
|
|
1213
|
-
Api.get('/api/tables/' + encodeURIComponent(table) + '/rows/' + encodeURIComponent(pk) + '/references')
|
|
1214
|
-
.then((data) => {
|
|
1215
|
-
btn.classList.remove('opacity-50');
|
|
1216
|
-
const refTable = btn.dataset.refTable;
|
|
1217
|
-
const refs = ((data && data.references) || []).filter((r) => !refTable || r.table === refTable);
|
|
1218
|
-
btn.setAttribute('aria-expanded', 'true');
|
|
1219
|
-
rotateChevron(btn, true);
|
|
1220
|
-
showRefPop(btn, refs);
|
|
1221
|
-
})
|
|
1222
|
-
.catch((e) => {
|
|
1223
|
-
btn.classList.remove('opacity-50');
|
|
1224
|
-
UI.showError(e.message);
|
|
1225
|
-
});
|
|
1226
|
-
});
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
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
|
-
|
|
1346
|
-
// ---- Init ---------------------------------------------------------------
|
|
1347
|
-
|
|
1348
|
-
async function init() {
|
|
1349
|
-
try {
|
|
1350
|
-
info = await Api.get('/api/tables/' + t + '/info');
|
|
1351
|
-
} catch (e) {
|
|
1352
|
-
info = null;
|
|
1353
|
-
}
|
|
1354
|
-
try {
|
|
1355
|
-
fkOptions = await Api.get('/api/tables/' + t + '/fk-options');
|
|
1356
|
-
} catch (e) {
|
|
1357
|
-
fkOptions = {};
|
|
1358
|
-
}
|
|
1359
|
-
initFilters();
|
|
1360
|
-
initRowActions();
|
|
1361
|
-
initInlineEditing();
|
|
1362
|
-
initCellInspector();
|
|
1363
|
-
initBulk();
|
|
1364
|
-
initRowDelete();
|
|
1365
|
-
initPageSize();
|
|
1366
|
-
initImport();
|
|
1367
|
-
initRefToggles();
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
init();
|
|
1371
|
-
})();
|
|
1
|
+
!function(){"use strict";const e=window.APP&&window.APP.basePath||"",t=!(!window.APP||!window.APP.readonly),n=document.getElementById("browse-config"),s=n?JSON.parse(n.textContent):{table:"",filters:{},pkCols:[],hasPk:!1,readonly:t},o=encodeURIComponent(s.table),l=window.Utils?window.Utils.escapeHtml:function(e){return String(null==e?"":e)};let a=null,i={};function r(e){return a?a.columns.find(t=>t.name===e):null}function c(e){return a?a.foreignKeys.find(t=>t.from===e):null}function d(e,t){const n=e.find(e=>e.op===t);return n?n.value??null:null}function u(e,t,n){if(null==e||""===e)return"";const s=String(e);if(t){const e=s.match(/^(\d{4}-\d{2}-\d{2})/);return e?e[1]:""}if(n){const e=s.match(/(\d{2}:\d{2}(?::\d{2})?)/);return e?e[1]:""}const o=s.match(/^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?/);return o?o[1]+"T"+(o[2]?o[2]+":"+o[3]+(o[4]?":"+o[4]:""):"00:00:00"):""}function p(e,t,n){const s=document.createElement("option");return s.value=t,s.textContent=n,e.appendChild(s),s}function m(e){const t=document.createElement("label");t.className="block";const n=document.createElement("span");n.className="field-label !mb-1 font-mono",n.textContent=e.name,t.appendChild(n);const o=c(e.name),l=(e.type||"").toUpperCase(),a="BOOLEAN"===l||"BOOL"===l,r=l.startsWith("INT")||"REAL"===l||"NUMERIC"===l||l.startsWith("DECIMAL")||"FLOAT"===l||"DOUBLE"===l||l.startsWith("BIGINT")||l.startsWith("SMALLINT")||l.startsWith("SERIAL")||"MONEY"===l,m="DATE"===l,f="TIME"===l||"TIMETZ"===l||l.startsWith("TIME ")||l.startsWith("TIME("),h="DATETIME"===l||l.includes("TIMESTAMP"),b=m||f||h,g=s.filters[e.name],v=function(e){return null==e?[]:Array.isArray(e)?e.filter(e=>e&&"object"==typeof e&&"string"==typeof e.op):"object"==typeof e?[e]:[]}(g),y=function(e){return"string"==typeof e?e:null}(g);if(o){const n=document.createElement("select");n.className="filter-input field-select !py-1.5 !text-[13px]",n.dataset.column=e.name,n.dataset.kind="fk",p(n,"","(any)"),p(n,"__NULL__","(not set / NULL)"),(i[e.name]||[]).forEach(e=>p(n,String(e.value),""===e.label?String(e.value):e.label));let s=d(v,"eq");return null==s&&y&&y.startsWith("=")&&(s=y.slice(1)),"__NULL__"===s?n.value="__NULL__":null!=s&&[...n.options].some(e=>e.value===String(s))&&(n.value=String(s)),t.appendChild(n),t}if(a){const n=document.createElement("select");n.className="filter-input field-select !py-1.5 !text-[13px]",n.dataset.column=e.name,n.dataset.kind="bool",p(n,"","(any)"),p(n,"1","true"),p(n,"0","false");let s=d(v,"eq");return null==s&&y&&y.startsWith("=")&&(s=y.slice(1)),"1"!==s&&"0"!==s&&"true"!==s&&"false"!==s||(n.value="true"===s||"1"===s?"1":"0"),t.appendChild(n),t}if(b||r){const n=document.createElement("div");n.className="flex items-center gap-1.5";const s=document.createElement("input"),o=document.createElement("input");if(b)s.type=o.type=m?"date":f?"time":"datetime-local",(f||h)&&(s.step=o.step="1");else{s.type=o.type="number";const e=l.startsWith("INTEGER")||"INT"===l||l.startsWith("BIGINT")||l.startsWith("SMALLINT")||l.startsWith("SERIAL");s.step=o.step=e?"1":"any"}s.className="filter-input field-input !py-1.5 !text-[13px]",o.className="filter-input field-input !py-1.5 !text-[13px]",s.dataset.column=e.name,s.dataset.bound="min",s.placeholder=b?"from":"min",o.dataset.column=e.name,o.dataset.bound="max",o.placeholder=b?"to":"max";const a=function(e){const t=e.find(e=>"between"===e.op);return t?{min:t.value??null,max:t.max??null}:{min:null,max:null}}(v),i=d(v,"gte")??d(v,"gt")??a.min,r=d(v,"lte")??d(v,"lt")??a.max;return null!=i&&(s.value=b?u(i,m,f):String(i)),null!=r&&(o.value=b?u(r,m,f):String(r)),n.appendChild(s),n.appendChild(o),t.appendChild(n),t}const w=document.createElement("input");w.type="text",w.className="filter-input field-input !py-1.5 !text-[13px]",w.dataset.column=e.name,w.dataset.kind="text",w.placeholder="BLOB"===l?"0x… or text":"filter…",w.autocomplete="off",w.spellcheck=!1;let x=y;if(null==x){const e=d(v,"prefix"),t=d(v,"eq"),n=d(v,"like");null!=e?x=String(e)+"*":null!=t?x="="+String(t):null!=n&&(x=String(n))}return null!=x&&(w.value=x),t.appendChild(w),t}function f(t){let n="";const s=window.location.pathname.match(/\/tables\/([^/]+)(?:\/|$)/);if(s&&(n=decodeURIComponent(s[1])),!n)return;const o=document.getElementById("page-size"),l=new URLSearchParams({page:"1",size:o?o.value:"50"});Object.keys(t).length&&l.set("f",JSON.stringify(t)),window.location.href=e+"/tables/"+encodeURIComponent(n)+"?"+l.toString()}function h(){const e=document.getElementById("filter-form"),t=document.getElementById("clear-filter"),n=document.getElementById("filter-fields");e&&n&&a&&(a.columns.forEach(e=>n.appendChild(m(e))),e.addEventListener("submit",e=>{e.preventDefault(),f(function(){const e={},t={};document.querySelectorAll("#filter-fields .filter-input").forEach(e=>{const n=e.dataset.column;n&&(t[n]=t[n]||[]).push(e)});for(const n of Object.keys(t)){const s=t[n],o=s[0],l=o.dataset.kind||"";if("fk"===l){const t=o.value;if(""===t)continue;e[n]="__NULL__"===t?{op:"null"}:{op:"eq",value:t}}else if("bool"===l){const t=o.value;if(""===t)continue;e[n]={op:"eq",value:t}}else if(2===s.length&&o.dataset.bound){const t=s.find(e=>"min"===e.dataset.bound),o=s.find(e=>"max"===e.dataset.bound),l=t&&""!==t.value?t.value:null,a=o&&""!==o.value?o.value:null;null!=l&&null!=a?e[n]={op:"between",value:l,max:a}:null!=l?e[n]={op:"gte",value:l}:null!=a&&(e[n]={op:"lte",value:a})}else{const t=(o.value||"").trim();t&&(e[n]=t)}}return e}())})),t&&t.addEventListener("click",()=>f({}))}let b=null;function g(){b&&(b.classList.remove("cell-focused"),b=null)}function v(e){e&&e!==b&&!e.classList.contains("js-editing")&&(g(),b=e,e.classList.add("cell-focused"),e.scrollIntoView({block:"nearest",inline:"nearest"}))}function y(e){const t=e.closest("tr"),n=t&&t.dataset.pk,s=e.dataset.col;if(!n||!s)return;const o=e.dataset.origValue;w(e,"true"===e.dataset.origNull?null:o),e.classList.remove("cell-dirty"),delete e.dataset.origValue,delete e.dataset.origNull,E.delete(n+"::"+s),L(),UI.showToast('Reverted edit in column "'+s+'".',"info")}function w(e,t){const n=null==t,s=n?"":String(t);e.dataset.null=String(n),e.dataset.value=s,e.classList.remove("js-editing","opacity-60");const o=void 0!==e.dataset.origValue,a=e.dataset.origValue,i="true"===e.dataset.origNull,c=o?"Staged edit: "+(n?"NULL":s)+" (Saved: "+(i?"NULL":a)+")":s,d=o?'<button type="button" class="cell-revert-btn js-revert-cell" title="Revert this field to saved value">↺</button>':"";if(n)return void(e.innerHTML='<div class="flex items-center justify-between gap-1"><span class="chip chip-null js-cell-value">NULL</span>'+d+"</div>");const u=r(e.dataset.col),p=(u?.type||e.dataset.type||"").toUpperCase(),m="true"===e.dataset.blob||p.includes("BLOB")||/^0x[0-9a-f]{8,}$/i.test(s),f="true"===e.dataset.json||s.startsWith("{")&&s.endsWith("}")||s.startsWith("[")&&s.endsWith("]");if(m){const t=s.startsWith("0x")?Math.floor((s.length-2)/2):s.length;e.innerHTML='<div class="flex items-center justify-between gap-1 js-cell-value"><div class="flex items-center gap-1.5 min-w-0"><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 · '+t+' B</span><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"><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></button></div>'+d+"</div>"}else e.innerHTML=f?'<div class="flex items-center justify-between gap-1 js-cell-value"><div class="flex items-center gap-1.5 min-w-0"><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><span class="block max-w-[140px] truncate font-mono text-[12px] text-base-content/70" title="'+l(c)+'">'+l(s)+'</span><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"><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></button></div>'+d+"</div>":'<div class="flex items-center justify-between gap-1 group/cell"><span class="js-cell-value block max-w-xs truncate font-mono text-[13px]" title="'+l(c)+'">'+l(s.length>120?s.slice(0,120)+"…":s)+'</span><div class="flex items-center gap-0.5 shrink-0">'+d+(s.length>30?'<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"><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></button>':"")+"</div></div>"}function x(e){return"true"===e.dataset.null?null:e.dataset.value??""}const E=new Map;function k(e,t,n){const s=e.dataset.pk,o=t.dataset.col;if(!s||!o)return;if(!r(o))return;let l,i=!1;if("checkbox"===n.type)l=n.checked?"1":"0";else{const e=void 0===n.value?"":String(n.value);""===e||"__NULL__"===e?(l="",i=!0):l=e}var d;void 0===t.dataset.origValue&&(t.dataset.origValue=t.dataset.value,t.dataset.origNull="true"===t.dataset.null?"true":"false"),w(t,i?null:l),E.set(s+"::"+o,{td:t,pk:s,col:o,rawValue:l,setNull:i,isPk:(d=o,!!a&&-1!==a.primaryKey.indexOf(d)),isFk:!!c(o),origValue:t.dataset.origValue,origNull:"true"===t.dataset.origNull}),t.classList.add("cell-dirty"),L()}function L(){if(!N||!T)return;const e=E.size;T.textContent=String(e),N.classList.toggle("hidden",0===e)}function I(){if(!E.size||t)return;const e=new Map;for(const t of E.values()){let n=e.get(t.pk);n||(n={id:t.pk,values:{},nulls:[]},e.set(t.pk,n)),t.setNull?n.nulls.push(t.col):n.values[t.col]=t.rawValue}const n=[...e.values()];j&&(j.disabled=!0),Api.post("/api/tables/"+o+"/rows/bulk-update",{updates:n}).then(e=>{UI.showToast(e.message||"Changes applied.","success"),window.location.reload()}).catch(e=>{UI.showError(e.message),j&&(j.disabled=!1)})}async function S(){const e=E.size;if(!e)return;if(await UI.confirm({title:"Discard Changes",message:"You have "+e+" pending inline edit(s). Are you sure you want to discard all staged changes and revert to the saved values?",confirmText:"Discard "+e+" change(s)",danger:!0})){for(const e of E.values())w(e.td,e.origNull?null:e.origValue),e.td.classList.remove("cell-dirty"),delete e.td.dataset.origValue,delete e.td.dataset.origNull;E.clear(),L(),UI.showToast("Pending edits discarded.","info")}}function A(){if(E.size){var e=document.createElement("div");e.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";var t=document.createElement("div");t.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";var n="";E.forEach((e,t)=>{var s=e.origNull?'<span class="chip chip-null text-[10px]">NULL</span>':l(e.origValue),o=e.setNull?'<span class="chip chip-null text-[10px]">NULL</span>':l(e.rawValue);n+='<tr class="hover:bg-base-200/50"><td class="font-mono text-xs font-semibold">'+l(e.pk)+'</td><td class="font-mono text-xs text-primary font-medium">'+l(e.col)+'</td><td class="font-mono text-xs text-error line-through opacity-80 max-w-xs truncate">'+s+'</td><td class="font-mono text-xs text-success font-medium max-w-xs truncate">'+o+'</td><td class="text-right"><button type="button" class="btn btn-ghost btn-xs text-warning js-modal-revert-item" data-key="'+l(t)+'">Revert</button></td></tr>'}),t.innerHTML='<div class="flex items-center justify-between border-b border-base-200 px-6 py-4"> <div class="flex items-center gap-2"> <h3 class="text-base font-bold text-base-content">Staged Changes Diff</h3> <span class="chip bg-warning/15 text-warning font-mono">'+E.size+' edit(s)</span> </div> <button type="button" class="btn btn-ghost btn-xs btn-circle js-diff-close text-base-content/40 hover:text-base-content">×</button></div><div class="flex-1 overflow-y-auto p-4"> <table class="table table-sm"> <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> <tbody id="diff-rows-body">'+n+'</tbody> </table></div><div class="flex items-center justify-between border-t border-base-200 bg-base-200/30 px-6 py-4"> <button type="button" id="diff-modal-discard" class="btn btn-ghost btn-sm text-error">Discard All</button> <div class="flex gap-2"> <button type="button" class="btn btn-ghost btn-sm js-diff-close">Cancel</button> <button type="button" id="diff-modal-apply" class="btn btn-primary btn-sm gap-1"> <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> Apply '+E.size+" Change(s) </button> </div></div>",e.appendChild(t),document.body.appendChild(e),requestAnimationFrame(()=>{e.classList.remove("opacity-0"),e.classList.add("opacity-100"),t.classList.remove("scale-95"),t.classList.add("scale-100")}),t.querySelectorAll(".js-diff-close").forEach(e=>e.addEventListener("click",s)),e.addEventListener("click",t=>{t.target===e&&s()}),t.querySelectorAll(".js-modal-revert-item").forEach(e=>{e.addEventListener("click",()=>{const t=e.dataset.key,n=E.get(t);n&&(y(n.td),e.closest("tr").remove(),E.size||s())})}),t.querySelector("#diff-modal-apply").addEventListener("click",()=>{s(),I()}),t.querySelector("#diff-modal-discard").addEventListener("click",async()=>{s(),await S()})}function s(){e.classList.remove("opacity-100"),e.classList.add("opacity-0"),t.classList.remove("scale-100"),t.classList.add("scale-95"),setTimeout(()=>e.remove(),180)}}let N=null,T=null,j=null;function C(e){if(t)return;if(e.classList.contains("js-editing"))return;const n=e.closest("tr");if(!n||!n.dataset.pk||!n.dataset.table)return;if(!r(e.dataset.col))return;v(e);const s=document.querySelector(".js-editing");s&&s._finish&&s._finish(!0),e._origNull="true"===e.dataset.null,e._origValue=x(e);const o=function(e){const t=e.dataset.col,n=r(t);if(!n)return null;const s=c(t),o=(n.type||"").toUpperCase(),l="BOOLEAN"===o||"BOOL"===o,a="DATE"===o,d="TIME"===o||"TIMETZ"===o||o.startsWith("TIME ")||o.startsWith("TIME("),m="DATETIME"===o||o.includes("TIMESTAMP"),f=a||d||m,h=o.startsWith("INT")||"REAL"===o||"NUMERIC"===o||o.startsWith("DECIMAL")||"FLOAT"===o||"DOUBLE"===o||o.startsWith("BIGINT")||o.startsWith("SMALLINT")||o.startsWith("SERIAL")||"MONEY"===o,b="BLOB"===o||"BYTEA"===o||"BINARY"===o,g="UUID"===o,v=o.includes("JSON"),y=o.endsWith("[]")||o.startsWith("_")||o.includes("ARRAY"),w="INTERVAL"===o,E="POINT"===o||"GEOMETRY"===o||"BOX"===o||"CIRCLE"===o||"POLYGON"===o,k="BIT"===o||"VARBIT"===o||o.startsWith("BIT(")||o.startsWith("VARBIT("),L=x(e),I=null===L||""===L;let S;if(l)S=document.createElement("input"),S.type="checkbox",S.className="checkbox checkbox-primary checkbox-sm js-inline-ctrl",S.checked=1===L||!0===L||"1"===L||"true"===L;else if(s)S=document.createElement("select"),S.className="field-select !w-64 !py-1.5 !text-[13px] js-inline-ctrl",p(S,"","(not set / NULL)"),(i[t]||[]).forEach(e=>{const t=p(S,String(e.value),""===e.label?String(e.value):e.label);I||String(L)!==String(e.value)||(t.selected=!0)});else if(f)S=document.createElement("input"),S.type=a?"date":d?"time":"datetime-local",(d||m)&&(S.step="1"),S.className="field-input !w-64 !py-1.5 !text-[13px] js-inline-ctrl font-mono",I||(S.value=u(String(L),a,d)),setTimeout(()=>{if("function"==typeof S.showPicker)try{S.showPicker()}catch(e){}},50);else if(h){S=document.createElement("input"),S.type="number";const e=o.startsWith("INTEGER")||"INT"===o||o.startsWith("BIGINT")||o.startsWith("SMALLINT")||o.startsWith("SERIAL");S.step=e?"1":"any",S.className="field-input !w-48 !py-1.5 !text-[13px] js-inline-ctrl font-mono",I||(S.value=String(L))}else g?(S=document.createElement("input"),S.type="text",S.className="field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl",S.placeholder="00000000-0000-0000-0000-000000000000",I||(S.value=String(L))):y?(S=document.createElement("input"),S.type="text",S.className="field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl",S.placeholder="{item1, item2}",I||(S.value=String(L))):w?(S=document.createElement("input"),S.type="text",S.className="field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl",S.placeholder="e.g. '1 day'",I||(S.value=String(L))):E?(S=document.createElement("input"),S.type="text",S.className="field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl",S.placeholder="(x, y)",I||(S.value=String(L))):k?(S=document.createElement("input"),S.type="text",S.className="field-input font-mono !w-64 !py-1.5 !text-[12px] js-inline-ctrl",S.placeholder="101010",I||(S.value=String(L))):(S=document.createElement("input"),S.type="text",S.className="field-input !w-64 !py-1.5 !text-[13px] js-inline-ctrl",S.placeholder=b?"BYTEA"===o?"\\x… or 0x… hex":"0x… hex or text":v?'{"key":"value"}':"",I||(S.value=String(L)));return S}(e);if(!o)return;e.classList.add("js-editing"),e.innerHTML="",e.appendChild(o);let l=!1;const a=(t,s)=>{l||(l=!0,t?k(n,e,o):function(e){w(e,e._origNull?null:e._origValue),e.classList.remove("js-editing")}(e),"down"===s?U("down"):"right"===s?U("right"):"left"===s&&U("left"))};e._finish=a,o.addEventListener("keydown",e=>{"Enter"===e.key?(e.preventDefault(),a(!0,"down")):"Tab"===e.key?(e.preventDefault(),a(!0,e.shiftKey?"left":"right")):"Escape"===e.key&&(e.preventDefault(),a(!1))}),"SELECT"===o.tagName||"checkbox"===o.type?o.addEventListener("change",()=>a(!0)):o.addEventListener("blur",()=>a(!0)),o.focus(),"function"==typeof o.select&&o.select()}function U(e){if(!b){const e=Array.prototype.slice.call(document.querySelectorAll("#main-table tbody tr td.cell-editable"));return void(e.length&&v(e[0]))}const t=b.closest("tr");if(!t)return;const n=Array.prototype.slice.call(t.querySelectorAll("td.cell-editable")),s=n.indexOf(b);if("right"===e)if(s<n.length-1)v(n[s+1]);else{const e=t.nextElementSibling;if(e){const t=Array.prototype.slice.call(e.querySelectorAll("td.cell-editable"));t.length&&v(t[0])}}else if("left"===e)if(s>0)v(n[s-1]);else{const e=t.previousElementSibling;if(e){const t=Array.prototype.slice.call(e.querySelectorAll("td.cell-editable"));t.length&&v(t[t.length-1])}}else if("down"===e){const e=t.nextElementSibling;if(e){const t=Array.prototype.slice.call(e.querySelectorAll("td.cell-editable"));t[s]&&v(t[s])}}else if("up"===e){const e=t.previousElementSibling;if(e){const t=Array.prototype.slice.call(e.querySelectorAll("td.cell-editable"));t[s]&&v(t[s])}}}function B(){N=function(){const e=document.createElement("div");return e.id="changes-bar",e.className="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",e.innerHTML='<div class="flex items-center gap-2"><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><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></div><div class="h-4 w-px bg-base-300"></div><div class="flex items-center gap-1.5"><button type="button" id="changes-review" class="btn btn-ghost btn-sm gap-1.5 text-xs font-medium"><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>Review Diff</button><button type="button" id="changes-apply" class="btn btn-primary btn-sm gap-1 text-xs font-semibold"><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>Apply</button><button type="button" id="changes-discard" class="btn btn-ghost btn-sm text-xs text-base-content/60 hover:text-error">Discard</button></div>',document.body.appendChild(e),e}(),T=document.getElementById("changes-count"),j=document.getElementById("changes-apply");const e=document.getElementById("changes-discard"),n=document.getElementById("changes-review");j&&j.addEventListener("click",I),e&&e.addEventListener("click",S),n&&n.addEventListener("click",A),document.addEventListener("click",e=>{const t=e.target.closest(".js-revert-cell");if(t){e.stopPropagation(),e.preventDefault();const n=t.closest("td");return void(n&&y(n))}const n=e.target.closest("td.cell-editable");n&&!n.classList.contains("js-editing")&&v(n)}),t||document.querySelectorAll(".js-inline-cell").forEach(e=>{e.addEventListener("dblclick",()=>C(e))}),document.addEventListener("keydown",e=>{if(document.querySelector(".js-editing"))return;const n=document.activeElement?document.activeElement.tagName:"";if("INPUT"!==n&&"TEXTAREA"!==n&&"SELECT"!==n&&b)if("ArrowRight"===e.key)e.preventDefault(),U("right");else if("ArrowLeft"===e.key)e.preventDefault(),U("left");else if("ArrowDown"===e.key)e.preventDefault(),U("down");else if("ArrowUp"===e.key)e.preventDefault(),U("up");else if("Tab"===e.key)e.preventDefault(),U(e.shiftKey?"left":"right");else if("Enter"===e.key)e.preventDefault(),t||C(b);else if("Escape"===e.key)g();else if(" "!==e.key||t){if((e.ctrlKey||e.metaKey)&&"c"===e.key.toLowerCase()){const e=x(b);null!=e&&navigator.clipboard.writeText(String(e)).then(()=>{UI.showToast("Cell value copied.","info")})}}else{const t=r(b.dataset.col);if(t&&"BOOLEAN"===(t.type||"").toUpperCase()){e.preventDefault();const t=b.closest("tr"),n=x(b),s=1===n||"1"===n||!0===n||"true"===n?"0":"1";k(t,b,{value:s,type:"checkbox",checked:"1"===s})}}})}function R(){return Array.prototype.slice.call(document.querySelectorAll(".row-checkbox:checked")).map(e=>e.value).filter(Boolean)}async function M(e){const t=R();if(t.length)try{const n=await Api.post("/api/tables/"+o+"/rows/bulk-export",{ids:t,format:e}),s=new Blob([n.content],{type:"json"===e?"application/json;charset=utf-8":"text/csv;charset=utf-8"}),l=document.createElement("a");l.href=URL.createObjectURL(s),l.download=n.filename,document.body.appendChild(l),l.click(),l.remove(),URL.revokeObjectURL(l.href),UI.showToast("Exported "+n.rowCount+" row(s).","success")}catch(e){UI.showError(e.message)}}async function O(){const e=R();if(!e.length||t)return;let n;try{n=await Api.post("/api/tables/"+o+"/rows/bulk-impact",{ids:e})}catch(e){return void UI.showError(e.message)}const a=n&&n.references||[],i=a.length?'<p class="mb-2 font-medium text-warning">⚠ Other tables reference these records:</p><ul class="mb-2 list-disc space-y-1 pl-5 text-xs">'+a.map(e=>'<li><code class="font-mono font-bold">'+l(e.table)+"."+l(e.from)+'</code> — <b class="tabular-nums">'+e.count+"</b> row(s) point at them</li>").join("")+'</ul><p class="text-[11px] text-base-content/55">Depending on foreign key rules, those rows may cascade-delete or block this action.</p>':'<p class="text-xs text-base-content/60">No other tables reference these rows — safe to delete.</p>';if(await UI.confirm({title:"Delete Selected Rows",message:"Delete "+e.length+' selected row(s) from table "'+s.table+'"? This action cannot be undone.',item:s.table,itemType:"Table",detailsHtml:i,confirmText:"Delete "+e.length+" row(s)",danger:!0}))try{const t=await Api.post("/api/tables/"+o+"/rows/bulk-delete",{ids:e,confirmImpact:!0});UI.showToast(t.message||"Rows deleted.","success"),window.location.reload()}catch(e){UI.showError(e.message)}}function D(e,t){const n=e.querySelector(".js-ref-chevron");n&&n.classList.toggle("rotate-180",t)}let W=null;function q(){const e=document.querySelector(".js-ref-pop");e&&(e._cleanup&&e._cleanup(),e.remove()),W&&(D(W,!1),W.setAttribute("aria-expanded","false"),W.classList.remove("opacity-50"),W=null)}function _(t,n){q();const s=document.createElement("div");s.className="js-ref-pop app-card js-ref-anim z-50 min-w-[280px] max-w-[460px] overflow-hidden shadow-lift",s.innerHTML=n.length?n.map(t=>function(e,t){const n=t+"/tables/"+encodeURIComponent(e.table),s=e.count??e.total??(e.rows?e.rows.length:0),o=e.columns||(e.rows&&e.rows.length>0?Object.keys(e.rows[0]).filter(e=>"_rowid_"!==e):[]);let a='<div class="flex items-center justify-between gap-2 border-b border-base-200 px-3 py-2"><span class="min-w-0 truncate"><a class="link link-primary font-mono text-xs font-semibold" href="'+n+'">'+l(e.table)+'</a><span class="ml-1.5 text-xs text-base-content/45">via <code class="font-mono">'+l(e.from)+"</code>"+(void 0!==e.value?' = <code class="font-mono">'+l(String(e.value))+"</code>":"")+'</span></span><span class="chip '+(s?"chip-pk":"chip-type")+'">'+l(s+" row"+(1===s?"":"s"))+"</span></div>";return e.rows&&e.rows.length&&o.length?a+'<div class="max-h-64 overflow-auto"><table class="w-full"><thead><tr class="bg-base-200/40"><tr>'+o.map(function(e){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">'+l(e)+"</span></th>"}).join("")+"</tr></tr></thead><tbody>"+e.rows.map(function(e){return'<tr class="border-t border-base-200/60 hover:bg-primary/[0.04]">'+o.map(function(t){const n=e[t];if(null==n)return'<td class="px-2.5 py-1.5 align-middle"><span class="text-[11px] text-base-content/40">NULL</span></td>';const s=String(n),o=s.length>120?s.slice(0,120)+"…":s;return'<td class="max-w-[180px] truncate px-2.5 py-1.5 align-middle font-mono text-[11px] text-base-content/80" title="'+l(s)+'">'+l(o)+"</td>"}).join("")+"</tr>"}).join("")+'</tbody></table></div><div class="flex justify-end border-t border-base-200 px-3 py-1.5"><a class="btn btn-ghost btn-xs text-base-content/60 hover:text-primary" href="'+n+'">Open table</a></div>':a+'<div class="px-3 py-3 text-xs text-base-content/45">No matching rows.</div>'}(t,e)).join('<div class="h-px bg-base-200"></div>'):'<div class="px-4 py-3 text-sm text-base-content/45">No related rows for this record.</div>',s.style.visibility="hidden",document.body.appendChild(s);const o=Math.min(460,Math.max(280,s.offsetWidth)),a=s.offsetHeight,i=t.getBoundingClientRect();let r=i.left;r+o>window.innerWidth-8&&(r=Math.max(8,window.innerWidth-o-8));let c=i.bottom+6;c+a>window.innerHeight-8&&(c=Math.max(8,i.top-a-6)),s.style.position="fixed",s.style.left=r+"px",s.style.top=c+"px",s.style.visibility="visible",W=t;const d=e=>{e&&e.target&&s.contains(e.target)||q()};window.addEventListener("scroll",d,!0),window.addEventListener("resize",d);const u=e=>{s.contains(e.target)||e.target===t||t.contains(e.target)||q()};document.addEventListener("click",u,!0);const p=e=>{"Escape"===e.key&&q()};document.addEventListener("keydown",p),s._cleanup=()=>{window.removeEventListener("scroll",d,!0),window.removeEventListener("resize",d),document.removeEventListener("click",u,!0),document.removeEventListener("keydown",p)}}!async function(){try{a=await Api.get("/api/tables/"+o+"/info")}catch(e){a=null}try{i=await Api.get("/api/tables/"+o+"/fk-options")}catch(e){i={}}h(),document.querySelectorAll(".js-row-copy-json").forEach(e=>{e.addEventListener("click",t=>{t.stopPropagation();const n=e.closest("tr");if(!n)return;const s={};n.querySelectorAll("td.cell-editable").forEach(e=>{const t=e.dataset.col;t&&(s[t]="true"===e.dataset.null?null:e.dataset.value)}),navigator.clipboard.writeText(JSON.stringify(s,null,2)).then(()=>{UI.showToast("Row copied as JSON.","success")})})}),document.querySelectorAll(".js-row-copy-sql").forEach(e=>{e.addEventListener("click",t=>{t.stopPropagation();const n=e.closest("tr");if(!n)return;const o=[],l=[];n.querySelectorAll("td.cell-editable").forEach(e=>{const t=e.dataset.col;if(t)if(o.push('"'+t.replace(/"/g,'""')+'"'),"true"===e.dataset.null)l.push("NULL");else{const t=e.dataset.value;l.push("'"+String(t).replace(/'/g,"''")+"'")}});const a='INSERT INTO "'+s.table.replace(/"/g,'""')+'" ('+o.join(", ")+") VALUES ("+l.join(", ")+");";navigator.clipboard.writeText(a).then(()=>{UI.showToast("SQL INSERT copied to clipboard.","success")})})}),document.querySelectorAll(".js-row-duplicate").forEach(t=>{t.addEventListener("click",n=>{n.stopPropagation();const s=t.dataset.pk;s&&(window.location.href=e+"/tables/"+o+"/rows/new?duplicate="+encodeURIComponent(s))})}),document.querySelectorAll(".dropdown").forEach(e=>{const t=e.querySelector('label[role="button"]')||e.querySelector("button");t&&t.addEventListener("click",()=>{const n=t.getBoundingClientRect(),s=window.innerHeight-n.bottom;e.classList.toggle("dropdown-top",s<220)})}),B(),document.addEventListener("click",e=>{const n=e.target.closest(".js-inspect-btn");if(!n)return;e.stopPropagation(),e.preventDefault();const o=n.closest("td"),l=n.closest("tr");if(!o||!l)return;const a=o.dataset.col,i="true"===o.dataset.null,c=o.dataset.value,d=l.dataset.pk,u=l.dataset.table||s.table,p="true"===o.dataset.blob,m="true"===o.dataset.json,f=o.dataset.type||(r(a)?.type??"");window.Inspector&&window.Inspector.open&&window.Inspector.open({table:u,pk:d,col:a,colType:f,value:c,isNull:i,isBlob:p,isJson:m,readonly:t,onSave:e=>{t||k(l,o,{value:e,type:"text"})}})}),function(){const e=document.getElementById("bulk-bar");if(!e)return;const n=document.getElementById("select-all-rows"),s=document.getElementById("bulk-count"),o=document.getElementById("bulk-delete"),l=document.getElementById("bulk-export-csv"),a=document.getElementById("bulk-export-json"),i=document.getElementById("bulk-clear"),r=Array.prototype.slice.call(document.querySelectorAll(".row-checkbox"));function c(){const l=R().length;s&&(s.textContent=String(l)),e.classList.toggle("hidden",0===l),n&&(n.checked=l>0&&l===r.length),o&&(o.disabled=t||0===l)}r.forEach(e=>e.addEventListener("change",c)),n&&n.addEventListener("change",()=>{r.forEach(e=>{e.checked=n.checked}),c()}),i&&i.addEventListener("click",()=>{r.forEach(e=>{e.checked=!1}),c()}),l&&l.addEventListener("click",()=>M("csv")),a&&a.addEventListener("click",()=>M("json")),o&&o.addEventListener("click",O)}(),document.querySelectorAll(".js-delete-row").forEach(e=>{e.addEventListener("click",async()=>{const t=e.dataset.table,n=e.dataset.pk;t&&n&&await UI.confirm({title:"Delete Row",message:"Are you sure you want to permanently delete this row? This action cannot be undone.",item:n,itemType:"Primary Key",confirmText:"Delete row",danger:!0})&&Api.del("/api/tables/"+encodeURIComponent(t)+"/row/"+encodeURIComponent(n)).then(()=>{UI.showToast("Row deleted.","success"),window.location.reload()}).catch(e=>UI.showError(e.message))})}),function(){const t=document.getElementById("page-size");t&&t.addEventListener("change",()=>{const n=t.dataset.table,s=t.value,o=t.dataset.orderby||"",l=t.dataset.orderdir||"asc",a=t.dataset.filters||"";let i=e+"/tables/"+encodeURIComponent(n)+"?page=1&size="+encodeURIComponent(s)+"&orderBy="+encodeURIComponent(o)+"&orderDir="+encodeURIComponent(l);a&&(i+="&"+a),window.location.href=i})}(),function(){const e=document.getElementById("import-csv"),t=document.getElementById("import-panel"),n=document.getElementById("import-csv-close"),s=document.getElementById("import-csv-text"),o=document.getElementById("import-csv-file"),l=document.getElementById("import-csv-submit");e&&t&&e.addEventListener("click",()=>{t.classList.toggle("hidden"),t.classList.contains("hidden")||s.focus()}),n&&n.addEventListener("click",()=>t.classList.add("hidden")),o&&o.addEventListener("change",()=>{const e=o.files&&o.files[0];if(!e)return;const t=new FileReader;t.onload=()=>{s.value=String(t.result||"")},t.readAsText(e)}),l&&s&&l.addEventListener("click",async()=>{const e=s.value.trim();if(!e)return UI.showError("Paste or choose a CSV file first.");const t=window.location.pathname.match(/\/tables\/([^/]+)(?:\/|$)/),n=t?decodeURIComponent(t[1]):"";if(!n)return UI.showError("Could not determine the table.");l.disabled=!0;try{const t=await Api.post("/api/tables/"+encodeURIComponent(n)+"/rows/import",{csv:e});UI.showToast(t.message||"Import complete.","success"),window.location.reload()}catch(e){UI.showError(e.message)}finally{l.disabled=!1}})}(),document.querySelectorAll(".js-ref-toggle").forEach(e=>{e.addEventListener("click",t=>{if(t.stopPropagation(),"true"===e.getAttribute("aria-expanded"))return void q();const n=e.dataset.table,s=e.dataset.pk;n&&s&&(e.classList.add("opacity-50"),Api.get("/api/tables/"+encodeURIComponent(n)+"/rows/"+encodeURIComponent(s)+"/references").then(t=>{e.classList.remove("opacity-50");const n=e.dataset.refTable,s=(t&&t.references||[]).filter(e=>!n||e.table===n);e.setAttribute("aria-expanded","true"),D(e,!0),_(e,s)}).catch(t=>{e.classList.remove("opacity-50"),UI.showError(t.message)}))})})}()}();
|