feat(sidebar): add connection multi-select toolbar

This commit is contained in:
Moe. 2026-07-04 03:50:08 +08:00 committed by GitHub
parent 30b9460442
commit 42826f8f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 318 additions and 52 deletions

View File

@ -2,8 +2,10 @@
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft, ChevronsUp } from "@lucide/vue";
import { Upload, Download, FolderPlus, RefreshCw, ChevronsLeft, ChevronsUp, Trash2, FolderInput, Check, Minus, Square, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
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";
@ -22,16 +24,41 @@ const emit = defineEmits<{
collapse: [];
}>();
type ImportSource = "dbx" | "navicat" | "dbeaver" | "datagrip";
const { t } = useI18n();
const connectionStore = useConnectionStore();
const { toast } = useToast();
const connectionTreeRef = ref<InstanceType<typeof ConnectionTree>>();
const showDeleteSelectedConfirm = ref(false);
const showCreateSelectedGroupDialog = ref(false);
const selectedGroupName = ref("");
const UNGROUPED_GROUP_VALUE = "__ungrouped";
const importSourceItems = computed(() => [
{ value: "dbx", label: t("sidebar.importDbx") },
{ value: "navicat", label: t("sidebar.importNavicat") },
{ value: "dbeaver", label: t("sidebar.importDbeaver") },
{ value: "datagrip", label: t("sidebar.importDatagrip") },
]);
const connectionIdSet = computed(() => new Set(connectionStore.connections.map((connection) => connection.id)));
const allConnectionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
const selectedConnectionIds = computed(() => (connectionStore.connectionMultiSelectActive ? connectionStore.selectedTreeNodeIds.filter((id) => connectionIdSet.value.has(id)) : []));
const selectedConnectionCount = computed(() => selectedConnectionIds.value.length);
const showConnectionMultiSelectToolbar = computed(() => connectionStore.connectionMultiSelectActive && selectedConnectionCount.value > 0);
const allConnectionsSelected = computed(() => allConnectionIds.value.length > 0 && selectedConnectionCount.value === allConnectionIds.value.length);
const selectAllIcon = computed(() => (allConnectionsSelected.value ? Check : selectedConnectionCount.value > 0 ? Minus : Square));
const selectAllLabel = computed(() => (allConnectionsSelected.value ? t("connectionGroup.deselectAllConnections") : t("connectionGroup.selectAllConnections")));
const moveGroupItems = computed(() => [
...connectionStore.sidebarLayout.groups.map((group) => ({
value: group.id,
label: group.name,
})),
{
value: UNGROUPED_GROUP_VALUE,
label: t("connectionGroup.ungrouped"),
separatorBefore: connectionStore.sidebarLayout.groups.length > 0,
},
]);
async function refreshTree() {
try {
@ -45,6 +72,10 @@ function createNewGroup() {
void connectionTreeRef.value?.createNewGroup();
}
function selectImportSource(source: string) {
emit("import", source as ImportSource);
}
function collapseAllTreeNodes() {
connectionTreeRef.value?.collapseAllTreeNodes();
}
@ -53,6 +84,66 @@ function focusSearch(): boolean {
return connectionTreeRef.value?.focusSearch() ?? false;
}
function clearConnectionMultiSelection() {
connectionStore.connectionMultiSelectActive = false;
connectionStore.selectedTreeNodeIds = [];
connectionStore.selectedTreeNodeId = null;
connectionStore.treeSelectionAnchorId = null;
}
function toggleAllConnectionsSelected() {
if (allConnectionsSelected.value) {
clearConnectionMultiSelection();
return;
}
const ids = allConnectionIds.value;
connectionStore.connectionMultiSelectActive = ids.length > 0;
connectionStore.selectedTreeNodeIds = ids;
connectionStore.selectedTreeNodeId = ids[0] ?? null;
connectionStore.treeSelectionAnchorId = ids[0] ?? null;
}
async function confirmDeleteSelectedConnections() {
const ids = selectedConnectionIds.value;
if (ids.length === 0) return;
try {
await connectionStore.removeConnections(ids);
for (const connectionId of ids) {
connectionStore.disconnect(connectionId).catch((error) => {
console.warn("[DBX][connection:delete:disconnect-failed]", { connectionId, error });
});
}
clearConnectionMultiSelection();
showDeleteSelectedConfirm.value = false;
toast(t("connection.deletedSelected", { count: ids.length }), 2000);
} catch (e: any) {
toast(t("connection.saveFailed", { message: e?.message || String(e) }), 5000);
}
}
function moveSelectedConnectionsToGroup(value: string) {
const groupId = value === UNGROUPED_GROUP_VALUE ? null : value;
for (const connectionId of selectedConnectionIds.value) {
connectionStore.moveConnectionToGroup(connectionId, groupId);
}
}
function openCreateSelectedGroupDialog() {
selectedGroupName.value = "";
showCreateSelectedGroupDialog.value = true;
}
function confirmCreateSelectedGroup() {
const name = selectedGroupName.value.trim();
if (!name || selectedConnectionIds.value.length === 0) return;
const groupId = connectionStore.createConnectionGroup(name);
for (const connectionId of selectedConnectionIds.value) {
connectionStore.moveConnectionToGroup(connectionId, groupId);
}
showCreateSelectedGroupDialog.value = false;
}
defineExpose({ focusSearch });
</script>
@ -62,55 +153,125 @@ defineExpose({ focusSearch });
<div class="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-1 self-stretch" data-tauri-drag-region />
<LightTooltip :text="t('sidebar.import')" side="bottom" :delay="0" :close-delay="0" nowrap>
<span class="inline-flex">
<LightDropdown
model-value=""
:items="importSourceItems"
:aria-label="t('sidebar.import')"
:trigger-icon="Download"
trigger-class="inline-flex h-6 w-5 items-center justify-center rounded-md outline-none hover:bg-muted hover:text-foreground focus-visible:ring-0"
trigger-icon-class="h-4 w-4"
content-class="w-44"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="(source) => emit('import', source as 'dbx' | 'navicat' | 'dbeaver' | 'datagrip')"
/>
</span>
</LightTooltip>
<LightTooltip :text="t('sidebar.export')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="emit('export')">
<Upload class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('sidebar.collapseAll')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="collapseAllTreeNodes">
<ChevronsUp class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('connectionGroup.createGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="createNewGroup">
<FolderPlus class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('contextMenu.refreshChildren')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="refreshTree">
<RefreshCw class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('sidebar.collapse')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('collapse')">
<ChevronsLeft class="h-3.5 w-3.5" />
</Button>
</LightTooltip>
<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" />
</Button>
</LightTooltip>
<LightTooltip :text="t('connectionGroup.moveToGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
<span class="inline-flex">
<LightDropdown
model-value=""
:items="moveGroupItems"
:aria-label="t('connectionGroup.moveToGroup')"
:trigger-icon="FolderInput"
trigger-class="inline-flex h-5 w-5 items-center justify-center rounded-md outline-none hover:bg-muted hover:text-foreground focus-visible:ring-0"
trigger-icon-class="h-3.5 w-3.5"
content-class="w-44"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="moveSelectedConnectionsToGroup"
/>
</span>
</LightTooltip>
<LightTooltip :text="t('contextMenu.deleteSelectedConnections', { count: selectedConnectionCount })" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5 text-destructive hover:text-destructive" @click="showDeleteSelectedConfirm = true">
<Trash2 class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="selectAllLabel" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="toggleAllConnectionsSelected">
<component :is="selectAllIcon" class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('connectionGroup.exitMultiSelect')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="clearConnectionMultiSelection">
<X class="h-3 w-3" />
</Button>
</LightTooltip>
</template>
<template v-else>
<LightTooltip :text="t('sidebar.import')" side="bottom" :delay="0" :close-delay="0" nowrap>
<span class="inline-flex">
<LightDropdown
model-value=""
:items="importSourceItems"
:aria-label="t('sidebar.import')"
:trigger-icon="Download"
trigger-class="inline-flex h-6 w-5 items-center justify-center rounded-md outline-none hover:bg-muted hover:text-foreground focus-visible:ring-0"
trigger-icon-class="h-4 w-4"
content-class="w-44"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="selectImportSource"
/>
</span>
</LightTooltip>
<LightTooltip :text="t('sidebar.export')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="emit('export')">
<Upload class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('sidebar.collapseAll')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="collapseAllTreeNodes">
<ChevronsUp class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('connectionGroup.createGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="createNewGroup">
<FolderPlus class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('contextMenu.refreshChildren')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="refreshTree">
<RefreshCw class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('sidebar.collapse')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('collapse')">
<ChevronsLeft class="h-3.5 w-3.5" />
</Button>
</LightTooltip>
</template>
</div>
<div class="flex-1 min-h-0">
<ConnectionTree ref="connectionTreeRef" />
</div>
</div>
<div class="panel-resize-handle panel-resize-handle--right" @mousedown="emit('startResize', $event)" />
<Dialog v-model:open="showDeleteSelectedConfirm">
<DialogContent class="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>{{ t("contextMenu.confirmDeleteTitle") }}</DialogTitle>
</DialogHeader>
<p class="text-sm text-muted-foreground">
{{ t("contextMenu.confirmDeleteSelectedMessage", { count: selectedConnectionCount }) }}
</p>
<DialogFooter>
<Button variant="outline" @click="showDeleteSelectedConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="destructive" @click="confirmDeleteSelectedConnections">{{ t("contextMenu.deleteSelectedConnections", { count: selectedConnectionCount }) }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="showCreateSelectedGroupDialog">
<DialogContent class="sm:max-w-[360px]">
<DialogHeader>
<DialogTitle>{{ t("connectionGroup.createGroup") }}</DialogTitle>
</DialogHeader>
<Input v-model="selectedGroupName" :placeholder="t('connectionGroup.groupNamePlaceholder')" @keydown.enter.prevent="confirmCreateSelectedGroup" />
<DialogFooter>
<Button variant="outline" @click="showCreateSelectedGroupDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button :disabled="!selectedGroupName.trim()" @click="confirmCreateSelectedGroup">{{ t("connectionGroup.createGroup") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</template>

View File

@ -440,6 +440,7 @@ function clearSidebarSelection() {
// Clicking the blank area of the tree clears the current selection. Row
// clicks call event.stopPropagation(), so this only fires for blank clicks
// (issue #681 selection wasn't cleared in double-click activation mode).
store.connectionMultiSelectActive = false;
store.selectedTreeNodeId = null;
store.selectedTreeNodeIds = [];
store.treeSelectionAnchorId = null;

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount, inject } from "vue";
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount, inject, type Component } from "vue";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
@ -48,6 +48,7 @@ import {
ListFilter,
Package,
Clipboard,
Check,
UsersRound,
Lock,
HardDriveDownload,
@ -56,8 +57,9 @@ import {
ListX,
Info,
Archive,
Square,
} from "@lucide/vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import CustomContextMenu from "@/components/ui/CustomContextMenu.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
@ -151,6 +153,19 @@ const labelOverflowing = ref(false);
let labelResizeObserver: ResizeObserver | null = null;
let labelMeasureFrame = 0;
interface ContextMenuItem {
label: string;
action?: () => void;
disabled?: boolean;
separator?: boolean;
icon?: Component;
iconClass?: string;
shortcut?: string;
variant?: "default" | "destructive";
visible?: boolean;
children?: ContextMenuItem[];
}
function cancelLabelOverflowMeasure() {
if (!labelMeasureFrame) return;
window.cancelAnimationFrame(labelMeasureFrame);
@ -715,12 +730,14 @@ function selectedTreeNodesInVisibleOrder(): TreeNode[] {
}
function selectSingleTreeNode(node: TreeNode) {
connectionStore.connectionMultiSelectActive = false;
connectionStore.selectedTreeNodeId = node.id;
connectionStore.selectedTreeNodeIds = [node.id];
connectionStore.treeSelectionAnchorId = node.id;
}
function toggleTreeNodeSelection(node: TreeNode) {
connectionStore.connectionMultiSelectActive = false;
const ids = new Set(connectionStore.selectedTreeNodeIds);
if (ids.has(node.id)) ids.delete(node.id);
else ids.add(node.id);
@ -730,6 +747,7 @@ function toggleTreeNodeSelection(node: TreeNode) {
}
function selectTreeNodeRange(node: TreeNode) {
connectionStore.connectionMultiSelectActive = false;
const visible = visibleTreeNodes();
const anchorId = connectionStore.treeSelectionAnchorId || connectionStore.selectedTreeNodeId || node.id;
const currentIndex = sidebarTreeContext ? sidebarTreeContext.getVisibleNodeIndex(node.id) : -1;
@ -751,6 +769,30 @@ function selectTreeNodeRange(node: TreeNode) {
connectionStore.selectedTreeNodeId = node.id;
}
const selectedConnectionIds = computed(() => {
const connectionIds = new Set(connectionStore.connections.map((connection) => connection.id));
return connectionStore.selectedTreeNodeIds.filter((id) => connectionIds.has(id));
});
const isConnectionSelectionChecked = computed(() => connectionStore.connectionMultiSelectActive && props.node.type === "connection" && !!props.node.connectionId && selectedConnectionIds.value.includes(props.node.connectionId));
function toggleConnectionMultiSelection(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (props.node.type !== "connection" || !props.node.connectionId) return;
const next = new Set(connectionStore.connectionMultiSelectActive ? selectedConnectionIds.value : []);
if (next.has(props.node.connectionId)) next.delete(props.node.connectionId);
else next.add(props.node.connectionId);
const ids = [...next];
connectionStore.selectedTreeNodeIds = ids;
connectionStore.selectedTreeNodeId = ids.includes(props.node.connectionId) ? props.node.connectionId : (ids[0] ?? null);
connectionStore.treeSelectionAnchorId = props.node.connectionId;
connectionStore.connectionMultiSelectActive = ids.length > 0;
rowRef.value?.focus({ preventScroll: true });
}
function onClick(event: MouseEvent) {
if (suppressNextTableReferenceClick) {
suppressNextTableReferenceClick = false;
@ -4529,19 +4571,22 @@ function treeItemMenuItems(): ContextMenuItem[] {
</script>
<template>
<CustomContextMenu :items="treeItemMenuItems()" v-slot="{ onContextMenu }">
<div @contextmenu="onTreeItemContextMenu($event, onContextMenu)">
<CustomContextMenu :items="treeItemMenuItems()" v-slot="contextMenuSlot">
<div @contextmenu="onTreeItemContextMenu($event, contextMenuSlot.onContextMenu)">
<LightTooltip :text="displayLabel(node)" :disabled="isTooltipDisabled()" side="right" :side-offset="8" :delay="0" :close-delay="0">
<div
ref="rowRef"
class="group flex items-center gap-1.5 py-1 px-2 cursor-pointer hover:bg-accent relative outline-none"
class="group flex items-center gap-1.5 py-1 px-2 cursor-pointer relative outline-none"
style="contain: layout style"
:class="[
rowWidthClass,
{
'group/sidebar-row': true,
'ring-1 ring-primary/50 bg-primary/5': showDropInside,
'opacity-50': isDragging,
'tree-item-connection-tint': connectionColor,
'hover:bg-accent': node.type !== 'connection',
'hover:bg-secondary/60': node.type === 'connection',
rounded: !isSelected && !isMultiSelected,
'tree-item-active rounded-none': connectionColor && (isSelected || isMultiSelected),
'tree-item-active rounded-md': !connectionColor && (isSelected || isMultiSelected),
@ -4567,7 +4612,7 @@ function treeItemMenuItems(): ContextMenuItem[] {
</button>
</template>
<span v-else class="w-3.5 h-3.5 shrink-0" />
<DatabaseIcon v-if="node.type === 'connection'" :db-type="connectionIconType(node.connectionId)" class="w-3.5 h-3.5 shrink-0" />
<DatabaseIcon v-if="node.type === 'connection'" :db-type="connectionIconType(node.connectionId)" class="h-3.5 w-3.5 shrink-0" />
<Loader2 v-else-if="node.type === 'load-more' && node.isLoading" class="w-3.5 h-3.5 shrink-0 animate-spin text-primary" />
<component v-else :is="getIconInfo(node)?.icon || Database" class="w-3.5 h-3.5 shrink-0" :class="nodeIconClass" />
<input
@ -4599,6 +4644,18 @@ function treeItemMenuItems(): ContextMenuItem[] {
<Badge v-if="isConnectionReadonly" variant="secondary" class="h-4 px-1.5 text-[10px] gap-0.5"><Lock class="w-2.5 h-2.5" />{{ t("connection.readOnlyBadge") }}</Badge>
<ConnectionErrorIndicator v-if="node.type === 'connection'" :connection-id="node.connectionId" trigger-class="h-4 w-4" />
<Pin v-if="isPinned" class="w-3 h-3 shrink-0 text-primary fill-current" aria-hidden="true" />
<button
v-if="node.type === 'connection'"
type="button"
class="ml-auto flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground/55 opacity-0 transition-colors transition-opacity hover:bg-secondary/45 hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/sidebar-row:opacity-100"
:class="{ 'opacity-100': isConnectionSelectionChecked || connectionStore.connectionMultiSelectActive }"
:aria-label="isConnectionSelectionChecked ? t('connectionGroup.deselectConnection') : t('connectionGroup.selectConnection')"
@mousedown.stop
@click="toggleConnectionMultiSelection"
>
<Check v-if="isConnectionSelectionChecked" class="h-3 w-3 text-primary" />
<Square v-else class="h-3 w-3 stroke-[1.7]" />
</button>
</div>
<template v-if="detailTooltip" #content>
<div class="w-max min-w-40 max-w-[min(28rem,calc(100vw-24px))] rounded-md border border-border bg-popover p-2 text-popover-foreground shadow-lg">

View File

@ -23,6 +23,10 @@ defineEmits<{
close: [];
}>();
defineSlots<{
default(props: { onContextMenu: (event: MouseEvent) => void }): any;
}>();
// ---- module-level singleton state ----
const openMenus = new Set<() => void>();
let globalSetup = false;
@ -261,7 +265,7 @@ onBeforeUnmount(() => {
</script>
<template>
<slot :on-context-menu="onContextMenu" />
<slot :onContextMenu="onContextMenu" />
<!-- Main menu -->
<Teleport to="body">
<div v-if="show" ref="menuRef" :style="{ position: 'fixed', left: x + 'px', top: y + 'px', zIndex: 9999 }" class="bg-popover text-popover-foreground min-w-40 rounded-[6px] p-1 overflow-x-hidden overflow-y-auto ring-1 ring-foreground/10 shadow-lg">

View File

@ -47,6 +47,12 @@ const emit = defineEmits<{
"update:open": [value: boolean];
}>();
defineSlots<{
"trigger-label"?(props: { value: string; label: string; loading: boolean }): any;
"option-label"?(props: { option: string; label: string }): any;
"custom-option-label"?(props: { value: string }): any;
}>();
const open = ref(false);
const searchText = ref("");
const searchInput = ref<InstanceType<typeof Input>>();

View File

@ -1079,6 +1079,11 @@ export default {
connectionGroup: {
createGroup: "New Group",
groupNamePlaceholder: "Group name",
selectConnection: "Select connection",
deselectConnection: "Deselect connection",
selectAllConnections: "Select all connections",
deselectAllConnections: "Deselect all connections",
exitMultiSelect: "Exit selection",
renameGroup: "Rename Group",
deleteGroup: "Delete Group",
moveToGroup: "Move to Group",

View File

@ -1050,6 +1050,11 @@ export default withEnglishFallback({
newGroupDefault: "Nuevo grupo",
deleteGroupConfirmTitle: "Eliminar grupo",
deleteGroupConfirmMessage: '¿Eliminar el grupo "{name}"? Las conexiones dentro serán movidas al nivel superior.',
selectConnection: "Seleccionar conexión",
deselectConnection: "Deseleccionar conexión",
selectAllConnections: "Seleccionar todas las conexiones",
deselectAllConnections: "Deseleccionar todas las conexiones",
exitMultiSelect: "Salir de selección múltiple",
},
databaseSearch: {
title: "Buscar en base de datos",

View File

@ -1048,6 +1048,11 @@ export default withEnglishFallback({
newGroupDefault: "Nuovo Gruppo",
deleteGroupConfirmTitle: "Elimina Gruppo",
deleteGroupConfirmMessage: 'Eliminare il gruppo "{name}"? Le connessioni al suo interno verranno spostate al livello principale.',
selectConnection: "Seleziona connessione",
deselectConnection: "Deseleziona connessione",
selectAllConnections: "Seleziona tutte le connessioni",
deselectAllConnections: "Deseleziona tutte le connessioni",
exitMultiSelect: "Esci dalla selezione multipla",
},
databaseSearch: {
title: "Cerca nel Database",

View File

@ -1048,6 +1048,11 @@ export default withEnglishFallback({
newGroupDefault: "新しいグループ",
deleteGroupConfirmTitle: "グループを削除",
deleteGroupConfirmMessage: "グループ「{name}」を削除しますか?グループ内の接続はトップレベルに移動されます。",
selectConnection: "接続を選択",
deselectConnection: "接続の選択を解除",
selectAllConnections: "すべての接続を選択",
deselectAllConnections: "すべての接続の選択を解除",
exitMultiSelect: "複数選択を終了",
},
databaseSearch: {
title: "データベース検索",

View File

@ -1049,6 +1049,11 @@ export default withEnglishFallback({
newGroupDefault: "Novo Grupo",
deleteGroupConfirmTitle: "Excluir Grupo",
deleteGroupConfirmMessage: 'Excluir o grupo "{name}"? As conexões dentro dele serão movidas para o nível superior.',
selectConnection: "Selecionar conexão",
deselectConnection: "Desmarcar conexão",
selectAllConnections: "Selecionar todas as conexões",
deselectAllConnections: "Desmarcar todas as conexões",
exitMultiSelect: "Sair da seleção múltipla",
},
databaseSearch: {
title: "Pesquisar no Banco de Dados",

View File

@ -1081,6 +1081,11 @@ export default withEnglishFallback({
connectionGroup: {
createGroup: "新建分组",
groupNamePlaceholder: "分组名称",
selectConnection: "选择连接",
deselectConnection: "取消选择连接",
selectAllConnections: "全选连接",
deselectAllConnections: "取消全选连接",
exitMultiSelect: "退出多选",
renameGroup: "重命名分组",
deleteGroup: "删除分组",
moveToGroup: "移至分组",

View File

@ -1049,6 +1049,11 @@ export default withEnglishFallback({
newGroupDefault: "新群組",
deleteGroupConfirmTitle: "刪除群組",
deleteGroupConfirmMessage: "刪除群組「{name}」?其中的連線將移到頂層。",
selectConnection: "選擇連接",
deselectConnection: "取消選擇連接",
selectAllConnections: "全選連接",
deselectAllConnections: "取消全選連接",
exitMultiSelect: "退出多選",
},
databaseSearch: {
title: "搜尋資料庫",

View File

@ -177,6 +177,7 @@ export const useConnectionStore = defineStore("connection", () => {
const selectedTreeNodeId = ref<string | null>(null);
const selectedTreeNodeIds = ref<string[]>([]);
const treeSelectionAnchorId = ref<string | null>(null);
const connectionMultiSelectActive = ref(false);
const treeClipboard = ref<TreeClipboard | null>(null);
watch(activeConnectionId, (id) => {
@ -3956,6 +3957,7 @@ export const useConnectionStore = defineStore("connection", () => {
selectedTreeNodeId,
selectedTreeNodeIds,
treeSelectionAnchorId,
connectionMultiSelectActive,
treeClipboard,
treeNodes,
removeTreeNode,