sqlite-hub 0.16.0 → 0.17.2
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 +106 -19
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +481 -32
- package/frontend/js/components/modal.js +270 -50
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +18 -1
- package/frontend/js/components/rowEditorPanel.js +42 -34
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +655 -2
- package/frontend/js/utils/jsonPreview.js +31 -0
- package/frontend/js/utils/markdownDocuments.js +248 -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/documents.js +300 -0
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +39 -3
- package/frontend/styles/components.css +19 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +2 -1
- package/server/middleware/localRequestSecurity.js +84 -0
- package/server/routes/connections.js +18 -1
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +18 -1
- package/server/services/nativeFileDialogService.js +168 -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 +313 -0
- package/server/utils/sqliteTypes.js +17 -8
- package/tests/cli-args.test.js +100 -0
- package/tests/connections-file-dialog-route.test.js +46 -0
- package/tests/copy-column-modal.test.js +97 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-blob.test.js +46 -0
- package/tests/json-preview.test.js +49 -0
- package/tests/local-request-security.test.js +85 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/native-file-dialog.test.js +78 -0
- package/tests/query-results-truncation.test.js +20 -0
- package/tests/row-editor-null-values.test.js +131 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -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/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
|
+
chooseCreateDatabasePath,
|
|
28
|
+
createDocument,
|
|
29
|
+
createDocumentFromMarkdownExport,
|
|
27
30
|
clearCurrentQuery,
|
|
28
31
|
clearDataRowSelection,
|
|
29
32
|
clearEditorRowSelection,
|
|
@@ -48,12 +51,15 @@ import {
|
|
|
48
51
|
openDeleteQueryHistoryModal,
|
|
49
52
|
openDeleteQueryChartModal,
|
|
50
53
|
openDataExportModal,
|
|
54
|
+
openDeleteDocumentModal,
|
|
51
55
|
openQueryExportModal,
|
|
52
56
|
openDataRowByIdentity,
|
|
53
57
|
openEditConnectionModal,
|
|
54
58
|
openCreateQueryChartModal,
|
|
55
59
|
openCopyColumnModal,
|
|
56
60
|
openEditQueryChartModal,
|
|
61
|
+
openDocumentInsertNoteModal,
|
|
62
|
+
openDocumentInsertTableModal,
|
|
57
63
|
preserveCurrentDataRowSelectionForReload,
|
|
58
64
|
openDataRowUpdatePreview,
|
|
59
65
|
openEditorRowUpdatePreview,
|
|
@@ -66,6 +72,7 @@ import {
|
|
|
66
72
|
runQueryHistoryItem,
|
|
67
73
|
skipCurrentMediaTaggingItem,
|
|
68
74
|
saveCurrentQueryChartDraft,
|
|
75
|
+
saveCurrentDocument,
|
|
69
76
|
saveCurrentMediaTaggingConfig,
|
|
70
77
|
selectDataRow,
|
|
71
78
|
selectEditorRow,
|
|
@@ -90,6 +97,9 @@ import {
|
|
|
90
97
|
setEditorPanelVisibility,
|
|
91
98
|
setEditorTab,
|
|
92
99
|
submitDeleteChartConfirmation,
|
|
100
|
+
submitDeleteDocumentConfirmation,
|
|
101
|
+
submitDocumentInsertNote,
|
|
102
|
+
submitDocumentInsertTable,
|
|
93
103
|
submitCreateMediaTaggingTagTable,
|
|
94
104
|
submitCreateMediaTaggingMappingTable,
|
|
95
105
|
submitDeleteQueryHistoryConfirmation,
|
|
@@ -100,6 +110,7 @@ import {
|
|
|
100
110
|
setQueryHistorySearchInput,
|
|
101
111
|
setQueryHistoryTab,
|
|
102
112
|
setCopyColumnModalError,
|
|
113
|
+
setCopyColumnModalEditedText,
|
|
103
114
|
setCopyColumnModalSubmitting,
|
|
104
115
|
setRoute,
|
|
105
116
|
saveQueryHistoryNotes,
|
|
@@ -107,6 +118,8 @@ import {
|
|
|
107
118
|
saveCurrentTableDesignerDraft,
|
|
108
119
|
toggleChartsResultsPanel,
|
|
109
120
|
toggleChartsSqlPanel,
|
|
121
|
+
toggleCurrentDocumentTodo,
|
|
122
|
+
toggleDocumentsPane,
|
|
110
123
|
setMediaTaggingWorkflowMediaDetailsVisible,
|
|
111
124
|
setMediaTaggingWorkflowMediaRotationDegrees,
|
|
112
125
|
queueTableDesignerCsvImport,
|
|
@@ -124,6 +137,8 @@ import {
|
|
|
124
137
|
updateCurrentMediaTaggingField,
|
|
125
138
|
updateCurrentMediaTaggingTagFormField,
|
|
126
139
|
updateCopyColumnModalFormatField,
|
|
140
|
+
updateCurrentDocumentDraftField,
|
|
141
|
+
updateDocumentInsertQuerySelection,
|
|
127
142
|
updateCurrentQueryChartDraftConfigField,
|
|
128
143
|
updateCurrentQueryChartDraftField,
|
|
129
144
|
updateCurrentTableDesignerColumnField,
|
|
@@ -136,6 +151,7 @@ import {
|
|
|
136
151
|
import { renderChartsDetail, renderChartsView } from './views/charts.js';
|
|
137
152
|
import { renderConnectionsView } from './views/connections.js';
|
|
138
153
|
import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
|
|
154
|
+
import { renderDocumentsView } from './views/documents.js';
|
|
139
155
|
import { renderEditorView } from './views/editor.js';
|
|
140
156
|
import { renderLandingView } from './views/landing.js';
|
|
141
157
|
import { renderMediaTaggingView } from './views/mediaTagging.js';
|
|
@@ -163,10 +179,19 @@ import {
|
|
|
163
179
|
stringifyRowEditorJson,
|
|
164
180
|
} from './utils/rowEditorJson.js';
|
|
165
181
|
import { getTimestampPreviewForField } from './utils/timestampPreview.js';
|
|
182
|
+
import {
|
|
183
|
+
buildRowEditorSubmittedValues,
|
|
184
|
+
getRowEditorValueState,
|
|
185
|
+
getRowEditorValueStateLabel,
|
|
186
|
+
} from './utils/rowEditorValues.js';
|
|
166
187
|
import {
|
|
167
188
|
formatTextCellCharacterCount,
|
|
168
189
|
getTextCellCharacterCount,
|
|
169
190
|
} from './utils/textCellStats.js';
|
|
191
|
+
import {
|
|
192
|
+
captureTableHorizontalScrollState,
|
|
193
|
+
restoreTableHorizontalScrollState,
|
|
194
|
+
} from './utils/tableScrollState.js';
|
|
170
195
|
|
|
171
196
|
const appRoot = document.querySelector('#app');
|
|
172
197
|
|
|
@@ -197,6 +222,10 @@ let lastRenderedLockedRoute = false;
|
|
|
197
222
|
let pendingNewTableDesignerAutofocus = false;
|
|
198
223
|
let pendingQueryEditorFocus = false;
|
|
199
224
|
let pendingMediaTaggingTagSearchFocus = false;
|
|
225
|
+
let documentAutosaveTimer = null;
|
|
226
|
+
let pendingDocumentAutosaveId = null;
|
|
227
|
+
|
|
228
|
+
const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
|
|
200
229
|
|
|
201
230
|
const APP_TITLE = 'SQLite Hub';
|
|
202
231
|
const ROUTE_TITLE_SEGMENTS = {
|
|
@@ -207,6 +236,7 @@ const ROUTE_TITLE_SEGMENTS = {
|
|
|
207
236
|
editor: 'SQL Editor',
|
|
208
237
|
editorResults: 'SQL Editor',
|
|
209
238
|
charts: 'Charts',
|
|
239
|
+
documents: 'Documents',
|
|
210
240
|
tableDesigner: 'Table Designer',
|
|
211
241
|
mediaTaggingSetup: 'Media Tagging',
|
|
212
242
|
mediaTaggingQueue: 'Tagging Queue',
|
|
@@ -797,13 +827,58 @@ function readFileAsText(file) {
|
|
|
797
827
|
resolve(String(reader.result ?? ''));
|
|
798
828
|
};
|
|
799
829
|
reader.onerror = () => {
|
|
800
|
-
reject(reader.error ?? new Error('The selected
|
|
830
|
+
reject(reader.error ?? new Error('The selected file could not be read.'));
|
|
801
831
|
};
|
|
802
832
|
|
|
803
833
|
reader.readAsText(file);
|
|
804
834
|
});
|
|
805
835
|
}
|
|
806
836
|
|
|
837
|
+
function clearDocumentAutosaveTimer() {
|
|
838
|
+
if (documentAutosaveTimer !== null) {
|
|
839
|
+
window.clearTimeout(documentAutosaveTimer);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
documentAutosaveTimer = null;
|
|
843
|
+
pendingDocumentAutosaveId = null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function scheduleDocumentAutosave(documentId) {
|
|
847
|
+
clearDocumentAutosaveTimer();
|
|
848
|
+
|
|
849
|
+
if (!documentId) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
pendingDocumentAutosaveId = String(documentId);
|
|
854
|
+
documentAutosaveTimer = window.setTimeout(async () => {
|
|
855
|
+
const state = getState();
|
|
856
|
+
const scheduledDocumentId = pendingDocumentAutosaveId;
|
|
857
|
+
|
|
858
|
+
documentAutosaveTimer = null;
|
|
859
|
+
pendingDocumentAutosaveId = null;
|
|
860
|
+
|
|
861
|
+
if (
|
|
862
|
+
state.route.name !== 'documents' ||
|
|
863
|
+
String(state.documents.selectedId ?? '') !== scheduledDocumentId ||
|
|
864
|
+
!state.documents.dirty
|
|
865
|
+
) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (state.documents.saving) {
|
|
870
|
+
scheduleDocumentAutosave(scheduledDocumentId);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (state.documents.deleting) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
await saveCurrentDocument({ toast: false });
|
|
879
|
+
}, DOCUMENT_AUTOSAVE_DELAY_MS);
|
|
880
|
+
}
|
|
881
|
+
|
|
807
882
|
async function buildConnectionLogoUpload(file) {
|
|
808
883
|
if (!(file instanceof File) || !file.size) {
|
|
809
884
|
return null;
|
|
@@ -852,6 +927,8 @@ function resolveView(state) {
|
|
|
852
927
|
return renderOverviewView(state);
|
|
853
928
|
case 'charts':
|
|
854
929
|
return renderChartsView(state);
|
|
930
|
+
case 'documents':
|
|
931
|
+
return renderDocumentsView(state);
|
|
855
932
|
case 'data':
|
|
856
933
|
return renderDataView(state);
|
|
857
934
|
case 'editor':
|
|
@@ -1101,6 +1178,39 @@ async function handleTableDesignerCsvImport(fileInput) {
|
|
|
1101
1178
|
}
|
|
1102
1179
|
}
|
|
1103
1180
|
|
|
1181
|
+
async function handleDocumentMarkdownImport(fileInput) {
|
|
1182
|
+
if (!(fileInput instanceof HTMLInputElement)) {
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const file = fileInput.files?.[0];
|
|
1187
|
+
|
|
1188
|
+
if (!(file instanceof File)) {
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
try {
|
|
1193
|
+
const content = await readFileAsText(file);
|
|
1194
|
+
const document = await createDocumentFromMarkdownExport({
|
|
1195
|
+
filename: file.name,
|
|
1196
|
+
content,
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
if (!document?.id) {
|
|
1200
|
+
showToast('Markdown document could not be imported.', 'alert');
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
clearDocumentAutosaveTimer();
|
|
1205
|
+
showToast(`Document "${document.filename}" imported.`, 'success');
|
|
1206
|
+
router.navigate(`/documents/${encodeURIComponent(document.id)}`);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
showToast(error?.message || 'Markdown import failed.', 'alert');
|
|
1209
|
+
} finally {
|
|
1210
|
+
fileInput.value = '';
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1104
1214
|
function renderApp(state) {
|
|
1105
1215
|
syncDocumentTitle(state);
|
|
1106
1216
|
|
|
@@ -1118,6 +1228,7 @@ function renderApp(state) {
|
|
|
1118
1228
|
'editorResults',
|
|
1119
1229
|
'data',
|
|
1120
1230
|
'charts',
|
|
1231
|
+
'documents',
|
|
1121
1232
|
'structure',
|
|
1122
1233
|
'tableDesigner',
|
|
1123
1234
|
'mediaTaggingSetup',
|
|
@@ -1156,6 +1267,10 @@ function renderApp(state) {
|
|
|
1156
1267
|
|
|
1157
1268
|
const focusedInput = captureFocusedInputState();
|
|
1158
1269
|
const queryHistoryScrollState = captureQueryHistoryScrollState();
|
|
1270
|
+
const tableHorizontalScrollState = captureTableHorizontalScrollState({
|
|
1271
|
+
routeName: lastRenderedRouteName,
|
|
1272
|
+
scrollNodes: shellRefs.view.querySelectorAll('[data-table-horizontal-scroll]'),
|
|
1273
|
+
});
|
|
1159
1274
|
const isEnteringNewTableDesignerRoute =
|
|
1160
1275
|
state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
|
|
1161
1276
|
|
|
@@ -1228,6 +1343,14 @@ function renderApp(state) {
|
|
|
1228
1343
|
restoreQueryHistoryScrollState(queryHistoryScrollState);
|
|
1229
1344
|
}
|
|
1230
1345
|
|
|
1346
|
+
if (mainChanged && !mainPatched) {
|
|
1347
|
+
restoreTableHorizontalScrollState({
|
|
1348
|
+
snapshot: tableHorizontalScrollState,
|
|
1349
|
+
routeName: state.route.name,
|
|
1350
|
+
scrollNodes: shellRefs.view.querySelectorAll('[data-table-horizontal-scroll]'),
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1231
1354
|
if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
|
|
1232
1355
|
if (focusQueryEditorInput()) {
|
|
1233
1356
|
pendingQueryEditorFocus = false;
|
|
@@ -1471,6 +1594,53 @@ function syncRowEditorFilePathPreview(inputNode) {
|
|
|
1471
1594
|
previewNode.hidden = false;
|
|
1472
1595
|
}
|
|
1473
1596
|
|
|
1597
|
+
function getRowEditorValueStateClassName(state) {
|
|
1598
|
+
if (state === 'null') {
|
|
1599
|
+
return 'border px-2 py-1 text-[9px] border-primary-container/35 bg-primary-container/15 text-primary-container';
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (state === 'empty') {
|
|
1603
|
+
return 'border px-2 py-1 text-[9px] border-outline-variant/35 bg-surface-container-high text-on-surface-variant';
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
return 'border px-2 py-1 text-[9px] border-outline-variant/20 bg-surface-container text-on-surface-variant';
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function syncRowEditorValueState(controlNode) {
|
|
1610
|
+
const fieldNode = controlNode.closest('[data-row-editor-field]');
|
|
1611
|
+
const valueInput = fieldNode?.querySelector('[data-row-editor-value-source]');
|
|
1612
|
+
const stateBadge = fieldNode?.querySelector('[data-row-editor-value-state]');
|
|
1613
|
+
|
|
1614
|
+
if (!fieldNode || !valueInput || !stateBadge) {
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const valueState = getRowEditorValueState(valueInput.value);
|
|
1619
|
+
|
|
1620
|
+
valueInput.dataset.rowEditorDirty = 'true';
|
|
1621
|
+
stateBadge.dataset.valueState = valueState;
|
|
1622
|
+
stateBadge.className = getRowEditorValueStateClassName(valueState);
|
|
1623
|
+
stateBadge.textContent = getRowEditorValueStateLabel(valueState);
|
|
1624
|
+
|
|
1625
|
+
syncRowEditorTimestampPreview(valueInput);
|
|
1626
|
+
syncRowEditorFilePathPreview(valueInput);
|
|
1627
|
+
syncRowEditorCharacterCount(valueInput);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function getRowEditorFieldMetadata(form) {
|
|
1631
|
+
return Object.fromEntries(
|
|
1632
|
+
Array.from(form.elements)
|
|
1633
|
+
.filter(element => element.name?.startsWith('field:'))
|
|
1634
|
+
.map(element => [
|
|
1635
|
+
element.name.slice('field:'.length),
|
|
1636
|
+
{
|
|
1637
|
+
initialState: element.closest('[data-row-editor-field]')?.dataset?.rowEditorInitialState,
|
|
1638
|
+
dirty: element.dataset.rowEditorDirty === 'true',
|
|
1639
|
+
},
|
|
1640
|
+
]),
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1474
1644
|
function closeCopyColumnMenus(exceptMenu = null) {
|
|
1475
1645
|
document.querySelectorAll('[data-copy-column-menu][open]').forEach(menu => {
|
|
1476
1646
|
if (menu !== exceptMenu && menu instanceof HTMLDetailsElement) {
|
|
@@ -1520,6 +1690,69 @@ function buildCopyColumnExportFilename(columnName, copyMode) {
|
|
|
1520
1690
|
return `${columnSlug}-${metadata.suffix}.${metadata.extension}`;
|
|
1521
1691
|
}
|
|
1522
1692
|
|
|
1693
|
+
function normalizeMarkdownDownloadFilename(value) {
|
|
1694
|
+
let filename = String(value ?? '')
|
|
1695
|
+
.trim()
|
|
1696
|
+
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
1697
|
+
.replace(/[\\/]+/g, ' ')
|
|
1698
|
+
.replace(/\s+/g, ' ')
|
|
1699
|
+
.replace(/^\.+/, '')
|
|
1700
|
+
.trim();
|
|
1701
|
+
|
|
1702
|
+
if (!filename) {
|
|
1703
|
+
filename = 'document.md';
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (!/\.md$/i.test(filename)) {
|
|
1707
|
+
filename = `${filename}.md`;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
return filename;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function countEditedMarkdownTodoItems(text) {
|
|
1714
|
+
const lines = String(text ?? '')
|
|
1715
|
+
.split(/\r\n|\r|\n/g)
|
|
1716
|
+
.filter(line => line.trim());
|
|
1717
|
+
|
|
1718
|
+
return lines.length;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function buildDocumentTimestampSlug(date = new Date()) {
|
|
1722
|
+
const pad = value => String(value).padStart(2, '0');
|
|
1723
|
+
|
|
1724
|
+
return [
|
|
1725
|
+
date.getFullYear(),
|
|
1726
|
+
pad(date.getMonth() + 1),
|
|
1727
|
+
pad(date.getDate()),
|
|
1728
|
+
pad(date.getHours()),
|
|
1729
|
+
pad(date.getMinutes()),
|
|
1730
|
+
].join('-');
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function buildCopyColumnDocumentFilename(columnName) {
|
|
1734
|
+
const columnSlug = slugifyExportFilenamePart(columnName);
|
|
1735
|
+
|
|
1736
|
+
return `${columnSlug}-todos-${buildDocumentTimestampSlug()}.md`;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function buildCopyColumnDocumentContent({ state, columnName, text, valueCount }) {
|
|
1740
|
+
const activeConnection = state.connections.active;
|
|
1741
|
+
const lines = [
|
|
1742
|
+
`# ${columnName || 'Column'} todos`,
|
|
1743
|
+
'',
|
|
1744
|
+
`- Database: ${activeConnection?.label || 'Active database'}`,
|
|
1745
|
+
`- Column: ${columnName || 'N/A'}`,
|
|
1746
|
+
`- Items: ${valueCount}`,
|
|
1747
|
+
`- Generated: ${new Date().toLocaleString()}`,
|
|
1748
|
+
'',
|
|
1749
|
+
];
|
|
1750
|
+
|
|
1751
|
+
lines.push('## Todos', '', String(text ?? '').trim(), '');
|
|
1752
|
+
|
|
1753
|
+
return lines.join('\n');
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1523
1756
|
function downloadTextFile({ text, filename, mimeType }) {
|
|
1524
1757
|
const blob = new Blob([text], { type: mimeType });
|
|
1525
1758
|
const url = URL.createObjectURL(blob);
|
|
@@ -1568,21 +1801,24 @@ function applyChangedRowEditorFormValues(rowObject) {
|
|
|
1568
1801
|
|
|
1569
1802
|
const nextRowObject = { ...rowObject };
|
|
1570
1803
|
const formData = new FormData(form);
|
|
1804
|
+
const submittedValues = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
1571
1805
|
|
|
1572
|
-
for (const [
|
|
1573
|
-
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1806
|
+
for (const [fieldName, value] of Object.entries(submittedValues)) {
|
|
1807
|
+
const key = `field:${fieldName}`;
|
|
1577
1808
|
const control = Array.from(form.elements).find(element => element.name === key);
|
|
1578
1809
|
const initialValue = control?.dataset?.rowEditorInitialValue;
|
|
1579
|
-
const
|
|
1810
|
+
const initialState = control?.closest('[data-row-editor-field]')?.dataset?.rowEditorInitialState;
|
|
1811
|
+
const currentState = getRowEditorValueState(value);
|
|
1812
|
+
const currentValue = value === null ? null : String(value);
|
|
1580
1813
|
|
|
1581
|
-
if (
|
|
1814
|
+
if (
|
|
1815
|
+
initialState === currentState &&
|
|
1816
|
+
(currentState === 'null' || (initialValue !== undefined && currentValue === initialValue))
|
|
1817
|
+
) {
|
|
1582
1818
|
continue;
|
|
1583
1819
|
}
|
|
1584
1820
|
|
|
1585
|
-
nextRowObject[
|
|
1821
|
+
nextRowObject[fieldName] = currentValue;
|
|
1586
1822
|
}
|
|
1587
1823
|
|
|
1588
1824
|
return nextRowObject;
|
|
@@ -1677,6 +1913,37 @@ function exportRowEditorJson() {
|
|
|
1677
1913
|
showToast(`Row from "${payload.label}" exported as JSON.`, 'success');
|
|
1678
1914
|
}
|
|
1679
1915
|
|
|
1916
|
+
function exportCurrentDocumentMarkdown() {
|
|
1917
|
+
const documents = getState().documents;
|
|
1918
|
+
|
|
1919
|
+
if (!documents.selectedId) {
|
|
1920
|
+
showToast('No document is selected.', 'alert');
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
const filename = normalizeMarkdownDownloadFilename(documents.draftFilename || documents.selected?.filename);
|
|
1925
|
+
|
|
1926
|
+
downloadTextFile({
|
|
1927
|
+
text: documents.draftContent ?? '',
|
|
1928
|
+
filename,
|
|
1929
|
+
mimeType: 'text/markdown;charset=utf-8',
|
|
1930
|
+
});
|
|
1931
|
+
showToast(`Document "${filename}" exported.`, 'success');
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
function getCurrentDocumentEditorInsertionRange() {
|
|
1935
|
+
const textarea = document.querySelector('.documents-editor-input');
|
|
1936
|
+
|
|
1937
|
+
if (!(textarea instanceof HTMLTextAreaElement)) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
return {
|
|
1942
|
+
start: textarea.selectionStart,
|
|
1943
|
+
end: textarea.selectionEnd,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1680
1947
|
function preserveDataRowEditorSelectionForReload() {
|
|
1681
1948
|
preserveCurrentDataRowSelectionForReload();
|
|
1682
1949
|
}
|
|
@@ -1698,6 +1965,9 @@ async function submitCopyColumnModal(formData) {
|
|
|
1698
1965
|
const outputSeparator = lineBreaks ? '\n' : separator;
|
|
1699
1966
|
const intent = String(formData.get('intent') ?? 'copy');
|
|
1700
1967
|
const isExportIntent = intent === 'export';
|
|
1968
|
+
const isDocumentIntent = intent === 'document';
|
|
1969
|
+
const isMarkdownTodo = isMarkdownTodoCopyColumnMode(copyMode);
|
|
1970
|
+
const editedText = formData.has('editedText') ? String(formData.get('editedText') ?? '') : null;
|
|
1701
1971
|
const result = getCopyColumnResult(state, scope);
|
|
1702
1972
|
const hasColumn = (result?.columns ?? []).some(column => String(column) === columnName);
|
|
1703
1973
|
|
|
@@ -1710,20 +1980,57 @@ async function submitCopyColumnModal(formData) {
|
|
|
1710
1980
|
return;
|
|
1711
1981
|
}
|
|
1712
1982
|
|
|
1713
|
-
if (!
|
|
1983
|
+
if (!isMarkdownTodo) {
|
|
1714
1984
|
storeCopyColumnPreferences({ separator, wrapper, lineBreaks });
|
|
1985
|
+
} else if (editedText !== null) {
|
|
1986
|
+
setCopyColumnModalEditedText(editedText);
|
|
1715
1987
|
}
|
|
1716
1988
|
setCopyColumnModalSubmitting(true);
|
|
1717
1989
|
|
|
1718
|
-
const
|
|
1990
|
+
const generatedOutput = buildCopyColumnText({
|
|
1719
1991
|
result,
|
|
1720
1992
|
columnName,
|
|
1721
1993
|
copyMode,
|
|
1722
1994
|
separator: outputSeparator,
|
|
1723
1995
|
wrapper,
|
|
1724
1996
|
});
|
|
1997
|
+
const text = isMarkdownTodo && editedText !== null ? editedText : generatedOutput.text;
|
|
1998
|
+
const valueCount = isMarkdownTodo && editedText !== null
|
|
1999
|
+
? countEditedMarkdownTodoItems(editedText)
|
|
2000
|
+
: generatedOutput.valueCount;
|
|
1725
2001
|
|
|
1726
2002
|
try {
|
|
2003
|
+
if (isDocumentIntent) {
|
|
2004
|
+
if (!isMarkdownTodo) {
|
|
2005
|
+
throw new Error('Document export is only available for Markdown Todo columns.');
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
const document = await createDocumentFromMarkdownExport({
|
|
2009
|
+
filename: buildCopyColumnDocumentFilename(columnName),
|
|
2010
|
+
title: `${columnName || 'Column'} todos`,
|
|
2011
|
+
content: buildCopyColumnDocumentContent({
|
|
2012
|
+
state,
|
|
2013
|
+
columnName,
|
|
2014
|
+
text,
|
|
2015
|
+
valueCount,
|
|
2016
|
+
}),
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
if (!document) {
|
|
2020
|
+
throw new Error('Document could not be created.');
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
closeModal();
|
|
2024
|
+
showToast(
|
|
2025
|
+
`Document "${document.filename}" created · ${formatNumber(valueCount)} ${
|
|
2026
|
+
valueCount === 1 ? 'item' : 'items'
|
|
2027
|
+
}`,
|
|
2028
|
+
'success',
|
|
2029
|
+
);
|
|
2030
|
+
router.navigate(`/documents/${encodeURIComponent(document.id)}`);
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1727
2034
|
if (isExportIntent) {
|
|
1728
2035
|
const metadata = getCopyColumnExportMetadata(copyMode);
|
|
1729
2036
|
|
|
@@ -1754,10 +2061,23 @@ async function submitCopyColumnModal(formData) {
|
|
|
1754
2061
|
);
|
|
1755
2062
|
} catch (error) {
|
|
1756
2063
|
setCopyColumnModalError({
|
|
1757
|
-
code:
|
|
1758
|
-
|
|
2064
|
+
code: isDocumentIntent
|
|
2065
|
+
? 'COPY_COLUMN_DOCUMENT_EXPORT_FAILED'
|
|
2066
|
+
: isExportIntent
|
|
2067
|
+
? 'COPY_COLUMN_EXPORT_FAILED'
|
|
2068
|
+
: 'CLIPBOARD_ACCESS_FAILED',
|
|
2069
|
+
message:
|
|
2070
|
+
error?.message ||
|
|
2071
|
+
(isDocumentIntent
|
|
2072
|
+
? 'Document export failed.'
|
|
2073
|
+
: isExportIntent
|
|
2074
|
+
? 'Column export failed.'
|
|
2075
|
+
: 'Clipboard access failed.'),
|
|
1759
2076
|
});
|
|
1760
|
-
showToast(
|
|
2077
|
+
showToast(
|
|
2078
|
+
isDocumentIntent ? 'Document export failed.' : isExportIntent ? 'Column export failed.' : 'Clipboard access failed.',
|
|
2079
|
+
'alert',
|
|
2080
|
+
);
|
|
1761
2081
|
}
|
|
1762
2082
|
}
|
|
1763
2083
|
|
|
@@ -1794,6 +2114,58 @@ async function handleAction(actionNode) {
|
|
|
1794
2114
|
mode: actionNode.dataset.copyMode,
|
|
1795
2115
|
});
|
|
1796
2116
|
return;
|
|
2117
|
+
case 'create-document': {
|
|
2118
|
+
const document = await createDocument();
|
|
2119
|
+
|
|
2120
|
+
if (document?.id) {
|
|
2121
|
+
router.navigate(`/documents/${encodeURIComponent(document.id)}`);
|
|
2122
|
+
}
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
case 'select-document': {
|
|
2126
|
+
const documentId = String(actionNode.dataset.documentId ?? '').trim();
|
|
2127
|
+
|
|
2128
|
+
if (documentId) {
|
|
2129
|
+
clearDocumentAutosaveTimer();
|
|
2130
|
+
router.navigate(`/documents/${encodeURIComponent(documentId)}`);
|
|
2131
|
+
}
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
case 'save-document':
|
|
2135
|
+
clearDocumentAutosaveTimer();
|
|
2136
|
+
await saveCurrentDocument();
|
|
2137
|
+
return;
|
|
2138
|
+
case 'export-document-markdown':
|
|
2139
|
+
exportCurrentDocumentMarkdown();
|
|
2140
|
+
return;
|
|
2141
|
+
case 'open-document-insert-table-modal':
|
|
2142
|
+
await openDocumentInsertTableModal(getCurrentDocumentEditorInsertionRange());
|
|
2143
|
+
return;
|
|
2144
|
+
case 'open-document-insert-note-modal':
|
|
2145
|
+
await openDocumentInsertNoteModal(getCurrentDocumentEditorInsertionRange());
|
|
2146
|
+
return;
|
|
2147
|
+
case 'import-document-markdown': {
|
|
2148
|
+
const fileInput = document.querySelector('[data-bind="document-import-file"]');
|
|
2149
|
+
|
|
2150
|
+
if (!(fileInput instanceof HTMLInputElement)) {
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
fileInput.value = '';
|
|
2155
|
+
fileInput.click();
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
case 'delete-document':
|
|
2159
|
+
clearDocumentAutosaveTimer();
|
|
2160
|
+
openDeleteDocumentModal();
|
|
2161
|
+
return;
|
|
2162
|
+
case 'toggle-document-pane':
|
|
2163
|
+
toggleDocumentsPane(actionNode.dataset.pane);
|
|
2164
|
+
return;
|
|
2165
|
+
case 'toggle-document-todo':
|
|
2166
|
+
clearDocumentAutosaveTimer();
|
|
2167
|
+
await toggleCurrentDocumentTodo(actionNode.dataset.lineIndex);
|
|
2168
|
+
return;
|
|
1797
2169
|
case 'open-create-query-chart-modal':
|
|
1798
2170
|
openCreateQueryChartModal();
|
|
1799
2171
|
return;
|
|
@@ -1809,6 +2181,33 @@ async function handleAction(actionNode) {
|
|
|
1809
2181
|
case 'edit-connection':
|
|
1810
2182
|
openEditConnectionModal(actionNode.dataset.connectionId);
|
|
1811
2183
|
return;
|
|
2184
|
+
case 'choose-create-database-path': {
|
|
2185
|
+
const labelNode = actionNode.querySelector('[data-create-database-path-button-label]');
|
|
2186
|
+
|
|
2187
|
+
actionNode.setAttribute('disabled', '');
|
|
2188
|
+
if (labelNode) {
|
|
2189
|
+
labelNode.textContent = 'Choosing...';
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const selectedPath = await chooseCreateDatabasePath();
|
|
2193
|
+
const pathInput = document.querySelector(
|
|
2194
|
+
'[data-form="create-connection"] [data-create-database-path]',
|
|
2195
|
+
);
|
|
2196
|
+
|
|
2197
|
+
if (selectedPath && pathInput instanceof HTMLInputElement) {
|
|
2198
|
+
pathInput.value = selectedPath;
|
|
2199
|
+
pathInput.focus({ preventScroll: true });
|
|
2200
|
+
pathInput.setSelectionRange(pathInput.value.length, pathInput.value.length);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
if (actionNode.isConnected) {
|
|
2204
|
+
actionNode.removeAttribute('disabled');
|
|
2205
|
+
if (labelNode) {
|
|
2206
|
+
labelNode.textContent = 'Browse...';
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
1812
2211
|
case 'close-modal':
|
|
1813
2212
|
closeModal();
|
|
1814
2213
|
return;
|
|
@@ -2284,6 +2683,20 @@ document.addEventListener('keydown', event => {
|
|
|
2284
2683
|
const target = event.target;
|
|
2285
2684
|
const state = getState();
|
|
2286
2685
|
|
|
2686
|
+
if (
|
|
2687
|
+
(event.key === 's' || event.key === 'S') &&
|
|
2688
|
+
(event.ctrlKey || event.metaKey) &&
|
|
2689
|
+
!event.altKey &&
|
|
2690
|
+
!event.shiftKey &&
|
|
2691
|
+
!event.defaultPrevented &&
|
|
2692
|
+
state.route.name === 'documents'
|
|
2693
|
+
) {
|
|
2694
|
+
event.preventDefault();
|
|
2695
|
+
clearDocumentAutosaveTimer();
|
|
2696
|
+
void saveCurrentDocument();
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2287
2700
|
// Handle Enter key in tag form fields to trigger create tag
|
|
2288
2701
|
if (
|
|
2289
2702
|
event.key === 'Enter' &&
|
|
@@ -2375,6 +2788,7 @@ document.addEventListener('keydown', event => {
|
|
|
2375
2788
|
|
|
2376
2789
|
document.addEventListener('input', event => {
|
|
2377
2790
|
const target = event.target instanceof Element ? event.target : null;
|
|
2791
|
+
const valueInput = target?.closest('[data-row-editor-value-source]');
|
|
2378
2792
|
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2379
2793
|
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2380
2794
|
|
|
@@ -2387,6 +2801,10 @@ document.addEventListener('input', event => {
|
|
|
2387
2801
|
syncRowEditorCharacterCount(textCellInput);
|
|
2388
2802
|
}
|
|
2389
2803
|
|
|
2804
|
+
if (valueInput) {
|
|
2805
|
+
syncRowEditorValueState(valueInput);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2390
2808
|
const bindNode = event.target.closest('[data-bind]');
|
|
2391
2809
|
|
|
2392
2810
|
if (!bindNode) {
|
|
@@ -2409,6 +2827,12 @@ document.addEventListener('input', event => {
|
|
|
2409
2827
|
return;
|
|
2410
2828
|
}
|
|
2411
2829
|
|
|
2830
|
+
if (bindNode.dataset.bind === 'document-field') {
|
|
2831
|
+
updateCurrentDocumentDraftField(bindNode.dataset.field, bindNode.value);
|
|
2832
|
+
scheduleDocumentAutosave(getState().documents.selectedId);
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2412
2836
|
if (bindNode.dataset.bind === 'data-search-query') {
|
|
2413
2837
|
void setDataSearchQuery(bindNode.value);
|
|
2414
2838
|
return;
|
|
@@ -2525,6 +2949,7 @@ document.addEventListener(
|
|
|
2525
2949
|
|
|
2526
2950
|
document.addEventListener('change', event => {
|
|
2527
2951
|
const target = event.target instanceof Element ? event.target : null;
|
|
2952
|
+
const valueControl = target?.closest('[data-row-editor-value-source]');
|
|
2528
2953
|
const timestampInput = target?.closest('[data-row-editor-timestamp-source]');
|
|
2529
2954
|
const textCellInput = target?.closest('[data-row-editor-text-source]');
|
|
2530
2955
|
|
|
@@ -2537,6 +2962,10 @@ document.addEventListener('change', event => {
|
|
|
2537
2962
|
syncRowEditorCharacterCount(textCellInput);
|
|
2538
2963
|
}
|
|
2539
2964
|
|
|
2965
|
+
if (valueControl) {
|
|
2966
|
+
syncRowEditorValueState(valueControl);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2540
2969
|
const bindNode = event.target.closest('[data-bind]');
|
|
2541
2970
|
|
|
2542
2971
|
if (!bindNode) {
|
|
@@ -2579,6 +3008,16 @@ document.addEventListener('change', event => {
|
|
|
2579
3008
|
return;
|
|
2580
3009
|
}
|
|
2581
3010
|
|
|
3011
|
+
if (bindNode.dataset.bind === 'document-import-file') {
|
|
3012
|
+
void handleDocumentMarkdownImport(bindNode);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
if (bindNode.dataset.bind === 'document-insert-query-select') {
|
|
3017
|
+
updateDocumentInsertQuerySelection(bindNode.value);
|
|
3018
|
+
return;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
2582
3021
|
if (bindNode.dataset.bind === 'table-designer-field') {
|
|
2583
3022
|
if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
|
|
2584
3023
|
updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
|
|
@@ -2757,6 +3196,32 @@ document.addEventListener('submit', async event => {
|
|
|
2757
3196
|
case 'delete-query-history-confirm':
|
|
2758
3197
|
await submitDeleteQueryHistoryConfirmation();
|
|
2759
3198
|
return;
|
|
3199
|
+
case 'delete-document-confirm': {
|
|
3200
|
+
const result = await submitDeleteDocumentConfirmation();
|
|
3201
|
+
|
|
3202
|
+
if (result.deleted) {
|
|
3203
|
+
router.navigate(
|
|
3204
|
+
result.nextDocumentId ? `/documents/${encodeURIComponent(result.nextDocumentId)}` : '/documents',
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
case 'document-insert-table': {
|
|
3210
|
+
const inserted = await submitDocumentInsertTable();
|
|
3211
|
+
|
|
3212
|
+
if (inserted) {
|
|
3213
|
+
scheduleDocumentAutosave(getState().documents.selectedId);
|
|
3214
|
+
}
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
case 'document-insert-note': {
|
|
3218
|
+
const inserted = submitDocumentInsertNote();
|
|
3219
|
+
|
|
3220
|
+
if (inserted) {
|
|
3221
|
+
scheduleDocumentAutosave(getState().documents.selectedId);
|
|
3222
|
+
}
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
2760
3225
|
case 'apply-row-update-preview':
|
|
2761
3226
|
await submitRowUpdatePreviewConfirmation();
|
|
2762
3227
|
return;
|
|
@@ -2770,15 +3235,7 @@ document.addEventListener('submit', async event => {
|
|
|
2770
3235
|
await submitCopyColumnModal(formData);
|
|
2771
3236
|
return;
|
|
2772
3237
|
case 'save-data-row': {
|
|
2773
|
-
const values =
|
|
2774
|
-
|
|
2775
|
-
for (const [key, value] of formData.entries()) {
|
|
2776
|
-
if (!key.startsWith('field:')) {
|
|
2777
|
-
continue;
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2780
|
-
values[key.slice('field:'.length)] = String(value ?? '');
|
|
2781
|
-
}
|
|
3238
|
+
const values = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
2782
3239
|
|
|
2783
3240
|
let rowIdentity = null;
|
|
2784
3241
|
const rawRowIdentity = String(formData.get('rowIdentity') ?? '').trim();
|
|
@@ -2795,15 +3252,7 @@ document.addEventListener('submit', async event => {
|
|
|
2795
3252
|
return;
|
|
2796
3253
|
}
|
|
2797
3254
|
case 'save-editor-row': {
|
|
2798
|
-
const values =
|
|
2799
|
-
|
|
2800
|
-
for (const [key, value] of formData.entries()) {
|
|
2801
|
-
if (!key.startsWith('field:')) {
|
|
2802
|
-
continue;
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
values[key.slice('field:'.length)] = String(value ?? '');
|
|
2806
|
-
}
|
|
3255
|
+
const values = buildRowEditorSubmittedValues(formData, getRowEditorFieldMetadata(form));
|
|
2807
3256
|
|
|
2808
3257
|
await openEditorRowUpdatePreview(String(formData.get('rowIndex') ?? ''), values);
|
|
2809
3258
|
return;
|