@wordpress/block-library 9.34.1-next.2f1c7c01b.0 → 9.35.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/block/edit.js +2 -2
- package/build/block/edit.js.map +2 -2
- package/build/block-keyboard-shortcuts/index.js +17 -7
- package/build/block-keyboard-shortcuts/index.js.map +2 -2
- package/build/cover/deprecated.js +15 -3
- package/build/cover/deprecated.js.map +2 -2
- package/build/cover/edit/inspector-controls.js +1 -1
- package/build/cover/edit/inspector-controls.js.map +2 -2
- package/build/cover/transforms.js +10 -2
- package/build/cover/transforms.js.map +2 -2
- package/build/embed/icons.js +2 -2
- package/build/embed/icons.js.map +2 -2
- package/build/embed/variations.js +3 -3
- package/build/embed/variations.js.map +2 -2
- package/build/heading/index.js +3 -1
- package/build/heading/index.js.map +3 -3
- package/build/heading/transforms.js +10 -3
- package/build/heading/transforms.js.map +2 -2
- package/build/heading/variations.js +55 -0
- package/build/heading/variations.js.map +7 -0
- package/build/html/edit.js +54 -44
- package/build/html/edit.js.map +3 -3
- package/build/html/modal.js +328 -0
- package/build/html/modal.js.map +7 -0
- package/build/html/utils.js +72 -0
- package/build/html/utils.js.map +7 -0
- package/build/navigation-link/edit.js +25 -10
- package/build/navigation-link/edit.js.map +2 -2
- package/build/navigation-link/link-ui/index.js +8 -3
- package/build/navigation-link/link-ui/index.js.map +2 -2
- package/build/navigation-link/shared/controls.js +42 -7
- package/build/navigation-link/shared/controls.js.map +2 -2
- package/build/navigation-link/shared/use-entity-binding.js +31 -2
- package/build/navigation-link/shared/use-entity-binding.js.map +3 -3
- package/build/paragraph/block.json +1 -3
- package/build/paragraph/deprecated.js +65 -12
- package/build/paragraph/deprecated.js.map +2 -2
- package/build/paragraph/edit.js +14 -25
- package/build/paragraph/edit.js.map +2 -2
- package/build/paragraph/index.js +3 -1
- package/build/paragraph/index.js.map +3 -3
- package/build/paragraph/save.js +3 -3
- package/build/paragraph/save.js.map +2 -2
- package/build/paragraph/transforms.js +7 -1
- package/build/paragraph/transforms.js.map +2 -2
- package/build/paragraph/variations.js +57 -0
- package/build/paragraph/variations.js.map +7 -0
- package/build-module/block/edit.js +2 -2
- package/build-module/block/edit.js.map +2 -2
- package/build-module/block-keyboard-shortcuts/index.js +17 -7
- package/build-module/block-keyboard-shortcuts/index.js.map +2 -2
- package/build-module/cover/deprecated.js +15 -3
- package/build-module/cover/deprecated.js.map +2 -2
- package/build-module/cover/edit/inspector-controls.js +1 -1
- package/build-module/cover/edit/inspector-controls.js.map +2 -2
- package/build-module/cover/transforms.js +10 -2
- package/build-module/cover/transforms.js.map +2 -2
- package/build-module/embed/icons.js +2 -2
- package/build-module/embed/icons.js.map +2 -2
- package/build-module/embed/variations.js +3 -3
- package/build-module/embed/variations.js.map +2 -2
- package/build-module/heading/index.js +3 -1
- package/build-module/heading/index.js.map +2 -2
- package/build-module/heading/transforms.js +10 -3
- package/build-module/heading/transforms.js.map +2 -2
- package/build-module/heading/variations.js +34 -0
- package/build-module/heading/variations.js.map +7 -0
- package/build-module/html/edit.js +62 -51
- package/build-module/html/edit.js.map +2 -2
- package/build-module/html/modal.js +304 -0
- package/build-module/html/modal.js.map +7 -0
- package/build-module/html/utils.js +46 -0
- package/build-module/html/utils.js.map +7 -0
- package/build-module/navigation-link/edit.js +25 -10
- package/build-module/navigation-link/edit.js.map +2 -2
- package/build-module/navigation-link/link-ui/index.js +8 -3
- package/build-module/navigation-link/link-ui/index.js.map +2 -2
- package/build-module/navigation-link/shared/controls.js +42 -7
- package/build-module/navigation-link/shared/controls.js.map +2 -2
- package/build-module/navigation-link/shared/use-entity-binding.js +35 -3
- package/build-module/navigation-link/shared/use-entity-binding.js.map +2 -2
- package/build-module/paragraph/block.json +1 -3
- package/build-module/paragraph/deprecated.js +65 -12
- package/build-module/paragraph/deprecated.js.map +2 -2
- package/build-module/paragraph/edit.js +14 -26
- package/build-module/paragraph/edit.js.map +2 -2
- package/build-module/paragraph/index.js +3 -1
- package/build-module/paragraph/index.js.map +2 -2
- package/build-module/paragraph/save.js +3 -3
- package/build-module/paragraph/save.js.map +2 -2
- package/build-module/paragraph/transforms.js +7 -1
- package/build-module/paragraph/transforms.js.map +2 -2
- package/build-module/paragraph/variations.js +36 -0
- package/build-module/paragraph/variations.js.map +7 -0
- package/build-style/accordion-heading/style-rtl.css +19 -3
- package/build-style/accordion-heading/style.css +19 -3
- package/build-style/accordion-panel/style-rtl.css +4 -1
- package/build-style/accordion-panel/style.css +4 -1
- package/build-style/common-rtl.css +3 -3
- package/build-style/common.css +3 -3
- package/build-style/editor-rtl.css +62 -21
- package/build-style/editor.css +62 -21
- package/build-style/embed/style-rtl.css +5 -0
- package/build-style/embed/style.css +5 -0
- package/build-style/html/editor-rtl.css +55 -21
- package/build-style/html/editor.css +55 -21
- package/build-style/navigation-link/editor-rtl.css +7 -0
- package/build-style/navigation-link/editor.css +7 -0
- package/build-style/style-rtl.css +31 -7
- package/build-style/style.css +31 -7
- package/package.json +37 -37
- package/src/accordion-heading/style.scss +40 -7
- package/src/accordion-panel/style.scss +6 -1
- package/src/block/edit.js +2 -2
- package/src/block-keyboard-shortcuts/index.js +23 -9
- package/src/common.scss +6 -5
- package/src/cover/deprecated.js +15 -3
- package/src/cover/edit/inspector-controls.js +1 -1
- package/src/cover/transforms.js +10 -2
- package/src/embed/icons.js +2 -4
- package/src/embed/style.scss +6 -0
- package/src/embed/variations.js +3 -3
- package/src/heading/index.js +2 -0
- package/src/heading/transforms.js +10 -3
- package/src/heading/variations.js +37 -0
- package/src/html/edit.js +62 -56
- package/src/html/editor.scss +69 -10
- package/src/html/modal.js +290 -0
- package/src/html/test/utils.js +234 -0
- package/src/html/utils.js +75 -0
- package/src/navigation-link/edit.js +44 -13
- package/src/navigation-link/editor.scss +7 -0
- package/src/navigation-link/index.php +65 -2
- package/src/navigation-link/link-ui/index.js +9 -8
- package/src/navigation-link/shared/controls.js +70 -12
- package/src/navigation-link/shared/test/controls.js +5 -0
- package/src/navigation-link/shared/test/use-entity-binding.js +14 -1
- package/src/navigation-link/shared/use-entity-binding.js +57 -9
- package/src/paragraph/block.json +1 -3
- package/src/paragraph/deprecated.js +87 -20
- package/src/paragraph/edit.js +7 -18
- package/src/paragraph/edit.native.js +18 -6
- package/src/paragraph/index.js +2 -0
- package/src/paragraph/save.js +4 -3
- package/src/paragraph/test/edit.native.js +5 -5
- package/src/paragraph/transforms.js +7 -1
- package/src/paragraph/variations.js +39 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -97,21 +97,35 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
|
|
|
97
97
|
const hasId = Number.isInteger( id );
|
|
98
98
|
const blockEditingMode = useBlockEditingMode();
|
|
99
99
|
|
|
100
|
-
const postStatus = useSelect(
|
|
100
|
+
const { postStatus, isDeleted } = useSelect(
|
|
101
101
|
( select ) => {
|
|
102
102
|
if ( ! isPostType ) {
|
|
103
|
-
return null;
|
|
103
|
+
return { postStatus: null, isDeleted: false };
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Fetching the posts status is an "expensive" operation. Especially for sites with large navigations.
|
|
107
107
|
// When the block is rendered in a template or other disabled contexts we can skip this check in order
|
|
108
108
|
// to avoid all these additional requests that don't really add any value in that mode.
|
|
109
109
|
if ( blockEditingMode === 'disabled' || ! enabled ) {
|
|
110
|
-
return null;
|
|
110
|
+
return { postStatus: null, isDeleted: false };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
const { getEntityRecord } =
|
|
114
|
-
|
|
113
|
+
const { getEntityRecord, hasFinishedResolution } =
|
|
114
|
+
select( coreStore );
|
|
115
|
+
const entityRecord = getEntityRecord( 'postType', type, id );
|
|
116
|
+
const hasResolved = hasFinishedResolution( 'getEntityRecord', [
|
|
117
|
+
'postType',
|
|
118
|
+
type,
|
|
119
|
+
id,
|
|
120
|
+
] );
|
|
121
|
+
|
|
122
|
+
// If resolution has finished and entityRecord is undefined, the entity was deleted.
|
|
123
|
+
const deleted = hasResolved && entityRecord === undefined;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
postStatus: entityRecord?.status,
|
|
127
|
+
isDeleted: deleted,
|
|
128
|
+
};
|
|
115
129
|
},
|
|
116
130
|
[ isPostType, blockEditingMode, enabled, type, id ]
|
|
117
131
|
);
|
|
@@ -121,11 +135,13 @@ const useIsInvalidLink = ( kind, type, id, enabled ) => {
|
|
|
121
135
|
// 2. It has an id.
|
|
122
136
|
// 3. It's neither null, nor undefined, as valid items might be either of those while loading.
|
|
123
137
|
// If those conditions are met, check if
|
|
124
|
-
// 1. The post status is
|
|
125
|
-
// 2. The
|
|
138
|
+
// 1. The post status is trash (trashed).
|
|
139
|
+
// 2. The entity doesn't exist (deleted).
|
|
126
140
|
// If either of those is true, invalidate.
|
|
127
141
|
const isInvalid =
|
|
128
|
-
isPostType &&
|
|
142
|
+
isPostType &&
|
|
143
|
+
hasId &&
|
|
144
|
+
( isDeleted || ( postStatus && 'trash' === postStatus ) );
|
|
129
145
|
const isDraft = 'draft' === postStatus;
|
|
130
146
|
|
|
131
147
|
return [ isInvalid, isDraft ];
|
|
@@ -248,7 +264,12 @@ export default function NavigationLinkEdit( {
|
|
|
248
264
|
const { getBlocks } = useSelect( blockEditorStore );
|
|
249
265
|
|
|
250
266
|
// URL binding logic
|
|
251
|
-
const {
|
|
267
|
+
const {
|
|
268
|
+
clearBinding,
|
|
269
|
+
createBinding,
|
|
270
|
+
hasUrlBinding,
|
|
271
|
+
isBoundEntityAvailable,
|
|
272
|
+
} = useEntityBinding( {
|
|
252
273
|
clientId,
|
|
253
274
|
attributes,
|
|
254
275
|
} );
|
|
@@ -405,14 +426,23 @@ export default function NavigationLinkEdit( {
|
|
|
405
426
|
}
|
|
406
427
|
);
|
|
407
428
|
|
|
408
|
-
if (
|
|
429
|
+
if (
|
|
430
|
+
! url ||
|
|
431
|
+
isInvalid ||
|
|
432
|
+
isDraft ||
|
|
433
|
+
( hasUrlBinding && ! isBoundEntityAvailable )
|
|
434
|
+
) {
|
|
409
435
|
blockProps.onClick = () => {
|
|
410
436
|
setIsLinkOpen( true );
|
|
411
437
|
};
|
|
412
438
|
}
|
|
413
439
|
|
|
414
440
|
const classes = clsx( 'wp-block-navigation-item__content', {
|
|
415
|
-
'wp-block-navigation-link__placeholder':
|
|
441
|
+
'wp-block-navigation-link__placeholder':
|
|
442
|
+
! url ||
|
|
443
|
+
isInvalid ||
|
|
444
|
+
isDraft ||
|
|
445
|
+
( hasUrlBinding && ! isBoundEntityAvailable ),
|
|
416
446
|
} );
|
|
417
447
|
|
|
418
448
|
const missingText = getMissingText( type );
|
|
@@ -531,9 +561,10 @@ export default function NavigationLinkEdit( {
|
|
|
531
561
|
link={ attributes }
|
|
532
562
|
onClose={ () => {
|
|
533
563
|
setIsLinkOpen( false );
|
|
534
|
-
// If there is no link
|
|
564
|
+
// If there is no link and no binding, remove the auto-inserted block.
|
|
535
565
|
// This avoids empty blocks which can provided a poor UX.
|
|
536
|
-
if (
|
|
566
|
+
// Don't remove if binding exists (even if entity is unavailable) so user can fix it.
|
|
567
|
+
if ( ! url && ! hasUrlBinding ) {
|
|
537
568
|
onReplace( [] );
|
|
538
569
|
} else if ( isNewLink.current ) {
|
|
539
570
|
// If we just created a new link, select it
|
|
@@ -312,11 +312,50 @@ function build_variation_for_navigation_link( $entity, $kind ) {
|
|
|
312
312
|
$title = '';
|
|
313
313
|
$description = '';
|
|
314
314
|
|
|
315
|
+
// Get default labels based on entity type
|
|
316
|
+
$default_labels = null;
|
|
317
|
+
if ( $entity instanceof WP_Post_Type ) {
|
|
318
|
+
$default_labels = WP_Post_Type::get_default_labels();
|
|
319
|
+
} elseif ( $entity instanceof WP_Taxonomy ) {
|
|
320
|
+
$default_labels = WP_Taxonomy::get_default_labels();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Get title and check if it's default
|
|
324
|
+
$is_default_title = false;
|
|
315
325
|
if ( property_exists( $entity->labels, 'item_link' ) ) {
|
|
316
326
|
$title = $entity->labels->item_link;
|
|
327
|
+
if ( isset( $default_labels['item_link'] ) ) {
|
|
328
|
+
$is_default_title = in_array( $title, $default_labels['item_link'], true );
|
|
329
|
+
}
|
|
317
330
|
}
|
|
331
|
+
|
|
332
|
+
// Get description and check if it's default
|
|
333
|
+
$is_default_description = false;
|
|
318
334
|
if ( property_exists( $entity->labels, 'item_link_description' ) ) {
|
|
319
335
|
$description = $entity->labels->item_link_description;
|
|
336
|
+
if ( isset( $default_labels['item_link_description'] ) ) {
|
|
337
|
+
$is_default_description = in_array( $description, $default_labels['item_link_description'], true );
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Calculate singular name once (used for both title and description)
|
|
342
|
+
$singular = isset( $entity->labels->singular_name ) ? $entity->labels->singular_name : ucfirst( $entity->name );
|
|
343
|
+
|
|
344
|
+
// Set default title if needed
|
|
345
|
+
if ( $is_default_title || '' === $title ) {
|
|
346
|
+
/* translators: %s: Singular label of the entity. */
|
|
347
|
+
$title = sprintf( __( '%s link' ), $singular );
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Default description if needed.
|
|
351
|
+
// Use a single space character instead of an empty string to prevent fallback to the
|
|
352
|
+
// block.json default description ("Add a page, link, or another item to your navigation.").
|
|
353
|
+
// An empty string would be treated as missing and trigger the fallback, while a single
|
|
354
|
+
// space appears blank in the UI but prevents the fallback behavior.
|
|
355
|
+
// We avoid generating descriptions like "A link to a %s" to prevent grammatical errors
|
|
356
|
+
// (e.g., "A link to a event" should be "A link to an event").
|
|
357
|
+
if ( $is_default_description || '' === $description ) {
|
|
358
|
+
$description = ' ';
|
|
320
359
|
}
|
|
321
360
|
|
|
322
361
|
$variation = array(
|
|
@@ -368,6 +407,7 @@ function build_variation_for_navigation_link( $entity, $kind ) {
|
|
|
368
407
|
*
|
|
369
408
|
* @param array $variations Array of registered variations for a block type.
|
|
370
409
|
* @param WP_Block_Type $block_type The full block type object.
|
|
410
|
+
* @return array Numerically indexed array of block variations.
|
|
371
411
|
*/
|
|
372
412
|
function block_core_navigation_link_filter_variations( $variations, $block_type ) {
|
|
373
413
|
if ( 'core/navigation-link' !== $block_type->name ) {
|
|
@@ -375,7 +415,28 @@ function block_core_navigation_link_filter_variations( $variations, $block_type
|
|
|
375
415
|
}
|
|
376
416
|
|
|
377
417
|
$generated_variations = block_core_navigation_link_build_variations();
|
|
378
|
-
|
|
418
|
+
|
|
419
|
+
/*
|
|
420
|
+
* IMPORTANT: Order matters for deduplication.
|
|
421
|
+
*
|
|
422
|
+
* The variations returned from this filter are bootstrapped to JavaScript and
|
|
423
|
+
* processed by the block variations reducer. The reducer uses `getUniqueItemsByName()`
|
|
424
|
+
* (packages/blocks/src/store/reducer.js:51-57) which keeps the FIRST variation with
|
|
425
|
+
* a given 'name' and discards later duplicates when processing the array in order.
|
|
426
|
+
*
|
|
427
|
+
* By placing generated variations first in `array_merge()`, the improved
|
|
428
|
+
* labels (e.g., "Product link" instead of generic "Post Link") are processed first
|
|
429
|
+
* and preserved. The generic incoming variations are then discarded as duplicates.
|
|
430
|
+
*
|
|
431
|
+
* Why `array_merge()` instead of manual deduplication?
|
|
432
|
+
* - Both arrays use numeric indices (0, 1, 2...), so `array_merge()` concatenates
|
|
433
|
+
* and re-indexes them sequentially, preserving order
|
|
434
|
+
* - The reducer handles deduplication, so it is not needed here
|
|
435
|
+
* - This keeps the PHP code simple and relies on the established JavaScript behavior
|
|
436
|
+
*
|
|
437
|
+
* See: https://github.com/WordPress/gutenberg/pull/72517
|
|
438
|
+
*/
|
|
439
|
+
return array_merge( $generated_variations, $variations );
|
|
379
440
|
}
|
|
380
441
|
|
|
381
442
|
/**
|
|
@@ -419,7 +480,9 @@ function block_core_navigation_link_build_variations() {
|
|
|
419
480
|
}
|
|
420
481
|
}
|
|
421
482
|
|
|
422
|
-
|
|
483
|
+
$all_variations = array_merge( $built_ins, $variations );
|
|
484
|
+
|
|
485
|
+
return $all_variations;
|
|
423
486
|
}
|
|
424
487
|
|
|
425
488
|
/**
|
|
@@ -26,6 +26,7 @@ import { useInstanceId } from '@wordpress/compose';
|
|
|
26
26
|
*/
|
|
27
27
|
import { LinkUIPageCreator } from './page-creator';
|
|
28
28
|
import LinkUIBlockInserter from './block-inserter';
|
|
29
|
+
import { useEntityBinding } from '../shared/use-entity-binding';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Given the Link block's type attribute, return the query params to give to
|
|
@@ -66,7 +67,8 @@ export function getSuggestionsQuery( type, kind ) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
function UnforwardedLinkUI( props, ref ) {
|
|
69
|
-
const { label, url, opensInNewTab, type, kind, id
|
|
70
|
+
const { label, url, opensInNewTab, type, kind, id } = props.link;
|
|
71
|
+
const { clientId } = props;
|
|
70
72
|
const postType = type || 'page';
|
|
71
73
|
|
|
72
74
|
const [ addingBlock, setAddingBlock ] = useState( false );
|
|
@@ -78,12 +80,11 @@ function UnforwardedLinkUI( props, ref ) {
|
|
|
78
80
|
name: postType,
|
|
79
81
|
} );
|
|
80
82
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
!! id;
|
|
83
|
+
// Use the entity binding hook to get binding status
|
|
84
|
+
const { isBoundEntityAvailable } = useEntityBinding( {
|
|
85
|
+
clientId,
|
|
86
|
+
attributes: props.link,
|
|
87
|
+
} );
|
|
87
88
|
|
|
88
89
|
// Memoize link value to avoid overriding the LinkControl's internal state.
|
|
89
90
|
// This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/50976#issuecomment-1568226407.
|
|
@@ -152,7 +153,7 @@ function UnforwardedLinkUI( props, ref ) {
|
|
|
152
153
|
onChange={ props.onChange }
|
|
153
154
|
onRemove={ props.onRemove }
|
|
154
155
|
onCancel={ props.onCancel }
|
|
155
|
-
handleEntities={
|
|
156
|
+
handleEntities={ isBoundEntityAvailable }
|
|
156
157
|
renderControlBottom={ () => {
|
|
157
158
|
// Don't show the tools when there is submitted link (preview state).
|
|
158
159
|
if ( link?.url?.length ) {
|
|
@@ -87,10 +87,11 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
87
87
|
}, [ url ] );
|
|
88
88
|
|
|
89
89
|
// Use the entity binding hook internally
|
|
90
|
-
const { hasUrlBinding, clearBinding } =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
const { hasUrlBinding, isBoundEntityAvailable, clearBinding } =
|
|
91
|
+
useEntityBinding( {
|
|
92
|
+
clientId,
|
|
93
|
+
attributes,
|
|
94
|
+
} );
|
|
94
95
|
|
|
95
96
|
// Get direct store dispatch to bypass setBoundAttributes wrapper
|
|
96
97
|
const { updateBlockAttributes } = useDispatch( blockEditorStore );
|
|
@@ -111,8 +112,7 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
useEffect( () => {
|
|
114
|
-
//
|
|
115
|
-
// only want to focus the input if the url is not bound to an entity.
|
|
115
|
+
// Only want to focus the input if the url is not bound to an entity.
|
|
116
116
|
if ( ! hasUrlBinding && shouldFocusURLInputRef.current ) {
|
|
117
117
|
// focuses and highlights the url input value, giving the user
|
|
118
118
|
// the ability to delete the value quickly or edit it.
|
|
@@ -165,12 +165,28 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
165
165
|
__next40pxDefaultSize
|
|
166
166
|
id={ inputId }
|
|
167
167
|
label={ __( 'Link' ) }
|
|
168
|
-
value={
|
|
168
|
+
value={ ( () => {
|
|
169
|
+
if ( hasUrlBinding && ! isBoundEntityAvailable ) {
|
|
170
|
+
return '';
|
|
171
|
+
}
|
|
172
|
+
return inputValue ? safeDecodeURI( inputValue ) : '';
|
|
173
|
+
} )() }
|
|
169
174
|
autoComplete="off"
|
|
170
175
|
type="url"
|
|
171
176
|
disabled={ hasUrlBinding }
|
|
177
|
+
aria-invalid={
|
|
178
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
179
|
+
? 'true'
|
|
180
|
+
: undefined
|
|
181
|
+
}
|
|
182
|
+
aria-describedby={ helpTextId }
|
|
183
|
+
className={
|
|
184
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
185
|
+
? 'navigation-link-control__input-with-error-suffix'
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
172
188
|
onChange={ ( newValue ) => {
|
|
173
|
-
if (
|
|
189
|
+
if ( isBoundEntityAvailable ) {
|
|
174
190
|
return;
|
|
175
191
|
}
|
|
176
192
|
|
|
@@ -180,13 +196,13 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
180
196
|
setInputValue( newValue );
|
|
181
197
|
} }
|
|
182
198
|
onFocus={ () => {
|
|
183
|
-
if (
|
|
199
|
+
if ( isBoundEntityAvailable ) {
|
|
184
200
|
return;
|
|
185
201
|
}
|
|
186
202
|
lastURLRef.current = url;
|
|
187
203
|
} }
|
|
188
204
|
onBlur={ () => {
|
|
189
|
-
if (
|
|
205
|
+
if ( isBoundEntityAvailable ) {
|
|
190
206
|
return;
|
|
191
207
|
}
|
|
192
208
|
|
|
@@ -204,11 +220,19 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
204
220
|
} );
|
|
205
221
|
} }
|
|
206
222
|
help={
|
|
207
|
-
hasUrlBinding && (
|
|
208
|
-
<
|
|
223
|
+
hasUrlBinding && ! isBoundEntityAvailable ? (
|
|
224
|
+
<MissingEntityHelpText
|
|
225
|
+
id={ helpTextId }
|
|
209
226
|
type={ attributes.type }
|
|
210
227
|
kind={ attributes.kind }
|
|
211
228
|
/>
|
|
229
|
+
) : (
|
|
230
|
+
isBoundEntityAvailable && (
|
|
231
|
+
<BindingHelpText
|
|
232
|
+
type={ attributes.type }
|
|
233
|
+
kind={ attributes.kind }
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
212
236
|
)
|
|
213
237
|
}
|
|
214
238
|
suffix={
|
|
@@ -225,6 +249,11 @@ export function Controls( { attributes, setAttributes, clientId } ) {
|
|
|
225
249
|
showTooltip
|
|
226
250
|
label={ __( 'Unsync and edit' ) }
|
|
227
251
|
__next40pxDefaultSize
|
|
252
|
+
className={
|
|
253
|
+
hasUrlBinding && ! isBoundEntityAvailable
|
|
254
|
+
? 'navigation-link-control__error-suffix-button'
|
|
255
|
+
: undefined
|
|
256
|
+
}
|
|
228
257
|
/>
|
|
229
258
|
)
|
|
230
259
|
}
|
|
@@ -306,3 +335,32 @@ function BindingHelpText( { type, kind } ) {
|
|
|
306
335
|
entityType
|
|
307
336
|
);
|
|
308
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Component to display error help text for missing entity bindings.
|
|
341
|
+
*
|
|
342
|
+
* @param {Object} props - Component props
|
|
343
|
+
* @param {string} props.id - ID for the help text element (for aria-describedby)
|
|
344
|
+
* @param {string} props.type - The entity type
|
|
345
|
+
* @param {string} props.kind - The entity kind
|
|
346
|
+
* @return {JSX.Element} Error help text component
|
|
347
|
+
*/
|
|
348
|
+
function MissingEntityHelpText( { id, type, kind } ) {
|
|
349
|
+
const entityType = getEntityTypeName( type, kind );
|
|
350
|
+
return (
|
|
351
|
+
<span
|
|
352
|
+
id={ id }
|
|
353
|
+
className="navigation-link-control__error-text"
|
|
354
|
+
role="alert"
|
|
355
|
+
aria-live="polite"
|
|
356
|
+
>
|
|
357
|
+
{ sprintf(
|
|
358
|
+
/* translators: %s is the entity type (e.g., "page", "post", "category") */
|
|
359
|
+
__(
|
|
360
|
+
'Synced %s is missing. Please update or remove this link.'
|
|
361
|
+
),
|
|
362
|
+
entityType
|
|
363
|
+
) }
|
|
364
|
+
</span>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
@@ -28,6 +28,7 @@ jest.mock( '../../../utils/hooks', () => ( {
|
|
|
28
28
|
jest.mock( '../use-entity-binding', () => ( {
|
|
29
29
|
useEntityBinding: jest.fn( () => ( {
|
|
30
30
|
hasUrlBinding: false,
|
|
31
|
+
isBoundEntityAvailable: false,
|
|
31
32
|
clearBinding: jest.fn(),
|
|
32
33
|
} ) ),
|
|
33
34
|
} ) );
|
|
@@ -202,6 +203,7 @@ describe( 'Controls', () => {
|
|
|
202
203
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
203
204
|
useEntityBinding.mockReturnValue( {
|
|
204
205
|
hasUrlBinding: true,
|
|
206
|
+
isBoundEntityAvailable: true,
|
|
205
207
|
clearBinding: jest.fn(),
|
|
206
208
|
} );
|
|
207
209
|
|
|
@@ -225,6 +227,7 @@ describe( 'Controls', () => {
|
|
|
225
227
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
226
228
|
useEntityBinding.mockReturnValue( {
|
|
227
229
|
hasUrlBinding: true,
|
|
230
|
+
isBoundEntityAvailable: true,
|
|
228
231
|
clearBinding: jest.fn(),
|
|
229
232
|
} );
|
|
230
233
|
|
|
@@ -262,6 +265,7 @@ describe( 'Controls', () => {
|
|
|
262
265
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
263
266
|
useEntityBinding.mockReturnValue( {
|
|
264
267
|
hasUrlBinding: true,
|
|
268
|
+
isBoundEntityAvailable: true,
|
|
265
269
|
clearBinding: jest.fn(),
|
|
266
270
|
} );
|
|
267
271
|
|
|
@@ -285,6 +289,7 @@ describe( 'Controls', () => {
|
|
|
285
289
|
const { useEntityBinding } = require( '../use-entity-binding' );
|
|
286
290
|
useEntityBinding.mockReturnValue( {
|
|
287
291
|
hasUrlBinding: true,
|
|
292
|
+
isBoundEntityAvailable: true,
|
|
288
293
|
clearBinding: jest.fn(),
|
|
289
294
|
} );
|
|
290
295
|
|
|
@@ -18,12 +18,23 @@ import {
|
|
|
18
18
|
// Mock the entire @wordpress/block-editor module
|
|
19
19
|
jest.mock( '@wordpress/block-editor', () => ( {
|
|
20
20
|
useBlockBindingsUtils: jest.fn(),
|
|
21
|
+
useBlockEditingMode: jest.fn(),
|
|
21
22
|
} ) );
|
|
22
23
|
|
|
24
|
+
// Mock useSelect specifically to avoid needing to set up full data store
|
|
25
|
+
jest.mock( '@wordpress/data/src/components/use-select', () => {
|
|
26
|
+
const mock = jest.fn();
|
|
27
|
+
return mock;
|
|
28
|
+
} );
|
|
29
|
+
|
|
23
30
|
/**
|
|
24
31
|
* WordPress dependencies
|
|
25
32
|
*/
|
|
26
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
useBlockBindingsUtils,
|
|
35
|
+
useBlockEditingMode,
|
|
36
|
+
} from '@wordpress/block-editor';
|
|
37
|
+
import { useSelect } from '@wordpress/data';
|
|
27
38
|
|
|
28
39
|
describe( 'useEntityBinding', () => {
|
|
29
40
|
const mockUpdateBlockBindings = jest.fn();
|
|
@@ -33,6 +44,8 @@ describe( 'useEntityBinding', () => {
|
|
|
33
44
|
useBlockBindingsUtils.mockReturnValue( {
|
|
34
45
|
updateBlockBindings: mockUpdateBlockBindings,
|
|
35
46
|
} );
|
|
47
|
+
useBlockEditingMode.mockReturnValue( 'default' );
|
|
48
|
+
useSelect.mockReturnValue( true );
|
|
36
49
|
} );
|
|
37
50
|
|
|
38
51
|
describe( 'hasUrlBinding', () => {
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* WordPress dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { useCallback } from '@wordpress/element';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
useBlockBindingsUtils,
|
|
7
|
+
useBlockEditingMode,
|
|
8
|
+
} from '@wordpress/block-editor';
|
|
9
|
+
import { useSelect } from '@wordpress/data';
|
|
10
|
+
import { store as coreStore } from '@wordpress/core-data';
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* Builds entity binding configuration for navigation link URLs.
|
|
@@ -16,7 +21,7 @@ import { useBlockBindingsUtils } from '@wordpress/block-editor';
|
|
|
16
21
|
* @throws {Error} If kind is not 'post-type' or 'taxonomy'
|
|
17
22
|
*/
|
|
18
23
|
export function buildNavigationLinkEntityBinding( kind ) {
|
|
19
|
-
// Validate kind parameter exists
|
|
24
|
+
// Validate kind parameter exists.
|
|
20
25
|
if ( kind === undefined ) {
|
|
21
26
|
throw new Error(
|
|
22
27
|
'buildNavigationLinkEntityBinding requires a kind parameter. ' +
|
|
@@ -24,7 +29,7 @@ export function buildNavigationLinkEntityBinding( kind ) {
|
|
|
24
29
|
);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
// Validate kind parameter value
|
|
32
|
+
// Validate kind parameter value.
|
|
28
33
|
if ( kind !== 'post-type' && kind !== 'taxonomy' ) {
|
|
29
34
|
throw new Error(
|
|
30
35
|
`Invalid kind "${ kind }" provided to buildNavigationLinkEntityBinding. ` +
|
|
@@ -57,7 +62,8 @@ export function buildNavigationLinkEntityBinding( kind ) {
|
|
|
57
62
|
*/
|
|
58
63
|
export function useEntityBinding( { clientId, attributes } ) {
|
|
59
64
|
const { updateBlockBindings } = useBlockBindingsUtils( clientId );
|
|
60
|
-
const { metadata, id, kind } = attributes;
|
|
65
|
+
const { metadata, id, kind, type } = attributes;
|
|
66
|
+
const blockEditingMode = useBlockEditingMode();
|
|
61
67
|
|
|
62
68
|
const hasUrlBinding = !! metadata?.bindings?.url && !! id;
|
|
63
69
|
const expectedSource =
|
|
@@ -65,6 +71,47 @@ export function useEntityBinding( { clientId, attributes } ) {
|
|
|
65
71
|
const hasCorrectBinding =
|
|
66
72
|
hasUrlBinding && metadata?.bindings?.url?.source === expectedSource;
|
|
67
73
|
|
|
74
|
+
// Check if the bound entity is available (not deleted).
|
|
75
|
+
const isBoundEntityAvailable = useSelect(
|
|
76
|
+
( select ) => {
|
|
77
|
+
// First check: metadata/binding must exist
|
|
78
|
+
if ( ! hasCorrectBinding || ! id ) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isPostType = kind === 'post-type';
|
|
83
|
+
const isTaxonomy = kind === 'taxonomy';
|
|
84
|
+
|
|
85
|
+
// Only check entity availability for post types and taxonomies.
|
|
86
|
+
if ( ! isPostType && ! isTaxonomy ) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip check in disabled contexts to avoid unnecessary requests.
|
|
91
|
+
if ( blockEditingMode === 'disabled' ) {
|
|
92
|
+
return true; // Assume available in disabled contexts.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Second check: entity must exist
|
|
96
|
+
const { getEntityRecord, hasFinishedResolution } =
|
|
97
|
+
select( coreStore );
|
|
98
|
+
|
|
99
|
+
// Use the correct entity type based on kind.
|
|
100
|
+
const entityType = isTaxonomy ? 'taxonomy' : 'postType';
|
|
101
|
+
const entityRecord = getEntityRecord( entityType, type, id );
|
|
102
|
+
const hasResolved = hasFinishedResolution( 'getEntityRecord', [
|
|
103
|
+
entityType,
|
|
104
|
+
type,
|
|
105
|
+
id,
|
|
106
|
+
] );
|
|
107
|
+
|
|
108
|
+
// If resolution has finished and entityRecord is undefined, the entity was deleted.
|
|
109
|
+
// Return true if entity exists, false if deleted.
|
|
110
|
+
return hasResolved ? entityRecord !== undefined : true;
|
|
111
|
+
},
|
|
112
|
+
[ kind, type, id, hasCorrectBinding, blockEditingMode ]
|
|
113
|
+
);
|
|
114
|
+
|
|
68
115
|
const clearBinding = useCallback( () => {
|
|
69
116
|
if ( hasUrlBinding ) {
|
|
70
117
|
updateBlockBindings( { url: undefined } );
|
|
@@ -73,11 +120,11 @@ export function useEntityBinding( { clientId, attributes } ) {
|
|
|
73
120
|
|
|
74
121
|
const createBinding = useCallback(
|
|
75
122
|
( updatedAttributes ) => {
|
|
76
|
-
// Use updated attributes if provided, otherwise fall back to closure attributes
|
|
77
|
-
// updatedAttributes needed to access the most up-to-date data when called synchronously
|
|
123
|
+
// Use updated attributes if provided, otherwise fall back to closure attributes.
|
|
124
|
+
// updatedAttributes needed to access the most up-to-date data when called synchronously.
|
|
78
125
|
const kindToUse = updatedAttributes?.kind ?? kind;
|
|
79
126
|
|
|
80
|
-
// Avoid creating binding if no kind is provided
|
|
127
|
+
// Avoid creating binding if no kind is provided.
|
|
81
128
|
if ( ! kindToUse ) {
|
|
82
129
|
return;
|
|
83
130
|
}
|
|
@@ -91,14 +138,15 @@ export function useEntityBinding( { clientId, attributes } ) {
|
|
|
91
138
|
'Failed to create entity binding:',
|
|
92
139
|
error.message
|
|
93
140
|
);
|
|
94
|
-
// Don't create binding if validation fails
|
|
141
|
+
// Don't create binding if validation fails.
|
|
95
142
|
}
|
|
96
143
|
},
|
|
97
|
-
[ updateBlockBindings, kind
|
|
144
|
+
[ updateBlockBindings, kind ]
|
|
98
145
|
);
|
|
99
146
|
|
|
100
147
|
return {
|
|
101
148
|
hasUrlBinding: hasCorrectBinding,
|
|
149
|
+
isBoundEntityAvailable,
|
|
102
150
|
clearBinding,
|
|
103
151
|
createBinding,
|
|
104
152
|
};
|
package/src/paragraph/block.json
CHANGED
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
"keywords": [ "text" ],
|
|
9
9
|
"textdomain": "default",
|
|
10
10
|
"attributes": {
|
|
11
|
-
"align": {
|
|
12
|
-
"type": "string"
|
|
13
|
-
},
|
|
14
11
|
"content": {
|
|
15
12
|
"type": "rich-text",
|
|
16
13
|
"source": "rich-text",
|
|
@@ -58,6 +55,7 @@
|
|
|
58
55
|
"typography": {
|
|
59
56
|
"fontSize": true,
|
|
60
57
|
"lineHeight": true,
|
|
58
|
+
"textAlign": true,
|
|
61
59
|
"__experimentalFontFamily": true,
|
|
62
60
|
"__experimentalTextDecoration": true,
|
|
63
61
|
"__experimentalFontStyle": true,
|