mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +00:00
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:
@@ -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) => {
|
||||
|
||||
+156
-805
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user