sqlite-hub 0.17.2 → 1.1.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.
Files changed (85) hide show
  1. package/README.md +143 -47
  2. package/bin/sqlite-hub.js +206 -444
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +263 -34
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +77 -21
  30. package/frontend/js/components/queryChartRenderer.js +28 -3
  31. package/frontend/js/components/queryEditor.js +20 -24
  32. package/frontend/js/components/queryHistoryDetail.js +1 -1
  33. package/frontend/js/components/queryHistoryHeader.js +48 -0
  34. package/frontend/js/components/queryHistoryList.js +132 -0
  35. package/frontend/js/components/queryHistoryPanel.js +72 -136
  36. package/frontend/js/components/structureGraph.js +699 -89
  37. package/frontend/js/components/tableDesignerEditor.js +3 -5
  38. package/frontend/js/store.js +188 -7
  39. package/frontend/js/utils/exportFilenames.js +3 -1
  40. package/frontend/js/views/charts.js +320 -169
  41. package/frontend/js/views/connections.js +59 -64
  42. package/frontend/js/views/data.js +12 -20
  43. package/frontend/js/views/editor.js +0 -2
  44. package/frontend/js/views/settings.js +219 -5
  45. package/frontend/js/views/structure.js +27 -13
  46. package/frontend/styles/components.css +155 -0
  47. package/frontend/styles/structure-graph.css +140 -35
  48. package/frontend/styles/tailwind.generated.css +2 -2
  49. package/frontend/styles/views.css +12 -6
  50. package/package.json +7 -6
  51. package/server/middleware/apiTokenAuth.js +30 -0
  52. package/server/routes/connections.js +17 -0
  53. package/server/routes/export.js +89 -22
  54. package/server/routes/externalApi.js +304 -0
  55. package/server/routes/settings.js +90 -21
  56. package/server/server.js +22 -1
  57. package/server/services/apiTokenService.js +101 -0
  58. package/server/services/appInfoService.js +215 -0
  59. package/server/services/databaseCommandService.js +443 -0
  60. package/server/services/nativeFileDialogService.js +93 -1
  61. package/server/services/sqlite/exportService.js +307 -22
  62. package/server/services/storage/appStateStore.js +113 -0
  63. package/server/utils/errors.js +7 -0
  64. package/tests/api-token-auth.test.js +236 -0
  65. package/tests/cli-args.test.js +16 -3
  66. package/tests/cli-service-delegation.test.js +43 -0
  67. package/tests/connections-file-dialog-route.test.js +43 -0
  68. package/tests/copy-column-modal.test.js +34 -0
  69. package/tests/database-command-service.test.js +139 -0
  70. package/tests/export-blob.test.js +54 -1
  71. package/tests/export-filenames.test.js +4 -0
  72. package/tests/form-controls.test.js +34 -0
  73. package/tests/native-file-dialog.test.js +27 -0
  74. package/tests/settings-api-tokens-route.test.js +97 -0
  75. package/tests/settings-metadata.test.js +99 -1
  76. package/tests/settings-view.test.js +75 -0
  77. package/frontend/assets/mockups/connections.png +0 -0
  78. package/frontend/assets/mockups/data.png +0 -0
  79. package/frontend/assets/mockups/data_row_editor.png +0 -0
  80. package/frontend/assets/mockups/home.png +0 -0
  81. package/frontend/assets/mockups/sql_editor.png +0 -0
  82. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  83. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  84. package/frontend/assets/mockups/structure.png +0 -0
  85. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -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
- <input
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
- ${renderField({
159
- label: "SQLite File Path",
160
- name: "path",
161
- placeholder: "/absolute/path/to/database.sqlite",
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",
@@ -272,13 +286,12 @@ export function renderCreateDatabaseForm(modal) {
272
286
  New SQLite File Path
273
287
  </span>
274
288
  <span class="flex items-stretch gap-2">
275
- <input
276
- class="control-input min-w-0 flex-1 border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
277
- data-create-database-path
278
- name="path"
279
- placeholder="/absolute/path/to/new-database.sqlite"
280
- type="text"
281
- />
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
+ })}
282
295
  <button
283
296
  class="standard-button flex-none"
284
297
  data-action="choose-create-database-path"
@@ -847,6 +860,28 @@ function renderDeleteDocumentForm(modal) {
847
860
  ].join("");
848
861
  }
849
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
+
850
885
  function getDocumentInsertQueryTitle(query) {
851
886
  return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
852
887
  }
@@ -983,6 +1018,20 @@ function getExportOptions() {
983
1018
  meta: ".md",
984
1019
  preview: ["| id | name |", "| --- | --- |"],
985
1020
  },
