@wordpress/core-data 7.38.0 → 7.39.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/build/actions.cjs +17 -6
- package/build/actions.cjs.map +2 -2
- package/build/awareness/base-awareness.cjs +62 -0
- package/build/awareness/base-awareness.cjs.map +7 -0
- package/build/awareness/config.cjs +34 -0
- package/build/awareness/config.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +130 -0
- package/build/awareness/post-editor-awareness.cjs.map +7 -0
- package/build/awareness/types.cjs +19 -0
- package/build/awareness/types.cjs.map +7 -0
- package/build/awareness/utils.cjs +116 -0
- package/build/awareness/utils.cjs.map +7 -0
- package/build/entities.cjs +27 -2
- package/build/entities.cjs.map +2 -2
- package/build/resolvers.cjs +43 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/block-selection-history.cjs +101 -0
- package/build/utils/block-selection-history.cjs.map +7 -0
- package/build/utils/crdt-selection.cjs +139 -0
- package/build/utils/crdt-selection.cjs.map +7 -0
- package/build/utils/crdt-user-selections.cjs +171 -0
- package/build/utils/crdt-user-selections.cjs.map +7 -0
- package/build/utils/crdt-utils.cjs +29 -0
- package/build/utils/crdt-utils.cjs.map +3 -3
- package/build/utils/crdt.cjs +16 -4
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +17 -6
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/base-awareness.mjs +35 -0
- package/build-module/awareness/base-awareness.mjs.map +7 -0
- package/build-module/awareness/config.mjs +8 -0
- package/build-module/awareness/config.mjs.map +7 -0
- package/build-module/awareness/post-editor-awareness.mjs +111 -0
- package/build-module/awareness/post-editor-awareness.mjs.map +7 -0
- package/build-module/awareness/types.mjs +1 -0
- package/build-module/awareness/types.mjs.map +7 -0
- package/build-module/awareness/utils.mjs +90 -0
- package/build-module/awareness/utils.mjs.map +7 -0
- package/build-module/entities.mjs +28 -2
- package/build-module/entities.mjs.map +2 -2
- package/build-module/resolvers.mjs +43 -2
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/block-selection-history.mjs +75 -0
- package/build-module/utils/block-selection-history.mjs.map +7 -0
- package/build-module/utils/crdt-selection.mjs +115 -0
- package/build-module/utils/crdt-selection.mjs.map +7 -0
- package/build-module/utils/crdt-user-selections.mjs +144 -0
- package/build-module/utils/crdt-user-selections.mjs.map +7 -0
- package/build-module/utils/crdt-utils.mjs +28 -0
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +18 -3
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/base-awareness.d.ts +19 -0
- package/build-types/awareness/base-awareness.d.ts.map +1 -0
- package/build-types/awareness/config.d.ts +9 -0
- package/build-types/awareness/config.d.ts.map +1 -0
- package/build-types/awareness/post-editor-awareness.d.ts +38 -0
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +32 -0
- package/build-types/awareness/types.d.ts.map +1 -0
- package/build-types/awareness/utils.d.ts +22 -0
- package/build-types/awareness/utils.d.ts.map +1 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/entity-types/test/attachment.test.d.ts +10 -0
- package/build-types/entity-types/test/attachment.test.d.ts.map +1 -0
- package/build-types/index.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +12 -2
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/block-selection-history.d.ts +47 -0
- package/build-types/utils/block-selection-history.d.ts.map +1 -0
- package/build-types/utils/crdt-selection.d.ts +16 -0
- package/build-types/utils/crdt-selection.d.ts.map +1 -0
- package/build-types/utils/crdt-user-selections.d.ts +66 -0
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -0
- package/build-types/utils/crdt-utils.d.ts +12 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +8 -14
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/build-types/utils/test/block-selection-history.test.d.ts +2 -0
- package/build-types/utils/test/block-selection-history.test.d.ts.map +1 -0
- package/build-types/utils/test/crdt-blocks.d.ts +2 -0
- package/build-types/utils/test/crdt-blocks.d.ts.map +1 -0
- package/build-types/utils/test/crdt.d.ts +2 -0
- package/build-types/utils/test/crdt.d.ts.map +1 -0
- package/package.json +21 -18
- package/src/actions.js +40 -7
- package/src/awareness/base-awareness.ts +50 -0
- package/src/awareness/config.ts +9 -0
- package/src/awareness/post-editor-awareness.ts +167 -0
- package/src/awareness/types.ts +38 -0
- package/src/awareness/utils.ts +159 -0
- package/src/entities.js +32 -2
- package/src/entity-types/test/attachment.test.ts +4 -4
- package/src/resolvers.js +53 -1
- package/src/test/actions.js +402 -0
- package/src/test/entity-provider.js +2 -0
- package/src/test/resolvers.js +4 -0
- package/src/types.ts +12 -3
- package/src/utils/block-selection-history.ts +176 -0
- package/src/utils/crdt-selection.ts +205 -0
- package/src/utils/crdt-user-selections.ts +336 -0
- package/src/utils/crdt-utils.ts +54 -0
- package/src/utils/crdt.ts +36 -3
- package/src/utils/test/block-selection-history.test.ts +764 -0
- package/src/utils/test/crdt-blocks.ts +11 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { dispatch, select, subscribe } from '@wordpress/data';
|
|
5
|
+
import type { Y } from '@wordpress/sync';
|
|
6
|
+
// @ts-ignore No exported types for block editor store selectors.
|
|
7
|
+
import { store as blockEditorStore } from '@wordpress/block-editor';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal dependencies
|
|
11
|
+
*/
|
|
12
|
+
import { BaseAwarenessState, baseEqualityFieldChecks } from './base-awareness';
|
|
13
|
+
import {
|
|
14
|
+
AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,
|
|
15
|
+
LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
|
|
16
|
+
} from './config';
|
|
17
|
+
import { STORE_NAME as coreStore } from '../name';
|
|
18
|
+
import {
|
|
19
|
+
areSelectionsStatesEqual,
|
|
20
|
+
getSelectionState,
|
|
21
|
+
} from '../utils/crdt-user-selections';
|
|
22
|
+
|
|
23
|
+
import type { WPBlockSelection } from '../types';
|
|
24
|
+
import type { EditorState, PostEditorState } from './types';
|
|
25
|
+
|
|
26
|
+
export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
|
|
27
|
+
protected equalityFieldChecks = {
|
|
28
|
+
...baseEqualityFieldChecks,
|
|
29
|
+
editorState: this.areEditorStatesEqual,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
public constructor(
|
|
33
|
+
doc: Y.Doc,
|
|
34
|
+
private kind: string,
|
|
35
|
+
private name: string,
|
|
36
|
+
private postId: number
|
|
37
|
+
) {
|
|
38
|
+
super( doc );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public setUp(): void {
|
|
42
|
+
super.setUp();
|
|
43
|
+
|
|
44
|
+
this.subscribeToUserSelectionChanges();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Subscribe to user selection changes and update the selection state.
|
|
49
|
+
*/
|
|
50
|
+
private subscribeToUserSelectionChanges(): void {
|
|
51
|
+
const {
|
|
52
|
+
getSelectionStart,
|
|
53
|
+
getSelectionEnd,
|
|
54
|
+
getSelectedBlocksInitialCaretPosition,
|
|
55
|
+
} = select( blockEditorStore );
|
|
56
|
+
|
|
57
|
+
// Keep track of the current selection in the outer scope so we can compare
|
|
58
|
+
// in the subscription.
|
|
59
|
+
let selectionStart = getSelectionStart();
|
|
60
|
+
let selectionEnd = getSelectionEnd();
|
|
61
|
+
let localCursorTimeout: NodeJS.Timeout | null = null;
|
|
62
|
+
|
|
63
|
+
subscribe( () => {
|
|
64
|
+
const newSelectionStart = getSelectionStart();
|
|
65
|
+
const newSelectionEnd = getSelectionEnd();
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
newSelectionStart === selectionStart &&
|
|
69
|
+
newSelectionEnd === selectionEnd
|
|
70
|
+
) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
selectionStart = newSelectionStart;
|
|
75
|
+
selectionEnd = newSelectionEnd;
|
|
76
|
+
|
|
77
|
+
// Typically selection position is only persisted after typing in a block, which
|
|
78
|
+
// can cause selection position to be reset by other users making block updates.
|
|
79
|
+
// Ensure we update the controlled selection right away, persisting our cursor position locally.
|
|
80
|
+
const initialPosition = getSelectedBlocksInitialCaretPosition();
|
|
81
|
+
void this.updateSelectionInEntityRecord(
|
|
82
|
+
selectionStart,
|
|
83
|
+
selectionEnd,
|
|
84
|
+
initialPosition
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// We receive two selection changes in quick succession
|
|
88
|
+
// from local selection events:
|
|
89
|
+
// { clientId: "123...", attributeKey: "content", offset: undefined }
|
|
90
|
+
// { clientId: "123...", attributeKey: "content", offset: 554 }
|
|
91
|
+
// Add a short debounce to avoid sending the first selection change.
|
|
92
|
+
if ( localCursorTimeout ) {
|
|
93
|
+
clearTimeout( localCursorTimeout );
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
localCursorTimeout = setTimeout( () => {
|
|
97
|
+
const selectionState = getSelectionState(
|
|
98
|
+
selectionStart,
|
|
99
|
+
selectionEnd,
|
|
100
|
+
this.doc
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
this.setThrottledLocalStateField(
|
|
104
|
+
'editorState',
|
|
105
|
+
{ selection: selectionState },
|
|
106
|
+
AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS
|
|
107
|
+
);
|
|
108
|
+
}, LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS );
|
|
109
|
+
} );
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update the entity record with the current user's selection.
|
|
114
|
+
*
|
|
115
|
+
* @param selectionStart - The start position of the selection.
|
|
116
|
+
* @param selectionEnd - The end position of the selection.
|
|
117
|
+
* @param initialPosition - The initial position of the selection.
|
|
118
|
+
*/
|
|
119
|
+
private async updateSelectionInEntityRecord(
|
|
120
|
+
selectionStart: WPBlockSelection,
|
|
121
|
+
selectionEnd: WPBlockSelection,
|
|
122
|
+
initialPosition: number | null
|
|
123
|
+
): Promise< void > {
|
|
124
|
+
// Send an entityRecord `selection` update if we have a selection.
|
|
125
|
+
//
|
|
126
|
+
// Normally WordPress updates the `selection` property of the post when changes are made to blocks.
|
|
127
|
+
// In a multi-user setup, block changes can occur from other users. When an entity is updated from another
|
|
128
|
+
// user's changes, useBlockSync() in Gutenberg will reset the user's selection to the last saved selection.
|
|
129
|
+
//
|
|
130
|
+
// Manually adding an edit for each movement ensures that other user's changes to the document will
|
|
131
|
+
// not cause the local user's selection to reset to the last local change location.
|
|
132
|
+
const edits = {
|
|
133
|
+
selection: { selectionStart, selectionEnd, initialPosition },
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const options = {
|
|
137
|
+
undoIgnore: true,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// @ts-ignore Types are not provided when using store name instead of store instance.
|
|
141
|
+
dispatch( coreStore ).editEntityRecord(
|
|
142
|
+
this.kind,
|
|
143
|
+
this.name,
|
|
144
|
+
this.postId,
|
|
145
|
+
edits,
|
|
146
|
+
options
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if two editor states are equal.
|
|
152
|
+
*
|
|
153
|
+
* @param state1 - The first editor state.
|
|
154
|
+
* @param state2 - The second editor state.
|
|
155
|
+
* @return True if the editor states are equal, false otherwise.
|
|
156
|
+
*/
|
|
157
|
+
private areEditorStatesEqual(
|
|
158
|
+
state1?: EditorState,
|
|
159
|
+
state2?: EditorState
|
|
160
|
+
): boolean {
|
|
161
|
+
if ( ! state1 || ! state2 ) {
|
|
162
|
+
return state1 === state2;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return areSelectionsStatesEqual( state1.selection, state2.selection );
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import type { SelectionState } from '../utils/crdt-user-selections';
|
|
5
|
+
import type { User } from '../entity-types';
|
|
6
|
+
|
|
7
|
+
export type UserInfo = Pick<
|
|
8
|
+
User< 'view' >,
|
|
9
|
+
'id' | 'name' | 'slug' | 'avatar_urls'
|
|
10
|
+
> & {
|
|
11
|
+
browserType: string;
|
|
12
|
+
color: string;
|
|
13
|
+
enteredAt: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This base state represents the presence of the user. We expect it to be
|
|
18
|
+
* extended to include additional state describing the user's current activity.
|
|
19
|
+
* This state must be serializable and compact.
|
|
20
|
+
*/
|
|
21
|
+
export interface BaseState {
|
|
22
|
+
userInfo: UserInfo;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The editor state includes information about the user's current selection.
|
|
27
|
+
*/
|
|
28
|
+
export interface EditorState {
|
|
29
|
+
selection: SelectionState;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The post editor state extends the base state with information used to render
|
|
34
|
+
* presence indicators in the post editor.
|
|
35
|
+
*/
|
|
36
|
+
export interface PostEditorState extends BaseState {
|
|
37
|
+
editorState?: EditorState;
|
|
38
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import type { User } from '../entity-types';
|
|
5
|
+
import type { UserInfo } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The color palette for the user highlight.
|
|
9
|
+
*/
|
|
10
|
+
const COLOR_PALETTE = [
|
|
11
|
+
'#3858E9', // blueberry
|
|
12
|
+
'#B42AED', // purple
|
|
13
|
+
'#E33184', // pink
|
|
14
|
+
'#F3661D', // orange
|
|
15
|
+
'#ECBD3A', // yellow
|
|
16
|
+
'#97FE17', // green
|
|
17
|
+
'#00FDD9', // teal
|
|
18
|
+
'#37C5F0', // cyan
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a random integer between min and max, inclusive.
|
|
23
|
+
*
|
|
24
|
+
* @param min - The minimum value.
|
|
25
|
+
* @param max - The maximum value.
|
|
26
|
+
* @return A random integer between min and max.
|
|
27
|
+
*/
|
|
28
|
+
function generateRandomInt( min: number, max: number ): number {
|
|
29
|
+
return Math.floor( Math.random() * ( max - min + 1 ) ) + min;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get a unique user color from the palette, or generate a variation if none are available.
|
|
34
|
+
* If the previously used color is available from localStorage, use it.
|
|
35
|
+
*
|
|
36
|
+
* @param existingColors - Colors that are already in use.
|
|
37
|
+
* @return The new user color, in hex format.
|
|
38
|
+
*/
|
|
39
|
+
function getNewUserColor( existingColors: string[] ): string {
|
|
40
|
+
const availableColors = COLOR_PALETTE.filter(
|
|
41
|
+
( color ) => ! existingColors.includes( color )
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
let hexColor: string;
|
|
45
|
+
|
|
46
|
+
if ( availableColors.length > 0 ) {
|
|
47
|
+
const randomIndex = generateRandomInt( 0, availableColors.length - 1 );
|
|
48
|
+
hexColor = availableColors[ randomIndex ];
|
|
49
|
+
} else {
|
|
50
|
+
// All colors are used, generate a variation of a random palette color
|
|
51
|
+
const randomIndex = generateRandomInt( 0, COLOR_PALETTE.length - 1 );
|
|
52
|
+
const baseColor = COLOR_PALETTE[ randomIndex ];
|
|
53
|
+
hexColor = generateColorVariation( baseColor );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return hexColor;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a variation of a hex color by adjusting its lightness.
|
|
61
|
+
*
|
|
62
|
+
* @param hexColor - The base hex color (e.g., '#3858E9').
|
|
63
|
+
* @return A varied hex color.
|
|
64
|
+
*/
|
|
65
|
+
function generateColorVariation( hexColor: string ): string {
|
|
66
|
+
// Parse hex to RGB
|
|
67
|
+
const r = parseInt( hexColor.slice( 1, 3 ), 16 );
|
|
68
|
+
const g = parseInt( hexColor.slice( 3, 5 ), 16 );
|
|
69
|
+
const b = parseInt( hexColor.slice( 5, 7 ), 16 );
|
|
70
|
+
|
|
71
|
+
// Apply a random lightness shift (-30 to +30)
|
|
72
|
+
const shift = generateRandomInt( -30, 30 );
|
|
73
|
+
const newR = Math.min( 255, Math.max( 0, r + shift ) );
|
|
74
|
+
const newG = Math.min( 255, Math.max( 0, g + shift ) );
|
|
75
|
+
const newB = Math.min( 255, Math.max( 0, b + shift ) );
|
|
76
|
+
|
|
77
|
+
// Convert back to hex
|
|
78
|
+
const toHex = ( n: number ) =>
|
|
79
|
+
n.toString( 16 ).padStart( 2, '0' ).toUpperCase();
|
|
80
|
+
return `#${ toHex( newR ) }${ toHex( newG ) }${ toHex( newB ) }`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the browser name from the user agent.
|
|
85
|
+
* @return The browser name.
|
|
86
|
+
*/
|
|
87
|
+
function getBrowserName(): string {
|
|
88
|
+
const userAgent = window.navigator.userAgent;
|
|
89
|
+
let browserName = 'Unknown';
|
|
90
|
+
|
|
91
|
+
if ( userAgent.includes( 'Firefox' ) ) {
|
|
92
|
+
browserName = 'Firefox';
|
|
93
|
+
} else if ( userAgent.includes( 'Edg' ) ) {
|
|
94
|
+
browserName = 'Microsoft Edge';
|
|
95
|
+
} else if (
|
|
96
|
+
userAgent.includes( 'Chrome' ) &&
|
|
97
|
+
! userAgent.includes( 'Edg' )
|
|
98
|
+
) {
|
|
99
|
+
browserName = 'Chrome';
|
|
100
|
+
} else if (
|
|
101
|
+
userAgent.includes( 'Safari' ) &&
|
|
102
|
+
! userAgent.includes( 'Chrome' )
|
|
103
|
+
) {
|
|
104
|
+
browserName = 'Safari';
|
|
105
|
+
} else if (
|
|
106
|
+
userAgent.includes( 'MSIE' ) ||
|
|
107
|
+
userAgent.includes( 'Trident' )
|
|
108
|
+
) {
|
|
109
|
+
browserName = 'Internet Explorer';
|
|
110
|
+
} else if ( userAgent.includes( 'Opera' ) || userAgent.includes( 'OPR' ) ) {
|
|
111
|
+
browserName = 'Opera';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return browserName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if two user infos are equal.
|
|
119
|
+
*
|
|
120
|
+
* @param userInfo1 - The first user info.
|
|
121
|
+
* @param userInfo2 - The second user info.
|
|
122
|
+
* @return True if the user infos are equal, false otherwise.
|
|
123
|
+
*/
|
|
124
|
+
export function areUserInfosEqual(
|
|
125
|
+
userInfo1?: UserInfo,
|
|
126
|
+
userInfo2?: UserInfo
|
|
127
|
+
): boolean {
|
|
128
|
+
if ( ! userInfo1 || ! userInfo2 ) {
|
|
129
|
+
return userInfo1 === userInfo2;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ( Object.keys( userInfo1 ).length !== Object.keys( userInfo2 ).length ) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return Object.entries( userInfo1 ).every( ( [ key, value ] ) => {
|
|
137
|
+
// Update this function with any non-primitive fields added to UserInfo.
|
|
138
|
+
return value === userInfo2[ key as keyof UserInfo ];
|
|
139
|
+
} );
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a user info object from a current user and a list of existing colors.
|
|
144
|
+
*
|
|
145
|
+
* @param currentUser - The current user.
|
|
146
|
+
* @param existingColors - The existing colors.
|
|
147
|
+
* @return The user info object.
|
|
148
|
+
*/
|
|
149
|
+
export function generateUserInfo(
|
|
150
|
+
currentUser: User< 'view' >,
|
|
151
|
+
existingColors: string[]
|
|
152
|
+
): UserInfo {
|
|
153
|
+
return {
|
|
154
|
+
...currentUser,
|
|
155
|
+
browserType: getBrowserName(),
|
|
156
|
+
color: getNewUserColor( existingColors ),
|
|
157
|
+
enteredAt: Date.now(),
|
|
158
|
+
};
|
|
159
|
+
}
|
package/src/entities.js
CHANGED
|
@@ -13,9 +13,11 @@ import { __ } from '@wordpress/i18n';
|
|
|
13
13
|
/**
|
|
14
14
|
* Internal dependencies
|
|
15
15
|
*/
|
|
16
|
+
import { PostEditorAwareness } from './awareness/post-editor-awareness';
|
|
16
17
|
import { getSyncManager } from './sync';
|
|
17
18
|
import {
|
|
18
19
|
applyPostChangesToCRDTDoc,
|
|
20
|
+
defaultSyncConfig,
|
|
19
21
|
getPostChangesFromCRDTDoc,
|
|
20
22
|
} from './utils/crdt';
|
|
21
23
|
|
|
@@ -218,7 +220,16 @@ export const rootEntitiesConfig = [
|
|
|
218
220
|
plural: 'fontCollections',
|
|
219
221
|
key: 'slug',
|
|
220
222
|
},
|
|
221
|
-
]
|
|
223
|
+
].map( ( entity ) => {
|
|
224
|
+
const syncEnabledRootEntities = new Set( [ 'comment' ] );
|
|
225
|
+
|
|
226
|
+
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
|
|
227
|
+
if ( syncEnabledRootEntities.has( entity.name ) ) {
|
|
228
|
+
entity.syncConfig = defaultSyncConfig;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return entity;
|
|
232
|
+
} );
|
|
222
233
|
|
|
223
234
|
export const deprecatedEntities = {
|
|
224
235
|
root: {
|
|
@@ -358,6 +369,19 @@ async function loadPostTypeEntities() {
|
|
|
358
369
|
applyChangesToCRDTDoc: ( crdtDoc, changes ) =>
|
|
359
370
|
applyPostChangesToCRDTDoc( crdtDoc, changes, postType ),
|
|
360
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Create the awareness instance for the entity's CRDT document.
|
|
374
|
+
*
|
|
375
|
+
* @param {import('@wordpress/sync').CRDTDoc} ydoc
|
|
376
|
+
* @param {import('@wordpress/sync').ObjectID} objectId
|
|
377
|
+
* @return {import('@wordpress/sync').AwarenessState} AwarenessState instance
|
|
378
|
+
*/
|
|
379
|
+
createAwareness: ( ydoc, objectId ) => {
|
|
380
|
+
const kind = 'postType';
|
|
381
|
+
const id = parseInt( objectId, 10 );
|
|
382
|
+
return new PostEditorAwareness( ydoc, kind, name, id );
|
|
383
|
+
},
|
|
384
|
+
|
|
361
385
|
/**
|
|
362
386
|
* Extract changes from a CRDT document that can be used to update the
|
|
363
387
|
* local editor state.
|
|
@@ -399,7 +423,7 @@ async function loadTaxonomyEntities() {
|
|
|
399
423
|
} );
|
|
400
424
|
return Object.entries( taxonomies ?? {} ).map( ( [ name, taxonomy ] ) => {
|
|
401
425
|
const namespace = taxonomy?.rest_namespace ?? 'wp/v2';
|
|
402
|
-
|
|
426
|
+
const entity = {
|
|
403
427
|
kind: 'taxonomy',
|
|
404
428
|
baseURL: `/${ namespace }/${ taxonomy.rest_base }`,
|
|
405
429
|
baseURLParams: { context: 'edit' },
|
|
@@ -408,6 +432,12 @@ async function loadTaxonomyEntities() {
|
|
|
408
432
|
getTitle: ( record ) => record?.name,
|
|
409
433
|
supportsPagination: true,
|
|
410
434
|
};
|
|
435
|
+
|
|
436
|
+
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
|
|
437
|
+
entity.syncConfig = defaultSyncConfig;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return entity;
|
|
411
441
|
} );
|
|
412
442
|
}
|
|
413
443
|
|
|
@@ -20,7 +20,7 @@ describe( 'Attachment type', () => {
|
|
|
20
20
|
describe( 'Image attachment', () => {
|
|
21
21
|
it( 'should validate against real image attachment from REST API', () => {
|
|
22
22
|
const attachment: Attachment< 'edit' > =
|
|
23
|
-
imageAttachmentFixture as Attachment< 'edit' >;
|
|
23
|
+
imageAttachmentFixture as unknown as Attachment< 'edit' >;
|
|
24
24
|
|
|
25
25
|
// Edit-context fields
|
|
26
26
|
expect( attachment.permalink_template ).toBeDefined();
|
|
@@ -38,7 +38,7 @@ describe( 'Attachment type', () => {
|
|
|
38
38
|
describe( 'Zip file attachment', () => {
|
|
39
39
|
it( 'should validate against real zip file attachment from REST API', () => {
|
|
40
40
|
const attachment: Attachment< 'edit' > =
|
|
41
|
-
zipAttachmentFixture as Attachment< 'edit' >;
|
|
41
|
+
zipAttachmentFixture as unknown as Attachment< 'edit' >;
|
|
42
42
|
|
|
43
43
|
// Edit-context fields
|
|
44
44
|
expect( attachment.permalink_template ).toBeDefined();
|
|
@@ -58,7 +58,7 @@ describe( 'Attachment type', () => {
|
|
|
58
58
|
describe( 'Audio file attachment', () => {
|
|
59
59
|
it( 'should validate against real audio attachment from REST API', () => {
|
|
60
60
|
const attachment: Attachment< 'edit' > =
|
|
61
|
-
audioAttachmentFixture as Attachment< 'edit' >;
|
|
61
|
+
audioAttachmentFixture as unknown as Attachment< 'edit' >;
|
|
62
62
|
|
|
63
63
|
// Edit-context fields
|
|
64
64
|
expect( attachment.permalink_template ).toBeDefined();
|
|
@@ -79,7 +79,7 @@ describe( 'Attachment type', () => {
|
|
|
79
79
|
describe( 'Video file attachment', () => {
|
|
80
80
|
it( 'should validate against real video attachment from REST API', () => {
|
|
81
81
|
const attachment: Attachment< 'edit' > =
|
|
82
|
-
videoAttachmentFixture as Attachment< 'edit' >;
|
|
82
|
+
videoAttachmentFixture as unknown as Attachment< 'edit' >;
|
|
83
83
|
|
|
84
84
|
// Edit-context fields
|
|
85
85
|
expect( attachment.permalink_template ).toBeDefined();
|
package/src/resolvers.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isNumericID,
|
|
27
27
|
} from './utils';
|
|
28
28
|
import { fetchBlockPatterns } from './fetch';
|
|
29
|
+
import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Requests authors from the REST API.
|
|
@@ -192,7 +193,7 @@ export const getEntityRecord =
|
|
|
192
193
|
recordWithTransients,
|
|
193
194
|
{
|
|
194
195
|
// Handle edits sourced from the sync manager.
|
|
195
|
-
editRecord: ( edits ) => {
|
|
196
|
+
editRecord: ( edits, options = {} ) => {
|
|
196
197
|
if ( ! Object.keys( edits ).length ) {
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
@@ -206,6 +207,7 @@ export const getEntityRecord =
|
|
|
206
207
|
meta: {
|
|
207
208
|
undo: undefined,
|
|
208
209
|
},
|
|
210
|
+
options,
|
|
209
211
|
} );
|
|
210
212
|
},
|
|
211
213
|
// Get the current entity record (with edits)
|
|
@@ -232,6 +234,36 @@ export const getEntityRecord =
|
|
|
232
234
|
key
|
|
233
235
|
);
|
|
234
236
|
},
|
|
237
|
+
addUndoMeta: ( ydoc, meta ) => {
|
|
238
|
+
const selectionHistory =
|
|
239
|
+
getSelectionHistory( ydoc );
|
|
240
|
+
|
|
241
|
+
if ( selectionHistory ) {
|
|
242
|
+
meta.set(
|
|
243
|
+
'selectionHistory',
|
|
244
|
+
selectionHistory
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
restoreUndoMeta: ( ydoc, meta ) => {
|
|
249
|
+
const selectionHistory =
|
|
250
|
+
meta.get( 'selectionHistory' );
|
|
251
|
+
|
|
252
|
+
if ( selectionHistory ) {
|
|
253
|
+
// Because Yjs initiates an undo, we need to
|
|
254
|
+
// wait until the content is restored before
|
|
255
|
+
// we can update the selection.
|
|
256
|
+
// Use setTimeout() to wait until content is
|
|
257
|
+
// finished updating, and then set the correct
|
|
258
|
+
// selection.
|
|
259
|
+
setTimeout( () => {
|
|
260
|
+
restoreSelection(
|
|
261
|
+
selectionHistory,
|
|
262
|
+
ydoc
|
|
263
|
+
);
|
|
264
|
+
}, 0 );
|
|
265
|
+
}
|
|
266
|
+
},
|
|
235
267
|
}
|
|
236
268
|
);
|
|
237
269
|
}
|
|
@@ -430,6 +462,26 @@ export const getEntityRecords =
|
|
|
430
462
|
};
|
|
431
463
|
}
|
|
432
464
|
|
|
465
|
+
if ( globalThis.IS_GUTENBERG_PLUGIN ) {
|
|
466
|
+
if ( entityConfig.syncConfig && -1 === query.per_page ) {
|
|
467
|
+
const objectType = `${ kind }/${ name }`;
|
|
468
|
+
getSyncManager()?.loadCollection(
|
|
469
|
+
entityConfig.syncConfig,
|
|
470
|
+
objectType,
|
|
471
|
+
{
|
|
472
|
+
refetchRecords: async () => {
|
|
473
|
+
dispatch.receiveEntityRecords(
|
|
474
|
+
kind,
|
|
475
|
+
name,
|
|
476
|
+
await apiFetch( { path, parse: true } ),
|
|
477
|
+
query
|
|
478
|
+
);
|
|
479
|
+
},
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
433
485
|
// If we request fields but the result doesn't contain the fields,
|
|
434
486
|
// explicitly set these fields as "undefined"
|
|
435
487
|
// that way we consider the query "fulfilled".
|