feat(sidebar): add connection copy paste shortcuts
This commit is contained in:
parent
d8047a77b4
commit
90d2ede740
|
|
@ -1019,7 +1019,7 @@ function copySingleTableToClipboard(row: ObjectBrowserRow) {
|
|||
|
||||
function openPasteTableDialog() {
|
||||
const clipboard = connectionStore.treeClipboard;
|
||||
if (!canPasteTableClipboard() || !clipboard) {
|
||||
if (!canPasteTableClipboard() || clipboard?.kind !== "table-copy") {
|
||||
toast(t("contextMenu.noTableToPaste"), 2000);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import { useSettingsStore } from "@/stores/settingsStore";
|
|||
import { useToast } from "@/composables/useToast";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
import { filterSidebarSearchRootsByConnectionState, filterSidebarTree } from "@/lib/sidebarSearchTree";
|
||||
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { copyNameForTreeNode } from "@/lib/treeNodeClick";
|
||||
import { copyToClipboard, eventTargetAllowsAppClipboardShortcut } from "@/lib/clipboard";
|
||||
import { copyToClipboard } from "@/lib/clipboard";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionEditTarget } from "@/lib/sidebarConnectionSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebarTypeSearch";
|
||||
import { usesTreeSchemaMode } from "@/lib/databaseFeatureSupport";
|
||||
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
|
|
@ -781,15 +782,22 @@ function focusSearchAtEnd() {
|
|||
function onWindowKeydown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented) return;
|
||||
if (sidebarShortcutTargetIsActive(event.target)) {
|
||||
if (eventTargetAllowsAppClipboardShortcut(event, "c")) {
|
||||
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isEditConnectionShortcut(event)) {
|
||||
if (requestSelectedConnectionEdit()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isCopySidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts)) {
|
||||
if (copySelectedSidebarNames()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (eventTargetAllowsAppClipboardShortcut(event, "v")) {
|
||||
if (requestSelectedSidebarPasteTable()) {
|
||||
if (sidebarShortcutTargetAllowsAppShortcut(event.target) && isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts)) {
|
||||
if (requestSelectedSidebarPaste()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
|
@ -820,14 +828,39 @@ function sidebarShortcutTargetIsActive(target: EventTarget | null): boolean {
|
|||
return pointerInsideTree.value && (!active || active === document.body || root.contains(active));
|
||||
}
|
||||
|
||||
function sidebarShortcutTargetAllowsAppShortcut(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return true;
|
||||
return !(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target.isContentEditable || !!target.closest("[contenteditable='true'], [role='textbox']"));
|
||||
}
|
||||
|
||||
function selectedSidebarNodesInVisibleOrder(): TreeNode[] {
|
||||
const selectedIds = new Set(store.selectedTreeNodeIds);
|
||||
return visibleNodes.value.filter((node) => selectedIds.has(node.id));
|
||||
}
|
||||
|
||||
function isEditConnectionShortcut(event: KeyboardEvent): boolean {
|
||||
return isEditSidebarConnectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
||||
}
|
||||
|
||||
function requestSelectedConnectionEdit(): boolean {
|
||||
const selectedNodeId = store.selectedTreeNodeId;
|
||||
const currentNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
|
||||
if (!currentNode) return false;
|
||||
const editTarget = selectedConnectionEditTarget(currentNode, selectedSidebarNodesInVisibleOrder());
|
||||
if (!editTarget) return false;
|
||||
store.startEditing(editTarget.connectionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function copySelectedSidebarNames(): boolean {
|
||||
const nodes = selectedSidebarNodesInVisibleOrder();
|
||||
if (nodes.length === 0) return false;
|
||||
const connectionNodes = selectedConnectionClipboardNodes(nodes);
|
||||
if (connectionNodes.length > 0) {
|
||||
const copiedCount = store.copyConnectionsToTreeClipboard(connectionNodes.map((node) => node.connectionId));
|
||||
if (copiedCount > 0) toast(t("connection.copied"), 2000);
|
||||
return copiedCount > 0;
|
||||
}
|
||||
const tableNodes = nodes.filter((node) => node.type === "table" && !!node.connectionId && !!node.database);
|
||||
store.treeClipboard =
|
||||
tableNodes.length > 0
|
||||
|
|
@ -847,9 +880,20 @@ function copySelectedSidebarNames(): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
function requestSelectedSidebarPasteTable(): boolean {
|
||||
function requestSelectedSidebarPaste(): boolean {
|
||||
const clipboard = store.treeClipboard;
|
||||
const selectedNodeId = store.selectedTreeNodeId;
|
||||
if (clipboard?.kind === "connection-copy") {
|
||||
const selectedNode = selectedNodeId ? visibleNodes.value.find((node) => node.id === selectedNodeId) : null;
|
||||
const targetGroupId = connectionPasteTargetGroupId(selectedNode, (connectionId) => store.groupIdForConnection(connectionId));
|
||||
void store
|
||||
.pasteConnectionClipboard(targetGroupId)
|
||||
.then((count) => {
|
||||
if (count > 0) toast(count > 1 ? t("connection.duplicatedSelected", { count }) : t("connection.duplicated"), 2000);
|
||||
})
|
||||
.catch((e: any) => toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000));
|
||||
return true;
|
||||
}
|
||||
if (clipboard?.kind !== "table-copy" || clipboard.tables.length === 0 || !selectedNodeId) return false;
|
||||
window.dispatchEvent(new CustomEvent("dbx:sidebar-request-paste-table", { detail: { nodeId: selectedNodeId } }));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ import { clearActiveTableReferencePayload, createTableReferencePayload, createTa
|
|||
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/tableEditing";
|
||||
import { tableOpenPageLimit } from "@/lib/tableOpenPageLimit";
|
||||
import { supportsDatabaseCreation, supportsDatabaseSearch, supportsFieldLineage, supportsObjectBrowserTreeNode, supportsSchemaDiagram, supportsSqlFileExecution, supportsTableImport, supportsTableTruncate, supportsTableStructureEditing, usesTreeSchemaMode } from "@/lib/databaseCapabilities";
|
||||
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, sidebarSelectionCopyAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/treeNodeClick";
|
||||
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/treeNodeClick";
|
||||
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { formatSqlInsert } from "@/lib/exportFormats";
|
||||
import { joinExportedDdls } from "@/lib/ddlExport";
|
||||
import { fetchTableDataForExport } from "@/lib/tableDataExport";
|
||||
|
|
@ -118,7 +119,7 @@ import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableD
|
|||
import { sidebarDisplayTableName } from "@/lib/sidebarTableNameDisplay";
|
||||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebarLabelTooltip";
|
||||
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebarTreeSelection";
|
||||
import { selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets } from "@/lib/sidebarConnectionSelection";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionDeleteTargets, selectedConnectionDuplicateTargets, selectedConnectionEditTarget } from "@/lib/sidebarConnectionSelection";
|
||||
import { supportsDatabaseUserAdmin } from "@/lib/databaseUserAdmin";
|
||||
import { canCloseSidebarDatabaseConnection, isSidebarDatabaseOpened } from "@/lib/sidebarDatabaseOpenState";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebarTreeContext";
|
||||
|
|
@ -842,6 +843,12 @@ function onKeydown(event: KeyboardEvent) {
|
|||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (isEditConnectionShortcut(event)) {
|
||||
if (!requestEditSelectedConnection()) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (!event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "F2") {
|
||||
if (!requestRenameSelectedNode()) return;
|
||||
event.preventDefault();
|
||||
|
|
@ -860,8 +867,7 @@ function onKeydown(event: KeyboardEvent) {
|
|||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const action = sidebarSelectionCopyAction(event);
|
||||
if (action !== "copy-name") return;
|
||||
if (!isCopyTreeSelectionShortcut(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
copySelectedNames();
|
||||
|
|
@ -872,7 +878,15 @@ function isDeleteTreeNodeShortcut(event: KeyboardEvent): boolean {
|
|||
}
|
||||
|
||||
function isPasteTreeClipboardShortcut(event: KeyboardEvent): boolean {
|
||||
return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "v";
|
||||
return isPasteSidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
||||
}
|
||||
|
||||
function isEditConnectionShortcut(event: KeyboardEvent): boolean {
|
||||
return isEditSidebarConnectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
||||
}
|
||||
|
||||
function isCopyTreeSelectionShortcut(event: KeyboardEvent): boolean {
|
||||
return isCopySidebarSelectionShortcut(event, settingsStore.editorSettings.shortcuts);
|
||||
}
|
||||
|
||||
function pasteTableTargetContext(): TableClipboardContext | null {
|
||||
|
|
@ -891,6 +905,16 @@ function canPasteTreeClipboardToCurrentNode(): boolean {
|
|||
|
||||
function requestPasteTreeClipboard(): boolean {
|
||||
const clipboard = connectionStore.treeClipboard;
|
||||
if (clipboard?.kind === "connection-copy") {
|
||||
const targetGroupId = connectionPasteTargetGroupId(props.node, (connectionId) => connectionStore.groupIdForConnection(connectionId));
|
||||
void connectionStore
|
||||
.pasteConnectionClipboard(targetGroupId)
|
||||
.then((count) => {
|
||||
if (count > 0) toast(count > 1 ? t("connection.duplicatedSelected", { count }) : t("connection.duplicated"), 2000);
|
||||
})
|
||||
.catch((e: any) => toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000));
|
||||
return true;
|
||||
}
|
||||
if (clipboard?.kind !== "table-copy" || !canPasteTreeClipboardToCurrentNode()) return false;
|
||||
pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());
|
||||
pasteTableEntries.value = clipboard.tables.map((entry) => ({
|
||||
|
|
@ -927,6 +951,11 @@ function canRefreshTreeNodeShortcut(): boolean {
|
|||
function requestRenameSelectedNode(): boolean {
|
||||
const selected = selectedTreeNodesInVisibleOrder();
|
||||
if (selected.length > 1 && selected.some((node) => node.id === props.node.id)) return false;
|
||||
const editTarget = selectedConnectionEditTarget(props.node, selected);
|
||||
if (editTarget) {
|
||||
connectionStore.startEditing(editTarget.connectionId);
|
||||
return true;
|
||||
}
|
||||
if (canRenameObject.value) {
|
||||
openRenameObjectDialog();
|
||||
return true;
|
||||
|
|
@ -938,6 +967,13 @@ function requestRenameSelectedNode(): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
function requestEditSelectedConnection(): boolean {
|
||||
const editTarget = selectedConnectionEditTarget(props.node, selectedTreeNodesInVisibleOrder());
|
||||
if (!editTarget) return false;
|
||||
connectionStore.startEditing(editTarget.connectionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestDeleteSelectedNode(): boolean {
|
||||
if (requestDropSelectedNodes()) return true;
|
||||
if (props.node.type === "connection") {
|
||||
|
|
@ -1568,6 +1604,12 @@ async function copyFinalProxyPort() {
|
|||
async function copySelectedNames() {
|
||||
const selectedNodes = selectedTreeNodesInVisibleOrder();
|
||||
const nodes = selectedNodes.length > 1 && selectedNodes.some((node) => node.id === props.node.id) ? selectedNodes : [props.node];
|
||||
const connectionTargets = selectedConnectionClipboardTargets(props.node, nodes);
|
||||
if (connectionTargets.length > 0) {
|
||||
const copiedCount = connectionStore.copyConnectionsToTreeClipboard(connectionTargets.map((node) => node.connectionId));
|
||||
if (copiedCount > 0) toast(t("connection.copied"), 2000);
|
||||
return;
|
||||
}
|
||||
updateTreeClipboardForNodes(nodes);
|
||||
try {
|
||||
await copyToClipboard(nodes.map(copyNameForTreeNode).join("\n"));
|
||||
|
|
@ -3917,7 +3959,8 @@ onBeforeUnmount(() => {
|
|||
|
||||
// ---- CustomContextMenu ----
|
||||
|
||||
const shortcutCopyName = computed(() => formatShortcut("Mod+C"));
|
||||
const shortcutCopyName = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.copySidebarSelection));
|
||||
const shortcutEditConnection = computed(() => formatShortcut(settingsStore.editorSettings.shortcuts.editSidebarConnection));
|
||||
const shortcutRename = "F2";
|
||||
const shortcutRefresh = "F5";
|
||||
const shortcutDelete = "Delete";
|
||||
|
|
@ -4108,7 +4151,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
|||
icon: ListFilter,
|
||||
});
|
||||
}
|
||||
items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil });
|
||||
items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil, shortcut: shortcutEditConnection.value });
|
||||
if (revealConnectionFilePath.value) {
|
||||
items.push({
|
||||
label: t("contextMenu.revealDatabaseFile"),
|
||||
|
|
|
|||
|
|
@ -2894,10 +2894,14 @@ export default {
|
|||
shortcutToggleTranspose: "Toggle transpose view",
|
||||
shortcutCancelSearch: "Cancel search",
|
||||
shortcutToggleSidebar: "Toggle sidebar",
|
||||
shortcutCopySidebarSelection: "Copy sidebar selection",
|
||||
shortcutPasteSidebarSelection: "Paste into sidebar",
|
||||
shortcutEditSidebarConnection: "Edit sidebar connection",
|
||||
shortcutScopeGlobal: "Global",
|
||||
shortcutScopeEditor: "SQL editor",
|
||||
shortcutScopeGrid: "Data grid",
|
||||
shortcutScopeSearch: "Search fields",
|
||||
shortcutScopeSidebar: "Sidebar",
|
||||
shortcutPressShortcut: "Press shortcut",
|
||||
shortcutConflict: "This shortcut conflicts with another action in the same scope.",
|
||||
shortcutClear: "Clear shortcut",
|
||||
|
|
|
|||
|
|
@ -2850,10 +2850,14 @@ export default withEnglishFallback({
|
|||
shortcutRefreshData: "Actualizar datos",
|
||||
shortcutToggleTranspose: "Alternar vista transpuesta",
|
||||
shortcutCancelSearch: "Cancelar búsqueda",
|
||||
shortcutCopySidebarSelection: "Copiar selección de la barra lateral",
|
||||
shortcutPasteSidebarSelection: "Pegar en la barra lateral",
|
||||
shortcutEditSidebarConnection: "Editar conexión de la barra lateral",
|
||||
shortcutScopeGlobal: "Global",
|
||||
shortcutScopeEditor: "Editor SQL",
|
||||
shortcutScopeGrid: "Cuadrícula de datos",
|
||||
shortcutScopeSearch: "Campos de búsqueda",
|
||||
shortcutScopeSidebar: "Barra lateral",
|
||||
shortcutPressShortcut: "Presiona un atajo",
|
||||
shortcutConflict: "Este atajo entra en conflicto con otra acción del mismo ámbito.",
|
||||
shortcutClear: "Borrar atajo",
|
||||
|
|
|
|||
|
|
@ -2848,10 +2848,14 @@ export default withEnglishFallback({
|
|||
shortcutToggleTranspose: "Attiva/disattiva vista trasposta",
|
||||
shortcutCancelSearch: "Annulla ricerca",
|
||||
shortcutToggleSidebar: "Attiva/disattiva barra laterale",
|
||||
shortcutCopySidebarSelection: "Copia selezione barra laterale",
|
||||
shortcutPasteSidebarSelection: "Incolla nella barra laterale",
|
||||
shortcutEditSidebarConnection: "Modifica connessione barra laterale",
|
||||
shortcutScopeGlobal: "Globale",
|
||||
shortcutScopeEditor: "Editor SQL",
|
||||
shortcutScopeGrid: "Griglia dati",
|
||||
shortcutScopeSearch: "Cerca campi",
|
||||
shortcutScopeSidebar: "Barra laterale",
|
||||
shortcutPressShortcut: "Premi scorciatoia",
|
||||
shortcutConflict: "Questa scorciatoia è in conflitto con un'altra azione nello stesso ambito.",
|
||||
shortcutClear: "Cancella scorciatoia",
|
||||
|
|
|
|||
|
|
@ -2832,10 +2832,14 @@ export default withEnglishFallback({
|
|||
shortcutToggleTranspose: "転置ビューを切替",
|
||||
shortcutCancelSearch: "検索をキャンセル",
|
||||
shortcutToggleSidebar: "サイドバーを切替",
|
||||
shortcutCopySidebarSelection: "サイドバーの選択をコピー",
|
||||
shortcutPasteSidebarSelection: "サイドバーに貼り付け",
|
||||
shortcutEditSidebarConnection: "サイドバー接続を編集",
|
||||
shortcutScopeGlobal: "グローバル",
|
||||
shortcutScopeEditor: "SQLエディタ",
|
||||
shortcutScopeGrid: "データグリッド",
|
||||
shortcutScopeSearch: "フィールド検索",
|
||||
shortcutScopeSidebar: "サイドバー",
|
||||
shortcutPressShortcut: "ショートカットを押してください",
|
||||
shortcutConflict: "このショートカットは同じスコープ内の別のアクションと競合しています。",
|
||||
shortcutClear: "ショートカットをクリア",
|
||||
|
|
|
|||
|
|
@ -2849,10 +2849,14 @@ export default withEnglishFallback({
|
|||
shortcutRefreshData: "Atualizar dados",
|
||||
shortcutToggleTranspose: "Alternar visualização transposta",
|
||||
shortcutCancelSearch: "Cancelar pesquisa",
|
||||
shortcutCopySidebarSelection: "Copiar seleção da barra lateral",
|
||||
shortcutPasteSidebarSelection: "Colar na barra lateral",
|
||||
shortcutEditSidebarConnection: "Editar conexão da barra lateral",
|
||||
shortcutScopeGlobal: "Global",
|
||||
shortcutScopeEditor: "Editor SQL",
|
||||
shortcutScopeGrid: "Grade de dados",
|
||||
shortcutScopeSearch: "Campos de pesquisa",
|
||||
shortcutScopeSidebar: "Barra lateral",
|
||||
shortcutPressShortcut: "Pressione o atalho",
|
||||
shortcutConflict: "Este atalho conflita com outra ação no mesmo escopo.",
|
||||
shortcutClear: "Limpar atalho",
|
||||
|
|
|
|||
|
|
@ -2894,10 +2894,14 @@ export default withEnglishFallback({
|
|||
shortcutRefreshData: "刷新数据",
|
||||
shortcutToggleTranspose: "切换转置视图",
|
||||
shortcutCancelSearch: "取消搜索",
|
||||
shortcutCopySidebarSelection: "复制侧边栏选中项",
|
||||
shortcutPasteSidebarSelection: "粘贴到侧边栏",
|
||||
shortcutEditSidebarConnection: "编辑侧边栏连接",
|
||||
shortcutScopeGlobal: "全局",
|
||||
shortcutScopeEditor: "SQL 编辑器",
|
||||
shortcutScopeGrid: "数据表格",
|
||||
shortcutScopeSearch: "搜索框",
|
||||
shortcutScopeSidebar: "侧边栏",
|
||||
shortcutPressShortcut: "按下快捷键",
|
||||
shortcutConflict: "这个快捷键与同一作用域内的其他操作冲突。",
|
||||
shortcutClear: "清除快捷键",
|
||||
|
|
|
|||
|
|
@ -2736,10 +2736,14 @@ export default withEnglishFallback({
|
|||
shortcutRefreshData: "重新整理資料",
|
||||
shortcutToggleTranspose: "切換轉置檢視",
|
||||
shortcutCancelSearch: "取消搜尋",
|
||||
shortcutCopySidebarSelection: "複製側邊欄選取項",
|
||||
shortcutPasteSidebarSelection: "貼到側邊欄",
|
||||
shortcutEditSidebarConnection: "編輯側邊欄連線",
|
||||
shortcutScopeGlobal: "全域",
|
||||
shortcutScopeEditor: "SQL 編輯器",
|
||||
shortcutScopeGrid: "資料表格",
|
||||
shortcutScopeSearch: "搜尋框",
|
||||
shortcutScopeSidebar: "側邊欄",
|
||||
shortcutPressShortcut: "按下快速鍵",
|
||||
shortcutConflict: "這個快速鍵與同一作用域內的其他操作衝突。",
|
||||
shortcutClear: "清除快速鍵",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DEFAULT_SHORTCUT_SETTINGS, SHORTCUT_DEFINITIONS, findShortcutConflict,
|
|||
|
||||
describe("shortcutRegistry editor actions", () => {
|
||||
const formatterEditorActionIds: ShortcutActionId[] = ["formatSql", "indentMore", "indentLess", "duplicateLine", "deleteLine", "moveLineUp", "moveLineDown", "copyLineUp", "copyLineDown", "undo", "redo", "selectAll"];
|
||||
const sidebarShortcutActionIds: ShortcutActionId[] = ["copySidebarSelection", "pasteSidebarSelection", "editSidebarConnection"];
|
||||
|
||||
it("registers formatter editor shortcuts in the generic editor scope", () => {
|
||||
for (const actionId of formatterEditorActionIds) {
|
||||
|
|
@ -36,4 +37,20 @@ describe("shortcutRegistry editor actions", () => {
|
|||
|
||||
expect(findShortcutConflict("duplicateLine", shortcuts.duplicateLine, shortcuts)).toBe("find");
|
||||
});
|
||||
|
||||
it("registers sidebar shortcuts in the sidebar scope", () => {
|
||||
for (const actionId of sidebarShortcutActionIds) {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
|
||||
|
||||
expect(definition?.scope).toBe("sidebar");
|
||||
expect(DEFAULT_SHORTCUT_SETTINGS[actionId]).toBe(definition?.defaultShortcut);
|
||||
}
|
||||
});
|
||||
|
||||
it("detects conflicts only within sidebar shortcuts", () => {
|
||||
const shortcuts = normalizeShortcutSettings({ copySidebarSelection: "Mod+E" });
|
||||
|
||||
expect(findShortcutConflict("copySidebarSelection", shortcuts.copySidebarSelection, shortcuts)).toBe("editSidebarConnection");
|
||||
expect(findShortcutConflict("copyCurrentRow", shortcuts.copyCurrentRow, shortcuts)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -146,6 +146,18 @@ export function isToggleSidebarShortcut(event: ShortcutLikeEvent, shortcuts?: Pa
|
|||
return matchesShortcut(event, actionShortcut("toggleSidebar", shortcuts));
|
||||
}
|
||||
|
||||
export function isCopySidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("copySidebarSelection", shortcuts));
|
||||
}
|
||||
|
||||
export function isPasteSidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("pasteSidebarSelection", shortcuts));
|
||||
}
|
||||
|
||||
export function isEditSidebarConnectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("editSidebarConnection", shortcuts));
|
||||
}
|
||||
|
||||
export function isQuickOpenShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("quickOpen", shortcuts));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ export type ShortcutActionId =
|
|||
| "refreshData"
|
||||
| "toggleTranspose"
|
||||
| "cancelSearch"
|
||||
| "toggleSidebar";
|
||||
| "toggleSidebar"
|
||||
| "copySidebarSelection"
|
||||
| "pasteSidebarSelection"
|
||||
| "editSidebarConnection";
|
||||
|
||||
export type ShortcutScope = "global" | "editor" | "grid" | "search";
|
||||
export type ShortcutScope = "global" | "editor" | "grid" | "search" | "sidebar";
|
||||
|
||||
export interface ShortcutDefinition {
|
||||
id: ShortcutActionId;
|
||||
|
|
@ -306,6 +309,24 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
|||
scope: "global",
|
||||
defaultShortcut: "Mod+B",
|
||||
},
|
||||
{
|
||||
id: "copySidebarSelection",
|
||||
labelKey: "settings.shortcutCopySidebarSelection",
|
||||
scope: "sidebar",
|
||||
defaultShortcut: "Mod+C",
|
||||
},
|
||||
{
|
||||
id: "pasteSidebarSelection",
|
||||
labelKey: "settings.shortcutPasteSidebarSelection",
|
||||
scope: "sidebar",
|
||||
defaultShortcut: "Mod+V",
|
||||
},
|
||||
{
|
||||
id: "editSidebarConnection",
|
||||
labelKey: "settings.shortcutEditSidebarConnection",
|
||||
scope: "sidebar",
|
||||
defaultShortcut: "Mod+E",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_SHORTCUT_SETTINGS: ShortcutSettings = Object.fromEntries(SHORTCUT_DEFINITIONS.map((definition) => [definition.id, definition.defaultShortcut])) as ShortcutSettings;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { TreeNode } from "@/types/database";
|
|||
|
||||
type ConnectionTreeNode = TreeNode & { connectionId: string };
|
||||
|
||||
function isConnectionNode(node: TreeNode): node is ConnectionTreeNode {
|
||||
export function isConnectionNode(node: TreeNode): node is ConnectionTreeNode {
|
||||
return node.type === "connection" && !!node.connectionId;
|
||||
}
|
||||
|
||||
|
|
@ -22,3 +22,26 @@ export function selectedConnectionDeleteTargets(currentNode: TreeNode, selectedN
|
|||
export function selectedConnectionDuplicateTargets(currentNode: TreeNode, selectedNodes: TreeNode[]): ConnectionTreeNode[] {
|
||||
return selectedConnectionActionTargets(currentNode, selectedNodes);
|
||||
}
|
||||
|
||||
export function selectedConnectionClipboardTargets(currentNode: TreeNode, selectedNodes: TreeNode[]): ConnectionTreeNode[] {
|
||||
return selectedConnectionActionTargets(currentNode, selectedNodes);
|
||||
}
|
||||
|
||||
export function selectedConnectionEditTarget(currentNode: TreeNode, selectedNodes: TreeNode[]): ConnectionTreeNode | null {
|
||||
if (!isConnectionNode(currentNode)) return null;
|
||||
const selectedContainsCurrent = selectedNodes.some((node) => node.id === currentNode.id);
|
||||
if (selectedNodes.length > 1 && selectedContainsCurrent) return null;
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
export function selectedConnectionClipboardNodes(selectedNodes: TreeNode[]): ConnectionTreeNode[] {
|
||||
if (selectedNodes.length === 0 || !selectedNodes.every(isConnectionNode)) return [];
|
||||
return selectedNodes;
|
||||
}
|
||||
|
||||
export function connectionPasteTargetGroupId(node: TreeNode | null | undefined, groupIdForConnection: (connectionId: string) => string | null): string | null {
|
||||
if (!node) return null;
|
||||
if (node.type === "connection-group") return node.id;
|
||||
if (isConnectionNode(node)) return groupIdForConnection(node.connectionId);
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,11 +147,21 @@ interface TreeClipboardTableEntry {
|
|||
tableName: string;
|
||||
}
|
||||
|
||||
export interface TreeClipboard {
|
||||
kind: "table-copy";
|
||||
tables: TreeClipboardTableEntry[];
|
||||
interface TreeClipboardConnectionEntry {
|
||||
config: ConnectionConfig;
|
||||
sourceGroupId: string | null;
|
||||
}
|
||||
|
||||
export type TreeClipboard =
|
||||
| {
|
||||
kind: "table-copy";
|
||||
tables: TreeClipboardTableEntry[];
|
||||
}
|
||||
| {
|
||||
kind: "connection-copy";
|
||||
connections: TreeClipboardConnectionEntry[];
|
||||
};
|
||||
|
||||
interface LoadTreeOptions {
|
||||
force?: boolean;
|
||||
expectedSidebarSearchQuery?: string;
|
||||
|
|
@ -1017,6 +1027,43 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
stopCreatingConnectionInGroup();
|
||||
}
|
||||
|
||||
function copyConnectionsToTreeClipboard(connectionIds: Iterable<string>): number {
|
||||
const seen = new Set<string>();
|
||||
const entries: TreeClipboardConnectionEntry[] = [];
|
||||
for (const connectionId of connectionIds) {
|
||||
if (seen.has(connectionId)) continue;
|
||||
seen.add(connectionId);
|
||||
const config = getConfig(connectionId);
|
||||
if (!config) continue;
|
||||
entries.push({
|
||||
config: { ...config },
|
||||
sourceGroupId: findConnectionLocation(sidebarLayout.value, connectionId)?.groupId ?? null,
|
||||
});
|
||||
}
|
||||
if (!entries.length) return 0;
|
||||
treeClipboard.value = { kind: "connection-copy", connections: entries };
|
||||
return entries.length;
|
||||
}
|
||||
|
||||
async function pasteConnectionClipboard(targetGroupId?: string | null): Promise<number> {
|
||||
const clipboard = treeClipboard.value;
|
||||
if (clipboard?.kind !== "connection-copy" || clipboard.connections.length === 0) return 0;
|
||||
|
||||
let pastedCount = 0;
|
||||
for (const entry of clipboard.connections) {
|
||||
await addConnection(
|
||||
{
|
||||
...entry.config,
|
||||
id: uuid(),
|
||||
name: `${entry.config.name} (Copy)`,
|
||||
},
|
||||
targetGroupId === undefined ? entry.sourceGroupId : targetGroupId,
|
||||
);
|
||||
pastedCount += 1;
|
||||
}
|
||||
return pastedCount;
|
||||
}
|
||||
|
||||
function invalidateCompletionCache(connectionId: string, database?: string) {
|
||||
const cachePrefix = database == null ? `${connectionId}:` : `${connectionId}:${database}:`;
|
||||
const exactCacheKey = database == null ? null : `${connectionId}:${database}`;
|
||||
|
|
@ -3979,6 +4026,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||
isTreeNodePinned,
|
||||
toggleTreeNodePin,
|
||||
addConnection,
|
||||
copyConnectionsToTreeClipboard,
|
||||
pasteConnectionClipboard,
|
||||
addEphemeralConnection,
|
||||
updateConnection,
|
||||
setDefaultDatabase,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import { test } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts";
|
||||
import type { ConnectionConfig, SidebarLayout, TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function installMemoryStorage() {
|
||||
const values = new Map<string, string>();
|
||||
const original = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
configurable: true,
|
||||
value: {
|
||||
getItem: (key: string) => values.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => values.set(key, value),
|
||||
removeItem: (key: string) => values.delete(key),
|
||||
clear: () => values.clear(),
|
||||
},
|
||||
});
|
||||
return {
|
||||
restore() {
|
||||
if (original) Object.defineProperty(globalThis, "localStorage", original);
|
||||
else Reflect.deleteProperty(globalThis, "localStorage");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function conn(id: string, name: string): ConnectionConfig {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
db_type: "mysql",
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "root",
|
||||
password: "secret",
|
||||
};
|
||||
}
|
||||
|
||||
async function withConnectionStore(initialConnections: ConnectionConfig[], initialLayout: SidebarLayout | null, run: (store: ReturnType<typeof useConnectionStore>) => Promise<void>) {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const storage = installMemoryStorage();
|
||||
let savedConnections = initialConnections;
|
||||
let savedLayout = initialLayout;
|
||||
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/connection/list") {
|
||||
return new Response(JSON.stringify(savedConnections), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url === "/api/layout/sidebar") {
|
||||
if (init?.method === "POST") {
|
||||
savedLayout = JSON.parse(String(init.body ?? "null"));
|
||||
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response(JSON.stringify(savedLayout), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url === "/api/connection/save") {
|
||||
savedConnections = JSON.parse(String(init?.body ?? "[]"));
|
||||
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response("null", { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
setActivePinia(createPinia());
|
||||
const store = useConnectionStore();
|
||||
await store.initFromDisk();
|
||||
await run(store);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
storage.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function connectionLabels(nodes: TreeNode[]): string[] {
|
||||
const labels: string[] = [];
|
||||
for (const node of nodes) {
|
||||
if (node.type === "connection") labels.push(node.label);
|
||||
if (node.children) labels.push(...connectionLabels(node.children));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
test("copies and pastes a root connection without writing to the OS clipboard", async () => {
|
||||
await withConnectionStore([conn("conn-1", "Main MySQL")], { groups: [], order: [{ type: "connection", id: "conn-1" }] }, async (store) => {
|
||||
assert.equal(store.copyConnectionsToTreeClipboard(["conn-1"]), 1);
|
||||
assert.equal(store.treeClipboard?.kind, "connection-copy");
|
||||
|
||||
const pasted = await store.pasteConnectionClipboard(null);
|
||||
|
||||
assert.equal(pasted, 1);
|
||||
assert.equal(store.connections.length, 2);
|
||||
assert.deepEqual(connectionLabels(store.treeNodes), ["Main MySQL", "Main MySQL (Copy)"]);
|
||||
assert.notEqual(store.connections[0].id, store.connections[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
test("pastes a copied grouped connection into the same group when that group is targeted", async () => {
|
||||
const originalConnection = conn("conn-1", "Grouped MySQL");
|
||||
const layout: SidebarLayout = {
|
||||
groups: [{ id: "group-1", name: "Group", collapsed: false }],
|
||||
order: [{ type: "group", id: "group-1", children: [{ type: "connection", id: "conn-1" }] }],
|
||||
};
|
||||
|
||||
await withConnectionStore([originalConnection], layout, async (store) => {
|
||||
assert.equal(store.copyConnectionsToTreeClipboard(["conn-1"]), 1);
|
||||
const pasted = await store.pasteConnectionClipboard();
|
||||
|
||||
assert.equal(pasted, 1);
|
||||
assert.equal(store.treeNodes[0].type, "connection-group");
|
||||
assert.deepEqual(
|
||||
store.treeNodes[0].children?.map((node) => node.label),
|
||||
["Grouped MySQL", "Grouped MySQL (Copy)"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("pastes copied connections into an explicitly selected target group", async () => {
|
||||
const layout: SidebarLayout = {
|
||||
groups: [
|
||||
{ id: "group-1", name: "Source", collapsed: false },
|
||||
{ id: "group-2", name: "Target", collapsed: false },
|
||||
],
|
||||
order: [
|
||||
{ type: "group", id: "group-1", children: [{ type: "connection", id: "conn-1" }] },
|
||||
{ type: "group", id: "group-2", children: [] },
|
||||
],
|
||||
};
|
||||
|
||||
await withConnectionStore([conn("conn-1", "Source MySQL")], layout, async (store) => {
|
||||
store.copyConnectionsToTreeClipboard(["conn-1"]);
|
||||
await store.pasteConnectionClipboard("group-2");
|
||||
|
||||
assert.equal(store.treeNodes[1].type, "connection-group");
|
||||
assert.deepEqual(
|
||||
store.treeNodes[1].children?.map((node) => node.label),
|
||||
["Source MySQL (Copy)"],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("pastes multiple copied connections in the copied order", async () => {
|
||||
const layout: SidebarLayout = {
|
||||
groups: [],
|
||||
order: [
|
||||
{ type: "connection", id: "conn-1" },
|
||||
{ type: "connection", id: "conn-2" },
|
||||
],
|
||||
};
|
||||
|
||||
await withConnectionStore([conn("conn-1", "First"), conn("conn-2", "Second")], layout, async (store) => {
|
||||
store.copyConnectionsToTreeClipboard(["conn-2", "conn-1"]);
|
||||
await store.pasteConnectionClipboard(null);
|
||||
|
||||
assert.deepEqual(connectionLabels(store.treeNodes), ["First", "Second", "Second (Copy)", "First (Copy)"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -5,12 +5,15 @@ import {
|
|||
isBrowserReloadShortcut,
|
||||
isCancelSearchShortcut,
|
||||
isCloseTabShortcut,
|
||||
isCopySidebarSelectionShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
isEditSidebarConnectionShortcut,
|
||||
isFocusSearchShortcut,
|
||||
isModRShortcut,
|
||||
isNewQueryShortcut,
|
||||
isObjectSourceSaveShortcutTarget,
|
||||
isOpenSettingsShortcut,
|
||||
isPasteSidebarSelectionShortcut,
|
||||
isResetZoomShortcut,
|
||||
isRefreshDataShortcut,
|
||||
isSaveShortcut,
|
||||
|
|
@ -246,3 +249,21 @@ test("matches Escape for cancelling search", () => {
|
|||
test("ignores cancelling search while composing", () => {
|
||||
assert.equal(isCancelSearchShortcut({ key: "Escape", isComposing: true }), false);
|
||||
});
|
||||
|
||||
test("matches configurable sidebar shortcuts", () => {
|
||||
assert.equal(isCopySidebarSelectionShortcut({ key: "c", metaKey: true }), true);
|
||||
assert.equal(isPasteSidebarSelectionShortcut({ key: "v", ctrlKey: true }), true);
|
||||
assert.equal(isEditSidebarConnectionShortcut({ key: "e", metaKey: true }), true);
|
||||
|
||||
const shortcuts = {
|
||||
copySidebarSelection: "Alt+C",
|
||||
pasteSidebarSelection: "Alt+V",
|
||||
editSidebarConnection: "Shift+Mod+E",
|
||||
} as any;
|
||||
|
||||
assert.equal(isCopySidebarSelectionShortcut({ key: "c", metaKey: true }, shortcuts), false);
|
||||
assert.equal(isCopySidebarSelectionShortcut({ key: "c", altKey: true }, shortcuts), true);
|
||||
assert.equal(isPasteSidebarSelectionShortcut({ key: "v", altKey: true }, shortcuts), true);
|
||||
assert.equal(isEditSidebarConnectionShortcut({ key: "e", metaKey: true }, shortcuts), false);
|
||||
assert.equal(isEditSidebarConnectionShortcut({ key: "e", ctrlKey: true, shiftKey: true }, shortcuts), true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -161,6 +161,9 @@ test("defaults shortcut settings", () => {
|
|||
assert.equal(settings.shortcuts.resetUiZoom, "Mod+0");
|
||||
assert.equal(settings.shortcuts.refreshData, "F5");
|
||||
assert.equal(settings.shortcuts.toggleTranspose, "Tab");
|
||||
assert.equal(settings.shortcuts.copySidebarSelection, "Mod+C");
|
||||
assert.equal(settings.shortcuts.pasteSidebarSelection, "Mod+V");
|
||||
assert.equal(settings.shortcuts.editSidebarConnection, "Mod+E");
|
||||
});
|
||||
|
||||
test("keeps saved shortcut overrides", () => {
|
||||
|
|
@ -172,6 +175,7 @@ test("keeps saved shortcut overrides", () => {
|
|||
newQuery: "Shift+Mod+N",
|
||||
openSettings: "Shift+Mod+P",
|
||||
zoomInUi: "Alt+Mod+=",
|
||||
editSidebarConnection: "Alt+E",
|
||||
} as any,
|
||||
});
|
||||
|
||||
|
|
@ -181,6 +185,7 @@ test("keeps saved shortcut overrides", () => {
|
|||
assert.equal(settings.shortcuts.newQuery, "Shift+Mod+N");
|
||||
assert.equal(settings.shortcuts.openSettings, "Shift+Mod+P");
|
||||
assert.equal(settings.shortcuts.zoomInUi, "Alt+Mod+=");
|
||||
assert.equal(settings.shortcuts.editSidebarConnection, "Alt+E");
|
||||
assert.equal(settings.shortcuts.saveSql, "Mod+S");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
import { test } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selectedConnectionClipboardTargets, selectedConnectionEditTarget } from "../../apps/desktop/src/lib/sidebarConnectionSelection.ts";
|
||||
import type { TreeNode } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
function connectionNode(id: string): TreeNode {
|
||||
return {
|
||||
id,
|
||||
label: id,
|
||||
type: "connection",
|
||||
connectionId: id,
|
||||
};
|
||||
}
|
||||
|
||||
test("uses selected connections as clipboard targets only when the selection is connection-only", () => {
|
||||
const first = connectionNode("conn-1");
|
||||
const second = connectionNode("conn-2");
|
||||
const table: TreeNode = {
|
||||
id: "table-1",
|
||||
label: "users",
|
||||
type: "table",
|
||||
connectionId: "conn-1",
|
||||
database: "main",
|
||||
};
|
||||
|
||||
assert.deepEqual(selectedConnectionClipboardNodes([first, second]).map((node) => node.connectionId), ["conn-1", "conn-2"]);
|
||||
assert.deepEqual(selectedConnectionClipboardNodes([first, table]), []);
|
||||
assert.deepEqual(selectedConnectionClipboardTargets(first, [first, second]).map((node) => node.connectionId), ["conn-1", "conn-2"]);
|
||||
assert.deepEqual(selectedConnectionClipboardTargets(first, [first, table]).map((node) => node.connectionId), ["conn-1"]);
|
||||
});
|
||||
|
||||
test("resolves connection paste target groups from the selected sidebar node", () => {
|
||||
const group: TreeNode = {
|
||||
id: "group-1",
|
||||
label: "Group",
|
||||
type: "connection-group",
|
||||
};
|
||||
const connection = connectionNode("conn-1");
|
||||
const table: TreeNode = {
|
||||
id: "table-1",
|
||||
label: "users",
|
||||
type: "table",
|
||||
connectionId: "conn-1",
|
||||
database: "main",
|
||||
};
|
||||
|
||||
const groupIdForConnection = (connectionId: string) => (connectionId === "conn-1" ? "group-1" : null);
|
||||
|
||||
assert.equal(connectionPasteTargetGroupId(group, groupIdForConnection), "group-1");
|
||||
assert.equal(connectionPasteTargetGroupId(connection, groupIdForConnection), "group-1");
|
||||
assert.equal(connectionPasteTargetGroupId(table, groupIdForConnection), null);
|
||||
assert.equal(connectionPasteTargetGroupId(null, groupIdForConnection), null);
|
||||
});
|
||||
|
||||
test("allows editing only a single selected connection", () => {
|
||||
const first = connectionNode("conn-1");
|
||||
const second = connectionNode("conn-2");
|
||||
const table: TreeNode = {
|
||||
id: "table-1",
|
||||
label: "users",
|
||||
type: "table",
|
||||
connectionId: "conn-1",
|
||||
database: "main",
|
||||
};
|
||||
|
||||
assert.equal(selectedConnectionEditTarget(first, [first])?.connectionId, "conn-1");
|
||||
assert.equal(selectedConnectionEditTarget(first, [first, second]), null);
|
||||
assert.equal(selectedConnectionEditTarget(table, [table]), null);
|
||||
});
|
||||
Loading…
Reference in New Issue