idea-manager 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/next.config.ts +0 -1
- package/package.json +2 -2
- package/{src/app/icon.svg → public/favicon.svg} +2 -2
- package/src/app/api/filesystem/route.ts +49 -0
- package/src/app/api/projects/[id]/cleanup/route.ts +32 -0
- package/src/app/api/projects/[id]/items/[itemId]/refine/route.ts +36 -0
- package/src/app/api/projects/[id]/items/[itemId]/route.ts +23 -1
- package/src/app/api/projects/[id]/items/route.ts +51 -1
- package/src/app/api/projects/[id]/scan/route.ts +73 -0
- package/src/app/api/projects/[id]/scan/stream/route.ts +112 -0
- package/src/app/api/projects/[id]/structure/route.ts +34 -3
- package/src/app/api/projects/[id]/structure/stream/route.ts +157 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.ts +60 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.ts +26 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route.ts +39 -0
- package/src/app/api/projects/[id]/sub-projects/[subId]/tasks/route.ts +33 -0
- package/src/app/api/projects/[id]/sub-projects/route.ts +31 -0
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/globals.css +465 -5
- package/src/app/layout.tsx +3 -0
- package/src/app/page.tsx +260 -88
- package/src/app/projects/[id]/page.tsx +366 -183
- package/src/cli.ts +10 -10
- package/src/components/DirectoryPicker.tsx +137 -0
- package/src/components/ScanPanel.tsx +743 -0
- package/src/components/brainstorm/Editor.tsx +20 -4
- package/src/components/brainstorm/MemoPin.tsx +91 -5
- package/src/components/dashboard/SubProjectCard.tsx +76 -0
- package/src/components/dashboard/TabBar.tsx +42 -0
- package/src/components/task/ProjectTree.tsx +223 -0
- package/src/components/task/PromptEditor.tsx +107 -0
- package/src/components/task/StatusFlow.tsx +43 -0
- package/src/components/task/TaskChat.tsx +134 -0
- package/src/components/task/TaskDetail.tsx +205 -0
- package/src/components/task/TaskList.tsx +119 -0
- package/src/components/tree/CardView.tsx +206 -0
- package/src/components/tree/RefinePopover.tsx +157 -0
- package/src/components/tree/TreeNode.tsx +147 -38
- package/src/components/tree/TreeView.tsx +270 -26
- package/src/components/ui/ConfirmDialog.tsx +88 -0
- package/src/lib/ai/chat-responder.ts +4 -2
- package/src/lib/ai/cleanup.ts +87 -0
- package/src/lib/ai/client.ts +175 -58
- package/src/lib/ai/prompter.ts +19 -24
- package/src/lib/ai/refiner.ts +128 -0
- package/src/lib/ai/structurer.ts +340 -11
- package/src/lib/db/queries/context.ts +76 -0
- package/src/lib/db/queries/items.ts +133 -12
- package/src/lib/db/queries/projects.ts +12 -8
- package/src/lib/db/queries/sub-projects.ts +122 -0
- package/src/lib/db/queries/task-conversations.ts +27 -0
- package/src/lib/db/queries/task-prompts.ts +32 -0
- package/src/lib/db/queries/tasks.ts +133 -0
- package/src/lib/db/schema.ts +75 -0
- package/src/lib/mcp/server.ts +38 -39
- package/src/lib/mcp/tools.ts +47 -45
- package/src/lib/scanner.ts +573 -0
- package/src/lib/task-store.ts +97 -0
- package/src/types/index.ts +65 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
interface IItemTree {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
item_type: string;
|
|
10
|
+
priority: string;
|
|
11
|
+
status: string;
|
|
12
|
+
is_locked: boolean;
|
|
13
|
+
is_pinned: boolean;
|
|
14
|
+
children: IItemTree[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RefinePopoverProps {
|
|
18
|
+
itemId: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
|
|
24
|
+
onTreeRefresh: (tree: IItemTree[]) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function RefinePopover({
|
|
28
|
+
itemId,
|
|
29
|
+
projectId,
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
onClose,
|
|
33
|
+
onItemUpdate,
|
|
34
|
+
onTreeRefresh,
|
|
35
|
+
}: RefinePopoverProps) {
|
|
36
|
+
const [input, setInput] = useState('');
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const [messages, setMessages] = useState<{ role: 'user' | 'assistant'; text: string }[]>([]);
|
|
39
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
40
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
inputRef.current?.focus();
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
48
|
+
}, [messages]);
|
|
49
|
+
|
|
50
|
+
const handleSubmit = async () => {
|
|
51
|
+
const trimmed = input.trim();
|
|
52
|
+
if (!trimmed || loading) return;
|
|
53
|
+
|
|
54
|
+
setMessages(prev => [...prev, { role: 'user', text: trimmed }]);
|
|
55
|
+
setInput('');
|
|
56
|
+
setLoading(true);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`/api/projects/${projectId}/items/${itemId}/refine`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ message: trimmed }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (res.ok) {
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
setMessages(prev => [
|
|
68
|
+
...prev,
|
|
69
|
+
{ role: 'assistant', text: `"${data.title}"\n${data.description}` },
|
|
70
|
+
]);
|
|
71
|
+
// Refresh the entire tree if structural changes were made
|
|
72
|
+
if (data.tree) {
|
|
73
|
+
onTreeRefresh(data.tree);
|
|
74
|
+
} else {
|
|
75
|
+
onItemUpdate(itemId, { title: data.title, description: data.description });
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
setMessages(prev => [
|
|
80
|
+
...prev,
|
|
81
|
+
{ role: 'assistant', text: `오류: ${data.error || '다듬기에 실패했습니다'}` },
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
setMessages(prev => [
|
|
86
|
+
...prev,
|
|
87
|
+
{ role: 'assistant', text: '오류: AI 연결에 실패했습니다' },
|
|
88
|
+
]);
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
95
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
handleSubmit();
|
|
98
|
+
}
|
|
99
|
+
if (e.key === 'Escape') {
|
|
100
|
+
onClose();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="refine-popover">
|
|
106
|
+
<div className="refine-header">
|
|
107
|
+
<span className="text-xs font-medium">다듬기: {title}</span>
|
|
108
|
+
<button onClick={onClose} className="refine-close">×</button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{description && (
|
|
112
|
+
<div className="refine-context">{description}</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<div className="refine-messages">
|
|
116
|
+
{messages.length === 0 && (
|
|
117
|
+
<div className="text-xs text-muted-foreground text-center py-2">
|
|
118
|
+
이 항목을 어떻게 다듬을지 알려주세요
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
{messages.map((m, i) => (
|
|
122
|
+
<div key={i} className={`memo-popover-bubble memo-popover-bubble-${m.role === 'user' ? 'user' : 'ai'}`}>
|
|
123
|
+
{m.text.split('\n').map((line, j) => (
|
|
124
|
+
<p key={j} className={j > 0 ? 'mt-1' : ''}>{line}</p>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
{loading && (
|
|
129
|
+
<div className="memo-popover-bubble memo-popover-bubble-ai chat-loading">
|
|
130
|
+
<span className="dot" /><span className="dot" /><span className="dot" />
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
<div ref={messagesEndRef} />
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="memo-popover-input-area">
|
|
137
|
+
<textarea
|
|
138
|
+
ref={inputRef}
|
|
139
|
+
value={input}
|
|
140
|
+
onChange={(e) => setInput(e.target.value)}
|
|
141
|
+
onKeyDown={handleKeyDown}
|
|
142
|
+
placeholder="예: 하위 항목을 상세하게 추가해줘, 범위를 줄여줘..."
|
|
143
|
+
rows={1}
|
|
144
|
+
disabled={loading}
|
|
145
|
+
className="memo-popover-input"
|
|
146
|
+
/>
|
|
147
|
+
<button
|
|
148
|
+
onClick={handleSubmit}
|
|
149
|
+
disabled={!input.trim() || loading}
|
|
150
|
+
className="memo-popover-send"
|
|
151
|
+
>
|
|
152
|
+
전송
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
|
|
4
4
|
import LockToggle from './LockToggle';
|
|
5
5
|
import StatusBadge from './StatusBadge';
|
|
6
6
|
import ItemDetail from './ItemDetail';
|
|
7
|
+
import RefinePopover from './RefinePopover';
|
|
7
8
|
|
|
8
9
|
interface IItemTree {
|
|
9
10
|
id: string;
|
|
@@ -14,6 +15,7 @@ interface IItemTree {
|
|
|
14
15
|
priority: string;
|
|
15
16
|
status: string;
|
|
16
17
|
is_locked: boolean;
|
|
18
|
+
is_pinned: boolean;
|
|
17
19
|
children: IItemTree[];
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -22,70 +24,171 @@ interface TreeNodeProps {
|
|
|
22
24
|
depth: number;
|
|
23
25
|
projectId: string;
|
|
24
26
|
onItemUpdate: (itemId: string, data: Record<string, unknown>) => void;
|
|
27
|
+
onItemDelete: (itemId: string) => void;
|
|
28
|
+
onTreeRefresh: (tree: IItemTree[]) => void;
|
|
29
|
+
selectMode?: boolean;
|
|
30
|
+
selected?: Set<string>;
|
|
31
|
+
onToggleSelect?: (id: string) => void;
|
|
32
|
+
defaultExpanded?: boolean;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
const
|
|
28
|
-
feature: '\u{1F4E6}',
|
|
29
|
-
task: '\u{2705}',
|
|
30
|
-
bug: '\u{1F41B}',
|
|
31
|
-
idea: '\u{1F4A1}',
|
|
32
|
-
note: '\u{1F4DD}',
|
|
35
|
+
const typeConfig: Record<string, { icon: string; color: string; label: string }> = {
|
|
36
|
+
feature: { icon: '\u{1F4E6}', color: 'var(--primary)', label: '기능' },
|
|
37
|
+
task: { icon: '\u{2705}', color: 'var(--success)', label: '작업' },
|
|
38
|
+
bug: { icon: '\u{1F41B}', color: 'var(--destructive)', label: '버그' },
|
|
39
|
+
idea: { icon: '\u{1F4A1}', color: 'var(--warning)', label: '아이디어' },
|
|
40
|
+
note: { icon: '\u{1F4DD}', color: 'var(--muted-foreground)', label: '메모' },
|
|
33
41
|
};
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
function countDescendantStatus(item: IItemTree): { total: number; done: number } {
|
|
44
|
+
let total = 0;
|
|
45
|
+
let done = 0;
|
|
46
|
+
for (const child of item.children) {
|
|
47
|
+
total++;
|
|
48
|
+
if (child.status === 'done') done++;
|
|
49
|
+
const sub = countDescendantStatus(child);
|
|
50
|
+
total += sub.total;
|
|
51
|
+
done += sub.done;
|
|
52
|
+
}
|
|
53
|
+
return { total, done };
|
|
54
|
+
}
|
|
40
55
|
|
|
41
|
-
export default function TreeNode({ item, depth, projectId, onItemUpdate }: TreeNodeProps) {
|
|
42
|
-
const [expanded, setExpanded] = useState(
|
|
56
|
+
export default function TreeNode({ item, depth, projectId, onItemUpdate, onItemDelete, onTreeRefresh, selectMode, selected, onToggleSelect, defaultExpanded }: TreeNodeProps) {
|
|
57
|
+
const [expanded, setExpanded] = useState(defaultExpanded ?? depth < 2);
|
|
43
58
|
const [showDetail, setShowDetail] = useState(false);
|
|
59
|
+
const [showRefine, setShowRefine] = useState(false);
|
|
44
60
|
const hasChildren = item.children.length > 0;
|
|
61
|
+
const baseCfg = typeConfig[item.item_type] || typeConfig.note;
|
|
62
|
+
const cfg = item.status === 'done'
|
|
63
|
+
? { icon: '\u{2705}', color: 'var(--success)', label: baseCfg.label }
|
|
64
|
+
: item.status === 'in_progress'
|
|
65
|
+
? { ...baseCfg, color: 'var(--primary)' }
|
|
66
|
+
: baseCfg;
|
|
67
|
+
const childStats = hasChildren ? countDescendantStatus(item) : null;
|
|
68
|
+
|
|
69
|
+
const handleDelete = (e: React.MouseEvent) => {
|
|
70
|
+
e.stopPropagation();
|
|
71
|
+
onItemDelete(item.id);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handlePinToggle = (e: React.MouseEvent) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
onItemUpdate(item.id, { is_pinned: !item.is_pinned });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleRefineToggle = (e: React.MouseEvent) => {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
setShowRefine(!showRefine);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isDone = item.status === 'done';
|
|
45
85
|
|
|
46
86
|
return (
|
|
47
87
|
<div className="select-none">
|
|
48
88
|
<div
|
|
49
|
-
className=
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
className={`tree-node-row group ${isDone ? 'opacity-50' : ''}`}
|
|
90
|
+
style={{
|
|
91
|
+
paddingLeft: `${depth * 20 + 8}px`,
|
|
92
|
+
borderLeft: depth === 0 ? `3px solid hsl(${cfg.color})` : undefined,
|
|
93
|
+
}}
|
|
94
|
+
onClick={() => selectMode ? onToggleSelect?.(item.id) : setShowDetail(!showDetail)}
|
|
53
95
|
>
|
|
96
|
+
{selectMode && (
|
|
97
|
+
<input
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={selected?.has(item.id) ?? false}
|
|
100
|
+
onChange={() => onToggleSelect?.(item.id)}
|
|
101
|
+
onClick={(e) => e.stopPropagation()}
|
|
102
|
+
className="w-3.5 h-3.5 flex-shrink-0 accent-accent"
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
54
106
|
{hasChildren ? (
|
|
55
107
|
<button
|
|
56
108
|
onClick={(e) => {
|
|
57
109
|
e.stopPropagation();
|
|
58
110
|
setExpanded(!expanded);
|
|
59
111
|
}}
|
|
60
|
-
className="text-muted-foreground hover:text-foreground text-
|
|
112
|
+
className="text-muted-foreground hover:text-foreground text-[10px] w-4 flex-shrink-0 transition-transform"
|
|
113
|
+
style={{ transform: expanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
|
|
61
114
|
>
|
|
62
|
-
|
|
115
|
+
▼
|
|
63
116
|
</button>
|
|
64
117
|
) : (
|
|
65
|
-
<span className="w-4 flex-shrink-0"
|
|
118
|
+
<span className="w-4 flex-shrink-0 text-center text-muted-foreground/30 text-[10px]">·</span>
|
|
66
119
|
)}
|
|
67
120
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
121
|
+
{/* Main content area */}
|
|
122
|
+
<div className="flex-1 min-w-0 py-0.5">
|
|
123
|
+
<div className="flex items-center gap-1.5">
|
|
124
|
+
<span className="text-sm flex-shrink-0">{cfg.icon}</span>
|
|
125
|
+
<span className={`text-sm truncate ${isDone ? 'line-through' : ''}`}>
|
|
126
|
+
{item.title}
|
|
127
|
+
</span>
|
|
128
|
+
{item.is_pinned && (
|
|
129
|
+
<span className="text-[10px] flex-shrink-0" title="고정됨">📌</span>
|
|
130
|
+
)}
|
|
131
|
+
{/* Child progress */}
|
|
132
|
+
{childStats && childStats.total > 0 && (
|
|
133
|
+
<span className="tree-progress-badge flex-shrink-0">
|
|
134
|
+
{childStats.done}/{childStats.total}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
{/* Description subtitle */}
|
|
139
|
+
{item.description && !showDetail && (
|
|
140
|
+
<p className="text-[11px] text-muted-foreground/50 truncate mt-0.5 leading-tight">
|
|
141
|
+
{item.description}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
78
145
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
{/* Right side controls */}
|
|
147
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
148
|
+
{/* Hover actions */}
|
|
149
|
+
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
150
|
+
<LockToggle
|
|
151
|
+
isLocked={item.is_locked}
|
|
152
|
+
onToggle={(locked) => onItemUpdate(item.id, { is_locked: locked })}
|
|
153
|
+
/>
|
|
154
|
+
<button onClick={handlePinToggle} className="tree-icon-btn" title={item.is_pinned ? '고정 해제' : '고정'}>
|
|
155
|
+
{item.is_pinned ? '📌' : '📍'}
|
|
156
|
+
</button>
|
|
157
|
+
<button onClick={handleRefineToggle} className="tree-action-btn" title="다듬기">
|
|
158
|
+
다듬기
|
|
159
|
+
</button>
|
|
160
|
+
<button onClick={handleDelete} className="tree-action-btn tree-action-btn-danger" title="삭제">
|
|
161
|
+
✕
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
82
164
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
165
|
+
{/* Always visible: priority + status */}
|
|
166
|
+
<span className="tree-priority-dot" style={{
|
|
167
|
+
background: item.priority === 'high' ? 'hsl(var(--destructive))'
|
|
168
|
+
: item.priority === 'medium' ? 'hsl(var(--warning))'
|
|
169
|
+
: 'hsl(var(--success))'
|
|
170
|
+
}} title={item.priority} />
|
|
171
|
+
<StatusBadge
|
|
172
|
+
status={item.status}
|
|
173
|
+
onStatusChange={(status) => onItemUpdate(item.id, { status })}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
87
176
|
</div>
|
|
88
177
|
|
|
178
|
+
{showRefine && (
|
|
179
|
+
<div style={{ marginLeft: `${depth * 20 + 28}px` }}>
|
|
180
|
+
<RefinePopover
|
|
181
|
+
itemId={item.id}
|
|
182
|
+
projectId={projectId}
|
|
183
|
+
title={item.title}
|
|
184
|
+
description={item.description}
|
|
185
|
+
onClose={() => setShowRefine(false)}
|
|
186
|
+
onItemUpdate={onItemUpdate}
|
|
187
|
+
onTreeRefresh={onTreeRefresh}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
89
192
|
{showDetail && (
|
|
90
193
|
<ItemDetail
|
|
91
194
|
itemId={item.id}
|
|
@@ -101,7 +204,7 @@ export default function TreeNode({ item, depth, projectId, onItemUpdate }: TreeN
|
|
|
101
204
|
)}
|
|
102
205
|
|
|
103
206
|
{expanded && hasChildren && (
|
|
104
|
-
<div>
|
|
207
|
+
<div className={depth === 0 ? 'tree-children-group' : ''}>
|
|
105
208
|
{item.children.map((child) => (
|
|
106
209
|
<TreeNode
|
|
107
210
|
key={child.id}
|
|
@@ -109,6 +212,12 @@ export default function TreeNode({ item, depth, projectId, onItemUpdate }: TreeN
|
|
|
109
212
|
depth={depth + 1}
|
|
110
213
|
projectId={projectId}
|
|
111
214
|
onItemUpdate={onItemUpdate}
|
|
215
|
+
onItemDelete={onItemDelete}
|
|
216
|
+
onTreeRefresh={onTreeRefresh}
|
|
217
|
+
selectMode={selectMode}
|
|
218
|
+
selected={selected}
|
|
219
|
+
onToggleSelect={onToggleSelect}
|
|
220
|
+
defaultExpanded={defaultExpanded}
|
|
112
221
|
/>
|
|
113
222
|
))}
|
|
114
223
|
</div>
|