sqlite-hub 0.12.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/frontend/js/api.js +4 -2
- package/frontend/js/app.js +632 -12
- package/frontend/js/components/modal.js +432 -3
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +101 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +227 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +0 -3
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/package.json +2 -3
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- package/fill.js +0 -526
package/frontend/js/app.js
CHANGED
|
@@ -52,7 +52,9 @@ import {
|
|
|
52
52
|
openDataRowByIdentity,
|
|
53
53
|
openEditConnectionModal,
|
|
54
54
|
openCreateQueryChartModal,
|
|
55
|
+
openCopyColumnModal,
|
|
55
56
|
openEditQueryChartModal,
|
|
57
|
+
preserveCurrentDataRowSelectionForReload,
|
|
56
58
|
openDataRowUpdatePreview,
|
|
57
59
|
openEditorRowUpdatePreview,
|
|
58
60
|
refreshCurrentRoute,
|
|
@@ -97,6 +99,8 @@ import {
|
|
|
97
99
|
sortEditorResultsByColumn,
|
|
98
100
|
setQueryHistorySearchInput,
|
|
99
101
|
setQueryHistoryTab,
|
|
102
|
+
setCopyColumnModalError,
|
|
103
|
+
setCopyColumnModalSubmitting,
|
|
100
104
|
setRoute,
|
|
101
105
|
saveQueryHistoryNotes,
|
|
102
106
|
saveQueryHistoryTitle,
|
|
@@ -107,6 +111,7 @@ import {
|
|
|
107
111
|
setMediaTaggingWorkflowMediaRotationDegrees,
|
|
108
112
|
queueTableDesignerCsvImport,
|
|
109
113
|
showToast,
|
|
114
|
+
storeCopyColumnPreferences,
|
|
110
115
|
submitCreateConnection,
|
|
111
116
|
createCurrentMediaTag,
|
|
112
117
|
submitDeleteRowConfirmation,
|
|
@@ -118,9 +123,11 @@ import {
|
|
|
118
123
|
toggleQueryHistorySavedState,
|
|
119
124
|
updateCurrentMediaTaggingField,
|
|
120
125
|
updateCurrentMediaTaggingTagFormField,
|
|
126
|
+
updateCopyColumnModalFormatField,
|
|
121
127
|
updateCurrentQueryChartDraftConfigField,
|
|
122
128
|
updateCurrentQueryChartDraftField,
|
|
123
129
|
updateCurrentTableDesignerColumnField,
|
|
130
|
+
updateCurrentTableDesignerConstraintField,
|
|
124
131
|
updateCurrentTableDesignerField,
|
|
125
132
|
addCurrentTableDesignerColumn,
|
|
126
133
|
applyCurrentMediaTaggingSelection,
|
|
@@ -137,7 +144,29 @@ import { renderSettingsView } from './views/settings.js';
|
|
|
137
144
|
import { renderStructureView } from './views/structure.js';
|
|
138
145
|
import { renderTableDesignerView } from './views/tableDesigner.js';
|
|
139
146
|
import { replaceChildrenFromRenderedMarkup, replaceElementFromRenderedMarkup } from './utils/dom.js';
|
|
140
|
-
import {
|
|
147
|
+
import {
|
|
148
|
+
buildCopyColumnText,
|
|
149
|
+
getCopyColumnExportMetadata,
|
|
150
|
+
isMarkdownTodoCopyColumnMode,
|
|
151
|
+
normalizeCopyColumnMode,
|
|
152
|
+
} from './utils/copyColumnExport.js';
|
|
153
|
+
import { formatNumber, highlightSql } from './utils/format.js';
|
|
154
|
+
import {
|
|
155
|
+
compactPathForDisplay,
|
|
156
|
+
detectFilePathValue,
|
|
157
|
+
getPathTypeLabel,
|
|
158
|
+
} from './utils/filePathPreview.js';
|
|
159
|
+
import { formatSqlQuery } from './utils/sqlFormatter.js';
|
|
160
|
+
import {
|
|
161
|
+
buildDataRowEditorJsonObject,
|
|
162
|
+
buildEditorRowEditorJsonObject,
|
|
163
|
+
stringifyRowEditorJson,
|
|
164
|
+
} from './utils/rowEditorJson.js';
|
|
165
|
+
import { getTimestampPreviewForField } from './utils/timestampPreview.js';
|
|
166
|
+
import {
|
|
167
|
+
formatTextCellCharacterCount,
|
|
168
|
+
getTextCellCharacterCount,
|
|
169
|
+
} from './utils/textCellStats.js';
|
|
141
170
|
|
|
142
171
|
const appRoot = document.querySelector('#app');
|
|
143
172
|
|
|
@@ -1254,6 +1283,39 @@ async function executeEditorQueryAndNavigate() {
|
|
|
1254
1283
|
router.navigate(success && activeTab === 'results' ? '/editor/results' : '/editor');
|
|
1255
1284
|
}
|
|
1256
1285
|
|
|
1286
|
+
function formatCurrentQuery() {
|
|
1287
|
+
const currentQuery = getState().editor.sqlText ?? '';
|
|
1288
|
+
const formattedQuery = formatSqlQuery(currentQuery);
|
|
1289
|
+
|
|
1290
|
+
if (!formattedQuery) {
|
|
1291
|
+
showToast('No SQL query to format.', 'alert');
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (formattedQuery === currentQuery) {
|
|
1296
|
+
focusQueryEditorInput();
|
|
1297
|
+
showToast('SQL query is already formatted.', 'muted');
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
invalidateMainRenderCache();
|
|
1302
|
+
setCurrentQuery(formattedQuery);
|
|
1303
|
+
|
|
1304
|
+
const input = document.querySelector('[data-bind="current-query"]');
|
|
1305
|
+
|
|
1306
|
+
if (input instanceof HTMLTextAreaElement) {
|
|
1307
|
+
input.value = formattedQuery;
|
|
1308
|
+
syncQueryEditorHighlight(input);
|
|
1309
|
+
syncQueryEditorScroll(input);
|
|
1310
|
+
input.focus({ preventScroll: true });
|
|
1311
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
1312
|
+
} else {
|
|
1313
|
+
pendingQueryEditorFocus = true;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
showToast('SQL query formatted.', 'success');
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1257
1319
|
const OPENABLE_URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
1258
1320
|
|
|
1259
1321
|
function getOpenableUrl(value) {
|
|
@@ -1291,6 +1353,414 @@ function openRowEditorUrl(actionNode) {
|
|
|
1291
1353
|
link.remove();
|
|
1292
1354
|
}
|
|
1293
1355
|
|
|
1356
|
+
function getExportFilenameFromAction(actionNode) {
|
|
1357
|
+
const input = actionNode.closest('[data-export-modal]')?.querySelector('input[name="filename"]');
|
|
1358
|
+
return input instanceof HTMLInputElement ? input.value : '';
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function syncRowEditorTimestampPreview(inputNode) {
|
|
1362
|
+
const fieldNode = inputNode.closest('[data-row-editor-field]');
|
|
1363
|
+
const previewNode = fieldNode?.querySelector('[data-row-editor-timestamp-preview]');
|
|
1364
|
+
|
|
1365
|
+
if (!fieldNode || !previewNode) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
|
|
1370
|
+
const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
|
|
1371
|
+
const tableMeta = {
|
|
1372
|
+
columns: [
|
|
1373
|
+
{
|
|
1374
|
+
name: columnName,
|
|
1375
|
+
primaryKeyPosition: protectedKeyColumn ? 1 : 0,
|
|
1376
|
+
foreignKey: protectedKeyColumn,
|
|
1377
|
+
},
|
|
1378
|
+
],
|
|
1379
|
+
foreignKeys: [],
|
|
1380
|
+
};
|
|
1381
|
+
const preview = getTimestampPreviewForField({
|
|
1382
|
+
columnName,
|
|
1383
|
+
value: inputNode.value,
|
|
1384
|
+
tableMeta,
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
if (preview.kind !== 'timestamp') {
|
|
1388
|
+
previewNode.hidden = true;
|
|
1389
|
+
previewNode.textContent = '';
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
previewNode.hidden = false;
|
|
1394
|
+
previewNode.textContent = `Interpretiert als Datum: ${preview.formatted}`;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function syncRowEditorCharacterCount(inputNode) {
|
|
1398
|
+
const fieldNode = inputNode.closest('[data-row-editor-field]');
|
|
1399
|
+
const countNode = fieldNode?.querySelector('[data-row-editor-char-count]');
|
|
1400
|
+
|
|
1401
|
+
if (!fieldNode || !countNode) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const count = getTextCellCharacterCount(inputNode.value);
|
|
1406
|
+
|
|
1407
|
+
if (count === null) {
|
|
1408
|
+
countNode.hidden = true;
|
|
1409
|
+
countNode.textContent = '';
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
countNode.hidden = false;
|
|
1414
|
+
countNode.textContent = formatTextCellCharacterCount(count);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
function syncRowEditorFilePathPreview(inputNode) {
|
|
1418
|
+
const fieldNode = inputNode.closest('[data-row-editor-field]');
|
|
1419
|
+
const previewNode = fieldNode?.querySelector('[data-row-editor-filepath-preview]');
|
|
1420
|
+
|
|
1421
|
+
if (!fieldNode || !previewNode) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const columnName = fieldNode.dataset.rowEditorColumnName ?? '';
|
|
1426
|
+
const protectedKeyColumn = fieldNode.dataset.rowEditorProtectedKey === 'true';
|
|
1427
|
+
const tableMeta = {
|
|
1428
|
+
columns: [
|
|
1429
|
+
{
|
|
1430
|
+
name: columnName,
|
|
1431
|
+
primaryKeyPosition: protectedKeyColumn ? 1 : 0,
|
|
1432
|
+
foreignKey: protectedKeyColumn,
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1435
|
+
foreignKeys: [],
|
|
1436
|
+
};
|
|
1437
|
+
const preview = detectFilePathValue(inputNode.value, columnName, tableMeta);
|
|
1438
|
+
|
|
1439
|
+
if (!preview) {
|
|
1440
|
+
previewNode.hidden = true;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const filenameNode = previewNode.querySelector('[data-row-editor-filepath-filename]');
|
|
1445
|
+
const directoryRowNode = previewNode.querySelector('[data-row-editor-filepath-directory-row]');
|
|
1446
|
+
const directoryNode = previewNode.querySelector('[data-row-editor-filepath-directory]');
|
|
1447
|
+
const extensionNode = previewNode.querySelector('[data-row-editor-filepath-extension]');
|
|
1448
|
+
const typeNode = previewNode.querySelector('[data-row-editor-filepath-type]');
|
|
1449
|
+
|
|
1450
|
+
if (filenameNode) {
|
|
1451
|
+
filenameNode.textContent = preview.fileName ?? 'N/A';
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (directoryRowNode) {
|
|
1455
|
+
directoryRowNode.hidden = !preview.directory;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (directoryNode) {
|
|
1459
|
+
directoryNode.textContent = preview.directory ? compactPathForDisplay(preview.directory, 72) : '';
|
|
1460
|
+
directoryNode.setAttribute('title', preview.directory ?? '');
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
if (extensionNode) {
|
|
1464
|
+
extensionNode.textContent = preview.extension ?? 'N/A';
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
if (typeNode) {
|
|
1468
|
+
typeNode.textContent = getPathTypeLabel(preview.pathType);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
previewNode.hidden = false;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function closeCopyColumnMenus(exceptMenu = null) {
|
|
1475
|
+
document.querySelectorAll('[data-copy-column-menu][open]').forEach(menu => {
|
|
1476
|
+
if (menu !== exceptMenu && menu instanceof HTMLDetailsElement) {
|
|
1477
|
+
menu.open = false;
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function closeSidebarDatabasePickers(exceptPicker = null) {
|
|
1483
|
+
document.querySelectorAll('.sidebar-db-picker[open]').forEach(picker => {
|
|
1484
|
+
if (picker !== exceptPicker && picker instanceof HTMLDetailsElement) {
|
|
1485
|
+
picker.open = false;
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function openCopyColumnMenu(headerNode) {
|
|
1491
|
+
const menu = headerNode?.querySelector('[data-copy-column-menu]');
|
|
1492
|
+
|
|
1493
|
+
if (!(menu instanceof HTMLDetailsElement)) {
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
closeCopyColumnMenus(menu);
|
|
1498
|
+
menu.open = true;
|
|
1499
|
+
menu.querySelector('summary')?.focus({ preventScroll: true });
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function getCopyColumnResult(state, scope) {
|
|
1503
|
+
return scope === 'charts' ? state.charts.result : state.editor.result;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function slugifyExportFilenamePart(value, fallback = 'column') {
|
|
1507
|
+
const slug = String(value ?? '')
|
|
1508
|
+
.trim()
|
|
1509
|
+
.toLowerCase()
|
|
1510
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1511
|
+
.replace(/^-+|-+$/g, '');
|
|
1512
|
+
|
|
1513
|
+
return slug || fallback;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function buildCopyColumnExportFilename(columnName, copyMode) {
|
|
1517
|
+
const metadata = getCopyColumnExportMetadata(copyMode);
|
|
1518
|
+
const columnSlug = slugifyExportFilenamePart(columnName);
|
|
1519
|
+
|
|
1520
|
+
return `${columnSlug}-${metadata.suffix}.${metadata.extension}`;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
function downloadTextFile({ text, filename, mimeType }) {
|
|
1524
|
+
const blob = new Blob([text], { type: mimeType });
|
|
1525
|
+
const url = URL.createObjectURL(blob);
|
|
1526
|
+
const link = document.createElement('a');
|
|
1527
|
+
|
|
1528
|
+
link.href = url;
|
|
1529
|
+
link.download = filename;
|
|
1530
|
+
document.body.appendChild(link);
|
|
1531
|
+
link.click();
|
|
1532
|
+
link.remove();
|
|
1533
|
+
URL.revokeObjectURL(url);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function getSelectedDataBrowserRowForJson(state) {
|
|
1537
|
+
if (state.dataBrowser.selectedRow) {
|
|
1538
|
+
return state.dataBrowser.selectedRow;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const rowIndex = state.dataBrowser.selectedRowIndex;
|
|
1542
|
+
|
|
1543
|
+
if (typeof rowIndex !== 'number') {
|
|
1544
|
+
return null;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function getSelectedEditorResultRowForJson(state) {
|
|
1551
|
+
const rowIndex = state.editor.selectedRowIndex;
|
|
1552
|
+
|
|
1553
|
+
if (typeof rowIndex !== 'number') {
|
|
1554
|
+
return null;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
return state.editor.result?.rows?.[rowIndex] ?? null;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function applyChangedRowEditorFormValues(rowObject) {
|
|
1561
|
+
const form = shellRefs.panel.querySelector(
|
|
1562
|
+
'form[data-form="save-data-row"], form[data-form="save-editor-row"]',
|
|
1563
|
+
);
|
|
1564
|
+
|
|
1565
|
+
if (!form) {
|
|
1566
|
+
return rowObject;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const nextRowObject = { ...rowObject };
|
|
1570
|
+
const formData = new FormData(form);
|
|
1571
|
+
|
|
1572
|
+
for (const [key, value] of formData.entries()) {
|
|
1573
|
+
if (!key.startsWith('field:')) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const control = Array.from(form.elements).find(element => element.name === key);
|
|
1578
|
+
const initialValue = control?.dataset?.rowEditorInitialValue;
|
|
1579
|
+
const currentValue = String(value ?? '');
|
|
1580
|
+
|
|
1581
|
+
if (initialValue !== undefined && currentValue === initialValue) {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
nextRowObject[key.slice('field:'.length)] = currentValue;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
return nextRowObject;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function buildRowEditorJsonPayload(state) {
|
|
1592
|
+
if (state.route.name === 'data') {
|
|
1593
|
+
const row = getSelectedDataBrowserRowForJson(state);
|
|
1594
|
+
const table = state.dataBrowser.table;
|
|
1595
|
+
const tableName = table?.name ?? state.dataBrowser.selectedTable ?? 'row';
|
|
1596
|
+
|
|
1597
|
+
if (!row || !table) {
|
|
1598
|
+
return null;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
const rowObject = applyChangedRowEditorFormValues(
|
|
1602
|
+
buildDataRowEditorJsonObject({
|
|
1603
|
+
row,
|
|
1604
|
+
columns: table.columns ?? table.columnMeta ?? [],
|
|
1605
|
+
}),
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
return {
|
|
1609
|
+
filename: `${slugifyExportFilenamePart(tableName, 'row')}-row.json`,
|
|
1610
|
+
label: tableName,
|
|
1611
|
+
text: stringifyRowEditorJson(rowObject),
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (state.route.name === 'editorResults') {
|
|
1616
|
+
const row = getSelectedEditorResultRowForJson(state);
|
|
1617
|
+
const result = state.editor.result;
|
|
1618
|
+
const rowIndex = state.editor.selectedRowIndex;
|
|
1619
|
+
const tableName = result?.editing?.tableName ?? 'query-result';
|
|
1620
|
+
|
|
1621
|
+
if (!row || !result) {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const rowObject = applyChangedRowEditorFormValues(
|
|
1626
|
+
buildEditorRowEditorJsonObject({
|
|
1627
|
+
row,
|
|
1628
|
+
editingColumns: result.editing?.columns ?? [],
|
|
1629
|
+
resultColumns: result.columns ?? [],
|
|
1630
|
+
}),
|
|
1631
|
+
);
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
filename: `${slugifyExportFilenamePart(tableName, 'query-result')}-row-${Number(rowIndex) + 1}.json`,
|
|
1635
|
+
label: tableName,
|
|
1636
|
+
text: stringifyRowEditorJson(rowObject),
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
return null;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async function copyRowEditorJson() {
|
|
1644
|
+
const payload = buildRowEditorJsonPayload(getState());
|
|
1645
|
+
|
|
1646
|
+
if (!payload) {
|
|
1647
|
+
showToast('No row is selected.', 'alert');
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
if (!navigator.clipboard?.writeText) {
|
|
1652
|
+
showToast('Clipboard API is not available.', 'alert');
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
try {
|
|
1657
|
+
await navigator.clipboard.writeText(payload.text);
|
|
1658
|
+
showToast(`Row from "${payload.label}" copied as JSON.`, 'success');
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
showToast('Row JSON could not be copied.', 'alert');
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function exportRowEditorJson() {
|
|
1665
|
+
const payload = buildRowEditorJsonPayload(getState());
|
|
1666
|
+
|
|
1667
|
+
if (!payload) {
|
|
1668
|
+
showToast('No row is selected.', 'alert');
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
downloadTextFile({
|
|
1673
|
+
text: payload.text,
|
|
1674
|
+
filename: payload.filename,
|
|
1675
|
+
mimeType: 'application/json;charset=utf-8',
|
|
1676
|
+
});
|
|
1677
|
+
showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function preserveDataRowEditorSelectionForReload() {
|
|
1681
|
+
preserveCurrentDataRowSelectionForReload();
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
async function submitCopyColumnModal(formData) {
|
|
1685
|
+
const state = getState();
|
|
1686
|
+
const modal = state.modal;
|
|
1687
|
+
|
|
1688
|
+
if (modal?.kind !== 'copy-column') {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const scope = String(formData.get('scope') ?? modal.scope ?? 'editor');
|
|
1693
|
+
const columnName = String(formData.get('columnName') ?? modal.columnName ?? '');
|
|
1694
|
+
const copyMode = normalizeCopyColumnMode(String(formData.get('copyMode') ?? modal.copyMode ?? 'column'));
|
|
1695
|
+
const separator = String(formData.get('separator') ?? ',');
|
|
1696
|
+
const wrapper = String(formData.get('wrapper') ?? '');
|
|
1697
|
+
const lineBreaks = formData.get('lineBreaks') === 'on';
|
|
1698
|
+
const outputSeparator = lineBreaks ? '\n' : separator;
|
|
1699
|
+
const intent = String(formData.get('intent') ?? 'copy');
|
|
1700
|
+
const isExportIntent = intent === 'export';
|
|
1701
|
+
const result = getCopyColumnResult(state, scope);
|
|
1702
|
+
const hasColumn = (result?.columns ?? []).some(column => String(column) === columnName);
|
|
1703
|
+
|
|
1704
|
+
if (!hasColumn) {
|
|
1705
|
+
setCopyColumnModalError({
|
|
1706
|
+
code: 'COPY_COLUMN_UNAVAILABLE',
|
|
1707
|
+
message: 'Column is no longer available in the current result set.',
|
|
1708
|
+
});
|
|
1709
|
+
showToast('Column could not be copied.', 'alert');
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (!isMarkdownTodoCopyColumnMode(copyMode)) {
|
|
1714
|
+
storeCopyColumnPreferences({ separator, wrapper, lineBreaks });
|
|
1715
|
+
}
|
|
1716
|
+
setCopyColumnModalSubmitting(true);
|
|
1717
|
+
|
|
1718
|
+
const { text, valueCount } = buildCopyColumnText({
|
|
1719
|
+
result,
|
|
1720
|
+
columnName,
|
|
1721
|
+
copyMode,
|
|
1722
|
+
separator: outputSeparator,
|
|
1723
|
+
wrapper,
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
try {
|
|
1727
|
+
if (isExportIntent) {
|
|
1728
|
+
const metadata = getCopyColumnExportMetadata(copyMode);
|
|
1729
|
+
|
|
1730
|
+
downloadTextFile({
|
|
1731
|
+
text,
|
|
1732
|
+
filename: buildCopyColumnExportFilename(columnName, copyMode),
|
|
1733
|
+
mimeType: metadata.mimeType,
|
|
1734
|
+
});
|
|
1735
|
+
closeModal();
|
|
1736
|
+
showToast(
|
|
1737
|
+
`Column "${columnName}" exported as ${metadata.label} · ${formatNumber(valueCount)} ${
|
|
1738
|
+
valueCount === 1 ? 'value' : 'values'
|
|
1739
|
+
}`,
|
|
1740
|
+
'success',
|
|
1741
|
+
);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
if (!navigator.clipboard?.writeText) {
|
|
1746
|
+
throw new Error('Clipboard API is not available.');
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
await navigator.clipboard.writeText(text);
|
|
1750
|
+
closeModal();
|
|
1751
|
+
showToast(
|
|
1752
|
+
`Column "${columnName}" copied · ${formatNumber(valueCount)} ${valueCount === 1 ? 'value' : 'values'}`,
|
|
1753
|
+
'success',
|
|
1754
|
+
);
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
setCopyColumnModalError({
|
|
1757
|
+
code: isExportIntent ? 'COPY_COLUMN_EXPORT_FAILED' : 'CLIPBOARD_ACCESS_FAILED',
|
|
1758
|
+
message: error?.message || (isExportIntent ? 'Column export failed.' : 'Clipboard access failed.'),
|
|
1759
|
+
});
|
|
1760
|
+
showToast(isExportIntent ? 'Column export failed.' : 'Clipboard access failed.', 'alert');
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1294
1764
|
async function handleAction(actionNode) {
|
|
1295
1765
|
const { action } = actionNode.dataset;
|
|
1296
1766
|
|
|
@@ -1304,8 +1774,25 @@ async function handleAction(actionNode) {
|
|
|
1304
1774
|
case 'open-row-editor-url':
|
|
1305
1775
|
openRowEditorUrl(actionNode);
|
|
1306
1776
|
return;
|
|
1777
|
+
case 'copy-row-editor-json':
|
|
1778
|
+
await copyRowEditorJson();
|
|
1779
|
+
return;
|
|
1780
|
+
case 'export-row-editor-json':
|
|
1781
|
+
exportRowEditorJson();
|
|
1782
|
+
return;
|
|
1307
1783
|
case 'open-modal':
|
|
1308
|
-
openModal(actionNode.dataset.modal
|
|
1784
|
+
openModal(actionNode.dataset.modal, {
|
|
1785
|
+
columnId: actionNode.dataset.columnId,
|
|
1786
|
+
columnName: actionNode.dataset.columnName,
|
|
1787
|
+
});
|
|
1788
|
+
return;
|
|
1789
|
+
case 'open-copy-column-modal':
|
|
1790
|
+
closeCopyColumnMenus();
|
|
1791
|
+
openCopyColumnModal({
|
|
1792
|
+
scope: actionNode.dataset.resultScope,
|
|
1793
|
+
columnName: actionNode.dataset.columnName ?? '',
|
|
1794
|
+
mode: actionNode.dataset.copyMode,
|
|
1795
|
+
});
|
|
1309
1796
|
return;
|
|
1310
1797
|
case 'open-create-query-chart-modal':
|
|
1311
1798
|
openCreateQueryChartModal();
|
|
@@ -1329,6 +1816,7 @@ async function handleAction(actionNode) {
|
|
|
1329
1816
|
dismissToast(actionNode.dataset.toastId);
|
|
1330
1817
|
return;
|
|
1331
1818
|
case 'select-connection': {
|
|
1819
|
+
closeSidebarDatabasePickers();
|
|
1332
1820
|
resetStructureGraphForDatabaseChange();
|
|
1333
1821
|
const next = await selectConnection(actionNode.dataset.connectionId);
|
|
1334
1822
|
if (next) {
|
|
@@ -1384,6 +1872,9 @@ async function handleAction(actionNode) {
|
|
|
1384
1872
|
await executeEditorQueryAndNavigate();
|
|
1385
1873
|
return;
|
|
1386
1874
|
}
|
|
1875
|
+
case 'format-current-query':
|
|
1876
|
+
formatCurrentQuery();
|
|
1877
|
+
return;
|
|
1387
1878
|
case 'delete-data-row':
|
|
1388
1879
|
openDeleteDataRowModal(actionNode.dataset.rowIndex);
|
|
1389
1880
|
return;
|
|
@@ -1507,9 +1998,10 @@ async function handleAction(actionNode) {
|
|
|
1507
1998
|
return;
|
|
1508
1999
|
case 'export-query-format': {
|
|
1509
2000
|
const format = actionNode.dataset.exportFormat;
|
|
2001
|
+
const filename = getExportFilenameFromAction(actionNode);
|
|
1510
2002
|
|
|
1511
2003
|
if (format === 'table') {
|
|
1512
|
-
const imported = await duplicateCurrentQueryAsTable();
|
|
2004
|
+
const imported = await duplicateCurrentQueryAsTable(filename);
|
|
1513
2005
|
|
|
1514
2006
|
if (imported) {
|
|
1515
2007
|
router.navigate('/table-designer/new');
|
|
@@ -1518,7 +2010,7 @@ async function handleAction(actionNode) {
|
|
|
1518
2010
|
return;
|
|
1519
2011
|
}
|
|
1520
2012
|
|
|
1521
|
-
await exportCurrentQueryFormat(format);
|
|
2013
|
+
await exportCurrentQueryFormat(format, filename);
|
|
1522
2014
|
return;
|
|
1523
2015
|
}
|
|
1524
2016
|
case 'open-data-export-modal':
|
|
@@ -1526,9 +2018,10 @@ async function handleAction(actionNode) {
|
|
|
1526
2018
|
return;
|
|
1527
2019
|
case 'export-data-format': {
|
|
1528
2020
|
const format = actionNode.dataset.exportFormat;
|
|
2021
|
+
const filename = getExportFilenameFromAction(actionNode);
|
|
1529
2022
|
|
|
1530
2023
|
if (format === 'table') {
|
|
1531
|
-
const imported = await duplicateCurrentDataTableAsTable();
|
|
2024
|
+
const imported = await duplicateCurrentDataTableAsTable(filename);
|
|
1532
2025
|
|
|
1533
2026
|
if (imported) {
|
|
1534
2027
|
router.navigate('/table-designer/new');
|
|
@@ -1537,7 +2030,7 @@ async function handleAction(actionNode) {
|
|
|
1537
2030
|
return;
|
|
1538
2031
|
}
|
|
1539
2032
|
|
|
1540
|
-
await exportCurrentDataTableFormat(format);
|
|
2033
|
+
await exportCurrentDataTableFormat(format, filename);
|
|
1541
2034
|
return;
|
|
1542
2035
|
}
|
|
1543
2036
|
case 'toggle-data-tables':
|
|
@@ -1715,6 +2208,7 @@ async function handleAction(actionNode) {
|
|
|
1715
2208
|
}
|
|
1716
2209
|
return;
|
|
1717
2210
|
case 'reload-data-route':
|
|
2211
|
+
preserveDataRowEditorSelectionForReload();
|
|
1718
2212
|
await refreshCurrentRoute();
|
|
1719
2213
|
return;
|
|
1720
2214
|
default:
|
|
@@ -1735,7 +2229,37 @@ function canApplyMediaTaggingShortcut(state) {
|
|
|
1735
2229
|
}
|
|
1736
2230
|
|
|
1737
2231
|
document.addEventListener('click', event => {
|
|
1738
|
-
const
|
|
2232
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2233
|
+
|
|
2234
|
+
if (!target) {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
const copyColumnMenu = target.closest('[data-copy-column-menu]');
|
|
2239
|
+
|
|
2240
|
+
if (!copyColumnMenu) {
|
|
2241
|
+
closeCopyColumnMenus();
|
|
2242
|
+
} else if (target.closest('.query-result-column-menu__toggle')) {
|
|
2243
|
+
window.requestAnimationFrame(() => {
|
|
2244
|
+
if (copyColumnMenu instanceof HTMLDetailsElement && copyColumnMenu.open) {
|
|
2245
|
+
closeCopyColumnMenus(copyColumnMenu);
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const sidebarDatabasePicker = target.closest('.sidebar-db-picker');
|
|
2251
|
+
|
|
2252
|
+
if (!sidebarDatabasePicker) {
|
|
2253
|
+
closeSidebarDatabasePickers();
|
|
2254
|
+
} else if (target.closest('.sidebar-footer-card')) {
|
|
2255
|
+
window.requestAnimationFrame(() => {
|
|
2256
|
+
if (sidebarDatabasePicker instanceof HTMLDetailsElement && sidebarDatabasePicker.open) {
|
|
2257
|
+
closeSidebarDatabasePickers(sidebarDatabasePicker);
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const actionNode = target.closest('[data-action]');
|
|
1739
2263
|
|
|
1740
2264
|
if (!actionNode) {
|
|
1741
2265
|
return;
|
|
@@ -1744,6 +2268,18 @@ document.addEventListener('click', event => {
|
|
|
1744
2268
|
handleAction(actionNode);
|
|
1745
2269
|
});
|
|
1746
2270
|
|
|
2271
|
+
document.addEventListener('contextmenu', event => {
|
|
2272
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2273
|
+
const headerNode = target?.closest('[data-result-column-header]');
|
|
2274
|
+
|
|
2275
|
+
if (!headerNode) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
event.preventDefault();
|
|
2280
|
+
openCopyColumnMenu(headerNode);
|
|
2281
|
+
});
|
|
2282
|
+
|
|
1747
2283
|
document.addEventListener('keydown', event => {
|
|
1748
2284
|
const target = event.target;
|
|
1749
2285
|
const state = getState();
|
|
@@ -1810,6 +2346,18 @@ document.addEventListener('keydown', event => {
|
|
|
1810
2346
|
return;
|
|
1811
2347
|
}
|
|
1812
2348
|
|
|
2349
|
+
if (document.querySelector('[data-copy-column-menu][open]')) {
|
|
2350
|
+
event.preventDefault();
|
|
2351
|
+
closeCopyColumnMenus();
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
if (document.querySelector('.sidebar-db-picker[open]')) {
|
|
2356
|
+
event.preventDefault();
|
|
2357
|
+
closeSidebarDatabasePickers();
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
1813
2361
|
if (
|
|
1814
2362
|
state.route.name === 'data' &&
|
|
1815
2363
|
(typeof state.dataBrowser.selectedRowIndex === 'number' || Boolean(state.dataBrowser.selectedRow))
|
|
@@ -1826,12 +2374,33 @@ document.addEventListener('keydown', event => {
|
|
|
1826
2374
|
});
|
|
1827
2375
|
|
|
1828
2376
|
document.addEventListener('input', event => {
|
|
2377
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2378
|
+
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2379
|
+
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2380
|
+
|
|
2381
|
+
if (timestampInput) {
|
|
2382
|
+
syncRowEditorTimestampPreview(timestampInput);
|
|
2383
|
+
syncRowEditorFilePathPreview(timestampInput);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
if (textCellInput) {
|
|
2387
|
+
syncRowEditorCharacterCount(textCellInput);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
1829
2390
|
const bindNode = event.target.closest('[data-bind]');
|
|
1830
2391
|
|
|
1831
2392
|
if (!bindNode) {
|
|
1832
2393
|
return;
|
|
1833
2394
|
}
|
|
1834
2395
|
|
|
2396
|
+
if (bindNode.dataset.bind === 'copy-column-format-field') {
|
|
2397
|
+
updateCopyColumnModalFormatField(
|
|
2398
|
+
bindNode.dataset.field,
|
|
2399
|
+
bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox' ? bindNode.checked : bindNode.value,
|
|
2400
|
+
);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
1835
2404
|
if (bindNode.dataset.bind === 'current-query') {
|
|
1836
2405
|
invalidateMainRenderCache();
|
|
1837
2406
|
syncQueryEditorHighlight(bindNode);
|
|
@@ -1873,7 +2442,12 @@ document.addEventListener('input', event => {
|
|
|
1873
2442
|
return;
|
|
1874
2443
|
}
|
|
1875
2444
|
|
|
1876
|
-
updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
|
|
2445
|
+
updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
|
|
2446
|
+
|
|
2447
|
+
if (!syncTableDesignerDraftUi(bindNode)) {
|
|
2448
|
+
renderApp(getState());
|
|
2449
|
+
}
|
|
2450
|
+
|
|
1877
2451
|
return;
|
|
1878
2452
|
}
|
|
1879
2453
|
|
|
@@ -1882,7 +2456,14 @@ document.addEventListener('input', event => {
|
|
|
1882
2456
|
return;
|
|
1883
2457
|
}
|
|
1884
2458
|
|
|
1885
|
-
updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value
|
|
2459
|
+
updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
|
|
2460
|
+
notify: false,
|
|
2461
|
+
});
|
|
2462
|
+
|
|
2463
|
+
if (!syncTableDesignerDraftUi(bindNode)) {
|
|
2464
|
+
renderApp(getState());
|
|
2465
|
+
}
|
|
2466
|
+
|
|
1886
2467
|
return;
|
|
1887
2468
|
}
|
|
1888
2469
|
|
|
@@ -1943,6 +2524,19 @@ document.addEventListener(
|
|
|
1943
2524
|
);
|
|
1944
2525
|
|
|
1945
2526
|
document.addEventListener('change', event => {
|
|
2527
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
2528
|
+
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2529
|
+
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2530
|
+
|
|
2531
|
+
if (timestampInput) {
|
|
2532
|
+
syncRowEditorTimestampPreview(timestampInput);
|
|
2533
|
+
syncRowEditorFilePathPreview(timestampInput);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
if (textCellInput) {
|
|
2537
|
+
syncRowEditorCharacterCount(textCellInput);
|
|
2538
|
+
}
|
|
2539
|
+
|
|
1946
2540
|
const bindNode = event.target.closest('[data-bind]');
|
|
1947
2541
|
|
|
1948
2542
|
if (!bindNode) {
|
|
@@ -2001,7 +2595,12 @@ document.addEventListener('change', event => {
|
|
|
2001
2595
|
return;
|
|
2002
2596
|
}
|
|
2003
2597
|
|
|
2004
|
-
updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value);
|
|
2598
|
+
updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.value, { notify: false });
|
|
2599
|
+
|
|
2600
|
+
if (!syncTableDesignerDraftUi(bindNode)) {
|
|
2601
|
+
renderApp(getState());
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2005
2604
|
return;
|
|
2006
2605
|
}
|
|
2007
2606
|
|
|
@@ -2018,7 +2617,14 @@ document.addEventListener('change', event => {
|
|
|
2018
2617
|
return;
|
|
2019
2618
|
}
|
|
2020
2619
|
|
|
2021
|
-
updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value
|
|
2620
|
+
updateCurrentTableDesignerColumnField(bindNode.dataset.columnId, bindNode.dataset.field, bindNode.value, {
|
|
2621
|
+
notify: false,
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
if (!syncTableDesignerDraftUi(bindNode)) {
|
|
2625
|
+
renderApp(getState());
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2022
2628
|
return;
|
|
2023
2629
|
}
|
|
2024
2630
|
|
|
@@ -2027,6 +2633,16 @@ document.addEventListener('change', event => {
|
|
|
2027
2633
|
return;
|
|
2028
2634
|
}
|
|
2029
2635
|
|
|
2636
|
+
if (bindNode.dataset.bind === 'table-designer-constraint-field') {
|
|
2637
|
+
updateCurrentTableDesignerConstraintField(
|
|
2638
|
+
bindNode.dataset.constraintKind,
|
|
2639
|
+
bindNode.dataset.constraintId,
|
|
2640
|
+
bindNode.dataset.field,
|
|
2641
|
+
bindNode.value,
|
|
2642
|
+
);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2030
2646
|
if (bindNode.dataset.bind === 'query-chart-draft:chartType') {
|
|
2031
2647
|
updateCurrentQueryChartDraftField('chartType', bindNode.value);
|
|
2032
2648
|
return;
|
|
@@ -2057,7 +2673,8 @@ document.addEventListener('submit', async event => {
|
|
|
2057
2673
|
}
|
|
2058
2674
|
|
|
2059
2675
|
event.preventDefault();
|
|
2060
|
-
const
|
|
2676
|
+
const submitter = event.submitter instanceof HTMLElement ? event.submitter : null;
|
|
2677
|
+
const formData = submitter ? new FormData(form, submitter) : new FormData(form);
|
|
2061
2678
|
|
|
2062
2679
|
switch (form.dataset.form) {
|
|
2063
2680
|
case 'open-connection': {
|
|
@@ -2149,6 +2766,9 @@ document.addEventListener('submit', async event => {
|
|
|
2149
2766
|
case 'create-media-tagging-mapping-table':
|
|
2150
2767
|
await submitCreateMediaTaggingMappingTable();
|
|
2151
2768
|
return;
|
|
2769
|
+
case 'copy-column':
|
|
2770
|
+
await submitCopyColumnModal(formData);
|
|
2771
|
+
return;
|
|
2152
2772
|
case 'save-data-row': {
|
|
2153
2773
|
const values = {};
|
|
2154
2774
|
|