feat(desktop): paste copied tree table as clone

This commit is contained in:
t8y2 committed 2026-06-02 02:06:36 +08:00
1 parent 02d07a4593
commit 0d89ee3fe9
2 files changed
+69 -5

No files matched your search

@@ -153,6 +153,7 @@ const { toast } = useToast();
const { highlight } = useSqlHighlighter();
type StructureCopyFormat = "tsv" | "markdown";
type DuplicateStructureSource = TreeNode & { connectionId: string; database: string };
const { getDatabaseOptions } = useDatabaseOptions();
const showVisibleDatabasesDialog = ref(false);
@@ -519,6 +520,12 @@ function isEditableShortcutTarget(target: EventTarget | null): boolean {
function onKeydown(event: KeyboardEvent) {
if ((!isSelected.value && !isMultiSelected.value) || isEditableShortcutTarget(event.target)) return;
if (isPasteTreeClipboardShortcut(event)) {
if (!requestPasteTreeClipboard()) return;
event.preventDefault();
event.stopPropagation();
return;
}
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F2") {
if (!requestRenameSelectedNode()) return;
event.preventDefault();
@@ -548,6 +555,24 @@ function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
return event.key === "Delete" || event.key === "Backspace";
}
function isPasteTreeClipboardShortcut(event: KeyboardEvent): boolean {
return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "v";
}
function requestPasteTreeClipboard(): boolean {
const clipboard = connectionStore.treeClipboard;
if (clipboard?.kind !== "table-structure") return false;
duplicateStructure({
id: `clipboard:${clipboard.connectionId}:${clipboard.database}:${clipboard.schema || ""}:${clipboard.tableName}`,
type: "table",
label: clipboard.tableName,
connectionId: clipboard.connectionId,
database: clipboard.database,
schema: clipboard.schema,
});
return true;
}
function requestRefreshSelectedNode(): boolean {
if (!canRefreshTreeNodeShortcut()) return false;
void refresh();
@@ -842,6 +867,7 @@ async function confirmDelete() {
}
async function copyName() {
updateTreeClipboardForNodes([props.node]);
try {
await copyToClipboard(copyNameForTreeNode(props.node));
toast(t("connection.copied"), 2000);
@@ -854,6 +880,7 @@ async function copySelectedNames() {
const selectedNodes = selectedTreeNodesInVisibleOrder();
const nodes =
selectedNodes.length > 1 && selectedNodes.some((node) => node.id === props.node.id) ? selectedNodes : [props.node];
updateTreeClipboardForNodes(nodes);
try {
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"));
toast(t("connection.copied"), 2000);
@@ -862,6 +889,25 @@ async function copySelectedNames() {
}
}
function updateTreeClipboardForNodes(nodes: TreeNode[]) {
const tableNodes = nodes.filter(
(node): node is DuplicateStructureSource =>
node.type === "table" && !!node.connectionId && !!node.database && typeof node.label === "string",
);
if (nodes.length !== 1 || tableNodes.length !== 1) {
connectionStore.treeClipboard = null;
return;
}
const table = tableNodes[0]!;
connectionStore.treeClipboard = {
kind: "table-structure",
connectionId: table.connectionId,
database: table.database,
schema: table.schema,
tableName: table.label,
};
}
async function duplicateConnection() {
const connId = props.node.connectionId;
if (!connId) return;
@@ -901,6 +947,7 @@ const dropDatabasePreviewSql = ref("");
const dropSchemaPreviewSql = ref("");
const showDuplicateDialog = ref(false);
const duplicateTableName = ref("");
const duplicateStructureSource = ref<DuplicateStructureSource | null>(null);
const showCreateDatabaseDialog = ref(false);
const createDatabaseName = ref("");
@@ -1712,20 +1759,27 @@ async function confirmDropSchema() {
}
}
function duplicateStructure() {
duplicateTableName.value = `${props.node.label}_copy`;
function duplicateStructure(source: TreeNode = props.node) {
if (!isDuplicateStructureSource(source)) return;
duplicateStructureSource.value = source;
duplicateTableName.value = `${source.label}_copy`;
showDuplicateDialog.value = true;
}
function isDuplicateStructureSource(node: TreeNode): node is DuplicateStructureSource {
return node.type === "table" && !!node.connectionId && !!node.database;
}
async function confirmDuplicateStructure() {
const node = props.node;
const node = duplicateStructureSource.value || (isDuplicateStructureSource(props.node) ? props.node : null);
const newName = duplicateTableName.value.trim();
if (!newName || !node.connectionId || !node.database) return;
if (!newName || !node) return;
showDuplicateDialog.value = false;
try {
await connectionStore.ensureConnected(node.connectionId);
const databaseType = connectionStore.getConfig(node.connectionId)?.db_type;
const sql = await buildDuplicateTableStructureSql({
databaseType: currentDatabaseType(),
databaseType,
schema: node.schema,
sourceName: node.label,
targetName: newName,