From 289bf68962bb0856c41f1d9d74a69a8959032a01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:00:49 +0000 Subject: [PATCH 01/21] Initial plan From 75494b4a1aa07afdc141903e34309409a0d13a31 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:12:07 +0000 Subject: [PATCH 02/21] Add page creator appender to Page List block Co-authored-by: scruffian <275961+scruffian@users.noreply.github.com> --- packages/block-library/src/page-list/edit.js | 12 +- .../block-library/src/page-list/editor.scss | 12 ++ .../src/page-list/page-creator-appender.js | 184 ++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 packages/block-library/src/page-list/page-creator-appender.js diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index cd665bb4434972..e85acca6f17eba 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -39,6 +39,7 @@ import { ConvertToLinksModal, } from './convert-to-links-modal'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; +import { PageCreatorAppender } from './page-creator-appender'; // We only show the edit option when page count is <= MAX_PAGE_COUNT // Performance of Navigation Links is not good past this value. @@ -259,12 +260,14 @@ export default function PageListEdit( { parentClientId, hasDraggedChild, isChildOfNavigation, + isSelected, } = useSelect( ( select ) => { const { getBlockParentsByBlockName, hasSelectedInnerBlock, hasDraggedInnerBlock, + getSelectedBlockClientId, } = select( blockEditorStore ); const blockParents = getBlockParentsByBlockName( clientId, @@ -282,6 +285,7 @@ export default function PageListEdit( { hasSelectedChild: hasSelectedInnerBlock( clientId, true ), hasDraggedChild: hasDraggedInnerBlock( clientId, true ), parentClientId: navigationBlockParents[ 0 ], + isSelected: getSelectedBlockClientId() === clientId, }; }, [ clientId ] @@ -294,8 +298,14 @@ export default function PageListEdit( { parentPageID, } ); + // Create a custom appender that allows creating new pages + const customAppender = () => ( + + ); + const innerBlocksProps = useInnerBlocksProps( blockProps, { - renderAppender: false, + renderAppender: + isChildOfNavigation && isSelected ? customAppender : false, __unstableDisableDropZone: true, templateLock: isChildOfNavigation ? false : 'all', onInput: NOOP, diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss index 2813dc1675b8ea..0a99f35b62a673 100644 --- a/packages/block-library/src/page-list/editor.scss +++ b/packages/block-library/src/page-list/editor.scss @@ -61,3 +61,15 @@ .wp-block-page-list__loading-indicator-container { padding: $grid-unit-10 $grid-unit-15; } + +// Page creator appender styling +.page-list-appender { + display: flex; + gap: $grid-unit-10; + align-items: center; + padding: $grid-unit-10; + + &__button { + flex-shrink: 0; + } +} diff --git a/packages/block-library/src/page-list/page-creator-appender.js b/packages/block-library/src/page-list/page-creator-appender.js new file mode 100644 index 00000000000000..19bb88a1770cbf --- /dev/null +++ b/packages/block-library/src/page-list/page-creator-appender.js @@ -0,0 +1,184 @@ +/** + * WordPress dependencies + */ +import { + Button, + TextControl, + Notice, + Modal, + __experimentalVStack as VStack, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useState } from '@wordpress/element'; +import { plus } from '@wordpress/icons'; +import { InnerBlocks } from '@wordpress/block-editor'; + +/** + * Component for creating new pages from within the Page List block. + * + * @param {Object} props Component props. + * @param {number} props.parentPageID The parent page ID for the new page. + */ +export function PageCreatorAppender( { parentPageID } ) { + const [ isCreating, setIsCreating ] = useState( false ); + const [ title, setTitle ] = useState( '' ); + + // Check if the title is valid for submission + const isTitleValid = title.trim().length > 0; + + // Get the last created entity record (without ID) to track creation state + const { lastError, isSaving } = useSelect( + ( select ) => ( { + lastError: select( coreStore ).getLastEntitySaveError( + 'postType', + 'page' + ), + isSaving: select( coreStore ).isSavingEntityRecord( + 'postType', + 'page' + ), + } ), + [] + ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function createPage( event ) { + event.preventDefault(); + if ( isSaving || ! isTitleValid ) { + return; + } + + try { + const pageData = { + title, + status: 'publish', + }; + + // If there's a parent page ID, set it + if ( parentPageID ) { + pageData.parent = parentPageID; + } + + const savedRecord = await saveEntityRecord( + 'postType', + 'page', + pageData, + { throwOnError: true } + ); + + if ( savedRecord ) { + // Show success notice + createSuccessNotice( + sprintf( + // translators: %s: the title of the new page. + __( 'Page "%s" created successfully.' ), + decodeEntities( savedRecord.title.rendered ) + ), + { + type: 'snackbar', + id: 'page-created-success', + } + ); + + // Reset the form + setTitle( '' ); + setIsCreating( false ); + } + } catch ( error ) { + // Show error notice + createErrorNotice( + __( 'Failed to create page. Please try again.' ), + { + type: 'snackbar', + id: 'page-created-error', + } + ); + } + } + + const isSubmitDisabled = isSaving || ! isTitleValid; + + return ( + <> +
+ + +
+ + { isCreating && ( + { + setIsCreating( false ); + setTitle( '' ); + } } + size="small" + > +
+ +

+ { __( + 'Create a new page to add to your Page List.' + ) } +

+ + + + { lastError && ( + + { lastError.message } + + ) } + + + + + +
+
+
+ ) } + + ); +} From 6152589fb39728f44a49b7507e3f44b4173b7c10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:00:23 +0000 Subject: [PATCH 03/21] Refactor: hijack block appender instead of creating separate component Co-authored-by: scruffian <275961+scruffian@users.noreply.github.com> --- packages/block-library/src/page-list/edit.js | 170 +++++++++++++++- .../block-library/src/page-list/editor.scss | 12 -- .../src/page-list/page-creator-appender.js | 184 ------------------ 3 files changed, 166 insertions(+), 200 deletions(-) delete mode 100644 packages/block-library/src/page-list/page-creator-appender.js diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index e85acca6f17eba..cb5194399b2981 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -22,13 +22,20 @@ import { Notice, ComboboxControl, Button, + Modal, + TextControl, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalVStack as VStack, + __experimentalHStack as HStack, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useMemo, useState, useEffect, useCallback } from '@wordpress/element'; -import { useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; +import { store as noticesStore } from '@wordpress/notices'; +import { decodeEntities } from '@wordpress/html-entities'; +import { Icon, plus } from '@wordpress/icons'; /** * Internal dependencies @@ -39,7 +46,6 @@ import { ConvertToLinksModal, } from './convert-to-links-modal'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; -import { PageCreatorAppender } from './page-creator-appender'; // We only show the edit option when page count is <= MAX_PAGE_COUNT // Performance of Navigation Links is not good past this value. @@ -128,6 +134,10 @@ export default function PageListEdit( { const closeModal = () => setOpen( false ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + // Page creation state + const [ isCreatingPage, setIsCreatingPage ] = useState( false ); + const [ pageTitle, setPageTitle ] = useState( '' ); + const { records: pages, hasResolved: hasResolvedPages } = useEntityRecords( 'postType', 'page', @@ -142,6 +152,92 @@ export default function PageListEdit( { } ); + // Page creation helpers + const { lastError, isSaving } = useSelect( + ( select ) => ( { + lastError: select( coreStore ).getLastEntitySaveError( + 'postType', + 'page' + ), + isSaving: select( coreStore ).isSavingEntityRecord( + 'postType', + 'page' + ), + } ), + [] + ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + const isTitleValid = pageTitle.trim().length > 0; + + const createPage = useCallback( + async ( event ) => { + event.preventDefault(); + if ( isSaving || ! isTitleValid ) { + return; + } + + try { + const pageData = { + title: pageTitle, + status: 'publish', + }; + + // If there's a parent page ID, set it + if ( parentPageID ) { + pageData.parent = parentPageID; + } + + const savedRecord = await saveEntityRecord( + 'postType', + 'page', + pageData, + { throwOnError: true } + ); + + if ( savedRecord ) { + // Show success notice + createSuccessNotice( + sprintf( + // translators: %s: the title of the new page. + __( 'Page "%s" created successfully.' ), + decodeEntities( savedRecord.title.rendered ) + ), + { + type: 'snackbar', + id: 'page-created-success', + } + ); + + // Reset the form + setPageTitle( '' ); + setIsCreatingPage( false ); + } + } catch ( error ) { + // Show error notice + createErrorNotice( + __( 'Failed to create page. Please try again.' ), + { + type: 'snackbar', + id: 'page-created-error', + } + ); + } + }, + [ + isSaving, + isTitleValid, + pageTitle, + parentPageID, + saveEntityRecord, + createSuccessNotice, + createErrorNotice, + ] + ); + const allowConvertToLinks = 'showSubmenuIcon' in context && pages?.length > 0 && @@ -298,9 +394,17 @@ export default function PageListEdit( { parentPageID, } ); - // Create a custom appender that allows creating new pages + // Create a custom appender that hijacks the block appender to create pages const customAppender = () => ( - + ); const innerBlocksProps = useInnerBlocksProps( blockProps, { @@ -414,6 +518,64 @@ export default function PageListEdit( { pages={ pages } parentPageID={ parentPageID } /> + { isCreatingPage && ( + { + setIsCreatingPage( false ); + setPageTitle( '' ); + } } + size="small" + > +
+ +

+ { __( + 'Create a new page to add to your Page List.' + ) } +

+ + + + { lastError && ( + + { lastError.message } + + ) } + + + + + +
+
+
+ ) } ); } diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss index 0a99f35b62a673..2813dc1675b8ea 100644 --- a/packages/block-library/src/page-list/editor.scss +++ b/packages/block-library/src/page-list/editor.scss @@ -61,15 +61,3 @@ .wp-block-page-list__loading-indicator-container { padding: $grid-unit-10 $grid-unit-15; } - -// Page creator appender styling -.page-list-appender { - display: flex; - gap: $grid-unit-10; - align-items: center; - padding: $grid-unit-10; - - &__button { - flex-shrink: 0; - } -} diff --git a/packages/block-library/src/page-list/page-creator-appender.js b/packages/block-library/src/page-list/page-creator-appender.js deleted file mode 100644 index 19bb88a1770cbf..00000000000000 --- a/packages/block-library/src/page-list/page-creator-appender.js +++ /dev/null @@ -1,184 +0,0 @@ -/** - * WordPress dependencies - */ -import { - Button, - TextControl, - Notice, - Modal, - __experimentalVStack as VStack, - __experimentalHStack as HStack, -} from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { store as noticesStore } from '@wordpress/notices'; -import { decodeEntities } from '@wordpress/html-entities'; -import { useState } from '@wordpress/element'; -import { plus } from '@wordpress/icons'; -import { InnerBlocks } from '@wordpress/block-editor'; - -/** - * Component for creating new pages from within the Page List block. - * - * @param {Object} props Component props. - * @param {number} props.parentPageID The parent page ID for the new page. - */ -export function PageCreatorAppender( { parentPageID } ) { - const [ isCreating, setIsCreating ] = useState( false ); - const [ title, setTitle ] = useState( '' ); - - // Check if the title is valid for submission - const isTitleValid = title.trim().length > 0; - - // Get the last created entity record (without ID) to track creation state - const { lastError, isSaving } = useSelect( - ( select ) => ( { - lastError: select( coreStore ).getLastEntitySaveError( - 'postType', - 'page' - ), - isSaving: select( coreStore ).isSavingEntityRecord( - 'postType', - 'page' - ), - } ), - [] - ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function createPage( event ) { - event.preventDefault(); - if ( isSaving || ! isTitleValid ) { - return; - } - - try { - const pageData = { - title, - status: 'publish', - }; - - // If there's a parent page ID, set it - if ( parentPageID ) { - pageData.parent = parentPageID; - } - - const savedRecord = await saveEntityRecord( - 'postType', - 'page', - pageData, - { throwOnError: true } - ); - - if ( savedRecord ) { - // Show success notice - createSuccessNotice( - sprintf( - // translators: %s: the title of the new page. - __( 'Page "%s" created successfully.' ), - decodeEntities( savedRecord.title.rendered ) - ), - { - type: 'snackbar', - id: 'page-created-success', - } - ); - - // Reset the form - setTitle( '' ); - setIsCreating( false ); - } - } catch ( error ) { - // Show error notice - createErrorNotice( - __( 'Failed to create page. Please try again.' ), - { - type: 'snackbar', - id: 'page-created-error', - } - ); - } - } - - const isSubmitDisabled = isSaving || ! isTitleValid; - - return ( - <> -
- - -
- - { isCreating && ( - { - setIsCreating( false ); - setTitle( '' ); - } } - size="small" - > -
- -

- { __( - 'Create a new page to add to your Page List.' - ) } -

- - - - { lastError && ( - - { lastError.message } - - ) } - - - - - -
-
-
- ) } - - ); -} From 54fd9e136f4913f1b8026e77ac414685d0e35d86 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Fri, 27 Feb 2026 13:22:57 +0000 Subject: [PATCH 04/21] Replace Page List custom modal with reusable PageCreatorAppender Use the existing LinkUIPageCreator from the navigation-link block instead of a custom Modal for creating pages. Extract a shared PageCreatorAppender component that handles menu_order and parent page context. Show the appender when the Page List block or its children are selected, but not when inside a Navigation block. Co-Authored-By: Claude Opus 4.6 --- .../navigation-link/link-ui/page-creator.js | 13 +- packages/block-library/src/page-list/edit.js | 224 +++--------------- .../src/page-list/page-creator-appender.js | 76 ++++++ 3 files changed, 117 insertions(+), 196 deletions(-) create mode 100644 packages/block-library/src/page-list/page-creator-appender.js diff --git a/packages/block-library/src/navigation-link/link-ui/page-creator.js b/packages/block-library/src/navigation-link/link-ui/page-creator.js index 30d1799f190979..33631348628ec2 100644 --- a/packages/block-library/src/navigation-link/link-ui/page-creator.js +++ b/packages/block-library/src/navigation-link/link-ui/page-creator.js @@ -24,17 +24,19 @@ import DialogWrapper from './dialog-wrapper'; /** * Component for creating new pages within the Navigation Link UI. * - * @param {Object} props Component props. - * @param {string} props.postType The post type to create. - * @param {Function} props.onBack Callback when user wants to go back. - * @param {Function} props.onPageCreated Callback when page is successfully created. - * @param {string} [props.initialTitle] Initial title to pre-fill the form. + * @param {Object} props Component props. + * @param {string} props.postType The post type to create. + * @param {Function} props.onBack Callback when user wants to go back. + * @param {Function} props.onPageCreated Callback when page is successfully created. + * @param {string} [props.initialTitle] Initial title to pre-fill the form. + * @param {Object} [props.additionalData] Additional data to include when saving the entity record. */ export function LinkUIPageCreator( { postType, onBack, onPageCreated, initialTitle = '', + additionalData = {}, } ) { const [ title, setTitle ] = useState( initialTitle ); const [ shouldPublish, setShouldPublish ] = useState( true ); @@ -74,6 +76,7 @@ export function LinkUIPageCreator( { { title, status: shouldPublish ? 'publish' : 'draft', + ...additionalData, }, { throwOnError: true } ); diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index cb5194399b2981..0364e15fd8491c 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -22,20 +22,13 @@ import { Notice, ComboboxControl, Button, - Modal, - TextControl, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, - __experimentalVStack as VStack, - __experimentalHStack as HStack, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useMemo, useState, useEffect, useCallback } from '@wordpress/element'; -import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as noticesStore } from '@wordpress/notices'; -import { decodeEntities } from '@wordpress/html-entities'; -import { Icon, plus } from '@wordpress/icons'; +import { useEntityRecords } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -46,11 +39,15 @@ import { ConvertToLinksModal, } from './convert-to-links-modal'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; +import PageCreatorAppender, { + PageCreatorContext, +} from './page-creator-appender'; // We only show the edit option when page count is <= MAX_PAGE_COUNT // Performance of Navigation Links is not good past this value. const MAX_PAGE_COUNT = 100; const NOOP = () => {}; + function BlockContent( { blockProps, innerBlocksProps, @@ -134,10 +131,6 @@ export default function PageListEdit( { const closeModal = () => setOpen( false ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); - // Page creation state - const [ isCreatingPage, setIsCreatingPage ] = useState( false ); - const [ pageTitle, setPageTitle ] = useState( '' ); - const { records: pages, hasResolved: hasResolvedPages } = useEntityRecords( 'postType', 'page', @@ -152,92 +145,6 @@ export default function PageListEdit( { } ); - // Page creation helpers - const { lastError, isSaving } = useSelect( - ( select ) => ( { - lastError: select( coreStore ).getLastEntitySaveError( - 'postType', - 'page' - ), - isSaving: select( coreStore ).isSavingEntityRecord( - 'postType', - 'page' - ), - } ), - [] - ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - const isTitleValid = pageTitle.trim().length > 0; - - const createPage = useCallback( - async ( event ) => { - event.preventDefault(); - if ( isSaving || ! isTitleValid ) { - return; - } - - try { - const pageData = { - title: pageTitle, - status: 'publish', - }; - - // If there's a parent page ID, set it - if ( parentPageID ) { - pageData.parent = parentPageID; - } - - const savedRecord = await saveEntityRecord( - 'postType', - 'page', - pageData, - { throwOnError: true } - ); - - if ( savedRecord ) { - // Show success notice - createSuccessNotice( - sprintf( - // translators: %s: the title of the new page. - __( 'Page "%s" created successfully.' ), - decodeEntities( savedRecord.title.rendered ) - ), - { - type: 'snackbar', - id: 'page-created-success', - } - ); - - // Reset the form - setPageTitle( '' ); - setIsCreatingPage( false ); - } - } catch ( error ) { - // Show error notice - createErrorNotice( - __( 'Failed to create page. Please try again.' ), - { - type: 'snackbar', - id: 'page-created-error', - } - ); - } - }, - [ - isSaving, - isTitleValid, - pageTitle, - parentPageID, - saveEntityRecord, - createSuccessNotice, - createErrorNotice, - ] - ); - const allowConvertToLinks = 'showSubmenuIcon' in context && pages?.length > 0 && @@ -354,7 +261,6 @@ export default function PageListEdit( { isNested, hasSelectedChild, parentClientId, - hasDraggedChild, isChildOfNavigation, isSelected, } = useSelect( @@ -362,7 +268,6 @@ export default function PageListEdit( { const { getBlockParentsByBlockName, hasSelectedInnerBlock, - hasDraggedInnerBlock, getSelectedBlockClientId, } = select( blockEditorStore ); const blockParents = getBlockParentsByBlockName( @@ -379,7 +284,6 @@ export default function PageListEdit( { isNested: blockParents.length > 0, isChildOfNavigation: navigationBlockParents.length > 0, hasSelectedChild: hasSelectedInnerBlock( clientId, true ), - hasDraggedChild: hasDraggedInnerBlock( clientId, true ), parentClientId: navigationBlockParents[ 0 ], isSelected: getSelectedBlockClientId() === clientId, }; @@ -394,50 +298,46 @@ export default function PageListEdit( { parentPageID, } ); - // Create a custom appender that hijacks the block appender to create pages - const customAppender = () => ( - + const showAppender = + ! isChildOfNavigation && ( isSelected || hasSelectedChild ); + + const nextMenuOrder = useMemo( () => { + if ( ! pages?.length ) { + return 0; + } + return ( + pages.reduce( + ( max, page ) => Math.max( max, page.menu_order ), + 0 + ) + 1 + ); + }, [ pages ] ); + + const additionalPageData = useMemo( + () => ( { + menu_order: nextMenuOrder, + ...( parentPageID ? { parent: parentPageID } : {} ), + } ), + [ nextMenuOrder, parentPageID ] ); const innerBlocksProps = useInnerBlocksProps( blockProps, { - renderAppender: - isChildOfNavigation && isSelected ? customAppender : false, + renderAppender: showAppender ? PageCreatorAppender : false, __unstableDisableDropZone: true, - templateLock: isChildOfNavigation ? false : 'all', + templateLock: showAppender ? false : 'all', onInput: NOOP, onChange: NOOP, value: blockList, } ); - const { selectBlock } = useDispatch( blockEditorStore ); - - useEffect( () => { - if ( hasSelectedChild || hasDraggedChild ) { - openModal(); - selectBlock( parentClientId ); - } - }, [ - hasSelectedChild, - hasDraggedChild, - parentClientId, - selectBlock, - openModal, - ] ); - useEffect( () => { setAttributes( { isNested } ); }, [ isNested, setAttributes ] ); return ( - <> + { ( pagesTree.length > 0 || allowConvertToLinks ) && ( - { isCreatingPage && ( - { - setIsCreatingPage( false ); - setPageTitle( '' ); - } } - size="small" - > -
- -

- { __( - 'Create a new page to add to your Page List.' - ) } -

- - - - { lastError && ( - - { lastError.message } - - ) } - - - - - -
-
-
- ) } - +
); } diff --git a/packages/block-library/src/page-list/page-creator-appender.js b/packages/block-library/src/page-list/page-creator-appender.js new file mode 100644 index 00000000000000..85cb1da2db71d5 --- /dev/null +++ b/packages/block-library/src/page-list/page-creator-appender.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ +import { + createContext, + useContext, + useState, + useMemo, + useRef, +} from '@wordpress/element'; +import { Button, Popover } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { Icon, plus } from '@wordpress/icons'; +import { useEntityRecords } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { LinkUIPageCreator } from '../navigation-link/link-ui/page-creator'; + +const MAX_PAGE_COUNT = 100; + +export const PageCreatorContext = createContext( null ); + +export default function PageCreatorAppender() { + const [ showCreator, setShowCreator ] = useState( false ); + const ref = useRef(); + const contextData = useContext( PageCreatorContext ); + + const { records: pages } = useEntityRecords( 'postType', 'page', { + per_page: MAX_PAGE_COUNT, + _fields: [ 'id', 'link', 'menu_order', 'parent', 'title', 'type' ], + orderby: 'menu_order', + order: 'asc', + } ); + + const additionalData = useMemo( () => { + // If context provides data (e.g., from Page List with parentPageID), + // use it directly. + if ( contextData?.additionalData ) { + return contextData.additionalData; + } + // Otherwise compute menu_order from pages. + const maxMenuOrder = + pages?.reduce( + ( max, page ) => Math.max( max, page.menu_order ), + 0 + ) ?? 0; + return { menu_order: maxMenuOrder + 1 }; + }, [ contextData, pages ] ); + + return ( + <> + + { showCreator && ( + + setShowCreator( false ) } + onPageCreated={ () => setShowCreator( false ) } + additionalData={ additionalData } + /> + + ) } + + ); +} From 4bb5e6ea6eaea355595030208ce41ccc8a30259e Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Fri, 27 Feb 2026 14:01:04 +0000 Subject: [PATCH 05/21] Address review feedback on Page List page creator appender - Replace generic additionalData with explicit menuOrder and parent props - Remove redundant useEntityRecords query from PageCreatorAppender - Make DialogWrapper back button conditional (only when onBack provided) - Add onClose prop to LinkUIPageCreator for dismissing without back nav - Revert templateLock to trunk behavior to prevent page reordering - Render appender inside block root element alongside inner blocks children Co-Authored-By: Claude Opus 4.6 --- .../navigation-link/link-ui/dialog-wrapper.js | 7 ++++- .../navigation-link/link-ui/page-creator.js | 27 ++++++++++------ packages/block-library/src/page-list/edit.js | 22 ++++++++----- .../src/page-list/page-creator-appender.js | 31 ++----------------- 4 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/block-library/src/navigation-link/link-ui/dialog-wrapper.js b/packages/block-library/src/navigation-link/link-ui/dialog-wrapper.js index e37166a1dfc7c2..c470ac78de037a 100644 --- a/packages/block-library/src/navigation-link/link-ui/dialog-wrapper.js +++ b/packages/block-library/src/navigation-link/link-ui/dialog-wrapper.js @@ -64,7 +64,12 @@ function DialogWrapper( { className, title, description, onBack, children } ) {

{ description }

- + { onBack && ( + + ) } { children } diff --git a/packages/block-library/src/navigation-link/link-ui/page-creator.js b/packages/block-library/src/navigation-link/link-ui/page-creator.js index 33631348628ec2..03ae91197105e2 100644 --- a/packages/block-library/src/navigation-link/link-ui/page-creator.js +++ b/packages/block-library/src/navigation-link/link-ui/page-creator.js @@ -24,19 +24,23 @@ import DialogWrapper from './dialog-wrapper'; /** * Component for creating new pages within the Navigation Link UI. * - * @param {Object} props Component props. - * @param {string} props.postType The post type to create. - * @param {Function} props.onBack Callback when user wants to go back. - * @param {Function} props.onPageCreated Callback when page is successfully created. - * @param {string} [props.initialTitle] Initial title to pre-fill the form. - * @param {Object} [props.additionalData] Additional data to include when saving the entity record. + * @param {Object} props Component props. + * @param {string} props.postType The post type to create. + * @param {Function} [props.onBack] Callback when user wants to go back. Shows a back button when provided. + * @param {Function} props.onClose Callback to close the creator without going back. + * @param {Function} props.onPageCreated Callback when page is successfully created. + * @param {string} [props.initialTitle] Initial title to pre-fill the form. + * @param {number} [props.menuOrder] Menu order for the new page. + * @param {number} [props.parent] Parent page ID for the new page. */ export function LinkUIPageCreator( { postType, onBack, + onClose, onPageCreated, initialTitle = '', - additionalData = {}, + menuOrder, + parent, } ) { const [ title, setTitle ] = useState( initialTitle ); const [ shouldPublish, setShouldPublish ] = useState( true ); @@ -76,7 +80,10 @@ export function LinkUIPageCreator( { { title, status: shouldPublish ? 'publish' : 'draft', - ...additionalData, + ...( menuOrder !== undefined && { + menu_order: menuOrder, + } ), + ...( parent !== undefined && { parent } ), }, { throwOnError: true } ); @@ -120,6 +127,8 @@ export function LinkUIPageCreator( { const isSubmitDisabled = isSaving || ! isTitleValid; + const handleDismiss = onBack || onClose; + return ( diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index 0364e15fd8491c..9707d57ab1d6c8 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -55,6 +55,7 @@ function BlockContent( { blockList, pages, parentPageID, + showAppender, } ) { if ( ! hasResolvedPages ) { return ( @@ -115,7 +116,13 @@ function BlockContent( { } if ( pages.length > 0 ) { - return
    ; + const { children, ...ulProps } = innerBlocksProps; + return ( +
      + { children } + { showAppender && } +
    + ); } } @@ -313,18 +320,18 @@ export default function PageListEdit( { ); }, [ pages ] ); - const additionalPageData = useMemo( + const pageCreatorContextValue = useMemo( () => ( { - menu_order: nextMenuOrder, + menuOrder: nextMenuOrder, ...( parentPageID ? { parent: parentPageID } : {} ), } ), [ nextMenuOrder, parentPageID ] ); const innerBlocksProps = useInnerBlocksProps( blockProps, { - renderAppender: showAppender ? PageCreatorAppender : false, + renderAppender: false, __unstableDisableDropZone: true, - templateLock: showAppender ? false : 'all', + templateLock: isChildOfNavigation ? false : 'all', onInput: NOOP, onChange: NOOP, value: blockList, @@ -335,9 +342,7 @@ export default function PageListEdit( { }, [ isNested, setAttributes ] ); return ( - + { ( pagesTree.length > 0 || allowConvertToLinks ) && ( ); diff --git a/packages/block-library/src/page-list/page-creator-appender.js b/packages/block-library/src/page-list/page-creator-appender.js index 85cb1da2db71d5..ada8ed5cbc209e 100644 --- a/packages/block-library/src/page-list/page-creator-appender.js +++ b/packages/block-library/src/page-list/page-creator-appender.js @@ -5,21 +5,17 @@ import { createContext, useContext, useState, - useMemo, useRef, } from '@wordpress/element'; import { Button, Popover } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Icon, plus } from '@wordpress/icons'; -import { useEntityRecords } from '@wordpress/core-data'; /** * Internal dependencies */ import { LinkUIPageCreator } from '../navigation-link/link-ui/page-creator'; -const MAX_PAGE_COUNT = 100; - export const PageCreatorContext = createContext( null ); export default function PageCreatorAppender() { @@ -27,28 +23,6 @@ export default function PageCreatorAppender() { const ref = useRef(); const contextData = useContext( PageCreatorContext ); - const { records: pages } = useEntityRecords( 'postType', 'page', { - per_page: MAX_PAGE_COUNT, - _fields: [ 'id', 'link', 'menu_order', 'parent', 'title', 'type' ], - orderby: 'menu_order', - order: 'asc', - } ); - - const additionalData = useMemo( () => { - // If context provides data (e.g., from Page List with parentPageID), - // use it directly. - if ( contextData?.additionalData ) { - return contextData.additionalData; - } - // Otherwise compute menu_order from pages. - const maxMenuOrder = - pages?.reduce( - ( max, page ) => Math.max( max, page.menu_order ), - 0 - ) ?? 0; - return { menu_order: maxMenuOrder + 1 }; - }, [ contextData, pages ] ); - return ( <> + /> { showCreator && ( Date: Tue, 3 Mar 2026 11:29:25 +0000 Subject: [PATCH 10/21] Use own class for page creator appender button styling The block-editor-inserter__toggle and block-list-appender__toggle classes require specific parent contexts we don't have. Use a dedicated class with styles in editor.scss instead. Co-Authored-By: Claude Opus 4.6 --- packages/block-library/src/page-list/editor.scss | 14 ++++++++++++++ .../src/page-list/page-creator-appender.js | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss index ee555d430d6284..711cee5e6642ac 100644 --- a/packages/block-library/src/page-list/editor.scss +++ b/packages/block-library/src/page-list/editor.scss @@ -68,3 +68,17 @@ .wp-block-page-list__loading-indicator-container { padding: $grid-unit-10 $grid-unit-15; } + +// Page creator appender button. +.wp-block-page-list__appender-toggle.components-button { + background: $gray-900; + color: $white; + padding: 0; + min-width: $button-size-small; + height: $button-size-small; + + &:hover { + color: $white; + background: var(--wp-admin-theme-color); + } +} diff --git a/packages/block-library/src/page-list/page-creator-appender.js b/packages/block-library/src/page-list/page-creator-appender.js index 9a53b9d343e102..b341d0c3e4e152 100644 --- a/packages/block-library/src/page-list/page-creator-appender.js +++ b/packages/block-library/src/page-list/page-creator-appender.js @@ -28,7 +28,7 @@ export default function PageCreatorAppender() {