@lobehub/lobehub 2.0.0-next.254 → 2.0.0-next.256
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/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/en-US/plugin.json +1 -0
- package/locales/zh-CN/plugin.json +1 -0
- package/package.json +1 -1
- package/packages/builtin-tool-notebook/src/client/Placeholder/CreateDocument.tsx +6 -6
- package/packages/builtin-tool-notebook/src/client/Render/CreateDocument/DocumentCard.tsx +23 -10
- package/packages/builtin-tool-notebook/src/client/Streaming/CreateDocument/index.tsx +1 -1
- package/packages/database/src/models/__tests__/agent.test.ts +91 -4
- package/packages/database/src/models/agent.ts +15 -7
- package/packages/editor-runtime/src/EditorRuntime.ts +1 -1
- package/packages/editor-runtime/src/__tests__/EditorRuntime.real.test.ts +65 -4
- package/packages/editor-runtime/src/__tests__/__snapshots__/EditorRuntime.real.test.ts.snap +108 -17
- package/packages/editor-runtime/src/__tests__/fixtures/remove-then-add.json +636 -0
- package/packages/editor-runtime/src/__tests__/fixtures/remove.json +1 -0
- package/packages/types/src/agent/agentConfig.ts +8 -8
- package/src/app/[variants]/(main)/chat/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/group/features/Portal/_layout/Mobile.tsx +2 -1
- package/src/app/[variants]/(main)/home/_layout/hooks/useCreateMenuItems.tsx +2 -2
- package/src/app/[variants]/(main)/page/{features/PageTitle → PageTitle}/index.tsx +0 -1
- package/src/app/[variants]/(main)/page/[id]/index.tsx +43 -1
- package/src/app/[variants]/(main)/page/_layout/Body/AllPagesDrawer/Content.tsx +15 -15
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/Editing.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/index.tsx +7 -12
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +5 -5
- package/src/app/[variants]/(main)/page/_layout/Body/List/index.tsx +7 -7
- package/src/app/[variants]/(main)/page/_layout/Body/index.tsx +15 -9
- package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +3 -3
- package/src/app/[variants]/(main)/page/_layout/DataSync.tsx +15 -0
- package/src/app/[variants]/(main)/page/_layout/Header/AddButton.tsx +2 -2
- package/src/app/[variants]/(main)/page/_layout/index.tsx +2 -0
- package/src/app/[variants]/(main)/page/index.tsx +3 -7
- package/src/components/Editor/AutoSaveHint.tsx +1 -1
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Render/Intervention/ApprovalActions.tsx +2 -2
- package/src/features/Conversation/Messages/AssistantGroup/Tool/index.tsx +18 -15
- package/src/features/Conversation/Messages/AssistantGroup/components/Group.tsx +3 -3
- package/src/features/Conversation/Messages/Contexts/MessageAggregationContext.ts +15 -0
- package/src/features/Conversation/Messages/Supervisor/components/Group.tsx +3 -3
- package/src/features/Conversation/Messages/User/Actions/index.tsx +5 -1
- package/src/features/EditorCanvas/AutoSaveHint.tsx +37 -0
- package/src/features/{PageEditor → EditorCanvas}/DiffAllToolbar.tsx +57 -16
- package/src/features/EditorCanvas/DocumentIdMode.tsx +111 -0
- package/src/features/EditorCanvas/EditorCanvas.tsx +148 -0
- package/src/features/EditorCanvas/EditorDataMode.tsx +64 -0
- package/src/features/EditorCanvas/ErrorBoundary.tsx +66 -0
- package/src/features/EditorCanvas/InlineToolbar.tsx +245 -0
- package/src/features/EditorCanvas/InternalEditor.tsx +134 -0
- package/src/features/{PageEditor/EditorCanvas → EditorCanvas}/actions.ts +10 -8
- package/src/features/EditorCanvas/index.ts +9 -0
- package/src/features/PageEditor/EditorCanvas/index.tsx +14 -111
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +95 -0
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +1 -1
- package/src/features/PageEditor/Header/Breadcrumb.tsx +2 -2
- package/src/features/PageEditor/Header/index.tsx +15 -18
- package/src/features/PageEditor/Header/useMenu.tsx +12 -9
- package/src/features/PageEditor/PageEditor.tsx +45 -21
- package/src/features/PageEditor/PageEditorProvider.tsx +13 -1
- package/src/features/PageEditor/PageTitle/index.tsx +2 -2
- package/src/features/PageEditor/StoreUpdater.tsx +35 -308
- package/src/features/PageEditor/{Body/Title.tsx → TitleSection.tsx} +16 -16
- package/src/features/PageEditor/store/action.ts +96 -188
- package/src/features/PageEditor/store/initialState.ts +16 -21
- package/src/features/PageEditor/store/selectors.ts +3 -4
- package/src/features/PageExplorer/PageExplorerPlaceholder.tsx +22 -14
- package/src/features/PageExplorer/index.tsx +34 -67
- package/src/features/Portal/Artifacts/index.ts +0 -2
- package/src/features/Portal/Document/AutoSaveHint.tsx +7 -6
- package/src/features/Portal/Document/Body.tsx +1 -3
- package/src/features/Portal/Document/EditorCanvas.tsx +7 -50
- package/src/features/Portal/Document/Header.tsx +13 -10
- package/src/features/Portal/Document/TodoList.tsx +6 -4
- package/src/features/Portal/Document/Wrapper.tsx +3 -11
- package/src/features/Portal/Document/index.ts +0 -2
- package/src/features/Portal/FilePreview/index.ts +0 -2
- package/src/features/Portal/GroupThread/index.ts +0 -3
- package/src/features/Portal/MessageDetail/index.ts +0 -2
- package/src/features/Portal/Notebook/index.ts +0 -2
- package/src/features/Portal/Plugins/index.ts +0 -2
- package/src/features/Portal/Thread/index.ts +0 -3
- package/src/features/Portal/components/Header.tsx +18 -6
- package/src/features/Portal/router.tsx +34 -97
- package/src/features/Portal/type.ts +0 -2
- package/src/libs/next/config/define-config.ts +4 -1
- package/src/locales/default/plugin.ts +1 -0
- package/src/store/chat/slices/portal/action.test.ts +218 -15
- package/src/store/chat/slices/portal/action.ts +194 -41
- package/src/store/chat/slices/portal/initialState.ts +40 -1
- package/src/store/chat/slices/portal/selectors/thread.ts +44 -3
- package/src/store/chat/slices/portal/selectors.test.ts +119 -17
- package/src/store/chat/slices/portal/selectors.ts +117 -36
- package/src/store/document/index.ts +17 -5
- package/src/store/document/slices/document/action.ts +209 -0
- package/src/store/document/slices/document/index.ts +6 -0
- package/src/store/document/slices/editor/action.test.ts +340 -0
- package/src/store/document/slices/editor/action.ts +133 -149
- package/src/store/document/slices/editor/index.ts +9 -2
- package/src/store/document/slices/editor/initialState.ts +66 -29
- package/src/store/document/slices/editor/reducer.test.ts +217 -0
- package/src/store/document/slices/editor/reducer.ts +67 -0
- package/src/store/document/slices/editor/selectors.test.ts +395 -0
- package/src/store/document/slices/editor/selectors.ts +107 -5
- package/src/store/document/store.ts +12 -13
- package/src/store/file/slices/document/action.ts +19 -188
- package/src/store/file/slices/document/initialState.ts +0 -30
- package/src/store/file/slices/document/selectors.ts +25 -59
- package/src/store/notebook/index.ts +5 -4
- package/src/store/page/index.ts +2 -0
- package/src/store/page/initialState.ts +92 -0
- package/src/store/page/selectors.ts +5 -0
- package/src/store/page/slices/crud/action.ts +477 -0
- package/src/store/page/slices/crud/index.ts +2 -0
- package/src/store/page/slices/crud/initialState.ts +7 -0
- package/src/store/page/slices/internal/action.ts +32 -0
- package/src/store/page/slices/internal/index.ts +2 -0
- package/src/store/page/slices/internal/reducer.ts +105 -0
- package/src/store/page/slices/list/action.ts +206 -0
- package/src/store/page/slices/list/index.ts +3 -0
- package/src/store/page/slices/list/initialState.ts +29 -0
- package/src/store/page/slices/list/selectors.ts +90 -0
- package/src/store/page/slices/selection/action.ts +67 -0
- package/src/store/page/slices/selection/index.ts +2 -0
- package/src/store/page/slices/selection/initialState.ts +11 -0
- package/src/store/page/store.ts +29 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +10 -20
- package/src/utils/identifier.ts +8 -2
- package/src/features/Conversation/Messages/AssistantGroup/components/GroupContext.ts +0 -15
- package/src/features/Conversation/Messages/Supervisor/components/GroupContext.ts +0 -15
- package/src/features/PageEditor/Body/index.tsx +0 -68
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +0 -316
- package/src/features/PageEditor/Header/AutoSaveHint.tsx +0 -27
- package/src/features/Portal/Artifacts/useEnable.ts +0 -4
- package/src/features/Portal/Document/DocumentEditorProvider.tsx +0 -34
- package/src/features/Portal/Document/StoreUpdater.tsx +0 -80
- package/src/features/Portal/Document/Title.tsx +0 -54
- package/src/features/Portal/Document/store/action.ts +0 -114
- package/src/features/Portal/Document/store/index.ts +0 -21
- package/src/features/Portal/Document/store/initialState.ts +0 -24
- package/src/features/Portal/Document/useEnable.ts +0 -8
- package/src/features/Portal/FilePreview/useEnable.ts +0 -6
- package/src/features/Portal/GroupThread/hook.ts +0 -9
- package/src/features/Portal/MessageDetail/useEnable.ts +0 -4
- package/src/features/Portal/Notebook/useEnable.ts +0 -6
- package/src/features/Portal/Plugins/useEnable.ts +0 -6
- package/src/features/Portal/Thread/hook.ts +0 -8
- package/src/store/document/slices/notebook/action.ts +0 -119
- package/src/store/document/slices/notebook/index.ts +0 -3
- package/src/store/document/slices/notebook/initialState.ts +0 -12
- package/src/store/document/slices/notebook/selectors.ts +0 -26
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import type { SWRResponse } from 'swr';
|
|
2
|
+
import { type StateCreator } from 'zustand/vanilla';
|
|
3
|
+
|
|
4
|
+
import { useClientDataSWRWithSync } from '@/libs/swr';
|
|
5
|
+
import { documentService } from '@/services/document';
|
|
6
|
+
import { DocumentSourceType, type LobeDocument } from '@/types/document';
|
|
7
|
+
import { standardizeIdentifier } from '@/utils/identifier';
|
|
8
|
+
import { setNamespace } from '@/utils/storeDebug';
|
|
9
|
+
|
|
10
|
+
import { type PageStore } from '../../store';
|
|
11
|
+
|
|
12
|
+
const n = setNamespace('page/crud');
|
|
13
|
+
|
|
14
|
+
const EDITOR_PAGE_FILE_TYPE = 'custom/document';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Page update parameters - flattened for easier use
|
|
18
|
+
*/
|
|
19
|
+
export interface PageUpdateParams {
|
|
20
|
+
emoji?: string;
|
|
21
|
+
title?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CrudAction {
|
|
25
|
+
/**
|
|
26
|
+
* Create a new page with optimistic update (for page explorer)
|
|
27
|
+
*/
|
|
28
|
+
createNewPage: (title: string) => Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Create a new optimistic page immediately in documents array
|
|
31
|
+
*/
|
|
32
|
+
createOptimisticPage: (title?: string) => string;
|
|
33
|
+
/**
|
|
34
|
+
* Create a new page with markdown content (not optimistic, waits for server response)
|
|
35
|
+
*/
|
|
36
|
+
createPage: (params: {
|
|
37
|
+
content?: string;
|
|
38
|
+
knowledgeBaseId?: string;
|
|
39
|
+
parentId?: string;
|
|
40
|
+
title: string;
|
|
41
|
+
}) => Promise<{ [key: string]: any; id: string }>;
|
|
42
|
+
/**
|
|
43
|
+
* Delete a page and update selection if needed
|
|
44
|
+
*/
|
|
45
|
+
deletePage: (pageId: string) => Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Duplicate an existing page
|
|
48
|
+
*/
|
|
49
|
+
duplicatePage: (pageId: string) => Promise<{ [key: string]: any; id: string }>;
|
|
50
|
+
/**
|
|
51
|
+
* Fetch full page detail by ID and update documents array
|
|
52
|
+
*/
|
|
53
|
+
fetchPageDetail: (pageId: string) => Promise<void>;
|
|
54
|
+
navigateToPage: (pageId: string | null) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Remove a page (deletes from documents table)
|
|
57
|
+
*/
|
|
58
|
+
removePage: (pageId: string) => Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Remove a temp page from documents array
|
|
61
|
+
*/
|
|
62
|
+
removeTempPage: (tempId: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Rename a page
|
|
65
|
+
*/
|
|
66
|
+
renamePage: (pageId: string, title: string, emoji?: string) => Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Replace a temp page with real page data
|
|
69
|
+
*/
|
|
70
|
+
replaceTempPageWithReal: (tempId: string, realPage: LobeDocument) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Update page directly (no optimistic update)
|
|
73
|
+
*/
|
|
74
|
+
updatePage: (pageId: string, updates: Partial<LobeDocument>) => Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Optimistically update page in documents array and queue for DB sync
|
|
77
|
+
*/
|
|
78
|
+
updatePageOptimistically: (pageId: string, updates: PageUpdateParams) => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* SWR hook to fetch page detail with caching and auto-sync to store
|
|
81
|
+
*/
|
|
82
|
+
useFetchPageDetail: (pageId: string | undefined) => SWRResponse<LobeDocument | null>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const createCrudSlice: StateCreator<
|
|
86
|
+
PageStore,
|
|
87
|
+
[['zustand/devtools', never]],
|
|
88
|
+
[],
|
|
89
|
+
CrudAction
|
|
90
|
+
> = (set, get) => ({
|
|
91
|
+
createNewPage: async (title: string) => {
|
|
92
|
+
const { createOptimisticPage, createPage, replaceTempPageWithReal } = get();
|
|
93
|
+
|
|
94
|
+
// Create optimistic page immediately
|
|
95
|
+
const tempPageId = createOptimisticPage(title);
|
|
96
|
+
set({ isCreatingNew: true, selectedPageId: tempPageId }, false, n('createNewPage/start'));
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
// Create real page
|
|
100
|
+
const newPage = await createPage({ content: '', title });
|
|
101
|
+
|
|
102
|
+
// Convert to LobeDocument
|
|
103
|
+
const realPage: LobeDocument = {
|
|
104
|
+
content: newPage.content || '',
|
|
105
|
+
createdAt: newPage.createdAt ? new Date(newPage.createdAt) : new Date(),
|
|
106
|
+
editorData:
|
|
107
|
+
typeof newPage.editorData === 'string'
|
|
108
|
+
? JSON.parse(newPage.editorData)
|
|
109
|
+
: newPage.editorData || null,
|
|
110
|
+
fileType: 'custom/document',
|
|
111
|
+
filename: newPage.title || title,
|
|
112
|
+
id: newPage.id,
|
|
113
|
+
metadata: newPage.metadata || {},
|
|
114
|
+
source: 'document',
|
|
115
|
+
sourceType: DocumentSourceType.EDITOR,
|
|
116
|
+
title: newPage.title || title,
|
|
117
|
+
totalCharCount: newPage.content?.length || 0,
|
|
118
|
+
totalLineCount: 0,
|
|
119
|
+
updatedAt: newPage.updatedAt ? new Date(newPage.updatedAt) : new Date(),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Replace optimistic with real
|
|
123
|
+
replaceTempPageWithReal(tempPageId, realPage);
|
|
124
|
+
set({ isCreatingNew: false, selectedPageId: newPage.id }, false, n('createNewPage/success'));
|
|
125
|
+
|
|
126
|
+
// Navigate to the new page
|
|
127
|
+
get().navigateToPage(newPage.id);
|
|
128
|
+
|
|
129
|
+
return newPage.id;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('Failed to create page:', error);
|
|
132
|
+
get().removeTempPage(tempPageId);
|
|
133
|
+
set({ isCreatingNew: false, selectedPageId: null }, false, n('createNewPage/error'));
|
|
134
|
+
get().navigate?.('/page');
|
|
135
|
+
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
createOptimisticPage: (title = 'Untitled') => {
|
|
140
|
+
// Generate temporary ID with prefix to identify optimistic pages
|
|
141
|
+
const tempId = `temp-page-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
142
|
+
const now = new Date();
|
|
143
|
+
|
|
144
|
+
const newPage: LobeDocument = {
|
|
145
|
+
content: null,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
editorData: null,
|
|
148
|
+
fileType: EDITOR_PAGE_FILE_TYPE,
|
|
149
|
+
filename: title,
|
|
150
|
+
id: tempId,
|
|
151
|
+
metadata: {},
|
|
152
|
+
source: 'document',
|
|
153
|
+
sourceType: DocumentSourceType.EDITOR,
|
|
154
|
+
title: title,
|
|
155
|
+
totalCharCount: 0,
|
|
156
|
+
totalLineCount: 0,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Add to documents array via internal dispatch
|
|
161
|
+
get().internal_dispatchDocuments({ document: newPage, type: 'addDocument' });
|
|
162
|
+
|
|
163
|
+
return tempId;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
createPage: async ({ title, content = '', knowledgeBaseId, parentId }) => {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
|
|
169
|
+
const newPage = await documentService.createDocument({
|
|
170
|
+
content,
|
|
171
|
+
editorData: '{}',
|
|
172
|
+
fileType: EDITOR_PAGE_FILE_TYPE,
|
|
173
|
+
knowledgeBaseId,
|
|
174
|
+
metadata: {
|
|
175
|
+
createdAt: now,
|
|
176
|
+
},
|
|
177
|
+
parentId,
|
|
178
|
+
title,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return newPage;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
deletePage: async (pageId: string) => {
|
|
185
|
+
const { selectedPageId } = get();
|
|
186
|
+
|
|
187
|
+
if (selectedPageId === pageId) {
|
|
188
|
+
set({ isCreatingNew: false, selectedPageId: null }, false, n('deletePage'));
|
|
189
|
+
get().navigateToPage(null);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
duplicatePage: async (pageId) => {
|
|
194
|
+
// Fetch the source page
|
|
195
|
+
const sourcePage = await documentService.getDocumentById(pageId);
|
|
196
|
+
|
|
197
|
+
if (!sourcePage) {
|
|
198
|
+
throw new Error(`Page with ID ${pageId} not found`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create a new page with copied properties
|
|
202
|
+
const newPage = await documentService.createDocument({
|
|
203
|
+
content: sourcePage.content || '',
|
|
204
|
+
editorData: sourcePage.editorData
|
|
205
|
+
? typeof sourcePage.editorData === 'string'
|
|
206
|
+
? sourcePage.editorData
|
|
207
|
+
: JSON.stringify(sourcePage.editorData)
|
|
208
|
+
: '{}',
|
|
209
|
+
fileType: sourcePage.fileType,
|
|
210
|
+
metadata: {
|
|
211
|
+
...sourcePage.metadata,
|
|
212
|
+
createdAt: Date.now(),
|
|
213
|
+
duplicatedFrom: pageId,
|
|
214
|
+
},
|
|
215
|
+
title: `${sourcePage.title} (Copy)`,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Add the new page to documents array via internal dispatch
|
|
219
|
+
const editorPage: LobeDocument = {
|
|
220
|
+
content: newPage.content || null,
|
|
221
|
+
createdAt: newPage.createdAt ? new Date(newPage.createdAt) : new Date(),
|
|
222
|
+
editorData:
|
|
223
|
+
typeof newPage.editorData === 'string'
|
|
224
|
+
? JSON.parse(newPage.editorData)
|
|
225
|
+
: newPage.editorData || null,
|
|
226
|
+
fileType: newPage.fileType,
|
|
227
|
+
filename: newPage.title || newPage.filename || '',
|
|
228
|
+
id: newPage.id,
|
|
229
|
+
metadata: newPage.metadata || {},
|
|
230
|
+
source: 'document',
|
|
231
|
+
sourceType: DocumentSourceType.EDITOR,
|
|
232
|
+
title: newPage.title || '',
|
|
233
|
+
totalCharCount: newPage.content?.length || 0,
|
|
234
|
+
totalLineCount: 0,
|
|
235
|
+
updatedAt: newPage.updatedAt ? new Date(newPage.updatedAt) : new Date(),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
get().internal_dispatchDocuments({ document: editorPage, type: 'addDocument' });
|
|
239
|
+
|
|
240
|
+
return newPage;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
fetchPageDetail: async (pageId) => {
|
|
244
|
+
try {
|
|
245
|
+
const document = await documentService.getDocumentById(pageId);
|
|
246
|
+
|
|
247
|
+
if (!document) {
|
|
248
|
+
console.warn(`[fetchPageDetail] Page not found: ${pageId}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fullPage: LobeDocument = {
|
|
253
|
+
content: document.content || null,
|
|
254
|
+
createdAt: document.createdAt ? new Date(document.createdAt) : new Date(),
|
|
255
|
+
editorData:
|
|
256
|
+
typeof document.editorData === 'string'
|
|
257
|
+
? JSON.parse(document.editorData)
|
|
258
|
+
: document.editorData || null,
|
|
259
|
+
fileType: document.fileType,
|
|
260
|
+
filename: document.title || document.filename || 'Untitled',
|
|
261
|
+
id: document.id,
|
|
262
|
+
metadata: document.metadata || {},
|
|
263
|
+
source: 'document',
|
|
264
|
+
sourceType: DocumentSourceType.EDITOR,
|
|
265
|
+
title: document.title || '',
|
|
266
|
+
totalCharCount: document.content?.length || 0,
|
|
267
|
+
totalLineCount: 0,
|
|
268
|
+
updatedAt: document.updatedAt ? new Date(document.updatedAt) : new Date(),
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Update document via internal dispatch
|
|
272
|
+
const { documents } = get();
|
|
273
|
+
if (documents?.some((doc) => doc.id === pageId)) {
|
|
274
|
+
get().internal_dispatchDocuments({
|
|
275
|
+
document: fullPage,
|
|
276
|
+
id: pageId,
|
|
277
|
+
type: 'updateDocument',
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
get().internal_dispatchDocuments({ document: fullPage, type: 'addDocument' });
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('[fetchPageDetail] Failed to fetch page:', error);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
navigateToPage: (pageId) => {
|
|
288
|
+
if (!pageId) {
|
|
289
|
+
get().navigate?.('/page');
|
|
290
|
+
} else {
|
|
291
|
+
get().navigate?.(`/page/${standardizeIdentifier(pageId)}`);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
removePage: async (pageId) => {
|
|
296
|
+
const { documents, selectedPageId } = get();
|
|
297
|
+
|
|
298
|
+
// Store original documents for rollback
|
|
299
|
+
const originalDocuments = documents;
|
|
300
|
+
|
|
301
|
+
// Remove from documents array via internal dispatch (optimistic update)
|
|
302
|
+
get().internal_dispatchDocuments({ id: pageId, type: 'removeDocument' });
|
|
303
|
+
|
|
304
|
+
// Clear selected page ID if the deleted page is currently selected
|
|
305
|
+
if (selectedPageId === pageId) {
|
|
306
|
+
set({ selectedPageId: null }, false, n('removePage/clearSelection'));
|
|
307
|
+
get().navigateToPage(null);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
// Delete from documents table
|
|
312
|
+
await documentService.deleteDocument(pageId);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error('Failed to delete page:', error);
|
|
315
|
+
// Restore documents on error
|
|
316
|
+
if (originalDocuments) {
|
|
317
|
+
get().internal_dispatchDocuments({ documents: originalDocuments, type: 'setDocuments' });
|
|
318
|
+
}
|
|
319
|
+
if (selectedPageId === pageId) {
|
|
320
|
+
set({ selectedPageId: pageId }, false, n('removePage/restoreSelection'));
|
|
321
|
+
get().navigateToPage(pageId);
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
removeTempPage: (tempId) => {
|
|
328
|
+
get().internal_dispatchDocuments({ id: tempId, type: 'removeDocument' });
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
renamePage: async (pageId: string, title: string, emoji?: string) => {
|
|
332
|
+
const { updatePageOptimistically } = get();
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await updatePageOptimistically(pageId, { emoji, title });
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error('Failed to rename page:', error);
|
|
338
|
+
} finally {
|
|
339
|
+
set({ renamingPageId: null }, false, n('renamePage'));
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
replaceTempPageWithReal: (tempId, realPage) => {
|
|
344
|
+
get().internal_dispatchDocuments({
|
|
345
|
+
document: realPage,
|
|
346
|
+
oldId: tempId,
|
|
347
|
+
type: 'replaceDocument',
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
updatePage: async (id, updates) => {
|
|
352
|
+
await documentService.updateDocument({
|
|
353
|
+
content: updates.content ?? undefined,
|
|
354
|
+
editorData: updates.editorData
|
|
355
|
+
? typeof updates.editorData === 'string'
|
|
356
|
+
? updates.editorData
|
|
357
|
+
: JSON.stringify(updates.editorData)
|
|
358
|
+
: undefined,
|
|
359
|
+
id,
|
|
360
|
+
metadata: updates.metadata,
|
|
361
|
+
parentId: updates.parentId !== undefined ? updates.parentId : undefined,
|
|
362
|
+
title: updates.title,
|
|
363
|
+
});
|
|
364
|
+
await get().refreshDocuments();
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
updatePageOptimistically: async (pageId, updates) => {
|
|
368
|
+
const { documents } = get();
|
|
369
|
+
|
|
370
|
+
// Find the page in documents array
|
|
371
|
+
const existingPage = documents?.find((doc) => doc.id === pageId);
|
|
372
|
+
|
|
373
|
+
if (!existingPage) {
|
|
374
|
+
console.warn('[updatePageOptimistically] Page not found:', pageId);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Build updated metadata with emoji
|
|
379
|
+
const updatedMetadata = {
|
|
380
|
+
...existingPage.metadata,
|
|
381
|
+
...(updates.emoji !== undefined ? { emoji: updates.emoji } : {}),
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Clean up undefined values from metadata
|
|
385
|
+
const cleanedMetadata = Object.fromEntries(
|
|
386
|
+
Object.entries(updatedMetadata).filter(([, v]) => v !== undefined),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const updatedPage: LobeDocument = {
|
|
390
|
+
...existingPage,
|
|
391
|
+
metadata: cleanedMetadata,
|
|
392
|
+
title: updates.title ?? existingPage.title,
|
|
393
|
+
updatedAt: new Date(),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// Update documents array via internal dispatch (optimistic)
|
|
397
|
+
get().internal_dispatchDocuments({ document: updatedPage, id: pageId, type: 'updateDocument' });
|
|
398
|
+
|
|
399
|
+
// Queue background sync to DB
|
|
400
|
+
try {
|
|
401
|
+
await documentService.updateDocument({
|
|
402
|
+
content: updatedPage.content || '',
|
|
403
|
+
editorData:
|
|
404
|
+
typeof updatedPage.editorData === 'string'
|
|
405
|
+
? updatedPage.editorData
|
|
406
|
+
: JSON.stringify(updatedPage.editorData || {}),
|
|
407
|
+
id: pageId,
|
|
408
|
+
metadata: updatedPage.metadata || {},
|
|
409
|
+
parentId: updatedPage.parentId || undefined,
|
|
410
|
+
title: updatedPage.title || updatedPage.filename,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// After successful sync, refresh document list to get server state
|
|
414
|
+
await get().refreshDocuments();
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('[updatePageOptimistically] Failed to sync to DB:', error);
|
|
417
|
+
// On error, revert by restoring original page
|
|
418
|
+
get().internal_dispatchDocuments({
|
|
419
|
+
document: existingPage,
|
|
420
|
+
id: pageId,
|
|
421
|
+
type: 'updateDocument',
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
useFetchPageDetail: (pageId) => {
|
|
427
|
+
const swrKey = pageId ? ['pageDetail', pageId] : null;
|
|
428
|
+
|
|
429
|
+
return useClientDataSWRWithSync<LobeDocument | null>(
|
|
430
|
+
swrKey,
|
|
431
|
+
async () => {
|
|
432
|
+
if (!pageId) return null;
|
|
433
|
+
|
|
434
|
+
const document = await documentService.getDocumentById(pageId);
|
|
435
|
+
if (!document) {
|
|
436
|
+
console.warn(`[useFetchPageDetail] Page not found: ${pageId}`);
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Transform API response to LobeDocument format
|
|
441
|
+
const fullPage: LobeDocument = {
|
|
442
|
+
content: document.content || null,
|
|
443
|
+
createdAt: document.createdAt ? new Date(document.createdAt) : new Date(),
|
|
444
|
+
editorData:
|
|
445
|
+
typeof document.editorData === 'string'
|
|
446
|
+
? JSON.parse(document.editorData)
|
|
447
|
+
: document.editorData || null,
|
|
448
|
+
fileType: document.fileType,
|
|
449
|
+
filename: document.title || document.filename || 'Untitled',
|
|
450
|
+
id: document.id,
|
|
451
|
+
metadata: document.metadata || {},
|
|
452
|
+
source: 'document',
|
|
453
|
+
sourceType: DocumentSourceType.EDITOR,
|
|
454
|
+
title: document.title || '',
|
|
455
|
+
totalCharCount: document.content?.length || 0,
|
|
456
|
+
totalLineCount: 0,
|
|
457
|
+
updatedAt: document.updatedAt ? new Date(document.updatedAt) : new Date(),
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return fullPage;
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
focusThrottleInterval: 5000,
|
|
464
|
+
onData: (document) => {
|
|
465
|
+
if (!document || !pageId) return;
|
|
466
|
+
|
|
467
|
+
// Auto-sync to documents array via internal dispatch
|
|
468
|
+
const { documents } = get();
|
|
469
|
+
if (documents?.some((doc) => doc.id === pageId)) {
|
|
470
|
+
get().internal_dispatchDocuments({ document, id: pageId, type: 'updateDocument' });
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
revalidateOnFocus: true,
|
|
474
|
+
},
|
|
475
|
+
);
|
|
476
|
+
},
|
|
477
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import isEqual from 'fast-deep-equal';
|
|
2
|
+
import { type StateCreator } from 'zustand/vanilla';
|
|
3
|
+
|
|
4
|
+
import { setNamespace } from '@/utils/storeDebug';
|
|
5
|
+
|
|
6
|
+
import { type PageStore } from '../../store';
|
|
7
|
+
import { type DocumentsDispatch, documentsReducer } from './reducer';
|
|
8
|
+
|
|
9
|
+
const n = setNamespace('page/internal');
|
|
10
|
+
|
|
11
|
+
export interface InternalAction {
|
|
12
|
+
/**
|
|
13
|
+
* Dispatch action to update documents array
|
|
14
|
+
*/
|
|
15
|
+
internal_dispatchDocuments: (payload: DocumentsDispatch, action?: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const createInternalSlice: StateCreator<
|
|
19
|
+
PageStore,
|
|
20
|
+
[['zustand/devtools', never]],
|
|
21
|
+
[],
|
|
22
|
+
InternalAction
|
|
23
|
+
> = (set, get) => ({
|
|
24
|
+
internal_dispatchDocuments: (payload, action) => {
|
|
25
|
+
const { documents } = get();
|
|
26
|
+
const nextDocuments = documentsReducer(documents, payload);
|
|
27
|
+
|
|
28
|
+
if (isEqual(documents, nextDocuments)) return;
|
|
29
|
+
|
|
30
|
+
set({ documents: nextDocuments }, false, action ?? n(`dispatchDocuments/${payload.type}`));
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { type LobeDocument } from '@/types/document';
|
|
4
|
+
|
|
5
|
+
// ============ Action Types ============
|
|
6
|
+
|
|
7
|
+
interface AddDocumentAction {
|
|
8
|
+
document: LobeDocument;
|
|
9
|
+
type: 'addDocument';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RemoveDocumentAction {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'removeDocument';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UpdateDocumentAction {
|
|
18
|
+
document: LobeDocument;
|
|
19
|
+
id: string;
|
|
20
|
+
type: 'updateDocument';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ReplaceDocumentAction {
|
|
24
|
+
document: LobeDocument;
|
|
25
|
+
oldId: string;
|
|
26
|
+
type: 'replaceDocument';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SetDocumentsAction {
|
|
30
|
+
documents: LobeDocument[];
|
|
31
|
+
type: 'setDocuments';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface AppendDocumentsAction {
|
|
35
|
+
documents: LobeDocument[];
|
|
36
|
+
type: 'appendDocuments';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type DocumentsDispatch =
|
|
40
|
+
| AddDocumentAction
|
|
41
|
+
| RemoveDocumentAction
|
|
42
|
+
| UpdateDocumentAction
|
|
43
|
+
| ReplaceDocumentAction
|
|
44
|
+
| SetDocumentsAction
|
|
45
|
+
| AppendDocumentsAction;
|
|
46
|
+
|
|
47
|
+
// ============ Reducer ============
|
|
48
|
+
|
|
49
|
+
export const documentsReducer = (
|
|
50
|
+
state: LobeDocument[] | undefined,
|
|
51
|
+
payload: DocumentsDispatch,
|
|
52
|
+
): LobeDocument[] | undefined => {
|
|
53
|
+
switch (payload.type) {
|
|
54
|
+
case 'addDocument': {
|
|
55
|
+
return produce(state ?? [], (draft) => {
|
|
56
|
+
// Add to the beginning
|
|
57
|
+
draft.unshift(payload.document);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'removeDocument': {
|
|
62
|
+
if (!state) return state;
|
|
63
|
+
return produce(state, (draft) => {
|
|
64
|
+
const index = draft.findIndex((doc) => doc.id === payload.id);
|
|
65
|
+
if (index !== -1) {
|
|
66
|
+
draft.splice(index, 1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'updateDocument': {
|
|
72
|
+
if (!state) return state;
|
|
73
|
+
return produce(state, (draft) => {
|
|
74
|
+
const index = draft.findIndex((doc) => doc.id === payload.id);
|
|
75
|
+
if (index !== -1) {
|
|
76
|
+
draft[index] = payload.document;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'replaceDocument': {
|
|
82
|
+
if (!state) return [payload.document];
|
|
83
|
+
return produce(state, (draft) => {
|
|
84
|
+
const index = draft.findIndex((doc) => doc.id === payload.oldId);
|
|
85
|
+
if (index !== -1) {
|
|
86
|
+
draft[index] = payload.document;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'setDocuments': {
|
|
92
|
+
return payload.documents;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'appendDocuments': {
|
|
96
|
+
return produce(state ?? [], (draft) => {
|
|
97
|
+
draft.push(...payload.documents);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
default: {
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|