Files
dbx/apps/desktop/src/lib/sidebar/treeNodeClick.ts
T

205 lines
10 KiB
TypeScript

import type { DatabaseType, ObjectSourceKind, TreeNode, TreeNodeType } from "@/types/database";
import { customTypeCapabilities, supportsTypeObjectSource } from "@/lib/database/databaseObjectCapabilities";
import { matchesShortcut, type ShortcutLikeEvent } from "@/lib/editor/keyboardShortcuts";
export type TreeNodeRowAction = "open-data" | "open-source" | "open-extension-details" | "open-saved-sql" | "open-object-browser" | "open-object-browser-and-expand" | "toggle" | "none";
export type TreeNodeRowDoubleClickAction = "open-data" | "activate-data" | "open-database-browser" | "open-object-browser" | "open-object-browser-and-expand" | "open-source" | "open-extension-details" | "open-saved-sql" | "toggle" | "none";
export type SidebarSelectionCopyAction = "copy-name" | "none";
export type SidebarActivation = "single" | "double";
const dataNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view"]);
const documentBrowserNodeTypes = new Set<TreeNodeType>(["mongo-collection", "mongo-bucket", "dynamodb-table"]);
const toggleLeafNodeTypes = new Set<TreeNodeType>([
"redis-db",
"mq-tenant",
"mqtt-topic",
"etcd-root",
"etcd-dashboard",
"etcd-access-control",
"nacos-namespace",
"nacos-access-control",
"zookeeper-root",
"consul-root",
"consul-overview",
"mongo-gridfs",
"mongo-collection",
"mongo-bucket",
"dynamodb-table",
"vector-collection",
"elasticsearch-index",
"meilisearch-system",
"user-admin",
"dameng-users",
"dameng-roles",
]);
// These are application entry points rather than database objects. They should
// always navigate on a single click, even when the user prefers double-click
// activation for ordinary tree objects.
const directNavigationTreeNodeTypes = new Set<TreeNodeType>(["consul-root", "consul-overview", "etcd-root", "etcd-dashboard", "etcd-access-control", "nacos-namespace", "nacos-access-control", "meilisearch-system"]);
const repeatableNavigationTreeNodeTypes = new Set<TreeNodeType>(["etcd-root", "etcd-dashboard", "etcd-access-control", "nacos-namespace", "nacos-access-control"]);
export function isDirectNavigationTreeNode(type: TreeNodeType): boolean {
return directNavigationTreeNodeTypes.has(type);
}
export function isRepeatableNavigationTreeNode(type: TreeNodeType): boolean {
return repeatableNavigationTreeNodeTypes.has(type);
}
export function shouldActivateTreeNodeOnSingleClick(type: TreeNodeType, activation: SidebarActivation = "single"): boolean {
return activation !== "double" || isDirectNavigationTreeNode(type);
}
const databaseActivationNodeTypes = new Set<TreeNodeType>(["database", "schema", "mongo-db"]);
export function shouldBrowseObjectsOnDatabaseActivation(type: TreeNodeType, enabled: boolean): boolean {
return enabled && databaseActivationNodeTypes.has(type);
}
const sourceNodeTypes = new Set<TreeNodeType>(["materialized_view", "procedure", "function", "trigger", "event", "sequence", "synonym", "job", "package", "package-body", "type", "type-body"]);
const savedSqlNodeTypes = new Set<TreeNodeType>(["saved-sql-file"]);
const tableChildGroupNodeTypes = new Set<TreeNodeType>(["group-columns", "group-indexes", "group-fkeys", "group-triggers", "group-constraints", "group-partitions", "group-table-partitions", "group-table-subpartitions"]);
const databaseChildGroupNodeTypes = new Set<TreeNodeType>([
"group-tables",
"group-dolt-system-tables",
"group-views",
"group-materialized-views",
"group-procedures",
"group-functions",
"group-triggers",
"group-events",
"group-sequences",
"group-synonyms",
"group-jobs",
"group-packages",
"group-types",
]);
const displayPathObjectNodeTypes = new Set<TreeNodeType>(["table", "view", "materialized_view", "procedure", "function", "trigger", "event"]);
export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKind | null {
if (type === "view") return "VIEW";
if (type === "materialized_view") return "MATERIALIZED_VIEW";
if (type === "procedure") return "PROCEDURE";
if (type === "function") return "FUNCTION";
if (type === "trigger") return "TRIGGER";
if (type === "event") return "EVENT";
if (type === "sequence") return "SEQUENCE";
if (type === "synonym") return "SYNONYM";
if (type === "job") return "JOB";
if (type === "package") return "PACKAGE";
if (type === "package-body") return "PACKAGE_BODY";
if (type === "type") return "TYPE";
if (type === "type-body") return "TYPE_BODY";
return null;
}
export interface TreeNodeObjectSourceTarget {
name: string;
schema?: string;
objectType: ObjectSourceKind;
signature?: string;
}
export function objectSourceTargetForTreeNode(node: TreeNode): TreeNodeObjectSourceTarget | null {
if ((node.type === "procedure" || node.type === "function") && node.parentType === "package" && node.parentName) {
return {
name: node.parentName,
schema: node.parentSchema || node.schema,
objectType: "PACKAGE",
signature: node.signature,
};
}
const objectType = objectSourceKindForTreeNode(node.type);
if (!objectType) return null;
return {
name: node.objectName || node.label,
schema: node.schema,
objectType,
signature: node.signature,
};
}
export function isDocumentBrowserTreeNode(type: TreeNodeType): boolean {
return documentBrowserNodeTypes.has(type);
}
/**
* Whether a tree node type may open an object source dialog for the given
* connection type. TYPE/TYPE_BODY only have a real source implementation on
* Xugu; other databases list types without a DDL getter this cycle.
*/
function canOpenTreeNodeSource(type: TreeNodeType, dbType?: DatabaseType): boolean {
if (type === "type" || type === "type-body") return supportsTypeObjectSource(dbType);
return true;
}
export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activation: SidebarActivation = "single", dbType?: DatabaseType, browseObjectsOnDatabaseActivation = false, canOpenObjectBrowser = false): TreeNodeRowAction {
if (!shouldActivateTreeNodeOnSingleClick(type, activation)) return "none";
if (canOpenObjectBrowser && shouldBrowseObjectsOnDatabaseActivation(type, browseObjectsOnDatabaseActivation)) {
return canExpand ? "open-object-browser-and-expand" : "open-object-browser";
}
if (type === "extension") return "open-extension-details";
if (savedSqlNodeTypes.has(type)) return "open-saved-sql";
if (dataNodeTypes.has(type)) return "open-data";
// PostgreSQL-family custom types: open read-only details (toggle when expandable).
if (type === "type" && customTypeCapabilities(dbType).details) return canExpand ? "toggle" : "none";
// Xugu and other databases: expandable package/type nodes toggle their members.
if ((type === "package" || type === "type") && canExpand) return "toggle";
if (sourceNodeTypes.has(type) && canOpenTreeNodeSource(type, dbType)) return "open-source";
if (toggleLeafNodeTypes.has(type)) return "toggle";
if (canExpand) return "toggle";
return "none";
}
export function shouldRunTreeNodeRowAction(action: TreeNodeRowAction, clickDetail: number, allowRepeatedToggle = false): boolean {
if (action === "toggle" && allowRepeatedToggle) return true;
// Double-clicks emit a second click before dblclick; leave that event to the
// double-click handler so expandable database rows do not toggle first.
return action !== "none" && clickDetail <= 1;
}
export function treeNodeRowDoubleClickAction(type: TreeNodeType, canOpenObjectBrowser: boolean, activation: SidebarActivation = "single", canExpand = false, dbType?: DatabaseType, canOpenDatabaseBrowser = false, browseObjectsOnDatabaseActivation = false): TreeNodeRowDoubleClickAction {
// Single-click activation already handles the first click in a dblclick
// sequence. Only double-click activation needs a second-stage table action.
if (type === "table") return activation === "double" ? "activate-data" : "none";
// 双击激活模式下,双击连接节点应当展开树(与单击模式下单击展开一致),
// “打开数据库浏览”只保留给单击激活模式下的双击手势。
if (type === "connection" && canOpenDatabaseBrowser && activation !== "double") return "open-database-browser";
if (activation === "double") {
if (canOpenObjectBrowser && shouldBrowseObjectsOnDatabaseActivation(type, browseObjectsOnDatabaseActivation)) {
return canExpand ? "open-object-browser-and-expand" : "open-object-browser";
}
if (canOpenObjectBrowser && type === "object-browser") return "open-object-browser";
if (type === "extension") return "open-extension-details";
if (dataNodeTypes.has(type)) return "open-data";
if (type === "type" && customTypeCapabilities(dbType).details) return canExpand ? "toggle" : "none";
if (sourceNodeTypes.has(type) && canOpenTreeNodeSource(type, dbType)) return "open-source";
if (savedSqlNodeTypes.has(type)) return "open-saved-sql";
if (toggleLeafNodeTypes.has(type)) return "toggle";
if (canExpand) return "toggle";
}
return "none";
}
export function sidebarSelectionCopyAction(event: ShortcutLikeEvent, platform?: string): SidebarSelectionCopyAction {
return matchesShortcut(event, "Mod+C", platform) ? "copy-name" : "none";
}
export function copyNameForTreeNode(node: TreeNode): string {
if (tableChildGroupNodeTypes.has(node.type) && node.tableName) return node.tableName;
if (databaseChildGroupNodeTypes.has(node.type)) return node.schema || node.database || node.label;
if (node.type === "column") {
if (node.meta && "name" in node.meta) return node.meta.name;
return node.label.replace(/\s+\(.+\)$/, "");
}
return node.label;
}
export function copyDisplayPathForTreeNode(node: TreeNode, connectionName: string): string | null {
const connection = connectionName.trim();
const database = node.database?.trim();
if (!connection || !database) return null;
if (node.type === "database") return `${connection}.${database}`;
if (!displayPathObjectNodeTypes.has(node.type)) return null;
const objectName = (node.objectName || (node.type === "table" ? node.tableName : undefined) || node.label).trim();
return objectName ? `${connection}.${database}.${objectName}` : null;
}