@lobehub/lobehub 2.0.0-next.255 → 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 +25 -0
- package/changelog/v1.json +9 -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/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/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/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
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
export { createEditorSlice, type EditorAction } from './action';
|
|
2
|
-
export {
|
|
1
|
+
export { createEditorSlice, type EditorAction, type SaveMetadata } from './action';
|
|
2
|
+
export {
|
|
3
|
+
createInitialEditorContentState,
|
|
4
|
+
type DocumentSourceType,
|
|
5
|
+
type EditorContentState,
|
|
6
|
+
type EditorState,
|
|
7
|
+
initialEditorState,
|
|
8
|
+
} from './initialState';
|
|
9
|
+
export { type DocumentDispatch,documentReducer } from './reducer';
|
|
3
10
|
export { editorSelectors } from './selectors';
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import { type IEditor } from '@lobehub/editor';
|
|
4
|
+
import { type EditorState as LobehubEditorState } from '@lobehub/editor/react';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Document source type - determines which service to use for persistence
|
|
8
|
+
*/
|
|
9
|
+
export type DocumentSourceType = 'notebook' | 'page';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Editor content state for a single document
|
|
13
|
+
* Only contains editor-related state, NOT document metadata (title, emoji, etc.)
|
|
14
|
+
*/
|
|
15
|
+
export interface EditorContentState {
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
17
|
+
* Whether auto-save is enabled for this document
|
|
18
|
+
* Defaults to true. Set to false if the consumer handles saving themselves.
|
|
14
19
|
*/
|
|
15
|
-
|
|
20
|
+
autoSave?: boolean;
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
22
|
+
* Document content (markdown)
|
|
18
23
|
*/
|
|
19
|
-
|
|
24
|
+
content: string;
|
|
20
25
|
/**
|
|
21
|
-
* Editor
|
|
26
|
+
* Editor JSON data (BlockNote format)
|
|
22
27
|
*/
|
|
23
|
-
|
|
28
|
+
editorData: any;
|
|
24
29
|
/**
|
|
25
30
|
* Whether there are unsaved changes
|
|
26
31
|
*/
|
|
@@ -33,30 +38,62 @@ export interface EditorState {
|
|
|
33
38
|
* Last updated time
|
|
34
39
|
*/
|
|
35
40
|
lastUpdatedTime: Date | null;
|
|
36
|
-
/**
|
|
37
|
-
* Edit mode: 'edit' or 'preview'
|
|
38
|
-
*/
|
|
39
|
-
mode: 'edit' | 'preview';
|
|
40
41
|
/**
|
|
41
42
|
* Current save status
|
|
42
43
|
*/
|
|
43
44
|
saveStatus: 'idle' | 'saving' | 'saved';
|
|
44
45
|
/**
|
|
45
|
-
*
|
|
46
|
+
* Document source type - determines which service to call for persistence
|
|
46
47
|
*/
|
|
47
|
-
|
|
48
|
+
sourceType: DocumentSourceType;
|
|
49
|
+
/**
|
|
50
|
+
* Topic ID (for notebook documents, used for save routing)
|
|
51
|
+
*/
|
|
52
|
+
topicId?: string;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Global editor state
|
|
57
|
+
*/
|
|
58
|
+
export interface EditorState {
|
|
59
|
+
/**
|
|
60
|
+
* Currently active document ID
|
|
61
|
+
*/
|
|
62
|
+
activeDocumentId: string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Map of editor content states by document ID
|
|
65
|
+
*/
|
|
66
|
+
documents: Record<string, EditorContentState>;
|
|
67
|
+
/**
|
|
68
|
+
* Shared editor instance
|
|
69
|
+
*/
|
|
70
|
+
editor: IEditor | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Editor state from useEditorState hook
|
|
73
|
+
*/
|
|
74
|
+
editorState: LobehubEditorState | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create initial state for a new document's editor content
|
|
79
|
+
*/
|
|
80
|
+
export const createInitialEditorContentState = (
|
|
81
|
+
sourceType: DocumentSourceType,
|
|
82
|
+
overrides?: Partial<EditorContentState>,
|
|
83
|
+
): EditorContentState => ({
|
|
84
|
+
content: '',
|
|
85
|
+
editorData: null,
|
|
56
86
|
isDirty: false,
|
|
57
87
|
lastSavedContent: '',
|
|
58
88
|
lastUpdatedTime: null,
|
|
59
|
-
mode: 'edit',
|
|
60
89
|
saveStatus: 'idle',
|
|
61
|
-
|
|
90
|
+
sourceType,
|
|
91
|
+
...overrides,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const initialEditorState: EditorState = {
|
|
95
|
+
activeDocumentId: undefined,
|
|
96
|
+
documents: {},
|
|
97
|
+
editor: undefined,
|
|
98
|
+
editorState: undefined,
|
|
62
99
|
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { type EditorContentState } from './initialState';
|
|
4
|
+
import { type DocumentDispatch, documentReducer } from './reducer';
|
|
5
|
+
|
|
6
|
+
describe('documentReducer', () => {
|
|
7
|
+
let state: Record<string, EditorContentState>;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
state = {};
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('addDocument', () => {
|
|
14
|
+
it('should add a new document to state', () => {
|
|
15
|
+
const payload: DocumentDispatch = {
|
|
16
|
+
id: 'doc-1',
|
|
17
|
+
type: 'addDocument',
|
|
18
|
+
value: { sourceType: 'page' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const newState = documentReducer(state, payload);
|
|
22
|
+
|
|
23
|
+
expect(newState['doc-1']).toBeDefined();
|
|
24
|
+
expect(newState['doc-1'].sourceType).toBe('page');
|
|
25
|
+
expect(newState['doc-1'].isDirty).toBe(false);
|
|
26
|
+
expect(newState['doc-1'].saveStatus).toBe('idle');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should add document with all provided values', () => {
|
|
30
|
+
const payload: DocumentDispatch = {
|
|
31
|
+
id: 'doc-1',
|
|
32
|
+
type: 'addDocument',
|
|
33
|
+
value: {
|
|
34
|
+
autoSave: false,
|
|
35
|
+
content: '# Test content',
|
|
36
|
+
editorData: { type: 'doc' },
|
|
37
|
+
lastSavedContent: '# Test content',
|
|
38
|
+
sourceType: 'notebook',
|
|
39
|
+
topicId: 'topic-1',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const newState = documentReducer(state, payload);
|
|
44
|
+
|
|
45
|
+
expect(newState['doc-1']).toMatchObject({
|
|
46
|
+
autoSave: false,
|
|
47
|
+
content: '# Test content',
|
|
48
|
+
editorData: { type: 'doc' },
|
|
49
|
+
isDirty: false,
|
|
50
|
+
lastSavedContent: '# Test content',
|
|
51
|
+
sourceType: 'notebook',
|
|
52
|
+
topicId: 'topic-1',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should merge with existing document when ID exists', () => {
|
|
57
|
+
// First add a document
|
|
58
|
+
const initialPayload: DocumentDispatch = {
|
|
59
|
+
id: 'doc-1',
|
|
60
|
+
type: 'addDocument',
|
|
61
|
+
value: { content: 'Original', sourceType: 'page' },
|
|
62
|
+
};
|
|
63
|
+
state = documentReducer(state, initialPayload);
|
|
64
|
+
|
|
65
|
+
// Then add again with same ID
|
|
66
|
+
const updatePayload: DocumentDispatch = {
|
|
67
|
+
id: 'doc-1',
|
|
68
|
+
type: 'addDocument',
|
|
69
|
+
value: { content: 'Updated', sourceType: 'page' },
|
|
70
|
+
};
|
|
71
|
+
const newState = documentReducer(state, updatePayload);
|
|
72
|
+
|
|
73
|
+
expect(newState['doc-1'].content).toBe('Updated');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('updateDocument', () => {
|
|
78
|
+
it('should update an existing document', () => {
|
|
79
|
+
// First add a document
|
|
80
|
+
const addPayload: DocumentDispatch = {
|
|
81
|
+
id: 'doc-1',
|
|
82
|
+
type: 'addDocument',
|
|
83
|
+
value: { content: 'Original', sourceType: 'page' },
|
|
84
|
+
};
|
|
85
|
+
state = documentReducer(state, addPayload);
|
|
86
|
+
|
|
87
|
+
// Then update it
|
|
88
|
+
const updatePayload: DocumentDispatch = {
|
|
89
|
+
id: 'doc-1',
|
|
90
|
+
type: 'updateDocument',
|
|
91
|
+
value: { content: 'Updated', isDirty: true },
|
|
92
|
+
};
|
|
93
|
+
const newState = documentReducer(state, updatePayload);
|
|
94
|
+
|
|
95
|
+
expect(newState['doc-1'].content).toBe('Updated');
|
|
96
|
+
expect(newState['doc-1'].isDirty).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should not modify state if document does not exist', () => {
|
|
100
|
+
const payload: DocumentDispatch = {
|
|
101
|
+
id: 'non-existent',
|
|
102
|
+
type: 'updateDocument',
|
|
103
|
+
value: { content: 'Test' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const newState = documentReducer(state, payload);
|
|
107
|
+
|
|
108
|
+
expect(newState['non-existent']).toBeUndefined();
|
|
109
|
+
expect(newState).toBe(state);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should not create new state reference if values are equal', () => {
|
|
113
|
+
// First add a document
|
|
114
|
+
const addPayload: DocumentDispatch = {
|
|
115
|
+
id: 'doc-1',
|
|
116
|
+
type: 'addDocument',
|
|
117
|
+
value: { content: 'Test', sourceType: 'page' },
|
|
118
|
+
};
|
|
119
|
+
state = documentReducer(state, addPayload);
|
|
120
|
+
|
|
121
|
+
// Update with same value
|
|
122
|
+
const updatePayload: DocumentDispatch = {
|
|
123
|
+
id: 'doc-1',
|
|
124
|
+
type: 'updateDocument',
|
|
125
|
+
value: { content: 'Test' },
|
|
126
|
+
};
|
|
127
|
+
const newState = documentReducer(state, updatePayload);
|
|
128
|
+
|
|
129
|
+
// Should be the same reference since nothing changed
|
|
130
|
+
expect(newState).toBe(state);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('deleteDocument', () => {
|
|
135
|
+
it('should delete an existing document', () => {
|
|
136
|
+
// First add a document
|
|
137
|
+
const addPayload: DocumentDispatch = {
|
|
138
|
+
id: 'doc-1',
|
|
139
|
+
type: 'addDocument',
|
|
140
|
+
value: { sourceType: 'page' },
|
|
141
|
+
};
|
|
142
|
+
state = documentReducer(state, addPayload);
|
|
143
|
+
expect(state['doc-1']).toBeDefined();
|
|
144
|
+
|
|
145
|
+
// Then delete it
|
|
146
|
+
const deletePayload: DocumentDispatch = {
|
|
147
|
+
id: 'doc-1',
|
|
148
|
+
type: 'deleteDocument',
|
|
149
|
+
};
|
|
150
|
+
const newState = documentReducer(state, deletePayload);
|
|
151
|
+
|
|
152
|
+
expect(newState['doc-1']).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should not affect other documents when deleting one', () => {
|
|
156
|
+
// Add two documents
|
|
157
|
+
state = documentReducer(state, {
|
|
158
|
+
id: 'doc-1',
|
|
159
|
+
type: 'addDocument',
|
|
160
|
+
value: { sourceType: 'page' },
|
|
161
|
+
});
|
|
162
|
+
state = documentReducer(state, {
|
|
163
|
+
id: 'doc-2',
|
|
164
|
+
type: 'addDocument',
|
|
165
|
+
value: { sourceType: 'notebook' },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Delete one
|
|
169
|
+
const newState = documentReducer(state, {
|
|
170
|
+
id: 'doc-1',
|
|
171
|
+
type: 'deleteDocument',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(newState['doc-1']).toBeUndefined();
|
|
175
|
+
expect(newState['doc-2']).toBeDefined();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('default', () => {
|
|
180
|
+
it('should return the original state for unknown action type', () => {
|
|
181
|
+
const payload = {
|
|
182
|
+
id: 'doc-1',
|
|
183
|
+
type: 'unknown',
|
|
184
|
+
} as unknown as DocumentDispatch;
|
|
185
|
+
|
|
186
|
+
const newState = documentReducer(state, payload);
|
|
187
|
+
|
|
188
|
+
expect(newState).toBe(state);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('immutability', () => {
|
|
193
|
+
it('should generate immutable state object', () => {
|
|
194
|
+
const payload: DocumentDispatch = {
|
|
195
|
+
id: 'doc-1',
|
|
196
|
+
type: 'addDocument',
|
|
197
|
+
value: { sourceType: 'page' },
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const newState = documentReducer(state, payload);
|
|
201
|
+
|
|
202
|
+
expect(newState).not.toBe(state);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not modify the original state object', () => {
|
|
206
|
+
const payload: DocumentDispatch = {
|
|
207
|
+
id: 'doc-1',
|
|
208
|
+
type: 'addDocument',
|
|
209
|
+
value: { sourceType: 'page' },
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
documentReducer(state, payload);
|
|
213
|
+
|
|
214
|
+
expect(state).toEqual({});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { produce } from 'immer';
|
|
2
|
+
|
|
3
|
+
import { type EditorContentState, createInitialEditorContentState } from './initialState';
|
|
4
|
+
|
|
5
|
+
// ============ Action Types ============
|
|
6
|
+
|
|
7
|
+
interface AddDocumentAction {
|
|
8
|
+
id: string;
|
|
9
|
+
type: 'addDocument';
|
|
10
|
+
value: Partial<EditorContentState> & { sourceType: EditorContentState['sourceType'] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface UpdateDocumentAction {
|
|
14
|
+
id: string;
|
|
15
|
+
type: 'updateDocument';
|
|
16
|
+
value: Partial<EditorContentState>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface DeleteDocumentAction {
|
|
20
|
+
id: string;
|
|
21
|
+
type: 'deleteDocument';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type DocumentDispatch = AddDocumentAction | UpdateDocumentAction | DeleteDocumentAction;
|
|
25
|
+
|
|
26
|
+
// ============ Reducer ============
|
|
27
|
+
|
|
28
|
+
export const documentReducer = (
|
|
29
|
+
state: Record<string, EditorContentState> = {},
|
|
30
|
+
payload: DocumentDispatch,
|
|
31
|
+
): Record<string, EditorContentState> => {
|
|
32
|
+
switch (payload.type) {
|
|
33
|
+
case 'addDocument': {
|
|
34
|
+
return produce(state, (draft) => {
|
|
35
|
+
const { id, value } = payload;
|
|
36
|
+
const existingDoc = draft[id];
|
|
37
|
+
|
|
38
|
+
// Create new document state, merging with existing if present
|
|
39
|
+
draft[id] = existingDoc
|
|
40
|
+
? { ...existingDoc, ...value }
|
|
41
|
+
: createInitialEditorContentState(value.sourceType, value);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
case 'updateDocument': {
|
|
46
|
+
return produce(state, (draft) => {
|
|
47
|
+
const { id, value } = payload;
|
|
48
|
+
const currentDoc = draft[id];
|
|
49
|
+
|
|
50
|
+
if (currentDoc) {
|
|
51
|
+
// Directly assign to let immer handle change detection
|
|
52
|
+
Object.assign(draft[id], value);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'deleteDocument': {
|
|
58
|
+
return produce(state, (draft) => {
|
|
59
|
+
delete draft[payload.id];
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
default: {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|