From 55913fe1e86e2891b4f86aa1cf1cfae8b9cf5e25 Mon Sep 17 00:00:00 2001 From: Benni <109149472+benniledl@users.noreply.github.com> Date: Sun, 10 Nov 2024 13:47:41 +0100 Subject: [PATCH 01/18] Add 6.6.2 to Version in WordPress (#66870) Update versions-in-wordpress.md Co-authored-by: benniledl Co-authored-by: t-hamano --- docs/contributors/versions-in-wordpress.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index 62347f2d644a6..4ba7b34da1555 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -7,6 +7,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | | 18.6-19.3 | 6.7 | +| 17.8-18.5 | 6.6.2 | | 17.8-18.5 | 6.6.1 | | 17.8-18.5 | 6.6 | | 16.8-17.7 | 6.5.5 | From dd042a265afc0d4679858220441dd45c98b27ed3 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Mon, 11 Nov 2024 08:50:37 +0100 Subject: [PATCH 02/18] Popover: Fix missing label of the headerTitle Close button. (#66813) * Popover: Fix missing label of the headerTitle Close button. * Add changelog entry. Co-authored-by: afercia Co-authored-by: Mamaduka --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/popover/index.tsx | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 97cb5d117e17d..abd32b37c4c65 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `Radio`: Deprecate 36px default size ([#66572](https://github.com/WordPress/gutenberg/pull/66572)). +### Bug Fixes + +- `Popover`: Fix missing label of the headerTitle Close button ([#66813](https://github.com/WordPress/gutenberg/pull/66813)). + ### Enhancements - `MenuItem`: Add 40px size prop on Button ([#66596](https://github.com/WordPress/gutenberg/pull/66596)). diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 1b7fb31539c6d..7db476db6b1db 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -39,6 +39,7 @@ import { import { close } from '@wordpress/icons'; import deprecated from '@wordpress/deprecated'; import { Path, SVG } from '@wordpress/primitives'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -422,6 +423,7 @@ const UnforwardedPopover = ( size="small" icon={ close } onClick={ onClose } + label={ __( 'Close' ) } /> ) } From 17a18097fa7943d0c889e1b4d0c74cd4090b6fa3 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 11 Nov 2024 20:21:25 +1100 Subject: [PATCH 03/18] Style book:reduce margin selector specificity to avoid overriding global block styles (#66895) Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw --- packages/edit-site/src/components/style-book/constants.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts index 6aa280c937d42..9aace34e64cbf 100644 --- a/packages/edit-site/src/components/style-book/constants.ts +++ b/packages/edit-site/src/components/style-book/constants.ts @@ -254,11 +254,10 @@ export const STYLE_BOOK_IFRAME_STYLES = ` .edit-site-style-book__example-preview .block-list-appender { display: none; } - - .edit-site-style-book__example-preview .is-root-container > .wp-block:first-child { + :where(.is-root-container > .wp-block:first-child) { margin-top: 0; } - .edit-site-style-book__example-preview .is-root-container > .wp-block:last-child { + :where(.is-root-container > .wp-block:last-child) { margin-bottom: 0; } `; From de5262393ca74a542e1eea04ea8c48ed4c270e7d Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:44:50 +0100 Subject: [PATCH 04/18] Inserter: Add 'Starter Content' category to the inserter (#66819) Co-authored-by: ellatrix Co-authored-by: youknowriad Co-authored-by: carolinan --- .../block-patterns-tab/pattern-category-previews.js | 8 ++++++++ .../inserter/block-patterns-tab/use-pattern-categories.js | 8 ++++++++ .../src/components/inserter/block-patterns-tab/utils.js | 5 +++++ 3 files changed, 21 insertions(+) diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js index 2c5d96d5ff973..a19a579ae5c0c 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js @@ -31,6 +31,7 @@ import { isPatternFiltered, allPatternsCategory, myPatternsCategory, + starterPatternsCategory, INSERTER_PATTERN_TYPES, } from './utils'; import { store as blockEditorStore } from '../../../store'; @@ -86,6 +87,13 @@ export function PatternCategoryPreviews( { return true; } + if ( + category.name === starterPatternsCategory.name && + pattern.blockTypes?.includes( 'core/post-content' ) + ) { + return true; + } + if ( category.name === 'uncategorized' ) { // The uncategorized category should show all the patterns without any category... if ( ! pattern.categories ) { diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js index cff5fbf341382..2adc6b48579dd 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js @@ -14,6 +14,7 @@ import { isPatternFiltered, allPatternsCategory, myPatternsCategory, + starterPatternsCategory, INSERTER_PATTERN_TYPES, } from './utils'; @@ -67,6 +68,13 @@ export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { label: _x( 'Uncategorized' ), } ); } + if ( + filteredPatterns.some( ( pattern ) => + pattern.blockTypes?.includes( 'core/post-content' ) + ) + ) { + categories.unshift( starterPatternsCategory ); + } if ( filteredPatterns.some( ( pattern ) => pattern.type === INSERTER_PATTERN_TYPES.user diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js index f8ba47f3790e1..58065ac33ad2b 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js @@ -25,6 +25,11 @@ export const myPatternsCategory = { label: __( 'My patterns' ), }; +export const starterPatternsCategory = { + name: 'core/starter-content', + label: __( 'Starter Content' ), +}; + export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { const isUserPattern = pattern.name.startsWith( 'core/block' ); const isDirectoryPattern = From c9e4eab4b7bc8df076bad584c2b16ef06fdc210c Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Mon, 11 Nov 2024 11:47:14 +0100 Subject: [PATCH 05/18] Fix TypeError when duplicating uncategorized theme patterns (#66889) * Fix TypeError when duplicating uncategorized patterns Only try to match the pattern categories and get the category label if the theme pattern uses the optional `categories` parameter. Co-authored-by: carolinan Co-authored-by: ntsekouras Co-authored-by: getdave --- packages/patterns/src/components/duplicate-pattern-modal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/patterns/src/components/duplicate-pattern-modal.js b/packages/patterns/src/components/duplicate-pattern-modal.js index 2b51e82f22da4..70fb0830e0f1c 100644 --- a/packages/patterns/src/components/duplicate-pattern-modal.js +++ b/packages/patterns/src/components/duplicate-pattern-modal.js @@ -17,7 +17,7 @@ function getTermLabels( pattern, categories ) { if ( pattern.type !== PATTERN_TYPES.user ) { return categories.core ?.filter( ( category ) => - pattern.categories.includes( category.name ) + pattern.categories?.includes( category.name ) ) .map( ( category ) => category.label ); } From 249c0cf495ed89df95e039572974bc948fa89731 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:51:47 +0100 Subject: [PATCH 06/18] Revert: Fix unable to remove empty blocks on merge (#65262) + alternative (#66564) Co-authored-by: ellatrix Co-authored-by: ntsekouras Co-authored-by: kspilarski Co-authored-by: talldan Co-authored-by: ndiego Co-authored-by: kevin940726 Co-authored-by: richtabor --- .../src/components/block-list/block.js | 76 +++++++------------ packages/blocks/README.md | 4 - packages/blocks/src/api/index.js | 9 --- packages/blocks/src/api/utils.js | 68 ++++------------- .../editor/various/splitting-merging.spec.js | 19 +++-- 5 files changed, 56 insertions(+), 120 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 2215625596dc5..6d4655189d972 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -24,8 +24,8 @@ import { isReusableBlock, getBlockDefaultClassName, hasBlockSupport, + createBlock, store as blocksStore, - privateApis as blocksPrivateApis, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; @@ -47,8 +47,6 @@ import { PrivateBlockContext } from './private-block-context'; import { unlock } from '../../lock-unlock'; -const { isUnmodifiedBlockContent } = unlock( blocksPrivateApis ); - /** * Merges wrapper props with special handling for classNames and styles. * @@ -313,6 +311,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { function switchToDefaultOrRemove() { const block = getBlock( clientId ); const defaultBlockName = getDefaultBlockName(); + const defaultBlockType = getBlockType( defaultBlockName ); if ( getBlockName( clientId ) !== defaultBlockName ) { const replacement = switchToBlockType( block, @@ -329,6 +328,15 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { selectBlock( nextBlockClientId ); } ); } + } else if ( defaultBlockType.merge ) { + const attributes = defaultBlockType.merge( + {}, + block.attributes + ); + replaceBlocks( + [ clientId ], + [ createBlock( defaultBlockName, attributes ) ] + ); } } @@ -342,6 +350,9 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { * to the moved block. */ function moveFirstItemUp( _clientId, changeSelection = true ) { + const wrapperBlockName = getBlockName( _clientId ); + const wrapperBlockType = getBlockType( wrapperBlockName ); + const isTextualWrapper = wrapperBlockType.category === 'text'; const targetRootClientId = getBlockRootClientId( _clientId ); const blockOrder = getBlockOrder( _clientId ); const [ firstClientId ] = blockOrder; @@ -351,50 +362,14 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { isUnmodifiedBlock( getBlock( firstClientId ) ) ) { removeBlock( _clientId ); - } else { + } else if ( isTextualWrapper ) { registry.batch( () => { - const firstBlock = getBlock( firstClientId ); - const isFirstBlockContentUnmodified = - isUnmodifiedBlockContent( firstBlock ); - const defaultBlockName = getDefaultBlockName(); - const replacement = switchToBlockType( - firstBlock, - defaultBlockName - ); - const canTransformToDefaultBlock = - !! replacement?.length && - replacement.every( ( block ) => - canInsertBlockType( block.name, _clientId ) - ); - if ( - isFirstBlockContentUnmodified && - canTransformToDefaultBlock - ) { - // Step 1: If the block is empty and can be transformed to the default block type. - replaceBlocks( - firstClientId, - replacement, - changeSelection - ); - } else if ( - isFirstBlockContentUnmodified && - firstBlock.name === defaultBlockName - ) { - // Step 2: If the block is empty and is already the default block type. - removeBlock( firstClientId ); - const nextBlockClientId = - getNextBlockClientId( clientId ); - if ( nextBlockClientId ) { - selectBlock( nextBlockClientId ); - } - } else if ( canInsertBlockType( - firstBlock.name, + getBlockName( firstClientId ), targetRootClientId ) ) { - // Step 3: If the block can be moved up. moveBlocksToPosition( [ firstClientId ], _clientId, @@ -402,17 +377,21 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { getBlockIndex( _clientId ) ); } else { - const canLiftAndTransformToDefaultBlock = - !! replacement?.length && + const replacement = switchToBlockType( + getBlock( firstClientId ), + getDefaultBlockName() + ); + + if ( + replacement && + replacement.length && replacement.every( ( block ) => canInsertBlockType( block.name, targetRootClientId ) - ); - - if ( canLiftAndTransformToDefaultBlock ) { - // Step 4: If the block can be transformed to the default block type and moved up. + ) + ) { insertBlocks( replacement, getBlockIndex( _clientId ), @@ -421,7 +400,6 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { ); removeBlock( firstClientId, false ); } else { - // Step 5: Continue the default behavior. switchToDefaultOrRemove(); } } @@ -433,6 +411,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { removeBlock( _clientId, false ); } } ); + } else { + switchToDefaultOrRemove(); } } diff --git a/packages/blocks/README.md b/packages/blocks/README.md index f4805e1c60b38..3e5a88a2b92c1 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -503,10 +503,6 @@ _Returns_ - `Array|string`: A list of blocks or a string, depending on `handlerMode`. -### privateApis - -Undocumented declaration. - ### rawHandler Converts an HTML string to known blocks. diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 0b38b8e29e68a..3ace68be87393 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -1,9 +1,3 @@ -/** - * Internal dependencies - */ -import { lock } from '../lock-unlock'; -import { isUnmodifiedBlockContent } from './utils'; - // The blocktype is the most important concept within the block API. It defines // all aspects of the block configuration and its interfaces, including `edit` // and `save`. The transforms specification allows converting one blocktype to @@ -175,6 +169,3 @@ export { __EXPERIMENTAL_ELEMENTS, __EXPERIMENTAL_PATHS_WITH_OVERRIDE, } from './constants'; - -export const privateApis = {}; -lock( privateApis, { isUnmodifiedBlockContent } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 7bace4ff84c29..20f0f6a85ed09 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -30,30 +30,6 @@ extend( [ namesPlugin, a11yPlugin ] ); */ const ICON_COLORS = [ '#191e23', '#f8f9f9' ]; -/** - * Determines whether the block's attribute is equal to the default attribute - * which means the attribute is unmodified. - * @param {Object} attributeDefinition The attribute's definition of the block type. - * @param {*} value The attribute's value. - * @return {boolean} Whether the attribute is unmodified. - */ -function isUnmodifiedAttribute( attributeDefinition, value ) { - // Every attribute that has a default must match the default. - if ( attributeDefinition.hasOwnProperty( 'default' ) ) { - return value === attributeDefinition.default; - } - - // The rich text type is a bit different from the rest because it - // has an implicit default value of an empty RichTextData instance, - // so check the length of the value. - if ( attributeDefinition.type === 'rich-text' ) { - return ! value?.length; - } - - // Every attribute that doesn't have a default should be undefined. - return value === undefined; -} - /** * Determines whether the block's attributes are equal to the default attributes * which means the block is unmodified. @@ -67,7 +43,20 @@ export function isUnmodifiedBlock( block ) { ( [ key, definition ] ) => { const value = block.attributes[ key ]; - return isUnmodifiedAttribute( definition, value ); + // Every attribute that has a default must match the default. + if ( definition.hasOwnProperty( 'default' ) ) { + return value === definition.default; + } + + // The rich text type is a bit different from the rest because it + // has an implicit default value of an empty RichTextData instance, + // so check the length of the value. + if ( definition.type === 'rich-text' ) { + return ! value?.length; + } + + // Every attribute that doesn't have a default should be undefined. + return value === undefined; } ); } @@ -84,35 +73,6 @@ export function isUnmodifiedDefaultBlock( block ) { return block.name === getDefaultBlockName() && isUnmodifiedBlock( block ); } -/** - * Determines whether the block content is unmodified. A block content is - * considered unmodified if all the attributes that have a role of 'content' - * are equal to the default attributes (or undefined). - * If the block does not have any attributes with a role of 'content', it - * will be considered unmodified if all the attributes are equal to the default - * attributes (or undefined). - * - * @param {WPBlock} block Block Object - * @return {boolean} Whether the block content is unmodified. - */ -export function isUnmodifiedBlockContent( block ) { - const contentAttributes = getBlockAttributesNamesByRole( - block.name, - 'content' - ); - - if ( contentAttributes.length === 0 ) { - return isUnmodifiedBlock( block ); - } - - return contentAttributes.every( ( key ) => { - const definition = getBlockType( block.name )?.attributes[ key ]; - const value = block.attributes[ key ]; - - return isUnmodifiedAttribute( definition, value ); - } ); -} - /** * Function that checks if the parameter is a valid icon. * diff --git a/test/e2e/specs/editor/various/splitting-merging.spec.js b/test/e2e/specs/editor/various/splitting-merging.spec.js index eba9f1d3163fd..146039a7c7d1b 100644 --- a/test/e2e/specs/editor/various/splitting-merging.spec.js +++ b/test/e2e/specs/editor/various/splitting-merging.spec.js @@ -393,6 +393,11 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { attributes: { content: 'heading', level: 2 }, innerBlocks: [], }; + const paragraphWithContent = { + name: 'core/paragraph', + attributes: { content: 'heading', dropCap: false }, + innerBlocks: [], + }; const placeholderBlock = { name: 'core/separator' }; await editor.insertBlock( { name: 'core/group', @@ -407,6 +412,9 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { .getByRole( 'document', { name: 'Empty block' } ) .focus(); + // Remove the alignment. + await page.keyboard.press( 'Backspace' ); + // Remove the empty paragraph block. await page.keyboard.press( 'Backspace' ); await expect .poll( editor.getBlocks, 'Remove the default empty block' ) @@ -422,8 +430,7 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { }, ] ); - // Move the caret to the beginning of the empty heading block. - await page.keyboard.press( 'ArrowDown' ); + // Convert the heading to a default block. await page.keyboard.press( 'Backspace' ); await expect .poll( @@ -441,6 +448,9 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { ], }, ] ); + // Remove the alignment. + await page.keyboard.press( 'Backspace' ); + // Remove the empty default block. await page.keyboard.press( 'Backspace' ); await expect.poll( editor.getBlocks ).toEqual( [ { @@ -453,17 +463,16 @@ test.describe( 'splitting and merging blocks (@firefox, @webkit)', () => { }, ] ); - // Move the caret to the beginning of the "heading" heading block. - await page.keyboard.press( 'ArrowDown' ); + // Convert a non-empty non-default block to a default block. await page.keyboard.press( 'Backspace' ); await expect .poll( editor.getBlocks, 'Lift the non-empty non-default block' ) .toEqual( [ - headingWithContent, { name: 'core/group', attributes: { tagName: 'div' }, innerBlocks: [ + paragraphWithContent, expect.objectContaining( placeholderBlock ), ], }, From c80e6a8383b6923014f5b081c00e15d5814e2c8f Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:48:50 +0100 Subject: [PATCH 07/18] Safari: prevent focus capturing caused by flex display (#66402) Co-authored-by: ellatrix Co-authored-by: mcsf Co-authored-by: youknowriad --- packages/block-library/src/group/editor.scss | 12 ----- .../src/component/event-listeners/index.js | 2 + .../event-listeners/prevent-focus-capture.js | 44 +++++++++++++++++++ .../various/multi-block-selection.spec.js | 9 ++-- 4 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 packages/rich-text/src/component/event-listeners/prevent-focus-capture.js diff --git a/packages/block-library/src/group/editor.scss b/packages/block-library/src/group/editor.scss index 12f5028b96ec3..739a9cd0cf852 100644 --- a/packages/block-library/src/group/editor.scss +++ b/packages/block-library/src/group/editor.scss @@ -10,18 +10,6 @@ } } -// Reset user select, but the next rule should take precedence for nested -// groups. -:where([data-has-multi-selection]:not([contenteditable="true"]) .wp-block-group > *) { - user-select: initial; -} - -// When we are not multi-selecting, prevent children from capturing the -// selection, which happens when the group is flex and children inlined. -:where([data-has-multi-selection]:not([contenteditable="true"]) .wp-block-group) { - user-select: none; -} - // Place block list appender in the same place content will appear. [data-type="core/group"].is-selected { .block-list-appender { diff --git a/packages/rich-text/src/component/event-listeners/index.js b/packages/rich-text/src/component/event-listeners/index.js index 4f69db36db06a..d4f760abdc735 100644 --- a/packages/rich-text/src/component/event-listeners/index.js +++ b/packages/rich-text/src/component/event-listeners/index.js @@ -13,6 +13,7 @@ import formatBoundaries from './format-boundaries'; import deleteHandler from './delete'; import inputAndSelection from './input-and-selection'; import selectionChangeCompat from './selection-change-compat'; +import { preventFocusCapture } from './prevent-focus-capture'; const allEventListeners = [ copyHandler, @@ -21,6 +22,7 @@ const allEventListeners = [ deleteHandler, inputAndSelection, selectionChangeCompat, + preventFocusCapture, ]; export function useEventListeners( props ) { diff --git a/packages/rich-text/src/component/event-listeners/prevent-focus-capture.js b/packages/rich-text/src/component/event-listeners/prevent-focus-capture.js new file mode 100644 index 0000000000000..8dc97f73673ce --- /dev/null +++ b/packages/rich-text/src/component/event-listeners/prevent-focus-capture.js @@ -0,0 +1,44 @@ +/** + * Prevents focus from being captured by the element when clicking _outside_ + * around the element. This may happen when the parent element is flex. + * @see https://github.com/WordPress/gutenberg/pull/65857 + * @see https://github.com/WordPress/gutenberg/pull/66402 + */ +export function preventFocusCapture() { + return ( element ) => { + const { ownerDocument } = element; + const { defaultView } = ownerDocument; + + let value = null; + + function onPointerDown( event ) { + // Abort if the event is default prevented, we will not get a pointer up event. + if ( event.defaultPrevented ) { + return; + } + if ( event.target === element ) { + return; + } + if ( ! event.target.contains( element ) ) { + return; + } + value = element.getAttribute( 'contenteditable' ); + element.setAttribute( 'contenteditable', 'false' ); + defaultView.getSelection().removeAllRanges(); + } + + function onPointerUp() { + if ( value !== null ) { + element.setAttribute( 'contenteditable', value ); + value = null; + } + } + + defaultView.addEventListener( 'pointerdown', onPointerDown ); + defaultView.addEventListener( 'pointerup', onPointerUp ); + return () => { + defaultView.removeEventListener( 'pointerdown', onPointerDown ); + defaultView.removeEventListener( 'pointerup', onPointerUp ); + }; + }; +} diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 9148c316078a4..41fb120023541 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -728,7 +728,7 @@ test.describe( 'Multi-block selection (@firefox, @webkit)', () => { ] ); } ); - test( 'should clear selection when clicking next to blocks (-firefox)', async ( { + test( 'should clear selection when clicking next to blocks', async ( { page, editor, multiBlockSelectionUtils, @@ -752,16 +752,13 @@ test.describe( 'Multi-block selection (@firefox, @webkit)', () => { name: 'Block: Paragraph', } ) .filter( { hasText: '1' } ); + // For some reason in Chrome it requires two clicks, even though it + // doesn't when testing manually. await paragraph1.click( { position: { x: -1, y: 0 }, // Use force since it's outside the bounding box of the element. force: true, } ); - - await expect - .poll( multiBlockSelectionUtils.getSelectedFlatIndices ) - .toEqual( [ 1 ] ); - await paragraph1.click( { position: { x: -1, y: 0 }, // Use force since it's outside the bounding box of the element. From a1a0fa3e36f352f3672d602ec0d0c76baf983dda Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Tue, 12 Nov 2024 10:10:45 +1100 Subject: [PATCH 08/18] Add a landing section to stylebook tabs (#66545) Co-authored-by: tellthemachines Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: jasmussen Co-authored-by: beafialho --- .../src/components/global-styles/ui.js | 5 + .../sidebar-global-styles-wrapper/index.js | 5 + .../src/components/style-book/constants.ts | 9 ++ .../src/components/style-book/examples.tsx | 106 +++++++++++++++++- .../src/components/style-book/index.js | 18 ++- test/e2e/specs/site-editor/style-book.spec.js | 14 +-- 6 files changed, 146 insertions(+), 11 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 2edea0fdbc3da..0cab4c13aa3ee 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -202,6 +202,11 @@ function GlobalStylesStyleBook() { navigator.goTo( '/colors/palette' ); return; } + if ( blockName === 'typography' ) { + // Go to typography Global Styles. + navigator.goTo( '/typography' ); + return; + } // Now go to the selected block. navigator.goTo( '/blocks/' + encodeURIComponent( blockName ) ); diff --git a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js index afa9f489dde22..342fb1b5db52d 100644 --- a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js +++ b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js @@ -132,6 +132,11 @@ export default function GlobalStylesUIWrapper() { onPathChange( '/colors/palette' ); return; } + if ( blockName === 'typography' ) { + // Go to typography Global Styles. + onPathChange( '/typography' ); + return; + } // Now go to the selected block. onPathChange( diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts index 9aace34e64cbf..7b13e3d4ef7f6 100644 --- a/packages/edit-site/src/components/style-book/constants.ts +++ b/packages/edit-site/src/components/style-book/constants.ts @@ -107,6 +107,11 @@ export const STYLE_BOOK_THEME_SUBCATEGORIES: Omit< ]; export const STYLE_BOOK_CATEGORIES: StyleBookCategory[] = [ + { + slug: 'overview', + title: __( 'Overview' ), + blocks: [], + }, { slug: 'text', title: __( 'Text' ), @@ -249,6 +254,10 @@ export const STYLE_BOOK_IFRAME_STYLES = ` .edit-site-style-book__example-preview { width: 100%; } + + .is-wide .edit-site-style-book__example-preview { + width: calc(100% - 120px); + } .edit-site-style-book__example-preview .block-editor-block-list__insertion-point, .edit-site-style-book__example-preview .block-list-appender { diff --git a/packages/edit-site/src/components/style-book/examples.tsx b/packages/edit-site/src/components/style-book/examples.tsx index 9f4badd99a658..2fefe227ca52b 100644 --- a/packages/edit-site/src/components/style-book/examples.tsx +++ b/packages/edit-site/src/components/style-book/examples.tsx @@ -62,6 +62,103 @@ function getColorExamples( colors: MultiOriginPalettes ): BlockExample[] { return examples; } +/** + * Returns examples for the overview page. + * + * @param {MultiOriginPalettes} colors Global Styles color palettes per origin. + * @return {BlockExample[]} An array of block examples. + */ +function getOverviewBlockExamples( + colors: MultiOriginPalettes +): BlockExample[] { + const examples: BlockExample[] = []; + + // Get theme palette from colors. + const themePalette = colors.colors.find( + ( origin: ColorOrigin ) => origin.slug === 'theme' + ); + + if ( themePalette ) { + const themeColorexample: BlockExample = { + name: 'theme-colors', + title: __( 'Colors' ), + category: 'overview', + content: ( + + ), + }; + + examples.push( themeColorexample ); + } + + const headingBlock = createBlock( 'core/heading', { + content: __( + `AaBbCcDdEeFfGgHhiiJjKkLIMmNnOoPpQakRrssTtUuVVWwXxxYyZzOl23356789X{(…)},2!*&:/A@HELFO™` + ), + level: 1, + } ); + const firstParagraphBlock = createBlock( 'core/paragraph', { + content: __( + `A paragraph in a website refers to a distinct block of text that is used to present and organize information. It is a fundamental unit of content in web design and is typically composed of a group of related sentences or thoughts focused on a particular topic or idea. Paragraphs play a crucial role in improving the readability and user experience of a website. They break down the text into smaller, manageable chunks, allowing readers to scan the content more easily.` + ), + } ); + const secondParagraphBlock = createBlock( 'core/paragraph', { + content: __( + `Additionally, paragraphs help structure the flow of information and provide logical breaks between different concepts or pieces of information. In terms of formatting, paragraphs in websites are commonly denoted by a vertical gap or indentation between each block of text. This visual separation helps visually distinguish one paragraph from another, creating a clear and organized layout that guides the reader through the content smoothly.` + ), + } ); + + const textExample = { + name: 'typography', + title: __( 'Typography' ), + category: 'overview', + blocks: [ + headingBlock, + createBlock( + 'core/group', + { + layout: { + type: 'grid', + columnCount: 2, + minimumColumnWidth: '12rem', + }, + style: { + spacing: { + blockGap: '1.5rem', + }, + }, + }, + [ firstParagraphBlock, secondParagraphBlock ] + ), + ], + }; + examples.push( textExample ); + + const otherBlockExamples = [ + 'core/image', + 'core/separator', + 'core/buttons', + 'core/pullquote', + 'core/search', + ]; + + // Get examples for other blocks and put them in order of above array. + otherBlockExamples.forEach( ( blockName ) => { + const blockType = getBlockType( blockName ); + if ( blockType && blockType.example ) { + const blockExample: BlockExample = { + name: blockName, + title: blockType.title, + category: 'overview', + blocks: getBlockFromExample( blockName, blockType.example ), + }; + examples.push( blockExample ); + } + } ); + + return examples; +} + /** * Returns a list of examples for registered block types. * @@ -109,5 +206,12 @@ export function getExamples( colors: MultiOriginPalettes ): BlockExample[] { }; const colorExamples = getColorExamples( colors ); - return [ headingsExample, ...colorExamples, ...nonHeadingBlockExamples ]; + const overviewBlockExamples = getOverviewBlockExamples( colors ); + + return [ + headingsExample, + ...colorExamples, + ...nonHeadingBlockExamples, + ...overviewBlockExamples, + ]; } diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index 9918c169ff6ab..de4c38bd40c05 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -203,6 +203,22 @@ function StyleBook( { [ examples ] ); + const examplesForSinglePageUse = []; + const overviewCategoryExamples = getExamplesByCategory( + { slug: 'overview' }, + examples + ); + examplesForSinglePageUse.push( ...overviewCategoryExamples.examples ); + const otherExamples = examples.filter( ( example ) => { + return ( + example.category !== 'overview' && + ! overviewCategoryExamples.examples.find( + ( overviewExample ) => overviewExample.name === example.name + ) + ); + } ); + examplesForSinglePageUse.push( ...otherExamples ); + const { base: baseConfig } = useContext( GlobalStylesContext ); const goTo = getStyleBookNavigationFromPath( path ); @@ -286,7 +302,7 @@ function StyleBook( { ) : ( { '[name="style-book-canvas"]' ); + // In the Overview tab, expect a button for the main typography section. await expect( styleBookIframe.getByRole( 'button', { - name: 'Open Headings styles in Styles panel', - } ) - ).toBeVisible(); - await expect( - styleBookIframe.getByRole( 'button', { - name: 'Open Paragraph styles in Styles panel', + name: 'Open Typography styles in Styles panel', } ) ).toBeVisible(); @@ -83,13 +79,13 @@ test.describe( 'Style Book', () => { await page .frameLocator( '[name="style-book-canvas"]' ) .getByRole( 'button', { - name: 'Open Headings styles in Styles panel', + name: 'Open Image styles in Styles panel', } ) .click(); await expect( page.locator( - 'role=region[name="Editor settings"i] >> role=heading[name="Heading"i]' + 'role=region[name="Editor settings"i] >> role=heading[name="Image"i]' ) ).toBeVisible(); } ); @@ -103,7 +99,7 @@ test.describe( 'Style Book', () => { await page .frameLocator( '[name="style-book-canvas"]' ) .getByRole( 'button', { - name: 'Open Quote styles in Styles panel', + name: 'Open Pullquote styles in Styles panel', } ) .click(); From caf9d0b8beaf7ee29b130b886313dce718d5de57 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:47:34 +1000 Subject: [PATCH 09/18] Section Styles: Fix insecure properties removal for inner block types and elements (#66896) Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: BogdanUngureanu Co-authored-by: ramonjd --- backport-changelog/6.8/7759.md | 4 + lib/class-wp-theme-json-gutenberg.php | 77 +++++++++--- phpunit/class-wp-theme-json-test.php | 172 ++++++++++++++++++++++++++ 3 files changed, 234 insertions(+), 19 deletions(-) create mode 100644 backport-changelog/6.8/7759.md diff --git a/backport-changelog/6.8/7759.md b/backport-changelog/6.8/7759.md new file mode 100644 index 0000000000000..a0ad85b06e6b0 --- /dev/null +++ b/backport-changelog/6.8/7759.md @@ -0,0 +1,4 @@ +https://github.com/WordPress/wordpress-develop/pull/7759 + +* https://github.com/WordPress/gutenberg/pull/66896 + diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index dafa8b25f278f..d505916450caf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -3565,26 +3565,12 @@ public static function remove_insecure_properties( $theme_json, $origin = 'theme $variation_output = static::remove_insecure_styles( $variation_input ); - // Process a variation's elements and element pseudo selector styles. - if ( isset( $variation_input['elements'] ) ) { - foreach ( $valid_element_names as $element_name ) { - $element_input = $variation_input['elements'][ $element_name ] ?? null; - if ( $element_input ) { - $element_output = static::remove_insecure_styles( $element_input ); - - if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { - foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { - if ( isset( $element_input[ $pseudo_selector ] ) ) { - $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] ); - } - } - } + if ( isset( $variation_input['blocks'] ) ) { + $variation_output['blocks'] = static::remove_insecure_inner_block_styles( $variation_input['blocks'] ); + } - if ( ! empty( $element_output ) ) { - _wp_array_set( $variation_output, array( 'elements', $element_name ), $element_output ); - } - } - } + if ( isset( $variation_input['elements'] ) ) { + $variation_output['elements'] = static::remove_insecure_element_styles( $variation_input['elements'] ); } if ( ! empty( $variation_output ) ) { @@ -3622,6 +3608,59 @@ public static function remove_insecure_properties( $theme_json, $origin = 'theme return $theme_json; } + /** + * Remove insecure element styles within a variation or block. + * + * @since 6.8.0 + * + * @param array $elements The elements to process. + * @return array The sanitized elements styles. + */ + protected static function remove_insecure_element_styles( $elements ) { + $sanitized = array(); + $valid_element_names = array_keys( static::ELEMENTS ); + + foreach ( $valid_element_names as $element_name ) { + $element_input = $elements[ $element_name ] ?? null; + if ( $element_input ) { + $element_output = static::remove_insecure_styles( $element_input ); + + if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { + foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { + if ( isset( $element_input[ $pseudo_selector ] ) ) { + $element_output[ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $pseudo_selector ] ); + } + } + } + + $sanitized[ $element_name ] = $element_output; + } + } + return $sanitized; + } + + /** + * Remove insecure styles from inner blocks and their elements. + * + * @since 6.8.0 + * + * @param array $blocks The block styles to process. + * @return array Sanitized block type styles. + */ + protected static function remove_insecure_inner_block_styles( $blocks ) { + $sanitized = array(); + foreach ( $blocks as $block_type => $block_input ) { + $block_output = static::remove_insecure_styles( $block_input ); + + if ( isset( $block_input['elements'] ) ) { + $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] ); + } + + $sanitized[ $block_type ] = $block_output; + } + return $sanitized; + } + /** * Processes a setting node and returns the same node * without the insecure settings. diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 50a0af4f9b3ff..f77086cf0dd04 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -4387,6 +4387,178 @@ public function test_block_style_variations_with_invalid_properties() { $this->assertSameSetsWithIndex( $expected, $actual ); } + public function test_block_style_variations_with_inner_blocks_and_elements() { + wp_set_current_user( static::$administrator_id ); + gutenberg_register_block_style( + array( 'core/group' ), + array( + 'name' => 'custom-group', + 'label' => 'Custom Group', + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'color' => array( + 'background' => 'blue', + ), + 'variations' => array( + 'custom-group' => array( + 'color' => array( + 'background' => 'purple', + ), + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'green', + ), + ), + ), + ), + ), + 'core/heading' => array( + 'typography' => array( + 'fontSize' => '24px', + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'orange', + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $expected ); + + // The sanitization processes blocks in a specific order which might differ to the theme.json input. + $this->assertEqualsCanonicalizing( + $expected, + $actual, + 'Block style variations data does not match when inner blocks or element styles present' + ); + } + + public function test_block_style_variations_with_invalid_inner_block_or_element_styles() { + wp_set_current_user( static::$administrator_id ); + gutenberg_register_block_style( + array( 'core/group' ), + array( + 'name' => 'custom-group', + 'label' => 'Custom Group', + ) + ); + + $input = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'custom-group' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'typography' => array( + 'fontSize' => 'alert(1)', // Should be removed. + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + 'css' => 'unsafe-value', // Should be removed. + ), + ), + 'custom' => 'unsafe-value', // Should be removed. + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + 'javascript' => 'alert(1)', // Should be removed. + ), + ), + ), + ), + ), + ), + ), + ); + + $expected = array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'custom-group' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + ), + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $input ); + + // The sanitization processes blocks in a specific order which might differ to the theme.json input. + $this->assertEqualsCanonicalizing( + $expected, + $actual, + 'Insecure properties were not removed from block style variation inner block types or elements' + ); + } + /** * Tests generating the spacing presets array based on the spacing scale provided. * From dacc42916b944b54ecc088273efe71c8edaa9959 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 12 Nov 2024 08:29:46 +0200 Subject: [PATCH 10/18] Cover: Show DropZone only when dragging withing the block (#66912) Co-authored-by: ntsekouras Co-authored-by: ellatrix Co-authored-by: Mamaduka Co-authored-by: richtabor --- packages/block-library/src/cover/editor.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index da5d4be26ac96..4dfc301a23114 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -107,10 +107,6 @@ } } -// When uploading background images, show a transparent overlay. -.wp-block-cover > .components-drop-zone .components-drop-zone__content { - opacity: 0.8 !important; -} // Remove the parallax fixed background when in the patterns preview panel as it // doesn't work with the transforms that are applied to resize the block in that context. From e6d38fee95ede2493a0f2ee8105d3665e7174411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:10:50 +0100 Subject: [PATCH 11/18] DataViews Fields API: default getValueFromId supports nested objects (#66890) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: cbravobernal --- packages/dataviews/src/normalize-fields.ts | 19 +++++++- .../dataviews/src/test/normalize-fields.ts | 45 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 packages/dataviews/src/test/normalize-fields.ts diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 5ef219e45a478..562f29fcce84f 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -11,6 +11,22 @@ import type { import { getControl } from './dataform-controls'; import DataFormCombinedEdit from './components/dataform-combined-edit'; +const getValueFromId = + ( id: string ) => + ( { item }: { item: any } ) => { + const path = id.split( '.' ); + let value = item; + for ( const segment of path ) { + if ( value.hasOwnProperty( segment ) ) { + value = value[ segment ]; + } else { + value = undefined; + } + } + + return value; + }; + /** * Apply default values and normalize the fields config. * @@ -23,8 +39,7 @@ export function normalizeFields< Item >( return fields.map( ( field ) => { const fieldTypeDefinition = getFieldTypeDefinition( field.type ); - const getValue = - field.getValue || ( ( { item } ) => ( item as any )[ field.id ] ); + const getValue = field.getValue || getValueFromId( field.id ); const sort = field.sort ?? diff --git a/packages/dataviews/src/test/normalize-fields.ts b/packages/dataviews/src/test/normalize-fields.ts new file mode 100644 index 0000000000000..dce6a19766dca --- /dev/null +++ b/packages/dataviews/src/test/normalize-fields.ts @@ -0,0 +1,45 @@ +/** + * Internal dependencies + */ +import { normalizeFields } from '../normalize-fields'; +import type { Field } from '../types'; + +describe( 'normalizeFields: default getValue', () => { + describe( 'getValue from ID', () => { + it( 'user', () => { + const item = { user: 'value' }; + const fields: Field< {} >[] = [ + { + id: 'user', + }, + ]; + const normalizedFields = normalizeFields( fields ); + const result = normalizedFields[ 0 ].getValue( { item } ); + expect( result ).toBe( 'value' ); + } ); + + it( 'user.name', () => { + const item = { user: { name: 'value' } }; + const fields: Field< {} >[] = [ + { + id: 'user.name', + }, + ]; + const normalizedFields = normalizeFields( fields ); + const result = normalizedFields[ 0 ].getValue( { item } ); + expect( result ).toBe( 'value' ); + } ); + + it( 'user.name.first', () => { + const item = { user: { name: { first: 'value' } } }; + const fields: Field< {} >[] = [ + { + id: 'user.name.first', + }, + ]; + const normalizedFields = normalizeFields( fields ); + const result = normalizedFields[ 0 ].getValue( { item } ); + expect( result ).toBe( 'value' ); + } ); + } ); +} ); From e07351becf8e1628b0234621bb5009df46d0432f Mon Sep 17 00:00:00 2001 From: Luigi Teschio Date: Tue, 12 Nov 2024 12:52:16 +0100 Subject: [PATCH 12/18] DataViews: Implement `isItemClickable` and `onClickItem` props (#66365) Co-authored-by: gigitux Co-authored-by: oandregal Co-authored-by: louwie17 Co-authored-by: youknowriad Co-authored-by: mcsf Co-authored-by: jameskoster --- packages/dataviews/README.md | 8 ++++ .../src/components/dataviews-context/index.ts | 4 ++ .../src/components/dataviews-layout/index.tsx | 4 ++ .../src/components/dataviews/index.tsx | 11 +++++- .../src/components/dataviews/style.scss | 12 ++++-- .../src/dataviews-layouts/grid/index.tsx | 34 ++++++++++++++--- .../src/dataviews-layouts/grid/style.scss | 5 +++ .../src/dataviews-layouts/table/index.tsx | 37 +++++++++++++++++-- .../utils/get-clickable-item-props.ts | 22 +++++++++++ packages/dataviews/src/types.ts | 2 + .../src/components/post-fields/index.js | 36 ++++-------------- .../src/components/post-list/index.js | 12 ++++-- .../src/components/post-list/style.scss | 32 ++++++++++++++-- packages/edit-site/src/style.scss | 4 +- packages/fields/src/style.scss | 2 + packages/fields/src/styles.scss | 1 - 16 files changed, 175 insertions(+), 51 deletions(-) create mode 100644 packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts create mode 100644 packages/fields/src/style.scss delete mode 100644 packages/fields/src/styles.scss diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index ff20386862929..621e3c7ba71ce 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -358,6 +358,14 @@ Callback that signals the user selected one of more items. It receives the list If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves as a controlled component, otherwise, it behaves like an uncontrolled component. +### `isItemClickable`: `function` + +A function that determines if a media field or a primary field are clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. + +### `onClickItem`: `function` + +A callback function that is triggered when a user clicks on a media field or primary field. This function is currently implemented only in the `grid` and `list` views. + #### `header`: React component React component to be rendered next to the view config button. diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index 3936288b3095b..87acade73bc81 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -26,6 +26,8 @@ type DataViewsContextType< Item > = { openedFilter: string | null; setOpenedFilter: ( openedFilter: string | null ) => void; getItemId: ( item: Item ) => string; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; density: number; }; @@ -43,6 +45,8 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { setOpenedFilter: () => {}, openedFilter: null, getItemId: ( item ) => item.id, + onClickItem: () => {}, + isItemClickable: () => false, density: 0, } ); diff --git a/packages/dataviews/src/components/dataviews-layout/index.tsx b/packages/dataviews/src/components/dataviews-layout/index.tsx index bae4071fe2f77..4ef0125b1f64b 100644 --- a/packages/dataviews/src/components/dataviews-layout/index.tsx +++ b/packages/dataviews/src/components/dataviews-layout/index.tsx @@ -28,6 +28,8 @@ export default function DataViewsLayout() { onChangeSelection, setOpenedFilter, density, + onClickItem, + isItemClickable, } = useContext( DataViewsContext ); const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) @@ -44,6 +46,8 @@ export default function DataViewsLayout() { onChangeSelection={ onChangeSelection } selection={ selection } setOpenedFilter={ setOpenedFilter } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } view={ view } density={ density } /> diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index da60ab15ecade..77a5cb8740f71 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -44,12 +44,17 @@ type DataViewsProps< Item > = { defaultLayouts: SupportedLayouts; selection?: string[]; onChangeSelection?: ( items: string[] ) => void; + onClickItem?: ( item: Item ) => void; + isItemClickable?: ( item: Item ) => boolean; header?: ReactNode; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); const defaultGetItemId = ( item: ItemWithId ) => item.id; +const defaultIsItemClickable = () => false; +const defaultOnClickItem = () => {}; +const EMPTY_ARRAY: any[] = []; export default function DataViews< Item >( { view, @@ -57,7 +62,7 @@ export default function DataViews< Item >( { fields, search = true, searchLabel = undefined, - actions = [], + actions = EMPTY_ARRAY, data, getItemId = defaultGetItemId, isLoading = false, @@ -65,6 +70,8 @@ export default function DataViews< Item >( { defaultLayouts, selection: selectionProperty, onChangeSelection, + onClickItem = defaultOnClickItem, + isItemClickable = defaultIsItemClickable, header, }: DataViewsProps< Item > ) { const [ selectionState, setSelectionState ] = useState< string[] >( [] ); @@ -110,6 +117,8 @@ export default function DataViews< Item >( { openedFilter, setOpenedFilter, getItemId, + isItemClickable, + onClickItem, density, } } > diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index aa8fbcfb009c0..bd75a1ff9e2a1 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -19,7 +19,7 @@ position: sticky; left: 0; transition: padding ease-out 0.1s; - @include reduce-motion("transition"); + @include reduce-motion( "transition" ); } .dataviews-view-list__primary-field, @@ -62,6 +62,13 @@ } } +.dataviews-view-list__primary-field--clickable, +.dataviews-view-grid__primary-field--clickable, +.dataviews-view-grid__media--clickable, +.dataviews-view-table__primary-field > .dataviews-view-table__cell-content--clickable { + cursor: pointer; +} + .dataviews-no-results, .dataviews-loading { padding: 0 $grid-unit-60; @@ -70,7 +77,7 @@ align-items: center; justify-content: center; transition: padding ease-out 0.1s; - @include reduce-motion("transition"); + @include reduce-motion( "transition" ); } /* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @@ -86,4 +93,3 @@ padding-right: $grid-unit-30; } } - diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index 230ffe0dc50b5..91cc87ec7b35b 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -24,11 +24,14 @@ import SingleSelectionCheckbox from '../../components/dataviews-selection-checkb import { useHasAPossibleBulkAction } from '../../components/dataviews-bulk-actions'; import type { Action, NormalizedField, ViewGridProps } from '../../types'; import type { SetSelection } from '../../private-types'; +import getClickableItemProps from '../utils/get-clickable-item-props'; interface GridItemProps< Item > { selection: string[]; onChangeSelection: SetSelection; getItemId: ( item: Item ) => string; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; item: Item; actions: Action< Item >[]; mediaField?: NormalizedField< Item >; @@ -41,6 +44,8 @@ interface GridItemProps< Item > { function GridItem< Item >( { selection, onChangeSelection, + onClickItem, + isItemClickable, getItemId, item, actions, @@ -59,6 +64,21 @@ function GridItem< Item >( { const renderedPrimaryField = primaryField?.render ? ( ) : null; + + const clickableMediaItemProps = getClickableItemProps( + item, + isItemClickable, + onClickItem, + 'dataviews-view-grid__media' + ); + + const clickablePrimaryItemProps = getClickableItemProps( + item, + isItemClickable, + onClickItem, + 'dataviews-view-grid__primary-field' + ); + return ( ( { } } } > -
- { renderedMediaField } -
+
{ renderedMediaField }
( { justify="space-between" className="dataviews-view-grid__title-actions" > - - { renderedPrimaryField } + +
+ { renderedPrimaryField } +
@@ -170,6 +190,8 @@ export default function ViewGrid< Item >( { getItemId, isLoading, onChangeSelection, + onClickItem, + isItemClickable, selection, view, density, @@ -223,6 +245,8 @@ export default function ViewGrid< Item >( { key={ getItemId( item ) } selection={ selection } onChangeSelection={ onChangeSelection } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } getItemId={ getItemId } item={ item } actions={ actions } diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index 6286ed94685a0..55768240a1871 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -17,8 +17,13 @@ .dataviews-view-grid__primary-field { min-height: $grid-unit-40; // Preserve layout when there is no ellipsis button + + &--clickable { + width: fit-content; + } } + &.is-selected { .dataviews-view-grid__fields .dataviews-view-grid__field .dataviews-view-grid__field-value { color: $gray-900; diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index 4e1607b01489c..8ef41db1c3879 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -35,11 +35,14 @@ import type { import type { SetSelection } from '../../private-types'; import ColumnHeaderMenu from './column-header-menu'; import { getVisibleFieldIds } from '../index'; +import getClickableItemProps from '../utils/get-clickable-item-props'; interface TableColumnFieldProps< Item > { primaryField?: NormalizedField< Item >; field: NormalizedField< Item >; item: Item; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableColumnCombinedProps< Item > { @@ -48,6 +51,8 @@ interface TableColumnCombinedProps< Item > { field: CombinedField; item: Item; view: ViewTableType; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableColumnProps< Item > { @@ -56,6 +61,8 @@ interface TableColumnProps< Item > { item: Item; column: string; view: ViewTableType; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } interface TableRowProps< Item > { @@ -69,6 +76,8 @@ interface TableRowProps< Item > { selection: string[]; getItemId: ( item: Item ) => string; onChangeSelection: SetSelection; + isItemClickable: ( item: Item ) => boolean; + onClickItem: ( item: Item ) => void; } function TableColumn< Item >( { @@ -102,15 +111,29 @@ function TableColumnField< Item >( { primaryField, item, field, + isItemClickable, + onClickItem, }: TableColumnFieldProps< Item > ) { + const isPrimaryField = primaryField?.id === field.id; + const isItemClickableField = ( i: Item ) => + isItemClickable( i ) && isPrimaryField; + + const clickableProps = getClickableItemProps( + item, + isItemClickableField, + onClickItem, + 'dataviews-view-table__cell-content' + ); + return (
- +
+ +
); } @@ -139,6 +162,8 @@ function TableRow< Item >( { primaryField, selection, getItemId, + isItemClickable, + onClickItem, onChangeSelection, }: TableRowProps< Item > ) { const hasPossibleBulkAction = useHasAPossibleBulkAction( actions, item ); @@ -214,6 +239,8 @@ function TableRow< Item >( { ( { onChangeSelection, selection, setOpenedFilter, + onClickItem, + isItemClickable, view, }: ViewTableProps< Item > ) { const headerMenuRefs = useRef< @@ -392,6 +421,8 @@ function ViewTable< Item >( { selection={ selection } getItemId={ getItemId } onChangeSelection={ onChangeSelection } + onClickItem={ onClickItem } + isItemClickable={ isItemClickable } /> ) ) } diff --git a/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts new file mode 100644 index 0000000000000..e2a6081a68fa3 --- /dev/null +++ b/packages/dataviews/src/dataviews-layouts/utils/get-clickable-item-props.ts @@ -0,0 +1,22 @@ +export default function getClickableItemProps< Item >( + item: Item, + isItemClickable: ( item: Item ) => boolean, + onClickItem: ( item: Item ) => void, + className: string +) { + if ( ! isItemClickable( item ) ) { + return { className }; + } + + return { + className: `${ className } ${ className }--clickable`, + role: 'button', + tabIndex: 0, + onClick: () => onClickItem( item ), + onKeyDown: ( event: React.KeyboardEvent ) => { + if ( event.key === 'Enter' || event.key === '' ) { + onClickItem( item ); + } + }, + }; +} diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 0ea0965704d18..71990f72d4eec 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -498,6 +498,8 @@ export interface ViewBaseProps< Item > { onChangeSelection: SetSelection; selection: string[]; setOpenedFilter: ( fieldId: string ) => void; + onClickItem: ( item: Item ) => void; + isItemClickable: ( item: Item ) => boolean; view: View; density: number; } diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index e659a4f96f23f..54f47052b144c 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -36,12 +36,7 @@ import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ -import { - LAYOUT_GRID, - LAYOUT_TABLE, - OPERATOR_IS_ANY, -} from '../../utils/constants'; -import { default as Link } from '../routes/link'; +import { OPERATOR_IS_ANY } from '../../utils/constants'; // See https://github.com/WordPress/gutenberg/issues/55886 // We do not support custom statutes at the moment. @@ -139,7 +134,7 @@ function PostAuthorField( { item } ) { ); } -function usePostFields( viewType ) { +function usePostFields() { const { records: authors, isResolving: isLoadingAuthors } = useEntityRecords( 'root', 'user', { per_page: -1 } ); @@ -164,30 +159,10 @@ function usePostFields( viewType ) { ? item.title : item.title?.raw, render: ( { item } ) => { - const addLink = - [ LAYOUT_TABLE, LAYOUT_GRID ].includes( viewType ) && - item.status !== 'trash'; const renderedTitle = typeof item.title === 'string' ? item.title : item.title?.rendered; - const title = addLink ? ( - - { decodeEntities( renderedTitle ) || - __( '(no title)' ) } - - ) : ( - - { decodeEntities( renderedTitle ) || - __( '(no title)' ) } - - ); let suffix = ''; if ( item.id === frontPageId ) { @@ -210,7 +185,10 @@ function usePostFields( viewType ) { alignment="center" justify="flex-start" > - { title } + + { decodeEntities( renderedTitle ) || + __( '(no title)' ) } + { suffix } ); @@ -355,7 +333,7 @@ function usePostFields( viewType ) { }, passwordField, ], - [ authors, viewType, frontPageId, postsPageId ] + [ authors, frontPageId, postsPageId ] ); return { diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 4985af3050bd8..4639cb3c950b7 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -208,9 +208,7 @@ export default function PostList( { postType } ) { return found?.filters ?? []; }; - const { isLoading: isLoadingFields, fields: _fields } = usePostFields( - view.type - ); + const { isLoading: isLoadingFields, fields: _fields } = usePostFields(); const fields = useMemo( () => { const activeViewFilters = getActiveViewFilters( defaultViews, @@ -402,6 +400,14 @@ export default function PostList( { postType } ) { onChangeView={ setView } selection={ selection } onChangeSelection={ onChangeSelection } + isItemClickable={ ( item ) => item.status !== 'trash' } + onClickItem={ ( { id } ) => { + history.push( { + postId: id, + postType, + canvas: 'edit', + } ); + } } getItemId={ getItemId } defaultLayouts={ defaultLayouts } header={ diff --git a/packages/edit-site/src/components/post-list/style.scss b/packages/edit-site/src/components/post-list/style.scss index db6a32408c792..14bb11b41d445 100644 --- a/packages/edit-site/src/components/post-list/style.scss +++ b/packages/edit-site/src/components/post-list/style.scss @@ -9,7 +9,9 @@ width: 100%; border-radius: $grid-unit-05; - &.is-layout-table:not(:has(.edit-site-post-list__featured-image-button)), + &.is-layout-table:not( + :has(.edit-site-post-list__featured-image-button) +), &.is-layout-table .edit-site-post-list__featured-image-button { width: $grid-unit-40; height: $grid-unit-40; @@ -46,7 +48,9 @@ border-radius: $grid-unit-05; &:focus-visible { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: + 0 0 0 var(--wp-admin-border-width-focus) + var(--wp-admin-theme-color); // Windows High Contrast mode will show this outline, but not the box-shadow. outline: 2px solid transparent; } @@ -54,7 +58,9 @@ .dataviews-view-grid__card.is-selected { .edit-site-post-list__featured-image-button::after { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: + inset 0 0 0 var(--wp-admin-border-width-focus) + var(--wp-admin-theme-color); background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } } @@ -64,6 +70,26 @@ overflow: hidden; } +.dataviews-view-grid__primary-field.dataviews-view-grid__primary-field--clickable +.edit-site-post-list__title +span, +.dataviews-view-table__primary-field > .dataviews-view-table__cell-content--clickable +.edit-site-post-list__title +span { + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; + flex-grow: 0; + color: $gray-900; + + &:hover { + color: var(--wp-admin-theme-color); + } + @include link-reset(); +} + .edit-site-post-list__title-badge { background: $gray-100; color: $gray-800; diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 03ec43648f120..0e5744fe362e3 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -1,7 +1,5 @@ @import "../../dataviews/src/style.scss"; -@import "../../fields/src/styles.scss"; -@import "../../fields/src/fields/featured-image/style.scss"; - +@import "../../fields/src/style.scss"; @import "./components/add-new-template/style.scss"; @import "./components/block-editor/style.scss"; @import "./components/canvas-loader/style.scss"; diff --git a/packages/fields/src/style.scss b/packages/fields/src/style.scss new file mode 100644 index 0000000000000..1639f455ba093 --- /dev/null +++ b/packages/fields/src/style.scss @@ -0,0 +1,2 @@ +@import "./fields/slug/style.scss"; +@import "./fields/featured-image/style.scss"; diff --git a/packages/fields/src/styles.scss b/packages/fields/src/styles.scss deleted file mode 100644 index cdb130337f1cd..0000000000000 --- a/packages/fields/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -@import "./fields/slug/style.scss"; From f3e8f4a5c11a8bf2f7b4e8e2ff3c7963c23194f3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 12 Nov 2024 13:17:30 +0100 Subject: [PATCH 13/18] Site Editor: Avoid using edited post selectors in welcome guide (#66926) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- packages/edit-site/src/components/editor/index.js | 8 +++++++- packages/edit-site/src/components/welcome-guide/index.js | 6 +++--- packages/edit-site/src/components/welcome-guide/page.js | 8 +------- .../edit-site/src/components/welcome-guide/template.js | 9 +-------- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 8f0ca0c5b2971..94b56a197e1bc 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -218,7 +218,13 @@ export default function EditSiteEditor( { isPostsList = false } ) { { isEditMode && } { ! isReady ? : null } - { isEditMode && } + { isEditMode && ( + + ) } { isReady && ( - - + { postType === 'page' && } + { postType === 'wp_template' && } ); } diff --git a/packages/edit-site/src/components/welcome-guide/page.js b/packages/edit-site/src/components/welcome-guide/page.js index db89d9b653ad5..41bb80342280c 100644 --- a/packages/edit-site/src/components/welcome-guide/page.js +++ b/packages/edit-site/src/components/welcome-guide/page.js @@ -6,11 +6,6 @@ import { Guide } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../store'; - export default function WelcomeGuidePage() { const { toggle } = useDispatch( preferencesStore ); @@ -23,8 +18,7 @@ export default function WelcomeGuidePage() { 'core/edit-site', 'welcomeGuide' ); - const { isPage } = select( editSiteStore ); - return isPageActive && ! isEditorActive && isPage(); + return isPageActive && ! isEditorActive; }, [] ); if ( ! isVisible ) { diff --git a/packages/edit-site/src/components/welcome-guide/template.js b/packages/edit-site/src/components/welcome-guide/template.js index e6568a23bb3a3..bf08bc7fa668b 100644 --- a/packages/edit-site/src/components/welcome-guide/template.js +++ b/packages/edit-site/src/components/welcome-guide/template.js @@ -7,16 +7,9 @@ import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as editorStore } from '@wordpress/editor'; -/** - * Internal dependencies - */ -import useEditedEntityRecord from '../use-edited-entity-record'; - export default function WelcomeGuideTemplate() { const { toggle } = useDispatch( preferencesStore ); - const { isLoaded, record } = useEditedEntityRecord(); - const isPostTypeTemplate = isLoaded && record.type === 'wp_template'; const { isActive, hasPreviousEntity } = useSelect( ( select ) => { const { getEditorSettings } = select( editorStore ); const { get } = select( preferencesStore ); @@ -26,7 +19,7 @@ export default function WelcomeGuideTemplate() { !! getEditorSettings().onNavigateToPreviousEntityRecord, }; }, [] ); - const isVisible = isActive && isPostTypeTemplate && hasPreviousEntity; + const isVisible = isActive && hasPreviousEntity; if ( ! isVisible ) { return null; From 99fd9c71f6fe422b5d43bb65b3b3b36fa28fb507 Mon Sep 17 00:00:00 2001 From: Ankit Kumar Shah Date: Tue, 12 Nov 2024 19:02:14 +0530 Subject: [PATCH 14/18] Media & Text: Set `.wp-block-media-text__media a` display to block (#66915) Ajusted the display property from `inline-block` to `block` to resolve layout issues in the Media & Text block anchor link. Co-authored-by: Infinite-Null Co-authored-by: carolinan Co-authored-by: Mamaduka Co-authored-by: ajmaurya99 --- packages/block-library/src/media-text/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/media-text/style.scss b/packages/block-library/src/media-text/style.scss index 0f7a34f05548c..727380a3759c6 100644 --- a/packages/block-library/src/media-text/style.scss +++ b/packages/block-library/src/media-text/style.scss @@ -69,7 +69,7 @@ } .wp-block-media-text__media a { - display: inline-block; + display: block; } .wp-block-media-text__media img, From d2bc9ea5ef15c5aa56f4db2a1dca6718ce679693 Mon Sep 17 00:00:00 2001 From: David Arenas Date: Tue, 12 Nov 2024 15:12:09 +0100 Subject: [PATCH 15/18] Interactivity API: fix property modification from inherited context two or more levels above (#66872) * Add failing test * Fix code * Add changelog entry * Define "modal" prop in submenu context Co-authored-by: DAreRodz Co-authored-by: sirreal Co-authored-by: michalczaplinski Co-authored-by: danielpost --- .../block-library/src/navigation/index.php | 2 +- packages/interactivity/CHANGELOG.md | 4 ++++ packages/interactivity/src/proxies/context.ts | 3 +++ .../src/proxies/test/context-proxy.ts | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index fa9bb5a56f801..9484ad13ed002 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -813,7 +813,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut ) ) { // Add directives to the parent `
  • `. $tags->set_attribute( 'data-wp-interactive', 'core/navigation' ); - $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu" }' ); + $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu", "modal": null }' ); $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 488fcc77d7541..6963ed57a48ae 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Fix property modification from inherited context two or more levels above ([#66872](https://github.com/WordPress/gutenberg/pull/66872)). + ## 6.11.0 (2024-10-30) ### Bug Fixes diff --git a/packages/interactivity/src/proxies/context.ts b/packages/interactivity/src/proxies/context.ts index 64517c91a6940..8d5bc54a8b831 100644 --- a/packages/interactivity/src/proxies/context.ts +++ b/packages/interactivity/src/proxies/context.ts @@ -38,6 +38,9 @@ const contextHandlers: ProxyHandler< object > = { getOwnPropertyDescriptor: ( target, key ) => descriptor( target, key ) || descriptor( contextObjectToFallback.get( target ), key ), + has: ( target, key ) => + Reflect.has( target, key ) || + Reflect.has( contextObjectToFallback.get( target ), key ), }; /** diff --git a/packages/interactivity/src/proxies/test/context-proxy.ts b/packages/interactivity/src/proxies/test/context-proxy.ts index 1e4a969d0f9dc..6ae041d34dbc0 100644 --- a/packages/interactivity/src/proxies/test/context-proxy.ts +++ b/packages/interactivity/src/proxies/test/context-proxy.ts @@ -137,6 +137,24 @@ describe( 'Interactivity API', () => { expect( context.a ).toBe( 2 ); expect( state.a ).toBe( 2 ); } ); + + it( "should modify props inherited from fallback's ancestors", () => { + const ancestor: any = proxifyContext( + { ancestorProp: 'ancestor' }, + {} + ); + const fallback: any = proxifyContext( + { fallbackProp: 'fallback' }, + ancestor + ); + const context: any = proxifyContext( {}, fallback ); + + context.ancestorProp = 'modified'; + + expect( context.ancestorProp ).toBe( 'modified' ); + expect( fallback.ancestorProp ).toBe( 'modified' ); + expect( ancestor.ancestorProp ).toBe( 'modified' ); + } ); } ); describe( 'computations', () => { From af6d302df00db44d3c5526972128d3ae65470bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:12:11 +0100 Subject: [PATCH 16/18] Post fields: move `status` from `edit-site` to `fields` package (#66937) Co-authored-by: oandregal Co-authored-by: youknowriad --- .../src/components/post-fields/index.js | 81 +------------------ packages/fields/README.md | 4 + packages/fields/src/fields/index.ts | 1 + packages/fields/src/fields/status/index.tsx | 32 ++++++++ .../src/fields/status/status-elements.tsx | 50 ++++++++++++ .../fields/src/fields/status/status-view.tsx | 28 +++++++ 6 files changed, 118 insertions(+), 78 deletions(-) create mode 100644 packages/fields/src/fields/status/index.tsx create mode 100644 packages/fields/src/fields/status/status-elements.tsx create mode 100644 packages/fields/src/fields/status/status-view.tsx diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index 54f47052b144c..3471499c8f21c 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -13,6 +13,7 @@ import { slugField, parentField, passwordField, + statusField, } from '@wordpress/fields'; import { createInterpolateElement, @@ -20,82 +21,17 @@ import { useState, } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; -import { - trash, - drafts, - published, - scheduled, - pending, - notAllowed, - commentAuthorAvatar as authorIcon, -} from '@wordpress/icons'; +import { commentAuthorAvatar as authorIcon } from '@wordpress/icons'; import { __experimentalHStack as HStack, Icon } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; -/** - * Internal dependencies - */ -import { OPERATOR_IS_ANY } from '../../utils/constants'; - -// See https://github.com/WordPress/gutenberg/issues/55886 -// We do not support custom statutes at the moment. -const STATUSES = [ - { - value: 'draft', - label: __( 'Draft' ), - icon: drafts, - description: __( 'Not ready to publish.' ), - }, - { - value: 'future', - label: __( 'Scheduled' ), - icon: scheduled, - description: __( 'Publish automatically on a chosen date.' ), - }, - { - value: 'pending', - label: __( 'Pending Review' ), - icon: pending, - description: __( 'Waiting for review before publishing.' ), - }, - { - value: 'private', - label: __( 'Private' ), - icon: notAllowed, - description: __( 'Only visible to site admins and editors.' ), - }, - { - value: 'publish', - label: __( 'Published' ), - icon: published, - description: __( 'Visible to everyone.' ), - }, - { value: 'trash', label: __( 'Trash' ), icon: trash }, -]; - const getFormattedDate = ( dateToDisplay ) => dateI18n( getSettings().formats.datetimeAbbreviated, getDate( dateToDisplay ) ); -function PostStatusField( { item } ) { - const status = STATUSES.find( ( { value } ) => value === item.status ); - const label = status?.label || item.status; - const icon = status?.icon; - return ( - - { icon && ( -
    - -
    - ) } - { label } -
    - ); -} - function PostAuthorField( { item } ) { const { text, imageUrl } = useSelect( ( select ) => { @@ -214,18 +150,7 @@ function usePostFields() { : nameB.localeCompare( nameA ); }, }, - { - label: __( 'Status' ), - id: 'status', - type: 'text', - elements: STATUSES, - render: PostStatusField, - Edit: 'radio', - enableSorting: false, - filterBy: { - operators: [ OPERATOR_IS_ANY ], - }, - }, + statusField, { label: __( 'Date' ), id: 'date', diff --git a/packages/fields/README.md b/packages/fields/README.md index 214f3d6ee3a50..1571dd72e6a79 100644 --- a/packages/fields/README.md +++ b/packages/fields/README.md @@ -82,6 +82,10 @@ Undocumented declaration. Undocumented declaration. +### statusField + +Status field for BasePost. + ### titleField Undocumented declaration. diff --git a/packages/fields/src/fields/index.ts b/packages/fields/src/fields/index.ts index 29cbdeb2a4ba6..9a4799f13a0d1 100644 --- a/packages/fields/src/fields/index.ts +++ b/packages/fields/src/fields/index.ts @@ -4,3 +4,4 @@ export { default as orderField } from './order'; export { default as featuredImageField } from './featured-image'; export { default as parentField } from './parent'; export { default as passwordField } from './password'; +export { default as statusField } from './status'; diff --git a/packages/fields/src/fields/status/index.tsx b/packages/fields/src/fields/status/index.tsx new file mode 100644 index 0000000000000..374277bc7260e --- /dev/null +++ b/packages/fields/src/fields/status/index.tsx @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import type { Field } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { BasePost } from '../../types'; +import StatusView from './status-view'; +import STATUSES from './status-elements'; + +const OPERATOR_IS_ANY = 'isAny'; + +const statusField: Field< BasePost > = { + label: __( 'Status' ), + id: 'status', + type: 'text', + elements: STATUSES, + render: StatusView, + Edit: 'radio', + enableSorting: false, + filterBy: { + operators: [ OPERATOR_IS_ANY ], + }, +}; + +/** + * Status field for BasePost. + */ +export default statusField; diff --git a/packages/fields/src/fields/status/status-elements.tsx b/packages/fields/src/fields/status/status-elements.tsx new file mode 100644 index 0000000000000..079612270e637 --- /dev/null +++ b/packages/fields/src/fields/status/status-elements.tsx @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { + trash, + drafts, + published, + scheduled, + pending, + notAllowed, +} from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +// See https://github.com/WordPress/gutenberg/issues/55886 +// We do not support custom statutes at the moment. +const STATUSES = [ + { + value: 'draft', + label: __( 'Draft' ), + icon: drafts, + description: __( 'Not ready to publish.' ), + }, + { + value: 'future', + label: __( 'Scheduled' ), + icon: scheduled, + description: __( 'Publish automatically on a chosen date.' ), + }, + { + value: 'pending', + label: __( 'Pending Review' ), + icon: pending, + description: __( 'Waiting for review before publishing.' ), + }, + { + value: 'private', + label: __( 'Private' ), + icon: notAllowed, + description: __( 'Only visible to site admins and editors.' ), + }, + { + value: 'publish', + label: __( 'Published' ), + icon: published, + description: __( 'Visible to everyone.' ), + }, + { value: 'trash', label: __( 'Trash' ), icon: trash }, +]; + +export default STATUSES; diff --git a/packages/fields/src/fields/status/status-view.tsx b/packages/fields/src/fields/status/status-view.tsx new file mode 100644 index 0000000000000..4f3c0547431ac --- /dev/null +++ b/packages/fields/src/fields/status/status-view.tsx @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { __experimentalHStack as HStack, Icon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import type { BasePost } from '../../types'; +import STATUSES from './status-elements'; + +function StatusView( { item }: { item: BasePost } ) { + const status = STATUSES.find( ( { value } ) => value === item.status ); + const label = status?.label || item.status; + const icon = status?.icon; + return ( + + { icon && ( +
    + +
    + ) } + { label } +
    + ); +} + +export default StatusView; From 7a6f7a8d62b17c63e9faff14576b65a3afa4d162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:47:02 +0100 Subject: [PATCH 17/18] Post fields: clean up (#66941) --- .../featured-image/featured-image-edit.tsx | 3 ++- .../featured-image/featured-image-view.tsx | 2 +- .../fields/src/fields/featured-image/index.ts | 3 +-- packages/fields/src/fields/order/index.ts | 3 ++- packages/fields/src/fields/parent/index.ts | 3 +-- .../fields/src/fields/parent/parent-edit.tsx | 18 +++++++++--------- .../fields/src/fields/parent/parent-view.tsx | 4 ++-- packages/fields/src/fields/password/index.tsx | 1 - packages/fields/src/fields/slug/index.ts | 3 +-- 9 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/fields/src/fields/featured-image/featured-image-edit.tsx b/packages/fields/src/fields/featured-image/featured-image-edit.tsx index b0dc612cdcfa5..ee51e5c60f13e 100644 --- a/packages/fields/src/fields/featured-image/featured-image-edit.tsx +++ b/packages/fields/src/fields/featured-image/featured-image-edit.tsx @@ -9,11 +9,12 @@ import { MediaUpload } from '@wordpress/media-utils'; import { lineSolid } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; import type { DataFormControlProps } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ import type { BasePost } from '../../types'; -import { __ } from '@wordpress/i18n'; export const FeaturedImageEdit = ( { data, diff --git a/packages/fields/src/fields/featured-image/featured-image-view.tsx b/packages/fields/src/fields/featured-image/featured-image-view.tsx index 36793e6f2ff9a..1f4b55183f238 100644 --- a/packages/fields/src/fields/featured-image/featured-image-view.tsx +++ b/packages/fields/src/fields/featured-image/featured-image-view.tsx @@ -3,12 +3,12 @@ */ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import type { DataViewRenderFieldProps } from '@wordpress/dataviews'; /** * Internal dependencies */ import type { BasePost } from '../../types'; -import type { DataViewRenderFieldProps } from '@wordpress/dataviews'; export const FeaturedImageView = ( { item, diff --git a/packages/fields/src/fields/featured-image/index.ts b/packages/fields/src/fields/featured-image/index.ts index 44f9a1b406464..62d7e8240aded 100644 --- a/packages/fields/src/fields/featured-image/index.ts +++ b/packages/fields/src/fields/featured-image/index.ts @@ -2,12 +2,12 @@ * WordPress dependencies */ import type { Field } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { BasePost } from '../../types'; -import { __ } from '@wordpress/i18n'; import { FeaturedImageEdit } from './featured-image-edit'; import { FeaturedImageView } from './featured-image-view'; @@ -15,7 +15,6 @@ const featuredImageField: Field< BasePost > = { id: 'featured_media', type: 'text', label: __( 'Featured Image' ), - getValue: ( { item } ) => item.featured_media, Edit: FeaturedImageEdit, render: FeaturedImageView, enableSorting: false, diff --git a/packages/fields/src/fields/order/index.ts b/packages/fields/src/fields/order/index.ts index 2fc0a216dcfa0..984a94c6427fc 100644 --- a/packages/fields/src/fields/order/index.ts +++ b/packages/fields/src/fields/order/index.ts @@ -3,14 +3,15 @@ */ import type { Field } from '@wordpress/dataviews'; import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ import type { BasePost } from '../../types'; const orderField: Field< BasePost > = { - type: 'integer', id: 'menu_order', + type: 'integer', label: __( 'Order' ), description: __( 'Determines the order of pages.' ), }; diff --git a/packages/fields/src/fields/parent/index.ts b/packages/fields/src/fields/parent/index.ts index 2476d071b8165..8b833e1d9369d 100644 --- a/packages/fields/src/fields/parent/index.ts +++ b/packages/fields/src/fields/parent/index.ts @@ -2,12 +2,12 @@ * WordPress dependencies */ import type { Field } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { BasePost } from '../../types'; -import { __ } from '@wordpress/i18n'; import { ParentEdit } from './parent-edit'; import { ParentView } from './parent-view'; @@ -15,7 +15,6 @@ const parentField: Field< BasePost > = { id: 'parent', type: 'text', label: __( 'Parent' ), - getValue: ( { item } ) => item.parent, Edit: ParentEdit, render: ParentView, enableSorting: true, diff --git a/packages/fields/src/fields/parent/parent-edit.tsx b/packages/fields/src/fields/parent/parent-edit.tsx index 030287b8f8fc5..21cdbee7a365a 100644 --- a/packages/fields/src/fields/parent/parent-edit.tsx +++ b/packages/fields/src/fields/parent/parent-edit.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; + /** * WordPress dependencies */ @@ -12,21 +17,16 @@ import { // @ts-ignore import { store as coreStore } from '@wordpress/core-data'; import type { DataFormControlProps } from '@wordpress/dataviews'; - -/** - * External dependencies - */ -import removeAccents from 'remove-accents'; +import { debounce } from '@wordpress/compose'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __, sprintf } from '@wordpress/i18n'; +import { filterURLForDisplay } from '@wordpress/url'; /** * Internal dependencies */ -import { debounce } from '@wordpress/compose'; -import { decodeEntities } from '@wordpress/html-entities'; -import { __, sprintf } from '@wordpress/i18n'; import type { BasePost } from '../../types'; import { getTitleWithFallbackName } from './utils'; -import { filterURLForDisplay } from '@wordpress/url'; type TreeBase = { id: number; diff --git a/packages/fields/src/fields/parent/parent-view.tsx b/packages/fields/src/fields/parent/parent-view.tsx index f0d449db726c3..20c6cb939b4b9 100644 --- a/packages/fields/src/fields/parent/parent-view.tsx +++ b/packages/fields/src/fields/parent/parent-view.tsx @@ -3,14 +3,14 @@ */ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import type { DataViewRenderFieldProps } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { BasePost } from '../../types'; -import type { DataViewRenderFieldProps } from '@wordpress/dataviews'; import { getTitleWithFallbackName } from './utils'; -import { __ } from '@wordpress/i18n'; export const ParentView = ( { item, diff --git a/packages/fields/src/fields/password/index.tsx b/packages/fields/src/fields/password/index.tsx index aa7bc57e3f7ca..dacd0d7435998 100644 --- a/packages/fields/src/fields/password/index.tsx +++ b/packages/fields/src/fields/password/index.tsx @@ -12,7 +12,6 @@ import PasswordEdit from './edit'; const passwordField: Field< BasePost > = { id: 'password', type: 'text', - getValue: ( { item } ) => item.password, Edit: PasswordEdit, enableSorting: false, enableHiding: false, diff --git a/packages/fields/src/fields/slug/index.ts b/packages/fields/src/fields/slug/index.ts index 4e81996ceaa6e..c43fcc679622a 100644 --- a/packages/fields/src/fields/slug/index.ts +++ b/packages/fields/src/fields/slug/index.ts @@ -2,12 +2,12 @@ * WordPress dependencies */ import type { Field } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import type { BasePost } from '../../types'; -import { __ } from '@wordpress/i18n'; import SlugEdit from './slug-edit'; import SlugView from './slug-view'; @@ -15,7 +15,6 @@ const slugField: Field< BasePost > = { id: 'slug', type: 'text', label: __( 'Slug' ), - getValue: ( { item } ) => item.slug, Edit: SlugEdit, render: SlugView, }; From a315a904894faf5ca86110f033238510eab19752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:59:09 +0100 Subject: [PATCH 18/18] Post fields: move `comment_status` from `edit-site` to `fields` package (#66934) Co-authored-by: oandregal Co-authored-by: youknowriad --- .../src/components/post-fields/index.js | 28 +------------ packages/fields/README.md | 4 ++ .../src/fields/comment-status/index.tsx | 40 +++++++++++++++++++ packages/fields/src/fields/index.ts | 1 + 4 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 packages/fields/src/fields/comment-status/index.tsx diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index 3471499c8f21c..083d971d102eb 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -14,6 +14,7 @@ import { parentField, passwordField, statusField, + commentStatusField, } from '@wordpress/fields'; import { createInterpolateElement, @@ -230,32 +231,7 @@ function usePostFields() { }, slugField, parentField, - { - id: 'comment_status', - label: __( 'Discussion' ), - type: 'text', - Edit: 'radio', - enableSorting: false, - filterBy: { - operators: [], - }, - elements: [ - { - value: 'open', - label: __( 'Open' ), - description: __( - 'Visitors can add new comments and replies.' - ), - }, - { - value: 'closed', - label: __( 'Closed' ), - description: __( - 'Visitors cannot add new comments or replies. Existing comments remain visible.' - ), - }, - ], - }, + commentStatusField, passwordField, ], [ authors, frontPageId, postsPageId ] diff --git a/packages/fields/README.md b/packages/fields/README.md index 1571dd72e6a79..1d673c4d46c7b 100644 --- a/packages/fields/README.md +++ b/packages/fields/README.md @@ -14,6 +14,10 @@ npm install @wordpress/fields --save +### commentStatusField + +Comment status field for BasePost. + ### deletePost Undocumented declaration. diff --git a/packages/fields/src/fields/comment-status/index.tsx b/packages/fields/src/fields/comment-status/index.tsx new file mode 100644 index 0000000000000..7f373bc14e210 --- /dev/null +++ b/packages/fields/src/fields/comment-status/index.tsx @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import type { Field } from '@wordpress/dataviews'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { BasePost } from '../../types'; + +const commentStatusField: Field< BasePost > = { + id: 'comment_status', + label: __( 'Discussion' ), + type: 'text', + Edit: 'radio', + enableSorting: false, + filterBy: { + operators: [], + }, + elements: [ + { + value: 'open', + label: __( 'Open' ), + description: __( 'Visitors can add new comments and replies.' ), + }, + { + value: 'closed', + label: __( 'Closed' ), + description: __( + 'Visitors cannot add new comments or replies. Existing comments remain visible.' + ), + }, + ], +}; + +/** + * Comment status field for BasePost. + */ +export default commentStatusField; diff --git a/packages/fields/src/fields/index.ts b/packages/fields/src/fields/index.ts index 9a4799f13a0d1..fba34eb2388a3 100644 --- a/packages/fields/src/fields/index.ts +++ b/packages/fields/src/fields/index.ts @@ -5,3 +5,4 @@ export { default as featuredImageField } from './featured-image'; export { default as parentField } from './parent'; export { default as passwordField } from './password'; export { default as statusField } from './status'; +export { default as commentStatusField } from './comment-status';