diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 0df73d1996deb5..627de5e8652e47 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -4,50 +4,15 @@ import { external } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useDispatch, useSelect } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf, _x } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useMemo, useState, useEffect } from '@wordpress/element'; -import { DataForm } from '@wordpress/dataviews'; -import { - Button, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { - TEMPLATE_PART_POST_TYPE, - TEMPLATE_POST_TYPE, - PATTERN_POST_TYPE, -} from '../../store/constants'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { getItemTitle } from '../../dataviews/actions/utils'; - -// TODO: this should be shared with other components (see post-fields in edit-site). -const fields = [ - { - type: 'text', - id: 'title', - label: __( 'Title' ), - placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, - }, - { - type: 'integer', - id: 'menu_order', - label: __( 'Order' ), - description: __( 'Determines the order of pages.' ), - }, -]; - -const formDuplicateAction = { - fields: [ 'title' ], -}; const viewPostAction = { id: 'view-post', @@ -100,172 +65,6 @@ const postRevisionsAction = { }, }; -const useDuplicatePostAction = ( postType ) => { - const userCanCreatePost = useSelect( - ( select ) => { - return select( coreStore ).canUser( 'create', { - kind: 'postType', - name: postType, - } ); - }, - [ postType ] - ); - return useMemo( - () => - userCanCreatePost && { - id: 'duplicate-post', - label: _x( 'Duplicate', 'action label' ), - isEligible( { status } ) { - return status !== 'trash'; - }, - RenderModal: ( { items, closeModal, onActionPerformed } ) => { - const [ item, setItem ] = useState( { - ...items[ 0 ], - title: sprintf( - /* translators: %s: Existing template title */ - __( '%s (Copy)' ), - getItemTitle( items[ 0 ] ) - ), - } ); - - const [ isCreatingPage, setIsCreatingPage ] = - useState( false ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function createPage( event ) { - event.preventDefault(); - - if ( isCreatingPage ) { - return; - } - - const newItemOject = { - status: 'draft', - title: item.title, - slug: item.title || __( 'No title' ), - comment_status: item.comment_status, - content: - typeof item.content === 'string' - ? item.content - : item.content.raw, - excerpt: item.excerpt.raw, - meta: item.meta, - parent: item.parent, - password: item.password, - template: item.template, - format: item.format, - featured_media: item.featured_media, - menu_order: item.menu_order, - ping_status: item.ping_status, - }; - const assignablePropertiesPrefix = 'wp:action-assign-'; - // Get all the properties that the current user is able to assign normally author, categories, tags, - // and custom taxonomies. - const assignableProperties = Object.keys( - item?._links || {} - ) - .filter( ( property ) => - property.startsWith( - assignablePropertiesPrefix - ) - ) - .map( ( property ) => - property.slice( - assignablePropertiesPrefix.length - ) - ); - assignableProperties.forEach( ( property ) => { - if ( item[ property ] ) { - newItemOject[ property ] = item[ property ]; - } - } ); - setIsCreatingPage( true ); - try { - const newItem = await saveEntityRecord( - 'postType', - item.type, - newItemOject, - { throwOnError: true } - ); - - createSuccessNotice( - sprintf( - // translators: %s: Title of the created template e.g: "Category". - __( '"%s" successfully created.' ), - decodeEntities( - newItem.title?.rendered || item.title - ) - ), - { - id: 'duplicate-post-action', - type: 'snackbar', - } - ); - - if ( onActionPerformed ) { - onActionPerformed( [ newItem ] ); - } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while duplicating the page.' - ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - setIsCreatingPage( false ); - closeModal(); - } - } - - return ( -
- - - setItem( { - ...item, - ...changes, - } ) - } - /> - - - - - -
- ); - }, - }, - [ userCanCreatePost ] - ); -}; - export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions, postTypeObject } = useSelect( ( select ) => { @@ -284,12 +83,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { registerPostTypeActions( postType ); }, [ registerPostTypeActions, postType ] ); - const duplicatePostAction = useDuplicatePostAction( postType ); - const isTemplateOrTemplatePart = [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - ].includes( postType ); - const isPattern = postType === PATTERN_POST_TYPE; const isLoaded = !! postTypeObject; const supportsRevisions = !! postTypeObject?.supports?.revisions; return useMemo( () => { @@ -300,11 +93,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { let actions = [ postTypeObject?.viewable && viewPostAction, supportsRevisions && postRevisionsAction, - globalThis.IS_GUTENBERG_PLUGIN - ? ! isTemplateOrTemplatePart && - ! isPattern && - duplicatePostAction - : false, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -370,10 +158,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) { return actions; }, [ defaultActions, - isTemplateOrTemplatePart, - isPattern, postTypeObject?.viewable, - duplicatePostAction, onActionPerformed, isLoaded, supportsRevisions, diff --git a/packages/editor/src/dataviews/actions/duplicate-post.native.tsx b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx new file mode 100644 index 00000000000000..5468aa649abbd4 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx @@ -0,0 +1,3 @@ +const duplicatePost = undefined; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/duplicate-post.tsx b/packages/editor/src/dataviews/actions/duplicate-post.tsx new file mode 100644 index 00000000000000..0979d30da39519 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.tsx @@ -0,0 +1,174 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { store as coreStore } from '@wordpress/core-data'; +import { __, sprintf, _x } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useState } from '@wordpress/element'; +import { DataForm } from '@wordpress/dataviews'; +import { + Button, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { getItemTitle } from '../../dataviews/actions/utils'; +import type { CoreDataError, BasePost } from '../types'; +import { titleField } from '../fields'; + +const fields = [ titleField ]; +const formDuplicateAction = { + fields: [ 'title' ], +}; + +const duplicatePost: Action< BasePost > = { + id: 'duplicate-post', + label: _x( 'Duplicate', 'action label' ), + isEligible( { status } ) { + return status !== 'trash'; + }, + RenderModal: ( { items, closeModal, onActionPerformed } ) => { + const [ item, setItem ] = useState< BasePost >( { + ...items[ 0 ], + title: sprintf( + /* translators: %s: Existing template title */ + __( '%s (Copy)' ), + getItemTitle( items[ 0 ] ) + ), + } ); + + const [ isCreatingPage, setIsCreatingPage ] = useState( false ); + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function createPage( event: React.FormEvent ) { + event.preventDefault(); + + if ( isCreatingPage ) { + return; + } + + const newItemOject = { + status: 'draft', + title: item.title, + slug: item.title || __( 'No title' ), + comment_status: item.comment_status, + content: + typeof item.content === 'string' + ? item.content + : item.content.raw, + excerpt: + typeof item.excerpt === 'string' + ? item.excerpt + : item.excerpt?.raw, + meta: item.meta, + parent: item.parent, + password: item.password, + template: item.template, + format: item.format, + featured_media: item.featured_media, + menu_order: item.menu_order, + ping_status: item.ping_status, + }; + const assignablePropertiesPrefix = 'wp:action-assign-'; + // Get all the properties that the current user is able to assign normally author, categories, tags, + // and custom taxonomies. + const assignableProperties = Object.keys( item?._links || {} ) + .filter( ( property ) => + property.startsWith( assignablePropertiesPrefix ) + ) + .map( ( property ) => + property.slice( assignablePropertiesPrefix.length ) + ); + assignableProperties.forEach( ( property ) => { + if ( item.hasOwnProperty( property ) ) { + // @ts-ignore + newItemOject[ property ] = item[ property ]; + } + } ); + setIsCreatingPage( true ); + try { + const newItem = await saveEntityRecord( + 'postType', + item.type, + newItemOject, + { throwOnError: true } + ); + + createSuccessNotice( + sprintf( + // translators: %s: Title of the created template e.g: "Category". + __( '"%s" successfully created.' ), + decodeEntities( newItem.title?.rendered || item.title ) + ), + { + id: 'duplicate-post-action', + type: 'snackbar', + } + ); + + if ( onActionPerformed ) { + onActionPerformed( [ newItem ] ); + } + } catch ( error ) { + const typedError = error as CoreDataError; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while duplicating the page.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } finally { + setIsCreatingPage( false ); + closeModal?.(); + } + } + + return ( +
+ + + setItem( ( prev ) => ( { + ...prev, + ...changes, + } ) ) + } + /> + + + + + +
+ ); + }, +}; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/reorder-page.tsx b/packages/editor/src/dataviews/actions/reorder-page.tsx index 1b9524123e7c95..1820884d8d8c73 100644 --- a/packages/editor/src/dataviews/actions/reorder-page.tsx +++ b/packages/editor/src/dataviews/actions/reorder-page.tsx @@ -17,7 +17,7 @@ import type { Action, RenderModalProps } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { CoreDataError, PostWithPageAttributesSupport } from '../types'; +import type { CoreDataError, BasePost } from '../types'; import { orderField } from '../fields'; const fields = [ orderField ]; @@ -29,7 +29,7 @@ function ReorderModal( { items, closeModal, onActionPerformed, -}: RenderModalProps< PostWithPageAttributesSupport > ) { +}: RenderModalProps< BasePost > ) { const [ item, setItem ] = useState( items[ 0 ] ); const orderInput = item.menu_order; const { editEntityRecord, saveEditedEntityRecord } = @@ -113,7 +113,7 @@ function ReorderModal( { ); } -const reorderPage: Action< PostWithPageAttributesSupport > = { +const reorderPage: Action< BasePost > = { id: 'order-pages', label: __( 'Order' ), isEligible( { status } ) { diff --git a/packages/editor/src/dataviews/fields/index.ts b/packages/editor/src/dataviews/fields/index.ts index ea30d15dab600b..b215172eaf7f02 100644 --- a/packages/editor/src/dataviews/fields/index.ts +++ b/packages/editor/src/dataviews/fields/index.ts @@ -7,17 +7,18 @@ import type { Field } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { BasePost, PostWithPageAttributesSupport } from '../types'; +import type { BasePost } from '../types'; +import { getItemTitle } from '../actions/utils'; export const titleField: Field< BasePost > = { type: 'text', id: 'title', label: __( 'Title' ), placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, + getValue: ( { item } ) => getItemTitle( item ), }; -export const orderField: Field< PostWithPageAttributesSupport > = { +export const orderField: Field< BasePost > = { type: 'integer', id: 'menu_order', label: __( 'Order' ), diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 80449d1b7a0798..d5b299b012e364 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -21,6 +21,7 @@ import restorePost from '../actions/restore-post'; import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import duplicatePost from '../actions/duplicate-post'; export function registerEntityAction< Item >( kind: string, @@ -87,6 +88,14 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + // @ts-ignore + globalThis.IS_GUTENBERG_PLUGIN + ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( + postTypeConfig.slug + ) && + canCreate && + duplicatePost + : undefined, postTypeConfig.slug === 'wp_template_part' && canCreate && currentTheme?.is_block_theme && diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 5750ab96eeae81..514953d6691290 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -7,7 +7,7 @@ type PostStatus = | 'auto-draft' | 'trash'; -export interface BasePost { +export interface CommonPost { status?: PostStatus; title: string | { rendered: string } | { raw: string }; content: string | { raw: string; rendered: string }; @@ -16,7 +16,21 @@ export interface BasePost { blocks?: Object[]; } -export interface Template extends BasePost { +export interface BasePost extends CommonPost { + comment_status?: 'open' | 'closed'; + excerpt?: string | { raw: string; rendered: string }; + meta?: Record< string, any >; + parent?: number; + password?: string; + template?: string; + format?: string; + featured_media?: number; + menu_order?: number; + ping_status?: 'open' | 'closed'; + _links?: Record< string, { href: string }[] >; +} + +export interface Template extends CommonPost { type: 'wp_template'; is_custom: boolean; source: string; @@ -24,7 +38,7 @@ export interface Template extends BasePost { id: string; } -export interface TemplatePart extends BasePost { +export interface TemplatePart extends CommonPost { type: 'wp_template_part'; source: string; has_theme_file: boolean; @@ -32,16 +46,12 @@ export interface TemplatePart extends BasePost { area: string; } -export interface Pattern extends BasePost { +export interface Pattern extends CommonPost { slug: string; title: { raw: string }; wp_pattern_sync_status: string; } -export interface PostWithPageAttributesSupport extends BasePost { - menu_order: number; -} - export type Post = Template | TemplatePart | Pattern | BasePost; export type PostWithPermissions = Post & {