fix(sidebar): improve connection multi-select actions
This commit is contained in:
24 files changed
+510
-44
No files matched your search
@@ -212,6 +212,44 @@ describe("AppSidebar connection multi-select moves", () => {
|
||||
expect(mocks.store.selectedTreeNodeIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("shows the selected count beside the connection title instead of as a toolbar item", async () => {
|
||||
setSelection(["conn-visible", "conn-hidden"]);
|
||||
const host = await mountSidebar();
|
||||
|
||||
const count = host.querySelector("[data-connection-selection-count]");
|
||||
expect(count?.textContent?.trim()).toBe("connectionGroup.selectedConnections");
|
||||
expect(count?.previousElementSibling?.textContent?.trim()).toBe("sidebar.connections");
|
||||
});
|
||||
|
||||
it("exits connection multi-select when Escape is pressed in the sidebar", async () => {
|
||||
setSelection(["conn-visible", "conn-hidden"]);
|
||||
const host = await mountSidebar();
|
||||
const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
|
||||
|
||||
host.querySelector<HTMLElement>(".app-sidebar-panel")!.dispatchEvent(event);
|
||||
await nextTick();
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(mocks.store.selectedTreeNodeIds).toEqual([]);
|
||||
expect(mocks.store.selectedTreeNodeId).toBeNull();
|
||||
expect(mocks.store.treeSelectionAnchorId).toBeNull();
|
||||
expect(mocks.store.connectionMultiSelectActive).toBe(false);
|
||||
});
|
||||
|
||||
it("does not exit connection multi-select when Escape comes from an input", async () => {
|
||||
setSelection(["conn-visible", "conn-hidden"]);
|
||||
const host = await mountSidebar();
|
||||
click(host.querySelector('[data-tooltip="connectionGroup.createGroup"] button'));
|
||||
await nextTick();
|
||||
const input = host.querySelector<HTMLInputElement>("[data-dialog] input");
|
||||
|
||||
input!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.store.selectedTreeNodeIds).toEqual(["conn-visible", "conn-hidden"]);
|
||||
expect(mocks.store.connectionMultiSelectActive).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps selection on cancel and releases it after creating a new group", async () => {
|
||||
setSelection(["conn-visible", "conn-hidden"]);
|
||||
const host = await mountSidebar();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
||||
import LightDropdown from "@/components/ui/LightDropdown.vue";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import ConnectionTree from "@/components/sidebar/ConnectionTree.vue";
|
||||
import { applyConnectionMultiSelection, emptyConnectionMultiSelection } from "@/lib/sidebar/sidebarConnectionMultiSelect";
|
||||
import { applyConnectionMultiSelection, emptyConnectionMultiSelection, isExitConnectionMultiSelectionShortcut } from "@/lib/sidebar/sidebarConnectionMultiSelect";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
|
||||
@@ -90,6 +90,18 @@ function clearConnectionMultiSelection() {
|
||||
applyConnectionMultiSelection(connectionStore, emptyConnectionMultiSelection());
|
||||
}
|
||||
|
||||
function isEditableSidebarTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable || !!target.closest("[contenteditable='true'], [role='textbox']");
|
||||
}
|
||||
|
||||
function onSidebarKeydown(event: KeyboardEvent) {
|
||||
if (event.defaultPrevented || !connectionStore.connectionMultiSelectActive || !isExitConnectionMultiSelectionShortcut(event) || isEditableSidebarTarget(event.target)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clearConnectionMultiSelection();
|
||||
}
|
||||
|
||||
function toggleAllConnectionsSelected() {
|
||||
if (allConnectionsSelected.value) {
|
||||
clearConnectionMultiSelection();
|
||||
@@ -153,13 +165,17 @@ defineExpose({ focusSearch });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }">
|
||||
<div class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }" @keydown="onSidebarKeydown">
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="app-sidebar-toolbar flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20" :class="classicLayout ? 'h-9' : 'h-10'">
|
||||
<span class="flex self-stretch items-center truncate" data-tauri-drag-region>{{ t("sidebar.connections") }}</span>
|
||||
<span class="flex min-w-0 self-stretch items-center" data-tauri-drag-region>
|
||||
<span class="truncate" data-tauri-drag-region>{{ t("sidebar.connections") }}</span>
|
||||
<span v-if="showConnectionMultiSelectToolbar" class="ml-1.5 shrink-0 text-[11px] font-normal text-muted-foreground/80" data-connection-selection-count>
|
||||
{{ t("connectionGroup.selectedConnections", { count: selectedConnectionCount }) }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex-1 self-stretch" data-tauri-drag-region />
|
||||
<template v-if="showConnectionMultiSelectToolbar">
|
||||
<span class="mr-1 text-[11px] font-medium text-muted-foreground">{{ selectedConnectionCount }}</span>
|
||||
<LightTooltip :text="t('connectionGroup.createGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click="openCreateSelectedGroupDialog">
|
||||
<FolderPlus class="h-3 w-3" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type ComponentPublicInstance, type CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, CircleDot, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
|
||||
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, CircleDot, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw, Loader2, Unplug } from "@lucide/vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
@@ -52,6 +52,7 @@ import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { alignedSidebarCommentLabelWidths, isSidebarCommentAlignableNode, sidebarTreeNaturalContentWidth, sidebarTreeNodeComment, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
import { formatSidebarObjectStorage, sidebarTableStorageScopes, supportsSidebarTableStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
|
||||
import { sidebarScrollbarGeometry as calculateSidebarScrollbarGeometry } from "@/lib/sidebar/sidebarScrollbar";
|
||||
import { disconnectSidebarConnections } from "@/lib/sidebar/sidebarConnectionDisconnect";
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useConnectionStore();
|
||||
@@ -61,6 +62,7 @@ const { toast } = useToast();
|
||||
const searchQuery = ref("");
|
||||
const deferredSearchQuery = ref("");
|
||||
const showConnectedConnectionsOnly = ref(false);
|
||||
const isDisconnectingAllActiveConnections = ref(false);
|
||||
const searchInputRef = ref<HTMLInputElement>();
|
||||
const rootRef = ref<HTMLElement>();
|
||||
const pointerInsideTree = ref(false);
|
||||
@@ -159,6 +161,39 @@ watch(
|
||||
{ flush: "sync" },
|
||||
);
|
||||
|
||||
watch(
|
||||
[showConnectedConnectionsOnly, () => store.connectedIds.size],
|
||||
([showConnectedOnly, activeConnectionCount]) => {
|
||||
if (showConnectedOnly && activeConnectionCount === 0) showConnectedConnectionsOnly.value = false;
|
||||
},
|
||||
{ flush: "sync" },
|
||||
);
|
||||
|
||||
async function disconnectAllActiveConnections() {
|
||||
if (isDisconnectingAllActiveConnections.value) return;
|
||||
const connectionIds = [...store.connectedIds];
|
||||
if (!connectionIds.length) {
|
||||
showConnectedConnectionsOnly.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isDisconnectingAllActiveConnections.value = true;
|
||||
try {
|
||||
const result = await disconnectSidebarConnections(connectionIds, (connectionId) => store.disconnect(connectionId));
|
||||
if (!result.failed) {
|
||||
toast(t("connection.disconnectedSelected", { count: connectionIds.length }), 2000);
|
||||
} else if (result.succeeded > 0) {
|
||||
toast(t("connection.disconnectSelectedPartial", { succeeded: result.succeeded, failed: result.failed }), 5000);
|
||||
} else {
|
||||
const message = result.firstError instanceof Error ? result.firstError.message : String(result.firstError);
|
||||
toast(t("connection.saveFailed", { message }), 5000);
|
||||
}
|
||||
} finally {
|
||||
isDisconnectingAllActiveConnections.value = false;
|
||||
if (store.connectedIds.size === 0) showConnectedConnectionsOnly.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshActiveSidebarTableSearches() {
|
||||
if (isTreeSearchFiltering.value) return;
|
||||
for (const parentNodeId of Object.keys(store.sidebarTableSearchQueries)) {
|
||||
@@ -1974,6 +2009,13 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
</div>
|
||||
</div>
|
||||
</CustomContextMenu>
|
||||
<div v-if="showConnectedConnectionsOnly && store.connectedIds.size > 0" class="shrink-0 border-t border-border bg-background px-2 py-2">
|
||||
<Button type="button" variant="outline" size="sm" class="h-7 w-full justify-center gap-1.5 text-xs" :disabled="isDisconnectingAllActiveConnections" @click="disconnectAllActiveConnections">
|
||||
<Loader2 v-if="isDisconnectingAllActiveConnections" class="h-3.5 w-3.5 animate-spin" />
|
||||
<Unplug v-else class="h-3.5 w-3.5" />
|
||||
{{ t("sidebar.disconnectAllActiveConnections") }}
|
||||
</Button>
|
||||
</div>
|
||||
<SidebarDdlViewDialog
|
||||
v-if="sidebarDdlTarget"
|
||||
v-model:open="sidebarDdlOpen"
|
||||
|
||||
@@ -140,7 +140,7 @@ import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabil
|
||||
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
|
||||
import { defaultPasteTableMode, pasteTableModeCopiesData, supportsWholeRowTableDataCopy, tableClipboardMatchesTarget, tableClipboardMenuState, tableClipboardSourceContext, tableDataCopyColumnOptions, type TableClipboardContext, type TableClipboardTableContext } from "@/lib/table/tableClipboard";
|
||||
import { selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { selectedSidebarBatchTargets, selectedTreeNodesInVisibleOrder as orderSelectedTreeNodes } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { connectionPasteTargetGroupId, selectedConnectionClipboardTargets, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { connectionSupportsDatabaseUserAdmin, resolveDatabaseUserAdminProviderForConnection, type DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
import { authorizationPlanSql, authorizationPlanStatus, buildCreateDatabaseAuthorizationPlan, executeAuthorizationPlan, type AuthorizationPlan, type AuthorizationStepResult } from "@/lib/database/databaseAuthorizationPlan";
|
||||
@@ -368,6 +368,8 @@ const {
|
||||
canBackupSqliteDatabase,
|
||||
backupSqliteDatabase,
|
||||
disconnectConnection,
|
||||
connectionDisconnectMenuLabel,
|
||||
canDisconnectConnection,
|
||||
cancelConnectionAttempt,
|
||||
closeDatabaseConnection,
|
||||
isPinned,
|
||||
@@ -1934,14 +1936,7 @@ async function refreshMutatedTableDataTabsForNodes(nodes: readonly TreeNode[]) {
|
||||
}
|
||||
|
||||
function selectedBatchDropTargets(): TreeNode[] {
|
||||
const selected = selectedTreeNodesInVisibleOrder();
|
||||
if (selected.length <= 1 || !selected.some((node) => node.id === activeNode.value.id)) return [];
|
||||
const first = selected[0];
|
||||
if (!first?.connectionId || !first.database || !selected.every((node) => node.type === first.type)) return [];
|
||||
if (!selected.every((node) => node.connectionId === first.connectionId && node.database === first.database && canDropTreeNode(node))) {
|
||||
return [];
|
||||
}
|
||||
return selected;
|
||||
return selectedSidebarBatchTargets(activeNode.value, selectedTreeNodesInVisibleOrder(), canDropTreeNode);
|
||||
}
|
||||
|
||||
function selectedBatchTableTargets(): TreeNode[] {
|
||||
@@ -4152,10 +4147,10 @@ function buildConnectionSidebarMenu(context: SidebarMenuFactoryContext): boolean
|
||||
if (node.type === "connection") {
|
||||
if (isConnecting.value) {
|
||||
items.push({ label: t("connection.cancelConnecting"), action: cancelConnectionAttempt, icon: X });
|
||||
} else if (canDisconnectConnection()) {
|
||||
items.push({ label: connectionDisconnectMenuLabel(), action: disconnectConnection, icon: Unplug });
|
||||
} else if (!isConnected.value) {
|
||||
items.push({ label: t("contextMenu.openConnection"), action: toggle, icon: Plug });
|
||||
} else {
|
||||
items.push({ label: t("contextMenu.closeConnection"), action: disconnectConnection, icon: Unplug });
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
@@ -5044,11 +5039,17 @@ function bindMenuTarget(items: ContextMenuItem[], target: SidebarActionTarget, s
|
||||
function buildContextMenu(node: TreeNode): ContextMenuItem[] {
|
||||
const previousNode = activeNode.value;
|
||||
const menuContext = createSidebarMenuContext(node, connectionStore.selectedTreeNodeIds, databaseTypeForNode(node));
|
||||
acceptedSelectionIds = menuContext.selectedNodeIds;
|
||||
activateRuntimeNode(node);
|
||||
claimTreeItemDialogOwnership();
|
||||
ensureDangerDialogRouting();
|
||||
routeTreeItemDialogController();
|
||||
const rawItems = treeItemMenuItems();
|
||||
let rawItems: ContextMenuItem[];
|
||||
try {
|
||||
rawItems = treeItemMenuItems();
|
||||
} finally {
|
||||
acceptedSelectionIds = null;
|
||||
}
|
||||
// Normalization is intentionally evaluated only when a menu opens. Besides
|
||||
// parity tests, it provides deterministic action identifiers for diagnostics.
|
||||
normalizeSidebarMenuDescriptors(menuContext, rawItems);
|
||||
|
||||
@@ -59,8 +59,8 @@ import { connectionDisplayUrlScheme } from "@/lib/connection/connectionPresentat
|
||||
import { hexToRgba } from "@/lib/common/color";
|
||||
import { sidebarDisplayTableName } from "@/lib/sidebar/sidebarTableNameDisplay";
|
||||
import { shouldMeasureSidebarLabelOverflow } from "@/lib/sidebar/sidebarLabelTooltip";
|
||||
import { treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { applyConnectionMultiSelection, connectionMultiSelectionAfterToggle } from "@/lib/sidebar/sidebarConnectionMultiSelect";
|
||||
import { filterSidebarModifierSelectionIds, supportsSidebarModifierSelection, treeSelectionRangeIdsByIndex, treeSelectionRangeIds } from "@/lib/sidebar/sidebarTreeSelection";
|
||||
import { applyConnectionMultiSelection, applyTreeNodeSelection, connectionMultiSelectionAfterToggle } from "@/lib/sidebar/sidebarConnectionMultiSelect";
|
||||
import { isSidebarDatabaseOpenForVisual } from "@/lib/sidebar/sidebarDatabaseOpenState";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { connectionCanConfigureSidebarVisibleDatabases } from "@/lib/sidebar/sidebarVisibleFilterMenu";
|
||||
@@ -524,25 +524,45 @@ function selectSingleTreeNode(node: TreeNode) {
|
||||
}
|
||||
|
||||
function toggleTreeNodeSelection(node: TreeNode) {
|
||||
connectionStore.connectionMultiSelectActive = false;
|
||||
if (!supportsSidebarModifierSelection(node)) {
|
||||
selectSingleTreeNode(node);
|
||||
return;
|
||||
}
|
||||
const ids = new Set(connectionStore.selectedTreeNodeIds);
|
||||
if (ids.has(node.id)) ids.delete(node.id);
|
||||
else ids.add(node.id);
|
||||
connectionStore.selectedTreeNodeIds = ids.size ? [...ids] : [node.id];
|
||||
connectionStore.selectedTreeNodeId = node.id;
|
||||
connectionStore.treeSelectionAnchorId = node.id;
|
||||
const filteredIds = filterSidebarModifierSelectionIds(visibleTreeNodes(), [...ids]);
|
||||
applyTreeNodeSelection(
|
||||
connectionStore,
|
||||
{
|
||||
nodeIds: filteredIds.length ? filteredIds : [node.id],
|
||||
activeNodeId: node.id,
|
||||
anchorNodeId: node.id,
|
||||
},
|
||||
new Set(connectionStore.connections.map((connection) => connection.id)),
|
||||
);
|
||||
}
|
||||
|
||||
function selectTreeNodeRange(node: TreeNode) {
|
||||
connectionStore.connectionMultiSelectActive = false;
|
||||
if (!supportsSidebarModifierSelection(node)) {
|
||||
selectSingleTreeNode(node);
|
||||
return;
|
||||
}
|
||||
const visible = visibleTreeNodes();
|
||||
const anchorId = connectionStore.treeSelectionAnchorId || connectionStore.selectedTreeNodeId || node.id;
|
||||
const currentIndex = sidebarTreeContext ? sidebarTreeContext.getVisibleNodeIndex(node.id) : -1;
|
||||
const anchorIndex = sidebarTreeContext ? sidebarTreeContext.getVisibleNodeIndex(anchorId) : -1;
|
||||
|
||||
if (sidebarTreeContext && currentIndex >= 0 && anchorIndex >= 0) {
|
||||
connectionStore.selectedTreeNodeIds = treeSelectionRangeIdsByIndex(visible, currentIndex, anchorIndex, node.id);
|
||||
connectionStore.selectedTreeNodeId = node.id;
|
||||
applyTreeNodeSelection(
|
||||
connectionStore,
|
||||
{
|
||||
nodeIds: filterSidebarModifierSelectionIds(visible, treeSelectionRangeIdsByIndex(visible, currentIndex, anchorIndex, node.id)),
|
||||
activeNodeId: node.id,
|
||||
anchorNodeId: anchorId,
|
||||
},
|
||||
new Set(connectionStore.connections.map((connection) => connection.id)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -551,9 +571,16 @@ function selectTreeNodeRange(node: TreeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeIds = treeSelectionRangeIds(visible, node.id, anchorId, connectionStore.selectedTreeNodeId);
|
||||
connectionStore.selectedTreeNodeIds = rangeIds;
|
||||
connectionStore.selectedTreeNodeId = node.id;
|
||||
const rangeIds = filterSidebarModifierSelectionIds(visible, treeSelectionRangeIds(visible, node.id, anchorId, connectionStore.selectedTreeNodeId));
|
||||
applyTreeNodeSelection(
|
||||
connectionStore,
|
||||
{
|
||||
nodeIds: rangeIds,
|
||||
activeNodeId: node.id,
|
||||
anchorNodeId: anchorId,
|
||||
},
|
||||
new Set(connectionStore.connections.map((connection) => connection.id)),
|
||||
);
|
||||
}
|
||||
|
||||
function selectedConnectionIdsForAction(): string[] {
|
||||
|
||||
+32
@@ -101,6 +101,7 @@ afterEach(() => {
|
||||
connectionStore.selectedTreeNodeId = null;
|
||||
connectionStore.selectedTreeNodeIds = [];
|
||||
connectionStore.selectedTreeNodeIdsSet = new Set<string>();
|
||||
connectionStore.treeNodes = [];
|
||||
connectionStore.treeSelectionAnchorId = null;
|
||||
settingsStore.editorSettings.sidebarActivation = "double";
|
||||
vi.restoreAllMocks();
|
||||
@@ -144,6 +145,37 @@ describe("TreeItem load-more activation", () => {
|
||||
expect(host.handleRowClick).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drops a selected object group when modifier-selecting a real table", async () => {
|
||||
const group: TreeNode = {
|
||||
id: "connection-1:app:dbo:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "connection-1",
|
||||
database: "app",
|
||||
schema: "dbo",
|
||||
};
|
||||
const table: TreeNode = {
|
||||
id: "connection-1:app:dbo:orders",
|
||||
label: "orders",
|
||||
type: "table",
|
||||
connectionId: "connection-1",
|
||||
database: "app",
|
||||
schema: "dbo",
|
||||
};
|
||||
connectionStore.treeNodes = [{ ...group, children: [table] }];
|
||||
connectionStore.selectedTreeNodeId = group.id;
|
||||
connectionStore.selectedTreeNodeIds = [group.id];
|
||||
connectionStore.selectedTreeNodeIdsSet = new Set([group.id]);
|
||||
connectionStore.treeSelectionAnchorId = group.id;
|
||||
const { row } = await mountTreeItem(table);
|
||||
|
||||
row.dispatchEvent(new MouseEvent("click", { bubbles: true, detail: 1, metaKey: true }));
|
||||
|
||||
expect(connectionStore.selectedTreeNodeIds).toEqual([table.id]);
|
||||
expect(connectionStore.selectedTreeNodeId).toBe(table.id);
|
||||
expect(connectionStore.treeSelectionAnchorId).toBe(table.id);
|
||||
});
|
||||
|
||||
it("shows the invalid marker for objects reported as invalid", async () => {
|
||||
const node: TreeNode = {
|
||||
id: "connection-1:app:dbo:broken_proc",
|
||||
|
||||
Reference in new issue
Block a user