sqlite-hub 0.11.1 → 0.12.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 +44 -13
- package/database.sqlite +0 -0
- package/fill.js +526 -0
- package/frontend/js/api.js +61 -11
- package/frontend/js/app.js +96 -30
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +95 -0
- package/frontend/js/components/queryEditor.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +145 -6
- package/frontend/js/components/sidebar.js +5 -5
- package/frontend/js/store.js +219 -26
- package/frontend/js/views/data.js +5 -2
- package/frontend/js/views/editor.js +2 -0
- package/frontend/js/views/settings.js +1 -1
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +95 -95
- package/package.json +1 -1
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +199 -1
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +76 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
2
|
|
|
3
|
+
const URL_PATTERN = /^https?:\/\/[^\s<>"']+$/i;
|
|
4
|
+
|
|
3
5
|
function getJsonPreview(value) {
|
|
4
6
|
if (value === null || value === undefined) {
|
|
5
7
|
return null;
|
|
@@ -28,6 +30,114 @@ function getJsonPreview(value) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function getUrlValue(value) {
|
|
34
|
+
const text = String(value ?? "").trim();
|
|
35
|
+
|
|
36
|
+
if (!URL_PATTERN.test(text)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(text);
|
|
42
|
+
|
|
43
|
+
return ["http:", "https:"].includes(url.protocol) ? url.href : null;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function withUrlBadge(badges = [], url) {
|
|
50
|
+
if (!url) {
|
|
51
|
+
return badges;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const hasUrlBadge = badges.some((badge) => {
|
|
55
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
56
|
+
return String(label ?? "").toUpperCase() === "URL";
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getAllowedValues(field) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
|
|
65
|
+
return (Array.isArray(field.allowedValues) ? field.allowedValues : [])
|
|
66
|
+
.map((value) => String(value))
|
|
67
|
+
.filter((value) => {
|
|
68
|
+
if (seen.has(value)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
seen.add(value);
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withCheckBadge(badges = [], allowedValues = []) {
|
|
78
|
+
if (!allowedValues.length) {
|
|
79
|
+
return badges;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const hasCheckBadge = badges.some((badge) => {
|
|
83
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
84
|
+
return String(label ?? "").toUpperCase() === "CHECK";
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return hasCheckBadge ? badges : [...badges, { label: "CHECK", tone: "check" }];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderOpenUrlButton(url) {
|
|
91
|
+
if (!url) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `
|
|
96
|
+
<div class="mt-2">
|
|
97
|
+
<button
|
|
98
|
+
class="standard-button"
|
|
99
|
+
data-action="open-row-editor-url"
|
|
100
|
+
data-url="${escapeHtml(url)}"
|
|
101
|
+
type="button"
|
|
102
|
+
>
|
|
103
|
+
<span class="material-symbols-outlined text-sm">open_in_new</span>
|
|
104
|
+
Open in tab
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderAllowedValuesSelect(field, allowedValues) {
|
|
111
|
+
const currentValue = String(field.value ?? "");
|
|
112
|
+
const hasCurrentAllowedValue = allowedValues.includes(currentValue);
|
|
113
|
+
const shouldRenderEmptyOption = field.notNull !== true;
|
|
114
|
+
const shouldRenderCurrentOption =
|
|
115
|
+
currentValue !== "" && !hasCurrentAllowedValue;
|
|
116
|
+
const options = [
|
|
117
|
+
shouldRenderEmptyOption
|
|
118
|
+
? `<option value="" ${currentValue === "" ? "selected" : ""}>NULL / empty</option>`
|
|
119
|
+
: "",
|
|
120
|
+
shouldRenderCurrentOption
|
|
121
|
+
? `<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue)}</option>`
|
|
122
|
+
: "",
|
|
123
|
+
...allowedValues.map(
|
|
124
|
+
(value) =>
|
|
125
|
+
`<option value="${escapeHtml(value)}" ${
|
|
126
|
+
value === currentValue ? "selected" : ""
|
|
127
|
+
}>${escapeHtml(value)}</option>`
|
|
128
|
+
),
|
|
129
|
+
].join("");
|
|
130
|
+
|
|
131
|
+
return `
|
|
132
|
+
<select
|
|
133
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
134
|
+
name="field:${escapeHtml(field.name)}"
|
|
135
|
+
>
|
|
136
|
+
${options}
|
|
137
|
+
</select>
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
|
|
31
141
|
function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
32
142
|
return `
|
|
33
143
|
<div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
|
|
@@ -42,12 +152,15 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
|
|
|
42
152
|
}
|
|
43
153
|
|
|
44
154
|
function renderReadonlyField(label, value) {
|
|
45
|
-
const
|
|
155
|
+
const url = getUrlValue(value);
|
|
156
|
+
const badges = withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url);
|
|
46
157
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
47
158
|
const jsonPreview = getJsonPreview(value);
|
|
48
159
|
|
|
49
160
|
return `
|
|
50
|
-
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3"
|
|
161
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3" ${
|
|
162
|
+
url ? "data-row-editor-url-field" : ""
|
|
163
|
+
}>
|
|
51
164
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
52
165
|
<span>${escapeHtml(displayLabel)}</span>
|
|
53
166
|
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
@@ -57,18 +170,22 @@ function renderReadonlyField(label, value) {
|
|
|
57
170
|
? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
|
|
58
171
|
: `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
|
|
59
172
|
}
|
|
173
|
+
${renderOpenUrlButton(url)}
|
|
60
174
|
</div>
|
|
61
175
|
`;
|
|
62
176
|
}
|
|
63
177
|
|
|
64
178
|
function renderEditableField(field) {
|
|
65
|
-
const
|
|
179
|
+
const url = getUrlValue(field.value);
|
|
180
|
+
const allowedValues = getAllowedValues(field);
|
|
181
|
+
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
182
|
+
const badges = withUrlBadge(baseBadges, url);
|
|
66
183
|
const jsonPreview = getJsonPreview(field.value);
|
|
67
184
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
68
185
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
69
186
|
|
|
70
187
|
return `
|
|
71
|
-
<
|
|
188
|
+
<div class="block space-y-2" ${url ? "data-row-editor-url-field" : ""}>
|
|
72
189
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
73
190
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
74
191
|
${badges.map((badge) => renderFieldBadge(badge)).join("")}
|
|
@@ -79,7 +196,9 @@ function renderEditableField(field) {
|
|
|
79
196
|
: ""
|
|
80
197
|
}
|
|
81
198
|
${
|
|
82
|
-
|
|
199
|
+
allowedValues.length && !jsonPreview
|
|
200
|
+
? renderAllowedValuesSelect(field, allowedValues)
|
|
201
|
+
: inputType === "number" && !jsonPreview
|
|
83
202
|
? `
|
|
84
203
|
<input
|
|
85
204
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
@@ -89,6 +208,18 @@ function renderEditableField(field) {
|
|
|
89
208
|
value="${escapeHtml(field.value ?? "")}"
|
|
90
209
|
/>
|
|
91
210
|
`
|
|
211
|
+
: url && !jsonPreview
|
|
212
|
+
? `
|
|
213
|
+
<input
|
|
214
|
+
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
215
|
+
name="field:${escapeHtml(field.name)}"
|
|
216
|
+
data-row-editor-url-input
|
|
217
|
+
spellcheck="false"
|
|
218
|
+
type="text"
|
|
219
|
+
value="${escapeHtml(field.value ?? "")}"
|
|
220
|
+
/>
|
|
221
|
+
${renderOpenUrlButton(url)}
|
|
222
|
+
`
|
|
92
223
|
: `
|
|
93
224
|
<textarea
|
|
94
225
|
class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
|
|
@@ -99,7 +230,7 @@ function renderEditableField(field) {
|
|
|
99
230
|
>${escapeHtml(field.value ?? "")}</textarea>
|
|
100
231
|
`
|
|
101
232
|
}
|
|
102
|
-
</
|
|
233
|
+
</div>
|
|
103
234
|
`;
|
|
104
235
|
}
|
|
105
236
|
|
|
@@ -112,6 +243,14 @@ function getFieldBadgeClassName(tone) {
|
|
|
112
243
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
113
244
|
}
|
|
114
245
|
|
|
246
|
+
if (tone === "url") {
|
|
247
|
+
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (tone === "check") {
|
|
251
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
252
|
+
}
|
|
253
|
+
|
|
115
254
|
return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
|
|
116
255
|
}
|
|
117
256
|
|
|
@@ -6,16 +6,16 @@ const sidebarItems = [
|
|
|
6
6
|
{ label: 'Overview', href: '#/overview', key: 'overview', icon: 'dashboard' },
|
|
7
7
|
{ label: 'Data', href: '#/data', key: 'data', icon: 'table_rows' },
|
|
8
8
|
{ label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
|
|
9
|
-
{ label: '
|
|
9
|
+
{ label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
|
|
10
10
|
{ label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
|
|
11
|
-
{ label: '
|
|
11
|
+
{ label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
|
|
12
12
|
{
|
|
13
|
-
label: '
|
|
13
|
+
label: 'MEDIA_TAGGING',
|
|
14
14
|
key: 'mediaTagging',
|
|
15
15
|
icon: 'sell',
|
|
16
16
|
children: [
|
|
17
|
-
{ label: '
|
|
18
|
-
{ label: '
|
|
17
|
+
{ label: 'SETUP', href: '#/media-tagging', key: 'mediaTaggingSetup' },
|
|
18
|
+
{ label: 'TAGGING_QUEUE', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
|
|
19
19
|
],
|
|
20
20
|
},
|
|
21
21
|
{ label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
|
package/frontend/js/store.js
CHANGED
|
@@ -50,6 +50,11 @@ const QUERY_HISTORY_PAGE_SIZE = 30;
|
|
|
50
50
|
const QUERY_HISTORY_RUN_LIMIT = 8;
|
|
51
51
|
const CHART_HEIGHT_PRESETS = new Set(['small', 'medium', 'large']);
|
|
52
52
|
const EDITOR_RESULT_TABS = new Set(['results', 'performance', 'messages']);
|
|
53
|
+
const TEXT_EXPORT_FORMAT_LABELS = {
|
|
54
|
+
csv: 'CSV',
|
|
55
|
+
tsv: 'TSV',
|
|
56
|
+
md: 'Markdown',
|
|
57
|
+
};
|
|
53
58
|
const MISSING_DATABASE_ERROR = {
|
|
54
59
|
code: 'ACTIVE_DATABASE_REQUIRED',
|
|
55
60
|
message: 'No active SQLite database selected.',
|
|
@@ -1978,14 +1983,20 @@ async function previewMediaTaggingDraft(options = {}) {
|
|
|
1978
1983
|
}
|
|
1979
1984
|
}
|
|
1980
1985
|
|
|
1981
|
-
function invalidateDatabaseCaches() {
|
|
1986
|
+
function invalidateDatabaseCaches(options = {}) {
|
|
1987
|
+
const preserveDataBrowserState = options.preserveDataBrowserState === true;
|
|
1988
|
+
|
|
1982
1989
|
state.overview.data = null;
|
|
1983
1990
|
state.dataBrowser.tables = [];
|
|
1984
|
-
|
|
1991
|
+
if (!preserveDataBrowserState) {
|
|
1992
|
+
state.dataBrowser.selectedTable = null;
|
|
1993
|
+
}
|
|
1985
1994
|
state.dataBrowser.table = null;
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1995
|
+
if (!preserveDataBrowserState) {
|
|
1996
|
+
state.dataBrowser.page = 1;
|
|
1997
|
+
resetDataBrowserTableSearch();
|
|
1998
|
+
resetDataBrowserSearch();
|
|
1999
|
+
}
|
|
1989
2000
|
clearDataBrowserRowSelectionState();
|
|
1990
2001
|
state.dataBrowser.pendingOpenRow = null;
|
|
1991
2002
|
state.dataBrowser.exportLoading = false;
|
|
@@ -2253,6 +2264,34 @@ export function openModal(kind) {
|
|
|
2253
2264
|
emitChange();
|
|
2254
2265
|
}
|
|
2255
2266
|
|
|
2267
|
+
export function openQueryExportModal() {
|
|
2268
|
+
if (!String(state.editor.sqlText ?? '').trim()) {
|
|
2269
|
+
pushToast('Enter a query before exporting.', 'alert');
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
state.modal = {
|
|
2274
|
+
kind: 'query-export',
|
|
2275
|
+
error: null,
|
|
2276
|
+
submitting: false,
|
|
2277
|
+
};
|
|
2278
|
+
emitChange();
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
export function openDataExportModal() {
|
|
2282
|
+
if (!state.dataBrowser.selectedTable) {
|
|
2283
|
+
pushToast('No table selected for export.', 'alert');
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
state.modal = {
|
|
2288
|
+
kind: 'data-export',
|
|
2289
|
+
error: null,
|
|
2290
|
+
submitting: false,
|
|
2291
|
+
};
|
|
2292
|
+
emitChange();
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2256
2295
|
export function openEditConnectionModal(id) {
|
|
2257
2296
|
const connection = state.connections.recent.find(entry => entry.id === id);
|
|
2258
2297
|
|
|
@@ -2845,7 +2884,7 @@ export async function executeCurrentQuery() {
|
|
|
2845
2884
|
state.editor.result = response.data;
|
|
2846
2885
|
resetEditorResultSort();
|
|
2847
2886
|
state.editor.error = null;
|
|
2848
|
-
invalidateDatabaseCaches();
|
|
2887
|
+
invalidateDatabaseCaches({ preserveDataBrowserState: true });
|
|
2849
2888
|
await refreshQueryHistoryState();
|
|
2850
2889
|
pushToast(response.message || `Executed ${response.data.statementCount} SQL statement(s).`, 'success');
|
|
2851
2890
|
return true;
|
|
@@ -3158,9 +3197,15 @@ export function addCurrentTableDesignerColumn() {
|
|
|
3158
3197
|
return nextColumn?.id ?? null;
|
|
3159
3198
|
}
|
|
3160
3199
|
|
|
3161
|
-
export function queueTableDesignerCsvImport(fileName, csvText) {
|
|
3200
|
+
export function queueTableDesignerCsvImport(fileName, csvText, options = {}) {
|
|
3162
3201
|
try {
|
|
3163
|
-
const imported = createTableDesignerDraftFromCsvImport(
|
|
3202
|
+
const imported = createTableDesignerDraftFromCsvImport(
|
|
3203
|
+
{ fileName, csvText },
|
|
3204
|
+
{
|
|
3205
|
+
...getTableDesignerContext(),
|
|
3206
|
+
...(options.context ?? {}),
|
|
3207
|
+
},
|
|
3208
|
+
);
|
|
3164
3209
|
|
|
3165
3210
|
state.tableDesigner.pendingImportedDraft = imported.draft;
|
|
3166
3211
|
state.tableDesigner.selectedTableName = null;
|
|
@@ -3169,6 +3214,10 @@ export function queueTableDesignerCsvImport(fileName, csvText) {
|
|
|
3169
3214
|
emitChange();
|
|
3170
3215
|
return imported;
|
|
3171
3216
|
} catch (error) {
|
|
3217
|
+
if (options.throwOnError) {
|
|
3218
|
+
throw error;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3172
3221
|
pushToast(error?.message || 'CSV import failed.', 'alert');
|
|
3173
3222
|
return null;
|
|
3174
3223
|
}
|
|
@@ -4132,25 +4181,127 @@ export async function submitDeleteQueryHistoryConfirmation() {
|
|
|
4132
4181
|
return deleted;
|
|
4133
4182
|
}
|
|
4134
4183
|
|
|
4135
|
-
|
|
4184
|
+
function beginCurrentQueryExport() {
|
|
4136
4185
|
state.editor.exportLoading = true;
|
|
4186
|
+
if (state.modal?.kind === 'query-export') {
|
|
4187
|
+
state.modal.submitting = true;
|
|
4188
|
+
state.modal.error = null;
|
|
4189
|
+
}
|
|
4137
4190
|
emitChange();
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
function reportCurrentQueryExportError(error) {
|
|
4194
|
+
if (state.modal?.kind === 'query-export') {
|
|
4195
|
+
withModalError(error);
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
state.editor.error = normalizeError(error);
|
|
4200
|
+
emitChange();
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
function finishCurrentQueryExport() {
|
|
4204
|
+
state.editor.exportLoading = false;
|
|
4205
|
+
if (state.modal?.kind === 'query-export') {
|
|
4206
|
+
state.modal.submitting = false;
|
|
4207
|
+
}
|
|
4208
|
+
emitChange();
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
export async function exportCurrentQueryFormat(format = 'csv') {
|
|
4212
|
+
const normalizedFormat = String(format ?? 'csv').toLowerCase();
|
|
4213
|
+
const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
|
|
4214
|
+
|
|
4215
|
+
beginCurrentQueryExport();
|
|
4138
4216
|
|
|
4139
4217
|
try {
|
|
4140
|
-
await api.
|
|
4141
|
-
|
|
4218
|
+
await api.downloadQueryExport(state.editor.sqlText, normalizedFormat);
|
|
4219
|
+
closeModalInternal();
|
|
4220
|
+
pushToast(`${label} export started.`, 'success');
|
|
4142
4221
|
return true;
|
|
4143
4222
|
} catch (error) {
|
|
4144
|
-
|
|
4145
|
-
emitChange();
|
|
4223
|
+
reportCurrentQueryExportError(error);
|
|
4146
4224
|
return false;
|
|
4147
4225
|
} finally {
|
|
4148
|
-
|
|
4226
|
+
finishCurrentQueryExport();
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
export async function duplicateCurrentQueryAsTable() {
|
|
4231
|
+
beginCurrentQueryExport();
|
|
4232
|
+
|
|
4233
|
+
try {
|
|
4234
|
+
const response = await api.getQueryExport(state.editor.sqlText, 'csv');
|
|
4235
|
+
const exportData = response?.data ?? {};
|
|
4236
|
+
const imported = queueTableDesignerCsvImport(
|
|
4237
|
+
exportData.filename || 'query-results.csv',
|
|
4238
|
+
exportData.content || '',
|
|
4239
|
+
{ throwOnError: true },
|
|
4240
|
+
);
|
|
4241
|
+
|
|
4242
|
+
closeModalInternal();
|
|
4243
|
+
pushToast(
|
|
4244
|
+
`Table draft created from ${imported.importedRowCount} row${imported.importedRowCount === 1 ? '' : 's'}.`,
|
|
4245
|
+
'success',
|
|
4246
|
+
);
|
|
4247
|
+
return imported;
|
|
4248
|
+
} catch (error) {
|
|
4249
|
+
reportCurrentQueryExportError(error);
|
|
4250
|
+
return null;
|
|
4251
|
+
} finally {
|
|
4252
|
+
finishCurrentQueryExport();
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
export async function exportCurrentQueryCsv() {
|
|
4257
|
+
try {
|
|
4258
|
+
return await exportCurrentQueryFormat('csv');
|
|
4259
|
+
} catch (error) {
|
|
4260
|
+
state.editor.error = normalizeError(error);
|
|
4149
4261
|
emitChange();
|
|
4262
|
+
return false;
|
|
4150
4263
|
}
|
|
4151
4264
|
}
|
|
4152
4265
|
|
|
4153
|
-
|
|
4266
|
+
function getCurrentDataTableExportOptions(format = 'csv') {
|
|
4267
|
+
return {
|
|
4268
|
+
sortColumn: state.dataBrowser.sortColumn,
|
|
4269
|
+
sortDirection: state.dataBrowser.sortDirection,
|
|
4270
|
+
filterColumn: state.dataBrowser.searchColumn,
|
|
4271
|
+
filterOperator: state.dataBrowser.filterOperator,
|
|
4272
|
+
filterValue: state.dataBrowser.searchQuery,
|
|
4273
|
+
format,
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
|
|
4277
|
+
function beginCurrentDataTableExport() {
|
|
4278
|
+
state.dataBrowser.exportLoading = true;
|
|
4279
|
+
if (state.modal?.kind === 'data-export') {
|
|
4280
|
+
state.modal.submitting = true;
|
|
4281
|
+
state.modal.error = null;
|
|
4282
|
+
}
|
|
4283
|
+
emitChange();
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
function reportCurrentDataTableExportError(error) {
|
|
4287
|
+
if (state.modal?.kind === 'data-export') {
|
|
4288
|
+
withModalError(error);
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
state.dataBrowser.error = normalizeError(error);
|
|
4293
|
+
emitChange();
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
function finishCurrentDataTableExport() {
|
|
4297
|
+
state.dataBrowser.exportLoading = false;
|
|
4298
|
+
if (state.modal?.kind === 'data-export') {
|
|
4299
|
+
state.modal.submitting = false;
|
|
4300
|
+
}
|
|
4301
|
+
emitChange();
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
export async function exportCurrentDataTableFormat(format = 'csv') {
|
|
4154
4305
|
const tableName = state.dataBrowser.selectedTable;
|
|
4155
4306
|
|
|
4156
4307
|
if (!tableName) {
|
|
@@ -4158,26 +4309,68 @@ export async function exportCurrentDataTableCsv() {
|
|
|
4158
4309
|
return false;
|
|
4159
4310
|
}
|
|
4160
4311
|
|
|
4161
|
-
|
|
4162
|
-
|
|
4312
|
+
const normalizedFormat = String(format ?? 'csv').toLowerCase();
|
|
4313
|
+
const label = TEXT_EXPORT_FORMAT_LABELS[normalizedFormat] ?? TEXT_EXPORT_FORMAT_LABELS.csv;
|
|
4314
|
+
|
|
4315
|
+
beginCurrentDataTableExport();
|
|
4163
4316
|
|
|
4164
4317
|
try {
|
|
4165
|
-
await api.
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
});
|
|
4169
|
-
pushToast(`CSV export started for ${tableName}.`, 'success');
|
|
4318
|
+
await api.downloadTableExport(tableName, getCurrentDataTableExportOptions(normalizedFormat));
|
|
4319
|
+
closeModalInternal();
|
|
4320
|
+
pushToast(`${label} export started for ${tableName}.`, 'success');
|
|
4170
4321
|
return true;
|
|
4171
4322
|
} catch (error) {
|
|
4172
|
-
|
|
4173
|
-
emitChange();
|
|
4323
|
+
reportCurrentDataTableExportError(error);
|
|
4174
4324
|
return false;
|
|
4175
4325
|
} finally {
|
|
4176
|
-
|
|
4177
|
-
|
|
4326
|
+
finishCurrentDataTableExport();
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
export async function duplicateCurrentDataTableAsTable() {
|
|
4331
|
+
const tableName = state.dataBrowser.selectedTable;
|
|
4332
|
+
|
|
4333
|
+
if (!tableName) {
|
|
4334
|
+
pushToast('No table selected for export.', 'alert');
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
beginCurrentDataTableExport();
|
|
4339
|
+
|
|
4340
|
+
try {
|
|
4341
|
+
const response = await api.getTableExport(tableName, getCurrentDataTableExportOptions('csv'));
|
|
4342
|
+
const exportData = response?.data ?? {};
|
|
4343
|
+
const imported = queueTableDesignerCsvImport(
|
|
4344
|
+
exportData.filename || `${tableName}.csv`,
|
|
4345
|
+
exportData.content || '',
|
|
4346
|
+
{
|
|
4347
|
+
throwOnError: true,
|
|
4348
|
+
context: {
|
|
4349
|
+
catalogTables: state.tableDesigner.tables?.length
|
|
4350
|
+
? state.tableDesigner.tables
|
|
4351
|
+
: state.dataBrowser.tables,
|
|
4352
|
+
},
|
|
4353
|
+
},
|
|
4354
|
+
);
|
|
4355
|
+
|
|
4356
|
+
closeModalInternal();
|
|
4357
|
+
pushToast(
|
|
4358
|
+
`Table draft created from ${imported.importedRowCount} row${imported.importedRowCount === 1 ? '' : 's'}.`,
|
|
4359
|
+
'success',
|
|
4360
|
+
);
|
|
4361
|
+
return imported;
|
|
4362
|
+
} catch (error) {
|
|
4363
|
+
reportCurrentDataTableExportError(error);
|
|
4364
|
+
return null;
|
|
4365
|
+
} finally {
|
|
4366
|
+
finishCurrentDataTableExport();
|
|
4178
4367
|
}
|
|
4179
4368
|
}
|
|
4180
4369
|
|
|
4370
|
+
export async function exportCurrentDataTableCsv() {
|
|
4371
|
+
return exportCurrentDataTableFormat('csv');
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4181
4374
|
export function sortEditorResultsByColumn(columnName) {
|
|
4182
4375
|
const normalizedColumn = String(columnName ?? '').trim();
|
|
4183
4376
|
const result = state.editor.result;
|
|
@@ -142,10 +142,11 @@ function renderWorkspaceHeader(state) {
|
|
|
142
142
|
</button>
|
|
143
143
|
<button
|
|
144
144
|
class="standard-button"
|
|
145
|
-
data-action="
|
|
145
|
+
data-action="open-data-export-modal"
|
|
146
146
|
type="button"
|
|
147
147
|
>
|
|
148
|
-
|
|
148
|
+
<span class="material-symbols-outlined text-sm">download</span>
|
|
149
|
+
${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export'}
|
|
149
150
|
</button>`
|
|
150
151
|
: ''
|
|
151
152
|
}
|
|
@@ -574,6 +575,8 @@ export function renderDataRowEditorPanel(state) {
|
|
|
574
575
|
label: column.name,
|
|
575
576
|
badges: getColumnBadges(column),
|
|
576
577
|
...getColumnNumberInputMeta(column),
|
|
578
|
+
allowedValues: column.allowedValues ?? [],
|
|
579
|
+
notNull: Boolean(column.notNull),
|
|
577
580
|
value: value === null || value === undefined ? '' : String(value),
|
|
578
581
|
};
|
|
579
582
|
}),
|
|
@@ -193,6 +193,8 @@ function renderEditorRowPanel(state) {
|
|
|
193
193
|
return {
|
|
194
194
|
name: column.sourceColumn,
|
|
195
195
|
label: column.sourceColumn,
|
|
196
|
+
allowedValues: column.allowedValues ?? [],
|
|
197
|
+
notNull: Boolean(column.notNull),
|
|
196
198
|
value: value === null || value === undefined ? '' : String(value),
|
|
197
199
|
};
|
|
198
200
|
}),
|
|
@@ -34,7 +34,7 @@ function renderSettingsContent(state) {
|
|
|
34
34
|
<div class="space-y-5 p-6">
|
|
35
35
|
<div>
|
|
36
36
|
<div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
|
|
37
|
-
|
|
37
|
+
Current_Version
|
|
38
38
|
</div>
|
|
39
39
|
<div class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
40
40
|
v${escapeHtml(state.settings.appVersion ?? '0.0.0')}
|