@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
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
54
|
-
setCurrentViewItemId(
|
|
52
|
+
// Switch to page view mode with real ID
|
|
53
|
+
setCurrentViewItemId(realId);
|
|
55
54
|
setMode('page');
|
|
56
|
-
}, [
|
|
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';
|