@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.
Files changed (87) hide show
  1. package/.github/workflows/release.yml +4 -0
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +5 -0
  4. package/locales/en-US/file.json +2 -0
  5. package/locales/zh-CN/discover.json +3 -0
  6. package/locales/zh-CN/file.json +2 -0
  7. package/package.json +3 -3
  8. package/packages/types/package.json +2 -2
  9. package/packages/types/src/discover/mcp.ts +3 -1
  10. package/src/app/[variants]/(main)/community/(list)/(home)/index.tsx +2 -0
  11. package/src/app/[variants]/(main)/community/(list)/features/SortButton/index.tsx +4 -0
  12. package/src/app/[variants]/(main)/community/(list)/mcp/features/Category/index.tsx +7 -3
  13. package/src/app/[variants]/(main)/community/(list)/mcp/index.tsx +2 -2
  14. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Editing.tsx +1 -1
  15. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/Item.tsx +1 -1
  16. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/index.tsx +1 -1
  17. package/src/app/[variants]/(main)/home/_layout/Body/Project/List/useDropdownMenu.tsx +1 -1
  18. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Editing.tsx +1 -1
  19. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/index.tsx +1 -1
  20. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/useDropdownMenu.tsx +1 -1
  21. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/index.tsx +16 -6
  22. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/{KnowledgeBase.tsx → index.tsx} +2 -3
  23. package/src/app/[variants]/(main)/resource/(home)/_layout/Sidebar.tsx +2 -2
  24. package/src/app/[variants]/(main)/resource/(home)/index.tsx +23 -10
  25. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +12 -37
  26. package/src/app/[variants]/(main)/resource/features/hooks/useKnowledgeItem.ts +1 -1
  27. package/src/app/[variants]/(main)/resource/features/store/action.ts +9 -39
  28. package/src/app/[variants]/(main)/resource/library/_layout/Header/LibraryHead.tsx +1 -1
  29. package/src/app/[variants]/(main)/resource/library/index.tsx +13 -6
  30. package/src/features/LibraryModal/AddFilesToKnowledgeBase/SelectForm.tsx +1 -1
  31. package/src/features/LibraryModal/CreateNew/CreateForm.tsx +1 -1
  32. package/src/features/PageEditor/Header/Breadcrumb.tsx +1 -1
  33. package/src/features/PageEditor/store/action.ts +5 -2
  34. package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +5 -7
  35. package/src/features/ResourceManager/components/Explorer/Header/Breadcrumb.tsx +1 -1
  36. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +57 -26
  37. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +35 -6
  38. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +20 -14
  39. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +41 -31
  40. package/src/features/ResourceManager/components/Explorer/MasonryView/MasonryFileItem/index.tsx +1 -1
  41. package/src/features/ResourceManager/components/Explorer/MasonryView/Skeleton.tsx +6 -2
  42. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +29 -18
  43. package/src/features/ResourceManager/components/Explorer/MoveToFolderModal.tsx +7 -34
  44. package/src/features/ResourceManager/components/Explorer/ToolBar/BatchActionsDropdown.tsx +1 -1
  45. package/src/features/ResourceManager/components/Explorer/index.tsx +58 -18
  46. package/src/features/ResourceManager/components/Explorer/useCheckTaskStatus.ts +6 -4
  47. package/src/features/ResourceManager/components/Header/AddButton.tsx +58 -35
  48. package/src/features/ResourceManager/components/Header/hooks/useNotionImport.ts +5 -5
  49. package/src/features/ResourceManager/components/Tree/TreeSkeleton.tsx +19 -9
  50. package/src/features/ResourceManager/components/Tree/index.tsx +110 -5
  51. package/src/features/ResourceManager/components/UploadDock/index.tsx +2 -1
  52. package/src/features/ResourceManager/constants.ts +3 -0
  53. package/src/hooks/useMCPCategory.tsx +7 -0
  54. package/src/locales/default/discover.ts +3 -0
  55. package/src/locales/default/file.ts +2 -0
  56. package/src/services/file/index.ts +34 -1
  57. package/src/services/resource/index.ts +249 -0
  58. package/src/store/discover/slices/mcp/action.ts +1 -1
  59. package/src/store/file/slices/chat/action.ts +2 -1
  60. package/src/store/file/slices/document/action.ts +10 -7
  61. package/src/store/file/slices/fileManager/action.ts +14 -4
  62. package/src/store/file/slices/fileManager/initialState.ts +2 -0
  63. package/src/store/file/slices/resource/action.ts +432 -0
  64. package/src/store/file/slices/resource/hooks.ts +82 -0
  65. package/src/store/file/slices/resource/initialState.ts +67 -0
  66. package/src/store/file/slices/resource/syncEngine.ts +326 -0
  67. package/src/store/file/store.ts +6 -1
  68. package/src/store/{knowledgeBase → library}/initialState.ts +2 -2
  69. package/src/store/{knowledgeBase → library}/slices/content/action.test.ts +37 -51
  70. package/src/store/{knowledgeBase → library}/slices/content/action.ts +8 -4
  71. package/src/store/{knowledgeBase → library}/slices/crud/action.ts +1 -1
  72. package/src/store/{knowledgeBase → library}/slices/crud/selectors.ts +1 -1
  73. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/dataset.ts +1 -1
  74. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/evaluation.ts +1 -1
  75. package/src/store/{knowledgeBase → library}/slices/ragEval/actions/index.ts +1 -1
  76. package/src/types/resource.ts +133 -0
  77. package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/List/index.tsx +0 -25
  78. /package/src/app/[variants]/(main)/resource/(home)/_layout/Body/LibraryList/{List/Item → Item}/Actions.tsx +0 -0
  79. /package/src/store/{knowledgeBase → library}/index.ts +0 -0
  80. /package/src/store/{knowledgeBase → library}/selectors.ts +0 -0
  81. /package/src/store/{knowledgeBase → library}/slices/content/index.ts +0 -0
  82. /package/src/store/{knowledgeBase → library}/slices/crud/action.test.ts +0 -0
  83. /package/src/store/{knowledgeBase → library}/slices/crud/index.ts +0 -0
  84. /package/src/store/{knowledgeBase → library}/slices/crud/initialState.ts +0 -0
  85. /package/src/store/{knowledgeBase → library}/slices/ragEval/index.ts +0 -0
  86. /package/src/store/{knowledgeBase → library}/slices/ragEval/initialState.ts +0 -0
  87. /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
