Skip to content
144 changes: 144 additions & 0 deletions src/libs/ConnectionSettingTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { faFolder } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ConnectionConfigTree } from 'drivers/SQLLikeConnection';
import Icon from 'renderer/components/Icon';
import { TreeViewItemData } from 'renderer/components/TreeView';

export default class ConnectionSettingTree {
protected tree: ConnectionConfigTree[];
protected dict: Record<string, ConnectionConfigTree> = {};

constructor(tree: ConnectionConfigTree[]) {
this.tree = tree;
this.rebuildHashTable();
}

protected rebuildHashTable() {
this.dict = {};
this.buildHashTable(this.tree);
}

protected buildHashTable(tree?: ConnectionConfigTree[]) {
if (!tree) return;
for (const node of tree) {
this.dict[node.id] = node;
this.buildHashTable(node.children);
}
}

protected buildTreeViewInternal(
root?: ConnectionConfigTree[]
): TreeViewItemData<ConnectionConfigTree>[] {
if (!root) return [];

return root.map((config) => {
return {
id: config.id,
data: config,
icon:
config.nodeType === 'folder' ? (
<FontAwesomeIcon icon={faFolder} color="#f39c12" />
) : (
<Icon.MySql />
),
text: config.name,
children:
config.children && config.children.length > 0
? this.buildTreeViewInternal(config.children)
: undefined,
};
});
}

protected sortConnection(tree: ConnectionConfigTree[]) {
const tmp = [...tree];
tmp.sort((a, b) => {
if (a.nodeType === 'folder' && b.nodeType === 'folder')
return a.name.localeCompare(b.name);
else if (a.nodeType === 'folder') {
return -1;
} else if (b.nodeType === 'folder') {
return 1;
}
return a.name.localeCompare(b.name);
});
return tmp;
}

buildTreeView() {
return this.buildTreeViewInternal(this.tree);
}

getAllNodes() {
return Object.values(this.dict);
}

getById(id?: string) {
if (!id) return;
return this.dict[id];
}

getNewTree() {
return [...this.tree];
}

isParentAndChild(
parent: ConnectionConfigTree,
child: ConnectionConfigTree
): boolean {
let ptr: ConnectionConfigTree | undefined = child;
while (ptr) {
if (ptr.id === parent.id) return true;
ptr = this.getById(ptr.parentId);
}
return false;
}

detachFromParent(node: ConnectionConfigTree) {
if (node.parentId) {
const parent = this.getById(node.parentId);
if (parent?.children) {
parent.children = parent.children.filter(
(child) => child.id !== node.id
);
}
} else {
this.tree = this.tree.filter((child) => child.id !== node.id);
}
}

moveNodeToRoot(from: ConnectionConfigTree) {
this.detachFromParent(from);
this.insertNode(from);
}

moveNode(from: ConnectionConfigTree, to: ConnectionConfigTree) {
// Stop operation if we are trying to move parent node
// into its child node. It is impossible operation
if (this.isParentAndChild(from, to)) return;

this.detachFromParent(from);
this.insertNode(from, to.id);
}

insertNode(node: ConnectionConfigTree, parentId?: string) {
const parent: ConnectionConfigTree | undefined = this.getById(parentId);

if (parent) {
const folderParent =
parent.nodeType === 'folder' ? parent : this.getById(parent.parentId);

if (folderParent?.children) {
node.parentId = folderParent.id;
folderParent.children = this.sortConnection([
...folderParent.children,
node,
]);
return;
}
}

node.parentId = undefined;
this.tree = this.sortConnection([...this.tree, node]);
}
}
14 changes: 13 additions & 1 deletion src/renderer/hooks/useIndexDbConnections.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { db } from 'renderer/db';
import { ConnectionConfigTree } from 'drivers/SQLLikeConnection';
import ConnectionSettingTree from 'libs/ConnectionSettingTree';

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

const connectionTree = useMemo(() => {
return new ConnectionSettingTree(connections ?? []);
}, [connections]);


