Skip to content

Commit c849368

Browse files
authored
fix: drag connection folder to its child will remove the whole folder (#114)
* feat: add connection folders * feat: support inline rename * feat: support drag into connection node * feat: add warning when remove connection or folder * feat: save the collapsed setting * fixing some code smell * feat: fix code smell * feat: when new folder or new connection, collapsed the folder * feat: make the collapsed prevent duplicate key * fix bug which you can drag parent node to child node
1 parent 772ec1a commit c849368

File tree

4 files changed

+200
-139
lines changed

4 files changed

+200
-139
lines changed

src/libs/ConnectionSettingTree.tsx

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { faFolder } from '@fortawesome/free-solid-svg-icons';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { ConnectionConfigTree } from 'drivers/SQLLikeConnection';
4+
import Icon from 'renderer/components/Icon';
5+
import { TreeViewItemData } from 'renderer/components/TreeView';
6+
7+
export default class ConnectionSettingTree {
8+
protected tree: ConnectionConfigTree[];
9+
protected dict: Record<string, ConnectionConfigTree> = {};
10+
11+
constructor(tree: ConnectionConfigTree[]) {
12+
this.tree = tree;
13+
this.rebuildHashTable();
14+
}
15+
16+
protected rebuildHashTable() {
17+
this.dict = {};
18+
this.buildHashTable(this.tree);
19+
}
20+
21+
protected buildHashTable(tree?: ConnectionConfigTree[]) {
22+
if (!tree) return;
23+
for (const node of tree) {
24+
this.dict[node.id] = node;
25+
this.buildHashTable(node.children);
26+
}
27+
}
28+
29+
protected buildTreeViewInternal(
30+
root?: ConnectionConfigTree[]
31+
): TreeViewItemData<ConnectionConfigTree>[] {
32+
if (!root) return [];
33+
34+
return root.map((config) => {
35+
return {
36+
id: config.id,
37+
data: config,
38+
icon:
39+
config.nodeType === 'folder' ? (
40+
<FontAwesomeIcon icon={faFolder} color="#f39c12" />
41+
) : (
42+
<Icon.MySql />
43+
),
44+
text: config.name,
45+
children:
46+
config.children && config.children.length > 0
47+
? this.buildTreeViewInternal(config.children)
48+
: undefined,
49+
};
50+
});
51+
}
52+
53+
protected sortConnection(tree: ConnectionConfigTree[]) {
54+
const tmp = [...tree];
55+
tmp.sort((a, b) => {
56+
if (a.nodeType === 'folder' && b.nodeType === 'folder')
57+
return a.name.localeCompare(b.name);
58+
else if (a.nodeType === 'folder') {
59+
return -1;
60+
} else if (b.nodeType === 'folder') {
61+
return 1;
62+
}
63+
return a.name.localeCompare(b.name);
64+
});
65+
return tmp;
66+
}
67+
68+
buildTreeView() {
69+
return this.buildTreeViewInternal(this.tree);
70+
}
71+
72+
getAllNodes() {
73+
return Object.values(this.dict);
74+
}
75+
76+
getById(id?: string) {
77+
if (!id) return;
78+
return this.dict[id];
79+
}
80+
81+
getNewTree() {
82+
return [...this.tree];
83+
}
84+
85+
isParentAndChild(
86+
parent: ConnectionConfigTree,
87+
child: ConnectionConfigTree
88+
): boolean {
89+
let ptr: ConnectionConfigTree | undefined = child;
90+
while (ptr) {
91+
if (ptr.id === parent.id) return true;
92+
ptr = this.getById(ptr.parentId);
93+
}
94+
return false;
95+
}
96+
97+
detachFromParent(node: ConnectionConfigTree) {
98+
if (node.parentId) {
99+
const parent = this.getById(node.parentId);
100+
if (parent?.children) {
101+
parent.children = parent.children.filter(
102+
(child) => child.id !== node.id
103+
);
104+
}
105+
} else {
106+
this.tree = this.tree.filter((child) => child.id !== node.id);
107+
}
108+
}
109+
110+
moveNodeToRoot(from: ConnectionConfigTree) {
111+
this.detachFromParent(from);
112+
this.insertNode(from);
113+
}
114+
115+
moveNode(from: ConnectionConfigTree, to: ConnectionConfigTree) {
116+
// Stop operation if we are trying to move parent node
117+
// into its child node. It is impossible operation
118+
if (this.isParentAndChild(from, to)) return;
119+
120+
this.detachFromParent(from);
121+
this.insertNode(from, to.id);
122+
}
123+
124+
insertNode(node: ConnectionConfigTree, parentId?: string) {
125+
const parent: ConnectionConfigTree | undefined = this.getById(parentId);
126+
127+
if (parent) {
128+
const folderParent =
129+
parent.nodeType === 'folder' ? parent : this.getById(parent.parentId);
130+
131+
if (folderParent?.children) {
132+
node.parentId = folderParent.id;
133+
folderParent.children = this.sortConnection([
134+
...folderParent.children,
135+
node,
136+
]);
137+
return;
138+
}
139+
}
140+
141+
node.parentId = undefined;
142+
this.tree = this.sortConnection([...this.tree, node]);
143+
}
144+
}

src/renderer/hooks/useIndexDbConnections.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import { useCallback, useEffect, useMemo, useState } from 'react';
22
import { db } from 'renderer/db';
33
import { ConnectionConfigTree } from 'drivers/SQLLikeConnection';
4+
import ConnectionSettingTree from 'libs/ConnectionSettingTree';
45

56
export function useIndexDbConnection() {
67
const [connections, setInternalConnections] =
78
useState<ConnectionConfigTree[]>();
89

10+
const connectionTree = useMemo(() => {
11+
return new ConnectionSettingTree(connections ?? []);
12+
}, [connections]);
13+
14+
915
const initialCollapsed = useMemo<string[]>(() => {
1016
try {
1117
return JSON.parse(localStorage.getItem('db_collapsed_keys') ?? '[]');
@@ -36,5 +42,11 @@ export function useIndexDbConnection() {
3642
[setInternalConnections]
3743
);
3844

39-
return { connections, setConnections, initialCollapsed, saveCollapsed };
45+
return {
46+
connections,
47+
setConnections,
48+
connectionTree,
49+
initialCollapsed,
50+
saveCollapsed,
51+
};
4052
}

src/renderer/screens/HomeScreen/index.tsx

Lines changed: 24 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { useCallback, useEffect, useState, useMemo } from 'react';
2-
import Icon from 'renderer/components/Icon';
3-
42
import {
53
ConnectionConfigTree,
64
ConnectionStoreItem,
@@ -19,7 +17,7 @@ import { useIndexDbConnection } from 'renderer/hooks/useIndexDbConnections';
1917
import TreeView, { TreeViewItemData } from 'renderer/components/TreeView';
2018
import useConnectionContextMenu from './useConnectionContextMenu';
2119
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
22-
import { faCircleDot, faFolder } from '@fortawesome/free-solid-svg-icons';
20+
import { faCircleDot } from '@fortawesome/free-solid-svg-icons';
2321
import ListViewEmptyState from 'renderer/components/ListView/ListViewEmptyState';
2422

2523
const WELCOME_SCREEN_ID = '00000000000000000000';
@@ -42,8 +40,13 @@ export function sortConnection(tree: ConnectionConfigTree[]) {
4240
export default function HomeScreen() {
4341
const { connect } = useConnection();
4442

45-
const { connections, setConnections, initialCollapsed, saveCollapsed } =
46-
useIndexDbConnection();
43+
const {
44+
connections,
45+
setConnections,
46+
connectionTree,
47+
initialCollapsed,
48+
saveCollapsed,
49+
} = useIndexDbConnection();
4750
const [selectedItem, setSelectedItem] = useState<
4851
TreeViewItemData<ConnectionConfigTree> | undefined
4952
>({ id: WELCOME_SCREEN_ID });
@@ -68,32 +71,8 @@ export default function HomeScreen() {
6871

6972
const [renameSelectedItem, setRenameSelectedItem] = useState(false);
7073

71-
const { treeItems, treeDict } = useMemo(() => {
72-
const treeDict: Record<string, ConnectionConfigTree> = {};
73-
74-
function buildTree(
75-
configs: ConnectionConfigTree[]
76-
): TreeViewItemData<ConnectionConfigTree>[] {
77-
return configs.map((config) => {
78-
treeDict[config.id] = config;
79-
80-
return {
81-
id: config.id,
82-
data: config,
83-
icon:
84-
config.nodeType === 'folder' ? (
85-
<FontAwesomeIcon icon={faFolder} color="#f39c12" />
86-
) : (
87-
<Icon.MySql />
88-
),
89-
text: config.name,
90-
children:
91-
config.children && config.children.length > 0
92-
? buildTree(config.children)
93-
: undefined,
94-
};
95-
});
96-
}
74+
const treeItems = useMemo(() => {
75+
const treeNode = connectionTree.buildTreeView();
9776

9877
const welcomeNode = {
9978
id: WELCOME_SCREEN_ID,
@@ -103,26 +82,18 @@ export default function HomeScreen() {
10382
text: 'Welcome to QueryMaster',
10483
} as TreeViewItemData<ConnectionConfigTree>;
10584

106-
if (connections) {
107-
const treeNode = buildTree(connections);
108-
return {
109-
treeItems: treeNode.length > 0 ? [welcomeNode, ...treeNode] : [],
110-
treeDict,
111-
};
112-
}
113-
114-
return { treeItems: [], treeDict };
115-
}, [connections]);
85+
return treeNode.length > 0 ? [welcomeNode, ...treeNode] : [];
86+
}, [connectionTree]);
11687

11788
const setSaveCollapsedKeys = useCallback(
11889
(keys: string[] | undefined) => {
11990
const legitKeys = Array.from(
120-
new Set(keys?.filter((key) => !!treeDict[key]))
91+
new Set(keys?.filter((key) => !!connectionTree.getById(key)))
12192
);
12293

12394
setCollapsedKeys(legitKeys);
12495
},
125-
[setCollapsedKeys, saveCollapsed, treeDict]
96+
[setCollapsedKeys, saveCollapsed, connectionTree]
12697
);
12798

12899
// -----------------------------------------------
@@ -142,55 +113,15 @@ export default function HomeScreen() {
142113
from: TreeViewItemData<ConnectionConfigTree>,
143114
to: TreeViewItemData<ConnectionConfigTree>
144115
) => {
145-
if (connections) {
146-
let toData;
147-
148-
// You cannot drag anything into connection
149-
if (to.data?.nodeType === 'connection') {
150-
if (to.data?.parentId) {
151-
const parentTo = treeDict[to.data.parentId];
152-
if (parentTo) {
153-
toData = parentTo;
154-
}
155-
}
156-
} else {
157-
toData = to.data;
158-
}
159-
160-
const fromData = from.data;
161-
if (!fromData) return;
162-
163-
let newConnection = connections;
164-
165-
// Remove itself from its parent;
166-
if (fromData.parentId) {
167-
const parent = treeDict[fromData.parentId];
168-
if (parent?.children) {
169-
parent.children = parent.children.filter(
170-
(child) => child.id !== fromData.id
171-
);
172-
}
173-
} else {
174-
newConnection = connections.filter(
175-
(child) => child.id !== fromData.id
176-
);
177-
}
178-
179-
if (toData) {
180-
fromData.parentId = toData.id;
181-
toData.children = sortConnection([
182-
...(toData.children || []),
183-
fromData,
184-
]);
185-
} else {
186-
fromData.parentId = undefined;
187-
newConnection = [...newConnection, fromData];
188-
}
189-
190-
setConnections(sortConnection(newConnection));
116+
if (from.data && to.data) {
117+
connectionTree.moveNode(from.data, to.data);
118+
setConnections(connectionTree.getNewTree());
119+
} else if (from.data) {
120+
connectionTree.moveNodeToRoot(from.data);
121+
setConnections(connectionTree.getNewTree());
191122
}
192123
},
193-
[treeDict, connections, setConnections]
124+
[connectionTree, setConnections]
194125
);
195126

196127
const handleRenameExit = useCallback(
@@ -208,7 +139,7 @@ export default function HomeScreen() {
208139
prev ? { ...prev, name: newValue } : prev
209140
);
210141

211-
const parent = treeDict[selectedItem.id];
142+
const parent = connectionTree.getById(selectedItem.id);
212143
if (parent?.children) {
213144
parent.children = sortConnection(parent.children);
214145
}
@@ -218,7 +149,7 @@ export default function HomeScreen() {
218149
setRenameSelectedItem(false);
219150
},
220151
[
221-
treeDict,
152+
connectionTree,
222153
connections,
223154
setConnections,
224155
selectedItem,
@@ -259,7 +190,7 @@ export default function HomeScreen() {
259190
setConnections,
260191
setRenameSelectedItem,
261192
selectedItem,
262-
treeDict,
193+
connectionTree,
263194
});
264195

265196
return (

0 commit comments

Comments
 (0)