@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.
Files changed (126) hide show
  1. package/.cursor/rules/microcopy-cn.mdc +75 -63
  2. package/.cursor/rules/microcopy-en.mdc +4 -8
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +8 -8
  5. package/README.zh-CN.md +8 -8
  6. package/apps/desktop/src/main/locales/default/common.ts +2 -2
  7. package/changelog/v1.json +5 -0
  8. package/docs/development/database-schema.dbml +4 -0
  9. package/e2e/CLAUDE.md +9 -8
  10. package/e2e/cucumber.config.js +1 -0
  11. package/e2e/src/features/page/README.md +118 -0
  12. package/e2e/src/features/page/crud.feature +62 -0
  13. package/e2e/src/features/page/editor-content.feature +93 -0
  14. package/e2e/src/features/page/editor-meta.feature +60 -0
  15. package/e2e/src/steps/agent/conversation.steps.ts +4 -4
  16. package/e2e/src/steps/home/sidebarAgent.steps.ts +91 -94
  17. package/e2e/src/steps/home/sidebarGroup.steps.ts +4 -4
  18. package/e2e/src/steps/hooks.ts +2 -0
  19. package/e2e/src/steps/page/editor-content.steps.ts +344 -0
  20. package/e2e/src/steps/page/editor-meta.steps.ts +410 -0
  21. package/e2e/src/steps/page/page-crud.steps.ts +363 -0
  22. package/e2e/src/support/world.ts +12 -0
  23. package/locales/ar/file.json +2 -0
  24. package/locales/bg-BG/file.json +2 -0
  25. package/locales/de-DE/file.json +2 -0
  26. package/locales/en-US/auth.json +1 -1
  27. package/locales/en-US/file.json +2 -0
  28. package/locales/en-US/metadata.json +2 -2
  29. package/locales/es-ES/file.json +2 -0
  30. package/locales/fa-IR/file.json +2 -0
  31. package/locales/fr-FR/file.json +2 -0
  32. package/locales/it-IT/file.json +2 -0
  33. package/locales/ja-JP/file.json +2 -0
  34. package/locales/ko-KR/file.json +2 -0
  35. package/locales/nl-NL/file.json +2 -0
  36. package/locales/pl-PL/file.json +2 -0
  37. package/locales/pt-BR/file.json +2 -0
  38. package/locales/ru-RU/file.json +2 -0
  39. package/locales/tr-TR/file.json +2 -0
  40. package/locales/vi-VN/file.json +2 -0
  41. package/locales/zh-CN/file.json +2 -0
  42. package/locales/zh-TW/file.json +2 -0
  43. package/package.json +1 -1
  44. package/packages/builtin-agents/src/agents/agent-builder/index.ts +1 -1
  45. package/packages/builtin-agents/src/agents/group-agent-builder/index.ts +1 -1
  46. package/packages/builtin-agents/src/agents/page-agent/index.ts +1 -1
  47. package/packages/const/src/settings/group.ts +0 -10
  48. package/packages/database/migrations/0068_update_group_data.sql +4 -0
  49. package/packages/database/migrations/meta/0068_snapshot.json +9588 -0
  50. package/packages/database/migrations/meta/_journal.json +7 -0
  51. package/packages/database/src/models/__tests__/chatGroup.test.ts +5 -7
  52. package/packages/database/src/models/__tests__/knowledgeBase.test.ts +185 -0
  53. package/packages/database/src/models/knowledgeBase.ts +67 -3
  54. package/packages/database/src/repositories/agentGroup/index.test.ts +23 -29
  55. package/packages/database/src/repositories/agentGroup/index.ts +4 -9
  56. package/packages/database/src/repositories/knowledge/index.ts +3 -3
  57. package/packages/database/src/schemas/chatGroup.ts +4 -3
  58. package/packages/database/src/types/chatGroup.ts +0 -7
  59. package/packages/types/src/agentGroup/index.ts +30 -9
  60. package/src/app/[variants]/(main)/home/_layout/Body/Agent/ModalProvider.tsx +9 -32
  61. package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +3 -37
  62. package/src/app/[variants]/(main)/home/_layout/hooks/useSessionGroupMenuItems.tsx +7 -53
  63. package/src/app/[variants]/(main)/home/features/RecentPage/List.tsx +2 -1
  64. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +1 -1
  65. package/src/app/[variants]/(main)/resource/library/_layout/Sidebar.tsx +2 -2
  66. package/src/app/[variants]/(main)/resource/library/features/LibraryMenu.tsx +2 -2
  67. package/src/app/[variants]/(mobile)/chat/settings/features/SettingButton.tsx +2 -12
  68. package/src/components/ChatGroupWizard/ChatGroupWizard.tsx +5 -27
  69. package/src/components/DragUpload/index.tsx +24 -27
  70. package/src/components/MemberSelectionModal/MemberSelectionModal.tsx +2 -11
  71. package/src/features/CommandMenu/useCommandMenu.ts +4 -14
  72. package/src/features/ResourceManager/components/Editor/index.tsx +2 -3
  73. package/src/features/ResourceManager/components/Explorer/Header/index.tsx +13 -17
  74. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +1 -1
  75. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/TruncatedFileName.tsx +130 -0
  76. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +36 -4
  77. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +4 -3
  78. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +58 -2
  79. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +58 -6
  80. package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +2 -5
  81. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +9 -5
  82. package/src/features/ResourceManager/components/Explorer/index.tsx +11 -56
  83. package/src/features/ResourceManager/components/Header/AddButton.tsx +5 -6
  84. package/src/features/ResourceManager/components/LibraryHierarchy/HierarchyNode.tsx +382 -0
  85. package/src/features/ResourceManager/components/LibraryHierarchy/index.tsx +396 -0
  86. package/src/features/ResourceManager/components/LibraryHierarchy/styles.ts +19 -0
  87. package/src/features/ResourceManager/components/LibraryHierarchy/treeState.ts +178 -0
  88. package/src/features/ResourceManager/components/LibraryHierarchy/types.ts +10 -0
  89. package/src/features/ResourceManager/index.tsx +3 -0
  90. package/src/layout/GlobalProvider/GroupWizardProvider.tsx +6 -29
  91. package/src/locales/default/auth.ts +1 -1
  92. package/src/locales/default/file.ts +2 -0
  93. package/src/locales/default/metadata.ts +2 -2
  94. package/src/server/modules/AgentRuntime/AgentRuntimeCoordinator.ts +30 -30
  95. package/src/server/modules/AgentRuntime/AgentStateManager.ts +23 -23
  96. package/src/server/modules/AgentRuntime/InMemoryAgentStateManager.ts +16 -16
  97. package/src/server/modules/AgentRuntime/InMemoryStreamEventManager.ts +13 -13
  98. package/src/server/modules/AgentRuntime/RuntimeExecutors.ts +2 -2
  99. package/src/server/modules/AgentRuntime/StreamEventManager.ts +18 -18
  100. package/src/server/modules/AgentRuntime/types.ts +21 -21
  101. package/src/server/routers/lambda/__tests__/agentGroup.test.ts +8 -8
  102. package/src/server/routers/lambda/agentGroup.ts +10 -12
  103. package/src/server/services/document/index.ts +1 -0
  104. package/src/store/agentGroup/slices/curd.test.ts +4 -4
  105. package/src/store/file/slices/fileManager/action.ts +12 -4
  106. package/src/store/home/slices/homeInput/action.ts +0 -3
  107. package/src/store/session/slices/session/action.ts +5 -9
  108. package/src/app/[variants]/(mobile)/chat/settings/features/AgentTeamSettings/index.tsx +0 -95
  109. package/src/features/GroupChatSettings/AgentCard.tsx +0 -154
  110. package/src/features/GroupChatSettings/AgentTeamChatSettings.tsx +0 -179
  111. package/src/features/GroupChatSettings/AgentTeamMembersSettings.tsx +0 -244
  112. package/src/features/GroupChatSettings/AgentTeamMetaSettings.tsx +0 -94
  113. package/src/features/GroupChatSettings/AgentTeamSettings.tsx +0 -54
  114. package/src/features/GroupChatSettings/GroupCategory/index.tsx +0 -30
  115. package/src/features/GroupChatSettings/GroupCategory/useGroupCategory.tsx +0 -42
  116. package/src/features/GroupChatSettings/GroupChatSettingsProvider.tsx +0 -19
  117. package/src/features/GroupChatSettings/HostMemberCard.tsx +0 -113
  118. package/src/features/GroupChatSettings/StoreUpdater.tsx +0 -34
  119. package/src/features/GroupChatSettings/hooks/useGroupChatSettings.ts +0 -25
  120. package/src/features/GroupChatSettings/index.ts +0 -16
  121. package/src/features/GroupChatSettings/store/action.ts +0 -105
  122. package/src/features/GroupChatSettings/store/index.ts +0 -18
  123. package/src/features/GroupChatSettings/store/initialState.ts +0 -23
  124. package/src/features/GroupChatSettings/store/selectors.ts +0 -13
  125. package/src/features/ResourceManager/components/Tree/index.tsx +0 -883
  126. /package/src/features/ResourceManager/components/{Tree → LibraryHierarchy}/TreeSkeleton.tsx +0 -0
