sqlite-hub 0.12.0 → 0.16.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 (46) hide show
  1. package/README.md +12 -4
  2. package/frontend/js/api.js +4 -2
  3. package/frontend/js/app.js +632 -12
  4. package/frontend/js/components/modal.js +432 -3
  5. package/frontend/js/components/queryEditor.js +9 -1
  6. package/frontend/js/components/queryResults.js +79 -17
  7. package/frontend/js/components/rowEditorPanel.js +204 -10
  8. package/frontend/js/components/sidebar.js +101 -18
  9. package/frontend/js/components/tableDesignerEditor.js +69 -11
  10. package/frontend/js/store.js +227 -9
  11. package/frontend/js/utils/copyColumnExport.js +117 -0
  12. package/frontend/js/utils/exportFilenames.js +32 -0
  13. package/frontend/js/utils/filePathPreview.js +315 -0
  14. package/frontend/js/utils/format.js +1 -1
  15. package/frontend/js/utils/rowEditorJson.js +65 -0
  16. package/frontend/js/utils/sqlFormatter.js +691 -0
  17. package/frontend/js/utils/tableDesigner.js +178 -6
  18. package/frontend/js/utils/textCellStats.js +20 -0
  19. package/frontend/js/utils/timestampPreview.js +264 -0
  20. package/frontend/js/views/charts.js +3 -1
  21. package/frontend/js/views/data.js +34 -2
  22. package/frontend/js/views/editor.js +48 -1
  23. package/frontend/js/views/settings.js +0 -3
  24. package/frontend/js/views/structure.js +154 -212
  25. package/frontend/styles/base.css +6 -0
  26. package/frontend/styles/components.css +463 -2
  27. package/frontend/styles/structure-graph.css +0 -3
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +1 -1
  30. package/package.json +2 -3
  31. package/server/services/sqlite/introspection.js +10 -0
  32. package/server/services/sqlite/sqlExecutor.js +29 -0
  33. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  34. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  35. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  36. package/server/services/sqlite/tableDesignerService.js +1 -1
  37. package/tests/check-constraint-options.test.js +14 -0
  38. package/tests/export-filenames.test.js +34 -0
  39. package/tests/file-path-preview.test.js +165 -0
  40. package/tests/row-editor-json.test.js +82 -0
  41. package/tests/row-editor-timestamp-preview.test.js +192 -0
  42. package/tests/sql-formatter.test.js +173 -0
  43. package/tests/sql-highlight.test.js +38 -0
  44. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  45. package/tests/text-cell-stats.test.js +38 -0
  46. package/fill.js +0 -526
@@ -1,4 +1,10 @@
1
- import { escapeHtml, highlightSql, truncateMiddle } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from "../utils/format.js";
2
+ import {
3
+ buildCopyColumnPreviewText,
4
+ getCopyColumnActionLabel,
5
+ getCopyColumnExportMetadata,
6
+ isMarkdownTodoCopyColumnMode,
7
+ } from "../utils/copyColumnExport.js";
2
8
  import { renderConnectionLogo } from "./connectionLogo.js";
