feat: implement drag-drop and data migration (Phase 3 & 5)
Phase 3 - Drag-drop functionality: - Add useTreeDragDrop composable with drag state management - Implement drag constraints (prevent dropping on self/descendants) - Add visual feedback (opacity, drop indicators, highlight) - Support drop positions: before/after/inside folder Phase 5 - Data migration and sync: - Add MigrationPrompt component for first-time users - Implement bidirectional sync between connections and session tree - Add syncNewConnections/syncDeletedConnections/syncConnectionName methods - Create useConnectionSync composable for automatic sync - Support migration from old layout with user prompt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
92
frontend/src/components/MigrationPrompt.vue
Normal file
92
frontend/src/components/MigrationPrompt.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Info, X } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
migrate: []
|
||||||
|
dismiss: []
|
||||||
|
close: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const dontShowAgain = ref(false)
|
||||||
|
|
||||||
|
function handleMigrate() {
|
||||||
|
if (dontShowAgain.value) {
|
||||||
|
localStorage.setItem('ssh-manager.migration-dismissed', 'true')
|
||||||
|
}
|
||||||
|
emit('migrate')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDismiss() {
|
||||||
|
if (dontShowAgain.value) {
|
||||||
|
localStorage.setItem('ssh-manager.migration-dismissed', 'true')
|
||||||
|
}
|
||||||
|
emit('dismiss')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||||
|
@click.self="emit('close')"
|
||||||
|
>
|
||||||
|
<div class="bg-slate-800 rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||||
|
<div class="flex items-start gap-3 p-5 border-b border-slate-700">
|
||||||
|
<Info class="w-5 h-5 text-cyan-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-base font-medium text-slate-100">欢迎使用新版布局</h3>
|
||||||
|
<p class="text-sm text-slate-400 mt-1">
|
||||||
|
我们检测到您之前使用过旧版布局,是否要迁移您的会话数据?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="emit('close')"
|
||||||
|
class="text-slate-400 hover:text-slate-200 transition-colors"
|
||||||
|
>
|
||||||
|
<X class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div class="bg-slate-900 rounded p-3 text-sm text-slate-300">
|
||||||
|
<p class="font-medium mb-2">迁移内容:</p>
|
||||||
|
<ul class="space-y-1 text-slate-400">
|
||||||
|
<li>• 所有连接将被导入到会话树</li>
|
||||||
|
<li>• 打开的终端和 SFTP 标签页将被保留</li>
|
||||||
|
<li>• 您可以随时切换回旧版布局</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-300 cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="dontShowAgain"
|
||||||
|
type="checkbox"
|
||||||
|
class="w-4 h-4 rounded border-slate-600 bg-slate-900 text-cyan-500 focus:ring-cyan-500 focus:ring-offset-0"
|
||||||
|
/>
|
||||||
|
<span>不再显示此提示</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 p-5 border-t border-slate-700">
|
||||||
|
<button
|
||||||
|
@click="handleMigrate"
|
||||||
|
class="flex-1 px-4 py-2 rounded bg-cyan-600 hover:bg-cyan-500 text-white text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
立即迁移
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleDismiss"
|
||||||
|
class="flex-1 px-4 py-2 rounded bg-slate-700 hover:bg-slate-600 text-slate-200 text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
稍后再说
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, provide } from 'vue'
|
||||||
import { useSessionTreeStore } from '../stores/sessionTree'
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
||||||
import { useWorkspaceStore } from '../stores/workspace'
|
import { useWorkspaceStore } from '../stores/workspace'
|
||||||
|
import { useTreeDragDrop } from '../composables/useTreeDragDrop'
|
||||||
import SessionTreeNode from './SessionTreeNode.vue'
|
import SessionTreeNode from './SessionTreeNode.vue'
|
||||||
import { FolderPlus } from 'lucide-vue-next'
|
import { FolderPlus } from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -13,6 +14,10 @@ const newFolderName = ref('')
|
|||||||
|
|
||||||
const rootNodes = computed(() => treeStore.rootNodes)
|
const rootNodes = computed(() => treeStore.rootNodes)
|
||||||
|
|
||||||
|
// Provide drag-drop handlers to child components
|
||||||
|
const dragHandlers = useTreeDragDrop()
|
||||||
|
provide('dragHandlers', dragHandlers)
|
||||||
|
|
||||||
function handleNodeClick(nodeId: string) {
|
function handleNodeClick(nodeId: string) {
|
||||||
const node = treeStore.nodes.find(n => n.id === nodeId)
|
const node = treeStore.nodes.find(n => n.id === nodeId)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, inject } from 'vue'
|
||||||
import { useSessionTreeStore } from '../stores/sessionTree'
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
||||||
import { useConnectionsStore } from '../stores/connections'
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
import type { SessionTreeNode as TreeNode } from '../types/sessionTree'
|
import type { SessionTreeNode as TreeNode } from '../types/sessionTree'
|
||||||
@@ -29,22 +29,88 @@ const connection = computed(() => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Drag-drop functionality
|
||||||
|
const dragHandlers = inject<any>('dragHandlers', null)
|
||||||
|
|
||||||
|
const isDragging = computed(() =>
|
||||||
|
dragHandlers?.draggedNode.value?.id === props.node.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const isDropTarget = computed(() =>
|
||||||
|
dragHandlers?.dropTarget.value?.id === props.node.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const dropPosition = computed(() =>
|
||||||
|
isDropTarget.value ? dragHandlers?.dropPosition.value : null
|
||||||
|
)
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
emit('click', props.node.id)
|
emit('click', props.node.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: DragEvent) {
|
||||||
|
if (dragHandlers) {
|
||||||
|
event.dataTransfer!.effectAllowed = 'move'
|
||||||
|
dragHandlers.handleDragStart(props.node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event: DragEvent) {
|
||||||
|
if (dragHandlers) {
|
||||||
|
dragHandlers.handleDragOver(event, props.node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave() {
|
||||||
|
if (dragHandlers) {
|
||||||
|
dragHandlers.handleDragLeave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent) {
|
||||||
|
if (dragHandlers) {
|
||||||
|
dragHandlers.handleDrop(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
if (dragHandlers) {
|
||||||
|
dragHandlers.handleDragEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded cursor-pointer hover:bg-slate-800 transition-colors"
|
draggable="true"
|
||||||
|
class="flex items-center gap-1.5 px-2 py-1.5 rounded cursor-pointer hover:bg-slate-800 transition-colors relative"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-slate-800 text-cyan-300': isSelected,
|
'bg-slate-800 text-cyan-300': isSelected,
|
||||||
'text-slate-300': !isSelected,
|
'text-slate-300': !isSelected,
|
||||||
|
'opacity-50': isDragging,
|
||||||
|
'ring-2 ring-cyan-500': isDropTarget && dropPosition === 'inside',
|
||||||
}"
|
}"
|
||||||
:style="{ paddingLeft: `${level * 16 + 8}px` }"
|
:style="{ paddingLeft: `${level * 16 + 8}px` }"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
|
@dragstart="handleDragStart"
|
||||||
|
@dragover="handleDragOver"
|
||||||
|
@dragleave="handleDragLeave"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
>
|
>
|
||||||
|
<!-- Drop indicator - before -->
|
||||||
|
<div
|
||||||
|
v-if="isDropTarget && dropPosition === 'before'"
|
||||||
|
class="absolute top-0 left-0 right-0 h-0.5 bg-cyan-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Drop indicator - after -->
|
||||||
|
<div
|
||||||
|
v-if="isDropTarget && dropPosition === 'after'"
|
||||||
|
class="absolute bottom-0 left-0 right-0 h-0.5 bg-cyan-500"
|
||||||
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="node.type === 'folder'"
|
v-if="node.type === 'folder'"
|
||||||
class="w-4 h-4 flex items-center justify-center"
|
class="w-4 h-4 flex items-center justify-center"
|
||||||
|
|||||||
38
frontend/src/composables/useConnectionSync.ts
Normal file
38
frontend/src/composables/useConnectionSync.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { watch } from 'vue'
|
||||||
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
||||||
|
|
||||||
|
export function useConnectionSync() {
|
||||||
|
const connectionsStore = useConnectionsStore()
|
||||||
|
const treeStore = useSessionTreeStore()
|
||||||
|
|
||||||
|
// Watch for new connections
|
||||||
|
watch(
|
||||||
|
() => connectionsStore.connections.length,
|
||||||
|
(newLength, oldLength) => {
|
||||||
|
if (newLength > oldLength) {
|
||||||
|
// New connection added
|
||||||
|
treeStore.syncNewConnections()
|
||||||
|
} else if (newLength < oldLength) {
|
||||||
|
// Connection deleted
|
||||||
|
treeStore.syncDeletedConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watch for connection name changes
|
||||||
|
watch(
|
||||||
|
() => connectionsStore.connections.map(c => ({ id: c.id, name: c.name })),
|
||||||
|
(newConnections, oldConnections) => {
|
||||||
|
if (!oldConnections) return
|
||||||
|
|
||||||
|
newConnections.forEach((newConn, index) => {
|
||||||
|
const oldConn = oldConnections[index]
|
||||||
|
if (oldConn && oldConn.id === newConn.id && oldConn.name !== newConn.name) {
|
||||||
|
treeStore.syncConnectionName(newConn.id, newConn.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
112
frontend/src/composables/useTreeDragDrop.ts
Normal file
112
frontend/src/composables/useTreeDragDrop.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
||||||
|
import type { SessionTreeNode } from '../types/sessionTree'
|
||||||
|
|
||||||
|
export function useTreeDragDrop() {
|
||||||
|
const treeStore = useSessionTreeStore()
|
||||||
|
const draggedNode = ref<SessionTreeNode | null>(null)
|
||||||
|
const dropTarget = ref<SessionTreeNode | null>(null)
|
||||||
|
const dropPosition = ref<'before' | 'after' | 'inside' | null>(null)
|
||||||
|
|
||||||
|
function canDropOn(target: SessionTreeNode, dragged: SessionTreeNode): boolean {
|
||||||
|
// Can't drop on itself
|
||||||
|
if (target.id === dragged.id) return false
|
||||||
|
|
||||||
|
// Can't drop on own descendants
|
||||||
|
if (isDescendant(target.id, dragged.id)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDescendant(nodeId: string, ancestorId: string): boolean {
|
||||||
|
const node = treeStore.nodes.find(n => n.id === nodeId)
|
||||||
|
if (!node || !node.parentId) return false
|
||||||
|
if (node.parentId === ancestorId) return true
|
||||||
|
return isDescendant(node.parentId, ancestorId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(node: SessionTreeNode) {
|
||||||
|
draggedNode.value = node
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event: DragEvent, target: SessionTreeNode) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!draggedNode.value || !canDropOn(target, draggedNode.value)) {
|
||||||
|
dropTarget.value = null
|
||||||
|
dropPosition.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
||||||
|
const y = event.clientY - rect.top
|
||||||
|
const height = rect.height
|
||||||
|
|
||||||
|
dropTarget.value = target
|
||||||
|
|
||||||
|
// Determine drop position based on cursor position
|
||||||
|
if (target.type === 'folder') {
|
||||||
|
if (y < height * 0.25) {
|
||||||
|
dropPosition.value = 'before'
|
||||||
|
} else if (y > height * 0.75) {
|
||||||
|
dropPosition.value = 'after'
|
||||||
|
} else {
|
||||||
|
dropPosition.value = 'inside'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connections can only have before/after
|
||||||
|
dropPosition.value = y < height / 2 ? 'before' : 'after'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave() {
|
||||||
|
dropTarget.value = null
|
||||||
|
dropPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (!draggedNode.value || !dropTarget.value || !dropPosition.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const dragged = draggedNode.value
|
||||||
|
const target = dropTarget.value
|
||||||
|
const position = dropPosition.value
|
||||||
|
|
||||||
|
if (position === 'inside' && target.type === 'folder') {
|
||||||
|
// Move inside folder
|
||||||
|
treeStore.moveNode(dragged.id, target.id, 0)
|
||||||
|
} else {
|
||||||
|
// Move before/after target
|
||||||
|
const targetOrder = target.order
|
||||||
|
const newParentId = target.parentId
|
||||||
|
const newOrder = position === 'before' ? targetOrder : targetOrder + 1
|
||||||
|
|
||||||
|
treeStore.moveNode(dragged.id, newParentId, newOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
draggedNode.value = null
|
||||||
|
dropTarget.value = null
|
||||||
|
dropPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
draggedNode.value = null
|
||||||
|
dropTarget.value = null
|
||||||
|
dropPosition.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
draggedNode,
|
||||||
|
dropTarget,
|
||||||
|
dropPosition,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDrop,
|
||||||
|
handleDragEnd,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useSessionTreeStore } from '../stores/sessionTree'
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
||||||
import { useWorkspaceStore } from '../stores/workspace'
|
import { useWorkspaceStore } from '../stores/workspace'
|
||||||
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
|
import { useConnectionSync } from '../composables/useConnectionSync'
|
||||||
import TopToolbar from '../components/TopToolbar.vue'
|
import TopToolbar from '../components/TopToolbar.vue'
|
||||||
import SessionTree from '../components/SessionTree.vue'
|
import SessionTree from '../components/SessionTree.vue'
|
||||||
import WorkspacePanel from '../components/WorkspacePanel.vue'
|
import WorkspacePanel from '../components/WorkspacePanel.vue'
|
||||||
|
import MigrationPrompt from '../components/MigrationPrompt.vue'
|
||||||
|
|
||||||
const treeStore = useSessionTreeStore()
|
const treeStore = useSessionTreeStore()
|
||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
|
const connectionsStore = useConnectionsStore()
|
||||||
|
|
||||||
onMounted(() => {
|
const showMigrationPrompt = ref(false)
|
||||||
|
|
||||||
|
// Enable bidirectional sync
|
||||||
|
useConnectionSync()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
treeStore.restore()
|
treeStore.restore()
|
||||||
workspaceStore.restore()
|
workspaceStore.restore()
|
||||||
|
|
||||||
if (treeStore.nodes.length === 0) {
|
// Check if migration is needed
|
||||||
treeStore.initFromConnections()
|
const migrationDismissed = localStorage.getItem('ssh-manager.migration-dismissed')
|
||||||
|
const hasOldData = connectionsStore.connections.length > 0
|
||||||
|
const hasNewData = treeStore.nodes.length > 0
|
||||||
|
|
||||||
|
if (!migrationDismissed && hasOldData && !hasNewData) {
|
||||||
|
showMigrationPrompt.value = true
|
||||||
|
} else if (treeStore.nodes.length === 0) {
|
||||||
|
await treeStore.initFromConnections()
|
||||||
|
} else {
|
||||||
|
// Sync with connections store
|
||||||
|
treeStore.syncNewConnections()
|
||||||
|
treeStore.syncDeletedConnections()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function handleMigrate() {
|
||||||
|
await treeStore.initFromConnections()
|
||||||
|
showMigrationPrompt.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDismiss() {
|
||||||
|
showMigrationPrompt.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
showMigrationPrompt.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,5 +65,12 @@ onMounted(() => {
|
|||||||
<WorkspacePanel />
|
<WorkspacePanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MigrationPrompt
|
||||||
|
:show="showMigrationPrompt"
|
||||||
|
@migrate="handleMigrate"
|
||||||
|
@dismiss="handleDismiss"
|
||||||
|
@close="handleClose"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -155,5 +155,62 @@ export const useSessionTreeStore = defineStore('sessionTree', {
|
|||||||
this.createConnectionNode(conn.id, conn.name, defaultFolder.id)
|
this.createConnectionNode(conn.id, conn.name, defaultFolder.id)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Sync with connections store - add new connections
|
||||||
|
syncNewConnections() {
|
||||||
|
const connectionsStore = useConnectionsStore()
|
||||||
|
const existingConnectionIds = new Set(
|
||||||
|
this.nodes
|
||||||
|
.filter(n => n.type === 'connection' && n.connectionId)
|
||||||
|
.map(n => n.connectionId!)
|
||||||
|
)
|
||||||
|
|
||||||
|
const newConnections = connectionsStore.connections.filter(
|
||||||
|
conn => !existingConnectionIds.has(conn.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newConnections.length > 0) {
|
||||||
|
// Add to root or default folder
|
||||||
|
let targetParentId: string | null = null
|
||||||
|
const defaultFolder = this.nodes.find(
|
||||||
|
n => n.type === 'folder' && n.name === '我的连接'
|
||||||
|
)
|
||||||
|
if (defaultFolder) {
|
||||||
|
targetParentId = defaultFolder.id
|
||||||
|
}
|
||||||
|
|
||||||
|
newConnections.forEach(conn => {
|
||||||
|
this.createConnectionNode(conn.id, conn.name, targetParentId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sync with connections store - remove deleted connections
|
||||||
|
syncDeletedConnections() {
|
||||||
|
const connectionsStore = useConnectionsStore()
|
||||||
|
const validConnectionIds = new Set(
|
||||||
|
connectionsStore.connections.map(c => c.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodesToDelete = this.nodes.filter(
|
||||||
|
n => n.type === 'connection' &&
|
||||||
|
n.connectionId &&
|
||||||
|
!validConnectionIds.has(n.connectionId)
|
||||||
|
)
|
||||||
|
|
||||||
|
nodesToDelete.forEach(node => {
|
||||||
|
this.deleteNode(node.id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Update connection name when changed
|
||||||
|
syncConnectionName(connectionId: number, newName: string) {
|
||||||
|
const node = this.nodes.find(
|
||||||
|
n => n.type === 'connection' && n.connectionId === connectionId
|
||||||
|
)
|
||||||
|
if (node && node.name !== newName) {
|
||||||
|
this.renameNode(node.id, newName)
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user