Add drag and drop to resort connection list

Add filter search connection
This commit is contained in:
tiny-craft 2023-07-02 22:25:23 +08:00
parent d573cab0c0
commit 0f29a3c34f
9 changed files with 168 additions and 30 deletions

View File

@ -119,6 +119,17 @@ func (c *connectionService) RemoveConnection(name string) (resp types.JSResp) {
return return
} }
// SaveSortedConnection save sorted connection after drag
func (c *connectionService) SaveSortedConnection(sortedConns types.Connections) (resp types.JSResp) {
err := c.conns.SaveSortedConnection(sortedConns)
if err != nil {
resp.Msg = err.Error()
return
}
resp.Success = true
return
}
// CreateGroup create new group // CreateGroup create new group
func (c *connectionService) CreateGroup(name string) (resp types.JSResp) { func (c *connectionService) CreateGroup(name string) (resp types.JSResp) {
err := c.conns.CreateGroup(name) err := c.conns.CreateGroup(name)

View File

@ -5,6 +5,7 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"sync" "sync"
"tinyrdm/backend/types" "tinyrdm/backend/types"
sliceutil "tinyrdm/backend/utils/slice"
) )
type ConnectionsStorage struct { type ConnectionsStorage struct {
@ -240,6 +241,47 @@ func (c *ConnectionsStorage) RemoveConnection(name string) error {
return c.saveConnections(conns) return c.saveConnections(conns)
} }
// SaveSortedConnection save connection after sort
func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Connections) error {
c.mutex.Lock()
defer c.mutex.Unlock()
conns := c.GetConnectionsFlat()
takeConn := func(name string) (types.Connection, bool) {
idx, ok := sliceutil.Find(conns, func(i int) bool {
return conns[i].Name == name
})
if ok {
ret := conns[idx]
conns = append(conns[:idx], conns[idx+1:]...)
return ret, true
}
return types.Connection{}, false
}
var replaceConn func(connections types.Connections) types.Connections
replaceConn = func(cons types.Connections) types.Connections {
var newConns types.Connections
for _, conn := range cons {
if conn.Type == "group" {
newConns = append(newConns, types.Connection{
ConnectionConfig: types.ConnectionConfig{
Name: conn.Name,
},
Type: "group",
Connections: replaceConn(conn.Connections),
})
} else {
if foundConn, ok := takeConn(conn.Name); ok {
newConns = append(newConns, foundConn)
}
}
}
return newConns
}
conns = replaceConn(sortedConns)
return c.saveConnections(conns)
}
// CreateGroup create new group // CreateGroup create new group
func (c *ConnectionsStorage) CreateGroup(name string) error { func (c *ConnectionsStorage) CreateGroup(name string) error {
c.mutex.Lock() c.mutex.Lock()

View File

@ -4,7 +4,7 @@ type ConnectionCategory int
type ConnectionConfig struct { type ConnectionConfig struct {
Name string `json:"name" yaml:"name"` Name string `json:"name" yaml:"name"`
Group string `json:"group" yaml:"-"` Group string `json:"group,omitempty" yaml:"-"`
Addr string `json:"addr,omitempty" yaml:"addr,omitempty"` Addr string `json:"addr,omitempty" yaml:"addr,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty"`

View File

@ -9,13 +9,11 @@ import Filter from '../icons/Filter.vue'
import ConnectionTree from './ConnectionTree.vue' import ConnectionTree from './ConnectionTree.vue'
import Unlink from '../icons/Unlink.vue' import Unlink from '../icons/Unlink.vue'
import useConnectionStore from '../../stores/connections.js' import useConnectionStore from '../../stores/connections.js'
import { ref } from 'vue'
const dialogStore = useDialogStore() const dialogStore = useDialogStore()
const connectionStore = useConnectionStore() const connectionStore = useConnectionStore()
const filterPattern = ref('')
const onSort = () => {
dialogStore.openPreferencesDialog()
}
const onDisconnectAll = () => { const onDisconnectAll = () => {
connectionStore.closeAllConnection() connectionStore.closeAllConnection()
@ -24,7 +22,7 @@ const onDisconnectAll = () => {
<template> <template>
<div class="nav-pane-container flex-box-v"> <div class="nav-pane-container flex-box-v">
<connection-tree /> <connection-tree :filter-pattern="filterPattern" />
<!-- bottom function bar --> <!-- bottom function bar -->
<div class="nav-pane-bottom flex-box-h"> <div class="nav-pane-bottom flex-box-h">
@ -53,9 +51,7 @@ const onDisconnectAll = () => {
t-tooltip="disconnect_all" t-tooltip="disconnect_all"
@click="onDisconnectAll" @click="onDisconnectAll"
/> />
<n-divider style="margin: 0 4px; --n-color: #aaa; width: 2px" vertical /> <n-input v-model:value="filterPattern" :placeholder="$t('filter')" clearable>
<icon-button :icon="Sort" color="#555" size="20" stroke-width="4" t-tooltip="sort_conn" @click="onSort" />
<n-input placeholder="">
<template #prefix> <template #prefix>
<n-icon :component="Filter" color="#aaa" size="20" /> <n-icon :component="Filter" color="#aaa" size="20" />
</template> </template>

View File

@ -6,7 +6,7 @@ import { NIcon, useDialog, useMessage } from 'naive-ui'
import { ConnectionType } from '../../consts/connection_type.js' import { ConnectionType } from '../../consts/connection_type.js'
import ToggleFolder from '../icons/ToggleFolder.vue' import ToggleFolder from '../icons/ToggleFolder.vue'
import ToggleServer from '../icons/ToggleServer.vue' import ToggleServer from '../icons/ToggleServer.vue'
import { indexOf } from 'lodash' import { debounce, indexOf, throttle } from 'lodash'
import Config from '../icons/Config.vue' import Config from '../icons/Config.vue'
import Delete from '../icons/Delete.vue' import Delete from '../icons/Delete.vue'
import Unlink from '../icons/Unlink.vue' import Unlink from '../icons/Unlink.vue'
@ -28,6 +28,12 @@ const message = useMessage()
const expandedKeys = ref([]) const expandedKeys = ref([])
const selectedKeys = ref([]) const selectedKeys = ref([])
const props = defineProps({
filterPattern: {
type: String,
},
})
onMounted(async () => { onMounted(async () => {
try { try {
loadingConnection.value = true loadingConnection.value = true
@ -152,11 +158,11 @@ const renderPrefix = ({ option }) => {
} }
} }
const onUpdateExpandedKeys = (keys, option, meta) => { const onUpdateExpandedKeys = (keys, option) => {
expandedKeys.value = keys expandedKeys.value = keys
} }
const onUpdateSelectedKeys = (keys, option, meta) => { const onUpdateSelectedKeys = (keys, option) => {
selectedKeys.value = keys selectedKeys.value = keys
} }
@ -259,6 +265,56 @@ const handleSelectContextMenu = (key) => {
} }
console.warn('TODO: handle context menu:' + key) console.warn('TODO: handle context menu:' + key)
} }
const findSiblingsAndIndex = (node, nodes) => {
if (!nodes) {
return [null, null]
}
for (let i = 0; i < nodes.length; ++i) {
const siblingNode = nodes[i]
if (siblingNode.key === node.key) {
return [nodes, i]
}
const [siblings, index] = findSiblingsAndIndex(node, siblingNode.children)
if (siblings && index !== null) {
return [siblings, index]
}
}
return [null, null]
}
// delay save until stop drop after 2 seconds
const saveSort = debounce(connectionStore.saveConnectionSort, 2000, { trailing: true })
const handleDrop = ({ node, dragNode, dropPosition }) => {
const [dragNodeSiblings, dragNodeIndex] = findSiblingsAndIndex(dragNode, connectionStore.connections)
if (dragNodeSiblings === null || dragNodeIndex === null) {
return
}
dragNodeSiblings.splice(dragNodeIndex, 1)
if (dropPosition === 'inside') {
if (node.children) {
node.children.unshift(dragNode)
} else {
node.children = [dragNode]
}
} else if (dropPosition === 'before') {
const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
if (nodeSiblings === null || nodeIndex === null) {
return
}
nodeSiblings.splice(nodeIndex, 0, dragNode)
} else if (dropPosition === 'after') {
const [nodeSiblings, nodeIndex] = findSiblingsAndIndex(node, connectionStore.connections)
if (nodeSiblings === null || nodeIndex === null) {
return
}
nodeSiblings.splice(nodeIndex + 1, 0, dragNode)
}
connectionStore.connections = Array.from(connectionStore.connections)
saveSort()
}
const saveDrop = () => {}
</script> </script>
<template> <template>
@ -267,15 +323,18 @@ const handleSelectContextMenu = (key) => {
:block-line="true" :block-line="true"
:block-node="true" :block-node="true"
:cancelable="false" :cancelable="false"
:draggable="true"
:data="connectionStore.connections" :data="connectionStore.connections"
:expand-on-click="true" :expand-on-click="true"
:expanded-keys="expandedKeys" :expanded-keys="expandedKeys"
:on-update:selected-keys="onUpdateSelectedKeys" @update:selected-keys="onUpdateSelectedKeys"
:node-props="nodeProps" :node-props="nodeProps"
:on-update:expanded-keys="onUpdateExpandedKeys" @update:expanded-keys="onUpdateExpandedKeys"
:selected-keys="selectedKeys" :selected-keys="selectedKeys"
:render-label="renderLabel" :render-label="renderLabel"
:render-prefix="renderPrefix" :render-prefix="renderPrefix"
@drop="handleDrop"
:pattern="props.filterPattern"
class="fill-height" class="fill-height"
virtual-scroll virtual-scroll
/> />

View File

@ -142,7 +142,7 @@ const expandKey = (key) => {
const message = useMessage() const message = useMessage()
const dialog = useDialog() const dialog = useDialog()
const onUpdateExpanded = (value, option, meta) => { const onUpdateExpanded = (value, option) => {
expandedKeys.value = value expandedKeys.value = value
if (!meta.node) { if (!meta.node) {
return return
@ -158,7 +158,7 @@ const onUpdateExpanded = (value, option, meta) => {
} }
} }
const onUpdateSelectedKeys = (keys, option, meta) => { const onUpdateSelectedKeys = (keys, option) => {
selectedKeys.value = keys selectedKeys.value = keys
} }
@ -325,10 +325,10 @@ const handleOutsideContextMenu = () => {
:expand-on-click="false" :expand-on-click="false"
:expanded-keys="expandedKeys" :expanded-keys="expandedKeys"
:selected-keys="selectedKeys" :selected-keys="selectedKeys"
:on-update:selected-keys="onUpdateSelectedKeys" @update:selected-keys="onUpdateSelectedKeys"
:node-props="nodeProps" :node-props="nodeProps"
:on-load="onLoadTree" @load="onLoadTree"
:on-update:expanded-keys="onUpdateExpanded" @update:expanded-keys="onUpdateExpanded"
:render-label="renderLabel" :render-label="renderLabel"
:render-prefix="renderPrefix" :render-prefix="renderPrefix"
:render-suffix="renderSuffix" :render-suffix="renderSuffix"

View File

@ -9,6 +9,7 @@
"new_group": "Add New Group", "new_group": "Add New Group",
"rename_group": "Rename Group", "rename_group": "Rename Group",
"disconnect_all": "Disconnect all connections", "disconnect_all": "Disconnect all connections",
"filter": "Filter",
"sort_conn": "Resort Connections", "sort_conn": "Resort Connections",
"reload_key": "Reload Current Key", "reload_key": "Reload Current Key",
"close_confirm": "Confirm close this tab and connection", "close_confirm": "Confirm close this tab and connection",

View File

@ -9,6 +9,7 @@
"new_group": "添加新分组", "new_group": "添加新分组",
"rename_group": "重命名分组", "rename_group": "重命名分组",
"disconnect_all": "断开所有连接", "disconnect_all": "断开所有连接",
"filter": "筛选",
"sort_conn": "调整连接顺序", "sort_conn": "调整连接顺序",
"reload_key": "重新载入此键内容", "reload_key": "重新载入此键内容",
"close_confirm": "是否关闭当前连接", "close_confirm": "是否关闭当前连接",

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { get, isEmpty, last, remove, size, sortedIndexBy, split, uniq } from 'lodash' import { get, isEmpty, last, map, remove, size, sortedIndexBy, split, uniq } from 'lodash'
import { import {
AddHashField, AddHashField,
AddListItem, AddListItem,
@ -17,6 +17,7 @@ import {
RenameGroup, RenameGroup,
RenameKey, RenameKey,
SaveConnection, SaveConnection,
SaveSortedConnection,
SetHashValue, SetHashValue,
SetKeyTTL, SetKeyTTL,
SetKeyValue, SetKeyValue,
@ -176,7 +177,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Create a new connection or update current connection profile * create a new connection or update current connection profile
* @param {string} name set null if create a new connection * @param {string} name set null if create a new connection
* @param {{}} param * @param {{}} param
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
@ -193,7 +194,34 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Check if connection is connected * save connection
* @returns {Promise<void>}
*/
async saveConnectionSort() {
const mapToList = (conns) => {
const list = []
for (const conn of conns) {
if (conn.type === ConnectionType.Group) {
const children = mapToList(conn.children)
list.push({
name: conn.label,
type: 'group',
connections: children,
})
} else if (conn.type === ConnectionType.Server) {
list.push({
name: conn.name,
})
}
}
return list
}
const s = mapToList(this.connections)
SaveSortedConnection(s)
},
/**
* check if connection is connected
* @param name * @param name
* @returns {boolean} * @returns {boolean}
*/ */
@ -203,7 +231,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Open connection * open connection
* @param {string} name * @param {string} name
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@ -241,7 +269,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Close connection * close connection
* @param {string} name * @param {string} name
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
@ -260,7 +288,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Close all connection * close all connection
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async closeAllConnection() { async closeAllConnection() {
@ -274,7 +302,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Remove connection * remove connection
* @param name * @param name
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
*/ */
@ -290,7 +318,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Create connection group * create connection group
* @param name * @param name
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
*/ */
@ -304,7 +332,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Rename connection group * rename connection group
* @param name * @param name
* @param newName * @param newName
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
@ -322,7 +350,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Remove group by name * remove group by name
* @param {string} name * @param {string} name
* @param {boolean} [includeConn] * @param {boolean} [includeConn]
* @returns {Promise<{success: boolean, [msg]: string}>} * @returns {Promise<{success: boolean, [msg]: string}>}
@ -337,7 +365,7 @@ const useConnectionStore = defineStore('connections', {
}, },
/** /**
* Open database and load all keys * open database and load all keys
* @param connName * @param connName
* @param db * @param db
* @returns {Promise<void>} * @returns {Promise<void>}