3
9
  import {
4
10
  analyzeQueryChartResult,
@@ -829,7 +835,21 @@ function renderTextExportModal(modal, action) {
829
835
  const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
830
836
 
831
837
  return `
832
- <div class="space-y-5">
838
+ <div class="space-y-5" data-export-modal>
839
+ <label class="block space-y-2">
840
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
841
+ Filename
842
+ </span>
843
+ <input
844
+ autocomplete="off"
845
+ 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"
846
+ name="filename"
847
+ spellcheck="false"
848
+ type="text"
849
+ value="${escapeHtml(modal.filename ?? "")}"
850
+ ${disabledAttribute}
851
+ />
852
+ </label>
833
853
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
834
854
  ${getExportOptions()
835
855
  .map(
@@ -885,6 +905,399 @@ function renderDataExportModal(modal) {
885
905
  return renderTextExportModal(modal, "export-data-format");
886
906
  }
887
907
 
908
+ function getCopyColumnResult(state, modal) {
909
+ return modal.scope === "charts" ? state.charts.result : state.editor.result;
910
+ }
911
+
912
+ function renderCopyColumnPreview(modal, state) {
913
+ const result = getCopyColumnResult(state, modal);
914
+ const separator = Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
915
+ ? "\n"
916
+ : String(modal.separator ?? ",");
917
+ const wrapper = String(modal.wrapper ?? '"');
918
+ const preview = buildCopyColumnPreviewText({
919
+ result,
920
+ columnName: modal.columnName,
921
+ copyMode: modal.copyMode,
922
+ separator,
923
+ wrapper,
924
+ maxRows: isMarkdownTodoCopyColumnMode(modal.copyMode) ? 10 : 4,
925
+ });
926
+
927
+ if (!preview) {
928
+ return "";
929
+ }
930
+
931
+ return `
932
+ <div class="space-y-2">
933
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
934
+ Preview
935
+ </div>
936
+ <pre class="copy-column-preview custom-scrollbar">${escapeHtml(preview)}</pre>
937
+ </div>
938
+ `;
939
+ }
940
+
941
+ function renderCopyColumnLineBreaksField({ checked = false, disabled = false } = {}) {
942
+ return `
943
+ <label class="block space-y-2">
944
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
945
+ Format
946
+ </span>
947
+ <span class="standard-checkbox ${disabled ? "is-disabled" : ""}">
948
+ <input
949
+ ${checked ? "checked" : ""}
950
+ ${disabled ? "disabled" : ""}
951
+ data-bind="copy-column-format-field"
952
+ data-field="lineBreaks"
953
+ name="lineBreaks"
954
+ type="checkbox"
955
+ />
956
+ <span>Line breaks</span>
957
+ </span>
958
+ </label>
959
+ `;
960
+ }
961
+
962
+ function renderCopyColumnFormatField({ label, name, value = "" }) {
963
+ return `
964
+ <label class="block space-y-2">
965
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
966
+ ${escapeHtml(label)}
967
+ </span>
968
+ <input
969
+ 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"
970
+ data-bind="copy-column-format-field"
971
+ data-field="${escapeHtml(name)}"
972
+ name="${escapeHtml(name)}"
973
+ type="text"
974
+ value="${escapeHtml(value)}"
975
+ />
976
+ </label>
977
+ `;
978
+ }
979
+
980
+ function renderCopyColumnModal(modal, state) {
981
+ const result = getCopyColumnResult(state, modal);
982
+ const rows = result?.rows ?? [];
983
+ const valueCount = modal.copyMode === "first-10" ? Math.min(rows.length, 10) : rows.length;
984
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
985
+ const exportMetadata = getCopyColumnExportMetadata(modal.copyMode);
986
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
987
+ const lineBreaks = isMarkdownTodo || Boolean(modal.lineBreaks);
988
+ const formatFieldsMarkup = isMarkdownTodo
989
+ ? renderCopyColumnLineBreaksField({
990
+ checked: true,
991
+ disabled: true,
992
+ })
993
+ : `
994
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
995
+ ${renderCopyColumnFormatField({
996
+ label: "Separator",
997
+ name: "separator",
998
+ value: modal.separator ?? ",",
999
+ })}
1000
+ ${renderCopyColumnFormatField({
1001
+ label: "Wrapper",
1002
+ name: "wrapper",
1003
+ value: modal.wrapper ?? '"',
1004
+ })}
1005
+ ${renderCopyColumnLineBreaksField({
1006
+ checked: lineBreaks,
1007
+ })}
1008
+ </div>
1009
+ `;
1010
+
1011
+ return `
1012
+ <form class="space-y-5" data-form="copy-column">
1013
+ <input name="scope" type="hidden" value="${escapeHtml(modal.scope ?? "editor")}" />
1014
+ <input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ?? "")}" />
1015
+ <input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ?? "column")}" />
1016
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
1017
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1018
+ Column
1019
+ </div>
1020
+ <div class="mt-2 flex min-w-0 items-center justify-between gap-4">
1021
+ <code class="min-w-0 truncate font-mono text-sm text-primary-container" title="${escapeHtml(
1022
+ modal.columnName ?? ""
1023
+ )}">${escapeHtml(modal.columnName ?? "")}</code>
1024
+ <span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/50">
1025
+ ${escapeHtml(getCopyColumnActionLabel(modal.copyMode))} · ${formatNumber(valueCount)}
1026
+ </span>
1027
+ </div>
1028
+ </div>
1029
+ ${formatFieldsMarkup}
1030
+ ${renderCopyColumnPreview(modal, state)}
1031
+ ${renderError(modal.error)}
1032
+ <div class="flex flex-wrap items-center justify-end gap-3 pt-2">
1033
+ <button
1034
+ class="standard-button"
1035
+ data-action="close-modal"
1036
+ type="button"
1037
+ ${disabledAttribute}
1038
+ >
1039
+ Cancel
1040
+ </button>
1041
+ <button
1042
+ class="standard-button"
1043
+ name="intent"
1044
+ type="submit"
1045
+ value="export"
1046
+ ${disabledAttribute}
1047
+ >
1048
+ ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1049
+ </button>
1050
+ <button
1051
+ class="signature-button"
1052
+ name="intent"
1053
+ type="submit"
1054
+ value="copy"
1055
+ ${disabledAttribute}
1056
+ >
1057
+ ${modal.submitting ? "Working..." : "Copy"}
1058
+ </button>
1059
+ </div>
1060
+ </form>
1061
+ `;
1062
+ }
1063
+
1064
+ function getTableDesignerUniqueConstraintTypeLabel(constraint) {
1065
+ if (constraint.partial) {
1066
+ return "Partial unique index";
1067
+ }
1068
+
1069
+ if ((constraint.columns?.length ?? 0) > 1) {
1070
+ return "Multi-column unique";
1071
+ }
1072
+
1073
+ return "Unique constraint";
1074
+ }
1075
+
1076
+ function renderTableDesignerUniqueConstraintExpression(constraint) {
1077
+ const expression = String(constraint.expression || constraint.sql || "").trim();
1078
+
1079
+ if (expression) {
1080
+ return expression;
1081
+ }
1082
+
1083
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1084
+ return columns.length ? `UNIQUE (${columns.join(", ")})` : "UNIQUE constraint";
1085
+ }
1086
+
1087
+ function renderTableDesignerCheckConstraintExpression(constraint) {
1088
+ return String(constraint.expression || "").trim() || "CHECK constraint";
1089
+ }
1090
+
1091
+ function normalizeTableDesignerConstraintColumnName(name) {
1092
+ return String(name ?? "").trim().toLowerCase();
1093
+ }
1094
+
1095
+ function tableDesignerConstraintIncludesColumn(constraint, columnName) {
1096
+ const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1097
+
1098
+ if (!normalizedColumn) {
1099
+ return false;
1100
+ }
1101
+
1102
+ return (constraint.columns ?? []).some(
1103
+ (constraintColumn) =>
1104
+ normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn
1105
+ );
1106
+ }
1107
+
1108
+ function renderTableDesignerUniqueConstraintEditor(constraint) {
1109
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1110
+
1111
+ return `
1112
+ <article class="table-designer-constraint-editor">
1113
+ <div class="table-designer-constraint-editor__header">
1114
+ <div class="min-w-0">
1115
+ <div class="table-designer-constraint-editor__type">
1116
+ ${escapeHtml(getTableDesignerUniqueConstraintTypeLabel(constraint))}
1117
+ ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ""}
1118
+ </div>
1119
+ <input
1120
+ class="table-designer-field table-designer-constraint-editor__name"
1121
+ data-bind="table-designer-constraint-field"
1122
+ data-constraint-id="${escapeHtml(constraint.id)}"
1123
+ data-constraint-kind="unique"
1124
+ data-field="name"
1125
+ spellcheck="false"
1126
+ type="text"
1127
+ value="${escapeHtml(constraint.name || "UNIQUE constraint")}"
1128
+ />
1129
+ </div>
1130
+ <div class="status-badge status-badge--muted">Rebuild</div>
1131
+ </div>
1132
+ ${
1133
+ columns.length
1134
+ ? `
1135
+ <div class="table-designer-constraint__columns">
1136
+ ${columns.map((column) => `<span>${escapeHtml(column)}</span>`).join("")}
1137
+ </div>
1138
+ `
1139
+ : ""
1140
+ }
1141
+ <textarea
1142
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1143
+ data-bind="table-designer-constraint-field"
1144
+ data-constraint-id="${escapeHtml(constraint.id)}"
1145
+ data-constraint-kind="unique"
1146
+ data-field="expression"
1147
+ spellcheck="false"
1148
+ >${escapeHtml(renderTableDesignerUniqueConstraintExpression(constraint))}</textarea>
1149
+ </article>
1150
+ `;
1151
+ }
1152
+
1153
+ function renderTableDesignerCheckConstraintEditor(constraint) {
1154
+ const columns = constraint.columns ?? [];
1155
+ const allowedValues = columns.flatMap((column) =>
1156
+ (column.allowedValues ?? []).map((value) => ({
1157
+ columnName: column.name,
1158
+ value,
1159
+ }))
1160
+ );
1161
+
1162
+ return `
1163
+ <article class="table-designer-constraint-editor">
1164
+ <div class="table-designer-constraint-editor__header">
1165
+ <div class="min-w-0">
1166
+ <div class="table-designer-constraint-editor__type">
1167
+ ${columns.length ? escapeHtml(columns.map((column) => column.name).join(", ")) : "Table check"}
1168
+ </div>
1169
+ <input
1170
+ class="table-designer-field table-designer-constraint-editor__name"
1171
+ data-bind="table-designer-constraint-field"
1172
+ data-constraint-id="${escapeHtml(constraint.id)}"
1173
+ data-constraint-kind="check"
1174
+ data-field="name"
1175
+ spellcheck="false"
1176
+ type="text"
1177
+ value="${escapeHtml(constraint.name || "CHECK constraint")}"
1178
+ />
1179
+ </div>
1180
+ <div class="status-badge status-badge--muted">Rebuild</div>
1181
+ </div>
1182
+ ${
1183
+ allowedValues.length
1184
+ ? `
1185
+ <div class="table-designer-constraint__values custom-scrollbar">
1186
+ ${allowedValues
1187
+ .map(
1188
+ (entry) => `
1189
+ <span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
1190
+ `
1191
+ )
1192
+ .join("")}
1193
+ </div>
1194
+ `
1195
+ : ""
1196
+ }
1197
+ <textarea
1198
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1199
+ data-bind="table-designer-constraint-field"
1200
+ data-constraint-id="${escapeHtml(constraint.id)}"
1201
+ data-constraint-kind="check"
1202
+ data-field="expression"
1203
+ spellcheck="false"
1204
+ >${escapeHtml(renderTableDesignerCheckConstraintExpression(constraint))}</textarea>
1205
+ </article>
1206
+ `;
1207
+ }
1208
+
1209
+ function renderTableDesignerConstraintSection({ title, count, emptyText, body }) {
1210
+ return `
1211
+ <section class="table-designer-constraints-modal__section">
1212
+ <div class="table-designer-constraints-modal__section-header">
1213
+ <div class="table-designer-constraints-modal__section-title">${escapeHtml(title)}</div>
1214
+ <div class="status-badge status-badge--muted">${formatNumber(count)}</div>
1215
+ </div>
1216
+ ${
1217
+ count
1218
+ ? `<div class="table-designer-constraints__list">${body}</div>`
1219
+ : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
1220
+ }
1221
+ </section>
1222
+ `;
1223
+ }
1224
+
1225
+ function renderTableDesignerConstraintsModal(modal, state) {
1226
+ const draft = state.tableDesigner?.draft;
1227
+
1228
+ if (!draft) {
1229
+ return `
1230
+ <div class="table-designer-constraints-modal__empty">
1231
+ No active table designer draft.
1232
+ </div>
1233
+ <div class="flex items-center justify-end gap-3 pt-2">
1234
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1235
+ </div>
1236
+ `;
1237
+ }
1238
+
1239
+ const selectedColumn = modal.columnId
1240
+ ? (draft.columns ?? []).find((column) => column.id === modal.columnId)
1241
+ : null;
1242
+ const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? "").trim();
1243
+ const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
1244
+ const selectedColumnLabel = selectedColumnName || "Unnamed column";
1245
+ const allUniqueConstraints = draft.mode === "edit" ? draft.uniqueConstraints ?? [] : [];
1246
+ const allCheckConstraints = draft.mode === "edit" ? draft.checkConstraints ?? [] : [];
1247
+ const uniqueConstraints = hasSelectedColumn
1248
+ ? allUniqueConstraints.filter((constraint) =>
1249
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1250
+ )
1251
+ : allUniqueConstraints;
1252
+ const checkConstraints = hasSelectedColumn
1253
+ ? allCheckConstraints.filter((constraint) =>
1254
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1255
+ )
1256
+ : allCheckConstraints;
1257
+ const totalCount = uniqueConstraints.length + checkConstraints.length;
1258
+
1259
+ return `
1260
+ <div class="table-designer-constraints-modal">
1261
+ <div class="table-designer-constraints-modal__summary">
1262
+ <div class="min-w-0">
1263
+ <div class="table-designer-constraints-modal__label">Table</div>
1264
+ <code title="${escapeHtml(draft.tableName ?? "")}">${escapeHtml(draft.tableName ?? "")}</code>
1265
+ </div>
1266
+ ${
1267
+ hasSelectedColumn
1268
+ ? `
1269
+ <div class="min-w-0">
1270
+ <div class="table-designer-constraints-modal__label">Column</div>
1271
+ <code title="${escapeHtml(selectedColumnLabel)}">${escapeHtml(selectedColumnLabel)}</code>
1272
+ </div>
1273
+ `
1274
+ : ""
1275
+ }
1276
+ <div class="status-badge status-badge--primary">V2 · ${formatNumber(totalCount)}</div>
1277
+ </div>
1278
+ ${renderTableDesignerConstraintSection({
1279
+ title: "Check constraints",
1280
+ count: checkConstraints.length,
1281
+ emptyText: hasSelectedColumn
1282
+ ? "No check constraints detected for this column."
1283
+ : "No check constraints detected.",
1284
+ body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(""),
1285
+ })}
1286
+ ${renderTableDesignerConstraintSection({
1287
+ title: hasSelectedColumn ? "Related unique constraints" : "Unique constraints",
1288
+ count: uniqueConstraints.length,
1289
+ emptyText: hasSelectedColumn
1290
+ ? "No related multi-column or partial unique constraints detected."
1291
+ : "No multi-column or partial unique constraints detected.",
1292
+ body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(""),
1293
+ })}
1294
+ <div class="flex items-center justify-end gap-3 pt-2">
1295
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1296
+ </div>
1297
+ </div>
1298
+ `;
1299
+ }
1300
+
888
1301
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
889
1302
  const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
890
1303
  const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
@@ -1076,6 +1489,16 @@ export function renderModal(state) {
1076
1489
  title: "Export Table",
1077
1490
  body: renderDataExportModal(modal),
1078
1491
  },
1492
+ "copy-column": {
1493
+ eyebrow: "Results // Copy or export column values",
1494
+ title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? "Export Markdown Todo" : "Copy column",
1495
+ body: renderCopyColumnModal(modal, state),
1496
+ },
1497
+ "table-designer-constraints": {
1498
+ eyebrow: "Table Designer // Constraints",
1499
+ title: "Checks",
1500
+ body: renderTableDesignerConstraintsModal(modal, state),
1501
+ },
1079
1502
  "create-media-tagging-tag-table": {
1080
1503
  eyebrow: "Media Tagging // Create default tag table",
1081
1504
  title: "Create Tag Table",
@@ -1096,7 +1519,13 @@ export function renderModal(state) {
1096
1519
 
1097
1520
  return `
