Skip to content

Commit 772ec1a

Browse files
authored
feat: add connection folders (#112)
* 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
1 parent aa46097 commit 772ec1a

File tree

10 files changed

+670
-203
lines changed

10 files changed

+670
-203
lines changed

src/drivers/SQLLikeConnection.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export interface SqliteConnectionConfig {
2424
path: string;
2525
}
2626

27+
export interface ConnectionConfigTree {
28+
id: string;
29+
name: string;
30+
nodeType: 'folder' | 'connection';
31+
config?: ConnectionStoreItem;
32+
parentId?: string;
33+
children?: ConnectionConfigTree[];
34+
}
35+
2736
export interface ConnectionStoreItem {
2837
id: string;
2938
name: string;

src/renderer/components/ListViewItem/index.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1-
import { ReactElement, useMemo } from 'react';
1+
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
22
import styles from './styles.module.scss';
33
import Icon from '../Icon';
44
import { useAppFeature } from 'renderer/contexts/AppFeatureProvider';
55

66
interface ListViewItemProps {
77
text: string;
8+
draggable?: boolean;
89
highlight?: string;
910
icon?: ReactElement;
1011
changed?: boolean;
1112
selected?: boolean;
1213
onClick?: () => void;
1314
onDoubleClick?: () => void;
15+
onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void;
16+
onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
17+
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
1418
onContextMenu?: React.MouseEventHandler<HTMLDivElement>;
1519

20+
renaming?: boolean;
21+
onRenamed?: (newName: string | null) => void;
22+
1623
// This is used for rendering TreeView
1724
depth?: number;
1825
hasCollapsed?: boolean;
@@ -36,6 +43,12 @@ export default function ListViewItem({
3643
onClick,
3744
onDoubleClick,
3845
onContextMenu,
46+
draggable,
47+
onDragStart,
48+
onDragOver,
49+
onDrop,
50+
renaming,
51+
onRenamed,
3952

4053
// For TreeView
4154
depth,
@@ -44,6 +57,18 @@ export default function ListViewItem({
4457
onCollapsedClick,
4558
}: ListViewItemProps) {
4659
const { theme } = useAppFeature();
60+
const [renameDraftValue, setRenameDraftValue] = useState('');
61+
62+
useEffect(() => {
63+
setRenameDraftValue(text);
64+
}, [renaming, text, setRenameDraftValue]);
65+
66+
const onRenameDone = useCallback(
67+
(value: string | null) => {
68+
if (onRenamed) onRenamed(value);
69+
},
70+
[onRenamed]
71+
);
4772

4873
const finalText = useMemo(() => {
4974
if (highlight) {
@@ -75,6 +100,10 @@ export default function ListViewItem({
75100
onClick={onClick}
76101
onDoubleClick={onDoubleClick}
77102
onContextMenu={onContextMenu}
103+
draggable={draggable}
104+
onDragOver={onDragOver}
105+
onDragStart={onDragStart}
106+
onDrop={onDrop}
78107
>
79108
{!!depth &&
80109
new Array(depth)
@@ -103,10 +132,31 @@ export default function ListViewItem({
103132
</div>
104133
))}
105134
{!hasCollapsed && <div className={styles.icon}>{icon}</div>}
106-
<div
107-
className={styles.text}
108-
dangerouslySetInnerHTML={{ __html: finalText }}
109-
/>
135+
{renaming ? (
136+
<div className={styles.text}>
137+
<input
138+
autoFocus
139+
onBlur={() => {
140+
onRenameDone(renameDraftValue);
141+
}}
142+
onKeyDown={(e) => {
143+
if (e.key === 'Enter') {
144+
onRenameDone(renameDraftValue);
145+
} else if (e.key === 'Escape') {
146+
onRenameDone(null);
147+
}
148+
}}
149+
type="text"
150+
value={renameDraftValue}
151+
onChange={(e) => setRenameDraftValue(e.currentTarget.value)}
152+
/>
153+
</div>
154+
) : (
155+
<div
156+
className={styles.text}
157+
dangerouslySetInnerHTML={{ __html: finalText }}
158+
/>
159+
)}
110160
</div>
111161
);
112162
}

src/renderer/components/ListViewItem/styles.module.scss

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
text-overflow: ellipsis;
2727
flex-grow: 1;
2828
overflow: hidden;
29+
30+
input {
31+
border: 0;
32+
background: inherit;
33+
color: inherit;
34+
outline: none;
35+
width: 100%;
36+
}
2937
}
3038

3139
.icon {
@@ -34,7 +42,7 @@
3442
display: flex;
3543
align-items: center;
3644
justify-content: center;
37-
45+
3846
img {
3947
width: 20px;
4048
height: 20px;
@@ -63,4 +71,4 @@
6371
border-top: 1px solid var(--color-list-line-guide);
6472
width: 10px;
6573
}
66-
}
74+
}

src/renderer/components/TreeView/index.tsx

Lines changed: 99 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import styles from './styles.module.scss';
22
import ListViewItem from '../ListViewItem';
33
import { ReactElement, useCallback } from 'react';
44

5+
let GLOBAL_TREE_DRAG_ITEM: unknown;
6+
57
export interface TreeViewItemData<T> {
68
id: string;
79
text?: string;
@@ -10,44 +12,62 @@ export interface TreeViewItemData<T> {
1012
children?: TreeViewItemData<T>[];
1113
}
1214

13-
interface TreeViewProps<T> {
14-
items: TreeViewItemData<T>[];
15-
selected?: TreeViewItemData<T>;
16-
collapsedKeys?: string[];
15+
interface TreeViewCommonProps<T> {
16+
draggable?: boolean;
17+
onDragItem?: (from: TreeViewItemData<T>, to: TreeViewItemData<T>) => void;
1718
onCollapsedChange?: (value?: string[]) => void;
1819
onSelectChange?: (value?: TreeViewItemData<T>) => void;
1920
onDoubleClick?: (value: TreeViewItemData<T>) => void;
20-
onContextMenu?: React.MouseEventHandler;
21+
selected?: TreeViewItemData<T>;
22+
collapsedKeys?: string[];
2123
highlight?: string;
2224
highlightDepth?: number;
23-
}
2425

25-
function TreeViewItem<T>({
26-
item,
27-
depth,
26+
/**
27+
* When renameSelectedItem is true, it will render, the current
28+
* selected item as editable field.
29+
*/
30+
renameSelectedItem?: boolean;
2831

29-
selected,
30-
onSelectChange,
32+
/**
33+
* When Enter or Lost Focus, it will treat as successful rename
34+
* If user press escape, it will cancel the rename
35+
*
36+
* @param newName The new name that we just rename into.
37+
* If it is null, it means we cancel the rename
38+
* @returns
39+
*/
40+
onRenamedSelectedItem?: (newName: string | null) => void;
41+
}
3142

32-
collapsedKeys,
33-
onCollapsedChange,
34-
onDoubleClick,
43+
interface TreeViewProps<T> extends TreeViewCommonProps<T> {
44+
items: TreeViewItemData<T>[];
45+
onBeforeSelectChange?: () => Promise<boolean>;
46+
onContextMenu?: React.MouseEventHandler;
47+
emptyState?: ReactElement;
48+
}
3549

36-
highlight,
37-
highlightDepth,
38-
}: {
50+
interface TreeViewItemProps<T> extends TreeViewCommonProps<T> {
3951
item: TreeViewItemData<T>;
4052
depth: number;
53+
}
4154

42-
selected?: TreeViewItemData<T>;
43-
onSelectChange?: (value?: TreeViewItemData<T>) => void;
55+
function TreeViewItem<T>(props: TreeViewItemProps<T>) {
56+
const { depth, item, ...common } = props;
57+
const {
58+
collapsedKeys,
59+
draggable,
60+
onDragItem,
61+
onDoubleClick,
62+
highlight,
63+
selected,
64+
onSelectChange,
65+
onCollapsedChange,
66+
highlightDepth,
67+
renameSelectedItem,
68+
onRenamedSelectedItem,
69+
} = props;
4470

45-
onCollapsedChange?: (value?: string[]) => void;
46-
collapsedKeys?: string[];
47-
onDoubleClick?: (value: TreeViewItemData<T>) => void;
48-
highlight?: string;
49-
highlightDepth?: number;
50-
}) {
5171
const hasCollapsed = item.children && item.children.length > 0;
5272
const isCollapsed = collapsedKeys?.includes(item.id);
5373

@@ -57,9 +77,23 @@ function TreeViewItem<T>({
5777
}
5878
}, [onSelectChange, item]);
5979

80+
const isSelected = selected?.id === item.id;
81+
6082
return (
6183
<div>
6284
<ListViewItem
85+
draggable={draggable}
86+
onDragStart={() => {
87+
GLOBAL_TREE_DRAG_ITEM = item;
88+
}}
89+
onDragOver={(e) => e.preventDefault()}
90+
onDrop={() => {
91+
if (onDragItem) {
92+
if (GLOBAL_TREE_DRAG_ITEM) {
93+
onDragItem(GLOBAL_TREE_DRAG_ITEM as TreeViewItemData<T>, item);
94+
}
95+
}
96+
}}
6397
key={item.id}
6498
text={item.text || ''}
6599
icon={item.icon}
@@ -72,7 +106,9 @@ function TreeViewItem<T>({
72106
highlight={depth === highlightDepth ? highlight : undefined}
73107
hasCollapsed={hasCollapsed}
74108
collapsed={isCollapsed}
75-
selected={selected?.id === item.id}
109+
selected={isSelected}
110+
renaming={isSelected && renameSelectedItem}
111+
onRenamed={onRenamedSelectedItem}
76112
onClick={onSelectChangeCallback}
77113
onContextMenu={onSelectChangeCallback}
78114
onCollapsedClick={() => {
@@ -94,16 +130,10 @@ function TreeViewItem<T>({
94130
{item.children?.map((item) => {
95131
return (
96132
<TreeViewItem
133+
{...common}
97134
key={item.id}
98135
item={item}
99136
depth={depth + 1}
100-
highlight={highlight}
101-
highlightDepth={highlightDepth}
102-
selected={selected}
103-
onSelectChange={onSelectChange}
104-
collapsedKeys={collapsedKeys}
105-
onCollapsedChange={onCollapsedChange}
106-
onDoubleClick={onDoubleClick}
107137
/>
108138
);
109139
})}
@@ -113,35 +143,44 @@ function TreeViewItem<T>({
113143
);
114144
}
115145

116-
export default function TreeView<T>({
117-
items,
118-
selected,
119-
onSelectChange,
120-
onCollapsedChange,
121-
collapsedKeys,
122-
onDoubleClick,
123-
onContextMenu,
124-
highlight,
125-
highlightDepth,
126-
}: TreeViewProps<T>) {
146+
export default function TreeView<T>(props: TreeViewProps<T>) {
147+
const {
148+
items,
149+
onSelectChange,
150+
onBeforeSelectChange,
151+
onContextMenu,
152+
emptyState,
153+
...common
154+
} = props;
155+
156+
const onSelectChangeWithHook = useCallback(
157+
(item: TreeViewItemData<T> | undefined) => {
158+
if (onSelectChange) {
159+
if (onBeforeSelectChange) {
160+
onBeforeSelectChange().then(() => onSelectChange(item));
161+
} else {
162+
onSelectChange(item);
163+
}
164+
}
165+
},
166+
[onSelectChange, onBeforeSelectChange]
167+
);
168+
127169
return (
128170
<div className={`${styles.treeView} scroll`} onContextMenu={onContextMenu}>
129-
{items.map((item) => {
130-
return (
131-
<TreeViewItem
132-
key={item.id}
133-
item={item}
134-
depth={0}
135-
highlight={highlight}
136-
highlightDepth={highlightDepth}
137-
selected={selected}
138-
onSelectChange={onSelectChange}
139-
onDoubleClick={onDoubleClick}
140-
onCollapsedChange={onCollapsedChange}
141-
collapsedKeys={collapsedKeys}
142-
/>
143-
);
144-
})}
171+
{items.length > 0
172+
? items.map((item) => {
173+
return (
174+
<TreeViewItem
175+
{...common}
176+
onSelectChange={onSelectChangeWithHook}
177+
key={item.id}
178+
item={item}
179+
depth={0}
180+
/>
181+
);
182+
})
183+
: emptyState}
145184
</div>
146185
);
147186
}

0 commit comments

Comments
 (0)