@wordpress/core-data 7.46.0 → 7.48.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 +4 -0
- package/README.md +0 -27
- package/build/actions.cjs +0 -19
- package/build/actions.cjs.map +2 -2
- package/build/awareness/block-lookup.cjs +13 -0
- package/build/awareness/block-lookup.cjs.map +2 -2
- package/build/awareness/post-editor-awareness.cjs +21 -9
- package/build/awareness/post-editor-awareness.cjs.map +2 -2
- package/build/hooks/use-entity-block-editor.cjs +4 -4
- package/build/hooks/use-entity-block-editor.cjs.map +2 -2
- package/build/hooks/use-post-editor-awareness-state.cjs +2 -1
- package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
- package/build/hooks/use-resource-permissions.cjs +3 -5
- package/build/hooks/use-resource-permissions.cjs.map +2 -2
- package/build/index.cjs +0 -6
- package/build/index.cjs.map +2 -2
- package/build/parsed-blocks-cache.cjs +36 -0
- package/build/parsed-blocks-cache.cjs.map +7 -0
- package/build/private-actions.cjs +25 -2
- package/build/private-actions.cjs.map +2 -2
- package/build/private-apis.cjs +9 -5
- package/build/private-apis.cjs.map +3 -3
- package/build/private-selectors.cjs +15 -0
- package/build/private-selectors.cjs.map +2 -2
- package/build/resolvers.cjs +12 -2
- package/build/resolvers.cjs.map +2 -2
- package/build/selectors.cjs +0 -15
- package/build/selectors.cjs.map +2 -2
- package/build/sync.cjs +5 -0
- package/build/sync.cjs.map +2 -2
- package/build/types.cjs +0 -16
- package/build/types.cjs.map +3 -3
- package/build/utils/block-selection-history.cjs +5 -4
- package/build/utils/block-selection-history.cjs.map +2 -2
- package/build/utils/crdt-blocks.cjs +3 -0
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +10 -2
- package/build/utils/crdt-user-selections.cjs.map +3 -3
- package/build/utils/crdt-utils.cjs +23 -0
- package/build/utils/crdt-utils.cjs.map +2 -2
- package/build/utils/crdt.cjs +28 -4
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +0 -18
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/block-lookup.mjs +12 -0
- package/build-module/awareness/block-lookup.mjs.map +2 -2
- package/build-module/awareness/post-editor-awareness.mjs +26 -9
- package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
- package/build-module/hooks/use-entity-block-editor.mjs +2 -2
- package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
- package/build-module/hooks/use-post-editor-awareness-state.mjs +2 -1
- package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
- package/build-module/hooks/use-resource-permissions.mjs +3 -5
- package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
- package/build-module/index.mjs +0 -4
- package/build-module/index.mjs.map +2 -2
- package/build-module/parsed-blocks-cache.mjs +10 -0
- package/build-module/parsed-blocks-cache.mjs.map +7 -0
- package/build-module/private-actions.mjs +23 -1
- package/build-module/private-actions.mjs.map +2 -2
- package/build-module/private-apis.mjs +12 -5
- package/build-module/private-apis.mjs.map +3 -3
- package/build-module/private-selectors.mjs +14 -0
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/resolvers.mjs +12 -2
- 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 +4 -0
- package/build-module/sync.mjs.map +2 -2
- package/build-module/types.mjs +0 -9
- package/build-module/types.mjs.map +4 -4
- package/build-module/utils/block-selection-history.mjs +6 -4
- package/build-module/utils/block-selection-history.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +3 -0
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +10 -2
- package/build-module/utils/crdt-user-selections.mjs.map +3 -3
- package/build-module/utils/crdt-utils.mjs +22 -0
- package/build-module/utils/crdt-utils.mjs.map +2 -2
- package/build-module/utils/crdt.mjs +32 -5
- 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/block-lookup.d.ts +12 -0
- package/build-types/awareness/block-lookup.d.ts.map +1 -1
- package/build-types/awareness/post-editor-awareness.d.ts +2 -5
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
- package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
- package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
- package/build-types/index.d.ts +0 -8
- 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 +20 -0
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts +10 -0
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/queried-data/selectors.d.ts +1 -1
- package/build-types/queried-data/selectors.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/selectors.d.ts +0 -9
- package/build-types/selectors.d.ts.map +1 -1
- package/build-types/sync.d.ts +6 -0
- package/build-types/sync.d.ts.map +1 -1
- package/build-types/types.d.ts +3 -10
- 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.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +10 -1
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
- package/build-types/utils/crdt-utils.d.ts +11 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -1
- package/build-types/utils/crdt.d.ts +5 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +18 -18
- package/src/actions.js +0 -29
- package/src/awareness/block-lookup.ts +34 -0
- package/src/awareness/post-editor-awareness.ts +32 -14
- package/src/awareness/test/block-lookup.ts +70 -0
- package/src/awareness/test/post-editor-awareness.ts +243 -0
- package/src/hooks/test/use-post-editor-awareness-state.ts +3 -0
- package/src/hooks/test/use-resource-permissions.js +57 -0
- package/src/hooks/use-entity-block-editor.js +2 -2
- package/src/hooks/use-post-editor-awareness-state.ts +1 -0
- package/src/hooks/use-resource-permissions.ts +5 -7
- package/src/index.js +0 -7
- package/src/parsed-blocks-cache.js +12 -0
- package/src/private-actions.js +34 -0
- package/src/{private-apis.js → private-apis.ts} +13 -3
- package/src/private-selectors.ts +33 -0
- package/src/resolvers.js +27 -5
- package/src/selectors.ts +0 -32
- package/src/sync.ts +9 -0
- package/src/test/resolvers.js +13 -7
- package/src/types.ts +16 -11
- package/src/utils/block-selection-history.ts +10 -7
- package/src/utils/crdt-blocks.ts +24 -0
- package/src/utils/crdt-user-selections.ts +15 -2
- package/src/utils/crdt-utils.ts +41 -0
- package/src/utils/crdt.ts +83 -10
- package/src/utils/test/block-selection-history.test.ts +42 -0
- package/src/utils/test/crdt-blocks.ts +37 -0
- package/src/utils/test/crdt-user-selections.ts +39 -0
- package/src/utils/test/crdt-utils.ts +52 -0
- package/src/utils/test/crdt.ts +208 -2
|
@@ -127,6 +127,63 @@ describe( 'useResourcePermissions', () => {
|
|
|
127
127
|
);
|
|
128
128
|
} );
|
|
129
129
|
|
|
130
|
+
it( 'normalizes id-less entity resources before resolving permissions', async () => {
|
|
131
|
+
let data;
|
|
132
|
+
triggerFetch.mockImplementation( ( options ) => {
|
|
133
|
+
if ( options.path === '/wp/v2/types?context=view' ) {
|
|
134
|
+
return {
|
|
135
|
+
wp_navigation: {
|
|
136
|
+
name: 'Navigation Menus',
|
|
137
|
+
slug: 'wp_navigation',
|
|
138
|
+
rest_base: 'navigation',
|
|
139
|
+
rest_namespace: 'wp/v2',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
options.path === '/wp/v2/navigation' &&
|
|
145
|
+
options.method === 'OPTIONS'
|
|
146
|
+
) {
|
|
147
|
+
return {
|
|
148
|
+
headers: new Headers( { allow: 'GET, POST' } ),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Unexpected request: ${ JSON.stringify( options ) }`
|
|
153
|
+
);
|
|
154
|
+
} );
|
|
155
|
+
|
|
156
|
+
const TestComponent = () => {
|
|
157
|
+
data = useResourcePermissions( {
|
|
158
|
+
kind: 'postType',
|
|
159
|
+
name: 'wp_navigation',
|
|
160
|
+
id: undefined,
|
|
161
|
+
} );
|
|
162
|
+
return <div />;
|
|
163
|
+
};
|
|
164
|
+
render(
|
|
165
|
+
<RegistryProvider value={ registry }>
|
|
166
|
+
<TestComponent />
|
|
167
|
+
</RegistryProvider>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
await waitFor( () =>
|
|
171
|
+
expect( data ).toEqual( {
|
|
172
|
+
status: 'SUCCESS',
|
|
173
|
+
isResolving: false,
|
|
174
|
+
hasResolved: true,
|
|
175
|
+
canCreate: true,
|
|
176
|
+
canRead: true,
|
|
177
|
+
} )
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(
|
|
181
|
+
triggerFetch.mock.calls.filter(
|
|
182
|
+
( [ options ] ) => options.path === '/wp/v2/navigation'
|
|
183
|
+
)
|
|
184
|
+
).toHaveLength( 1 );
|
|
185
|
+
} );
|
|
186
|
+
|
|
130
187
|
it( 'retrieves the relevant permissions for an entity', async () => {
|
|
131
188
|
let data;
|
|
132
189
|
const TestComponent = () => {
|
|
@@ -11,9 +11,9 @@ import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
|
|
|
11
11
|
import { STORE_NAME } from '../name';
|
|
12
12
|
import useEntityId from './use-entity-id';
|
|
13
13
|
import { updateFootnotesFromMeta } from '../footnotes';
|
|
14
|
+
import { parsedBlocksCache, getCacheKey } from '../parsed-blocks-cache';
|
|
14
15
|
|
|
15
16
|
const EMPTY_ARRAY = [];
|
|
16
|
-
const parsedBlocksCache = new Map();
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Hook that returns block content getters and setters for
|
|
@@ -69,7 +69,7 @@ export default function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
|
|
|
69
69
|
|
|
70
70
|
// Cache parsed blocks by entity identity. Store the content
|
|
71
71
|
// alongside the blocks so we can validate it hasn't changed.
|
|
72
|
-
const cacheKey =
|
|
72
|
+
const cacheKey = getCacheKey( kind, name, id );
|
|
73
73
|
const cached = parsedBlocksCache.get( cacheKey );
|
|
74
74
|
let _blocks;
|
|
75
75
|
|
|
@@ -145,15 +145,13 @@ function useResourcePermissions< IdType = void >(
|
|
|
145
145
|
( resolve ) => {
|
|
146
146
|
const hasId = isEntity ? !! resource.id : !! id;
|
|
147
147
|
const { canUser } = resolve( coreStore );
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
: resource
|
|
153
|
-
);
|
|
148
|
+
const collectionResource = isEntity
|
|
149
|
+
? { kind: resource.kind, name: resource.name }
|
|
150
|
+
: resource;
|
|
151
|
+
const create = canUser( 'create', collectionResource );
|
|
154
152
|
|
|
155
153
|
if ( ! hasId ) {
|
|
156
|
-
const read = canUser( 'read',
|
|
154
|
+
const read = canUser( 'read', collectionResource );
|
|
157
155
|
|
|
158
156
|
const isResolving = create.isResolving || read.isResolving;
|
|
159
157
|
const hasResolved = create.hasResolved && read.hasResolved;
|
package/src/index.js
CHANGED
|
@@ -133,13 +133,6 @@ 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
|
-
export { SelectionDirection } from './types';
|
|
142
|
-
|
|
143
136
|
export { default as EntityProvider } from './entity-provider';
|
|
144
137
|
export * from './entity-provider';
|
|
145
138
|
export * from './entity-types';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cache of blocks parsed from an entity's `content` string, keyed by
|
|
3
|
+
* `kind:name:id`. Populated both eagerly by the `getEntityRecord` resolver
|
|
4
|
+
* (when the sync manager parses content for transient edits) and lazily by
|
|
5
|
+
* `useEntityBlockEditor`. The stored `content` string acts as a validator so
|
|
6
|
+
* stale entries are discarded when the underlying record changes.
|
|
7
|
+
*/
|
|
8
|
+
export const parsedBlocksCache = new Map();
|
|
9
|
+
|
|
10
|
+
export function getCacheKey( kind, name, id ) {
|
|
11
|
+
return `${ kind }:${ name }:${ id }`;
|
|
12
|
+
}
|
package/src/private-actions.js
CHANGED
|
@@ -7,6 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
9
|
import { STORE_NAME } from './name';
|
|
10
|
+
import { getSyncManager, hasSyncManager } from './sync';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Returns an action object used in signalling that the registered post meta
|
|
@@ -163,6 +164,7 @@ export function receiveEditorAssets( assets ) {
|
|
|
163
164
|
|
|
164
165
|
/**
|
|
165
166
|
* Returns an action object used to set whether collaboration is supported.
|
|
167
|
+
* When set to false, also disconnects all sync entities.
|
|
166
168
|
*
|
|
167
169
|
* @param {boolean} supported Whether collaboration is supported.
|
|
168
170
|
*
|
|
@@ -172,6 +174,9 @@ export const setCollaborationSupported =
|
|
|
172
174
|
( supported ) =>
|
|
173
175
|
( { dispatch } ) => {
|
|
174
176
|
dispatch( { type: 'SET_COLLABORATION_SUPPORTED', supported } );
|
|
177
|
+
if ( ! supported && hasSyncManager() ) {
|
|
178
|
+
getSyncManager().unloadAll();
|
|
179
|
+
}
|
|
175
180
|
};
|
|
176
181
|
|
|
177
182
|
/**
|
|
@@ -191,3 +196,32 @@ export function receiveViewConfig( kind, name, config ) {
|
|
|
191
196
|
config,
|
|
192
197
|
};
|
|
193
198
|
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns an action object used to set the sync connection status for an entity or collection.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} kind Kind of the entity.
|
|
204
|
+
* @param {string} name Name of the entity.
|
|
205
|
+
* @param {number|string|null} key The entity key, or null for collections.
|
|
206
|
+
* @param {Object|null} status The connection state object or null on unload.
|
|
207
|
+
*
|
|
208
|
+
* @return {Object} Action object.
|
|
209
|
+
*/
|
|
210
|
+
export function setSyncConnectionStatus( kind, name, key, status ) {
|
|
211
|
+
if ( ! status ) {
|
|
212
|
+
return {
|
|
213
|
+
type: 'CLEAR_SYNC_CONNECTION_STATUS',
|
|
214
|
+
kind,
|
|
215
|
+
name,
|
|
216
|
+
key,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
type: 'SET_SYNC_CONNECTION_STATUS',
|
|
222
|
+
kind,
|
|
223
|
+
name,
|
|
224
|
+
key,
|
|
225
|
+
status,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -12,9 +12,12 @@ import {
|
|
|
12
12
|
} from './hooks/use-post-editor-awareness-state';
|
|
13
13
|
import { lock } from './lock-unlock';
|
|
14
14
|
import { retrySyncConnection } from './sync';
|
|
15
|
+
import {
|
|
16
|
+
SelectionType,
|
|
17
|
+
SelectionDirection,
|
|
18
|
+
} from './utils/crdt-user-selections';
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
lock( privateApis, {
|
|
20
|
+
const lockedApis = {
|
|
18
21
|
useEntityRecordsWithPermissions,
|
|
19
22
|
RECEIVE_INTERMEDIATE_RESULTS,
|
|
20
23
|
retrySyncConnection,
|
|
@@ -23,4 +26,11 @@ lock( privateApis, {
|
|
|
23
26
|
useOnCollaboratorJoin,
|
|
24
27
|
useOnCollaboratorLeave,
|
|
25
28
|
useOnPostSave,
|
|
26
|
-
|
|
29
|
+
SelectionType,
|
|
30
|
+
SelectionDirection,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CoreDataPrivateApis = typeof lockedApis;
|
|
34
|
+
|
|
35
|
+
export const privateApis = {};
|
|
36
|
+
lock( privateApis, lockedApis );
|
package/src/private-selectors.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { createSelector, createRegistrySelector } from '@wordpress/data';
|
|
5
|
+
import type { ConnectionStatus } from '@wordpress/sync';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Internal dependencies
|
|
@@ -347,3 +348,35 @@ export function getViewConfig(
|
|
|
347
348
|
}
|
|
348
349
|
);
|
|
349
350
|
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Returns the current sync connection status across all entities. Prioritizes
|
|
354
|
+
* disconnected states, then connecting, then connected.
|
|
355
|
+
*
|
|
356
|
+
* @param state Data state.
|
|
357
|
+
*
|
|
358
|
+
* @return The current sync connection state, prioritized by importance.
|
|
359
|
+
*/
|
|
360
|
+
export function getSyncConnectionStatus(
|
|
361
|
+
state: State
|
|
362
|
+
): ConnectionStatus | undefined {
|
|
363
|
+
if ( ! state.syncConnectionStatuses ) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ];
|
|
368
|
+
|
|
369
|
+
let coalesced: ConnectionStatus | undefined;
|
|
370
|
+
|
|
371
|
+
for ( const status of Object.values( state.syncConnectionStatuses ) ) {
|
|
372
|
+
if (
|
|
373
|
+
! coalesced ||
|
|
374
|
+
PRIORITIZED_STATUSES.indexOf( status.status ) <
|
|
375
|
+
PRIORITIZED_STATUSES.indexOf( coalesced.status )
|
|
376
|
+
) {
|
|
377
|
+
coalesced = status;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return coalesced;
|
|
382
|
+
}
|
package/src/resolvers.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from './utils';
|
|
29
29
|
import { fetchBlockPatterns } from './fetch';
|
|
30
30
|
import { restoreSelection, getSelectionHistory } from './utils/crdt-selection';
|
|
31
|
+
import { parsedBlocksCache, getCacheKey } from './parsed-blocks-cache';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Requests authors from the REST API.
|
|
@@ -180,6 +181,18 @@ export const getEntityRecord =
|
|
|
180
181
|
transientConfig.read( recordWithTransients );
|
|
181
182
|
} );
|
|
182
183
|
|
|
184
|
+
// Share the parsed blocks with `useEntityBlockEditor` so the
|
|
185
|
+
// editor doesn't re-parse the same `content` string.
|
|
186
|
+
if (
|
|
187
|
+
recordWithTransients.blocks &&
|
|
188
|
+
typeof recordWithTransients.content?.raw === 'string'
|
|
189
|
+
) {
|
|
190
|
+
parsedBlocksCache.set( getCacheKey( kind, name, key ), {
|
|
191
|
+
content: recordWithTransients.content.raw,
|
|
192
|
+
blocks: recordWithTransients.blocks,
|
|
193
|
+
} );
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
// Load the entity record for syncing. Do not await promise.
|
|
184
197
|
void getSyncManager()?.load(
|
|
185
198
|
entityConfig.syncConfig,
|
|
@@ -247,13 +260,18 @@ export const getEntityRecord =
|
|
|
247
260
|
return;
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
// Trigger a save to persist the CRDT document. The
|
|
251
|
-
// pre-persist hooks will create the persisted CRDT
|
|
252
|
-
// and apply it to the record's meta.
|
|
263
|
+
// Trigger a minimal save to persist the CRDT document. The
|
|
264
|
+
// entity's pre-persist hooks will create the persisted CRDT
|
|
265
|
+
// document and apply it to the record's meta.
|
|
266
|
+
const entityIdKey =
|
|
267
|
+
entityConfig.key || DEFAULT_ENTITY_KEY;
|
|
253
268
|
dispatch.saveEntityRecord(
|
|
254
269
|
kind,
|
|
255
270
|
name,
|
|
256
|
-
|
|
271
|
+
{
|
|
272
|
+
[ entityIdKey ]:
|
|
273
|
+
editedRecord[ entityIdKey ],
|
|
274
|
+
},
|
|
257
275
|
{ __unstableSkipSyncUpdate: true }
|
|
258
276
|
);
|
|
259
277
|
} );
|
|
@@ -1014,10 +1032,14 @@ export const getDefaultTemplateId =
|
|
|
1014
1032
|
};
|
|
1015
1033
|
|
|
1016
1034
|
getDefaultTemplateId.shouldInvalidate = ( action ) => {
|
|
1035
|
+
// Only invalidate on real saves; `persistedEdits` is absent on
|
|
1036
|
+
// initial fetches so the kickoff's own site read doesn't wipe
|
|
1037
|
+
// the just-resolved template id.
|
|
1017
1038
|
return (
|
|
1018
1039
|
action.type === 'RECEIVE_ITEMS' &&
|
|
1019
1040
|
action.kind === 'root' &&
|
|
1020
|
-
action.name === 'site'
|
|
1041
|
+
action.name === 'site' &&
|
|
1042
|
+
!! action.persistedEdits
|
|
1021
1043
|
);
|
|
1022
1044
|
};
|
|
1023
1045
|
|
package/src/selectors.ts
CHANGED
|
@@ -1661,35 +1661,3 @@ export const getRevision = createSelector(
|
|
|
1661
1661
|
];
|
|
1662
1662
|
}
|
|
1663
1663
|
);
|
|
1664
|
-
|
|
1665
|
-
/**
|
|
1666
|
-
* Returns the current sync connection status across all entities. Prioritizes
|
|
1667
|
-
* disconnected states, then connecting, then connected.
|
|
1668
|
-
*
|
|
1669
|
-
* @param state Data state.
|
|
1670
|
-
*
|
|
1671
|
-
* @return The current sync connection state, prioritized by importance.
|
|
1672
|
-
*/
|
|
1673
|
-
export function getSyncConnectionStatus(
|
|
1674
|
-
state: State
|
|
1675
|
-
): ConnectionStatus | undefined {
|
|
1676
|
-
if ( ! state.syncConnectionStatuses ) {
|
|
1677
|
-
return undefined;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const PRIORITIZED_STATUSES = [ 'disconnected', 'connecting', 'connected' ];
|
|
1681
|
-
|
|
1682
|
-
let coalesced: ConnectionStatus | undefined;
|
|
1683
|
-
|
|
1684
|
-
for ( const status of Object.values( state.syncConnectionStatuses ) ) {
|
|
1685
|
-
if (
|
|
1686
|
-
! coalesced ||
|
|
1687
|
-
PRIORITIZED_STATUSES.indexOf( status.status ) <
|
|
1688
|
-
PRIORITIZED_STATUSES.indexOf( coalesced.status )
|
|
1689
|
-
) {
|
|
1690
|
-
coalesced = status;
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
return coalesced;
|
|
1695
|
-
}
|
package/src/sync.ts
CHANGED
|
@@ -43,3 +43,12 @@ export function getSyncManager(): SyncManager | undefined {
|
|
|
43
43
|
|
|
44
44
|
return syncManager;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return whether a sync manager has already been created. Use this when you
|
|
49
|
+
* only want to interact with an existing sync manager (e.g. to tear it down),
|
|
50
|
+
* without `getSyncManager()` bootstrapping one if none exists.
|
|
51
|
+
*/
|
|
52
|
+
export function hasSyncManager(): boolean {
|
|
53
|
+
return Boolean( syncManager );
|
|
54
|
+
}
|
package/src/test/resolvers.js
CHANGED
|
@@ -234,9 +234,14 @@ describe( 'getEntityRecord', () => {
|
|
|
234
234
|
expect( dispatch.saveEntityRecord ).not.toHaveBeenCalled();
|
|
235
235
|
} );
|
|
236
236
|
|
|
237
|
-
it( 'persistCRDTDoc
|
|
237
|
+
it( 'persistCRDTDoc saves only the entity ID and omits REST-invalid fields', async () => {
|
|
238
238
|
const POST_RECORD = { id: 1, title: 'Test Post', meta: {} };
|
|
239
|
-
const EDITED_RECORD = {
|
|
239
|
+
const EDITED_RECORD = {
|
|
240
|
+
id: 1,
|
|
241
|
+
title: 'Edited Post',
|
|
242
|
+
ping_status: '',
|
|
243
|
+
meta: { _crdt_document: 'doc2' },
|
|
244
|
+
};
|
|
240
245
|
const POST_RESPONSE = {
|
|
241
246
|
json: () => Promise.resolve( POST_RECORD ),
|
|
242
247
|
};
|
|
@@ -283,11 +288,12 @@ describe( 'getEntityRecord', () => {
|
|
|
283
288
|
resolveSelectWithSync.getEditedEntityRecord
|
|
284
289
|
).toHaveBeenCalledWith( 'postType', 'post', 1 );
|
|
285
290
|
|
|
286
|
-
// Should
|
|
291
|
+
// Should only send the entity ID. The pre-persist hook creates the
|
|
292
|
+
// persisted CRDT meta without round-tripping the full edited record.
|
|
287
293
|
expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
|
|
288
294
|
'postType',
|
|
289
295
|
'post',
|
|
290
|
-
|
|
296
|
+
{ id: 1 },
|
|
291
297
|
{ __unstableSkipSyncUpdate: true }
|
|
292
298
|
);
|
|
293
299
|
} );
|
|
@@ -335,11 +341,11 @@ describe( 'getEntityRecord', () => {
|
|
|
335
341
|
handlers.persistCRDTDoc();
|
|
336
342
|
await resolveSelectWithSync.getEditedEntityRecord();
|
|
337
343
|
|
|
338
|
-
// Should save the
|
|
344
|
+
// Should save only the entity ID even with no edits.
|
|
339
345
|
expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
|
|
340
346
|
'postType',
|
|
341
347
|
'post',
|
|
342
|
-
|
|
348
|
+
{ id: 1 },
|
|
343
349
|
{ __unstableSkipSyncUpdate: true }
|
|
344
350
|
);
|
|
345
351
|
} );
|
|
@@ -437,7 +443,7 @@ describe( 'getEntityRecord', () => {
|
|
|
437
443
|
expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith(
|
|
438
444
|
'postType',
|
|
439
445
|
'post',
|
|
440
|
-
|
|
446
|
+
{ id: 1 },
|
|
441
447
|
{ __unstableSkipSyncUpdate: true }
|
|
442
448
|
);
|
|
443
449
|
expect( syncManager.update ).toHaveBeenCalledWith(
|
package/src/types.ts
CHANGED
|
@@ -6,7 +6,10 @@ import type { ConnectionStatusDisconnected, Y } from '@wordpress/sync';
|
|
|
6
6
|
/**
|
|
7
7
|
* Internal dependencies
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
SelectionType,
|
|
11
|
+
SelectionDirection,
|
|
12
|
+
} from './utils/crdt-user-selections';
|
|
10
13
|
|
|
11
14
|
export type { ConnectionStatus } from '@wordpress/sync';
|
|
12
15
|
|
|
@@ -127,17 +130,11 @@ export type CursorPosition = {
|
|
|
127
130
|
// character. With both of these values as editor state, a change in perceived
|
|
128
131
|
// position will always result in a redraw.
|
|
129
132
|
absoluteOffset: number;
|
|
130
|
-
};
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/** The caret is at the end of the selection (default / left-to-right). */
|
|
137
|
-
Forward = 'f',
|
|
138
|
-
/** The caret is at the start of the selection (right-to-left). */
|
|
139
|
-
Backward = 'b',
|
|
140
|
-
}
|
|
134
|
+
// The sender's `WPBlockSelection.attributeKey` (e.g. `content` or
|
|
135
|
+
// `body.0.cells.0.content`).
|
|
136
|
+
attributeKey?: string;
|
|
137
|
+
};
|
|
141
138
|
|
|
142
139
|
export type SelectionNone = {
|
|
143
140
|
// The user has not made a selection.
|
|
@@ -192,4 +189,12 @@ export type SelectionState =
|
|
|
192
189
|
export interface ResolvedSelection {
|
|
193
190
|
richTextOffset: number | null;
|
|
194
191
|
localClientId: string | null;
|
|
192
|
+
|
|
193
|
+
// Identifier of the RichText attribute within the block, e.g.:
|
|
194
|
+
// - `content` on a core/paragraph block
|
|
195
|
+
// - `citation` on a quote block
|
|
196
|
+
// - a dot path into a nested attribute like `body.0.cells.0.content` for a
|
|
197
|
+
// core/table cell.
|
|
198
|
+
// Set to `null` for WholeBlock selections.
|
|
199
|
+
attributeKey: string | null;
|
|
195
200
|
}
|
|
@@ -12,6 +12,7 @@ import { Y } from '@wordpress/sync';
|
|
|
12
12
|
import {
|
|
13
13
|
asRichTextOffset,
|
|
14
14
|
findBlockByClientIdInDoc,
|
|
15
|
+
getYTextByAttributeKey,
|
|
15
16
|
richTextOffsetToHtmlIndex,
|
|
16
17
|
} from './crdt-utils';
|
|
17
18
|
import type { WPBlockSelection, WPSelection } from '../types';
|
|
@@ -147,14 +148,16 @@ function convertWPBlockSelectionToSelection(
|
|
|
147
148
|
const attributes = block?.get( 'attributes' );
|
|
148
149
|
const attributeKey = selection.attributeKey;
|
|
149
150
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const isYText = changedYText instanceof Y.Text;
|
|
155
|
-
const isFullyDefinedSelection = attributeKey && clientId;
|
|
151
|
+
let changedYText: Y.Text | null = null;
|
|
152
|
+
if ( attributeKey && attributes ) {
|
|
153
|
+
changedYText = getYTextByAttributeKey( attributes, attributeKey );
|
|
154
|
+
}
|
|
156
155
|
|
|
157
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
! ( changedYText instanceof Y.Text ) ||
|
|
158
|
+
! attributeKey ||
|
|
159
|
+
! clientId
|
|
160
|
+
) {
|
|
158
161
|
// We either don't have a valid YText (it's been deleted) or we've
|
|
159
162
|
// been passed a selection that's just a block clientId.
|
|
160
163
|
// Store as BlockSelection.
|
package/src/utils/crdt-blocks.ts
CHANGED
|
@@ -599,6 +599,30 @@ export function mergeCrdtBlocks(
|
|
|
599
599
|
break;
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
+
case 'clientId': {
|
|
603
|
+
// Never overwrite the local block's clientId with the
|
|
604
|
+
// incoming one. Some callers (e.g. the Code Editor flow
|
|
605
|
+
// that parses raw HTML into blocks on every keystroke)
|
|
606
|
+
// produce randomized clientIds for blocks whose content
|
|
607
|
+
// has changed on every sync. Without this case the default
|
|
608
|
+
// branch would replace the stable Y.Doc clientId with
|
|
609
|
+
// a new one, causing remote peers to remount the block
|
|
610
|
+
// and flash the block's content on reload.
|
|
611
|
+
//
|
|
612
|
+
// This mirrors the clientId exclusion in `areBlocksEqual`.
|
|
613
|
+
// Convergence is preserved. Because we're not writing
|
|
614
|
+
// to the clientId, Yjs doesn't send an update to peers
|
|
615
|
+
// telling them to change the clientId, so everyone
|
|
616
|
+
// sees the same clientId per block.
|
|
617
|
+
// Inserts still use a new clientId via createNewYBlock,
|
|
618
|
+
// and the duplicate-clientId sweep below catches any
|
|
619
|
+
// edge cases. The clientId is anchored to the
|
|
620
|
+
// slot in the array rather than to specific content,
|
|
621
|
+
// which is consistent with areBlocksEqual ignoring
|
|
622
|
+
// clientId when diffing.
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
602
626
|
default:
|
|
603
627
|
if (
|
|
604
628
|
! fastDeepEqual(
|
|
@@ -15,6 +15,7 @@ import type { YBlock, YBlocks } from './crdt-blocks';
|
|
|
15
15
|
import {
|
|
16
16
|
asRichTextOffset,
|
|
17
17
|
getRootMap,
|
|
18
|
+
getYTextByAttributeKey,
|
|
18
19
|
richTextOffsetToHtmlIndex,
|
|
19
20
|
} from './crdt-utils';
|
|
20
21
|
import type {
|
|
@@ -26,7 +27,6 @@ import type {
|
|
|
26
27
|
SelectionInOneBlock,
|
|
27
28
|
SelectionInMultipleBlocks,
|
|
28
29
|
SelectionWholeBlock,
|
|
29
|
-
SelectionDirection,
|
|
30
30
|
CursorPosition,
|
|
31
31
|
} from '../types';
|
|
32
32
|
|
|
@@ -41,6 +41,16 @@ export enum SelectionType {
|
|
|
41
41
|
WholeBlock = 'whole-block',
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The direction of a text selection, indicating where the caret sits.
|
|
46
|
+
*/
|
|
47
|
+
export enum SelectionDirection {
|
|
48
|
+
/** The caret is at the end of the selection (default / left-to-right). */
|
|
49
|
+
Forward = 'f',
|
|
50
|
+
/** The caret is at the start of the selection (right-to-left). */
|
|
51
|
+
Backward = 'b',
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* Converts WordPress block editor selection to a SelectionState.
|
|
46
56
|
*
|
|
@@ -173,7 +183,9 @@ function getCursorPosition(
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
const attributes = block.get( 'attributes' );
|
|
176
|
-
const currentYText = attributes
|
|
186
|
+
const currentYText = attributes
|
|
187
|
+
? getYTextByAttributeKey( attributes, selection.attributeKey )
|
|
188
|
+
: null;
|
|
177
189
|
|
|
178
190
|
// If the attribute is not a Y.Text, return null.
|
|
179
191
|
if ( ! ( currentYText instanceof Y.Text ) ) {
|
|
@@ -191,6 +203,7 @@ function getCursorPosition(
|
|
|
191
203
|
return {
|
|
192
204
|
relativePosition,
|
|
193
205
|
absoluteOffset: selection.offset,
|
|
206
|
+
attributeKey: selection.attributeKey,
|
|
194
207
|
};
|
|
195
208
|
}
|
|
196
209
|
|
package/src/utils/crdt-utils.ts
CHANGED
|
@@ -125,6 +125,47 @@ export function asHtmlStringIndex( index: number ): HtmlStringIndex {
|
|
|
125
125
|
return index as HtmlStringIndex;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a selection attribute key to a Y.Text value.
|
|
130
|
+
*
|
|
131
|
+
* RichText identifiers are normally top-level block attribute keys, but nested
|
|
132
|
+
* rich-text fields can provide a dot path such as `body.0.cells.0.content`.
|
|
133
|
+
*
|
|
134
|
+
* @param attributes The block attributes map.
|
|
135
|
+
* @param attributeKey The top-level attribute key or nested attribute path.
|
|
136
|
+
* @return The matching Y.Text, or null if the path is not a rich-text field.
|
|
137
|
+
*/
|
|
138
|
+
export function getYTextByAttributeKey(
|
|
139
|
+
attributes: Y.Map< unknown >,
|
|
140
|
+
attributeKey: string
|
|
141
|
+
): Y.Text | null {
|
|
142
|
+
const directValue = attributes.get( attributeKey );
|
|
143
|
+
if ( directValue instanceof Y.Text ) {
|
|
144
|
+
return directValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let value: unknown = attributes;
|
|
148
|
+
for ( const pathPart of attributeKey.split( '.' ) ) {
|
|
149
|
+
if ( value instanceof Y.Map ) {
|
|
150
|
+
value = value.get( pathPart );
|
|
151
|
+
} else if ( value instanceof Y.Array ) {
|
|
152
|
+
const index = Number.parseInt( pathPart, 10 );
|
|
153
|
+
if (
|
|
154
|
+
! Number.isSafeInteger( index ) ||
|
|
155
|
+
index < 0 ||
|
|
156
|
+
index.toString() !== pathPart
|
|
157
|
+
) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
value = value.get( index );
|
|
161
|
+
} else {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return value instanceof Y.Text ? value : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
128
169
|
/**
|
|
129
170
|
* Given a block ID and a Y.Doc, find the block in the document.
|
|
130
171
|
*
|