@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,395 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { DocumentStore } from '../../store';
|
|
4
|
+
import { editorSelectors } from './selectors';
|
|
5
|
+
|
|
6
|
+
describe('DocumentStore - Editor Selectors', () => {
|
|
7
|
+
const createMockState = (overrides?: Partial<DocumentStore>): DocumentStore =>
|
|
8
|
+
({
|
|
9
|
+
activeDocumentId: undefined,
|
|
10
|
+
documents: {},
|
|
11
|
+
editor: undefined,
|
|
12
|
+
editorState: undefined,
|
|
13
|
+
...overrides,
|
|
14
|
+
}) as DocumentStore;
|
|
15
|
+
|
|
16
|
+
describe('activeDocumentId', () => {
|
|
17
|
+
it('should return undefined when no document is active', () => {
|
|
18
|
+
const state = createMockState();
|
|
19
|
+
expect(editorSelectors.activeDocumentId(state)).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return the active document ID', () => {
|
|
23
|
+
const state = createMockState({ activeDocumentId: 'doc-1' });
|
|
24
|
+
expect(editorSelectors.activeDocumentId(state)).toBe('doc-1');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('activeDocument', () => {
|
|
29
|
+
it('should return undefined when no document is active', () => {
|
|
30
|
+
const state = createMockState();
|
|
31
|
+
expect(editorSelectors.activeDocument(state)).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return the active document', () => {
|
|
35
|
+
const doc = {
|
|
36
|
+
content: 'Hello',
|
|
37
|
+
editorData: null,
|
|
38
|
+
isDirty: false,
|
|
39
|
+
lastSavedContent: 'Hello',
|
|
40
|
+
lastUpdatedTime: null,
|
|
41
|
+
saveStatus: 'idle' as const,
|
|
42
|
+
sourceType: 'notebook' as const,
|
|
43
|
+
};
|
|
44
|
+
const state = createMockState({
|
|
45
|
+
activeDocumentId: 'doc-1',
|
|
46
|
+
documents: { 'doc-1': doc },
|
|
47
|
+
});
|
|
48
|
+
expect(editorSelectors.activeDocument(state)).toEqual(doc);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('isEditing', () => {
|
|
53
|
+
it('should return false when no document is active', () => {
|
|
54
|
+
const state = createMockState();
|
|
55
|
+
expect(editorSelectors.isEditing(state)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return true when a document is active', () => {
|
|
59
|
+
const state = createMockState({ activeDocumentId: 'doc-1' });
|
|
60
|
+
expect(editorSelectors.isEditing(state)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('documentById', () => {
|
|
65
|
+
it('should return undefined for non-existent document', () => {
|
|
66
|
+
const state = createMockState();
|
|
67
|
+
expect(editorSelectors.documentById('non-existent')(state)).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return the document by ID', () => {
|
|
71
|
+
const doc = {
|
|
72
|
+
content: 'Test',
|
|
73
|
+
editorData: null,
|
|
74
|
+
isDirty: false,
|
|
75
|
+
lastSavedContent: 'Test',
|
|
76
|
+
lastUpdatedTime: null,
|
|
77
|
+
saveStatus: 'idle' as const,
|
|
78
|
+
sourceType: 'page' as const,
|
|
79
|
+
};
|
|
80
|
+
const state = createMockState({
|
|
81
|
+
documents: { 'doc-1': doc },
|
|
82
|
+
});
|
|
83
|
+
expect(editorSelectors.documentById('doc-1')(state)).toEqual(doc);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('isDirty', () => {
|
|
88
|
+
it('should return false for non-existent document', () => {
|
|
89
|
+
const state = createMockState();
|
|
90
|
+
expect(editorSelectors.isDirty('non-existent')(state)).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should return isDirty status', () => {
|
|
94
|
+
const state = createMockState({
|
|
95
|
+
documents: {
|
|
96
|
+
'doc-1': {
|
|
97
|
+
content: '',
|
|
98
|
+
editorData: null,
|
|
99
|
+
isDirty: true,
|
|
100
|
+
lastSavedContent: '',
|
|
101
|
+
lastUpdatedTime: null,
|
|
102
|
+
saveStatus: 'idle' as const,
|
|
103
|
+
sourceType: 'notebook' as const,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
expect(editorSelectors.isDirty('doc-1')(state)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('saveStatus', () => {
|
|
112
|
+
it('should return idle for non-existent document', () => {
|
|
113
|
+
const state = createMockState();
|
|
114
|
+
expect(editorSelectors.saveStatus('non-existent')(state)).toBe('idle');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return save status', () => {
|
|
118
|
+
const state = createMockState({
|
|
119
|
+
documents: {
|
|
120
|
+
'doc-1': {
|
|
121
|
+
content: '',
|
|
122
|
+
editorData: null,
|
|
123
|
+
isDirty: false,
|
|
124
|
+
lastSavedContent: '',
|
|
125
|
+
lastUpdatedTime: null,
|
|
126
|
+
saveStatus: 'saving' as const,
|
|
127
|
+
sourceType: 'notebook' as const,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
expect(editorSelectors.saveStatus('doc-1')(state)).toBe('saving');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('content', () => {
|
|
136
|
+
it('should return empty string for non-existent document', () => {
|
|
137
|
+
const state = createMockState();
|
|
138
|
+
expect(editorSelectors.content('non-existent')(state)).toBe('');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return content', () => {
|
|
142
|
+
const state = createMockState({
|
|
143
|
+
documents: {
|
|
144
|
+
'doc-1': {
|
|
145
|
+
content: '# Hello',
|
|
146
|
+
editorData: null,
|
|
147
|
+
isDirty: false,
|
|
148
|
+
lastSavedContent: '',
|
|
149
|
+
lastUpdatedTime: null,
|
|
150
|
+
saveStatus: 'idle' as const,
|
|
151
|
+
sourceType: 'notebook' as const,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
expect(editorSelectors.content('doc-1')(state)).toBe('# Hello');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('editorData', () => {
|
|
160
|
+
it('should return undefined for non-existent document', () => {
|
|
161
|
+
const state = createMockState();
|
|
162
|
+
expect(editorSelectors.editorData('non-existent')(state)).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return editorData', () => {
|
|
166
|
+
const mockEditorData = { type: 'doc', content: [] };
|
|
167
|
+
const state = createMockState({
|
|
168
|
+
documents: {
|
|
169
|
+
'doc-1': {
|
|
170
|
+
content: '',
|
|
171
|
+
editorData: mockEditorData,
|
|
172
|
+
isDirty: false,
|
|
173
|
+
lastSavedContent: '',
|
|
174
|
+
lastUpdatedTime: null,
|
|
175
|
+
saveStatus: 'idle' as const,
|
|
176
|
+
sourceType: 'page' as const,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
expect(editorSelectors.editorData('doc-1')(state)).toEqual(mockEditorData);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('sourceType', () => {
|
|
185
|
+
it('should return undefined for non-existent document', () => {
|
|
186
|
+
const state = createMockState();
|
|
187
|
+
expect(editorSelectors.sourceType('non-existent')(state)).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should return sourceType', () => {
|
|
191
|
+
const state = createMockState({
|
|
192
|
+
documents: {
|
|
193
|
+
'doc-1': {
|
|
194
|
+
content: '',
|
|
195
|
+
editorData: null,
|
|
196
|
+
isDirty: false,
|
|
197
|
+
lastSavedContent: '',
|
|
198
|
+
lastUpdatedTime: null,
|
|
199
|
+
saveStatus: 'idle' as const,
|
|
200
|
+
sourceType: 'page' as const,
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
expect(editorSelectors.sourceType('doc-1')(state)).toBe('page');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe('activeIsDirty', () => {
|
|
209
|
+
it('should return false when no document is active', () => {
|
|
210
|
+
const state = createMockState();
|
|
211
|
+
expect(editorSelectors.activeIsDirty(state)).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should return isDirty of active document', () => {
|
|
215
|
+
const state = createMockState({
|
|
216
|
+
activeDocumentId: 'doc-1',
|
|
217
|
+
documents: {
|
|
218
|
+
'doc-1': {
|
|
219
|
+
content: '',
|
|
220
|
+
editorData: null,
|
|
221
|
+
isDirty: true,
|
|
222
|
+
lastSavedContent: '',
|
|
223
|
+
lastUpdatedTime: null,
|
|
224
|
+
saveStatus: 'idle' as const,
|
|
225
|
+
sourceType: 'notebook' as const,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
expect(editorSelectors.activeIsDirty(state)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('activeContent', () => {
|
|
234
|
+
it('should return empty string when no document is active', () => {
|
|
235
|
+
const state = createMockState();
|
|
236
|
+
expect(editorSelectors.activeContent(state)).toBe('');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should return content of active document', () => {
|
|
240
|
+
const state = createMockState({
|
|
241
|
+
activeDocumentId: 'doc-1',
|
|
242
|
+
documents: {
|
|
243
|
+
'doc-1': {
|
|
244
|
+
content: '# Active Doc',
|
|
245
|
+
editorData: null,
|
|
246
|
+
isDirty: false,
|
|
247
|
+
lastSavedContent: '',
|
|
248
|
+
lastUpdatedTime: null,
|
|
249
|
+
saveStatus: 'idle' as const,
|
|
250
|
+
sourceType: 'notebook' as const,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
expect(editorSelectors.activeContent(state)).toBe('# Active Doc');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('canSave', () => {
|
|
259
|
+
it('should return false when no document is active', () => {
|
|
260
|
+
const state = createMockState();
|
|
261
|
+
expect(editorSelectors.canSave(state)).toBeFalsy();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should return false when document is not dirty', () => {
|
|
265
|
+
const state = createMockState({
|
|
266
|
+
activeDocumentId: 'doc-1',
|
|
267
|
+
documents: {
|
|
268
|
+
'doc-1': {
|
|
269
|
+
content: '',
|
|
270
|
+
editorData: null,
|
|
271
|
+
isDirty: false,
|
|
272
|
+
lastSavedContent: '',
|
|
273
|
+
lastUpdatedTime: null,
|
|
274
|
+
saveStatus: 'idle' as const,
|
|
275
|
+
sourceType: 'notebook' as const,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
expect(editorSelectors.canSave(state)).toBeFalsy();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return false when document is saving', () => {
|
|
283
|
+
const state = createMockState({
|
|
284
|
+
activeDocumentId: 'doc-1',
|
|
285
|
+
documents: {
|
|
286
|
+
'doc-1': {
|
|
287
|
+
content: '',
|
|
288
|
+
editorData: null,
|
|
289
|
+
isDirty: true,
|
|
290
|
+
lastSavedContent: '',
|
|
291
|
+
lastUpdatedTime: null,
|
|
292
|
+
saveStatus: 'saving' as const,
|
|
293
|
+
sourceType: 'notebook' as const,
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
expect(editorSelectors.canSave(state)).toBeFalsy();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should return true when document is dirty and not saving', () => {
|
|
301
|
+
const state = createMockState({
|
|
302
|
+
activeDocumentId: 'doc-1',
|
|
303
|
+
documents: {
|
|
304
|
+
'doc-1': {
|
|
305
|
+
content: '',
|
|
306
|
+
editorData: null,
|
|
307
|
+
isDirty: true,
|
|
308
|
+
lastSavedContent: '',
|
|
309
|
+
lastUpdatedTime: null,
|
|
310
|
+
saveStatus: 'idle' as const,
|
|
311
|
+
sourceType: 'notebook' as const,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
expect(editorSelectors.canSave(state)).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('documentIds', () => {
|
|
320
|
+
it('should return empty array when no documents', () => {
|
|
321
|
+
const state = createMockState();
|
|
322
|
+
expect(editorSelectors.documentIds(state)).toEqual([]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should return all document IDs', () => {
|
|
326
|
+
const state = createMockState({
|
|
327
|
+
documents: {
|
|
328
|
+
'doc-1': {} as any,
|
|
329
|
+
'doc-2': {} as any,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
expect(editorSelectors.documentIds(state)).toEqual(['doc-1', 'doc-2']);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('documentCount', () => {
|
|
337
|
+
it('should return 0 when no documents', () => {
|
|
338
|
+
const state = createMockState();
|
|
339
|
+
expect(editorSelectors.documentCount(state)).toBe(0);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should return document count', () => {
|
|
343
|
+
const state = createMockState({
|
|
344
|
+
documents: {
|
|
345
|
+
'doc-1': {} as any,
|
|
346
|
+
'doc-2': {} as any,
|
|
347
|
+
'doc-3': {} as any,
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
expect(editorSelectors.documentCount(state)).toBe(3);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('hasDocument', () => {
|
|
355
|
+
it('should return false for non-existent document', () => {
|
|
356
|
+
const state = createMockState();
|
|
357
|
+
expect(editorSelectors.hasDocument('non-existent')(state)).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should return true for existing document', () => {
|
|
361
|
+
const state = createMockState({
|
|
362
|
+
documents: {
|
|
363
|
+
'doc-1': {} as any,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
expect(editorSelectors.hasDocument('doc-1')(state)).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe('editor', () => {
|
|
371
|
+
it('should return undefined when no editor', () => {
|
|
372
|
+
const state = createMockState();
|
|
373
|
+
expect(editorSelectors.editor(state)).toBeUndefined();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should return editor instance', () => {
|
|
377
|
+
const mockEditor = { focus: () => {} } as any;
|
|
378
|
+
const state = createMockState({ editor: mockEditor });
|
|
379
|
+
expect(editorSelectors.editor(state)).toBe(mockEditor);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('editorState', () => {
|
|
384
|
+
it('should return undefined when no editorState', () => {
|
|
385
|
+
const state = createMockState();
|
|
386
|
+
expect(editorSelectors.editorState(state)).toBeUndefined();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should return editorState', () => {
|
|
390
|
+
const mockEditorState = { isBold: true } as any;
|
|
391
|
+
const state = createMockState({ editorState: mockEditorState });
|
|
392
|
+
expect(editorSelectors.editorState(state)).toBe(mockEditorState);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -1,16 +1,118 @@
|
|
|
1
|
+
/* eslint-disable sort-keys-fix/sort-keys-fix */
|
|
1
2
|
import type { DocumentStore } from '../../store';
|
|
3
|
+
import type { EditorContentState } from './initialState';
|
|
4
|
+
|
|
5
|
+
// ===== Active Document Selectors =====
|
|
6
|
+
|
|
7
|
+
const activeDocumentId = (s: DocumentStore) => s.activeDocumentId;
|
|
8
|
+
|
|
9
|
+
const activeDocument = (s: DocumentStore): EditorContentState | undefined =>
|
|
10
|
+
s.activeDocumentId ? s.documents[s.activeDocumentId] : undefined;
|
|
2
11
|
|
|
3
12
|
const isEditing = (s: DocumentStore) => !!s.activeDocumentId;
|
|
4
13
|
|
|
5
|
-
|
|
14
|
+
// ===== Document by ID Selectors =====
|
|
15
|
+
|
|
16
|
+
const documentById = (id: string) => (s: DocumentStore) => s.documents[id];
|
|
17
|
+
|
|
18
|
+
const isDirty = (id: string) => (s: DocumentStore) => s.documents[id]?.isDirty ?? false;
|
|
19
|
+
|
|
20
|
+
const saveStatus = (id: string) => (s: DocumentStore) => s.documents[id]?.saveStatus ?? 'idle';
|
|
21
|
+
|
|
22
|
+
const content = (id: string) => (s: DocumentStore) => s.documents[id]?.content ?? '';
|
|
23
|
+
|
|
24
|
+
const editorData = (id: string) => (s: DocumentStore) => s.documents[id]?.editorData;
|
|
25
|
+
|
|
26
|
+
const sourceType = (id: string) => (s: DocumentStore) => s.documents[id]?.sourceType;
|
|
27
|
+
|
|
28
|
+
const lastUpdatedTime = (id: string) => (s: DocumentStore) =>
|
|
29
|
+
s.documents[id]?.lastUpdatedTime?.toISOString();
|
|
30
|
+
|
|
31
|
+
// ===== Active Document Convenience Selectors =====
|
|
32
|
+
|
|
33
|
+
const activeIsDirty = (s: DocumentStore) => {
|
|
34
|
+
const doc = activeDocument(s);
|
|
35
|
+
return doc?.isDirty ?? false;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const activeSaveStatus = (s: DocumentStore) => {
|
|
39
|
+
const doc = activeDocument(s);
|
|
40
|
+
return doc?.saveStatus ?? 'idle';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const activeContent = (s: DocumentStore) => {
|
|
44
|
+
const doc = activeDocument(s);
|
|
45
|
+
return doc?.content ?? '';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const activeEditorData = (s: DocumentStore) => {
|
|
49
|
+
const doc = activeDocument(s);
|
|
50
|
+
return doc?.editorData;
|
|
51
|
+
};
|
|
6
52
|
|
|
7
|
-
const
|
|
53
|
+
const activeSourceType = (s: DocumentStore) => {
|
|
54
|
+
const doc = activeDocument(s);
|
|
55
|
+
return doc?.sourceType;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const activeLastUpdatedTime = (s: DocumentStore) => {
|
|
59
|
+
const doc = activeDocument(s);
|
|
60
|
+
return doc?.lastUpdatedTime;
|
|
61
|
+
};
|
|
8
62
|
|
|
9
|
-
|
|
63
|
+
// ===== Editor State Selectors =====
|
|
64
|
+
|
|
65
|
+
const editor = (s: DocumentStore) => s.editor;
|
|
66
|
+
|
|
67
|
+
const editorState = (s: DocumentStore) => s.editorState;
|
|
68
|
+
|
|
69
|
+
const canSave = (s: DocumentStore) => {
|
|
70
|
+
const doc = activeDocument(s);
|
|
71
|
+
return doc?.isDirty && doc?.saveStatus !== 'saving';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ===== Document List Selectors =====
|
|
75
|
+
|
|
76
|
+
const documentIds = (s: DocumentStore) => Object.keys(s.documents);
|
|
77
|
+
|
|
78
|
+
const documentCount = (s: DocumentStore) => Object.keys(s.documents).length;
|
|
79
|
+
|
|
80
|
+
const hasDocument = (id: string) => (s: DocumentStore) => id in s.documents;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if a document is still loading (not yet in the store)
|
|
84
|
+
*/
|
|
85
|
+
const isDocumentLoading = (id: string | undefined) => (s: DocumentStore) => !id || !s.documents[id];
|
|
10
86
|
|
|
11
87
|
export const editorSelectors = {
|
|
88
|
+
// Active document
|
|
89
|
+
activeContent,
|
|
90
|
+
activeDocument,
|
|
91
|
+
activeDocumentId,
|
|
92
|
+
activeEditorData,
|
|
93
|
+
activeIsDirty,
|
|
94
|
+
activeLastUpdatedTime,
|
|
95
|
+
activeSaveStatus,
|
|
96
|
+
activeSourceType,
|
|
97
|
+
|
|
98
|
+
// By ID
|
|
99
|
+
content,
|
|
100
|
+
documentById,
|
|
101
|
+
editorData,
|
|
102
|
+
hasDocument,
|
|
103
|
+
isDocumentLoading,
|
|
104
|
+
isDirty,
|
|
105
|
+
lastUpdatedTime,
|
|
106
|
+
saveStatus,
|
|
107
|
+
sourceType,
|
|
108
|
+
|
|
109
|
+
// Editor
|
|
12
110
|
canSave,
|
|
13
|
-
|
|
111
|
+
editor,
|
|
112
|
+
editorState,
|
|
14
113
|
isEditing,
|
|
15
|
-
|
|
114
|
+
|
|
115
|
+
// List
|
|
116
|
+
documentCount,
|
|
117
|
+
documentIds,
|
|
16
118
|
};
|
|
@@ -3,35 +3,34 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|
|
3
3
|
import { type StateCreator } from 'zustand/vanilla';
|
|
4
4
|
|
|
5
5
|
import { createDevtools } from '../middleware/createDevtools';
|
|
6
|
-
import { type
|
|
6
|
+
import { type DocumentAction, createDocumentSlice } from './slices/document';
|
|
7
7
|
import {
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './slices/
|
|
8
|
+
type EditorAction,
|
|
9
|
+
type EditorState,
|
|
10
|
+
createEditorSlice,
|
|
11
|
+
initialEditorState,
|
|
12
|
+
} from './slices/editor';
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
export type DocumentState = EditorState
|
|
14
|
+
// State type
|
|
15
|
+
export type DocumentState = EditorState;
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
export type
|
|
17
|
+
// Action type
|
|
18
|
+
export type DocumentStoreAction = DocumentAction & EditorAction;
|
|
19
19
|
|
|
20
20
|
// Full store type
|
|
21
|
-
export type DocumentStore = DocumentState &
|
|
21
|
+
export type DocumentStore = DocumentState & DocumentStoreAction;
|
|
22
22
|
|
|
23
23
|
// Initial state
|
|
24
24
|
const initialState: DocumentState = {
|
|
25
25
|
...initialEditorState,
|
|
26
|
-
...initialNotebookState,
|
|
27
26
|
};
|
|
28
27
|
|
|
29
28
|
const createStore: StateCreator<DocumentStore, [['zustand/devtools', never]]> = (
|
|
30
29
|
...parameters
|
|
31
30
|
) => ({
|
|
32
31
|
...initialState,
|
|
32
|
+
...createDocumentSlice(...parameters),
|
|
33
33
|
...createEditorSlice(...parameters),
|
|
34
|
-
...createNotebookSlice(...parameters),
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
const devtools = createDevtools('document');
|