1098
1521
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1099
- <div class="w-full ${modal.kind === "chart-editor" || modal.kind === "row-update-preview" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1522
+ <div class="w-full ${
1523
+ modal.kind === "chart-editor" ||
1524
+ modal.kind === "row-update-preview" ||
1525
+ modal.kind === "table-designer-constraints"
1526
+ ? "max-w-3xl"
1527
+ : "max-w-xl"
1528
+ } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1100
1529
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
1101
1530
  <div>
1102
1531
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
@@ -69,9 +69,17 @@ export function renderQueryEditor({
69
69
  data-next-value="${historyVisible ? "false" : "true"}"
70
70
  type="button"
71
71
  >
72
- <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "history"}</span>
72
+ <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
73
73
  ${historyVisible ? "Hide History" : "Show History"}
74
74
  </button>
75
+ <button
76
+ class="${secondaryButtonClass}"
77
+ data-action="format-current-query"
78
+ type="button"
79
+ >
80
+ <span class="material-symbols-outlined text-sm">format_align_left</span>
81
+ Format
82
+ </button>
75
83
  <button
76
84
  class="${secondaryButtonClass}"
77
85
  data-action="clear-query"
@@ -9,31 +9,93 @@ function getSortIcon(columnName, sortColumn, sortDirection) {
9
9
  return sortDirection === "desc" ? "south" : "north";
10
10
  }
11
11
 
12
- function renderSortableHeader(columnName, sortColumn, sortDirection) {
12
+ const COPY_COLUMN_ACTIONS = [
13
+ { mode: "column", label: "Copy column" },
14
+ { mode: "column-with-header", label: "Copy column with header" },
15
+ { mode: "first-10", label: "Copy first 10" },
16
+ { mode: "markdown-todo", label: "Export as Markdown Todo" },
17
+ ];
18
+
19
+ function renderColumnActionMenu(columnName, resultScope) {
20
+ return `
21
+ <details class="query-result-column-menu" data-copy-column-menu>
22
+ <summary
23
+ aria-label="Column actions for ${escapeHtml(columnName)}"
24
+ class="query-result-column-menu__toggle"
25
+ title="Column actions"
26
+ >
27
+ <span class="material-symbols-outlined" aria-hidden="true">more_vert</span>
28
+ </summary>
29
+ <div class="query-result-column-menu__panel" role="menu">
30
+ ${COPY_COLUMN_ACTIONS.map(
31
+ (item) => `
32
+ <button
33
+ class="query-result-column-menu__item"
34
+ data-action="open-copy-column-modal"
35
+ data-column-name="${escapeHtml(columnName)}"
36
+ data-copy-mode="${escapeHtml(item.mode)}"
37
+ data-result-scope="${escapeHtml(resultScope)}"
38
+ role="menuitem"
39
+ type="button"
40
+ >
41
+ ${escapeHtml(item.label)}
42
+ </button>
43
+ `
44
+ ).join("")}
45
+ </div>
46
+ </details>
47
+ `;
48
+ }
49
+
50
+ function renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }) {
13
51
  const isActive = columnName === sortColumn;
52
+ const labelMarkup = `<span class="query-result-column-label truncate" title="${escapeHtml(columnName)}">${escapeHtml(
53
+ columnName
54
+ )}</span>`;
14
55
 
15
56
  return `
16
- <button
17
- class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
18
- isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
19
- }"
20
- data-action="sort-editor-results-column"
57
+ <div
58
+ class="query-result-column-header"
21
59
  data-column-name="${escapeHtml(columnName)}"
22
- type="button"
60
+ data-result-column-header
61
+ data-result-scope="${escapeHtml(resultScope)}"
23
62
  >
24
- <span class="truncate">${escapeHtml(columnName)}</span>
25
- <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
26
- columnName,
27
- sortColumn,
28
- sortDirection
29
- )}</span>
30
- </button>
63
+ ${
64
+ sortAction
65
+ ? `
66
+ <button
67
+ class="query-result-column-sort ${
68
+ isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
69
+ }"
70
+ data-action="${escapeHtml(sortAction)}"
71
+ data-column-name="${escapeHtml(columnName)}"
72
+ type="button"
73
+ >
74
+ ${labelMarkup}
75
+ <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
76
+ columnName,
77
+ sortColumn,
78
+ sortDirection
79
+ )}</span>
80
+ </button>
81
+ `
82
+ : `<span class="query-result-column-static text-on-surface-variant">${labelMarkup}</span>`
83
+ }
84
+ ${renderColumnActionMenu(columnName, resultScope)}
85
+ </div>
31
86
  `;
32
87
  }
33
88
 
34
89
  export function renderQueryResultsPane(
35
90
  result,
36
- { selectedRowIndex = null, editable = false, sortColumn = null, sortDirection = null } = {}
91
+ {
92
+ selectedRowIndex = null,
93
+ editable = false,
94
+ sortColumn = null,
95
+ sortDirection = null,
96
+ resultScope = "editor",
97
+ sortAction = "sort-editor-results-column",
98
+ } = {}
37
99
  ) {
38
100
  if (!result) {
39
101
  return `
@@ -48,8 +110,8 @@ export function renderQueryResultsPane(
48
110
 
49
111
  const columns = (result.columns ?? []).map((columnName) => ({
50
112
  headerClassName:
51
- "border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
52
- renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
113
+ "query-result-header-cell border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
114
+ renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, { resultScope, sortAction }),
53
115
  cellClassName: "px-4 py-3 align-top text-on-surface",
54
116
  render: (row) => {
55
117
  const value = formatCellValue(row[columnName]);