@@ -12,11 +12,8 @@ import { useFetchResources, useResourceStore } from '@/store/file/slices/resourc
12
12
  import EmptyPlaceholder from './EmptyPlaceholder';
13
13
  import Header from './Header';
14
14
  import ListView from './ListView';
15
- import ListViewSkeleton from './ListView/Skeleton';
16
15
  import MasonryView from './MasonryView';
17
- import MasonryViewSkeleton from './MasonryView/Skeleton';
18
16
  import { useCheckTaskStatus } from './useCheckTaskStatus';
19
- import { useMasonryColumnCount } from './useMasonryColumnCount';
20
17
  import { useResourceExplorer } from './useResourceExplorer';
21
18
 
22
19
  /**
@@ -32,27 +29,16 @@ const ResourceExplorer = memo(() => {
32
29
  useResourceManagerUrlSync();
33
30
 
34
31
  // Get state from Resource Manager store
35
- const [
36
- libraryId,
37
- category,
38
- viewMode,
39
- isTransitioning,
40
- isMasonryReady,
41
- searchQuery,
42
- setSelectedFileIds,
43
- sorter,
44
- sortType,
45
- ] = useResourceManagerStore((s) => [
46
- s.libraryId,
47
- s.category,
48
- s.viewMode,
49
- s.isTransitioning,
50
- s.isMasonryReady,
51
- s.searchQuery,
52
- s.setSelectedFileIds,
53
- s.sorter,
54
- s.sortType,
55
- ]);
32
+ const [libraryId, category, viewMode, searchQuery, setSelectedFileIds, sorter, sortType] =
33
+ useResourceManagerStore((s) => [
34
+ s.libraryId,
35
+ s.category,
36
+ s.viewMode,
37
+ s.searchQuery,
38
+ s.setSelectedFileIds,
39
+ s.sorter,
40
+ s.sortType,
41
+ ]);
56
42
 
57
43
  // Get folder path for empty state check
58
44
  const { currentFolderSlug } = useFolderPath();
@@ -77,19 +63,7 @@ const ResourceExplorer = memo(() => {
77
63
  const { isLoading, isValidating } = useFetchResources(queryParams);
78
64
 
79
65
  // Get resource data from store (updated by SWR hook)
80
- const { resourceList, queryParams: currentQueryParams } = useResourceStore();
81
-
82
- // Check if we're navigating to a different view (different query params)
83
- const isNavigating = useMemo(() => {
84
- if (!currentQueryParams || !queryParams) return false;
85
-
86
- return (
87
- currentQueryParams.libraryId !== queryParams.libraryId ||
88
- currentQueryParams.parentId !== queryParams.parentId ||
89
- currentQueryParams.category !== queryParams.category ||
90
- currentQueryParams.q !== queryParams.q
91
- );
92
- }, [currentQueryParams, queryParams]);
66
+ const { resourceList } = useResourceStore();
93
67
 
94
68
  // Map ResourceItem[] to FileListItem[] for compatibility
95
69
  // TODO: Eventually update all consumers to use ResourceItem directly
@@ -119,19 +93,6 @@ const ResourceExplorer = memo(() => {
119
93
  setSelectedFileIds([]);
120
94
  }, [category, libraryId, searchQuery, setSelectedFileIds]);
121
95
 
122
- // Computed values
123
- const columnCount = useMasonryColumnCount();
124
-
125
- // Show skeleton when:
126
- // 1. Initial load with no data (isLoading && no data)
127
- // 2. Navigating to different folder/category (isNavigating && isValidating)
128
- // 3. View mode transitions
129
- const showSkeleton =
130
- (isLoading && (!data || data.length >= 5)) ||
131
- (isNavigating && isValidating) ||
132
- (viewMode === 'list' && isTransitioning) ||
133
- (viewMode === 'masonry' && (isTransitioning || !isMasonryReady));
134
-
135
96
  const showEmptyStatus = !isLoading && !isValidating && data?.length === 0 && !currentFolderSlug;
136
97
 
137
98
  return (
@@ -139,12 +100,6 @@ const ResourceExplorer = memo(() => {
139
100
  <Header />
140
101
  {showEmptyStatus ? (
141
102
  <EmptyPlaceholder />
142
- ) : showSkeleton ? (
143
- viewMode === 'list' ? (
144
- <ListViewSkeleton />
145
- ) : (
146
- <MasonryViewSkeleton columnCount={columnCount} />
147
- )
148
103
  ) : viewMode === 'list' ? (
149
104
  <ListView />
150
105
  ) : (
@@ -22,7 +22,6 @@ const AddButton = () => {
22
22
  const { t } = useTranslation('file');
23
23
  const pushDockFileList = useFileStore((s) => s.pushDockFileList);
24
24
  const uploadFolderWithStructure = useFileStore((s) => s.uploadFolderWithStructure);
25
- const createResource = useFileStore((s) => s.createResource);
26
25
  const createResourceAndSync = useFileStore((s) => s.createResourceAndSync);
27
26
 
28
27
  // TODO: Migrate Notion import to use createResource
@@ -39,9 +38,9 @@ const AddButton = () => {
39
38
  ]);
40
39
 
41
40
  const handleOpenPageEditor = useCallback(async () => {
42
- // Create a new page with optimistic update - instant UI feedback
41
+ // Create a new page and wait for server sync - ensures page editor can load the document
43
42
  const untitledTitle = t('pageList.untitled');
44
- const tempId = await createResource({
43
+ const realId = await createResourceAndSync({
45
44
  content: '',
46
45
  fileType: 'custom/document',
47
46
  knowledgeBaseId: libraryId,
@@ -50,10 +49,10 @@ const AddButton = () => {
50
49
  title: untitledTitle,
51
50
  });
52
51
 
53
- // Switch to page view mode immediately (temp ID works)
54
- setCurrentViewItemId(tempId);
52
+ // Switch to page view mode with real ID
53
+ setCurrentViewItemId(realId);
55
54
  setMode('page');
56
- }, [createResource, currentFolderId, libraryId, setCurrentViewItemId, setMode, t]);
55
+ }, [createResourceAndSync, currentFolderId, libraryId, setCurrentViewItemId, setMode, t]);
57
56
 
58
57
  const handleCreateFolder = useCallback(async () => {
59
58
  // Create folder and wait for sync to complete before triggering rename
@@ -0,0 +1,382 @@
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 { 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, useRef, useState } from 'react';
10
+ import { useNavigate } from 'react-router-dom';
11
+
12
+ import {
13
+ getTransparentDragImage,
14
+ useDragActive,
15
+ useDragState,
16
+ } from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
17
+ import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
18
+ import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
19
+ import FileIcon from '@/components/FileIcon';
20
+ import { useFileStore } from '@/store/file';
21
+
22
+ import { useFileItemDropdown } from '../Explorer/ItemDropdown/useFileItemDropdown';
23
+ import { styles } from './styles';
24
+ import { clearTreeFolderCache } from './treeState';
25
+ import type { TreeItem } from './types';
26
+
27
+ interface HierarchyNodeProps {
28
+ expandedFolders: Set<string>;
29
+ folderChildrenCache: Map<string, TreeItem[]>;
30
+ item: TreeItem;
31
+ level?: number;
32
+ loadingFolders: Set<string>;
33
+ onLoadFolder: (_: string) => Promise<void>;
34
+ onToggleFolder: (_: string) => void;
35
+ selectedKey: string | null;
36
+ updateKey?: number;
37
+ }
38
+
39
+ // Row component for folder / file tree (virtualized by flattening visible nodes)
40
+ export const HierarchyNode = memo<HierarchyNodeProps>(
41
+ ({
42
+ item,
43
+ level = 0,
44
+ expandedFolders,
45
+ loadingFolders,
46
+ onToggleFolder,
47
+ onLoadFolder,
48
+ selectedKey,
49
+ folderChildrenCache,
50
+ }) => {
51
+ const navigate = useNavigate();
52
+ const { currentFolderSlug } = useFolderPath();
53
+ const { message } = App.useApp();
54
+
55
+ const [setMode, setCurrentViewItemId, libraryId] = useResourceManagerStore((s) => [
56
+ s.setMode,
57
+ s.setCurrentViewItemId,
58
+ s.libraryId,
59
+ ]);
60
+
61
+ const renameFolder = useFileStore((s) => s.renameFolder);
62
+
63
+ const [isRenaming, setIsRenaming] = useState(false);
64
+ const [renamingValue, setRenamingValue] = useState(item.name);
65
+ const inputRef = useRef<any>(null);
66
+
67
+ // Memoize computed values that don't change frequently
68
+ const { itemKey } = useMemo(
69
+ () => ({
70
+ itemKey: item.slug || item.id,
71
+ }),
72
+ [item.slug, item.id],
73
+ );
74
+
75
+ const handleRenameStart = useCallback(() => {
76
+ setIsRenaming(true);
77
+ setRenamingValue(item.name);
78
+ // Focus input after render
79
+ setTimeout(() => {
80
+ inputRef.current?.focus();
81
+ inputRef.current?.select();
82
+ }, 0);
83
+ }, [item.name]);
84
+
85
+ const handleRenameConfirm = useCallback(async () => {
86
+ if (!renamingValue.trim()) {
87
+ message.error('Folder name cannot be empty');
88
+ return;
89
+ }
90
+
91
+ if (renamingValue.trim() === item.name) {
92
+ setIsRenaming(false);
93
+ return;
94
+ }
95
+
96
+ try {
97
+ await renameFolder(item.id, renamingValue.trim());
98
+ if (libraryId) {
99
+ await clearTreeFolderCache(libraryId);
100
+ }
101
+ message.success('Renamed successfully');
102
+ setIsRenaming(false);
103
+ } catch (error) {
104
+ console.error('Rename error:', error);
105
+ message.error('Rename failed');
106
+ }
107
+ }, [item.id, item.name, libraryId, renamingValue, renameFolder, message]);
108
+
109
+ const handleRenameCancel = useCallback(() => {
110
+ setIsRenaming(false);
111
+ setRenamingValue(item.name);
112
+ }, [item.name]);
113
+
114
+ const { menuItems } = useFileItemDropdown({
115
+ fileType: item.fileType,
116
+ filename: item.name,
117
+ id: item.id,
118
+ libraryId,
119
+ onRenameStart: item.isFolder ? handleRenameStart : undefined,
120
+ sourceType: item.sourceType,
121
+ url: item.url,
122
+ });
123
+
124
+ const isDragActive = useDragActive();
125
+ const { setCurrentDrag } = useDragState();
126
+ const [isDragging, setIsDragging] = useState(false);
127
+ const [isOver, setIsOver] = useState(false);
128
+
129
+ // Memoize drag data to prevent recreation
130
+ const dragData = useMemo(
131
+ () => ({
132
+ fileType: item.fileType,
133
+ isFolder: item.isFolder,
134
+ name: item.name,
135
+ sourceType: item.sourceType,
136
+ }),
137
+ [item.fileType, item.isFolder, item.name, item.sourceType],
138
+ );
139
+
140
+ // Native HTML5 drag event handlers
141
+ const handleDragStart = useCallback(
142
+ (e: React.DragEvent<HTMLDivElement>) => {
143
+ setIsDragging(true);
144
+ setCurrentDrag({
145
+ data: dragData,
146
+ id: item.id,
147
+ type: item.isFolder ? 'folder' : 'file',
148
+ });
149
+
150
+ // Set drag image to be transparent (we use custom overlay)
151
+ const img = getTransparentDragImage();
152
+ if (img && e.dataTransfer) {
153
+ e.dataTransfer.setDragImage(img, 0, 0);
154
+ }
155
+ if (e.dataTransfer) {
156
+ e.dataTransfer.effectAllowed = 'move';
157
+ }
158
+ },
159
+ [dragData, item.id, item.isFolder, setCurrentDrag],
160
+ );
161
+
162
+ const handleDragEnd = useCallback(() => {
163
+ setIsDragging(false);
164
+ }, []);
165
+
166
+ const handleDragOver = useCallback(
167
+ (e: React.DragEvent<HTMLDivElement>) => {
168
+ if (!item.isFolder || !isDragActive) return;
169
+
170
+ e.preventDefault();
171
+ e.stopPropagation();
172
+ setIsOver(true);
173
+ },
174
+ [item.isFolder, isDragActive],
175
+ );
176
+
177
+ const handleDragLeave = useCallback(() => {
178
+ setIsOver(false);
179
+ }, []);
180
+
181
+ const handleDrop = useCallback(() => {
182
+ // Clear the highlight after drop
183
+ setIsOver(false);
184
+ }, []);
185
+
186
+ const handleItemClick = useCallback(() => {
187
+ // Open file modal using slug-based routing
188
+ const currentPath = currentFolderSlug
189
+ ? `/resource/library/${libraryId}/${currentFolderSlug}`
190
+ : `/resource/library/${libraryId}`;
191
+
192
+ setCurrentViewItemId(itemKey);
193
+ navigate(`${currentPath}?file=${itemKey}`);
194
+
195
+ if (itemKey.startsWith('doc')) {
196
+ setMode('page');
197
+ } else {
198
+ // Set mode to 'file' immediately to prevent flickering to list view
199
+ setMode('editor');
200
+ }
201
+ }, [itemKey, currentFolderSlug, libraryId, navigate, setMode, setCurrentViewItemId]);
202
+
203
+ const handleFolderClick = useCallback(
204
+ (folderId: string, folderSlug?: string | null) => {
205
+ const navKey = folderSlug || folderId;
206
+ navigate(`/resource/library/${libraryId}/${navKey}`);
207
+
208
+ setMode('explorer');
209
+ },
210
+ [libraryId, navigate],
211
+ );
212
+
213
+ if (item.isFolder) {
214
+ const isExpanded = expandedFolders.has(itemKey);
215
+ const isActive = selectedKey === itemKey;
216
+ const isLoading = loadingFolders.has(itemKey);
217
+
218
+ const handleToggle = async () => {
219
+ // Toggle folder expansion
220
+ onToggleFolder(itemKey);
221
+
222
+ // Only load if not already cached
223
+ if (!isExpanded && !folderChildrenCache.has(itemKey)) {
224
+ await onLoadFolder(itemKey);
225
+ }
226
+ };
227
+
228
+ return (
229
+ <Flexbox gap={2}>
230
+ <Block
231
+ align={'center'}
232
+ className={cx(
233
+ styles.treeItem,
234
+ isOver && styles.fileItemDragOver,
235
+ isDragging && styles.dragging,
236
+ )}
237
+ clickable
238
+ data-drop-target-id={item.id}
239
+ data-is-folder={String(item.isFolder)}
240
+ draggable
241
+ gap={8}
242
+ height={36}
243
+ horizontal
244
+ onClick={() => handleFolderClick(item.id, item.slug)}
245
+ onContextMenu={(e) => {
246
+ e.preventDefault();
247
+ showContextMenu(menuItems());
248
+ }}
249
+ onDragEnd={handleDragEnd}
250
+ onDragLeave={handleDragLeave}
251
+ onDragOver={handleDragOver}
252
+ onDragStart={handleDragStart}
253
+ onDrop={handleDrop}
254
+ paddingInline={4}
255
+ style={{
256
+ paddingInlineStart: level * 12 + 4,
257
+ }}
258
+ variant={isActive ? 'filled' : 'borderless'}
259
+ >
260
+ {isLoading ? (
261
+ <ActionIcon icon={LoadingOutlined as any} size={'small'} spin style={{ width: 20 }} />
262
+ ) : (
263
+ <motion.div
264
+ animate={{ rotate: isExpanded ? 0 : -90 }}
265
+ initial={false}
266
+ transition={{ duration: 0.2, ease: 'easeInOut' }}
267
+ >
268
+ <ActionIcon
269
+ icon={CaretDownFilled as any}
270
+ onClick={(e) => {
271
+ e.stopPropagation();
272
+ handleToggle();
273
+ }}
274
+ size={'small'}
275
+ style={{ width: 20 }}
276
+ />
277
+ </motion.div>
278
+ )}
279
+ <Flexbox
280
+ align={'center'}
281
+ flex={1}
282
+ gap={8}
283
+ horizontal
284
+ style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
285
+ >
286
+ <Icon icon={isExpanded ? FolderOpenIcon : FolderIcon} size={18} />
287
+ {isRenaming ? (
288
+ <Input
289
+ onBlur={handleRenameConfirm}
290
+ onChange={(e) => setRenamingValue(e.target.value)}
291
+ onClick={(e) => e.stopPropagation()}
292
+ onKeyDown={(e) => {
293
+ if (e.key === 'Enter') {
294
+ e.preventDefault();
295
+ handleRenameConfirm();
296
+ } else if (e.key === 'Escape') {
297
+ e.preventDefault();
298
+ handleRenameCancel();
299
+ }
300
+ }}
301
+ onPointerDown={(e) => e.stopPropagation()}
302
+ ref={inputRef}
303
+ size="small"
304
+ style={{ flex: 1 }}
305
+ value={renamingValue}
306
+ />
307
+ ) : (
308
+ <span
309
+ style={{
310
+ flex: 1,
311
+ overflow: 'hidden',
312
+ textOverflow: 'ellipsis',
313
+ whiteSpace: 'nowrap',
314
+ }}
315
+ >
316
+ {item.name}
317
+ </span>
318
+ )}
319
+ </Flexbox>
320
+ </Block>
321
+ </Flexbox>
322
+ );
323
+ }
324
+
325
+ // Render as file
326
+ const isActive = selectedKey === itemKey;
327
+ return (
328
+ <Flexbox gap={2}>
329
+ <Block
330
+ align={'center'}
331
+ className={cx(styles.treeItem, isDragging && styles.dragging)}
332
+ clickable
333
+ data-drop-target-id={item.id}
334
+ data-is-folder={false}
335
+ draggable
336
+ gap={8}
337
+ height={36}
338
+ horizontal
339
+ onClick={handleItemClick}
340
+ onContextMenu={(e) => {
341
+ e.preventDefault();
342
+ showContextMenu(menuItems());
343
+ }}
344
+ onDragEnd={handleDragEnd}
345
+ onDragStart={handleDragStart}
346
+ paddingInline={4}
347
+ style={{
348
+ paddingInlineStart: level * 12 + 4,
349
+ }}
350
+ variant={isActive ? 'filled' : 'borderless'}
351
+ >
352
+ <div style={{ width: 20 }} />
353
+ <Flexbox
354
+ align={'center'}
355
+ flex={1}
356
+ gap={8}
357
+ horizontal
358
+ style={{ minHeight: 28, minWidth: 0, overflow: 'hidden' }}
359
+ >
360
+ {item.sourceType === 'document' ? (
361
+ <Icon icon={FileText} size={18} />
362
+ ) : (
363
+ <FileIcon fileName={item.name} fileType={item.fileType} size={18} />
364
+ )}
365
+ <span
366
+ style={{
367
+ flex: 1,
368
+ overflow: 'hidden',
369
+ textOverflow: 'ellipsis',
370
+ whiteSpace: 'nowrap',
371
+ }}
372
+ >
373
+ {item.name}
374
+ </span>
375
+ </Flexbox>
376
+ </Block>
377
+ </Flexbox>
378
+ );
379
+ },
380
+ );
381
+
382
+ HierarchyNode.displayName = 'HierarchyNode';