@wordpress/core-data 7.40.2-next.v.202602241322.0 → 7.40.2
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/README.md +0 -27
- package/build/actions.cjs +9 -26
- package/build/actions.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +45 -6
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/awareness/types.cjs.map +1 -1
- package/build/entities.cjs +50 -23
- package/build/entities.cjs.map +2 -2
- package/build/entity-provider.cjs +15 -6
- package/build/entity-provider.cjs.map +2 -2
- package/build/hooks/use-entity-prop.cjs +33 -2
- package/build/hooks/use-entity-prop.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +120 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/index.cjs +0 -3
- package/build/index.cjs.map +2 -2
- package/build/private-actions.cjs +27 -3
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +14 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/private-selectors.cjs +22 -2
- package/build/private-selectors.cjs.map +2 -2
- package/build/queried-data/actions.cjs +1 -1
- package/build/queried-data/actions.cjs.map +2 -2
- package/build/queried-data/reducer.cjs +19 -13
- package/build/queried-data/reducer.cjs.map +2 -2
- package/build/reducer.cjs +19 -2
- package/build/reducer.cjs.map +2 -2
- package/build/resolvers.cjs +16 -6
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +11 -5
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +1 -1
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +125 -49
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-selection.cjs +50 -19
- package/build/utils/crdt-selection.cjs.map +2 -2
- package/build/utils/crdt-text.cjs +52 -0
- package/build/utils/crdt-text.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +16 -6
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build/utils/crdt-utils.cjs +54 -2
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +38 -29
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -26
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +47 -7
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/entities.mjs +53 -24
- package/build-module/entities.mjs.map +2 -2
- package/build-module/entity-provider.mjs +15 -6
- package/build-module/entity-provider.mjs.map +2 -2
- package/build-module/hooks/use-entity-prop.mjs +34 -3
- package/build-module/hooks/use-entity-prop.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +117 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/index.mjs +0 -2
- package/build-module/index.mjs.map +2 -2
- package/build-module/private-actions.mjs +24 -2
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +21 -6
- package/build-module/private-apis.mjs.map +3 -3
- package/build-module/private-selectors.mjs +19 -1
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/queried-data/actions.mjs +1 -1
- package/build-module/queried-data/actions.mjs.map +2 -2
- package/build-module/queried-data/reducer.mjs +19 -13
- package/build-module/queried-data/reducer.mjs.map +2 -2
- package/build-module/reducer.mjs +18 -2
- package/build-module/reducer.mjs.map +2 -2
- package/build-module/resolvers.mjs +16 -6
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/selectors.mjs +0 -14
- package/build-module/selectors.mjs.map +2 -2
- package/build-module/sync.mjs +7 -3
- package/build-module/sync.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +5 -2
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +124 -49
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-selection.mjs +53 -20
- package/build-module/utils/crdt-selection.mjs.map +2 -2
- package/build-module/utils/crdt-text.mjs +26 -0
- package/build-module/utils/crdt-text.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +16 -7
- package/build-module/utils/crdt-user-selections.mjs.map +3 -3
- package/build-module/utils/crdt-utils.mjs +51 -1
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +42 -34
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts +0 -11
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -2
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/awareness/types.d.ts +6 -1
- package/build-types/awareness/types.d.ts.map +1 -1
- package/build-types/entities.d.ts +20 -1
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-provider.d.ts +11 -6
- package/build-types/entity-provider.d.ts.map +1 -1
- package/build-types/hooks/use-entity-prop.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts +40 -7
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/index.d.ts +0 -3
- package/build-types/index.d.ts.map +1 -1
- package/build-types/parsed-blocks-cache.d.ts +10 -0
- package/build-types/parsed-blocks-cache.d.ts.map +1 -0
- package/build-types/private-actions.d.ts +12 -0
- package/build-types/private-actions.d.ts.map +1 -1
- package/build-types/private-apis.d.ts +21 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +17 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/reducer.d.ts.map +1 -1
- package/build-types/reducer.d.ts +15 -0
- package/build-types/reducer.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +1 -9
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +2 -2
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +11 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +12 -1
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-selection.d.ts +10 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -1
- package/build-types/utils/crdt-text.d.ts +16 -0
- package/build-types/utils/crdt-text.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +17 -4
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +20 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +14 -7
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/normalize-query-for-resolution.d.ts +12 -0
- package/build-types/utils/normalize-query-for-resolution.d.ts.map +1 -0
- package/build-types/utils/save-crdt-doc.d.ts +8 -0
- package/build-types/utils/save-crdt-doc.d.ts.map +1 -0
- package/build-types/utils/test/crdt-utils.d.ts +2 -0
- package/build-types/utils/test/crdt-utils.d.ts.map +1 -0
- package/package.json +18 -18
- package/src/actions.js +26 -41
- package/src/awareness/post-editor-awareness.ts +106 -7
- package/src/awareness/test/post-editor-awareness.ts +50 -10
- package/src/awareness/types.ts +7 -1
- package/src/entities.js +62 -24
- package/src/entity-provider.js +24 -11
- package/src/hooks/test/use-post-editor-awareness-state.ts +446 -3
- package/src/hooks/use-entity-prop.js +43 -3
- package/src/hooks/use-post-editor-awareness-state.ts +229 -7
- package/src/index.js +0 -6
- package/src/private-actions.js +43 -1
- package/src/{private-apis.js → private-apis.ts} +21 -3
- package/src/private-selectors.ts +43 -0
- package/src/queried-data/actions.js +1 -1
- package/src/queried-data/reducer.js +26 -14
- package/src/reducer.js +36 -1
- package/src/resolvers.js +23 -7
- package/src/selectors.ts +1 -32
- package/src/sync.ts +6 -2
- package/src/test/entities.js +231 -14
- package/src/test/private-actions.js +1 -1
- package/src/test/resolvers.js +64 -11
- package/src/test/store.js +116 -0
- package/src/types.ts +21 -2
- package/src/utils/block-selection-history.ts +5 -2
- package/src/utils/crdt-blocks.ts +250 -81
- package/src/utils/crdt-selection.ts +92 -26
- package/src/utils/crdt-text.ts +43 -0
- package/src/utils/crdt-user-selections.ts +29 -8
- package/src/utils/crdt-utils.ts +99 -0
- package/src/utils/crdt.ts +72 -46
- package/src/utils/test/crdt-blocks.ts +1177 -22
- package/src/utils/test/crdt-user-selections.ts +5 -0
- package/src/utils/test/crdt-utils.ts +387 -0
- package/src/utils/test/crdt.ts +423 -60
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
useResolvedSelection,
|
|
12
12
|
useGetDebugData,
|
|
13
13
|
useIsDisconnected,
|
|
14
|
+
useOnCollaboratorJoin,
|
|
15
|
+
useOnCollaboratorLeave,
|
|
16
|
+
useOnPostSave,
|
|
14
17
|
} from '../use-post-editor-awareness-state';
|
|
15
18
|
import { getSyncManager } from '../../sync';
|
|
16
19
|
import { SelectionType } from '../../utils/crdt-user-selections';
|
|
@@ -66,6 +69,9 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
66
69
|
onStateChange: jest.Mock;
|
|
67
70
|
convertSelectionStateToAbsolute: jest.Mock;
|
|
68
71
|
getDebugData: jest.Mock;
|
|
72
|
+
doc: {
|
|
73
|
+
getMap: jest.Mock;
|
|
74
|
+
};
|
|
69
75
|
};
|
|
70
76
|
let mockSyncManager: {
|
|
71
77
|
getAwareness: jest.Mock;
|
|
@@ -73,9 +79,29 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
73
79
|
let stateChangeCallback:
|
|
74
80
|
| ( ( newState: PostEditorAwarenessState[] ) => void )
|
|
75
81
|
| null;
|
|
82
|
+
let stateMapObserver:
|
|
83
|
+
| ( ( event: { keysChanged: Set< string > } ) => void )
|
|
84
|
+
| null;
|
|
85
|
+
let mockStateMapData: Record< string, unknown >;
|
|
86
|
+
let mockRecordMapData: Record< string, unknown >;
|
|
76
87
|
|
|
77
88
|
beforeEach( () => {
|
|
78
89
|
stateChangeCallback = null;
|
|
90
|
+
stateMapObserver = null;
|
|
91
|
+
mockStateMapData = {};
|
|
92
|
+
mockRecordMapData = {};
|
|
93
|
+
|
|
94
|
+
const mockStateMap = {
|
|
95
|
+
get: jest.fn( ( key: string ) => mockStateMapData[ key ] ),
|
|
96
|
+
observe: jest.fn( ( observer: typeof stateMapObserver ) => {
|
|
97
|
+
stateMapObserver = observer;
|
|
98
|
+
} ),
|
|
99
|
+
unobserve: jest.fn(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const mockRecordMap = {
|
|
103
|
+
get: jest.fn( ( key: string ) => mockRecordMapData[ key ] ),
|
|
104
|
+
};
|
|
79
105
|
|
|
80
106
|
mockAwareness = {
|
|
81
107
|
setUp: jest.fn(),
|
|
@@ -86,6 +112,17 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
86
112
|
} ),
|
|
87
113
|
convertSelectionStateToAbsolute: jest.fn().mockReturnValue( null ),
|
|
88
114
|
getDebugData: jest.fn().mockReturnValue( createMockDebugData() ),
|
|
115
|
+
doc: {
|
|
116
|
+
getMap: jest.fn( ( name: string ) => {
|
|
117
|
+
if ( name === 'state' ) {
|
|
118
|
+
return mockStateMap;
|
|
119
|
+
}
|
|
120
|
+
if ( name === 'document' ) {
|
|
121
|
+
return mockRecordMap;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
} ),
|
|
125
|
+
},
|
|
89
126
|
};
|
|
90
127
|
|
|
91
128
|
mockSyncManager = {
|
|
@@ -256,7 +293,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
256
293
|
};
|
|
257
294
|
|
|
258
295
|
expect( result.current( mockSelection ) ).toEqual( {
|
|
259
|
-
|
|
296
|
+
richTextOffset: null,
|
|
260
297
|
localClientId: null,
|
|
261
298
|
} );
|
|
262
299
|
} );
|
|
@@ -270,7 +307,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
270
307
|
},
|
|
271
308
|
};
|
|
272
309
|
mockAwareness.convertSelectionStateToAbsolute.mockReturnValue( {
|
|
273
|
-
|
|
310
|
+
richTextOffset: 10,
|
|
274
311
|
localClientId: 'block-1',
|
|
275
312
|
} );
|
|
276
313
|
|
|
@@ -284,7 +321,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
284
321
|
mockAwareness.convertSelectionStateToAbsolute
|
|
285
322
|
).toHaveBeenCalledWith( mockSelection );
|
|
286
323
|
expect( position ).toEqual( {
|
|
287
|
-
|
|
324
|
+
richTextOffset: 10,
|
|
288
325
|
localClientId: 'block-1',
|
|
289
326
|
} );
|
|
290
327
|
} );
|
|
@@ -481,4 +518,410 @@ describe( 'use-post-editor-awareness-state hooks', () => {
|
|
|
481
518
|
expect( result.current ).toBe( false );
|
|
482
519
|
} );
|
|
483
520
|
} );
|
|
521
|
+
|
|
522
|
+
describe( 'useOnCollaboratorJoin', () => {
|
|
523
|
+
const me = createMockActiveUser( {
|
|
524
|
+
clientId: 1,
|
|
525
|
+
isMe: true,
|
|
526
|
+
collaboratorInfo: {
|
|
527
|
+
id: 1,
|
|
528
|
+
name: 'Me',
|
|
529
|
+
slug: 'me',
|
|
530
|
+
avatar_urls: mockAvatarUrls,
|
|
531
|
+
browserType: 'Chrome',
|
|
532
|
+
enteredAt: 1704067200000,
|
|
533
|
+
},
|
|
534
|
+
} );
|
|
535
|
+
|
|
536
|
+
const alice = createMockActiveUser( {
|
|
537
|
+
clientId: 2,
|
|
538
|
+
isMe: false,
|
|
539
|
+
collaboratorInfo: {
|
|
540
|
+
id: 100,
|
|
541
|
+
name: 'Alice',
|
|
542
|
+
slug: 'alice',
|
|
543
|
+
avatar_urls: mockAvatarUrls,
|
|
544
|
+
browserType: 'Chrome',
|
|
545
|
+
enteredAt: 1704067300000,
|
|
546
|
+
},
|
|
547
|
+
} );
|
|
548
|
+
|
|
549
|
+
test( 'should not fire on initial mount', () => {
|
|
550
|
+
const callback = jest.fn();
|
|
551
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
552
|
+
|
|
553
|
+
renderHook( () => useOnCollaboratorJoin( 123, 'post', callback ) );
|
|
554
|
+
|
|
555
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
556
|
+
} );
|
|
557
|
+
|
|
558
|
+
test( 'should not fire when collaborators load after initially empty state', async () => {
|
|
559
|
+
const callback = jest.fn();
|
|
560
|
+
mockAwareness.getCurrentState.mockReturnValue( [] );
|
|
561
|
+
|
|
562
|
+
renderHook( () => useOnCollaboratorJoin( 123, 'post', callback ) );
|
|
563
|
+
|
|
564
|
+
// Simulate store hydration: collaborators appear
|
|
565
|
+
act( () => {
|
|
566
|
+
stateChangeCallback?.( [ me, alice ] );
|
|
567
|
+
} );
|
|
568
|
+
|
|
569
|
+
await waitFor( () => {
|
|
570
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
571
|
+
} );
|
|
572
|
+
} );
|
|
573
|
+
|
|
574
|
+
test( 'should fire callback when a new collaborator joins', async () => {
|
|
575
|
+
const callback = jest.fn();
|
|
576
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me ] );
|
|
577
|
+
|
|
578
|
+
renderHook( () => useOnCollaboratorJoin( 123, 'post', callback ) );
|
|
579
|
+
|
|
580
|
+
// Alice joins
|
|
581
|
+
act( () => {
|
|
582
|
+
stateChangeCallback?.( [ me, alice ] );
|
|
583
|
+
} );
|
|
584
|
+
|
|
585
|
+
await waitFor( () => {
|
|
586
|
+
expect( callback ).toHaveBeenCalledWith( alice, me );
|
|
587
|
+
} );
|
|
588
|
+
} );
|
|
589
|
+
|
|
590
|
+
test( 'should not fire callback for the current user', async () => {
|
|
591
|
+
const callback = jest.fn();
|
|
592
|
+
mockAwareness.getCurrentState.mockReturnValue( [] );
|
|
593
|
+
|
|
594
|
+
renderHook( () => useOnCollaboratorJoin( 123, 'post', callback ) );
|
|
595
|
+
|
|
596
|
+
// First: hydrate with alice so prevCollaborators is non-empty
|
|
597
|
+
act( () => {
|
|
598
|
+
stateChangeCallback?.( [ alice ] );
|
|
599
|
+
} );
|
|
600
|
+
|
|
601
|
+
// Now "me" appears
|
|
602
|
+
act( () => {
|
|
603
|
+
stateChangeCallback?.( [ alice, me ] );
|
|
604
|
+
} );
|
|
605
|
+
|
|
606
|
+
await waitFor( () => {
|
|
607
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
608
|
+
} );
|
|
609
|
+
} );
|
|
610
|
+
|
|
611
|
+
test( 'should not fire when postId is null', () => {
|
|
612
|
+
const callback = jest.fn();
|
|
613
|
+
|
|
614
|
+
renderHook( () => useOnCollaboratorJoin( null, 'post', callback ) );
|
|
615
|
+
|
|
616
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
617
|
+
expect( mockSyncManager.getAwareness ).not.toHaveBeenCalled();
|
|
618
|
+
} );
|
|
619
|
+
} );
|
|
620
|
+
|
|
621
|
+
describe( 'useOnCollaboratorLeave', () => {
|
|
622
|
+
const me = createMockActiveUser( {
|
|
623
|
+
clientId: 1,
|
|
624
|
+
isMe: true,
|
|
625
|
+
} );
|
|
626
|
+
|
|
627
|
+
const alice = createMockActiveUser( {
|
|
628
|
+
clientId: 2,
|
|
629
|
+
isMe: false,
|
|
630
|
+
isConnected: true,
|
|
631
|
+
collaboratorInfo: {
|
|
632
|
+
id: 100,
|
|
633
|
+
name: 'Alice',
|
|
634
|
+
slug: 'alice',
|
|
635
|
+
avatar_urls: mockAvatarUrls,
|
|
636
|
+
browserType: 'Chrome',
|
|
637
|
+
enteredAt: 1704067300000,
|
|
638
|
+
},
|
|
639
|
+
} );
|
|
640
|
+
|
|
641
|
+
test( 'should fire callback when a connected collaborator disconnects', async () => {
|
|
642
|
+
const callback = jest.fn();
|
|
643
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
644
|
+
|
|
645
|
+
renderHook( () => useOnCollaboratorLeave( 123, 'post', callback ) );
|
|
646
|
+
|
|
647
|
+
// Alice disconnects
|
|
648
|
+
act( () => {
|
|
649
|
+
stateChangeCallback?.( [
|
|
650
|
+
me,
|
|
651
|
+
{ ...alice, isConnected: false },
|
|
652
|
+
] );
|
|
653
|
+
} );
|
|
654
|
+
|
|
655
|
+
await waitFor( () => {
|
|
656
|
+
expect( callback ).toHaveBeenCalledWith( alice );
|
|
657
|
+
} );
|
|
658
|
+
} );
|
|
659
|
+
|
|
660
|
+
test( 'should fire callback when a connected collaborator disappears from the list', async () => {
|
|
661
|
+
const callback = jest.fn();
|
|
662
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
663
|
+
|
|
664
|
+
renderHook( () => useOnCollaboratorLeave( 123, 'post', callback ) );
|
|
665
|
+
|
|
666
|
+
// Alice disappears entirely
|
|
667
|
+
act( () => {
|
|
668
|
+
stateChangeCallback?.( [ me ] );
|
|
669
|
+
} );
|
|
670
|
+
|
|
671
|
+
await waitFor( () => {
|
|
672
|
+
expect( callback ).toHaveBeenCalledWith( alice );
|
|
673
|
+
} );
|
|
674
|
+
} );
|
|
675
|
+
|
|
676
|
+
test( 'should not fire callback when an already-disconnected collaborator is removed', async () => {
|
|
677
|
+
const callback = jest.fn();
|
|
678
|
+
const disconnectedAlice = { ...alice, isConnected: false };
|
|
679
|
+
mockAwareness.getCurrentState.mockReturnValue( [
|
|
680
|
+
me,
|
|
681
|
+
disconnectedAlice,
|
|
682
|
+
] );
|
|
683
|
+
|
|
684
|
+
renderHook( () => useOnCollaboratorLeave( 123, 'post', callback ) );
|
|
685
|
+
|
|
686
|
+
// Disconnected Alice is removed from list (cleanup after delay)
|
|
687
|
+
act( () => {
|
|
688
|
+
stateChangeCallback?.( [ me ] );
|
|
689
|
+
} );
|
|
690
|
+
|
|
691
|
+
await waitFor( () => {
|
|
692
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
693
|
+
} );
|
|
694
|
+
} );
|
|
695
|
+
|
|
696
|
+
test( 'should not fire callback for the current user disconnecting', async () => {
|
|
697
|
+
const callback = jest.fn();
|
|
698
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
699
|
+
|
|
700
|
+
renderHook( () => useOnCollaboratorLeave( 123, 'post', callback ) );
|
|
701
|
+
|
|
702
|
+
// "Me" disconnects
|
|
703
|
+
act( () => {
|
|
704
|
+
stateChangeCallback?.( [
|
|
705
|
+
{ ...me, isConnected: false },
|
|
706
|
+
alice,
|
|
707
|
+
] );
|
|
708
|
+
} );
|
|
709
|
+
|
|
710
|
+
await waitFor( () => {
|
|
711
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
712
|
+
} );
|
|
713
|
+
} );
|
|
714
|
+
|
|
715
|
+
test( 'should not fire on initial mount', () => {
|
|
716
|
+
const callback = jest.fn();
|
|
717
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
718
|
+
|
|
719
|
+
renderHook( () => useOnCollaboratorLeave( 123, 'post', callback ) );
|
|
720
|
+
|
|
721
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
722
|
+
} );
|
|
723
|
+
} );
|
|
724
|
+
|
|
725
|
+
describe( 'useOnPostSave', () => {
|
|
726
|
+
const me = createMockActiveUser( {
|
|
727
|
+
clientId: 1,
|
|
728
|
+
isMe: true,
|
|
729
|
+
} );
|
|
730
|
+
|
|
731
|
+
const alice = createMockActiveUser( {
|
|
732
|
+
clientId: 2,
|
|
733
|
+
isMe: false,
|
|
734
|
+
collaboratorInfo: {
|
|
735
|
+
id: 100,
|
|
736
|
+
name: 'Alice',
|
|
737
|
+
slug: 'alice',
|
|
738
|
+
avatar_urls: mockAvatarUrls,
|
|
739
|
+
browserType: 'Chrome',
|
|
740
|
+
enteredAt: 1704067300000,
|
|
741
|
+
},
|
|
742
|
+
} );
|
|
743
|
+
|
|
744
|
+
test( 'should fire callback when a remote collaborator saves', async () => {
|
|
745
|
+
const callback = jest.fn();
|
|
746
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
747
|
+
|
|
748
|
+
renderHook( () => useOnPostSave( 123, 'post', callback ) );
|
|
749
|
+
|
|
750
|
+
// Simulate a save event via the Y.Doc state map
|
|
751
|
+
const savedAt = Date.now() + 1000;
|
|
752
|
+
mockStateMapData = {
|
|
753
|
+
savedAt,
|
|
754
|
+
savedBy: alice.clientId,
|
|
755
|
+
};
|
|
756
|
+
mockRecordMapData = { status: 'draft' };
|
|
757
|
+
|
|
758
|
+
act( () => {
|
|
759
|
+
stateMapObserver?.( {
|
|
760
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
761
|
+
} );
|
|
762
|
+
} );
|
|
763
|
+
|
|
764
|
+
await waitFor( () => {
|
|
765
|
+
expect( callback ).toHaveBeenCalledWith(
|
|
766
|
+
{
|
|
767
|
+
savedAt,
|
|
768
|
+
savedByClientId: alice.clientId,
|
|
769
|
+
postStatus: 'draft',
|
|
770
|
+
},
|
|
771
|
+
alice,
|
|
772
|
+
null
|
|
773
|
+
);
|
|
774
|
+
} );
|
|
775
|
+
} );
|
|
776
|
+
|
|
777
|
+
test( 'should pass previous save event on subsequent saves', async () => {
|
|
778
|
+
const callback = jest.fn();
|
|
779
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
780
|
+
|
|
781
|
+
renderHook( () => useOnPostSave( 123, 'post', callback ) );
|
|
782
|
+
|
|
783
|
+
// First save
|
|
784
|
+
const firstSavedAt = Date.now() + 1000;
|
|
785
|
+
mockStateMapData = {
|
|
786
|
+
savedAt: firstSavedAt,
|
|
787
|
+
savedBy: alice.clientId,
|
|
788
|
+
};
|
|
789
|
+
mockRecordMapData = { status: 'draft' };
|
|
790
|
+
|
|
791
|
+
act( () => {
|
|
792
|
+
stateMapObserver?.( {
|
|
793
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
794
|
+
} );
|
|
795
|
+
} );
|
|
796
|
+
|
|
797
|
+
await waitFor( () => {
|
|
798
|
+
expect( callback ).toHaveBeenCalledTimes( 1 );
|
|
799
|
+
} );
|
|
800
|
+
|
|
801
|
+
// Second save
|
|
802
|
+
const secondSavedAt = Date.now() + 2000;
|
|
803
|
+
mockStateMapData = {
|
|
804
|
+
savedAt: secondSavedAt,
|
|
805
|
+
savedBy: alice.clientId,
|
|
806
|
+
};
|
|
807
|
+
mockRecordMapData = { status: 'publish' };
|
|
808
|
+
|
|
809
|
+
act( () => {
|
|
810
|
+
stateMapObserver?.( {
|
|
811
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
812
|
+
} );
|
|
813
|
+
} );
|
|
814
|
+
|
|
815
|
+
await waitFor( () => {
|
|
816
|
+
expect( callback ).toHaveBeenCalledTimes( 2 );
|
|
817
|
+
} );
|
|
818
|
+
|
|
819
|
+
expect( callback ).toHaveBeenLastCalledWith(
|
|
820
|
+
{
|
|
821
|
+
savedAt: secondSavedAt,
|
|
822
|
+
savedByClientId: alice.clientId,
|
|
823
|
+
postStatus: 'publish',
|
|
824
|
+
},
|
|
825
|
+
alice,
|
|
826
|
+
{
|
|
827
|
+
savedAt: firstSavedAt,
|
|
828
|
+
savedByClientId: alice.clientId,
|
|
829
|
+
postStatus: 'draft',
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
} );
|
|
833
|
+
|
|
834
|
+
test( 'should not fire callback when the current user saves', async () => {
|
|
835
|
+
const callback = jest.fn();
|
|
836
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
837
|
+
|
|
838
|
+
renderHook( () => useOnPostSave( 123, 'post', callback ) );
|
|
839
|
+
|
|
840
|
+
// Simulate a save event by "me"
|
|
841
|
+
const savedAt = Date.now() + 1000;
|
|
842
|
+
mockStateMapData = {
|
|
843
|
+
savedAt,
|
|
844
|
+
savedBy: me.clientId,
|
|
845
|
+
};
|
|
846
|
+
mockRecordMapData = { status: 'draft' };
|
|
847
|
+
|
|
848
|
+
act( () => {
|
|
849
|
+
stateMapObserver?.( {
|
|
850
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
851
|
+
} );
|
|
852
|
+
} );
|
|
853
|
+
|
|
854
|
+
await waitFor( () => {
|
|
855
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
856
|
+
} );
|
|
857
|
+
} );
|
|
858
|
+
|
|
859
|
+
test( 'should not fire callback when saver is not in the collaborator list', async () => {
|
|
860
|
+
const callback = jest.fn();
|
|
861
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me ] );
|
|
862
|
+
|
|
863
|
+
renderHook( () => useOnPostSave( 123, 'post', callback ) );
|
|
864
|
+
|
|
865
|
+
// Simulate a save from an unknown client
|
|
866
|
+
const savedAt = Date.now() + 1000;
|
|
867
|
+
mockStateMapData = {
|
|
868
|
+
savedAt,
|
|
869
|
+
savedBy: 99999,
|
|
870
|
+
};
|
|
871
|
+
mockRecordMapData = { status: 'draft' };
|
|
872
|
+
|
|
873
|
+
act( () => {
|
|
874
|
+
stateMapObserver?.( {
|
|
875
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
876
|
+
} );
|
|
877
|
+
} );
|
|
878
|
+
|
|
879
|
+
await waitFor( () => {
|
|
880
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
881
|
+
} );
|
|
882
|
+
} );
|
|
883
|
+
|
|
884
|
+
test( 'should not fire duplicate callbacks for the same savedAt timestamp', async () => {
|
|
885
|
+
const callback = jest.fn();
|
|
886
|
+
mockAwareness.getCurrentState.mockReturnValue( [ me, alice ] );
|
|
887
|
+
|
|
888
|
+
renderHook( () => useOnPostSave( 123, 'post', callback ) );
|
|
889
|
+
|
|
890
|
+
const savedAt = Date.now() + 1000;
|
|
891
|
+
mockStateMapData = {
|
|
892
|
+
savedAt,
|
|
893
|
+
savedBy: alice.clientId,
|
|
894
|
+
};
|
|
895
|
+
mockRecordMapData = { status: 'draft' };
|
|
896
|
+
|
|
897
|
+
// First save event
|
|
898
|
+
act( () => {
|
|
899
|
+
stateMapObserver?.( {
|
|
900
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
901
|
+
} );
|
|
902
|
+
} );
|
|
903
|
+
|
|
904
|
+
await waitFor( () => {
|
|
905
|
+
expect( callback ).toHaveBeenCalledTimes( 1 );
|
|
906
|
+
} );
|
|
907
|
+
|
|
908
|
+
// Same savedAt again (e.g. component re-render)
|
|
909
|
+
act( () => {
|
|
910
|
+
stateMapObserver?.( {
|
|
911
|
+
keysChanged: new Set( [ 'savedAt' ] ),
|
|
912
|
+
} );
|
|
913
|
+
} );
|
|
914
|
+
|
|
915
|
+
// Should still be 1 call
|
|
916
|
+
expect( callback ).toHaveBeenCalledTimes( 1 );
|
|
917
|
+
} );
|
|
918
|
+
|
|
919
|
+
test( 'should not fire when postId is null', () => {
|
|
920
|
+
const callback = jest.fn();
|
|
921
|
+
|
|
922
|
+
renderHook( () => useOnPostSave( null, 'post', callback ) );
|
|
923
|
+
|
|
924
|
+
expect( callback ).not.toHaveBeenCalled();
|
|
925
|
+
} );
|
|
926
|
+
} );
|
|
484
927
|
} );
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
|
-
import { useCallback } from '@wordpress/element';
|
|
4
|
+
import { useCallback, useContext } from '@wordpress/element';
|
|
5
5
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Internal dependencies
|
|
9
9
|
*/
|
|
10
10
|
import { STORE_NAME } from '../name';
|
|
11
|
+
import { DEFAULT_ENTITY_KEY } from '../entities';
|
|
12
|
+
import { EntityContext } from '../entity-context';
|
|
11
13
|
import useEntityId from './use-entity-id';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -30,9 +32,44 @@ import useEntityId from './use-entity-id';
|
|
|
30
32
|
export default function useEntityProp( kind, name, prop, _id ) {
|
|
31
33
|
const providerId = useEntityId( kind, name );
|
|
32
34
|
const id = _id ?? providerId;
|
|
35
|
+
const context = useContext( EntityContext );
|
|
36
|
+
const revisionId = context?.revisionId;
|
|
33
37
|
|
|
34
38
|
const { value, fullValue } = useSelect(
|
|
35
39
|
( select ) => {
|
|
40
|
+
if ( revisionId ) {
|
|
41
|
+
// Use getRevisions (not getRevision) to read from the
|
|
42
|
+
// already-cached collection. Using getRevision would
|
|
43
|
+
// trigger a redundant single-revision API fetch that
|
|
44
|
+
// can wipe the collection due to a race condition.
|
|
45
|
+
// See https://github.com/WordPress/gutenberg/pull/76043.
|
|
46
|
+
const revisions = select( STORE_NAME ).getRevisions(
|
|
47
|
+
kind,
|
|
48
|
+
name,
|
|
49
|
+
id,
|
|
50
|
+
{
|
|
51
|
+
per_page: -1,
|
|
52
|
+
context: 'edit',
|
|
53
|
+
_fields:
|
|
54
|
+
'id,date,author,meta,title.raw,excerpt.raw,content.raw',
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
const entityConfig = select( STORE_NAME ).getEntityConfig(
|
|
58
|
+
kind,
|
|
59
|
+
name
|
|
60
|
+
);
|
|
61
|
+
const revKey = entityConfig?.revisionKey || DEFAULT_ENTITY_KEY;
|
|
62
|
+
const revision = revisions?.find(
|
|
63
|
+
( r ) => r[ revKey ] === revisionId
|
|
64
|
+
);
|
|
65
|
+
return revision
|
|
66
|
+
? {
|
|
67
|
+
value: revision[ prop ]?.raw ?? revision[ prop ],
|
|
68
|
+
fullValue: revision[ prop ],
|
|
69
|
+
}
|
|
70
|
+
: {};
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
const { getEntityRecord, getEditedEntityRecord } =
|
|
37
74
|
select( STORE_NAME );
|
|
38
75
|
const record = getEntityRecord( kind, name, id ); // Trigger resolver.
|
|
@@ -44,16 +81,19 @@ export default function useEntityProp( kind, name, prop, _id ) {
|
|
|
44
81
|
}
|
|
45
82
|
: {};
|
|
46
83
|
},
|
|
47
|
-
[ kind, name, id, prop ]
|
|
84
|
+
[ kind, name, id, prop, revisionId ]
|
|
48
85
|
);
|
|
49
86
|
const { editEntityRecord } = useDispatch( STORE_NAME );
|
|
50
87
|
const setValue = useCallback(
|
|
51
88
|
( newValue ) => {
|
|
89
|
+
if ( revisionId ) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
52
92
|
editEntityRecord( kind, name, id, {
|
|
53
93
|
[ prop ]: newValue,
|
|
54
94
|
} );
|
|
55
95
|
},
|
|
56
|
-
[ editEntityRecord, kind, name, id, prop ]
|
|
96
|
+
[ editEntityRecord, kind, name, id, prop, revisionId ]
|
|
57
97
|
);
|
|
58
98
|
|
|
59
99
|
return [ value, setValue, fullValue ];
|