1021
+ {
1022
+ label: "JSON",
1023
+ format: "json",
1024
+ icon: "data_object",
1025
+ meta: ".json",
1026
+ preview: ['[', ' {"id":1,"name":"Acme"}', ']'],
1027
+ },
1028
+ {
1029
+ label: "Parquet",
1030
+ format: "parquet",
1031
+ icon: "dataset",
1032
+ meta: ".parquet",
1033
+ preview: ["columnar binary", "typed row groups"],
1034
+ },
986
1035
  {
987
1036
  label: "Duplicate",
988
1037
  format: "table",
@@ -1012,7 +1061,7 @@ function renderTextExportModal(modal, action) {
1012
1061
  ${disabledAttribute}
1013
1062
  />
1014
1063
  </label>
1015
- <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
1064
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
1016
1065
  ${getExportOptions()
1017
1066
  .map(
1018
1067
  (option) => `
@@ -1687,6 +1736,11 @@ export function renderModal(state) {
1687
1736
  title: "Delete Document",
1688
1737
  body: renderDeleteDocumentForm(modal),
1689
1738
  },
1739
+ "delete-api-token": {
1740
+ eyebrow: "Settings // Confirm token deletion",
1741
+ title: "Delete API Token",
1742
+ body: renderDeleteApiTokenForm(modal),
1743
+ },
1690
1744
  "document-insert-table": {
1691
1745
  eyebrow: "Documents // Saved query output",
1692
1746
  title: "Insert Table",
@@ -1744,6 +1798,8 @@ export function renderModal(state) {
1744
1798
  modal.kind === "row-update-preview" ||
1745
1799
  modal.kind === "table-designer-constraints"
1746
1800
  ? "max-w-3xl"
1801
+ : modal.kind === "query-export" || modal.kind === "data-export"
1802
+ ? "max-w-4xl"
1747
1803
  : "max-w-xl"
1748
1804
  } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1749
1805
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
@@ -7,6 +7,7 @@ import {
7
7
  import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
8
8
 
9
9
  const chartInstances = new Map();
10
+ const chartHosts = new Map();
10
11
  const resizeObservers = new Map();
11
12
 
12
13
  function getEchartsRuntime() {
@@ -41,6 +42,8 @@ function disposeChart(chartId) {
41
42
  chartInstances.delete(chartId);
42
43
  }
43
44
 
45
+ chartHosts.delete(chartId);
46
+
44
47
  const observer = resizeObservers.get(chartId);
45
48
 
46
49
  if (observer) {
@@ -58,7 +61,15 @@ export function mountQueryChartRenderer(state) {
58
61
  const charts = state.charts.detail?.charts ?? [];
59
62
  const result = state.charts.result;
60
63
 
61
- teardownQueryChartRenderer();
64
+ const activeChartIds = new Set(charts.map((chart) => String(chart.id)));
65
+
66
+ Array.from(chartInstances.keys()).forEach((chartId) => {
67
+ const host = chartHosts.get(chartId);
68
+
69
+ if (!activeChartIds.has(chartId) || !(host instanceof HTMLElement) || !host.isConnected) {
70
+ disposeChart(chartId);
71
+ }
72
+ });
62
73
 
63
74
  if (!echartsRuntime || !result || !charts.length) {
64
75
  return;
@@ -85,17 +96,31 @@ export function mountQueryChartRenderer(state) {
85
96
  return;
86
97
  }
87
98
 
99
+ const chartId = String(chart.id);
100
+ const existingInstance = chartInstances.get(chartId);
101
+ const existingHost = chartHosts.get(chartId);
102
+
103
+ if (existingInstance && existingHost === host) {
104
+ existingInstance.resize();
105
+ return;
106
+ }
107
+
108
+ if (existingInstance) {
109
+ disposeChart(chartId);
110
+ }
111
+
88
112
  const instance = echartsRuntime.init(host);
89
113
 
90
114
  instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
91
- chartInstances.set(String(chart.id), instance);
115
+ chartInstances.set(chartId, instance);
116
+ chartHosts.set(chartId, host);
92
117
 
93
118
  const resizeObserver = new ResizeObserver(() => {
94
119
  instance.resize();
95
120
  });
96
121
 
97
122
  resizeObserver.observe(host);
98
- resizeObservers.set(String(chart.id), resizeObserver);
123
+ resizeObservers.set(chartId, resizeObserver);
99
124
  });
100
125
  }
101
126
 
@@ -1,5 +1,4 @@
1
1
  import { escapeHtml, highlightSql } from "../utils/format.js";
2
- import { renderActionBar } from "./actionBar.js";
3
2
 
4
3
  function renderLineNumbers(query) {
5
4
  const lineCount = Math.max(1, String(query || "").split("\n").length);
@@ -40,20 +39,11 @@ export function renderQueryEditor({
40
39
  query,
41
40
  executing = false,
42
41
  exporting = false,
43
- historyLoading = false,
44
- historyTotal = 0,
45
42
  editorVisible = true,
46
43
  historyVisible = true,
47
44
  }) {
48
45
  const secondaryButtonClass = "standard-button";
49
46
  const left = `
50
- <div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40">
51
- <span class="material-symbols-outlined text-xs">history</span>
52
- ${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
53
- </div>
54
- `;
55
-
56
- const right = `
57
47
  <button
58
48
  class="${secondaryButtonClass}"
59
49
  data-action="toggle-editor-panel"
@@ -63,15 +53,9 @@ export function renderQueryEditor({
63
53
  <span class="material-symbols-outlined text-sm">${editorVisible ? "keyboard_arrow_down" : "terminal"}</span>
64
54
  ${editorVisible ? "Hide Editor" : "Show Editor"}
65
55
  </button>
66
- <button
67
- class="${secondaryButtonClass}"
68
- data-action="toggle-query-history-panel"
69
- data-next-value="${historyVisible ? "false" : "true"}"
70
- type="button"
71
- >
72
- <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
73
- ${historyVisible ? "Hide History" : "Show History"}
74
- </button>
56
+ `;
57
+
58
+ const center = `
75
59
  <button
76
60
  class="${secondaryButtonClass}"
77
61
  data-action="format-current-query"
@@ -104,14 +88,26 @@ export function renderQueryEditor({
104
88
  </button>
105
89
  `;
106
90
 
91
+ const right = `
92
+ <button
93
+ class="${secondaryButtonClass}"
94
+ data-action="toggle-query-history-panel"
95
+ data-next-value="${historyVisible ? "false" : "true"}"
96
+ type="button"
97
+ >
98
+ <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
99
+ ${historyVisible ? "Hide History" : "Show History"}
100
+ </button>
101
+ `;
102
+
107
103
  return `
108
104
  <div class="flex h-full min-h-0 flex-col">
109
105
  <div class="bg-surface-container-low px-6 py-3">
110
- ${renderActionBar({
111
- left,
112
- right,
113
- className: "flex-wrap",
114
- })}
106
+ <div class="query-editor-toolbar">
107
+ <div class="query-editor-toolbar__left">${left}</div>
108
+ <div class="query-editor-toolbar__center">${center}</div>
109
+ <div class="query-editor-toolbar__right">${right}</div>
110
+ </div>
115
111
  </div>
116
112
  ${
117
113
  editorVisible
@@ -163,7 +163,7 @@ export function renderQueryHistoryDetail({
163
163
  escapeHtml(item.displayTitle),
164
164
  "</h2></div>",
165
165
  '<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
166
- '</div><div class="mt-4 flex flex-wrap gap-2">',
166
+ '</div><div class="query-history-badge-row mt-4 flex flex-wrap gap-2">',
167
167
  statusMarkup,
168
168
  "</div></div>",
169
169
  '<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">',
@@ -0,0 +1,48 @@
1
+ import { escapeHtml } from '../utils/format.js';
2
+
3
+ export function renderQueryHistoryHeader({
4
+ title = 'Query History',
5
+ icon = 'history',
6
+ closeAction = 'toggle-query-history-panel',
7
+ closeTitle = 'Hide query history',
8
+ nextValue = 'false',
9
+ } = {}) {
10
+ return `
11
+ <div class="flex items-center justify-between gap-3">
12
+ <div class="flex items-center gap-2">
13
+ <span class="material-symbols-outlined text-[18px] text-primary-container">${escapeHtml(icon)}</span>
14
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
15
+ ${escapeHtml(title)}
16
+ </span>
17
+ </div>
18
+ <button
19
+ class="query-history-icon-button"
20
+ data-action="${escapeHtml(closeAction)}"
21
+ data-next-value="${escapeHtml(nextValue)}"
22
+ title="${escapeHtml(closeTitle)}"
23
+ type="button"
24
+ >
25
+ <span class="material-symbols-outlined text-[18px]">close</span>
26
+ </button>
27
+ </div>
28
+ `;
29
+ }
30
+
31
+ export function renderQueryHistorySearch({
32
+ bind = 'query-history-search',
33
+ value = '',
34
+ placeholder = 'Search SQL, titles, notes...',
35
+ } = {}) {
36
+ return `
37
+ <label class="mt-4 block">
38
+ <span class="sr-only">Search query history</span>
39
+ <input
40
+ class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
41
+ data-bind="${escapeHtml(bind)}"
42
+ placeholder="${escapeHtml(placeholder)}"
43
+ type="search"
44
+ value="${escapeHtml(value)}"
45
+ />
46
+ </label>
47
+ `;
48
+ }
@@ -0,0 +1,132 @@
1
+ import { escapeHtml, formatNumber } from '../utils/format.js';
2
+
3
+ export function getQueryTypeTone(queryType) {
4
+ if (queryType === 'select' || queryType === 'update') {
5
+ return 'success';
6
+ }
7
+
8
+ if (queryType === 'pragma') {
9
+ return 'primary';
10
+ }
11
+
12
+ return 'muted';
13
+ }
14
+
15
+ export function renderQueryHistoryBadgeRow(markup, { compact = true, className = '' } = {}) {
16
+ const rowClasses = ['query-history-badge-row', compact ? 'query-history-badge-row--compact' : '', className]
17
+ .filter(Boolean)
18
+ .join(' ');
19
+
20
+ return `<div class="${rowClasses}">${markup}</div>`;
21
+ }
22
+
23
+ export function renderQueryHistoryTabs({ tabs = [], activeTab = 'recent', action, count = null, countAttr = '' }) {
24
+ const activeCount = count ?? tabs.find(tab => tab.id === activeTab)?.count ?? 0;
25
+
26
+ return [
27
+ '<div class="query-history-tabs">',
28
+ tabs
29
+ .map(tab =>
30
+ [
31
+ '<button class="query-history-tab ',
32
+ activeTab === tab.id ? 'is-active' : '',
33
+ '" data-action="',
34
+ escapeHtml(action),
35
+ '" data-tab="',
36
+ escapeHtml(tab.id),
37
+ '" type="button">',
38
+ escapeHtml(tab.label),
39
+ '</button>',
40
+ ].join(''),
41
+ )
42
+ .join(''),
43
+ '<span class="query-history-tabs__count" ',
44
+ countAttr,
45
+ '>',
46
+ escapeHtml(formatNumber(activeCount)),
47
+ '</span></div>',
48
+ ].join('');
49
+ }
50
+
51
+ export function renderQueryHistoryIconButton({
52
+ action,
53
+ historyId = null,
54
+ icon,
55
+ title,
56
+ active = false,
57
+ nextValue = null,
58
+ attrs = '',
59
+ }) {
60
+ return [
61
+ '<button class="query-history-icon-button ',
62
+ active ? 'is-active' : '',
63
+ '" data-action="',
64
+ escapeHtml(action),
65
+ '"',
66
+ historyId === null || historyId === undefined ? '' : ` data-history-id="${escapeHtml(historyId)}"`,
67
+ nextValue === null || nextValue === undefined ? '' : ` data-next-value="${escapeHtml(nextValue)}"`,
68
+ attrs ? ` ${attrs}` : '',
69
+ ' title="',
70
+ escapeHtml(title),
71
+ '" type="button"><span class="material-symbols-outlined text-[18px]">',
72
+ escapeHtml(icon),
73
+ '</span></button>',
74
+ ].join('');
75
+ }
76
+
77
+ export function renderQueryHistoryActionGroup(actions = []) {
78
+ return `<div class="query-history-item-actions">${actions.filter(Boolean).join('')}</div>`;
79
+ }
80
+
81
+ export function renderQueryHistoryListItem({
82
+ title,
83
+ preview,
84
+ historyId,
85
+ active = false,
86
+ error = false,
87
+ hitAction,
88
+ hitAttrs = '',
89
+ itemAttrs = '',
90
+ titleAttrs = '',
91
+ badgesMarkup = '',
92
+ footerMetaMarkup = '',
93
+ actionsMarkup = '',
94
+ }) {
95
+ const itemClasses = ['query-history-item', active ? 'is-active' : '', error ? 'is-error' : '']
96
+ .filter(Boolean)
97
+ .join(' ');
98
+ const footerMeta = String(footerMetaMarkup ?? '').trim()
99
+ ? ['<div class="query-history-item-meta">', footerMetaMarkup, '</div>'].join('')
100
+ : '';
101
+
102
+ return [
103
+ '<article class="',
104
+ itemClasses,
105
+ '"',
106
+ itemAttrs ? ` ${itemAttrs}` : '',
107
+ '><button class="query-history-item-hit ',
108
+ active ? 'is-active' : '',
109
+ '" data-action="',
110
+ escapeHtml(hitAction),
111
+ '" data-history-id="',
112
+ escapeHtml(historyId),
113
+ '"',
114
+ hitAttrs ? ` ${hitAttrs}` : '',
115
+ ' type="button">',
116
+ '<div class="query-history-item-main">',
117
+ '<span class="query-history-item-title"',
118
+ titleAttrs ? ` ${titleAttrs}` : '',
119
+ '>',
120
+ escapeHtml(title),
121
+ '</span>',
122
+ badgesMarkup,
123
+ '</div>',
124
+ '<p class="query-history-sql-preview">',
125
+ escapeHtml(preview),
126
+ '</p></button>',
127
+ '<div class="query-history-item-footer">',
128
+ footerMeta,
129
+ actionsMarkup,
130
+ '</div></article>',
131
+ ].join('');
132
+ }