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.
@@ -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 badges = Array.isArray(label?.badges) ? label.badges : [];
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 badges = Array.isArray(field.badges) ? field.badges : [];
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
- <label class="block space-y-2">
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
- inputType === "number" && !jsonPreview
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
- </label>
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: 'SQL Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
9
+ { label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
10
10
  { label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
11
- { label: 'Table Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
11
+ { label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
12
12
  {
13
- label: 'Media Tagging',
13
+ label: 'MEDIA_TAGGING',
14
14
  key: 'mediaTagging',
15
15
  icon: 'sell',
16
16
  children: [
17
- { label: 'Setup', href: '#/media-tagging', key: 'mediaTaggingSetup' },
18
- { label: 'Tagging Queue', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
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' },
@@ -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
- state.dataBrowser.selectedTable = null;
1991
+ if (!preserveDataBrowserState) {
1992
+ state.dataBrowser.selectedTable = null;
1993
+ }
1985
1994
  state.dataBrowser.table = null;
1986
- state.dataBrowser.page = 1;
1987
- resetDataBrowserTableSearch();
1988
- resetDataBrowserSearch();
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({ fileName, csvText }, getTableDesignerContext());
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
- export async function exportCurrentQueryCsv() {
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.downloadQueryCsv(state.editor.sqlText);
4141
- pushToast('Query export started.', 'success');
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
- state.editor.error = normalizeError(error);
4145
- emitChange();
4223
+ reportCurrentQueryExportError(error);
4146
4224
  return false;
4147
4225
  } finally {
4148
- state.editor.exportLoading = false;
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
- export async function exportCurrentDataTableCsv() {
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
- state.dataBrowser.exportLoading = true;
4162
- emitChange();
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.downloadTableCsv(tableName, {
4166
- sortColumn: state.dataBrowser.sortColumn,
4167
- sortDirection: state.dataBrowser.sortDirection,
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
- state.dataBrowser.error = normalizeError(error);
4173
- emitChange();
4323
+ reportCurrentDataTableExportError(error);
4174
4324
  return false;
4175
4325
  } finally {
4176
- state.dataBrowser.exportLoading = false;
4177
- emitChange();
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="export-data-csv"
145
+ data-action="open-data-export-modal"
146
146
  type="button"
147
147
  >
148
- ${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export CSV'}
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
- Current Version
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')}