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.
- package/README.md +12 -4
- package/frontend/js/api.js +4 -2
- package/frontend/js/app.js +632 -12
- package/frontend/js/components/modal.js +432 -3
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +101 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +227 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +0 -3
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +463 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/package.json +2 -3
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- 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 ${
|
|
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" : "
|
|
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
|
-
|
|
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
|
-
<
|
|
17
|
-
class="
|
|
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
|
-
|
|
60
|
+
data-result-column-header
|
|
61
|
+
data-result-scope="${escapeHtml(resultScope)}"
|
|
23
62
|
>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
{
|
|
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]);
|