From f6fb143e11d98549acc83e0f9d4a7e5b10b8ce04 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 17 Aug 2023 20:43:52 +0700 Subject: [PATCH 01/10] feat: add connection folders --- src/drivers/SQLLikeConnection.ts | 9 + .../components/ListViewItem/index.tsx | 12 + src/renderer/components/TreeView/index.tsx | 72 +++-- src/renderer/db.ts | 23 ++ src/renderer/hooks/useIndexDbConnections.ts | 28 ++ src/renderer/screens/HomeScreen/index.tsx | 278 ++++++++++-------- .../HomeScreen/useConnectionContextMenu.tsx | 104 +++++++ src/renderer/utils/generateDatabaseName.ts | 12 - 8 files changed, 383 insertions(+), 155 deletions(-) create mode 100644 src/renderer/hooks/useIndexDbConnections.ts create mode 100644 src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx delete mode 100644 src/renderer/utils/generateDatabaseName.ts diff --git a/src/drivers/SQLLikeConnection.ts b/src/drivers/SQLLikeConnection.ts index c8cde25..7679471 100644 --- a/src/drivers/SQLLikeConnection.ts +++ b/src/drivers/SQLLikeConnection.ts @@ -24,6 +24,15 @@ export interface SqliteConnectionConfig { path: string; } +export interface ConnectionConfigTree { + id: string; + name: string; + nodeType: 'folder' | 'connection'; + config?: ConnectionStoreItem; + parentId?: string; + children?: ConnectionConfigTree[]; +} + export interface ConnectionStoreItem { id: string; name: string; diff --git a/src/renderer/components/ListViewItem/index.tsx b/src/renderer/components/ListViewItem/index.tsx index 9ffed3c..cee9d90 100644 --- a/src/renderer/components/ListViewItem/index.tsx +++ b/src/renderer/components/ListViewItem/index.tsx @@ -5,12 +5,16 @@ import { useAppFeature } from 'renderer/contexts/AppFeatureProvider'; interface ListViewItemProps { text: string; + draggable?: boolean; highlight?: string; icon?: ReactElement; changed?: boolean; selected?: boolean; onClick?: () => void; onDoubleClick?: () => void; + onDragStart?: (e: React.DragEvent) => void; + onDragOver?: (e: React.DragEvent) => void; + onDrop?: (e: React.DragEvent) => void; onContextMenu?: React.MouseEventHandler; // This is used for rendering TreeView @@ -36,6 +40,10 @@ export default function ListViewItem({ onClick, onDoubleClick, onContextMenu, + draggable, + onDragStart, + onDragOver, + onDrop, // For TreeView depth, @@ -75,6 +83,10 @@ export default function ListViewItem({ onClick={onClick} onDoubleClick={onDoubleClick} onContextMenu={onContextMenu} + draggable={draggable} + onDragOver={onDragOver} + onDragStart={onDragStart} + onDrop={onDrop} > {!!depth && new Array(depth) diff --git a/src/renderer/components/TreeView/index.tsx b/src/renderer/components/TreeView/index.tsx index db5bdc0..6628eef 100644 --- a/src/renderer/components/TreeView/index.tsx +++ b/src/renderer/components/TreeView/index.tsx @@ -2,6 +2,8 @@ import styles from './styles.module.scss'; import ListViewItem from '../ListViewItem'; import { ReactElement, useCallback } from 'react'; +let GLOBAL_TREE_DRAG_ITEM: unknown; + export interface TreeViewItemData { id: string; text?: string; @@ -14,40 +16,44 @@ interface TreeViewProps { items: TreeViewItemData[]; selected?: TreeViewItemData; collapsedKeys?: string[]; + draggable?: boolean; + onDragItem?: (from: TreeViewItemData, to: TreeViewItemData) => void; onCollapsedChange?: (value?: string[]) => void; onSelectChange?: (value?: TreeViewItemData) => void; + onBeforeSelectChange?: () => Promise; onDoubleClick?: (value: TreeViewItemData) => void; onContextMenu?: React.MouseEventHandler; highlight?: string; highlightDepth?: number; } +interface TreeViewItemProps { + draggable?: boolean; + onDragItem?: (from: TreeViewItemData, to: TreeViewItemData) => void; + item: TreeViewItemData; + depth: number; + selected?: TreeViewItemData; + onSelectChange?: (value?: TreeViewItemData) => void; + onCollapsedChange?: (value?: string[]) => void; + collapsedKeys?: string[]; + onDoubleClick?: (value: TreeViewItemData) => void; + highlight?: string; + highlightDepth?: number; +} + function TreeViewItem({ item, depth, - + draggable, selected, onSelectChange, - collapsedKeys, onCollapsedChange, onDoubleClick, - highlight, highlightDepth, -}: { - item: TreeViewItemData; - depth: number; - - selected?: TreeViewItemData; - onSelectChange?: (value?: TreeViewItemData) => void; - - onCollapsedChange?: (value?: string[]) => void; - collapsedKeys?: string[]; - onDoubleClick?: (value: TreeViewItemData) => void; - highlight?: string; - highlightDepth?: number; -}) { + onDragItem, +}: TreeViewItemProps) { const hasCollapsed = item.children && item.children.length > 0; const isCollapsed = collapsedKeys?.includes(item.id); @@ -60,6 +66,18 @@ function TreeViewItem({ return (
{ + GLOBAL_TREE_DRAG_ITEM = item; + }} + onDragOver={(e) => e.preventDefault()} + onDrop={() => { + if (onDragItem) { + if (GLOBAL_TREE_DRAG_ITEM) { + onDragItem(GLOBAL_TREE_DRAG_ITEM as TreeViewItemData, item); + } + } + }} key={item.id} text={item.text || ''} icon={item.icon} @@ -94,6 +112,8 @@ function TreeViewItem({ {item.children?.map((item) => { return ( ({ export default function TreeView({ items, + draggable, + onDragItem, selected, onSelectChange, + onBeforeSelectChange, onCollapsedChange, collapsedKeys, onDoubleClick, @@ -124,18 +147,33 @@ export default function TreeView({ highlight, highlightDepth, }: TreeViewProps) { + const onSelectChangeWithHook = useCallback( + (item: TreeViewItemData | undefined) => { + if (onSelectChange) { + if (onBeforeSelectChange) { + onBeforeSelectChange().then(() => onSelectChange(item)); + } else { + onSelectChange(item); + } + } + }, + [onSelectChange, onBeforeSelectChange] + ); + return (
{items.map((item) => { return ( { + const legacyItems = await trans.table('database_config').toArray(); + trans.table('setting').put({ + name: 'connections', + value: legacyItems.map( + (item) => + ({ + id: item.id, + config: item, + name: item.name, + nodeType: 'connection', + } as ConnectionConfigTree) + ), + }); + }); diff --git a/src/renderer/hooks/useIndexDbConnections.ts b/src/renderer/hooks/useIndexDbConnections.ts new file mode 100644 index 0000000..2c9fcdf --- /dev/null +++ b/src/renderer/hooks/useIndexDbConnections.ts @@ -0,0 +1,28 @@ +import { useCallback, useEffect, useState } from 'react'; +import { db } from 'renderer/db'; +import { ConnectionConfigTree } from 'drivers/SQLLikeConnection'; + +export function useIndexDbConnection() { + const [connections, setInternalConnections] = + useState(); + + useEffect(() => { + db.table<{ name: string; value: ConnectionConfigTree[] }>('setting') + .get('connections') + .then((connections) => { + if (connections) { + setInternalConnections(connections.value); + } + }); + }, [setInternalConnections]); + + const setConnections = useCallback( + (value: ConnectionConfigTree[]) => { + db.table('setting').put({ name: 'connections', value }); + setInternalConnections(value); + }, + [setInternalConnections] + ); + + return { connections, setConnections }; +} diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 605dfeb..2f779fb 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -1,12 +1,8 @@ -import { v1 as uuidv1 } from 'uuid'; -import { useCallback, useEffect, useState } from 'react'; - -import ListView from 'renderer/components/ListView'; +import { useCallback, useEffect, useState, useMemo } from 'react'; import Icon from 'renderer/components/Icon'; -import generateDatabaseName from 'renderer/utils/generateDatabaseName'; import { - ConnectionStoreConfig, + ConnectionConfigTree, ConnectionStoreItem, } from 'drivers/SQLLikeConnection'; import styles from './styles.module.scss'; @@ -16,123 +12,169 @@ import Button from 'renderer/components/Button'; import deepEqual from 'deep-equal'; import { useDebounce } from 'hooks/useDebounce'; -import ListViewEmptyState from 'renderer/components/ListView/ListViewEmptyState'; import WelcomeScreen from '../WelcomeScreen'; -import { useContextMenu } from 'renderer/contexts/ContextMenuProvider'; -import { db } from 'renderer/db'; import { useConnection } from 'renderer/App'; import SplitterLayout from 'renderer/components/Splitter/Splitter'; +import { useIndexDbConnection } from 'renderer/hooks/useIndexDbConnections'; +import TreeView, { TreeViewItemData } from 'renderer/components/TreeView'; +import useConnectionContextMenu from './useConnectionContextMenu'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFolder } from '@fortawesome/free-solid-svg-icons'; export default function HomeScreen() { const { connect } = useConnection(); - const [connectionList, setConnectionList] = useState( - [] - ); - const [selectedItem, setSelectedItem] = useState(); + + const { connections, setConnections } = useIndexDbConnection(); + const [selectedItem, setSelectedItem] = + useState>(); const [selectedItemChanged, setSelectedItemChanged] = useState(); + const [collapsedKeys, setCollapsedKeys] = useState([]); + useEffect(() => { - setSelectedItemChanged(selectedItem); + setSelectedItemChanged(selectedItem?.data?.config); }, [selectedItem, setSelectedItemChanged]); // Check if the selected item has unsaved changed const hasChange = useDebounce( - !!selectedItem && + !!selectedItem?.data && !!selectedItemChanged && - !deepEqual(selectedItem, selectedItemChanged), + !deepEqual(selectedItem.data.config, selectedItemChanged), 200 ); - useEffect(() => { - db.table('database_config').toArray().then(setConnectionList); - }, [setConnectionList]); + const { treeItems, treeDict } = useMemo(() => { + const treeDict: Record = {}; - // ---------------------------------------------- - // Handle duplicated database - // ---------------------------------------------- - const onDuplicateClick = useCallback(() => { - if (selectedItem) { - const newDuplicateDatabase: ConnectionStoreItem = { - ...selectedItem, - config: { ...selectedItem.config }, - name: generateDatabaseName(connectionList, selectedItem.name), - id: uuidv1(), - }; - - setConnectionList((prev) => { - const selectedIndex = prev.findIndex((db) => db.id === selectedItem.id); - return [ - ...prev.slice(0, selectedIndex + 1), - newDuplicateDatabase, - ...prev.slice(selectedIndex + 1), - ]; + function buildTree( + configs: ConnectionConfigTree[] + ): TreeViewItemData[] { + return configs.map((config) => { + treeDict[config.id] = config; + + return { + id: config.id, + data: config, + icon: + config.nodeType === 'folder' ? ( + + ) : ( + + ), + text: config.name, + children: + config.children && config.children.length > 0 + ? buildTree(config.children) + : undefined, + }; }); + } - setSelectedItem(newDuplicateDatabase); - db.table('database_config').put(newDuplicateDatabase); + if (connections) { + return { treeItems: buildTree(connections), treeDict }; } - }, [selectedItem, setConnectionList, connectionList, setSelectedItem]); + + return { treeItems: [], treeDict }; + }, [connections]); // ---------------------------------------------- - // Handle remove database + // Handle duplicated database // ---------------------------------------------- - const onRemoveClick = useCallback(() => { - if (selectedItem) { - setConnectionList((prev) => - prev.filter((db) => db.id !== selectedItem.id) - ); - setSelectedItem(undefined); - db.table('database_config').delete(selectedItem.id); - } - }, [selectedItem, setSelectedItem, setConnectionList]); + // const onDuplicateClick = useCallback(() => { + // if (selectedItem) { + // const newDuplicateDatabase: ConnectionStoreItem = { + // ...selectedItem, + // config: { ...selectedItem.config }, + // name: generateDatabaseName(connectionList, selectedItem.name), + // id: uuidv1(), + // }; + + // setConnectionList((prev) => { + // const selectedIndex = prev.findIndex((db) => db.id === selectedItem.id); + // return [ + // ...prev.slice(0, selectedIndex + 1), + // newDuplicateDatabase, + // ...prev.slice(selectedIndex + 1), + // ]; + // }); - // ----------------------------------------------- - // Handle save database - // ----------------------------------------------- + // setSelectedItem(newDuplicateDatabase); + // db.table('database_config').put(newDuplicateDatabase); + // } + // }, [selectedItem, setConnectionList, connectionList, setSelectedItem]); + + // // ---------------------------------------------- + // // Handle remove database + // // ---------------------------------------------- + // const onRemoveClick = useCallback(() => { + // if (selectedItem) { + // setConnectionList((prev) => + // prev.filter((db) => db.id !== selectedItem.id) + // ); + // setSelectedItem(undefined); + // db.table('database_config').delete(selectedItem.id); + // } + // }, [selectedItem, setSelectedItem, setConnectionList]); + + // // ----------------------------------------------- + // // Handle save database + // // ----------------------------------------------- const onSaveClick = useCallback(() => { - if (selectedItemChanged) { - setSelectedItem(selectedItemChanged); - setConnectionList((prev) => - prev.map((db) => { - if (db.id === selectedItemChanged.id) return selectedItemChanged; - return db; - }) - ); - db.table('database_config').put(selectedItemChanged); + if (selectedItemChanged && selectedItem?.data && connections) { + selectedItem.text = selectedItemChanged.name; + selectedItem.data.name = selectedItemChanged.name; + selectedItem.data.config = selectedItemChanged; + setSelectedItem(selectedItem); + setConnections([...connections]); } - }, [selectedItem, selectedItemChanged, setSelectedItem, setConnectionList]); + }, [connections, selectedItem, selectedItemChanged, setConnections]); - // ---------------------------------------------- - // Handle new connection - // ---------------------------------------------- - const newMySQLDatabaseSetting = useCallback(() => { - const newDatabaseSetting = { - id: uuidv1(), - name: generateDatabaseName(connectionList, 'Unnamed'), - type: 'mysql', - config: { - database: '', - host: '', - password: '', - port: '3306', - user: '', - } as ConnectionStoreConfig, - }; - - setConnectionList((prev) => [...prev, newDatabaseSetting]); - setSelectedItem(newDatabaseSetting); - }, [setConnectionList, setSelectedItem, connectionList]); - - // ----------------------------------------------- - // Handle before select change - // ----------------------------------------------- + const handleDragAndOverItem = useCallback( + ( + from: TreeViewItemData, + to: TreeViewItemData + ) => { + if (connections) { + // You cannot drag anything into connection + if (to.data?.nodeType === 'connection') return; + + const fromData = from.data; + const toData = to.data; + + if (!toData) return; + if (!fromData) return; + + // Remove itself from its parent; + if (fromData.parentId) { + const parent = treeDict[fromData.parentId]; + if (parent && parent.children) { + parent.children = parent.children.filter( + (child) => child.id !== fromData.id + ); + } + } + + fromData.parentId = toData.id; + toData.children = [...(toData.children || []), fromData]; + + setConnections([ + ...connections.filter((child) => child.id !== fromData.id), + ]); + } + }, + [treeDict, connections, setConnections] + ); + + // // ----------------------------------------------- + // // Handle before select change + // // ----------------------------------------------- const onBeforeSelectChange = useCallback(async () => { - if (hasChange && selectedItem) { + if (hasChange && selectedItemChanged) { const buttonIndex = await window.electron.showMessageBox({ title: 'Save modifications?', type: 'warning', - message: `Setting for ${selectedItem.name} were changed`, + message: `Setting for ${selectedItemChanged.name} were changed`, buttons: ['Yes', 'No', 'Cancel'], }); @@ -146,29 +188,13 @@ export default function HomeScreen() { } return true; - }, [selectedItem, onSaveClick, hasChange]); - - const { handleContextMenu } = useContextMenu(() => { - return [ - { - text: 'New MySQL Database', - icon: , - onClick: newMySQLDatabaseSetting, - separator: true, - }, - { - text: 'Duplicate', - onClick: onDuplicateClick, - disabled: !selectedItem, - }, - { - text: 'Remove', - onClick: onRemoveClick, - disabled: !selectedItem, - destructive: true, - }, - ]; - }, [newMySQLDatabaseSetting, onDuplicateClick, onRemoveClick, selectedItem]); + }, [selectedItemChanged, onSaveClick, hasChange]); + + const { handleContextMenu } = useConnectionContextMenu({ + connections, + setSelectedItem, + setConnections, + }); return (
@@ -179,23 +205,23 @@ export default function HomeScreen() { primaryMinSize={500} >
- - } - items={connectionList} - changeItemKeys={hasChange && selectedItem ? [selectedItem.id] : []} + { + if (item.data?.config) { + connect(item.data?.config); + } + }} + selected={selectedItem} onBeforeSelectChange={onBeforeSelectChange} - onDoubleClick={(item) => connect(item)} - extractMeta={(item) => ({ - icon: , - text: item.name, - key: item.id, - })} onContextMenu={handleContextMenu} /> + {/* */}
diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx new file mode 100644 index 0000000..9833e15 --- /dev/null +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -0,0 +1,104 @@ +import { v1 as uuidv1 } from 'uuid'; +import { useCallback } from 'react'; +import Icon from 'renderer/components/Icon'; +import { useContextMenu } from 'renderer/contexts/ContextMenuProvider'; +import generateIncrementalName from 'renderer/utils/generateIncrementalName'; +import { + ConnectionConfigTree, + ConnectionStoreConfig, +} from 'drivers/SQLLikeConnection'; +import { TreeViewItemData } from 'renderer/components/TreeView'; + +export default function useConnectionContextMenu({ + connections, + setConnections, + setSelectedItem, +}: { + connections: ConnectionConfigTree[] | undefined; + setConnections: (v: ConnectionConfigTree[]) => void; + setSelectedItem: ( + v: TreeViewItemData | undefined + ) => void; +}) { + // // ---------------------------------------------- + // // Handle new connection + // // ---------------------------------------------- + const newMySQLDatabaseSetting = useCallback(() => { + const newConnectionId = uuidv1(); + const newConfig = { + id: newConnectionId, + name: generateIncrementalName( + (connections || []).map((c) => c.name), + 'Unnamed' + ), + type: 'mysql', + config: { + database: '', + host: '', + password: '', + port: '3306', + user: '', + } as ConnectionStoreConfig, + }; + + const newTreeNode: ConnectionConfigTree = { + id: newConfig.id, + name: newConfig.name, + nodeType: 'connection', + config: newConfig, + }; + + setSelectedItem({ + id: newTreeNode.id, + text: newTreeNode.name, + data: newTreeNode, + icon: , + }); + + setConnections([...(connections || []), newTreeNode]); + }, [setConnections, setSelectedItem, connections]); + + const newFolderClicked = useCallback(() => { + const newFolderId = uuidv1(); + const newFolderName = generateIncrementalName( + (connections || []).map((c) => c.name), + 'Unnamed Folders' + ); + + const newTreeNode: ConnectionConfigTree = { + id: newFolderId, + name: newFolderName, + nodeType: 'folder', + children: [], + }; + + setConnections([...(connections || []), newTreeNode]); + }, [setConnections, connections]); + + const { handleContextMenu } = useContextMenu(() => { + return [ + { + text: 'New Folder', + onClick: newFolderClicked, + }, + { + text: 'New MySQL Database', + icon: , + onClick: newMySQLDatabaseSetting, + }, + // { + // text: 'Duplicate', + // onClick: onDuplicateClick, + // disabled: !selectedItem, + // }, + // { + // text: 'Remove', + // onClick: onRemoveClick, + // disabled: !selectedItem, + // destructive: true, + // }, + ]; + }, [newMySQLDatabaseSetting, newFolderClicked]); + + return { handleContextMenu }; +} diff --git a/src/renderer/utils/generateDatabaseName.ts b/src/renderer/utils/generateDatabaseName.ts deleted file mode 100644 index 41d72d9..0000000 --- a/src/renderer/utils/generateDatabaseName.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ConnectionStoreItem } from 'drivers/SQLLikeConnection'; -import generateIncrementalName from './generateIncrementalName'; - -export default function generateDatabaseName( - dbs: ConnectionStoreItem[], - name: string -) { - return generateIncrementalName( - dbs.map((db) => db.name), - name - ); -} From 6cfb5d7877df27fa571b36fc5e1db538b3cc307e Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 08:38:36 +0700 Subject: [PATCH 02/10] feat: support inline rename --- .../components/ListViewItem/index.tsx | 48 ++++++- .../ListViewItem/styles.module.scss | 12 +- src/renderer/components/TreeView/index.tsx | 121 +++++++++--------- .../HomeScreen/DatabaseConfigEditor.tsx | 1 - src/renderer/screens/HomeScreen/index.tsx | 38 +++++- .../HomeScreen/useConnectionContextMenu.tsx | 15 ++- 6 files changed, 158 insertions(+), 77 deletions(-) diff --git a/src/renderer/components/ListViewItem/index.tsx b/src/renderer/components/ListViewItem/index.tsx index cee9d90..fdfee4e 100644 --- a/src/renderer/components/ListViewItem/index.tsx +++ b/src/renderer/components/ListViewItem/index.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useMemo } from 'react'; +import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import styles from './styles.module.scss'; import Icon from '../Icon'; import { useAppFeature } from 'renderer/contexts/AppFeatureProvider'; @@ -17,6 +17,9 @@ interface ListViewItemProps { onDrop?: (e: React.DragEvent) => void; onContextMenu?: React.MouseEventHandler; + renaming?: boolean; + onRenamed?: (newName: string | null) => void; + // This is used for rendering TreeView depth?: number; hasCollapsed?: boolean; @@ -44,6 +47,8 @@ export default function ListViewItem({ onDragStart, onDragOver, onDrop, + renaming, + onRenamed, // For TreeView depth, @@ -52,6 +57,18 @@ export default function ListViewItem({ onCollapsedClick, }: ListViewItemProps) { const { theme } = useAppFeature(); + const [renameDraftValue, setRenameDraftValue] = useState(''); + + useEffect(() => { + setRenameDraftValue(text); + }, [renaming, text, setRenameDraftValue]); + + const onRenameDone = useCallback( + (value: string | null) => { + if (onRenamed) onRenamed(value); + }, + [onRenamed] + ); const finalText = useMemo(() => { if (highlight) { @@ -115,10 +132,31 @@ export default function ListViewItem({
))} {!hasCollapsed &&
{icon}
} -
+ {renaming ? ( +
+ { + onRenameDone(renameDraftValue); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + onRenameDone(renameDraftValue); + } else if (e.key === 'Escape') { + onRenameDone(null); + } + }} + type="text" + value={renameDraftValue} + onChange={(e) => setRenameDraftValue(e.currentTarget.value)} + /> +
+ ) : ( +
+ )}
); } diff --git a/src/renderer/components/ListViewItem/styles.module.scss b/src/renderer/components/ListViewItem/styles.module.scss index 5ae99da..d85432b 100644 --- a/src/renderer/components/ListViewItem/styles.module.scss +++ b/src/renderer/components/ListViewItem/styles.module.scss @@ -26,6 +26,14 @@ text-overflow: ellipsis; flex-grow: 1; overflow: hidden; + + input { + border: 0; + background: inherit; + color: inherit; + outline: none; + width: 100%; + } } .icon { @@ -34,7 +42,7 @@ display: flex; align-items: center; justify-content: center; - + img { width: 20px; height: 20px; @@ -63,4 +71,4 @@ border-top: 1px solid var(--color-list-line-guide); width: 10px; } -} \ No newline at end of file +} diff --git a/src/renderer/components/TreeView/index.tsx b/src/renderer/components/TreeView/index.tsx index 6628eef..58d281b 100644 --- a/src/renderer/components/TreeView/index.tsx +++ b/src/renderer/components/TreeView/index.tsx @@ -12,48 +12,61 @@ export interface TreeViewItemData { children?: TreeViewItemData[]; } -interface TreeViewProps { - items: TreeViewItemData[]; - selected?: TreeViewItemData; - collapsedKeys?: string[]; +interface TreeViewCommonProps { draggable?: boolean; onDragItem?: (from: TreeViewItemData, to: TreeViewItemData) => void; onCollapsedChange?: (value?: string[]) => void; onSelectChange?: (value?: TreeViewItemData) => void; - onBeforeSelectChange?: () => Promise; onDoubleClick?: (value: TreeViewItemData) => void; - onContextMenu?: React.MouseEventHandler; + selected?: TreeViewItemData; + collapsedKeys?: string[]; highlight?: string; highlightDepth?: number; + + /** + * When renameSelectedItem is true, it will render, the current + * selected item as editable field. + */ + renameSelectedItem?: boolean; + + /** + * When Enter or Lost Focus, it will treat as successful rename + * If user press escape, it will cancel the rename + * + * @param newName The new name that we just rename into. + * If it is null, it means we cancel the rename + * @returns + */ + onRenamedSelectedItem?: (newName: string | null) => void; } -interface TreeViewItemProps { - draggable?: boolean; - onDragItem?: (from: TreeViewItemData, to: TreeViewItemData) => void; +interface TreeViewProps extends TreeViewCommonProps { + items: TreeViewItemData[]; + onBeforeSelectChange?: () => Promise; + onContextMenu?: React.MouseEventHandler; +} + +interface TreeViewItemProps extends TreeViewCommonProps { item: TreeViewItemData; depth: number; - selected?: TreeViewItemData; - onSelectChange?: (value?: TreeViewItemData) => void; - onCollapsedChange?: (value?: string[]) => void; - collapsedKeys?: string[]; - onDoubleClick?: (value: TreeViewItemData) => void; - highlight?: string; - highlightDepth?: number; } -function TreeViewItem({ - item, - depth, - draggable, - selected, - onSelectChange, - collapsedKeys, - onCollapsedChange, - onDoubleClick, - highlight, - highlightDepth, - onDragItem, -}: TreeViewItemProps) { +function TreeViewItem(props: TreeViewItemProps) { + const { depth, item, ...common } = props; + const { + collapsedKeys, + draggable, + onDragItem, + onDoubleClick, + highlight, + selected, + onSelectChange, + onCollapsedChange, + highlightDepth, + renameSelectedItem, + onRenamedSelectedItem, + } = props; + const hasCollapsed = item.children && item.children.length > 0; const isCollapsed = collapsedKeys?.includes(item.id); @@ -63,6 +76,8 @@ function TreeViewItem({ } }, [onSelectChange, item]); + const isSelected = selected?.id === item.id; + return (
({ highlight={depth === highlightDepth ? highlight : undefined} hasCollapsed={hasCollapsed} collapsed={isCollapsed} - selected={selected?.id === item.id} + selected={isSelected} + renaming={isSelected && renameSelectedItem} + onRenamed={onRenamedSelectedItem} onClick={onSelectChangeCallback} onContextMenu={onSelectChangeCallback} onCollapsedClick={() => { @@ -112,18 +129,10 @@ function TreeViewItem({ {item.children?.map((item) => { return ( ); })} @@ -133,20 +142,15 @@ function TreeViewItem({ ); } -export default function TreeView({ - items, - draggable, - onDragItem, - selected, - onSelectChange, - onBeforeSelectChange, - onCollapsedChange, - collapsedKeys, - onDoubleClick, - onContextMenu, - highlight, - highlightDepth, -}: TreeViewProps) { +export default function TreeView(props: TreeViewProps) { + const { + items, + onSelectChange, + onBeforeSelectChange, + onContextMenu, + ...common + } = props; + const onSelectChangeWithHook = useCallback( (item: TreeViewItemData | undefined) => { if (onSelectChange) { @@ -165,18 +169,11 @@ export default function TreeView({ {items.map((item) => { return ( ); })} diff --git a/src/renderer/screens/HomeScreen/DatabaseConfigEditor.tsx b/src/renderer/screens/HomeScreen/DatabaseConfigEditor.tsx index 7dda67c..1201cb8 100644 --- a/src/renderer/screens/HomeScreen/DatabaseConfigEditor.tsx +++ b/src/renderer/screens/HomeScreen/DatabaseConfigEditor.tsx @@ -43,7 +43,6 @@ export default function DatabaseConfigEditor({ { onChange({ ...value, name: v }); diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 2f779fb..291337b 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -44,6 +44,8 @@ export default function HomeScreen() { 200 ); + const [renameSelectedItem, setRenameSelectedItem] = useState(false); + const { treeItems, treeDict } = useMemo(() => { const treeDict: Record = {}; @@ -166,9 +168,36 @@ export default function HomeScreen() { [treeDict, connections, setConnections] ); - // // ----------------------------------------------- - // // Handle before select change - // // ----------------------------------------------- + const handleRenameExit = useCallback( + (newValue: string | null) => { + if (connections && selectedItem && newValue) { + selectedItem.text = newValue; + if (selectedItem.data) { + selectedItem.data.name = newValue; + if (selectedItem.data.config) { + selectedItem.data.config.name = newValue; + } + } + + setSelectedItemChanged((prev) => + prev ? { ...prev, name: newValue } : prev + ); + setConnections([...connections]); + } + setRenameSelectedItem(false); + }, + [ + connections, + setConnections, + selectedItem, + setSelectedItemChanged, + setRenameSelectedItem, + ] + ); + + // ----------------------------------------------- + // Handle before select change + // ----------------------------------------------- const onBeforeSelectChange = useCallback(async () => { if (hasChange && selectedItemChanged) { const buttonIndex = await window.electron.showMessageBox({ @@ -194,6 +223,7 @@ export default function HomeScreen() { connections, setSelectedItem, setConnections, + setRenameSelectedItem, }); return ( @@ -207,6 +237,8 @@ export default function HomeScreen() {
void; setSelectedItem: ( v: TreeViewItemData | undefined ) => void; + setRenameSelectedItem: (v: boolean) => void; }) { - // // ---------------------------------------------- - // // Handle new connection - // // ---------------------------------------------- + // ---------------------------------------------- + // Handle new connection + // ---------------------------------------------- const newMySQLDatabaseSetting = useCallback(() => { const newConnectionId = uuidv1(); const newConfig = { @@ -77,6 +79,11 @@ export default function useConnectionContextMenu({ const { handleContextMenu } = useContextMenu(() => { return [ + { + text: 'Rename', + onClick: () => setRenameSelectedItem(true), + separator: true, + }, { text: 'New Folder', onClick: newFolderClicked, @@ -98,7 +105,7 @@ export default function useConnectionContextMenu({ // destructive: true, // }, ]; - }, [newMySQLDatabaseSetting, newFolderClicked]); + }, [newMySQLDatabaseSetting, newFolderClicked, setRenameSelectedItem]); return { handleContextMenu }; } From 40f7f790eee1246f785ddb76dbda7dcb9ce0b955 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 08:59:25 +0700 Subject: [PATCH 03/10] feat: support drag into connection node --- src/renderer/screens/HomeScreen/index.tsx | 62 +++++++++++++++++++---- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 291337b..b699f86 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -21,6 +21,21 @@ import useConnectionContextMenu from './useConnectionContextMenu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFolder } from '@fortawesome/free-solid-svg-icons'; +function 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; +} + export default function HomeScreen() { const { connect } = useConnection(); @@ -138,15 +153,25 @@ export default function HomeScreen() { to: TreeViewItemData ) => { if (connections) { + let toData; + // You cannot drag anything into connection - if (to.data?.nodeType === 'connection') return; + 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; - const toData = to.data; - - if (!toData) return; if (!fromData) return; + let newConnection = connections; + // Remove itself from its parent; if (fromData.parentId) { const parent = treeDict[fromData.parentId]; @@ -155,14 +180,24 @@ export default function HomeScreen() { (child) => child.id !== fromData.id ); } + } else { + newConnection = connections.filter( + (child) => child.id !== fromData.id + ); } - fromData.parentId = toData.id; - toData.children = [...(toData.children || []), fromData]; + if (toData) { + fromData.parentId = toData.id; + toData.children = sortConnection([ + ...(toData.children || []), + fromData, + ]); + } else { + fromData.parentId = undefined; + newConnection = [...newConnection, fromData]; + } - setConnections([ - ...connections.filter((child) => child.id !== fromData.id), - ]); + setConnections(sortConnection(newConnection)); } }, [treeDict, connections, setConnections] @@ -182,11 +217,18 @@ export default function HomeScreen() { setSelectedItemChanged((prev) => prev ? { ...prev, name: newValue } : prev ); - setConnections([...connections]); + + const parent = treeDict[selectedItem.id]; + if (parent && parent.children) { + parent.children = sortConnection(parent.children); + } + + setConnections(sortConnection(connections)); } setRenameSelectedItem(false); }, [ + treeDict, connections, setConnections, selectedItem, From 7eb840de6629441941747decaecc443a2e5eee42 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 09:19:40 +0700 Subject: [PATCH 04/10] feat: add warning when remove connection or folder --- src/renderer/screens/HomeScreen/index.tsx | 47 ++------------- .../HomeScreen/useConnectionContextMenu.tsx | 57 +++++++++++++++---- 2 files changed, 50 insertions(+), 54 deletions(-) diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index b699f86..c79e998 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -95,48 +95,9 @@ export default function HomeScreen() { return { treeItems: [], treeDict }; }, [connections]); - // ---------------------------------------------- - // Handle duplicated database - // ---------------------------------------------- - // const onDuplicateClick = useCallback(() => { - // if (selectedItem) { - // const newDuplicateDatabase: ConnectionStoreItem = { - // ...selectedItem, - // config: { ...selectedItem.config }, - // name: generateDatabaseName(connectionList, selectedItem.name), - // id: uuidv1(), - // }; - - // setConnectionList((prev) => { - // const selectedIndex = prev.findIndex((db) => db.id === selectedItem.id); - // return [ - // ...prev.slice(0, selectedIndex + 1), - // newDuplicateDatabase, - // ...prev.slice(selectedIndex + 1), - // ]; - // }); - - // setSelectedItem(newDuplicateDatabase); - // db.table('database_config').put(newDuplicateDatabase); - // } - // }, [selectedItem, setConnectionList, connectionList, setSelectedItem]); - - // // ---------------------------------------------- - // // Handle remove database - // // ---------------------------------------------- - // const onRemoveClick = useCallback(() => { - // if (selectedItem) { - // setConnectionList((prev) => - // prev.filter((db) => db.id !== selectedItem.id) - // ); - // setSelectedItem(undefined); - // db.table('database_config').delete(selectedItem.id); - // } - // }, [selectedItem, setSelectedItem, setConnectionList]); - - // // ----------------------------------------------- - // // Handle save database - // // ----------------------------------------------- + // ----------------------------------------------- + // Handle save database + // ----------------------------------------------- const onSaveClick = useCallback(() => { if (selectedItemChanged && selectedItem?.data && connections) { selectedItem.text = selectedItemChanged.name; @@ -266,6 +227,8 @@ export default function HomeScreen() { setSelectedItem, setConnections, setRenameSelectedItem, + selectedItem, + treeDict, }); return ( diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index d4e9cb7..c4fce4f 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -10,18 +10,49 @@ import { import { TreeViewItemData } from 'renderer/components/TreeView'; export default function useConnectionContextMenu({ + treeDict, connections, + selectedItem, setConnections, setSelectedItem, setRenameSelectedItem, }: { + treeDict: Record; connections: ConnectionConfigTree[] | undefined; + selectedItem?: TreeViewItemData; setConnections: (v: ConnectionConfigTree[]) => void; setSelectedItem: ( v: TreeViewItemData | undefined ) => void; setRenameSelectedItem: (v: boolean) => void; }) { + // ---------------------------------------------- + // Handle remove database + // ---------------------------------------------- + const onRemoveClick = useCallback(async () => { + if (selectedItem && connections) { + const buttonIndex = await window.electron.showMessageBox({ + title: 'Do you want to remove?', + message: `Do you want to remove ${selectedItem.text}?`, + buttons: ['Yes', 'No'], + }); + + if (buttonIndex !== 0) return; + + if (selectedItem.data?.parentId) { + const parent = treeDict[selectedItem.data.parentId]; + if (parent && parent.children) { + parent.children = parent.children.filter( + (node) => node.id === selectedItem.id + ); + } + } + + setConnections(connections.filter((db) => db.id !== selectedItem.id)); + setSelectedItem(undefined); + } + }, [selectedItem, setSelectedItem, connections, setConnections, treeDict]); + // ---------------------------------------------- // Handle new connection // ---------------------------------------------- @@ -81,6 +112,7 @@ export default function useConnectionContextMenu({ return [ { text: 'Rename', + disabled: !selectedItem, onClick: () => setRenameSelectedItem(true), separator: true, }, @@ -92,20 +124,21 @@ export default function useConnectionContextMenu({ text: 'New MySQL Database', icon: , onClick: newMySQLDatabaseSetting, + separator: true, + }, + { + text: 'Remove', + onClick: onRemoveClick, + disabled: !selectedItem, + destructive: true, }, - // { - // text: 'Duplicate', - // onClick: onDuplicateClick, - // disabled: !selectedItem, - // }, - // { - // text: 'Remove', - // onClick: onRemoveClick, - // disabled: !selectedItem, - // destructive: true, - // }, ]; - }, [newMySQLDatabaseSetting, newFolderClicked, setRenameSelectedItem]); + }, [ + selectedItem, + newMySQLDatabaseSetting, + newFolderClicked, + setRenameSelectedItem, + ]); return { handleContextMenu }; } From 5b309f377d68cbfe852517aa076b7d95f995abef Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 10:48:44 +0700 Subject: [PATCH 05/10] feat: save the collapsed setting --- src/renderer/hooks/useIndexDbConnections.ts | 16 +++++- src/renderer/screens/HomeScreen/index.tsx | 47 ++++++++++++++---- .../HomeScreen/useConnectionContextMenu.tsx | 49 ++++++++++++++++--- 3 files changed, 94 insertions(+), 18 deletions(-) diff --git a/src/renderer/hooks/useIndexDbConnections.ts b/src/renderer/hooks/useIndexDbConnections.ts index 2c9fcdf..0092f50 100644 --- a/src/renderer/hooks/useIndexDbConnections.ts +++ b/src/renderer/hooks/useIndexDbConnections.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { db } from 'renderer/db'; import { ConnectionConfigTree } from 'drivers/SQLLikeConnection'; @@ -6,6 +6,18 @@ export function useIndexDbConnection() { const [connections, setInternalConnections] = useState(); + const initialCollapsed = useMemo(() => { + try { + return JSON.parse(localStorage.getItem('db_collapsed_keys') || '[]'); + } catch { + return []; + } + }, []); + + const saveCollapsed = useCallback((keys: string[]) => { + localStorage.setItem('db_collapsed_keys', JSON.stringify(keys)); + }, []); + useEffect(() => { db.table<{ name: string; value: ConnectionConfigTree[] }>('setting') .get('connections') @@ -24,5 +36,5 @@ export function useIndexDbConnection() { [setInternalConnections] ); - return { connections, setConnections }; + return { connections, setConnections, initialCollapsed, saveCollapsed }; } diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index c79e998..64a705a 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -19,9 +19,11 @@ import { useIndexDbConnection } from 'renderer/hooks/useIndexDbConnections'; import TreeView, { TreeViewItemData } from 'renderer/components/TreeView'; import useConnectionContextMenu from './useConnectionContextMenu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faFolder } from '@fortawesome/free-solid-svg-icons'; +import { faCircleDot, faFolder } from '@fortawesome/free-solid-svg-icons'; -function sortConnection(tree: ConnectionConfigTree[]) { +const WELCOME_SCREEN_ID = '00000000000000000000'; + +export function sortConnection(tree: ConnectionConfigTree[]) { const tmp = [...tree]; tmp.sort((a, b) => { if (a.nodeType === 'folder' && b.nodeType === 'folder') @@ -39,13 +41,17 @@ function sortConnection(tree: ConnectionConfigTree[]) { export default function HomeScreen() { const { connect } = useConnection(); - const { connections, setConnections } = useIndexDbConnection(); - const [selectedItem, setSelectedItem] = - useState>(); + const { connections, setConnections, initialCollapsed, saveCollapsed } = + useIndexDbConnection(); + const [selectedItem, setSelectedItem] = useState< + TreeViewItemData | undefined + >({ id: WELCOME_SCREEN_ID }); const [selectedItemChanged, setSelectedItemChanged] = useState(); - const [collapsedKeys, setCollapsedKeys] = useState([]); + const [collapsedKeys, setCollapsedKeys] = useState( + initialCollapsed + ); useEffect(() => { setSelectedItemChanged(selectedItem?.data?.config); @@ -75,7 +81,7 @@ export default function HomeScreen() { data: config, icon: config.nodeType === 'folder' ? ( - + ) : ( ), @@ -89,12 +95,35 @@ export default function HomeScreen() { } if (connections) { - return { treeItems: buildTree(connections), treeDict }; + return { + treeItems: [ + { + id: WELCOME_SCREEN_ID, + icon: ( + + ), + text: 'Welcome to QueryMaster', + } as TreeViewItemData, + ...buildTree(connections), + ], + treeDict, + }; } return { treeItems: [], treeDict }; }, [connections]); + const setSaveCollapsedKeys = useCallback( + (keys: string[] | undefined) => { + setCollapsedKeys(keys?.filter((key) => !!treeDict[key])); + saveCollapsed(keys || []); + }, + [setCollapsedKeys, saveCollapsed, treeDict] + ); + // ----------------------------------------------- // Handle save database // ----------------------------------------------- @@ -246,7 +275,7 @@ export default function HomeScreen() { onRenamedSelectedItem={handleRenameExit} onDragItem={handleDragAndOverItem} items={treeItems} - onCollapsedChange={setCollapsedKeys} + onCollapsedChange={setSaveCollapsedKeys} collapsedKeys={collapsedKeys} onSelectChange={setSelectedItem} onDoubleClick={(item) => { diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index c4fce4f..e836d9f 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -8,6 +8,37 @@ import { ConnectionStoreConfig, } from 'drivers/SQLLikeConnection'; import { TreeViewItemData } from 'renderer/components/TreeView'; +import { sortConnection } from '.'; + +function insertNodeToConnection( + connections: ConnectionConfigTree[] | undefined, + selectedItem: TreeViewItemData | undefined, + treeDict: Record, + newNode: ConnectionConfigTree +) { + if (connections) { + let insideFolder: ConnectionConfigTree | undefined; + if (selectedItem && selectedItem.data) { + if (selectedItem.data.nodeType === 'folder') { + insideFolder = selectedItem.data; + } else if (selectedItem.data?.parentId) { + insideFolder = treeDict[selectedItem.data.parentId]; + } + } + + if (insideFolder && insideFolder.children) { + newNode.parentId = insideFolder.id; + insideFolder.children = sortConnection([ + ...insideFolder.children, + newNode, + ]); + return [...connections]; + } else { + return sortConnection([...connections, newNode]); + } + } + return []; +} export default function useConnectionContextMenu({ treeDict, @@ -43,7 +74,7 @@ export default function useConnectionContextMenu({ const parent = treeDict[selectedItem.data.parentId]; if (parent && parent.children) { parent.children = parent.children.filter( - (node) => node.id === selectedItem.id + (node) => node.id !== selectedItem.id ); } } @@ -61,7 +92,7 @@ export default function useConnectionContextMenu({ const newConfig = { id: newConnectionId, name: generateIncrementalName( - (connections || []).map((c) => c.name), + Object.values(treeDict).map((node) => node.name), 'Unnamed' ), type: 'mysql', @@ -88,13 +119,15 @@ export default function useConnectionContextMenu({ icon: , }); - setConnections([...(connections || []), newTreeNode]); - }, [setConnections, setSelectedItem, connections]); + setConnections( + insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) + ); + }, [setConnections, setSelectedItem, selectedItem, connections, treeDict]); const newFolderClicked = useCallback(() => { const newFolderId = uuidv1(); const newFolderName = generateIncrementalName( - (connections || []).map((c) => c.name), + Object.values(treeDict).map((node) => node.name), 'Unnamed Folders' ); @@ -105,8 +138,10 @@ export default function useConnectionContextMenu({ children: [], }; - setConnections([...(connections || []), newTreeNode]); - }, [setConnections, connections]); + setConnections( + insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) + ); + }, [setConnections, connections, selectedItem, treeDict]); const { handleContextMenu } = useContextMenu(() => { return [ From 381098a61f0f303b75aac1e1fc7ed4cf61e61000 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 10:51:05 +0700 Subject: [PATCH 06/10] fixing some code smell --- src/renderer/screens/HomeScreen/index.tsx | 5 ++--- src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 64a705a..d02c34c 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -132,7 +132,6 @@ export default function HomeScreen() { selectedItem.text = selectedItemChanged.name; selectedItem.data.name = selectedItemChanged.name; selectedItem.data.config = selectedItemChanged; - setSelectedItem(selectedItem); setConnections([...connections]); } }, [connections, selectedItem, selectedItemChanged, setConnections]); @@ -165,7 +164,7 @@ export default function HomeScreen() { // Remove itself from its parent; if (fromData.parentId) { const parent = treeDict[fromData.parentId]; - if (parent && parent.children) { + if (parent?.children) { parent.children = parent.children.filter( (child) => child.id !== fromData.id ); @@ -209,7 +208,7 @@ export default function HomeScreen() { ); const parent = treeDict[selectedItem.id]; - if (parent && parent.children) { + if (parent?.children) { parent.children = sortConnection(parent.children); } diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index e836d9f..29a06bd 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -72,7 +72,7 @@ export default function useConnectionContextMenu({ if (selectedItem.data?.parentId) { const parent = treeDict[selectedItem.data.parentId]; - if (parent && parent.children) { + if (parent?.children) { parent.children = parent.children.filter( (node) => node.id !== selectedItem.id ); From dd5426012f7938d41e03f0da900a04d508adb28e Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 10:58:50 +0700 Subject: [PATCH 07/10] feat: fix code smell --- src/renderer/hooks/useIndexDbConnections.ts | 2 +- src/renderer/screens/HomeScreen/index.tsx | 29 +++++++++---------- .../HomeScreen/useConnectionContextMenu.tsx | 4 +-- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/renderer/hooks/useIndexDbConnections.ts b/src/renderer/hooks/useIndexDbConnections.ts index 0092f50..2c110bc 100644 --- a/src/renderer/hooks/useIndexDbConnections.ts +++ b/src/renderer/hooks/useIndexDbConnections.ts @@ -8,7 +8,7 @@ export function useIndexDbConnection() { const initialCollapsed = useMemo(() => { try { - return JSON.parse(localStorage.getItem('db_collapsed_keys') || '[]'); + return JSON.parse(localStorage.getItem('db_collapsed_keys') ?? '[]'); } catch { return []; } diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index d02c34c..05bd916 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -94,21 +94,18 @@ export default function HomeScreen() { }); } + const welcomeNode = { + id: WELCOME_SCREEN_ID, + icon: ( + + ), + text: 'Welcome to QueryMaster', + } as TreeViewItemData; + if (connections) { + const treeNode = buildTree(connections); return { - treeItems: [ - { - id: WELCOME_SCREEN_ID, - icon: ( - - ), - text: 'Welcome to QueryMaster', - } as TreeViewItemData, - ...buildTree(connections), - ], + treeItems: treeNode.length > 0 ? [welcomeNode, ...treeNode] : [], treeDict, }; } @@ -119,7 +116,7 @@ export default function HomeScreen() { const setSaveCollapsedKeys = useCallback( (keys: string[] | undefined) => { setCollapsedKeys(keys?.filter((key) => !!treeDict[key])); - saveCollapsed(keys || []); + saveCollapsed(keys ?? []); }, [setCollapsedKeys, saveCollapsed, treeDict] ); @@ -332,8 +329,10 @@ export default function HomeScreen() { onChange={setSelectedItemChanged} />
- ) : ( + ) : !selectedItem?.data?.nodeType ? ( + ) : ( +
)}
diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index 29a06bd..1deeece 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -18,7 +18,7 @@ function insertNodeToConnection( ) { if (connections) { let insideFolder: ConnectionConfigTree | undefined; - if (selectedItem && selectedItem.data) { + if (selectedItem?.data) { if (selectedItem.data.nodeType === 'folder') { insideFolder = selectedItem.data; } else if (selectedItem.data?.parentId) { @@ -26,7 +26,7 @@ function insertNodeToConnection( } } - if (insideFolder && insideFolder.children) { + if (insideFolder?.children) { newNode.parentId = insideFolder.id; insideFolder.children = sortConnection([ ...insideFolder.children, From 9180569c14400f557efa4e92b88cf1df48d20c48 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 11:07:49 +0700 Subject: [PATCH 08/10] feat: when new folder or new connection, collapsed the folder --- src/renderer/components/TreeView/index.tsx | 26 +++++++----- src/renderer/screens/HomeScreen/index.tsx | 7 +++- .../HomeScreen/useConnectionContextMenu.tsx | 42 +++++++++++++++++-- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/TreeView/index.tsx b/src/renderer/components/TreeView/index.tsx index 58d281b..fac7c46 100644 --- a/src/renderer/components/TreeView/index.tsx +++ b/src/renderer/components/TreeView/index.tsx @@ -44,6 +44,7 @@ interface TreeViewProps extends TreeViewCommonProps { items: TreeViewItemData[]; onBeforeSelectChange?: () => Promise; onContextMenu?: React.MouseEventHandler; + emptyState?: ReactElement; } interface TreeViewItemProps extends TreeViewCommonProps { @@ -148,6 +149,7 @@ export default function TreeView(props: TreeViewProps) { onSelectChange, onBeforeSelectChange, onContextMenu, + emptyState, ...common } = props; @@ -166,17 +168,19 @@ export default function TreeView(props: TreeViewProps) { return (
- {items.map((item) => { - return ( - - ); - })} + {items.length > 0 + ? items.map((item) => { + return ( + + ); + }) + : emptyState}
); } diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 05bd916..2874eef 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -20,6 +20,7 @@ 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 ListViewEmptyState from 'renderer/components/ListView/ListViewEmptyState'; const WELCOME_SCREEN_ID = '00000000000000000000'; @@ -249,6 +250,8 @@ export default function HomeScreen() { const { handleContextMenu } = useConnectionContextMenu({ connections, + setSaveCollapsedKeys, + collapsedKeys, setSelectedItem, setConnections, setRenameSelectedItem, @@ -282,8 +285,10 @@ export default function HomeScreen() { selected={selectedItem} onBeforeSelectChange={onBeforeSelectChange} onContextMenu={handleContextMenu} + emptyState={ + + } /> - {/* */}
diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index 1deeece..930b2be 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -47,6 +47,8 @@ export default function useConnectionContextMenu({ setConnections, setSelectedItem, setRenameSelectedItem, + setSaveCollapsedKeys, + collapsedKeys, }: { treeDict: Record; connections: ConnectionConfigTree[] | undefined; @@ -56,6 +58,8 @@ export default function useConnectionContextMenu({ v: TreeViewItemData | undefined ) => void; setRenameSelectedItem: (v: boolean) => void; + collapsedKeys: string[] | undefined; + setSaveCollapsedKeys: (v: string[] | undefined) => void; }) { // ---------------------------------------------- // Handle remove database @@ -122,7 +126,19 @@ export default function useConnectionContextMenu({ setConnections( insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) ); - }, [setConnections, setSelectedItem, selectedItem, connections, treeDict]); + + if (selectedItem) { + setSaveCollapsedKeys([...(collapsedKeys ?? []), selectedItem.id]); + } + }, [ + setConnections, + setSelectedItem, + selectedItem, + connections, + treeDict, + collapsedKeys, + setSaveCollapsedKeys, + ]); const newFolderClicked = useCallback(() => { const newFolderId = uuidv1(); @@ -138,16 +154,34 @@ export default function useConnectionContextMenu({ children: [], }; + setSelectedItem({ + id: newTreeNode.id, + text: newTreeNode.name, + data: newTreeNode, + }); + setConnections( insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) ); - }, [setConnections, connections, selectedItem, treeDict]); + + if (selectedItem) { + setSaveCollapsedKeys([...(collapsedKeys ?? []), selectedItem.id]); + } + }, [ + setConnections, + setSelectedItem, + connections, + selectedItem, + treeDict, + collapsedKeys, + setSaveCollapsedKeys, + ]); const { handleContextMenu } = useContextMenu(() => { return [ { text: 'Rename', - disabled: !selectedItem, + disabled: !selectedItem?.data, onClick: () => setRenameSelectedItem(true), separator: true, }, @@ -164,7 +198,7 @@ export default function useConnectionContextMenu({ { text: 'Remove', onClick: onRemoveClick, - disabled: !selectedItem, + disabled: !selectedItem?.data, destructive: true, }, ]; From 075a520d1c0b5100f96dc3c6b829fe73eb68dedb Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 11:10:25 +0700 Subject: [PATCH 09/10] feat: make the collapsed prevent duplicate key --- src/renderer/screens/HomeScreen/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 2874eef..4b4542f 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -116,8 +116,11 @@ export default function HomeScreen() { const setSaveCollapsedKeys = useCallback( (keys: string[] | undefined) => { - setCollapsedKeys(keys?.filter((key) => !!treeDict[key])); - saveCollapsed(keys ?? []); + const legitKeys = Array.from( + new Set(keys?.filter((key) => !!treeDict[key])) + ); + + setCollapsedKeys(legitKeys); }, [setCollapsedKeys, saveCollapsed, treeDict] ); From 13426935a1981bfabb24190694ea0ead57bab4d8 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Fri, 18 Aug 2023 21:31:15 +0700 Subject: [PATCH 10/10] fix bug which you can drag parent node to child node --- src/libs/ConnectionSettingTree.tsx | 144 ++++++++++++++++++ src/renderer/hooks/useIndexDbConnections.ts | 13 +- src/renderer/screens/HomeScreen/index.tsx | 117 +++----------- .../HomeScreen/useConnectionContextMenu.tsx | 64 +++----- 4 files changed, 199 insertions(+), 139 deletions(-) create mode 100644 src/libs/ConnectionSettingTree.tsx diff --git a/src/libs/ConnectionSettingTree.tsx b/src/libs/ConnectionSettingTree.tsx new file mode 100644 index 0000000..d3f9c64 --- /dev/null +++ b/src/libs/ConnectionSettingTree.tsx @@ -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 = {}; + + 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[] { + if (!root) return []; + + return root.map((config) => { + return { + id: config.id, + data: config, + icon: + config.nodeType === 'folder' ? ( + + ) : ( + + ), + 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]); + } +} diff --git a/src/renderer/hooks/useIndexDbConnections.ts b/src/renderer/hooks/useIndexDbConnections.ts index 2c110bc..fbad23b 100644 --- a/src/renderer/hooks/useIndexDbConnections.ts +++ b/src/renderer/hooks/useIndexDbConnections.ts @@ -1,11 +1,16 @@ 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(); + const connectionTree = useMemo(() => { + return new ConnectionSettingTree(connections ?? []); + }, [connections]); + const initialCollapsed = useMemo(() => { try { return JSON.parse(localStorage.getItem('db_collapsed_keys') ?? '[]'); @@ -36,5 +41,11 @@ export function useIndexDbConnection() { [setInternalConnections] ); - return { connections, setConnections, initialCollapsed, saveCollapsed }; + return { + connections, + setConnections, + connectionTree, + initialCollapsed, + saveCollapsed, + }; } diff --git a/src/renderer/screens/HomeScreen/index.tsx b/src/renderer/screens/HomeScreen/index.tsx index 4b4542f..12244f8 100644 --- a/src/renderer/screens/HomeScreen/index.tsx +++ b/src/renderer/screens/HomeScreen/index.tsx @@ -1,6 +1,4 @@ import { useCallback, useEffect, useState, useMemo } from 'react'; -import Icon from 'renderer/components/Icon'; - import { ConnectionConfigTree, ConnectionStoreItem, @@ -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'; @@ -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 | undefined >({ id: WELCOME_SCREEN_ID }); @@ -68,32 +71,8 @@ export default function HomeScreen() { const [renameSelectedItem, setRenameSelectedItem] = useState(false); - const { treeItems, treeDict } = useMemo(() => { - const treeDict: Record = {}; - - function buildTree( - configs: ConnectionConfigTree[] - ): TreeViewItemData[] { - return configs.map((config) => { - treeDict[config.id] = config; - - return { - id: config.id, - data: config, - icon: - config.nodeType === 'folder' ? ( - - ) : ( - - ), - 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, @@ -103,26 +82,18 @@ export default function HomeScreen() { text: 'Welcome to QueryMaster', } as TreeViewItemData; - 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] ); // ----------------------------------------------- @@ -142,55 +113,15 @@ export default function HomeScreen() { from: TreeViewItemData, to: TreeViewItemData ) => { - 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( @@ -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); } @@ -218,7 +149,7 @@ export default function HomeScreen() { setRenameSelectedItem(false); }, [ - treeDict, + connectionTree, connections, setConnections, selectedItem, @@ -259,7 +190,7 @@ export default function HomeScreen() { setConnections, setRenameSelectedItem, selectedItem, - treeDict, + connectionTree, }); return ( diff --git a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx index 930b2be..542cf43 100644 --- a/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx +++ b/src/renderer/screens/HomeScreen/useConnectionContextMenu.tsx @@ -8,40 +8,10 @@ import { ConnectionStoreConfig, } from 'drivers/SQLLikeConnection'; import { TreeViewItemData } from 'renderer/components/TreeView'; -import { sortConnection } from '.'; - -function insertNodeToConnection( - connections: ConnectionConfigTree[] | undefined, - selectedItem: TreeViewItemData | undefined, - treeDict: Record, - newNode: ConnectionConfigTree -) { - if (connections) { - let insideFolder: ConnectionConfigTree | undefined; - if (selectedItem?.data) { - if (selectedItem.data.nodeType === 'folder') { - insideFolder = selectedItem.data; - } else if (selectedItem.data?.parentId) { - insideFolder = treeDict[selectedItem.data.parentId]; - } - } - - if (insideFolder?.children) { - newNode.parentId = insideFolder.id; - insideFolder.children = sortConnection([ - ...insideFolder.children, - newNode, - ]); - return [...connections]; - } else { - return sortConnection([...connections, newNode]); - } - } - return []; -} +import ConnectionSettingTree from 'libs/ConnectionSettingTree'; export default function useConnectionContextMenu({ - treeDict, + connectionTree, connections, selectedItem, setConnections, @@ -50,7 +20,7 @@ export default function useConnectionContextMenu({ setSaveCollapsedKeys, collapsedKeys, }: { - treeDict: Record; + connectionTree: ConnectionSettingTree; connections: ConnectionConfigTree[] | undefined; selectedItem?: TreeViewItemData; setConnections: (v: ConnectionConfigTree[]) => void; @@ -75,7 +45,7 @@ export default function useConnectionContextMenu({ if (buttonIndex !== 0) return; if (selectedItem.data?.parentId) { - const parent = treeDict[selectedItem.data.parentId]; + const parent = connectionTree.getById(selectedItem.data.parentId); if (parent?.children) { parent.children = parent.children.filter( (node) => node.id !== selectedItem.id @@ -86,7 +56,13 @@ export default function useConnectionContextMenu({ setConnections(connections.filter((db) => db.id !== selectedItem.id)); setSelectedItem(undefined); } - }, [selectedItem, setSelectedItem, connections, setConnections, treeDict]); + }, [ + selectedItem, + setSelectedItem, + connections, + setConnections, + connectionTree, + ]); // ---------------------------------------------- // Handle new connection @@ -96,7 +72,7 @@ export default function useConnectionContextMenu({ const newConfig = { id: newConnectionId, name: generateIncrementalName( - Object.values(treeDict).map((node) => node.name), + connectionTree.getAllNodes().map((node) => node.name), 'Unnamed' ), type: 'mysql', @@ -123,9 +99,8 @@ export default function useConnectionContextMenu({ icon: , }); - setConnections( - insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) - ); + connectionTree.insertNode(newTreeNode, selectedItem?.id); + setConnections(connectionTree.getNewTree()); if (selectedItem) { setSaveCollapsedKeys([...(collapsedKeys ?? []), selectedItem.id]); @@ -135,7 +110,7 @@ export default function useConnectionContextMenu({ setSelectedItem, selectedItem, connections, - treeDict, + connectionTree, collapsedKeys, setSaveCollapsedKeys, ]); @@ -143,7 +118,7 @@ export default function useConnectionContextMenu({ const newFolderClicked = useCallback(() => { const newFolderId = uuidv1(); const newFolderName = generateIncrementalName( - Object.values(treeDict).map((node) => node.name), + connectionTree.getAllNodes().map((node) => node.name), 'Unnamed Folders' ); @@ -160,9 +135,8 @@ export default function useConnectionContextMenu({ data: newTreeNode, }); - setConnections( - insertNodeToConnection(connections, selectedItem, treeDict, newTreeNode) - ); + connectionTree.insertNode(newTreeNode, selectedItem?.id); + setConnections(connectionTree.getNewTree()); if (selectedItem) { setSaveCollapsedKeys([...(collapsedKeys ?? []), selectedItem.id]); @@ -172,7 +146,7 @@ export default function useConnectionContextMenu({ setSelectedItem, connections, selectedItem, - treeDict, + connectionTree, collapsedKeys, setSaveCollapsedKeys, ]);