sqlite-hub 0.17.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -19
- package/bin/sqlite-hub.js +83 -401
- package/examples/api/queries.js +31 -0
- package/examples/api/rows.js +27 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/frontend/js/api.js +25 -0
- package/frontend/js/app.js +183 -26
- package/frontend/js/components/formControls.js +38 -0
- package/frontend/js/components/modal.js +79 -19
- package/frontend/js/components/queryResults.js +18 -1
- package/frontend/js/components/rowEditorPanel.js +42 -34
- package/frontend/js/store.js +129 -2
- package/frontend/js/utils/jsonPreview.js +31 -0
- package/frontend/js/utils/rowEditorValues.js +41 -0
- package/frontend/js/utils/tableScrollState.js +39 -0
- package/frontend/js/views/charts.js +8 -0
- package/frontend/js/views/data.js +4 -1
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +141 -5
- package/frontend/styles/components.css +6 -0
- package/frontend/styles/tailwind.generated.css +2 -2
- package/package.json +6 -6
- package/server/middleware/apiTokenAuth.js +30 -0
- package/server/middleware/localRequestSecurity.js +84 -0
- package/server/routes/connections.js +35 -1
- package/server/routes/externalApi.js +222 -0
- package/server/routes/settings.js +64 -4
- package/server/server.js +34 -2
- package/server/services/apiTokenService.js +101 -0
- package/server/services/databaseCommandService.js +399 -0
- package/server/services/nativeFileDialogService.js +260 -0
- package/server/services/sqlite/dataBrowserService.js +0 -4
- package/server/services/sqlite/exportService.js +5 -1
- package/server/services/sqlite/sqlExecutor.js +51 -2
- package/server/services/storage/appStateStore.js +113 -0
- package/server/utils/errors.js +7 -0
- package/server/utils/sqliteTypes.js +17 -8
- package/tests/api-token-auth.test.js +127 -0
- package/tests/cli-service-delegation.test.js +43 -0
- package/tests/connections-file-dialog-route.test.js +89 -0
- package/tests/copy-column-modal.test.js +48 -0
- package/tests/database-command-service.test.js +102 -0
- package/tests/export-blob.test.js +46 -0
- package/tests/form-controls.test.js +34 -0
- package/tests/json-preview.test.js +49 -0
- package/tests/local-request-security.test.js +85 -0
- package/tests/native-file-dialog.test.js +105 -0
- package/tests/query-results-truncation.test.js +20 -0
- package/tests/row-editor-null-values.test.js +131 -0
- package/tests/settings-api-tokens-route.test.js +76 -0
- package/tests/settings-view.test.js +47 -0
- package/tests/sql-identifier-safety.test.js +9 -3
- package/tests/sql-result-limit.test.js +66 -0
- package/tests/table-scroll-state.test.js +70 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/frontend/js/api.js
CHANGED
|
@@ -86,6 +86,18 @@ export function createConnection(payload) {
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
export function chooseOpenDatabasePath() {
|
|
90
|
+
return request("/api/connections/choose-open-path", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function chooseCreateDatabasePath() {
|
|
96
|
+
return request("/api/connections/choose-create-path", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
export function importSql(payload) {
|
|
90
102
|
return request("/api/connections/import-sql", {
|
|
91
103
|
method: "POST",
|
|
@@ -419,6 +431,19 @@ export function patchSettings(settings) {
|
|
|
419
431
|
});
|
|
420
432
|
}
|
|
421
433
|
|
|
434
|
+
export function createApiToken(name) {
|
|
435
|
+
return request("/api/settings/api-tokens", {
|
|
436
|
+
method: "POST",
|
|
437
|
+
body: { name },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function deleteApiToken(tokenId) {
|
|
442
|
+
return request(`/api/settings/api-tokens/${encodeURIComponent(tokenId)}`, {
|
|
443
|
+
method: "DELETE",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
422
447
|
const TEXT_EXPORT_EXTENSIONS = {
|
|
423
448
|
csv: "csv",
|
|
424
449
|
tsv: "tsv",
|
package/frontend/js/app.js
CHANGED
|
@@ -24,6 +24,9 @@ import { renderTopNav } from './components/topNav.js';
|
|
|
24
24
|
import { createRouter } from './router.js';
|
|
25
25
|
import {
|
|
26
26
|
createActiveConnectionBackup,
|
|
27
|
+
createSettingsApiToken,
|
|
28
|
+
chooseOpenDatabasePath,
|
|
29
|
+
chooseCreateDatabasePath,
|
|
27
30
|
createDocument,
|
|
28
31
|
createDocumentFromMarkdownExport,
|
|
29
32
|
clearCurrentQuery,
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
openDeleteDataRowModal,
|
|
49
52
|
openDeleteEditorRowModal,
|
|
50
53
|
openDeleteQueryHistoryModal,
|
|
54
|
+
openDeleteSettingsApiTokenModal,
|
|
51
55
|
openDeleteQueryChartModal,
|
|
52
56
|
openDataExportModal,
|
|
53
57
|
openDeleteDocumentModal,
|
|
@@ -102,6 +106,7 @@ import {
|
|
|
102
106
|
submitCreateMediaTaggingTagTable,
|
|
103
107
|
submitCreateMediaTaggingMappingTable,
|
|
104
108
|
submitDeleteQueryHistoryConfirmation,
|
|
109
|
+
submitDeleteSettingsApiTokenConfirmation,
|
|
105
110
|
submitRowUpdatePreviewConfirmation,
|
|
106
111
|
setQueryHistoryPanelVisibility,
|
|
107
112
|
sortDataTableByColumn,
|
|
@@ -112,6 +117,7 @@ import {
|
|
|
112
117
|
setCopyColumnModalEditedText,
|
|
113
118
|
setCopyColumnModalSubmitting,
|
|
114
119
|
setRoute,
|
|
120
|
+
setSettingsSection,
|
|
115
121
|
saveQueryHistoryNotes,
|
|
116
122
|
saveQueryHistoryTitle,
|
|
117
123
|
saveCurrentTableDesignerDraft,
|
|
@@ -178,10 +184,19 @@ import {
|
|
|
178
184
|
stringifyRowEditorJson,
|
|
179
185
|
} from './utils/rowEditorJson.js';
|
|
180
186
|
import { getTimestampPreviewForField } from './utils/timestampPreview.js';
|
|
187
|
+
import {
|
|
188
|
+
buildRowEditorSubmittedValues,
|
|
189
|
+
getRowEditorValueState,
|
|
190
|
+
getRowEditorValueStateLabel,
|
|
191
|
+
} from './utils/rowEditorValues.js';
|
|
181
192
|
import {
|
|
182
193
|
formatTextCellCharacterCount,
|
|
183
194
|
getTextCellCharacterCount,
|
|
184
195
|
} from './utils/textCellStats.js';
|
|
196
|
+
import {
|
|
197
|
+
captureTableHorizontalScrollState,
|
|
198
|
+
restoreTableHorizontalScrollState,
|
|
199
|
+
} from './utils/tableScrollState.js';
|
|
185
200
|
|
|
186
201
|
const appRoot = document.querySelector('#app');
|
|
187
202
|
|
|
@@ -1257,6 +1272,10 @@ function renderApp(state) {
|
|
|
1257
1272
|
|
|
1258
1273
|
const focusedInput = captureFocusedInputState();
|
|
1259
1274
|
const queryHistoryScrollState = captureQueryHistoryScrollState();
|
|
1275
|
+
const tableHorizontalScrollState = captureTableHorizontalScrollState({
|
|
1276
|
+
routeName: lastRenderedRouteName,
|
|
1277
|
+
scrollNodes: shellRefs.view.querySelectorAll('[data-table-horizontal-scroll]'),
|
|
1278
|
+
});
|
|
1260
1279
|
const isEnteringNewTableDesignerRoute =
|
|
1261
1280
|
state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
|
|
1262
1281
|
|
|
@@ -1329,6 +1348,14 @@ function renderApp(state) {
|
|
|
1329
1348
|
restoreQueryHistoryScrollState(queryHistoryScrollState);
|
|
1330
1349
|
}
|
|
1331
1350
|
|
|
1351
|
+
if (mainChanged && !mainPatched) {
|
|
1352
|
+
restoreTableHorizontalScrollState({
|
|
1353
|
+
snapshot: tableHorizontalScrollState,
|
|
1354
|
+
routeName: state.route.name,
|
|
1355
|
+
scrollNodes: shellRefs.view.querySelectorAll('[data-table-horizontal-scroll]'),
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1332
1359
|
if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
|
|
1333
1360
|
if (focusQueryEditorInput()) {
|
|
1334
1361
|
pendingQueryEditorFocus = false;
|
|
@@ -1572,6 +1599,53 @@ function syncRowEditorFilePathPreview(inputNode) {
|
|
|
1572
1599
|
previewNode.hidden = false;
|
|
1573
1600
|
}
|
|
1574
1601
|
|
|
1602
|
+
function getRowEditorValueStateClassName(state) {
|
|
1603
|
+
if (state === 'null') {
|
|
1604
|
+
return 'border px-2 py-1 text-[9px] border-primary-container/35 bg-primary-container/15 text-primary-container';
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (state === 'empty') {
|
|
1608
|
+
return 'border px-2 py-1 text-[9px] border-outline-variant/35 bg-surface-container-high text-on-surface-variant';
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return 'border px-2 py-1 text-[9px] border-outline-variant/20 bg-surface-container text-on-surface-variant';
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function syncRowEditorValueState(controlNode) {
|
|
1615
|
+
const fieldNode = controlNode.closest('[data-row-editor-field]');
|
|
1616
|
+
const valueInput = fieldNode?.querySelector('[data-row-editor-value-source]');
|
|
1617
|
+
const stateBadge = fieldNode?.querySelector('[data-row-editor-value-state]');
|
|
1618
|
+
|
|
1619
|
+
if (!fieldNode || !valueInput || !stateBadge) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const valueState = getRowEditorValueState(valueInput.value);
|
|
1624
|
+
|
|
1625
|
+
valueInput.dataset.rowEditorDirty = 'true';
|
|
1626
|
+
stateBadge.dataset.valueState = valueState;
|
|
1627
|
+
stateBadge.className = getRowEditorValueStateClassName(valueState);
|
|
1628
|
+
stateBadge.textContent = getRowEditorValueStateLabel(valueState);
|
|
1629
|
+
|
|
1630
|
+
syncRowEditorTimestampPreview(valueInput);
|
|
1631
|
+
syncRowEditorFilePathPreview(valueInput);
|
|
1632
|
+
syncRowEditorCharacterCount(valueInput);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function getRowEditorFieldMetadata(form) {
|
|
1636
|
+
return Object.fromEntries(
|
|
1637
|
+
Array.from(form.elements)
|
|
1638
|
+
.filter(element => element.name?.startsWith('field:'))
|
|
1639
|
+
.map(element => [
|
|
1640
|
+
element.name.slice('field:'.length),
|
|
1641
|
+
{
|
|
1642
|
+
initialState: element.closest('[data-row-editor-field]')?.dataset?.rowEditorInitialState,
|
|
1643
|
+
dirty: element.dataset.rowEditorDirty === 'true',
|
|
1644
|
+
},
|
|
1645
|
+
]),
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1575
1649
|
function closeCopyColumnMenus(exceptMenu = null) {
|
|
1576
1650
|
document.querySelectorAll('[data-copy-column-menu][open]').forEach(menu => {
|
|
1577
1651
|
if (menu !== exceptMenu && menu instanceof HTMLDetailsElement) {
|
|
@@ -1732,21 +1806,24 @@ function applyChangedRowEditorFormValues(rowObject) {
|
|
|
1732
1806
|
|
|
1733
1807
|
const nextRowObject = { ...rowObject };
|
|
1734
1808
|
const formData = new FormData(form);
|
|
1809
|
+
const submittedValues = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
1735
1810
|
|
|
1736
|
-
for (const [
|
|
1737
|
-
|
|
1738
|
-
continue;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1811
|
+
for (const [fieldName, value] of Object.entries(submittedValues)) {
|
|
1812
|
+
const key = `field:${fieldName}`;
|
|
1741
1813
|
const control = Array.from(form.elements).find(element => element.name === key);
|
|
1742
1814
|
const initialValue = control?.dataset?.rowEditorInitialValue;
|
|
1743
|
-
const
|
|
1815
|
+
const initialState = control?.closest('[data-row-editor-field]')?.dataset?.rowEditorInitialState;
|
|
1816
|
+
const currentState = getRowEditorValueState(value);
|
|
1817
|
+
const currentValue = value === null ? null : String(value);
|
|
1744
1818
|
|
|
1745
|
-
if (
|
|
1819
|
+
if (
|
|
1820
|
+
initialState === currentState &&
|
|
1821
|
+
(currentState === 'null' || (initialValue !== undefined && currentValue === initialValue))
|
|
1822
|
+
) {
|
|
1746
1823
|
continue;
|
|
1747
1824
|
}
|
|
1748
1825
|
|
|
1749
|
-
nextRowObject[
|
|
1826
|
+
nextRowObject[fieldName] = currentValue;
|
|
1750
1827
|
}
|
|
1751
1828
|
|
|
1752
1829
|
return nextRowObject;
|
|
@@ -2019,6 +2096,32 @@ async function handleAction(actionNode) {
|
|
|
2019
2096
|
case 'refresh-view':
|
|
2020
2097
|
await refreshCurrentRoute();
|
|
2021
2098
|
return;
|
|
2099
|
+
case 'set-settings-section':
|
|
2100
|
+
setSettingsSection(actionNode.dataset.section);
|
|
2101
|
+
return;
|
|
2102
|
+
case 'open-delete-api-token-modal':
|
|
2103
|
+
openDeleteSettingsApiTokenModal(actionNode.dataset.tokenId);
|
|
2104
|
+
return;
|
|
2105
|
+
case 'copy-created-api-token': {
|
|
2106
|
+
const tokenInput = document.querySelector('[data-created-api-token]');
|
|
2107
|
+
const token = tokenInput instanceof HTMLInputElement ? tokenInput.value : '';
|
|
2108
|
+
|
|
2109
|
+
if (token && navigator.clipboard?.writeText) {
|
|
2110
|
+
await navigator.clipboard.writeText(token);
|
|
2111
|
+
showToast('API token copied.', 'success');
|
|
2112
|
+
}
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
case 'copy-database-id': {
|
|
2116
|
+
const databaseIdNode = document.querySelector('[data-database-id]');
|
|
2117
|
+
const databaseId = databaseIdNode?.textContent?.trim() ?? '';
|
|
2118
|
+
|
|
2119
|
+
if (databaseId && navigator.clipboard?.writeText) {
|
|
2120
|
+
await navigator.clipboard.writeText(databaseId);
|
|
2121
|
+
showToast('Database ID copied.', 'success');
|
|
2122
|
+
}
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2022
2125
|
case 'open-row-editor-url':
|
|
2023
2126
|
openRowEditorUrl(actionNode);
|
|
2024
2127
|
return;
|
|
@@ -2109,6 +2212,60 @@ async function handleAction(actionNode) {
|
|
|
2109
2212
|
case 'edit-connection':
|
|
2110
2213
|
openEditConnectionModal(actionNode.dataset.connectionId);
|
|
2111
2214
|
return;
|
|
2215
|
+
case 'choose-open-database-path': {
|
|
2216
|
+
const labelNode = actionNode.querySelector('[data-open-database-path-button-label]');
|
|
2217
|
+
|
|
2218
|
+
actionNode.setAttribute('disabled', '');
|
|
2219
|
+
if (labelNode) {
|
|
2220
|
+
labelNode.textContent = 'Choosing...';
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const selectedPath = await chooseOpenDatabasePath();
|
|
2224
|
+
const pathInput = document.querySelector(
|
|
2225
|
+
'[data-form="open-connection"] [data-open-database-path]',
|
|
2226
|
+
);
|
|
2227
|
+
|
|
2228
|
+
if (selectedPath && pathInput instanceof HTMLInputElement) {
|
|
2229
|
+
pathInput.value = selectedPath;
|
|
2230
|
+
pathInput.focus({ preventScroll: true });
|
|
2231
|
+
pathInput.setSelectionRange(pathInput.value.length, pathInput.value.length);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
if (actionNode.isConnected) {
|
|
2235
|
+
actionNode.removeAttribute('disabled');
|
|
2236
|
+
if (labelNode) {
|
|
2237
|
+
labelNode.textContent = 'Browse...';
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
case 'choose-create-database-path': {
|
|
2243
|
+
const labelNode = actionNode.querySelector('[data-create-database-path-button-label]');
|
|
2244
|
+
|
|
2245
|
+
actionNode.setAttribute('disabled', '');
|
|
2246
|
+
if (labelNode) {
|
|
2247
|
+
labelNode.textContent = 'Choosing...';
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const selectedPath = await chooseCreateDatabasePath();
|
|
2251
|
+
const pathInput = document.querySelector(
|
|
2252
|
+
'[data-form="create-connection"] [data-create-database-path]',
|
|
2253
|
+
);
|
|
2254
|
+
|
|
2255
|
+
if (selectedPath && pathInput instanceof HTMLInputElement) {
|
|
2256
|
+
pathInput.value = selectedPath;
|
|
2257
|
+
pathInput.focus({ preventScroll: true });
|
|
2258
|
+
pathInput.setSelectionRange(pathInput.value.length, pathInput.value.length);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
if (actionNode.isConnected) {
|
|
2262
|
+
actionNode.removeAttribute('disabled');
|
|
2263
|
+
if (labelNode) {
|
|
2264
|
+
labelNode.textContent = 'Browse...';
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2112
2269
|
case 'close-modal':
|
|
2113
2270
|
closeModal();
|
|
2114
2271
|
return;
|
|
@@ -2689,6 +2846,7 @@ document.addEventListener('keydown', event => {
|
|
|
2689
2846
|
|
|
2690
2847
|
document.addEventListener('input', event => {
|
|
2691
2848
|
const target = event.target instanceof Element ? event.target : null;
|
|
2849
|
+
const valueInput = target?.closest('[data-row-editor-value-source]');
|
|
2692
2850
|
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2693
2851
|
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2694
2852
|
|
|
@@ -2701,6 +2859,10 @@ document.addEventListener('input', event => {
|
|
|
2701
2859
|
syncRowEditorCharacterCount(textCellInput);
|
|
2702
2860
|
}
|
|
2703
2861
|
|
|
2862
|
+
if (valueInput) {
|
|
2863
|
+
syncRowEditorValueState(valueInput);
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2704
2866
|
const bindNode = event.target.closest('[data-bind]');
|
|
2705
2867
|
|
|
2706
2868
|
if (!bindNode) {
|
|
@@ -2845,6 +3007,7 @@ document.addEventListener(
|
|
|
2845
3007
|
|
|
2846
3008
|
document.addEventListener('change', event => {
|
|
2847
3009
|
const target = event.target instanceof Element ? event.target : null;
|
|
3010
|
+
const valueControl = target?.closest('[data-row-editor-value-source]');
|
|
2848
3011
|
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2849
3012
|
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2850
3013
|
|
|
@@ -2857,6 +3020,10 @@ document.addEventListener('change', event => {
|
|
|
2857
3020
|
syncRowEditorCharacterCount(textCellInput);
|
|
2858
3021
|
}
|
|
2859
3022
|
|
|
3023
|
+
if (valueControl) {
|
|
3024
|
+
syncRowEditorValueState(valueControl);
|
|
3025
|
+
}
|
|
3026
|
+
|
|
2860
3027
|
const bindNode = event.target.closest('[data-bind]');
|
|
2861
3028
|
|
|
2862
3029
|
if (!bindNode) {
|
|
@@ -3007,6 +3174,9 @@ document.addEventListener('submit', async event => {
|
|
|
3007
3174
|
const formData = submitter ? new FormData(form, submitter) : new FormData(form);
|
|
3008
3175
|
|
|
3009
3176
|
switch (form.dataset.form) {
|
|
3177
|
+
case 'create-api-token':
|
|
3178
|
+
await createSettingsApiToken(String(formData.get('name') ?? ''));
|
|
3179
|
+
return;
|
|
3010
3180
|
case 'open-connection': {
|
|
3011
3181
|
resetStructureGraphForDatabaseChange();
|
|
3012
3182
|
const connection = await submitOpenConnection({
|
|
@@ -3097,6 +3267,9 @@ document.addEventListener('submit', async event => {
|
|
|
3097
3267
|
}
|
|
3098
3268
|
return;
|
|
3099
3269
|
}
|
|
3270
|
+
case 'delete-api-token-confirm':
|
|
3271
|
+
await submitDeleteSettingsApiTokenConfirmation();
|
|
3272
|
+
return;
|
|
3100
3273
|
case 'document-insert-table': {
|
|
3101
3274
|
const inserted = await submitDocumentInsertTable();
|
|
3102
3275
|
|
|
@@ -3126,15 +3299,7 @@ document.addEventListener('submit', async event => {
|
|
|
3126
3299
|
await submitCopyColumnModal(formData);
|
|
3127
3300
|
return;
|
|
3128
3301
|
case 'save-data-row': {
|
|
3129
|
-
const values =
|
|
3130
|
-
|
|
3131
|
-
for (const [key, value] of formData.entries()) {
|
|
3132
|
-
if (!key.startsWith('field:')) {
|
|
3133
|
-
continue;
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
values[key.slice('field:'.length)] = String(value ?? '');
|
|
3137
|
-
}
|
|
3302
|
+
const values = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
3138
3303
|
|
|
3139
3304
|
let rowIdentity = null;
|
|
3140
3305
|
const rawRowIdentity = String(formData.get('rowIdentity') ?? '').trim();
|
|
@@ -3151,15 +3316,7 @@ document.addEventListener('submit', async event => {
|
|
|
3151
3316
|
return;
|
|
3152
3317
|
}
|
|
3153
3318
|
case 'save-editor-row': {
|
|
3154
|
-
const values =
|
|
3155
|
-
|
|
3156
|
-
for (const [key, value] of formData.entries()) {
|
|
3157
|
-
if (!key.startsWith('field:')) {
|
|
3158
|
-
continue;
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
values[key.slice('field:'.length)] = String(value ?? '');
|
|
3162
|
-
}
|
|
3319
|
+
const values = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
3163
3320
|
|
|
3164
3321
|
await openEditorRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values);
|
|
3165
3322
|
return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { escapeHtml } from '../utils/format.js';
|
|
2
|
+
|
|
3
|
+
export const STANDARD_TEXT_INPUT_CLASS =
|
|
4
|
+
'control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors placeholder:text-on-surface-variant/35 focus:border-primary-container';
|
|
5
|
+
|
|
6
|
+
export function renderTextInput({
|
|
7
|
+
className = '',
|
|
8
|
+
dataAttributes = {},
|
|
9
|
+
disabled = false,
|
|
10
|
+
maxlength = null,
|
|
11
|
+
name = '',
|
|
12
|
+
placeholder = '',
|
|
13
|
+
readonly = false,
|
|
14
|
+
spellcheck = null,
|
|
15
|
+
type = 'text',
|
|
16
|
+
value = '',
|
|
17
|
+
} = {}) {
|
|
18
|
+
const attributes = [
|
|
19
|
+
`class="${escapeHtml([STANDARD_TEXT_INPUT_CLASS, className].filter(Boolean).join(' '))}"`,
|
|
20
|
+
name ? `name="${escapeHtml(name)}"` : '',
|
|
21
|
+
placeholder ? `placeholder="${escapeHtml(placeholder)}"` : '',
|
|
22
|
+
`type="${escapeHtml(type)}"`,
|
|
23
|
+
`value="${escapeHtml(value)}"`,
|
|
24
|
+
Number.isInteger(maxlength) ? `maxlength="${maxlength}"` : '',
|
|
25
|
+
disabled ? 'disabled' : '',
|
|
26
|
+
readonly ? 'readonly' : '',
|
|
27
|
+
typeof spellcheck === 'boolean' ? `spellcheck="${spellcheck}"` : '',
|
|
28
|
+
...Object.entries(dataAttributes).map(([name, attributeValue]) => {
|
|
29
|
+
const attributeName = `data-${String(name).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
|
|
30
|
+
|
|
31
|
+
return attributeValue === true
|
|
32
|
+
? attributeName
|
|
33
|
+
: `${attributeName}="${escapeHtml(attributeValue)}"`;
|
|
34
|
+
}),
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
|
|
37
|
+
return `<input ${attributes.join(' ')} />`;
|
|
38
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isMarkdownTodoCopyColumnMode,
|
|
8
8
|
} from "../utils/copyColumnExport.js";
|
|
9
9
|
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
10
|
+
import { renderTextInput } from "./formControls.js";
|
|
10
11
|
import {
|
|
11
12
|
analyzeQueryChartResult,
|
|
12
13
|
getQueryChartTypeLabel,
|
|
@@ -27,13 +28,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
|
|
|
27
28
|
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
28
29
|
${escapeHtml(label)}
|
|
29
30
|
</span>
|
|
30
|
-
|
|
31
|
-
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
32
|
-
name="${escapeHtml(name)}"
|
|
33
|
-
placeholder="${escapeHtml(placeholder)}"
|
|
34
|
-
type="${escapeHtml(type)}"
|
|
35
|
-
value="${escapeHtml(value)}"
|
|
36
|
-
/>
|
|
31
|
+
${renderTextInput({ name, placeholder, type, value })}
|
|
37
32
|
</label>
|
|
38
33
|
`;
|
|
39
34
|
}
|
|
@@ -152,14 +147,33 @@ function renderError(error) {
|
|
|
152
147
|
`;
|
|
153
148
|
}
|
|
154
149
|
|
|
155
|
-
function renderOpenConnectionForm(modal) {
|
|
150
|
+
export function renderOpenConnectionForm(modal) {
|
|
156
151
|
return `
|
|
157
152
|
<form class="space-y-5" data-form="open-connection">
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
<label class="block space-y-2">
|
|
154
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
155
|
+
SQLite File Path
|
|
156
|
+
</span>
|
|
157
|
+
<span class="flex items-stretch gap-2">
|
|
158
|
+
${renderTextInput({
|
|
159
|
+
className: "min-w-0 flex-1",
|
|
160
|
+
dataAttributes: { openDatabasePath: true },
|
|
161
|
+
name: "path",
|
|
162
|
+
placeholder: "/absolute/path/to/database.sqlite",
|
|
163
|
+
})}
|
|
164
|
+
<button
|
|
165
|
+
class="standard-button flex-none"
|
|
166
|
+
data-action="choose-open-database-path"
|
|
167
|
+
type="button"
|
|
168
|
+
>
|
|
169
|
+
<span class="material-symbols-outlined text-sm">folder_open</span>
|
|
170
|
+
<span data-open-database-path-button-label>Browse...</span>
|
|
171
|
+
</button>
|
|
172
|
+
</span>
|
|
173
|
+
<span class="block text-[11px] leading-5 text-on-surface-variant/60">
|
|
174
|
+
Choose an existing SQLite file, or enter an absolute path manually.
|
|
175
|
+
</span>
|
|
176
|
+
</label>
|
|
163
177
|
${renderField({
|
|
164
178
|
label: "Label",
|
|
165
179
|
name: "label",
|
|
@@ -264,14 +278,33 @@ function renderEditConnectionForm(modal) {
|
|
|
264
278
|
`;
|
|
265
279
|
}
|
|
266
280
|
|
|
267
|
-
function renderCreateDatabaseForm(modal) {
|
|
281
|
+
export function renderCreateDatabaseForm(modal) {
|
|
268
282
|
return `
|
|
269
283
|
<form class="space-y-5" data-form="create-connection">
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
284
|
+
<label class="block space-y-2">
|
|
285
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
286
|
+
New SQLite File Path
|
|
287
|
+
</span>
|
|
288
|
+
<span class="flex items-stretch gap-2">
|
|
289
|
+
${renderTextInput({
|
|
290
|
+
className: "min-w-0 flex-1",
|
|
291
|
+
dataAttributes: { createDatabasePath: true },
|
|
292
|
+
name: "path",
|
|
293
|
+
placeholder: "/absolute/path/to/new-database.sqlite",
|
|
294
|
+
})}
|
|
295
|
+
<button
|
|
296
|
+
class="standard-button flex-none"
|
|
297
|
+
data-action="choose-create-database-path"
|
|
298
|
+
type="button"
|
|
299
|
+
>
|
|
300
|
+
<span class="material-symbols-outlined text-sm">folder_open</span>
|
|
301
|
+
<span data-create-database-path-button-label>Browse...</span>
|
|
302
|
+
</button>
|
|
303
|
+
</span>
|
|
304
|
+
<span class="block text-[11px] leading-5 text-on-surface-variant/60">
|
|
305
|
+
Choose a folder and filename, or enter an absolute path manually.
|
|
306
|
+
</span>
|
|
307
|
+
</label>
|
|
275
308
|
${renderField({
|
|
276
309
|
label: "Label",
|
|
277
310
|
name: "label",
|
|
@@ -827,6 +860,28 @@ function renderDeleteDocumentForm(modal) {
|
|
|
827
860
|
].join("");
|
|
828
861
|
}
|
|
829
862
|
|
|
863
|
+
export function renderDeleteApiTokenForm(modal) {
|
|
864
|
+
return [
|
|
865
|
+
'<form class="space-y-5" data-form="delete-api-token-confirm"><div class="space-y-3">',
|
|
866
|
+
'<p class="text-sm leading-7 text-on-surface">Delete API token <span class="font-bold text-primary-container">',
|
|
867
|
+
escapeHtml(modal.tokenName ?? "API token"),
|
|
868
|
+
"</span>?</p>",
|
|
869
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">Requests using this token will immediately lose access to <span class="font-semibold text-on-surface">',
|
|
870
|
+
escapeHtml(modal.databaseLabel ?? "the active database"),
|
|
871
|
+
".</span></p>",
|
|
872
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-[10px] text-on-surface-variant/55">',
|
|
873
|
+
escapeHtml(modal.tokenPrefix ?? ""),
|
|
874
|
+
"...</div>",
|
|
875
|
+
"</div>",
|
|
876
|
+
renderError(modal.error),
|
|
877
|
+
'<div class="flex items-center justify-between gap-3 pt-2">',
|
|
878
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
879
|
+
'<button class="delete-button" type="submit">',
|
|
880
|
+
modal.submitting ? "Deleting..." : "Delete Token",
|
|
881
|
+
"</button></div></form>",
|
|
882
|
+
].join("");
|
|
883
|
+
}
|
|
884
|
+
|
|
830
885
|
function getDocumentInsertQueryTitle(query) {
|
|
831
886
|
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
|
|
832
887
|
}
|
|
@@ -1667,6 +1722,11 @@ export function renderModal(state) {
|
|
|
1667
1722
|
title: "Delete Document",
|
|
1668
1723
|
body: renderDeleteDocumentForm(modal),
|
|
1669
1724
|
},
|
|
1725
|
+
"delete-api-token": {
|
|
1726
|
+
eyebrow: "Settings // Confirm token deletion",
|
|
1727
|
+
title: "Delete API Token",
|
|
1728
|
+
body: renderDeleteApiTokenForm(modal),
|
|
1729
|
+
},
|
|
1670
1730
|
"document-insert-table": {
|
|
1671
1731
|
eyebrow: "Documents // Saved query output",
|
|
1672
1732
|
title: "Insert Table",
|
|
@@ -95,6 +95,7 @@ export function renderQueryResultsPane(
|
|
|
95
95
|
sortDirection = null,
|
|
96
96
|
resultScope = "editor",
|
|
97
97
|
sortAction = "sort-editor-results-column",
|
|
98
|
+
scrollKey = "",
|
|
98
99
|
} = {}
|
|
99
100
|
) {
|
|
100
101
|
if (!result) {
|
|
@@ -124,7 +125,23 @@ export function renderQueryResultsPane(
|
|
|
124
125
|
|
|
125
126
|
return `
|
|
126
127
|
<div class="relative flex h-full min-h-0 flex-col overflow-hidden bg-surface-container">
|
|
127
|
-
|
|
128
|
+
${
|
|
129
|
+
result.truncated
|
|
130
|
+
? `
|
|
131
|
+
<div class="border-b border-primary-container/20 bg-primary-container/10 px-4 py-2 text-xs text-on-surface">
|
|
132
|
+
Showing the first ${escapeHtml(String(result.rowLimit ?? result.rows?.length ?? 0))} rows. Refine the query or export it to process the complete result set.
|
|
133
|
+
</div>
|
|
134
|
+
`
|
|
135
|
+
: ""
|
|
136
|
+
}
|
|
137
|
+
<div
|
|
138
|
+
class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest"
|
|
139
|
+
${
|
|
140
|
+
scrollKey
|
|
141
|
+
? `data-table-horizontal-scroll data-table-scroll-key="${escapeHtml(scrollKey)}"`
|
|
142
|
+
: ""
|
|
143
|
+
}
|
|
144
|
+
>
|
|
128
145
|
${
|
|
129
146
|
result.columns?.length
|
|
130
147
|
? renderDataGrid({
|