@wordpress/core-data 7.39.1-next.v.202602091733.0 → 7.40.0
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 +2 -0
- package/README.md +41 -0
- package/build/actions.cjs +52 -0
- package/build/actions.cjs.map +2 -2
- package/build/awareness/base-awareness.cjs +1 -8
- package/build/awareness/base-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/awareness/utils.cjs +8 -51
- package/build/awareness/utils.cjs.map +2 -2
- package/build/entities.cjs +7 -1
- package/build/entities.cjs.map +2 -2
- package/build/hooks/use-entity-block-editor.cjs +13 -19
- package/build/hooks/use-entity-block-editor.cjs.map +2 -2
- package/build/index.cjs +6 -1
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +8 -0
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +2 -1
- package/build/private-apis.cjs.map +2 -2
- package/build/private-selectors.cjs +5 -0
- package/build/private-selectors.cjs.map +2 -2
- package/build/reducer.cjs +31 -1
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +26 -1
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +15 -0
- package/build/selectors.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +5 -3
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt.cjs +23 -19
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +50 -0
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/base-awareness.mjs +1 -8
- package/build-module/awareness/base-awareness.mjs.map +2 -2
- package/build-module/awareness/utils.mjs +8 -51
- package/build-module/awareness/utils.mjs.map +2 -2
- package/build-module/entities.mjs +7 -1
- package/build-module/entities.mjs.map +2 -2
- package/build-module/hooks/use-entity-block-editor.mjs +13 -19
- package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
- package/build-module/index.mjs +3 -0
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +7 -0
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +6 -2
- package/build-module/private-apis.mjs.map +2 -2
- package/build-module/private-selectors.mjs +8 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/reducer.mjs +29 -1
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +25 -1
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +14 -0
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +3 -2
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +25 -20
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +12 -0
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts.map +1 -1
- package/build-types/awareness/test/awareness-state.d.ts +2 -0
- package/build-types/awareness/test/awareness-state.d.ts.map +1 -0
- package/build-types/awareness/test/base-awareness.d.ts +2 -0
- package/build-types/awareness/test/base-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/post-editor-awareness.d.ts +2 -0
- package/build-types/awareness/test/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/typed-awareness.d.ts +2 -0
- package/build-types/awareness/test/typed-awareness.d.ts.map +1 -0
- package/build-types/awareness/test/utils.d.ts +2 -0
- package/build-types/awareness/test/utils.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +0 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/awareness/utils.d.ts +2 -3
- package/build-types/awareness/utils.d.ts.map +1 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/hooks/test/use-post-editor-awareness-state.d.ts +2 -0
- package/build-types/hooks/test/use-post-editor-awareness-state.d.ts.map +1 -0
- package/build-types/hooks/use-entity-block-editor.d.ts.map +1 -1
- package/build-types/index.d.ts +5 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-actions.d.ts +8 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +8 -1
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/reducer.d.ts +23 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts +3 -0
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +17 -0
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +9 -0
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +6 -4
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +78 -0
- package/src/awareness/base-awareness.ts +1 -14
- package/src/awareness/test/awareness-state.ts +417 -0
- package/src/awareness/test/base-awareness.ts +321 -0
- package/src/awareness/test/post-editor-awareness.ts +561 -0
- package/src/awareness/test/typed-awareness.ts +148 -0
- package/src/awareness/test/utils.ts +305 -0
- package/src/awareness/types.ts +0 -1
- package/src/awareness/utils.ts +8 -82
- package/src/entities.js +7 -1
- package/src/hooks/test/use-post-editor-awareness-state.ts +477 -0
- package/src/hooks/use-entity-block-editor.js +15 -21
- package/src/index.js +7 -0
- package/src/private-actions.js +14 -0
- package/src/private-apis.js +5 -1
- package/src/private-selectors.ts +16 -1
- package/src/reducer.js +45 -0
- package/src/resolvers.js +31 -2
- package/src/selectors.ts +41 -0
- package/src/test/actions.js +79 -0
- package/src/test/entity-provider.js +74 -0
- package/src/test/resolvers.js +2 -0
- package/src/test/store.js +30 -0
- package/src/utils/crdt-blocks.ts +2 -2
- package/src/utils/crdt.ts +44 -29
- package/src/utils/test/crdt.ts +212 -7
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
useActiveCollaborators,
|
|
11
|
+
useGetAbsolutePositionIndex,
|
|
12
|
+
useGetDebugData,
|
|
13
|
+
useIsDisconnected,
|
|
14
|
+
} from '../use-post-editor-awareness-state';
|
|
15
|
+
import { getSyncManager } from '../../sync';
|
|
16
|
+
import { SelectionType } from '../../utils/crdt-user-selections';
|
|
17
|
+
import type {
|
|
18
|
+
PostEditorAwarenessState,
|
|
19
|
+
YDocDebugData,
|
|
20
|
+
} from '../../awareness/types';
|
|
21
|
+
import type { SelectionCursor } from '../../types';
|
|
22
|
+
|
|
23
|
+
// Mock the sync module
|
|
24
|
+
jest.mock( '../../sync', () => ( {
|
|
25
|
+
getSyncManager: jest.fn(),
|
|
26
|
+
} ) );
|
|
27
|
+
|
|
28
|
+
const mockAvatarUrls = {
|
|
29
|
+
'24': 'https://example.com/avatar-24.png',
|
|
30
|
+
'48': 'https://example.com/avatar-48.png',
|
|
31
|
+
'96': 'https://example.com/avatar-96.png',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createMockActiveUser = (
|
|
35
|
+
overrides: Partial< PostEditorAwarenessState > = {}
|
|
36
|
+
): PostEditorAwarenessState => ( {
|
|
37
|
+
clientId: 12345,
|
|
38
|
+
isMe: false,
|
|
39
|
+
isConnected: true,
|
|
40
|
+
collaboratorInfo: {
|
|
41
|
+
id: 1,
|
|
42
|
+
name: 'Test User',
|
|
43
|
+
slug: 'test-user',
|
|
44
|
+
avatar_urls: mockAvatarUrls,
|
|
45
|
+
browserType: 'Chrome',
|
|
46
|
+
enteredAt: 1704067200000,
|
|
47
|
+
},
|
|
48
|
+
editorState: {
|
|
49
|
+
selection: {
|
|
50
|
+
type: SelectionType.None,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
...overrides,
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
const createMockDebugData = (): YDocDebugData => ( {
|
|
57
|
+
doc: { testKey: 'testValue' },
|
|
58
|
+
clients: {},
|
|
59
|
+
collaboratorMap: {},
|
|
60
|
+
} );
|
|
61
|
+
|
|
62
|
+
describe( 'use-post-editor-awareness-state hooks', () => {
|
|
63
|
+
let mockAwareness: {
|
|
64
|
+
setUp: jest.Mock;
|
|
65
|
+
getCurrentState: jest.Mock;
|
|
66
|
+
onStateChange: jest.Mock;
|
|
67
|
+
getAbsolutePositionIndex: jest.Mock;
|
|
68
|
+
getDebugData: jest.Mock;
|
|
69
|
+
};
|
|
70
|
+
let mockSyncManager: {
|
|
71
|
+
getAwareness: jest.Mock;
|
|
72
|
+
};
|
|
73
|
+
let stateChangeCallback:
|
|
74
|
+
| ( ( newState: PostEditorAwarenessState[] ) => void )
|
|
75
|
+
| null;
|
|
76
|
+
|
|
77
|
+
beforeEach( () => {
|
|
78
|
+
stateChangeCallback = null;
|
|
79
|
+
|
|
80
|
+
mockAwareness = {
|
|
81
|
+
setUp: jest.fn(),
|
|
82
|
+
getCurrentState: jest.fn().mockReturnValue( [] ),
|
|
83
|
+
onStateChange: jest.fn( ( callback ) => {
|
|
84
|
+
stateChangeCallback = callback;
|
|
85
|
+
return jest.fn(); // unsubscribe function
|
|
86
|
+
} ),
|
|
87
|
+
getAbsolutePositionIndex: jest.fn().mockReturnValue( null ),
|
|
88
|
+
getDebugData: jest.fn().mockReturnValue( createMockDebugData() ),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
mockSyncManager = {
|
|
92
|
+
getAwareness: jest.fn().mockReturnValue( mockAwareness ),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
( getSyncManager as jest.Mock ).mockReturnValue( mockSyncManager );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
afterEach( () => {
|
|
99
|
+
jest.clearAllMocks();
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
describe( 'useActiveUsers', () => {
|
|
103
|
+
test( 'should return empty array when postId is null', () => {
|
|
104
|
+
const { result } = renderHook( () =>
|
|
105
|
+
useActiveCollaborators( null, 'post' )
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect( result.current ).toEqual( [] );
|
|
109
|
+
expect( mockSyncManager.getAwareness ).not.toHaveBeenCalled();
|
|
110
|
+
} );
|
|
111
|
+
|
|
112
|
+
test( 'should return empty array when postType is null', () => {
|
|
113
|
+
const { result } = renderHook( () =>
|
|
114
|
+
useActiveCollaborators( 123, null )
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect( result.current ).toEqual( [] );
|
|
118
|
+
expect( mockSyncManager.getAwareness ).not.toHaveBeenCalled();
|
|
119
|
+
} );
|
|
120
|
+
|
|
121
|
+
test( 'should return empty array when getSyncManager returns undefined', () => {
|
|
122
|
+
( getSyncManager as jest.Mock ).mockReturnValue( undefined );
|
|
123
|
+
|
|
124
|
+
const { result } = renderHook( () =>
|
|
125
|
+
useActiveCollaborators( 123, 'post' )
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect( result.current ).toEqual( [] );
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
test( 'should return empty array when awareness is not available', () => {
|
|
132
|
+
mockSyncManager.getAwareness.mockReturnValue( undefined );
|
|
133
|
+
|
|
134
|
+
const { result } = renderHook( () =>
|
|
135
|
+
useActiveCollaborators( 123, 'post' )
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect( result.current ).toEqual( [] );
|
|
139
|
+
} );
|
|
140
|
+
|
|
141
|
+
test( 'should call getAwareness with correct parameters', () => {
|
|
142
|
+
renderHook( () => useActiveCollaborators( 123, 'post' ) );
|
|
143
|
+
|
|
144
|
+
expect( mockSyncManager.getAwareness ).toHaveBeenCalledWith(
|
|
145
|
+
'postType/post',
|
|
146
|
+
'123'
|
|
147
|
+
);
|
|
148
|
+
} );
|
|
149
|
+
|
|
150
|
+
test( 'should call awareness.setUp', () => {
|
|
151
|
+
renderHook( () => useActiveCollaborators( 123, 'post' ) );
|
|
152
|
+
|
|
153
|
+
expect( mockAwareness.setUp ).toHaveBeenCalled();
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
test( 'should return initial state from getCurrentState', () => {
|
|
157
|
+
const mockUsers = [ createMockActiveUser() ];
|
|
158
|
+
mockAwareness.getCurrentState.mockReturnValue( mockUsers );
|
|
159
|
+
|
|
160
|
+
const { result } = renderHook( () =>
|
|
161
|
+
useActiveCollaborators( 123, 'post' )
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect( result.current ).toEqual( mockUsers );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
test( 'should subscribe to state changes', () => {
|
|
168
|
+
renderHook( () => useActiveCollaborators( 123, 'post' ) );
|
|
169
|
+
|
|
170
|
+
expect( mockAwareness.onStateChange ).toHaveBeenCalled();
|
|
171
|
+
} );
|
|
172
|
+
|
|
173
|
+
test( 'should update state when awareness emits changes', async () => {
|
|
174
|
+
const initialUsers: PostEditorAwarenessState[] = [];
|
|
175
|
+
const updatedUsers = [ createMockActiveUser() ];
|
|
176
|
+
|
|
177
|
+
mockAwareness.getCurrentState.mockReturnValue( initialUsers );
|
|
178
|
+
|
|
179
|
+
const { result } = renderHook( () =>
|
|
180
|
+
useActiveCollaborators( 123, 'post' )
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect( result.current ).toEqual( initialUsers );
|
|
184
|
+
|
|
185
|
+
// Simulate awareness state change
|
|
186
|
+
act( () => {
|
|
187
|
+
stateChangeCallback?.( updatedUsers );
|
|
188
|
+
} );
|
|
189
|
+
|
|
190
|
+
await waitFor( () => {
|
|
191
|
+
expect( result.current ).toEqual( updatedUsers );
|
|
192
|
+
} );
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
test( 'should unsubscribe when postId changes', () => {
|
|
196
|
+
const unsubscribe = jest.fn();
|
|
197
|
+
mockAwareness.onStateChange.mockReturnValue( unsubscribe );
|
|
198
|
+
|
|
199
|
+
const { rerender } = renderHook(
|
|
200
|
+
( { postId } ) => useActiveCollaborators( postId, 'post' ),
|
|
201
|
+
{ initialProps: { postId: 123 as number | null } }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect( unsubscribe ).not.toHaveBeenCalled();
|
|
205
|
+
|
|
206
|
+
rerender( { postId: 456 } );
|
|
207
|
+
|
|
208
|
+
expect( unsubscribe ).toHaveBeenCalled();
|
|
209
|
+
} );
|
|
210
|
+
|
|
211
|
+
test( 'should unsubscribe when postType changes', () => {
|
|
212
|
+
const unsubscribe = jest.fn();
|
|
213
|
+
mockAwareness.onStateChange.mockReturnValue( unsubscribe );
|
|
214
|
+
|
|
215
|
+
const { rerender } = renderHook(
|
|
216
|
+
( { postType } ) => useActiveCollaborators( 123, postType ),
|
|
217
|
+
{ initialProps: { postType: 'post' as string | null } }
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect( unsubscribe ).not.toHaveBeenCalled();
|
|
221
|
+
|
|
222
|
+
rerender( { postType: 'page' } );
|
|
223
|
+
|
|
224
|
+
expect( unsubscribe ).toHaveBeenCalled();
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
test( 'should reset state when postId becomes null', () => {
|
|
228
|
+
const mockUsers = [ createMockActiveUser() ];
|
|
229
|
+
mockAwareness.getCurrentState.mockReturnValue( mockUsers );
|
|
230
|
+
|
|
231
|
+
const { result, rerender } = renderHook(
|
|
232
|
+
( { postId } ) => useActiveCollaborators( postId, 'post' ),
|
|
233
|
+
{ initialProps: { postId: 123 as number | null } }
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
expect( result.current ).toEqual( mockUsers );
|
|
237
|
+
|
|
238
|
+
rerender( { postId: null } );
|
|
239
|
+
|
|
240
|
+
expect( result.current ).toEqual( [] );
|
|
241
|
+
} );
|
|
242
|
+
} );
|
|
243
|
+
|
|
244
|
+
describe( 'useGetAbsolutePositionIndex', () => {
|
|
245
|
+
test( 'should return function that returns null when postId is null', () => {
|
|
246
|
+
const { result } = renderHook( () =>
|
|
247
|
+
useGetAbsolutePositionIndex( null, 'post' )
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const mockSelection: SelectionCursor = {
|
|
251
|
+
type: SelectionType.Cursor,
|
|
252
|
+
blockId: 'block-1',
|
|
253
|
+
cursorPosition: {
|
|
254
|
+
relativePosition: {} as any,
|
|
255
|
+
absoluteOffset: 5,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
expect( result.current( mockSelection ) ).toBeNull();
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
test( 'should call awareness.getAbsolutePositionIndex with selection', () => {
|
|
263
|
+
const mockSelection: SelectionCursor = {
|
|
264
|
+
type: SelectionType.Cursor,
|
|
265
|
+
blockId: 'block-1',
|
|
266
|
+
cursorPosition: {
|
|
267
|
+
relativePosition: {} as any,
|
|
268
|
+
absoluteOffset: 5,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
mockAwareness.getAbsolutePositionIndex.mockReturnValue( 10 );
|
|
272
|
+
|
|
273
|
+
const { result } = renderHook( () =>
|
|
274
|
+
useGetAbsolutePositionIndex( 123, 'post' )
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const position = result.current( mockSelection );
|
|
278
|
+
|
|
279
|
+
expect(
|
|
280
|
+
mockAwareness.getAbsolutePositionIndex
|
|
281
|
+
).toHaveBeenCalledWith( mockSelection );
|
|
282
|
+
expect( position ).toBe( 10 );
|
|
283
|
+
} );
|
|
284
|
+
} );
|
|
285
|
+
|
|
286
|
+
describe( 'useGetDebugData', () => {
|
|
287
|
+
test( 'should return default debug data when postId is null', () => {
|
|
288
|
+
const { result } = renderHook( () =>
|
|
289
|
+
useGetDebugData( null, 'post' )
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect( result.current ).toEqual( {
|
|
293
|
+
doc: {},
|
|
294
|
+
clients: {},
|
|
295
|
+
collaboratorMap: {},
|
|
296
|
+
} );
|
|
297
|
+
} );
|
|
298
|
+
|
|
299
|
+
test( 'should call awareness.getDebugData and return result', () => {
|
|
300
|
+
const mockDebugData = createMockDebugData();
|
|
301
|
+
mockAwareness.getDebugData.mockReturnValue( mockDebugData );
|
|
302
|
+
|
|
303
|
+
const { result } = renderHook( () =>
|
|
304
|
+
useGetDebugData( 123, 'post' )
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect( result.current ).toEqual( mockDebugData );
|
|
308
|
+
} );
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
describe( 'useIsDisconnected', () => {
|
|
312
|
+
test( 'should return false when postId is null', () => {
|
|
313
|
+
const { result } = renderHook( () =>
|
|
314
|
+
useIsDisconnected( null, 'post' )
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect( result.current ).toBe( false );
|
|
318
|
+
} );
|
|
319
|
+
|
|
320
|
+
test( 'should return false when current user is connected', () => {
|
|
321
|
+
const connectedUser = createMockActiveUser( {
|
|
322
|
+
isMe: true,
|
|
323
|
+
isConnected: true,
|
|
324
|
+
} );
|
|
325
|
+
mockAwareness.getCurrentState.mockReturnValue( [ connectedUser ] );
|
|
326
|
+
|
|
327
|
+
const { result } = renderHook( () =>
|
|
328
|
+
useIsDisconnected( 123, 'post' )
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect( result.current ).toBe( false );
|
|
332
|
+
} );
|
|
333
|
+
|
|
334
|
+
test( 'should return true when current user is disconnected', () => {
|
|
335
|
+
const disconnectedUser = createMockActiveUser( {
|
|
336
|
+
isMe: true,
|
|
337
|
+
isConnected: false,
|
|
338
|
+
} );
|
|
339
|
+
mockAwareness.getCurrentState.mockReturnValue( [
|
|
340
|
+
disconnectedUser,
|
|
341
|
+
] );
|
|
342
|
+
|
|
343
|
+
const { result } = renderHook( () =>
|
|
344
|
+
useIsDisconnected( 123, 'post' )
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
expect( result.current ).toBe( true );
|
|
348
|
+
} );
|
|
349
|
+
|
|
350
|
+
test( 'should return false when no user is marked as me', () => {
|
|
351
|
+
const otherUser = createMockActiveUser( {
|
|
352
|
+
isMe: false,
|
|
353
|
+
isConnected: false,
|
|
354
|
+
} );
|
|
355
|
+
mockAwareness.getCurrentState.mockReturnValue( [ otherUser ] );
|
|
356
|
+
|
|
357
|
+
const { result } = renderHook( () =>
|
|
358
|
+
useIsDisconnected( 123, 'post' )
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect( result.current ).toBe( false );
|
|
362
|
+
} );
|
|
363
|
+
|
|
364
|
+
test( 'should update when state changes to disconnected', async () => {
|
|
365
|
+
const connectedUser = createMockActiveUser( {
|
|
366
|
+
isMe: true,
|
|
367
|
+
isConnected: true,
|
|
368
|
+
} );
|
|
369
|
+
const disconnectedUser = createMockActiveUser( {
|
|
370
|
+
isMe: true,
|
|
371
|
+
isConnected: false,
|
|
372
|
+
} );
|
|
373
|
+
|
|
374
|
+
mockAwareness.getCurrentState.mockReturnValue( [ connectedUser ] );
|
|
375
|
+
|
|
376
|
+
const { result } = renderHook( () =>
|
|
377
|
+
useIsDisconnected( 123, 'post' )
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect( result.current ).toBe( false );
|
|
381
|
+
|
|
382
|
+
// Simulate disconnection
|
|
383
|
+
act( () => {
|
|
384
|
+
stateChangeCallback?.( [ disconnectedUser ] );
|
|
385
|
+
} );
|
|
386
|
+
|
|
387
|
+
await waitFor( () => {
|
|
388
|
+
expect( result.current ).toBe( true );
|
|
389
|
+
} );
|
|
390
|
+
} );
|
|
391
|
+
} );
|
|
392
|
+
|
|
393
|
+
describe( 'hook cleanup', () => {
|
|
394
|
+
test( 'should unsubscribe on unmount', () => {
|
|
395
|
+
const unsubscribe = jest.fn();
|
|
396
|
+
mockAwareness.onStateChange.mockReturnValue( unsubscribe );
|
|
397
|
+
|
|
398
|
+
const { unmount } = renderHook( () =>
|
|
399
|
+
useActiveCollaborators( 123, 'post' )
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect( unsubscribe ).not.toHaveBeenCalled();
|
|
403
|
+
|
|
404
|
+
unmount();
|
|
405
|
+
|
|
406
|
+
expect( unsubscribe ).toHaveBeenCalled();
|
|
407
|
+
} );
|
|
408
|
+
} );
|
|
409
|
+
|
|
410
|
+
describe( 'multiple users scenario', () => {
|
|
411
|
+
test( 'should handle multiple active users', () => {
|
|
412
|
+
const user1 = createMockActiveUser( {
|
|
413
|
+
clientId: 1,
|
|
414
|
+
isMe: true,
|
|
415
|
+
collaboratorInfo: {
|
|
416
|
+
id: 1,
|
|
417
|
+
name: 'User One',
|
|
418
|
+
slug: 'user-one',
|
|
419
|
+
avatar_urls: mockAvatarUrls,
|
|
420
|
+
browserType: 'Chrome',
|
|
421
|
+
enteredAt: 1704067200000,
|
|
422
|
+
},
|
|
423
|
+
} );
|
|
424
|
+
const user2 = createMockActiveUser( {
|
|
425
|
+
clientId: 2,
|
|
426
|
+
isMe: false,
|
|
427
|
+
collaboratorInfo: {
|
|
428
|
+
id: 2,
|
|
429
|
+
name: 'User Two',
|
|
430
|
+
slug: 'user-two',
|
|
431
|
+
avatar_urls: mockAvatarUrls,
|
|
432
|
+
browserType: 'Firefox',
|
|
433
|
+
enteredAt: 1704067300000,
|
|
434
|
+
},
|
|
435
|
+
} );
|
|
436
|
+
|
|
437
|
+
mockAwareness.getCurrentState.mockReturnValue( [ user1, user2 ] );
|
|
438
|
+
|
|
439
|
+
const { result } = renderHook( () =>
|
|
440
|
+
useActiveCollaborators( 123, 'post' )
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
expect( result.current ).toHaveLength( 2 );
|
|
444
|
+
expect( result.current[ 0 ].collaboratorInfo.name ).toBe(
|
|
445
|
+
'User One'
|
|
446
|
+
);
|
|
447
|
+
expect( result.current[ 1 ].collaboratorInfo.name ).toBe(
|
|
448
|
+
'User Two'
|
|
449
|
+
);
|
|
450
|
+
} );
|
|
451
|
+
|
|
452
|
+
test( 'should identify correct user as disconnected among multiple', () => {
|
|
453
|
+
const meConnected = createMockActiveUser( {
|
|
454
|
+
clientId: 1,
|
|
455
|
+
isMe: true,
|
|
456
|
+
isConnected: true,
|
|
457
|
+
} );
|
|
458
|
+
const otherDisconnected = createMockActiveUser( {
|
|
459
|
+
clientId: 2,
|
|
460
|
+
isMe: false,
|
|
461
|
+
isConnected: false,
|
|
462
|
+
} );
|
|
463
|
+
|
|
464
|
+
mockAwareness.getCurrentState.mockReturnValue( [
|
|
465
|
+
meConnected,
|
|
466
|
+
otherDisconnected,
|
|
467
|
+
] );
|
|
468
|
+
|
|
469
|
+
const { result } = renderHook( () =>
|
|
470
|
+
useIsDisconnected( 123, 'post' )
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Should be false because *I* am connected (other user's status doesn't matter)
|
|
474
|
+
expect( result.current ).toBe( false );
|
|
475
|
+
} );
|
|
476
|
+
} );
|
|
477
|
+
} );
|
|
@@ -13,7 +13,7 @@ import useEntityId from './use-entity-id';
|
|
|
13
13
|
import { updateFootnotesFromMeta } from '../footnotes';
|
|
14
14
|
|
|
15
15
|
const EMPTY_ARRAY = [];
|
|
16
|
-
const parsedBlocksCache = new
|
|
16
|
+
const parsedBlocksCache = new Map();
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Hook that returns block content getters and setters for
|
|
@@ -36,7 +36,6 @@ const parsedBlocksCache = new WeakMap();
|
|
|
36
36
|
export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
|
|
37
37
|
const providerId = useEntityId( kind, name );
|
|
38
38
|
const id = _id ?? providerId;
|
|
39
|
-
const { getEntityRecord, getEntityRecordEdits } = useSelect( STORE_NAME );
|
|
40
39
|
const { content, editedBlocks, meta } = useSelect(
|
|
41
40
|
( select ) => {
|
|
42
41
|
if ( ! id ) {
|
|
@@ -68,28 +67,21 @@ export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
|
|
|
68
67
|
return EMPTY_ARRAY;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
let _blocks = parsedBlocksCache.get( cackeKey );
|
|
70
|
+
// Cache parsed blocks by entity identity. Store the content
|
|
71
|
+
// alongside the blocks so we can validate it hasn't changed.
|
|
72
|
+
const cacheKey = `${ kind }:${ name }:${ id }`;
|
|
73
|
+
const cached = parsedBlocksCache.get( cacheKey );
|
|
74
|
+
let _blocks;
|
|
77
75
|
|
|
78
|
-
if (
|
|
76
|
+
if ( cached && cached.content === content ) {
|
|
77
|
+
_blocks = cached.blocks;
|
|
78
|
+
} else {
|
|
79
79
|
_blocks = parse( content );
|
|
80
|
-
parsedBlocksCache.set(
|
|
80
|
+
parsedBlocksCache.set( cacheKey, { content, blocks: _blocks } );
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return _blocks;
|
|
84
|
-
}, [
|
|
85
|
-
kind,
|
|
86
|
-
name,
|
|
87
|
-
id,
|
|
88
|
-
editedBlocks,
|
|
89
|
-
content,
|
|
90
|
-
getEntityRecord,
|
|
91
|
-
getEntityRecordEdits,
|
|
92
|
-
] );
|
|
84
|
+
}, [ kind, name, id, editedBlocks, content ] );
|
|
93
85
|
|
|
94
86
|
const onChange = useCallback(
|
|
95
87
|
( newBlocks, options ) => {
|
|
@@ -128,8 +120,10 @@ export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
|
|
|
128
120
|
const onInput = useCallback(
|
|
129
121
|
( newBlocks, options ) => {
|
|
130
122
|
const { selection, ...rest } = options;
|
|
131
|
-
const
|
|
132
|
-
|
|
123
|
+
const edits = {
|
|
124
|
+
selection,
|
|
125
|
+
...updateFootnotesFromMeta( newBlocks, meta ),
|
|
126
|
+
};
|
|
133
127
|
|
|
134
128
|
editEntityRecord( kind, name, id, edits, {
|
|
135
129
|
isCached: true,
|
package/src/index.js
CHANGED
|
@@ -133,6 +133,12 @@ unlock( store ).registerPrivateSelectors( privateSelectors );
|
|
|
133
133
|
unlock( store ).registerPrivateActions( privateActions );
|
|
134
134
|
register( store ); // Register store after unlocking private selectors to allow resolvers to use them.
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Enums cannot be exported private without losing the ability to narrow types
|
|
138
|
+
* based on their values (they blur to string type).
|
|
139
|
+
*/
|
|
140
|
+
export { SelectionType } from './utils/crdt-user-selections';
|
|
141
|
+
|
|
136
142
|
export { default as EntityProvider } from './entity-provider';
|
|
137
143
|
export * from './entity-provider';
|
|
138
144
|
export * from './entity-types';
|
|
@@ -140,3 +146,4 @@ export * from './awareness/types';
|
|
|
140
146
|
export * from './fetch';
|
|
141
147
|
export * from './hooks';
|
|
142
148
|
export * from './private-apis';
|
|
149
|
+
export * from './types';
|
package/src/private-actions.js
CHANGED
|
@@ -160,3 +160,17 @@ export function receiveEditorAssets( assets ) {
|
|
|
160
160
|
assets,
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Returns an action object used to receive icons.
|
|
166
|
+
*
|
|
167
|
+
* @param {Array} icons List of icons.
|
|
168
|
+
*
|
|
169
|
+
* @return {Object} Action object.
|
|
170
|
+
*/
|
|
171
|
+
export function receiveIcons( icons ) {
|
|
172
|
+
return {
|
|
173
|
+
type: 'RECEIVE_ICONS',
|
|
174
|
+
icons,
|
|
175
|
+
};
|
|
176
|
+
}
|
package/src/private-apis.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { useEntityRecordsWithPermissions } from './hooks/use-entity-records';
|
|
5
5
|
import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
useActiveCollaborators,
|
|
8
|
+
useGetAbsolutePositionIndex,
|
|
9
|
+
} from './hooks/use-post-editor-awareness-state';
|
|
7
10
|
import { lock } from './lock-unlock';
|
|
8
11
|
|
|
9
12
|
export const privateApis = {};
|
|
@@ -11,4 +14,5 @@ lock( privateApis, {
|
|
|
11
14
|
useEntityRecordsWithPermissions,
|
|
12
15
|
RECEIVE_INTERMEDIATE_RESULTS,
|
|
13
16
|
useActiveCollaborators,
|
|
17
|
+
useGetAbsolutePositionIndex,
|
|
14
18
|
} );
|
package/src/private-selectors.ts
CHANGED
|
@@ -6,7 +6,12 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getDefaultTemplateId,
|
|
11
|
+
getEntityRecord,
|
|
12
|
+
type State,
|
|
13
|
+
type Icon,
|
|
14
|
+
} from './selectors';
|
|
10
15
|
import { STORE_NAME } from './name';
|
|
11
16
|
import { unlock } from './lock-unlock';
|
|
12
17
|
import { getSyncManager } from './sync';
|
|
@@ -308,3 +313,13 @@ export function getEditorSettings(
|
|
|
308
313
|
export function getEditorAssets( state: State ): Record< string, any > | null {
|
|
309
314
|
return state.editorAssets;
|
|
310
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Returns the list of available icons.
|
|
319
|
+
*
|
|
320
|
+
* @param state Data state.
|
|
321
|
+
* @return The list of icons or empty array if not loaded.
|
|
322
|
+
*/
|
|
323
|
+
export function getIcons( state: State ): Icon[] {
|
|
324
|
+
return state.icons ?? [];
|
|
325
|
+
}
|
package/src/reducer.js
CHANGED
|
@@ -660,6 +660,49 @@ export function editorAssets( state = null, action ) {
|
|
|
660
660
|
return state;
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Reducer managing icons.
|
|
665
|
+
*
|
|
666
|
+
* @param {Array} state Current state.
|
|
667
|
+
* @param {Object} action Action object.
|
|
668
|
+
*
|
|
669
|
+
* @return {Array} Updated state.
|
|
670
|
+
*/
|
|
671
|
+
export function icons( state = [], action ) {
|
|
672
|
+
switch ( action.type ) {
|
|
673
|
+
case 'RECEIVE_ICONS':
|
|
674
|
+
return action.icons;
|
|
675
|
+
}
|
|
676
|
+
return state;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Reducer managing sync connection states for entities.
|
|
681
|
+
* Keyed by "kind/name:id" (e.g., "postType/post:123").
|
|
682
|
+
*
|
|
683
|
+
* @param {Object} state Current state.
|
|
684
|
+
* @param {Object} action Dispatched action.
|
|
685
|
+
*
|
|
686
|
+
* @return {Object} Updated state.
|
|
687
|
+
*/
|
|
688
|
+
export function syncConnectionStatuses( state = {}, action ) {
|
|
689
|
+
switch ( action.type ) {
|
|
690
|
+
case 'SET_SYNC_CONNECTION_STATUS': {
|
|
691
|
+
const key = `${ action.kind }/${ action.name }:${ action.key }`;
|
|
692
|
+
return {
|
|
693
|
+
...state,
|
|
694
|
+
[ key ]: action.status,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
case 'CLEAR_SYNC_CONNECTION_STATUS': {
|
|
698
|
+
const key = `${ action.kind }/${ action.name }:${ action.key }`;
|
|
699
|
+
const { [ key ]: _, ...rest } = state;
|
|
700
|
+
return rest;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return state;
|
|
704
|
+
}
|
|
705
|
+
|
|
663
706
|
export default combineReducers( {
|
|
664
707
|
users,
|
|
665
708
|
currentTheme,
|
|
@@ -682,4 +725,6 @@ export default combineReducers( {
|
|
|
682
725
|
registeredPostMeta,
|
|
683
726
|
editorSettings,
|
|
684
727
|
editorAssets,
|
|
728
|
+
icons,
|
|
729
|
+
syncConnectionStatuses,
|
|
685
730
|
} );
|