@lobehub/lobehub 2.0.0-next.267 → 2.0.0-next.268
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/.cursor/rules/microcopy-cn.mdc +75 -63
- package/.cursor/rules/microcopy-en.mdc +4 -8
- package/CHANGELOG.md +25 -0
- package/README.md +8 -8
- package/README.zh-CN.md +8 -8
- package/apps/desktop/src/main/locales/default/common.ts +2 -2
- package/changelog/v1.json +5 -0
- package/docs/development/database-schema.dbml +4 -0
- package/e2e/CLAUDE.md +9 -8
- package/e2e/cucumber.config.js +1 -0
- package/e2e/src/features/page/README.md +118 -0
- package/e2e/src/features/page/crud.feature +62 -0
- package/e2e/src/features/page/editor-content.feature +93 -0
- package/e2e/src/features/page/editor-meta.feature +60 -0
- package/e2e/src/steps/agent/conversation.steps.ts +4 -4
- package/e2e/src/steps/home/sidebarAgent.steps.ts +91 -94
- package/e2e/src/steps/home/sidebarGroup.steps.ts +4 -4
- package/e2e/src/steps/hooks.ts +2 -0
- package/e2e/src/steps/page/editor-content.steps.ts +344 -0
- package/e2e/src/steps/page/editor-meta.steps.ts +410 -0
- package/e2e/src/steps/page/page-crud.steps.ts +363 -0
- package/e2e/src/support/world.ts +12 -0
- package/locales/ar/file.json +2 -0
- package/locales/bg-BG/file.json +2 -0
- package/locales/de-DE/file.json +2 -0
- package/locales/en-US/auth.json +1 -1
- package/locales/en-US/file.json +2 -0
- package/locales/en-US/metadata.json +2 -2
- package/locales/es-ES/file.json +2 -0
- package/locales/fa-IR/file.json +2 -0
- package/locales/fr-FR/file.json +2 -0
- package/locales/it-IT/file.json +2 -0
- package/locales/ja-JP/file.json +2 -0
- package/locales/ko-KR/file.json +2 -0
- package/locales/nl-NL/file.json +2 -0
- package/locales/pl-PL/file.json +2 -0
- package/locales/pt-BR/file.json +2 -0
- package/locales/ru-RU/file.json +2 -0
- package/locales/tr-TR/file.json +2 -0
- package/locales/vi-VN/file.json +2 -0
- package/locales/zh-CN/file.json +2 -0
- package/locales/zh-TW/file.json +2 -0
- package/package.json +1 -1
- package/packages/builtin-agents/src/agents/agent-builder/index.ts +1 -1
- package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +1 -1
- package/packages/builtin-agents/src/agents/page-agent/index.ts +1 -1
- package/packages/const/src/settings/group.ts +0 -10
- package/packages/database/migrations/0068_update_group_data.sql +4 -0
- package/packages/database/migrations/meta/0068_snapshot.json +9588 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/models/__tests__/chatGroup.test.ts +5 -7
- package/packages/database/src/models/__tests__/knowledgeBase.test.ts +185 -0
- package/packages/database/src/models/knowledgeBase.ts +67 -3
- package/packages/database/src/repositories/agentGroup/index.test.ts +23 -29
- package/packages/database/src/repositories/agentGroup/index.ts +4 -9
- package/packages/database/src/repositories/knowledge/index.ts +3 -3
- package/packages/database/src/schemas/chatGroup.ts +4 -3
- package/packages/database/src/types/chatGroup.ts +0 -7
- package/packages/types/src/agentGroup/index.ts +30 -9
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +9 -32
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +3 -37
- package/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +7 -53
- package/src/app/[variants]/(main)/home/features/RecentPage/List.tsx +2 -1
- package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +1 -1
- package/src/app/[variants]/(main)/resource/library/_layout/Sidebar.tsx +2 -2
- package/src/app/[variants]/(main)/resource/library/features/LibraryMenu.tsx +2 -2
- package/src/app/[variants]/(mobile)/chat/settings/features/SettingButton.tsx +2 -12
- package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +5 -27
- package/src/components/DragUpload/index.tsx +24 -27
- package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +2 -11
- package/src/features/CommandMenu/useCommandMenu.ts +4 -14
- package/src/features/ResourceManager/components/Editor/index.tsx +2 -3
- package/src/features/ResourceManager/components/Explorer/Header/index.tsx +13 -17
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/TruncatedFileName.tsx +130 -0
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +36 -4
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +4 -3
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +58 -2
- package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +58 -6
- package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +2 -5
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +9 -5
- package/src/features/ResourceManager/components/Explorer/index.tsx +11 -56
- package/src/features/ResourceManager/components/Header/AddButton.tsx +5 -6
- package/src/features/ResourceManager/components/LibraryHierarchy/HierarchyNode.tsx +382 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/index.tsx +396 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/styles.ts +19 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/treeState.ts +178 -0
- package/src/features/ResourceManager/components/LibraryHierarchy/types.ts +10 -0
- package/src/features/ResourceManager/index.tsx +3 -0
- package/src/layout/GlobalProvider/GroupWizardProvider.tsx +6 -29
- package/src/locales/default/auth.ts +1 -1
- package/src/locales/default/file.ts +2 -0
- package/src/locales/default/metadata.ts +2 -2
- package/src/server/modules/AgentRuntime/AgentRuntimeCoordinator.ts +30 -30
- package/src/server/modules/AgentRuntime/AgentStateManager.ts +23 -23
- package/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +16 -16
- package/src/server/modules/AgentRuntime/InMemoryStreamEventManager.ts +13 -13
- package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -2
- package/src/server/modules/AgentRuntime/StreamEventManager.ts +18 -18
- package/src/server/modules/AgentRuntime/types.ts +21 -21
- package/src/server/routers/lambda/__tests__/agentGroup.test.ts +8 -8
- package/src/server/routers/lambda/agentGroup.ts +10 -12
- package/src/server/services/document/index.ts +1 -0
- package/src/store/agentGroup/slices/curd.test.ts +4 -4
- package/src/store/file/slices/fileManager/action.ts +12 -4
- package/src/store/home/slices/homeInput/action.ts +0 -3
- package/src/store/session/slices/session/action.ts +5 -9
- package/src/app/[variants]/(mobile)/chat/settings/features/AgentTeamSettings/index.tsx +0 -95
- package/src/features/GroupChatSettings/AgentCard.tsx +0 -154
- package/src/features/GroupChatSettings/AgentTeamChatSettings.tsx +0 -179
- package/src/features/GroupChatSettings/AgentTeamMembersSettings.tsx +0 -244
- package/src/features/GroupChatSettings/AgentTeamMetaSettings.tsx +0 -94
- package/src/features/GroupChatSettings/AgentTeamSettings.tsx +0 -54
- package/src/features/GroupChatSettings/GroupCategory/index.tsx +0 -30
- package/src/features/GroupChatSettings/GroupCategory/useGroupCategory.tsx +0 -42
- package/src/features/GroupChatSettings/GroupChatSettingsProvider.tsx +0 -19
- package/src/features/GroupChatSettings/HostMemberCard.tsx +0 -113
- package/src/features/GroupChatSettings/StoreUpdater.tsx +0 -34
- package/src/features/GroupChatSettings/hooks/useGroupChatSettings.ts +0 -25
- package/src/features/GroupChatSettings/index.ts +0 -16
- package/src/features/GroupChatSettings/store/action.ts +0 -105
- package/src/features/GroupChatSettings/store/index.ts +0 -18
- package/src/features/GroupChatSettings/store/initialState.ts +0 -23
- package/src/features/GroupChatSettings/store/selectors.ts +0 -13
- package/src/features/ResourceManager/components/Tree/index.tsx +0 -883
- /package/src/features/ResourceManager/components/{Tree → LibraryHierarchy}/TreeSkeleton.tsx +0 -0
|
@@ -1,883 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { CaretDownFilled, LoadingOutlined } from '@ant-design/icons';
|
|
4
|
-
import { ActionIcon, Block, Flexbox, Icon, showContextMenu } from '@lobehub/ui';
|
|
5
|
-
import { App, Input } from 'antd';
|
|
6
|
-
import { createStaticStyles, cx } from 'antd-style';
|
|
7
|
-
import { FileText, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
|
8
|
-
import * as motion from 'motion/react-m';
|
|
9
|
-
import React, { memo, useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
|
10
|
-
import { useNavigate } from 'react-router-dom';
|
|
11
|
-
import { VList } from 'virtua';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
getTransparentDragImage,
|
|
15
|
-
useDragActive,
|
|
16
|
-
useDragState,
|
|
17
|
-
} from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
|
|
18
|
-
import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
|
|
19
|
-
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
|
|
20
|
-
import FileIcon from '@/components/FileIcon';
|
|
21
|
-
import { fileService } from '@/services/file';
|
|
22
|
-
import { useFileStore } from '@/store/file';
|
|
23
|
-
import type { ResourceItem } from '@/types/resource';
|
|
24
|
-
|
|
25
|
-
import { useFileItemDropdown } from '../Explorer/ItemDropdown/useFileItemDropdown';
|
|
26
|
-
import TreeSkeleton from './TreeSkeleton';
|
|
27
|
-
|
|
28
|
-
// Module-level state to persist expansion across re-renders
|
|
29
|
-
const treeState = new Map<
|
|
30
|
-
string,
|
|
31
|
-
{
|
|
32
|
-
expandedFolders: Set<string>;
|
|
33
|
-
folderChildrenCache: Map<string, any[]>;
|
|
34
|
-
loadedFolders: Set<string>;
|
|
35
|
-
loadingFolders: Set<string>;
|
|
36
|
-
}
|
|
37
|
-
>();
|
|
38
|
-
|
|
39
|
-
const TREE_REFRESH_EVENT = 'resource-tree-refresh';
|
|
40
|
-
|
|
41
|
-
const emitTreeRefresh = (knowledgeBaseId: string) => {
|
|
42
|
-
if (typeof window === 'undefined') return;
|
|
43
|
-
window.dispatchEvent(
|
|
44
|
-
new CustomEvent(TREE_REFRESH_EVENT, {
|
|
45
|
-
detail: { knowledgeBaseId },
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const getTreeState = (knowledgeBaseId: string) => {
|
|
51
|
-
if (!treeState.has(knowledgeBaseId)) {
|
|
52
|
-
treeState.set(knowledgeBaseId, {
|
|
53
|
-
expandedFolders: new Set(),
|
|
54
|
-
folderChildrenCache: new Map(),
|
|
55
|
-
loadedFolders: new Set(),
|
|
56
|
-
loadingFolders: new Set(),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return treeState.get(knowledgeBaseId)!;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Clear and reload all expanded folders
|
|
64
|
-
* This should be called along with file store's refreshFileList()
|
|
65
|
-
* Simpler approach: reload all expanded folders to avoid ID vs slug issues
|
|
66
|
-
*/
|
|
67
|
-
export const clearTreeFolderCache = async (knowledgeBaseId: string) => {
|
|
68
|
-
const state = treeState.get(knowledgeBaseId);
|
|
69
|
-
if (!state) return;
|
|
70
|
-
|
|
71
|
-
const { resourceList } = useFileStore.getState();
|
|
72
|
-
|
|
73
|
-
const resolveParentId = (key: string | null | undefined) => {
|
|
74
|
-
if (!key) return null;
|
|
75
|
-
// Prefer id match
|
|
76
|
-
const byId = resourceList.find(
|
|
77
|
-
(item) => item.knowledgeBaseId === knowledgeBaseId && item.id === key,
|
|
78
|
-
);
|
|
79
|
-
if (byId) return byId.id;
|
|
80
|
-
// Fallback to slug match
|
|
81
|
-
const bySlug = resourceList.find(
|
|
82
|
-
(item) => item.knowledgeBaseId === knowledgeBaseId && item.slug === key,
|
|
83
|
-
);
|
|
84
|
-
return bySlug?.id ?? key;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const buildChildrenFromStore = (parentKey: string | null) => {
|
|
88
|
-
const parentId = resolveParentId(parentKey);
|
|
89
|
-
return resourceList
|
|
90
|
-
.filter(
|
|
91
|
-
(item) =>
|
|
92
|
-
item.knowledgeBaseId === knowledgeBaseId &&
|
|
93
|
-
(item.parentId ?? null) === (parentId ?? null),
|
|
94
|
-
)
|
|
95
|
-
.map<ResourceItem>((item) => item)
|
|
96
|
-
.map((item) => ({
|
|
97
|
-
fileType: item.fileType,
|
|
98
|
-
id: item.id,
|
|
99
|
-
isFolder: item.fileType === 'custom/folder',
|
|
100
|
-
name: item.name,
|
|
101
|
-
slug: item.slug,
|
|
102
|
-
sourceType: item.sourceType,
|
|
103
|
-
url: item.url || '',
|
|
104
|
-
}))
|
|
105
|
-
.sort((a, b) => {
|
|
106
|
-
if (a.isFolder && !b.isFolder) return -1;
|
|
107
|
-
if (!a.isFolder && b.isFolder) return 1;
|
|
108
|
-
return a.name.localeCompare(b.name);
|
|
109
|
-
});
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Get list of all currently expanded folders before clearing
|
|
113
|
-
const expandedFoldersList = Array.from(state.expandedFolders);
|
|
114
|
-
|
|
115
|
-
// Clear all caches
|
|
116
|
-
state.folderChildrenCache.clear();
|
|
117
|
-
state.loadedFolders.clear();
|
|
118
|
-
|
|
119
|
-
// Reload each expanded folder
|
|
120
|
-
for (const folderKey of expandedFoldersList) {
|
|
121
|
-
// Prefer local store (explorer data) to avoid stale remote state
|
|
122
|
-
const localChildren = buildChildrenFromStore(folderKey);
|
|
123
|
-
if (localChildren.length > 0) {
|
|
124
|
-
state.folderChildrenCache.set(folderKey, localChildren);
|
|
125
|
-
state.loadedFolders.add(folderKey);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Fallback to remote fetch if store has no data (e.g., initial load)
|
|
130
|
-
try {
|
|
131
|
-
const response = await fileService.getKnowledgeItems({
|
|
132
|
-
knowledgeBaseId,
|
|
133
|
-
parentId: folderKey,
|
|
134
|
-
showFilesInKnowledgeBase: false,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (response?.items) {
|
|
138
|
-
const childItems = response.items.map((item) => ({
|
|
139
|
-
fileType: item.fileType,
|
|
140
|
-
id: item.id,
|
|
141
|
-
isFolder: item.fileType === 'custom/folder',
|
|
142
|
-
name: item.name,
|
|
143
|
-
slug: item.slug,
|
|
144
|
-
sourceType: item.sourceType,
|
|
145
|
-
url: item.url,
|
|
146
|
-
}));
|
|
147
|
-
|
|
148
|
-
// Sort children: folders first, then files
|
|
149
|
-
const sortedChildren = childItems.sort((a, b) => {
|
|
150
|
-
if (a.isFolder && !b.isFolder) return -1;
|
|
151
|
-
if (!a.isFolder && b.isFolder) return 1;
|
|
152
|
-
return a.name.localeCompare(b.name);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
state.folderChildrenCache.set(folderKey, sortedChildren);
|
|
156
|
-
state.loadedFolders.add(folderKey);
|
|
157
|
-
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error(`Failed to reload folder ${folderKey}:`, error);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Revalidate SWR caches for root and expanded folders to keep list and tree in sync
|
|
164
|
-
try {
|
|
165
|
-
const { mutate } = await import('swr');
|
|
166
|
-
const revalidateFolder = (parentId: string | null) =>
|
|
167
|
-
mutate(
|
|
168
|
-
[
|
|
169
|
-
'useFetchKnowledgeItems',
|
|
170
|
-
{
|
|
171
|
-
knowledgeBaseId,
|
|
172
|
-
parentId,
|
|
173
|
-
showFilesInKnowledgeBase: false,
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
undefined,
|
|
177
|
-
{ revalidate: true },
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
await Promise.all([
|
|
181
|
-
revalidateFolder(null),
|
|
182
|
-
...expandedFoldersList.map((folderKey) => revalidateFolder(folderKey)),
|
|
183
|
-
]);
|
|
184
|
-
} catch (error) {
|
|
185
|
-
console.error('Failed to revalidate tree SWR cache:', error);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
emitTreeRefresh(knowledgeBaseId);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
192
|
-
dragging: css`
|
|
193
|
-
will-change: transform;
|
|
194
|
-
opacity: 0.5;
|
|
195
|
-
`,
|
|
196
|
-
fileItemDragOver: css`
|
|
197
|
-
color: ${cssVar.colorBgElevated} !important;
|
|
198
|
-
background-color: ${cssVar.colorText} !important;
|
|
199
|
-
|
|
200
|
-
* {
|
|
201
|
-
color: ${cssVar.colorBgElevated} !important;
|
|
202
|
-
}
|
|
203
|
-
`,
|
|
204
|
-
treeItem: css`
|
|
205
|
-
cursor: pointer;
|
|
206
|
-
`,
|
|
207
|
-
}));
|
|
208
|
-
|
|
209
|
-
interface TreeItem {
|
|
210
|
-
children?: TreeItem[];
|
|
211
|
-
fileType: string;
|
|
212
|
-
id: string;
|
|
213
|
-
isFolder: boolean;
|
|
214
|
-
name: string;
|
|
215
|
-
slug?: string | null;
|
|
216
|
-
sourceType?: string;
|
|
217
|
-
url: string;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Row component for folder / file tree (virtualized by flattening visible nodes)
|
|
221
|
-
const FileTreeRow = memo<{
|
|
222
|
-
expandedFolders: Set<string>;
|
|
223
|
-
folderChildrenCache: Map<string, TreeItem[]>;
|
|
224
|
-
item: TreeItem;
|
|
225
|
-
level?: number;
|
|
226
|
-
loadingFolders: Set<string>;
|
|
227
|
-
onLoadFolder: (_: string) => Promise<void>;
|
|
228
|
-
onToggleFolder: (_: string) => void;
|
|
229
|
-
selectedKey: string | null;
|
|
230
|
-
updateKey?: number;
|
|
231
|
-
}>(
|
|
232
|
-
({
|
|
233
|
-
item,
|
|
234
|
-
level = 0,
|
|
235
|
-
expandedFolders,
|
|
236
|
-
loadingFolders,
|
|
237
|
-
onToggleFolder,
|
|
238
|
-
onLoadFolder,
|
|
239
|
-
selectedKey,
|
|
240
|
-
|
|
241
|
-
folderChildrenCache,
|
|
242
|
-
}) => {
|
|
243
|
-
const navigate = useNavigate();
|
|
244
|
-
const { currentFolderSlug } = useFolderPath();
|
|
245
|
-
const { message } = App.useApp();
|
|
246
|
-
|
|
247
|
-
const [setMode, setCurrentViewItemId, libraryId] = useResourceManagerStore((s) => [
|
|
248
|
-
s.setMode,
|
|
249
|
-
s.setCurrentViewItemId,
|
|
250
|
-
s.libraryId,
|
|
251
|
-
]);
|
|
252
|
-
|
|
253
|
-
const renameFolder = useFileStore((s) => s.renameFolder);
|
|
254
|
-
|
|
255
|
-
const [isRenaming, setIsRenaming] = useState(false);
|
|
256
|
-
const [renamingValue, setRenamingValue] = useState(item.name);
|
|
257
|
-
const inputRef = useRef<any>(null);
|
|
258
|
-
|
|
259
|
-
// Memoize computed values that don't change frequently
|
|
260
|
-
const { itemKey } = useMemo(
|
|
261
|
-
() => ({
|
|
262
|
-
itemKey: item.slug || item.id,
|
|
263
|
-
}),
|
|
264
|
-
[item.slug, item.id],
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const handleRenameStart = useCallback(() => {
|
|
268
|
-
setIsRenaming(true);
|
|
269
|
-
setRenamingValue(item.name);
|
|
270
|
-
// Focus input after render
|
|
271
|
-
setTimeout(() => {
|
|
272
|
-
inputRef.current?.focus();
|
|
273
|
-
inputRef.current?.select();
|
|
274
|
-
}, 0);
|
|
275
|
-
}, [item.name]);
|
|
276
|
-
|
|
277
|
-
const handleRenameConfirm = useCallback(async () => {
|
|
278
|
-
if (!renamingValue.trim()) {
|
|
279
|
-
message.error('Folder name cannot be empty');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (renamingValue.trim() === item.name) {
|
|
284
|
-
setIsRenaming(false);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
try {
|
|
289
|
-
await renameFolder(item.id, renamingValue.trim());
|
|
290
|
-
if (libraryId) {
|
|
291
|
-
await clearTreeFolderCache(libraryId);
|
|
292
|
-
}
|
|
293
|
-
message.success('Renamed successfully');
|
|
294
|
-
setIsRenaming(false);
|
|
295
|
-
} catch (error) {
|
|
296
|
-
console.error('Rename error:', error);
|
|
297
|
-
message.error('Rename failed');
|
|
298
|
-
}
|
|
299
|
-
}, [item.id, item.name, libraryId, renamingValue, renameFolder, message]);
|
|
300
|
-
|
|
301
|
-
const handleRenameCancel = useCallback(() => {
|
|
302
|
-
setIsRenaming(false);
|
|
303
|
-
setRenamingValue(item.name);
|
|
304
|
-
}, [item.name]);
|
|
305
|
-
|
|
306
|
-
const { menuItems } = useFileItemDropdown({
|
|
307
|
-
fileType: item.fileType,
|
|
308
|
-
filename: item.name,
|
|
309
|
-
id: item.id,
|
|
310
|
-
libraryId,
|
|
311
|
-
onRenameStart: item.isFolder ? handleRenameStart : undefined,
|
|
312
|
-
sourceType: item.sourceType,
|
|
313
|
-
url: item.url,
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const isDragActive = useDragActive();
|
|
317
|
-
const { setCurrentDrag } = useDragState();
|
|
318
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
319
|
-
const [isOver, setIsOver] = useState(false);
|
|
320
|
-
|
|
321
|
-
// Memoize drag data to prevent recreation
|
|
322
|
-
const dragData = useMemo(
|
|
323
|
-
() => ({
|
|
324
|
-
fileType: item.fileType,
|
|
325
|
-
isFolder: item.isFolder,
|
|
326
|
-
name: item.name,
|
|
327
|
-
sourceType: item.sourceType,
|
|
328
|
-
}),
|
|
329
|
-
[item.fileType, item.isFolder, item.name, item.sourceType],
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// Native HTML5 drag event handlers
|
|
333
|
-
const handleDragStart = useCallback(
|
|
334
|
-
(e: React.DragEvent) => {
|
|
335
|
-
setIsDragging(true);
|
|
336
|
-
setCurrentDrag({
|
|
337
|
-
data: dragData,
|
|
338
|
-
id: item.id,
|
|
339
|
-
type: item.isFolder ? 'folder' : 'file',
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Set drag image to be transparent (we use custom overlay)
|
|
343
|
-
const img = getTransparentDragImage();
|
|
344
|
-
if (img) {
|
|
345
|
-
e.dataTransfer.setDragImage(img, 0, 0);
|
|
346
|
-
}
|
|
347
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
348
|
-
},
|
|
349
|
-
[dragData, item.id, item.isFolder, setCurrentDrag],
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const handleDragEnd = useCallback(() => {
|
|
353
|
-
setIsDragging(false);
|
|
354
|
-
}, []);
|
|
355
|
-
|
|
356
|
-
const handleDragOver = useCallback(
|
|
357
|
-
(e: React.DragEvent) => {
|
|
358
|
-
if (!item.isFolder || !isDragActive) return;
|
|
359
|
-
|
|
360
|
-
e.preventDefault();
|
|
361
|
-
e.stopPropagation();
|
|
362
|
-
setIsOver(true);
|
|
363
|
-
},
|
|
364
|
-
[item.isFolder, isDragActive],
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
const handleDragLeave = useCallback(() => {
|
|
368
|
-
setIsOver(false);
|
|
369
|
-
}, []);
|
|
370
|
-
|
|
371
|
-
const handleDrop = useCallback(() => {
|
|
372
|
-
// Clear the highlight after drop
|
|
373
|
-
setIsOver(false);
|
|
374
|
-
}, []);
|
|
375
|
-
|
|
376
|
-
const handleItemClick = useCallback(() => {
|
|
377
|
-
// Open file modal using slug-based routing
|
|
378
|
-
const currentPath = currentFolderSlug
|
|
379
|
-
? `/resource/library/${libraryId}/${currentFolderSlug}`
|
|
380
|
-
: `/resource/library/${libraryId}`;
|
|
381
|
-
|
|
382
|
-
setCurrentViewItemId(itemKey);
|
|
383
|
-
navigate(`${currentPath}?file=${itemKey}`);
|
|
384
|
-
|
|
385
|
-
if (itemKey.startsWith('doc')) {
|
|
386
|
-
setMode('page');
|
|
387
|
-
} else {
|
|
388
|
-
// Set mode to 'file' immediately to prevent flickering to list view
|
|
389
|
-
setMode('editor');
|
|
390
|
-
}
|
|
391
|
-
}, [itemKey, currentFolderSlug, libraryId, navigate, setMode, setCurrentViewItemId]);
|
|
392
|
-
|
|
393
|
-
const handleFolderClick = useCallback(
|
|
394
|
-
(folderId: string, folderSlug?: string | null) => {
|
|
395
|
-
const navKey = folderSlug || folderId;
|
|
396
|
-
navigate(`/resource/library/${libraryId}/${navKey}`);
|
|
397
|
-
|
|
398
|
-
setMode('explorer');
|
|
399
|
-
},
|
|
400
|
-
[libraryId, navigate],
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
if (item.isFolder) {
|
|
404
|
-
const isExpanded = expandedFolders.has(itemKey);
|
|
405
|
-
const isActive = selectedKey === itemKey;
|
|
406
|
-
const isLoading = loadingFolders.has(itemKey);
|
|
407
|
-
|
|
408
|
-
const handleToggle = async () => {
|
|
409
|
-
// Toggle folder expansion
|
|
410
|
-
onToggleFolder(itemKey);
|
|
411
|
-
|
|
412
|
-
// Only load if not already cached
|
|
413
|
-
if (!isExpanded && !folderChildrenCache.has(itemKey)) {
|
|
414
|
-
await onLoadFolder(itemKey);
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
return (
|
|
419
|
-
<Flexbox gap={2}>
|
|
420
|
-
<Block
|
|
421
|
-
align={'center'}
|
|
422
|
-
className={cx(
|
|
423
|
-
styles.treeItem,
|
|
424
|
-
isOver && styles.fileItemDragOver,
|
|
425
|
-
isDragging && styles.dragging,
|
|
426
|
-
)}
|
|
427
|
-
clickable
|
|
428
|
-
data-drop-target-id={item.id}
|
|
429
|
-
data-is-folder={String(item.isFolder)}
|
|
430
|
-
draggable
|
|
431
|
-
gap={8}
|
|
432
|
-
height={36}
|
|
433
|
-
horizontal
|
|
434
|
-
onClick={() => handleFolderClick(item.id, item.slug)}
|
|
435
|
-
onContextMenu={(e) => {
|
|
436
|
-
e.preventDefault();
|
|
437
|
-
showContextMenu(menuItems());
|
|
438
|
-
}}
|
|
439
|
-
onDragEnd={handleDragEnd}
|
|
440
|
-
onDragLeave={handleDragLeave}
|
|
441
|
-
onDragOver={handleDragOver}
|
|
442
|
-
onDragStart={handleDragStart}
|
|
443
|
-
onDrop={handleDrop}
|
|
444
|
-
paddingInline={4}
|
|
445
|
-
style={{
|
|
446
|
-
paddingInlineStart: level * 12 + 4,
|
|
447
|
-
}}
|
|
448
|
-
variant={isActive ? 'filled' : 'borderless'}
|
|
449
|
-
>
|
|
450
|
-
{isLoading ? (
|
|
451
|
-
<ActionIcon icon={LoadingOutlined as any} size={'small'} spin style={{ width: 20 }} />
|
|
452
|
-
) : (
|
|
453
|
-
<motion.div
|
|
454
|
-
animate={{ rotate: isExpanded ? 0 : -90 }}
|
|
455
|
-
initial={false}
|
|
456
|
-
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
457
|
-
>
|
|
458
|
-
<ActionIcon
|
|
459
|
-
icon={CaretDownFilled as any}
|
|
460
|
-
onClick={(e) => {
|
|
461
|
-
e.stopPropagation();
|
|
462
|
-
handleToggle();
|
|
463
|
-
}}
|
|
464
|
-
size={'small'}
|
|
465
|
-
style={{ width: 20 }}
|
|
466
|
-
/>
|
|
467
|
-
</motion.div>
|
|
468
|
-
)}
|
|
469
|
-
<Flexbox
|
|
470
|
-
align={'center'}
|
|
471
|
-
flex={1}
|
|
472
|
-
gap={8}
|
|
473
|
-
horizontal
|
|
474
|
-
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
|
475
|
-
>
|
|
476
|
-
<Icon icon={isExpanded ? FolderOpenIcon : FolderIcon} size={18} />
|
|
477
|
-
{isRenaming ? (
|
|
478
|
-
<Input
|
|
479
|
-
onBlur={handleRenameConfirm}
|
|
480
|
-
onChange={(e) => setRenamingValue(e.target.value)}
|
|
481
|
-
onClick={(e) => e.stopPropagation()}
|
|
482
|
-
onKeyDown={(e) => {
|
|
483
|
-
if (e.key === 'Enter') {
|
|
484
|
-
e.preventDefault();
|
|
485
|
-
handleRenameConfirm();
|
|
486
|
-
} else if (e.key === 'Escape') {
|
|
487
|
-
e.preventDefault();
|
|
488
|
-
handleRenameCancel();
|
|
489
|
-
}
|
|
490
|
-
}}
|
|
491
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
492
|
-
ref={inputRef}
|
|
493
|
-
size="small"
|
|
494
|
-
style={{ flex: 1 }}
|
|
495
|
-
value={renamingValue}
|
|
496
|
-
/>
|
|
497
|
-
) : (
|
|
498
|
-
<span
|
|
499
|
-
style={{
|
|
500
|
-
flex: 1,
|
|
501
|
-
overflow: 'hidden',
|
|
502
|
-
textOverflow: 'ellipsis',
|
|
503
|
-
whiteSpace: 'nowrap',
|
|
504
|
-
}}
|
|
505
|
-
>
|
|
506
|
-
{item.name}
|
|
507
|
-
</span>
|
|
508
|
-
)}
|
|
509
|
-
</Flexbox>
|
|
510
|
-
</Block>
|
|
511
|
-
</Flexbox>
|
|
512
|
-
);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Render as file
|
|
516
|
-
const isActive = selectedKey === itemKey;
|
|
517
|
-
return (
|
|
518
|
-
<Flexbox gap={2}>
|
|
519
|
-
<Block
|
|
520
|
-
align={'center'}
|
|
521
|
-
className={cx(styles.treeItem, isDragging && styles.dragging)}
|
|
522
|
-
clickable
|
|
523
|
-
data-drop-target-id={item.id}
|
|
524
|
-
data-is-folder={false}
|
|
525
|
-
draggable
|
|
526
|
-
gap={8}
|
|
527
|
-
height={36}
|
|
528
|
-
horizontal
|
|
529
|
-
onClick={handleItemClick}
|
|
530
|
-
onContextMenu={(e) => {
|
|
531
|
-
e.preventDefault();
|
|
532
|
-
showContextMenu(menuItems());
|
|
533
|
-
}}
|
|
534
|
-
onDragEnd={handleDragEnd}
|
|
535
|
-
onDragStart={handleDragStart}
|
|
536
|
-
paddingInline={4}
|
|
537
|
-
style={{
|
|
538
|
-
paddingInlineStart: level * 12 + 4,
|
|
539
|
-
}}
|
|
540
|
-
variant={isActive ? 'filled' : 'borderless'}
|
|
541
|
-
>
|
|
542
|
-
<div style={{ width: 20 }} />
|
|
543
|
-
<Flexbox
|
|
544
|
-
align={'center'}
|
|
545
|
-
flex={1}
|
|
546
|
-
gap={8}
|
|
547
|
-
horizontal
|
|
548
|
-
style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
|
|
549
|
-
>
|
|
550
|
-
{item.sourceType === 'document' ? (
|
|
551
|
-
<Icon icon={FileText} size={18} />
|
|
552
|
-
) : (
|
|
553
|
-
<FileIcon fileName={item.name} fileType={item.fileType} size={18} />
|
|
554
|
-
)}
|
|
555
|
-
<span
|
|
556
|
-
style={{
|
|
557
|
-
flex: 1,
|
|
558
|
-
overflow: 'hidden',
|
|
559
|
-
textOverflow: 'ellipsis',
|
|
560
|
-
whiteSpace: 'nowrap',
|
|
561
|
-
}}
|
|
562
|
-
>
|
|
563
|
-
{item.name}
|
|
564
|
-
</span>
|
|
565
|
-
</Flexbox>
|
|
566
|
-
</Block>
|
|
567
|
-
</Flexbox>
|
|
568
|
-
);
|
|
569
|
-
},
|
|
570
|
-
);
|
|
571
|
-
|
|
572
|
-
FileTreeRow.displayName = 'FileTreeRow';
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* As a sidebar along with the Explorer to work
|
|
576
|
-
*/
|
|
577
|
-
const FileTree = memo(() => {
|
|
578
|
-
const { currentFolderSlug } = useFolderPath();
|
|
579
|
-
|
|
580
|
-
const [useFetchKnowledgeItems, useFetchFolderBreadcrumb, useFetchKnowledgeItem] = useFileStore(
|
|
581
|
-
(s) => [s.useFetchKnowledgeItems, s.useFetchFolderBreadcrumb, s.useFetchKnowledgeItem],
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
const [libraryId, currentViewItemId] = useResourceManagerStore((s) => [
|
|
585
|
-
s.libraryId,
|
|
586
|
-
s.currentViewItemId,
|
|
587
|
-
]);
|
|
588
|
-
|
|
589
|
-
// Force re-render when tree state changes
|
|
590
|
-
const [updateKey, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
591
|
-
|
|
592
|
-
// Get the persisted state for this knowledge base
|
|
593
|
-
const state = React.useMemo(() => getTreeState(libraryId || ''), [libraryId]);
|
|
594
|
-
const { expandedFolders, folderChildrenCache, loadingFolders } = state;
|
|
595
|
-
|
|
596
|
-
// Fetch breadcrumb for current folder
|
|
597
|
-
const { data: folderBreadcrumb } = useFetchFolderBreadcrumb(currentFolderSlug);
|
|
598
|
-
|
|
599
|
-
// Fetch current file when viewing a file
|
|
600
|
-
const { data: currentFile } = useFetchKnowledgeItem(currentViewItemId);
|
|
601
|
-
|
|
602
|
-
// Track parent folder key for file selection - stored in a ref to avoid hook order issues
|
|
603
|
-
const parentFolderKeyRef = React.useRef<string | null>(null);
|
|
604
|
-
|
|
605
|
-
// Fetch root level data using SWR
|
|
606
|
-
const { data: rootData, isLoading } = useFetchKnowledgeItems({
|
|
607
|
-
knowledgeBaseId: libraryId,
|
|
608
|
-
parentId: null,
|
|
609
|
-
showFilesInKnowledgeBase: false,
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
// Sort items: folders first, then files
|
|
613
|
-
const sortItems = useCallback(<T extends TreeItem>(items: T[]): T[] => {
|
|
614
|
-
return [...items].sort((a, b) => {
|
|
615
|
-
// Folders first
|
|
616
|
-
if (a.isFolder && !b.isFolder) return -1;
|
|
617
|
-
if (!a.isFolder && b.isFolder) return 1;
|
|
618
|
-
// Then alphabetically by name
|
|
619
|
-
return a.name.localeCompare(b.name);
|
|
620
|
-
});
|
|
621
|
-
}, []);
|
|
622
|
-
|
|
623
|
-
// Convert root data to tree items
|
|
624
|
-
const items: TreeItem[] = React.useMemo(() => {
|
|
625
|
-
if (!rootData) return [];
|
|
626
|
-
|
|
627
|
-
const mappedItems: TreeItem[] = rootData.map((item) => ({
|
|
628
|
-
fileType: item.fileType,
|
|
629
|
-
id: item.id,
|
|
630
|
-
isFolder: item.fileType === 'custom/folder',
|
|
631
|
-
name: item.name,
|
|
632
|
-
slug: item.slug,
|
|
633
|
-
sourceType: item.sourceType,
|
|
634
|
-
url: item.url,
|
|
635
|
-
}));
|
|
636
|
-
|
|
637
|
-
return sortItems(mappedItems);
|
|
638
|
-
}, [rootData, sortItems, updateKey]);
|
|
639
|
-
|
|
640
|
-
const visibleNodes = React.useMemo(() => {
|
|
641
|
-
interface VisibleNode {
|
|
642
|
-
item: TreeItem;
|
|
643
|
-
key: string;
|
|
644
|
-
level: number;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
const result: VisibleNode[] = [];
|
|
648
|
-
|
|
649
|
-
const walk = (nodes: TreeItem[], level: number) => {
|
|
650
|
-
for (const node of nodes) {
|
|
651
|
-
const key = node.slug || node.id;
|
|
652
|
-
|
|
653
|
-
result.push({ item: node, key, level });
|
|
654
|
-
|
|
655
|
-
if (!node.isFolder) continue;
|
|
656
|
-
if (!expandedFolders.has(key)) continue;
|
|
657
|
-
|
|
658
|
-
const children = folderChildrenCache.get(key);
|
|
659
|
-
if (!children || children.length === 0) continue;
|
|
660
|
-
|
|
661
|
-
walk(children, level + 1);
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
walk(items, 0);
|
|
666
|
-
|
|
667
|
-
return result;
|
|
668
|
-
// NOTE: expandedFolders / folderChildrenCache are mutated in-place, so rely on updateKey for recompute
|
|
669
|
-
}, [items, expandedFolders, folderChildrenCache, updateKey]);
|
|
670
|
-
|
|
671
|
-
const handleLoadFolder = useCallback(
|
|
672
|
-
async (folderId: string) => {
|
|
673
|
-
// Set loading state
|
|
674
|
-
state.loadingFolders.add(folderId);
|
|
675
|
-
forceUpdate();
|
|
676
|
-
|
|
677
|
-
try {
|
|
678
|
-
// Use SWR mutate to trigger a fetch that will be cached and shared with FileExplorer
|
|
679
|
-
const { mutate: swrMutate } = await import('swr');
|
|
680
|
-
const response = await swrMutate(
|
|
681
|
-
[
|
|
682
|
-
'useFetchKnowledgeItems',
|
|
683
|
-
{
|
|
684
|
-
knowledgeBaseId: libraryId,
|
|
685
|
-
parentId: folderId,
|
|
686
|
-
showFilesInKnowledgeBase: false,
|
|
687
|
-
},
|
|
688
|
-
],
|
|
689
|
-
() =>
|
|
690
|
-
fileService.getKnowledgeItems({
|
|
691
|
-
knowledgeBaseId: libraryId,
|
|
692
|
-
parentId: folderId,
|
|
693
|
-
showFilesInKnowledgeBase: false,
|
|
694
|
-
}),
|
|
695
|
-
{
|
|
696
|
-
revalidate: false, // Don't revalidate immediately after mutation
|
|
697
|
-
},
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
if (!response || !response.items) {
|
|
701
|
-
console.error('Failed to load folder contents: no data returned');
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const childItems: TreeItem[] = response.items.map((item) => ({
|
|
706
|
-
fileType: item.fileType,
|
|
707
|
-
id: item.id,
|
|
708
|
-
isFolder: item.fileType === 'custom/folder',
|
|
709
|
-
name: item.name,
|
|
710
|
-
slug: item.slug,
|
|
711
|
-
sourceType: item.sourceType,
|
|
712
|
-
url: item.url,
|
|
713
|
-
}));
|
|
714
|
-
|
|
715
|
-
// Sort children: folders first, then files
|
|
716
|
-
const sortedChildren = sortItems(childItems);
|
|
717
|
-
|
|
718
|
-
// Store children in cache
|
|
719
|
-
state.folderChildrenCache.set(folderId, sortedChildren);
|
|
720
|
-
state.loadedFolders.add(folderId);
|
|
721
|
-
} catch (error) {
|
|
722
|
-
console.error('Failed to load folder contents:', error);
|
|
723
|
-
} finally {
|
|
724
|
-
// Clear loading state
|
|
725
|
-
state.loadingFolders.delete(folderId);
|
|
726
|
-
// Trigger re-render
|
|
727
|
-
forceUpdate();
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
[libraryId, sortItems, state, forceUpdate],
|
|
731
|
-
);
|
|
732
|
-
|
|
733
|
-
const handleToggleFolder = useCallback(
|
|
734
|
-
(folderId: string) => {
|
|
735
|
-
if (state.expandedFolders.has(folderId)) {
|
|
736
|
-
state.expandedFolders.delete(folderId);
|
|
737
|
-
} else {
|
|
738
|
-
state.expandedFolders.add(folderId);
|
|
739
|
-
}
|
|
740
|
-
// Trigger re-render
|
|
741
|
-
forceUpdate();
|
|
742
|
-
},
|
|
743
|
-
[state, forceUpdate],
|
|
744
|
-
);
|
|
745
|
-
|
|
746
|
-
// Reset parent folder key when switching libraries
|
|
747
|
-
React.useEffect(() => {
|
|
748
|
-
parentFolderKeyRef.current = null;
|
|
749
|
-
}, [libraryId]);
|
|
750
|
-
|
|
751
|
-
// Listen for external tree refresh events (triggered when cache is cleared)
|
|
752
|
-
React.useEffect(() => {
|
|
753
|
-
if (typeof window === 'undefined') return;
|
|
754
|
-
|
|
755
|
-
const handleTreeRefresh = (event: Event) => {
|
|
756
|
-
const detail = (event as CustomEvent<{ knowledgeBaseId?: string }>).detail;
|
|
757
|
-
if (detail?.knowledgeBaseId && libraryId && detail.knowledgeBaseId !== libraryId) return;
|
|
758
|
-
forceUpdate();
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
window.addEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
|
|
762
|
-
return () => {
|
|
763
|
-
window.removeEventListener(TREE_REFRESH_EVENT, handleTreeRefresh);
|
|
764
|
-
};
|
|
765
|
-
}, [libraryId, forceUpdate]);
|
|
766
|
-
|
|
767
|
-
// Auto-expand folders when navigating to a folder in Explorer
|
|
768
|
-
React.useEffect(() => {
|
|
769
|
-
if (!folderBreadcrumb || folderBreadcrumb.length === 0) return;
|
|
770
|
-
|
|
771
|
-
let hasChanges = false;
|
|
772
|
-
|
|
773
|
-
// Expand all folders in the breadcrumb path
|
|
774
|
-
for (const crumb of folderBreadcrumb) {
|
|
775
|
-
const key = crumb.slug || crumb.id;
|
|
776
|
-
if (!state.expandedFolders.has(key)) {
|
|
777
|
-
state.expandedFolders.add(key);
|
|
778
|
-
hasChanges = true;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Load folder contents if not already loaded
|
|
782
|
-
if (!state.loadedFolders.has(key) && !state.loadingFolders.has(key)) {
|
|
783
|
-
handleLoadFolder(key);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (hasChanges) {
|
|
788
|
-
forceUpdate();
|
|
789
|
-
}
|
|
790
|
-
}, [folderBreadcrumb, state, forceUpdate, handleLoadFolder]);
|
|
791
|
-
|
|
792
|
-
// Auto-expand parent folder when viewing a file
|
|
793
|
-
React.useEffect(() => {
|
|
794
|
-
if (!currentFile || !currentViewItemId) {
|
|
795
|
-
parentFolderKeyRef.current = null;
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// If the file has a parent folder, expand the path to it
|
|
800
|
-
if (currentFile.parentId) {
|
|
801
|
-
// Fetch the parent folder's breadcrumb to get the full path
|
|
802
|
-
const fetchParentPath = async () => {
|
|
803
|
-
try {
|
|
804
|
-
const parentBreadcrumb = await fileService.getFolderBreadcrumb(currentFile.parentId!);
|
|
805
|
-
|
|
806
|
-
if (!parentBreadcrumb || parentBreadcrumb.length === 0) return;
|
|
807
|
-
|
|
808
|
-
let hasChanges = false;
|
|
809
|
-
|
|
810
|
-
// The last item in breadcrumb is the immediate parent folder
|
|
811
|
-
const parentFolder = parentBreadcrumb.at(-1)!;
|
|
812
|
-
const parentKey = parentFolder.slug || parentFolder.id;
|
|
813
|
-
parentFolderKeyRef.current = parentKey;
|
|
814
|
-
|
|
815
|
-
// Expand all folders in the parent's breadcrumb path
|
|
816
|
-
for (const crumb of parentBreadcrumb) {
|
|
817
|
-
const key = crumb.slug || crumb.id;
|
|
818
|
-
if (!state.expandedFolders.has(key)) {
|
|
819
|
-
state.expandedFolders.add(key);
|
|
820
|
-
hasChanges = true;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Load folder contents if not already loaded
|
|
824
|
-
if (!state.loadedFolders.has(key) && !state.loadingFolders.has(key)) {
|
|
825
|
-
handleLoadFolder(key);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
if (hasChanges) {
|
|
830
|
-
forceUpdate();
|
|
831
|
-
}
|
|
832
|
-
} catch (error) {
|
|
833
|
-
console.error('Failed to fetch parent folder breadcrumb:', error);
|
|
834
|
-
}
|
|
835
|
-
};
|
|
836
|
-
|
|
837
|
-
fetchParentPath();
|
|
838
|
-
} else {
|
|
839
|
-
parentFolderKeyRef.current = null;
|
|
840
|
-
}
|
|
841
|
-
}, [currentFile, currentViewItemId, state, forceUpdate, handleLoadFolder]);
|
|
842
|
-
|
|
843
|
-
if (isLoading) {
|
|
844
|
-
return <TreeSkeleton />;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Determine which item should be highlighted
|
|
848
|
-
// If viewing a file, highlight its parent folder
|
|
849
|
-
// Otherwise, highlight the current folder
|
|
850
|
-
const selectedKey =
|
|
851
|
-
currentViewItemId && parentFolderKeyRef.current
|
|
852
|
-
? parentFolderKeyRef.current
|
|
853
|
-
: currentFolderSlug;
|
|
854
|
-
|
|
855
|
-
return (
|
|
856
|
-
<Flexbox paddingInline={4} style={{ height: '100%' }}>
|
|
857
|
-
<VList
|
|
858
|
-
bufferSize={typeof window !== 'undefined' ? window.innerHeight : 0}
|
|
859
|
-
style={{ height: '100%' }}
|
|
860
|
-
>
|
|
861
|
-
{visibleNodes.map(({ item, key, level }) => (
|
|
862
|
-
<div key={key} style={{ paddingBottom: 2 }}>
|
|
863
|
-
<FileTreeRow
|
|
864
|
-
expandedFolders={expandedFolders}
|
|
865
|
-
folderChildrenCache={folderChildrenCache}
|
|
866
|
-
item={item}
|
|
867
|
-
level={level}
|
|
868
|
-
loadingFolders={loadingFolders}
|
|
869
|
-
onLoadFolder={handleLoadFolder}
|
|
870
|
-
onToggleFolder={handleToggleFolder}
|
|
871
|
-
selectedKey={selectedKey}
|
|
872
|
-
updateKey={updateKey}
|
|
873
|
-
/>
|
|
874
|
-
</div>
|
|
875
|
-
))}
|
|
876
|
-
</VList>
|
|
877
|
-
</Flexbox>
|
|
878
|
-
);
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
FileTree.displayName = 'FileTree';
|
|
882
|
-
|
|
883
|
-
export default FileTree;
|