const initialCollapsed = useMemo<string[]>(() => {
try {
return JSON.parse(localStorage.getItem('db_collapsed_keys') ?? '[]');
Expand Down Expand Up @@ -36,5 +42,11 @@ export function useIndexDbConnection() {
[setInternalConnections]
);

return { connections, setConnections, initialCollapsed, saveCollapsed };
return {
connections,
setConnections,
connectionTree,
initialCollapsed,
saveCollapsed,
};
}
117 changes: 24 additions & 93 deletions src/renderer/screens/HomeScreen/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { useCallback, useEffect, useState, useMemo } from 'react';
import Icon from 'renderer/components/Icon';

import {
ConnectionConfigTree,
ConnectionStoreItem,
Expand All @@ -19,7 +17,7 @@ import { useIndexDbConnection } from 'renderer/hooks/useIndexDbConnections';
import TreeView, { TreeViewItemData } from 'renderer/components/TreeView';
import useConnectionContextMenu from './useConnectionContextMenu';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleDot, faFolder } from '@fortawesome/free-solid-svg-icons';
import { faCircleDot } from '@fortawesome/free-solid-svg-icons';
import ListViewEmptyState from 'renderer/components/ListView/ListViewEmptyState';

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

const { connections, setConnections, initialCollapsed, saveCollapsed } =
useIndexDbConnection();
const {
connections,
setConnections,
connectionTree,
initialCollapsed,
saveCollapsed,
} = useIndexDbConnection();
const [selectedItem, setSelectedItem] = useState<
TreeViewItemData<ConnectionConfigTree> | undefined
>({ id: WELCOME_SCREEN_ID });
Expand All @@ -68,32 +71,8 @@ export default function HomeScreen() {

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

const { treeItems, treeDict } = useMemo(() => {
const treeDict: Record<string, ConnectionConfigTree> = {};

function buildTree(
configs: ConnectionConfigTree[]
): TreeViewItemData<ConnectionConfigTree>[] {
return configs.map((config) => {
treeDict[config.id] = config;

return {
id: config.id,
data: config,
icon:
config.nodeType === 'folder' ? (
<FontAwesomeIcon icon={faFolder} color="#f39c12" />
) : (
<Icon.MySql />
),
text: config.name,
children:
config.children && config.children.length > 0
? buildTree(config.children)
: undefined,
};
});
}
const treeItems = useMemo(() => {
const treeNode = connectionTree.buildTreeView();

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

if (connections) {
const treeNode = buildTree(connections);
return {
treeItems: treeNode.length > 0 ? [welcomeNode, ...treeNode] : [],
treeDict,
};
}

return { treeItems: [], treeDict };
}, [connections]);
return treeNode.length > 0 ? [welcomeNode, ...treeNode] : [];
}, [connectionTree]);

const setSaveCollapsedKeys = useCallback(
(keys: string[] | undefined) => {
const legitKeys = Array.from(
new Set(keys?.filter((key) => !!treeDict[key]))
new Set(keys?.filter((key) => !!connectionTree.getById(key)))
);

setCollapsedKeys(legitKeys);
},
[setCollapsedKeys, saveCollapsed, treeDict]
[setCollapsedKeys, saveCollapsed, connectionTree]
);

// -----------------------------------------------
Expand All @@ -142,55 +113,15 @@ export default function HomeScreen() {
from: TreeViewItemData<ConnectionConfigTree>,
to: TreeViewItemData<ConnectionConfigTree>
) => {
if (connections) {
let toData;

// You cannot drag anything into connection
if (to.data?.nodeType === 'connection') {
if (to.data?.parentId) {
const parentTo = treeDict[to.data.parentId];
if (parentTo) {
toData = parentTo;
}
}
} else {
toData = to.data;
}

const fromData = from.data;
if (!fromData) return;

let newConnection = connections;

// Remove itself from its parent;
if (fromData.parentId) {
const parent = treeDict[fromData.parentId];
if (parent?.children) {
parent.children = parent.children.filter(
(child) => child.id !== fromData.id
);
}
} else {
newConnection = connections.filter(
(child) => child.id !== fromData.id
);
}

if (toData) {
fromData.parentId = toData.id;
toData.children = sortConnection([
...(toData.children || []),
fromData,
]);
} else {
fromData.parentId = undefined;
newConnection = [...newConnection, fromData];
}

setConnections(sortConnection(newConnection));
if (from.data && to.data) {
connectionTree.moveNode(from.data, to.data);
setConnections(connectionTree.getNewTree());
} else if (from.data) {
connectionTree.moveNodeToRoot(from.data);
setConnections(connectionTree.getNewTree());
}
},
[treeDict, connections, setConnections]
[connectionTree, setConnections]
);

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

const parent = treeDict[selectedItem.id];
const parent = connectionTree.getById(selectedItem.id);
if (parent?.children) {
parent.children = sortConnection(parent.children);
}
Expand All @@ -218,7 +149,7 @@ export default function HomeScreen() {
setRenameSelectedItem(false);
},
[
treeDict,
connectionTree,
connections,
setConnections,
selectedItem,
Expand Down Expand Up @@ -259,7 +190,7 @@ export default function HomeScreen() {
setConnections,
setRenameSelectedItem,
selectedItem,
treeDict,
connectionTree,
});

return (
Expand Down
Loading