@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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { StateCreator } from 'zustand/vanilla';
|
|
3
|
+
|
|
4
|
+
import { resourceService } from '@/services/resource';
|
|
5
|
+
import type { CreateResourceParams, ResourceItem, UpdateResourceParams } from '@/types/resource';
|
|
6
|
+
|
|
7
|
+
import type { FileStore } from '../../store';
|
|
8
|
+
import { type ResourceState, initialResourceState } from './initialState';
|
|
9
|
+
import { ResourceSyncEngine } from './syncEngine';
|
|
10
|
+
|
|
11
|
+
const log = debug('resource-manager:action');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resource slice actions
|
|
15
|
+
*/
|
|
16
|
+
export interface ResourceAction {
|
|
17
|
+
/**
|
|
18
|
+
* Clear all resources and reset state
|
|
19
|
+
*/
|
|
20
|
+
clearResources: () => void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new resource with optimistic update
|
|
24
|
+
* Returns temp ID for immediate UI feedback
|
|
25
|
+
*/
|
|
26
|
+
createResource: (params: CreateResourceParams) => Promise<string>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new resource and wait for sync to complete
|
|
30
|
+
* Returns real ID from server (useful for auto-rename after creation)
|
|
31
|
+
*/
|
|
32
|
+
createResourceAndSync: (params: CreateResourceParams) => Promise<string>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Delete a resource with optimistic update
|
|
36
|
+
*/
|
|
37
|
+
deleteResource: (id: string) => Promise<void>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Flush pending sync operations immediately
|
|
41
|
+
*/
|
|
42
|
+
flushSync: () => Promise<void>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load more resources (pagination)
|
|
46
|
+
*/
|
|
47
|
+
loadMoreResources: () => Promise<void>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Move a resource to a different parent folder
|
|
51
|
+
*/
|
|
52
|
+
moveResource: (id: string, parentId: string | null) => Promise<void>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Retry a failed sync operation
|
|
56
|
+
*/
|
|
57
|
+
retrySync: (resourceId: string) => Promise<void>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update a resource with optimistic update
|
|
61
|
+
*/
|
|
62
|
+
updateResource: (id: string, updates: UpdateResourceParams) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let syncEngineInstance: ResourceSyncEngine | null = null;
|
|
66
|
+
|
|
67
|
+
export const createResourceSlice: StateCreator<
|
|
68
|
+
FileStore,
|
|
69
|
+
[['zustand/devtools', never]],
|
|
70
|
+
[],
|
|
71
|
+
ResourceAction & ResourceState
|
|
72
|
+
> = (set, get) => {
|
|
73
|
+
// Initialize sync engine (singleton per store instance)
|
|
74
|
+
const getSyncEngine = () => {
|
|
75
|
+
if (!syncEngineInstance) {
|
|
76
|
+
syncEngineInstance = new ResourceSyncEngine(
|
|
77
|
+
() => {
|
|
78
|
+
const state = get();
|
|
79
|
+
return {
|
|
80
|
+
resourceList: state.resourceList || [],
|
|
81
|
+
resourceMap: state.resourceMap || new Map(),
|
|
82
|
+
syncQueue: state.syncQueue || [],
|
|
83
|
+
syncingIds: state.syncingIds || new Set(),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
(partial) => {
|
|
87
|
+
set(partial as any, false, 'syncEngine/update');
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return syncEngineInstance;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...initialResourceState,
|
|
96
|
+
|
|
97
|
+
clearResources: () => {
|
|
98
|
+
set(
|
|
99
|
+
{
|
|
100
|
+
hasMore: false,
|
|
101
|
+
offset: 0,
|
|
102
|
+
queryParams: undefined,
|
|
103
|
+
resourceList: [],
|
|
104
|
+
resourceMap: new Map(),
|
|
105
|
+
syncQueue: [],
|
|
106
|
+
total: 0,
|
|
107
|
+
},
|
|
108
|
+
false,
|
|
109
|
+
'clearResources',
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
createResource: async (params) => {
|
|
114
|
+
const tempId = `temp-resource-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
115
|
+
|
|
116
|
+
// 1. Create optimistic resource
|
|
117
|
+
const optimisticResource: ResourceItem = {
|
|
118
|
+
_optimistic: { isPending: true, retryCount: 0 },
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
fileType: params.fileType,
|
|
121
|
+
id: tempId,
|
|
122
|
+
knowledgeBaseId: params.knowledgeBaseId,
|
|
123
|
+
name: 'title' in params ? params.title : params.name,
|
|
124
|
+
parentId: params.parentId,
|
|
125
|
+
size: 'size' in params ? params.size : 0,
|
|
126
|
+
sourceType: params.sourceType,
|
|
127
|
+
updatedAt: new Date(),
|
|
128
|
+
...(params.sourceType === 'file'
|
|
129
|
+
? {
|
|
130
|
+
url: 'url' in params ? params.url : '',
|
|
131
|
+
}
|
|
132
|
+
: {
|
|
133
|
+
content: 'content' in params ? params.content : '',
|
|
134
|
+
editorData: 'editorData' in params ? params.editorData : {},
|
|
135
|
+
slug: 'slug' in params ? params.slug : undefined,
|
|
136
|
+
title: 'title' in params ? params.title : 'Untitled',
|
|
137
|
+
}),
|
|
138
|
+
metadata: params.metadata,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// 2. Update store immediately (UI instant feedback)
|
|
142
|
+
const { resourceMap, resourceList } = get();
|
|
143
|
+
const newMap = new Map(resourceMap);
|
|
144
|
+
newMap.set(tempId, optimisticResource);
|
|
145
|
+
|
|
146
|
+
set(
|
|
147
|
+
{
|
|
148
|
+
resourceList: [optimisticResource, ...resourceList],
|
|
149
|
+
resourceMap: newMap,
|
|
150
|
+
},
|
|
151
|
+
false,
|
|
152
|
+
'createResource/optimistic',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// 3. Enqueue sync (background)
|
|
156
|
+
const syncEngine = getSyncEngine();
|
|
157
|
+
syncEngine.enqueue({
|
|
158
|
+
id: `sync-${tempId}`,
|
|
159
|
+
payload: params,
|
|
160
|
+
resourceId: tempId,
|
|
161
|
+
retryCount: 0,
|
|
162
|
+
timestamp: new Date(),
|
|
163
|
+
type: 'create',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return tempId;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
createResourceAndSync: async (params) => {
|
|
170
|
+
const tempId = `temp-resource-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
171
|
+
|
|
172
|
+
// 1. Create optimistic resource
|
|
173
|
+
const optimisticResource: ResourceItem = {
|
|
174
|
+
_optimistic: { isPending: true, retryCount: 0 },
|
|
175
|
+
createdAt: new Date(),
|
|
176
|
+
fileType: params.fileType,
|
|
177
|
+
id: tempId,
|
|
178
|
+
knowledgeBaseId: params.knowledgeBaseId,
|
|
179
|
+
name: 'title' in params ? params.title : params.name,
|
|
180
|
+
parentId: params.parentId,
|
|
181
|
+
size: 'size' in params ? params.size : 0,
|
|
182
|
+
sourceType: params.sourceType,
|
|
183
|
+
updatedAt: new Date(),
|
|
184
|
+
...(params.sourceType === 'file'
|
|
185
|
+
? {
|
|
186
|
+
url: 'url' in params ? params.url : '',
|
|
187
|
+
}
|
|
188
|
+
: {
|
|
189
|
+
content: 'content' in params ? params.content : '',
|
|
190
|
+
editorData: 'editorData' in params ? params.editorData : {},
|
|
191
|
+
slug: 'slug' in params ? params.slug : undefined,
|
|
192
|
+
title: 'title' in params ? params.title : 'Untitled',
|
|
193
|
+
}),
|
|
194
|
+
metadata: params.metadata,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 2. Update store immediately (UI instant feedback)
|
|
198
|
+
const { resourceMap, resourceList } = get();
|
|
199
|
+
const newMap = new Map(resourceMap);
|
|
200
|
+
newMap.set(tempId, optimisticResource);
|
|
201
|
+
|
|
202
|
+
set(
|
|
203
|
+
{
|
|
204
|
+
resourceList: [optimisticResource, ...resourceList],
|
|
205
|
+
resourceMap: newMap,
|
|
206
|
+
},
|
|
207
|
+
false,
|
|
208
|
+
'createResourceAndSync/optimistic',
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// 3. Enqueue sync and wait for completion
|
|
212
|
+
const syncEngine = getSyncEngine();
|
|
213
|
+
const realId = await syncEngine.enqueue({
|
|
214
|
+
id: `sync-${tempId}`,
|
|
215
|
+
payload: params,
|
|
216
|
+
resourceId: tempId,
|
|
217
|
+
retryCount: 0,
|
|
218
|
+
timestamp: new Date(),
|
|
219
|
+
type: 'create',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return (realId as string) || tempId;
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
deleteResource: async (id) => {
|
|
226
|
+
// 1. Remove immediately (optimistic)
|
|
227
|
+
const { resourceMap, resourceList } = get();
|
|
228
|
+
const newMap = new Map(resourceMap);
|
|
229
|
+
newMap.delete(id);
|
|
230
|
+
|
|
231
|
+
log('deleteResource', id, newMap, resourceList);
|
|
232
|
+
|
|
233
|
+
set(
|
|
234
|
+
{
|
|
235
|
+
resourceList: resourceList.filter((r) => r.id !== id),
|
|
236
|
+
resourceMap: newMap,
|
|
237
|
+
},
|
|
238
|
+
false,
|
|
239
|
+
'deleteResource/optimistic',
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// 2. Enqueue sync (background)
|
|
243
|
+
const syncEngine = getSyncEngine();
|
|
244
|
+
syncEngine.enqueue({
|
|
245
|
+
id: `sync-${id}-${Date.now()}`,
|
|
246
|
+
payload: {},
|
|
247
|
+
resourceId: id,
|
|
248
|
+
retryCount: 0,
|
|
249
|
+
timestamp: new Date(),
|
|
250
|
+
type: 'delete',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
log('enqueue deleteResource', id, syncEngine);
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
flushSync: async () => {
|
|
257
|
+
const syncEngine = getSyncEngine();
|
|
258
|
+
await syncEngine.flush();
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
loadMoreResources: async () => {
|
|
262
|
+
const { offset, queryParams, hasMore } = get();
|
|
263
|
+
if (!hasMore || !queryParams) return;
|
|
264
|
+
|
|
265
|
+
set({ isLoadingMore: true }, false, 'loadMoreResources/start');
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const { items } = await resourceService.queryResources({
|
|
269
|
+
...queryParams,
|
|
270
|
+
limit: 50,
|
|
271
|
+
offset,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Merge into existing map/list
|
|
275
|
+
const { resourceMap, resourceList } = get();
|
|
276
|
+
const newMap = new Map(resourceMap);
|
|
277
|
+
items.forEach((item) => newMap.set(item.id, item));
|
|
278
|
+
|
|
279
|
+
set(
|
|
280
|
+
{
|
|
281
|
+
hasMore: items.length === 50,
|
|
282
|
+
isLoadingMore: false,
|
|
283
|
+
offset: offset + items.length,
|
|
284
|
+
resourceList: [...resourceList, ...items],
|
|
285
|
+
resourceMap: newMap,
|
|
286
|
+
},
|
|
287
|
+
false,
|
|
288
|
+
'loadMoreResources/success',
|
|
289
|
+
);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
set({ isLoadingMore: false }, false, 'loadMoreResources/error');
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
moveResource: async (id, parentId) => {
|
|
297
|
+
// 1. Optimistically remove from current view (it's moving away)
|
|
298
|
+
const { resourceMap, resourceList } = get();
|
|
299
|
+
const existing = resourceMap.get(id);
|
|
300
|
+
|
|
301
|
+
if (!existing) {
|
|
302
|
+
console.warn(`Resource ${id} not found for move`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Remove from list and map immediately
|
|
307
|
+
const newMap = new Map(resourceMap);
|
|
308
|
+
newMap.delete(id);
|
|
309
|
+
|
|
310
|
+
set(
|
|
311
|
+
{
|
|
312
|
+
resourceList: resourceList.filter((r) => r.id !== id),
|
|
313
|
+
resourceMap: newMap,
|
|
314
|
+
},
|
|
315
|
+
false,
|
|
316
|
+
'moveResource/optimistic',
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// 2. Enqueue move operation (background sync) and wait for it to complete
|
|
320
|
+
const syncEngine = getSyncEngine();
|
|
321
|
+
return syncEngine.enqueue({
|
|
322
|
+
id: `sync-move-${id}-${Date.now()}`,
|
|
323
|
+
payload: { parentId },
|
|
324
|
+
resourceId: id,
|
|
325
|
+
retryCount: 0,
|
|
326
|
+
timestamp: new Date(),
|
|
327
|
+
type: 'move',
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
retrySync: async (resourceId) => {
|
|
332
|
+
// Find the resource and re-enqueue if it has an error
|
|
333
|
+
const { resourceMap } = get();
|
|
334
|
+
const resource = resourceMap.get(resourceId);
|
|
335
|
+
|
|
336
|
+
if (resource?._optimistic?.error) {
|
|
337
|
+
// Clear error state
|
|
338
|
+
const updated = {
|
|
339
|
+
...resource,
|
|
340
|
+
_optimistic: {
|
|
341
|
+
isPending: true,
|
|
342
|
+
retryCount: 0,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const newMap = new Map(resourceMap);
|
|
347
|
+
newMap.set(resourceId, updated);
|
|
348
|
+
|
|
349
|
+
const { resourceList } = get();
|
|
350
|
+
const listIndex = resourceList.findIndex((r) => r.id === resourceId);
|
|
351
|
+
const newList = [...resourceList];
|
|
352
|
+
if (listIndex >= 0) {
|
|
353
|
+
newList[listIndex] = updated;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
set(
|
|
357
|
+
{
|
|
358
|
+
resourceList: newList,
|
|
359
|
+
resourceMap: newMap,
|
|
360
|
+
},
|
|
361
|
+
false,
|
|
362
|
+
'retrySync',
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Re-enqueue the operation
|
|
366
|
+
// Note: We need to reconstruct the original operation
|
|
367
|
+
// For now, we'll just try an update operation
|
|
368
|
+
const syncEngine = getSyncEngine();
|
|
369
|
+
syncEngine.enqueue({
|
|
370
|
+
id: `sync-retry-${resourceId}-${Date.now()}`,
|
|
371
|
+
payload: {}, // Empty update to trigger re-sync
|
|
372
|
+
resourceId,
|
|
373
|
+
retryCount: 0,
|
|
374
|
+
timestamp: new Date(),
|
|
375
|
+
type: 'update',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
updateResource: async (id, updates) => {
|
|
381
|
+
// 1. Apply updates immediately (optimistic)
|
|
382
|
+
const { resourceMap, resourceList } = get();
|
|
383
|
+
const existing = resourceMap.get(id);
|
|
384
|
+
|
|
385
|
+
if (!existing) {
|
|
386
|
+
console.warn(`Resource ${id} not found for update`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
log('updateResource', id, existing, updates);
|
|
391
|
+
|
|
392
|
+
const updated: ResourceItem = {
|
|
393
|
+
...existing,
|
|
394
|
+
...updates,
|
|
395
|
+
_optimistic: { isPending: true, retryCount: 0 },
|
|
396
|
+
name: updates.name || updates.title || existing.name,
|
|
397
|
+
updatedAt: new Date(),
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const newMap = new Map(resourceMap);
|
|
401
|
+
newMap.set(id, updated);
|
|
402
|
+
|
|
403
|
+
const listIndex = resourceList.findIndex((r) => r.id === id);
|
|
404
|
+
const newList = [...resourceList];
|
|
405
|
+
if (listIndex >= 0) {
|
|
406
|
+
newList[listIndex] = updated;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
set(
|
|
410
|
+
{
|
|
411
|
+
resourceList: newList,
|
|
412
|
+
resourceMap: newMap,
|
|
413
|
+
},
|
|
414
|
+
false,
|
|
415
|
+
'updateResource/optimistic',
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
// 2. Enqueue sync (background)
|
|
419
|
+
const syncEngine = getSyncEngine();
|
|
420
|
+
syncEngine.enqueue({
|
|
421
|
+
id: `sync-${id}-${Date.now()}`,
|
|
422
|
+
payload: updates,
|
|
423
|
+
resourceId: id,
|
|
424
|
+
retryCount: 0,
|
|
425
|
+
timestamp: new Date(),
|
|
426
|
+
type: 'update',
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
log('enqueue updateResource', id, syncEngine);
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isEqual } from 'es-toolkit';
|
|
2
|
+
import { shallow } from 'zustand/shallow';
|
|
3
|
+
|
|
4
|
+
import { mutate, useClientDataSWR } from '@/libs/swr';
|
|
5
|
+
import { resourceService } from '@/services/resource';
|
|
6
|
+
import type { ResourceQueryParams } from '@/types/resource';
|
|
7
|
+
|
|
8
|
+
import { useFileStore } from '../../store';
|
|
9
|
+
|
|
10
|
+
const SWR_KEY_RESOURCES = 'SWR_RESOURCES';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Revalidate resources with current or specific query params
|
|
14
|
+
* This can be called from outside React components (e.g., store actions)
|
|
15
|
+
*/
|
|
16
|
+
export const revalidateResources = async (params?: ResourceQueryParams) => {
|
|
17
|
+
const queryParams = params || useFileStore.getState().queryParams;
|
|
18
|
+
if (queryParams) {
|
|
19
|
+
await mutate([SWR_KEY_RESOURCES, queryParams]);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom SWR hook for fetching resources with caching and revalidation
|
|
25
|
+
*/
|
|
26
|
+
export const useFetchResources = (params: ResourceQueryParams | null, enable = true) => {
|
|
27
|
+
return useClientDataSWR(
|
|
28
|
+
enable && params ? [SWR_KEY_RESOURCES, params] : null,
|
|
29
|
+
async ([, queryParams]: [string, ResourceQueryParams]) => {
|
|
30
|
+
const response = await resourceService.queryResources({
|
|
31
|
+
...queryParams,
|
|
32
|
+
limit: queryParams.limit || 50,
|
|
33
|
+
offset: 0,
|
|
34
|
+
});
|
|
35
|
+
return response;
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
// SWR configuration for optimal UX
|
|
39
|
+
dedupingInterval: 2000,
|
|
40
|
+
onSuccess: (data: { hasMore: boolean; items: any[]; total?: number }) => {
|
|
41
|
+
const { resourceList, resourceMap } = useFileStore.getState();
|
|
42
|
+
|
|
43
|
+
const newResourceMap = new Map(data.items.map((item: any) => [item.id, item]));
|
|
44
|
+
const newResourceList = data.items;
|
|
45
|
+
|
|
46
|
+
// Only update store if data actually changed
|
|
47
|
+
if (!isEqual(newResourceList, resourceList) || !isEqual(newResourceMap, resourceMap)) {
|
|
48
|
+
useFileStore.setState(
|
|
49
|
+
{
|
|
50
|
+
hasMore: data.hasMore,
|
|
51
|
+
offset: data.items.length,
|
|
52
|
+
queryParams: params ?? undefined,
|
|
53
|
+
resourceList: newResourceList,
|
|
54
|
+
resourceMap: newResourceMap,
|
|
55
|
+
total: data.total,
|
|
56
|
+
},
|
|
57
|
+
false,
|
|
58
|
+
'useFetchResources/success',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
revalidateOnFocus: true,
|
|
63
|
+
revalidateOnReconnect: true,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Hook to access resource store state
|
|
70
|
+
*/
|
|
71
|
+
export const useResourceStore = () => {
|
|
72
|
+
return useFileStore(
|
|
73
|
+
(s) => ({
|
|
74
|
+
hasMore: s.hasMore,
|
|
75
|
+
queryParams: s.queryParams,
|
|
76
|
+
resourceList: s.resourceList,
|
|
77
|
+
resourceMap: s.resourceMap,
|
|
78
|
+
total: s.total,
|
|
79
|
+
}),
|
|
80
|
+
shallow,
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ResourceItem, ResourceQueryParams, SyncOperation } from '@/types/resource';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resource slice state
|
|
5
|
+
*/
|
|
6
|
+
export interface ResourceState {
|
|
7
|
+
/**
|
|
8
|
+
* Pagination state
|
|
9
|
+
*/
|
|
10
|
+
hasMore: boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Loading states
|
|
14
|
+
*/
|
|
15
|
+
isLoadingMore: boolean;
|
|
16
|
+
|
|
17
|
+
isSyncing: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sync status
|
|
21
|
+
*/
|
|
22
|
+
lastSyncTime?: Date;
|
|
23
|
+
|
|
24
|
+
offset: number;
|
|
25
|
+
/**
|
|
26
|
+
* Current query parameters
|
|
27
|
+
*/
|
|
28
|
+
queryParams?: ResourceQueryParams;
|
|
29
|
+
/**
|
|
30
|
+
* Derived sorted/filtered list (computed from map)
|
|
31
|
+
* Used for rendering in UI
|
|
32
|
+
*/
|
|
33
|
+
resourceList: ResourceItem[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Primary store - Map for O(1) lookups
|
|
37
|
+
*/
|
|
38
|
+
resourceMap: Map<string, ResourceItem>;
|
|
39
|
+
|
|
40
|
+
syncError?: Error;
|
|
41
|
+
/**
|
|
42
|
+
* Sync queue (FIFO)
|
|
43
|
+
* Contains pending operations to be synced to server
|
|
44
|
+
*/
|
|
45
|
+
syncQueue: SyncOperation[];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Track which resources are currently syncing
|
|
49
|
+
*/
|
|
50
|
+
syncingIds: Set<string>;
|
|
51
|
+
total: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initial state for resource slice
|
|
56
|
+
*/
|
|
57
|
+
export const initialResourceState: ResourceState = {
|
|
58
|
+
hasMore: false,
|
|
59
|
+
isLoadingMore: false,
|
|
60
|
+
isSyncing: false,
|
|
61
|
+
offset: 0,
|
|
62
|
+
resourceList: [],
|
|
63
|
+
resourceMap: new Map(),
|
|
64
|
+
syncQueue: [],
|
|
65
|
+
syncingIds: new Set(),
|
|
66
|
+
total: 0,
|
|
67
|
+
};
|