@lobehub/lobehub 2.0.0-next.249 → 2.0.0-next.250
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/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/locales/en-US/file.json +2 -0
- package/locales/zh-CN/discover.json +3 -0
- package/locales/zh-CN/file.json +2 -0
- package/package.json +3 -3
- package/packages/types/package.json +2 -2
- package/packages/types/src/discover/mcp.ts +3 -1
- package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -0
- package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
- package/src/app/[variants]/(main)/community/(list)/mcp/features/Category/index.tsx +7 -3
- package/src/app/[variants]/(main)/community/(list)/mcp/index.tsx +2 -2
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Editing.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/index.tsx +1 -1
- package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Editing.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/index.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/useDropdownMenu.tsx +1 -1
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/index.tsx +16 -6
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/{KnowledgeBase.tsx → index.tsx} +2 -3
- package/src/app/[variants]/(main)/resource/(home)/_layout/Sidebar.tsx +2 -2
- package/src/app/[variants]/(main)/resource/(home)/index.tsx +23 -10
- package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +12 -37
- package/src/app/[variants]/(main)/resource/features/hooks/useKnowledgeItem.ts +1 -1
- package/src/app/[variants]/(main)/resource/features/store/action.ts +9 -39
- package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +1 -1
- package/src/app/[variants]/(main)/resource/library/index.tsx +13 -6
- package/src/features/LibraryModal/AddFilesToKnowledgeBase/SelectForm.tsx +1 -1
- package/src/features/LibraryModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +1 -1
- package/src/features/PageEditor/store/action.ts +5 -2
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +5 -7
- package/src/features/ResourceManager/components/Explorer/Header/Breadcrumb.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +57 -26
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +35 -6
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +20 -14
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +41 -31
- package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/MasonryView/Skeleton.tsx +6 -2
- package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +29 -18
- package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +7 -34
- package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +1 -1
- package/src/features/ResourceManager/components/Explorer/index.tsx +58 -18
- package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +6 -4
- package/src/features/ResourceManager/components/Header/AddButton.tsx +58 -35
- package/src/features/ResourceManager/components/Header/hooks/useNotionImport.ts +5 -5
- package/src/features/ResourceManager/components/Tree/TreeSkeleton.tsx +19 -9
- package/src/features/ResourceManager/components/Tree/index.tsx +110 -5
- package/src/features/ResourceManager/components/UploadDock/index.tsx +2 -1
- package/src/features/ResourceManager/constants.ts +3 -0
- package/src/hooks/useMCPCategory.tsx +7 -0
- package/src/locales/default/discover.ts +3 -0
- package/src/locales/default/file.ts +2 -0
- package/src/services/file/index.ts +34 -1
- package/src/services/resource/index.ts +249 -0
- package/src/store/discover/slices/mcp/action.ts +1 -1
- package/src/store/file/slices/chat/action.ts +2 -1
- package/src/store/file/slices/document/action.ts +10 -7
- package/src/store/file/slices/fileManager/action.ts +14 -4
- package/src/store/file/slices/fileManager/initialState.ts +2 -0
- package/src/store/file/slices/resource/action.ts +432 -0
- package/src/store/file/slices/resource/hooks.ts +82 -0
- package/src/store/file/slices/resource/initialState.ts +67 -0
- package/src/store/file/slices/resource/syncEngine.ts +326 -0
- package/src/store/file/store.ts +6 -1
- package/src/store/{knowledgeBase → library}/initialState.ts +2 -2
- package/src/store/{knowledgeBase → library}/slices/content/action.test.ts +37 -51
- package/src/store/{knowledgeBase → library}/slices/content/action.ts +8 -4
- package/src/store/{knowledgeBase → library}/slices/crud/action.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/crud/selectors.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/dataset.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/evaluation.ts +1 -1
- package/src/store/{knowledgeBase → library}/slices/ragEval/actions/index.ts +1 -1
- package/src/types/resource.ts +133 -0
- package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/index.tsx +0 -25
- /package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Actions.tsx +0 -0
- /package/src/store/{knowledgeBase → library}/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/selectors.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/content/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/action.test.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/crud/initialState.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/ragEval/index.ts +0 -0
- /package/src/store/{knowledgeBase → library}/slices/ragEval/initialState.ts +0 -0
- /package/src/store/{knowledgeBase → library}/store.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { memo, useEffect } from 'react';
|
|
4
|
-
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { memo, useEffect, useLayoutEffect } from 'react';
|
|
4
|
+
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
|
5
5
|
|
|
6
6
|
import Container from '@/app/[variants]/(main)/resource/library/features/Container';
|
|
7
7
|
import NProgress from '@/components/NProgress';
|
|
@@ -14,6 +14,7 @@ import { useResourceManagerStore } from '../features/store';
|
|
|
14
14
|
const MainContent = memo(() => {
|
|
15
15
|
const { id: knowledgeBaseId } = useParams<{ id: string }>();
|
|
16
16
|
const [searchParams] = useSearchParams();
|
|
17
|
+
const location = useLocation();
|
|
17
18
|
const [setMode, setCurrentViewItemId, setLibraryId] = useResourceManagerStore((s) => [
|
|
18
19
|
s.setMode,
|
|
19
20
|
s.setCurrentViewItemId,
|
|
@@ -30,10 +31,16 @@ const MainContent = memo(() => {
|
|
|
30
31
|
// Load knowledge base data
|
|
31
32
|
useKnowledgeBaseItem(knowledgeBaseId || '');
|
|
32
33
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
// Sync libraryId from URL params using useLayoutEffect
|
|
35
|
+
// useLayoutEffect runs synchronously before browser paint, ensuring state is set
|
|
36
|
+
// before Explorer component renders and computes query parameters
|
|
37
|
+
// IMPORTANT: Only depend on knowledgeBaseId and location.pathname, NOT currentLibraryId to avoid feedback loop
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const isOnLibraryRoute = location.pathname.includes('/library/');
|
|
40
|
+
if (isOnLibraryRoute) {
|
|
41
|
+
setLibraryId(knowledgeBaseId);
|
|
42
|
+
}
|
|
43
|
+
}, [knowledgeBaseId, setLibraryId, location.pathname]);
|
|
37
44
|
|
|
38
45
|
// Sync file view mode from URL
|
|
39
46
|
useEffect(() => {
|
|
@@ -5,7 +5,7 @@ import { memo, useState } from 'react';
|
|
|
5
5
|
import { Trans, useTranslation } from 'react-i18next';
|
|
6
6
|
|
|
7
7
|
import RepoIcon from '@/components/LibIcon';
|
|
8
|
-
import { useKnowledgeBaseStore } from '@/store/
|
|
8
|
+
import { useKnowledgeBaseStore } from '@/store/library';
|
|
9
9
|
|
|
10
10
|
interface CreateFormProps {
|
|
11
11
|
fileIds: string[];
|
|
@@ -2,7 +2,7 @@ import { Button, Form, Input, TextArea } from '@lobehub/ui';
|
|
|
2
2
|
import { memo, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
|
|
5
|
-
import { useKnowledgeBaseStore } from '@/store/
|
|
5
|
+
import { useKnowledgeBaseStore } from '@/store/library';
|
|
6
6
|
import { type CreateKnowledgeBaseParams } from '@/types/knowledgeBase';
|
|
7
7
|
|
|
8
8
|
interface CreateFormProps {
|
|
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
|
|
6
6
|
import { useFileStore } from '@/store/file';
|
|
7
|
-
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/
|
|
7
|
+
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/library';
|
|
8
8
|
|
|
9
9
|
import { usePageEditorStore } from '../store';
|
|
10
10
|
|
|
@@ -181,7 +181,7 @@ export const store: (initState?: Partial<State>) => StateCreator<Store> =
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
const { updateDocumentOptimistically, replaceTempDocumentWithReal
|
|
184
|
+
const { updateDocumentOptimistically, replaceTempDocumentWithReal } =
|
|
185
185
|
useFileStore.getState();
|
|
186
186
|
|
|
187
187
|
if (currentDocId && !currentDocId.startsWith('temp-document-')) {
|
|
@@ -241,7 +241,10 @@ export const store: (initState?: Partial<State>) => StateCreator<Store> =
|
|
|
241
241
|
|
|
242
242
|
set({ currentDocId: newPage.id });
|
|
243
243
|
onDocumentIdChange?.(newPage.id);
|
|
244
|
-
|
|
244
|
+
|
|
245
|
+
// Refetch resource list to show newly created page
|
|
246
|
+
const { revalidateResources } = await import('@/store/file/slices/resource/hooks');
|
|
247
|
+
await revalidateResources();
|
|
245
248
|
}
|
|
246
249
|
|
|
247
250
|
if (hadFocus) {
|
|
@@ -82,7 +82,6 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
|
|
|
82
82
|
createOptimisticDocument,
|
|
83
83
|
replaceTempDocumentWithReal,
|
|
84
84
|
setSelectedPageId,
|
|
85
|
-
refreshFileList,
|
|
86
85
|
fetchDocuments,
|
|
87
86
|
] = useFileStore((s) => [
|
|
88
87
|
s.createNewPage,
|
|
@@ -90,7 +89,6 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
|
|
|
90
89
|
s.createOptimisticDocument,
|
|
91
90
|
s.replaceTempDocumentWithReal,
|
|
92
91
|
s.setSelectedPageId,
|
|
93
|
-
s.refreshFileList,
|
|
94
92
|
s.fetchDocuments,
|
|
95
93
|
]);
|
|
96
94
|
|
|
@@ -98,7 +96,11 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
|
|
|
98
96
|
createDocument,
|
|
99
97
|
currentFolderId: null,
|
|
100
98
|
libraryId: knowledgeBaseId ?? null,
|
|
101
|
-
|
|
99
|
+
refetchResources: async () => {
|
|
100
|
+
const { revalidateResources } = await import('@/store/file/slices/resource/hooks');
|
|
101
|
+
await revalidateResources();
|
|
102
|
+
await fetchDocuments({ pageOnly: true });
|
|
103
|
+
},
|
|
102
104
|
t,
|
|
103
105
|
});
|
|
104
106
|
|
|
@@ -107,10 +109,6 @@ const PageExplorerPlaceholder = memo<PageExplorerPlaceholderProps>(
|
|
|
107
109
|
event: React.ChangeEvent<HTMLInputElement>,
|
|
108
110
|
) => {
|
|
109
111
|
await notionImport.handleNotionImport(event);
|
|
110
|
-
// Fetch documents to update the UI immediately
|
|
111
|
-
// The hook calls refreshFileList which invalidates SWR cache,
|
|
112
|
-
// but we need to explicitly fetch to update the zustand store
|
|
113
|
-
await fetchDocuments({ pageOnly: true });
|
|
114
112
|
};
|
|
115
113
|
|
|
116
114
|
const handleCreateDocument = async (content: string, title: string) => {
|
|
@@ -7,7 +7,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
|
7
7
|
import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
|
|
8
8
|
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
|
|
9
9
|
import { useFileStore } from '@/store/file';
|
|
10
|
-
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/
|
|
10
|
+
import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/library';
|
|
11
11
|
import { FilesTabs } from '@/types/files';
|
|
12
12
|
|
|
13
13
|
const styles = createStaticStyles(({ css, cssVar }) => ({
|
package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx
CHANGED
|
@@ -12,11 +12,14 @@ import {
|
|
|
12
12
|
} from 'lucide-react';
|
|
13
13
|
import { useCallback } from 'react';
|
|
14
14
|
import { useTranslation } from 'react-i18next';
|
|
15
|
+
import { shallow } from 'zustand/shallow';
|
|
15
16
|
|
|
16
17
|
import RepoIcon from '@/components/LibIcon';
|
|
18
|
+
import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
|
|
19
|
+
import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
|
|
17
20
|
import { documentService } from '@/services/document';
|
|
18
21
|
import { useFileStore } from '@/store/file';
|
|
19
|
-
import { useKnowledgeBaseStore } from '@/store/
|
|
22
|
+
import { useKnowledgeBaseStore } from '@/store/library';
|
|
20
23
|
import { downloadFile } from '@/utils/client/downloadFile';
|
|
21
24
|
|
|
22
25
|
import MoveToFolderModal from '../MoveToFolderModal';
|
|
@@ -26,7 +29,7 @@ interface UseFileItemDropdownParams {
|
|
|
26
29
|
fileType: string;
|
|
27
30
|
filename: string;
|
|
28
31
|
id: string;
|
|
29
|
-
|
|
32
|
+
libraryId?: string;
|
|
30
33
|
onRenameStart?: () => void;
|
|
31
34
|
sourceType?: string;
|
|
32
35
|
url: string;
|
|
@@ -36,9 +39,12 @@ interface UseFileItemDropdownReturn {
|
|
|
36
39
|
menuItems: () => ItemType[];
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Shared with folder tree and explorer
|
|
44
|
+
*/
|
|
39
45
|
export const useFileItemDropdown = ({
|
|
40
46
|
id,
|
|
41
|
-
|
|
47
|
+
libraryId,
|
|
42
48
|
url,
|
|
43
49
|
filename,
|
|
44
50
|
fileType,
|
|
@@ -48,7 +54,13 @@ export const useFileItemDropdown = ({
|
|
|
48
54
|
const { t } = useTranslation(['components', 'common', 'knowledgeBase']);
|
|
49
55
|
const { message, modal } = App.useApp();
|
|
50
56
|
|
|
51
|
-
const
|
|
57
|
+
const { deleteResource, refreshFileList } = useFileStore(
|
|
58
|
+
(s) => ({
|
|
59
|
+
deleteResource: s.deleteResource,
|
|
60
|
+
refreshFileList: s.refreshFileList,
|
|
61
|
+
}),
|
|
62
|
+
shallow,
|
|
63
|
+
);
|
|
52
64
|
const [removeFilesFromKnowledgeBase, addFilesToKnowledgeBase, useFetchKnowledgeBaseList] =
|
|
53
65
|
useKnowledgeBaseStore((s) => [
|
|
54
66
|
s.removeFilesFromKnowledgeBase,
|
|
@@ -59,17 +71,15 @@ export const useFileItemDropdown = ({
|
|
|
59
71
|
// Fetch knowledge bases - SWR caches this across all dropdown instances
|
|
60
72
|
// Only the first call fetches from server, subsequent calls use cache
|
|
61
73
|
// The expensive menu computation is deferred until dropdown opens (menuItems is a function)
|
|
62
|
-
const { data:
|
|
74
|
+
const { data: libraries } = useFetchKnowledgeBaseList();
|
|
63
75
|
|
|
64
|
-
const
|
|
76
|
+
const isInLibrary = !!libraryId;
|
|
65
77
|
const isFolder = fileType === 'custom/folder';
|
|
66
|
-
const isPage = sourceType === 'document' || fileType ===
|
|
78
|
+
const isPage = sourceType === 'document' || fileType === PAGE_FILE_TYPE;
|
|
67
79
|
|
|
68
80
|
const menuItems = useCallback(() => {
|
|
69
81
|
// Filter out current knowledge base and create submenu items
|
|
70
|
-
const availableKnowledgeBases = (
|
|
71
|
-
(kb) => kb.id !== knowledgeBaseId,
|
|
72
|
-
);
|
|
82
|
+
const availableKnowledgeBases = (libraries || []).filter((kb) => kb.id !== libraryId);
|
|
73
83
|
|
|
74
84
|
const addToKnowledgeBaseSubmenu: ItemType[] = availableKnowledgeBases.map((kb) => ({
|
|
75
85
|
icon: <RepoIcon />,
|
|
@@ -92,8 +102,8 @@ export const useFileItemDropdown = ({
|
|
|
92
102
|
},
|
|
93
103
|
}));
|
|
94
104
|
|
|
95
|
-
const
|
|
96
|
-
|
|
105
|
+
const libraryRelatedActions = (
|
|
106
|
+
isInLibrary
|
|
97
107
|
? [
|
|
98
108
|
availableKnowledgeBases.length > 0 && {
|
|
99
109
|
children: addToKnowledgeBaseSubmenu,
|
|
@@ -113,7 +123,7 @@ export const useFileItemDropdown = ({
|
|
|
113
123
|
danger: true,
|
|
114
124
|
},
|
|
115
125
|
onOk: async () => {
|
|
116
|
-
await removeFilesFromKnowledgeBase(
|
|
126
|
+
await removeFilesFromKnowledgeBase(libraryId, [id]);
|
|
117
127
|
|
|
118
128
|
message.success(t('FileManager.actions.removeFromKnowledgeBaseSuccess'));
|
|
119
129
|
},
|
|
@@ -134,15 +144,15 @@ export const useFileItemDropdown = ({
|
|
|
134
144
|
]
|
|
135
145
|
) as ItemType[];
|
|
136
146
|
|
|
137
|
-
const hasKnowledgeBaseActions =
|
|
147
|
+
const hasKnowledgeBaseActions = libraryRelatedActions.some(Boolean);
|
|
138
148
|
|
|
139
149
|
return (
|
|
140
150
|
[
|
|
141
|
-
...
|
|
151
|
+
...libraryRelatedActions,
|
|
142
152
|
hasKnowledgeBaseActions && {
|
|
143
153
|
type: 'divider',
|
|
144
154
|
},
|
|
145
|
-
|
|
155
|
+
isInLibrary && {
|
|
146
156
|
icon: <Icon icon={FolderInputIcon} />,
|
|
147
157
|
key: 'moveToFolder',
|
|
148
158
|
label: t('FileManager.actions.moveToFolder'),
|
|
@@ -151,8 +161,7 @@ export const useFileItemDropdown = ({
|
|
|
151
161
|
|
|
152
162
|
createRawModal(MoveToFolderModal, {
|
|
153
163
|
fileId: id,
|
|
154
|
-
|
|
155
|
-
knowledgeBaseId,
|
|
164
|
+
knowledgeBaseId: libraryId,
|
|
156
165
|
});
|
|
157
166
|
},
|
|
158
167
|
},
|
|
@@ -176,8 +185,8 @@ export const useFileItemDropdown = ({
|
|
|
176
185
|
let urlToCopy = url;
|
|
177
186
|
if (isPage) {
|
|
178
187
|
const baseUrl = window.location.origin;
|
|
179
|
-
if (
|
|
180
|
-
urlToCopy = `${baseUrl}/resource/library/${
|
|
188
|
+
if (libraryId) {
|
|
189
|
+
urlToCopy = `${baseUrl}/resource/library/${libraryId}?file=${id}`;
|
|
181
190
|
} else {
|
|
182
191
|
urlToCopy = `${baseUrl}/resource?file=${id}`;
|
|
183
192
|
}
|
|
@@ -249,19 +258,41 @@ export const useFileItemDropdown = ({
|
|
|
249
258
|
: t('FileManager.actions.confirmDelete'),
|
|
250
259
|
okButtonProps: { danger: true },
|
|
251
260
|
onOk: async () => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
261
|
+
// Use optimistic delete - instant UI update, sync in background
|
|
262
|
+
await deleteResource(id);
|
|
263
|
+
|
|
264
|
+
// Ensure tree caches stay in sync with explorer
|
|
265
|
+
if (libraryId) {
|
|
266
|
+
await clearTreeFolderCache(libraryId);
|
|
257
267
|
}
|
|
268
|
+
await refreshFileList();
|
|
269
|
+
|
|
270
|
+
message.success(t('FileManager.actions.deleteSuccess'));
|
|
258
271
|
},
|
|
259
272
|
});
|
|
260
273
|
},
|
|
261
274
|
},
|
|
262
275
|
] as ItemType[]
|
|
263
276
|
).filter(Boolean);
|
|
264
|
-
}, [
|
|
277
|
+
}, [
|
|
278
|
+
addFilesToKnowledgeBase,
|
|
279
|
+
clearTreeFolderCache,
|
|
280
|
+
deleteResource,
|
|
281
|
+
filename,
|
|
282
|
+
id,
|
|
283
|
+
isFolder,
|
|
284
|
+
isInLibrary,
|
|
285
|
+
isPage,
|
|
286
|
+
libraries,
|
|
287
|
+
libraryId,
|
|
288
|
+
message,
|
|
289
|
+
modal,
|
|
290
|
+
onRenameStart,
|
|
291
|
+
refreshFileList,
|
|
292
|
+
removeFilesFromKnowledgeBase,
|
|
293
|
+
t,
|
|
294
|
+
url,
|
|
295
|
+
]);
|
|
265
296
|
|
|
266
297
|
return { menuItems };
|
|
267
298
|
};
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
} from '@/app/[variants]/(main)/resource/features/DndContextWrapper';
|
|
18
18
|
import { useResourceManagerStore } from '@/app/[variants]/(main)/resource/features/store';
|
|
19
19
|
import FileIcon from '@/components/FileIcon';
|
|
20
|
+
import { clearTreeFolderCache } from '@/features/ResourceManager/components/Tree';
|
|
21
|
+
import { PAGE_FILE_TYPE } from '@/features/ResourceManager/constants';
|
|
20
22
|
import { fileManagerSelectors, useFileStore } from '@/store/file';
|
|
21
23
|
import { type FileListItem as FileListItemType } from '@/types/files';
|
|
22
24
|
import { formatSize } from '@/utils/format';
|
|
@@ -142,7 +144,8 @@ const FileListItem = memo<FileListItemProps>(
|
|
|
142
144
|
(s) => ({
|
|
143
145
|
isCreatingFileParseTask: fileManagerSelectors.isCreatingFileParseTask(id)(s),
|
|
144
146
|
parseFiles: s.parseFilesToChunks,
|
|
145
|
-
|
|
147
|
+
refreshFileList: s.refreshFileList,
|
|
148
|
+
updateResource: s.updateResource,
|
|
146
149
|
}),
|
|
147
150
|
shallow,
|
|
148
151
|
);
|
|
@@ -161,6 +164,7 @@ const FileListItem = memo<FileListItemProps>(
|
|
|
161
164
|
const [isRenaming, setIsRenaming] = useState(false);
|
|
162
165
|
const [renamingValue, setRenamingValue] = useState(name);
|
|
163
166
|
const inputRef = useRef<any>(null);
|
|
167
|
+
const isConfirmingRef = useRef(false);
|
|
164
168
|
const isDragActive = useDragActive();
|
|
165
169
|
const { setCurrentDrag } = useDragState();
|
|
166
170
|
const [isDragging, setIsDragging] = useState(false);
|
|
@@ -170,10 +174,10 @@ const FileListItem = memo<FileListItemProps>(
|
|
|
170
174
|
const computedValues = useMemo(() => {
|
|
171
175
|
const isPDF = fileType?.toLowerCase() === 'pdf' || name?.toLowerCase().endsWith('.pdf');
|
|
172
176
|
return {
|
|
173
|
-
emoji: sourceType === 'document' || fileType ===
|
|
177
|
+
emoji: sourceType === 'document' || fileType === PAGE_FILE_TYPE ? metadata?.emoji : null,
|
|
174
178
|
isFolder: fileType === 'custom/folder',
|
|
175
179
|
// PDF files should not be treated as pages, even if they have sourceType='document'
|
|
176
|
-
isPage: !isPDF && (sourceType === 'document' || fileType ===
|
|
180
|
+
isPage: !isPDF && (sourceType === 'document' || fileType === PAGE_FILE_TYPE),
|
|
177
181
|
isSupportedForChunking: !isChunkingUnsupported(fileType),
|
|
178
182
|
};
|
|
179
183
|
}, [fileType, sourceType, metadata?.emoji, name]);
|
|
@@ -260,27 +264,52 @@ const FileListItem = memo<FileListItemProps>(
|
|
|
260
264
|
}, [name]);
|
|
261
265
|
|
|
262
266
|
const handleRenameConfirm = useCallback(async () => {
|
|
267
|
+
// Prevent duplicate calls (e.g., from both Enter key and onBlur)
|
|
268
|
+
if (isConfirmingRef.current) return;
|
|
269
|
+
isConfirmingRef.current = true;
|
|
270
|
+
|
|
263
271
|
if (!renamingValue.trim()) {
|
|
264
272
|
message.error(t('FileManager.actions.renameError'));
|
|
273
|
+
isConfirmingRef.current = false;
|
|
265
274
|
return;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
277
|
if (renamingValue.trim() === name) {
|
|
269
278
|
setIsRenaming(false);
|
|
279
|
+
isConfirmingRef.current = false;
|
|
270
280
|
return;
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
try {
|
|
274
|
-
|
|
284
|
+
// Use optimistic updateResource for instant UI update
|
|
285
|
+
await fileStoreState.updateResource(id, { name: renamingValue.trim() });
|
|
286
|
+
if (resourceManagerState.libraryId) {
|
|
287
|
+
await clearTreeFolderCache(resourceManagerState.libraryId);
|
|
288
|
+
}
|
|
289
|
+
await fileStoreState.refreshFileList();
|
|
290
|
+
|
|
275
291
|
message.success(t('FileManager.actions.renameSuccess'));
|
|
276
292
|
setIsRenaming(false);
|
|
277
293
|
} catch (error) {
|
|
278
294
|
console.error('Rename error:', error);
|
|
279
295
|
message.error(t('FileManager.actions.renameError'));
|
|
296
|
+
} finally {
|
|
297
|
+
isConfirmingRef.current = false;
|
|
280
298
|
}
|
|
281
|
-
}, [
|
|
299
|
+
}, [
|
|
300
|
+
fileStoreState.refreshFileList,
|
|
301
|
+
fileStoreState.updateResource,
|
|
302
|
+
id,
|
|
303
|
+
message,
|
|
304
|
+
name,
|
|
305
|
+
renamingValue,
|
|
306
|
+
resourceManagerState.libraryId,
|
|
307
|
+
t,
|
|
308
|
+
]);
|
|
282
309
|
|
|
283
310
|
const handleRenameCancel = useCallback(() => {
|
|
311
|
+
// Don't cancel if we're in the middle of confirming
|
|
312
|
+
if (isConfirmingRef.current) return;
|
|
284
313
|
setIsRenaming(false);
|
|
285
314
|
setRenamingValue(name);
|
|
286
315
|
}, [name]);
|
|
@@ -334,7 +363,7 @@ const FileListItem = memo<FileListItemProps>(
|
|
|
334
363
|
fileType,
|
|
335
364
|
filename: name,
|
|
336
365
|
id,
|
|
337
|
-
|
|
366
|
+
libraryId: resourceManagerState.libraryId,
|
|
338
367
|
onRenameStart: isFolder ? handleRenameStart : undefined,
|
|
339
368
|
sourceType,
|
|
340
369
|
url,
|
|
@@ -14,20 +14,25 @@ interface ListViewSkeletonProps {
|
|
|
14
14
|
|
|
15
15
|
const ListViewSkeleton = ({
|
|
16
16
|
columnWidths = { date: FILE_DATE_WIDTH, name: 400, size: FILE_SIZE_WIDTH },
|
|
17
|
-
count =
|
|
18
|
-
}: ListViewSkeletonProps) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
count = 6,
|
|
18
|
+
}: ListViewSkeletonProps) => {
|
|
19
|
+
// Calculate opacity gradient from 100% to 20%
|
|
20
|
+
const getOpacity = (index: number) => 1 - (index / (count - 1)) * 0.8;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Flexbox>
|
|
24
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
25
|
+
<Flexbox
|
|
26
|
+
align={'center'}
|
|
27
|
+
height={48}
|
|
28
|
+
horizontal
|
|
29
|
+
key={index}
|
|
30
|
+
paddingInline={8}
|
|
31
|
+
style={{
|
|
32
|
+
borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
|
|
33
|
+
opacity: getOpacity(index),
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
31
36
|
<Center height={40} style={{ paddingInline: 4 }}>
|
|
32
37
|
<Checkbox disabled />
|
|
33
38
|
</Center>
|
|
@@ -55,5 +60,6 @@ const ListViewSkeleton = ({
|
|
|
55
60
|
))}
|
|
56
61
|
</Flexbox>
|
|
57
62
|
);
|
|
63
|
+
};
|
|
58
64
|
|
|
59
65
|
export default ListViewSkeleton;
|
|
@@ -11,12 +11,14 @@ import { useDragActive } from '@/app/[variants]/(main)/resource/features/DndCont
|
|
|
11
11
|
import { useFolderPath } from '@/app/[variants]/(main)/resource/features/hooks/useFolderPath';
|
|
12
12
|
import {
|
|
13
13
|
useResourceManagerFetchFolderBreadcrumb,
|
|
14
|
-
useResourceManagerFetchKnowledgeItems,
|
|
15
14
|
useResourceManagerStore,
|
|
16
15
|
} from '@/app/[variants]/(main)/resource/features/store';
|
|
17
16
|
import { sortFileList } from '@/app/[variants]/(main)/resource/features/store/selectors';
|
|
17
|
+
import { useFileStore } from '@/store/file';
|
|
18
18
|
import { useGlobalStore } from '@/store/global';
|
|
19
19
|
import { INITIAL_STATUS } from '@/store/global/initialState';
|
|
20
|
+
import { type AsyncTaskStatus } from '@/types/asyncTask';
|
|
21
|
+
import { type FileListItem as FileListItemType } from '@/types/files';
|
|
20
22
|
|
|
21
23
|
import ColumnResizeHandle from './ColumnResizeHandle';
|
|
22
24
|
import FileListItem from './ListItem';
|
|
@@ -52,11 +54,7 @@ const styles = createStaticStyles(({ css }) => ({
|
|
|
52
54
|
}));
|
|
53
55
|
|
|
54
56
|
const ListView = memo(() => {
|
|
55
|
-
// Access all state from Resource Manager store
|
|
56
57
|
const [
|
|
57
|
-
libraryId,
|
|
58
|
-
category,
|
|
59
|
-
searchQuery,
|
|
60
58
|
selectFileIds,
|
|
61
59
|
setSelectedFileIds,
|
|
62
60
|
pendingRenameItemId,
|
|
@@ -65,9 +63,6 @@ const ListView = memo(() => {
|
|
|
65
63
|
sorter,
|
|
66
64
|
sortType,
|
|
67
65
|
] = useResourceManagerStore((s) => [
|
|
68
|
-
s.libraryId,
|
|
69
|
-
s.category,
|
|
70
|
-
s.searchQuery,
|
|
71
66
|
s.selectedFileIds,
|
|
72
67
|
s.setSelectedFileIds,
|
|
73
68
|
s.pendingRenameItemId,
|
|
@@ -86,12 +81,12 @@ const ListView = memo(() => {
|
|
|
86
81
|
const { t } = useTranslation(['components', 'file']);
|
|
87
82
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
|
88
83
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
89
|
-
const [lastSelectedIndex, setLastSelectedIndex] = useState<number | null>(null);
|
|
90
84
|
const isDragActive = useDragActive();
|
|
91
85
|
const [isDropZoneActive, setIsDropZoneActive] = useState(false);
|
|
92
86
|
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
93
87
|
const autoScrollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
94
88
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
89
|
+
const lastSelectedIndexRef = useRef<number | null>(null);
|
|
95
90
|
|
|
96
91
|
const { currentFolderSlug } = useFolderPath();
|
|
97
92
|
const { data: folderBreadcrumb } = useResourceManagerFetchFolderBreadcrumb(currentFolderSlug);
|
|
@@ -99,28 +94,43 @@ const ListView = memo(() => {
|
|
|
99
94
|
// Get current folder ID - either from breadcrumb or null for root
|
|
100
95
|
const currentFolderId = folderBreadcrumb?.at(-1)?.id || null;
|
|
101
96
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
const resourceList = useFileStore((s) => s.resourceList);
|
|
98
|
+
|
|
99
|
+
// Map ResourceItem[] to FileListItem[] for compatibility
|
|
100
|
+
const rawData =
|
|
101
|
+
resourceList?.map<FileListItemType>((item) => ({
|
|
102
|
+
...item,
|
|
103
|
+
chunkCount: item.chunkCount ?? null,
|
|
104
|
+
chunkingError: item.chunkingError ?? null,
|
|
105
|
+
chunkingStatus: (item.chunkingStatus ?? null) as AsyncTaskStatus | null,
|
|
106
|
+
embeddingError: item.embeddingError ?? null,
|
|
107
|
+
embeddingStatus: (item.embeddingStatus ?? null) as AsyncTaskStatus | null,
|
|
108
|
+
finishEmbedding: item.finishEmbedding ?? false,
|
|
109
|
+
url: item.url ?? '',
|
|
110
|
+
})) ?? [];
|
|
109
111
|
|
|
110
112
|
// Sort data using current sort settings
|
|
111
|
-
const data = sortFileList(rawData, sorter, sortType);
|
|
113
|
+
const data = sortFileList(rawData, sorter, sortType) || [];
|
|
114
|
+
|
|
115
|
+
const dataRef = useRef<FileListItemType[]>(data);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
dataRef.current = data;
|
|
119
|
+
}, [data]);
|
|
112
120
|
|
|
113
121
|
// Handle selection change with shift-click support for range selection
|
|
114
122
|
const handleSelectionChange = useCallback(
|
|
115
123
|
(id: string, checked: boolean, shiftKey: boolean, clickedIndex: number) => {
|
|
116
124
|
// Always get the latest state from the store to avoid stale closure issues
|
|
117
125
|
const currentSelected = useResourceManagerStore.getState().selectedFileIds;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
126
|
+
const lastIndex = lastSelectedIndexRef.current;
|
|
127
|
+
const list = dataRef.current;
|
|
128
|
+
|
|
129
|
+
if (shiftKey && lastIndex !== null && list.length > 0) {
|
|
130
|
+
// Shift-click: select range from lastIndex to current index
|
|
131
|
+
const start = Math.min(lastIndex, clickedIndex);
|
|
132
|
+
const end = Math.max(lastIndex, clickedIndex);
|
|
133
|
+
const rangeIds = list
|
|
124
134
|
.slice(start, end + 1)
|
|
125
135
|
.filter(Boolean)
|
|
126
136
|
.map((item) => item.id);
|
|
@@ -137,14 +147,14 @@ const ListView = memo(() => {
|
|
|
137
147
|
setSelectedFileIds(currentSelected.filter((item) => item !== id));
|
|
138
148
|
}
|
|
139
149
|
}
|
|
140
|
-
|
|
150
|
+
lastSelectedIndexRef.current = clickedIndex;
|
|
141
151
|
},
|
|
142
|
-
[
|
|
152
|
+
[setSelectedFileIds],
|
|
143
153
|
);
|
|
144
154
|
|
|
145
155
|
// Clean up invalid selections when data changes
|
|
146
156
|
useEffect(() => {
|
|
147
|
-
if (
|
|
157
|
+
if (selectFileIds.length > 0) {
|
|
148
158
|
const validFileIds = new Set(data.map((item) => item?.id).filter(Boolean));
|
|
149
159
|
const filteredSelection = selectFileIds.filter((id) => validFileIds.has(id));
|
|
150
160
|
if (filteredSelection.length !== selectFileIds.length) {
|
|
@@ -156,13 +166,13 @@ const ListView = memo(() => {
|
|
|
156
166
|
// Reset last selected index when all selections are cleared
|
|
157
167
|
useEffect(() => {
|
|
158
168
|
if (selectFileIds.length === 0) {
|
|
159
|
-
|
|
169
|
+
lastSelectedIndexRef.current = null;
|
|
160
170
|
}
|
|
161
171
|
}, [selectFileIds.length]);
|
|
162
172
|
|
|
163
173
|
// Calculate select all checkbox state
|
|
164
174
|
const { allSelected, indeterminate } = useMemo(() => {
|
|
165
|
-
const fileCount = data
|
|
175
|
+
const fileCount = data.length;
|
|
166
176
|
const selectedCount = selectFileIds.length;
|
|
167
177
|
return {
|
|
168
178
|
allSelected: fileCount > 0 && selectedCount === fileCount,
|
|
@@ -175,7 +185,7 @@ const ListView = memo(() => {
|
|
|
175
185
|
if (allSelected) {
|
|
176
186
|
setSelectedFileIds([]);
|
|
177
187
|
} else {
|
|
178
|
-
setSelectedFileIds(data
|
|
188
|
+
setSelectedFileIds(data.map((item) => item.id));
|
|
179
189
|
}
|
|
180
190
|
};
|
|
181
191
|
|
|
@@ -303,7 +313,7 @@ const ListView = memo(() => {
|
|
|
303
313
|
flexShrink: 0,
|
|
304
314
|
maxWidth: columnWidths.name,
|
|
305
315
|
minWidth: columnWidths.name,
|
|
306
|
-
paddingInline:
|
|
316
|
+
paddingInline: 20,
|
|
307
317
|
paddingInlineEnd: 16,
|
|
308
318
|
position: 'relative',
|
|
309
319
|
width: columnWidths.name,
|
|
@@ -364,7 +374,7 @@ const ListView = memo(() => {
|
|
|
364
374
|
>
|
|
365
375
|
<Virtuoso
|
|
366
376
|
components={{ Footer }}
|
|
367
|
-
data={data
|
|
377
|
+
data={data}
|
|
368
378
|
defaultItemHeight={48}
|
|
369
379
|
endReached={handleEndReached}
|
|
370
380
|
increaseViewportBy={{ bottom: 800, top: 1200 }}
|