- // Set libraryId from URL params (only when knowledgeBaseId changes)
34
- useEffect(() => {
35
- setLibraryId(knowledgeBaseId);
36
- }, [knowledgeBaseId, setLibraryId]);
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/knowledgeBase';
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/knowledgeBase';
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/knowledgeBase';
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, refreshFileList } =
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
- refreshFileList();
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
- refreshFileList,
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/knowledgeBase';
10
+ import { knowledgeBaseSelectors, useKnowledgeBaseStore } from '@/store/library';
11
11
  import { FilesTabs } from '@/types/files';
12
12
 
13
13
  const styles = createStaticStyles(({ css, cssVar }) => ({
@@ -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/knowledgeBase';
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
- knowledgeBaseId?: string;
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
- knowledgeBaseId,
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 [removeFile, refreshFileList] = useFileStore((s) => [s.removeFileItem, s.refreshFileList]);
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: knowledgeBases } = useFetchKnowledgeBaseList();
74
+ const { data: libraries } = useFetchKnowledgeBaseList();
63
75
 
64
- const inKnowledgeBase = !!knowledgeBaseId;
76
+ const isInLibrary = !!libraryId;
65
77
  const isFolder = fileType === 'custom/folder';
66
- const isPage = sourceType === 'document' || fileType === 'custom/document';
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 = (knowledgeBases || []).filter(
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 knowledgeBaseActions = (
96
- inKnowledgeBase
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(knowledgeBaseId, [id]);
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 = knowledgeBaseActions.some(Boolean);
147
+ const hasKnowledgeBaseActions = libraryRelatedActions.some(Boolean);
138
148
 
139
149
  return (
140
150
  [
141
- ...knowledgeBaseActions,
151
+ ...libraryRelatedActions,
142
152
  hasKnowledgeBaseActions && {
143
153
  type: 'divider',
144
154
  },
145
- inKnowledgeBase && {
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
- fileType,
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 (knowledgeBaseId) {
180
- urlToCopy = `${baseUrl}/resource/library/${knowledgeBaseId}?file=${id}`;
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
- if (isFolder || isPage) {
253
- await documentService.deleteDocument(id);
254
- await refreshFileList();
255
- } else {
256
- await removeFile(id);
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
- }, [inKnowledgeBase, isFolder, knowledgeBases, knowledgeBaseId, id]);
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
- renameFolder: s.renameFolder,
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 === 'custom/document' ? metadata?.emoji : null,
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 === 'custom/document'),
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
- await fileStoreState.renameFolder(id, renamingValue.trim());
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
- }, [renamingValue, name, fileStoreState.renameFolder, id, message, t]);
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
- knowledgeBaseId: resourceManagerState.libraryId,
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 = 3,
18
- }: ListViewSkeletonProps) => (
19
- <Flexbox>
20
- {Array.from({ length: count }).map((_, index) => (
21
- <Flexbox
22
- align={'center'}
23
- height={48}
24
- horizontal
25
- key={index}
26
- paddingInline={8}
27
- style={{
28
- borderBlockEnd: `1px solid ${cssVar.colorBorderSecondary}`,
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 { data: rawData } = useResourceManagerFetchKnowledgeItems({
103
- category,
104
- knowledgeBaseId: libraryId,
105
- parentId: currentFolderSlug || null,
106
- q: searchQuery ?? undefined,
107
- showFilesInKnowledgeBase: false,
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
- if (shiftKey && lastSelectedIndex !== null && data) {
120
- // Shift-click: select range from lastSelectedIndex to current index
121
- const start = Math.min(lastSelectedIndex, clickedIndex);
122
- const end = Math.max(lastSelectedIndex, clickedIndex);
123
- const rangeIds = data
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
- setLastSelectedIndex(clickedIndex);
150
+ lastSelectedIndexRef.current = clickedIndex;
141
151
  },
142
- [lastSelectedIndex, data, setSelectedFileIds],
152
+ [setSelectedFileIds],
143
153
  );
144
154
 
145
155
  // Clean up invalid selections when data changes
146
156
  useEffect(() => {
147
- if (data && selectFileIds.length > 0) {
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
- setLastSelectedIndex(null);
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?.length || 0;
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?.filter((item) => item).map((item) => item.id) || []);
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: 8,
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 }}
@@ -351,7 +351,7 @@ const MasonryFileItem = memo<MasonryFileItemProps>(
351
351
  fileType,
352
352
  filename: name,
353
353
  id,
354
- knowledgeBaseId,
354
+ libraryId: knowledgeBaseId,
355
355
  sourceType,
356
356
  url,
357
357
  });