feat: editor enhancements, DataGrid refactor, and sidebar improvements

- Add bracket auto-close and matching to SQL editor (closeBrackets, bracketMatching)
- Show truncation warning banner when query results exceed 10,000 rows
- Extract shared export format functions (CSV/JSON/SQL INSERT) to lib/exportFormats.ts
- Add progressive rendering to sidebar tree (50-node cap with "show more")
- Fix connection node expanding before connect succeeds
- Split DataGrid.vue (3177→2535 lines) into 4 composables:
  useDataGridExport, useDataGridColumnResize, useDataGridSelection, useDataGridEditor
This commit is contained in:
t8y2
2026-05-11 01:08:07 +08:00
parent 49f03b0c69
commit 14680238b8
13 changed files with 1304 additions and 839 deletions
+6 -2
View File
@@ -210,8 +210,9 @@ onMounted(async () => {
{ EditorState, Compartment, Prec },
{ sql, MSSQL, MySQL, PostgreSQL, SQLDialect },
{ basicSetup },
{ autocompletion, startCompletion },
{ autocompletion, startCompletion, closeBrackets, closeBracketsKeymap },
{ indentWithTab },
{ bracketMatching },
] = await Promise.all([
import("@codemirror/view"),
import("@codemirror/state"),
@@ -219,6 +220,7 @@ onMounted(async () => {
import("codemirror"),
import("@codemirror/autocomplete"),
import("@codemirror/commands"),
import("@codemirror/language"),
]);
editorViewModule = { EditorView, keymap } as typeof import("@codemirror/view");
fontThemeComp = new Compartment();
@@ -285,7 +287,9 @@ onMounted(async () => {
override: [async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos)],
}),
codeMirrorTheme.of(theme),
Prec.highest(keymap.of([indentWithTab])),
closeBrackets(),
bracketMatching(),
Prec.highest(keymap.of([...closeBracketsKeymap, indentWithTab])),
runKeymap,
wordWrapComp.of(ss.wordWrap ? EditorView.lineWrapping : []),
EditorView.updateListener.of((update) => {
File diff suppressed because it is too large Load Diff
+26 -1
View File
@@ -145,6 +145,23 @@ const filteredNodes = computed(() => {
return nodes;
});
const TOP_LEVEL_PAGE_SIZE = 50;
const topLevelDisplayLimit = ref(TOP_LEVEL_PAGE_SIZE);
const visibleFilteredNodes = computed(() => filteredNodes.value.slice(0, topLevelDisplayLimit.value));
const hasMoreTopLevel = computed(() => filteredNodes.value.length > topLevelDisplayLimit.value);
const remainingTopLevelCount = computed(() => filteredNodes.value.length - topLevelDisplayLimit.value);
function showMoreTopLevel() {
topLevelDisplayLimit.value += TOP_LEVEL_PAGE_SIZE;
}
watch(filteredNodes, () => {
topLevelDisplayLimit.value = TOP_LEVEL_PAGE_SIZE;
});
const pendingRenameGroupId = ref<string | null>(null);
function createNewGroup() {
@@ -227,7 +244,7 @@ function onSearchToggle(node: TreeNode) {
</div>
</div>
<TreeItem
v-for="node in filteredNodes"
v-for="node in visibleFilteredNodes"
:key="node.id"
:node="node"
:depth="0"
@@ -236,6 +253,14 @@ function onSearchToggle(node: TreeNode) {
@search-toggle="onSearchToggle"
@rename-started="pendingRenameGroupId = null"
/>
<div
v-if="hasMoreTopLevel"
class="flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-accent text-xs text-muted-foreground"
style="padding-left: 8px"
@click="showMoreTopLevel"
>
<span>{{ t("sidebar.showMore", { count: Math.min(TOP_LEVEL_PAGE_SIZE, remainingTopLevelCount) }) }}</span>
</div>
<div v-if="store.treeNodes.length === 0" class="px-3 py-8 text-center text-muted-foreground text-xs">
{{ t("sidebar.noConnections") }}
</div>
+14 -31
View File
@@ -80,6 +80,7 @@ import {
usesFetchFirst,
} from "@/lib/databaseCapabilities";
import { treeNodeRowAction } from "@/lib/treeNodeClick";
import { formatCsv, formatJson, formatSqlInsert } from "@/lib/exportFormats";
import { hexToRgba } from "@/lib/color";
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
import { isTauriRuntime } from "@/lib/tauriRuntime";
@@ -235,14 +236,10 @@ async function toggle() {
return;
}
const showSavedSqlWhileLoading =
node.type === "connection" && !node.isExpanded && node.children?.some((child) => child.type === "saved-sql-root");
if (node.isExpanded) {
node.isExpanded = false;
return;
}
if (showSavedSqlWhileLoading) node.isExpanded = true;
try {
if (node.type === "connection" && node.connectionId) {
@@ -938,38 +935,17 @@ async function exportData(format: "csv" | "json" | "sql") {
if (format === "csv") {
ext = "csv";
const esc = (v: string) => `"${v.replace(/"/g, '""')}"`;
const header = result.columns.map(esc).join(",");
const body = result.rows.map((row) => row.map((c) => esc(c === null ? "" : String(c))).join(",")).join("\n");
content = `${header}\n${body}`;
content = formatCsv(result.columns, result.rows);
} else if (format === "json") {
ext = "json";
const data = result.rows.map((row) => {
const obj: Record<string, unknown> = {};
result.columns.forEach((col, i) => {
obj[col] = row[i];
});
return obj;
});
content = JSON.stringify(data, null, 2);
content = formatJson(result.columns, result.rows);
} else {
ext = "sql";
const cols = result.columns.map((c) => quoteIdent(c)).join(", ");
const lines = result.rows.map((row) => {
const vals = row
.map((v) => {
if (v === null) return "NULL";
if (typeof v === "number" || typeof v === "boolean") return String(v);
return `'${String(v).replace(/'/g, "''")}'`;
})
.join(", ");
return `INSERT INTO ${qualifiedName} (${cols}) VALUES (${vals});`;
});
content = lines.join("\n");
content = formatSqlInsert(qualifiedName, result.columns, result.rows, quoteIdent);
}
await saveFileContent(content, `${node.label}.${ext}`, ext.toUpperCase(), ext);
toast(t("grid.exported"));
toast(result.truncated ? t("grid.exported") + " (truncated)" : t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
@@ -996,7 +972,7 @@ async function exportDataXlsx() {
rows: result.rows,
});
await saveBinaryFileContent(workbook, `${node.label}.xlsx`, "Excel", "xlsx");
toast(t("grid.exported"));
toast(result.truncated ? t("grid.exported") + " (truncated)" : t("grid.exported"));
} catch (e: any) {
toast(t("grid.exportFailed", { message: e?.message || String(e) }), 5000);
}
@@ -1215,7 +1191,7 @@ const rowStyle = computed(() => {
};
});
const CHILDREN_PAGE_SIZE = 100;
const CHILDREN_PAGE_SIZE = 50;
const displayLimit = ref(CHILDREN_PAGE_SIZE);
const visibleChildren = computed(() => {
@@ -1231,6 +1207,13 @@ function togglePin() {
connectionStore.toggleTreeNodePin(props.node.id);
}
watch(
() => props.node.isExpanded,
(expanded) => {
if (!expanded) displayLimit.value = CHILDREN_PAGE_SIZE;
},
);
async function showMore() {
if ((props.node.children?.length ?? 0) > displayLimit.value) {
displayLimit.value += CHILDREN_PAGE_SIZE;