@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
@@ -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
+ };