sqlite-hub 0.11.1 → 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 +52 -13
- package/database.sqlite +0 -0
- package/frontend/js/api.js +64 -12
- package/frontend/js/app.js +723 -37
- package/frontend/js/components/emptyState.js +42 -46
- package/frontend/js/components/modal.js +526 -2
- package/frontend/js/components/queryEditor.js +12 -3
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +345 -12
- package/frontend/js/components/sidebar.js +106 -23
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/store.js +437 -26
- 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 +39 -4
- package/frontend/js/views/editor.js +50 -1
- package/frontend/js/views/settings.js +1 -4
- 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 +95 -95
- package/package.json +2 -3
- package/server/routes/export.js +97 -12
- package/server/services/sqlite/dataBrowserService.js +2 -68
- package/server/services/sqlite/exportService.js +74 -15
- package/server/services/sqlite/introspection.js +209 -1
- package/server/services/sqlite/sqlExecutor.js +31 -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/server/services/sqlite/tableFilter.js +75 -0
- package/server/utils/csv.js +30 -4
- package/tests/check-constraint-options.test.js +90 -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
|
@@ -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,
|
|
@@ -800,6 +806,498 @@ function renderDeleteQueryHistoryForm(modal) {
|
|
|
800
806
|
].join("");
|
|
801
807
|
}
|
|
802
808
|
|
|
809
|
+
function renderQueryExportPreview(lines = []) {
|
|
810
|
+
return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function getExportOptions() {
|
|
814
|
+
return [
|
|
815
|
+
{ label: "CSV", format: "csv", icon: "table_rows", meta: ".csv", preview: ["id,name", "1,Acme"] },
|
|
816
|
+
{ label: "TSV", format: "tsv", icon: "view_column", meta: ".tsv", preview: ["id\tname", "1\tAcme"] },
|
|
817
|
+
{
|
|
818
|
+
label: "Markdown",
|
|
819
|
+
format: "md",
|
|
820
|
+
icon: "article",
|
|
821
|
+
meta: ".md",
|
|
822
|
+
preview: ["| id | name |", "| --- | --- |"],
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
label: "Duplicate",
|
|
826
|
+
format: "table",
|
|
827
|
+
icon: "table_chart",
|
|
828
|
+
meta: "as table",
|
|
829
|
+
preview: ["columns inferred", "rows prefilled"],
|
|
830
|
+
},
|
|
831
|
+
];
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function renderTextExportModal(modal, action) {
|
|
835
|
+
const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
|
|
836
|
+
|
|
837
|
+
return `
|
|
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>
|
|
853
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
854
|
+
${getExportOptions()
|
|
855
|
+
.map(
|
|
856
|
+
(option) => `
|
|
857
|
+
<button
|
|
858
|
+
class="group flex min-h-36 flex-col justify-between border border-outline-variant/20 bg-surface-container-low px-5 py-5 text-left text-on-surface transition-colors hover:border-primary-container/45 hover:bg-surface-container-high focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary-container disabled:cursor-wait disabled:opacity-60"
|
|
859
|
+
data-action="${escapeHtml(action)}"
|
|
860
|
+
data-export-format="${escapeHtml(option.format)}"
|
|
861
|
+
type="button"
|
|
862
|
+
${disabledAttribute}
|
|
863
|
+
>
|
|
864
|
+
<span class="flex w-full items-start justify-between gap-4">
|
|
865
|
+
<span class="flex min-w-0 items-center gap-3">
|
|
866
|
+
<span class="flex h-10 w-10 shrink-0 items-center justify-center border border-outline-variant/20 bg-surface-container-highest text-primary-container group-hover:border-primary-container/35">
|
|
867
|
+
<span class="material-symbols-outlined text-xl">${escapeHtml(option.icon)}</span>
|
|
868
|
+
</span>
|
|
869
|
+
<span class="min-w-0">
|
|
870
|
+
<span class="block truncate font-headline text-lg font-black uppercase tracking-normal text-primary-container">
|
|
871
|
+
${escapeHtml(option.label)}
|
|
872
|
+
</span>
|
|
873
|
+
<span class="mt-1 block font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
874
|
+
${escapeHtml(option.meta)}
|
|
875
|
+
</span>
|
|
876
|
+
</span>
|
|
877
|
+
</span>
|
|
878
|
+
<span class="material-symbols-outlined mt-1 text-base text-on-surface-variant/35 group-hover:text-primary-container">
|
|
879
|
+
arrow_forward
|
|
880
|
+
</span>
|
|
881
|
+
</span>
|
|
882
|
+
<span class="mt-5 block w-full overflow-hidden border border-outline-variant/10 bg-surface-container-lowest px-3 py-3 font-mono text-[11px] leading-5 text-on-surface-variant/70">
|
|
883
|
+
${renderQueryExportPreview(option.preview)}
|
|
884
|
+
</span>
|
|
885
|
+
</button>
|
|
886
|
+
`,
|
|
887
|
+
)
|
|
888
|
+
.join("")}
|
|
889
|
+
</div>
|
|
890
|
+
${renderError(modal.error)}
|
|
891
|
+
<div class="flex justify-end">
|
|
892
|
+
<button class="standard-button" data-action="close-modal" type="button">
|
|
893
|
+
Cancel
|
|
894
|
+
</button>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
`;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderQueryExportModal(modal) {
|
|
901
|
+
return renderTextExportModal(modal, "export-query-format");
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function renderDataExportModal(modal) {
|
|
905
|
+
return renderTextExportModal(modal, "export-data-format");
|
|
906
|
+
}
|
|
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
|
+
|
|
803
1301
|
function renderCreateMediaTaggingMappingTableForm(modal, state) {
|
|
804
1302
|
const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
|
|
805
1303
|
const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
|
|
@@ -981,6 +1479,26 @@ export function renderModal(state) {
|
|
|
981
1479
|
title: "Delete Query",
|
|
982
1480
|
body: renderDeleteQueryHistoryForm(modal),
|
|
983
1481
|
},
|
|
1482
|
+
"query-export": {
|
|
1483
|
+
eyebrow: "SQL Editor // Export query result",
|
|
1484
|
+
title: "Export Query",
|
|
1485
|
+
body: renderQueryExportModal(modal),
|
|
1486
|
+
},
|
|
1487
|
+
"data-export": {
|
|
1488
|
+
eyebrow: "Data Browser // Export table data",
|
|
1489
|
+
title: "Export Table",
|
|
1490
|
+
body: renderDataExportModal(modal),
|
|
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
|
+
},
|
|
984
1502
|
"create-media-tagging-tag-table": {
|
|
985
1503
|
eyebrow: "Media Tagging // Create default tag table",
|
|
986
1504
|
title: "Create Tag Table",
|
|
@@ -1001,7 +1519,13 @@ export function renderModal(state) {
|
|
|
1001
1519
|
|
|
1002
1520
|
return `
|
|
1003
1521
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
1004
|
-
<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)]">
|
|
1005
1529
|
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
1006
1530
|
<div>
|
|
1007
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"
|
|
@@ -81,10 +89,11 @@ export function renderQueryEditor({
|
|
|
81
89
|
</button>
|
|
82
90
|
<button
|
|
83
91
|
class="${secondaryButtonClass}"
|
|
84
|
-
data-action="
|
|
92
|
+
data-action="open-query-export-modal"
|
|
85
93
|
type="button"
|
|
86
94
|
>
|
|
87
|
-
|
|
95
|
+
<span class="material-symbols-outlined text-sm">download</span>
|
|
96
|
+
${exporting ? "Exporting..." : "Export"}
|
|
88
97
|
</button>
|
|
89
98
|
<button
|
|
90
99
|
class="signature-button"
|